Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keyboard Shortcuts / Hotkeys #441

Merged
merged 3 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/composables/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noyce.

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 setTool = (tool: Tools) => () => {
useToolStore().setCurrentTool(tool);
};

export const ACTION_TO_FUNC = {
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<Action, () => void>;
47 changes: 19 additions & 28 deletions src/composables/useKeyboardShortcuts.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,25 @@
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';
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();
export const actionToKey = ref(ACTION_TO_KEY);

floryst marked this conversation as resolved.
Show resolved Hide resolved
// @ts-ignore - map may not have all keys of tools
const activeToolStore = toolToStore[toolStore.currentTool];
if (!activeToolStore) return;

const labels = Object.entries(activeToolStore.labels);
const activeLabelIndex = labels.findIndex(
([name]) => name === activeToolStore.activeLabel
);
export function useKeyboardShortcuts() {
const keys = useMagicKeys();
let unwatchFuncs = [] as Array<ReturnType<typeof whenever>>;

const [nextLabel] = labels.at((activeLabelIndex + offset) % labels.length)!;
activeToolStore.setActiveLabel(nextLabel);
};
watch(
actionToKey,
(actionMap) => {
unwatchFuncs.forEach((unwatch) => unwatch());

export function useKeyboardShortcuts() {
onKeyDown(DECREMENT_LABEL_KEY, () => applyLabelOffset(-1));
onKeyDown(INCREMENT_LABEL_KEY, () => applyLabelOffset(1));
unwatchFuncs = getEntries(actionMap).map(([action, key]) => {
return whenever(keys[key], ACTION_TO_FUNC[action]);
});
},
{ immediate: true, deep: true }
);
}
21 changes: 18 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -203,11 +204,25 @@ 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<string, string> = {
CT: 'CT-AAA',
MR: 'CT-Coronary-Arteries-2',
US: 'US-Fetal',
};

// Keyboard shortcuts/hotkeys. Can add modifiers: 'Shift+Ctrl+A'
export const ACTION_TO_KEY = {
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<Action, string>;
20 changes: 20 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,23 @@ export const Messages = {
} as const;

export const ANNOTATION_TOOL_HANDLE_RADIUS = 10; // pixels

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];
22 changes: 20 additions & 2 deletions src/io/import/processors/handleConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -21,6 +23,8 @@ const dataBrowser = z
})
.optional();

const shortcuts = z.record(z.enum(ACTIONS), z.string()).optional();

// --------------------------------------------------------------------------
// Labels

Expand Down Expand Up @@ -54,6 +58,7 @@ const config = z.object({
layout,
dataBrowser,
labels,
shortcuts,
});

type Config = z.infer<typeof config>;
Expand Down Expand Up @@ -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);
};

/**
Expand All @@ -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();
}
Expand Down
9 changes: 9 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,12 @@ export const TypedArrayConstructorNames = [
'Float32Array',
'Float64Array',
];

// https://stackoverflow.com/a/74823834
type Entries<T> = {
[K in keyof T]-?: [K, T[K]];
}[keyof T][];

// Object.entries with keys preserved rather as string
export const getEntries = <T extends object>(obj: T) =>
Object.entries(obj) as Entries<T>;
Loading