React UI: Added shortcuts (#1230)

main
Boris Sekachev 6 years ago committed by GitHub
parent 8afb5dda2a
commit 78dad73de9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -34,6 +34,10 @@ export class DrawHandlerImpl implements DrawHandler {
private onDrawDone: (data: object, continueDraw?: boolean) => void;
private canvas: SVG.Container;
private text: SVG.Container;
private cursorPosition: {
x: number;
y: number;
};
private crosshair: {
x: SVG.Line;
y: SVG.Line;
@ -96,12 +100,13 @@ export class DrawHandlerImpl implements DrawHandler {
}
private addCrosshair(): void {
const { x, y } = this.cursorPosition;
this.crosshair = {
x: this.canvas.line(0, 0, this.canvas.node.clientWidth, 0).attr({
x: this.canvas.line(0, y, this.canvas.node.clientWidth, y).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
y: this.canvas.line(0, 0, 0, this.canvas.node.clientHeight).attr({
y: this.canvas.line(x, 0, x, this.canvas.node.clientHeight).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
@ -181,7 +186,6 @@ export class DrawHandlerImpl implements DrawHandler {
this.shapeSizeElement.update(this.drawInstance);
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
z_order: Number.MAX_SAFE_INTEGER,
});
}
@ -222,10 +226,6 @@ export class DrawHandlerImpl implements DrawHandler {
}
private drawPolyshape(): void {
this.drawInstance.attr({
z_order: Number.MAX_SAFE_INTEGER,
});
let size = this.drawData.numberOfPoints;
const sizeDecrement = function sizeDecrement(): void {
if (!--size) {
@ -371,18 +371,17 @@ export class DrawHandlerImpl implements DrawHandler {
// Common settings for rectangle and polyshapes
private pasteShape(): void {
this.drawInstance.attr({
z_order: Number.MAX_SAFE_INTEGER,
});
function moveShape(shape: SVG.Shape, x: number, y: number): void {
const bbox = shape.bbox();
shape.move(x - bbox.width / 2, y - bbox.height / 2);
}
this.canvas.on('mousemove.draw', (e: MouseEvent): void => {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
const { x: initialX, y: initialY } = this.cursorPosition;
moveShape(this.drawInstance, initialX, initialY);
const bbox = this.drawInstance.bbox();
this.drawInstance.move(x - bbox.width / 2, y - bbox.height / 2);
this.canvas.on('mousemove.draw', (): void => {
const { x, y } = this.cursorPosition; // was computer in another callback
moveShape(this.drawInstance, x, y);
});
}
@ -429,45 +428,53 @@ export class DrawHandlerImpl implements DrawHandler {
this.pastePolyshape();
}
private pastePoints(points: string): void {
this.drawInstance = (this.canvas as any).polyline(points)
private pastePoints(initialPoints: string): void {
function moveShape(
shape: SVG.PolyLine,
group: SVG.G,
x: number,
y: number,
scale: number,
): void {
const bbox = shape.bbox();
shape.move(x - bbox.width / 2, y - bbox.height / 2);
const points = shape.attr('points').split(' ');
const radius = consts.BASE_POINT_SIZE / scale;
group.children().forEach((child: SVG.Element, idx: number): void => {
const [px, py] = points[idx].split(',');
child.move(px - radius / 2, py - radius / 2);
});
}
const { x: initialX, y: initialY } = this.cursorPosition;
this.pointsGroup = this.canvas.group();
this.drawInstance = (this.canvas as any).polyline(initialPoints)
.addClass('cvat_canvas_shape_drawing').style({
'stroke-width': 0,
});
this.pointsGroup = this.canvas.group();
for (const point of points.split(' ')) {
let numOfPoints = initialPoints.split(' ').length;
while (numOfPoints) {
numOfPoints--;
const radius = consts.BASE_POINT_SIZE / this.geometry.scale;
const stroke = consts.POINTS_STROKE_WIDTH / this.geometry.scale;
const [x, y] = point.split(',').map((coord: string): number => +coord);
this.pointsGroup.circle().move(x - radius / 2, y - radius / 2)
.fill('white').stroke('black').attr({
r: radius,
'stroke-width': stroke,
});
this.pointsGroup.circle().fill('white').stroke('black').attr({
r: radius,
'stroke-width': stroke,
});
}
this.pointsGroup.attr({
z_order: Number.MAX_SAFE_INTEGER,
});
moveShape(
this.drawInstance, this.pointsGroup, initialX, initialY, this.geometry.scale,
);
this.canvas.on('mousemove.draw', (e: MouseEvent): void => {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
this.canvas.on('mousemove.draw', (): void => {
const { x, y } = this.cursorPosition; // was computer in another callback
moveShape(
this.drawInstance, this.pointsGroup, x, y, this.geometry.scale,
);
const bbox = this.drawInstance.bbox();
this.drawInstance.move(x - bbox.width / 2, y - bbox.height / 2);
const radius = consts.BASE_POINT_SIZE / this.geometry.scale;
const newPoints = this.drawInstance.attr('points').split(' ');
if (this.pointsGroup) {
this.pointsGroup.children()
.forEach((child: SVG.Element, idx: number): void => {
const [px, py] = newPoints[idx].split(',');
child.move(px - radius / 2, py - radius / 2);
});
}
});
this.pastePolyshape();
@ -593,23 +600,20 @@ export class DrawHandlerImpl implements DrawHandler {
this.crosshair = null;
this.drawInstance = null;
this.pointsGroup = null;
this.cursorPosition = {
x: 0,
y: 0,
};
this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
this.cursorPosition = { x, y };
if (this.crosshair) {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
this.crosshair.x.attr({
y1: y,
y2: y,
});
this.crosshair.y.attr({
x1: x,
x2: x,
});
this.crosshair.x.attr({ y1: y, y2: y });
this.crosshair.y.attr({ x1: x, x2: x });
}
});
}

@ -862,7 +862,7 @@
: (frame) => frame - 1;
for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
// First prepare all data for the frame
// Consider all shapes, tags, and tracks that have keyframe here
// Consider all shapes, tags, and not outside tracks that have keyframe here
// In particular consider first and last frame as keyframes for all frames
const statesData = [].concat(
(frame in this.shapes ? this.shapes[frame] : [])
@ -876,7 +876,10 @@
|| frame === frameFrom
|| frame === frameTo
));
statesData.push(...tracks.map((track) => track.get(frame)));
statesData.push(
...tracks.map((track) => track.get(frame))
.filter((state) => !state.outside),
);
// Nothing to filtering, go to the next iteration
if (!statesData.length) {

@ -1343,7 +1343,7 @@
return annotationsData;
};
Job.prototype.annotations.search.implementation = async function (filters, frameFrom, frameTo) {
Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) {
if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError(
'The filters argument must be an array of strings',
@ -1555,7 +1555,7 @@
return result;
};
Job.prototype.annotations.search.implementation = async function (filters, frameFrom, frameTo) {
Task.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) {
if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError(
'The filters argument must be an array of strings',

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "0.1.0",
"version": "0.5.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -9734,6 +9734,14 @@
"scheduler": "^0.17.0"
}
},
"react-hotkeys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz",
"integrity": "sha512-3n3OU8vLX/pfcJrR3xJ1zlww6KS1kEJt0Whxc4FiGV+MJrQ1mYSYI3qS/11d2MJDFm8IhOXMTFQirfu6AVOF6Q==",
"requires": {
"prop-types": "^15.6.1"
}
},
"react-is": {
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz",

@ -61,6 +61,7 @@
"prop-types": "^15.7.2",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-hotkeys": "^2.0.0",
"react-redux": "^7.1.1",
"react-router": "^5.1.0",
"react-router-dom": "^5.1.0",

@ -82,8 +82,10 @@ export enum AnnotationActionTypes {
GROUP_OBJECTS = 'GROUP_OBJECTS',
SPLIT_TRACK = 'SPLIT_TRACK',
COPY_SHAPE = 'COPY_SHAPE',
PASTE_SHAPE = 'PASTE_SHAPE',
EDIT_SHAPE = 'EDIT_SHAPE',
DRAW_SHAPE = 'DRAW_SHAPE',
REPEAT_DRAW_SHAPE = 'REPEAT_DRAW_SHAPE',
SHAPE_DRAWN = 'SHAPE_DRAWN',
RESET_CANVAS = 'RESET_CANVAS',
UPDATE_ANNOTATIONS_SUCCESS = 'UPDATE_ANNOTATIONS_SUCCESS',
@ -92,6 +94,8 @@ export enum AnnotationActionTypes {
CREATE_ANNOTATIONS_FAILED = 'CREATE_ANNOTATIONS_FAILED',
MERGE_ANNOTATIONS_SUCCESS = 'MERGE_ANNOTATIONS_SUCCESS',
MERGE_ANNOTATIONS_FAILED = 'MERGE_ANNOTATIONS_FAILED',
RESET_ANNOTATIONS_GROUP = 'RESET_ANNOTATIONS_GROUP',
GROUP_ANNOTATIONS = 'GROUP_ANNOTATIONS',
GROUP_ANNOTATIONS_SUCCESS = 'GROUP_ANNOTATIONS_SUCCESS',
GROUP_ANNOTATIONS_FAILED = 'GROUP_ANNOTATIONS_FAILED',
SPLIT_ANNOTATIONS_SUCCESS = 'SPLIT_ANNOTATIONS_SUCCESS',
@ -133,6 +137,7 @@ export enum AnnotationActionTypes {
ROTATE_FRAME = 'ROTATE_FRAME',
SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
ADD_Z_LAYER = 'ADD_Z_LAYER',
SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED',
}
export function addZLayer(): AnyAction {
@ -905,6 +910,11 @@ export function updateAnnotationsAsync(sessionInstance: any, frame: number, stat
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) {
// deactivate object to visualize changes immediately (UX)
dispatch(activateObject(null));
}
const promises = statesToUpdate
.map((objectState: any): Promise<any> => objectState.save());
const states = await Promise.all(promises);
@ -991,12 +1001,30 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
};
}
export function groupAnnotationsAsync(sessionInstance: any, frame: number, statesToGroup: any[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function resetAnnotationsGroup(): AnyAction {
return {
type: AnnotationActionTypes.RESET_ANNOTATIONS_GROUP,
payload: {},
};
}
export function groupAnnotationsAsync(
sessionInstance: any,
frame: number,
statesToGroup: any[],
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
await sessionInstance.annotations.group(statesToGroup);
const reset = getStore().getState().annotation.annotations.resetGroupFlag;
// The action below set resetFlag to false
dispatch({
type: AnnotationActionTypes.GROUP_ANNOTATIONS,
payload: {},
});
await sessionInstance.annotations.group(statesToGroup, reset);
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const history = await sessionInstance.actions.get();
@ -1099,3 +1127,94 @@ export function changeGroupColorAsync(
}
};
}
export function searchAnnotationsAsync(
sessionInstance: any,
frameFrom: number,
frameTo: number,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
const frame = await sessionInstance.annotations.search(filters, frameFrom, frameTo);
if (frame !== null) {
dispatch(changeFrameAsync(frame));
}
} catch (error) {
dispatch({
type: AnnotationActionTypes.SEARCH_ANNOTATIONS_FAILED,
payload: {
error,
},
});
}
};
}
export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const initialState = getStore().getState().annotation.drawing.activeInitialState;
const { instance: canvasInstance } = getStore().getState().annotation.canvas;
if (initialState) {
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (initialState.shapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (initialState.shapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (initialState.shapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
}
dispatch({
type: AnnotationActionTypes.PASTE_SHAPE,
payload: {
activeControl,
},
});
canvasInstance.cancel();
canvasInstance.draw({
enabled: true,
initialState,
});
}
};
}
export function repeatDrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
activeShapeType,
activeNumOfPoints,
activeRectDrawingMethod,
} = getStore().getState().annotation.drawing;
const { instance: canvasInstance } = getStore().getState().annotation.canvas;
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (activeShapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (activeShapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
} else if (activeShapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
}
dispatch({
type: AnnotationActionTypes.REPEAT_DRAW_SHAPE,
payload: {
activeControl,
},
});
canvasInstance.cancel();
canvasInstance.draw({
enabled: true,
rectDrawingMethod: activeRectDrawingMethod,
numberOfPoints: activeNumOfPoints,
shapeType: activeShapeType,
crosshair: activeShapeType === ShapeType.RECTANGLE,
});
};
}

@ -0,0 +1,11 @@
import { ActionUnion, createAction } from 'utils/redux';
export enum ShortcutsActionsTypes {
SWITCH_SHORTCUT_DIALOG = 'SWITCH_SHORTCUT_DIALOG',
}
export const shortcutsActions = {
switchShortcutsDialog: () => createAction(ShortcutsActionsTypes.SWITCH_SHORTCUT_DIALOG),
};
export type ShortcutsActions = ActionUnion<typeof shortcutsActions>;

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import {
Layout,
@ -12,17 +13,8 @@ import {
} from 'antd';
import { SliderValue } from 'antd/lib//slider';
import {
ColorBy,
GridColor,
ObjectType,
} from 'reducers/interfaces';
import {
Canvas,
} from 'cvat-canvas';
import { ColorBy, GridColor, ObjectType } from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas';
import getCore from 'cvat-core';
const cvat = getCore();
@ -75,6 +67,12 @@ interface Props {
onUpdateContextMenu(visible: boolean, left: number, top: number): void;
onAddZLayer(): void;
onSwitchZLayer(cur: number): void;
onChangeBrightnessLevel(level: number): void;
onChangeContrastLevel(level: number): void;
onChangeSaturationLevel(level: number): void;
onChangeGridOpacity(opacity: number): void;
onChangeGridColor(color: GridColor): void;
onSwitchGrid(enabled: boolean): void;
}
export default class CanvasWrapperComponent extends React.PureComponent<Props> {
@ -109,6 +107,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
activatedStateID,
curZLayer,
resetZoom,
grid,
gridOpacity,
gridColor,
brightnessLevel,
contrastLevel,
saturationLevel,
} = this.props;
if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
@ -132,6 +136,31 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
}
if (gridOpacity !== prevProps.gridOpacity
|| gridColor !== prevProps.gridColor
|| grid !== prevProps.grid) {
const gridElement = window.document.getElementById('cvat_canvas_grid');
const gridPattern = window.document.getElementById('cvat_canvas_grid_pattern');
if (gridElement) {
gridElement.style.display = grid ? 'block' : 'none';
}
if (gridPattern) {
gridPattern.style.stroke = gridColor.toLowerCase();
gridPattern.style.opacity = `${gridOpacity / 100}`;
}
}
if (brightnessLevel !== prevProps.brightnessLevel
|| contrastLevel !== prevProps.contrastLevel
|| saturationLevel !== prevProps.saturationLevel) {
const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) {
backgroundElement.style.filter = `brightness(${brightnessLevel / 100})`
+ `contrast(${contrastLevel / 100})`
+ `saturate(${saturationLevel / 100})`;
}
}
if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) {
this.updateCanvas();
}
@ -360,7 +389,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
// Filters
const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) {
backgroundElement.style.filter = `brightness(${brightnessLevel / 100}) contrast(${contrastLevel / 100}) saturate(${saturationLevel / 100})`;
backgroundElement.style.filter = `brightness(${brightnessLevel / 100})`
+ `contrast(${contrastLevel / 100})`
+ `saturate(${saturationLevel / 100})`;
}
// Events
@ -374,6 +405,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
});
canvasInstance.html().addEventListener('click', (): void => {
if (document.activeElement) {
(document.activeElement as HTMLElement).blur();
}
});
canvasInstance.html().addEventListener('contextmenu', (e: MouseEvent): void => {
const {
activatedStateID,
@ -488,10 +525,162 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
minZLayer,
onSwitchZLayer,
onAddZLayer,
brightnessLevel,
contrastLevel,
saturationLevel,
grid,
gridColor,
gridOpacity,
onChangeBrightnessLevel,
onChangeSaturationLevel,
onChangeContrastLevel,
onChangeGridColor,
onChangeGridOpacity,
onSwitchGrid,
} = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
}
};
const keyMap = {
INCREASE_BRIGHTNESS: {
name: 'Brightness+',
description: 'Increase brightness level for the image',
sequence: 'shift+b+=',
action: 'keypress',
},
DECREASE_BRIGHTNESS: {
name: 'Brightness-',
description: 'Decrease brightness level for the image',
sequence: 'shift+b+-',
action: 'keydown',
},
INCREASE_CONTRAST: {
name: 'Contrast+',
description: 'Increase contrast level for the image',
sequence: 'shift+c+=',
action: 'keydown',
},
DECREASE_CONTRAST: {
name: 'Contrast-',
description: 'Decrease contrast level for the image',
sequence: 'shift+c+-',
action: 'keydown',
},
INCREASE_SATURATION: {
name: 'Saturation+',
description: 'Increase saturation level for the image',
sequence: 'shift+s+=',
action: 'keydown',
},
DECREASE_SATURATION: {
name: 'Saturation-',
description: 'Increase contrast level for the image',
sequence: 'shift+s+-',
action: 'keydown',
},
INCREASE_GRID_OPACITY: {
name: 'Grid opacity+',
description: 'Make the grid more visible',
sequence: 'shift+g+=',
action: 'keydown',
},
DECREASE_GRID_OPACITY: {
name: 'Grid opacity-',
description: 'Make the grid less visible',
sequences: 'shift+g+-',
action: 'keydown',
},
CHANGE_GRID_COLOR: {
name: 'Grid color',
description: 'Set another color for the image grid',
sequence: 'shift+g+enter',
action: 'keydown',
},
};
const step = 10;
const handlers = {
INCREASE_BRIGHTNESS: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const maxLevel = 200;
if (brightnessLevel < maxLevel) {
onChangeBrightnessLevel(Math.min(brightnessLevel + step, maxLevel));
}
},
DECREASE_BRIGHTNESS: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const minLevel = 50;
if (brightnessLevel > minLevel) {
onChangeBrightnessLevel(Math.max(brightnessLevel - step, minLevel));
}
},
INCREASE_CONTRAST: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const maxLevel = 200;
if (contrastLevel < maxLevel) {
onChangeContrastLevel(Math.min(contrastLevel + step, maxLevel));
}
},
DECREASE_CONTRAST: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const minLevel = 50;
if (contrastLevel > minLevel) {
onChangeContrastLevel(Math.max(contrastLevel - step, minLevel));
}
},
INCREASE_SATURATION: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const maxLevel = 300;
if (saturationLevel < maxLevel) {
onChangeSaturationLevel(Math.min(saturationLevel + step, maxLevel));
}
},
DECREASE_SATURATION: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const minLevel = 0;
if (saturationLevel > minLevel) {
onChangeSaturationLevel(Math.max(saturationLevel - step, minLevel));
}
},
INCREASE_GRID_OPACITY: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const maxLevel = 100;
if (!grid) {
onSwitchGrid(true);
}
if (gridOpacity < maxLevel) {
onChangeGridOpacity(Math.min(gridOpacity + step, maxLevel));
}
},
DECREASE_GRID_OPACITY: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const minLevel = 0;
if (gridOpacity - step <= minLevel) {
onSwitchGrid(false);
}
if (gridOpacity > minLevel) {
onChangeGridOpacity(Math.max(gridOpacity - step, minLevel));
}
},
CHANGE_GRID_COLOR: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const colors = [GridColor.Black, GridColor.Blue,
GridColor.Green, GridColor.Red, GridColor.White];
const indexOf = colors.indexOf(gridColor) + 1;
const color = colors[indexOf >= colors.length ? 0 : indexOf];
onChangeGridColor(color);
},
};
return (
<Layout.Content style={{ position: 'relative' }}>
<GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
{/*
This element doesn't have any props
So, React isn't going to rerender it

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import {
Icon,
@ -12,7 +13,7 @@ import {
import {
ActiveControl,
Rotation
Rotation,
} from 'reducers/interfaces';
import {
@ -44,6 +45,9 @@ interface Props {
groupObjects(enabled: boolean): void;
splitTrack(enabled: boolean): void;
rotateFrame(rotation: Rotation): void;
repeatDrawShape(): void;
pasteShape(): void;
resetGroup(): void;
}
export default function ControlsSideBarComponent(props: Props): JSX.Element {
@ -55,14 +59,140 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
groupObjects,
splitTrack,
rotateFrame,
repeatDrawShape,
pasteShape,
resetGroup,
} = props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
}
};
const keyMap = {
PASTE_SHAPE: {
name: 'Paste shape',
description: 'Paste a shape from internal CVAT clipboard',
sequence: 'ctrl+v',
action: 'keydown',
},
SWITCH_DRAW_MODE: {
name: 'Draw mode',
description: 'Repeat the latest procedure of drawing with the same parameters',
sequence: 'n',
action: 'keydown',
},
SWITCH_MERGE_MODE: {
name: 'Merge mode',
description: 'Activate or deactivate mode to merging shapes',
sequence: 'm',
action: 'keydown',
},
SWITCH_GROUP_MODE: {
name: 'Group mode',
description: 'Activate or deactivate mode to grouping shapes',
sequence: 'g',
action: 'keydown',
},
RESET_GROUP: {
name: 'Reset group',
description: 'Reset group for selected shapes (in group mode)',
sequence: 'shift+g',
action: 'keyup',
},
CANCEL: {
name: 'Cancel',
description: 'Cancel any active canvas mode',
sequence: 'esc',
action: 'keydown',
},
CLOCKWISE_ROTATION: {
name: 'Rotate clockwise',
description: 'Change image angle (add 90 degrees)',
sequence: 'ctrl+r',
action: 'keydown',
},
ANTICLOCKWISE_ROTATION: {
name: 'Rotate anticlockwise',
description: 'Change image angle (substract 90 degrees)',
sequence: 'ctrl+shift+r',
action: 'keydown',
},
};
const handlers = {
PASTE_SHAPE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
canvasInstance.cancel();
pasteShape();
},
SWITCH_DRAW_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const drawing = [ActiveControl.DRAW_POINTS, ActiveControl.DRAW_POLYGON,
ActiveControl.DRAW_POLYLINE, ActiveControl.DRAW_RECTANGLE].includes(activeControl);
if (!drawing) {
canvasInstance.cancel();
// repeateDrawShapes gets all the latest parameters
// and calls canvasInstance.draw() with them
repeatDrawShape();
} else {
canvasInstance.draw({ enabled: false });
}
},
SWITCH_MERGE_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const merging = activeControl === ActiveControl.MERGE;
if (!merging) {
canvasInstance.cancel();
}
canvasInstance.merge({ enabled: !merging });
mergeObjects(!merging);
},
SWITCH_GROUP_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const grouping = activeControl === ActiveControl.GROUP;
if (!grouping) {
canvasInstance.cancel();
}
canvasInstance.group({ enabled: !grouping });
groupObjects(!grouping);
},
RESET_GROUP: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const grouping = activeControl === ActiveControl.GROUP;
if (!grouping) {
return;
}
resetGroup();
canvasInstance.group({ enabled: false });
groupObjects(false);
},
CANCEL: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (activeControl !== ActiveControl.CURSOR) {
canvasInstance.cancel();
}
},
CLOCKWISE_ROTATION: (event: KeyboardEvent | undefined) => {
preventDefault(event);
rotateFrame(Rotation.CLOCKWISE90);
},
ANTICLOCKWISE_ROTATION: (event: KeyboardEvent | undefined) => {
preventDefault(event);
rotateFrame(Rotation.ANTICLOCKWISE90);
},
};
return (
<Layout.Sider
className='cvat-canvas-controls-sidebar'
theme='light'
width={44}
>
<GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
<CursorControl canvasInstance={canvasInstance} activeControl={activeControl} />
<MoveControl canvasInstance={canvasInstance} activeControl={activeControl} />
<RotateControl rotateFrame={rotateFrame} />

@ -20,6 +20,7 @@ interface Props {
startFrame: number;
stopFrame: number;
frameNumber: number;
inputFrameRef: React.RefObject<InputNumber>;
onSliderChange(value: SliderValue): void;
onInputChange(value: number | undefined): void;
onURLIconClick(): void;
@ -30,6 +31,7 @@ function PlayerNavigation(props: Props): JSX.Element {
startFrame,
stopFrame,
frameNumber,
inputFrameRef,
onSliderChange,
onInputChange,
onURLIconClick,
@ -69,6 +71,7 @@ function PlayerNavigation(props: Props): JSX.Element {
value={frameNumber || 0}
// https://stackoverflow.com/questions/38256332/in-react-whats-the-difference-between-onchange-and-oninput
onChange={onInputChange}
ref={inputFrameRef}
/>
</Col>
</>

@ -8,6 +8,7 @@ import {
Row,
Col,
Layout,
InputNumber,
} from 'antd';
import { SliderValue } from 'antd/lib/slider';
@ -22,6 +23,7 @@ interface Props {
saving: boolean;
savingStatuses: string[];
frameNumber: number;
inputFrameRef: React.RefObject<InputNumber>;
startFrame: number;
stopFrame: number;
undoAction?: string;
@ -42,7 +44,7 @@ interface Props {
onRedoClick(): void;
}
function AnnotationTopBarComponent(props: Props): JSX.Element {
export default function AnnotationTopBarComponent(props: Props): JSX.Element {
const {
saving,
savingStatuses,
@ -50,6 +52,7 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
redoAction,
playing,
frameNumber,
inputFrameRef,
startFrame,
stopFrame,
showStatistics,
@ -96,6 +99,7 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
startFrame={startFrame}
stopFrame={stopFrame}
frameNumber={frameNumber}
inputFrameRef={inputFrameRef}
onSliderChange={onSliderChange}
onInputChange={onInputChange}
onURLIconClick={onURLIconClick}
@ -107,5 +111,3 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
</Layout.Header>
);
}
export default React.memo(AnnotationTopBarComponent);

@ -5,18 +5,17 @@
import 'antd/dist/antd.less';
import '../styles.scss';
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import {
Switch,
Route,
Redirect,
} from 'react-router';
import { Switch, Route, Redirect } from 'react-router';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { GlobalHotKeys, KeyMap, configure } from 'react-hotkeys';
import {
Spin,
Layout,
notification,
} from 'antd';
import ShorcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog';
import SettingsPageContainer from 'containers/settings-page/settings-page';
import TasksPageContainer from 'containers/tasks-page/tasks-page';
import CreateTaskPageContainer from 'containers/create-task-page/create-task-page';
@ -30,7 +29,7 @@ import HeaderContainer from 'containers/header/header';
import { NotificationsState } from 'reducers/interfaces';
type CVATAppProps = {
interface CVATAppProps {
loadFormats: () => void;
loadUsers: () => void;
loadAbout: () => void;
@ -38,6 +37,7 @@ type CVATAppProps = {
initPlugins: () => void;
resetErrors: () => void;
resetMessages: () => void;
switchShortcutsDialog: () => void;
userInitialized: boolean;
pluginsInitialized: boolean;
pluginsFetching: boolean;
@ -52,11 +52,12 @@ type CVATAppProps = {
installedTFSegmentation: boolean;
notifications: NotificationsState;
user: any;
};
}
export default class CVATApplication extends React.PureComponent<CVATAppProps> {
class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentProps> {
public componentDidMount(): void {
const { verifyAuthorized } = this.props;
configure({ ignoreRepeatedEventsWhenKeyHeldDown: false });
verifyAuthorized();
}
@ -190,7 +191,9 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
installedAutoAnnotation,
installedTFSegmentation,
installedTFAnnotation,
switchShortcutsDialog,
user,
history,
} = this.props;
const readyForRender = (userInitialized && user == null)
@ -200,13 +203,50 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
const withModels = installedAutoAnnotation
|| installedTFAnnotation || installedTFSegmentation;
const keyMap = {
SWITCH_SHORTCUTS: {
name: 'Show shortcuts',
description: 'Open/hide the list of available shortcuts',
sequence: 'f1',
action: 'keydown',
},
OPEN_SETTINGS: {
name: 'Open settings',
description: 'Go to the settings page or go back',
sequence: 'f2',
action: 'keydown',
},
};
const handlers = {
SWITCH_SHORTCUTS: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
switchShortcutsDialog();
},
OPEN_SETTINGS: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
if (history.location.pathname.endsWith('settings')) {
history.goBack();
} else {
history.push('/settings');
}
},
};
if (readyForRender) {
if (user) {
return (
<BrowserRouter>
<Layout>
<HeaderContainer> </HeaderContainer>
<Layout.Content>
<Layout>
<HeaderContainer> </HeaderContainer>
<Layout.Content>
<ShorcutsDialog />
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers}>
<Switch>
<Route exact path='/settings' component={SettingsPageContainer} />
<Route exact path='/tasks' component={TasksPageContainer} />
@ -219,22 +259,20 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
&& <Route exact path='/models/create' component={CreateModelPageContainer} /> }
<Redirect push to='/tasks' />
</Switch>
{/* eslint-disable-next-line */}
<a id='downloadAnchor' style={{ display: 'none' }} download/>
</Layout.Content>
</Layout>
</BrowserRouter>
</GlobalHotKeys>
{/* eslint-disable-next-line */}
<a id='downloadAnchor' style={{ display: 'none' }} download/>
</Layout.Content>
</Layout>
);
}
return (
<BrowserRouter>
<Switch>
<Route exact path='/auth/register' component={RegisterPageContainer} />
<Route exact path='/auth/login' component={LoginPageContainer} />
<Redirect to='/auth/login' />
</Switch>
</BrowserRouter>
<Switch>
<Route exact path='/auth/register' component={RegisterPageContainer} />
<Route exact path='/auth/login' component={LoginPageContainer} />
<Redirect to='/auth/login' />
</Switch>
);
}
@ -243,3 +281,5 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
);
}
}
export default withRouter(CVATApplication);

@ -0,0 +1,97 @@
import React from 'react';
import { getApplicationKeyMap } from 'react-hotkeys';
import { Modal, Table } from 'antd';
import { connect } from 'react-redux';
import { shortcutsActions } from 'actions/shortcuts-actions';
import { CombinedState } from 'reducers/interfaces';
interface StateToProps {
visible: boolean;
}
interface DispatchToProps {
switchShortcutsDialog(): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
shortcuts: {
visibleShortcutsHelp: visible,
},
} = state;
return {
visible,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
switchShortcutsDialog(): void {
dispatch(shortcutsActions.switchShortcutsDialog());
},
};
}
function ShorcutsDialog(props: StateToProps & DispatchToProps): JSX.Element | null {
const { visible, switchShortcutsDialog } = props;
const keyMap = getApplicationKeyMap();
const splitToRows = (data: string[]): JSX.Element[] => (
data.map((item: string, id: number): JSX.Element => (
// eslint-disable-next-line react/no-array-index-key
<span key={id}>
{item}
<br />
</span>
))
);
const columns = [{
title: 'Name',
dataIndex: 'name',
key: 'name',
}, {
title: 'Shortcut',
dataIndex: 'shortcut',
key: 'shortcut',
render: splitToRows,
}, {
title: 'Action',
dataIndex: 'action',
key: 'action',
render: splitToRows,
}, {
title: 'Description',
dataIndex: 'description',
key: 'description',
}];
const dataSource = Object.keys(keyMap).map((key: string, id: number) => ({
key: id,
name: keyMap[key].name || key,
description: keyMap[key].description || '',
shortcut: keyMap[key].sequences.map((value) => value.sequence),
action: keyMap[key].sequences.map((value) => value.action || 'keydown'),
}));
return (
<Modal
title='Active list of shortcuts'
visible={visible}
closable={false}
width={800}
onOk={switchShortcutsDialog}
cancelButtonProps={{ style: { display: 'none' } }}
zIndex={1001} /* default antd is 1000 */
>
<Table dataSource={dataSource} columns={columns} size='small' />
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ShorcutsDialog);

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RouteComponentProps } from 'react-router';
@ -80,15 +79,10 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
};
}
function AnnotationPageContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<AnnotationPageComponent {...props} />
);
}
export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps,
)(AnnotationPageContainer),
)(AnnotationPageComponent),
);

@ -2,11 +2,9 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import CanvasWrapperComponent from 'components/annotation-page/standard-workspace/canvas-wrapper';
import {
confirmCanvasReady,
dragCanvas,
@ -28,6 +26,14 @@ import {
addZLayer,
switchZLayer,
} from 'actions/annotation-actions';
import {
switchGrid,
changeGridColor,
changeGridOpacity,
changeBrightnessLevel,
changeContrastLevel,
changeSaturationLevel,
} from 'actions/settings-actions';
import {
ColorBy,
GridColor,
@ -86,6 +92,12 @@ interface DispatchToProps {
onUpdateContextMenu(visible: boolean, left: number, top: number): void;
onAddZLayer(): void;
onSwitchZLayer(cur: number): void;
onChangeBrightnessLevel(level: number): void;
onChangeContrastLevel(level: number): void;
onChangeSaturationLevel(level: number): void;
onChangeGridOpacity(opacity: number): void;
onChangeGridColor(color: GridColor): void;
onSwitchGrid(enabled: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -233,16 +245,28 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSwitchZLayer(cur: number): void {
dispatch(switchZLayer(cur));
},
onChangeBrightnessLevel(level: number): void {
dispatch(changeBrightnessLevel(level));
},
onChangeContrastLevel(level: number): void {
dispatch(changeContrastLevel(level));
},
onChangeSaturationLevel(level: number): void {
dispatch(changeSaturationLevel(level));
},
onChangeGridOpacity(opacity: number): void {
dispatch(changeGridOpacity(opacity));
},
onChangeGridColor(color: GridColor): void {
dispatch(changeGridColor(color));
},
onSwitchGrid(enabled: boolean): void {
dispatch(switchGrid(enabled));
},
};
}
function CanvasWrapperContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<CanvasWrapperComponent {...props} />
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(CanvasWrapperContainer);
)(CanvasWrapperComponent);

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import { Canvas } from 'cvat-canvas';
@ -12,6 +11,9 @@ import {
groupObjects,
splitTrack,
rotateCurrentFrame,
repeatDrawShapeAsync,
pasteShapeAsync,
resetAnnotationsGroup,
} from 'actions/annotation-actions';
import ControlsSideBarComponent from 'components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar';
import {
@ -31,6 +33,9 @@ interface DispatchToProps {
groupObjects(enabled: boolean): void;
splitTrack(enabled: boolean): void;
rotateFrame(angle: Rotation): void;
resetGroup(): void;
repeatDrawShape(): void;
pasteShape(): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -69,16 +74,19 @@ function dispatchToProps(dispatch: any): DispatchToProps {
rotateFrame(rotation: Rotation): void {
dispatch(rotateCurrentFrame(rotation));
},
repeatDrawShape(): void {
dispatch(repeatDrawShapeAsync());
},
pasteShape(): void {
dispatch(pasteShapeAsync());
},
resetGroup(): void {
dispatch(resetAnnotationsGroup());
},
};
}
function ControlsSideBarContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<ControlsSideBarComponent {...props} />
);
}
export default connect(
mapStateToProps,
dispatchToProps,
)(ControlsSideBarContainer);
)(ControlsSideBarComponent);

@ -115,8 +115,11 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
let statesLocked = true;
ownObjectStates.forEach((objectState: any) => {
statesHidden = statesHidden && objectState.hidden;
statesLocked = statesLocked && objectState.lock;
const { lock } = objectState;
if (!lock) {
statesHidden = statesHidden && objectState.hidden;
statesLocked = statesLocked && objectState.lock;
}
});
return {

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import LabelsListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/labels-list';
@ -29,12 +28,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
};
}
function LabelsListContainer(props: StateToProps): JSX.Element {
return (
<LabelsListComponent {...props} />
);
}
export default connect(
mapStateToProps,
)(LabelsListContainer);
)(LabelsListComponent);

@ -20,6 +20,7 @@ import {
copyShape as copyShapeAction,
activateObject as activateObjectAction,
propagateObject as propagateObjectAction,
pasteShapeAsync,
} from 'actions/annotation-actions';
import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item';
@ -134,6 +135,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
},
copyShape(objectState: any): void {
dispatch(copyShapeAction(objectState));
dispatch(pasteShapeAsync());
},
propagateObject(objectState: any): void {
dispatch(propagateObjectAction(objectState));

@ -4,6 +4,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { SelectValue } from 'antd/lib/select';
@ -11,13 +12,18 @@ import ObjectsListComponent from 'components/annotation-page/standard-workspace/
import {
updateAnnotationsAsync,
fetchAnnotationsAsync,
removeObjectAsync,
changeFrameAsync,
changeAnnotationsFilters as changeAnnotationsFiltersAction,
collapseObjectItems,
copyShape as copyShapeAction,
propagateObject as propagateObjectAction,
} from 'actions/annotation-actions';
import {
CombinedState,
StatesOrdering,
ObjectType,
} from 'reducers/interfaces';
interface StateToProps {
@ -29,6 +35,9 @@ interface StateToProps {
statesCollapsed: boolean;
objectStates: any[];
annotationsFilters: string[];
activatedStateID: number | null;
minZLayer: number;
maxZLayer: number;
annotationsFiltersHistory: string[];
}
@ -36,6 +45,10 @@ interface DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void;
changeAnnotationsFilters(sessionInstance: any, filters: string[]): void;
collapseStates(states: any[], value: boolean): void;
removeObject: (sessionInstance: any, objectState: any, force: boolean) => void;
copyShape: (objectState: any) => void;
propagateObject: (objectState: any) => void;
changeFrame(frame: number): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -46,6 +59,11 @@ function mapStateToProps(state: CombinedState): StateToProps {
filters: annotationsFilters,
filtersHistory: annotationsFiltersHistory,
collapsed,
activatedStateID,
zLayer: {
min: minZLayer,
max: maxZLayer,
},
},
job: {
instance: jobInstance,
@ -64,9 +82,11 @@ function mapStateToProps(state: CombinedState): StateToProps {
let statesCollapsed = true;
objectStates.forEach((objectState: any) => {
const { clientID } = objectState;
statesHidden = statesHidden && objectState.hidden;
statesLocked = statesLocked && objectState.lock;
const { clientID, lock } = objectState;
if (!lock) {
statesHidden = statesHidden && objectState.hidden;
statesLocked = statesLocked && objectState.lock;
}
const stateCollapsed = clientID in collapsed ? collapsed[clientID] : true;
statesCollapsed = statesCollapsed && stateCollapsed;
});
@ -80,6 +100,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
frameNumber,
jobInstance,
annotationsFilters,
activatedStateID,
minZLayer,
maxZLayer,
annotationsFiltersHistory,
};
}
@ -99,6 +122,18 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(changeAnnotationsFiltersAction(filters));
dispatch(fetchAnnotationsAsync(sessionInstance));
},
removeObject(sessionInstance: any, objectState: any, force: boolean): void {
dispatch(removeObjectAsync(sessionInstance, objectState, force));
},
copyShape(objectState: any): void {
dispatch(copyShapeAction(objectState));
},
propagateObject(objectState: any): void {
dispatch(propagateObjectAction(objectState));
},
changeFrame(frame: number): void {
dispatch(changeFrameAsync(frame));
},
};
}
@ -226,6 +261,19 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
public render(): JSX.Element {
const {
annotationsFilters,
statesHidden,
statesLocked,
activatedStateID,
objectStates,
frameNumber,
jobInstance,
updateAnnotations,
removeObject,
copyShape,
propagateObject,
changeFrame,
maxZLayer,
minZLayer,
annotationsFiltersHistory,
} = this.props;
const {
@ -233,22 +281,241 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
statesOrdering,
} = this.state;
const keyMap = {
SWITCH_ALL_LOCK: {
name: 'Lock/unlock all objects',
description: 'Change locked state for all objects in the side bar',
sequence: 't+l',
action: 'keydown',
},
SWITCH_LOCK: {
name: 'Lock/unlock an object',
description: 'Change locked state for an active object',
sequence: 'l',
action: 'keydown',
},
SWITCH_ALL_HIDDEN: {
name: 'Hide/show all objects',
description: 'Change hidden state for objects in the side bar',
sequence: 't+h',
action: 'keydown',
},
SWITCH_HIDDEN: {
name: 'Hide/show an object',
description: 'Change hidden state for an active object',
sequence: 'h',
action: 'keydown',
},
SWITCH_OCCLUDED: {
name: 'Switch occluded',
description: 'Change occluded property for an active object',
sequences: ['q', '/'],
action: 'keydown',
},
SWITCH_KEYFRAME: {
name: 'Switch keyframe',
description: 'Change keyframe property for an active track',
sequence: 'k',
action: 'keydown',
},
SWITCH_OUTSIDE: {
name: 'Switch outside',
description: 'Change outside property for an active track',
sequence: 'o',
action: 'keydown',
},
DELETE_OBJECT: {
name: 'Delete object',
description: 'Delete an active object. Use shift to force delete of locked objects',
sequences: ['del', 'shift+del'],
action: 'keydown',
},
TO_BACKGROUND: {
name: 'To background',
description: 'Put an active object "farther" from the user (decrease z axis value)',
sequences: ['-', '_'],
action: 'keydown',
},
TO_FOREGROUND: {
name: 'To foreground',
description: 'Put an active object "closer" to the user (increase z axis value)',
sequences: ['+', '='],
action: 'keydown',
},
COPY_SHAPE: {
name: 'Copy shape',
description: 'Copy shape to CVAT internal clipboard',
sequence: 'ctrl+c',
action: 'keydown',
},
PROPAGATE_OBJECT: {
name: 'Propagate object',
description: 'Make a copy of the object on the following frames',
sequence: 'ctrl+b',
action: 'keydown',
},
NEXT_KEY_FRAME: {
name: 'Next keyframe',
description: 'Go to the next keyframe of an active track',
sequence: 'r',
action: 'keydown',
},
PREV_KEY_FRAME: {
name: 'Previous keyframe',
description: 'Go to the previous keyframe of an active track',
sequence: 'e',
action: 'keydown',
},
};
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
}
};
const activatedStated = (): any | null => {
if (activatedStateID !== null) {
const [state] = objectStates
.filter((objectState: any): boolean => (
objectState.clientID === activatedStateID
));
return state || null;
}
return null;
};
const handlers = {
SWITCH_ALL_LOCK: (event: KeyboardEvent | undefined) => {
preventDefault(event);
this.lockAllStates(!statesLocked);
},
SWITCH_LOCK: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state) {
state.lock = !state.lock;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
SWITCH_ALL_HIDDEN: (event: KeyboardEvent | undefined) => {
preventDefault(event);
this.hideAllStates(!statesHidden);
},
SWITCH_HIDDEN: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state) {
state.hidden = !state.hidden;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
SWITCH_OCCLUDED: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
state.occluded = !state.occluded;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
SWITCH_KEYFRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) {
state.keyframe = !state.keyframe;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
SWITCH_OUTSIDE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) {
state.outside = !state.outside;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
DELETE_OBJECT: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state) {
removeObject(jobInstance, state, event ? event.shiftKey : false);
}
},
TO_BACKGROUND: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
state.zOrder = minZLayer - 1;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
TO_FOREGROUND: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
state.zOrder = maxZLayer + 1;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
COPY_SHAPE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
copyShape(state);
}
},
PROPAGATE_OBJECT: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
propagateObject(state);
}
},
NEXT_KEY_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) {
const frame = typeof (state.keyframes.next) === 'number'
? state.keyframes.next : null;
if (frame !== null) {
changeFrame(frame);
}
}
},
PREV_KEY_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) {
const frame = typeof (state.keyframes.prev) === 'number'
? state.keyframes.prev : null;
if (frame !== null) {
changeFrame(frame);
}
}
},
};
return (
<ObjectsListComponent
{...this.props}
statesOrdering={statesOrdering}
sortedStatesID={sortedStatesID}
annotationsFilters={annotationsFilters}
annotationsFiltersHistory={annotationsFiltersHistory}
changeStatesOrdering={this.onChangeStatesOrdering}
changeAnnotationsFilters={this.onChangeAnnotationsFilters}
lockAllStates={this.onLockAllStates}
unlockAllStates={this.onUnlockAllStates}
collapseAllStates={this.onCollapseAllStates}
expandAllStates={this.onExpandAllStates}
hideAllStates={this.onHideAllStates}
showAllStates={this.onShowAllStates}
/>
<>
<GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
<ObjectsListComponent
{...this.props}
statesOrdering={statesOrdering}
sortedStatesID={sortedStatesID}
annotationsFilters={annotationsFilters}
changeStatesOrdering={this.onChangeStatesOrdering}
changeAnnotationsFilters={this.onChangeAnnotationsFilters}
annotationsFiltersHistory={annotationsFiltersHistory}
lockAllStates={this.onLockAllStates}
unlockAllStates={this.onUnlockAllStates}
collapseAllStates={this.onCollapseAllStates}
expandAllStates={this.onExpandAllStates}
hideAllStates={this.onHideAllStates}
showAllStates={this.onShowAllStates}
/>
</>
);
}
}

@ -8,7 +8,9 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { RouteComponentProps } from 'react-router-dom';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { InputNumber } from 'antd';
import { SliderValue } from 'antd/lib/slider';
import {
@ -19,6 +21,7 @@ import {
showStatistics as showStatisticsAction,
undoActionAsync,
redoActionAsync,
searchAnnotationsAsync,
} from 'actions/annotation-actions';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
@ -47,6 +50,7 @@ interface DispatchToProps {
showStatistics(sessionInstance: any): void;
undo(sessionInstance: any, frameNumber: any): void;
redo(sessionInstance: any, frameNumber: any): void;
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -123,14 +127,23 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
redo(sessionInstance: any, frameNumber: any): void {
dispatch(redoActionAsync(sessionInstance, frameNumber));
},
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void {
dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo));
},
};
}
type Props = StateToProps & DispatchToProps & RouteComponentProps;
class AnnotationTopBarContainer extends React.PureComponent<Props> {
private inputFrameRef: React.RefObject<InputNumber>;
private autoSaveInterval: number | undefined;
private unblock: any;
constructor(props: Props) {
super(props);
this.inputFrameRef = React.createRef<InputNumber>();
}
public componentDidMount(): void {
const {
autoSave,
@ -421,6 +434,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
playing,
saving,
savingStatuses,
jobInstance,
jobInstance: {
startFrame,
stopFrame,
@ -428,33 +442,179 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
frameNumber,
undoAction,
redoAction,
searchAnnotations,
canvasIsReady,
} = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
}
};
const keyMap = {
SAVE_JOB: {
name: 'Save the job',
description: 'Send all changes of annotations to the server',
sequence: 'ctrl+s',
action: 'keydown',
},
UNDO: {
name: 'Undo action',
description: 'Cancel the latest action related with objects',
sequence: 'ctrl+z',
action: 'keydown',
},
REDO: {
name: 'Redo action',
description: 'Cancel undo action',
sequences: ['ctrl+shift+z', 'ctrl+y'],
action: 'keydown',
},
NEXT_FRAME: {
name: 'Next frame',
description: 'Go to the next frame',
sequence: 'f',
action: 'keydown',
},
PREV_FRAME: {
name: 'Previous frame',
description: 'Go to the previous frame',
sequence: 'd',
action: 'keydown',
},
FORWARD_FRAME: {
name: 'Forward frame',
description: 'Go forward with a step',
sequence: 'v',
action: 'keydown',
},
BACKWARD_FRAME: {
name: 'Backward frame',
description: 'Go backward with a step',
sequence: 'c',
action: 'keydown',
},
SEARCH_FORWARD: {
name: 'Search forward',
description: 'Search the next frame that satisfies to the filters',
sequence: 'right',
action: 'keydown',
},
SEARCH_BACKWARD: {
name: 'Search backward',
description: 'Search the previous frame that satisfies to the filters',
sequence: 'left',
action: 'keydown',
},
PLAY_PAUSE: {
name: 'Play/pause',
description: 'Start/stop automatic changing frames',
sequence: 'space',
action: 'keydown',
},
FOCUS_INPUT_FRAME: {
name: 'Focus input frame',
description: 'Focus on the element to change the current frame',
sequences: ['`', '~'],
action: 'keydown',
},
};
const handlers = {
UNDO: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (undoAction) {
this.undo();
}
},
REDO: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (redoAction) {
this.redo();
}
},
SAVE_JOB: (event: KeyboardEvent | undefined) => {
preventDefault(event);
this.onSaveAnnotation();
},
NEXT_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (canvasIsReady) {
this.onNextFrame();
}
},
PREV_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (canvasIsReady) {
this.onPrevFrame();
}
},
FORWARD_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (canvasIsReady) {
this.onForward();
}
},
BACKWARD_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (canvasIsReady) {
this.onBackward();
}
},
SEARCH_FORWARD: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (frameNumber + 1 <= stopFrame && canvasIsReady) {
searchAnnotations(jobInstance, frameNumber + 1, stopFrame);
}
},
SEARCH_BACKWARD: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (frameNumber - 1 >= startFrame && canvasIsReady) {
searchAnnotations(jobInstance, frameNumber - 1, startFrame);
}
},
PLAY_PAUSE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
this.onSwitchPlay();
},
FOCUS_INPUT_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (this.inputFrameRef.current) {
this.inputFrameRef.current.focus();
}
},
};
return (
<AnnotationTopBarComponent
showStatistics={this.showStatistics}
onSwitchPlay={this.onSwitchPlay}
onSaveAnnotation={this.onSaveAnnotation}
onPrevFrame={this.onPrevFrame}
onNextFrame={this.onNextFrame}
onForward={this.onForward}
onBackward={this.onBackward}
onFirstFrame={this.onFirstFrame}
onLastFrame={this.onLastFrame}
onSliderChange={this.onChangePlayerSliderValue}
onInputChange={this.onChangePlayerInputValue}
onURLIconClick={this.onURLIconClick}
playing={playing}
saving={saving}
savingStatuses={savingStatuses}
startFrame={startFrame}
stopFrame={stopFrame}
frameNumber={frameNumber}
undoAction={undoAction}
redoAction={redoAction}
onUndoClick={this.undo}
onRedoClick={this.redo}
/>
<>
<GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
<AnnotationTopBarComponent
showStatistics={this.showStatistics}
onSwitchPlay={this.onSwitchPlay}
onSaveAnnotation={this.onSaveAnnotation}
onPrevFrame={this.onPrevFrame}
onNextFrame={this.onNextFrame}
onForward={this.onForward}
onBackward={this.onBackward}
onFirstFrame={this.onFirstFrame}
onLastFrame={this.onLastFrame}
onSliderChange={this.onChangePlayerSliderValue}
onInputChange={this.onChangePlayerInputValue}
onURLIconClick={this.onURLIconClick}
playing={playing}
saving={saving}
savingStatuses={savingStatuses}
startFrame={startFrame}
stopFrame={stopFrame}
frameNumber={frameNumber}
inputFrameRef={this.inputFrameRef}
undoAction={undoAction}
redoAction={redoAction}
onUndoClick={this.undo}
onRedoClick={this.redo}
/>
</>
);
}
}

@ -5,6 +5,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { connect, Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import CVATApplication from './components/cvat-app';
@ -16,6 +17,7 @@ import { getFormatsAsync } from './actions/formats-actions';
import { checkPluginsAsync } from './actions/plugins-actions';
import { getUsersAsync } from './actions/users-actions';
import { getAboutAsync } from './actions/about-actions';
import { shortcutsActions } from './actions/shortcuts-actions';
import {
resetErrors,
resetMessages,
@ -54,6 +56,7 @@ interface DispatchToProps {
initPlugins: () => void;
resetErrors: () => void;
resetMessages: () => void;
switchShortcutsDialog: () => void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -90,24 +93,21 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
loadAbout: (): void => dispatch(getAboutAsync()),
resetErrors: (): void => dispatch(resetErrors()),
resetMessages: (): void => dispatch(resetMessages()),
switchShortcutsDialog: (): void => dispatch(shortcutsActions.switchShortcutsDialog()),
};
}
function reduxAppWrapper(props: StateToProps & DispatchToProps): JSX.Element {
return (
<CVATApplication {...props} />
);
}
const ReduxAppWrapper = connect(
mapStateToProps,
mapDispatchToProps,
)(reduxAppWrapper);
)(CVATApplication);
ReactDOM.render(
(
<Provider store={cvatStore}>
<ReduxAppWrapper />
<BrowserRouter>
<ReduxAppWrapper />
</BrowserRouter>
</Provider>
),
document.getElementById('root'),

@ -61,7 +61,10 @@ const defaultState: AnnotationState = {
collapsed: {},
states: [],
filters: [],
filtersHistory: JSON.parse(window.localStorage.getItem('filtersHistory') as string) || [],
filtersHistory: JSON.parse(
window.localStorage.getItem('filtersHistory') || '[]',
),
resetGroupFlag: false,
history: {
undo: [],
redo: [],
@ -419,6 +422,21 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.REPEAT_DRAW_SHAPE: {
const { activeControl } = action.payload;
return {
...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: {
...state.canvas,
activeControl,
},
};
}
case AnnotationActionTypes.MERGE_OBJECTS: {
const { enabled } = action.payload;
const activeControl = enabled
@ -554,6 +572,24 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.RESET_ANNOTATIONS_GROUP: {
return {
...state,
annotations: {
...state.annotations,
resetGroupFlag: true,
},
};
}
case AnnotationActionTypes.GROUP_ANNOTATIONS: {
return {
...state,
annotations: {
...state.annotations,
resetGroupFlag: false,
},
};
}
case AnnotationActionTypes.GROUP_ANNOTATIONS_SUCCESS: {
const {
states,
@ -662,25 +698,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.COPY_SHAPE: {
const {
objectState,
} = action.payload;
state.canvas.instance.cancel();
state.canvas.instance.draw({
enabled: true,
initialState: objectState,
});
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (objectState.shapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (objectState.shapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (objectState.shapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
}
case AnnotationActionTypes.PASTE_SHAPE: {
const { activeControl } = action.payload;
return {
...state,
@ -694,6 +713,19 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.COPY_SHAPE: {
const {
objectState,
} = action.payload;
return {
...state,
drawing: {
...state.drawing,
activeInitialState: objectState,
},
};
}
case AnnotationActionTypes.EDIT_SHAPE: {
const { enabled } = action.payload;
const activeControl = enabled

@ -229,6 +229,7 @@ export interface NotificationsState {
fetchingAnnotations: null | ErrorState;
undo: null | ErrorState;
redo: null | ErrorState;
search: null | ErrorState;
};
[index: string]: any;
@ -329,6 +330,7 @@ export interface AnnotationState {
activeNumOfPoints?: number;
activeLabelID: number;
activeObjectType: ObjectType;
activeInitialState?: any;
};
annotations: {
selectedStatesID: number[];
@ -337,6 +339,7 @@ export interface AnnotationState {
states: any[];
filters: string[];
filtersHistory: string[];
resetGroupFlag: boolean;
history: {
undo: string[];
redo: string[];
@ -423,6 +426,10 @@ export interface SettingsState {
player: PlayerSettingsState;
}
export interface ShortcutsState {
visibleShortcutsHelp: boolean;
}
export interface CombinedState {
auth: AuthState;
tasks: TasksState;
@ -435,4 +442,5 @@ export interface CombinedState {
notifications: NotificationsState;
annotation: AnnotationState;
settings: SettingsState;
shortcuts: ShortcutsState;
}

@ -73,6 +73,7 @@ const defaultState: NotificationsState = {
fetchingAnnotations: null,
undo: null,
redo: null,
search: null,
},
},
messages: {
@ -750,6 +751,21 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case AnnotationActionTypes.SEARCH_ANNOTATIONS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
search: {
message: 'Could not execute search annotations',
reason: action.payload.error.toString(),
},
},
},
};
}
case NotificationsActionType.RESET_ERRORS: {
return {
...state,

@ -14,6 +14,7 @@ import modelsReducer from './models-reducer';
import notificationsReducer from './notifications-reducer';
import annotationReducer from './annotation-reducer';
import settingsReducer from './settings-reducer';
import shortcutsReducer from './shortcuts-reducer';
export default function createRootReducer(): Reducer {
return combineReducers({
@ -28,5 +29,6 @@ export default function createRootReducer(): Reducer {
notifications: notificationsReducer,
annotation: annotationReducer,
settings: settingsReducer,
shortcuts: shortcutsReducer,
});
}

@ -0,0 +1,20 @@
import { ShortcutsActions, ShortcutsActionsTypes } from 'actions/shortcuts-actions';
import { ShortcutsState } from './interfaces';
const defaultState: ShortcutsState = {
visibleShortcutsHelp: false,
};
export default (state = defaultState, action: ShortcutsActions): ShortcutsState => {
switch (action.type) {
case ShortcutsActionsTypes.SWITCH_SHORTCUT_DIALOG: {
return {
...state,
visibleShortcutsHelp: !state.visibleShortcutsHelp,
};
}
default: {
return state;
}
}
};
Loading…
Cancel
Save