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 (
{ className={input_class_names} value={props.ui_variable === null ? "" : props.ui_variable} onChange={(e) => props.onChange(e)} + {...props} />
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");