From 845be3b48601cb72664a0db8ae9e66f1dfff16bb Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Thu, 19 Mar 2020 02:03:42 +0300 Subject: [PATCH] Added point deletion context menu --- cvat-ui/src/actions/annotation-actions.ts | 2 + .../canvas-point-context-menu.tsx | 9 +- .../standard-workspace/canvas-wrapper.tsx | 36 +++- .../standard-workspace/standard-workspace.tsx | 2 + .../standard-workspace/styles.scss | 4 +- .../canvas-context-menu.tsx | 17 +- .../canvas-point-context-menu.tsx | 180 ++++++++++++++++++ .../standard-workspace/canvas-wrapper.tsx | 13 +- cvat-ui/src/reducers/annotation-reducer.ts | 3 + cvat-ui/src/reducers/interfaces.ts | 1 + 10 files changed, 240 insertions(+), 27 deletions(-) create mode 100644 cvat-ui/src/containers/annotation-page/standard-workspace/canvas-point-context-menu.tsx diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 2652cee3..4041973d 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -318,6 +318,7 @@ export function updateCanvasContextMenu( left: number, top: number, type?: ContextMenuType, + pointID?: number, ): AnyAction { return { type: AnnotationActionTypes.UPDATE_CANVAS_CONTEXT_MENU, @@ -326,6 +327,7 @@ export function updateCanvasContextMenu( left, top, type, + pointID, }, }; } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-point-context-menu.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-point-context-menu.tsx index 4ce488f3..b2cf338d 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-point-context-menu.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-point-context-menu.tsx @@ -5,16 +5,21 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { + Button, +} from 'antd'; interface Props { activatedStateID: number | null; visible: boolean; left: number; top: number; + onPointDelete(): void; } export default function CanvasPointContextMenu(props: Props): JSX.Element | null { const { + onPointDelete, activatedStateID, visible, left, @@ -27,7 +32,9 @@ export default function CanvasPointContextMenu(props: Props): JSX.Element | null return ReactDOM.createPortal(
- Haha +
, window.document.body, ); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index f74e2c2c..0aedbdb0 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -18,16 +18,10 @@ import { GridColor, ObjectType, ContextMenuType, - Workspace + Workspace, } from 'reducers/interfaces'; import { Canvas } from 'cvat-canvas'; import getCore from 'cvat-core'; -import { - ColorBy, - GridColor, - ObjectType, - Workspace, -} from 'reducers/interfaces'; const cvat = getCore(); @@ -81,7 +75,8 @@ interface Props { onSplitAnnotations(sessionInstance: any, frame: number, state: any): void; onActivateObject(activatedStateID: number | null): void; onSelectObjects(selectedStatesID: number[]): void; - onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType): void; + onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, + pointID?: number): void; onAddZLayer(): void; onSwitchZLayer(cur: number): void; onChangeBrightnessLevel(level: number): void; @@ -333,8 +328,17 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; private onCanvasContextMenu = (e: MouseEvent): void => { - const { activatedStateID, onUpdateContextMenu } = this.props; - onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY); + const { + activatedStateID, + onUpdateContextMenu, + contextVisible, + contextType, + } = this.props; + + if (!contextVisible && contextType !== ContextMenuType.CANVAS_SHAPE_POINT) { + onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY, + ContextMenuType.CANVAS_SHAPE); + } }; private onCanvasShapeClicked = (e: any): void => { @@ -460,6 +464,16 @@ export default class CanvasWrapperComponent extends React.PureComponent { } }; + private onCanvasPointContextMenu = (e: any): void => { + const { + activatedStateID, + onUpdateContextMenu, + } = this.props; + + onUpdateContextMenu(activatedStateID !== null, e.detail.mouseEvent.clientX, + e.detail.mouseEvent.clientY, ContextMenuType.CANVAS_SHAPE_POINT, e.detail.pointID); + }; + private activateOnCanvas(): void { const { activatedStateID, @@ -599,6 +613,8 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().addEventListener('canvas.merged', this.onCanvasObjectsMerged); canvasInstance.html().addEventListener('canvas.groupped', this.onCanvasObjectsGroupped); canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted); + + canvasInstance.html().addEventListener('point.contextmenu', this.onCanvasPointContextMenu); } public render(): JSX.Element { 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 e9bfd6c4..5cfcba47 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 @@ -14,6 +14,7 @@ import ControlsSideBarContainer from 'containers/annotation-page/standard-worksp import ObjectSideBarContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; import PropagateConfirmContainer from 'containers/annotation-page/standard-workspace/propagate-confirm'; import CanvasContextMenuContainer from 'containers/annotation-page/standard-workspace/canvas-context-menu'; +import CanvasPointContextMenuContainer from 'containers/annotation-page/standard-workspace/canvas-point-context-menu'; export default function StandardWorkspaceComponent(): JSX.Element { return ( @@ -23,6 +24,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 5f276eb5..1dd7d769 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss @@ -136,10 +136,12 @@ .cvat-canvas-point-context-menu { opacity: 0.6; position: fixed; - width: 100px; + width: 135px; z-index: 10; max-height: 50%; overflow-y: auto; + background-color: #ffffff; + border-radius: 4px; &:hover { opacity: 1; diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-context-menu.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-context-menu.tsx index 7bd7b9fb..3d0a508c 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-context-menu.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-context-menu.tsx @@ -8,7 +8,6 @@ import { connect } from 'react-redux'; import { CombinedState, ContextMenuType } from 'reducers/interfaces'; import CanvasContextMenuComponent from 'components/annotation-page/standard-workspace/canvas-context-menu'; -import CanvasPointContextMenuComponent from 'components/annotation-page/standard-workspace/canvas-point-context-menu'; interface StateToProps { activatedStateID: number | null; @@ -183,12 +182,16 @@ class CanvasContextMenuContainer extends React.PureComponent { } = this.props; return ( - + <> + { type === ContextMenuType.CANVAS_SHAPE && ( + + )} + ); } } diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-point-context-menu.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-point-context-menu.tsx new file mode 100644 index 00000000..74d07ff8 --- /dev/null +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-point-context-menu.tsx @@ -0,0 +1,180 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; + +import { connect } from 'react-redux'; +import { CombinedState, ContextMenuType } from 'reducers/interfaces'; + +import { updateAnnotationsAsync, updateCanvasContextMenu } from 'actions/annotation-actions'; + +import CanvasPointContextMenuComponent from 'components/annotation-page/standard-workspace/canvas-point-context-menu'; + +interface StateToProps { + activatedStateID: number | null; + activetedPointID: number | null | undefined; + states: any[]; + visible: boolean; + top: number; + left: number; + type: ContextMenuType; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { + annotation: { + annotations: { + states, + activatedStateID, + }, + canvas: { + contextMenu: { + visible, + top, + left, + type, + pointID: activetedPointID, + }, + }, + }, + } = state; + + return { + activatedStateID, + activetedPointID, + states, + visible, + left, + top, + type, + }; +} + +interface DispatchToProps { + onUpdateAnnotations(states: any[]): void; + onCloseContextMenu(): void; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + onUpdateAnnotations(states: any[]): void { + dispatch(updateAnnotationsAsync(states)); + }, + onCloseContextMenu(): void { + dispatch(updateCanvasContextMenu(false, 0, 0)); + }, + }; +} + +type Props = StateToProps & DispatchToProps; + +interface State { + latestLeft: number; + latestTop: number; + left: number; + top: number; +} + +class CanvasContextMenuContainer extends React.PureComponent { + public constructor(props: Props) { + super(props); + + this.state = { + latestLeft: 0, + latestTop: 0, + left: 0, + top: 0, + }; + } + + static getDerivedStateFromProps(props: Props, state: State): State | null { + if (props.left === state.latestLeft + && props.top === state.latestTop) { + return null; + } + + return { + ...state, + latestLeft: props.left, + latestTop: props.top, + top: props.top, + left: props.left, + }; + } + + public componentDidUpdate(): void { + const { + top, + left, + } = this.state; + + const { + innerWidth, + innerHeight, + } = window; + + const [element] = window.document.getElementsByClassName('cvat-canvas-point-context-menu'); + if (element) { + const height = element.clientHeight; + const width = element.clientWidth; + + if (top + height > innerHeight || left + width > innerWidth) { + this.setState({ + top: top - Math.max(top + height - innerHeight, 0), + left: left - Math.max(left + width - innerWidth, 0), + }); + } + } + } + + private deletePoint(): void { + const { + activetedPointID, + activatedStateID, + states, + onUpdateAnnotations, + onCloseContextMenu, + } = this.props; + + const [objectState] = states.filter((e) => (e.clientID === activatedStateID)); + if (activetedPointID) { + objectState.points = objectState.points.slice(0, activetedPointID * 2) + .concat(objectState.points.slice(activetedPointID * 2 + 2)); + onUpdateAnnotations([objectState]); + onCloseContextMenu(); + } + } + + public render(): JSX.Element { + const { + visible, + activatedStateID, + type, + } = this.props; + + const { + top, + left, + } = this.state; + + return ( + <> + {type === ContextMenuType.CANVAS_SHAPE_POINT && ( + this.deletePoint()} + /> + )} + + ); + } +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(CanvasContextMenuContainer); diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx index 7d99f592..89f0af7e 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -96,7 +96,8 @@ interface DispatchToProps { onSplitAnnotations(sessionInstance: any, frame: number, state: any): void; onActivateObject: (activatedStateID: number | null) => void; onSelectObjects: (selectedStatesID: number[]) => void; - onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType): void; + onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, + pointID?: number): void; onAddZLayer(): void; onSwitchZLayer(cur: number): void; onChangeBrightnessLevel(level: number): void; @@ -257,13 +258,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { onSelectObjects(selectedStatesID: number[]): void { dispatch(selectObjects(selectedStatesID)); }, - onUpdateContextMenu( - visible: boolean, - left: number, - top: number, - type: ContextMenuType, - ): void { - dispatch(updateCanvasContextMenu(visible, left, top, type)); + onUpdateContextMenu(visible: boolean, left: number, top: number, + type: ContextMenuType, pointID?: number): void { + dispatch(updateCanvasContextMenu(visible, left, top, type, pointID)); }, onAddZLayer(): void { dispatch(addZLayer()); diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 70eaf594..fcb791d1 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -26,6 +26,7 @@ const defaultState: AnnotationState = { left: 0, top: 0, type: ContextMenuType.CANVAS_SHAPE, + pointID: null, }, instance: new Canvas(), ready: false, @@ -934,6 +935,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { left, top, type, + pointID, } = action.payload; return { @@ -946,6 +948,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { left, top, type, + pointID, }, }, }; diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 4c308a7b..1b888405 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -303,6 +303,7 @@ export interface AnnotationState { top: number; left: number; type: ContextMenuType; + pointID: number | null | undefined; }; instance: Canvas; ready: boolean;