From a3fb4fae1b8421fd7a61b3021d0d41c84d1082ef Mon Sep 17 00:00:00 2001 From: "Anastasiia.Balzamova" Date: Wed, 3 Jan 2024 10:05:16 -0700 Subject: [PATCH 1/4] #3713 Hot keys for toolbar buttons for macromolecules --- .../src/application/editor/Editor.ts | 71 +++++++++++++++++++ packages/ketcher-core/src/utilities/index.ts | 1 + .../src/utilities/keynorm.ts} | 7 +- .../script/ui/component/cliparea/cliparea.jsx | 2 +- .../src/script/ui/state/hotkeys.ts | 2 +- 5 files changed, 78 insertions(+), 5 deletions(-) rename packages/{ketcher-react/src/script/ui/data/convert/keynorm.js => ketcher-core/src/utilities/keynorm.ts} (94%) diff --git a/packages/ketcher-core/src/application/editor/Editor.ts b/packages/ketcher-core/src/application/editor/Editor.ts index d0795fcad0..ea05f9d79e 100644 --- a/packages/ketcher-core/src/application/editor/Editor.ts +++ b/packages/ketcher-core/src/application/editor/Editor.ts @@ -26,6 +26,7 @@ import { MacromoleculesConverter } from 'application/editor/MacromoleculesConver import { BaseMonomer } from 'domain/entities/BaseMonomer'; import { ketcherProvider } from 'application/utils'; import { SnakeMode } from './modes/internal'; +import { keyNorm } from '../../utilities/keynorm'; interface ICoreEditorConstructorParams { theme; @@ -61,6 +62,7 @@ export class CoreEditor { this.renderersContainer = new RenderersManager({ theme }); this.drawingEntitiesManager = new DrawingEntitiesManager(); this.domEventSetup(); + this.setupHotKeysEvents(); this.canvasOffset = this.canvas.getBoundingClientRect(); this.zoomTool = ZoomTool.initInstance(this.drawingEntitiesManager); // eslint-disable-next-line @typescript-eslint/no-this-alias @@ -74,6 +76,75 @@ export class CoreEditor { return editor; } + setupHotKeysEvents() { + document.addEventListener('keydown', (event) => { + const keySettings = { + exit: { + shortcut: ['Shift+Tab', 'Escape'], + action: () => { + this.onSelectTool('select-rectangle'); + //TODO: add switching to default button + }, + }, + undo: { + shortcut: ['Ctrl+z', 'Meta+z'], + action: () => { + this.onSelectHistory('undo'); + }, + }, + redo: { + shortcut: ['Shift+Ctrl+Z', 'Ctrl+Y', 'Shift+Meta+Z', 'Meta+y'], + action: () => { + this.onSelectHistory('redo'); + }, + }, + erase: { + shortcut: ['Del'], // cannot test on Mac + action: () => { + this.onSelectTool('erase'); + }, + }, + clear: { + shortcut: ['Ctrl+Del'], // cannot test on Mac + action: () => { + this.onSelectTool('clear'); + this.onSelectTool('select-rectangle'); + }, + }, + 'zoom-plus': { + shortcut: ['Ctrl+=', 'Meta+='], + action: () => { + //TODO + }, + }, + 'zoom-minus': { + shortcut: ['Ctrl+-', 'Meta+-'], + action: () => { + //TODO + }, + }, + 'zoom-reset': { + shortcut: ['Ctrl+0', 'Meta+0'], + action: () => { + //TODO + }, + }, + }; + const shortcut = keyNorm(event); + const key = Object.entries(keySettings).reduce((acc, cur) => { + const [key, value] = cur; + const shortcutExists = value.shortcut.find((el) => el === shortcut); + if (shortcutExists) acc += key; + return acc; + }, ''); + + if (keySettings[key] && keySettings[key].action) { + keySettings[key].action(); + event.preventDefault(); + } + }); + } + private subscribeEvents() { this.events.selectMonomer.add((monomer) => this.onSelectMonomer(monomer)); this.events.selectPreset.add((preset) => this.onSelectRNAPreset(preset)); diff --git a/packages/ketcher-core/src/utilities/index.ts b/packages/ketcher-core/src/utilities/index.ts index c2e74fda26..f503428c66 100644 --- a/packages/ketcher-core/src/utilities/index.ts +++ b/packages/ketcher-core/src/utilities/index.ts @@ -21,3 +21,4 @@ export * from './b64toBlob'; export * from './notifyRequestCompleted'; export * from './KetcherLogger'; export * from './SettingsManager'; +export * from './keynorm'; diff --git a/packages/ketcher-react/src/script/ui/data/convert/keynorm.js b/packages/ketcher-core/src/utilities/keynorm.ts similarity index 94% rename from packages/ketcher-react/src/script/ui/data/convert/keynorm.js rename to packages/ketcher-core/src/utilities/keynorm.ts index b047d98a64..7608e52b3e 100644 --- a/packages/ketcher-react/src/script/ui/data/convert/keynorm.js +++ b/packages/ketcher-core/src/utilities/keynorm.ts @@ -62,7 +62,8 @@ function modifiers(name, event, shift) { if (event.altKey) name = 'Alt+' + name; if (event.ctrlKey) name = 'Ctrl+' + name; if (event.metaKey) name = 'Meta+' + name; - if (shift !== false && event.shiftKey) name = 'Shift+' + name; + if (event.shiftKey && (shift !== false || event.key !== 'Shift')) + name = 'Shift+' + name; return name; } @@ -89,7 +90,7 @@ export function isControlKey(event) { function keyNorm(obj) { if (obj instanceof KeyboardEvent) // eslint-disable-line no-undef - return normalizeKeyEvent(...arguments); // eslint-disable-line prefer-rest-params + return normalizeKeyEvent(obj); // eslint-disable-line prefer-rest-params return typeof obj === 'object' ? normalizeKeyMap(obj) : normalizeKeyName(obj); } @@ -111,4 +112,4 @@ function lookup(map, event) { keyNorm.lookup = lookup; -export default keyNorm; +export { keyNorm }; diff --git a/packages/ketcher-react/src/script/ui/component/cliparea/cliparea.jsx b/packages/ketcher-react/src/script/ui/component/cliparea/cliparea.jsx index 077f6e9c41..fae491389c 100644 --- a/packages/ketcher-react/src/script/ui/component/cliparea/cliparea.jsx +++ b/packages/ketcher-react/src/script/ui/component/cliparea/cliparea.jsx @@ -18,7 +18,7 @@ import { Component, createRef } from 'react'; import clsx from 'clsx'; import classes from './cliparea.module.less'; import { KetcherLogger, notifyRequestCompleted } from 'ketcher-core'; -import { isControlKey } from '../../data/convert/keynorm'; +import { isControlKey } from 'ketcher-core'; import { isClipboardAPIAvailable, notifyCopyCut } from './clipboardUtils'; const ieCb = window.clipboardData; diff --git a/packages/ketcher-react/src/script/ui/state/hotkeys.ts b/packages/ketcher-react/src/script/ui/state/hotkeys.ts index 051ebfeeb9..419a21df1d 100644 --- a/packages/ketcher-react/src/script/ui/state/hotkeys.ts +++ b/packages/ketcher-react/src/script/ui/state/hotkeys.ts @@ -33,7 +33,7 @@ import { debounce, isEqual } from 'lodash/fp'; import { load, onAction, removeStructAction } from './shared'; import actions from '../action'; -import keyNorm from '../data/convert/keynorm'; +import { keyNorm } from 'ketcher-core'; import { isIE } from 'react-device-detect'; import { selectAbbreviationLookupValue, From 90befb08fb4c473dfc7a8e0a7c9b787ddbaf8613 Mon Sep 17 00:00:00 2001 From: Roman Rodionov Date: Thu, 4 Jan 2024 14:12:38 +0100 Subject: [PATCH 2/4] - implemented all events - implemented toolbar btn highlighting --- .../src/application/editor/Editor.ts | 62 ++----------------- .../src/application/editor/editorEvents.ts | 62 +++++++++++++++++++ .../src/application/editor/tools/Zoom.ts | 23 +++++-- .../domain/entities/DrawingEntitiesManager.ts | 14 +++++ .../ketcher-core/src/utilities/keynorm.ts | 8 ++- .../src/Editor.tsx | 16 ++--- 6 files changed, 111 insertions(+), 74 deletions(-) diff --git a/packages/ketcher-core/src/application/editor/Editor.ts b/packages/ketcher-core/src/application/editor/Editor.ts index ea05f9d79e..da76632077 100644 --- a/packages/ketcher-core/src/application/editor/Editor.ts +++ b/packages/ketcher-core/src/application/editor/Editor.ts @@ -17,6 +17,7 @@ import ZoomTool from './tools/Zoom'; import Coordinates from './shared/coordinates'; import { editorEvents, + hotkeysConfiguration, renderersEvents, resetEditorEvents, } from 'application/editor/editorEvents'; @@ -78,58 +79,7 @@ export class CoreEditor { setupHotKeysEvents() { document.addEventListener('keydown', (event) => { - const keySettings = { - exit: { - shortcut: ['Shift+Tab', 'Escape'], - action: () => { - this.onSelectTool('select-rectangle'); - //TODO: add switching to default button - }, - }, - undo: { - shortcut: ['Ctrl+z', 'Meta+z'], - action: () => { - this.onSelectHistory('undo'); - }, - }, - redo: { - shortcut: ['Shift+Ctrl+Z', 'Ctrl+Y', 'Shift+Meta+Z', 'Meta+y'], - action: () => { - this.onSelectHistory('redo'); - }, - }, - erase: { - shortcut: ['Del'], // cannot test on Mac - action: () => { - this.onSelectTool('erase'); - }, - }, - clear: { - shortcut: ['Ctrl+Del'], // cannot test on Mac - action: () => { - this.onSelectTool('clear'); - this.onSelectTool('select-rectangle'); - }, - }, - 'zoom-plus': { - shortcut: ['Ctrl+=', 'Meta+='], - action: () => { - //TODO - }, - }, - 'zoom-minus': { - shortcut: ['Ctrl+-', 'Meta+-'], - action: () => { - //TODO - }, - }, - 'zoom-reset': { - shortcut: ['Ctrl+0', 'Meta+0'], - action: () => { - //TODO - }, - }, - }; + const keySettings = hotkeysConfiguration; const shortcut = keyNorm(event); const key = Object.entries(keySettings).reduce((acc, cur) => { const [key, value] = cur; @@ -138,8 +88,8 @@ export class CoreEditor { return acc; }, ''); - if (keySettings[key] && keySettings[key].action) { - keySettings[key].action(); + if (keySettings[key] && keySettings[key].handler) { + keySettings[key].handler(this); event.preventDefault(); } }); @@ -175,7 +125,7 @@ export class CoreEditor { } } - private onSelectTool(tool: string) { + public onSelectTool(tool: string) { this.selectTool(tool); } @@ -211,7 +161,7 @@ export class CoreEditor { this.renderersContainer.update(command); } - private onSelectHistory(name: HistoryOperationType) { + public onSelectHistory(name: HistoryOperationType) { const history = new EditorHistory(this); if (name === 'undo') { history.undo(); diff --git a/packages/ketcher-core/src/application/editor/editorEvents.ts b/packages/ketcher-core/src/application/editor/editorEvents.ts index ac6d3ea0d9..acd57d3fd8 100644 --- a/packages/ketcher-core/src/application/editor/editorEvents.ts +++ b/packages/ketcher-core/src/application/editor/editorEvents.ts @@ -1,5 +1,7 @@ import { Subscription } from 'subscription'; import { ToolEventHandlerName } from 'application/editor/tools/Tool'; +import { CoreEditor } from 'application/editor/Editor'; +import ZoomTool from 'application/editor/tools/Zoom'; export let editorEvents; @@ -44,3 +46,63 @@ export const renderersEvents: ToolEventHandlerName[] = [ 'mouseLeaveDrawingEntity', 'mouseUpMonomer', ]; + +export const hotkeysConfiguration = { + exit: { + shortcut: ['Shift+Tab', 'Escape'], + handler: (editor: CoreEditor) => { + editor.events.selectTool.dispatch('select-rectangle'); + }, + }, + undo: { + shortcut: ['Ctrl+z', 'Meta+z'], + handler: (editor: CoreEditor) => { + editor.onSelectHistory('undo'); + }, + }, + redo: { + shortcut: ['Shift+Ctrl+Z', 'Ctrl+Y', 'Shift+Meta+Z', 'Meta+y'], + handler: (editor: CoreEditor) => { + editor.onSelectHistory('redo'); + }, + }, + erase: { + shortcut: ['Delete', 'Backspace'], + handler: (editor: CoreEditor) => { + editor.events.selectTool.dispatch('erase'); + }, + }, + clear: { + shortcut: ['Ctrl+Del', 'Ctrl+Backspace', 'Meta+Del', 'Meta+Backspace'], + handler: (editor: CoreEditor) => { + editor.events.selectTool.dispatch('clear'); + editor.events.selectTool.dispatch('select-rectangle'); + }, + }, + 'zoom-plus': { + shortcut: ['Ctrl+=', 'Meta+='], + handler: () => { + ZoomTool.instance.zoomIn(); + }, + }, + 'zoom-minus': { + shortcut: ['Ctrl+-', 'Meta+-'], + handler: () => { + ZoomTool.instance.zoomOut(); + }, + }, + 'zoom-reset': { + shortcut: ['Ctrl+0', 'Meta+0'], + handler: () => { + ZoomTool.instance.resetZoom(); + }, + }, + 'select-all': { + shortcut: ['Ctrl+a', 'Meta+a'], + handler: (editor: CoreEditor) => { + const modelChanges = + editor.drawingEntitiesManager.selectAllDrawingEntities(); + editor.renderersContainer.update(modelChanges); + }, + }, +}; diff --git a/packages/ketcher-core/src/application/editor/tools/Zoom.ts b/packages/ketcher-core/src/application/editor/tools/Zoom.ts index ec07afb148..083ea30930 100644 --- a/packages/ketcher-core/src/application/editor/tools/Zoom.ts +++ b/packages/ketcher-core/src/application/editor/tools/Zoom.ts @@ -255,16 +255,31 @@ class ZoomTool implements BaseTool { }; } + private get zoomStep() { + return 0.1; + } + + public zoomIn(zoomStep = this.zoomStep) { + this.zoom?.scaleTo(this.canvasWrapper, this.zoomLevel + zoomStep); + } + + public zoomOut(zoomStep = this.zoomStep) { + this.zoom?.scaleTo(this.canvasWrapper, this.zoomLevel - zoomStep); + } + + public resetZoom() { + this.zoom?.transform(this.canvasWrapper, new ZoomTransform(1, 0, 0)); + } + initMenuZoom() { - const zoomStep = 0.1; select('.zoom-in').on('click', () => { - this.zoom?.scaleTo(this.canvasWrapper, this.zoomLevel + zoomStep); + this.zoomIn(); }); select('.zoom-out').on('click', () => { - this.zoom?.scaleTo(this.canvasWrapper, this.zoomLevel - zoomStep); + this.zoomOut(); }); select('.zoom-reset').on('click', () => { - this.zoom?.transform(this.canvasWrapper, new ZoomTransform(1, 0, 0)); + this.resetZoom(); }); } diff --git a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts index 04e79014c3..f9a8889305 100644 --- a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts +++ b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts @@ -161,6 +161,20 @@ export class DrawingEntitiesManager { return command; } + public selectAllDrawingEntities() { + const command = new Command(); + + this.allEntities.forEach(([, drawingEntity]) => { + if (!drawingEntity.selected) { + drawingEntity.turnOnSelection(); + const operation = new DrawingEntitySelectOperation(drawingEntity); + command.addOperation(operation); + } + }); + + return command; + } + public moveDrawingEntityModelChange( drawingEntity: DrawingEntity, offset?: Vec2, diff --git a/packages/ketcher-core/src/utilities/keynorm.ts b/packages/ketcher-core/src/utilities/keynorm.ts index 7608e52b3e..f66063b00b 100644 --- a/packages/ketcher-core/src/utilities/keynorm.ts +++ b/packages/ketcher-core/src/utilities/keynorm.ts @@ -87,10 +87,12 @@ export function isControlKey(event) { return mac ? event.metaKey : event.ctrlKey; } +// TODO rename and unify after moving all hotkeys to core editor +// to handle all events in same way and to have same structure for all hotkey configs function keyNorm(obj) { - if (obj instanceof KeyboardEvent) - // eslint-disable-line no-undef - return normalizeKeyEvent(obj); // eslint-disable-line prefer-rest-params + if (obj instanceof KeyboardEvent) { + return normalizeKeyEvent(obj); + } return typeof obj === 'object' ? normalizeKeyMap(obj) : normalizeKeyName(obj); } diff --git a/packages/ketcher-polymer-editor-react/src/Editor.tsx b/packages/ketcher-polymer-editor-react/src/Editor.tsx index 6a64af1521..a699712f21 100644 --- a/packages/ketcher-polymer-editor-react/src/Editor.tsx +++ b/packages/ketcher-polymer-editor-react/src/Editor.tsx @@ -131,7 +131,6 @@ function Editor({ theme, togglerComponent }: EditorProps) { const editor = useAppSelector(selectEditor); const activeTool = useAppSelector(selectEditorActiveTool); const isLoading = useLoading(); - let keyboardEventListener; const [isMonomerLibraryHidden, setIsMonomerLibraryHidden] = useState(false); useEffect(() => { @@ -143,7 +142,6 @@ function Editor({ theme, togglerComponent }: EditorProps) { return () => { dispatch(destroyEditor(null)); dispatch(loadMonomerLibrary([])); - document.removeEventListener('keydown', keyboardEventListener); }; }, [dispatch]); @@ -174,15 +172,11 @@ function Editor({ theme, togglerComponent }: EditorProps) { ), ); - if (!keyboardEventListener) { - keyboardEventListener = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - dispatch(selectTool('select-rectangle')); - editor.events.selectTool.dispatch('select-rectangle'); - } - }; - document.addEventListener('keydown', keyboardEventListener); - } + editor.events.selectTool.add((toolName: string) => { + if (toolName !== activeTool) { + dispatch(selectTool(toolName)); + } + }); } }, [editor]); From 97074324d45261c0d85e6479e4e6aa9cd001ea79 Mon Sep 17 00:00:00 2001 From: Roman Rodionov Date: Thu, 4 Jan 2024 14:40:51 +0100 Subject: [PATCH 3/4] - fixed lint --- .../src/script/ui/component/cliparea/cliparea.jsx | 7 +++++-- packages/ketcher-react/src/script/ui/state/hotkeys.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/ketcher-react/src/script/ui/component/cliparea/cliparea.jsx b/packages/ketcher-react/src/script/ui/component/cliparea/cliparea.jsx index fae491389c..d991593ee0 100644 --- a/packages/ketcher-react/src/script/ui/component/cliparea/cliparea.jsx +++ b/packages/ketcher-react/src/script/ui/component/cliparea/cliparea.jsx @@ -17,8 +17,11 @@ import { Component, createRef } from 'react'; import clsx from 'clsx'; import classes from './cliparea.module.less'; -import { KetcherLogger, notifyRequestCompleted } from 'ketcher-core'; -import { isControlKey } from 'ketcher-core'; +import { + KetcherLogger, + notifyRequestCompleted, + isControlKey, +} from 'ketcher-core'; import { isClipboardAPIAvailable, notifyCopyCut } from './clipboardUtils'; const ieCb = window.clipboardData; diff --git a/packages/ketcher-react/src/script/ui/state/hotkeys.ts b/packages/ketcher-react/src/script/ui/state/hotkeys.ts index 419a21df1d..7109d2fc16 100644 --- a/packages/ketcher-react/src/script/ui/state/hotkeys.ts +++ b/packages/ketcher-react/src/script/ui/state/hotkeys.ts @@ -28,12 +28,12 @@ import { MolSerializer, runAsyncAction, SettingsManager, + keyNorm, } from 'ketcher-core'; import { debounce, isEqual } from 'lodash/fp'; import { load, onAction, removeStructAction } from './shared'; import actions from '../action'; -import { keyNorm } from 'ketcher-core'; import { isIE } from 'react-device-detect'; import { selectAbbreviationLookupValue, From 86b71ff97ac8d48b141189c59dfdeca61f582ddc Mon Sep 17 00:00:00 2001 From: Roman Rodionov Date: Fri, 5 Jan 2024 15:46:53 +0100 Subject: [PATCH 4/4] - fixed case with shift hotkeys --- .../src/application/editor/Editor.ts | 32 +++++++++---------- .../src/application/editor/editorEvents.ts | 2 +- .../ketcher-core/src/utilities/keynorm.ts | 29 +++++++++++++++-- .../src/Editor.tsx | 18 +++++++---- .../src/script/ui/state/hotkeys.ts | 28 ++-------------- 5 files changed, 57 insertions(+), 52 deletions(-) diff --git a/packages/ketcher-core/src/application/editor/Editor.ts b/packages/ketcher-core/src/application/editor/Editor.ts index a420f267e5..05f67f84d3 100644 --- a/packages/ketcher-core/src/application/editor/Editor.ts +++ b/packages/ketcher-core/src/application/editor/Editor.ts @@ -27,7 +27,7 @@ import { MacromoleculesConverter } from 'application/editor/MacromoleculesConver import { BaseMonomer } from 'domain/entities/BaseMonomer'; import { ketcherProvider } from 'application/utils'; import { SnakeMode } from './modes/internal'; -import { keyNorm } from '../../utilities/keynorm'; +import { initHotKeys, keyNorm } from '../../utilities/keynorm'; interface ICoreEditorConstructorParams { theme; @@ -53,6 +53,7 @@ export class CoreEditor { // private lastEvent: Event | undefined; private tool?: Tool | BaseTool; private micromoleculesEditor: Editor; + private hotKeyEventHandler: (event: unknown) => void = () => {}; constructor({ theme, canvas }: ICoreEditorConstructorParams) { this.theme = theme; @@ -77,22 +78,20 @@ export class CoreEditor { return editor; } + private handleHotKeyEvents(event) { + const keySettings = hotkeysConfiguration; + const hotKeys = initHotKeys(keySettings); + const shortcutKey = keyNorm.lookup(hotKeys, event); + + if (keySettings[shortcutKey] && keySettings[shortcutKey].handler) { + keySettings[shortcutKey].handler(this); + event.preventDefault(); + } + } + setupHotKeysEvents() { - document.addEventListener('keydown', (event) => { - const keySettings = hotkeysConfiguration; - const shortcut = keyNorm(event); - const key = Object.entries(keySettings).reduce((acc, cur) => { - const [key, value] = cur; - const shortcutExists = value.shortcut.find((el) => el === shortcut); - if (shortcutExists) acc += key; - return acc; - }, ''); - - if (keySettings[key] && keySettings[key].handler) { - keySettings[key].handler(this); - event.preventDefault(); - } - }); + this.hotKeyEventHandler = (event) => this.handleHotKeyEvents(event); + document.addEventListener('keydown', this.hotKeyEventHandler); } private subscribeEvents() { @@ -185,6 +184,7 @@ export class CoreEditor { for (const eventName in this.events) { this.events[eventName].handlers = []; } + document.removeEventListener('keydown', this.hotKeyEventHandler); } get trackedDomEvents() { diff --git a/packages/ketcher-core/src/application/editor/editorEvents.ts b/packages/ketcher-core/src/application/editor/editorEvents.ts index acd57d3fd8..9e95de1317 100644 --- a/packages/ketcher-core/src/application/editor/editorEvents.ts +++ b/packages/ketcher-core/src/application/editor/editorEvents.ts @@ -61,7 +61,7 @@ export const hotkeysConfiguration = { }, }, redo: { - shortcut: ['Shift+Ctrl+Z', 'Ctrl+Y', 'Shift+Meta+Z', 'Meta+y'], + shortcut: ['Mod+Ctrl+Z', 'Ctrl+Y', 'Mod+Meta+Z', 'Meta+y'], handler: (editor: CoreEditor) => { editor.onSelectHistory('redo'); }, diff --git a/packages/ketcher-core/src/utilities/keynorm.ts b/packages/ketcher-core/src/utilities/keynorm.ts index f66063b00b..d37f556186 100644 --- a/packages/ketcher-core/src/utilities/keynorm.ts +++ b/packages/ketcher-core/src/utilities/keynorm.ts @@ -62,8 +62,7 @@ function modifiers(name, event, shift) { if (event.altKey) name = 'Alt+' + name; if (event.ctrlKey) name = 'Ctrl+' + name; if (event.metaKey) name = 'Meta+' + name; - if (event.shiftKey && (shift !== false || event.key !== 'Shift')) - name = 'Shift+' + name; + if (shift !== false && event.shiftKey) name = 'Shift+' + name; return name; } @@ -97,11 +96,35 @@ function keyNorm(obj) { return typeof obj === 'object' ? normalizeKeyMap(obj) : normalizeKeyName(obj); } +function setHotKey(key, actName, hotKeys) { + if (Array.isArray(hotKeys[key])) hotKeys[key].push(actName); + else hotKeys[key] = [actName]; +} + +export function initHotKeys(actions) { + const hotKeys = {}; + let act; + + Object.keys(actions).forEach((actName) => { + act = actions[actName]; + if (!act.shortcut) return; + + if (Array.isArray(act.shortcut)) { + act.shortcut.forEach((key) => { + setHotKey(key, actName, hotKeys); + }); + } else { + setHotKey(act.shortcut, actName, hotKeys); + } + }); + + return keyNorm(hotKeys); +} + function lookup(map, event) { let name = rusToEng(KN.keyName(event), event); if (name === 'Add') name = '+'; // numpad '+' and '-' if (name === 'Subtract') name = '-'; - const isChar = name.length === 1 && name !== ' '; let res = map[modifiers(name, event, !isChar)]; let baseName; diff --git a/packages/ketcher-polymer-editor-react/src/Editor.tsx b/packages/ketcher-polymer-editor-react/src/Editor.tsx index 0ed192c9ce..758a40e1eb 100644 --- a/packages/ketcher-polymer-editor-react/src/Editor.tsx +++ b/packages/ketcher-polymer-editor-react/src/Editor.tsx @@ -180,6 +180,12 @@ function Editor({ theme, togglerComponent }: EditorProps) { ); useEffect(() => { + const handler = (toolName: string) => { + if (toolName !== activeTool) { + dispatch(selectTool(toolName)); + } + }; + if (editor) { editor.events.error.add((errorText) => { dispatch(openErrorTooltip(errorText)); @@ -195,13 +201,13 @@ function Editor({ theme, togglerComponent }: EditorProps) { }), ), ); - - editor.events.selectTool.add((toolName: string) => { - if (toolName !== activeTool) { - dispatch(selectTool(toolName)); - } - }); + editor.events.selectTool.add(handler); } + + return () => { + dispatch(selectTool(null)); + editor?.events.selectTool.remove(handler); + }; }, [editor]); const handleOpenPreview = useCallback((e) => { diff --git a/packages/ketcher-react/src/script/ui/state/hotkeys.ts b/packages/ketcher-react/src/script/ui/state/hotkeys.ts index 7109d2fc16..7513110cff 100644 --- a/packages/ketcher-react/src/script/ui/state/hotkeys.ts +++ b/packages/ketcher-react/src/script/ui/state/hotkeys.ts @@ -29,6 +29,7 @@ import { runAsyncAction, SettingsManager, keyNorm, + initHotKeys, } from 'ketcher-core'; import { debounce, isEqual } from 'lodash/fp'; import { load, onAction, removeStructAction } from './shared'; @@ -49,7 +50,7 @@ import { handleHotkeyOverItem } from './handleHotkeysOverItem'; export function initKeydownListener(element) { return function (dispatch, getState) { - const hotKeys = initHotKeys(); + const hotKeys = initHotKeys(actions); element.addEventListener('keydown', (event) => keyHandle(dispatch, getState, hotKeys, event), ); @@ -230,31 +231,6 @@ function getHoveredItem( return Object.keys(hoveredItem).length ? hoveredItem : null; } -function setHotKey(key, actName, hotKeys) { - if (Array.isArray(hotKeys[key])) hotKeys[key].push(actName); - else hotKeys[key] = [actName]; -} - -function initHotKeys() { - const hotKeys = {}; - let act; - - Object.keys(actions).forEach((actName) => { - act = actions[actName]; - if (!act.shortcut) return; - - if (Array.isArray(act.shortcut)) { - act.shortcut.forEach((key) => { - setHotKey(key, actName, hotKeys); - }); - } else { - setHotKey(act.shortcut, actName, hotKeys); - } - }); - - return keyNorm(hotKeys); -} - function checkGroupOnTool(group, actionTool) { let index = group.indexOf(actionTool.tool);