Merge pull request #1292 from opencv/dk/point-deletion

React UI: Point deletion context menu
main
Dmitry Kalinin 6 years ago committed by GitHub
commit 57e8083943
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -456,6 +456,27 @@ export class CanvasViewImpl implements CanvasView, Listener {
e.preventDefault(); 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) { if (value) {
(shape as any).selectize(value, { (shape as any).selectize(value, {
deepSelect: true, deepSelect: true,
@ -478,6 +499,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}); });
circle.on('dblclick', dblClickHandler); circle.on('dblclick', dblClickHandler);
circle.on('contextmenu', contextmenuHandler);
circle.addClass('cvat_canvas_selected_point'); circle.addClass('cvat_canvas_selected_point');
}); });
@ -487,6 +509,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}); });
circle.off('dblclick', dblClickHandler); circle.off('dblclick', dblClickHandler);
circle.off('contextmenu', contextmenuHandler);
circle.removeClass('cvat_canvas_selected_point'); circle.removeClass('cvat_canvas_selected_point');
}); });
@ -900,7 +923,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.activate(activeElement); 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]) .some((p: number, id: number): boolean => p !== drawnState.points[id])
) { ) {
const translatedPoints: number[] = translate(state.points); const translatedPoints: number[] = translate(state.points);
@ -1185,7 +1208,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
let shapeSizeElement: ShapeSizeElement | null = null; let shapeSizeElement: ShapeSizeElement | null = null;
let resized = false; 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; this.mode = Mode.RESIZE;
if (state.shapeType === 'rectangle') { if (state.shapeType === 'rectangle') {
shapeSizeElement = displayShapeSize(this.adoptedContent, this.adoptedText); shapeSizeElement = displayShapeSize(this.adoptedContent, this.adoptedText);

@ -18,6 +18,7 @@ import {
Task, Task,
FrameSpeed, FrameSpeed,
Rotation, Rotation,
ContextMenuType,
Workspace, Workspace,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
@ -369,13 +370,21 @@ ThunkAction<Promise<void>, {}, {}, 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 { return {
type: AnnotationActionTypes.UPDATE_CANVAS_CONTEXT_MENU, type: AnnotationActionTypes.UPDATE_CANVAS_CONTEXT_MENU,
payload: { payload: {
visible, visible,
left, left,
top, top,
type,
pointID,
}, },
}; };
} }

@ -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(
<div className='cvat-canvas-point-context-menu' style={{ top, left }}>
<Button type='link' icon='delete' onClick={onPointDelete}>
Delete point
</Button>
</div>,
window.document.body,
);
}

@ -4,20 +4,23 @@
import React from 'react'; import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys'; 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 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 { import {
ColorBy, ColorBy,
GridColor, GridColor,
ObjectType, ObjectType,
ContextMenuType,
Workspace, Workspace,
ShapeType,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { LogType } from 'cvat-logger';
import { Canvas } from 'cvat-canvas';
import getCore from 'cvat-core';
const cvat = getCore(); const cvat = getCore();
@ -51,6 +54,8 @@ interface Props {
contrastLevel: number; contrastLevel: number;
saturationLevel: number; saturationLevel: number;
resetZoom: boolean; resetZoom: boolean;
contextVisible: boolean;
contextType: ContextMenuType;
aamZoomMargin: number; aamZoomMargin: number;
workspace: Workspace; workspace: Workspace;
onSetupCanvas: () => void; onSetupCanvas: () => void;
@ -69,7 +74,8 @@ interface Props {
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void; onSplitAnnotations(sessionInstance: any, frame: number, state: any): void;
onActivateObject(activatedStateID: number | null): void; onActivateObject(activatedStateID: number | null): void;
onSelectObjects(selectedStatesID: number[]): 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; onAddZLayer(): void;
onSwitchZLayer(cur: number): void; onSwitchZLayer(cur: number): void;
onChangeBrightnessLevel(level: number): void; onChangeBrightnessLevel(level: number): void;
@ -223,7 +229,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().removeEventListener('canvas.drawn', this.onCanvasShapeDrawn); canvasInstance.html().removeEventListener('canvas.drawn', this.onCanvasShapeDrawn);
canvasInstance.html().removeEventListener('canvas.merged', this.onCanvasObjectsMerged); canvasInstance.html().removeEventListener('canvas.merged', this.onCanvasObjectsMerged);
canvasInstance.html().removeEventListener('canvas.groupped', this.onCanvasObjectsGroupped); 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); window.removeEventListener('resize', this.fitCanvas);
} }
@ -327,8 +335,16 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}; };
private onCanvasContextMenu = (e: MouseEvent): void => { private onCanvasContextMenu = (e: MouseEvent): void => {
const { activatedStateID, onUpdateContextMenu } = this.props; const {
onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY); 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 => { private onCanvasShapeDragged = (e: any): void => {
@ -476,6 +492,20 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
}; };
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 { private activateOnCanvas(): void {
const { const {
activatedStateID, activatedStateID,
@ -619,6 +649,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().addEventListener('canvas.merged', this.onCanvasObjectsMerged); canvasInstance.html().addEventListener('canvas.merged', this.onCanvasObjectsMerged);
canvasInstance.html().addEventListener('canvas.groupped', this.onCanvasObjectsGroupped); canvasInstance.html().addEventListener('canvas.groupped', this.onCanvasObjectsGroupped);
canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted); canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted);
canvasInstance.html().addEventListener('point.contextmenu', this.onCanvasPointContextMenu);
} }
public render(): JSX.Element { public render(): JSX.Element {

@ -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 ObjectSideBarContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar';
import PropagateConfirmContainer from 'containers/annotation-page/standard-workspace/propagate-confirm'; import PropagateConfirmContainer from 'containers/annotation-page/standard-workspace/propagate-confirm';
import CanvasContextMenuContainer from 'containers/annotation-page/standard-workspace/canvas-context-menu'; 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 { export default function StandardWorkspaceComponent(): JSX.Element {
return ( return (
@ -23,6 +24,7 @@ export default function StandardWorkspaceComponent(): JSX.Element {
<ObjectSideBarContainer /> <ObjectSideBarContainer />
<PropagateConfirmContainer /> <PropagateConfirmContainer />
<CanvasContextMenuContainer /> <CanvasContextMenuContainer />
<CanvasPointContextMenuContainer />
</Layout> </Layout>
); );
} }

@ -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 { .cvat-canvas-z-axis-wrapper {
position: absolute; position: absolute;
background: $background-color-2; background: $background-color-2;

@ -5,7 +5,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; 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'; import CanvasContextMenuComponent from 'components/annotation-page/standard-workspace/canvas-context-menu';
@ -14,6 +14,7 @@ interface StateToProps {
visible: boolean; visible: boolean;
top: number; top: number;
left: number; left: number;
type: ContextMenuType;
collapsed: boolean | undefined; collapsed: boolean | undefined;
} }
@ -29,6 +30,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
visible, visible,
top, top,
left, left,
type,
}, },
}, },
}, },
@ -40,6 +42,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
visible, visible,
left, left,
top, top,
type,
}; };
} }
@ -175,15 +178,20 @@ class CanvasContextMenuContainer extends React.PureComponent<Props, State> {
const { const {
visible, visible,
activatedStateID, activatedStateID,
type,
} = this.props; } = this.props;
return ( return (
<CanvasContextMenuComponent <>
left={left} { type === ContextMenuType.CANVAS_SHAPE && (
top={top} <CanvasContextMenuComponent
visible={visible} left={left}
activatedStateID={activatedStateID} top={top}
/> visible={visible}
activatedStateID={activatedStateID}
/>
)}
</>
); );
} }
} }

@ -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<Props, State> {
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 && (
<CanvasPointContextMenuComponent
left={left}
top={top}
visible={visible}
activatedStateID={activatedStateID}
onPointDelete={() => this.deletePoint()}
/>
)}
</>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(CanvasPointContextMenuContainer);

@ -39,6 +39,7 @@ import {
GridColor, GridColor,
ObjectType, ObjectType,
CombinedState, CombinedState,
ContextMenuType,
Workspace, Workspace,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
@ -74,6 +75,8 @@ interface StateToProps {
minZLayer: number; minZLayer: number;
maxZLayer: number; maxZLayer: number;
curZLayer: number; curZLayer: number;
contextVisible: boolean;
contextType: ContextMenuType;
} }
interface DispatchToProps { interface DispatchToProps {
@ -93,7 +96,8 @@ interface DispatchToProps {
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void; onSplitAnnotations(sessionInstance: any, frame: number, state: any): void;
onActivateObject: (activatedStateID: number | null) => void; onActivateObject: (activatedStateID: number | null) => void;
onSelectObjects: (selectedStatesID: number[]) => 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; onAddZLayer(): void;
onSwitchZLayer(cur: number): void; onSwitchZLayer(cur: number): void;
onChangeBrightnessLevel(level: number): void; onChangeBrightnessLevel(level: number): void;
@ -108,6 +112,10 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { const {
annotation: { annotation: {
canvas: { canvas: {
contextMenu: {
visible: contextVisible,
type: contextType,
},
instance: canvasInstance, instance: canvasInstance,
}, },
drawing: { drawing: {
@ -190,6 +198,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
curZLayer, curZLayer,
minZLayer, minZLayer,
maxZLayer, maxZLayer,
contextVisible,
contextType,
workspace, workspace,
}; };
} }
@ -248,8 +258,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSelectObjects(selectedStatesID: number[]): void { onSelectObjects(selectedStatesID: number[]): void {
dispatch(selectObjects(selectedStatesID)); dispatch(selectObjects(selectedStatesID));
}, },
onUpdateContextMenu(visible: boolean, left: number, top: number): void { onUpdateContextMenu(visible: boolean, left: number, top: number,
dispatch(updateCanvasContextMenu(visible, left, top)); type: ContextMenuType, pointID?: number): void {
dispatch(updateCanvasContextMenu(visible, left, top, pointID, type));
}, },
onAddZLayer(): void { onAddZLayer(): void {
dispatch(addZLayer()); dispatch(addZLayer());

@ -13,6 +13,7 @@ import {
ActiveControl, ActiveControl,
ShapeType, ShapeType,
ObjectType, ObjectType,
ContextMenuType,
Workspace, Workspace,
} from './interfaces'; } from './interfaces';
@ -25,6 +26,8 @@ const defaultState: AnnotationState = {
visible: false, visible: false,
left: 0, left: 0,
top: 0, top: 0,
type: ContextMenuType.CANVAS_SHAPE,
pointID: null,
}, },
instance: new Canvas(), instance: new Canvas(),
ready: false, ready: false,
@ -928,6 +931,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
visible, visible,
left, left,
top, top,
type,
pointID,
} = action.payload; } = action.payload;
return { return {
@ -939,6 +944,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
visible, visible,
left, left,
top, top,
type,
pointID,
}, },
}, },
}; };

@ -286,6 +286,7 @@ export enum StatesOrdering {
export enum ContextMenuType { export enum ContextMenuType {
CANVAS = 'canvas', CANVAS = 'canvas',
CANVAS_SHAPE = 'canvas_shape', CANVAS_SHAPE = 'canvas_shape',
CANVAS_SHAPE_POINT = 'canvas_shape_point',
} }
export enum Rotation { export enum Rotation {
@ -305,6 +306,8 @@ export interface AnnotationState {
visible: boolean; visible: boolean;
top: number; top: number;
left: number; left: number;
type: ContextMenuType;
pointID: number | null;
}; };
instance: Canvas; instance: Canvas;
ready: boolean; ready: boolean;

Loading…
Cancel
Save