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


import type { MouseEventParams } from 'lightweight-charts';

import { DrawingsNames } from '@src/constants';

import { Keys } from './Hotkeys';

type DrawingPointerModifier = Keys.shift | Keys.alt | Keys.control | Keys.meta;

interface DrawingKeyboardShortcut {
  drawingName: DrawingsNames;
  keys: readonly Keys[];
}

interface DrawingPointerShortcut {
  drawingName: DrawingsNames;
  modifiers: readonly DrawingPointerModifier[];
}

export const DRAWING_KEYBOARD_SHORTCUTS: readonly DrawingKeyboardShortcut[] = [
  {
    drawingName: DrawingsNames.trendLine,
    keys: [Keys.alt, Keys.t],
  },
  {
    drawingName: DrawingsNames.horizontalLine,
    keys: [Keys.alt, Keys.h],
  },
  {
    drawingName: DrawingsNames.verticalLine,
    keys: [Keys.alt, Keys.v],
  },
  {
    drawingName: DrawingsNames.fibonacciRetracement,
    keys: [Keys.alt, Keys.f],
  },
  {
    drawingName: DrawingsNames.rectangle,
    keys: [Keys.alt, Keys.shift, Keys.r],
  },
];

export const DRAWING_POINTER_SHORTCUTS: readonly DrawingPointerShortcut[] = [
  {
    drawingName: DrawingsNames.ruler,
    modifiers: [Keys.shift],
  },
];

export function findDrawingPointerShortcut(event: MouseEventParams): DrawingPointerShortcut | undefined {
  const sourceEvent = event.sourceEvent;

  if (!sourceEvent) {
    return undefined;
  }

  return DRAWING_POINTER_SHORTCUTS.find(
    ({ modifiers }) =>
      sourceEvent.shiftKey === modifiers.includes(Keys.shift) &&
      sourceEvent.altKey === modifiers.includes(Keys.alt) &&
      sourceEvent.ctrlKey === modifiers.includes(Keys.control) &&
      sourceEvent.metaKey === modifiers.includes(Keys.meta),
  );
}


import type { MouseEventParams } from 'lightweight-charts';

import { BehaviorSubject, Observable } from 'rxjs';

import { DrawingsNames } from '@src/constants';
import { ActiveDrawingTool, SettingsValues } from '@src/types';

import { Drawing } from './Drawings';
import { DrawingsManager } from './DrawingsManager';
import { DRAWING_KEYBOARD_SHORTCUTS, findDrawingPointerShortcut } from './drawingShortcuts';
import { Hotkeys, Keys } from './Hotkeys';

import type { Pane } from './Pane';

// todo: нужно дописывать класс)
export class DrawingsManagerCollection {
  private managersMap: Map<number, DrawingsManager> = new Map();
  private hotkeys: Hotkeys;

  private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
  private endlessMode$ = new BehaviorSubject(false);

  private isAwaitingDrawingStart = false;
  private unregisterHotkeys: (() => void)[] = [];

  constructor({ hotkeys }: { hotkeys: Hotkeys }) {
    this.hotkeys = hotkeys;

    for (const { keys, drawingName } of DRAWING_KEYBOARD_SHORTCUTS) {
      this.unregisterHotkeys.push(
        this.hotkeys.register({
          keys,
          callback: () => {
            this.activateDrawingTool(drawingName);
          },
        }),
      );
    }

    this.unregisterHotkeys.push(
      this.hotkeys.register({
        keys: [Keys.escape],
        callback: this.activateCrosshair,
      }),
    );
  }

  public getIsEndlessMode(): boolean {
    return this.endlessMode$.value;
  }

  public getActiveToolValue(): ActiveDrawingTool {
    return this.activeTool$.value;
  }

  public setActiveTool(next: ActiveDrawingTool): void {
    this.activeTool$.next(next);

    if (next === 'crosshair') {
      this.isAwaitingDrawingStart = false;
    }
  }

  public removeDrawingManager(paneId: number): void {
    this.managersMap.delete(paneId);
  }

  public addDrawingManager(manager: DrawingsManager, paneId: number): void {
    this.managersMap.set(paneId, manager);
  }

  public activateDrawingTool = (name: DrawingsNames): void => {
    this.cancelPendingDrawings();
    this.setActiveTool(name);
    this.isAwaitingDrawingStart = true;
  };

  public handlePaneClick(pane: Pane, event: MouseEventParams): void {
    const activeTool = this.activeTool$.value;

    if (activeTool === 'crosshair') {
      const pointerShortcut = findDrawingPointerShortcut(event);

      if (pointerShortcut) {
        pane.getDrawingManager().startDrawing(pointerShortcut.drawingName, event);
      }

      return;
    }

    if (!this.isAwaitingDrawingStart) {
      return;
    }

    this.isAwaitingDrawingStart = false;

    pane.getDrawingManager().startDrawing(activeTool, event);
  }

  public setEndlessDrawingMode = (value: boolean): void => {
    if (this.endlessMode$.value === value) {
      return;
    }

    this.endlessMode$.next(value);
  };

  public isEndlessDrawingsMode(): Observable<boolean> {
    return this.endlessMode$.asObservable();
  }

  public activateCrosshair = (): void => {
    this.cancelPendingDrawings();
    this.setActiveTool('crosshair');
  };

  public getActiveTool(): Observable<ActiveDrawingTool> {
    return this.activeTool$.asObservable();
  }

  public selectedDrawing(): Observable<Drawing | null> {
    return Array.from(this.managersMap.values())[0].selectedDrawing();
  }

  public updateSelectedDrawingSettings = (settings: SettingsValues): void => {
    for (const manager of this.managersMap.values()) {
      manager.updateSelectedDrawingSettings(settings);
    }
  };

  public toggleSelectedDrawingLock = (): void => {
    Array.from(this.managersMap.values())[0].toggleSelectedDrawingLock();
  };

  public openSelectedDrawingSettings = (): void => {
    Array.from(this.managersMap.values())[0].openSelectedDrawingSettings();
  };

  public deleteSelectedDrawing = (): void => {
    Array.from(this.managersMap.values())[0].deleteSelectedDrawing();
  };

  public destroy(): void {
    for (const unregisterHotkey of this.unregisterHotkeys) {
      unregisterHotkey();
    }

    this.unregisterHotkeys = [];

    this.activeTool$.complete();
    this.endlessMode$.complete();
  }

  private cancelPendingDrawings(): void {
    for (const manager of this.managersMap.values()) {
      manager.cancelPendingDrawing();
    }
  }
}


public startDrawing = async (name: DrawingsNames, event?: MouseEventParams): Promise<void> => {
  this.removePendingDrawings(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);
  }

  if (this.selectedDrawing$.value) {
    this.selectedDrawing$.next(null);
  }

  this.setActiveTool(name);

  const drawing = this.createDrawing({
    name,
    event,
  });

  this.DOM.refreshEntities();

  await drawing.waitForCreation();

  if (!this.findDrawing(drawing.id)) {
    if (previousSnapshot) {
      this.restoreDrawing(previousSnapshot);
    }

    return;
  }

  this.pushDrawingChange(previousSnapshot, this.createDrawingSnapshot(drawing));

  if (this.getActiveTool() === name) {
    this.selectedDrawing$.next(drawing);
    this.updateActiveTool();
  }

  this.DOM.refreshEntities();
};

private updateActiveTool(): void {
  const hasPendingDrawing = this.drawings$.value.some((drawing) => drawing.isCreationPending());

  if (hasPendingDrawing) {
    return;
  }

  const activeTool = this.getActiveTool();

  if (activeTool !== 'crosshair' && this.getIsEndlessMode()) {
    this.continueDrawing(activeTool);

    return;
  }

  this.setActiveTool('crosshair');
}

private initClickListener(): void {
  const handler = (param: MouseEventParams) => {
    if (param.paneIndex === undefined) {
      return;
    }

    const clickedPane = this.getPaneByIndex(param.paneIndex);

    if (!clickedPane) {
      return;
    }

    this.drawingsManagerCollection.handlePaneClick(clickedPane, param);
  };

  this.lwcChart.subscribeClick(handler);
  this.subscriptions.add(() => this.lwcChart.unsubscribeClick(handler));
}

continueDrawing: (name: DrawingsNames) => this.drawingsManagerCollection.activateDrawingTool(name),