Skip to content

Commit 5645592

Browse files
DustyShoePfannkuchensackdunkeroni
authored
Feat(canvas): add contextual tool hints to header bar (#9117)
* Feat(canvas): add contextual tool hints to header bar * convert switch statement to resolver map --------- Co-authored-by: Alexander Eichhorn <[email protected]> Co-authored-by: dunkeroni <[email protected]>
1 parent 413ea2d commit 5645592

6 files changed

Lines changed: 419 additions & 0 deletions

File tree

invokeai/frontend/web/public/locales/en.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2873,6 +2873,39 @@
28732873
"polygon": "Polygon",
28742874
"polygonHint": "Click to add points, click the first point to close."
28752875
},
2876+
"modifierHints": {
2877+
"keys": {
2878+
"control": "Ctrl",
2879+
"command": "Cmd",
2880+
"option": "Option",
2881+
"alt": "Alt",
2882+
"shift": "Shift",
2883+
"space": "Space",
2884+
"wheel": "Wheel",
2885+
"arrows": "Arrows",
2886+
"enter": "Enter",
2887+
"esc": "Esc"
2888+
},
2889+
"labels": {
2890+
"pan": "Pan",
2891+
"pickColor": "Pick color",
2892+
"straightLine": "Straight line",
2893+
"resizeBrush": "Resize brush",
2894+
"resizeEraser": "Resize eraser",
2895+
"subtractMask": "Subtract mask",
2896+
"snap45Degrees": "Snap to 45deg",
2897+
"lockAspectRatio": "Lock ratio",
2898+
"unlockAspectRatio": "Unlock ratio",
2899+
"scaleFromCenter": "Scale from center",
2900+
"fineGrid": "Fine grid",
2901+
"commitText": "Commit",
2902+
"newLine": "New line",
2903+
"cancelText": "Cancel",
2904+
"dragText": "Drag text",
2905+
"snapRotation": "Snap rotation",
2906+
"nudgeSelection": "Nudge selection"
2907+
}
2908+
},
28762909
"tool": {
28772910
"brush": "Brush",
28782911
"eraser": "Eraser",
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Flex, Kbd, Text } from '@invoke-ai/ui-library';
2+
import { useStore } from '@nanostores/react';
3+
import { useAppSelector } from 'app/store/storeHooks';
4+
import type { IDockviewHeaderActionsProps } from 'dockview';
5+
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
6+
import { selectLassoMode } from 'features/controlLayers/store/canvasSettingsSlice';
7+
import { selectBbox } from 'features/controlLayers/store/selectors';
8+
import type { Tool } from 'features/controlLayers/store/types';
9+
import { IS_MAC_OS } from 'features/system/components/HotkeysModal/useHotkeyData';
10+
import { atom } from 'nanostores';
11+
import { Fragment, memo, useMemo } from 'react';
12+
import { useTranslation } from 'react-i18next';
13+
14+
import { getCanvasToolModifierHints } from './canvasToolModifierHints';
15+
import { WORKSPACE_PANEL_ID } from './shared';
16+
17+
const $fallbackTool = atom<Tool>('move');
18+
const $fallbackToolBuffer = atom<Tool | null>(null);
19+
const $fallbackTextSession = atom<null>(null);
20+
21+
type CanvasToolModifierHintKey = ReturnType<typeof getCanvasToolModifierHints>[number]['keys'][number];
22+
23+
const formatKey = (key: CanvasToolModifierHintKey, t: (key: string) => string) => {
24+
switch (key) {
25+
case 'mod':
26+
return IS_MAC_OS ? t('controlLayers.modifierHints.keys.command') : t('controlLayers.modifierHints.keys.control');
27+
case 'alt':
28+
return IS_MAC_OS ? t('controlLayers.modifierHints.keys.option') : t('controlLayers.modifierHints.keys.alt');
29+
case 'shift':
30+
return t('controlLayers.modifierHints.keys.shift');
31+
case 'space':
32+
return t('controlLayers.modifierHints.keys.space');
33+
case 'wheel':
34+
return t('controlLayers.modifierHints.keys.wheel');
35+
case 'arrows':
36+
return t('controlLayers.modifierHints.keys.arrows');
37+
case 'enter':
38+
return t('controlLayers.modifierHints.keys.enter');
39+
case 'esc':
40+
return t('controlLayers.modifierHints.keys.esc');
41+
}
42+
};
43+
44+
export const DockviewCanvasHeaderActions = memo((props: IDockviewHeaderActionsProps) => {
45+
const { t } = useTranslation();
46+
const canvasManager = useCanvasManagerSafe();
47+
const lassoMode = useAppSelector(selectLassoMode);
48+
const bboxAspectRatioLocked = useAppSelector((state) => selectBbox(state).aspectRatio.isLocked);
49+
50+
const tool = useStore(canvasManager?.tool.$tool ?? $fallbackTool);
51+
const toolBuffer = useStore(canvasManager?.tool.$toolBuffer ?? $fallbackToolBuffer);
52+
const textSession = useStore(canvasManager?.tool.tools.text.$session ?? $fallbackTextSession);
53+
54+
const effectiveTool = useMemo<Tool>(() => {
55+
if (toolBuffer && (tool === 'view' || tool === 'colorPicker')) {
56+
return toolBuffer;
57+
}
58+
return tool;
59+
}, [tool, toolBuffer]);
60+
61+
const hints = useMemo(() => {
62+
if (!canvasManager || props.activePanel?.id !== WORKSPACE_PANEL_ID) {
63+
return [];
64+
}
65+
66+
return getCanvasToolModifierHints({
67+
tool: effectiveTool,
68+
lassoMode,
69+
bboxAspectRatioLocked,
70+
hasActiveTextSession: Boolean(textSession),
71+
});
72+
}, [bboxAspectRatioLocked, canvasManager, effectiveTool, lassoMode, props.activePanel?.id, textSession]);
73+
74+
if (hints.length === 0) {
75+
return null;
76+
}
77+
78+
return (
79+
<Flex
80+
h="full"
81+
alignItems="center"
82+
gap={4}
83+
pe={2}
84+
pointerEvents="none"
85+
userSelect="none"
86+
w="max-content"
87+
minW="max-content"
88+
>
89+
{hints.map((hint) => (
90+
<Flex key={hint.id} alignItems="center" gap={2} whiteSpace="nowrap">
91+
<Flex alignItems="center" gap={1} flexShrink={0}>
92+
{hint.keys.map((key, index) => (
93+
<Fragment key={`${hint.id}:${key}`}>
94+
{index > 0 && (
95+
<Text fontSize="xs" color="base.500">
96+
+
97+
</Text>
98+
)}
99+
<Kbd fontSize="xs">{formatKey(key, t)}</Kbd>
100+
</Fragment>
101+
))}
102+
</Flex>
103+
<Text fontSize="xs" color="base.300">
104+
{t(hint.labelKey)}
105+
</Text>
106+
</Flex>
107+
))}
108+
</Flex>
109+
);
110+
});
111+
112+
DockviewCanvasHeaderActions.displayName = 'DockviewCanvasHeaderActions';

invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { memo, useCallback, useEffect } from 'react';
2222

2323
import { CanvasTabLeftPanel } from './CanvasTabLeftPanel';
2424
import { CanvasWorkspacePanel } from './CanvasWorkspacePanel';
25+
import { DockviewCanvasHeaderActions } from './DockviewCanvasHeaderActions';
2526
import { DockviewTabCanvasViewer } from './DockviewTabCanvasViewer';
2627
import { DockviewTabCanvasWorkspace } from './DockviewTabCanvasWorkspace';
2728
import { DockviewTabLaunchpad } from './DockviewTabLaunchpad';
@@ -132,6 +133,7 @@ const MainPanel = memo(() => {
132133
onReady={onReady}
133134
theme={dockviewTheme}
134135
tabComponents={tabComponents}
136+
rightHeaderActionsComponent={DockviewCanvasHeaderActions}
135137
/>
136138
<FloatingCanvasLeftPanelButtons />
137139
<FloatingRightPanelButtons />
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { getCanvasToolModifierHintIds } from './canvasToolModifierHints';
4+
5+
describe('getCanvasToolModifierHintIds', () => {
6+
it('returns brush hints in priority order', () => {
7+
expect(
8+
getCanvasToolModifierHintIds({
9+
tool: 'brush',
10+
lassoMode: 'freehand',
11+
bboxAspectRatioLocked: false,
12+
hasActiveTextSession: false,
13+
})
14+
).toEqual(['shiftStraightLine', 'modWheelResizeBrush', 'spacePan', 'altPickColor']);
15+
});
16+
17+
it('omits alt color-picker hint for eraser', () => {
18+
expect(
19+
getCanvasToolModifierHintIds({
20+
tool: 'eraser',
21+
lassoMode: 'freehand',
22+
bboxAspectRatioLocked: false,
23+
hasActiveTextSession: false,
24+
})
25+
).toEqual(['shiftStraightLine', 'modWheelResizeEraser', 'spacePan']);
26+
});
27+
28+
it('adds polygon snapping for polygon lasso', () => {
29+
expect(
30+
getCanvasToolModifierHintIds({
31+
tool: 'lasso',
32+
lassoMode: 'polygon',
33+
bboxAspectRatioLocked: false,
34+
hasActiveTextSession: false,
35+
})
36+
).toEqual(['modSubtractMask', 'shiftSnap45Degrees', 'spacePan']);
37+
});
38+
39+
it('omits polygon snapping for freehand lasso', () => {
40+
expect(
41+
getCanvasToolModifierHintIds({
42+
tool: 'lasso',
43+
lassoMode: 'freehand',
44+
bboxAspectRatioLocked: false,
45+
hasActiveTextSession: false,
46+
})
47+
).toEqual(['modSubtractMask', 'spacePan']);
48+
});
49+
50+
it('switches the bbox aspect-ratio hint based on lock state', () => {
51+
expect(
52+
getCanvasToolModifierHintIds({
53+
tool: 'bbox',
54+
lassoMode: 'freehand',
55+
bboxAspectRatioLocked: false,
56+
hasActiveTextSession: false,
57+
})
58+
).toEqual(['shiftLockAspectRatio', 'altScaleFromCenter', 'modFineGrid']);
59+
60+
expect(
61+
getCanvasToolModifierHintIds({
62+
tool: 'bbox',
63+
lassoMode: 'freehand',
64+
bboxAspectRatioLocked: true,
65+
hasActiveTextSession: false,
66+
})
67+
).toEqual(['shiftUnlockAspectRatio', 'altScaleFromCenter', 'modFineGrid']);
68+
});
69+
70+
it('only shows text-session hints when a text session is active', () => {
71+
expect(
72+
getCanvasToolModifierHintIds({
73+
tool: 'text',
74+
lassoMode: 'freehand',
75+
bboxAspectRatioLocked: false,
76+
hasActiveTextSession: true,
77+
})
78+
).toEqual(['enterCommitText', 'shiftEnterNewLine', 'escCancelText', 'modDragText', 'shiftSnapRotation']);
79+
80+
expect(
81+
getCanvasToolModifierHintIds({
82+
tool: 'text',
83+
lassoMode: 'freehand',
84+
bboxAspectRatioLocked: false,
85+
hasActiveTextSession: false,
86+
})
87+
).toEqual(['spacePan', 'altPickColor']);
88+
});
89+
});

0 commit comments

Comments
 (0)