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();
}
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);

@ -18,6 +18,7 @@ import {
Task,
FrameSpeed,
Rotation,
ContextMenuType,
Workspace,
} 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 {
type: AnnotationActionTypes.UPDATE_CANVAS_CONTEXT_MENU,
payload: {
visible,
left,
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 { 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<Props> {
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<Props> {
};
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<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 {
const {
activatedStateID,
@ -619,6 +649,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
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 {

@ -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 {
<ObjectSideBarContainer />
<PropagateConfirmContainer />
<CanvasContextMenuContainer />
<CanvasPointContextMenuContainer />
</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 {
position: absolute;
background: $background-color-2;

@ -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<Props, State> {
const {
visible,
activatedStateID,
type,
} = this.props;
return (
<CanvasContextMenuComponent
left={left}
top={top}
visible={visible}
activatedStateID={activatedStateID}
/>
<>
{ type === ContextMenuType.CANVAS_SHAPE && (
<CanvasContextMenuComponent
left={left}
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,
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());

@ -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,
},
},
};

@ -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;

Loading…
Cancel
Save