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


diff --git a/src/components/Toolbar/index.tsx b/src/components/Toolbar/index.tsx
index d8ef129..132ba39 100644
--- a/src/components/Toolbar/index.tsx
+++ b/src/components/Toolbar/index.tsx
@@ -6,8 +6,6 @@ import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
 
 import { Observable } from 'rxjs';
 
-import { Hotkeys, Keys } from '@core/Hotkeys';
-
 import { MenuList } from '@src/components/Menu';
 import SplitDropdown from '@src/components/SplitDropdown';
 
@@ -42,12 +40,11 @@ import styles from './index.module.scss';
 
 interface ToolbarProps {
   toggleDOM: () => void;
-  addDrawing: (name: DrawingsNames) => Promise<void>;
+  addDrawing: (name: DrawingsNames) => void;
   setEndlessDrawingsMode: (value: boolean) => void;
   isEndlessDrawingsMode$: Observable<boolean>;
   activateCrosshair: () => void;
   activeTool$: Observable<ActiveDrawingTool>;
-  hotkeys: Hotkeys;
 }
 
 const implemented = {
@@ -68,7 +65,6 @@ export default function Toolbar({
   isEndlessDrawingsMode$,
   activateCrosshair,
   activeTool$,
-  hotkeys,
 }: ToolbarProps) {
   const [selectedLineType, setSelectedLineType] = useState<DrawingsNames>(DrawingsNames.trendLine);
   const [selectedMeasurementTool, setSelectedMeasurementTool] = useState(DrawingsNames.fixedRangeProfile);
@@ -76,14 +72,13 @@ export default function Toolbar({
   const [selectedGannAndFibonacci, setSelectedGannAndFibonacci] = useState(DrawingsNames.fibonacciRetracement);
 
   const isEndlessDrawingsMode = useObservable(isEndlessDrawingsMode$);
-
   const activeTool = useObservable(activeTool$, 'crosshair');
 
   const toolbarRef = useRef<HTMLDivElement | null>(null);
 
-  const createDrawingHandler = (setter: Dispatch<SetStateAction<DrawingsNames>>) => async (value: DrawingsNames) => {
+  const createDrawingHandler = (setter: Dispatch<SetStateAction<DrawingsNames>>) => (value: DrawingsNames) => {
     setter(value);
-    await addDrawing(value);
+    addDrawing(value);
   };
 
   const findOptionByValue = <T extends { value: string }>(options: T[], selectedValue: T['value']): T | undefined =>
@@ -102,61 +97,26 @@ export default function Toolbar({
   const getTooltipClassName = () => classNames(styles.tooltipHint, CHART_DRAWING_TOOLTIP);
 
   useEffect(() => {
-    const hashT = hotkeys.register({
-      keys: [Keys.alt, Keys.t],
-      callback: async () => {
-        await createDrawingHandler(setSelectedLineType)(DrawingsNames.trendLine);
-      },
-    });
-    const hashH = hotkeys.register({
-      keys: [Keys.alt, Keys.h],
-      callback: async () => {
-        await createDrawingHandler(setSelectedLineType)(DrawingsNames.horizontalLine);
-      },
-    });
-    const hashV = hotkeys.register({
-      keys: [Keys.alt, Keys.v],
-      callback: async () => {
-        await createDrawingHandler(setSelectedLineType)(DrawingsNames.verticalLine);
-      },
-    });
-    const hashF = hotkeys.register({
-      keys: [Keys.alt, Keys.f],
-      callback: async () => {
-        await createDrawingHandler(setSelectedGannAndFibonacci)(DrawingsNames.fibonacciRetracement);
-      },
-    });
-    // const hashShift = hotkeys.register({
-    //   keys: [Keys.shift],
-    //   pressHoldRequired: true,
-    //   callback: async () => {
-    //     await addDrawing(DrawingsNames.ruler);
-    //   },
-    // });
-
-    return () => {
-      hotkeys.unregister({
-        keys: [Keys.alt, Keys.t],
-        hash: hashT,
-      });
-      hotkeys.unregister({
-        keys: [Keys.alt, Keys.h],
-        hash: hashH,
-      });
-      hotkeys.unregister({
-        keys: [Keys.alt, Keys.v],
-        hash: hashV,
-      });
-      hotkeys.unregister({
-        keys: [Keys.alt, Keys.f],
-        hash: hashF,
-      });
-      // hotkeys.unregister({
-      //   keys: [Keys.shift],
-      //   hash: hashShift
-      // });
-    };
-  }, []);
+    if (activeTool === 'crosshair') {
+      return;
+    }
+
+    if (trendLines().some(({ value }) => value === activeTool)) {
+      setSelectedLineType(activeTool);
+      return;
+    }
+    if (gannAndFibonacciTools().some(({ value }) => value === activeTool)) {
+      setSelectedGannAndFibonacci(activeTool);
+      return;
+    }
+    if (geometricShapes().some(({ value }) => value === activeTool)) {
+      setSelectedGeometricShape(activeTool);
+      return;
+    }
+    if (measurementTools().some(({ value }) => value === activeTool)) {
+      setSelectedMeasurementTool(activeTool);
+    }
+  }, [activeTool]);
 
   return (
     <div
@@ -179,6 +139,7 @@ export default function Toolbar({
             />
           </Tooltip>
         )}
+
         {implemented.lines && (
           <SplitDropdown
             anchorRef={toolbarRef}
@@ -197,6 +158,7 @@ export default function Toolbar({
             />
           </SplitDropdown>
         )}
+
         {implemented.fib && (
           <SplitDropdown
             anchorRef={toolbarRef}
@@ -215,6 +177,7 @@ export default function Toolbar({
             />
           </SplitDropdown>
         )}
+
         {implemented.rectangle && (
           <SplitDropdown
             anchorRef={toolbarRef}
@@ -233,6 +196,7 @@ export default function Toolbar({
             />
           </SplitDropdown>
         )}
+
         {implemented.text && (
           <Tooltip
             tooltipClassName={getTooltipClassName()}
@@ -248,6 +212,7 @@ export default function Toolbar({
             />
           </Tooltip>
         )}
+
         {implemented.XABCD && (
           <Tooltip
             tooltipClassName={getTooltipClassName()}
@@ -263,6 +228,7 @@ export default function Toolbar({
             />
           </Tooltip>
         )}
+
         {implemented.position && (
           <SplitDropdown
             anchorRef={toolbarRef}
@@ -281,6 +247,7 @@ export default function Toolbar({
             />
           </SplitDropdown>
         )}
+
         {implemented.icons && (
           <Tooltip
             label={t('Pin')}
@@ -297,10 +264,12 @@ export default function Toolbar({
           </Tooltip>
         )}
       </div>
+
       <Divider
         direction="horizontal"
         pt={{ divider: { className: styles.divider } }}
       />
+
       <div className={classNames(styles.group)}>
         <Tooltip
           tooltipClassName={getTooltipClassName()}
@@ -331,10 +300,12 @@ export default function Toolbar({
           />
         </Tooltip>
       </div>
+
       <Divider
         direction="horizontal"
         pt={{ divider: { className: classNames(styles.divider) } }}
       />
+
       <div className={classNames(styles.group)}>
         <Tooltip
           tooltipClassName={getTooltipClassName()}
@@ -395,10 +366,12 @@ export default function Toolbar({
           />
         </Tooltip>
       </div>
+
       <Divider
         direction="horizontal"
         pt={{ divider: { className: classNames(styles.divider) } }}
       />
+
       <div className={classNames(styles.group)}>
         <Tooltip
           tooltipClassName={getTooltipClassName()}
@@ -424,7 +397,7 @@ export default function Toolbar({
           <Button
             size="sm"
             className={styles.button}
-            onClick={() => toggleDOM()}
+            onClick={toggleDOM}
             label={<LayersIcon />}
           />
         </Tooltip>
diff --git a/src/constants/drawing.ts b/src/constants/drawing.ts
index 86f887c..1869718 100644
--- a/src/constants/drawing.ts
+++ b/src/constants/drawing.ts
@@ -11,6 +11,7 @@ import { SliderPosition } from '@src/core/Drawings/sliderPosition';
 import { Text } from '@src/core/Drawings/text';
 import { Traectory } from '@src/core/Drawings/traectory';
 import { VolumeProfile } from '@src/core/Drawings/volumeProfile';
+import { Keys } from '@src/core/Hotkeys';
 import { t } from '@src/translations';
 import { DrawingConfig, DrawingParams, LineMarker } from '@src/types';
 
@@ -271,3 +272,26 @@ export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
     },
   },
 };
+
+export const DRAWING_HOTKEYS: { keys: readonly Keys[]; drawingName: DrawingsNames }[] = [
+  {
+    keys: [Keys.alt, Keys.t],
+    drawingName: DrawingsNames.trendLine,
+  },
+  {
+    keys: [Keys.alt, Keys.h],
+    drawingName: DrawingsNames.horizontalLine,
+  },
+  {
+    keys: [Keys.alt, Keys.v],
+    drawingName: DrawingsNames.verticalLine,
+  },
+  {
+    keys: [Keys.alt, Keys.f],
+    drawingName: DrawingsNames.fibonacciRetracement,
+  },
+  {
+    keys: [Keys.alt, Keys.shift, Keys.r],
+    drawingName: DrawingsNames.rectangle,
+  },
+];
diff --git a/src/core/Drawings.ts b/src/core/Drawings.ts
index 58e6e16..6df1b97 100644
--- a/src/core/Drawings.ts
+++ b/src/core/Drawings.ts
@@ -23,7 +23,6 @@ interface DrawingParams extends DOMObjectParams {
   deselect: () => void;
   isLocked?: boolean;
   hotkeys: Hotkeys;
-  resetActiveTool: () => void;
 }
 
 export class Drawing extends DOMObject {
@@ -35,9 +34,8 @@ export class Drawing extends DOMObject {
   private settingsSubject: BehaviorSubject<SettingsValues>;
   private subscriptions = new Subscription();
 
-  private escapeUnregisterHash: string | null = null;
-  private deleteUnregisterHash: string | null = null;
-  private copyUnregisterHash: string | null = null;
+  private unregisterDeleteHotkey = () => {};
+  private unregisterCopyHotkey = () => {};
 
   constructor({
     lwcChart,
@@ -58,7 +56,6 @@ export class Drawing extends DOMObject {
     isLocked = false,
     paneId,
     hotkeys,
-    resetActiveTool,
   }: DrawingParams) {
     super({
       id,
@@ -93,48 +90,30 @@ export class Drawing extends DOMObject {
       }),
     );
 
-    this.escapeUnregisterHash = hotkeys.register({
-      keys: [Keys.escape],
-      callback: () => {
-        this.delete();
-        resetActiveTool();
-      },
-    });
-
     this.subscriptions.add(
       selected$.subscribe((isSelectedDrawing) => {
-        if (isSelectedDrawing) {
-          this.deleteUnregisterHash = hotkeys.register({
-            keys: [Keys.delete],
-            callback: () => {
-              this.delete();
-            },
-          });
-
-          this.copyUnregisterHash = hotkeys.register({
-            keys: [Keys.mod, Keys.c],
-            callback: () => {
-              if (!this.isCreationPending()) {
-                onCopy();
-              }
-            },
-          });
-
+        if (!isSelectedDrawing) {
+          this.unregisterSelectedDrawingHotkeys();
           return;
         }
 
-        this.unregisterSelectedDrawingHotkeys();
+        this.unregisterDeleteHotkey = this.hotkeys.register({
+          keys: [Keys.delete],
+          callback: () => {
+            this.delete();
+          },
+        });
+
+        this.unregisterCopyHotkey = this.hotkeys.register({
+          keys: [Keys.mod, Keys.c],
+          callback: () => {
+            if (!this.isCreationPending()) {
+              onCopy();
+            }
+          },
+        });
       }),
     );
-
-    this.waitForCreation().then(() => {
-      hotkeys.unregister({
-        keys: [Keys.escape],
-        hash: this.escapeUnregisterHash,
-      });
-
-      this.escapeUnregisterHash = null;
-    });
   }
 
   public getDrawingName(): DrawingsNames {
@@ -231,13 +210,6 @@ export class Drawing extends DOMObject {
     this.subscriptions.unsubscribe();
     this.unregisterSelectedDrawingHotkeys();
 
-    this.hotkeys.unregister({
-      keys: [Keys.escape],
-      hash: this.escapeUnregisterHash,
-    });
-
-    this.escapeUnregisterHash = null;
-
     this.lockedSubject.complete();
     this.settingsSubject.complete();
 
@@ -246,17 +218,10 @@ export class Drawing extends DOMObject {
   }
 
   private unregisterSelectedDrawingHotkeys(): void {
-    this.hotkeys.unregister({
-      keys: [Keys.delete],
-      hash: this.deleteUnregisterHash,
-    });
-
-    this.hotkeys.unregister({
-      keys: [Keys.mod, Keys.c],
-      hash: this.copyUnregisterHash,
-    });
+    this.unregisterDeleteHotkey();
+    this.unregisterCopyHotkey();
 
-    this.deleteUnregisterHash = null;
-    this.copyUnregisterHash = null;
+    this.unregisterDeleteHotkey = () => {};
+    this.unregisterCopyHotkey = () => {};
   }
 }
diff --git a/src/core/DrawingsManager.tsx b/src/core/DrawingsManager.tsx
index ac05055..5d7d41b 100644
--- a/src/core/DrawingsManager.tsx
+++ b/src/core/DrawingsManager.tsx
@@ -31,6 +31,7 @@ interface DrawingsManagerParams {
   setActiveTool: (name: ActiveDrawingTool) => void;
   getActiveTool: () => ActiveDrawingTool;
   getIsEndlessMode: () => boolean;
+  continueDrawing: (name: DrawingsNames) => void;
 }
 
 export interface DrawingSnapshotItem extends Partial<DOMObjectSnapshot> {
@@ -66,13 +67,13 @@ export class DrawingsManager {
   private selectedDrawing$ = new BehaviorSubject<Drawing | null>(null); // todo: переместить в DrawingsManagerCollection
   private pendingSnapshot: DrawingsManagerSnapshot | null = null;
   private selectedDrawingSnapshot: DrawingSnapshotItem | null = null;
-  private recreateScheduled = false;
   private pane: Pane;
 
   private copyPasteBuffer: DrawingSnapshotItem | null = null;
   private setActiveTool: (name: ActiveDrawingTool) => void;
   private getActiveTool: () => ActiveDrawingTool;
   private getIsEndlessMode: () => boolean;
+  private continueDrawing: (name: DrawingsNames) => void;
 
   constructor({
     eventManager,
@@ -87,6 +88,7 @@ export class DrawingsManager {
     setActiveTool,
     getActiveTool,
     getIsEndlessMode,
+    continueDrawing,
   }: DrawingsManagerParams) {
     this.DOM = DOM;
     this.eventManager = eventManager;
@@ -99,6 +101,7 @@ export class DrawingsManager {
     this.setActiveTool = setActiveTool;
     this.getActiveTool = getActiveTool;
     this.getIsEndlessMode = getIsEndlessMode;
+    this.continueDrawing = continueDrawing;
 
     this.subscriptions.add(
       mainSeries$.subscribe((series) => {
@@ -196,7 +199,6 @@ export class DrawingsManager {
     });
 
     this.DOM.refreshEntities();
-    this.updateActiveTool();
   };
 
   private handleDoubleClick = (event: MouseEvent): void => {
@@ -218,7 +220,6 @@ export class DrawingsManager {
     drawing.getSeriesDrawing().doubleClick(event);
 
     this.DOM.refreshEntities();
-    this.updateActiveTool();
   };
 
   private handleContextMenu = (event: MouseEvent): void => {
@@ -361,7 +362,7 @@ export class DrawingsManager {
     });
   }
 
-  private updateActiveTool = (): void => {
+  private updateActiveTool(): void {
     const hasPendingDrawing = this.drawings$.value.some((drawing) => drawing.isCreationPending());
 
     if (hasPendingDrawing) {
@@ -369,45 +370,15 @@ export class DrawingsManager {
     }
 
     const activeTool = this.getActiveTool();
-    const isSingleInstanceTool = activeTool !== 'crosshair' && drawingsMap[activeTool]?.singleInstance;
 
-    if (activeTool !== 'crosshair' && this.getIsEndlessMode() && !isSingleInstanceTool) {
-      if (this.recreateScheduled) {
-        return;
-      }
-
-      this.recreateScheduled = true;
-
-      queueMicrotask(() => {
-        this.recreateScheduled = false;
-
-        const currentTool = this.getActiveTool();
-        const hasPendingAfterTick = this.drawings$.value.some((drawing) => drawing.isCreationPending());
-
-        if (currentTool === 'crosshair') {
-          return;
-        }
-
-        if (!this.getIsEndlessMode()) {
-          return;
-        }
-
-        if (drawingsMap[currentTool]?.singleInstance) {
-          return;
-        }
-
-        if (hasPendingAfterTick) {
-          return;
-        }
-
-        this.addDrawingForce(currentTool);
-      });
+    if (activeTool !== 'crosshair' && this.getIsEndlessMode()) {
+      this.continueDrawing(activeTool);
 
       return;
     }
 
     this.setActiveTool('crosshair');
-  };
+  }
 
   private removeDrawing = (id: string): void => {
     const drawing = this.findDrawing(id);
@@ -469,7 +440,11 @@ export class DrawingsManager {
     this.DOM.refreshEntities();
   }
 
-  public addDrawingForce = async (name: DrawingsNames, event?: MouseEventParams): Promise<void> => {
+  public addDrawingForce = async (
+    name: DrawingsNames,
+    event?: MouseEventParams,
+    allowEndlessMode = true,
+  ): Promise<void> => {
     this.removePendingDrawings(false);
 
     const previousDrawing = drawingsMap[name].singleInstance
@@ -482,8 +457,17 @@ export class DrawingsManager {
       this.removeDrawingInternal(previousDrawing.id, false);
     }
 
+    if (this.selectedDrawing$.value) {
+      this.selectedDrawing$.next(null);
+    }
+
     this.setActiveTool(name);
-    const drawing = this.createDrawing({ name, event });
+
+    const drawing = this.createDrawing({
+      name,
+      event,
+    });
+
     this.DOM.refreshEntities();
 
     await drawing.waitForCreation();
@@ -497,6 +481,18 @@ export class DrawingsManager {
     }
 
     this.pushDrawingChange(previousSnapshot, this.createDrawingSnapshot(drawing));
+
+    if (this.getActiveTool() === name) {
+      this.selectedDrawing$.next(drawing);
+
+      if (allowEndlessMode) {
+        this.updateActiveTool();
+      } else {
+        this.setActiveTool('crosshair');
+      }
+    }
+
+    this.DOM.refreshEntities();
   };
 
   private createDrawing({
@@ -515,11 +511,6 @@ export class DrawingsManager {
     }
 
     const { id, state, isLocked = false, zIndex, shouldUpdateDrawingsList = true } = options;
-    const shouldSelectAfterCreation = state === undefined;
-
-    if (shouldSelectAfterCreation && this.selectedDrawing$.value) {
-      this.selectedDrawing$.next(null);
-    }
 
     const config = drawingsMap[name];
     const drawingId = id ?? crypto.randomUUID();
@@ -593,9 +584,6 @@ export class DrawingsManager {
         isLocked,
         paneId: this.paneId,
         hotkeys: this.hotkeys,
-        resetActiveTool: () => {
-          this.setActiveTool('crosshair');
-        },
       });
 
     const entity = this.DOM.setEntity<Drawing>(drawingFactory, zIndex);
@@ -610,18 +598,6 @@ export class DrawingsManager {
       this.drawings$.next([...this.drawings$.value, entity].sort((left, right) => left.zIndex - right.zIndex));
     }
 
-    if (shouldSelectAfterCreation) {
-      entity.waitForCreation().then(() => {
-        if (!this.drawings$.value.includes(entity)) {
-          return;
-        }
-
-        this.selectedDrawing$.next(entity);
-        this.updateActiveTool();
-        this.DOM.refreshEntities();
-      });
-    }
-
     return entity;
   }
 
@@ -672,9 +648,8 @@ export class DrawingsManager {
     this.DOM.refreshEntities();
   }
 
-  public activateCrosshair(): void {
+  public cancelPendingDrawing(): void {
     this.removePendingDrawings(false);
-    this.setActiveTool('crosshair');
     this.DOM.refreshEntities();
   }
 
diff --git a/src/core/DrawingsManagerCollection.ts b/src/core/DrawingsManagerCollection.ts
index f5a4573..b7ce1a2 100644
--- a/src/core/DrawingsManagerCollection.ts
+++ b/src/core/DrawingsManagerCollection.ts
@@ -1,26 +1,46 @@
 import { BehaviorSubject, Observable } from 'rxjs';
 
-import { DrawingsNames } from '@src/constants';
+import { DRAWING_HOTKEYS, DrawingsNames } from '@src/constants';
 import { ActiveDrawingTool, SettingsValues } from '@src/types';
 
 import { Drawing } from './Drawings';
 import { DrawingsManager } from './DrawingsManager';
 import { Hotkeys, Keys } from './Hotkeys';
-import { PaneManager } from './PaneManager';
+
+import type { Pane } from './Pane';
+
+import type { MouseEventParams } from 'lightweight-charts';
+
 // todo: нужно дописывать класс)
 export class DrawingsManagerCollection {
   private managersMap: Map<number, DrawingsManager> = new Map();
-  private paneCollection: PaneManager;
   private hotkeys: Hotkeys;
 
   private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
   private endlessMode$ = new BehaviorSubject(false);
-  private paneClickListenerUnsub = () => {};
-  private escapeUnregisterHash: string | null = null;
+  private isWaitingForDrawingStart = false;
+  private unregisterHotkeys: (() => void)[] = [];
 
-  constructor({ paneCollection, hotkeys }: { paneCollection: PaneManager; hotkeys: Hotkeys }) {
-    this.paneCollection = paneCollection;
+  constructor({ hotkeys }: { hotkeys: Hotkeys }) {
     this.hotkeys = hotkeys;
+
+    for (const { keys, drawingName } of DRAWING_HOTKEYS) {
+      this.unregisterHotkeys.push(
+        this.hotkeys.register({
+          keys,
+          callback: () => {
+            this.addDrawingForce(drawingName);
+          },
+        }),
+      );
+    }
+
+    this.unregisterHotkeys.push(
+      this.hotkeys.register({
+        keys: [Keys.escape],
+        callback: this.activateCrosshair,
+      }),
+    );
   }
 
   public getIsEndlessMode(): boolean {
@@ -31,40 +51,51 @@ export class DrawingsManagerCollection {
     return this.activeTool$.value;
   }
 
-  public setActiveTool(next: ActiveDrawingTool) {
+  public setActiveTool(next: ActiveDrawingTool): void {
     this.activeTool$.next(next);
+
+    if (next === 'crosshair') {
+      this.isWaitingForDrawingStart = false;
+    }
   }
 
-  public removeDrawingManager(paneId: number) {
+  public removeDrawingManager(paneId: number): void {
     this.managersMap.delete(paneId);
   }
 
-  public addDrawingManager(manager: DrawingsManager, paneId: number) {
+  public addDrawingManager(manager: DrawingsManager, paneId: number): void {
     this.managersMap.set(paneId, manager);
   }
 
-  public addDrawingForce = async (name: DrawingsNames): Promise<void> => {
-    this.paneClickListenerUnsub();
+  public addDrawingForce = (name: DrawingsNames): void => {
+    this.cancelPendingDrawings();
     this.setActiveTool(name);
-
-    this.paneClickListenerUnsub = this.paneCollection.listenPanesToAddDrawing(name);
+    this.isWaitingForDrawingStart = true;
   };
 
-  public setEndlessDrawingMode = (value: boolean): void => {
-    if (value) {
-      this.escapeUnregisterHash = this.hotkeys.register({
-        keys: [Keys.escape],
-        callback: () => {
-          this.setEndlessDrawingMode(false);
-        },
-      });
-    } else {
-      this.hotkeys.unregister({
-        keys: [Keys.escape],
-        hash: this.escapeUnregisterHash,
-      });
+  public handlePaneClick(pane: Pane, event: MouseEventParams): void {
+    const activeTool = this.activeTool$.value;
+
+    if (activeTool === 'crosshair') {
+      if (event.sourceEvent?.shiftKey) {
+        pane.getDrawingManager().addDrawingForce(DrawingsNames.ruler, event, false);
+      }
+
+      return;
+    }
 
-      this.escapeUnregisterHash = null;
+    if (!this.isWaitingForDrawingStart) {
+      return;
+    }
+
+    this.isWaitingForDrawingStart = false;
+
+    pane.getDrawingManager().addDrawingForce(activeTool, event);
+  }
+
+  public setEndlessDrawingMode = (value: boolean): void => {
+    if (this.endlessMode$.value === value) {
+      return;
     }
 
     this.endlessMode$.next(value);
@@ -74,13 +105,10 @@ export class DrawingsManagerCollection {
     return this.endlessMode$.asObservable();
   }
 
-  public activateCrosshair(): void {
-    this.paneClickListenerUnsub();
-
-    Array.from(this.managersMap.values()).forEach((manager) => {
-      manager.activateCrosshair();
-    });
-  }
+  public activateCrosshair = (): void => {
+    this.cancelPendingDrawings();
+    this.setActiveTool('crosshair');
+  };
 
   public getActiveTool(): Observable<ActiveDrawingTool> {
     return this.activeTool$.asObservable();
@@ -91,9 +119,9 @@ export class DrawingsManagerCollection {
   }
 
   public updateSelectedDrawingSettings = (settings: SettingsValues): void => {
-    Array.from(this.managersMap.values()).forEach((manager) => {
+    for (const manager of this.managersMap.values()) {
       manager.updateSelectedDrawingSettings(settings);
-    });
+    }
   };
 
   public toggleSelectedDrawingLock = (): void => {
@@ -108,13 +136,20 @@ export class DrawingsManagerCollection {
     Array.from(this.managersMap.values())[0].deleteSelectedDrawing();
   };
 
-  public destroy() {
-    this.hotkeys.unregister({
-      keys: [Keys.escape],
-      hash: this.escapeUnregisterHash,
-    });
+  public destroy(): void {
+    for (const unregisterHotkey of this.unregisterHotkeys) {
+      unregisterHotkey();
+    }
+
+    this.unregisterHotkeys = [];
 
     this.activeTool$.complete();
-    this.paneClickListenerUnsub();
+    this.endlessMode$.complete();
+  }
+
+  private cancelPendingDrawings(): void {
+    for (const manager of this.managersMap.values()) {
+      manager.cancelPendingDrawing();
+    }
   }
 }
diff --git a/src/core/Hotkeys.ts b/src/core/Hotkeys.ts
index cfa9019..5636238 100644
--- a/src/core/Hotkeys.ts
+++ b/src/core/Hotkeys.ts
@@ -11,80 +11,82 @@ export enum Keys {
   'h' = 'keyh',
   'v' = 'keyv',
   'f' = 'keyf',
+  'r' = 'keyr',
+  'y' = 'keyy',
   'z' = 'keyz',
   'c' = 'keyc',
-  'mousedown' = 'mousedown',
 }
 
 type HotkeyCallback = () => void | Promise<void>;
+type UnregisterHotkey = () => void;
 
 interface RegisterHotkeyParams {
-  keys: Keys[];
+  keys: readonly Keys[];
   callback: HotkeyCallback;
-  pressHoldRequired?: boolean;
-}
-
-interface UnregisterHotkeyParams {
-  keys: Keys[];
-  hash?: string | null;
 }
 
 interface HotkeyRegistration {
-  keys: Keys[];
   callback: HotkeyCallback;
-  pressHoldRequired: boolean;
 }
 
 export interface IHotkeys {
-  register(params: RegisterHotkeyParams): string | null;
-  unregister(params: UnregisterHotkeyParams): void;
+  register(params: RegisterHotkeyParams): UnregisterHotkey;
+  destroy(): void;
 }
 
 // todo: возможно можно сделать синглтоном и использовать импортируя экземпляр класса,
 //  там где это нужно вместо props drilling, как сейчас
 
 export class Hotkeys implements IHotkeys {
-  private registrations = new Map<string, HotkeyRegistration>();
-  private activeHoldRegistrations = new Set<string>();
+  private registrations = new Map<string, HotkeyRegistration[]>();
 
   constructor() {
     document.addEventListener('keydown', this.handleKeyDown);
-    document.addEventListener('keyup', this.handleKeyUp);
   }
 
-  public register({ keys, callback, pressHoldRequired = false }: RegisterHotkeyParams): string | null {
-    if (keys.length === 0) {
-      console.error('[Hotkeys] попытка задать пустой хоткей');
+  public register({ keys, callback }: RegisterHotkeyParams): UnregisterHotkey {
+    validateKeys(keys);
+
+    const registration: HotkeyRegistration = {
+      callback,
+    };
 
-      return null;
+    const shortcuts = getRegistrationShortcuts(keys);
+
+    for (const shortcut of shortcuts) {
+      const registrations = this.registrations.get(shortcut);
+
+      if (registrations) {
+        registrations.push(registration);
+      } else {
+        this.registrations.set(shortcut, [registration]);
+      }
     }
 
-    const hash = `hotkeyCallback-${crypto.randomUUID()}`;
+    return () => {
+      for (const shortcut of shortcuts) {
+        const registrations = this.registrations.get(shortcut);
 
-    this.registrations.set(hash, {
-      keys,
-      callback,
-      pressHoldRequired,
-    });
+        if (!registrations) {
+          continue;
+        }
 
-    return hash;
-  }
+        const registrationIndex = registrations.lastIndexOf(registration);
 
-  public unregister({ hash }: UnregisterHotkeyParams): void {
-    if (!hash) {
-      return;
-    }
+        if (registrationIndex !== -1) {
+          registrations.splice(registrationIndex, 1);
+        }
 
-    this.registrations.delete(hash);
-    this.activeHoldRegistrations.delete(hash);
+        if (registrations.length === 0) {
+          this.registrations.delete(shortcut);
+        }
+      }
+    };
   }
 
   public destroy(): void {
     document.removeEventListener('keydown', this.handleKeyDown);
-    document.removeEventListener('keyup', this.handleKeyUp);
-
     this.registrations.clear();
-    this.activeHoldRegistrations.clear();
   }
 
   private handleKeyDown = async (event: KeyboardEvent): Promise<void> => {
@@ -92,102 +94,132 @@ export class Hotkeys implements IHotkeys {
       return;
     }
 
-    const registrations = [...this.registrations.entries()]
-      .filter(([, registration]) => matchesHotkey(event, registration.keys))
-      .reverse();
+    const registrations = this.registrations.get(getEventShortcut(event));
+    const registration = registrations?.[registrations.length - 1];
 
-    if (registrations.length === 0) {
+    if (!registration) {
       return;
     }
 
     event.preventDefault();
 
-    for (const [hash, registration] of registrations) {
-      if (registration.pressHoldRequired) {
-        this.activeHoldRegistrations.add(hash);
-      }
+    await registration.callback();
+  };
+}
+
+function validateKeys(keys: readonly Keys[]): void {
+  if (keys.length === 0) {
+    throw new Error('[Hotkeys] попытка задать пустой хоткей');
+  }
 
-      // eslint-disable-next-line no-await-in-loop
-      await registration.callback();
+  let primaryKeysCount = 0;
+  let hasMod = false;
+  let hasControl = false;
+  let hasMeta = false;
+
+  for (const key of keys) {
+    if (!isModifier(key)) {
+      primaryKeysCount += 1;
     }
-  };
 
-  private handleKeyUp = async (event: KeyboardEvent): Promise<void> => {
-    if (this.activeHoldRegistrations.size === 0) {
-      return;
+    if (key === Keys.mod) {
+      hasMod = true;
     }
 
-    const releasedKey = normalizeCode(event.code);
-    let shouldReset = false;
+    if (key === Keys.control) {
+      hasControl = true;
+    }
 
-    for (const hash of this.activeHoldRegistrations) {
-      const registration = this.registrations.get(hash);
+    if (key === Keys.meta) {
+      hasMeta = true;
+    }
+  }
 
-      if (!registration) {
-        this.activeHoldRegistrations.delete(hash);
-        continue;
-      }
+  if (primaryKeysCount > 1) {
+    throw new Error('[Hotkeys] хоткей может содержать только одну основную клавишу');
+  }
 
-      if (!registration.keys.some((key) => matchesReleasedKey(key, releasedKey))) {
-        continue;
-      }
+  if (hasMod && (hasControl || hasMeta)) {
+    throw new Error('[Hotkeys] mod нельзя использовать вместе с control или meta');
+  }
+}
 
-      this.activeHoldRegistrations.delete(hash);
-      shouldReset = true;
-    }
+function getRegistrationShortcuts(keys: readonly Keys[]): string[] {
+  if (!keys.includes(Keys.mod)) {
+    return [createShortcut(keys)];
+  }
 
-    if (!shouldReset) {
-      return;
-    }
+  const keysWithoutMod = keys.filter((key) => key !== Keys.mod);
 
-    const escapeCallbacks = [...this.registrations.values()]
-      .filter(({ keys }) => keys.length === 1 && keys[0] === Keys.escape)
-      .map(({ callback }) => callback)
-      .reverse();
+  return [
+    createShortcut([...keysWithoutMod, Keys.control]),
+    createShortcut([...keysWithoutMod, Keys.meta]),
+    createShortcut([...keysWithoutMod, Keys.control, Keys.meta]),
+  ];
+}
 
-    for (const callback of escapeCallbacks) {
-      // eslint-disable-next-line no-await-in-loop
-      await callback();
-    }
-  };
+function getEventShortcut(event: KeyboardEvent): string {
+  const key = normalizeCode(event.code);
+
+  return buildShortcut(event.ctrlKey, event.metaKey, event.shiftKey, event.altKey, isModifier(key) ? '' : key);
 }
 
-function matchesHotkey(event: KeyboardEvent, keys: Keys[]): boolean {
-  const eventKey = normalizeCode(event.code);
-  const primaryKeys = keys.filter((key) => !isModifier(key));
+function createShortcut(keys: readonly Keys[]): string {
+  let control = false;
+  let meta = false;
+  let shift = false;
+  let alt = false;
+  let primaryKey: Keys | '' = '';
 
-  if (primaryKeys.length > 1) {
-    return false;
-  }
+  for (const key of keys) {
+    switch (key) {
+      case Keys.control:
+        control = true;
+        break;
 
-  if (primaryKeys.length === 1 && primaryKeys[0] !== eventKey) {
-    return false;
-  }
+      case Keys.meta:
+        meta = true;
+        break;
 
-  if (primaryKeys.length === 0 && !matchesModifierKey(keys, eventKey)) {
-    return false;
-  }
+      case Keys.shift:
+        shift = true;
+        break;
+
+      case Keys.alt:
+        alt = true;
+        break;
 
-  if (keys.includes(Keys.mod)) {
-    if (!event.ctrlKey && !event.metaKey) {
-      return false;
+      case Keys.mod:
+        break;
+
+      default:
+        primaryKey = key;
     }
-  } else if (event.ctrlKey !== keys.includes(Keys.control) || event.metaKey !== keys.includes(Keys.meta)) {
-    return false;
   }
 
-  return event.shiftKey === keys.includes(Keys.shift) && event.altKey === keys.includes(Keys.alt);
+  return buildShortcut(control, meta, shift, alt, primaryKey);
 }
 
-function matchesModifierKey(keys: Keys[], eventKey: Keys): boolean {
-  return keys.includes(eventKey) || (keys.includes(Keys.mod) && (eventKey === Keys.control || eventKey === Keys.meta));
-}
+function buildShortcut(control: boolean, meta: boolean, shift: boolean, alt: boolean, primaryKey: Keys | ''): string {
+  let shortcut = '';
+
+  if (control) {
+    shortcut = Keys.control;
+  }
+  if (meta) {
+    shortcut += `${shortcut ? '+' : ''}${Keys.meta}`;
+  }
+  if (shift) {
+    shortcut += `${shortcut ? '+' : ''}${Keys.shift}`;
+  }
+  if (alt) {
+    shortcut += `${shortcut ? '+' : ''}${Keys.alt}`;
+  }
+  if (primaryKey) {
+    shortcut += `${shortcut ? '+' : ''}${primaryKey}`;
+  }
 
-function matchesReleasedKey(registeredKey: Keys, releasedKey: Keys): boolean {
-  return (
-    registeredKey === releasedKey ||
-    (registeredKey === Keys.mod && (releasedKey === Keys.control || releasedKey === Keys.meta))
-  );
+  return shortcut;
 }
 
 function isModifier(key: Keys): boolean {
diff --git a/src/core/MoexChart.tsx b/src/core/MoexChart.tsx
index 622fad5..f020aa6 100644
--- a/src/core/MoexChart.tsx
+++ b/src/core/MoexChart.tsx
@@ -189,6 +189,11 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
         callback: undoRedo.undo,
       });
 
+      this.hotkeys.register({
+        keys: [Keys.mod, Keys.y],
+        callback: undoRedo.redo,
+      });
+
       this.hotkeys.register({
         keys: [Keys.mod, Keys.shift, Keys.z],
         callback: undoRedo.redo,
@@ -367,7 +372,6 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
           isEndlessDrawingsMode$={drawingCollection.isEndlessDrawingsMode()}
           activateCrosshair={() => drawingCollection.activateCrosshair()}
           activeTool$={drawingCollection.getActiveTool()}
-          hotkeys={this.hotkeys}
         />,
       );
     }
diff --git a/src/core/Pane.tsx b/src/core/Pane.tsx
index 753f7bf..4fa6680 100644
--- a/src/core/Pane.tsx
+++ b/src/core/Pane.tsx
@@ -1,4 +1,4 @@
-import { IChartApi, IPaneApi, MouseEventParams, PriceScaleMode, Time } from 'lightweight-charts';
+import { IChartApi, IPaneApi, PriceScaleMode, Time } from 'lightweight-charts';
 
 import { BehaviorSubject, Subscription } from 'rxjs';
 
@@ -58,6 +58,7 @@ export interface PaneParams {
   setActiveTool: (name: ActiveDrawingTool) => void;
   getActiveTool: () => ActiveDrawingTool;
   getIsEndlessMode: () => boolean;
+  continueDrawing: (name: DrawingsNames) => void;
 }
 
 // todo: Pane, ему должна принадлежать mainSerie, а также IndicatorManager и drawingsManager, mouseEvents. Также перекинуть соответствующие/необходимые свойства из чарта, и из чарта удалить
@@ -65,7 +66,6 @@ export interface PaneParams {
 // todo: в CompareManage, при создании нового пейна для сравнения - инициализируем новый dataSource, принадлежащий только конкретному пейну. Убираем возможность добавлять индикаторы на такие пейны
 // todo: на каждый символ свой DataSource (учитывать что есть MainPane и "главный" DataSource, который инициализиурется во время старта moexChart)
 // todo: сделать два разных представления для compare, в зависимости от отображения на главном пейне или на второстепенном
-
 export class Pane implements ISerializable<PaneSnapshot> {
   private readonly id: number;
   private readonly isMain: boolean;
@@ -96,7 +96,6 @@ export class Pane implements ISerializable<PaneSnapshot> {
   private readonly onPriceScaleStateChange: () => void;
   private readonly subscriptions = new Subscription();
   private paneContainerSyncFrameId: number | null = null;
-  private initialDrawingClickListener: (event: MouseEventParams) => void = () => {};
 
   constructor({
     lwcChart,
@@ -122,6 +121,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
     setActiveTool,
     getActiveTool,
     getIsEndlessMode,
+    continueDrawing,
   }: PaneParams) {
     this.onDelete = onDelete;
     this.onPriceScaleStateChange = onPriceScaleStateChange;
@@ -194,6 +194,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
       setActiveTool,
       getActiveTool,
       getIsEndlessMode,
+      continueDrawing,
     });
 
     addDrawingManager(this.drawingsManager, this.id);
@@ -208,21 +209,6 @@ export class Pane implements ISerializable<PaneSnapshot> {
     );
   }
 
-  public fireClick = (event: MouseEventParams) => {
-    this.initialDrawingClickListener(event);
-  };
-
-  public unsubscribeInitialDrawingClick = () => {
-    this.initialDrawingClickListener = () => {};
-  };
-
-  public subscribeInitialDrawingClick = (cb: () => void, name: DrawingsNames) => {
-    this.initialDrawingClickListener = (event: MouseEventParams) => {
-      this.drawingsManager.addDrawingForce(name, event);
-      cb();
-    };
-  };
-
   public getLocalMainSeries = () => {
     return this.localMainSeries.value;
   };
@@ -262,6 +248,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
           resolve();
         }
       };
+
       check();
     });
   };
@@ -344,6 +331,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
       cancelAnimationFrame(this.paneContainerSyncFrameId);
       this.paneContainerSyncFrameId = null;
     }
+
     this.drawingsManager.destroy();
 
     this.subscriptions.unsubscribe();
@@ -497,6 +485,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
 
     if (!lwcPaneElement) {
       this.schedulePaneContainerSync();
+
       return;
     }
 
diff --git a/src/core/PaneManager.ts b/src/core/PaneManager.ts
index 20f8034..f95ba9c 100644
--- a/src/core/PaneManager.ts
+++ b/src/core/PaneManager.ts
@@ -28,7 +28,8 @@ type CollectionDependencies =
   | 'removeDrawingManager'
   | 'setActiveTool'
   | 'getActiveTool'
-  | 'getIsEndlessMode';
+  | 'getIsEndlessMode'
+  | 'continueDrawing';
 
 interface PaneManagerStartParams {
   compareEntities$: Observable<Indicator[]>;
@@ -42,7 +43,6 @@ type SharedPaneParams = Omit<PaneManagerParams, 'panesSnapshot'>;
 // todo: в CompareManage, при создании нового пейна для сравнения - инициализируем новый dataSource, принадлежащий только конкретному пейну. Убираем возможность добавлять индикаторы на такие пейны
 // todo: на каждый символ свой DataSource (учитывать что есть MainPane и "главный" DataSource, который инициализиурется во время старта moexChart)
 // todo: сделать два разных представления для compare, в зависимости от отображения на главном пейне или на второстепенном
-
 export class PaneManager implements ISerializable<PaneSnapshot[]> {
   private readonly sharedPaneParams: SharedPaneParams;
   private readonly lwcChart: IChartApi;
@@ -61,11 +61,11 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
       ...sharedPaneParams,
     };
     this.lwcChart = sharedPaneParams.lwcChart;
+
     const mainPaneSnapshot = panesSnapshot.find((paneSnapshot) => paneSnapshot.isMain);
     const mainPaneId = mainPaneSnapshot?.id ?? 0;
 
     this.drawingsManagerCollection = new DrawingsManagerCollection({
-      paneCollection: this,
       hotkeys: sharedPaneParams.hotkeys,
     });
 
@@ -117,21 +117,6 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
     });
   }
 
-  public listenPanesToAddDrawing(name: DrawingsNames): () => void {
-    const panes = Array.from(this.panesMap.values());
-    const unsub = () => {
-      panes.forEach((pane) => {
-        pane.unsubscribeInitialDrawingClick();
-      });
-    };
-
-    panes.forEach((pane) => {
-      pane.subscribeInitialDrawingClick(unsub, name);
-    });
-
-    return unsub;
-  }
-
   public getDrawingsCollectionManager(): DrawingsManagerCollection {
     return this.drawingsManagerCollection;
   }
@@ -158,7 +143,13 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
   }
 
   public getPaneByIndex(index: number): Pane | undefined {
-    return Array.from(this.panesMap.values()).find((pane) => pane.paneIndex() === index);
+    for (const pane of this.panesMap.values()) {
+      if (pane.paneIndex() === index) {
+        return pane;
+      }
+    }
+
+    return undefined;
   }
 
   public getPaneById(id: number): Pane | undefined {
@@ -236,9 +227,17 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
 
   private initClickListener(): void {
     const handler = (param: MouseEventParams) => {
-      if (param.paneIndex === undefined) return;
+      if (param.paneIndex === undefined) {
+        return;
+      }
+
       const clickedPane = this.getPaneByIndex(param.paneIndex);
-      clickedPane?.fireClick(param);
+
+      if (!clickedPane) {
+        return;
+      }
+
+      this.drawingsManagerCollection.handlePaneClick(clickedPane, param);
     };
 
     this.lwcChart.subscribeClick(handler);
@@ -260,8 +259,6 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
 
     const paneIndex = pane.paneIndex();
 
-    this.subscriptions.unsubscribe();
-
     this.panesMap.delete(id);
     pane.destroy();
 
@@ -289,6 +286,7 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
       setActiveTool: (name: ActiveDrawingTool) => this.drawingsManagerCollection.setActiveTool(name),
       getActiveTool: () => this.drawingsManagerCollection.getActiveToolValue(),
       getIsEndlessMode: () => this.drawingsManagerCollection.getIsEndlessMode(),
+      continueDrawing: (name: DrawingsNames) => this.drawingsManagerCollection.addDrawingForce(name),
     };
   }