React UI: Added annotation menus, added shape context menu, added some confirmations before dangerous actions (#1123)
* Annotation menu, modified tasks menu * Removed extra styles * Context menu using side panel * Mousewheel on draw * Added more cursor icons * Do not check .svg & .scss by eslintmain
parent
42614c28a1
commit
939de868a9
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
Icon,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
|
||||||
|
function isDefaultFormat(dumperName: string, taskMode: string): boolean {
|
||||||
|
return (dumperName === 'CVAT XML 1.1 for videos' && taskMode === 'interpolation')
|
||||||
|
|| (dumperName === 'CVAT XML 1.1 for images' && taskMode === 'annotation');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
taskMode: string;
|
||||||
|
menuKey: string;
|
||||||
|
dumpers: string[];
|
||||||
|
dumpActivities: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DumpSubmenu(props: Props): JSX.Element {
|
||||||
|
const {
|
||||||
|
taskMode,
|
||||||
|
menuKey,
|
||||||
|
dumpers,
|
||||||
|
dumpActivities,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.SubMenu key={menuKey} title='Dump annotations'>
|
||||||
|
{
|
||||||
|
dumpers.map((dumper: string): JSX.Element => {
|
||||||
|
const pending = (dumpActivities || []).includes(dumper);
|
||||||
|
const isDefault = isDefaultFormat(dumper, taskMode);
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
key={dumper}
|
||||||
|
disabled={pending}
|
||||||
|
className='cvat-menu-dump-submenu-item'
|
||||||
|
>
|
||||||
|
<Icon type='download' />
|
||||||
|
<Text strong={isDefault}>{dumper}</Text>
|
||||||
|
{pending && <Icon style={{ marginLeft: 10 }} type='loading' />}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Menu.SubMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,50 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Menu,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
} from 'antd';
|
|
||||||
|
|
||||||
import Text from 'antd/lib/typography/Text';
|
|
||||||
|
|
||||||
interface DumperItemComponentProps {
|
|
||||||
taskInstance: any;
|
|
||||||
dumper: any;
|
|
||||||
dumpActivity: string | null;
|
|
||||||
onDumpAnnotation: (task: any, dumper: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDefaultFormat(dumperName: string, taskMode: string): boolean {
|
|
||||||
return (dumperName === 'CVAT XML 1.1 for videos' && taskMode === 'interpolation')
|
|
||||||
|| (dumperName === 'CVAT XML 1.1 for images' && taskMode === 'annotation');
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DumperItemComponent(props: DumperItemComponentProps): JSX.Element {
|
|
||||||
const {
|
|
||||||
taskInstance,
|
|
||||||
dumpActivity,
|
|
||||||
} = props;
|
|
||||||
const { mode } = taskInstance;
|
|
||||||
const { dumper } = props;
|
|
||||||
const pending = !!dumpActivity;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu.Item className='cvat-actions-menu-dump-submenu-item' key={dumper.name}>
|
|
||||||
<Button
|
|
||||||
block
|
|
||||||
type='link'
|
|
||||||
disabled={pending}
|
|
||||||
onClick={(): void => {
|
|
||||||
props.onDumpAnnotation(taskInstance, dumper);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon type='download' />
|
|
||||||
<Text strong={isDefaultFormat(dumper.name, mode)}>
|
|
||||||
{dumper.name}
|
|
||||||
</Text>
|
|
||||||
{pending && <Icon type='loading' />}
|
|
||||||
</Button>
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Menu,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
} from 'antd';
|
|
||||||
|
|
||||||
import Text from 'antd/lib/typography/Text';
|
|
||||||
|
|
||||||
interface DumperItemComponentProps {
|
|
||||||
taskInstance: any;
|
|
||||||
exporter: any;
|
|
||||||
exportActivity: string | null;
|
|
||||||
onExportDataset: (task: any, exporter: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DumperItemComponent(props: DumperItemComponentProps): JSX.Element {
|
|
||||||
const {
|
|
||||||
taskInstance,
|
|
||||||
exporter,
|
|
||||||
exportActivity,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const pending = !!exportActivity;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu.Item className='cvat-actions-menu-export-submenu-item' key={exporter.name}>
|
|
||||||
<Button
|
|
||||||
block
|
|
||||||
type='link'
|
|
||||||
disabled={pending}
|
|
||||||
onClick={(): void => {
|
|
||||||
props.onExportDataset(taskInstance, exporter);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon type='export' />
|
|
||||||
<Text strong={exporter.is_default}>
|
|
||||||
{exporter.name}
|
|
||||||
</Text>
|
|
||||||
{pending && <Icon type='loading' />}
|
|
||||||
</Button>
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
Icon,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
menuKey: string;
|
||||||
|
exporters: string[];
|
||||||
|
exportActivities: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExportSubmenu(props: Props): JSX.Element {
|
||||||
|
const {
|
||||||
|
menuKey,
|
||||||
|
exporters,
|
||||||
|
exportActivities,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.SubMenu key={menuKey} title='Export as a dataset'>
|
||||||
|
{
|
||||||
|
exporters.map((exporter: string): JSX.Element => {
|
||||||
|
const pending = (exportActivities || []).includes(exporter);
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
key={exporter}
|
||||||
|
disabled={pending}
|
||||||
|
className='cvat-menu-export-submenu-item'
|
||||||
|
>
|
||||||
|
<Icon type='export' />
|
||||||
|
<Text>{exporter}</Text>
|
||||||
|
{pending && <Icon style={{ marginLeft: 10 }} type='loading' />}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Menu.SubMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
Icon,
|
||||||
|
Upload,
|
||||||
|
Button,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
menuKey: string;
|
||||||
|
loaders: string[];
|
||||||
|
loadActivity: string | null;
|
||||||
|
onFileUpload(file: File): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoadSubmenu(props: Props): JSX.Element {
|
||||||
|
const {
|
||||||
|
menuKey,
|
||||||
|
loaders,
|
||||||
|
loadActivity,
|
||||||
|
onFileUpload,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.SubMenu key={menuKey} title='Upload annotations'>
|
||||||
|
{
|
||||||
|
loaders.map((_loader: string): JSX.Element => {
|
||||||
|
const [loader, accept] = _loader.split('::');
|
||||||
|
const pending = loadActivity === loader;
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
key={loader}
|
||||||
|
disabled={!!loadActivity}
|
||||||
|
className='cvat-menu-load-submenu-item'
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
accept={accept}
|
||||||
|
multiple={false}
|
||||||
|
showUploadList={false}
|
||||||
|
beforeUpload={(file: File): boolean => {
|
||||||
|
onFileUpload(file);
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button block type='link' disabled={!!loadActivity}>
|
||||||
|
<Icon type='upload' />
|
||||||
|
<Text>{loader}</Text>
|
||||||
|
{pending && <Icon style={{ marginLeft: 10 }} type='loading' />}
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Menu.SubMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Menu,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
Upload,
|
|
||||||
} from 'antd';
|
|
||||||
|
|
||||||
import { RcFile } from 'antd/lib/upload';
|
|
||||||
import Text from 'antd/lib/typography/Text';
|
|
||||||
|
|
||||||
interface LoaderItemComponentProps {
|
|
||||||
taskInstance: any;
|
|
||||||
loader: any;
|
|
||||||
loadActivity: string | null;
|
|
||||||
onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LoaderItemComponent(props: LoaderItemComponentProps): JSX.Element {
|
|
||||||
const {
|
|
||||||
loader,
|
|
||||||
loadActivity,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const loadingWithThisLoader = loadActivity
|
|
||||||
&& loadActivity === loader.name
|
|
||||||
? loadActivity : null;
|
|
||||||
|
|
||||||
const pending = !!loadingWithThisLoader;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu.Item className='cvat-actions-menu-load-submenu-item' key={loader.name}>
|
|
||||||
<Upload
|
|
||||||
accept={`.${loader.format}`}
|
|
||||||
multiple={false}
|
|
||||||
showUploadList={false}
|
|
||||||
beforeUpload={(file: RcFile): boolean => {
|
|
||||||
props.onLoadAnnotation(
|
|
||||||
props.taskInstance,
|
|
||||||
loader,
|
|
||||||
file as File,
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button block type='link' disabled={!!loadActivity}>
|
|
||||||
<Icon type='upload' />
|
|
||||||
<Text>{loader.name}</Text>
|
|
||||||
{pending && <Icon type='loading' />}
|
|
||||||
</Button>
|
|
||||||
</Upload>
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
activatedStateID: number | null;
|
||||||
|
visible: boolean;
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CanvasContextMenu(props: Props): JSX.Element | null {
|
||||||
|
const {
|
||||||
|
activatedStateID,
|
||||||
|
visible,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (!visible || activatedStateID === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<div className='cvat-canvas-context-menu' style={{ top, left }}>
|
||||||
|
<ObjectItemContainer clientID={activatedStateID} />
|
||||||
|
</div>,
|
||||||
|
window.document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Menu, Modal,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
import { ClickParam } from 'antd/lib/menu/index';
|
||||||
|
|
||||||
|
import DumpSubmenu from 'components/actions-menu/dump-submenu';
|
||||||
|
import LoadSubmenu from 'components/actions-menu/load-submenu';
|
||||||
|
import ExportSubmenu from 'components/actions-menu/export-submenu';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
taskMode: string;
|
||||||
|
loaders: string[];
|
||||||
|
dumpers: string[];
|
||||||
|
exporters: string[];
|
||||||
|
loadActivity: string | null;
|
||||||
|
dumpActivities: string[] | null;
|
||||||
|
exportActivities: string[] | null;
|
||||||
|
onClickMenu(params: ClickParam, file?: File): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Actions {
|
||||||
|
DUMP_TASK_ANNO = 'dump_task_anno',
|
||||||
|
LOAD_JOB_ANNO = 'load_job_anno',
|
||||||
|
EXPORT_TASK_DATASET = 'export_task_dataset',
|
||||||
|
REMOVE_ANNO = 'remove_anno',
|
||||||
|
OPEN_TASK = 'open_task',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AnnotationMenuComponent(props: Props): JSX.Element {
|
||||||
|
const {
|
||||||
|
taskMode,
|
||||||
|
loaders,
|
||||||
|
dumpers,
|
||||||
|
exporters,
|
||||||
|
onClickMenu,
|
||||||
|
loadActivity,
|
||||||
|
dumpActivities,
|
||||||
|
exportActivities,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
let latestParams: ClickParam | null = null;
|
||||||
|
function onClickMenuWrapper(params: ClickParam | null, file?: File): void {
|
||||||
|
const copyParams = params || latestParams;
|
||||||
|
if (!copyParams) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
latestParams = params;
|
||||||
|
|
||||||
|
if (copyParams.keyPath.length === 2) {
|
||||||
|
const [, action] = copyParams.keyPath;
|
||||||
|
if (action === Actions.LOAD_JOB_ANNO) {
|
||||||
|
if (file) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: 'Current annotation will be lost',
|
||||||
|
content: 'You are going to upload new annotations to this job. Continue?',
|
||||||
|
onOk: () => {
|
||||||
|
onClickMenu(copyParams, file);
|
||||||
|
},
|
||||||
|
okButtonProps: {
|
||||||
|
type: 'danger',
|
||||||
|
},
|
||||||
|
okText: 'Update',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onClickMenu(copyParams);
|
||||||
|
}
|
||||||
|
} else if (copyParams.key === Actions.REMOVE_ANNO) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: 'All annotations will be removed',
|
||||||
|
content: 'You are goung to remove all annotations from the client. '
|
||||||
|
+ 'It will stay on the server till you save a job. Continue?',
|
||||||
|
onOk: () => {
|
||||||
|
onClickMenu(copyParams);
|
||||||
|
},
|
||||||
|
okButtonProps: {
|
||||||
|
type: 'danger',
|
||||||
|
},
|
||||||
|
okText: 'Delete',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onClickMenu(copyParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu onClick={onClickMenuWrapper} className='cvat-annotation-menu' selectable={false}>
|
||||||
|
{
|
||||||
|
DumpSubmenu({
|
||||||
|
taskMode,
|
||||||
|
dumpers,
|
||||||
|
dumpActivities,
|
||||||
|
menuKey: Actions.DUMP_TASK_ANNO,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{
|
||||||
|
LoadSubmenu({
|
||||||
|
loaders,
|
||||||
|
loadActivity,
|
||||||
|
onFileUpload: (file: File): void => {
|
||||||
|
onClickMenuWrapper(null, file);
|
||||||
|
},
|
||||||
|
menuKey: Actions.LOAD_JOB_ANNO,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ExportSubmenu({
|
||||||
|
exporters,
|
||||||
|
exportActivities,
|
||||||
|
menuKey: Actions.EXPORT_TASK_DATASET,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<Menu.Item key={Actions.REMOVE_ANNO}>
|
||||||
|
Remove annotations
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key={Actions.OPEN_TASK}>
|
||||||
|
Open the task
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,189 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { CombinedState } from 'reducers/interfaces';
|
||||||
|
|
||||||
|
import CanvasContextMenuComponent from 'components/annotation-page/standard-workspace/canvas-context-menu';
|
||||||
|
|
||||||
|
interface StateToProps {
|
||||||
|
activatedStateID: number | null;
|
||||||
|
visible: boolean;
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
collapsed: boolean | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: CombinedState): StateToProps {
|
||||||
|
const {
|
||||||
|
annotation: {
|
||||||
|
annotations: {
|
||||||
|
activatedStateID,
|
||||||
|
collapsed,
|
||||||
|
},
|
||||||
|
canvas: {
|
||||||
|
contextMenu: {
|
||||||
|
visible,
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
activatedStateID,
|
||||||
|
collapsed: activatedStateID !== null ? collapsed[activatedStateID] : undefined,
|
||||||
|
visible,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = StateToProps;
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
latestLeft: number;
|
||||||
|
latestTop: number;
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CanvasContextMenuContainer extends React.PureComponent<Props, State> {
|
||||||
|
private initialized: HTMLDivElement | null;
|
||||||
|
private dragging: boolean;
|
||||||
|
private dragInitPosX: number;
|
||||||
|
private dragInitPosY: number;
|
||||||
|
public constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.initialized = null;
|
||||||
|
this.dragging = false;
|
||||||
|
this.dragInitPosX = 0;
|
||||||
|
this.dragInitPosY = 0;
|
||||||
|
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 componentDidMount(): void {
|
||||||
|
this.updatePositionIfOutOfScreen();
|
||||||
|
window.addEventListener('mousemove', this.moveContextMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: Props): void {
|
||||||
|
const { collapsed } = this.props;
|
||||||
|
|
||||||
|
const [element] = window.document.getElementsByClassName('cvat-canvas-context-menu');
|
||||||
|
if (collapsed !== prevProps.collapsed && element) {
|
||||||
|
element.addEventListener('transitionend', () => {
|
||||||
|
this.updatePositionIfOutOfScreen();
|
||||||
|
}, { once: true });
|
||||||
|
} else if (element) {
|
||||||
|
this.updatePositionIfOutOfScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element && (!this.initialized || this.initialized !== element)) {
|
||||||
|
this.initialized = element as HTMLDivElement;
|
||||||
|
|
||||||
|
this.initialized.addEventListener('mousedown', (e: MouseEvent): any => {
|
||||||
|
this.dragging = true;
|
||||||
|
this.dragInitPosX = e.clientX;
|
||||||
|
this.dragInitPosY = e.clientY;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initialized.addEventListener('mouseup', () => {
|
||||||
|
this.dragging = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount(): void {
|
||||||
|
window.removeEventListener('mousemove', this.moveContextMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
private moveContextMenu = (e: MouseEvent): void => {
|
||||||
|
if (this.dragging) {
|
||||||
|
this.setState((state) => {
|
||||||
|
const value = {
|
||||||
|
left: state.left + e.clientX - this.dragInitPosX,
|
||||||
|
top: state.top + e.clientY - this.dragInitPosY,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.dragInitPosX = e.clientX;
|
||||||
|
this.dragInitPosY = e.clientY;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private updatePositionIfOutOfScreen(): void {
|
||||||
|
const {
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const {
|
||||||
|
innerWidth,
|
||||||
|
innerHeight,
|
||||||
|
} = window;
|
||||||
|
|
||||||
|
const [element] = window.document.getElementsByClassName('cvat-canvas-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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
const {
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const {
|
||||||
|
visible,
|
||||||
|
activatedStateID,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CanvasContextMenuComponent
|
||||||
|
left={left}
|
||||||
|
top={top}
|
||||||
|
visible={visible}
|
||||||
|
activatedStateID={activatedStateID}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
)(CanvasContextMenuContainer);
|
||||||
@ -0,0 +1,166 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { withRouter, RouteComponentProps } from 'react-router';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { ClickParam } from 'antd/lib/menu/index';
|
||||||
|
|
||||||
|
import { CombinedState } from 'reducers/interfaces';
|
||||||
|
import AnnotationMenuComponent, { Actions } from 'components/annotation-page/top-bar/annotation-menu';
|
||||||
|
|
||||||
|
import {
|
||||||
|
dumpAnnotationsAsync,
|
||||||
|
exportDatasetAsync,
|
||||||
|
} from 'actions/tasks-actions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
uploadJobAnnotationsAsync,
|
||||||
|
removeAnnotationsAsync,
|
||||||
|
} from 'actions/annotation-actions';
|
||||||
|
|
||||||
|
interface StateToProps {
|
||||||
|
annotationFormats: any[];
|
||||||
|
exporters: any[];
|
||||||
|
jobInstance: any;
|
||||||
|
loadActivity: string | null;
|
||||||
|
dumpActivities: string[] | null;
|
||||||
|
exportActivities: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchToProps {
|
||||||
|
loadAnnotations(job: any, loader: any, file: File): void;
|
||||||
|
dumpAnnotations(task: any, dumper: any): void;
|
||||||
|
exportDataset(task: any, exporter: any): void;
|
||||||
|
removeAnnotations(sessionInstance: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: CombinedState): StateToProps {
|
||||||
|
const {
|
||||||
|
annotation: {
|
||||||
|
activities: {
|
||||||
|
loads: jobLoads,
|
||||||
|
},
|
||||||
|
job: {
|
||||||
|
instance: jobInstance,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
formats: {
|
||||||
|
annotationFormats,
|
||||||
|
datasetFormats: exporters,
|
||||||
|
},
|
||||||
|
tasks: {
|
||||||
|
activities: {
|
||||||
|
dumps,
|
||||||
|
loads,
|
||||||
|
exports: activeExports,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = state;
|
||||||
|
|
||||||
|
const taskID = jobInstance.task.id;
|
||||||
|
const jobID = jobInstance.id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
dumpActivities: taskID in dumps ? dumps[taskID] : null,
|
||||||
|
exportActivities: taskID in activeExports ? activeExports[taskID] : null,
|
||||||
|
loadActivity: taskID in loads || jobID in jobLoads
|
||||||
|
? loads[taskID] || jobLoads[jobID] : null,
|
||||||
|
jobInstance,
|
||||||
|
annotationFormats,
|
||||||
|
exporters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||||
|
return {
|
||||||
|
loadAnnotations(job: any, loader: any, file: File): void {
|
||||||
|
dispatch(uploadJobAnnotationsAsync(job, loader, file));
|
||||||
|
},
|
||||||
|
dumpAnnotations(task: any, dumper: any): void {
|
||||||
|
dispatch(dumpAnnotationsAsync(task, dumper));
|
||||||
|
},
|
||||||
|
exportDataset(task: any, exporter: any): void {
|
||||||
|
dispatch(exportDatasetAsync(task, exporter));
|
||||||
|
},
|
||||||
|
removeAnnotations(sessionInstance: any): void {
|
||||||
|
dispatch(removeAnnotationsAsync(sessionInstance));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = StateToProps & DispatchToProps & RouteComponentProps;
|
||||||
|
|
||||||
|
function AnnotationMenuContainer(props: Props): JSX.Element {
|
||||||
|
const {
|
||||||
|
jobInstance,
|
||||||
|
annotationFormats,
|
||||||
|
exporters,
|
||||||
|
loadAnnotations,
|
||||||
|
dumpAnnotations,
|
||||||
|
exportDataset,
|
||||||
|
removeAnnotations,
|
||||||
|
history,
|
||||||
|
loadActivity,
|
||||||
|
dumpActivities,
|
||||||
|
exportActivities,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const loaders = annotationFormats
|
||||||
|
.map((format: any): any[] => format.loaders).flat();
|
||||||
|
|
||||||
|
const dumpers = annotationFormats
|
||||||
|
.map((format: any): any[] => format.dumpers).flat();
|
||||||
|
|
||||||
|
const onClickMenu = (params: ClickParam, file?: File): void => {
|
||||||
|
if (params.keyPath.length > 1) {
|
||||||
|
const [additionalKey, action] = params.keyPath;
|
||||||
|
if (action === Actions.DUMP_TASK_ANNO) {
|
||||||
|
const format = additionalKey;
|
||||||
|
const [dumper] = dumpers
|
||||||
|
.filter((_dumper: any): boolean => _dumper.name === format);
|
||||||
|
if (dumper) {
|
||||||
|
dumpAnnotations(jobInstance.task, dumper);
|
||||||
|
}
|
||||||
|
} else if (action === Actions.LOAD_JOB_ANNO) {
|
||||||
|
const [format] = additionalKey.split('::');
|
||||||
|
const [loader] = loaders
|
||||||
|
.filter((_loader: any): boolean => _loader.name === format);
|
||||||
|
if (loader && file) {
|
||||||
|
loadAnnotations(jobInstance, loader, file);
|
||||||
|
}
|
||||||
|
} else if (action === Actions.EXPORT_TASK_DATASET) {
|
||||||
|
const format = additionalKey;
|
||||||
|
const [exporter] = exporters
|
||||||
|
.filter((_exporter: any): boolean => _exporter.name === format);
|
||||||
|
if (exporter) {
|
||||||
|
exportDataset(jobInstance.task, exporter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const [action] = params.keyPath;
|
||||||
|
if (action === Actions.REMOVE_ANNO) {
|
||||||
|
removeAnnotations(jobInstance);
|
||||||
|
} else if (action === Actions.OPEN_TASK) {
|
||||||
|
history.push(`/tasks/${jobInstance.task.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnnotationMenuComponent
|
||||||
|
taskMode={jobInstance.task.mode}
|
||||||
|
loaders={loaders.map((loader: any): string => loader.name)}
|
||||||
|
dumpers={dumpers.map((dumper: any): string => dumper.name)}
|
||||||
|
exporters={exporters.map((exporter: any): string => exporter.name)}
|
||||||
|
loadActivity={loadActivity}
|
||||||
|
dumpActivities={dumpActivities}
|
||||||
|
exportActivities={exportActivities}
|
||||||
|
onClickMenu={onClickMenu}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(AnnotationMenuContainer),
|
||||||
|
);
|
||||||
Loading…
Reference in New Issue