Загрузка данных


diff --git a/CHANGELOG.md b/CHANGELOG.md
index 945fc82c5ad51d762dfc1982f0a583419f6ec892..dc4ba10ed6cb94be88752e319cccbd4f8da4830d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
 - Добавлены тултипы для элементов управления в шапке графика
 - Добавлен параметр `showSnapshotButton?: boolean` для управления отображением кнопки «Снимок графика»
 - Добавлен VWAP индикатор
+- Добавлена механика ctrl+c/ctrl+v для дровингов
 
 # 0.2.1
 
diff --git a/src/core/Drawings.ts b/src/core/Drawings.ts
index dc72e4e6bddef066e1e1907753944e365166c2b6..d80060268388852b2acd22de541901861ea4354a 100644
--- a/src/core/Drawings.ts
+++ b/src/core/Drawings.ts
@@ -15,7 +15,6 @@ interface DrawingParams extends DOMObjectParams {
   lwcChart: IChartApi;
   mainSeries: SeriesStrategies;
   onDelete: (id: string) => void;
-  onCopy: () => void;
   construct: (chart: IChartApi, series: ISeriesApi<SeriesType>, interaction: DrawingInteraction) => ISeriesDrawing;
   selected$: Observable<boolean>;
   isSelected: () => boolean;
@@ -35,7 +34,6 @@ export class Drawing extends DOMObject {
   private subscriptions = new Subscription();
 
   private unregisterDeleteHotkey = () => {};
-  private unregisterCopyHotkey = () => {};
 
   constructor({
     lwcChart,
@@ -44,7 +42,6 @@ export class Drawing extends DOMObject {
     drawingName,
     id,
     onDelete,
-    onCopy,
     zIndex,
     moveUp,
     moveDown,
@@ -103,15 +100,6 @@ export class Drawing extends DOMObject {
             this.delete();
           },
         });
-
-        this.unregisterCopyHotkey = this.hotkeys.register({
-          keys: [Keys.mod, Keys.c],
-          callback: () => {
-            if (!this.isCreationPending()) {
-              onCopy();
-            }
-          },
-        });
       }),
     );
   }
@@ -219,9 +207,7 @@ export class Drawing extends DOMObject {
 
   private unregisterSelectedDrawingHotkeys(): void {
     this.unregisterDeleteHotkey();
-    this.unregisterCopyHotkey();
 
     this.unregisterDeleteHotkey = () => {};
-    this.unregisterCopyHotkey = () => {};
   }
 }
diff --git a/src/core/Drawings/axisLine/axisLine.ts b/src/core/Drawings/axisLine/axisLine.ts
index 9da496e9abda866ef0c076e722b784b3433974e3..8bce3938d26a20a7ddfa9640033e2fc3a72cf043 100644
--- a/src/core/Drawings/axisLine/axisLine.ts
+++ b/src/core/Drawings/axisLine/axisLine.ts
@@ -36,6 +36,19 @@ interface AxisLineParams extends BaseDrawingParams {
   direction: AxisLineDirection;
 }
 
+export function isAxisLineState(state: unknown): state is AxisLineState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return (
+    'time' in state &&
+    'price' in state &&
+    ((typeof state.time === 'number' && state.price === null) ||
+      (state.time === null && typeof state.price === 'number'))
+  );
+}
+
 interface AxisLineState {
   hidden: boolean;
   mode: AxisLineMode;
diff --git a/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts b/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
index a288a14cad591c7118567b6e2718e3ac59e7ea6b..ccc8e3b691698e1783ad044b1fc162651b56d9e1 100644
--- a/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
+++ b/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
@@ -29,6 +29,23 @@ type PriceLabelKind = 'top' | 'bottom';
 
 type FibonacciRetracementParams = BaseDrawingParams;
 
+export function isFibonacciRetracementState(state: unknown): state is FibonacciRetracementState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return (
+    'startTime' in state &&
+    typeof state.startTime === 'number' &&
+    'endTime' in state &&
+    typeof state.endTime === 'number' &&
+    'startPrice' in state &&
+    typeof state.startPrice === 'number' &&
+    'endPrice' in state &&
+    typeof state.endPrice === 'number'
+  );
+}
+
 interface FibonacciRetracementState {
   hidden: boolean;
   mode: FibonacciRetracementMode;
diff --git a/src/core/Drawings/line/lineDrawing.ts b/src/core/Drawings/line/lineDrawing.ts
index 128255b28d9bd901e225f6d8c796e10de919980b..b21f8814cb7baf4267f114b63f04da81cd8b5e4e 100644
--- a/src/core/Drawings/line/lineDrawing.ts
+++ b/src/core/Drawings/line/lineDrawing.ts
@@ -5,6 +5,8 @@ import { BaseDrawingParams, ISeriesDrawing, LinearDrawingBase, TwoPointGeometry
 import { type ChartOptionsModel, Defaults, LineMarker, type SettingsTab } from '@src/types';
 import { formatDate, formatPrice } from '@src/utils';
 
+import { isAnchor } from '../utils';
+
 import { LineDrawingPaneView } from './paneView';
 import { createDefaultSettings, getLineDrawingSettingsTabs } from './settings';
 
@@ -24,6 +26,14 @@ interface LineDrawingParams extends BaseDrawingParams {
   defaultMarkers?: Partial<LineDrawingMarkers>;
 }
 
+export function isLineDrawingState(state: unknown): state is LineDrawingState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return 'startAnchor' in state && isAnchor(state.startAnchor) && 'endAnchor' in state && isAnchor(state.endAnchor);
+}
+
 interface LineDrawingState {
   hidden: boolean;
   mode: LineDrawingMode;
diff --git a/src/core/Drawings/ray/ray.ts b/src/core/Drawings/ray/ray.ts
index 46a152f0867d181894d1d1210a1698eb5ccf7261..a90c04a7ddd3d251334b15d6dfae716d30bc429f 100644
--- a/src/core/Drawings/ray/ray.ts
+++ b/src/core/Drawings/ray/ray.ts
@@ -1,6 +1,6 @@
 import { PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
 
-import { getDistanceToSegment } from '@core/Drawings/utils';
+import { getDistanceToSegment, isAnchor } from '@core/Drawings/utils';
 
 import { BaseDrawingParams, ISeriesDrawing, LinearDrawingBase } from '@src/core/Drawings/base';
 import { type ChartOptionsModel, Defaults, type SettingsTab } from '@src/types';
@@ -23,6 +23,19 @@ type PriceLabelKind = 'start' | 'direction';
 
 type RayParams = BaseDrawingParams;
 
+export function isRayState(state: unknown): state is RayState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return (
+    'startAnchor' in state &&
+    isAnchor(state.startAnchor) &&
+    'directionAnchor' in state &&
+    isAnchor(state.directionAnchor)
+  );
+}
+
 interface RayState {
   hidden: boolean;
   mode: RayMode;
diff --git a/src/core/Drawings/regressionTrend/regressionTrend.ts b/src/core/Drawings/regressionTrend/regressionTrend.ts
index 6ed24bfd367a9c32664674a6d64490e755e6eb9a..b9ad932850c9c26eeb097eb13ffb7b83a6b45774 100644
--- a/src/core/Drawings/regressionTrend/regressionTrend.ts
+++ b/src/core/Drawings/regressionTrend/regressionTrend.ts
@@ -25,6 +25,19 @@ type PriceLabelKind = 'start' | 'end';
 
 type RegressionTrendParams = BaseDrawingParams;
 
+export function isRegressionTrendState(state: unknown): state is RegressionTrendState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return (
+    'startTime' in state &&
+    typeof state.startTime === 'number' &&
+    'endTime' in state &&
+    typeof state.endTime === 'number'
+  );
+}
+
 interface RegressionTrendState {
   hidden: boolean;
   mode: RegressionTrendMode;
diff --git a/src/core/Drawings/sliderPosition/sliderPosition.ts b/src/core/Drawings/sliderPosition/sliderPosition.ts
index c9abaa9ac3918de4726334d0c6b7d8ef2983537b..1fa5d8d7bbb73335e7c4a9e9dccbd55a5bf7b3c4 100644
--- a/src/core/Drawings/sliderPosition/sliderPosition.ts
+++ b/src/core/Drawings/sliderPosition/sliderPosition.ts
@@ -51,6 +51,25 @@ interface SliderPositionParams extends BaseDrawingParams {
   resetTriggers?: Observable<unknown>[];
 }
 
+export function isSliderPositionState(state: unknown): state is SliderPositionState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return (
+    'startTime' in state &&
+    typeof state.startTime === 'number' &&
+    'endTime' in state &&
+    typeof state.endTime === 'number' &&
+    'entryPrice' in state &&
+    typeof state.entryPrice === 'number' &&
+    'stopPrice' in state &&
+    typeof state.stopPrice === 'number' &&
+    'targetPrice' in state &&
+    typeof state.targetPrice === 'number'
+  );
+}
+
 interface SliderPositionState {
   hidden: boolean;
   mode: SliderMode;
diff --git a/src/core/Drawings/text/text.ts b/src/core/Drawings/text/text.ts
index 03c59c46ef8dfb2eb95b2ab6be10cfd3ddff0b65..9489acb4130a608630839b4dc7c9560896bd54ee 100644
--- a/src/core/Drawings/text/text.ts
+++ b/src/core/Drawings/text/text.ts
@@ -9,7 +9,7 @@ import {
   getYCoordinateFromPrice,
   isPointInBounds,
 } from '@core/Drawings/helpers';
-import { updateViews } from '@core/Drawings/utils';
+import { isAnchor, updateViews } from '@core/Drawings/utils';
 
 import { BaseDrawingParams, DrawingBase, ISeriesDrawing } from '@src/core/Drawings/base';
 import { Defaults, SettingsTab } from '@src/types';
@@ -23,6 +23,14 @@ import type { ChartOptionsModel } from '@src/types';
 
 type TextMode = 'idle' | 'dragging' | 'ready';
 
+export function isTextState(state: unknown): state is TextState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return 'point' in state && isAnchor(state.point);
+}
+
 export interface TextState {
   hidden: boolean;
   mode: TextMode;
diff --git a/src/core/Drawings/traectory/traectory.ts b/src/core/Drawings/traectory/traectory.ts
index 8d2fb230572b8d8ba90ae0309c7ca6a990f4c88d..aad51bf06c25bc85543f7e8f7e287e24f00a8512 100644
--- a/src/core/Drawings/traectory/traectory.ts
+++ b/src/core/Drawings/traectory/traectory.ts
@@ -2,7 +2,7 @@ import { PrimitiveHoveredItem } from 'lightweight-charts';
 import { clamp } from 'lodash-es';
 
 import { clampPointToContainer, getContainerSize, isNearPoint } from '@core/Drawings/helpers';
-import { getDistanceToSegment } from '@core/Drawings/utils';
+import { getDistanceToSegment, isAnchor } from '@core/Drawings/utils';
 
 import { BaseDrawingParams, DrawingBase, ISeriesDrawing } from '@src/core/Drawings/base';
 
@@ -18,6 +18,14 @@ type TraectoryMode = 'idle' | 'drawing' | 'ready' | 'dragging-point' | 'dragging
 type TraectoryHandleId = `${number}`;
 type TraectoryParams = BaseDrawingParams;
 
+export function isTraectoryState(state: unknown): state is TraectoryState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return 'points' in state && Array.isArray(state.points) && state.points.every((point) => isAnchor(point));
+}
+
 export interface TraectoryState {
   hidden: boolean;
   mode: TraectoryMode;
diff --git a/src/core/Drawings/utils.ts b/src/core/Drawings/utils.ts
index a00fdf76fd8222319e2578fd891a5a0bf8baf12b..92cbcc6a3b8f89503446106dcd7173e2351a9b68 100644
--- a/src/core/Drawings/utils.ts
+++ b/src/core/Drawings/utils.ts
@@ -1,4 +1,4 @@
-import type { Point, UpdatableView } from './types';
+import type { Anchor, Point, UpdatableView } from './types';
 
 export function updateViews(views: readonly UpdatableView[]): void {
   for (const view of views) {
@@ -66,3 +66,14 @@ export function getDistanceToSegment(point: Point, startPoint: Point, endPoint:
 
   return Math.hypot(point.x - projectionX, point.y - projectionY);
 }
+
+export function isAnchor(value: unknown): value is Anchor {
+  return (
+    value !== null &&
+    typeof value === 'object' &&
+    'time' in value &&
+    typeof value.time === 'number' &&
+    'price' in value &&
+    typeof value.price === 'number'
+  );
+}
diff --git a/src/core/DrawingsManager.tsx b/src/core/DrawingsManager.tsx
index 5b1f486c6e87eaff0dd87fbceb90637769eb5ccb..24384f31b243688193cf346c8e12b09a49d57cb9 100644
--- a/src/core/DrawingsManager.tsx
+++ b/src/core/DrawingsManager.tsx
@@ -13,6 +13,7 @@ import { ModalRenderer } from '@src/core/ModalRenderer';
 import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
 import { ActiveDrawingTool, DOMObjectSnapshot } from '@src/types';
 
+import { COPPIED_DRWAING_ID } from './DrawingsManagerCollection';
 import { Pane } from './Pane';
 
 import type { DrawingInteraction } from '@src/core/Drawings/base';
@@ -69,7 +70,6 @@ export class DrawingsManager {
   private selectedDrawingSnapshot: DrawingSnapshotItem | null = null;
   private pane: Pane;
 
-  private copyPasteBuffer: DrawingSnapshotItem | null = null;
   private setActiveTool: (name: ActiveDrawingTool) => void;
   private getActiveTool: () => ActiveDrawingTool;
   private getIsEndlessMode: () => boolean;
@@ -120,36 +120,14 @@ export class DrawingsManager {
       }),
     );
 
+    // todo: перенести в panе
     window.addEventListener('pointerup', this.handlePointerUp);
     window.addEventListener('pointercancel', this.handlePointerUp);
     this.container.addEventListener('click', this.handleClick);
     this.container.addEventListener('pointerdown', this.handlePointerDown);
     this.container.addEventListener('dblclick', this.handleDoubleClick);
     this.container.addEventListener('contextmenu', this.handleContextMenu);
-    // todo: implement ctrl+v
-    // hotkeys.register({
-    //   keys: [Keys.control, Keys.v],
-    //   callback: () => {
-    //     const bufferWithAppliedPosition = {
-    //       ...this.copyPasteBuffer,
-    //       state: {
-    //         ...this.copyPasteBuffer?.state,
-    //         startAnchor: {
-    //           price: 73.36210252637723,
-    //           time: 1783679170
-    //         }
-    //       }
-    //     }
-    //
-    //     this.setSnapshot([
-    //       ...this.getSnapshot(),
-    //       bufferWithAppliedPosition
-    //     ])
-    //     // hotkeys.unregister({ // todo: unregister all else ctrl+c's
-    //     //   keys: [Keys.control, Keys.c]
-    //     // })
-    //   }
-    // })
+    this.container.addEventListener('contextmenu', this.handleContextMenu);
   }
 
   // TODO: handlePointerDown конфликтует с DrawingsManagerCollection.handlePaneClick
@@ -240,6 +218,7 @@ export class DrawingsManager {
   };
 
   private handleClick = (event: MouseEvent): void => {
+    // todo: move method to pane
     if (!this.isEventInPane(event)) {
       return;
     }
@@ -248,6 +227,8 @@ export class DrawingsManager {
 
     pendingDrawing?.getSeriesDrawing().click(event);
 
+    this.pane.setSelected();
+
     this.DOM.refreshEntities();
   };
 
@@ -561,11 +542,6 @@ export class DrawingsManager {
         drawingName: name,
         name: drawingLabelById()[name],
         onDelete: this.removeDrawing,
-        onCopy: () => {
-          if (createdDrawing) {
-            this.copyPasteBuffer = this.createDrawingSnapshot(createdDrawing);
-          }
-        },
         zIndex: entityZIndex,
         moveDown,
         moveUp,
@@ -634,13 +610,20 @@ export class DrawingsManager {
       drawings.push(
         this.createDrawing({
           name: item.drawingName,
-          options: {
-            id: item.id,
-            state: cloneDeep(item.state),
-            isLocked: item.isLocked,
-            zIndex: item.zIndex,
-            shouldUpdateDrawingsList: false,
-          },
+          options:
+            item.id === COPPIED_DRWAING_ID
+              ? {
+                  state: cloneDeep(item.state),
+                  isLocked: item.isLocked,
+                  shouldUpdateDrawingsList: false,
+                }
+              : {
+                  id: item.id,
+                  state: cloneDeep(item.state),
+                  isLocked: item.isLocked,
+                  zIndex: item.zIndex,
+                  shouldUpdateDrawingsList: false,
+                },
         }),
       );
 
@@ -698,6 +681,16 @@ export class DrawingsManager {
     this.removeDrawing(drawing.id);
   }
 
+  public getSelectedDrawingSnapshot(): DrawingSnapshotItem | null {
+    const drawing = this.selectedDrawing$.value;
+
+    if (!drawing) {
+      return null;
+    }
+
+    return this.createDrawingSnapshot(drawing);
+  }
+
   public toggleSelectedDrawingLock(): void {
     const drawing = this.selectedDrawing$.value;
 
@@ -768,7 +761,6 @@ export class DrawingsManager {
     this.drawings$.value.forEach((drawing) => drawing.destroy());
 
     this.selectedDrawingSnapshot = null;
-    this.copyPasteBuffer = null;
 
     this.subscriptions.unsubscribe();
     this.drawings$.complete();
diff --git a/src/core/DrawingsManagerCollection.ts b/src/core/DrawingsManagerCollection.ts
index 06359f4739f3b061fc75e939bb071ed0629aa35c..992999f1e553540f17cae07bb192de4912d1fee8 100644
--- a/src/core/DrawingsManagerCollection.ts
+++ b/src/core/DrawingsManagerCollection.ts
@@ -1,21 +1,43 @@
-import { BehaviorSubject, Observable } from 'rxjs';
+import { BehaviorSubject, Observable, Subscription } from 'rxjs';
 
 import { DrawingsNames } from '@src/constants';
 import { DRAWING_KEYBOARD_SHORTCUTS, findDrawingPointerShortcut } from '@src/core/Drawings/shortcuts';
 import { ActiveDrawingTool, SettingsValues } from '@src/types';
 
+import { ChartMouseEvents } from './ChartMouseEvents';
 import { Drawing } from './Drawings';
-import { DrawingsManager } from './DrawingsManager';
+import { isAxisLineState } from './Drawings/axisLine/axisLine';
+import { isFibonacciRetracementState } from './Drawings/fibonacciRetracement/fibonacciRetracement';
+import { getPriceFromYCoordinate, getTimeFromXCoordinate } from './Drawings/helpers';
+import { isLineDrawingState } from './Drawings/line/lineDrawing';
+import { isRayState } from './Drawings/ray/ray';
+import { isRegressionTrendState } from './Drawings/regressionTrend/regressionTrend';
+import { isSliderPositionState } from './Drawings/sliderPosition/sliderPosition';
+import { isTextState } from './Drawings/text/text';
+import { isTraectoryState } from './Drawings/traectory/traectory';
+import { SeriesApi } from './Drawings/types';
+import { DrawingsManager, DrawingSnapshotItem } from './DrawingsManager';
 import { Hotkeys, Keys } from './Hotkeys';
 
 import type { Pane } from './Pane';
+import type { IChartApi, MouseEventParams, Point, Time } from 'lightweight-charts';
 
-import type { MouseEventParams } from 'lightweight-charts';
+interface DrawingsManagerCollectionParams {
+  hotkeys: Hotkeys;
+  selectedPane: Observable<Pane | null>;
+  subscribeChartEvent: ChartMouseEvents['subscribe'];
+  lwcChart: IChartApi;
+}
+
+export const COPPIED_DRWAING_ID = 'coppied';
 
-// todo: нужно дописывать класс)
 export class DrawingsManagerCollection {
   private managersMap: Map<number, DrawingsManager> = new Map();
   private hotkeys: Hotkeys;
+  private selectedPane: Pane | null = null;
+  private copyPasteBuffer: DrawingSnapshotItem | null = null;
+  private cursorPos = new BehaviorSubject<null | Point>(null);
+  private copyPasteCoords: Point | null = null;
 
   private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
   private endlessMode$ = new BehaviorSubject(false);
@@ -23,8 +45,13 @@ export class DrawingsManagerCollection {
   private isAwaitingDrawingStart = false;
   private unregisterHotkeys: (() => void)[] = [];
 
-  constructor({ hotkeys }: { hotkeys: Hotkeys }) {
+  private subscriptions = new Subscription();
+
+  constructor({ hotkeys, selectedPane, subscribeChartEvent, lwcChart }: DrawingsManagerCollectionParams) {
     this.hotkeys = hotkeys;
+    selectedPane.subscribe((next) => {
+      this.selectedPane = next;
+    });
 
     for (const { keys, drawingName } of DRAWING_KEYBOARD_SHORTCUTS) {
       this.unregisterHotkeys.push(
@@ -37,6 +64,88 @@ export class DrawingsManagerCollection {
       );
     }
 
+    this.subscriptions.add(
+      subscribeChartEvent('crosshairMove', (param) => {
+        this.cursorPos.next(param.point ?? null);
+      }),
+    );
+
+    this.unregisterHotkeys.push(
+      this.hotkeys.register({
+        keys: [Keys.mod, Keys.c],
+        callback: () => {
+          const pane = this.selectedPane;
+          if (!pane) {
+            return;
+          }
+
+          const drawingManager = pane.getDrawingManager();
+
+          const yCoord = this.cursorPos.value?.y;
+          const xCoord = this.cursorPos.value?.x;
+
+          if (!yCoord || !xCoord) {
+            this.copyPasteBuffer = null;
+            return;
+          }
+
+          this.copyPasteBuffer = drawingManager.getSelectedDrawingSnapshot();
+          this.copyPasteCoords = { x: xCoord, y: yCoord };
+        },
+      }),
+    );
+
+    this.unregisterHotkeys.push(
+      this.hotkeys.register({
+        keys: [Keys.mod, Keys.v],
+        callback: () => {
+          const pane = this.selectedPane;
+          if (!pane) {
+            return;
+          }
+
+          const drawingManager = pane.getDrawingManager();
+
+          const drawingToPaste = this.copyPasteBuffer;
+
+          if (!drawingToPaste) {
+            return;
+          }
+
+          const { state } = drawingToPaste;
+
+          const yCoord = this.cursorPos.value?.y;
+          const xCoord = this.cursorPos.value?.x;
+
+          const prevYCoord = this.copyPasteCoords?.y;
+          const prevXCoord = this.copyPasteCoords?.x;
+
+          if (!yCoord || !xCoord || !prevYCoord || !prevXCoord) {
+            return;
+          }
+
+          const shift = {
+            priceDiff:
+              <number>getPriceFromYCoordinate(pane.getLocalMainSeries() as SeriesApi, yCoord) -
+              <number>getPriceFromYCoordinate(pane.getLocalMainSeries() as SeriesApi, prevYCoord),
+            timeDiff:
+              <number>getTimeFromXCoordinate(lwcChart, xCoord) - <number>getTimeFromXCoordinate(lwcChart, prevXCoord),
+          };
+
+          const shiftedState = shiftDrawingStateCoordsTo(state, shift);
+
+          const bufferWithAppliedPosition: DrawingSnapshotItem = {
+            ...drawingToPaste,
+            id: COPPIED_DRWAING_ID,
+            zIndex: drawingToPaste.zIndex,
+            state: shiftedState,
+          };
+
+          drawingManager.setSnapshot([...drawingManager.getSnapshot(), bufferWithAppliedPosition]);
+        },
+      }),
+    );
+
     this.unregisterHotkeys.push(
       this.hotkeys.register({
         keys: [Keys.escape],
@@ -135,6 +244,10 @@ export class DrawingsManagerCollection {
     Array.from(this.managersMap.values())[0].openSelectedDrawingSettings();
   };
 
+  public copySelectedDrawing = (): void => {
+    Array.from(this.managersMap.values())[0].deleteSelectedDrawing();
+  };
+
   public deleteSelectedDrawing = (): void => {
     Array.from(this.managersMap.values())[0].deleteSelectedDrawing();
   };
@@ -156,3 +269,129 @@ export class DrawingsManagerCollection {
     }
   }
 }
+
+function shiftDrawingStateCoordsTo(state: unknown, shift: { timeDiff: number; priceDiff: number }) {
+  if (isLineDrawingState(state)) {
+    // LineDrawing, ParallelChannelState, VolumeProfileState
+    const startAnchor = {
+      time: ((state.startAnchor?.time as number) + shift.timeDiff) as Time,
+      price: (state.startAnchor?.price as number) + shift.priceDiff,
+    };
+    const endAnchor = {
+      time: ((state.endAnchor?.time as number) + shift.timeDiff) as Time,
+      price: (state.endAnchor?.price as number) + shift.priceDiff,
+    };
+    return {
+      ...state,
+      startAnchor,
+      endAnchor,
+    };
+  }
+  if (isRayState(state)) {
+    // Ray
+    const startAnchor = {
+      time: ((state.startAnchor?.time as number) + shift.timeDiff) as Time,
+      price: (state.startAnchor?.price as number) + shift.priceDiff,
+    };
+
+    const directionAnchor = {
+      time: ((state.directionAnchor?.time as number) + shift.timeDiff) as Time,
+      price: (state.directionAnchor?.price as number) + shift.priceDiff,
+    };
+
+    return {
+      ...state,
+      startAnchor,
+      directionAnchor,
+    };
+  }
+
+  if (isSliderPositionState(state)) {
+    // SliderPositionState
+    const startTime = ((state.startTime as number) + shift.timeDiff) as Time;
+    const endTime = ((state.endTime as number) + shift.timeDiff) as Time;
+    const entryPrice = (state.entryPrice as number) + shift.priceDiff;
+    const stopPrice = (state.stopPrice as number) + shift.priceDiff;
+    const targetPrice = (state.targetPrice as number) + shift.priceDiff;
+
+    return {
+      ...state,
+      startTime,
+      endTime,
+      entryPrice,
+      stopPrice,
+      targetPrice,
+    };
+  }
+  if (isFibonacciRetracementState(state)) {
+    // FibonacciRetracement, DiapsonState, RectangleState
+    const startTime = ((state.startTime as number) + shift.timeDiff) as Time;
+    const endTime = ((state.endTime as number) + shift.timeDiff) as Time;
+    const startPrice = (state.startPrice as number) + shift.priceDiff;
+    const endPrice = (state.endPrice as number) + shift.priceDiff;
+
+    return {
+      ...state,
+      startTime,
+      endTime,
+      startPrice,
+      endPrice,
+    };
+  }
+  if (isRegressionTrendState(state)) {
+    // RegressionTrend
+    const startTime = ((state.startTime as number) + shift.timeDiff) as Time;
+    const endTime = ((state.endTime as number) + shift.timeDiff) as Time;
+
+    return {
+      ...state,
+      startTime,
+      endTime,
+    };
+  }
+
+  if (isAxisLineState(state)) {
+    // AxisLine
+    const time = ((state.time as number) + shift.timeDiff) as Time;
+    const price = (state.price as number) + shift.priceDiff;
+
+    return {
+      ...state,
+      time,
+      price,
+    };
+  }
+
+  if (isTextState(state)) {
+    // TextState
+    const point = {
+      time: ((state.point?.time as number) + shift.timeDiff) as Time,
+      price: (state.point?.price as number) + shift.priceDiff,
+    };
+
+    return {
+      ...state,
+      point,
+    };
+  }
+
+  if (isTraectoryState(state)) {
+    // TraectoryState
+
+    const points = state.points.map((point) => {
+      const anchor = {
+        time: ((point?.time as number) + shift.timeDiff) as Time,
+        price: (point?.price as number) + shift.priceDiff,
+      };
+
+      return anchor;
+    });
+
+    return {
+      ...state,
+      points,
+    };
+  }
+
+  return state;
+}
diff --git a/src/core/Pane.tsx b/src/core/Pane.tsx
index 0484870b14f536ffdf4d930fc32eccb47c1a5545..df0333525801649e77acf2a3b51262d15fe39d37 100644
--- a/src/core/Pane.tsx
+++ b/src/core/Pane.tsx
@@ -58,6 +58,7 @@ export interface PaneParams {
   getActiveTool: () => ActiveDrawingTool;
   getIsEndlessMode: () => boolean;
   continueDrawing: (name: DrawingsNames) => void;
+  deselectAllPanes: () => void;
 }
 
 // todo: Pane, ему должна принадлежать mainSerie, а также IndicatorManager и drawingsManager, mouseEvents. Также перекинуть соответствующие/необходимые свойства из чарта, и из чарта удалить
@@ -94,6 +95,8 @@ export class Pane implements ISerializable<PaneSnapshot> {
   private readonly onPriceScaleStateChange: () => void;
   private readonly subscriptions = new Subscription();
   private paneContainerSyncFrameId: number | null = null;
+  private isSelected = new BehaviorSubject<boolean>(false);
+  private deselectAllPanes: () => void;
 
   constructor({
     lwcChart,
@@ -120,6 +123,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
     getActiveTool,
     getIsEndlessMode,
     continueDrawing,
+    deselectAllPanes,
   }: PaneParams) {
     this.onDelete = onDelete;
     this.onPriceScaleStateChange = onPriceScaleStateChange;
@@ -129,6 +133,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
     this.subscribeChartEvent = subscribeChartEvent;
     this.isMain = isMainPane;
     this.id = id;
+    this.deselectAllPanes = deselectAllPanes;
 
     if (isMainPane) {
       this.lwcPane = this.lwcChart.panes()[MAIN_PANE_INDEX];
@@ -216,8 +221,25 @@ export class Pane implements ISerializable<PaneSnapshot> {
         this.legendContainer.style.display = hasRuler ? 'none' : '';
       }),
     );
+
+    this.subscriptions.add(
+      this.drawingsManager.entities().subscribe((drawings) => {
+        const hasRuler = drawings.some((drawing) => drawing.getDrawingName() === DrawingsNames.ruler);
+
+        this.legendContainer.style.display = hasRuler ? 'none' : '';
+      }),
+    );
   }
 
+  public setSelected = () => {
+    this.deselectAllPanes();
+    this.isSelected.next(true);
+  };
+
+  public deselected = () => {
+    this.isSelected.next(false);
+  };
+
   public getLocalMainSeries = () => {
     return this.localMainSeries.value;
   };
diff --git a/src/core/PaneManager.ts b/src/core/PaneManager.ts
index a2e59cbc7ffd88a421c57287c24a23be18694fa3..91c4eee9c7e37596dd510917be79d9d9a398f6a5 100644
--- a/src/core/PaneManager.ts
+++ b/src/core/PaneManager.ts
@@ -1,7 +1,7 @@
-import { type Observable, Subscription } from 'rxjs';
+import { BehaviorSubject, type Observable, Subscription } from 'rxjs';
 
 import { DataSource } from '@core/DataSource';
-import { DrawingsManagerSnapshot } from '@core/DrawingsManager';
+import { DrawingsManagerSnapshot, DrawingSnapshotItem } from '@core/DrawingsManager';
 import { DrawingsManagerCollection } from '@core/DrawingsManagerCollection';
 import { Pane, PaneParams } from '@core/Pane';
 import { PriceAxisLabels } from '@core/PriceAxisLabels';
@@ -29,7 +29,8 @@ type CollectionDependencies =
   | 'setActiveTool'
   | 'getActiveTool'
   | 'getIsEndlessMode'
-  | 'continueDrawing';
+  | 'continueDrawing'
+  | 'deselectAllPanes';
 
 interface PaneManagerStartParams {
   compareEntities$: Observable<Indicator[]>;
@@ -48,6 +49,7 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
   private readonly lwcChart: IChartApi;
   private readonly panesMap = new Map<number, Pane>();
 
+  private selectedPane = new BehaviorSubject<Pane | null>(null);
   private mainPane: Pane;
   private nextPaneId: number;
   private priceAxisLabels: PriceAxisLabels | null = null;
@@ -57,6 +59,13 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
   private readonly subscriptions = new Subscription();
 
   constructor({ panesSnapshot, ...sharedPaneParams }: PaneManagerParams) {
+    this.drawingsManagerCollection = new DrawingsManagerCollection({
+      hotkeys: sharedPaneParams.hotkeys,
+      selectedPane: this.selectedPane.asObservable(),
+      subscribeChartEvent: sharedPaneParams.subscribeChartEvent,
+      lwcChart: sharedPaneParams.lwcChart,
+    });
+
     this.sharedPaneParams = {
       ...sharedPaneParams,
     };
@@ -65,10 +74,6 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
     const mainPaneSnapshot = panesSnapshot.find((paneSnapshot) => paneSnapshot.isMain);
     const mainPaneId = mainPaneSnapshot?.id ?? 0;
 
-    this.drawingsManagerCollection = new DrawingsManagerCollection({
-      hotkeys: sharedPaneParams.hotkeys,
-    });
-
     this.mainPane = new Pane({
       ...this.sharedPaneParams,
       ...this.getCollectionDependencies(),
@@ -229,19 +234,25 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
     this.panesMap.clear();
   }
 
+  private deselectAllPanes(): void {
+    this.panesMap.forEach((pane) => {
+      pane.deselected();
+    });
+  }
+
   private initClickListener(): void {
     const handler = (param: MouseEventParams) => {
       if (param.paneIndex === undefined) {
         return;
       }
 
-      const clickedPane = this.getPaneByIndex(param.paneIndex);
+      this.selectedPane.next(this.getPaneByIndex(param.paneIndex) ?? null);
 
-      if (!clickedPane) {
+      if (!this.selectedPane.value) {
         return;
       }
 
-      this.drawingsManagerCollection.handlePaneClick(clickedPane, param);
+      this.drawingsManagerCollection.handlePaneClick(this.selectedPane.value, param);
     };
 
     this.lwcChart.subscribeClick(handler);
@@ -291,6 +302,7 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
       getActiveTool: () => this.drawingsManagerCollection.getActiveToolValue(),
       getIsEndlessMode: () => this.drawingsManagerCollection.getIsEndlessMode(),
       continueDrawing: (name: DrawingsNames) => this.drawingsManagerCollection.activateDrawingTool(name),
+      deselectAllPanes: () => this.deselectAllPanes(),
     };
   }
 
diff --git a/src/core/__tests__/Pane.test.ts b/src/core/__tests__/Pane.test.ts
index d425fb524776ca1157d81e679d9f23f978d1ffd2..61755d7ae33dd47bab015a96d81e5c09b15b7980 100644
--- a/src/core/__tests__/Pane.test.ts
+++ b/src/core/__tests__/Pane.test.ts
@@ -160,6 +160,7 @@ describe('Pane', () => {
       getActiveTool: jest.fn(),
       getIsEndlessMode: jest.fn(),
       continueDrawing: jest.fn(),
+      deselectAllPanes: jest.fn(),
     });
   }