From c0c469f8246ab20dbc00953ea06c31df7bfe1872 Mon Sep 17 00:00:00 2001 From: Kirill Lakhov Date: Tue, 5 Jul 2022 17:38:17 +0300 Subject: [PATCH] Tags on frame (#75) --- CHANGELOG.md | 1 + cvat-ui/package.json | 2 +- cvat-ui/src/actions/annotation-actions.ts | 32 ++++-- cvat-ui/src/actions/settings-actions.ts | 10 ++ .../annotation-page/canvas/canvas-wrapper.tsx | 10 ++ .../controls-side-bar/setup-tag-popover.tsx | 20 ++-- .../objects-side-bar/object-item-menu.tsx | 24 +--- .../standard-workspace/remove-confirm.tsx | 53 +++++++++ .../standard-workspace/standard-workspace.tsx | 4 +- .../standard-workspace/styles.scss | 21 +++- .../standard3D-workspace.tsx | 4 +- .../tag-annotation-workspace/frame-tags.tsx | 79 +++++++++++++ .../tag-annotation-workspace/styles.scss | 69 +++++++++--- .../shortcuts-select.tsx | 16 +-- .../tag-annotation-sidebar.tsx | 104 +++++++++--------- .../tag-annotation-workspace.tsx | 4 +- .../header/settings-modal/styles.scss | 1 + .../settings-modal/workspace-settings.tsx | 20 ++++ .../label-selector/label-selector.tsx | 21 +++- .../annotation-page/canvas/canvas-wrapper.tsx | 3 + .../controls-side-bar/setup-tag-popover.tsx | 56 ++++++++-- .../objects-side-bar/object-item.tsx | 14 +-- .../objects-side-bar/objects-list.tsx | 13 +-- .../settings-modal/workspace-settings.tsx | 8 ++ cvat-ui/src/reducers/annotation-reducer.ts | 30 ++++- cvat-ui/src/reducers/interfaces.ts | 5 + cvat-ui/src/reducers/settings-reducer.ts | 10 ++ package-lock.json | 2 +- .../case_22_tag_annotation_mode.js | 63 ++++++----- .../case_24_delete_unlock_lock_object.js | 23 ++-- .../case_37_object_make_copy.js | 17 ++- .../case_55_repeat_draw_feature.js | 9 +- .../case_113_new_organization_pipeline.js | 4 +- .../issues_prs2/issue_1540_add_remove_tag.js | 6 +- tests/cypress/support/commands.js | 2 + 35 files changed, 554 insertions(+), 206 deletions(-) create mode 100644 cvat-ui/src/components/annotation-page/standard-workspace/remove-confirm.tsx create mode 100644 cvat-ui/src/components/annotation-page/tag-annotation-workspace/frame-tags.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index f945964b..6ba04a45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Progressbars in CLI for file uploading and downloading () - `utils/cli` changed to `cvat-cli` package () - Support custom file name for backup () +- Possibility to display tags on frame () - Support source and target storages (server part) () - Tests for import/export annotation, dataset, backup from/to cloud storage () diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 5f16c80f..d07a9691 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.38.1", + "version": "1.39.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 0d4c8541..52e21120 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -139,7 +139,7 @@ export enum AnnotationActionTypes { REPEAT_DRAW_SHAPE = 'REPEAT_DRAW_SHAPE', SHAPE_DRAWN = 'SHAPE_DRAWN', RESET_CANVAS = 'RESET_CANVAS', - REMEMBER_CREATED_OBJECT = 'REMEMBER_CREATED_OBJECT', + REMEMBER_OBJECT = 'REMEMBER_OBJECT', UPDATE_ANNOTATIONS_SUCCESS = 'UPDATE_ANNOTATIONS_SUCCESS', UPDATE_ANNOTATIONS_FAILED = 'UPDATE_ANNOTATIONS_FAILED', CREATE_ANNOTATIONS_SUCCESS = 'CREATE_ANNOTATIONS_SUCCESS', @@ -156,6 +156,7 @@ export enum AnnotationActionTypes { COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE', COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS', ACTIVATE_OBJECT = 'ACTIVATE_OBJECT', + REMOVE_OBJECT = 'REMOVE_OBJECT', REMOVE_OBJECT_SUCCESS = 'REMOVE_OBJECT_SUCCESS', REMOVE_OBJECT_FAILED = 'REMOVE_OBJECT_FAILED', PROPAGATE_OBJECT = 'PROPAGATE_OBJECT', @@ -553,6 +554,16 @@ export function removeObjectAsync(sessionInstance: any, objectState: any, force: }; } +export function removeObject(objectState: any, force: boolean): AnyAction { + return { + type: AnnotationActionTypes.REMOVE_OBJECT, + payload: { + objectState, + force, + }, + }; +} + export function editShape(enabled: boolean): AnyAction { return { type: AnnotationActionTypes.EDIT_SHAPE, @@ -1195,7 +1206,7 @@ export function rememberObject(createParams: { activeCuboidDrawingMethod?: CuboidDrawingMethod; }): AnyAction { return { - type: AnnotationActionTypes.REMEMBER_CREATED_OBJECT, + type: AnnotationActionTypes.REMEMBER_OBJECT, payload: createParams, }; } @@ -1555,6 +1566,7 @@ export function repeatDrawShapeAsync(): ThunkAction { return async (dispatch: ActionCreator): Promise => { const { canvas: { instance: canvasInstance }, + annotations: { states }, job: { labels, instance: jobInstance }, player: { frame: { number: frameNumber }, @@ -1614,13 +1626,17 @@ export function repeatDrawShapeAsync(): ThunkAction { if (canvasInstance instanceof Canvas) { canvasInstance.cancel(); } + if (activeObjectType === ObjectType.TAG) { - const objectState = new cvat.classes.ObjectState({ - objectType: ObjectType.TAG, - label: labels.filter((label: any) => label.id === activeLabelID)[0], - frame: frameNumber, - }); - dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState])); + const tags = states.filter((objectState: any): boolean => objectState.objectType === ObjectType.TAG); + if (tags.every((objectState: any): boolean => objectState.label.id !== activeLabelID)) { + const objectState = new cvat.classes.ObjectState({ + objectType: ObjectType.TAG, + label: labels.filter((label: any) => label.id === activeLabelID)[0], + frame: frameNumber, + }); + dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState])); + } } else if (canvasInstance) { canvasInstance.draw({ enabled: true, diff --git a/cvat-ui/src/actions/settings-actions.ts b/cvat-ui/src/actions/settings-actions.ts index bc2a17f6..40e2da00 100644 --- a/cvat-ui/src/actions/settings-actions.ts +++ b/cvat-ui/src/actions/settings-actions.ts @@ -42,6 +42,7 @@ export enum SettingsActionTypes { SET_SETTINGS = 'SET_SETTINGS', SWITCH_TOOLS_BLOCKER_STATE = 'SWITCH_TOOLS_BLOCKER_STATE', SWITCH_SHOWING_DELETED_FRAMES = 'SWITCH_SHOWING_DELETED_FRAMES', + SWITCH_SHOWING_TAGS_ON_FRAME = 'SWITCH_SHOWING_TAGS_ON_FRAME', } export function changeShapesOpacity(opacity: number): AnyAction { @@ -350,3 +351,12 @@ export function switchShowingDeletedFrames(showDeletedFrames: boolean): AnyActio }, }; } + +export function switchShowingTagsOnFrame(showTagsOnFrame: boolean): AnyAction { + return { + type: SettingsActionTypes.SWITCH_SHOWING_TAGS_ON_FRAME, + payload: { + showTagsOnFrame, + }, + }; +} diff --git a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx index f6a760c8..a691bb50 100644 --- a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx @@ -18,6 +18,7 @@ import { Canvas3d } from 'cvat-canvas3d-wrapper'; import getCore from 'cvat-core-wrapper'; import consts from 'consts'; import CVATTooltip from 'components/common/cvat-tooltip'; +import FrameTags from 'components/annotation-page/tag-annotation-workspace/frame-tags'; import ImageSetupsContent from './image-setups-content'; import ContextImage from '../standard-workspace/context-image/context-image'; @@ -69,6 +70,7 @@ interface Props { keyMap: KeyMap; canvasBackgroundColor: string; switchableAutomaticBordering: boolean; + showTagsOnFrame: boolean; onSetupCanvas: () => void; onDragCanvas: (enabled: boolean) => void; onZoomCanvas: (enabled: boolean) => void; @@ -780,6 +782,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { keyMap, switchableAutomaticBordering, automaticBordering, + showTagsOnFrame, onSwitchAutomaticBordering, onSwitchZLayer, onAddZLayer, @@ -842,6 +845,13 @@ export default class CanvasWrapperComponent extends React.PureComponent { + + {showTagsOnFrame ? ( +
+ +
+ ) : null} + ; ); } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx index a202e346..fd45740d 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -8,6 +8,7 @@ import Button from 'antd/lib/button'; import Text from 'antd/lib/typography/Text'; import LabelSelector from 'components/label-selector/label-selector'; +import { PlusOutlined } from '@ant-design/icons'; import CVATTooltip from 'components/common/cvat-tooltip'; interface Props { @@ -37,20 +38,21 @@ function SetupTagPopover(props: Props): JSX.Element { Label - - + + onSetup(selectedLabelID)} /> - - - - - + @@ -230,7 +216,9 @@ export default function ItemMenu(props: Props): JSX.Element { return ( - {!readonly && } + {!readonly && objectType !== ObjectType.TAG && ( + + )} {!readonly && } {is2D && !readonly && [ShapeType.POLYGON, ShapeType.POLYLINE, ShapeType.CUBOID].includes(shapeType) && ( diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/remove-confirm.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/remove-confirm.tsx new file mode 100644 index 00000000..136504ab --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/remove-confirm.tsx @@ -0,0 +1,53 @@ +// Copyright (C) 2022 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useCallback, useEffect, useState } from 'react'; + +import { useDispatch, useSelector } from 'react-redux'; +import { CombinedState } from 'reducers/interfaces'; + +import Modal from 'antd/lib/modal'; +import { removeObjectAsync, removeObject as removeObjectAction } from 'actions/annotation-actions'; + +export default function RemoveConfirmComponent(): JSX.Element | null { + const [visible, setVisible] = useState(false); + const [title, setTitle] = useState(''); + const objectState = useSelector((state: CombinedState) => state.annotation.remove.objectState); + const force = useSelector((state: CombinedState) => state.annotation.remove.force); + const jobInstance = useSelector((state: CombinedState) => state.annotation.job.instance); + const dispatch = useDispatch(); + + const onOk = useCallback(() => { + dispatch(removeObjectAsync(jobInstance, objectState, true)); + }, [jobInstance, objectState]); + const onCancel = useCallback(() => { + dispatch(removeObjectAction(null, false)); + }, []); + + useEffect(() => { + const newVisible = !!objectState && !force && objectState.lock; + setTitle(objectState?.lock ? 'Object is locked' : 'Remove object'); + setVisible(newVisible); + if (!newVisible && objectState) { + dispatch(removeObjectAsync(jobInstance, objectState, true)); + } + }, [objectState, force]); + + return ( + +
+ Are you sure you want to remove it? +
+
+ ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx index 75adab15..d62d226b 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -14,6 +14,7 @@ import ObjectsListContainer from 'containers/annotation-page/standard-workspace/ import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; import CanvasPointContextMenuComponent from 'components/annotation-page/canvas/canvas-point-context-menu'; import IssueAggregatorComponent from 'components/annotation-page/review/issues-aggregator'; +import RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm'; export default function StandardWorkspaceComponent(): JSX.Element { return ( @@ -25,6 +26,7 @@ export default function StandardWorkspaceComponent(): JSX.Element { + ); } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss index 78bc89c8..a3fc7e59 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -268,8 +268,7 @@ margin-top: $grid-unit-size; } -.cvat-setup-tag-popover-content, -.cvat-draw-shape-popover-content { +.cvat-popover-content { padding: $grid-unit-size; border-radius: $grid-unit-size; background: $background-color-2; @@ -278,6 +277,22 @@ > div { margin-top: $grid-unit-size; } +} + +.cvat-setup-tag-popover-content { + @extend .cvat-popover-content; + + .ant-select { + width: 27 * $grid-unit-size; + } + + > div:last-child { + margin-bottom: $grid-unit-size; + } +} + +.cvat-draw-shape-popover-content { + @extend .cvat-popover-content; > div:nth-child(3) > div > div { width: 100%; diff --git a/cvat-ui/src/components/annotation-page/standard3D-workspace/standard3D-workspace.tsx b/cvat-ui/src/components/annotation-page/standard3D-workspace/standard3D-workspace.tsx index b9394fd2..3273bcfa 100644 --- a/cvat-ui/src/components/annotation-page/standard3D-workspace/standard3D-workspace.tsx +++ b/cvat-ui/src/components/annotation-page/standard3D-workspace/standard3D-workspace.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2021 Intel Corporation +// Copyright (C) 2021-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -13,6 +13,7 @@ import ObjectsListContainer from 'containers/annotation-page/standard-workspace/ import PropagateConfirmContainer from 'containers/annotation-page/standard-workspace/propagate-confirm'; import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu'; import CanvasPointContextMenuComponent from 'components/annotation-page/canvas/canvas-point-context-menu'; +import RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm'; export default function StandardWorkspace3DComponent(): JSX.Element { return ( @@ -23,6 +24,7 @@ export default function StandardWorkspace3DComponent(): JSX.Element { + ); } diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/frame-tags.tsx b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/frame-tags.tsx new file mode 100644 index 00000000..f6f14a36 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/frame-tags.tsx @@ -0,0 +1,79 @@ +// Copyright (C) 2022 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useState, useEffect } from 'react'; +import Tag from 'antd/lib/tag'; +import { connect } from 'react-redux'; +import { Action } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; + +import { + removeObject as removeObjectAction, +} from 'actions/annotation-actions'; +import { CombinedState, ObjectType } from 'reducers/interfaces'; + +interface StateToProps { + states: any[]; +} + +interface DispatchToProps { + removeObject(objectState: any): void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { + annotation: { + annotations: { states }, + }, + } = state; + + return { + states, + }; +} + +function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps { + return { + removeObject(objectState: any): void { + dispatch(removeObjectAction(objectState, false)); + }, + }; +} + +function FrameTags(props: StateToProps & DispatchToProps): JSX.Element { + const { + states, + removeObject, + } = props; + + const [frameTags, setFrameTags] = useState([] as any[]); + + const onRemoveState = (objectState: any): void => { + removeObject(objectState); + }; + + useEffect(() => { + setFrameTags(states.filter((objectState: any): boolean => objectState.objectType === ObjectType.TAG)); + }, [states]); + + return ( + <> + {frameTags.map((tag: any) => ( + { + onRemoveState(tag); + }} + key={tag.clientID} + closable + > + {tag.label.name} + + ))} + + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(FrameTags); diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss index a7c18550..dc0e1ebd 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -10,42 +10,79 @@ .cvat-tag-annotation-sidebar:not(.ant-layout-sider-collapsed) { background: $background-color-2; - padding: 5px; - - > div > .ant-row > .ant-col > .ant-tag { - margin: 4px; - } + padding: $grid-unit-size * 0.5; + padding-left: $grid-unit-size * 1.25; + overflow-y: auto; } .cvat-tag-annotation-sidebar-label-select { - padding-top: 40px; - padding-bottom: 15px; + padding-top: $grid-unit-size * 1.25; + padding-bottom: $grid-unit-size * 1.8; > .ant-col > .ant-select { - padding-left: 5px; - width: 230px; + width: $grid-unit-size * 25; } } .cvat-tag-annotation-sidebar-shortcut-help { - padding-top: 15px; + padding-top: $grid-unit-size * 1.8; text-align: center; } -.cvat-tag-annotation-sidebar-buttons, .cvat-tag-annotation-sidebar-checkbox-skip-frame { - padding-bottom: 15px; + padding-bottom: $grid-unit-size * 1.8; } .cvat-tag-annotation-label-selects { - padding-top: 10px; + padding-top: $grid-unit-size * 1.25; .ant-select { - width: 230px; - padding: 5px 10px; + width: $grid-unit-size * 29; + margin: $grid-unit-size * 0.5 0; + } + + .cvat-tag-annotation-shortcut-key { + margin-left: $grid-unit-size * 1.25; } } .labels-tag-annotation-sidebar-not-found-wrapper { margin-top: $grid-unit-size * 4; } + +.cvat-frame-tags { + .ant-tag { + margin: $grid-unit-size * 0.25; + display: inline-flex; + justify-content: center; + align-items: center; + + .ant-tag-close-icon { + margin-left: $grid-unit-size * 0.5; + font-size: $grid-unit-size * 1.5; + } + } +} + +.cvat-canvas-frame-tags { + @extend .cvat-frame-tags; + + position: absolute; + top: $layout-sm-grid-size; + left: $grid-unit-size; + z-index: 3; + + .ant-tag { + user-select: none; + } +} + +.cvat-tag-annotation-sidebar-tag-label { + margin-top: $grid-unit-size * 1.8; +} + +.cvat-add-tag-button { + margin-left: $grid-unit-size * 1.25; + width: $grid-unit-size * 3.5; + height: $grid-unit-size * 3.5; +} diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/shortcuts-select.tsx b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/shortcuts-select.tsx index 1c495a15..d176181c 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/shortcuts-select.tsx +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/shortcuts-select.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -8,7 +8,7 @@ import { Row, Col } from 'antd/lib/grid'; import Text from 'antd/lib/typography/Text'; import Select from 'antd/lib/select'; -import { CombinedState } from 'reducers/interfaces'; +import { CombinedState, DimensionType } from 'reducers/interfaces'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import { shift } from 'utils/math'; @@ -17,7 +17,7 @@ interface ShortcutLabelMap { } type Props = { - onAddTag(labelID: number): void; + onShortcutPress(event: KeyboardEvent | undefined, labelID: number): void; }; const defaultShortcutLabelMap = { @@ -34,7 +34,7 @@ const defaultShortcutLabelMap = { } as ShortcutLabelMap; const ShortcutsSelect = (props: Props): JSX.Element => { - const { onAddTag } = props; + const { onShortcutPress } = props; const { labels } = useSelector((state: CombinedState) => state.annotation.job); const [shortcutLabelMap, setShortcutLabelMap] = useState(defaultShortcutLabelMap); @@ -60,16 +60,16 @@ const ShortcutsSelect = (props: Props): JSX.Element => { keyMap[key] = { name: `Setup ${label.name} tag`, description: `Setup tag with "${label.name}" label`, - sequences: [`${id}`], + sequences: [`${id}`, `shift+${id}`], action: 'keydown', + applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D], }; handlers[key] = (event: KeyboardEvent | undefined) => { if (event) { event.preventDefault(); } - - onAddTag(label.id); + onShortcutPress(event, label.id); }; }); @@ -92,7 +92,6 @@ const ShortcutsSelect = (props: Props): JSX.Element => { .map((id) => ( - {`Key ${id}:`} + {`Key ${id}`} ))} diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx index 0aba3dfd..03621033 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx @@ -7,16 +7,17 @@ import { connect } from 'react-redux'; import { Action } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import { Row, Col } from 'antd/lib/grid'; -import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; +import { + MenuFoldOutlined, MenuUnfoldOutlined, PlusOutlined, +} from '@ant-design/icons'; import Layout, { SiderProps } from 'antd/lib/layout'; import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox/Checkbox'; import Button from 'antd/lib/button/button'; import Text from 'antd/lib/typography/Text'; -import Tag from 'antd/lib/tag'; import { createAnnotationsAsync, - removeObjectAsync, + removeObject as removeObjectAction, changeFrameAsync, rememberObject, } from 'actions/annotation-actions'; @@ -44,7 +45,7 @@ interface StateToProps { } interface DispatchToProps { - removeObject(jobInstance: any, objectState: any): void; + removeObject(objectState: any): void; createAnnotations(jobInstance: any, frame: number, objectStates: any[]): void; changeFrame(frame: number, fillBuffer?: boolean, frameStep?: number): void; onRememberObject(labelID: number): void; @@ -67,7 +68,7 @@ function mapStateToProps(state: CombinedState): StateToProps { jobInstance, labels, states, - canvasInstance, + canvasInstance: canvasInstance as Canvas | Canvas3d, frameNumber, keyMap, normalizedKeyMap, @@ -83,8 +84,8 @@ function mapDispatchToProps(dispatch: ThunkDispatch): createAnnotations(jobInstance: any, frame: number, objectStates: any[]): void { dispatch(createAnnotationsAsync(jobInstance, frame, objectStates)); }, - removeObject(jobInstance: any, objectState: any): void { - dispatch(removeObjectAsync(jobInstance, objectState, true)); + removeObject(objectState: any): void { + dispatch(removeObjectAction(objectState, false)); }, onRememberObject(labelID: number): void { dispatch(rememberObject({ activeObjectType: ObjectType.TAG, activeLabelID: labelID })); @@ -148,7 +149,8 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen }, []); useEffect(() => { - setFrameTags(states.filter((objectState: any): boolean => objectState.objectType === ObjectType.TAG)); + const tags = states.filter((objectState: any): boolean => objectState.objectType === ObjectType.TAG); + setFrameTags(tags); }, [states]); const siderProps: SiderProps = { @@ -166,8 +168,9 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen setSelectedLabelID(value.id); }; - const onRemoveState = (objectState: any): void => { - removeObject(jobInstance, objectState); + const onRemoveState = (labelID: number): void => { + const objectState = frameTags.find((tag: any): boolean => tag.label.id === labelID); + if (objectState) removeObject(objectState); }; const onChangeFrame = (): void => { @@ -179,17 +182,26 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen }; const onAddTag = (labelID: number): void => { - onRememberObject(labelID); + if (frameTags.every((objectState: any): boolean => objectState.label.id !== labelID)) { + onRememberObject(labelID); - const objectState = new cvat.classes.ObjectState({ - objectType: ObjectType.TAG, - label: labels.filter((label: any) => label.id === labelID)[0], - frame: frameNumber, - }); + const objectState = new cvat.classes.ObjectState({ + objectType: ObjectType.TAG, + label: labels.filter((label: any) => label.id === labelID)[0], + frame: frameNumber, + }); + createAnnotations(jobInstance, frameNumber, [objectState]); - createAnnotations(jobInstance, frameNumber, [objectState]); + if (skipFrame) onChangeFrame(); + } + }; - if (skipFrame) onChangeFrame(); + const onShortcutPress = (event: KeyboardEvent | undefined, labelID: number): void => { + if (event?.shiftKey) { + onRemoveState(labelID); + } else { + onAddTag(labelID); + } }; const subKeyMap = { @@ -199,7 +211,7 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen const handlers = { SWITCH_DRAW_MODE: (event: KeyboardEvent | undefined) => { preventDefault(event); - onAddTag(selectedLabelID); + onShortcutPress(event, selectedLabelID); }, }; @@ -239,18 +251,25 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen > {sidebarCollapsed ? : } - + - Tag label - + Tag label: - - - - - - + + + +