diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@application/context-menu/context-menu.html b/src/Apps/NetPad.Apps.App/App/src/core/@application/context-menu/context-menu.html index badc04bc..f55dfc85 100644 --- a/src/Apps/NetPad.Apps.App/App/src/core/@application/context-menu/context-menu.html +++ b/src/Apps/NetPad.Apps.App/App/src/core/@application/context-menu/context-menu.html @@ -18,7 +18,7 @@ innerHTML.bind="item.text">
- ${item.shortcut.keyComboString} + ${item.shortcut.keyCombo.asString}

diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@application/editor/editor-setup.ts b/src/Apps/NetPad.Apps.App/App/src/core/@application/editor/editor-setup.ts index de999f36..7386c9cf 100644 --- a/src/Apps/NetPad.Apps.App/App/src/core/@application/editor/editor-setup.ts +++ b/src/Apps/NetPad.Apps.App/App/src/core/@application/editor/editor-setup.ts @@ -21,9 +21,14 @@ import { IRenameProvider, ISignatureHelpProvider } from "./providers/interfaces"; +import {KeyCodeNum} from "@common"; +import {IEventBus, Settings, SettingsUpdatedEvent} from "@domain"; +import {ShortcutIds} from "@application/shortcuts/builtin-shortcuts"; export class EditorSetup { constructor( + private readonly settings: Settings, + @IEventBus private readonly eventBus: IEventBus, @all(ICommandProvider) private readonly commandProviders: ICommandProvider[], @all(IActionProvider) private readonly actionProviders: IActionProvider[], @all(ICompletionItemProvider) private readonly completionItemProviders: ICompletionItemProvider[], @@ -50,6 +55,7 @@ export class EditorSetup { this.registerThemes(); this.registerCommands(); this.registerActions(); + this.registerKeyboardShortcuts(); this.registerCompletionProviders(); this.registerSemanticTokensProviders(); this.registerDocumentSymbolProviders(); @@ -123,6 +129,68 @@ export class EditorSetup { }); } + private registerKeyboardShortcuts() { + // Currently we are only overriding the Command Palette keybinding. + let commandPaletteKeybinding: number; + + const addOrUpdateShortcuts = (settings: Settings) => { + const commandPaletteShortcutConfig = this.settings.keyboardShortcuts.shortcuts + .find(s => s.id === ShortcutIds.openCommandPalette); + + // If the config for this shortcut doesn't exist yet, or did but is now removed. + if (!commandPaletteShortcutConfig) { + if (commandPaletteKeybinding) { + // Disable previous rule + monaco.editor.addKeybindingRule({ + keybinding: commandPaletteKeybinding, + command: null, + }); + } + + monaco.editor.addKeybindingRule({ + keybinding: monaco.KeyCode.F1, + command: "editor.action.quickCommand", + }); + + return; + } + + if (commandPaletteKeybinding === undefined) { + // If this is first time we are customizing the command palette keybinding, + // disable default show command palette + monaco.editor.addKeybindingRule({ + keybinding: monaco.KeyCode.F1, + command: null, + }); + } else { + // Disable previous rule + monaco.editor.addKeybindingRule({ + keybinding: commandPaletteKeybinding, + command: null, + }); + } + + // Add a new rule for the new keybinding + const combo: number[] = []; + if (commandPaletteShortcutConfig.meta) combo.push(monaco.KeyMod.WinCtrl); + if (commandPaletteShortcutConfig.alt) combo.push(monaco.KeyMod.Alt); + if (commandPaletteShortcutConfig.ctrl) combo.push(monaco.KeyMod.CtrlCmd); + if (commandPaletteShortcutConfig.shift) combo.push(monaco.KeyMod.Shift); + if (commandPaletteShortcutConfig.key) combo.push(KeyCodeNum[commandPaletteShortcutConfig.key!.toString() as keyof typeof KeyCodeNum]); + + commandPaletteKeybinding = combo.reduce((a, b) => a | b, 0); + + monaco.editor.addKeybindingRule({ + keybinding: commandPaletteKeybinding, + command: "editor.action.quickCommand", + }); + }; + + this.eventBus.subscribeToServer(SettingsUpdatedEvent, event => addOrUpdateShortcuts(event.settings)); + + addOrUpdateShortcuts(this.settings); + } + private registerCompletionProviders() { for (const completionItemProvider of this.completionItemProviders) { monaco.languages.registerCompletionItemProvider(completionItemProvider.language, completionItemProvider); diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@application/index.ts b/src/Apps/NetPad.Apps.App/App/src/core/@application/index.ts index cf983589..dd8be7c8 100644 --- a/src/Apps/NetPad.Apps.App/App/src/core/@application/index.ts +++ b/src/Apps/NetPad.Apps.App/App/src/core/@application/index.ts @@ -31,8 +31,10 @@ export * from "./panes/ipane-host-view-state-controller"; export * from "./panes/pane-action" export * from "./panes/pane"; +export * from "./shortcuts/key-combo"; export * from "./shortcuts/shortcut"; export * from "./shortcuts/shortcut-action-execution-context"; +export * from "./shortcuts/ishortcut-manager"; export * from "./shortcuts/shortcut-manager"; export * from "./shortcuts/builtin-shortcuts"; diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@application/panes/pane-host/pane-host.html b/src/Apps/NetPad.Apps.App/App/src/core/@application/panes/pane-host/pane-host.html index 052a8633..7f92647b 100644 --- a/src/Apps/NetPad.Apps.App/App/src/core/@application/panes/pane-host/pane-host.html +++ b/src/Apps/NetPad.Apps.App/App/src/core/@application/panes/pane-host/pane-host.html @@ -5,7 +5,7 @@
+ title="${pane.name} ${pane.shortcut ? ('(' + pane.shortcut.keyCombo.asString + ')') : ''}"> ${pane.name}
diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@application/platforms/electron/services/electron-event-handler-background-service.ts b/src/Apps/NetPad.Apps.App/App/src/core/@application/platforms/electron/services/electron-event-handler-background-service.ts index 0292d3f1..2a69c67e 100644 --- a/src/Apps/NetPad.Apps.App/App/src/core/@application/platforms/electron/services/electron-event-handler-background-service.ts +++ b/src/Apps/NetPad.Apps.App/App/src/core/@application/platforms/electron/services/electron-event-handler-background-service.ts @@ -1,11 +1,10 @@ +import {IContainer} from "aurelia"; import {IBackgroundService, WithDisposables} from "@common"; -import {ChannelInfo, Settings} from "@domain"; +import {ChannelInfo} from "@domain"; +import {IShortcutManager, Shortcut} from "@application"; import {ElectronIpcGateway} from "./electron-ipc-gateway"; -import {IShortcutManager} from "@application"; import {IMainMenuService} from "../../../../../windows/main/titlebar/main-menu/main-menu-service"; import {IMenuItem} from "../../../../../windows/main/titlebar/main-menu/imenu-item"; -import {Shortcut} from "@application"; -import {IContainer} from "aurelia"; /** * Handles top-level IPC events sent by Electron's main process. @@ -71,7 +70,7 @@ export class ElectronEventHandlerBackgroundService extends WithDisposables imple return { name: shortcut.name, isEnabled: shortcut.isEnabled, - keyCombo: shortcut.keyComboString.split('+').map(x => x.trim()) + keyCombo: shortcut.keyCombo.asArray }; } } diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/builtin-shortcuts.ts b/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/builtin-shortcuts.ts index 89f1f227..61cd2698 100644 --- a/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/builtin-shortcuts.ts +++ b/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/builtin-shortcuts.ts @@ -1,14 +1,27 @@ -import {CreateScriptDto, IScriptService, ISettingService} from "@domain"; import {KeyCode} from "@common"; +import {CreateScriptDto, IScriptService, ISettingsService} from "@domain"; import {Shortcut} from "./shortcut"; -import {EditorUtil} from "../editor/editor-util"; import {ITextEditorService} from "../editor/text-editor-service"; -import {Explorer, NamespacesPane, OutputPane} from "../../../windows/main/panes"; -import {RunScriptEvent, TogglePaneEvent} from "@application" -import * as monaco from "monaco-editor"; + +export enum ShortcutIds { + openCommandPalette = "shortcut.commandpalette.open", + quickOpenDocument = "shortcut.documents.quickopen", + openLastActiveDocument = "shortcut.documents.switchtolastactive", + newDocument = "shortcut.documents.new", + closeDocument = "shortcut.documents.close", + saveDocument = "shortcut.documents.save", + saveAllDocuments = "shortcut.documents.saveall", + runDocument = "shortcut.documents.run", + openDocumentProperties = "shortcut.documents.properties", + openSettings = "shortcut.settings.open", + openOutput = "shortcut.output.open", + openExplorer = "shortcut.explorer.open", + openNamespaces = "shortcut.namespaces.open", + reloadWindow = "shortcut.window.reload", +} export const BuiltinShortcuts = [ - new Shortcut("Command Palette") + new Shortcut(ShortcutIds.openCommandPalette, "Command Palette") .withKey(KeyCode.F1) .hasAction(ctx => { const editor = ctx.container.get(ITextEditorService).active?.monaco; @@ -18,63 +31,61 @@ export const BuiltinShortcuts = [ editor.focus(); editor.trigger("", "editor.action.quickCommand", null); }) - .configurable(false) + .captureDefaultKeyCombo() + .configurable() .enabled(), - new Shortcut("Go to Script") + new Shortcut(ShortcutIds.quickOpenDocument, "Go to Script") .withCtrlKey() .withKey(KeyCode.KeyT) .hasAction(ctx => { - const activeScriptId = ctx.session.active?.script.id; - if (!activeScriptId) { - return; - } - - const editors = monaco.editor.getEditors(); - if (!editors.length) { - return; - } - - let editor = editors.find(e => { - const model = e.getModel(); - return !model ? false : (EditorUtil.getScriptId(model) === activeScriptId); - }) + const editor = ctx.container.get(ITextEditorService).active?.monaco; - if (!editor) { - editor = editors.find(e => e.hasTextFocus() || e.hasWidgetFocus()) || editors[0]; - } + if (!editor) return; editor.focus(); - editor.getAction("netpad.action.goToScript")?.run(); + editor.trigger("", "netpad.action.goToScript", null); }) - .configurable(false) + .captureDefaultKeyCombo() + .configurable() + .enabled(), + + new Shortcut(ShortcutIds.openLastActiveDocument, "Switch to Last Active Script") + .withCtrlKey() + .withKey(KeyCode.Tab) + .hasAction((ctx) => ctx.session.activateLastActive()) + .captureDefaultKeyCombo() + .configurable() .enabled(), - new Shortcut("New") + new Shortcut(ShortcutIds.newDocument, "New") .withCtrlKey() .withKey(KeyCode.KeyN) .hasAction((ctx) => ctx.container.get(IScriptService).create(new CreateScriptDto())) + .captureDefaultKeyCombo() .configurable() .enabled(), - new Shortcut("Close") + new Shortcut(ShortcutIds.closeDocument, "Close") .withCtrlKey() .withKey(KeyCode.KeyW) .hasAction((ctx) => { if (ctx.session.active) ctx.session.close(ctx.session.active.script.id); }) + .captureDefaultKeyCombo() .configurable() .enabled(), - new Shortcut("Save") + new Shortcut(ShortcutIds.saveDocument, "Save") .withCtrlKey() .withKey(KeyCode.KeyS) .hasAction((ctx) => { if (ctx.session.active) ctx.container.get(IScriptService).save(ctx.session.active.script.id); }) + .captureDefaultKeyCombo() .enabled(), - new Shortcut("Save All") + new Shortcut(ShortcutIds.saveAllDocuments, "Save All") .withCtrlKey() .withShiftKey() .withKey(KeyCode.KeyS) @@ -84,63 +95,79 @@ export const BuiltinShortcuts = [ await scriptService.save(environment.script.id); } }) + .captureDefaultKeyCombo() .enabled(), - new Shortcut("Run") + new Shortcut(ShortcutIds.runDocument, "Run") .withKey(KeyCode.F5) - .firesEvent(RunScriptEvent) + .firesEvent(async () => new (await import("@application/events/action-events")).RunScriptEvent()) + .captureDefaultKeyCombo() .configurable() .enabled(), - new Shortcut("Script Properties") + new Shortcut(ShortcutIds.openDocumentProperties, "Script Properties") .withKey(KeyCode.F4) .hasAction((ctx) => { if (ctx.session.active) { ctx.container.get(IScriptService).openConfigWindow(ctx.session.active.script.id, null); } }) + .captureDefaultKeyCombo() .configurable() .enabled(), - new Shortcut("Output") - .withCtrlKey() - .withKey(KeyCode.KeyR) - .firesEvent(() => new TogglePaneEvent(OutputPane)) + new Shortcut(ShortcutIds.openSettings, "Settings") + .withKey(KeyCode.F12) + .hasAction((ctx) => ctx.container.get(ISettingsService).openSettingsWindow(null)) + .captureDefaultKeyCombo() .configurable() .enabled(), - new Shortcut("Switch to Last Active Script") + new Shortcut(ShortcutIds.openOutput, "Output") .withCtrlKey() - .withKey(KeyCode.Tab) - .hasAction((ctx) => ctx.session.activateLastActive()) - .configurable() - .enabled(), + .withKey(KeyCode.KeyR) + .firesEvent(async () => { + const TogglePaneEvent = (await import("@application/events/action-events")).TogglePaneEvent; + const OutputPane = (await import("../../../windows/main/panes")).OutputPane; - new Shortcut("Settings") - .withKey(KeyCode.F12) - .hasAction((ctx) => ctx.container.get(ISettingService).openSettingsWindow(null)) + return new TogglePaneEvent(OutputPane); + }) + .captureDefaultKeyCombo() .configurable() .enabled(), - new Shortcut("Explorer") + new Shortcut(ShortcutIds.openExplorer, "Explorer") .withAltKey() .withKey(KeyCode.KeyE) - .firesEvent(() => new TogglePaneEvent(Explorer)) + .firesEvent(async () => { + const TogglePaneEvent = (await import("@application/events/action-events")).TogglePaneEvent; + const Explorer = (await import("../../../windows/main/panes")).Explorer; + + return new TogglePaneEvent(Explorer); + }) + .captureDefaultKeyCombo() .configurable() .enabled(), - new Shortcut("Namespaces") + new Shortcut(ShortcutIds.openNamespaces, "Namespaces") .withAltKey() .withKey(KeyCode.KeyN) - .firesEvent(() => new TogglePaneEvent(NamespacesPane)) + .firesEvent(async () => { + const TogglePaneEvent = (await import("@application/events/action-events")).TogglePaneEvent; + const NamespacesPane = (await import("../../../windows/main/panes")).NamespacesPane; + + return new TogglePaneEvent(NamespacesPane); + }) + .captureDefaultKeyCombo() .configurable() .enabled(), - new Shortcut("Reload") + new Shortcut(ShortcutIds.reloadWindow, "Reload") .withCtrlKey() .withShiftKey() .withKey(KeyCode.KeyR) .hasAction(() => window.location.reload()) + .captureDefaultKeyCombo() .configurable() .enabled(), ]; diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/ishortcut-manager.ts b/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/ishortcut-manager.ts new file mode 100644 index 00000000..32d1d3b6 --- /dev/null +++ b/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/ishortcut-manager.ts @@ -0,0 +1,41 @@ +import {DI} from "aurelia"; +import {Shortcut} from "./shortcut"; + +export interface IShortcutManager { + /** + * Initializes the ShortcutManager and starts listening for keyboard events. + */ + initialize(): void; + + /** + * Adds a shortcut to the shortcut registry. + * @param shortcut The shortcut to register. + */ + registerShortcut(shortcut: Shortcut): void; + + /** + * Removes a shortcut from the shortcut registry. + * @param shortcut The shortcut to unregister. + */ + unregisterShortcut(shortcut: Shortcut): void; + + /** + * Finds a shortcut by its ID, if one exists. + * @param id The id of the shortcut to get. + */ + getShortcut(id: string): Shortcut | undefined; + + /** + * Finds a shortcut by its name, if one exists. + * @param name The name of the shortcut to get. + */ + getShortcutByName(name: string): Shortcut | undefined; + + /** + * Executes a shortcut. + * @param shortcut + */ + executeShortcut(shortcut: Shortcut): Promise; +} + +export const IShortcutManager = DI.createInterface(); diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/key-combo.ts b/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/key-combo.ts new file mode 100644 index 00000000..7e28766d --- /dev/null +++ b/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/key-combo.ts @@ -0,0 +1,219 @@ +import {KeyCode} from "@common"; +import {IKeyboardShortcutConfiguration} from "@domain"; + +/** + * A combination of keyboard keys. + */ +export class KeyCombo { + public meta = false; + public alt = false; + public ctrl = false; + public shift = false; + public key?: KeyCode; + + public get hasModifier(): boolean { + return this.meta || this.alt || this.ctrl || this.shift; + } + + /** + * Sets whether META/Super key is required as part of this KeyCombo. + */ + public withMetaKey(required = true): KeyCombo { + this.meta = required; + return this; + } + + /** + * Sets whether ALT key is required as part of this KeyCombo. + */ + public withAltKey(required = true): KeyCombo { + this.alt = required; + return this; + } + + /** + * Sets whether CTRL key is required as part of this KeyCombo. + */ + public withCtrlKey(required = true): KeyCombo { + this.ctrl = required; + return this; + } + + /** + * Sets whether SHIFT key is required as part of this KeyCombo. + */ + public withShiftKey(required = true): KeyCombo { + this.shift = required; + return this; + } + + /** + * Sets whether CTRL key is required as part of this KeyCombo. + */ + public withKey(key: KeyCode | undefined): KeyCombo { + this.key = key; + return this; + } + + public updateFrom(config: IKeyboardShortcutConfiguration | KeyCombo) { + this.withMetaKey(config.meta) + .withAltKey(config.alt) + .withCtrlKey(config.ctrl) + .withShiftKey(config.shift) + .withKey(config instanceof KeyCombo ? config.key : config.key as KeyCode); + + return this; + } + + public copyTo(config: IKeyboardShortcutConfiguration) { + config.meta = this.meta; + config.alt = this.alt; + config.ctrl = this.ctrl; + config.shift = this.shift; + config.key = this.key; + + return this; + } + + /** + * Creates a deep copy of this KeyCombo instance. + */ + public clone(): KeyCombo { + return new KeyCombo().updateFrom(this); + } + + /** + * Determines if this KeyCombo matches the specified key combination. + * @param key Key code. + * @param ctrl Whether the ctrl key is pressed. + * @param alt Whether the alt key is pressed. + * @param shift Whether the shift key is pressed. + * @param meta Whether the meta key is pressed. + */ + public matches( + key: KeyCode | undefined, + ctrl: boolean, + alt: boolean, + shift: boolean, + meta: boolean + ): boolean; + + /** + * Determines if this KeyCombo matches they key combination in the specified keyboard event. + * @param event The keyboard event. + */ + public matches(event: KeyboardEvent): boolean; + + /** + * Determines if this KeyCombo has the same key combination as the specified key combo. + * @param keyCombo The KeyCombo to compare with. + */ + public matches(keyCombo: KeyCombo): boolean; + + public matches( + keyOrEventOrCombo: KeyCode | undefined | KeyboardEvent | KeyCombo, + ctrl?: boolean, + alt?: boolean, + shift?: boolean, + meta?: boolean + ): boolean { + let key: KeyCode | undefined; + + if (keyOrEventOrCombo instanceof KeyboardEvent) { + key = keyOrEventOrCombo.code as KeyCode; + ctrl = keyOrEventOrCombo.ctrlKey; + alt = keyOrEventOrCombo.altKey; + shift = keyOrEventOrCombo.shiftKey; + meta = keyOrEventOrCombo.metaKey; + } else if (keyOrEventOrCombo instanceof KeyCombo) { + return ( + this.key === keyOrEventOrCombo.key && + this.ctrl === keyOrEventOrCombo.ctrl && + this.alt === keyOrEventOrCombo.alt && + this.shift === keyOrEventOrCombo.shift && + this.meta === keyOrEventOrCombo.meta + ); + } + + return this.matchesKeyCombo(key, ctrl ?? false, alt ?? false, shift ?? false, meta ?? false); + } + + public matchesKeyCombo( + key: KeyCode | undefined | null, + ctrl: boolean, + alt: boolean, + shift: boolean, + meta: boolean + ): boolean { + if (!key) return false; + + if (this.key) { + return ( + this.key === key && + this.ctrl === ctrl && + this.alt === alt && + this.shift === shift && + this.meta === meta + ); + } else + return false; + } + + public get asArray(): string[] { + const combo: string[] = []; + if (this.meta) combo.push("Meta"); + if (this.alt) combo.push("Alt"); + if (this.ctrl) combo.push("Ctrl"); + if (this.shift) combo.push("Shift"); + if (this.key) + combo.push( + this.key + .replace("Key", "") + .replace("Digit", "") + .replace("Semicolon", ";") + .replace("Equal", "=") + .replace("Comma", ",") + .replace("Minus", "-") + .replace("Period", ".") + .replace("Slash", "/") + .replace("Backquote", "`") + .replace("BracketLeft", "[") + .replace("Backslash", "\\") + .replace("BracketRight", "]") + .replace("Quote", "'") + ); + + return combo; + } + + public get asString(): string { + return this.asArray.join(" + "); + } + + public toString(): string { + return this.asString; + } + + public static fromKeyboardEvent(event: KeyboardEvent): KeyCombo { + const combo = new KeyCombo(); + + if (event.metaKey) combo.withMetaKey(); + if (event.altKey) combo.withAltKey(); + if (event.ctrlKey) combo.withCtrlKey(); + if (event.shiftKey) combo.withShiftKey(); + + const key = event.key.toUpperCase(); + + if (["ALT", "CONTROL", "SHIFT", "META"].indexOf(key) < 0) { + const keyCode = KeyCode[event.code as keyof typeof KeyCode]; + if (!keyCode) throw new Error("Unknown keycode: " + event.code); + combo.withKey(keyCode); + } + + return combo; + } + + public static fromKeyCombo(keyCombo: KeyCombo): KeyCombo { + return keyCombo.clone(); + } +} diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/shortcut-manager.ts b/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/shortcut-manager.ts index 4bcef4f5..221b4964 100644 --- a/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/shortcut-manager.ts +++ b/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/shortcut-manager.ts @@ -1,60 +1,67 @@ -import {Constructable, DI, IContainer, ILogger} from "aurelia"; +import {Constructable, IContainer, ILogger} from "aurelia"; +import {IEventBus, Settings, SettingsUpdatedEvent} from "@domain"; import {Shortcut} from "./shortcut"; import {ShortcutActionExecutionContext} from "./shortcut-action-execution-context"; -import {IEventBus} from "@domain"; - -export interface IShortcutManager { - /** - * Initializes the ShortcutManager and starts listening for keyboard events. - */ - initialize(): void; - - /** - * Adds a shortcut to the shortcut registry. - * @param shortcut The shortcut to register. - */ - registerShortcut(shortcut: Shortcut): void; - - /** - * Removes a shortcut from the shortcut registry. - * @param shortcut The shortcut to unregister. - */ - unregisterShortcut(shortcut: Shortcut): void; - - /** - * Finds a shortcut by its name, if one exists. - * @param name The name of the shortcut to get. - */ - getShortcutByName(name: string): Shortcut | undefined; - - /** - * Executes a shortcut. - * @param shortcut - */ - executeShortcut(shortcut: Shortcut): void; -} - -export const IShortcutManager = DI.createInterface(); +import {BuiltinShortcuts} from "./builtin-shortcuts"; +import {IShortcutManager} from "./ishortcut-manager"; export class ShortcutManager implements IShortcutManager { private readonly registry: Shortcut[] = []; private readonly logger: ILogger; constructor( + private readonly settings: Settings, @IEventBus private readonly eventBus: IEventBus, @IContainer private readonly container: IContainer, @ILogger logger: ILogger) { this.logger = logger.scopeTo(nameof(ShortcutManager)); } - public getShortcutByName(name: string): Shortcut | undefined { - return this.registry.find(s => s.name === name); + public initialize() { + this.logger.debug("Initializing"); + + const builtInShortcuts = [...BuiltinShortcuts]; + + const addOrUpdateShortcuts = (settings: Settings) => { + const configs = settings.keyboardShortcuts.shortcuts; + + for (const builtinShortcut of builtInShortcuts) { + let shortcut = this.getShortcut(builtinShortcut.id); + + if (!shortcut) { + this.registerShortcut(builtinShortcut); + shortcut = builtinShortcut; + } + + const config = configs.find(x => x.id === shortcut!.id); + + if (config) { + shortcut.keyCombo.updateFrom(config); + } else { + shortcut.resetKeyCombo(); + } + } + }; + + this.eventBus.subscribeToServer(SettingsUpdatedEvent, event => addOrUpdateShortcuts(event.settings)); + + addOrUpdateShortcuts(this.settings); + + // Listen and process keyboard events + document.addEventListener("keydown", async (ev) => { + const shortcut = this.registry.find((s) => s.isEnabled && s.keyCombo.matches(ev)); + if (!shortcut) return; + + ev.preventDefault(); + + await this.executeShortcut(shortcut); + }); } public registerShortcut(shortcut: Shortcut) { - this.logger.debug(`Registering shortcut "${shortcut.name}"`); + this.logger.debug(`Registering shortcut "${shortcut.name}" (${shortcut.keyCombo.asString})`); - const existing = this.registry.findIndex((s) => s.matches(shortcut)); + const existing = this.registry.findIndex((s) => s.keyCombo.matches(shortcut.keyCombo)); if (existing >= 0) { this.registry[existing] = shortcut; @@ -63,8 +70,16 @@ export class ShortcutManager implements IShortcutManager { } } + public getShortcut(id: string): Shortcut | undefined { + return this.registry.find(s => s.id === id); + } + + public getShortcutByName(name: string): Shortcut | undefined { + return this.registry.find(s => s.name === name); + } + public unregisterShortcut(shortcut: Shortcut) { - this.logger.debug(`Unregistering shortcut "${shortcut.name}"`); + this.logger.debug(`Unregistering shortcut "${shortcut.name}" (${shortcut.keyCombo.asString})`); const ix = this.registry.indexOf(shortcut); @@ -73,21 +88,8 @@ export class ShortcutManager implements IShortcutManager { } } - public initialize() { - this.logger.debug("Initializing"); - - document.addEventListener("keydown", (ev) => { - const shortcut = this.registry.find((s) => s.isEnabled && s.matches(ev)); - if (!shortcut) return; - - this.executeShortcut(shortcut); - - ev.preventDefault(); - }); - } - - public executeShortcut(shortcut: Shortcut) { - this.logger.debug(`Executing shortcut "${shortcut.name}"`); + public async executeShortcut(shortcut: Shortcut) { + this.logger.debug(`Executing shortcut "${shortcut.name}" (${shortcut.keyCombo.asString})`); if (shortcut.action) { const context = new ShortcutActionExecutionContext(this.container); @@ -95,9 +97,14 @@ export class ShortcutManager implements IShortcutManager { } if (shortcut.event) { - const event = Object.hasOwnProperty.bind(shortcut.event)("prototype") - ? new (shortcut.event as Constructable)() - : (shortcut.event as () => Record)(); + let event: InstanceType; + + if (Object.hasOwnProperty.bind(shortcut.event)("prototype")) { + event = new (shortcut.event as Constructable)(); + } else { + const eventOrPromise = (shortcut.event as () => (unknown | Promise))(); + event = await Promise.resolve(eventOrPromise) as InstanceType; + } this.eventBus.publish(event); } diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/shortcut.ts b/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/shortcut.ts index f7e5cbb1..c5e0858c 100644 --- a/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/shortcut.ts +++ b/src/Apps/NetPad.Apps.App/App/src/core/@application/shortcuts/shortcut.ts @@ -1,52 +1,64 @@ import {Constructable} from "@aurelia/kernel/src/interfaces"; import {KeyCode} from "@common"; import {ShortcutActionExecutionContext} from "./shortcut-action-execution-context"; +import {KeyCombo} from "./key-combo"; /** * A shortcut that executes an action. */ export class Shortcut { - public ctrlKey = false; - public altKey = false; - public shiftKey = false; - public metaKey = false; - public key?: KeyCode; - public keyExpression?: (keyCode: KeyCode) => boolean; + public id: string; + public name: string; public action?: (context: ShortcutActionExecutionContext) => void; - public event?: Constructable | (() => unknown); + public event?: Constructable | (() => (unknown | Promise)); public isConfigurable = false; public isEnabled = false; + public keyCombo: KeyCombo; + public defaultKeyCombo: KeyCombo; + + constructor(id: string, name: string) { + this.id = id; + this.name = name; + this.keyCombo = new KeyCombo(); + this.defaultKeyCombo = new KeyCombo(); + } - constructor(public name: string) { + public withCtrlKey(required = true): Shortcut { + this.keyCombo.withCtrlKey(required); + return this; } - public withKey(key: KeyCode): Shortcut { - this.key = key; + public withAltKey(required = true): Shortcut { + this.keyCombo.withAltKey(required); return this; } - public withKeyExpression(expression: (keyCode: KeyCode) => boolean): Shortcut { - this.keyExpression = expression; + public withShiftKey(required = true): Shortcut { + this.keyCombo.withShiftKey(required); return this; } - public withCtrlKey(mustBePressed = true): Shortcut { - this.ctrlKey = mustBePressed; + public withMetaKey(required = true): Shortcut { + this.keyCombo.withMetaKey(required); return this; } - public withAltKey(mustBePressed = true): Shortcut { - this.altKey = mustBePressed; + public withKey(key: KeyCode): Shortcut { + this.keyCombo.withKey(key); return this; } - public withShiftKey(mustBePressed = true): Shortcut { - this.shiftKey = mustBePressed; + public captureDefaultKeyCombo(): Shortcut { + this.defaultKeyCombo = this.keyCombo.clone(); return this; } - public withMetaKey(mustBePressed = true): Shortcut { - this.metaKey = mustBePressed; + public get isDefaultKeyCombo(): boolean { + return this.defaultKeyCombo.matches(this.keyCombo); + } + + public resetKeyCombo(): Shortcut { + this.keyCombo.updateFrom(this.defaultKeyCombo); return this; } @@ -55,10 +67,10 @@ export class Shortcut { return this; } - public firesEvent(eventGetter: () => unknown): Shortcut; + public firesEvent(eventGetter: () => (unknown | Promise)): Shortcut; public firesEvent(eventType: TEventType): Shortcut; - public firesEvent(eventTypeOrGetter: TEventType | (() => unknown)): Shortcut { + public firesEvent(eventTypeOrGetter: TEventType | (() => (unknown | Promise))): Shortcut { this.event = eventTypeOrGetter; return this; } @@ -73,114 +85,7 @@ export class Shortcut { return this; } - /** - * Determines if this shortcut matches the specified key combination. - * @param key Key code. - * @param ctrl Whether the ctrl key is pressed. - * @param alt Whether the alt key is pressed. - * @param shift Whether the shift key is pressed. - * @param meta Whether the meta key is pressed. - */ - public matches( - key: KeyCode | undefined, - ctrl: boolean, - alt: boolean, - shift: boolean, - meta: boolean - ): boolean; - - /** - * Determines if this shortcut matches they key combination in the specified keyboard event. - * @param event The keyboard event. - */ - public matches(event: KeyboardEvent): boolean; - - /** - * Determines if this shortcut has the same key combination as the specified shortcut. - * @param shortcut The shortcut to compare with. - */ - public matches(shortcut: Shortcut): boolean; - - public matches( - keyOrEventOrShortcut: KeyCode | undefined | KeyboardEvent | Shortcut, - ctrl?: boolean, - alt?: boolean, - shift?: boolean, - meta?: boolean - ): boolean { - let key: KeyCode | undefined; - - if (keyOrEventOrShortcut instanceof KeyboardEvent) { - key = keyOrEventOrShortcut.code as KeyCode; - ctrl = keyOrEventOrShortcut.ctrlKey; - alt = keyOrEventOrShortcut.altKey; - shift = keyOrEventOrShortcut.shiftKey; - meta = keyOrEventOrShortcut.metaKey; - } else if (keyOrEventOrShortcut instanceof Shortcut) { - return ( - this.key === keyOrEventOrShortcut.key && - this.keyExpression?.toString() === keyOrEventOrShortcut.keyExpression?.toString() && - this.ctrlKey === keyOrEventOrShortcut.ctrlKey && - this.altKey === keyOrEventOrShortcut.altKey && - this.shiftKey === keyOrEventOrShortcut.shiftKey && - this.metaKey === keyOrEventOrShortcut.metaKey - ); - } - - return this.matchesKeyCombo(key, ctrl ?? false, alt ?? false, shift ?? false, meta ?? false); - } - - public matchesKeyCombo( - key: KeyCode | undefined | null, - ctrl: boolean, - alt: boolean, - shift: boolean, - meta: boolean - ): boolean { - if (!key) return false; - - if (this.key) { - return ( - this.key === key && - this.ctrlKey === ctrl && - this.altKey === alt && - this.shiftKey === shift && - this.metaKey === meta - ); - } else if (this.keyExpression) { - return this.keyExpression(key); - } else return false; - } - - public get keyComboString(): string { - const combo: string[] = []; - if (this.metaKey) combo.push("Meta"); - if (this.altKey) combo.push("Alt"); - if (this.ctrlKey) combo.push("Ctrl"); - if (this.shiftKey) combo.push("Shift"); - if (this.key) - combo.push( - this.key - .replace("Key", "") - .replace("Digit", "") - .replace("Semicolon", ";") - .replace("Equal", "=") - .replace("Comma", ",") - .replace("Minus", "-") - .replace("Period", ".") - .replace("Slash", "/") - .replace("Backquote", "`") - .replace("BracketLeft", "[") - .replace("Backslash", "\\") - .replace("BracketRight", "]") - .replace("Quote", "\"") - ); - if (this.keyExpression) combo.push("Custom Expression"); - - return combo.join(" + ").trim(); - } - public toString(): string { - return `${this.name} (${this.keyComboString})`; + return `${this.name} (${this.keyCombo.toString()})`; } } diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@common/utils/key-codes.ts b/src/Apps/NetPad.Apps.App/App/src/core/@common/utils/key-codes.ts index 14dde4f2..6acb6df9 100644 --- a/src/Apps/NetPad.Apps.App/App/src/core/@common/utils/key-codes.ts +++ b/src/Apps/NetPad.Apps.App/App/src/core/@common/utils/key-codes.ts @@ -103,3 +103,110 @@ export enum KeyCode { BracketRight = "BracketRight", Quote = "Quote", } + +/* eslint-disable @typescript-eslint/no-duplicate-enum-values */ +export enum KeyCodeNum { + Backspace = 1, + Tab = 2, + Enter = 3, + ShiftLeft = 4, + ShiftRight = 4, + ControlLeft = 5, + ControlRight = 5, + AltLeft = 6, + AltRight = 6, + Pause = 7, + CapsLock = 8, + Escape = 9, + Space = 10, + PageUp = 11, + PageDown = 12, + End = 13, + Home = 14, + ArrowLeft = 15, + ArrowUp = 16, + ArrowRight = 17, + ArrowDown = 18, + Insert = 19, + Delete = 20, + Digit0 = 21, + Digit1 = 22, + Digit2 = 23, + Digit3 = 24, + Digit4 = 25, + Digit5 = 26, + Digit6 = 27, + Digit7 = 28, + Digit8 = 29, + Digit9 = 30, + KeyA = 31, + KeyB = 32, + KeyC = 33, + KeyD = 34, + KeyE = 35, + KeyF = 36, + KeyG = 37, + KeyH = 38, + KeyI = 39, + KeyJ = 40, + KeyK = 41, + KeyL = 42, + KeyM = 43, + KeyN = 44, + KeyO = 45, + KeyP = 46, + KeyQ = 47, + KeyR = 48, + KeyS = 49, + KeyT = 50, + KeyU = 51, + KeyV = 52, + KeyW = 53, + KeyX = 54, + KeyY = 55, + KeyZ = 56, + MetaLeft = 57, + MetaRight = 57, + ContextMenu = 58, + Numpad0 = 98, + Numpad1 = 99, + Numpad2 = 100, + Numpad3 = 101, + Numpad4 = 102, + Numpad5 = 103, + Numpad6 = 104, + Numpad7 = 105, + Numpad8 = 106, + Numpad9 = 107, + NumpadMultiply = 108, + NumpadAdd = 109, + NumpadSubtract = 111, + NumpadDecimal = 112, + NumpadDivide = 113, + F1 = 59, + F2 = 60, + F3 = 61, + F4 = 62, + F5 = 63, + F6 = 64, + F7 = 65, + F8 = 66, + F9 = 67, + F10 = 68, + F11 = 69, + F12 = 70, + NumLock = 83, + ScrollLock = 84, + Semicolon = 85, + Equal = 86, + Comma = 87, + Minus = 88, + Period = 89, + Slash = 90, + Backquote = 91, + BracketLeft = 92, + Backslash = 93, + BracketRight = 94, + Quote = 95, +} +/* eslint-enable @typescript-eslint/no-duplicate-enum-values */ diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@domain/api.ts b/src/Apps/NetPad.Apps.App/App/src/core/@domain/api.ts index a9f3974f..154b01d1 100644 --- a/src/Apps/NetPad.Apps.App/App/src/core/@domain/api.ts +++ b/src/Apps/NetPad.Apps.App/App/src/core/@domain/api.ts @@ -4062,6 +4062,7 @@ export class Settings implements ISettings { appearance!: AppearanceOptions; editor!: EditorOptions; results!: ResultsOptions; + keyboardShortcuts!: KeyboardShortcutOptions; omniSharp!: OmniSharpOptions; constructor(data?: ISettings) { @@ -4075,6 +4076,7 @@ export class Settings implements ISettings { this.appearance = new AppearanceOptions(); this.editor = new EditorOptions(); this.results = new ResultsOptions(); + this.keyboardShortcuts = new KeyboardShortcutOptions(); this.omniSharp = new OmniSharpOptions(); } } @@ -4090,6 +4092,7 @@ export class Settings implements ISettings { this.appearance = _data["appearance"] ? AppearanceOptions.fromJS(_data["appearance"]) : new AppearanceOptions(); this.editor = _data["editor"] ? EditorOptions.fromJS(_data["editor"]) : new EditorOptions(); this.results = _data["results"] ? ResultsOptions.fromJS(_data["results"]) : new ResultsOptions(); + this.keyboardShortcuts = _data["keyboardShortcuts"] ? KeyboardShortcutOptions.fromJS(_data["keyboardShortcuts"]) : new KeyboardShortcutOptions(); this.omniSharp = _data["omniSharp"] ? OmniSharpOptions.fromJS(_data["omniSharp"]) : new OmniSharpOptions(); } } @@ -4112,6 +4115,7 @@ export class Settings implements ISettings { data["appearance"] = this.appearance ? this.appearance.toJSON() : undefined; data["editor"] = this.editor ? this.editor.toJSON() : undefined; data["results"] = this.results ? this.results.toJSON() : undefined; + data["keyboardShortcuts"] = this.keyboardShortcuts ? this.keyboardShortcuts.toJSON() : undefined; data["omniSharp"] = this.omniSharp ? this.omniSharp.toJSON() : undefined; return data; } @@ -4134,6 +4138,7 @@ export interface ISettings { appearance: AppearanceOptions; editor: EditorOptions; results: ResultsOptions; + keyboardShortcuts: KeyboardShortcutOptions; omniSharp: OmniSharpOptions; } @@ -4370,6 +4375,125 @@ export interface IResultsOptions { maxCollectionSerializeLength: number; } +export class KeyboardShortcutOptions implements IKeyboardShortcutOptions { + shortcuts!: KeyboardShortcutConfiguration[]; + + constructor(data?: IKeyboardShortcutOptions) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + if (!data) { + this.shortcuts = []; + } + } + + init(_data?: any) { + if (_data) { + if (Array.isArray(_data["shortcuts"])) { + this.shortcuts = [] as any; + for (let item of _data["shortcuts"]) + this.shortcuts!.push(KeyboardShortcutConfiguration.fromJS(item)); + } + } + } + + static fromJS(data: any): KeyboardShortcutOptions { + data = typeof data === 'object' ? data : {}; + let result = new KeyboardShortcutOptions(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.shortcuts)) { + data["shortcuts"] = []; + for (let item of this.shortcuts) + data["shortcuts"].push(item.toJSON()); + } + return data; + } + + clone(): KeyboardShortcutOptions { + const json = this.toJSON(); + let result = new KeyboardShortcutOptions(); + result.init(json); + return result; + } +} + +export interface IKeyboardShortcutOptions { + shortcuts: KeyboardShortcutConfiguration[]; +} + +export class KeyboardShortcutConfiguration implements IKeyboardShortcutConfiguration { + id!: string; + meta!: boolean; + alt!: boolean; + ctrl!: boolean; + shift!: boolean; + key?: KeyCode | undefined; + + constructor(data?: IKeyboardShortcutConfiguration) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.id = _data["id"]; + this.meta = _data["meta"]; + this.alt = _data["alt"]; + this.ctrl = _data["ctrl"]; + this.shift = _data["shift"]; + this.key = _data["key"]; + } + } + + static fromJS(data: any): KeyboardShortcutConfiguration { + data = typeof data === 'object' ? data : {}; + let result = new KeyboardShortcutConfiguration(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["meta"] = this.meta; + data["alt"] = this.alt; + data["ctrl"] = this.ctrl; + data["shift"] = this.shift; + data["key"] = this.key; + return data; + } + + clone(): KeyboardShortcutConfiguration { + const json = this.toJSON(); + let result = new KeyboardShortcutConfiguration(); + result.init(json); + return result; + } +} + +export interface IKeyboardShortcutConfiguration { + id: string; + meta: boolean; + alt: boolean; + ctrl: boolean; + shift: boolean; + key?: KeyCode | undefined; +} + +export type KeyCode = "Backspace" | "Tab" | "Enter" | "ShiftLeft" | "ShiftRight" | "ControlLeft" | "ControlRight" | "AltLeft" | "AltRight" | "Pause" | "CapsLock" | "Escape" | "Space" | "PageUp" | "PageDown" | "End" | "Home" | "ArrowLeft" | "ArrowUp" | "ArrowRight" | "ArrowDown" | "PrintScreen" | "Insert" | "Delete" | "Digit0" | "Digit1" | "Digit2" | "Digit3" | "Digit4" | "Digit5" | "Digit6" | "Digit7" | "Digit8" | "Digit9" | "KeyA" | "KeyB" | "KeyC" | "KeyD" | "KeyE" | "KeyF" | "KeyG" | "KeyH" | "KeyI" | "KeyJ" | "KeyK" | "KeyL" | "KeyM" | "KeyN" | "KeyO" | "KeyP" | "KeyQ" | "KeyR" | "KeyS" | "KeyT" | "KeyU" | "KeyV" | "KeyW" | "KeyX" | "KeyY" | "KeyZ" | "MetaLeft" | "MetaRight" | "ContextMenu" | "Numpad0" | "Numpad1" | "Numpad2" | "Numpad3" | "Numpad4" | "Numpad5" | "Numpad6" | "Numpad7" | "Numpad8" | "Numpad9" | "NumpadMultiply" | "NumpadAdd" | "NumpadSubtract" | "NumpadDecimal" | "NumpadDivide" | "F1" | "F2" | "F3" | "F4" | "F5" | "F6" | "F7" | "F8" | "F9" | "F10" | "F11" | "F12" | "NumLock" | "ScrollLock" | "Semicolon" | "Equal" | "Comma" | "Minus" | "Period" | "Slash" | "Backquote" | "BracketLeft" | "Backslash" | "BracketRight" | "Quote"; + export class OmniSharpOptions implements IOmniSharpOptions { enabled!: boolean; executablePath?: string | undefined; diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@domain/configuration/setting-service.ts b/src/Apps/NetPad.Apps.App/App/src/core/@domain/configuration/settings-service.ts similarity index 71% rename from src/Apps/NetPad.Apps.App/App/src/core/@domain/configuration/setting-service.ts rename to src/Apps/NetPad.Apps.App/App/src/core/@domain/configuration/settings-service.ts index a781f2cb..9232c949 100644 --- a/src/Apps/NetPad.Apps.App/App/src/core/@domain/configuration/setting-service.ts +++ b/src/Apps/NetPad.Apps.App/App/src/core/@domain/configuration/settings-service.ts @@ -1,13 +1,13 @@ import {DI, IHttpClient} from "aurelia"; import {ISettingsApiClient, Settings, SettingsApiClient} from "@domain"; -export interface ISettingService extends ISettingsApiClient { +export interface ISettingsService extends ISettingsApiClient { toggleTheme(): Promise; } -export const ISettingService = DI.createInterface(); +export const ISettingsService = DI.createInterface(); -export class SettingService extends SettingsApiClient implements ISettingService { +export class SettingsService extends SettingsApiClient implements ISettingsService { constructor(readonly settings: Settings, baseUrl: string, @IHttpClient http: IHttpClient) { diff --git a/src/Apps/NetPad.Apps.App/App/src/core/@domain/index.ts b/src/Apps/NetPad.Apps.App/App/src/core/@domain/index.ts index 411f4083..9fd35311 100644 --- a/src/Apps/NetPad.Apps.App/App/src/core/@domain/index.ts +++ b/src/Apps/NetPad.Apps.App/App/src/core/@domain/index.ts @@ -6,7 +6,7 @@ export * from "./events/ievent-bus"; export * from "./events/iipc-gateway"; export * from "./events/channel-info"; -export * from "./configuration/setting-service"; +export * from "./configuration/settings-service"; export * from "./app/app-service"; export * from "./window/window-service"; export * from "./window/window-state"; diff --git a/src/Apps/NetPad.Apps.App/App/src/main.ts b/src/Apps/NetPad.Apps.App/App/src/main.ts index a4881a58..5080bd1d 100644 --- a/src/Apps/NetPad.Apps.App/App/src/main.ts +++ b/src/Apps/NetPad.Apps.App/App/src/main.ts @@ -11,10 +11,10 @@ import { IEventBus, IIpcGateway, ISession, - ISettingService, + ISettingsService, Session, Settings, - SettingService, + SettingsService, } from "@domain"; import { ConsoleLogSink, @@ -50,7 +50,7 @@ const builder = Aurelia.register( Registration.singleton(IIpcGateway, SignalRIpcGateway), Registration.singleton(IEventBus, EventBus), Registration.singleton(ISession, Session), - Registration.singleton(ISettingService, SettingService), + Registration.singleton(ISettingsService, SettingsService), Registration.singleton(AppMutationObserver, AppMutationObserver), Registration.singleton(IBackgroundService, SettingsBackgroundService), LogConfig.register({ @@ -152,7 +152,7 @@ logger.debug(`Configuring platform: ${platform.constructor.name}`); platform.configure(builder); // Load app settings -const settings = await builder.container.get(ISettingService).get(); +const settings = await builder.container.get(ISettingsService).get(); builder.container.get(Settings).init(settings.toJSON()); // Start the app diff --git a/src/Apps/NetPad.Apps.App/App/src/styles/_common.scss b/src/Apps/NetPad.Apps.App/App/src/styles/_common.scss index b90edf7f..8c59454d 100644 --- a/src/Apps/NetPad.Apps.App/App/src/styles/_common.scss +++ b/src/Apps/NetPad.Apps.App/App/src/styles/_common.scss @@ -47,6 +47,14 @@ kbd { cursor: pointer; } +.text-clickable { + @extend %clickable; + + &:hover { + @include theme(color, textHoverColor); + } +} + .btn.btn-basic { background: transparent; } diff --git a/src/Apps/NetPad.Apps.App/App/src/styles/_icons.scss b/src/Apps/NetPad.Apps.App/App/src/styles/_icons.scss index 316773f7..09ff02c9 100644 --- a/src/Apps/NetPad.Apps.App/App/src/styles/_icons.scss +++ b/src/Apps/NetPad.Apps.App/App/src/styles/_icons.scss @@ -452,6 +452,11 @@ button > .icon-base { @extend .fa-pencil; } +.reset-keyboard-shortcut-icon { + @extend %icon-base; + @extend .fa-rotate-left; +} + .editor-background-settings-icon { @extend %icon-base; @extend .fa-align-left; diff --git a/src/Apps/NetPad.Apps.App/App/src/styles/themes/_dark.scss b/src/Apps/NetPad.Apps.App/App/src/styles/themes/_dark.scss index 595320b2..817ab155 100644 --- a/src/Apps/NetPad.Apps.App/App/src/styles/themes/_dark.scss +++ b/src/Apps/NetPad.Apps.App/App/src/styles/themes/_dark.scss @@ -1,6 +1,7 @@ $dark: ( textColor: #dcdcdc, textHighestContrastColor: #fff, + textHoverColor: #fff, textContrastColor: #34314b, backgroundColor: #222, backgroundDarkerColor: #1c1c1c, diff --git a/src/Apps/NetPad.Apps.App/App/src/styles/themes/_light.scss b/src/Apps/NetPad.Apps.App/App/src/styles/themes/_light.scss index 5a565d23..356ede74 100644 --- a/src/Apps/NetPad.Apps.App/App/src/styles/themes/_light.scss +++ b/src/Apps/NetPad.Apps.App/App/src/styles/themes/_light.scss @@ -1,6 +1,7 @@ $light: ( textColor: #34314b, textHighestContrastColor: #000, + textHoverColor: #000, textContrastColor: #d4d4d4, backgroundColor: rgb(243, 243, 243), backgroundDarkerColor: #f3f3f3, diff --git a/src/Apps/NetPad.Apps.App/App/src/windows/main/panes/explorer/explorer.ts b/src/Apps/NetPad.Apps.App/App/src/windows/main/panes/explorer/explorer.ts index 0fec27cd..44d0a4b9 100644 --- a/src/Apps/NetPad.Apps.App/App/src/windows/main/panes/explorer/explorer.ts +++ b/src/Apps/NetPad.Apps.App/App/src/windows/main/panes/explorer/explorer.ts @@ -1,10 +1,10 @@ import Split from "split.js"; -import {IShortcutManager, Pane} from "@application"; +import {IShortcutManager, Pane, ShortcutIds} from "@application"; export class Explorer extends Pane { constructor(@IShortcutManager private readonly shortcutManager: IShortcutManager) { super("Explorer", "explorer-icon"); - this.hasShortcut(shortcutManager.getShortcutByName("Explorer")); + this.hasShortcut(shortcutManager.getShortcut(ShortcutIds.openExplorer)); } public async attached() { diff --git a/src/Apps/NetPad.Apps.App/App/src/windows/main/panes/namespaces-pane/namespaces-pane.ts b/src/Apps/NetPad.Apps.App/App/src/windows/main/panes/namespaces-pane/namespaces-pane.ts index 2cbcc5b6..4528df80 100644 --- a/src/Apps/NetPad.Apps.App/App/src/windows/main/panes/namespaces-pane/namespaces-pane.ts +++ b/src/Apps/NetPad.Apps.App/App/src/windows/main/panes/namespaces-pane/namespaces-pane.ts @@ -1,7 +1,7 @@ -import {IShortcutManager, Pane} from "@application"; -import {IScriptService, ISession} from "@domain"; import {observable} from "@aurelia/runtime"; import {watch} from "@aurelia/runtime-html"; +import {IShortcutManager, Pane, ShortcutIds} from "@application"; +import {IScriptService, ISession} from "@domain"; import {Util} from "@common"; export class NamespacesPane extends Pane { @@ -14,7 +14,7 @@ export class NamespacesPane extends Pane { @IShortcutManager private readonly shortcutManager: IShortcutManager ) { super("Namespaces", "namespaces-icon"); - this.hasShortcut(shortcutManager.getShortcutByName("Namespaces")); + this.hasShortcut(shortcutManager.getShortcut(ShortcutIds.openNamespaces)); } public override get name() { diff --git a/src/Apps/NetPad.Apps.App/App/src/windows/main/panes/output-pane/output-pane.ts b/src/Apps/NetPad.Apps.App/App/src/windows/main/panes/output-pane/output-pane.ts index 7fb71b88..d4328606 100644 --- a/src/Apps/NetPad.Apps.App/App/src/windows/main/panes/output-pane/output-pane.ts +++ b/src/Apps/NetPad.Apps.App/App/src/windows/main/panes/output-pane/output-pane.ts @@ -1,4 +1,4 @@ -import {IShortcutManager, Pane, PaneAction} from "@application"; +import {IShortcutManager, Pane, PaneAction, ShortcutIds} from "@application"; import {ISession, IWindowService, Settings} from "@domain"; import {watch} from "@aurelia/runtime-html"; import {IContainer, PLATFORM} from "aurelia"; @@ -17,7 +17,7 @@ export class OutputPane extends Pane { private readonly settings: Settings) { super("Output", "output-icon", false); - this.hasShortcut(shortcutManager.getShortcutByName("Output")); + this.hasShortcut(shortcutManager.getShortcut(ShortcutIds.openOutput)); this._actions.push(new PaneAction( ' Pop out', diff --git a/src/Apps/NetPad.Apps.App/App/src/windows/main/statusbar/statusbar.ts b/src/Apps/NetPad.Apps.App/App/src/windows/main/statusbar/statusbar.ts index 918e2f06..f0d9dba7 100644 --- a/src/Apps/NetPad.Apps.App/App/src/windows/main/statusbar/statusbar.ts +++ b/src/Apps/NetPad.Apps.App/App/src/windows/main/statusbar/statusbar.ts @@ -3,7 +3,7 @@ import { AppStatusMessagePublishedEvent, IEventBus, ISession, - ISettingService, + ISettingsService, ScriptEnvironment, Settings, } from "@domain"; @@ -23,7 +23,7 @@ export class Statusbar { constructor(private readonly workbench: Workbench, private readonly settings: Settings, @ISession private readonly session: ISession, - @ISettingService private readonly settingsService: ISettingService, + @ISettingsService private readonly settingsService: ISettingsService, @IShortcutManager private readonly shortcutManager: IShortcutManager, private readonly dialogUtil: DialogUtil, @IEventBus private readonly eventBus: IEventBus) { diff --git a/src/Apps/NetPad.Apps.App/App/src/windows/main/titlebar/main-menu/main-menu-service.ts b/src/Apps/NetPad.Apps.App/App/src/windows/main/titlebar/main-menu/main-menu-service.ts index 3b8af3d3..65245d08 100644 --- a/src/Apps/NetPad.Apps.App/App/src/windows/main/titlebar/main-menu/main-menu-service.ts +++ b/src/Apps/NetPad.Apps.App/App/src/windows/main/titlebar/main-menu/main-menu-service.ts @@ -1,8 +1,8 @@ import {DI} from "aurelia"; import {IMenuItem} from "./imenu-item"; import {System} from "@common"; -import {ISettingService, IWindowService} from "@domain"; -import {IShortcutManager} from "@application"; +import {ISettingsService, IWindowService} from "@domain"; +import {IShortcutManager, ShortcutIds} from "@application"; import {ITextEditorService} from "@application/editor/text-editor-service"; import {AppUpdateDialog} from "@application/dialogs/app-update-dialog/app-update-dialog"; import {DialogUtil} from "@application/dialogs/dialog-util"; @@ -25,7 +25,7 @@ export class MainMenuService implements IMainMenuService { private readonly _items: IMenuItem[] = []; constructor( - @ISettingService private readonly settingsService: ISettingService, + @ISettingsService private readonly settingsService: ISettingsService, @IShortcutManager private readonly shortcutManager: IShortcutManager, @ITextEditorService private readonly textEditorService: ITextEditorService, @IWindowService private readonly windowService: IWindowService, @@ -39,12 +39,12 @@ export class MainMenuService implements IMainMenuService { id: "file.new", text: "New", icon: "add-script-icon", - shortcut: this.shortcutManager.getShortcutByName("New"), + shortcut: this.shortcutManager.getShortcut(ShortcutIds.newDocument), }, { id: "file.goToScript", text: "Go to Script", - shortcut: this.shortcutManager.getShortcutByName("Go to Script"), + shortcut: this.shortcutManager.getShortcut(ShortcutIds.quickOpenDocument), }, { isDivider: true @@ -53,25 +53,25 @@ export class MainMenuService implements IMainMenuService { id: "file.save", text: "Save", icon: "save-icon", - shortcut: this.shortcutManager.getShortcutByName("Save"), + shortcut: this.shortcutManager.getShortcut(ShortcutIds.saveDocument), }, { id: "file.saveAll", text: "Save All", icon: "save-icon", - shortcut: this.shortcutManager.getShortcutByName("Save All"), + shortcut: this.shortcutManager.getShortcut(ShortcutIds.saveAllDocuments), }, { id: "file.properties", text: "Properties", icon: "properties-icon", - shortcut: this.shortcutManager.getShortcutByName("Script Properties"), + shortcut: this.shortcutManager.getShortcut(ShortcutIds.openDocumentProperties), }, { id: "file.close", text: "Close", icon: "close-icon", - shortcut: this.shortcutManager.getShortcutByName("Close"), + shortcut: this.shortcutManager.getShortcut(ShortcutIds.closeDocument), }, { isDivider: true @@ -80,7 +80,7 @@ export class MainMenuService implements IMainMenuService { id: "file.settings", text: "Settings", icon: "settings-icon", - shortcut: this.shortcutManager.getShortcutByName("Settings"), + shortcut: this.shortcutManager.getShortcut(ShortcutIds.openSettings), }, { id: "file.exit", @@ -219,26 +219,26 @@ export class MainMenuService implements IMainMenuService { id: "view.output", text: "Output", icon: "output-icon", - shortcut: this.shortcutManager.getShortcutByName("Output"), + shortcut: this.shortcutManager.getShortcut(ShortcutIds.openOutput), }, { id: "view.explorer", text: "Explorer", icon: "explorer-icon", - shortcut: this.shortcutManager.getShortcutByName("Explorer"), + shortcut: this.shortcutManager.getShortcut(ShortcutIds.openExplorer), }, { id: "view.namespaces", text: "Namespaces", icon: "namespaces-icon", - shortcut: this.shortcutManager.getShortcutByName("Namespaces"), + shortcut: this.shortcutManager.getShortcut(ShortcutIds.openNamespaces), }, { isDivider: true }, { text: "Reload", - shortcut: this.shortcutManager.getShortcutByName("Reload"), + shortcut: this.shortcutManager.getShortcut(ShortcutIds.reloadWindow), }, { text: "Toggle Developer Tools", @@ -328,7 +328,7 @@ export class MainMenuService implements IMainMenuService { if (menuItem.click) { await menuItem.click(); } else if (menuItem.shortcut) { - this.shortcutManager.executeShortcut(menuItem.shortcut); + await this.shortcutManager.executeShortcut(menuItem.shortcut); } } diff --git a/src/Apps/NetPad.Apps.App/App/src/windows/main/titlebar/main-menu/main-menu.html b/src/Apps/NetPad.Apps.App/App/src/windows/main/titlebar/main-menu/main-menu.html index 4ae2964c..eca0b408 100644 --- a/src/Apps/NetPad.Apps.App/App/src/windows/main/titlebar/main-menu/main-menu.html +++ b/src/Apps/NetPad.Apps.App/App/src/windows/main/titlebar/main-menu/main-menu.html @@ -36,7 +36,7 @@ ${item.text}