Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change: ショートカットキー周りをHotkeyManagerにまとめる #1822

Merged
merged 52 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
9d55fb9
Add: HotkeyManagerを追加
sevenc-nanashi Feb 3, 2024
421903b
Add: とりあえずUndo/Redoは動いた
sevenc-nanashi Feb 3, 2024
22c572c
Change: Pluginに
sevenc-nanashi Feb 3, 2024
a83f4a3
Change: hotkey-jsに
sevenc-nanashi Feb 3, 2024
cd5469c
Change: hotkeyManagerに置き換え
sevenc-nanashi Feb 3, 2024
f5fe090
Change: デフォルトパラメータに
sevenc-nanashi Feb 3, 2024
b93a07c
Code: ドキュメントを追加
sevenc-nanashi Feb 3, 2024
60c7336
Add: sing側に同じ名前でショートカットを登録できるように
sevenc-nanashi Feb 4, 2024
35dce47
Revert: Spaceキー再生のコードを削除
sevenc-nanashi Feb 4, 2024
5e9d70e
Improve: 可読性を改善
sevenc-nanashi Feb 6, 2024
171d8d2
Delete: keepDefaultBehaviorを削除
sevenc-nanashi Feb 6, 2024
cf5c441
Change: scopeを使うように
sevenc-nanashi Feb 6, 2024
ccc6b62
Code: 「ショートカットキーの処理」を使うように
sevenc-nanashi Feb 6, 2024
933cc43
Merge: main -> refactor/hotkey
sevenc-nanashi Feb 6, 2024
e18c353
Fix: 同じキーバインドが登録されてると片方が動かなくなるのを修正
sevenc-nanashi Feb 8, 2024
693f738
Code: 可読性を向上
sevenc-nanashi Feb 10, 2024
735a6c7
Merge: main -> refactor/hotkey
sevenc-nanashi Feb 10, 2024
a226127
Code: 変数名周りを改修
sevenc-nanashi Feb 10, 2024
14c62cf
Change: HotkeyAction -> HotkeyActionName
sevenc-nanashi Feb 10, 2024
796405a
Change: ActionKey -> ActionId
sevenc-nanashi Feb 10, 2024
bd74cfc
Change: action ->name
sevenc-nanashi Feb 10, 2024
473a780
Change: window.electron.logInfoをを使うようにする
sevenc-nanashi Feb 10, 2024
84729c8
Code: 変数名を変更
sevenc-nanashi Feb 10, 2024
3881365
Change: undefined | HotkeySetting[]にする
sevenc-nanashi Feb 10, 2024
2a0b170
Add: テストを追加
sevenc-nanashi Feb 10, 2024
c3fac82
Refactor: refreshBindingsを分解
sevenc-nanashi Feb 10, 2024
299dc1c
Change: hotkeys.filterを一番上に持ってくる
sevenc-nanashi Feb 11, 2024
45d1992
Change: getSettingでthrowするようにする
sevenc-nanashi Feb 11, 2024
44f2bb7
Delete: eslintのignoreを使わないコードにする
sevenc-nanashi Feb 11, 2024
10f474f
Code: コメントを追加
sevenc-nanashi Feb 11, 2024
1571003
Change: as unknown asをなくす
sevenc-nanashi Feb 11, 2024
5e3c03f
Fix: 割り当て -> 未割り当て -> 割り当ての挙動を修正
sevenc-nanashi Feb 11, 2024
035eb23
Change: logをDIする
sevenc-nanashi Feb 11, 2024
c68fc07
Merge: main -> refactor/hotkey
sevenc-nanashi Feb 11, 2024
1ae9b38
Code: コメントを追加
sevenc-nanashi Feb 11, 2024
1452b82
Change: 条件を変更
sevenc-nanashi Feb 14, 2024
5c63293
下に移動
Hiroshiba Feb 14, 2024
7a5e6f8
Merge remote-tracking branch 'upstream/main' into pr/sevenc-nanashi/1…
Hiroshiba Feb 14, 2024
96ed5d7
to combination
Hiroshiba Feb 14, 2024
ed32545
Fix: 良い感じにクリーンアップするように
sevenc-nanashi Feb 15, 2024
5475247
Fix: テストを修正
sevenc-nanashi Feb 15, 2024
925fd13
Improve: 良い感じに
sevenc-nanashi Feb 16, 2024
017d69e
Improve: エラーメッセージを改善
sevenc-nanashi Feb 16, 2024
c90ae78
Change: IDを使わないように
sevenc-nanashi Feb 18, 2024
e81b28a
Refactor: isSameHotkeyTarget/isNotSameHotkeyTargetを追加
sevenc-nanashi Feb 18, 2024
81afdfc
Delete: unbindUnregisteredCombinationsを削除
sevenc-nanashi Feb 18, 2024
22e0236
Merge: main -> refactor/hotkey
sevenc-nanashi Feb 19, 2024
02ebfdd
unbindedCombinationsを下に
Hiroshiba Feb 20, 2024
7fefe74
doc集約
Hiroshiba Feb 20, 2024
92551fe
プライベート変数の説明追加
Hiroshiba Feb 20, 2024
116a796
Code: コメントをdoc commentに
sevenc-nanashi Feb 20, 2024
ea7a853
Change: beforeEachで登録するのをやめる
sevenc-nanashi Feb 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/components/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { watch, onMounted, ref, computed, toRaw } from "vue";
import { useGtm } from "@gtm-support/vue-gtm";
import { useRoute } from "vue-router";
import { EngineId } from "./type/preload";
import { EngineId } from "@/type/preload";
import ErrorBoundary from "@/components/ErrorBoundary.vue";
import { useStore } from "@/store";
import { useHotkeyManager } from "@/plugins/hotkeyPlugin";
Expand Down Expand Up @@ -47,6 +47,21 @@ watch(
{ immediate: true }
);

// エディタの切り替えを監視する
watch(
() => route.path,
async (unknownPath) => {
let path: "talk" | "song";
if (["/talk", "/song"].includes(unknownPath)) {
path = unknownPath.slice(1) as "talk" | "song";
} else {
path = "talk";
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
}

hotkeyManager.onEditorChange(path);
}
);

// ソフトウェアを初期化
const hotkeyManager = useHotkeyManager();
const isEnginesReady = ref(false);
Expand Down
14 changes: 14 additions & 0 deletions src/components/Sing/ToolBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,22 @@ import {
} from "@/sing/domain";
import CharacterMenuButton from "@/components/Sing/CharacterMenuButton.vue";
import { getStyleDescription } from "@/sing/viewHelper";
import { useHotkeyManager } from "@/plugins/hotkeyPlugin";

const store = useStore();
const hotkeyManager = useHotkeyManager();

hotkeyManager.register({
editor: "song",
action: "再生/停止",
callback: () => {
if (nowPlaying.value) {
stop();
} else {
play();
}
},
});

const userOrderedCharacterInfos = computed(() =>
store.getters.USER_ORDERED_CHARACTER_INFOS("singerLike")
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import router from "./router";
import { store, storeKey } from "./store";
import { ipcMessageReceiver } from "./plugins/ipcMessageReceiverPlugin";
import { hotkeyPlugin } from "./plugins/hotkeyPlugin";
import App from "@/components/App.vue";
import { markdownItPlugin } from "@/plugins/markdownItPlugin";

import "@quasar/extras/material-icons/material-icons.css";
Expand Down
140 changes: 77 additions & 63 deletions src/plugins/hotkeyPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* ショートカットキーを管理するプラグイン。
*
* HotkeyAction: 何をするか(再生するなど)の名称
* Combination: ショートカットキーを文字列で表したもの
* HotkeySetting: ユーザーが設定できるもの。ActionとCobinationのペア
* HotkeyEntry: プログラムが管理するもの。関数や発動条件など
*/
import { Plugin, inject } from "vue";
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
import hotkeys from "hotkeys-js";
import { HotkeyActionType, HotkeySettingType } from "@/type/preload";
Expand All @@ -11,8 +19,12 @@ export const useHotkeyManager = () => {
return hotkeyManager;
};

const log = (message: string, ...args: unknown[]) => {
console.log(`[HotkeyManager] ${message}`, ...args);
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
};

/**
* ショートカットキーの情報を格納する型
* ショートカットキーの処理を登録するための型
*/
type HotkeyEntry = {
/** どちらのエディタで有効か */
Expand All @@ -21,19 +33,22 @@ type HotkeyEntry = {
enableInTextbox?: boolean;
/** 名前。 */
action: HotkeyActionType;
/** ブラウザのデフォルトの挙動をキャンセルするか。デフォルトはfalse。 */
keepDefaultBehavior?: boolean;
/** ショートカットキーが押されたときの処理。 */
callback: (e: KeyboardEvent) => void;
};
type HotkeyEntryKey = `${"talk" | "song"}:${HotkeyActionType}`;

const entryToKey = (entry: HotkeyEntry): HotkeyEntryKey =>
`${entry.editor}:${entry.action}`;

/**
* ショートカットキーの管理を行うクラス。
*/
export class HotkeyManager {
actions: HotkeyEntry[] = [];
settings: HotkeySettingType[] = [];
registered: Partial<Record<HotkeyActionType, string>> = {};
private entries: HotkeyEntry[] = [];
private settings: HotkeySettingType[] = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(ちょっと細かい提案ですが)
これは「全部存在するか1つも存在しないか」だと思うので、それがわかるよう型を明示するのはどうでしょう?

Suggested change
private settings: HotkeySettingType[] = [];
private settings: HotkeySettingType[] | undefined = undefined;

/// 登録されているショートカットキーの組み合わせ。キーは「エディタ:アクション」で、値はショートカットキーの文字列。
private registeredCombination: Partial<Record<HotkeyEntryKey, string>> = {};

constructor() {
// デフォルトだとテキスト欄でのショートカットキーが効かないので、テキスト欄でも効くようにする
Expand All @@ -52,75 +67,66 @@ export class HotkeyManager {
if (this.settings.length === 0) {
return;
}
const changedActions = this.actions.filter(
const changedActions = this.entries.filter(
(a) =>
this.registered[a.action] !==
this.registeredCombination[entryToKey(a)] !==
this.settings.find((s) => s.action === a.action)?.combination
);
if (changedActions.length === 0) {
return;
}
const keysToRemove = new Set(
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
...changedActions
.map((key) =>
hotkeyToCombo(this.registeredCombination[entryToKey(key)] || "")
)
.filter((key) => key !== "")
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
);
for (const key of keysToRemove.values()) {
log("Unbind:", key);
hotkeys.unbind(key);
}
for (const action of changedActions) {
const registered = this.registered[action.action];
if (registered) {
hotkeys.unbind(hotkeyToCombo(registered));
}
const setting = this.settings.find((s) => s.action === action.action);
if (!setting) {
// unreachableのはず
throw new Error("assert: setting == undefined");
}

const actions = this.actions.filter((a) => a.action === action.action);
if (actions.length > 1) {
const songAction = actions.find((a) => a.editor === "song");
const talkAction = actions.find((a) => a.editor === "talk");
if (!songAction || !talkAction) {
throw new Error("assert: songAction && talkAction");
}
// talk/song両方で有効なショートカットキー
hotkeys(hotkeyToCombo(setting.combination), (e) => {
const path = location.hash.split("/")[1] as "talk" | "song";
const action = path === "talk" ? talkAction : songAction;
if (!action.enableInTextbox) {
const element = e.target as HTMLElement;
if (
element.tagName === "INPUT" ||
element.tagName === "SELECT" ||
element.tagName === "TEXTAREA" ||
(element instanceof HTMLElement &&
element.contentEditable === "true") ||
// メニュー項目ではショートカットキーを無効化
element.classList.contains("q-item")
) {
return;
}
}
if (!action.keepDefaultBehavior) e.preventDefault();
action.callback(e);
});
if (setting.combination === "") {
log("Skip(empty combination):", action.action, "in", action.editor);
} else {
hotkeys(hotkeyToCombo(setting.combination), (e) => {
if (!action.enableInTextbox) {
const element = e.target as HTMLElement;
if (
element.tagName === "INPUT" ||
element.tagName === "SELECT" ||
element.tagName === "TEXTAREA" ||
(element instanceof HTMLElement &&
element.contentEditable === "true") ||
// メニュー項目ではショートカットキーを無効化
element.classList.contains("q-item")
) {
return;
log(
"Bind:",
hotkeyToCombo(setting.combination),
"to",
action.action,
"in",
action.editor
);
hotkeys(
hotkeyToCombo(setting.combination),
{ scope: action.editor },
(e) => {
if (!action.enableInTextbox) {
const element = e.target as HTMLElement;
if (
element.tagName === "INPUT" ||
element.tagName === "SELECT" ||
element.tagName === "TEXTAREA" ||
(element instanceof HTMLElement &&
element.contentEditable === "true") ||
// メニュー項目ではショートカットキーを無効化
element.classList.contains("q-item")
) {
return;
}
}
}
// TODO: もっと良い感じの取得方法があれば変更する
const path = location.hash.split("/")[1] as "talk" | "song";
if (path === action.editor) {
if (!action.keepDefaultBehavior) e.preventDefault();
e.preventDefault();
action.callback(e);
}
});
);
}
this.registered[action.action] = setting.combination;
this.registeredCombination[entryToKey(action)] = setting.combination;
}
}

Expand All @@ -137,12 +143,20 @@ export class HotkeyManager {
}

/**
* ショートカットキーの登録を行う
* ショートカットキーの処理を登録する
*/
register(data: HotkeyEntry): void {
this.actions.push(data);
this.entries.push(data);
this.refreshBinding();
}

/**
* エディタが変更されたときに呼び出される。
*/
onEditorChange(editor: "talk" | "song"): void {
hotkeys.setScope(editor);
log("Editor changed to", editor);
}
}

const hotkeyToCombo = (hotkeyCombo: string) => {
Expand Down