diff --git a/package.json b/package.json
index 10d7069c38..72c281c52b 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,6 @@
"fast-array-diff": "1.1.0",
"fast-base64": "0.1.8",
"glob": "8.0.3",
- "hotkeys-js": "3.13.6",
"immer": "9.0.21",
"markdown-it": "13.0.2",
"midi-file": "1.2.4",
diff --git a/src/components/App.vue b/src/components/App.vue
index 1dc3451b31..6712656d82 100644
--- a/src/components/App.vue
+++ b/src/components/App.vue
@@ -20,7 +20,7 @@
diff --git a/src/components/Menu/MenuBar/MenuBar.vue b/src/components/Menu/MenuBar/MenuBar.vue
index 4efd2c0f46..ccba686cb3 100644
--- a/src/components/Menu/MenuBar/MenuBar.vue
+++ b/src/components/Menu/MenuBar/MenuBar.vue
@@ -32,8 +32,9 @@ import { MenuItemData, MenuItemRoot } from "../type";
import MenuButton from "../MenuButton.vue";
import TitleBarButtons from "./TitleBarButtons.vue";
import TitleBarEditorSwitcher from "./TitleBarEditorSwitcher.vue";
+import { EditorType } from "@/type/preload";
import { useStore } from "@/store";
-import { HotkeyAction, useHotkeyManager } from "@/plugins/hotkeyPlugin";
+import { useHotkeyManager } from "@/plugins/hotkeyPlugin";
import { useEngineIcons } from "@/composables/useEngineIcons";
const props = defineProps<{
@@ -42,7 +43,7 @@ const props = defineProps<{
/** 「編集」メニューのサブメニュー */
editSubMenuData: MenuItemData[];
/** エディタの種類 */
- editor: "talk" | "song";
+ editor: EditorType;
}>();
const store = useStore();
@@ -499,34 +500,23 @@ watch(uiLocked, () => {
}
});
-/**
- * 全エディタに対してホットキーを登録する
- * FIXME: hotkeyPlugin側で全エディタに対して登録できるようにする
- */
-function registerHotkeyForAllEditors(action: Omit) {
- registerHotkeyWithCleanup({
- editor: "talk",
- ...action,
- });
- registerHotkeyWithCleanup({
- editor: "song",
- ...action,
- });
-}
-
-registerHotkeyForAllEditors({
+registerHotkeyWithCleanup({
+ editor: "talk&song",
callback: createNewProject,
name: "新規プロジェクト",
});
-registerHotkeyForAllEditors({
+registerHotkeyWithCleanup({
+ editor: "talk&song",
callback: saveProject,
name: "プロジェクトを上書き保存",
});
-registerHotkeyForAllEditors({
+registerHotkeyWithCleanup({
+ editor: "talk&song",
callback: saveProjectAs,
name: "プロジェクトを名前を付けて保存",
});
-registerHotkeyForAllEditors({
+registerHotkeyWithCleanup({
+ editor: "talk&song",
callback: importProject,
name: "プロジェクトを読み込む",
});
diff --git a/src/plugins/hotkeyPlugin.ts b/src/plugins/hotkeyPlugin.ts
index c187075ef7..4301cfc7ca 100644
--- a/src/plugins/hotkeyPlugin.ts
+++ b/src/plugins/hotkeyPlugin.ts
@@ -4,15 +4,15 @@
* HotkeyAction: 実行する処理の名前とコールバックのペア
* HotkeySetting: ユーザーが設定できるもの。ActionとCobinationのペア
* Combination: ショートカットキーを文字列で表したもの
- * binding: hotkeys-js に登録したコールバック
- * bindingKey: hotkeys-js で使う、キーの文字列表記
+ * binding: 登録したコールバック
+ * bindingKey: キーの文字列表記
*/
import { Plugin, inject, onMounted, onUnmounted } from "vue";
-import hotkeys from "hotkeys-js";
import {
HotkeyActionNameType,
HotkeyCombination,
HotkeySettingType,
+ EditorType,
} from "@/type/preload";
import { createLogger } from "@/domain/frontend/log";
@@ -33,7 +33,7 @@ export const useHotkeyManager = () => {
return { hotkeyManager, registerHotkeyWithCleanup };
};
-type Editor = "talk" | "song";
+type Editor = "talk" | "song" | "talk&song";
type BindingKey = string & { __brand: "BindingKey" }; // BindingKey専用のブランド型
@@ -51,22 +51,6 @@ export type HotkeyAction = {
callback: (e: KeyboardEvent) => void;
};
-export type HotkeysJs = {
- (
- key: BindingKey,
- options: {
- scope: string;
- },
- callback: (e: KeyboardEvent) => void,
- ): void;
- unbind: (key: BindingKey, scope: string) => void;
- setScope: (scope: string) => void;
-};
-
-// デフォルトはテキストボックス内でショートカットキー無効なので有効にする
-hotkeys.filter = () => {
- return true;
-};
type Log = (message: string, ...args: unknown[]) => void;
type RegisteredCombination = {
@@ -92,20 +76,17 @@ const isNotSameHotkeyTarget = (a: HotkeyTarget) => (b: HotkeyTarget) => {
export class HotkeyManager {
/** 登録されたHotkeyAction */
private actions: HotkeyAction[] = [];
+ /** スコープ */
+ private scope: EditorType | undefined;
/** ユーザーのショートカットキー設定 */
private settings: HotkeySettingType[] | undefined; // ユーザーのショートカットキー設定
- /** hotkeys-jsに登録されたショートカットキーの組み合わせ */
+ /** 登録されたショートカットキーの組み合わせ */
private registeredCombinations: RegisteredCombination[] = [];
- private hotkeys: HotkeysJs;
private log: Log;
- constructor(
- hotkeys_: HotkeysJs = hotkeys,
- log: Log = createLogger("HotkeyManager").info,
- ) {
+ constructor(log: Log = createLogger("HotkeyManager").info) {
this.log = log;
- this.hotkeys = hotkeys_;
}
/**
@@ -170,7 +151,6 @@ export class HotkeyManager {
for (const combination of combinations) {
const bindingKey = combinationToBindingKey(combination.combination);
this.log("Unbind:", bindingKey, "in", combination.editor);
- this.hotkeys.unbind(bindingKey, combination.editor);
this.registeredCombinations = this.registeredCombinations.filter(
isNotSameHotkeyTarget(combination),
);
@@ -188,33 +168,6 @@ export class HotkeyManager {
"in",
action.editor,
);
- this.hotkeys(
- combinationToBindingKey(setting.combination),
- { scope: action.editor },
- (e) => {
- const element = e.target;
- // メニュー項目ではショートカットキーを無効化
- if (
- element instanceof HTMLElement &&
- element.classList.contains("q-item")
- ) {
- return;
- }
- if (!action.enableInTextbox) {
- if (
- element instanceof HTMLElement &&
- (element.tagName === "INPUT" ||
- element.tagName === "SELECT" ||
- element.tagName === "TEXTAREA" ||
- element.contentEditable === "true")
- ) {
- return;
- }
- }
- e.preventDefault();
- action.callback(e);
- },
- );
this.registeredCombinations = this.registeredCombinations.filter(
isNotSameHotkeyTarget(action),
);
@@ -265,27 +218,78 @@ export class HotkeyManager {
/**
* エディタが変更されたときに呼び出される。
*/
- onEditorChange(editor: "talk" | "song"): void {
- this.hotkeys.setScope(editor);
+ onEditorChange(editor: EditorType): void {
+ this.scope = editor;
this.log("Editor changed to", editor);
}
+
+ keyInput(e: KeyboardEvent): void {
+ const element = e.target;
+
+ if (this.scope == undefined) {
+ console.error("hotkeyPluginのスコープが未設定です");
+ return;
+ }
+
+ // メニュー項目・ダイアログではショートカットキーを無効化
+ if (
+ element instanceof HTMLElement &&
+ (element.getAttribute("role") == "menu" ||
+ element.classList.contains("q-dialog__inner"))
+ ) {
+ return;
+ }
+
+ const isInTextbox =
+ element instanceof HTMLElement &&
+ (element.tagName === "INPUT" ||
+ element.tagName === "SELECT" ||
+ element.tagName === "TEXTAREA" ||
+ element.contentEditable === "true");
+
+ const combination = combinationToBindingKey(eventToCombination(e));
+
+ const actions = this.actions
+ .filter((item) => !isInTextbox || item.enableInTextbox)
+ .filter(
+ (item) =>
+ combinationToBindingKey(this.getSetting(item).combination) ==
+ combination,
+ )
+ .filter((item) => {
+ if (item.editor === "talk&song") {
+ return this.scope === "talk" || this.scope === "song";
+ } else if (item.editor === "talk") {
+ return this.scope === "talk";
+ } else if (item.editor === "song") {
+ return this.scope === "song";
+ } else {
+ console.error("scopeに対する処理が設定されていません");
+ }
+ });
+ if (actions.length == 0) {
+ return;
+ }
+ e.preventDefault();
+ actions.forEach((action) => action.callback(e));
+ }
+
+ /**
+ * 現在登録されているHotkeyActionをすべて取得する
+ */
+ getAllActions(): HotkeyAction[] {
+ return this.actions;
+ }
}
-/** hotkeys-js用のキーに変換する */
+/** 判定用のキーに変換する */
const combinationToBindingKey = (
combination: HotkeyCombination,
): BindingKey => {
// MetaキーはCommandキーとして扱う
- // NOTE: hotkeys-jsにはWinキーが無く、Commandキーとして扱われている
// NOTE: Metaキーは以前採用していたmousetrapがそうだった名残り
- // NOTE: hotkeys-jsでは方向キーのarrowプレフィックスが不要
- const bindingKey = combination
- .toLowerCase()
- .split(" ")
- .map((key) => (key === "meta" ? "command" : key))
- .map((key) => key.replace("arrow", ""))
- .join("+");
- return bindingKey as BindingKey;
+ // 順番が違うものも一致させるために並べ替え
+ return combination.split(" ").sort().join(" ") as BindingKey;
};
export const hotkeyPlugin: Plugin = {
diff --git a/tests/unit/lib/hotkeyManager.spec.ts b/tests/unit/lib/hotkeyManager.spec.ts
index 57c1997d10..9aefe31927 100644
--- a/tests/unit/lib/hotkeyManager.spec.ts
+++ b/tests/unit/lib/hotkeyManager.spec.ts
@@ -1,54 +1,44 @@
import { describe, it, expect, beforeEach } from "vitest";
-import { HotkeyManager, HotkeysJs, HotkeyAction } from "@/plugins/hotkeyPlugin";
+import { HotkeyManager, HotkeyAction } from "@/plugins/hotkeyPlugin";
import { HotkeyCombination, HotkeySettingType } from "@/type/preload";
-type DummyHotkeysJs = HotkeysJs & {
- registeredHotkeys: {
- key: string;
- scope: string;
- // callbackを持たせると比較が面倒になるので持たせない
- // callback: (e: KeyboardEvent) => void;
- }[];
- currentScope: string;
+type DummyKeyboardEvent = {
+ key: string;
+ code: string;
+ target: EventTarget | null;
+ ctrlKey: boolean;
+ shiftKey: boolean;
+ altKey: boolean;
+ metaKey: boolean;
+ preventDefault: () => void;
};
-const createHotkeyManager = (): {
- hotkeyManager: HotkeyManager;
- dummyHotkeysJs: DummyHotkeysJs;
-} => {
- const registeredHotkeys: DummyHotkeysJs["registeredHotkeys"] = [];
- const dummyHotkeysJs: DummyHotkeysJs = (
- key: string,
- { scope }: { scope: string },
- ) => {
- if (registeredHotkeys.some((h) => h.key === key && h.scope === scope)) {
- throw new Error("assert: duplicate key");
- }
- registeredHotkeys.push({ key, scope });
- };
- dummyHotkeysJs.unbind = (key: string) => {
- const index = dummyHotkeysJs.registeredHotkeys.findIndex(
- (h) => h.key === key,
- );
- if (index === -1) {
- throw new Error("assert: unknown binding");
- }
- registeredHotkeys.splice(index, 1);
- };
- dummyHotkeysJs.setScope = (scope: string) => {
- dummyHotkeysJs.currentScope = scope;
- };
- dummyHotkeysJs.registeredHotkeys = registeredHotkeys;
- dummyHotkeysJs.currentScope = "talk";
- return {
- hotkeyManager: new HotkeyManager(dummyHotkeysJs, () => {
+const createDummyInput = (
+ combinationKey: string,
+ combinationCode: string,
+): DummyKeyboardEvent => {
+ const dummyInput: DummyKeyboardEvent = {
+ key: combinationKey,
+ code: combinationCode,
+ target: null,
+ ctrlKey: false,
+ shiftKey: false,
+ altKey: false,
+ metaKey: false,
+ preventDefault: () => {
/* noop */
- }),
- dummyHotkeysJs,
+ },
};
+ return dummyInput;
+};
+
+const createHotkeyManager = (): HotkeyManager => {
+ return new HotkeyManager(() => {
+ /* noop */
+ });
};
it("registerできる", () => {
- const { hotkeyManager } = createHotkeyManager();
+ const hotkeyManager = createHotkeyManager();
hotkeyManager.register({
editor: "talk",
name: "音声書き出し",
@@ -59,13 +49,11 @@ it("registerできる", () => {
});
it("unregisterできる", () => {
- const { hotkeyManager, dummyHotkeysJs } = createHotkeyManager();
+ const hotkeyManager = createHotkeyManager();
const action = {
editor: "talk",
name: "音声書き出し",
- callback: () => {
- /* noop */
- },
+ callback: vi.fn(),
} as const;
hotkeyManager.load([
{
@@ -73,21 +61,19 @@ it("unregisterできる", () => {
combination: HotkeyCombination("1"),
},
]);
+ hotkeyManager.onEditorChange("talk");
hotkeyManager.register(action);
- expect(dummyHotkeysJs.registeredHotkeys).toEqual([
- { key: "1", scope: "talk" },
- ]);
+ expect(hotkeyManager.getAllActions()).toStrictEqual([action]);
+
hotkeyManager.unregister(action);
- expect(dummyHotkeysJs.registeredHotkeys).toEqual([]);
+ expect(hotkeyManager.getAllActions()).toStrictEqual([]);
});
-const callback = () => {
- /* noop */
-};
+const callback = vi.fn();
const dummyAction: HotkeyAction = {
editor: "talk",
name: "音声書き出し",
- callback,
+ callback: callback,
};
const createDummySetting = (combination: string): HotkeySettingType => ({
action: "音声書き出し",
@@ -96,63 +82,61 @@ const createDummySetting = (combination: string): HotkeySettingType => ({
describe("設定変更", () => {
let hotkeyManager: HotkeyManager;
- let dummyHotkeysJs: DummyHotkeysJs;
beforeEach(() => {
- const { hotkeyManager: hotkeyManager_, dummyHotkeysJs: dummyHotkeysJs_ } =
- createHotkeyManager();
+ const hotkeyManager_ = createHotkeyManager();
hotkeyManager = hotkeyManager_;
- dummyHotkeysJs = dummyHotkeysJs_;
+ hotkeyManager.onEditorChange("talk");
+ callback.mockClear();
});
it("設定を登録するとhotkeysが更新される", () => {
hotkeyManager.register(dummyAction);
hotkeyManager.load([createDummySetting("1")]);
- expect(dummyHotkeysJs.registeredHotkeys).toEqual([
- { key: "1", scope: "talk" },
- ]);
+ hotkeyManager.keyInput(createDummyInput("1", "Digit1") as KeyboardEvent);
+ expect(callback).toHaveBeenCalledTimes(1);
});
it("設定を更新するとhotkeysが更新される", () => {
hotkeyManager.register(dummyAction);
hotkeyManager.load([createDummySetting("1")]);
- hotkeyManager.replace(createDummySetting("a"));
+ hotkeyManager.replace(createDummySetting("A"));
+ hotkeyManager.keyInput(createDummyInput("a", "KeyA") as KeyboardEvent);
- expect(dummyHotkeysJs.registeredHotkeys).toEqual([
- { key: "a", scope: "talk" },
- ]);
+ expect(callback).toHaveBeenCalledTimes(1);
});
it("未割り当てにするとhotkeysから削除される", () => {
hotkeyManager.register(dummyAction);
hotkeyManager.load([createDummySetting("1")]);
hotkeyManager.replace(createDummySetting(""));
- expect(dummyHotkeysJs.registeredHotkeys).toEqual([]);
+ hotkeyManager.keyInput(createDummyInput("1", "Digit1") as KeyboardEvent);
+ expect(callback).toHaveBeenCalledTimes(0);
});
it("未割り当てから割り当てるとhotkeysが更新される", () => {
hotkeyManager.register(dummyAction);
hotkeyManager.load([createDummySetting("")]);
- expect(dummyHotkeysJs.registeredHotkeys).toEqual([]);
+ hotkeyManager.keyInput(createDummyInput("1", "Digit1") as KeyboardEvent);
+ expect(callback).toHaveBeenCalledTimes(0);
+
hotkeyManager.replace(createDummySetting("1"));
- expect(dummyHotkeysJs.registeredHotkeys).toEqual([
- { key: "1", scope: "talk" },
- ]);
+ hotkeyManager.keyInput(createDummyInput("1", "Digit1") as KeyboardEvent);
+ expect(callback).toHaveBeenCalledTimes(1);
});
it("割り当て -> 未割り当て -> 割り当てでhotkeysが更新される", () => {
hotkeyManager.register(dummyAction);
hotkeyManager.load([createDummySetting("1")]);
- expect(dummyHotkeysJs.registeredHotkeys).toEqual([
- { key: "1", scope: "talk" },
- ]);
+ hotkeyManager.keyInput(createDummyInput("1", "Digit1") as KeyboardEvent);
+ expect(callback).toHaveBeenCalledTimes(1);
hotkeyManager.replace(createDummySetting(""));
- expect(dummyHotkeysJs.registeredHotkeys).toEqual([]);
+ hotkeyManager.keyInput(createDummyInput("1", "Digit1") as KeyboardEvent);
+ expect(callback).toHaveBeenCalledTimes(1); // 呼び出し回数が増えない
- hotkeyManager.replace(createDummySetting("a"));
- expect(dummyHotkeysJs.registeredHotkeys).toEqual([
- { key: "a", scope: "talk" },
- ]);
+ hotkeyManager.replace(createDummySetting("A"));
+ hotkeyManager.keyInput(createDummyInput("a", "KeyA") as KeyboardEvent);
+ expect(callback).toHaveBeenCalledTimes(2);
});
});