Added hotkeys to change labels (#3070)
* temp commit
* Added ability to change default label and object label by using Ctrl+{number}
* Removed extra changes
* Minor refactoring
* Added ability to change assigned keys
* Redesigned popover
* Added changelog record & updated version
* Added memoization
* Some minor changes
* Applied comments
Co-authored-by: Dmitry Kalinin <dmitry.kalinin@intel.com>
main
parent
d2a1d12fba
commit
7524202492
@ -0,0 +1,85 @@
|
|||||||
|
// Copyright (C) 2021 Intel Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import Popover from 'antd/lib/popover';
|
||||||
|
import Button from 'antd/lib/button';
|
||||||
|
import { Row, Col } from 'antd/lib/grid';
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
|
||||||
|
import { CombinedState } from 'reducers/interfaces';
|
||||||
|
import CVATTooltip from 'components/common/cvat-tooltip';
|
||||||
|
|
||||||
|
interface LabelKeySelectorPopoverProps {
|
||||||
|
updateLabelShortcutKey(updatedKey: string, labelID: number): void;
|
||||||
|
keyToLabelMapping: Record<string, number>;
|
||||||
|
labelID: number;
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LabelKeySelectorPopoverContentProps {
|
||||||
|
updateLabelShortcutKey(updatedKey: string, labelID: number): void;
|
||||||
|
labelID: number;
|
||||||
|
keyToLabelMapping: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent(props: LabelKeySelectorPopoverContentProps): JSX.Element {
|
||||||
|
const { keyToLabelMapping, labelID, updateLabelShortcutKey } = props;
|
||||||
|
const labels = useSelector((state: CombinedState) => state.annotation.job.labels);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='cvat-label-item-setup-shortcut-popover'>
|
||||||
|
{[['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['0']].map((arr, i_) => (
|
||||||
|
<Row justify='space-around' gutter={[16, 16]} key={i_}>
|
||||||
|
{arr.map((i) => {
|
||||||
|
const previousLabelID = keyToLabelMapping[i];
|
||||||
|
const labelName = Number.isInteger(previousLabelID) ?
|
||||||
|
labels.filter((label: any): boolean => label.id === previousLabelID)[0]?.name ||
|
||||||
|
'undefined' :
|
||||||
|
'None';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col key={i} span={8}>
|
||||||
|
<CVATTooltip title={labelName}>
|
||||||
|
<Button onClick={() => updateLabelShortcutKey(i, labelID)}>
|
||||||
|
<Text>{`${i}:`}</Text>
|
||||||
|
<Text type='secondary'>{labelName}</Text>
|
||||||
|
</Button>
|
||||||
|
</CVATTooltip>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemoizedContent = React.memo(PopoverContent);
|
||||||
|
|
||||||
|
function LabelKeySelectorPopover(props: LabelKeySelectorPopoverProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
children, labelID, updateLabelShortcutKey, keyToLabelMapping,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
destroyTooltipOnHide={{ keepParent: false }}
|
||||||
|
trigger='click'
|
||||||
|
content={(
|
||||||
|
<MemoizedContent
|
||||||
|
keyToLabelMapping={keyToLabelMapping}
|
||||||
|
labelID={labelID}
|
||||||
|
updateLabelShortcutKey={updateLabelShortcutKey}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
placement='left'
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(LabelKeySelectorPopover);
|
||||||
@ -1,26 +1,108 @@
|
|||||||
// Copyright (C) 2020 Intel Corporation
|
// Copyright (C) 2020-2021 Intel Corporation
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import message from 'antd/lib/message';
|
||||||
|
|
||||||
|
import { CombinedState } from 'reducers/interfaces';
|
||||||
|
import { rememberObject, updateAnnotationsAsync } from 'actions/annotation-actions';
|
||||||
import LabelItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/label-item';
|
import LabelItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/label-item';
|
||||||
|
import GlobalHotKeys from 'utils/mousetrap-react';
|
||||||
|
|
||||||
interface Props {
|
function LabelsListComponent(): JSX.Element {
|
||||||
labelIDs: number[];
|
const dispatch = useDispatch();
|
||||||
listHeight: number;
|
const {
|
||||||
}
|
annotation: {
|
||||||
|
job: { labels },
|
||||||
|
tabContentHeight: listHeight,
|
||||||
|
annotations: { activatedStateID, states },
|
||||||
|
},
|
||||||
|
shortcuts: { keyMap },
|
||||||
|
} = useSelector((state: CombinedState) => state);
|
||||||
|
const labelIDs = labels.map((label: any): number => label.id);
|
||||||
|
|
||||||
|
const [keyToLabelMapping, setKeyToLabelMapping] = useState<Record<string, number>>(
|
||||||
|
Object.fromEntries(labelIDs.slice(0, 10).map((labelID: number, idx: number) => [(idx + 1) % 10, labelID])),
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateLabelShortcutKey = useCallback(
|
||||||
|
(key: string, labelID: number) => {
|
||||||
|
// unassign any keys assigned to the current labels
|
||||||
|
const keyToLabelMappingCopy = { ...keyToLabelMapping };
|
||||||
|
for (const shortKey of Object.keys(keyToLabelMappingCopy)) {
|
||||||
|
if (keyToLabelMappingCopy[shortKey] === labelID) {
|
||||||
|
delete keyToLabelMappingCopy[shortKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === '—') {
|
||||||
|
setKeyToLabelMapping(keyToLabelMappingCopy);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
export default function LabelsListComponent(props: Props): JSX.Element {
|
// check if this key is assigned to another label
|
||||||
const { listHeight, labelIDs } = props;
|
if (key in keyToLabelMappingCopy) {
|
||||||
|
// try to find a new key for the other label
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const adjustedI = (i + 1) % 10;
|
||||||
|
if (!(adjustedI in keyToLabelMappingCopy)) {
|
||||||
|
keyToLabelMappingCopy[adjustedI] = keyToLabelMappingCopy[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// delete assigning to the other label
|
||||||
|
delete keyToLabelMappingCopy[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
// assigning to the current label
|
||||||
|
keyToLabelMappingCopy[key] = labelID;
|
||||||
|
setKeyToLabelMapping(keyToLabelMappingCopy);
|
||||||
|
},
|
||||||
|
[keyToLabelMapping],
|
||||||
|
);
|
||||||
|
|
||||||
|
const subKeyMap = {
|
||||||
|
SWITCH_LABEL: keyMap.SWITCH_LABEL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
SWITCH_LABEL: (event: KeyboardEvent | undefined, shortcut: string) => {
|
||||||
|
if (event) event.preventDefault();
|
||||||
|
const labelID = keyToLabelMapping[shortcut.split('+')[1].trim()];
|
||||||
|
const label = labels.filter((_label: any) => _label.id === labelID)[0];
|
||||||
|
if (Number.isInteger(labelID) && label) {
|
||||||
|
if (Number.isInteger(activatedStateID)) {
|
||||||
|
const activatedState = states.filter((state: any) => state.clientID === activatedStateID)[0];
|
||||||
|
if (activatedState) {
|
||||||
|
activatedState.label = label;
|
||||||
|
dispatch(updateAnnotationsAsync([activatedState]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch(rememberObject({ activeLabelID: labelID }));
|
||||||
|
message.destroy();
|
||||||
|
message.success(`Default label was changed to "${label.name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: listHeight }} className='cvat-objects-sidebar-labels-list'>
|
<div style={{ height: listHeight }} className='cvat-objects-sidebar-labels-list'>
|
||||||
|
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} />
|
||||||
{labelIDs.map(
|
{labelIDs.map(
|
||||||
(labelID: number): JSX.Element => (
|
(labelID: number): JSX.Element => (
|
||||||
<LabelItemContainer key={labelID} labelID={labelID} />
|
<LabelItemContainer
|
||||||
|
key={labelID}
|
||||||
|
labelID={labelID}
|
||||||
|
keyToLabelMapping={keyToLabelMapping}
|
||||||
|
updateLabelShortcutKey={updateLabelShortcutKey}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default React.memo(LabelsListComponent);
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
// Copyright (C) 2020 Intel Corporation
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import LabelsListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/labels-list';
|
|
||||||
import { CombinedState } from 'reducers/interfaces';
|
|
||||||
|
|
||||||
interface StateToProps {
|
|
||||||
labelIDs: number[];
|
|
||||||
listHeight: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapStateToProps(state: CombinedState): StateToProps {
|
|
||||||
const {
|
|
||||||
annotation: {
|
|
||||||
job: { labels },
|
|
||||||
tabContentHeight: listHeight,
|
|
||||||
},
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
return {
|
|
||||||
labelIDs: labels.map((label: any): number => label.id),
|
|
||||||
listHeight,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(LabelsListComponent);
|
|
||||||
Loading…
Reference in New Issue