diff --git a/locales/en.yml b/locales/en.yml
index 810d05e3..438af71e 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -94,6 +94,7 @@ config_page:
transcription: Transcription
vr: VR
others: Others
+ hotkeys: Hotkeys
advanced_settings: Advanced Settings
supporters: Supporters
about_vrct: About VRCT
diff --git a/src-ui/app/_app_controllers/GlobalHotKeyController.jsx b/src-ui/app/_app_controllers/GlobalHotKeyController.jsx
index c28af2e1..0387f3be 100644
--- a/src-ui/app/_app_controllers/GlobalHotKeyController.jsx
+++ b/src-ui/app/_app_controllers/GlobalHotKeyController.jsx
@@ -1,35 +1,76 @@
- import { appWindow } from "@tauri-apps/api/window";
- import { register, unregisterAll, isRegistered } from "@tauri-apps/api/globalShortcut";
- import { useEffect, useRef } from "react";
- import { store } from "@store";
+import { appWindow } from "@tauri-apps/api/window";
+import { register, unregisterAll, isRegistered } from "@tauri-apps/api/globalShortcut";
+import { useEffect } from "react";
+import { store } from "@store";
+import { useHotkeys } from "@logics_configs";
- export const GlobalHotKeyController = () => {
- const is_initialized = useRef(false);
- useEffect(() => {
- if (is_initialized.current) return;
-
- const registerShortcuts = async () => {
- const shortcut = "Alt+Y";
- const is_already_registered = await isRegistered(shortcut);
- if (is_already_registered) return;
-
- await register(shortcut, async () => {
- console.log("Shortcut triggered, setFocus");
- appWindow.unminimize();
- await appWindow.setFocus();
- store.text_area_ref.current?.focus();
- });
- };
-
- registerShortcuts();
- is_initialized.current = true;
-
- return () => {
- unregisterAll().catch((error) => {
- console.error("Failed to unregister shortcuts:", error);
- });
- };
- }, []);
-
- return null;
+// 修飾キーのパースを行う関数
+const parseHotkey = (hotkeyString) => {
+ const keyMap = {
+ Ctrl: "Control",
+ Alt: "Alt",
+ Shift: "Shift",
+ Meta: "Super",
};
+
+ // 入力文字列を分解して対応するキーを再結合
+ return hotkeyString
+ .map((key) => keyMap[key] || key) // 修飾キーをマップし、その他はそのまま
+ .join("+");
+};
+
+export const GlobalHotKeyController = () => {
+ const { currentHotkeys } = useHotkeys();
+
+ useEffect(() => {
+ const registerShortcuts = async () => {
+ const shortcut_raw = currentHotkeys.data.toggle_active_vrct;
+ console.log(shortcut_raw);
+
+
+ if (!shortcut_raw) {
+ console.warn("No hotkey defined.");
+ return;
+ }
+
+ // 入力文字列をTauriで使える形式に変換
+ const shortcut = parseHotkey(shortcut_raw);
+ // const shortcut = "F9";
+
+ try {
+ // 既存のショートカットをすべて解除
+ await unregisterAll();
+
+ // 新しいショートカットを登録
+ const isAlreadyRegistered = await isRegistered(shortcut);
+ if (!isAlreadyRegistered) {
+ await register(shortcut, async () => {
+ console.log(`Shortcut "${shortcut}" triggered, setting focus.`);
+ // const minimized = await appWindow.isMinimized();
+ // if (minimized === true) {
+ // appWindow.unminimize();
+ // await appWindow.setFocus();
+ // store.text_area_ref.current?.focus();
+ // } else {
+ // appWindow.minimize();
+ // }
+ });
+ console.log(`Registered global shortcut: ${shortcut}`);
+ }
+ } catch (error) {
+ console.error("Failed to register global shortcut:", error);
+ }
+ };
+
+ registerShortcuts();
+
+ // クリーンアップ関数でショートカットを解除
+ return () => {
+ unregisterAll().catch((error) => {
+ console.error("Failed to unregister shortcuts:", error);
+ });
+ };
+ }, [currentHotkeys.data.toggle_active_vrct]); // 監視対象を明確に指定
+
+ return null;
+};
diff --git a/src-ui/app/config_page/setting_section/setting_box/SettingBox.jsx b/src-ui/app/config_page/setting_section/setting_box/SettingBox.jsx
index eba09ff7..e61067a7 100644
--- a/src-ui/app/config_page/setting_section/setting_box/SettingBox.jsx
+++ b/src-ui/app/config_page/setting_section/setting_box/SettingBox.jsx
@@ -8,6 +8,7 @@ import {
Others,
AdvancedSettings,
Vr,
+ Hotkeys,
Supporters,
AboutVrct,
} from "@setting_box";
@@ -27,6 +28,8 @@ export const SettingBox = () => {
return ;
case "vr":
return ;
+ case "hotkeys":
+ return ;
case "advanced_settings":
return ;
case "supporters":
diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_entry/_Entry.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_entry/_Entry.jsx
index e8bba371..c48d99b7 100644
--- a/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_entry/_Entry.jsx
+++ b/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_entry/_Entry.jsx
@@ -8,16 +8,22 @@ const _Entry = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
+ },
+ blur: () => {
+ inputRef.current.blur();
}
}));
const input_class_names = clsx(styles.entry_input_area, {
- [styles.is_disabled]: props.is_disabled
+ [styles.is_disabled]: props.is_disabled,
+ });
+ const input_wrapper_class_names = clsx(styles.entry_wrapper, {
+ [styles.is_activated]: props.is_activated,
});
return (
diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_entry/_Entry.module.scss b/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_entry/_Entry.module.scss
index 5dd43ff1..7fc4a388 100644
--- a/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_entry/_Entry.module.scss
+++ b/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_entry/_Entry.module.scss
@@ -9,6 +9,9 @@
background-color: var(--dark_875_color);
border: 0.1rem solid var(--dark_750_color);
border-radius: 0.4rem;
+ &.is_activated {
+ border: 0.1rem solid var(--primary_400_color);
+ }
}
.entry_input_area {
diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/hotkeys_entry/HotkeysEntry.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/hotkeys_entry/HotkeysEntry.jsx
new file mode 100644
index 00000000..48ebeb11
--- /dev/null
+++ b/src-ui/app/config_page/setting_section/setting_box/_components/hotkeys_entry/HotkeysEntry.jsx
@@ -0,0 +1,134 @@
+import styles from "./HotkeysEntry.module.scss";
+import { _Entry } from "../_atoms/_entry/_Entry";
+import { useState, useRef } from "react";
+
+export const HotkeysEntry = (props) => {
+ const [is_accepting_input, setIsAcceptingInput] = useState(false); // キー入力受付中かどうか
+ const lastKeyRef = useRef(null); // 直前のキーを保持
+ const [displayValue, setDisplayValue] = useState(props.value[props.hotkey_id]); // 表示用の値
+ const isModifierOnlyRef = useRef(false); // 修飾キー単体かどうかのフラグ
+ const entryRef = useRef(null);
+
+ const pressedKeys = useRef(new Set()); // 押されているキーを追跡
+ const keysRef = useRef([]); // 最新のkeysを保存
+
+ const setHotkeys = (keys) => {
+ entryRef.current.blur();
+ props.setHotkeys({ [props.hotkey_id]: keys });
+ };
+
+ const handleKeyInput = (event) => {
+ const keys = [];
+ const nonModifierKeys = []; // 修飾キー以外を追跡する配列
+
+ // 修飾キーを判定して追加(重複防止)
+ if (event.ctrlKey && !keys.includes("Ctrl")) keys.push("Ctrl");
+ if (event.shiftKey && !keys.includes("Shift")) keys.push("Shift");
+ if (event.altKey && !keys.includes("Alt")) keys.push("Alt");
+ if (event.metaKey && !keys.includes("Super")) keys.push("Super");
+
+ let register_key = event.key === "Meta" ? "Super" : event.key;
+ // アルファベットの場合は大文字に変換
+ if (/^[a-zA-Z]$/.test(register_key)) {
+ register_key = register_key.toUpperCase();
+ }
+
+ // 修飾キー以外を追加
+ if (!["Control", "Shift", "Alt", "Meta"].includes(event.key)) {
+ keys.push(register_key);
+ nonModifierKeys.push(register_key); // 修飾キー以外のキーを追跡
+ }
+
+ // キーが既に追跡されていない場合のみ追加
+ if (!pressedKeys.current.has(register_key)) {
+ pressedKeys.current.add(register_key);
+ }
+
+ // 最新のキー構成を保存
+ keysRef.current = [...keys];
+
+ // 表示用の値を更新
+ setDisplayValue(keys.join(" + "));
+
+ // 修飾キー単体かどうかを更新
+ isModifierOnlyRef.current = nonModifierKeys.length === 0;
+
+ // 修飾キーのみの場合は登録処理をスキップ
+ if (isModifierOnlyRef.current) return;
+ };
+
+ const handleKeyDown = (event) => {
+ event.preventDefault(); // デフォルト動作を防ぐ
+
+ // 直前のキーと同じならスキップ
+ const currentKey = event.key;
+ if (lastKeyRef.current === currentKey) {
+ return;
+ }
+
+ lastKeyRef.current = currentKey; // 今回のキーを記録
+ handleKeyInput(event);
+ };
+
+ const handleKeyUp = (event) => {
+ lastKeyRef.current = null; // キーが離されたらリセット
+
+ // 修飾キーのみの場合でも表示は維持
+ if (isModifierOnlyRef.current) {
+ setDisplayValue(""); // 非修飾キーが含まれていた場合リセット
+ }
+
+ let register_key = event.key === "Meta" ? "Super" : event.key;
+ // アルファベットの場合は大文字に変換
+ if (/^[a-zA-Z]$/.test(register_key)) {
+ register_key = register_key.toUpperCase();
+ }
+ // 押されているキーから削除
+ pressedKeys.current.delete(register_key);
+
+ // 全てのキーが放された場合
+ if (pressedKeys.current.size === 0) {
+
+ // 修飾キーのみの場合はスキップ
+ const hasNonModifierKeys = keysRef.current.some(
+ (key) => !["Ctrl", "Shift", "Alt", "Super"].includes(key)
+ );
+ if (!hasNonModifierKeys) {
+ return;
+ }
+
+ // 保存されたキー構成を使用して登録
+ setHotkeys(keysRef.current);
+ }
+ };
+
+
+ const handleBlur = () => {
+ setIsAcceptingInput(false);
+ pressedKeys.current.clear();
+ };
+
+ return (
+
+ <_Entry
+ ref={entryRef}
+ type="text"
+ placeholder="Press hotkeys keys"
+ onFocus={() => [setIsAcceptingInput(true)]}
+ onBlur={handleBlur}
+ onKeyDown={handleKeyDown}
+ onKeyUp={handleKeyUp}
+ value={displayValue} // 表示用の値を設定
+ width="20rem"
+ is_activated={is_accepting_input}
+ readOnly
+ />
+
+
+ );
+};
diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/hotkeys_entry/HotkeysEntry.module.scss b/src-ui/app/config_page/setting_section/setting_box/_components/hotkeys_entry/HotkeysEntry.module.scss
new file mode 100644
index 00000000..199afbb7
--- /dev/null
+++ b/src-ui/app/config_page/setting_section/setting_box/_components/hotkeys_entry/HotkeysEntry.module.scss
@@ -0,0 +1,13 @@
+.container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 1rem;
+}
+
+.delete_button {
+ padding: 0.8rem;
+ font-size: 1.4rem;
+ background-color: var(--dark_800_color);
+ flex-shrink: 0;
+}
\ No newline at end of file
diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/index.js b/src-ui/app/config_page/setting_section/setting_box/_components/index.js
index 20cc6fe1..3a295d3c 100644
--- a/src-ui/app/config_page/setting_section/setting_box/_components/index.js
+++ b/src-ui/app/config_page/setting_section/setting_box/_components/index.js
@@ -3,6 +3,7 @@ export { ComputeDevice } from "./compute_device/ComputeDevice";
export { DeeplAuthKey, OpenWebpage_DeeplAuthKey } from "./deepl_auth_key/DeeplAuthKey";
export { DropdownMenu } from "./dropdown_menu/DropdownMenu";
export { Entry } from "./entry/Entry";
+export { HotkeysEntry } from "./hotkeys_entry/HotkeysEntry";
export { LabelComponent } from "./label_component/LabelComponent";
export { RadioButton } from "./radio_button/RadioButton";
export { SectionLabelComponent } from "./section_label_component/SectionLabelComponent";
diff --git a/src-ui/app/config_page/setting_section/setting_box/_templates/Templates.jsx b/src-ui/app/config_page/setting_section/setting_box/_templates/Templates.jsx
index 0afb017b..d47e5842 100644
--- a/src-ui/app/config_page/setting_section/setting_box/_templates/Templates.jsx
+++ b/src-ui/app/config_page/setting_section/setting_box/_templates/Templates.jsx
@@ -8,6 +8,7 @@ import {
Slider,
SwitchBox,
Entry,
+ HotkeysEntry,
RadioButton,
OpenWebpage_DeeplAuthKey,
DeeplAuthKey,
@@ -75,6 +76,10 @@ export const EntryContainer = (props) => (
);
+export const HotkeysEntryContainer = (props) => (
+
+);
+
export const RadioButtonContainer = (props) => (
);
diff --git a/src-ui/app/config_page/setting_section/setting_box/hotkeys/Hotkeys.jsx b/src-ui/app/config_page/setting_section/setting_box/hotkeys/Hotkeys.jsx
new file mode 100644
index 00000000..407db1a7
--- /dev/null
+++ b/src-ui/app/config_page/setting_section/setting_box/hotkeys/Hotkeys.jsx
@@ -0,0 +1,26 @@
+import { useHotkeys } from "@logics_configs";
+import styles from "./Hotkeys.module.scss";
+import { HotkeysEntryContainer } from "../_templates/Templates";
+
+export const Hotkeys = () => {
+ return (
+
+
+
+ );
+};
+
+const HotkeysBoxContainer = () => {
+ const { currentHotkeys, setHotkeys } = useHotkeys();
+ return (
+
+
+
+ );
+};
diff --git a/src-ui/app/config_page/setting_section/setting_box/hotkeys/Hotkeys.module.scss b/src-ui/app/config_page/setting_section/setting_box/hotkeys/Hotkeys.module.scss
new file mode 100644
index 00000000..a49fed11
--- /dev/null
+++ b/src-ui/app/config_page/setting_section/setting_box/hotkeys/Hotkeys.module.scss
@@ -0,0 +1,5 @@
+.container {
+ display: flex;
+ gap: 6.4rem;
+ flex-direction: column;
+}
\ No newline at end of file
diff --git a/src-ui/app/config_page/setting_section/setting_box/index.js b/src-ui/app/config_page/setting_section/setting_box/index.js
index 489f63ca..dc5798c3 100644
--- a/src-ui/app/config_page/setting_section/setting_box/index.js
+++ b/src-ui/app/config_page/setting_section/setting_box/index.js
@@ -5,5 +5,6 @@ export { Transcription } from "./transcription/Transcription";
export { Others, VrcMicMuteSyncContainer } from "./others/Others";
export { AdvancedSettings } from "./advanced_settings/AdvancedSettings";
export { Vr } from "./vr/Vr";
+export { Hotkeys } from "./hotkeys/Hotkeys";
export { AboutVrct } from "./about_vrct/AboutVrct";
export { Supporters } from "./supporters/Supporters";
\ No newline at end of file
diff --git a/src-ui/app/config_page/sidebar_section/SidebarSection.jsx b/src-ui/app/config_page/sidebar_section/SidebarSection.jsx
index 2b109910..292bb7cb 100644
--- a/src-ui/app/config_page/sidebar_section/SidebarSection.jsx
+++ b/src-ui/app/config_page/sidebar_section/SidebarSection.jsx
@@ -10,6 +10,7 @@ export const SidebarSection = () => {
+
diff --git a/src-ui/logics/configs/hotkeys/useHotkeys.js b/src-ui/logics/configs/hotkeys/useHotkeys.js
new file mode 100644
index 00000000..5349b33b
--- /dev/null
+++ b/src-ui/logics/configs/hotkeys/useHotkeys.js
@@ -0,0 +1,25 @@
+import { useStore_Hotkeys } from "@store";
+// import { useStdoutToPython } from "@logics/useStdoutToPython";
+
+export const useHotkeys = () => {
+ // const { asyncStdoutToPython } = useStdoutToPython();
+ const { currentHotkeys, updateHotkeys, pendingHotkeys } = useStore_Hotkeys();
+
+ const getHotkeys = () => {
+ // pendingHotkeys();
+ // asyncStdoutToPython("/get/data/osc_ip_address");
+ };
+
+ const setHotkeys = (hotkeys) => {
+ updateHotkeys(hotkeys);
+ // pendingHotkeys();
+ // asyncStdoutToPython("/set/data/osc_ip_address", osc_ip_address);
+ };
+
+ return {
+ currentHotkeys,
+ // getHotkeys,
+ updateHotkeys,
+ setHotkeys,
+ };
+};
\ No newline at end of file
diff --git a/src-ui/logics/configs/index.js b/src-ui/logics/configs/index.js
index b8426255..8bcf2a7c 100644
--- a/src-ui/logics/configs/index.js
+++ b/src-ui/logics/configs/index.js
@@ -51,6 +51,8 @@ export { useOverlayShowOnlyTranslatedMessages } from "./vr/useOverlayShowOnlyTra
export { useOverlayLargeLogSettings } from "./vr/useOverlayLargeLogSettings";
export { useSendTextToOverlay } from "./vr/useSendTextToOverlay";
+export { useHotkeys } from "./hotkeys/useHotkeys";
+
export { useOscIpAddress } from "./advanced_settings/useOscIpAddress";
export { useOscPort } from "./advanced_settings/useOscPort";
diff --git a/src-ui/store.js b/src-ui/store.js
index 7a94f8f4..53d30ed5 100644
--- a/src-ui/store.js
+++ b/src-ui/store.js
@@ -264,6 +264,11 @@ export const { atomInstance: Atom_EnableVrcMicMuteSync, useHook: useStore_Enable
export const { atomInstance: Atom_EnableSendMessageToVrc, useHook: useStore_EnableSendMessageToVrc } = createAtomWithHook(true, "EnableSendMessageToVrc");
export const { atomInstance: Atom_EnableSendReceivedMessageToVrc, useHook: useStore_EnableSendReceivedMessageToVrc } = createAtomWithHook(false, "EnableSendReceivedMessageToVrc");
+// Hotkeys
+export const { atomInstance: Atom_Hotkeys, useHook: useStore_Hotkeys } = createAtomWithHook({
+ toggle_active_vrct: null,
+}, "Hotkeys");
+
// Advanced Settings
export const { atomInstance: Atom_OscIpAddress, useHook: useStore_OscIpAddress } = createAtomWithHook("127.0.0.1", "OscIpAddress");
export const { atomInstance: Atom_OscPort, useHook: useStore_OscPort } = createAtomWithHook("9000", "OscPort");