Skip to content

Commit ae5694d

Browse files
Feat(canvas): Add temporary bbox move with C key hold (#9104)
* Feat(Canvas): BBox temp move on C hold * `fix(canvas): unify temporary bbox hotkey with tool state` * fix(canvas): stop bbox interaction on window blur * fix(canvas): update dockview hints for base tool state * fix(canvas): address bbox hotkey review follow-ups --------- Co-authored-by: Josh Corbett <joshwcorbett@icloud.com>
1 parent 93fd9ae commit ae5694d

16 files changed

Lines changed: 493 additions & 66 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,7 @@
743743
},
744744
"selectBboxTool": {
745745
"title": "Bbox Tool",
746-
"desc": "Select the bounding box tool."
746+
"desc": "Tap to select the bounding box tool, or hold to use it temporarily."
747747
},
748748
"decrementToolWidth": {
749749
"title": "Decrement Tool Width",

invokeai/frontend/web/src/features/controlLayers/components/Text/CanvasTextOverlay.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ const TextEditor = ({
395395
const modifierPressed = syncModifierState(event);
396396
if (!modifierPressed) {
397397
if (canvasManager.tool.$tool.get() !== 'text') {
398-
canvasManager.tool.$tool.set('text');
398+
canvasManager.tool.setBaseTool('text');
399399
}
400400
return;
401401
}
@@ -413,7 +413,7 @@ const TextEditor = ({
413413
setIsDragging(true);
414414
event.currentTarget.setPointerCapture(event.pointerId);
415415
},
416-
[anchor, canvasManager.tool.$tool, getStagePoint, syncModifierState]
416+
[anchor, canvasManager.tool, getStagePoint, syncModifierState]
417417
);
418418

419419
const handleBorderPointerDown = useCallback(

invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
22
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
3-
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
43
import { memo } from 'react';
54
import { useTranslation } from 'react-i18next';
65
import { PiBoundingBoxBold } from 'react-icons/pi';
@@ -10,14 +9,6 @@ export const ToolBboxButton = memo(() => {
109
const selectBbox = useSelectTool('bbox');
1110
const isSelected = useToolIsSelected('bbox');
1211

13-
useRegisteredHotkeys({
14-
id: 'selectBboxTool',
15-
category: 'canvas',
16-
callback: selectBbox,
17-
options: { enabled: !isSelected },
18-
dependencies: [selectBbox, isSelected],
19-
});
20-
2112
return (
2213
<Tooltip label={`${t('controlLayers.tool.bbox')} (C)`} placement="end">
2314
<IconButton

invokeai/frontend/web/src/features/controlLayers/components/Tool/hooks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const useToolIsSelected = (tool: Tool) => {
1313
export const useSelectTool = (tool: Tool) => {
1414
const canvasManager = useCanvasManager();
1515
const setTool = useCallback(() => {
16-
canvasManager.tool.$tool.set(tool);
17-
}, [canvasManager.tool.$tool, tool]);
16+
canvasManager.tool.setBaseTool(tool);
17+
}, [canvasManager.tool, tool]);
1818
return setTool;
1919
};

invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { useCanvasInvertMaskHotkey } from 'features/controlLayers/hooks/useCanva
2626
import { useCanvasMergeDownHotkey } from 'features/controlLayers/hooks/useCanvasMergeDownHotkey';
2727
import { useCanvasMergeVisibleHotkey } from 'features/controlLayers/hooks/useCanvasMergeVisibleHotkey';
2828
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
29+
import { useCanvasSelectBboxToolHotkey } from 'features/controlLayers/hooks/useCanvasSelectBboxToolHotkey';
2930
import { useCanvasToggleBboxHotkey } from 'features/controlLayers/hooks/useCanvasToggleBboxHotkey';
3031
import { useCanvasToggleNonRasterLayersHotkey } from 'features/controlLayers/hooks/useCanvasToggleNonRasterLayersHotkey';
3132
import { useCanvasTransformHotkey } from 'features/controlLayers/hooks/useCanvasTransformHotkey';
@@ -52,6 +53,7 @@ export const CanvasToolbar = memo(() => {
5253
useCanvasEntityQuickSwitchHotkey();
5354
useNextPrevEntityHotkeys();
5455
useCanvasTransformHotkey();
56+
useCanvasSelectBboxToolHotkey();
5557
useCanvasFilterHotkey();
5658
useCanvasInvertMaskHotkey();
5759
useCanvasToggleNonRasterLayersHotkey();
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
2+
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
3+
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
4+
import { useCallback, useEffect } from 'react';
5+
6+
export const useCanvasSelectBboxToolHotkey = () => {
7+
useAssertSingleton(useCanvasSelectBboxToolHotkey.name);
8+
const canvasManager = useCanvasManager();
9+
10+
const onKeyDown = useCallback(
11+
(event: KeyboardEvent) => {
12+
if (event.repeat) {
13+
return;
14+
}
15+
canvasManager.tool.onBboxToolHotkeyDown(event);
16+
},
17+
[canvasManager]
18+
);
19+
20+
const onKeyUp = useCallback(
21+
(event: KeyboardEvent) => {
22+
canvasManager.tool.onBboxToolHotkeyUp(event);
23+
},
24+
[canvasManager]
25+
);
26+
27+
useEffect(() => {
28+
return () => {
29+
canvasManager.tool.clearBboxToolHotkey();
30+
};
31+
}, [canvasManager]);
32+
33+
useRegisteredHotkeys({
34+
id: 'selectBboxTool',
35+
category: 'canvas',
36+
callback: onKeyDown,
37+
options: { keydown: true, keyup: false, preventDefault: true },
38+
dependencies: [onKeyDown],
39+
});
40+
41+
useRegisteredHotkeys({
42+
id: 'selectBboxTool',
43+
category: 'canvas',
44+
callback: onKeyUp,
45+
options: { keydown: false, keyup: true, preventDefault: true },
46+
dependencies: [onKeyUp],
47+
});
48+
};

invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityBufferObjectRenderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
8888
if (this.hasBuffer() && !this.manager.$isBusy.get()) {
8989
const isTemporaryShapesToolSwitch = shouldPreserveSuspendableShapesSession(
9090
this.manager.tool.$tool.get(),
91-
this.manager.tool.$toolBuffer.get(),
91+
this.manager.tool.$baseTool.get(),
9292
this.manager.tool.tools.rect.hasSuspendableSession()
9393
);
9494

invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBboxToolModule.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,22 @@ export class CanvasBboxToolModule extends CanvasModuleBase {
278278
});
279279
};
280280

281+
/**
282+
* Force-completes any in-flight bbox interaction when focus is lost.
283+
*
284+
* Temporary bbox selection can be cleared on window blur, unlike the old Space/Alt quick-switches. If blur happens
285+
* mid drag/transform, Konva can otherwise keep the bbox interaction alive after the tool switches away.
286+
*/
287+
stopInteraction = () => {
288+
if (this.konva.proxyRect.isDragging()) {
289+
this.konva.proxyRect.stopDrag();
290+
}
291+
292+
if (this.konva.transformer.isTransforming()) {
293+
this.konva.transformer.stopTransform();
294+
}
295+
};
296+
281297
/**
282298
* Handles the dragmove event on the bbox rect:
283299
* - Snaps the bbox position to the grid (determined by ctrl/meta key)

invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasLassoToolModule.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export class CanvasLassoToolModule extends CanvasModuleBase {
116116

117117
render = () => {
118118
const tool = this.parent.$tool.get();
119-
const isTemporaryViewSwitch = tool === 'view' && this.parent.$toolBuffer.get() === 'lasso';
119+
const isTemporaryViewSwitch = tool === 'view' && this.parent.$baseTool.get() === 'lasso';
120120
if (tool !== 'lasso' && !isTemporaryViewSwitch) {
121121
this.hidePreview();
122122
return;
@@ -130,7 +130,7 @@ export class CanvasLassoToolModule extends CanvasModuleBase {
130130

131131
onToolChanged = () => {
132132
const tool = this.parent.$tool.get();
133-
const isTemporaryViewSwitch = tool === 'view' && this.parent.$toolBuffer.get() === 'lasso';
133+
const isTemporaryViewSwitch = tool === 'view' && this.parent.$baseTool.get() === 'lasso';
134134
if (tool !== 'lasso' && !isTemporaryViewSwitch) {
135135
this.reset();
136136
}

invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasShapeToolModule.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export class CanvasShapeToolModule extends CanvasModuleBase {
176176
const tool = this.parent.$tool.get();
177177
const isTemporaryToolSwitch = shouldPreserveSuspendableShapesSession(
178178
tool,
179-
this.parent.$toolBuffer.get(),
179+
this.parent.$baseTool.get(),
180180
this.hasSuspendableSession()
181181
);
182182
if (tool !== 'rect' && !isTemporaryToolSwitch) {
@@ -192,7 +192,7 @@ export class CanvasShapeToolModule extends CanvasModuleBase {
192192
const tool = this.parent.$tool.get();
193193
const isTemporaryToolSwitch = shouldPreserveSuspendableShapesSession(
194194
tool,
195-
this.parent.$toolBuffer.get(),
195+
this.parent.$baseTool.get(),
196196
this.hasSuspendableSession()
197197
);
198198
if (tool !== 'rect' && !isTemporaryToolSwitch) {
@@ -393,7 +393,7 @@ export class CanvasShapeToolModule extends CanvasModuleBase {
393393
const tool = this.parent.$tool.get();
394394
const isTemporaryToolSwitch = shouldPreserveSuspendableShapesSession(
395395
tool,
396-
this.parent.$toolBuffer.get(),
396+
this.parent.$baseTool.get(),
397397
this.hasSuspendableSession()
398398
);
399399
if (tool !== 'rect' && !isTemporaryToolSwitch) {

0 commit comments

Comments
 (0)