From d63e0b0439d3362603ebff69dfe177d09dcaba3c Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Tue, 26 Sep 2023 10:43:16 -0400 Subject: [PATCH 1/3] refactor(shortcuts): make key to action function reactive --- src/composables/useKeyboardShortcuts.ts | 32 +++++++++++++++++++++---- src/utils/index.ts | 9 +++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/composables/useKeyboardShortcuts.ts b/src/composables/useKeyboardShortcuts.ts index 5d282bea0..6db4d4ddc 100644 --- a/src/composables/useKeyboardShortcuts.ts +++ b/src/composables/useKeyboardShortcuts.ts @@ -1,4 +1,5 @@ -import { onKeyDown } from '@vueuse/core'; +import { ref, watch } from 'vue'; +import { useMagicKeys, whenever } from '@vueuse/core'; import { DECREMENT_LABEL_KEY, INCREMENT_LABEL_KEY } from '../config'; import { useToolStore } from '../store/tools'; @@ -6,6 +7,7 @@ import { Tools } from '../store/tools/types'; import { useRectangleStore } from '../store/tools/rectangles'; import { useRulerStore } from '../store/tools/rulers'; import { usePolygonStore } from '../store/tools/polygons'; +import { getEntries } from '../utils'; const applyLabelOffset = (offset: number) => { const toolToStore = { @@ -15,7 +17,7 @@ const applyLabelOffset = (offset: number) => { }; const toolStore = useToolStore(); - // @ts-ignore - map may not have all keys of tools + // @ts-ignore - toolToStore may not have keys of all tools const activeToolStore = toolToStore[toolStore.currentTool]; if (!activeToolStore) return; @@ -28,7 +30,29 @@ const applyLabelOffset = (offset: number) => { activeToolStore.setActiveLabel(nextLabel); }; +const actionToFunc = { + 'decrement-label': () => applyLabelOffset(-1), + 'increment-label': () => applyLabelOffset(1), +}; + +const actionToKey = ref({ + 'decrement-label': DECREMENT_LABEL_KEY, + 'increment-label': INCREMENT_LABEL_KEY, +}); + export function useKeyboardShortcuts() { - onKeyDown(DECREMENT_LABEL_KEY, () => applyLabelOffset(-1)); - onKeyDown(INCREMENT_LABEL_KEY, () => applyLabelOffset(1)); + const keys = useMagicKeys(); + const unwatchFuncs = ref([] as Array>); + + watch( + actionToKey, + (actionMap) => { + unwatchFuncs.value.forEach((unwatch) => unwatch()); + + unwatchFuncs.value = getEntries(actionMap).map(([action, key]) => { + return whenever(keys[key], actionToFunc[action]); + }); + }, + { immediate: true, deep: true } + ); } diff --git a/src/utils/index.ts b/src/utils/index.ts index 2cc1d54e9..683b04344 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -289,3 +289,12 @@ export const TypedArrayConstructorNames = [ 'Float32Array', 'Float64Array', ]; + +// https://stackoverflow.com/a/74823834 +type Entries = { + [K in keyof T]-?: [K, T[K]]; +}[keyof T][]; + +// Object.entries with keys preserved +export const getEntries = (obj: T) => + Object.entries(obj) as Entries; From e68012f553dfe9580464df3f92c803e310340473 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Tue, 26 Sep 2023 14:33:42 -0400 Subject: [PATCH 2/3] feat(useKeyboardShortcuts): config.json adds to shortcuts --- src/composables/actions.ts | 32 ++++++++++++++++++ src/composables/useKeyboardShortcuts.ts | 41 +++--------------------- src/config.ts | 10 ++++-- src/constants.ts | 4 +++ src/io/import/processors/handleConfig.ts | 22 +++++++++++-- 5 files changed, 67 insertions(+), 42 deletions(-) create mode 100644 src/composables/actions.ts diff --git a/src/composables/actions.ts b/src/composables/actions.ts new file mode 100644 index 000000000..7783f13ea --- /dev/null +++ b/src/composables/actions.ts @@ -0,0 +1,32 @@ +import { useToolStore } from '../store/tools'; +import { Tools } from '../store/tools/types'; +import { useRectangleStore } from '../store/tools/rectangles'; +import { useRulerStore } from '../store/tools/rulers'; +import { usePolygonStore } from '../store/tools/polygons'; +import { Action } from '../constants'; + +const applyLabelOffset = (offset: number) => { + const toolToStore = { + [Tools.Rectangle]: useRectangleStore(), + [Tools.Ruler]: useRulerStore(), + [Tools.Polygon]: usePolygonStore(), + }; + const toolStore = useToolStore(); + + // @ts-ignore - toolToStore may not have keys of all tools + const activeToolStore = toolToStore[toolStore.currentTool]; + if (!activeToolStore) return; + + const labels = Object.entries(activeToolStore.labels); + const activeLabelIndex = labels.findIndex( + ([name]) => name === activeToolStore.activeLabel + ); + + const [nextLabel] = labels.at((activeLabelIndex + offset) % labels.length)!; + activeToolStore.setActiveLabel(nextLabel); +}; + +export const ACTION_TO_FUNC = { + 'decrement-label': () => applyLabelOffset(-1), + 'increment-label': () => applyLabelOffset(1), +} satisfies Record void>; diff --git a/src/composables/useKeyboardShortcuts.ts b/src/composables/useKeyboardShortcuts.ts index 6db4d4ddc..37fccdf32 100644 --- a/src/composables/useKeyboardShortcuts.ts +++ b/src/composables/useKeyboardShortcuts.ts @@ -1,44 +1,11 @@ import { ref, watch } from 'vue'; import { useMagicKeys, whenever } from '@vueuse/core'; -import { DECREMENT_LABEL_KEY, INCREMENT_LABEL_KEY } from '../config'; -import { useToolStore } from '../store/tools'; -import { Tools } from '../store/tools/types'; -import { useRectangleStore } from '../store/tools/rectangles'; -import { useRulerStore } from '../store/tools/rulers'; -import { usePolygonStore } from '../store/tools/polygons'; import { getEntries } from '../utils'; +import { ACTION_TO_KEY } from '../config'; +import { ACTION_TO_FUNC } from './actions'; -const applyLabelOffset = (offset: number) => { - const toolToStore = { - [Tools.Rectangle]: useRectangleStore(), - [Tools.Ruler]: useRulerStore(), - [Tools.Polygon]: usePolygonStore(), - }; - const toolStore = useToolStore(); - - // @ts-ignore - toolToStore may not have keys of all tools - const activeToolStore = toolToStore[toolStore.currentTool]; - if (!activeToolStore) return; - - const labels = Object.entries(activeToolStore.labels); - const activeLabelIndex = labels.findIndex( - ([name]) => name === activeToolStore.activeLabel - ); - - const [nextLabel] = labels.at((activeLabelIndex + offset) % labels.length)!; - activeToolStore.setActiveLabel(nextLabel); -}; - -const actionToFunc = { - 'decrement-label': () => applyLabelOffset(-1), - 'increment-label': () => applyLabelOffset(1), -}; - -const actionToKey = ref({ - 'decrement-label': DECREMENT_LABEL_KEY, - 'increment-label': INCREMENT_LABEL_KEY, -}); +export const actionToKey = ref(ACTION_TO_KEY); export function useKeyboardShortcuts() { const keys = useMagicKeys(); @@ -50,7 +17,7 @@ export function useKeyboardShortcuts() { unwatchFuncs.value.forEach((unwatch) => unwatch()); unwatchFuncs.value = getEntries(actionMap).map(([action, key]) => { - return whenever(keys[key], actionToFunc[action]); + return whenever(keys[key], ACTION_TO_FUNC[action]); }); }, { immediate: true, deep: true } diff --git a/src/config.ts b/src/config.ts index e05d5ad69..010a76728 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,6 +6,7 @@ import USFetusThumbnail from '@/src/assets/samples/3DUS-Fetus.jpg'; import { Layout, LayoutDirection } from './types/layout'; import { ViewSpec } from './types/views'; import { SampleDataset } from './types'; +import { Action } from './constants'; /** * These are the initial view IDs. @@ -203,11 +204,14 @@ export const POLYGON_LABEL_DEFAULTS = { white: { color: '#ffffff' }, }; -export const DECREMENT_LABEL_KEY = 'q'; -export const INCREMENT_LABEL_KEY = 'w'; - export const DEFAULT_PRESET_BY_MODALITY: Record = { CT: 'CT-AAA', MR: 'CT-Coronary-Arteries-2', US: 'US-Fetal', }; + +// Keyboard shortcuts/hotkeys +export const ACTION_TO_KEY = { + 'decrement-label': 'q', + 'increment-label': 'w', +} satisfies Record; diff --git a/src/constants.ts b/src/constants.ts index aa6f5a25e..d0beb9efe 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -47,3 +47,7 @@ export const Messages = { } as const; export const ANNOTATION_TOOL_HANDLE_RADIUS = 10; // pixels + +export const ACTIONS = ['decrement-label', 'increment-label'] as const; + +export type Action = (typeof ACTIONS)[number]; diff --git a/src/io/import/processors/handleConfig.ts b/src/io/import/processors/handleConfig.ts index 09217ee3a..ea4951fb0 100644 --- a/src/io/import/processors/handleConfig.ts +++ b/src/io/import/processors/handleConfig.ts @@ -7,7 +7,9 @@ import { useDataBrowserStore } from '@/src/store/data-browser'; import { usePolygonStore } from '@/src/store/tools/polygons'; import { useViewStore } from '@/src/store/views'; import { Layouts } from '@/src/config'; -import { zodEnumFromObjKeys } from '@/src/utils'; +import { ensureError, zodEnumFromObjKeys } from '@/src/utils'; +import { ACTIONS } from '@/src/constants'; +import { actionToKey } from '@/src/composables/useKeyboardShortcuts'; const layout = z .object({ @@ -21,6 +23,8 @@ const dataBrowser = z }) .optional(); +const shortcuts = z.record(z.enum(ACTIONS)).optional(); + // -------------------------------------------------------------------------- // Labels @@ -54,6 +58,7 @@ const config = z.object({ layout, dataBrowser, labels, + shortcuts, }); type Config = z.infer; @@ -93,10 +98,20 @@ const applyLayout = (manifest: Config) => { } }; +const applyShortcuts = (manifest: Config) => { + if (!manifest.shortcuts) return; + + actionToKey.value = { + ...actionToKey.value, + ...manifest.shortcuts, + }; +}; + const applyConfig = (manifest: Config) => { applyLayout(manifest); applyLabels(manifest); applySampleData(manifest); + applyShortcuts(manifest); }; /** @@ -111,7 +126,10 @@ const handleConfig: ImportHandler = async (dataSource, { done }) => { const manifest = await readConfigFile(fileSrc.file); applyConfig(manifest); } catch (err) { - return dataSource; + console.error(err); + throw new Error('Failed to parse config file', { + cause: ensureError(err), + }); } return done(); } From 3cda313dcb89409594b2155f54870c82cbe198a4 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Tue, 26 Sep 2023 15:34:46 -0400 Subject: [PATCH 3/3] feat(shortcuts): add annotation tool actions --- src/composables/actions.ts | 23 +++++++++++++++++++---- src/composables/useKeyboardShortcuts.ts | 6 +++--- src/config.ts | 17 ++++++++++++++--- src/constants.ts | 18 +++++++++++++++++- src/io/import/processors/handleConfig.ts | 2 +- src/utils/index.ts | 2 +- 6 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/composables/actions.ts b/src/composables/actions.ts index 7783f13ea..d19b4cca7 100644 --- a/src/composables/actions.ts +++ b/src/composables/actions.ts @@ -5,7 +5,7 @@ import { useRulerStore } from '../store/tools/rulers'; import { usePolygonStore } from '../store/tools/polygons'; import { Action } from '../constants'; -const applyLabelOffset = (offset: number) => { +const applyLabelOffset = (offset: number) => () => { const toolToStore = { [Tools.Rectangle]: useRectangleStore(), [Tools.Ruler]: useRulerStore(), @@ -26,7 +26,22 @@ const applyLabelOffset = (offset: number) => { activeToolStore.setActiveLabel(nextLabel); }; +const setTool = (tool: Tools) => () => { + useToolStore().setCurrentTool(tool); +}; + export const ACTION_TO_FUNC = { - 'decrement-label': () => applyLabelOffset(-1), - 'increment-label': () => applyLabelOffset(1), -} satisfies Record void>; + windowLevel: setTool(Tools.WindowLevel), + pan: setTool(Tools.Pan), + zoom: setTool(Tools.Zoom), + ruler: setTool(Tools.Ruler), + paint: setTool(Tools.Paint), + rectangle: setTool(Tools.Rectangle), + crosshairs: setTool(Tools.Crosshairs), + crop: setTool(Tools.Crop), + polygon: setTool(Tools.Polygon), + select: setTool(Tools.Select), + + decrementLabel: applyLabelOffset(-1), + incrementLabel: applyLabelOffset(1), +} as const satisfies Record void>; diff --git a/src/composables/useKeyboardShortcuts.ts b/src/composables/useKeyboardShortcuts.ts index 37fccdf32..7667684ee 100644 --- a/src/composables/useKeyboardShortcuts.ts +++ b/src/composables/useKeyboardShortcuts.ts @@ -9,14 +9,14 @@ export const actionToKey = ref(ACTION_TO_KEY); export function useKeyboardShortcuts() { const keys = useMagicKeys(); - const unwatchFuncs = ref([] as Array>); + let unwatchFuncs = [] as Array>; watch( actionToKey, (actionMap) => { - unwatchFuncs.value.forEach((unwatch) => unwatch()); + unwatchFuncs.forEach((unwatch) => unwatch()); - unwatchFuncs.value = getEntries(actionMap).map(([action, key]) => { + unwatchFuncs = getEntries(actionMap).map(([action, key]) => { return whenever(keys[key], ACTION_TO_FUNC[action]); }); }, diff --git a/src/config.ts b/src/config.ts index 010a76728..0d494e637 100644 --- a/src/config.ts +++ b/src/config.ts @@ -210,8 +210,19 @@ export const DEFAULT_PRESET_BY_MODALITY: Record = { US: 'US-Fetal', }; -// Keyboard shortcuts/hotkeys +// Keyboard shortcuts/hotkeys. Can add modifiers: 'Shift+Ctrl+A' export const ACTION_TO_KEY = { - 'decrement-label': 'q', - 'increment-label': 'w', + windowLevel: 'l', + pan: 'n', + zoom: 'z', + ruler: 'm', + paint: 'p', + rectangle: 'r', + crosshairs: 'c', + crop: 'b', + polygon: 'g', + select: 's', + + decrementLabel: 'q', + incrementLabel: 'w', } satisfies Record; diff --git a/src/constants.ts b/src/constants.ts index d0beb9efe..c09c1ba2d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -48,6 +48,22 @@ export const Messages = { export const ANNOTATION_TOOL_HANDLE_RADIUS = 10; // pixels -export const ACTIONS = ['decrement-label', 'increment-label'] as const; +export const ACTIONS = [ + // set the current tool + 'windowLevel', + 'pan', + 'zoom', + 'ruler', + 'paint', + 'rectangle', + 'crosshairs', + 'crop', + 'polygon', + 'select', + + // change the current label for the current tool + 'decrementLabel', + 'incrementLabel', +] as const; export type Action = (typeof ACTIONS)[number]; diff --git a/src/io/import/processors/handleConfig.ts b/src/io/import/processors/handleConfig.ts index ea4951fb0..c127abbaa 100644 --- a/src/io/import/processors/handleConfig.ts +++ b/src/io/import/processors/handleConfig.ts @@ -23,7 +23,7 @@ const dataBrowser = z }) .optional(); -const shortcuts = z.record(z.enum(ACTIONS)).optional(); +const shortcuts = z.record(z.enum(ACTIONS), z.string()).optional(); // -------------------------------------------------------------------------- // Labels diff --git a/src/utils/index.ts b/src/utils/index.ts index 683b04344..c1ac4e746 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -295,6 +295,6 @@ type Entries = { [K in keyof T]-?: [K, T[K]]; }[keyof T][]; -// Object.entries with keys preserved +// Object.entries with keys preserved rather as string export const getEntries = (obj: T) => Object.entries(obj) as Entries;