Skip to content

Commit

Permalink
Add shortcuts to brush tools (#8519)
Browse files Browse the repository at this point in the history
<!-- Raise an issue to propose your change
(https://github.com/cvat-ai/cvat/issues).
It helps to avoid duplication of efforts from multiple independent
contributors.
Discuss your ideas with maintainers to be sure that changes will be
approved and merged.
Read the [Contribution guide](https://docs.cvat.ai/docs/contributing/).
-->

<!-- Provide a general summary of your changes in the Title above -->

### Motivation and context
<!-- Why is this change required? What problem does it solve? If it
fixes an open
issue, please link to the issue here. Describe your changes in detail,
add
screenshots. -->
The pr adds shortcuts to actions in brush tool panel (in mask editing
mode)

![image](https://github.com/user-attachments/assets/a2559e58-250c-4726-a057-7ebe09f2050a)

### How has this been tested?
<!-- Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc. -->

### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply.
If an item isn't applicable for some reason, then ~~explicitly
strikethrough~~ the whole
line. If you don't do that, GitHub will show incorrect progress for the
pull request.
If you're unsure about any of these, don't hesitate to ask. We're here
to help! -->
- [x] I submit my changes into the `develop` branch
- [x] I have created a changelog fragment <!-- see top comment in
CHANGELOG.md -->
- [ ] I have updated the documentation accordingly
- [x] I have added tests to cover my changes
- ~~[ ] I have linked related issues (see [GitHub docs](

https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword))~~
- [x] I have increased versions of npm packages if it is necessary

([cvat-canvas](https://github.com/cvat-ai/cvat/tree/develop/cvat-canvas#versioning),

[cvat-core](https://github.com/cvat-ai/cvat/tree/develop/cvat-core#versioning),

[cvat-data](https://github.com/cvat-ai/cvat/tree/develop/cvat-data#versioning)
and

[cvat-ui](https://github.com/cvat-ai/cvat/tree/develop/cvat-ui#versioning))

### License

- [x] I submit _my code changes_ under the same [MIT License](
https://github.com/cvat-ai/cvat/blob/develop/LICENSE) that covers the
project.
  Feel free to contact the maintainers if that's a concern.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced keyboard shortcuts for various brush tools, enhancing user
efficiency.
	- Added tooltips displaying keyboard shortcuts for better usability.
	- Implemented a `LabelSelector` component for improved label selection.

- **Improvements**
- Updated sorting logic for shortcut settings to organize items by their
defined weights.

- **Refactor**
- Enhanced state management and control flow for brush tool
interactions.
- Added a new optional `weight` property to the key mapping interface
for better customization.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
klakhov authored Oct 11, 2024
1 parent 847b48c commit 12dac6d
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 56 deletions.
4 changes: 4 additions & 0 deletions changelog.d/20241009_101726_klakhov_brush_shortcuts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Added

- Keyboard shortcuts for **brush**, **eraser**, **polygon** and **polygon remove** tools on masks drawing toolbox
(<https://github.com/cvat-ai/cvat/pull/8519>)
2 changes: 1 addition & 1 deletion cvat-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.66.0",
"version": "1.66.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import CVATTooltip from 'components/common/cvat-tooltip';
import { CombinedState, ObjectType, ShapeType } from 'reducers';
import LabelSelector from 'components/label-selector/label-selector';
import { rememberObject, updateCanvasBrushTools } from 'actions/annotation-actions';
import { ShortcutScope } from 'utils/enums';
import GlobalHotKeys from 'utils/mousetrap-react';
import { subKeyMap } from 'utils/component-subkeymap';
import { registerComponentShortcuts } from 'actions/shortcuts-actions';
import useDraggable from './draggable-hoc';

const DraggableArea = (
Expand All @@ -32,13 +36,46 @@ const DraggableArea = (
</div>
);

const componentShortcuts = {
ACTIVATE_BRUSH_TOOL_STANDARD_CONTROLS: {
name: 'Brush tool',
description: 'Activate brush tool on masks drawing toolbox',
sequences: ['shift+1'],
scope: ShortcutScope.STANDARD_WORKSPACE_CONTROLS,
displayWeight: 10,
},
ACTIVATE_ERASER_TOOL_STANDARD_CONTROLS: {
name: 'Eraser tool',
description: 'Activate eraser tool on masks drawing toolbox',
sequences: ['shift+2'],
scope: ShortcutScope.STANDARD_WORKSPACE_CONTROLS,
displayWeight: 15,
},
ACTIVATE_POLYGON_TOOL_STANDARD_CONTROLS: {
name: 'Polygon tool',
description: 'Activate polygon tool on masks drawing toolbox',
sequences: ['shift+3'],
scope: ShortcutScope.STANDARD_WORKSPACE_CONTROLS,
displayWeight: 20,
},
ACTIVATE_POLYGON_REMOVE_TOOL_STANDARD_CONTROLS: {
name: 'Polygon remove tool',
description: 'Activate polygon remove tool on masks drawing toolbox',
sequences: ['shift+4'],
scope: ShortcutScope.STANDARD_WORKSPACE_CONTROLS,
displayWeight: 25,
},
};
registerComponentShortcuts(componentShortcuts);

const MIN_BRUSH_SIZE = 1;
function BrushTools(): React.ReactPortal | null {
const dispatch = useDispatch();
const defaultLabelID = useSelector((state: CombinedState) => state.annotation.drawing.activeLabelID);
const config = useSelector((state: CombinedState) => state.annotation.canvas.brushTools);
const canvasInstance = useSelector((state: CombinedState) => state.annotation.canvas.instance);
const labels = useSelector((state: CombinedState) => state.annotation.job.labels);
const { keyMap, normalizedKeyMap } = useSelector((state: CombinedState) => state.shortcuts);
const { visible } = config;

const [editableState, setEditableState] = useState<any | null>(null);
Expand All @@ -53,6 +90,26 @@ function BrushTools(): React.ReactPortal | null {
'polygon-minus': false,
});

const setBrushTool = useCallback(() => setCurrentTool('brush'), [setCurrentTool]);
const setEraserTool = useCallback(() => {
if (!blockedTools.eraser) {
setCurrentTool('eraser');
}
}, [setCurrentTool, blockedTools.eraser]);
const setPolygonTool = useCallback(() => setCurrentTool('polygon-plus'), [setCurrentTool]);
const setPolygonRemoveTool = useCallback(() => {
if (!blockedTools['polygon-minus']) {
setCurrentTool('polygon-minus');
}
}, [setCurrentTool, blockedTools['polygon-minus']]);

const handlers: Record<keyof typeof componentShortcuts, (event?: KeyboardEvent) => void> = {
ACTIVATE_BRUSH_TOOL_STANDARD_CONTROLS: setBrushTool,
ACTIVATE_ERASER_TOOL_STANDARD_CONTROLS: setEraserTool,
ACTIVATE_POLYGON_TOOL_STANDARD_CONTROLS: setPolygonTool,
ACTIVATE_POLYGON_REMOVE_TOOL_STANDARD_CONTROLS: setPolygonRemoveTool,
};

const [removeUnderlyingPixels, setRemoveUnderlyingPixels] = useState(false);
const dragBar = useDraggable(
(): number[] => {
Expand Down Expand Up @@ -99,7 +156,7 @@ function BrushTools(): React.ReactPortal | null {
type: currentTool,
size: brushSize,
form: brushForm,
color: label.color,
color: label.color as string,
onBlockUpdated,
},
onUpdateConfiguration,
Expand All @@ -112,7 +169,7 @@ function BrushTools(): React.ReactPortal | null {
type: currentTool,
size: brushSize,
form: brushForm,
color: label.color,
color: label.color as string,
onBlockUpdated,
},
onUpdateConfiguration,
Expand Down Expand Up @@ -202,68 +259,86 @@ function BrushTools(): React.ReactPortal | null {

return ReactDOM.createPortal((
<div className='cvat-brush-tools-toolbox' style={{ top, left, display: visible ? '' : 'none' }}>
<Button
type='text'
className='cvat-brush-tools-finish'
icon={<Icon component={CheckIcon} />}
onClick={() => {
if (canvasInstance instanceof Canvas) {
if (editableState) {
canvasInstance.edit({ enabled: false });
} else {
canvasInstance.draw({ enabled: false });
}
}
}}
<GlobalHotKeys
keyMap={subKeyMap(componentShortcuts, keyMap)}
handlers={handlers}
/>
{!editableState && (
<CVATTooltip title={`Finish ${normalizedKeyMap.SWITCH_DRAW_MODE_STANDARD_CONTROLS}`}>
<Button
type='text'
disabled={!!editableState}
className='cvat-brush-tools-continue'
icon={<Icon component={PlusIcon} />}
className='cvat-brush-tools-finish'
icon={<Icon component={CheckIcon} />}
onClick={() => {
if (canvasInstance instanceof Canvas) {
canvasInstance.draw({ enabled: false, continue: true });

dispatch(
rememberObject({
activeObjectType: ObjectType.SHAPE,
activeShapeType: ShapeType.MASK,
activeLabelID: defaultLabelID,
}),
);
if (editableState) {
canvasInstance.edit({ enabled: false });
} else {
canvasInstance.draw({ enabled: false });
}
}
}}
/>
</CVATTooltip>
{!editableState && (
<CVATTooltip title={`Continue ${normalizedKeyMap.SWITCH_REDRAW_MODE_STANDARD_CONTROLS}`}>
<Button
type='text'
disabled={!!editableState}
className='cvat-brush-tools-continue'
icon={<Icon component={PlusIcon} />}
onClick={() => {
if (canvasInstance instanceof Canvas && defaultLabelID) {
canvasInstance.draw({ enabled: false, continue: true });

dispatch(
rememberObject({
activeObjectType: ObjectType.SHAPE,
activeShapeType: ShapeType.MASK,
activeLabelID: defaultLabelID,
}),
);
}
}}
/>
</CVATTooltip>
)}
<hr />
<Button
type='text'
className={['cvat-brush-tools-brush', ...(currentTool === 'brush' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={BrushIcon} />}
onClick={() => setCurrentTool('brush')}
/>
<Button
type='text'
className={['cvat-brush-tools-eraser', ...(currentTool === 'eraser' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={EraserIcon} />}
onClick={() => setCurrentTool('eraser')}
disabled={blockedTools.eraser}
/>
<Button
type='text'
className={['cvat-brush-tools-polygon-plus', ...(currentTool === 'polygon-plus' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={PolygonPlusIcon} />}
onClick={() => setCurrentTool('polygon-plus')}
/>
<Button
type='text'
className={['cvat-brush-tools-polygon-minus', ...(currentTool === 'polygon-minus' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={PolygonMinusIcon} />}
onClick={() => setCurrentTool('polygon-minus')}
disabled={blockedTools['polygon-minus']}
/>
<CVATTooltip title={`Brush tool ${normalizedKeyMap.ACTIVATE_BRUSH_TOOL_STANDARD_CONTROLS}`}>
<Button
type='text'
className={['cvat-brush-tools-brush', ...(currentTool === 'brush' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={BrushIcon} />}
onClick={setBrushTool}
/>
</CVATTooltip>
<CVATTooltip title={`Eraser tool ${normalizedKeyMap.ACTIVATE_ERASER_TOOL_STANDARD_CONTROLS}`}>
<Button
type='text'
className={['cvat-brush-tools-eraser', ...(currentTool === 'eraser' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={EraserIcon} />}
onClick={setEraserTool}
disabled={blockedTools.eraser}
/>
</CVATTooltip>
<CVATTooltip title={`Polygon tool ${normalizedKeyMap.ACTIVATE_POLYGON_TOOL_STANDARD_CONTROLS}`}>
<Button
type='text'
className={['cvat-brush-tools-polygon-plus', ...(currentTool === 'polygon-plus' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={PolygonPlusIcon} />}
onClick={setPolygonTool}
/>
</CVATTooltip>
<CVATTooltip
title={`Polygon remove tool ${normalizedKeyMap.ACTIVATE_POLYGON_REMOVE_TOOL_STANDARD_CONTROLS}`}
>
<Button
type='text'
className={['cvat-brush-tools-polygon-minus', ...(currentTool === 'polygon-minus' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={PolygonMinusIcon} />}
onClick={setPolygonRemoveTool}
disabled={blockedTools['polygon-minus']}
/>
</CVATTooltip>
{ ['brush', 'eraser'].includes(currentTool) ? (
<CVATTooltip title='Brush size [Hold Alt + Right Mouse Click + Drag Left/Right]'>
<InputNumber
Expand Down Expand Up @@ -305,6 +380,7 @@ function BrushTools(): React.ReactPortal | null {
)}
{ dragBar }
</div>

), window.document.body);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function ShortcutsSettingsComponent(props: Props): JSX.Element {
const scopeItems = Object.values(ShortcutScope).map((scope: string) => {
const viewFilteredItems = filteredKeyMap.filter(
([, item]) => item.scope === scope,
);
).sort(([, item1], [, item2]) => (item1.displayWeight ?? 0) - (item2.displayWeight ?? 0));
if (viewFilteredItems.length === 0) {
return null;
}
Expand Down
1 change: 1 addition & 0 deletions cvat-ui/src/utils/mousetrap-react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface KeyMapItem {
scope: ShortcutScope;
nonActive?: boolean;
applicable?: string[];
displayWeight?: number;
}

export interface KeyMap {
Expand Down
25 changes: 25 additions & 0 deletions tests/cypress/e2e/features/masks_basics.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,31 @@ context('Manipulations with masks', { scrollBehavior: false }, () => {
cy.get('.cvat-brush-tools-underlying-pixels').should('not.have.class', 'cvat-brush-tools-active-tool');
cy.finishMaskDrawing();
});

it('Check brush tools shortcuts', () => {
const mask1 = [{
method: 'brush',
coordinates: [[450, 250], [600, 400]],
}];
cy.startMaskDrawing();
cy.drawMask(mask1);
cy.get('.cvat-brush-tools-polygon-minus').click();
cy.get('.cvat-brush-tools-polygon-minus').should('have.class', 'cvat-brush-tools-active-tool');

cy.get('body').type('{shift}{1}');
cy.get('.cvat-brush-tools-brush').should('have.class', 'cvat-brush-tools-active-tool');
cy.get('body').type('{shift}{2}');
cy.get('.cvat-brush-tools-eraser').should('have.class', 'cvat-brush-tools-active-tool');
cy.get('body').type('{shift}{3}');
cy.get('.cvat-brush-tools-polygon-plus').should('have.class', 'cvat-brush-tools-active-tool');
cy.get('body').type('{shift}{4}');
cy.get('.cvat-brush-tools-polygon-minus').should('have.class', 'cvat-brush-tools-active-tool');

cy.get('.cvat-brush-tools-finish').trigger('mouseover');
cy.get('.cvat-brush-tools-finish').trigger('mouseout');
cy.get('body').type('n');
cy.get('.cvat-brush-tools-toolbox').should('not.be.visible');
});
});

describe('Tests to make sure that empty masks cannot be created', () => {
Expand Down

0 comments on commit 12dac6d

Please sign in to comment.