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

@ -862,7 +862,7 @@
: (frame) => frame - 1; : (frame) => frame - 1;
for (let frame = frameFrom; predicate(frame); frame = update(frame)) { for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
// First prepare all data for the 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 // In particular consider first and last frame as keyframes for all frames
const statesData = [].concat( const statesData = [].concat(
(frame in this.shapes ? this.shapes[frame] : []) (frame in this.shapes ? this.shapes[frame] : [])
@ -876,7 +876,10 @@
|| frame === frameFrom || frame === frameFrom
|| frame === frameTo || 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 // Nothing to filtering, go to the next iteration
if (!statesData.length) { if (!statesData.length) {

@ -1343,7 +1343,7 @@
return annotationsData; 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')) { if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError( throw new ArgumentError(
'The filters argument must be an array of strings', 'The filters argument must be an array of strings',
@ -1555,7 +1555,7 @@
return result; 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')) { if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError( throw new ArgumentError(
'The filters argument must be an array of strings', 'The filters argument must be an array of strings',

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "0.1.0", "version": "0.5.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -9734,6 +9734,14 @@
"scheduler": "^0.17.0" "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": { "react-is": {
"version": "16.11.0", "version": "16.11.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz",

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

@ -82,8 +82,10 @@ export enum AnnotationActionTypes {
GROUP_OBJECTS = 'GROUP_OBJECTS', GROUP_OBJECTS = 'GROUP_OBJECTS',
SPLIT_TRACK = 'SPLIT_TRACK', SPLIT_TRACK = 'SPLIT_TRACK',
COPY_SHAPE = 'COPY_SHAPE', COPY_SHAPE = 'COPY_SHAPE',
PASTE_SHAPE = 'PASTE_SHAPE',
EDIT_SHAPE = 'EDIT_SHAPE', EDIT_SHAPE = 'EDIT_SHAPE',
DRAW_SHAPE = 'DRAW_SHAPE', DRAW_SHAPE = 'DRAW_SHAPE',
REPEAT_DRAW_SHAPE = 'REPEAT_DRAW_SHAPE',
SHAPE_DRAWN = 'SHAPE_DRAWN', SHAPE_DRAWN = 'SHAPE_DRAWN',
RESET_CANVAS = 'RESET_CANVAS', RESET_CANVAS = 'RESET_CANVAS',
UPDATE_ANNOTATIONS_SUCCESS = 'UPDATE_ANNOTATIONS_SUCCESS', UPDATE_ANNOTATIONS_SUCCESS = 'UPDATE_ANNOTATIONS_SUCCESS',
@ -92,6 +94,8 @@ export enum AnnotationActionTypes {
CREATE_ANNOTATIONS_FAILED = 'CREATE_ANNOTATIONS_FAILED', CREATE_ANNOTATIONS_FAILED = 'CREATE_ANNOTATIONS_FAILED',
MERGE_ANNOTATIONS_SUCCESS = 'MERGE_ANNOTATIONS_SUCCESS', MERGE_ANNOTATIONS_SUCCESS = 'MERGE_ANNOTATIONS_SUCCESS',
MERGE_ANNOTATIONS_FAILED = 'MERGE_ANNOTATIONS_FAILED', MERGE_ANNOTATIONS_FAILED = 'MERGE_ANNOTATIONS_FAILED',
RESET_ANNOTATIONS_GROUP = 'RESET_ANNOTATIONS_GROUP',
GROUP_ANNOTATIONS = 'GROUP_ANNOTATIONS',
GROUP_ANNOTATIONS_SUCCESS = 'GROUP_ANNOTATIONS_SUCCESS', GROUP_ANNOTATIONS_SUCCESS = 'GROUP_ANNOTATIONS_SUCCESS',
GROUP_ANNOTATIONS_FAILED = 'GROUP_ANNOTATIONS_FAILED', GROUP_ANNOTATIONS_FAILED = 'GROUP_ANNOTATIONS_FAILED',
SPLIT_ANNOTATIONS_SUCCESS = 'SPLIT_ANNOTATIONS_SUCCESS', SPLIT_ANNOTATIONS_SUCCESS = 'SPLIT_ANNOTATIONS_SUCCESS',
@ -133,6 +137,7 @@ export enum AnnotationActionTypes {
ROTATE_FRAME = 'ROTATE_FRAME', ROTATE_FRAME = 'ROTATE_FRAME',
SWITCH_Z_LAYER = 'SWITCH_Z_LAYER', SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
ADD_Z_LAYER = 'ADD_Z_LAYER', ADD_Z_LAYER = 'ADD_Z_LAYER',
SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED',
} }
export function addZLayer(): AnyAction { export function addZLayer(): AnyAction {
@ -905,6 +910,11 @@ export function updateAnnotationsAsync(sessionInstance: any, frame: number, stat
ThunkAction<Promise<void>, {}, {}, AnyAction> { ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) {
// deactivate object to visualize changes immediately (UX)
dispatch(activateObject(null));
}
const promises = statesToUpdate const promises = statesToUpdate
.map((objectState: any): Promise<any> => objectState.save()); .map((objectState: any): Promise<any> => objectState.save());
const states = await Promise.all(promises); const states = await Promise.all(promises);
@ -991,12 +1001,30 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}; };
} }
export function groupAnnotationsAsync(sessionInstance: any, frame: number, statesToGroup: any[]): export function resetAnnotationsGroup(): AnyAction {
ThunkAction<Promise<void>, {}, {}, 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> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); 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 const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters); .get(frame, showAllInterpolationTracks, filters);
const history = await sessionInstance.actions.get(); 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 // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { import {
Layout, Layout,
@ -12,17 +13,8 @@ import {
} from 'antd'; } from 'antd';
import { SliderValue } from 'antd/lib//slider'; import { SliderValue } from 'antd/lib//slider';
import { ColorBy, GridColor, ObjectType } from 'reducers/interfaces';
import { import { Canvas } from 'cvat-canvas';
ColorBy,
GridColor,
ObjectType,
} from 'reducers/interfaces';
import {
Canvas,
} from 'cvat-canvas';
import getCore from 'cvat-core'; import getCore from 'cvat-core';
const cvat = getCore(); const cvat = getCore();
@ -75,6 +67,12 @@ interface Props {
onUpdateContextMenu(visible: boolean, left: number, top: number): void; onUpdateContextMenu(visible: boolean, left: number, top: number): void;
onAddZLayer(): void; onAddZLayer(): void;
onSwitchZLayer(cur: number): 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> { export default class CanvasWrapperComponent extends React.PureComponent<Props> {
@ -109,6 +107,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
activatedStateID, activatedStateID,
curZLayer, curZLayer,
resetZoom, resetZoom,
grid,
gridOpacity,
gridColor,
brightnessLevel,
contrastLevel,
saturationLevel,
} = this.props; } = this.props;
if (prevProps.sidebarCollapsed !== sidebarCollapsed) { 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) { if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) {
this.updateCanvas(); this.updateCanvas();
} }
@ -360,7 +389,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
// Filters // Filters
const backgroundElement = window.document.getElementById('cvat_canvas_background'); const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) { 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 // 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 => { canvasInstance.html().addEventListener('contextmenu', (e: MouseEvent): void => {
const { const {
activatedStateID, activatedStateID,
@ -488,10 +525,162 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
minZLayer, minZLayer,
onSwitchZLayer, onSwitchZLayer,
onAddZLayer, onAddZLayer,
brightnessLevel,
contrastLevel,
saturationLevel,
grid,
gridColor,
gridOpacity,
onChangeBrightnessLevel,
onChangeSaturationLevel,
onChangeContrastLevel,
onChangeGridColor,
onChangeGridOpacity,
onSwitchGrid,
} = this.props; } = 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 ( return (
<Layout.Content style={{ position: 'relative' }}> <Layout.Content style={{ position: 'relative' }}>
<GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
{/* {/*
This element doesn't have any props This element doesn't have any props
So, React isn't going to rerender it So, React isn't going to rerender it

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { import {
Icon, Icon,
@ -12,7 +13,7 @@ import {
import { import {
ActiveControl, ActiveControl,
Rotation Rotation,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { import {
@ -44,6 +45,9 @@ interface Props {
groupObjects(enabled: boolean): void; groupObjects(enabled: boolean): void;
splitTrack(enabled: boolean): void; splitTrack(enabled: boolean): void;
rotateFrame(rotation: Rotation): void; rotateFrame(rotation: Rotation): void;
repeatDrawShape(): void;
pasteShape(): void;
resetGroup(): void;
} }
export default function ControlsSideBarComponent(props: Props): JSX.Element { export default function ControlsSideBarComponent(props: Props): JSX.Element {
@ -55,14 +59,140 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
groupObjects, groupObjects,
splitTrack, splitTrack,
rotateFrame, rotateFrame,
repeatDrawShape,
pasteShape,
resetGroup,
} = props; } = 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 ( return (
<Layout.Sider <Layout.Sider
className='cvat-canvas-controls-sidebar' className='cvat-canvas-controls-sidebar'
theme='light' theme='light'
width={44} width={44}
> >
<GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
<CursorControl canvasInstance={canvasInstance} activeControl={activeControl} /> <CursorControl canvasInstance={canvasInstance} activeControl={activeControl} />
<MoveControl canvasInstance={canvasInstance} activeControl={activeControl} /> <MoveControl canvasInstance={canvasInstance} activeControl={activeControl} />
<RotateControl rotateFrame={rotateFrame} /> <RotateControl rotateFrame={rotateFrame} />

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

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

@ -5,18 +5,17 @@
import 'antd/dist/antd.less'; import 'antd/dist/antd.less';
import '../styles.scss'; import '../styles.scss';
import React from 'react'; import React from 'react';
import { BrowserRouter } from 'react-router-dom'; import { Switch, Route, Redirect } from 'react-router';
import { import { withRouter, RouteComponentProps } from 'react-router-dom';
Switch, import { GlobalHotKeys, KeyMap, configure } from 'react-hotkeys';
Route,
Redirect,
} from 'react-router';
import { import {
Spin, Spin,
Layout, Layout,
notification, notification,
} from 'antd'; } from 'antd';
import ShorcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog';
import SettingsPageContainer from 'containers/settings-page/settings-page'; import SettingsPageContainer from 'containers/settings-page/settings-page';
import TasksPageContainer from 'containers/tasks-page/tasks-page'; import TasksPageContainer from 'containers/tasks-page/tasks-page';
import CreateTaskPageContainer from 'containers/create-task-page/create-task-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'; import { NotificationsState } from 'reducers/interfaces';
type CVATAppProps = { interface CVATAppProps {
loadFormats: () => void; loadFormats: () => void;
loadUsers: () => void; loadUsers: () => void;
loadAbout: () => void; loadAbout: () => void;
@ -38,6 +37,7 @@ type CVATAppProps = {
initPlugins: () => void; initPlugins: () => void;
resetErrors: () => void; resetErrors: () => void;
resetMessages: () => void; resetMessages: () => void;
switchShortcutsDialog: () => void;
userInitialized: boolean; userInitialized: boolean;
pluginsInitialized: boolean; pluginsInitialized: boolean;
pluginsFetching: boolean; pluginsFetching: boolean;
@ -52,11 +52,12 @@ type CVATAppProps = {
installedTFSegmentation: boolean; installedTFSegmentation: boolean;
notifications: NotificationsState; notifications: NotificationsState;
user: any; user: any;
}; }
export default class CVATApplication extends React.PureComponent<CVATAppProps> { class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentProps> {
public componentDidMount(): void { public componentDidMount(): void {
const { verifyAuthorized } = this.props; const { verifyAuthorized } = this.props;
configure({ ignoreRepeatedEventsWhenKeyHeldDown: false });
verifyAuthorized(); verifyAuthorized();
} }
@ -190,7 +191,9 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
installedAutoAnnotation, installedAutoAnnotation,
installedTFSegmentation, installedTFSegmentation,
installedTFAnnotation, installedTFAnnotation,
switchShortcutsDialog,
user, user,
history,
} = this.props; } = this.props;
const readyForRender = (userInitialized && user == null) const readyForRender = (userInitialized && user == null)
@ -200,13 +203,50 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
const withModels = installedAutoAnnotation const withModels = installedAutoAnnotation
|| installedTFAnnotation || installedTFSegmentation; || 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 (readyForRender) {
if (user) { if (user) {
return ( return (
<BrowserRouter> <Layout>
<Layout> <HeaderContainer> </HeaderContainer>
<HeaderContainer> </HeaderContainer> <Layout.Content>
<Layout.Content> <ShorcutsDialog />
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers}>
<Switch> <Switch>
<Route exact path='/settings' component={SettingsPageContainer} /> <Route exact path='/settings' component={SettingsPageContainer} />
<Route exact path='/tasks' component={TasksPageContainer} /> <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} /> } && <Route exact path='/models/create' component={CreateModelPageContainer} /> }
<Redirect push to='/tasks' /> <Redirect push to='/tasks' />
</Switch> </Switch>
{/* eslint-disable-next-line */} </GlobalHotKeys>
<a id='downloadAnchor' style={{ display: 'none' }} download/> {/* eslint-disable-next-line */}
</Layout.Content> <a id='downloadAnchor' style={{ display: 'none' }} download/>
</Layout> </Layout.Content>
</BrowserRouter> </Layout>
); );
} }
return ( return (
<BrowserRouter> <Switch>
<Switch> <Route exact path='/auth/register' component={RegisterPageContainer} />
<Route exact path='/auth/register' component={RegisterPageContainer} /> <Route exact path='/auth/login' component={LoginPageContainer} />
<Route exact path='/auth/login' component={LoginPageContainer} /> <Redirect to='/auth/login' />
<Redirect to='/auth/login' /> </Switch>
</Switch>
</BrowserRouter>
); );
} }
@ -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 // SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { RouteComponentProps } from 'react-router'; 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( export default withRouter(
connect( connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps, mapDispatchToProps,
)(AnnotationPageContainer), )(AnnotationPageComponent),
); );

@ -2,11 +2,9 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import CanvasWrapperComponent from 'components/annotation-page/standard-workspace/canvas-wrapper'; import CanvasWrapperComponent from 'components/annotation-page/standard-workspace/canvas-wrapper';
import { import {
confirmCanvasReady, confirmCanvasReady,
dragCanvas, dragCanvas,
@ -28,6 +26,14 @@ import {
addZLayer, addZLayer,
switchZLayer, switchZLayer,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import {
switchGrid,
changeGridColor,
changeGridOpacity,
changeBrightnessLevel,
changeContrastLevel,
changeSaturationLevel,
} from 'actions/settings-actions';
import { import {
ColorBy, ColorBy,
GridColor, GridColor,
@ -86,6 +92,12 @@ interface DispatchToProps {
onUpdateContextMenu(visible: boolean, left: number, top: number): void; onUpdateContextMenu(visible: boolean, left: number, top: number): void;
onAddZLayer(): void; onAddZLayer(): void;
onSwitchZLayer(cur: number): 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 { function mapStateToProps(state: CombinedState): StateToProps {
@ -233,16 +245,28 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSwitchZLayer(cur: number): void { onSwitchZLayer(cur: number): void {
dispatch(switchZLayer(cur)); 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( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps, mapDispatchToProps,
)(CanvasWrapperContainer); )(CanvasWrapperComponent);

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

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

@ -2,7 +2,6 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import LabelsListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/labels-list'; 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( export default connect(
mapStateToProps, mapStateToProps,
)(LabelsListContainer); )(LabelsListComponent);

@ -20,6 +20,7 @@ import {
copyShape as copyShapeAction, copyShape as copyShapeAction,
activateObject as activateObjectAction, activateObject as activateObjectAction,
propagateObject as propagateObjectAction, propagateObject as propagateObjectAction,
pasteShapeAsync,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item'; 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 { copyShape(objectState: any): void {
dispatch(copyShapeAction(objectState)); dispatch(copyShapeAction(objectState));
dispatch(pasteShapeAsync());
}, },
propagateObject(objectState: any): void { propagateObject(objectState: any): void {
dispatch(propagateObjectAction(objectState)); dispatch(propagateObjectAction(objectState));

@ -4,6 +4,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { SelectValue } from 'antd/lib/select'; import { SelectValue } from 'antd/lib/select';
@ -11,13 +12,18 @@ import ObjectsListComponent from 'components/annotation-page/standard-workspace/
import { import {
updateAnnotationsAsync, updateAnnotationsAsync,
fetchAnnotationsAsync, fetchAnnotationsAsync,
removeObjectAsync,
changeFrameAsync,
changeAnnotationsFilters as changeAnnotationsFiltersAction, changeAnnotationsFilters as changeAnnotationsFiltersAction,
collapseObjectItems, collapseObjectItems,
copyShape as copyShapeAction,
propagateObject as propagateObjectAction,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import { import {
CombinedState, CombinedState,
StatesOrdering, StatesOrdering,
ObjectType,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
interface StateToProps { interface StateToProps {
@ -29,6 +35,9 @@ interface StateToProps {
statesCollapsed: boolean; statesCollapsed: boolean;
objectStates: any[]; objectStates: any[];
annotationsFilters: string[]; annotationsFilters: string[];
activatedStateID: number | null;
minZLayer: number;
maxZLayer: number;
annotationsFiltersHistory: string[]; annotationsFiltersHistory: string[];
} }
@ -36,6 +45,10 @@ interface DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void; updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void;
changeAnnotationsFilters(sessionInstance: any, filters: string[]): void; changeAnnotationsFilters(sessionInstance: any, filters: string[]): void;
collapseStates(states: any[], value: boolean): 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 { function mapStateToProps(state: CombinedState): StateToProps {
@ -46,6 +59,11 @@ function mapStateToProps(state: CombinedState): StateToProps {
filters: annotationsFilters, filters: annotationsFilters,
filtersHistory: annotationsFiltersHistory, filtersHistory: annotationsFiltersHistory,
collapsed, collapsed,
activatedStateID,
zLayer: {
min: minZLayer,
max: maxZLayer,
},
}, },
job: { job: {
instance: jobInstance, instance: jobInstance,
@ -64,9 +82,11 @@ function mapStateToProps(state: CombinedState): StateToProps {
let statesCollapsed = true; let statesCollapsed = true;
objectStates.forEach((objectState: any) => { objectStates.forEach((objectState: any) => {
const { clientID } = objectState; const { clientID, lock } = objectState;
statesHidden = statesHidden && objectState.hidden; if (!lock) {
statesLocked = statesLocked && objectState.lock; statesHidden = statesHidden && objectState.hidden;
statesLocked = statesLocked && objectState.lock;
}
const stateCollapsed = clientID in collapsed ? collapsed[clientID] : true; const stateCollapsed = clientID in collapsed ? collapsed[clientID] : true;
statesCollapsed = statesCollapsed && stateCollapsed; statesCollapsed = statesCollapsed && stateCollapsed;
}); });
@ -80,6 +100,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
frameNumber, frameNumber,
jobInstance, jobInstance,
annotationsFilters, annotationsFilters,
activatedStateID,
minZLayer,
maxZLayer,
annotationsFiltersHistory, annotationsFiltersHistory,
}; };
} }
@ -99,6 +122,18 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(changeAnnotationsFiltersAction(filters)); dispatch(changeAnnotationsFiltersAction(filters));
dispatch(fetchAnnotationsAsync(sessionInstance)); 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 { public render(): JSX.Element {
const { const {
annotationsFilters, annotationsFilters,
statesHidden,
statesLocked,
activatedStateID,
objectStates,
frameNumber,
jobInstance,
updateAnnotations,
removeObject,
copyShape,
propagateObject,
changeFrame,
maxZLayer,
minZLayer,
annotationsFiltersHistory, annotationsFiltersHistory,
} = this.props; } = this.props;
const { const {
@ -233,22 +281,241 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
statesOrdering, statesOrdering,
} = this.state; } = 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 ( return (
<ObjectsListComponent <>
{...this.props} <GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
statesOrdering={statesOrdering} <ObjectsListComponent
sortedStatesID={sortedStatesID} {...this.props}
annotationsFilters={annotationsFilters} statesOrdering={statesOrdering}
annotationsFiltersHistory={annotationsFiltersHistory} sortedStatesID={sortedStatesID}
changeStatesOrdering={this.onChangeStatesOrdering} annotationsFilters={annotationsFilters}
changeAnnotationsFilters={this.onChangeAnnotationsFilters} changeStatesOrdering={this.onChangeStatesOrdering}
lockAllStates={this.onLockAllStates} changeAnnotationsFilters={this.onChangeAnnotationsFilters}
unlockAllStates={this.onUnlockAllStates} annotationsFiltersHistory={annotationsFiltersHistory}
collapseAllStates={this.onCollapseAllStates} lockAllStates={this.onLockAllStates}
expandAllStates={this.onExpandAllStates} unlockAllStates={this.onUnlockAllStates}
hideAllStates={this.onHideAllStates} collapseAllStates={this.onCollapseAllStates}
showAllStates={this.onShowAllStates} expandAllStates={this.onExpandAllStates}
/> hideAllStates={this.onHideAllStates}
showAllStates={this.onShowAllStates}
/>
</>
); );
} }
} }

@ -8,7 +8,9 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { RouteComponentProps } from 'react-router-dom'; import { RouteComponentProps } from 'react-router-dom';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { InputNumber } from 'antd';
import { SliderValue } from 'antd/lib/slider'; import { SliderValue } from 'antd/lib/slider';
import { import {
@ -19,6 +21,7 @@ import {
showStatistics as showStatisticsAction, showStatistics as showStatisticsAction,
undoActionAsync, undoActionAsync,
redoActionAsync, redoActionAsync,
searchAnnotationsAsync,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar'; import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
@ -47,6 +50,7 @@ interface DispatchToProps {
showStatistics(sessionInstance: any): void; showStatistics(sessionInstance: any): void;
undo(sessionInstance: any, frameNumber: any): void; undo(sessionInstance: any, frameNumber: any): void;
redo(sessionInstance: any, frameNumber: any): void; redo(sessionInstance: any, frameNumber: any): void;
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -123,14 +127,23 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
redo(sessionInstance: any, frameNumber: any): void { redo(sessionInstance: any, frameNumber: any): void {
dispatch(redoActionAsync(sessionInstance, frameNumber)); dispatch(redoActionAsync(sessionInstance, frameNumber));
}, },
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void {
dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo));
},
}; };
} }
type Props = StateToProps & DispatchToProps & RouteComponentProps; type Props = StateToProps & DispatchToProps & RouteComponentProps;
class AnnotationTopBarContainer extends React.PureComponent<Props> { class AnnotationTopBarContainer extends React.PureComponent<Props> {
private inputFrameRef: React.RefObject<InputNumber>;
private autoSaveInterval: number | undefined; private autoSaveInterval: number | undefined;
private unblock: any; private unblock: any;
constructor(props: Props) {
super(props);
this.inputFrameRef = React.createRef<InputNumber>();
}
public componentDidMount(): void { public componentDidMount(): void {
const { const {
autoSave, autoSave,
@ -421,6 +434,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
playing, playing,
saving, saving,
savingStatuses, savingStatuses,
jobInstance,
jobInstance: { jobInstance: {
startFrame, startFrame,
stopFrame, stopFrame,
@ -428,33 +442,179 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
frameNumber, frameNumber,
undoAction, undoAction,
redoAction, redoAction,
searchAnnotations,
canvasIsReady,
} = this.props; } = 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 ( return (
<AnnotationTopBarComponent <>
showStatistics={this.showStatistics} <GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
onSwitchPlay={this.onSwitchPlay} <AnnotationTopBarComponent
onSaveAnnotation={this.onSaveAnnotation} showStatistics={this.showStatistics}
onPrevFrame={this.onPrevFrame} onSwitchPlay={this.onSwitchPlay}
onNextFrame={this.onNextFrame} onSaveAnnotation={this.onSaveAnnotation}
onForward={this.onForward} onPrevFrame={this.onPrevFrame}
onBackward={this.onBackward} onNextFrame={this.onNextFrame}
onFirstFrame={this.onFirstFrame} onForward={this.onForward}
onLastFrame={this.onLastFrame} onBackward={this.onBackward}
onSliderChange={this.onChangePlayerSliderValue} onFirstFrame={this.onFirstFrame}
onInputChange={this.onChangePlayerInputValue} onLastFrame={this.onLastFrame}
onURLIconClick={this.onURLIconClick} onSliderChange={this.onChangePlayerSliderValue}
playing={playing} onInputChange={this.onChangePlayerInputValue}
saving={saving} onURLIconClick={this.onURLIconClick}
savingStatuses={savingStatuses} playing={playing}
startFrame={startFrame} saving={saving}
stopFrame={stopFrame} savingStatuses={savingStatuses}
frameNumber={frameNumber} startFrame={startFrame}
undoAction={undoAction} stopFrame={stopFrame}
redoAction={redoAction} frameNumber={frameNumber}
onUndoClick={this.undo} inputFrameRef={this.inputFrameRef}
onRedoClick={this.redo} undoAction={undoAction}
/> redoAction={redoAction}
onUndoClick={this.undo}
onRedoClick={this.redo}
/>
</>
); );
} }
} }

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

@ -61,7 +61,10 @@ const defaultState: AnnotationState = {
collapsed: {}, collapsed: {},
states: [], states: [],
filters: [], filters: [],
filtersHistory: JSON.parse(window.localStorage.getItem('filtersHistory') as string) || [], filtersHistory: JSON.parse(
window.localStorage.getItem('filtersHistory') || '[]',
),
resetGroupFlag: false,
history: { history: {
undo: [], undo: [],
redo: [], 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: { case AnnotationActionTypes.MERGE_OBJECTS: {
const { enabled } = action.payload; const { enabled } = action.payload;
const activeControl = enabled 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: { case AnnotationActionTypes.GROUP_ANNOTATIONS_SUCCESS: {
const { const {
states, states,
@ -662,25 +698,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
}; };
} }
case AnnotationActionTypes.COPY_SHAPE: { case AnnotationActionTypes.PASTE_SHAPE: {
const { const { activeControl } = action.payload;
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;
}
return { return {
...state, ...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: { case AnnotationActionTypes.EDIT_SHAPE: {
const { enabled } = action.payload; const { enabled } = action.payload;
const activeControl = enabled const activeControl = enabled

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

@ -73,6 +73,7 @@ const defaultState: NotificationsState = {
fetchingAnnotations: null, fetchingAnnotations: null,
undo: null, undo: null,
redo: null, redo: null,
search: null,
}, },
}, },
messages: { 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: { case NotificationsActionType.RESET_ERRORS: {
return { return {
...state, ...state,

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