feat: Help menu displaying keyboard shortcuts (#689)

Help menu that shows icon and keyboard shortcuts for registered actions. Refactored keyboard management to only allow one handler per key event/key combo

[AB#17122]
Esse commit está contido em:
Tanner Barlow
2019-03-22 10:00:45 -07:00
commit de Wallace Breza
commit b0cd040572
27 arquivos alterados com 548 adições e 215 exclusões
+12
Ver Arquivo
@@ -121,3 +121,15 @@
overflow: auto;
}
}
.app-help-menu-icon {
padding: 6px 10px;
color: #ccc;
display: inline-block;
&:hover {
color: #fff;
background-color: $lighter-2;
cursor: pointer;
}
}
+2 -1
Ver Arquivo
@@ -13,8 +13,8 @@ import { ErrorHandler } from "./react/components/common/errorHandler/errorHandle
import { KeyboardManager } from "./react/components/common/keyboardManager/keyboardManager";
import { TitleBar } from "./react/components/shell/titleBar";
import { StatusBar } from "./react/components/shell/statusBar";
import { strings } from "./common/strings";
import { StatusBarMetrics } from "./react/components/shell/statusBarMetrics";
import { HelpMenu } from "./react/components/shell/helpMenu";
interface IAppProps {
currentProject?: IProject;
@@ -77,6 +77,7 @@ export default class App extends React.Component<IAppProps> {
<div className={`app-shell platform-${platform}`}>
<TitleBar icon="fas fa-tags"
title={this.props.currentProject ? this.props.currentProject.name : ""}>
<div className="app-help-menu-icon"><HelpMenu/></div>
</TitleBar>
<div className="app-main">
<Sidebar project={this.props.currentProject} />
+27 -11
Ver Arquivo
@@ -15,6 +15,13 @@ export const english: IAppStrings = {
provider: "Provider",
homePage: "Home Page",
},
titleBar: {
help: "Help",
minimize: "Minimize",
maximize: "Maximize",
restore: "Restore",
close: "Close",
},
homePage: {
newProject: "New Project",
openLocalProject: {
@@ -167,17 +174,17 @@ export const english: IAppStrings = {
toolbar: {
select: "Select (V)",
pan: "Pan",
drawRectangle: "Draw Rectangle (R)",
drawPolygon: "Draw Polygon (P)",
copyRectangle: "Copy Rectangle (Ctrl + W)",
copy: "Copy Regions (Ctrl + C)",
cut: "Cut Regions (Ctrl + X)",
paste: "Paste Regions (Ctrl + V)",
removeAllRegions: "Remove All Regions (Ctrl + Delete)",
previousAsset: "Previous Asset (W)",
nextAsset: "Next Asset (S)",
saveProject: "Save Project (Ctrl + S)",
exportProject: "Export Project (Ctrl + E)",
drawRectangle: "Draw Rectangle",
drawPolygon: "Draw Polygon",
copyRectangle: "Copy Rectangle",
copy: "Copy Regions",
cut: "Cut Regions",
paste: "Paste Regions",
removeAllRegions: "Remove All Regions",
previousAsset: "Previous Asset",
nextAsset: "Next Asset",
saveProject: "Save Project",
exportProject: "Export Project",
},
videoPlayer: {
previousTaggedFrame: {
@@ -193,7 +200,16 @@ export const english: IAppStrings = {
tooltip: "Next Frame",
},
},
help: {
title: "Toggle Help Menu",
escape: "Escape Help Menu",
},
assetError: "Unable to load asset",
tags: {
hotKey: {
help: "Apply Tag with Hot Key",
},
},
canvas: {
removeAllRegions: {
title: "Remove All Regions",
+27 -11
Ver Arquivo
@@ -15,6 +15,13 @@ export const spanish: IAppStrings = {
provider: "Proveedor",
homePage: "Página de Inicio",
},
titleBar: {
help: "Ayuda",
minimize: "Minimizar",
maximize: "Maximizar",
restore: "Restaurar",
close: "Cerrar",
},
homePage: {
newProject: "Nuevo Proyecto",
recentProjects: "Proyectos Recientes",
@@ -168,17 +175,17 @@ export const spanish: IAppStrings = {
toolbar: {
select: "Seleccionar",
pan: "Pan",
drawRectangle: "Dibujar Rectángulo (R)",
drawPolygon: "Dibujar Polígono (P)",
copyRectangle: "Copia rectángulo (Ctrl + W)",
copy: "Copiar regiones (Ctrl + C)",
cut: "Cortar regiones (Ctrl + X)",
paste: "Pegar regiones (Ctrl + V)",
removeAllRegions: "Eliminar Todas Las Regiones (Ctrl + Delete)",
previousAsset: "Activo anterior (W)",
nextAsset: "Siguiente activo (S)",
saveProject: "Guardar Proyecto (Ctrl + S)",
exportProject: "Exprtar Proyecto (Ctrl + E)",
drawRectangle: "Dibujar Rectángulo",
drawPolygon: "Dibujar Polígono",
copyRectangle: "Copia rectángulo",
copy: "Copiar regiones",
cut: "Cortar regiones",
paste: "Pegar regiones",
removeAllRegions: "Eliminar Todas Las Regiones",
previousAsset: "Activo anterior",
nextAsset: "Siguiente activo",
saveProject: "Guardar Proyecto",
exportProject: "Exprtar Proyecto",
},
videoPlayer: {
previousTaggedFrame: {
@@ -194,7 +201,16 @@ export const spanish: IAppStrings = {
tooltip: "Siguiente marco",
},
},
help: {
title: "Abrir/cerrar el menú de ayuda",
escape: "Escapar el menú de ayuda",
},
assetError: "No se puede mostrar el activo",
tags: {
hotKey: {
help: "Aplicar etiqueta con tecla de acceso rápido",
},
},
canvas: {
removeAllRegions: {
title: "Borrar Regiones",
+29
Ver Arquivo
@@ -29,6 +29,9 @@ import { RegionDataType, RegionData } from "vott-ct/lib/js/CanvasTools/Core/Regi
import { randomIntInRange } from "./utils";
import { appInfo } from "./appInfo";
import { SelectionMode } from "vott-ct/lib/js/CanvasTools/Interface/ISelectorSettings";
import { IKeyboardBindingProps } from "../react/components/common/keyboardBinding/keyboardBinding";
import { KeyEventType } from "../react/components/common/keyboardManager/keyboardManager";
import { IKeyboardRegistrations } from "../react/components/common/keyboardManager/keyboardRegistrationManager";
export default class MockFactory {
@@ -942,6 +945,32 @@ export default class MockFactory {
};
}
public static createKeyboardRegistrations(count = 5, handlers?): IKeyboardRegistrations {
const keyDownRegs = {};
if (!handlers) {
handlers = [];
for (let i = 0; i < count; i++) {
handlers.push(jest.fn(() => i));
}
}
for (let i = 0; i < count; i++) {
const upper = String.fromCharCode(65 + i);
const lower = String.fromCharCode(97 + i);
const binding: IKeyboardBindingProps = {
displayName: `Binding ${i + 1}`,
accelerators: [upper, lower],
handler: handlers[i],
icon: `test-icon-${i + 1}`,
keyEventType: KeyEventType.KeyDown,
};
keyDownRegs[upper] = binding;
keyDownRegs[lower] = binding;
}
return {
keydown: keyDownRegs,
};
}
private static pageProps(projectId: string, method: string) {
return {
project: null,
+16
Ver Arquivo
@@ -18,6 +18,13 @@ export interface IAppStrings {
provider: string;
homePage: string;
};
titleBar: {
help: string;
minimize: string;
maximize: string;
restore: string;
close: string;
};
homePage: {
newProject: string;
openLocalProject: {
@@ -195,7 +202,16 @@ export interface IAppStrings {
tooltip: string,
},
}
help: {
title: string;
escape: string;
}
assetError: string;
tags: {
hotKey: {
help: string;
},
}
canvas: {
removeAllRegions: {
title: string;
+6 -5
Ver Arquivo
@@ -38,16 +38,17 @@ export enum ErrorCode {
// the enum key is in Pascal casing
Unknown = "unknown",
GenericRenderError = "genericRenderError",
CanvasError = "canvasError",
V1ImportError = "v1ImportError",
ProjectUploadError = "projectUploadError",
ProjectDeleteError = "projectDeleteError",
ProjectInvalidJson = "projectInvalidJson",
ProjectInvalidSecurityToken = "projectInvalidSecurityToken",
ProjectDuplicateName = "projectDuplicateName",
ProjectUploadError = "projectUploadError",
ProjectDeleteError = "projectDeleteError",
SecurityTokenNotFound = "securityTokenNotFound",
ExportFormatNotFound = "exportFormatNotFound",
CanvasError = "canvasError",
V1ImportError = "v1ImportError",
PasteRegionTooBigError = "pasteRegionTooBigError",
PasteRegionTooBig = "pasteRegionTooBig",
OverloadedKeyBinding = "overloadedKeyBinding",
}
/**
@@ -79,13 +79,17 @@ export class VideoAsset extends React.Component<IVideoAssetProps> {
<CustomVideoPlayerButton order={1.1}
accelerators={["ArrowLeft", "a", "A"]}
tooltip={strings.editorPage.videoPlayer.previousExpectedFrame.tooltip}
onClick={this.movePreviousExpectedFrame}>
onClick={this.movePreviousExpectedFrame}
icon={"fa-caret-left fa-lg"}
>
<i className="fas fa-caret-left fa-lg" />
</CustomVideoPlayerButton>
<CustomVideoPlayerButton order={1.2}
accelerators={["ArrowRight", "d", "D"]}
tooltip={strings.editorPage.videoPlayer.nextExpectedFrame.tooltip}
onClick={this.moveNextExpectedFrame}>
onClick={this.moveNextExpectedFrame}
icon={"fa-caret-right fa-lg"}
>
<i className="fas fa-caret-right fa-lg" />
</CustomVideoPlayerButton>
<CurrentTimeDisplay order={1.3} />
@@ -95,13 +99,17 @@ export class VideoAsset extends React.Component<IVideoAssetProps> {
<CustomVideoPlayerButton order={8.1}
accelerators={["q", "Q"]}
tooltip={strings.editorPage.videoPlayer.previousTaggedFrame.tooltip}
onClick={this.movePreviousTaggedFrame}>
onClick={this.movePreviousTaggedFrame}
icon={"fas fa-step-backward"}
>
<i className="fas fa-step-backward"></i>
</CustomVideoPlayerButton>
<CustomVideoPlayerButton order={8.2}
accelerators={["e", "E"]}
tooltip={strings.editorPage.videoPlayer.nextTaggedFrame.tooltip}
onClick={this.moveNextTaggedFrame}>
onClick={this.moveNextTaggedFrame}
icon={"fa-step-forward"}
>
<i className="fas fa-step-forward"></i>
</CustomVideoPlayerButton>
</ControlBar>
@@ -13,13 +13,14 @@ describe("Keyboard Binding Component", () => {
const accelerators = ["Ctrl+1"];
const defaultProps: IKeyboardBindingProps = {
displayName: "Keyboard binding",
keyEventType: KeyEventType.KeyDown,
accelerators,
onKeyEvent: onKeyDownHandler,
handler: onKeyDownHandler,
};
const registrationMock = KeyboardRegistrationManager as jest.Mocked<typeof KeyboardRegistrationManager>;
registrationMock.prototype.addHandler = jest.fn(() => deregisterFunc);
registrationMock.prototype.registerBinding = jest.fn(() => deregisterFunc);
function createComponent(props?: IKeyboardBindingProps): ReactWrapper {
props = props || defaultProps;
@@ -43,8 +44,13 @@ describe("Keyboard Binding Component", () => {
it("registered the keydown key code and event handler", () => {
wrapper = createComponent();
expect(registrationMock.prototype.addHandler).toBeCalledWith(
KeyEventType.KeyDown, defaultProps.accelerators, defaultProps.onKeyEvent);
const expectedBindingProps: IKeyboardBindingProps = {
accelerators: defaultProps.accelerators,
keyEventType: KeyEventType.KeyDown,
handler: defaultProps.handler,
displayName: expect.any(String),
};
expect(registrationMock.prototype.registerBinding).toBeCalledWith(expectedBindingProps);
});
it("registered the keyup key code and event handler", () => {
@@ -52,8 +58,13 @@ describe("Keyboard Binding Component", () => {
...defaultProps,
keyEventType: KeyEventType.KeyUp,
});
expect(registrationMock.prototype.addHandler).toBeCalledWith(
KeyEventType.KeyUp, defaultProps.accelerators, defaultProps.onKeyEvent);
const expectedBindingProps: IKeyboardBindingProps = {
accelerators: defaultProps.accelerators,
keyEventType: KeyEventType.KeyUp,
handler: defaultProps.handler,
displayName: expect.any(String),
};
expect(registrationMock.prototype.registerBinding).toBeCalledWith(expectedBindingProps);
});
it("registered the keypress key code and event handler", () => {
@@ -61,8 +72,13 @@ describe("Keyboard Binding Component", () => {
...defaultProps,
keyEventType: KeyEventType.KeyPress,
});
expect(registrationMock.prototype.addHandler).toBeCalledWith(
KeyEventType.KeyPress, defaultProps.accelerators, defaultProps.onKeyEvent);
const expectedBindingProps: IKeyboardBindingProps = {
accelerators: defaultProps.accelerators,
keyEventType: KeyEventType.KeyPress,
handler: defaultProps.handler,
displayName: expect.any(String),
};
expect(registrationMock.prototype.registerBinding).toBeCalledWith(expectedBindingProps);
});
it("deregisters the event handler", () => {
@@ -1,10 +1,20 @@
import { KeyboardContext, IKeyboardContext, KeyEventType } from "../keyboardManager/keyboardManager";
import React from "react";
/**
* Properties needed for a keyboard binding
*/
export interface IKeyboardBindingProps {
/** Keys that the action is bound to */
accelerators: string[];
onKeyEvent: (evt?: KeyboardEvent) => void;
/** Friendly name for keyboard binding for display in help menu */
displayName: string;
/** Action to trigger upon key event */
handler: (evt?: KeyboardEvent) => void;
/** Type of key event (keypress, keyup, keydown) */
keyEventType?: KeyEventType;
/** Icon to display in help menu */
icon?: string;
}
export class KeyboardBinding extends React.Component<IKeyboardBindingProps> {
@@ -14,11 +24,7 @@ export class KeyboardBinding extends React.Component<IKeyboardBindingProps> {
public componentDidMount() {
if (this.context && this.context.keyboard) {
this.deregisterBinding = this.context.keyboard.addHandler(
this.props.keyEventType || KeyEventType.KeyDown,
this.props.accelerators,
this.props.onKeyEvent,
);
this.deregisterBinding = this.context.keyboard.registerBinding(this.props);
} else {
console.warn("Keyboard Mananger context cannot be found - Keyboard binding has NOT been set.");
}
@@ -21,7 +21,7 @@ describe("Keyboard Manager Component", () => {
}
beforeEach(() => {
(registrationManagerMock.prototype.invokeHandlers as any).mockClear();
(registrationManagerMock.prototype.invokeHandler as any).mockClear();
addEventListenerSpy = jest.spyOn(window, "addEventListener");
removeEventListenerSpy = jest.spyOn(window, "removeEventListener");
wrapper = createComponent();
@@ -53,7 +53,7 @@ describe("Keyboard Manager Component", () => {
window.dispatchEvent(keyboardEvent);
expect(registrationManagerMock.prototype.invokeHandlers)
expect(registrationManagerMock.prototype.invokeHandler)
.toBeCalledWith(KeyEventType.KeyDown, "Ctrl+1", keyboardEvent);
});
@@ -66,7 +66,7 @@ describe("Keyboard Manager Component", () => {
window.dispatchEvent(keyboardEvent);
expect(registrationManagerMock.prototype.invokeHandlers)
expect(registrationManagerMock.prototype.invokeHandler)
.toBeCalledWith(KeyEventType.KeyUp, "Ctrl+1", keyboardEvent);
});
@@ -79,7 +79,7 @@ describe("Keyboard Manager Component", () => {
window.dispatchEvent(keyboardEvent);
expect(registrationManagerMock.prototype.invokeHandlers)
expect(registrationManagerMock.prototype.invokeHandler)
.toBeCalledWith(KeyEventType.KeyPress, "Ctrl+1", keyboardEvent);
});
@@ -93,7 +93,7 @@ describe("Keyboard Manager Component", () => {
window.dispatchEvent(keyboardEvent);
expect(registrationManagerMock.prototype.invokeHandlers)
expect(registrationManagerMock.prototype.invokeHandler)
.toBeCalledWith(KeyEventType.KeyDown, "Alt+1", keyboardEvent);
});
@@ -107,7 +107,7 @@ describe("Keyboard Manager Component", () => {
window.dispatchEvent(keyboardEvent);
expect(registrationManagerMock.prototype.invokeHandlers)
expect(registrationManagerMock.prototype.invokeHandler)
.toBeCalledWith(KeyEventType.KeyUp, "Alt+1", keyboardEvent);
});
@@ -121,7 +121,7 @@ describe("Keyboard Manager Component", () => {
window.dispatchEvent(keyboardEvent);
expect(registrationManagerMock.prototype.invokeHandlers)
expect(registrationManagerMock.prototype.invokeHandler)
.toBeCalledWith(KeyEventType.KeyPress, "Alt+1", keyboardEvent);
});
@@ -135,7 +135,7 @@ describe("Keyboard Manager Component", () => {
window.dispatchEvent(keyboardEvent);
expect(registrationManagerMock.prototype.invokeHandlers)
expect(registrationManagerMock.prototype.invokeHandler)
.toBeCalledWith(KeyEventType.KeyDown, "F1", keyboardEvent);
});
@@ -149,7 +149,7 @@ describe("Keyboard Manager Component", () => {
window.dispatchEvent(keyboardEvent);
expect(registrationManagerMock.prototype.invokeHandlers)
expect(registrationManagerMock.prototype.invokeHandler)
.toBeCalledWith(KeyEventType.KeyUp, "F1", keyboardEvent);
});
@@ -163,7 +163,7 @@ describe("Keyboard Manager Component", () => {
window.dispatchEvent(keyboardEvent);
expect(registrationManagerMock.prototype.invokeHandlers)
expect(registrationManagerMock.prototype.invokeHandler)
.toBeCalledWith(KeyEventType.KeyPress, "F1", keyboardEvent);
});
@@ -183,7 +183,7 @@ describe("Keyboard Manager Component", () => {
window.dispatchEvent(keyboardEvent);
expect(registrationManagerMock.prototype.invokeHandlers).not.toBeCalled();
expect(registrationManagerMock.prototype.invokeHandler).not.toBeCalled();
});
it("ignores keyboard events when UI is focused on an textarea element", () => {
@@ -201,7 +201,7 @@ describe("Keyboard Manager Component", () => {
window.dispatchEvent(keyboardEvent);
expect(registrationManagerMock.prototype.invokeHandlers).not.toBeCalled();
expect(registrationManagerMock.prototype.invokeHandler).not.toBeCalled();
});
it("ignores keyboard events when UI is focused on select elements", () => {
@@ -219,7 +219,7 @@ describe("Keyboard Manager Component", () => {
window.dispatchEvent(keyboardEvent);
expect(registrationManagerMock.prototype.invokeHandlers).not.toBeCalled();
expect(registrationManagerMock.prototype.invokeHandler).not.toBeCalled();
});
it("does not ignore keyboard events when UI is focused on other form elements", () => {
@@ -237,7 +237,7 @@ describe("Keyboard Manager Component", () => {
window.dispatchEvent(keyboardEvent);
expect(registrationManagerMock.prototype.invokeHandlers).toBeCalled();
expect(registrationManagerMock.prototype.invokeHandler).toBeCalled();
});
});
});
@@ -63,7 +63,7 @@ export class KeyboardManager extends React.Component<any, IKeyboardContext> {
return;
}
this.state.keyboard.invokeHandlers(evt.type as KeyEventType, this.getKeyParts(evt), evt);
this.state.keyboard.invokeHandler(evt.type as KeyEventType, this.getKeyParts(evt), evt);
}
private isDisabled(): boolean {
@@ -1,9 +1,21 @@
import { KeyboardRegistrationManager } from "./keyboardRegistrationManager";
import { KeyEventType } from "./keyboardManager";
import { IKeyboardBindingProps } from "../keyboardBinding/keyboardBinding";
describe("Keyboard Registration Manager", () => {
let keyboardManager: KeyboardRegistrationManager = null;
function addHandler(keyboardManager: KeyboardRegistrationManager,
keyEventType: KeyEventType, accelerators: string[], handler) {
const bindingProps: IKeyboardBindingProps = {
accelerators,
displayName: "test binding",
handler,
keyEventType,
};
return keyboardManager.registerBinding(bindingProps);
}
beforeEach(() => {
keyboardManager = new KeyboardRegistrationManager();
});
@@ -13,119 +25,70 @@ describe("Keyboard Registration Manager", () => {
expect(keyboardManager).not.toBeNull();
});
it("can add keybard event handlers", () => {
it("can add keyboard event handlers", () => {
const keyCode1 = "Ctrl+1";
const handler1 = (evt: KeyboardEvent) => null;
const keyCode2 = "Ctrl+S";
const handler2 = (evt: KeyboardEvent) => null;
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode1], handler1);
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode2], handler2);
addHandler(keyboardManager, KeyEventType.KeyDown, [keyCode1], handler1);
addHandler(keyboardManager, KeyEventType.KeyDown, [keyCode2], handler2);
const handlers1 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode1);
const handlers2 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode2);
const h1 = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode1);
const h2 = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode2);
expect(handlers1.length).toEqual(1);
expect(handlers2.length).toEqual(1);
expect(handlers1[0]).toBe(handler1);
expect(handlers2[0]).toBe(handler2);
expect(h1).toBe(handler1);
expect(h2).toBe(handler2);
});
it("can register handlers for same key code and different key event types", () => {
const keyCodeString = "Ctrl+H";
const keyCodes = [keyCodeString];
const handler1 = (evt: KeyboardEvent) => null;
const handler2 = (evt: KeyboardEvent) => null;
const handler3 = (evt: KeyboardEvent) => null;
const keyDownHandler = (evt: KeyboardEvent) => null;
const keyUpHandler = (evt: KeyboardEvent) => null;
const keyPressHandler = (evt: KeyboardEvent) => null;
keyboardManager.addHandler(KeyEventType.KeyDown, keyCodes, handler1);
addHandler(keyboardManager, KeyEventType.KeyDown, keyCodes, keyDownHandler);
keyboardManager.addHandler(KeyEventType.KeyUp, keyCodes, handler1);
keyboardManager.addHandler(KeyEventType.KeyUp, keyCodes, handler2);
addHandler(keyboardManager, KeyEventType.KeyUp, keyCodes, keyUpHandler);
keyboardManager.addHandler(KeyEventType.KeyPress, keyCodes, handler1);
keyboardManager.addHandler(KeyEventType.KeyPress, keyCodes, handler2);
keyboardManager.addHandler(KeyEventType.KeyPress, keyCodes, handler3);
addHandler(keyboardManager, KeyEventType.KeyPress, keyCodes, keyPressHandler);
const keyDownHandlers = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCodeString);
expect(keyDownHandlers.length).toEqual(1);
const keyUpHandlers = keyboardManager.getHandlers(KeyEventType.KeyUp, keyCodeString);
expect(keyUpHandlers.length).toEqual(2);
const keyPressHandlers = keyboardManager.getHandlers(KeyEventType.KeyPress, keyCodeString);
expect(keyPressHandlers.length).toEqual(3);
expect(keyboardManager.getHandler(KeyEventType.KeyDown, keyCodeString)).toBe(keyDownHandler);
expect(keyboardManager.getHandler(KeyEventType.KeyUp, keyCodeString)).toBe(keyUpHandler);
expect(keyboardManager.getHandler(KeyEventType.KeyPress, keyCodeString)).toBe(keyPressHandler);
});
it("can register multiple handlers for same key code", () => {
it("throws error when trying to register multiple handlers for same key code", () => {
const keyCode = "Ctrl+H";
const handler1 = (evt: KeyboardEvent) => null;
const handler2 = (evt: KeyboardEvent) => null;
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode], handler1);
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode], handler2);
const handlers = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode);
expect(handlers.length).toEqual(2);
});
it("list of handlers cannot be mutated outside of API", () => {
const keyCode = "Ctrl+K";
const handler = (evt: KeyboardEvent) => null;
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode], handler);
const handlers = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode);
const handlerCount = handlers.length;
// Attempt to add more handlers
handlers.push(handler, handler, handler);
const newHandlers = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode);
expect(newHandlers.length).toEqual(handlerCount);
addHandler(keyboardManager, KeyEventType.KeyDown, [keyCode], handler1);
expect(() => addHandler(keyboardManager, KeyEventType.KeyDown, [keyCode], handler2)).toThrowError();
});
it("can remove keyboard event handlers", () => {
const keyCode = "Ctrl+1";
const handler = (evt: KeyboardEvent) => null;
// Register keyboard handler
const deregister = keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode], handler);
const deregister = addHandler(keyboardManager, KeyEventType.KeyDown, [keyCode], jest.fn());
// Get registered handlers
let handlers = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode);
expect(handlers.length).toEqual(1);
let handler = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode);
expect(handler).not.toBeNull();
// Invoke deregister functions
deregister();
// Get registered handlers after deregistered
handlers = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode);
expect(handlers.length).toEqual(0);
handler = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode);
expect(handler).toBeNull();
});
it("get handlers for unregistered key code returns empty array", () => {
const handlers = keyboardManager.getHandlers(KeyEventType.KeyDown, "Alt+1");
expect(handlers.length).toEqual(0);
});
it("invokes registered keyboard handlers", () => {
const keyCode = "Ctrl+1";
const handler1 = jest.fn();
const handler2 = jest.fn();
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode], handler1);
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode], handler2);
const keyboardEvent = new KeyboardEvent("keydown", {
ctrlKey: true,
code: "1",
});
keyboardManager.invokeHandlers(KeyEventType.KeyDown, keyCode, keyboardEvent);
expect(handler1).toBeCalledWith(keyboardEvent);
expect(handler2).toBeCalledWith(keyboardEvent);
it("get handler for unregistered key code returns null", () => {
const handler = keyboardManager.getHandler(KeyEventType.KeyDown, "Alt+1");
expect(handler).toBeNull();
});
describe("array with mulitple keyCodes", () => {
@@ -135,15 +98,10 @@ describe("Keyboard Registration Manager", () => {
const keyCodes = [keyCode1, keyCode2];
const handler = jest.fn();
keyboardManager.addHandler(KeyEventType.KeyDown, keyCodes, handler);
addHandler(keyboardManager, KeyEventType.KeyDown, keyCodes, handler);
const handlers1 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode1);
expect(handlers1.length).toEqual(1);
expect(handlers1[0]).toEqual(handler);
const handlers2 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode2);
expect(handlers2.length).toEqual(1);
expect(handlers2[0]).toEqual(handler);
expect(keyboardManager.getHandler(KeyEventType.KeyDown, keyCode1)).toBe(handler);
expect(keyboardManager.getHandler(KeyEventType.KeyDown, keyCode2)).toBe(handler);
});
it("invoke the registered keyboard handlers", () => {
@@ -152,21 +110,21 @@ describe("Keyboard Registration Manager", () => {
const keyCodes = [keyCode1, keyCode2];
const handler = jest.fn();
keyboardManager.addHandler(KeyEventType.KeyDown, keyCodes, handler);
addHandler(keyboardManager, KeyEventType.KeyDown, keyCodes, handler);
const keyboardEvent1 = new KeyboardEvent("keydown", {
ctrlKey: true,
code: "1",
});
keyboardManager.invokeHandlers(KeyEventType.KeyDown, keyCode1, keyboardEvent1);
keyboardManager.invokeHandler(KeyEventType.KeyDown, keyCode1, keyboardEvent1);
expect(handler).toBeCalledWith(keyboardEvent1);
const keyboardEvent2 = new KeyboardEvent("keydown", {
code: "ArrowUp",
});
keyboardManager.invokeHandlers(KeyEventType.KeyDown, keyCode1, keyboardEvent2);
keyboardManager.invokeHandler(KeyEventType.KeyDown, keyCode1, keyboardEvent2);
expect(handler).toBeCalledWith(keyboardEvent2);
});
@@ -177,24 +135,24 @@ describe("Keyboard Registration Manager", () => {
const handler = (evt: KeyboardEvent) => null;
// Register keyboard handler
const deregister = keyboardManager.addHandler(KeyEventType.KeyDown, keyCodes, handler);
const deregister = addHandler(keyboardManager, KeyEventType.KeyDown, keyCodes, handler);
// Get registered handlers
let handlers1 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode1);
expect(handlers1.length).toEqual(1);
let h1 = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode1);
expect(h1).toBe(handler);
let handlers2 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode2);
expect(handlers2.length).toEqual(1);
let h2 = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode2);
expect(h2).toBe(handler);
// Invoke deregister function
deregister();
// Get registered handlers after deregistered
handlers1 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode1);
expect(handlers1.length).toEqual(0);
h1 = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode1);
expect(h1).toBeNull();
handlers2 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode2);
expect(handlers2.length).toEqual(0);
h2 = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode2);
expect(h2).toBeNull();
});
});
});
@@ -1,12 +1,14 @@
import Guard from "../../../../common/guard";
import { KeyboardManager, KeyEventType } from "./keyboardManager";
import { IKeyboardBindingProps } from "../keyboardBinding/keyboardBinding";
import { AppError, ErrorCode } from "../../../../models/applicationState";
/**
* A map of keyboard event registrations
*/
export interface IKeyboardRegistrations {
[keyEventType: string]: {
[key: string]: KeyboardEventHandler[],
[key: string]: IKeyboardBindingProps,
};
}
@@ -22,16 +24,14 @@ export class KeyboardRegistrationManager {
private registrations: IKeyboardRegistrations = {};
/**
* Registers a keyboard event handler for the specified key code
* @param keyEventType Type of key event (keydown, keyup, keypress)
* @param keyCodes a list of key code and key code combinations, ex) Ctrl+1
* @param handler The keyboard event handler
*
* @returns a function for deregistering the handler
* Registers a keyboard binding and returns a function to deregister that binding
* @param binding Properties for keyboard binding (type of key event, keyCodes, handler, etc.)
* @returns a function for deregistering the keyboard binding
*/
public addHandler(keyEventType: KeyEventType, keyCodes: string[], handler: KeyboardEventHandler): () => void {
public registerBinding = (binding: IKeyboardBindingProps) => {
const {keyEventType, accelerators, handler, displayName} = binding;
Guard.null(keyEventType);
Guard.expression(keyCodes, (keyCodes) => keyCodes.length > 0);
Guard.expression(accelerators, (keyCodes) => keyCodes.length > 0);
Guard.null(handler);
let eventTypeRegistrations = this.registrations[keyEventType];
@@ -40,22 +40,20 @@ export class KeyboardRegistrationManager {
this.registrations[keyEventType] = eventTypeRegistrations;
}
keyCodes.forEach((keyCode) => {
let keyRegistrations: KeyboardEventHandler[] = this.registrations[keyEventType][keyCode];
if (!keyRegistrations) {
keyRegistrations = [];
this.registrations[keyEventType][keyCode] = keyRegistrations;
accelerators.forEach((keyCode) => {
const currentBinding = this.registrations[keyEventType][keyCode];
if (currentBinding) {
let error = `Key code ${keyCode} on key event "${keyEventType}" `;
error += `already has binding registered: "${currentBinding.displayName}." `;
error += `Cannot register binding "${displayName}" with the same key code and key event type`;
throw new AppError(ErrorCode.OverloadedKeyBinding, error);
}
keyRegistrations.push(handler);
this.registrations[keyEventType][keyCode] = binding;
});
return () => {
keyCodes.forEach((keyCode) => {
const keyRegistrations: KeyboardEventHandler[] = this.registrations[keyEventType][keyCode];
const index = keyRegistrations.findIndex((h) => h === handler);
keyRegistrations.splice(index, 1);
binding.accelerators.forEach((keyCode) => {
delete this.registrations[binding.keyEventType][keyCode];
});
};
}
@@ -65,12 +63,16 @@ export class KeyboardRegistrationManager {
* @param keyEventType Type of key event (keydown, keyup, keypress)
* @param keyCode The key code combination, ex) Ctrl+1
*/
public getHandlers(keyEventType: KeyEventType, keyCode: string) {
public getHandler(keyEventType: KeyEventType, keyCode: string): (evt?: KeyboardEvent) => void {
Guard.null(keyEventType);
Guard.null(keyCode);
const keyEventTypeRegs = this.registrations[keyEventType];
return (keyEventTypeRegs && keyEventTypeRegs[keyCode]) ? [...keyEventTypeRegs[keyCode]] : [];
return (keyEventTypeRegs && keyEventTypeRegs[keyCode])
?
keyEventTypeRegs[keyCode].handler
:
null;
}
/**
@@ -79,11 +81,17 @@ export class KeyboardRegistrationManager {
* @param keyCode The key code combination, ex) Ctrl+1
* @param evt The keyboard event that was raised
*/
public invokeHandlers(keyEventType: KeyEventType, keyCode: string, evt: KeyboardEvent) {
public invokeHandler(keyEventType: KeyEventType, keyCode: string, evt: KeyboardEvent) {
Guard.null(keyCode);
Guard.null(evt);
const handlers = this.getHandlers(keyEventType, keyCode);
handlers.forEach((handler) => handler(evt));
const handler = this.getHandler(keyEventType, keyCode);
if (handler !== null) {
handler(evt);
}
}
public getRegistrations = () => {
return this.registrations;
}
}
@@ -1,4 +1,4 @@
import React, { SyntheticEvent } from "react";
import React, { SyntheticEvent, ReactElement } from "react";
import { Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap";
/**
@@ -16,11 +16,12 @@ export type MessageFormatHandler = (...params: any[]) => string;
*/
export interface IMessageBoxProps {
title: string;
message: string | Element | MessageFormatHandler;
message: string | ReactElement<any> | MessageFormatHandler;
params?: any[];
onButtonSelect?: (button: HTMLButtonElement) => void;
onCancel?: () => void;
show?: boolean;
hideFooter?: boolean;
}
/**
@@ -66,9 +67,9 @@ export default class MessageBox extends React.Component<IMessageBoxProps, IMessa
onClosed={this.onClosed}>
<ModalHeader toggle={this.toggle}>{this.props.title}</ModalHeader>
<ModalBody>{this.getMessage(this.props.message)}</ModalBody>
<ModalFooter onClick={this.onFooterClick}>
{!this.props.hideFooter && <ModalFooter onClick={this.onFooterClick}>
{this.props.children}
</ModalFooter>
</ModalFooter>}
</Modal>
);
}
@@ -100,7 +101,7 @@ export default class MessageBox extends React.Component<IMessageBoxProps, IMessa
}
}
private getMessage = (message: string | MessageFormatHandler | Element) => {
private getMessage = (message: string | MessageFormatHandler | ReactElement<any>) => {
if (typeof message === "function") {
return message.apply(this, this.props.params);
} else {
@@ -48,9 +48,10 @@ describe("Custom Video Player Button Component", () => {
expect(keyboardBinding.exists()).toBe(true);
expect(keyboardBinding.props()).toEqual({
displayName: defaultProps.tooltip,
keyEventType: KeyEventType.KeyDown,
accelerators: props.accelerators,
onKeyEvent: props.onClick,
handler: props.onClick,
});
});
@@ -5,10 +5,11 @@ import { KeyEventType } from "../keyboardManager/keyboardManager";
export interface ICustomVideoPlayerButtonProps {
order: number;
onClick: () => void;
icon?: string;
accelerators?: string[];
tooltip?: string;
player?: Player;
onClick: () => void;
}
export class CustomVideoPlayerButton extends React.Component<ICustomVideoPlayerButtonProps> {
@@ -17,8 +18,9 @@ export class CustomVideoPlayerButton extends React.Component<ICustomVideoPlayerB
<Fragment>
{this.props.accelerators &&
<KeyboardBinding keyEventType={KeyEventType.KeyDown}
displayName={this.props.tooltip}
accelerators={this.props.accelerators}
onKeyEvent={this.props.onClick} />
handler={this.props.onClick} />
}
<button
type="button"
@@ -253,7 +253,7 @@ export default class CanvasHelpers {
const defaultTargetY = 0;
if (boundingBox.height > height || boundingBox.width > width) {
throw new AppError(ErrorCode.PasteRegionTooBigError, strings.errors.pasteRegionTooBigError.message);
throw new AppError(ErrorCode.PasteRegionTooBig, strings.errors.pasteRegionTooBigError.message);
}
if (!CanvasHelpers.boundingBoxWithin(boundingBox, width, height)) {
@@ -480,22 +480,22 @@ describe("Editor Page Component", () => {
expect(removeAllRegionsConfirm).toBeCalled();
});
it("Calls copy regions with hot key", async () => {
it("Calls copy regions with hot key", () => {
dispatchKeyEvent("Ctrl+c");
expect(copyRegions).toBeCalled();
});
it("Calls cut regions with hot key", async () => {
it("Calls cut regions with hot key", () => {
dispatchKeyEvent("Ctrl+x");
expect(cutRegions).toBeCalled();
});
it("Calls paste regions with hot key", async () => {
it("Calls paste regions with hot key", () => {
dispatchKeyEvent("Ctrl+v");
expect(pasteRegions).toBeCalled();
});
it("Calls remove all regions confirmation with hot key", async () => {
it("Calls remove all regions confirmation with hot key", () => {
dispatchKeyEvent("Ctrl+Delete");
expect(removeAllRegionsConfirm).toBeCalled();
});
@@ -24,6 +24,7 @@ import CanvasHelpers from "./canvasHelpers";
import { tagColors } from "../../../../common/tagColors";
import { ToolbarItemName } from "../../../../registerToolbar";
import { SelectionMode } from "vott-ct/lib/js/CanvasTools/Interface/ISelectorSettings";
import { strings } from "../../../../common/strings";
/**
* Properties for Editor Page
@@ -134,10 +135,12 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
<div className="editor-page">
{[...Array(10).keys()].map((index) => {
return (<KeyboardBinding
displayName={strings.editorPage.tags.hotKey.help}
key={index}
keyEventType={KeyEventType.KeyDown}
accelerators={[`${index}`]}
onKeyEvent={this.handleTagHotKey} />);
icon={"fa-tag"}
handler={this.handleTagHotKey} />);
})}
<div className="editor-page-sidebar bg-lighter-1">
<EditorSideBar
@@ -68,7 +68,7 @@ describe("Editor Toolbar", () => {
const toolbar = wrapper.find(EditorToolbar) as ReactWrapper<IEditorToolbarProps, IEditorToolbarState>;
const select = toolbar.props().items[0];
const toolbarRegistry = ToolbarItemFactory.getToolbarItems();
expect(select.config).toHaveProperty("accelerators", ["v", "V" ]);
expect(select.config).toHaveProperty("accelerators", ["V", "v" ]);
expect(select.config).toEqual(toolbarRegistry[0].config);
});
});
+20
Ver Arquivo
@@ -0,0 +1,20 @@
@import '../../../assets/sass/theme.scss';
.keybinding {
font-weight: bolder;
}
.keybinding-icon {
vertical-align: bottom;
padding: 4px 15px;
}
.help-key.row {
color: #ccc;
padding: 2px;
&:hover {
color: #fff;
background-color: $lighter-2;
}
}
+61
Ver Arquivo
@@ -0,0 +1,61 @@
import { mount } from "enzyme";
import React from "react";
import MockFactory from "../../../common/mockFactory";
import { KeyboardManager } from "../common/keyboardManager/keyboardManager";
import { IKeyboardRegistrations,
KeyboardRegistrationManager } from "../common/keyboardManager/keyboardRegistrationManager";
import { HelpMenu, IHelpMenuProps } from "./helpMenu";
jest.mock("../common/keyboardManager/keyboardRegistrationManager");
describe("Help Menu", () => {
function createComponent(props?: IHelpMenuProps) {
return mount(
<KeyboardManager>
<HelpMenu {...props}/>
</KeyboardManager>,
);
}
const numberRegistrations = 5;
const keyboardRegistrations: IKeyboardRegistrations = MockFactory.createKeyboardRegistrations(numberRegistrations);
const registrationMock = KeyboardRegistrationManager as jest.Mocked<typeof KeyboardRegistrationManager>;
registrationMock.prototype.getRegistrations = jest.fn(() => keyboardRegistrations);
registrationMock.prototype.registerBinding = jest.fn(() => jest.fn());
it("Opens when button is clicked", () => {
const wrapper = createComponent();
expect(wrapper.exists("div.modal-content")).toBe(false);
wrapper.find("div.help-menu-button").simulate("click");
wrapper.update();
expect(wrapper.exists("div.modal-content")).toBe(true);
});
it("Pulls currently registered keyboard bindings upon opening", async () => {
const wrapper = createComponent();
expect(wrapper.exists("div.help-key.row")).toBe(false);
wrapper.find("div.help-menu-button").simulate("click");
expect(wrapper.exists("div.modal-content")).toBe(true);
expect(registrationMock.prototype.getRegistrations).toBeCalled();
await MockFactory.flushUi();
expect(wrapper.find("div.help-key.row")).toHaveLength(numberRegistrations);
});
it("Renders keyboard bindings with icon, key binding and display name", async () => {
const wrapper = createComponent();
wrapper.find("div.help-menu-button").simulate("click");
await MockFactory.flushUi();
expect(wrapper.exists(`div.col-1.keybinding-icon.fas.test-icon-1`)).toBe(true);
expect(wrapper.find("div.col-4.keybinding-accelerator").first().text()).toEqual("A");
expect(wrapper.find("div.col-6.keybinding-name").first().text()).toEqual("Binding 1");
});
it("Calls onClose handler when closed", () => {
const onClose = jest.fn();
const wrapper = createComponent({onClose});
wrapper.find("div.help-menu-button").simulate("click");
expect(wrapper.exists("div.modal-content")).toBe(true);
wrapper.find("button.close").simulate("click");
expect(onClose).toBeCalled();
expect(wrapper.exists("div.modal-content")).toBe(false);
});
});
+130
Ver Arquivo
@@ -0,0 +1,130 @@
import React from "react";
import MessageBox from "../common/messageBox/messageBox";
import { strings } from "../../../common/strings";
import { KeyboardContext, IKeyboardContext, KeyEventType } from "../common/keyboardManager/keyboardManager";
import { IKeyboardBindingProps, KeyboardBinding } from "../common/keyboardBinding/keyboardBinding";
import "./helpMenu.scss";
export interface IHelpMenuProps {
onClose?: () => void;
}
export interface IHelpMenuState {
show: boolean;
}
export class HelpMenu extends React.Component<IHelpMenuProps, IHelpMenuState> {
public static contextType = KeyboardContext;
public context!: IKeyboardContext;
public state = {
show: false,
};
private icon: string = "fa-question-circle";
public render() {
return (
<div className={"help-menu-button"} onClick={() => this.setState({show: true})}>
<i className={`fas ${this.icon}`}/>
<KeyboardBinding
displayName={strings.editorPage.help.title}
accelerators={["Ctrl+H", "Ctrl+h"]}
handler={() => this.setState({show: !this.state.show})}
icon={this.icon}
keyEventType={KeyEventType.KeyDown}
/>
<MessageBox
title={strings.titleBar.help}
message={this.getHelpBody()}
show={this.state.show}
onCancel={this.onClose}
hideFooter={true}
/>
</div>
);
}
private onClose = () => {
this.setState({show: false});
if (this.props.onClose) {
this.props.onClose();
}
}
private getHelpBody = () => {
const registrations = this.context.keyboard.getRegistrations()[KeyEventType.KeyDown];
if (!registrations) {
return;
}
const groupKeys = this.groupKeys(registrations);
return (
<div className="help-body container">
{
groupKeys.map((group) => group.length ? this.getRegistrationRow(group, registrations) : null)
}
</div>
);
}
private groupKeys = (registrations: {[key: string]: IKeyboardBindingProps}) => {
const allKeys = Object.keys(registrations);
const caseConsolidatedKeys = this.consolidateKeyCasings(allKeys);
const groups = [];
const alreadyGrouped = new Set();
for (const key of caseConsolidatedKeys) {
const group = [key];
if (!alreadyGrouped.has(key)) {
alreadyGrouped.add(key);
for (const otherKey of caseConsolidatedKeys) {
if (!alreadyGrouped.has(otherKey) &&
this.bindingEquals(registrations[key], registrations[otherKey])) {
group.push(otherKey);
alreadyGrouped.add(otherKey);
}
}
groups.push(group);
}
}
return groups;
}
private bindingEquals(binding1: IKeyboardBindingProps, binding2: IKeyboardBindingProps) {
return binding1 && binding2
&& binding1.displayName === binding2.displayName
&& binding1.handler === binding2.handler;
}
private consolidateKeyCasings = (allKeys: string[]): string[] => {
const lowerRegistrations = {};
for (const key of allKeys) {
const lowerKey = key.toLowerCase();
if (!lowerRegistrations[lowerKey]) {
lowerRegistrations[lowerKey] = key;
}
}
return Object.keys(lowerRegistrations).map((lowerKey) => lowerRegistrations[lowerKey]);
}
private getRegistrationRow = (group: string[], registrations: {[key: string]: IKeyboardBindingProps}) => {
const keyRegistration = registrations[group[0]];
if (keyRegistration) {
return (
<div className={"help-key row"}>
<div className={`col-1 keybinding-icon ${(keyRegistration.icon)
? `fas ${keyRegistration.icon}` : ""}`}/>
<div className="col-4 keybinding-accelerator">{this.stringifyGroup(group)}</div>
<div className="col-6 keybinding-name">{keyRegistration.displayName}</div>
</div>
);
}
}
private stringifyGroup(group: string[]): string {
return (group.length < 3) ? group.join(", ") : `${group[0]}-${group[group.length - 1]}`;
}
}
+10 -4
Ver Arquivo
@@ -2,6 +2,8 @@ import React, { Fragment } from "react";
import Menu, { MenuItem, SubMenu, Divider } from "rc-menu";
import { PlatformType } from "../../../common/hostProcess";
import "./titleBar.scss";
import { strings } from "../../../common/strings";
import { HelpMenu } from "./helpMenu";
export interface ITitleBarProps extends React.Props<TitleBar> {
icon?: string | JSX.Element;
@@ -85,20 +87,24 @@ export class TitleBar extends React.Component<ITitleBarProps, ITitleBarState> {
{this.props.children}
{this.state.platform === PlatformType.Windows &&
<ul>
<li title="Minimize" className="btn-window-minimize" onClick={this.minimizeWindow}>
<li title={strings.titleBar.minimize} className="btn-window-minimize"
onClick={this.minimizeWindow}>
<i className="far fa-window-minimize" />
</li>
{!this.state.maximized &&
<li title="Maximize" className="btn-window-maximize" onClick={this.maximizeWindow}>
<li title={strings.titleBar.maximize} className="btn-window-maximize"
onClick={this.maximizeWindow}>
<i className="far fa-window-maximize" />
</li>
}
{this.state.maximized &&
<li title="Restore" className="btn-window-restore" onClick={this.unmaximizeWindow}>
<li title={strings.titleBar.restore} className="btn-window-restore"
onClick={this.unmaximizeWindow}>
<i className="far fa-window-restore" />
</li>
}
<li title="Close" className="btn-window-close" onClick={this.closeWindow}>
<li title={strings.titleBar.close} className="btn-window-close"
onClick={this.closeWindow}>
<i className="fas fa-times" />
</li>
</ul>
+24 -2
Ver Arquivo
@@ -75,14 +75,16 @@ export abstract class ToolbarItem extends React.Component<IToolbarItemProps> {
{
accelerators &&
<KeyboardBinding
displayName={this.props.tooltip}
accelerators={accelerators}
onKeyEvent={this.onClick}
handler={this.onClick}
icon={this.props.icon}
keyEventType={KeyEventType.KeyDown}
/>
}
<button type="button"
className={className.join(" ")}
title={this.props.tooltip}
title={this.getTitle()}
onClick={this.onClick}>
<i className={"fas " + this.props.icon} />
</button>
@@ -92,6 +94,26 @@ export abstract class ToolbarItem extends React.Component<IToolbarItemProps> {
protected abstract onItemClick();
private getTitle = () => {
return `${this.props.tooltip}${this.getShortcut()}`;
}
private getShortcut = () => {
return ` (${this.consolidateKeyCasings(this.props.accelerators).join(", ")})`;
}
private consolidateKeyCasings = (accelerators: string[]): string[] => {
const consolidated: string[] = [];
if (accelerators) {
for (const a of accelerators) {
if (!consolidated.find((item) => item.toLowerCase() === a.toLowerCase())) {
consolidated.push(a);
}
}
}
return consolidated;
}
private onClick = () => {
if (this.onItemClick) {
this.onItemClick();
+11 -11
Ver Arquivo
@@ -36,7 +36,7 @@ export default function registerToolbar() {
icon: "fa-mouse-pointer",
group: ToolbarItemGroup.Canvas,
type: ToolbarItemType.State,
accelerators: ["v", "V"],
accelerators: ["V", "v"],
});
ToolbarItemFactory.register({
@@ -45,7 +45,7 @@ export default function registerToolbar() {
icon: "fa-vector-square",
group: ToolbarItemGroup.Canvas,
type: ToolbarItemType.State,
accelerators: ["r", "R"],
accelerators: ["R", "r"],
});
ToolbarItemFactory.register({
@@ -54,7 +54,7 @@ export default function registerToolbar() {
icon: "fa-draw-polygon",
group: ToolbarItemGroup.Canvas,
type: ToolbarItemType.State,
accelerators: ["p", "P"],
accelerators: ["P", "p"],
});
ToolbarItemFactory.register({
@@ -63,7 +63,7 @@ export default function registerToolbar() {
icon: "far fa-clone",
group: ToolbarItemGroup.Canvas,
type: ToolbarItemType.State,
accelerators: ["Ctrl+w", "Ctrl+W"],
accelerators: ["Ctrl+W", "Ctrl+w"],
});
ToolbarItemFactory.register({
@@ -72,7 +72,7 @@ export default function registerToolbar() {
icon: "fa-copy",
group: ToolbarItemGroup.Regions,
type: ToolbarItemType.Action,
accelerators: ["Ctrl+c", "Ctrl+C"],
accelerators: ["Ctrl+C", "Ctrl+c"],
});
ToolbarItemFactory.register({
@@ -81,7 +81,7 @@ export default function registerToolbar() {
icon: "fa-cut",
group: ToolbarItemGroup.Regions,
type: ToolbarItemType.Action,
accelerators: ["Ctrl+x", "Ctrl+X"],
accelerators: ["Ctrl+X", "Ctrl+x"],
});
ToolbarItemFactory.register({
@@ -90,7 +90,7 @@ export default function registerToolbar() {
icon: "fa-paste",
group: ToolbarItemGroup.Regions,
type: ToolbarItemType.Action,
accelerators: ["Ctrl+v", "Ctrl+V"],
accelerators: ["Ctrl+V", "Ctrl+v"],
});
ToolbarItemFactory.register({
@@ -108,7 +108,7 @@ export default function registerToolbar() {
icon: "fas fa-arrow-circle-up",
group: ToolbarItemGroup.Navigation,
type: ToolbarItemType.Action,
accelerators: ["ArrowUp", "w", "W"],
accelerators: ["ArrowUp", "W", "w"],
});
ToolbarItemFactory.register({
@@ -117,7 +117,7 @@ export default function registerToolbar() {
icon: "fas fa-arrow-circle-down",
group: ToolbarItemGroup.Navigation,
type: ToolbarItemType.Action,
accelerators: ["ArrowDown", "s", "S"],
accelerators: ["ArrowDown", "S", "s"],
});
ToolbarItemFactory.register({
@@ -126,7 +126,7 @@ export default function registerToolbar() {
icon: "fa-save",
group: ToolbarItemGroup.Project,
type: ToolbarItemType.Action,
accelerators: ["Ctrl+s", "Ctrl+S"],
accelerators: ["Ctrl+S", "Ctrl+s"],
}, SaveProject);
ToolbarItemFactory.register({
@@ -135,6 +135,6 @@ export default function registerToolbar() {
icon: "fa-external-link-square-alt",
group: ToolbarItemGroup.Project,
type: ToolbarItemType.Action,
accelerators: ["Ctrl+e", "Ctrl+E"],
accelerators: ["Ctrl+E", "Ctrl+e"],
}, ExportProject);
}