diff --git a/README.md b/README.md index 804e6e9..c6b3a9f 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,7 @@ Claude Code ──hook(stdin JSON)──▶ hook/claude-pet-hook.js ──PO - **點穿**:視窗預設 `setIgnoreMouseEvents(true, { forward: true })`,renderer 讀 canvas 該點的 alpha(或是否壓在氣泡上)來即時切換,所以透明處永遠點得到底下的視窗。判定的座標有兩個來源:renderer 自己的 mousemove,以及**主程序每 50 ms 的游標輪詢**。後者是必要的——視窗處於點穿狀態時 Electron 的 `forward` 不保證會把 mousemove 轉發進來(游標直接跳到寵物上常常收不到),只靠 mousemove 會在「游標還停在寵物身上時進入點穿」之後再也收不到事件,變成永遠卡在點穿、按不下去也拖不動。另外每一格畫完也會用最後已知座標重驗一次,因為換格會改變游標底下的像素。 - **拖曳**:由主程序用 `screen.getCursorScreenPoint()` 每 16 ms 更新視窗位置,游標離開視窗也不會掉;方向由水平位移決定。 +- **置頂要定期重新宣告**:Windows 有時候會把視窗踢出 topmost band,卻留著 `WS_EX_TOPMOST` 樣式位元——從外部檢查看到 `TOPMOST=true`,但 z-order 排在一般視窗(例如 VS Code)之下,寵物就被蓋住,而 Electron 這邊仍以為自己是置頂的,不會自己修好。所以每 2 秒重新宣告一次,並在螢幕配置變動時立刻補一次。`setAlwaysOnTop` 在狀態沒變時可能不會真的呼叫 `SetWindowPos`,因此再補一個 `moveTop()` 強制插回 topmost band 最上面;視窗是 `WS_EX_NOACTIVATE` + `focusable: false`,不會搶焦點。 - **行為邏輯以 Codex 為準**,細節見[與 Codex 的對照](#與-codex-的對照)。 - **顯示尺寸與 sprite 來源尺寸分開**:`CELL_W/CELL_H`(192 × 208)只用來從 spritesheet 取格子,畫到畫面上的是 `PET_W/PET_H`(126 × 137,Codex 的 mascot 尺寸)。視窗寬度另有 `MIN_WIN_W = 240` 的下限,免得寵物變小後氣泡被截掉。 - **Spritesheet 以 data URL 傳給 renderer**,避免 `file://` 跨來源污染 canvas 導致 `getImageData` 失敗。 diff --git a/main.js b/main.js index c727253..094ffa6 100644 --- a/main.js +++ b/main.js @@ -37,6 +37,7 @@ const PAD_BOTTOM = 8; const MIN_WIN_W = 240; // 視窗最小寬度,保留氣泡的可讀寬度(Codex 的氣泡也遠寬於寵物本身) // Codex 的 mascot 寬度可調範圍是 80–224 px(ike() 的 clamp);以 126 為 100%,這幾檔都落在範圍內 const SCALES = [0.65, 0.8, 1, 1.25, 1.5, 1.75]; +const TOPMOST_REASSERT_MS = 2000; // 多久重新宣告一次置頂(見 reassertTopmost) const DEFAULT_CONFIG = { activePetId: "xiao-nian", @@ -55,6 +56,7 @@ let tray = null; let pets = []; let dragTimer = null; let cursorTimer = null; +let topmostTimer = null; let lastCursor = { x: NaN, y: NaN }; let currentState = { anim: "idle", base: "idle", sessions: 0 }; @@ -279,7 +281,10 @@ function createWindow() { if (config.alwaysOnTop) win.setAlwaysOnTop(true, "screen-saver"); win.setIgnoreMouseEvents(true, { forward: true }); win.loadFile(path.join(APP_DIR, "renderer", "index.html")); - win.once("ready-to-show", () => win.showInactive()); + win.once("ready-to-show", () => { + win.showInactive(); + reassertTopmost(); + }); win.on("closed", () => { win = null; }); } @@ -326,6 +331,27 @@ function endDrag(save) { // ---------- cursor look ---------- +// Windows 有時候會把視窗踢出 topmost band,卻留著 WS_EX_TOPMOST 樣式位元: +// 從外部檢查會看到 TOPMOST=true,但 z-order 排在一般視窗(例如 VS Code)之下, +// 於是寵物就被蓋住,而 Electron 這邊仍以為自己是置頂的,不會自己修好。 +// setAlwaysOnTop 在狀態沒變時可能不會真的呼叫 SetWindowPos,所以再補一個 moveTop() +// 強制重新插回 topmost band 的最上面(視窗是 WS_EX_NOACTIVATE + focusable:false,不會搶焦點)。 +function reassertTopmost() { + if (!win || win.isDestroyed() || !config.alwaysOnTop || dragTimer) return; + try { + win.setAlwaysOnTop(true, "screen-saver"); + win.moveTop(); + } catch { /* 視窗正在關閉就算了 */ } +} + +function startTopmostGuard() { + topmostTimer = setInterval(reassertTopmost, TOPMOST_REASSERT_MS); + // 螢幕配置變動最容易觸發降級,變動後立刻補一次 + screen.on("display-metrics-changed", reassertTopmost); + screen.on("display-added", reassertTopmost); + screen.on("display-removed", reassertTopmost); +} + function startCursorPolling() { cursorTimer = setInterval(() => { if (!win || dragTimer) return; @@ -488,6 +514,7 @@ function buildMenu() { config.alwaysOnTop = item.checked; saveConfig(); win?.setAlwaysOnTop(item.checked, "screen-saver"); + if (item.checked) reassertTopmost(); }, }, { @@ -605,6 +632,7 @@ if (!app.requestSingleInstanceLock()) { createTray(); startServer(); startCursorPolling(); + startTopmostGuard(); appendLog(`started pet=${activePet()?.id || "none"} scale=${config.scale} port=${config.port}`); }); @@ -614,6 +642,7 @@ if (!app.requestSingleInstanceLock()) { app.on("before-quit", () => { if (cursorTimer) clearInterval(cursorTimer); + if (topmostTimer) clearInterval(topmostTimer); endDrag(false); }); } diff --git a/package-lock.json b/package-lock.json index 63f058f..6f80cd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-pet", - "version": "2.0.7", + "version": "2.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-pet", - "version": "2.0.7", + "version": "2.0.8", "devDependencies": { "electron": "^43.4.1", "electron-builder": "^26.0.12" diff --git a/package.json b/package.json index 79aecb5..be14387 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-pet", - "version": "2.0.7", + "version": "2.0.8", "description": "Codex Pets 相容的 Claude Code 桌面寵物(spriteVersionNumber 2:9 個動作列 + 16 方向追視)", "main": "main.js", "private": true,