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
|
||||
|
||||
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 GlobalHotKeys from 'utils/mousetrap-react';
|
||||
|
||||
interface Props {
|
||||
labelIDs: number[];
|
||||
listHeight: number;
|
||||
}
|
||||
function LabelsListComponent(): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
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 {
|
||||
const { listHeight, labelIDs } = props;
|
||||
// check if this key is assigned to another label
|
||||
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 (
|
||||
<div style={{ height: listHeight }} className='cvat-objects-sidebar-labels-list'>
|
||||
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} />
|
||||
{labelIDs.map(
|
||||
(labelID: number): JSX.Element => (
|
||||
<LabelItemContainer key={labelID} labelID={labelID} />
|
||||
<LabelItemContainer
|
||||
key={labelID}
|
||||
labelID={labelID}
|
||||
keyToLabelMapping={keyToLabelMapping}
|
||||
updateLabelShortcutKey={updateLabelShortcutKey}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</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