Загрузка данных
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),
};
}