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


diff --git a/src/components/FloatingToolbar/index.tsx b/src/components/FloatingToolbar/index.tsx
index d055097..cce1bd7 100644
--- a/src/components/FloatingToolbar/index.tsx
+++ b/src/components/FloatingToolbar/index.tsx
@@ -15,6 +15,7 @@ import type { Observable } from 'rxjs';
 
 interface FloatingDrawingToolbarProps {
   selectedDrawing$: Observable<Drawing | null>;
+  onUpdateSettings: (settings: SettingsValues) => void;
   onToggleLock: () => void;
   onOpenSettings: () => void;
   onDelete: () => void;
@@ -37,6 +38,7 @@ const TOOLBAR_TOP_OFFSET = 12;
 
 export function FloatingDrawingToolbar({
   selectedDrawing$,
+  onUpdateSettings,
   onToggleLock,
   onOpenSettings,
   onDelete,
@@ -235,7 +237,7 @@ export function FloatingDrawingToolbar({
             role={field.toolbar.role}
             value={String(settings[field.key] ?? field.defaultValue)}
             onChange={(value) => {
-              selectedDrawing.updateSettings({
+              onUpdateSettings({
                 [field.key]: value,
               });
             }}
diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx
index bc5a4bb..d0b8ba8 100644
--- a/src/components/Header/index.tsx
+++ b/src/components/Header/index.tsx
@@ -1,17 +1,14 @@
 import { Button, Divider } from 'exchange-elements/v2';
 import { useEffect, useState } from 'react';
-import { Observable } from 'rxjs';
 
 import { IndicatorsSelect } from '@components/IndicatorsSelect';
 
 import { IndicatorsIds } from '@src/constants';
 import { FullscreenController } from '@src/core/Fullscreen';
-import { UndoRedo } from '@src/core/UndoRedo';
+import { UndoRedo, type UndoRedoState } from '@src/core/UndoRedo';
 import { t } from '@src/translations';
 import { ChartSeriesType } from '@src/types';
-
 import { Timeframes } from '@src/types/timeframes';
-
 import { useObservable } from '@src/utils';
 
 import { Dropdown } from '../Dropdown';
@@ -26,11 +23,17 @@ import {
   SearchIcon,
   UndoIcon,
 } from '../Icon';
-
 import { SeriesMenu, TimeframesMenu } from '../Menu';
 
 import styles from './index.module.scss';
 
+import type { Observable } from 'rxjs';
+
+const defaultUndoRedoState: UndoRedoState = {
+  canUndo: false,
+  canRedo: false,
+};
+
 interface HeaderProps {
   timeframes: Timeframes[];
   selectedTimeframeObs: Observable<Timeframes>;
@@ -74,20 +77,39 @@ export function Header({
 }: HeaderProps) {
   const [isToolbarOpen, setIsToolbarOpen] = useState(false);
   const [isFullscreen, setIsFullscreen] = useState(fullscreen.isFullscreen);
+  const [undoRedoState, setUndoRedoState] = useState(defaultUndoRedoState);
 
   useEffect(() => {
-    const unsubscribe = fullscreen.onChange(() => setIsFullscreen(fullscreen.isFullscreen));
+    const unsubscribe = fullscreen.onChange(() => {
+      setIsFullscreen(fullscreen.isFullscreen);
+    });
 
     return unsubscribe;
   }, [fullscreen]);
 
+  useEffect(() => {
+    if (!undoRedo) {
+      setUndoRedoState(defaultUndoRedoState);
+
+      return;
+    }
+
+    const subscription = undoRedo.getState().subscribe(setUndoRedoState);
+
+    return () => {
+      subscription.unsubscribe();
+    };
+  }, [undoRedo]);
+
   const selectedTimeframe = useObservable(selectedTimeframeObs);
   const selectedSeries = useObservable(selectedSeriesObs);
 
   const seriesDropdownValue =
     selectedSeries === 'Line' ? <LineIcon /> : selectedSeries === 'Bar' ? <BarIcon /> : <CandleStickIcon />;
 
-  const handleOpenToolbar = () => setIsToolbarOpen(() => toggleToolbarVisible());
+  const handleOpenToolbar = (): void => {
+    setIsToolbarOpen(toggleToolbarVisible());
+  };
 
   return (
     <header className={styles.header}>
@@ -148,8 +170,8 @@ export function Header({
         {showSettingsModal && (
           <Button
             size="sm"
-            className={`${styles.button}`}
-            onClick={() => showSettingsModal()}
+            className={styles.button}
+            onClick={showSettingsModal}
             label={t('Settings')}
           />
         )}
@@ -165,27 +187,27 @@ export function Header({
         </Dropdown>
       </div>
 
-      {(!!undoRedo || showFullscreenButton) && (
+      {(undoRedo || showFullscreenButton) && (
         <Divider
           direction="vertical"
           pt={{ divider: { className: styles.divider } }}
         />
       )}
 
-      {!!undoRedo && (
+      {undoRedo && (
         <div className={styles.group}>
           <Button
             size="sm"
             className={styles.button}
-            onClick={() => undoRedo?.undo()}
-            disabled={!undoRedo?.canUndo()}
+            onClick={undoRedo.undo}
+            disabled={!undoRedoState.canUndo}
             label={<UndoIcon />}
           />
           <Button
             size="sm"
             className={styles.button}
-            onClick={() => undoRedo?.redo()}
-            disabled={!undoRedo?.canRedo()}
+            onClick={undoRedo.redo}
+            disabled={!undoRedoState.canRedo}
             label={<RedoIcon />}
           />
         </div>
diff --git a/src/core/DOMModel.tsx b/src/core/DOMModel.tsx
index 15c4d57..236e9ad 100644
--- a/src/core/DOMModel.tsx
+++ b/src/core/DOMModel.tsx
@@ -25,74 +25,60 @@ export class DOMModel {
   }
 
   public removeEntity = <T extends IDOMObject>(entity: T): void => {
-    this.entities.next(this.entities.value.filter((d) => d.id !== entity.id));
+    this.entities.next(this.entities.value.filter((item) => item.id !== entity.id));
   };
 
   public setEntity = <T extends IDOMObject>(
-    cb: (zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) => T,
+    callback: (zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) => T,
+    zIndex?: number,
   ): T => {
-    const entity = cb(this.lastZIndex++, this.moveUp, this.moveDown);
+    const entityZIndex = zIndex ?? this.lastZIndex;
+    const entity = callback(entityZIndex, this.moveUp, this.moveDown);
 
-    this.entities.next([...this.entities.value, entity].sort((a, b) => a.zIndex - b.zIndex));
+    this.lastZIndex = Math.max(this.lastZIndex, entityZIndex + 1);
+    this.entities.next([...this.entities.value, entity].sort((left, right) => left.zIndex - right.zIndex));
 
     return entity;
   };
 
   private moveUp = (id: string): void => {
-    const entities = this.entities.value;
-
-    const curr = entities.find((e) => e.id === id);
-
-    if (!curr) {
-      return;
-    }
-
-    const next = entities.find((e) => e.zIndex === curr.zIndex + 1);
-
-    const ent = entities.filter((e) => e.id !== id && e.zIndex !== curr.zIndex + 1);
-
-    if (!next) {
-      return;
-    }
-    const [left, right] = [curr.zIndex, next.zIndex];
-
-    curr.setZIndex(right);
-    next.setZIndex(left);
-
-    this.entities.next([...ent, curr, next].sort((a, b) => a.zIndex - b.zIndex));
+    this.moveEntity(id, 1);
   };
 
   private moveDown = (id: string): void => {
-    const entities = this.entities.value;
+    this.moveEntity(id, -1);
+  };
 
-    const curr = entities.find((e) => e.id === id);
+  private moveEntity(id: string, direction: -1 | 1): void {
+    const entities = [...this.entities.value].sort((left, right) => left.zIndex - right.zIndex);
 
-    if (!curr) {
+    const currentIndex = entities.findIndex((entity) => entity.id === id);
+
+    if (currentIndex === -1) {
       return;
     }
 
-    const prev = entities.find((e) => e.zIndex === curr.zIndex - 1);
-
-    const ent = entities.filter((e) => e.id !== id && e.zIndex !== curr.zIndex - 1);
+    const target = entities[currentIndex + direction];
 
-    if (!prev) {
+    if (!target) {
       return;
     }
 
-    const [left, right] = [curr.zIndex, prev.zIndex];
+    const current = entities[currentIndex];
+    const currentZIndex = current.zIndex;
 
-    curr.setZIndex(right);
-    prev.setZIndex(left);
+    current.setZIndex(target.zIndex);
+    target.setZIndex(currentZIndex);
 
-    this.entities.next([...ent, curr, prev].sort((a, b) => a.zIndex - b.zIndex));
-  };
+    this.entities.next(entities.sort((left, right) => left.zIndex - right.zIndex));
+  }
 
   public getEntities = (): Observable<IDOMObject[]> => {
     return this.entities.pipe(map((entities) => entities.filter((entity) => entity.shouldShowInObjectTree())));
   };
 
   public refreshEntities = (): void => {
-    this.entities.next([...this.entities.value]);
+    this.entities.next([...this.entities.value].sort((left, right) => left.zIndex - right.zIndex));
   };
 
   public toggleDOM = () => {
diff --git a/src/core/Drawings.ts b/src/core/Drawings.ts
index ed9f8da..abafeec 100644
--- a/src/core/Drawings.ts
+++ b/src/core/Drawings.ts
@@ -2,7 +2,6 @@ import { IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
 import { BehaviorSubject, Observable, Subscription } from 'rxjs';
 
 import { DOMObject, DOMObjectParams } from '@core/DOMObject';
-import { DrawingSnapshotItem } from '@core/DrawingsManager';
 import { Hotkeys, Keys } from '@core/Hotkeys';
 
 import { DrawingsNames } from '@src/constants';
@@ -11,13 +10,12 @@ import { SettingsTab, SettingsValues, ToolbarSettingField } from '@src/types/set
 
 import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
 
-type IDrawing = DOMObject;
-
 interface DrawingParams extends DOMObjectParams {
   drawingName: DrawingsNames;
   lwcChart: IChartApi;
   mainSeries: SeriesStrategies;
   onDelete: (id: string) => void;
+  onCopy: () => void;
   construct: (chart: IChartApi, series: ISeriesApi<SeriesType>, interaction: DrawingInteraction) => ISeriesDrawing;
   selected$: Observable<boolean>;
   isSelected: () => boolean;
@@ -25,16 +23,16 @@ interface DrawingParams extends DOMObjectParams {
   deselect: () => void;
   isLocked?: boolean;
   hotkeys: Hotkeys;
-  setCopyPasteBuffer: (copiedObject: DrawingSnapshotItem) => void;
   resetActiveTool: () => void;
 }
 
-export class Drawing extends DOMObject implements IDrawing {
+export class Drawing extends DOMObject {
   private lwcDrawing: ISeriesDrawing;
   private mainSeries: SeriesStrategies;
   private drawingName: DrawingsNames;
   private hotkeys: Hotkeys;
   private lockedSubject: BehaviorSubject<boolean>;
+  private settingsSubject: BehaviorSubject<SettingsValues>;
   private subscriptions = new Subscription();
 
   private escapeUnregisterHash: string | null = null;
@@ -48,6 +46,7 @@ export class Drawing extends DOMObject implements IDrawing {
     drawingName,
     id,
     onDelete,
+    onCopy,
     zIndex,
     moveUp,
     moveDown,
@@ -59,10 +58,17 @@ export class Drawing extends DOMObject implements IDrawing {
     isLocked = false,
     paneId,
     hotkeys,
-    setCopyPasteBuffer,
     resetActiveTool,
   }: DrawingParams) {
-    super({ id, name, zIndex, onDelete, moveUp, moveDown, paneId });
+    super({
+      id,
+      name,
+      zIndex,
+      onDelete,
+      moveUp,
+      moveDown,
+      paneId,
+    });
 
     this.hotkeys = hotkeys;
     this.mainSeries = mainSeries;
@@ -79,7 +85,13 @@ export class Drawing extends DOMObject implements IDrawing {
     };
 
     this.lwcDrawing = construct(lwcChart, mainSeries, interaction);
-    this.onDelete = onDelete;
+    this.settingsSubject = new BehaviorSubject(this.lwcDrawing.getSettings());
+
+    this.subscriptions.add(
+      this.lwcDrawing.subscribeSettings((settings) => {
+        this.settingsSubject.next(settings);
+      }),
+    );
 
     this.escapeUnregisterHash = hotkeys.register({
       keys: [Keys.escape],
@@ -100,15 +112,10 @@ export class Drawing extends DOMObject implements IDrawing {
           });
 
           this.copyUnregisterHash = hotkeys.register({
-            keys: [Keys.control, Keys.c],
+            keys: [Keys.mod, Keys.c],
             callback: () => {
               if (!this.isCreationPending()) {
-                setCopyPasteBuffer({
-                  id: this.id,
-                  drawingName: this.getDrawingName(),
-                  state: this.getState(),
-                  isLocked: this.isLocked(),
-                });
+                onCopy();
               }
             },
           });
@@ -116,22 +123,11 @@ export class Drawing extends DOMObject implements IDrawing {
           return;
         }
 
-        hotkeys.unregister({
-          keys: [Keys.delete],
-          hash: this.deleteUnregisterHash,
-        });
-
-        hotkeys.unregister({
-          keys: [Keys.control, Keys.c],
-          hash: this.copyUnregisterHash,
-        });
-
-        this.deleteUnregisterHash = null;
-        this.copyUnregisterHash = null;
+        this.unregisterSelectedDrawingHotkeys();
       }),
     );
 
-    this.afterCreation(() => {
+    this.waitForCreation().then(() => {
       hotkeys.unregister({
         keys: [Keys.escape],
         hash: this.escapeUnregisterHash,
@@ -141,11 +137,6 @@ export class Drawing extends DOMObject implements IDrawing {
     });
   }
 
-  public delete(): void {
-    this.destroy();
-    super.delete();
-  }
-
   public getDrawingName(): DrawingsNames {
     return this.drawingName;
   }
@@ -178,18 +169,26 @@ export class Drawing extends DOMObject implements IDrawing {
   }
 
   public subscribeSettings(callback: (settings: SettingsValues) => void): Subscription {
-    return this.lwcDrawing.subscribeSettings(callback);
+    return this.settingsSubject.subscribe(callback);
   }
 
   public isLocked(): boolean {
     return this.lockedSubject.value;
   }
 
+  public setLocked(isLocked: boolean): void {
+    if (this.lockedSubject.value === isLocked) {
+      return;
+    }
+
+    this.lockedSubject.next(isLocked);
+  }
+
   public toggleLock(): void {
-    this.lockedSubject.next(!this.lockedSubject.value);
+    this.setLocked(!this.isLocked());
   }
 
-  public async waitForCreation(): Promise<void> {
+  public waitForCreation(): Promise<void> {
     return this.lwcDrawing.waitTillReady();
   }
 
@@ -203,6 +202,7 @@ export class Drawing extends DOMObject implements IDrawing {
 
   public setState(state: unknown): void {
     this.lwcDrawing.setState(state);
+    this.settingsSubject.next(this.lwcDrawing.getSettings());
   }
 
   public getSettings(): SettingsValues {
@@ -229,29 +229,34 @@ export class Drawing extends DOMObject implements IDrawing {
 
   public destroy(): void {
     this.subscriptions.unsubscribe();
-
-    this.hotkeys.unregister({
-      keys: [Keys.delete],
-      hash: this.deleteUnregisterHash,
-    });
+    this.unregisterSelectedDrawingHotkeys();
 
     this.hotkeys.unregister({
       keys: [Keys.escape],
       hash: this.escapeUnregisterHash,
     });
 
-    this.hotkeys.unregister({
-      keys: [Keys.control, Keys.c],
-      hash: this.copyUnregisterHash,
-    });
+    this.escapeUnregisterHash = null;
 
     this.lockedSubject.complete();
+    this.settingsSubject.complete();
+
     this.mainSeries.detachPrimitive(this.lwcDrawing);
     this.lwcDrawing.destroy();
   }
 
-  private async afterCreation(callback: () => void): Promise<void> {
-    await this.lwcDrawing.waitTillReady();
-    callback();
+  private unregisterSelectedDrawingHotkeys(): void {
+    this.hotkeys.unregister({
+      keys: [Keys.delete],
+      hash: this.deleteUnregisterHash,
+    });
+
+    this.hotkeys.unregister({
+      keys: [Keys.mod, Keys.c],
+      hash: this.copyUnregisterHash,
+    });
+
+    this.deleteUnregisterHash = null;
+    this.copyUnregisterHash = null;
   }
 }
diff --git a/src/core/DrawingsManager.tsx b/src/core/DrawingsManager.tsx
index 569c8ee..34b1cc0 100644
--- a/src/core/DrawingsManager.tsx
+++ b/src/core/DrawingsManager.tsx
@@ -1,4 +1,5 @@
 import { IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
+import { cloneDeep, isEqual } from 'lodash-es';
 import { BehaviorSubject, distinctUntilChanged, map, Observable, Subscription } from 'rxjs';
 
 import { EventManager } from '@core';
@@ -13,6 +14,7 @@ import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
 import { ActiveDrawingTool } from '@src/types';
 
 import type { DrawingInteraction } from '@src/core/Drawings/common';
+import type { SettingsValues } from '@src/types/settings';
 
 interface DrawingsManagerParams {
   eventManager: EventManager;
@@ -30,12 +32,14 @@ export interface DrawingSnapshotItem {
   drawingName: DrawingsNames;
   state: unknown;
   isLocked?: boolean;
+  zIndex?: number;
 }
 
 interface CreateDrawingOptions {
   id?: string;
   state?: unknown;
   isLocked?: boolean;
+  zIndex?: number;
   shouldUpdateDrawingsList?: boolean;
 }
 
@@ -47,6 +51,8 @@ export class DrawingsManager {
   private DOM: DOMModel;
   private container: HTMLElement;
   private modalRenderer: ModalRenderer;
+  private paneId: number;
+  private hotkeys: Hotkeys;
 
   private mainSeries: SeriesStrategies | null = null;
   private subscriptions = new Subscription();
@@ -54,11 +60,10 @@ export class DrawingsManager {
   private selectedDrawing$ = new BehaviorSubject<Drawing | null>(null);
   private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
   private endlessMode$ = new BehaviorSubject(false);
-  private recreateScheduled = false;
   private pendingSnapshot: DrawingsManagerSnapshot | null = null;
-  private paneId: number;
+  private pointerDownSnapshot: DrawingSnapshotItem | null = null;
+  private recreateScheduled = false;
 
-  private hotkeys: Hotkeys;
   private copyPasteBuffer: DrawingSnapshotItem | null = null;
   private escapeUnregisterHash: string | null = null;
 
@@ -98,6 +103,7 @@ export class DrawingsManager {
     );
 
     window.addEventListener('pointerup', this.handlePointerUp);
+    window.addEventListener('pointercancel', this.handlePointerUp);
     this.container.addEventListener('click', this.handleClick);
     this.container.addEventListener('pointerdown', this.handlePointerDown);
     // todo: implement ctrl+v
@@ -127,10 +133,40 @@ export class DrawingsManager {
   }
 
   private handlePointerDown = (): void => {
+    this.pointerDownSnapshot = null;
+
+    queueMicrotask(() => {
+      const drawing = this.selectedDrawing$.value;
+
+      if (!drawing || drawing.isCreationPending()) {
+        return;
+      }
+
+      this.pointerDownSnapshot = this.createDrawingSnapshot(drawing);
+    });
+
     this.DOM.refreshEntities();
   };
 
   private handlePointerUp = (): void => {
+    const previousSnapshot = this.pointerDownSnapshot;
+
+    this.pointerDownSnapshot = null;
+
+    queueMicrotask(() => {
+      if (!previousSnapshot) {
+        return;
+      }
+
+      const drawing = this.findDrawing(previousSnapshot.id);
+
+      if (!drawing || drawing.isCreationPending()) {
+        return;
+      }
+
+      this.pushDrawingChange(previousSnapshot, this.createDrawingSnapshot(drawing));
+    });
+
     this.DOM.refreshEntities();
     this.updateActiveTool();
   };
@@ -140,6 +176,106 @@ export class DrawingsManager {
     this.updateActiveTool();
   };
 
+  private findDrawing(id: string): Drawing | undefined {
+    return this.drawings$.value.find((drawing) => drawing.id === id);
+  }
+
+  private createDrawingSnapshot(drawing: Drawing): DrawingSnapshotItem {
+    return {
+      id: drawing.id,
+      drawingName: drawing.getDrawingName(),
+      state: cloneDeep(drawing.getState()),
+      isLocked: drawing.isLocked(),
+      zIndex: drawing.zIndex,
+    };
+  }
+
+  private updateDrawing(drawing: Drawing, update: () => void): void {
+    if (drawing.isCreationPending()) {
+      return;
+    }
+
+    const previousSnapshot = this.createDrawingSnapshot(drawing);
+
+    update();
+
+    this.pushDrawingChange(previousSnapshot, this.createDrawingSnapshot(drawing));
+  }
+
+  private pushDrawingChange(
+    previousSnapshot: DrawingSnapshotItem | null,
+    nextSnapshot: DrawingSnapshotItem | null,
+  ): void {
+    if (isEqual(previousSnapshot, nextSnapshot)) {
+      return;
+    }
+
+    const previous = cloneDeep(previousSnapshot);
+    const next = cloneDeep(nextSnapshot);
+
+    this.eventManager.getUndoRedo().pushCommand({
+      undo: () => {
+        this.applyDrawingChange(next, previous);
+      },
+      redo: () => {
+        this.applyDrawingChange(previous, next);
+      },
+    });
+  }
+
+  private applyDrawingChange(
+    currentSnapshot: DrawingSnapshotItem | null,
+    nextSnapshot: DrawingSnapshotItem | null,
+  ): void {
+    if (currentSnapshot && nextSnapshot && currentSnapshot.id === nextSnapshot.id) {
+      const drawing = this.findDrawing(nextSnapshot.id);
+
+      if (drawing) {
+        drawing.setState(cloneDeep(nextSnapshot.state));
+        drawing.setLocked(nextSnapshot.isLocked ?? false);
+        this.DOM.refreshEntities();
+
+        return;
+      }
+    }
+
+    if (currentSnapshot) {
+      this.removeDrawingInternal(currentSnapshot.id, false);
+    }
+
+    if (nextSnapshot) {
+      this.restoreDrawing(nextSnapshot);
+    }
+
+    this.activeTool$.next('crosshair');
+  }
+
+  private restoreDrawing(snapshot: DrawingSnapshotItem): Drawing {
+    const existingDrawing = this.findDrawing(snapshot.id);
+
+    if (existingDrawing) {
+      existingDrawing.setState(cloneDeep(snapshot.state));
+      existingDrawing.setLocked(snapshot.isLocked ?? false);
+
+      if (snapshot.zIndex !== undefined) {
+        existingDrawing.setZIndex(snapshot.zIndex);
+      }
+
+      this.drawings$.next([...this.drawings$.value].sort((left, right) => left.zIndex - right.zIndex));
+
+      this.DOM.refreshEntities();
+
+      return existingDrawing;
+    }
+
+    return this.createDrawing(snapshot.drawingName, {
+      id: snapshot.id,
+      state: cloneDeep(snapshot.state),
+      isLocked: snapshot.isLocked,
+      zIndex: snapshot.zIndex,
+    });
+  }
+
   private updateActiveTool = (): void => {
     const hasPendingDrawing = this.drawings$.value.some((drawing) => drawing.isCreationPending());
 
@@ -179,7 +315,7 @@ export class DrawingsManager {
           return;
         }
 
-        this.createDrawing(currentTool);
+        this.addDrawingForce(currentTool);
       });
 
       return;
@@ -189,19 +325,32 @@ export class DrawingsManager {
   };
 
   private removeDrawing = (id: string): void => {
-    const drawing = this.drawings$.value.find((item) => item.id === id);
+    const drawing = this.findDrawing(id);
 
     if (!drawing) {
       return;
     }
 
-    this.removeDrawings([drawing]);
+    if (drawing.isCreationPending()) {
+      this.removeDrawingInternal(id);
+
+      return;
+    }
+
+    const snapshot = this.createDrawingSnapshot(drawing);
+
+    this.removeDrawingInternal(id);
+    this.pushDrawingChange(snapshot, null);
   };
 
-  private removeDrawingsByName(name: DrawingsNames, shouldUpdateTool = true): void {
-    const drawingsToRemove = this.drawings$.value.filter((drawing) => drawing.getDrawingName() === name);
+  private removeDrawingInternal(id: string, shouldUpdateTool = true): void {
+    const drawing = this.findDrawing(id);
 
-    this.removeDrawings(drawingsToRemove, shouldUpdateTool);
+    if (!drawing) {
+      return;
+    }
+
+    this.removeDrawings([drawing], shouldUpdateTool);
   }
 
   private removePendingDrawings(shouldUpdateTool = true): void {
@@ -235,26 +384,45 @@ export class DrawingsManager {
     this.DOM.refreshEntities();
   }
 
-  public addDrawingForce = (name: DrawingsNames): Promise<void> => {
+  public addDrawingForce = async (name: DrawingsNames): Promise<void> => {
     this.removePendingDrawings(false);
 
-    if (drawingsMap[name].singleInstance) {
-      this.removeDrawingsByName(name, false);
+    const previousDrawing = drawingsMap[name].singleInstance
+      ? this.drawings$.value.find((drawing) => drawing.getDrawingName() === name)
+      : undefined;
+
+    const previousSnapshot = previousDrawing ? this.createDrawingSnapshot(previousDrawing) : null;
+
+    if (previousDrawing) {
+      this.removeDrawingInternal(previousDrawing.id, false);
     }
 
     this.activeTool$.next(name);
     const drawing = this.createDrawing(name);
     this.DOM.refreshEntities();
 
-    return drawing.waitForCreation();
+    await drawing.waitForCreation();
+
+    if (!this.findDrawing(drawing.id)) {
+      if (previousSnapshot) {
+        this.restoreDrawing(previousSnapshot);
+      }
+
+      return;
+    }
+
+    this.pushDrawingChange(previousSnapshot, this.createDrawingSnapshot(drawing));
   };
 
   private createDrawing(name: DrawingsNames, options: CreateDrawingOptions = {}): Drawing {
-    if (!this.mainSeries) {
+    const { mainSeries } = this;
+
+    if (!mainSeries) {
       throw new Error('[Drawings] main series is not defined');
     }
 
-    const { id, state, isLocked = false, shouldUpdateDrawingsList = true } = options;
+    const { id, state, isLocked = false, zIndex, shouldUpdateDrawingsList = true } = options;
+
     const shouldSelectAfterCreation = state === undefined;
 
     if (shouldSelectAfterCreation && this.selectedDrawing$.value) {
@@ -296,15 +464,20 @@ export class DrawingsManager {
       });
     };
 
-    const drawingFactory = (zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) =>
+    const drawingFactory = (entityZIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) =>
       new Drawing({
         lwcChart: this.lwcChart,
-        mainSeries: this.mainSeries as SeriesStrategies,
+        mainSeries,
         id: drawingId,
         drawingName: name,
         name: drawingLabelById()[name],
         onDelete: this.removeDrawing,
-        zIndex,
+        onCopy: () => {
+          if (createdDrawing) {
+            this.copyPasteBuffer = this.createDrawingSnapshot(createdDrawing);
+          }
+        },
+        zIndex: entityZIndex,
         moveDown,
         moveUp,
         construct,
@@ -327,23 +500,21 @@ export class DrawingsManager {
         isLocked,
         paneId: this.paneId,
         hotkeys: this.hotkeys,
-        setCopyPasteBuffer: (copyPasteBuffer) => {
-          this.copyPasteBuffer = copyPasteBuffer;
-        },
         resetActiveTool: () => {
           this.activeTool$.next('crosshair');
         },
       });
 
-    const entity = this.DOM.setEntity<Drawing>(drawingFactory);
+    const entity = this.DOM.setEntity<Drawing>(drawingFactory, zIndex);
+
     createdDrawing = entity;
 
     if (state !== undefined) {
-      entity.setState(state);
+      entity.setState(cloneDeep(state));
     }
 
     if (shouldUpdateDrawingsList) {
-      this.drawings$.next([...this.drawings$.value, entity]);
+      this.drawings$.next([...this.drawings$.value, entity].sort((left, right) => left.zIndex - right.zIndex));
     }
 
     if (shouldSelectAfterCreation) {
@@ -364,12 +535,7 @@ export class DrawingsManager {
   public getSnapshot(): DrawingsManagerSnapshot {
     return this.drawings$.value
       .filter((drawing) => !drawing.isCreationPending())
-      .map((drawing) => ({
-        id: drawing.id,
-        drawingName: drawing.getDrawingName(),
-        state: drawing.getState(),
-        isLocked: drawing.isLocked(),
-      }));
+      .map((drawing) => this.createDrawingSnapshot(drawing));
   }
 
   public setSnapshot(snapshot: DrawingsManagerSnapshot): void {
@@ -378,10 +544,12 @@ export class DrawingsManager {
     }
 
     if (!this.mainSeries) {
-      this.pendingSnapshot = snapshot;
+      this.pendingSnapshot = cloneDeep(snapshot);
+
       return;
     }
 
+    this.pointerDownSnapshot = null;
     this.removeDrawings(this.drawings$.value, false);
 
     const restoredDrawings = snapshot.reduce<Drawing[]>((drawings, item) => {
@@ -392,8 +560,9 @@ export class DrawingsManager {
       drawings.push(
         this.createDrawing(item.drawingName, {
           id: item.id,
-          state: item.state,
+          state: cloneDeep(item.state),
           isLocked: item.isLocked,
+          zIndex: item.zIndex,
           shouldUpdateDrawingsList: false,
         }),
       );
@@ -401,7 +570,8 @@ export class DrawingsManager {
       return drawings;
     }, []);
 
-    this.drawings$.next(restoredDrawings);
+    this.drawings$.next(restoredDrawings.sort((left, right) => left.zIndex - right.zIndex));
+
     this.activeTool$.next('crosshair');
     this.DOM.refreshEntities();
   }
@@ -448,6 +618,18 @@ export class DrawingsManager {
     return this.selectedDrawing$.asObservable();
   }
 
+  public updateSelectedDrawingSettings = (settings: SettingsValues): void => {
+    const drawing = this.selectedDrawing$.value;
+
+    if (!drawing) {
+      return;
+    }
+
+    this.updateDrawing(drawing, () => {
+      drawing.updateSettings(settings);
+    });
+  };
+
   public openSelectedDrawingSettings(): void {
     const drawing = this.selectedDrawing$.value;
 
@@ -469,7 +651,15 @@ export class DrawingsManager {
   }
 
   public toggleSelectedDrawingLock(): void {
-    this.selectedDrawing$.value?.toggleLock();
+    const drawing = this.selectedDrawing$.value;
+
+    if (!drawing) {
+      return;
+    }
+
+    this.updateDrawing(drawing, () => {
+      drawing.toggleLock();
+    });
   }
 
   private openSettings = (drawing: Drawing): void => {
@@ -493,7 +683,15 @@ export class DrawingsManager {
       {
         size: 'sm',
         title: drawing.name,
-        onSave: () => drawing.updateSettings(settings),
+        onSave: () => {
+          if (!this.findDrawing(drawing.id)) {
+            return;
+          }
+
+          this.updateDrawing(drawing, () => {
+            drawing.updateSettings(settings);
+          });
+        },
       },
     );
   };
@@ -518,11 +716,15 @@ export class DrawingsManager {
     });
 
     window.removeEventListener('pointerup', this.handlePointerUp);
+    window.removeEventListener('pointercancel', this.handlePointerUp);
     this.container.removeEventListener('click', this.handleClick);
     this.container.removeEventListener('pointerdown', this.handlePointerDown);
 
     this.drawings$.value.forEach((drawing) => drawing.destroy());
 
+    this.pointerDownSnapshot = null;
+    this.copyPasteBuffer = null;
+
     this.subscriptions.unsubscribe();
     this.drawings$.complete();
     this.selectedDrawing$.complete();
diff --git a/src/core/Hotkeys.ts b/src/core/Hotkeys.ts
index 7231ede..4bd0338 100644
--- a/src/core/Hotkeys.ts
+++ b/src/core/Hotkeys.ts
@@ -2,9 +2,11 @@ export enum Keys {
   'escape' = 'escape',
   'delete' = 'delete',
   'tab' = 'tab',
-  'shift' = 'shiftleft',
-  'control' = 'controlleft',
-  'alt' = 'altleft',
+  'shift' = 'shift',
+  'control' = 'control',
+  'meta' = 'meta',
+  'mod' = 'mod',
+  'alt' = 'alt',
   't' = 'keyt',
   'h' = 'keyh',
   'v' = 'keyv',
@@ -14,161 +16,141 @@ export enum Keys {
   'mousedown' = 'mousedown',
 }
 
-export interface IHotkeys {
-  register: ({ keys, callback }: { keys: Keys[]; callback: () => void }) => void;
+type HotkeyCallback = () => void | Promise<void>;
+
+interface RegisterHotkeyParams {
+  keys: Keys[];
+  callback: HotkeyCallback;
 }
 
-type Tree = {
-  children: Map<Keys, Tree>;
-  callbacks: Map<string, () => void> | null;
-  pressHoldRequired?: boolean;
-};
+interface UnregisterHotkeyParams {
+  keys: Keys[];
+  hash?: string | null;
+}
+
+interface HotkeyRegistration {
+  keys: Keys[];
+  callback: HotkeyCallback;
+}
+
+export interface IHotkeys {
+  register(params: RegisterHotkeyParams): string | null;
+  unregister(params: UnregisterHotkeyParams): void;
+}
 
 // todo: возможно можно сделать синглтоном и использовать импортируя эк земпляр класса,
 //  там где это нужно вместо props drilling, как сейчас
 
 export class Hotkeys implements IHotkeys {
-  private tree: Tree = { children: new Map(), callbacks: null, pressHoldRequired: false };
-
-  private buffer: Keys[] = [];
-  private prevKey: null | Keys = null;
+  private registrations = new Map<string, HotkeyRegistration>();
 
   constructor() {
-    document.addEventListener('keydown', this.handleKeyDown.bind(this));
-    document.addEventListener('keyup', this.handleKeyUp.bind(this));
-  }
-
-  public unregister({ keys, hash }: { keys: Keys[]; hash?: string | null }) {
-    if (!hash) {
-      return;
-    }
-    let node = this.tree;
-
-    keys.forEach((key: Keys) => {
-      const child = node.children.get(key);
-      if (child) {
-        node = child;
-      } else {
-        throw new Error('[Hotkeys]: ошибка при отписке от хоткея');
-      }
-    });
-
-    if (!hash) {
-      node.callbacks = null;
-    }
-
-    node.callbacks?.delete(hash);
-
-    if (node.callbacks?.size === 0) {
-      node.callbacks = null;
-    }
+    document.addEventListener('keydown', this.handleKeyDown);
   }
 
-  public register({
-    keys,
-    callback,
-    pressHoldRequired = false,
-  }: {
-    keys: Keys[];
-    callback: () => void;
-    pressHoldRequired?: boolean;
-  }): string | null {
+  public register({ keys, callback }: RegisterHotkeyParams): string | null {
     if (keys.length === 0) {
       console.error('[Hotkeys] попытка задать пустой хоткей');
+
       return null;
     }
 
-    let node = this.tree;
-
-    keys.forEach((key: Keys) => {
-      if (!node.children.has(key)) {
-        node.children.set(key, { children: new Map(), callbacks: null, pressHoldRequired });
-      }
+    const hash = `hotkeyCallback-${crypto.randomUUID()}`;
 
-      node = node.children.get(key) as Tree;
+    this.registrations.set(hash, {
+      keys,
+      callback,
     });
 
-    if (node.callbacks === null || node.callbacks === undefined) {
-      node.callbacks = new Map();
-    }
-
-    const hash = `hotkeyCallback-${crypto.randomUUID()}`;
-    node.callbacks?.set(hash, callback);
     return hash;
   }
 
-  private findNode(keys: Keys[]): Tree | null {
-    let node = this.tree;
-
-    for (const key of keys) {
-      const k = normalize(key);
-      const child = node.children.get(k);
-      if (child) {
-        node = child;
-      } else {
-        return null;
-      }
+  public unregister({ hash }: UnregisterHotkeyParams): void {
+    if (!hash) {
+      return;
     }
-    return node;
+
+    this.registrations.delete(hash);
   }
 
-  private handleKeyUp = async () => {
-    const node = this.findNode(this.buffer);
-    if (!node) {
-      this.buffer = [];
-      this.prevKey = null;
-      return;
-    }
+  public destroy(): void {
+    document.removeEventListener('keydown', this.handleKeyDown);
+    this.registrations.clear();
+  }
 
-    if (node.callbacks && node.pressHoldRequired) {
-      await this.handleKeyDown.bind(this)({ code: 'Escape' } as KeyboardEvent);
+  private handleKeyDown = async (event: KeyboardEvent): Promise<void> => {
+    if (event.repeat || isEditableElement(event.target)) {
+      return;
     }
-    this.buffer = [];
-    this.prevKey = null;
-  };
 
-  private handleKeyDown = async (event: KeyboardEvent) => {
-    const key = normalize(event.code);
+    const callbacks = [...this.registrations.values()]
+      .filter(({ keys }) => matchesHotkey(event, keys))
+      .map(({ callback }) => callback)
+      .reverse();
 
-    if (key === this.prevKey) return;
+    if (callbacks.length === 0) {
+      return;
+    }
 
-    this.prevKey = key;
+    event.preventDefault();
 
-    if (key === Keys.escape) {
-      this.buffer = [];
+    for (const callback of callbacks) {
+      // eslint-disable-next-line no-await-in-loop
+      await callback();
     }
+  };
+}
 
-    this.buffer.push(key);
+function matchesHotkey(event: KeyboardEvent, keys: Keys[]): boolean {
+  const eventKey = normalizeCode(event.code);
+  const primaryKeys = keys.filter((key) => !isModifier(key));
 
-    const node = this.findNode(this.buffer);
+  if (primaryKeys.length !== 1 || primaryKeys[0] !== eventKey) {
+    return false;
+  }
 
-    if (!node) {
-      this.buffer = [];
-      this.prevKey = null;
-      return;
+  if (keys.includes(Keys.mod)) {
+    if (!event.ctrlKey && !event.metaKey) {
+      return false;
     }
+  } else if (event.ctrlKey !== keys.includes(Keys.control) || event.metaKey !== keys.includes(Keys.meta)) {
+    return false;
+  }
 
-    if (node.callbacks && node.children.size <= 0) {
-      event.preventDefault?.();
-      const callbacks = [];
+  return event.shiftKey === keys.includes(Keys.shift) && event.altKey === keys.includes(Keys.alt);
+}
 
-      for (const [_, cb] of node.callbacks) {
-        callbacks.push(cb);
-      }
+function isModifier(key: Keys): boolean {
+  return key === Keys.mod || key === Keys.control || key === Keys.meta || key === Keys.shift || key === Keys.alt;
+}
 
-      const reversedCallbacks = [...callbacks].reverse();
+function normalizeCode(code: string): Keys {
+  switch (code.toLowerCase()) {
+    case 'controlleft':
+    case 'controlright':
+      return Keys.control;
 
-      for (const cb of reversedCallbacks) {
-        // eslint-disable-next-line no-await-in-loop
-        await cb();
-      }
+    case 'metaleft':
+    case 'metaright':
+      return Keys.meta;
 
-      this.buffer = [];
-      this.prevKey = null;
-    }
-  };
+    case 'shiftleft':
+    case 'shiftright':
+      return Keys.shift;
+
+    case 'altleft':
+    case 'altright':
+      return Keys.alt;
+
+    default:
+      return code.toLowerCase() as Keys;
+  }
 }
 
-function normalize(key: string): Keys {
-  return key.toLowerCase() as Keys;
+function isEditableElement(target: EventTarget | null): boolean {
+  if (!(target instanceof HTMLElement)) {
+    return false;
+  }
+
+  return target.isContentEditable || target.closest('input, textarea, select, [contenteditable]') !== null;
 }
diff --git a/src/core/MoexChart.tsx b/src/core/MoexChart.tsx
index 2f46df1..350012a 100644
--- a/src/core/MoexChart.tsx
+++ b/src/core/MoexChart.tsx
@@ -5,7 +5,7 @@ import { Footer } from '@components/Footer';
 
 import { Header } from '@components/Header';
 import { DataSource, DataSourceParams } from '@core/DataSource';
-import { Hotkeys } from '@core/Hotkeys';
+import { Hotkeys, Keys } from '@core/Hotkeys';
 import { ModalRenderer } from '@core/ModalRenderer';
 import { FloatingDrawingToolbar } from '@src/components/FloatingToolbar';
 import { SettingsModal } from '@src/components/SettingsModal';
@@ -181,6 +181,20 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
 
     this.hotkeys = new Hotkeys();
 
+    if (config.chartCollectionPreset.undoRedoEnabled) {
+      const undoRedo = this.eventManager.getUndoRedo();
+
+      this.hotkeys.register({
+        keys: [Keys.mod, Keys.z],
+        callback: undoRedo.undo,
+      });
+
+      this.hotkeys.register({
+        keys: [Keys.mod, Keys.shift, Keys.z],
+        callback: undoRedo.redo,
+      });
+    }
+
     this.modalRenderer = new ModalRenderer(modalContainer);
 
     this.chart = new Chart({
@@ -291,6 +305,7 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
     this.drawingToolbarRenderer.renderComponent(
       <FloatingDrawingToolbar
         selectedDrawing$={drawingsManager.selectedDrawing()}
+        onUpdateSettings={drawingsManager.updateSelectedDrawingSettings}
         onToggleLock={() => drawingsManager.toggleSelectedDrawingLock()}
         onOpenSettings={() => drawingsManager.openSelectedDrawingSettings()}
         onDelete={() => drawingsManager.deleteSelectedDrawing()}
diff --git a/src/core/UndoRedo.ts b/src/core/UndoRedo.ts
index 25bab25..85ec5cf 100644
--- a/src/core/UndoRedo.ts
+++ b/src/core/UndoRedo.ts
@@ -1,8 +1,15 @@
+import { BehaviorSubject, Observable } from 'rxjs';
+
 import { ChartSeriesType, Intervals, SymbolInfo, TimeFormat, Timeframes } from '@src/types';
 import { DateFormat } from '@src/utils';
 
 export type UndoKey = keyof UndoConfig;
 
+export interface UndoRedoState {
+  canUndo: boolean;
+  canRedo: boolean;
+}
+
 interface UndoConfig {
   timeframe: (value: Timeframes) => void;
   seriesSelected: (value: ChartSeriesType) => void;
@@ -19,110 +26,169 @@ interface HistoryItem {
   next: unknown;
 }
 
+interface HistoryCommand {
+  kind: 'command';
+  undo: () => void;
+  redo: () => void;
+}
+
 interface HistoryGroup {
   kind: 'group';
   entries: HistoryEntry[];
 }
 
-type HistoryEntry = HistoryItem | HistoryGroup;
+type HistoryEntry = HistoryItem | HistoryCommand | HistoryGroup;
 
 export class UndoRedo {
   private undoStack: HistoryEntry[] = [];
   private redoStack: HistoryEntry[] = [];
-
   private groupStack: HistoryEntry[][] = [];
 
+  private state$ = new BehaviorSubject<UndoRedoState>({
+    canUndo: false,
+    canRedo: false,
+  });
+
   constructor(private readonly config: UndoConfig) {}
 
-  private beginGroup(): void {
+  public push(key: UndoKey, prev: unknown, next: unknown): void {
+    if (Object.is(prev, next)) {
+      return;
+    }
+
+    this.addEntry({
+      kind: 'item',
+      key,
+      prev,
+      next,
+    });
+  }
+
+  public pushCommand(command: Omit<HistoryCommand, 'kind'>): void {
+    this.addEntry({
+      kind: 'command',
+      ...command,
+    });
+  }
+
+  public group<T>(callback: () => T): T {
     this.groupStack.push([]);
+
+    try {
+      return callback();
+    } finally {
+      this.endGroup();
+    }
   }
 
-  private endGroup(): void {
-    const entries = this.groupStack.pop();
-    if (!entries || entries.length === 0) return;
+  public undo = (): void => {
+    const entry = this.undoStack.pop();
 
-    const group: HistoryGroup = { kind: 'group', entries };
+    if (!entry) {
+      return;
+    }
+
+    this.applyEntry(entry, 'undo');
+    this.redoStack.push(entry);
+    this.updateState();
+  };
 
-    const parent = this.groupStack[this.groupStack.length - 1];
-    if (parent) {
-      parent.push(group);
+  public redo = (): void => {
+    const entry = this.redoStack.pop();
+
+    if (!entry) {
       return;
     }
 
-    this.undoStack.push(group);
-    this.redoStack = [];
+    this.applyEntry(entry, 'redo');
+    this.undoStack.push(entry);
+    this.updateState();
+  };
+
+  public canUndo(): boolean {
+    return this.undoStack.length > 0;
   }
 
-  public group<T>(fn: () => T): T {
-    this.beginGroup();
-    try {
-      return fn();
-    } finally {
-      this.endGroup();
-    }
+  public canRedo(): boolean {
+    return this.redoStack.length > 0;
   }
 
-  public push(key: UndoKey, prev: unknown, next: unknown): void {
-    if (Object.is(prev, next)) return;
+  public getState(): Observable<UndoRedoState> {
+    return this.state$.asObservable();
+  }
 
-    const item: HistoryItem = { kind: 'item', key, prev, next };
+  public clear(): void {
+    this.undoStack = [];
+    this.redoStack = [];
+    this.groupStack = [];
+
+    this.updateState();
+  }
+
+  private endGroup(): void {
+    const entries = this.groupStack.pop();
+
+    if (!entries?.length) {
+      return;
+    }
 
+    this.addEntry({
+      kind: 'group',
+      entries,
+    });
+  }
+
+  private addEntry(entry: HistoryEntry): void {
     if (this.redoStack.length > 0) {
       this.redoStack = [];
     }
 
     const currentGroup = this.groupStack[this.groupStack.length - 1];
+
     if (currentGroup) {
-      currentGroup.push(item);
+      currentGroup.push(entry);
+
       return;
     }
 
-    this.undoStack.push(item);
+    this.undoStack.push(entry);
+    this.updateState();
   }
 
-  public undo(): void {
-    const entry = this.undoStack.pop();
-    if (!entry) return;
+  private applyEntry(entry: HistoryEntry, direction: 'undo' | 'redo'): void {
+    if (entry.kind === 'group') {
+      const entries = direction === 'undo' ? [...entry.entries].reverse() : entry.entries;
 
-    this.applyEntry(entry, 'undo');
-    this.redoStack.push(entry);
-  }
+      entries.forEach((groupEntry) => {
+        this.applyEntry(groupEntry, direction);
+      });
 
-  public redo(): void {
-    const entry = this.redoStack.pop();
-    if (!entry) return;
+      return;
+    }
 
-    this.applyEntry(entry, 'redo');
-    this.undoStack.push(entry);
-  }
+    if (entry.kind === 'command') {
+      entry[direction]();
 
-  private applyEntry(entry: HistoryEntry, direction: 'undo' | 'redo'): void {
-    if (entry.kind === 'group') {
-      const list = entry.entries;
-      if (direction === 'undo') {
-        for (let i = list.length - 1; i >= 0; i -= 1) this.applyEntry(list[i], direction);
-      } else {
-        for (let i = 0; i < list.length; i += 1) this.applyEntry(list[i], direction);
-      }
       return;
     }
 
-    const apply = this.config[entry.key] as (v: unknown) => void;
+    const apply = this.config[entry.key] as (value: unknown) => void;
+
     apply(direction === 'undo' ? entry.prev : entry.next);
   }
 
-  public canUndo(): boolean {
-    return this.undoStack.length > 0;
-  }
+  private updateState(): void {
+    const nextState: UndoRedoState = {
+      canUndo: this.undoStack.length > 0,
+      canRedo: this.redoStack.length > 0,
+    };
 
-  public canRedo(): boolean {
-    return this.redoStack.length > 0;
-  }
+    const currentState = this.state$.value;
 
-  public clear(): void {
-    this.undoStack = [];
-    this.redoStack = [];
-    this.groupStack = [];
+    if (currentState.canUndo === nextState.canUndo && currentState.canRedo === nextState.canRedo) {
+      return;
+    }
+
+    this.state$.next(nextState);
   }
 }