From 4623c65949322550445aa76198d494990f4d11e3 Mon Sep 17 00:00:00 2001 From: shinya Date: Tue, 10 Feb 2026 14:42:57 +0900 Subject: [PATCH 1/3] Window Focus Management Fixes and Full-Screen Mode Implementation --- src-tauri/Cargo.lock | 2 + src-tauri/Cargo.toml | 4 + src-tauri/capabilities/default.json | 7 ++ src-tauri/gen/schemas/capabilities.json | 2 +- src-tauri/src/main.rs | 43 ++++++- src-tauri/tauri.conf.json | 6 +- src/App.tsx | 151 ++++++++++++++++++++++-- src/components/Help.tsx | 1 + src/index.css | 70 +++++++++++ 9 files changed, 268 insertions(+), 18 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ce866df..5d0e832 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1680,6 +1680,8 @@ name = "lightning-timer" version = "0.2.0" dependencies = [ "dirs 5.0.1", + "objc2 0.6.2", + "objc2-app-kit", "serde", "serde_json", "tauri", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 33c0cdf..553e4fa 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,6 +23,10 @@ tauri = { version = "2.0", features = ["tray-icon", "devtools"] } tauri-plugin-store = "2.0" dirs = "5.0" +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = "0.6" +objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSWindow", "NSRunningApplication"] } + [features] # this feature is used for production builds or when `devPath` points to the filesystem # DO NOT REMOVE!! diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 3b8232d..de47952 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -12,6 +12,13 @@ "core:window:allow-set-resizable", "core:window:allow-set-decorations", "core:window:allow-set-always-on-top", + "core:window:allow-set-fullscreen", + "core:window:allow-set-max-size", + "core:window:allow-set-min-size", + "core:window:allow-set-focus", + "core:window:allow-set-position", + "core:window:allow-current-monitor", + "core:window:allow-outer-position", "store:default" ] } diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index 7d1b8cd..c4a05c4 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"main-capability":{"identifier":"main-capability","description":"Main window capabilities","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-set-always-on-top","core:window:allow-create","core:window:allow-close","core:window:allow-center","core:window:allow-set-size","core:window:allow-set-resizable","core:window:allow-set-decorations","core:window:allow-set-always-on-top","store:default"]},"timeup-capability":{"identifier":"timeup-capability","description":"Time Up window capabilities","local":true,"windows":["timeup"],"permissions":["core:default","core:window:allow-close"]}} \ No newline at end of file +{"main-capability":{"identifier":"main-capability","description":"Main window capabilities","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-set-always-on-top","core:window:allow-create","core:window:allow-close","core:window:allow-center","core:window:allow-set-size","core:window:allow-set-resizable","core:window:allow-set-decorations","core:window:allow-set-always-on-top","core:window:allow-set-fullscreen","core:window:allow-set-max-size","core:window:allow-set-min-size","core:window:allow-set-focus","core:window:allow-set-position","core:window:allow-current-monitor","core:window:allow-outer-position","store:default"]},"timeup-capability":{"identifier":"timeup-capability","description":"Time Up window capabilities","local":true,"windows":["timeup"],"permissions":["core:default","core:window:allow-close"]}} \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index aac4ec8..95cc259 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -8,6 +8,11 @@ use std::net::{SocketAddr, TcpListener}; use tauri::{AppHandle, Manager, WindowEvent}; use tauri_plugin_store::Builder as StoreBuilder; +#[cfg(target_os = "macos")] +use objc2_app_kit::NSApplication; +#[cfg(target_os = "macos")] +use objc2::MainThreadMarker; + #[derive(Serialize, Deserialize, Default, Debug)] pub struct WindowState { x: Option, @@ -83,6 +88,37 @@ fn restore_window_state(window: &tauri::WebviewWindow) -> Result<(), Box Result<(), String> { if let Some(window) = app.get_webview_window("main") { @@ -187,11 +223,8 @@ async fn set_window_resizable(app: AppHandle, resizable: bool) -> Result<(), Str #[tauri::command] async fn focus_window(app: AppHandle) -> Result<(), String> { if let Some(window) = app.get_webview_window("main") { - if let Err(e) = window.set_focus() { - println!("DEBUG: Failed to focus window: {}", e); - return Err(format!("Failed to focus window: {}", e)); - } - println!("DEBUG: Window focused"); + force_focus_window(&window); + println!("DEBUG: Window force-focused"); } Ok(()) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 85f929e..4b4082c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -15,10 +15,8 @@ "title": "Lightning Timer", "width": 800, "height": 200, - "minWidth": 400, - "minHeight": 200, - "maxWidth": 800, - "maxHeight": 200, + "minWidth": 200, + "minHeight": 100, "resizable": true, "alwaysOnTop": false, "decorations": false, diff --git a/src/App.tsx b/src/App.tsx index a2947c8..460d295 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback, useRef } from "react"; import { invoke, isTauri } from "@tauri-apps/api/core"; -import { getCurrentWindow } from "@tauri-apps/api/window"; +import { getCurrentWindow, currentMonitor, LogicalSize, PhysicalPosition } from "@tauri-apps/api/window"; import { Store } from "@tauri-apps/plugin-store"; import TimerDisplay from "./components/TimerDisplay"; import TimerControls from "./components/TimerControls"; @@ -42,6 +42,12 @@ const App: React.FC = () => { // TimeUP表示の状態管理 const [showTimeUp, setShowTimeUp] = useState(false); + // フルスクリーン状態管理 + const [isFullscreen, setIsFullscreen] = useState(false); + + // フルスクリーン前のウィンドウ位置を保存 + const savedWindowGeometryRef = useRef<{ x: number; y: number } | null>(null); + // アラーム再生フラグ(重複再生を防ぐ) const alarmPlayedRef = useRef(false); @@ -509,15 +515,108 @@ const App: React.FC = () => { height = 100; } + const currentWindow = getCurrentWindow(); + + // フルスクリーン中はモード切り替え時に解除 + if (isFullscreen) { + setIsFullscreen(false); + // 保存しておいた位置に戻す + if (savedWindowGeometryRef.current) { + const { x, y } = savedWindowGeometryRef.current; + await currentWindow.setPosition(new PhysicalPosition(x, y)); + savedWindowGeometryRef.current = null; + } + } + + // サイズ制約を一旦解除してからリサイズ + await currentWindow.setMinSize(null); + await currentWindow.setMaxSize(null); await invoke("set_window_size", { width, height }); - // ウィンドウのリサイズを無効化 - await invoke("set_window_resizable", { resizable: false }); + // min/maxサイズ制約でリサイズを防止 + const size = new LogicalSize(width, height); + await currentWindow.setMinSize(size); + await currentWindow.setMaxSize(size); } catch (error) { console.error("Failed to set window size:", error); } } - }, [settings]); + }, [settings, isFullscreen]); + + // フルスクリーン切り替え(手動フルスクリーン:ネイティブのsetFullscreenはmacOSデュアルディスプレイで問題があるため使わない) + const handleFullscreenToggle = useCallback(async () => { + if (!isTauri()) return; + + try { + const currentWindow = getCurrentWindow(); + const newFullscreen = !isFullscreen; + + if (newFullscreen) { + // フルスクリーン前のウィンドウ位置を保存 + const position = await currentWindow.outerPosition(); + savedWindowGeometryRef.current = { x: position.x, y: position.y }; + + // 現在のモニター情報を取得 + const monitor = await currentMonitor(); + if (!monitor) { + console.error("Failed to get current monitor"); + return; + } + + // サイズ制約を解除してモニターサイズに拡大 + await currentWindow.setMinSize(null); + await currentWindow.setMaxSize(null); + + // モニターの論理サイズを計算 + const logicalWidth = Math.round(monitor.size.width / monitor.scaleFactor); + const logicalHeight = Math.round(monitor.size.height / monitor.scaleFactor); + + // モニターの左上に移動してから全画面サイズに変更 + await currentWindow.setPosition(monitor.position); + await invoke("set_window_size", { width: logicalWidth, height: logicalHeight }); + } else { + // 元のモードのウィンドウサイズに戻す + let width: number; + let height: number; + if (settings.displayMode === "compact") { + width = 400; + height = 200; + } else if (settings.displayMode === "minimal") { + width = 200; + height = 100; + } else { + width = 800; + height = 200; + } + + // サイズ制約を一旦解除してからリサイズ + await currentWindow.setMinSize(null); + await currentWindow.setMaxSize(null); + + // 保存しておいた位置に先に戻す(リサイズ前にモニター内に戻す) + if (savedWindowGeometryRef.current) { + const { x, y } = savedWindowGeometryRef.current; + await currentWindow.setPosition(new PhysicalPosition(x, y)); + savedWindowGeometryRef.current = null; + } + + await invoke("set_window_size", { width, height }); + + // min/maxサイズ制約でリサイズを防止 + const size = new LogicalSize(width, height); + await currentWindow.setMinSize(size); + await currentWindow.setMaxSize(size); + } + + setIsFullscreen(newFullscreen); + + // WKWebViewのフォーカスを復帰 + await new Promise(resolve => setTimeout(resolve, 50)); + await invoke("focus_window"); + } catch (error) { + console.error("Failed to toggle fullscreen:", error); + } + }, [isFullscreen, settings.displayMode]); // キーボードイベントハンドラー const handleKeyDown = useCallback( @@ -562,6 +661,13 @@ const App: React.FC = () => { } } + // Cmd+Enter (Mac) / Ctrl+Enter (Windows/Linux) でフルスクリーン切り替え + else if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + handleFullscreenToggle(); + return; + } + // S、Space、EnterキーでSTART/PAUSE操作 else if ( key === "s" || @@ -616,17 +722,35 @@ const App: React.FC = () => { height = 100; } + const currentWindow = getCurrentWindow(); + + // フルスクリーン中はモード切り替え時に解除 + if (isFullscreen) { + setIsFullscreen(false); + // 保存しておいた位置に戻す + if (savedWindowGeometryRef.current) { + const { x, y } = savedWindowGeometryRef.current; + await currentWindow.setPosition(new PhysicalPosition(x, y)); + savedWindowGeometryRef.current = null; + } + } + + // サイズ制約を一旦解除してからリサイズ + await currentWindow.setMinSize(null); + await currentWindow.setMaxSize(null); await invoke("set_window_size", { width, height }); - // ウィンドウのリサイズを無効化 - await invoke("set_window_resizable", { resizable: false }); + // min/maxサイズ制約でリサイズを防止 + const size = new LogicalSize(width, height); + await currentWindow.setMinSize(size); + await currentWindow.setMaxSize(size); } catch (error) { console.error("Failed to set window size:", error); } } } }, - [timerState, startTimer, pauseTimer, resetTimer, updateTimerBoth, settings, stopAlarm] + [timerState, startTimer, pauseTimer, resetTimer, updateTimerBoth, settings, stopAlarm, handleFullscreenToggle, isFullscreen] ); // キーボードイベントリスナーの設定 @@ -679,7 +803,7 @@ const App: React.FC = () => { return (
@@ -707,6 +831,17 @@ const App: React.FC = () => { "⊞"} + {/* フルスクリーン切り替えボタン(右上) - 簡易モードのみ表示 */} + {settings.displayMode === "compact" && ( + + )} + {/* 情報アイコンボタン(右下) - 通常モードと簡易モードのみ表示 */} {settings.displayMode !== "minimal" && (