diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 5db41045..0074f281 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -456,6 +456,27 @@ export class CanvasViewImpl implements CanvasView, Listener { e.preventDefault(); } + function contextmenuHandler(e: MouseEvent): void { + const pointID = Array.prototype.indexOf + .call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target); + if (self.activeElement.clientID !== null) { + const [state] = self.controller.objects + .filter((_state: any): boolean => ( + _state.clientID === self.activeElement.clientID + )); + self.canvas.dispatchEvent(new CustomEvent('point.contextmenu', { + bubbles: false, + cancelable: true, + detail: { + mouseEvent: e, + objectState: state, + pointID, + }, + })); + } + e.preventDefault(); + } + if (value) { (shape as any).selectize(value, { deepSelect: true, @@ -478,6 +499,7 @@ export class CanvasViewImpl implements CanvasView, Listener { }); circle.on('dblclick', dblClickHandler); + circle.on('contextmenu', contextmenuHandler); circle.addClass('cvat_canvas_selected_point'); }); @@ -487,6 +509,7 @@ export class CanvasViewImpl implements CanvasView, Listener { }); circle.off('dblclick', dblClickHandler); + circle.off('contextmenu', contextmenuHandler); circle.removeClass('cvat_canvas_selected_point'); }); @@ -900,7 +923,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.activate(activeElement); } - if (state.points + if (state.points.length !== drawnState.points.length || state.points .some((p: number, id: number): boolean => p !== drawnState.points[id]) ) { const translatedPoints: number[] = translate(state.points); @@ -1185,7 +1208,11 @@ export class CanvasViewImpl implements CanvasView, Listener { let shapeSizeElement: ShapeSizeElement | null = null; let resized = false; - (shape as any).resize().on('resizestart', (): void => { + (shape as any).resize().on('resizestart', (e: any): void => { + if (e.detail.event.detail.event.button === 2) { + e.preventDefault(); + return; + } this.mode = Mode.RESIZE; if (state.shapeType === 'rectangle') { shapeSizeElement = displayShapeSize(this.adoptedContent, this.adoptedText); diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 678323c6..fbb02587 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -18,6 +18,7 @@ import { Task, FrameSpeed, Rotation, + ContextMenuType, Workspace, } from 'reducers/interfaces'; @@ -369,13 +370,21 @@ ThunkAction, {}, {}, AnyAction> { }; } -export function updateCanvasContextMenu(visible: boolean, left: number, top: number): AnyAction { +export function updateCanvasContextMenu( + visible: boolean, + left: number, + top: number, + pointID: number | null = null, + type?: ContextMenuType, +): AnyAction { return { type: AnnotationActionTypes.UPDATE_CANVAS_CONTEXT_MENU, payload: { visible, 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 new file mode 100644 index 00000000..b2cf338d --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-point-context-menu.tsx @@ -0,0 +1,41 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +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, + top, + } = props; + + if (!visible || activatedStateID === null) { + return null; + } + + return ReactDOM.createPortal( +
+ +
, + 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 16bc2e3e..4d55d31b 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 @@ -4,20 +4,23 @@ import React from 'react'; import { GlobalHotKeys, KeyMap } from 'react-hotkeys'; -import Slider, { SliderValue } from 'antd/lib/slider'; -import Layout from 'antd/lib/layout'; -import Icon from 'antd/lib/icon'; + import Tooltip from 'antd/lib/tooltip'; +import Icon from 'antd/lib/icon'; +import Layout from 'antd/lib/layout/layout'; +import Slider, { SliderValue } from 'antd/lib/slider'; -import { LogType } from 'cvat-logger'; -import { Canvas } from 'cvat-canvas'; -import getCore from 'cvat-core'; import { ColorBy, GridColor, ObjectType, + ContextMenuType, Workspace, + ShapeType, } from 'reducers/interfaces'; +import { LogType } from 'cvat-logger'; +import { Canvas } from 'cvat-canvas'; +import getCore from 'cvat-core'; const cvat = getCore(); @@ -51,6 +54,8 @@ interface Props { contrastLevel: number; saturationLevel: number; resetZoom: boolean; + contextVisible: boolean; + contextType: ContextMenuType; aamZoomMargin: number; workspace: Workspace; onSetupCanvas: () => void; @@ -69,7 +74,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): void; + onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, + pointID?: number): void; onAddZLayer(): void; onSwitchZLayer(cur: number): void; onChangeBrightnessLevel(level: number): void; @@ -223,7 +229,9 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().removeEventListener('canvas.drawn', this.onCanvasShapeDrawn); canvasInstance.html().removeEventListener('canvas.merged', this.onCanvasObjectsMerged); canvasInstance.html().removeEventListener('canvas.groupped', this.onCanvasObjectsGroupped); - canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted); + canvasInstance.html().removeEventListener('canvas.splitted', this.onCanvasTrackSplitted); + + canvasInstance.html().removeEventListener('point.contextmenu', this.onCanvasPointContextMenu); window.removeEventListener('resize', this.fitCanvas); } @@ -327,8 +335,16 @@ 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, + contextType, + } = this.props; + + if (contextType !== ContextMenuType.CANVAS_SHAPE_POINT) { + onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY, + ContextMenuType.CANVAS_SHAPE); + } }; private onCanvasShapeDragged = (e: any): void => { @@ -476,6 +492,20 @@ export default class CanvasWrapperComponent extends React.PureComponent { } }; + private onCanvasPointContextMenu = (e: any): void => { + const { + activatedStateID, + onUpdateContextMenu, + annotations, + } = this.props; + + const [state] = annotations.filter((el: any) => (el.clientID === activatedStateID)); + if (state.shapeType !== ShapeType.RECTANGLE) { + onUpdateContextMenu(activatedStateID !== null, e.detail.mouseEvent.clientX, + e.detail.mouseEvent.clientY, ContextMenuType.CANVAS_SHAPE_POINT, e.detail.pointID); + } + }; + private activateOnCanvas(): void { const { activatedStateID, @@ -619,6 +649,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/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss index ac89b303..005ff868 100644 --- a/cvat-ui/src/components/annotation-page/styles.scss +++ b/cvat-ui/src/components/annotation-page/styles.scss @@ -229,6 +229,21 @@ } } +.cvat-canvas-point-context-menu { + opacity: 0.6; + position: fixed; + width: 135px; + z-index: 10; + max-height: 50%; + overflow-y: auto; + background-color: #ffffff; + border-radius: 4px; + + &:hover { + opacity: 1; + } +} + .cvat-canvas-z-axis-wrapper { position: absolute; background: $background-color-2; 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 fd2ff980..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 @@ -5,7 +5,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import { CombinedState } from 'reducers/interfaces'; +import { CombinedState, ContextMenuType } from 'reducers/interfaces'; import CanvasContextMenuComponent from 'components/annotation-page/standard-workspace/canvas-context-menu'; @@ -14,6 +14,7 @@ interface StateToProps { visible: boolean; top: number; left: number; + type: ContextMenuType; collapsed: boolean | undefined; } @@ -29,6 +30,7 @@ function mapStateToProps(state: CombinedState): StateToProps { visible, top, left, + type, }, }, }, @@ -40,6 +42,7 @@ function mapStateToProps(state: CombinedState): StateToProps { visible, left, top, + type, }; } @@ -175,15 +178,20 @@ class CanvasContextMenuContainer extends React.PureComponent { const { visible, activatedStateID, + type, } = 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..5db5363c --- /dev/null +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-point-context-menu.tsx @@ -0,0 +1,193 @@ +// 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; + activatedPointID: number | null; + 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: activatedPointID, + }, + }, + }, + } = state; + + return { + activatedStateID, + activatedPointID, + 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 { + activatedStateID: number | null; + activatedPointID: number | null; + latestLeft: number; + latestTop: number; + left: number; + top: number; +} + +class CanvasPointContextMenuContainer extends React.PureComponent { + public constructor(props: Props) { + super(props); + + this.state = { + activatedStateID: null, + activatedPointID: null, + latestLeft: 0, + latestTop: 0, + left: 0, + top: 0, + }; + } + + static getDerivedStateFromProps(props: Props, state: State): State { + const newState: State = { ...state }; + + if (props.left !== state.latestLeft + || props.top !== state.latestTop) { + newState.latestLeft = props.left; + newState.latestTop = props.top; + newState.top = props.top; + newState.left = props.left; + } + + if (typeof state.activatedStateID !== typeof props.activatedStateID + || typeof state.activatedPointID !== typeof props.activatedPointID) { + newState.activatedStateID = props.activatedStateID; + newState.activatedPointID = props.activatedPointID; + } + + + return newState; + } + + 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 { + states, + onUpdateAnnotations, + onCloseContextMenu, + } = this.props; + + const { + activatedStateID, + activatedPointID, + } = this.state; + + const [objectState] = states.filter((e) => (e.clientID === activatedStateID)); + if (typeof activatedPointID === 'number') { + objectState.points = objectState.points.slice(0, activatedPointID * 2) + .concat(objectState.points.slice(activatedPointID * 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, +)(CanvasPointContextMenuContainer); 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 e9265bfa..75645fc2 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 @@ -39,6 +39,7 @@ import { GridColor, ObjectType, CombinedState, + ContextMenuType, Workspace, } from 'reducers/interfaces'; @@ -74,6 +75,8 @@ interface StateToProps { minZLayer: number; maxZLayer: number; curZLayer: number; + contextVisible: boolean; + contextType: ContextMenuType; } interface DispatchToProps { @@ -93,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): void; + onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, + pointID?: number): void; onAddZLayer(): void; onSwitchZLayer(cur: number): void; onChangeBrightnessLevel(level: number): void; @@ -108,6 +112,10 @@ function mapStateToProps(state: CombinedState): StateToProps { const { annotation: { canvas: { + contextMenu: { + visible: contextVisible, + type: contextType, + }, instance: canvasInstance, }, drawing: { @@ -190,6 +198,8 @@ function mapStateToProps(state: CombinedState): StateToProps { curZLayer, minZLayer, maxZLayer, + contextVisible, + contextType, workspace, }; } @@ -248,8 +258,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { onSelectObjects(selectedStatesID: number[]): void { dispatch(selectObjects(selectedStatesID)); }, - onUpdateContextMenu(visible: boolean, left: number, top: number): void { - dispatch(updateCanvasContextMenu(visible, left, top)); + onUpdateContextMenu(visible: boolean, left: number, top: number, + type: ContextMenuType, pointID?: number): void { + dispatch(updateCanvasContextMenu(visible, left, top, pointID, type)); }, onAddZLayer(): void { dispatch(addZLayer()); diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 3deb1af3..6f13b16f 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -13,6 +13,7 @@ import { ActiveControl, ShapeType, ObjectType, + ContextMenuType, Workspace, } from './interfaces'; @@ -25,6 +26,8 @@ const defaultState: AnnotationState = { visible: false, left: 0, top: 0, + type: ContextMenuType.CANVAS_SHAPE, + pointID: null, }, instance: new Canvas(), ready: false, @@ -928,6 +931,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { visible, left, top, + type, + pointID, } = action.payload; return { @@ -939,6 +944,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { visible, left, top, + type, + pointID, }, }, }; diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 8dbb2720..f2ead557 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -286,6 +286,7 @@ export enum StatesOrdering { export enum ContextMenuType { CANVAS = 'canvas', CANVAS_SHAPE = 'canvas_shape', + CANVAS_SHAPE_POINT = 'canvas_shape_point', } export enum Rotation { @@ -305,6 +306,8 @@ export interface AnnotationState { visible: boolean; top: number; left: number; + type: ContextMenuType; + pointID: number | null; }; instance: Canvas; ready: boolean;