diff --git a/apps/client/src/services/shortcuts.ts b/apps/client/src/services/shortcuts.ts index 2c6345fcb..167dc01d9 100644 --- a/apps/client/src/services/shortcuts.ts +++ b/apps/client/src/services/shortcuts.ts @@ -1,7 +1,7 @@ import utils from "./utils.js"; type ElementType = HTMLElement | Document; -type Handler = (e: KeyboardEvent) => void; +export type Handler = (e: KeyboardEvent) => void; export interface ShortcutBinding { element: HTMLElement | Document; diff --git a/apps/client/src/widgets/launch_bar/GenericButtons.tsx b/apps/client/src/widgets/launch_bar/GenericButtons.tsx index 06e5bd750..a348bef9a 100644 --- a/apps/client/src/widgets/launch_bar/GenericButtons.tsx +++ b/apps/client/src/widgets/launch_bar/GenericButtons.tsx @@ -2,16 +2,22 @@ import appContext from "../../components/app_context"; import FNote from "../../entities/fnote"; import link_context_menu from "../../menus/link_context_menu"; import { escapeHtml, isCtrlKey } from "../../services/utils"; +import { useGlobalShortcut, useNoteLabel } from "../react/hooks"; import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets"; export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNoteId }: { - launcherNote: FNote, - getTargetNoteId: (launcherNote: FNote) => string | null | Promise, - getHoistedNoteId?: (launcherNote: FNote) => string | null + launcherNote: FNote; + getTargetNoteId: (launcherNote: FNote) => string | null | Promise; + getHoistedNoteId?: (launcherNote: FNote) => string | null; + keyboardShortcut?: string; }) { const { icon, title } = useLauncherIconAndTitle(launcherNote); - async function launch(evt: MouseEvent) { + // Keyboard shortcut. + const [ shortcut ] = useNoteLabel(launcherNote, "keyboardShortcut"); + useGlobalShortcut(shortcut, launch); + + async function launch(evt: MouseEvent | KeyboardEvent) { if (evt.which === 3) { return; } diff --git a/apps/client/src/widgets/launch_bar/LauncherDefinitions.tsx b/apps/client/src/widgets/launch_bar/LauncherDefinitions.tsx index a47326ebe..9c4f915e1 100644 --- a/apps/client/src/widgets/launch_bar/LauncherDefinitions.tsx +++ b/apps/client/src/widgets/launch_bar/LauncherDefinitions.tsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useMemo, useState } from "preact/hooks"; -import { useLegacyWidget, useNoteContext, useNoteLabel, useNoteRelationTarget, useTriliumOptionBool } from "../react/hooks"; +import { useGlobalShortcut, useLegacyWidget, useNoteContext, useNoteLabel, useNoteRelationTarget, useTriliumOptionBool } from "../react/hooks"; import { ParentComponent } from "../react/react_utils"; import BasicWidget from "../basic_widget"; import FNote from "../../entities/fnote"; @@ -52,20 +52,27 @@ export function NoteLauncher({ launcherNote, ...restProps }: { launcherNote: FNo export function ScriptLauncher({ launcherNote }: LauncherNoteProps) { const { icon, title } = useLauncherIconAndTitle(launcherNote); + + async function launch() { + if (launcherNote.isLabelTruthy("scriptInLauncherContent")) { + await launcherNote.executeScript(); + } else { + const script = await launcherNote.getRelationTarget("script"); + if (script) { + await script.executeScript(); + } + } + } + + // Keyboard shortcut. + const [ shortcut ] = useNoteLabel(launcherNote, "keyboardShortcut"); + useGlobalShortcut(shortcut, launch); + return ( { - if (launcherNote.isLabelTruthy("scriptInLauncherContent")) { - await launcherNote.executeScript(); - } else { - const script = await launcherNote.getRelationTarget("script"); - if (script) { - await script.executeScript(); - } - } - }} + onClick={launch} /> ) } diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 3646113cd..3cd4cffff 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -20,9 +20,9 @@ import options, { type OptionValue } from "../../services/options"; import protected_session_holder from "../../services/protected_session_holder"; import SpacedUpdate from "../../services/spaced_update"; import toast, { ToastOptions } from "../../services/toast"; -import utils, { escapeRegExp, reloadFrontendApp } from "../../services/utils"; +import utils, { escapeRegExp, randomString, reloadFrontendApp } from "../../services/utils"; import server from "../../services/server"; -import { removeIndividualBinding } from "../../services/shortcuts"; +import shortcuts, { Handler, removeIndividualBinding } from "../../services/shortcuts"; import froca from "../../services/froca"; export function useTriliumEvent(eventName: T, handler: (data: EventData) => void) { @@ -812,6 +812,21 @@ export function useKeyboardShortcuts(scope: "code-detail" | "text-detail", conta }, [ scope, containerRef, parentComponent, ntxId ]); } +/** + * Register a global shortcut. Internally it uses the shortcut service and assignes a random namespace to make it unique. + * + * @param keyboardShortcut the keyboard shortcut combination to register. + * @param handler the corresponding handler to be called when the keyboard shortcut is invoked by the user. + */ +export function useGlobalShortcut(keyboardShortcut: string | null | undefined, handler: Handler) { + useEffect(() => { + if (!keyboardShortcut) return; + const namespace = randomString(10); + shortcuts.bindGlobalShortcut(keyboardShortcut, handler, namespace); + return () => shortcuts.removeGlobalShortcut(namespace); + }, [ keyboardShortcut, handler ]); +} + /** * Indicates that the current note is in read-only mode, while an editing mode is available, * and provides a way to switch to editing mode. diff --git a/packages/commons/src/lib/attribute_names.ts b/packages/commons/src/lib/attribute_names.ts index 16793b964..0295af0b2 100644 --- a/packages/commons/src/lib/attribute_names.ts +++ b/packages/commons/src/lib/attribute_names.ts @@ -27,6 +27,7 @@ type Labels = { // Launch bar bookmarkFolder: boolean; command: string; + keyboardShortcut: string; // Collection-specific viewType: string;