Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeba11e643 | ||
|
|
cea80daa06 | ||
|
|
35865b9e17 | ||
|
|
30d3e481e4 |
@@ -102,6 +102,7 @@ npm run autostart:install # (可選)開機自動啟動
|
|||||||
- **Claude Code hooks** — 安裝 / 移除 hooks
|
- **Claude Code hooks** — 安裝 / 移除 hooks
|
||||||
- **回到預設位置** — 跑到螢幕外面時用
|
- **回到預設位置** — 跑到螢幕外面時用
|
||||||
- **開啟設定檔** / **開啟事件紀錄**
|
- **開啟設定檔** / **開啟事件紀錄**
|
||||||
|
- **重啟**(portable 版會延遲兩秒重新執行原本的 `-portable.exe`,因為外殼在程式結束時會刪掉解壓目錄)
|
||||||
- **結束**
|
- **結束**
|
||||||
|
|
||||||
## 行為對照表
|
## 行為對照表
|
||||||
@@ -244,7 +245,9 @@ Claude Code ──hook(stdin JSON)──▶ hook/claude-pet-hook.js ──PO
|
|||||||
- **點穿由主程序決定**:視窗預設 `setIgnoreMouseEvents(true, { forward: true })`;主程序每 50 ms 自己輪詢游標(`screen.getCursorScreenPoint()`),對照 renderer 回報的**人物方框**與(顯示中的)氣泡矩形,游標在上面就接滑鼠、否則穿透(`lib/hit-test.js`,純邏輯可單元測試)。決策不經過 renderer:以前是 renderer 判定再用 IPC 叫主程序切,renderer 一旦例外、卡住或狀態錯亂(例如放開事件沒送到、`pressed` 一直是 true),點穿就永遠不會再更新,寵物就點不到了。renderer 沒回報矩形或掛掉時,主程序用版面公式算方框當備援;renderer 掛掉會記錄並自動重載。判定用方框而不是讀 canvas 該點的 alpha:人物的手腳與邊緣會隨動畫在透明/不透明之間切換(以小念為例,hover 時只有 44% 的人物像素每一幀都不透明),逐像素判定會讓點穿狀態跟著動畫高速抖動,按下去那一瞬間剛好是透明格,滑鼠事件就穿到底下的視窗去了。主程序輪詢也是必要的——視窗處於點穿狀態時 Electron 的 `forward` 不保證會把 mousemove 轉發進來(游標直接跳到寵物上常常收不到)。
|
- **點穿由主程序決定**:視窗預設 `setIgnoreMouseEvents(true, { forward: true })`;主程序每 50 ms 自己輪詢游標(`screen.getCursorScreenPoint()`),對照 renderer 回報的**人物方框**與(顯示中的)氣泡矩形,游標在上面就接滑鼠、否則穿透(`lib/hit-test.js`,純邏輯可單元測試)。決策不經過 renderer:以前是 renderer 判定再用 IPC 叫主程序切,renderer 一旦例外、卡住或狀態錯亂(例如放開事件沒送到、`pressed` 一直是 true),點穿就永遠不會再更新,寵物就點不到了。renderer 沒回報矩形或掛掉時,主程序用版面公式算方框當備援;renderer 掛掉會記錄並自動重載。判定用方框而不是讀 canvas 該點的 alpha:人物的手腳與邊緣會隨動畫在透明/不透明之間切換(以小念為例,hover 時只有 44% 的人物像素每一幀都不透明),逐像素判定會讓點穿狀態跟著動畫高速抖動,按下去那一瞬間剛好是透明格,滑鼠事件就穿到底下的視窗去了。主程序輪詢也是必要的——視窗處於點穿狀態時 Electron 的 `forward` 不保證會把 mousemove 轉發進來(游標直接跳到寵物上常常收不到)。
|
||||||
- **拖曳結束一定通知 renderer**:視窗永遠不取得焦點,Windows 對背景視窗的滑鼠 capture 有限制,放開的那一下若落在視窗外 renderer 收不到 mouseup。主程序不管因為 mouseup、逾時(20 秒)或其他原因結束拖曳,都送 `pet:drag-ended` 讓 renderer 清掉按住/拖曳狀態。
|
- **拖曳結束一定通知 renderer**:視窗永遠不取得焦點,Windows 對背景視窗的滑鼠 capture 有限制,放開的那一下若落在視窗外 renderer 收不到 mouseup。主程序不管因為 mouseup、逾時(20 秒)或其他原因結束拖曳,都送 `pet:drag-ended` 讓 renderer 清掉按住/拖曳狀態。
|
||||||
- **右鍵選單開著時暫停置頂重宣告**:否則 `moveTop()` 會把寵物視窗抬到選單上面,人物方框整塊接滑鼠,被蓋住的選單項目就點不到。
|
- **右鍵選單開著時暫停置頂重宣告**:否則 `moveTop()` 會把寵物視窗抬到選單上面,人物方框整塊接滑鼠,被蓋住的選單項目就點不到。
|
||||||
- **診斷**:`GET /state` 除了動畫狀態,還會回 `bounds` 與 `mouse`(目前是否點穿、游標的視窗相對座標、是否在方框/氣泡內、矩形來源是 renderer 還是備援、切換次數、輪詢次數、renderer 錯誤數、最近幾次拖曳結束的原因);renderer 的例外、拖曳開始/結束、選單開關都會寫進事件紀錄。
|
- **診斷**:`GET /state` 除了動畫狀態,還會回 `bounds`、`mouse`(目前是否點穿、游標的視窗相對座標、是否在方框/氣泡內、矩形來源是 renderer 還是備援、切換次數、輪詢次數、renderer 錯誤數、最近幾次拖曳結束的原因、鎖定/解鎖/睡眠/喚醒事件)與 `rendererMouse`(renderer 這邊的 pressed/drag/hovered/overlay)。renderer 收到的每個 mousedown/mouseup、hover 進出、例外,以及拖曳開始/結束、選單開關、鎖定/解鎖都會寫進事件紀錄——「點不到寵物」時先看紀錄裡有沒有 `mousedown`,就能分清是 OS 沒把事件送進來,還是 renderer 收到了但狀態卡住。
|
||||||
|
- **解鎖螢幕後 mousedown 會被吃掉**:實測(v2.0.13 的事件紀錄)鎖定再解鎖後,renderer 只收得到 mouseup 與 mousemove,左右鍵的 mousedown 全部消失,直到重啟才恢復——症狀就是「不能拖曳、不能右鍵」。行為符合 Windows 對永不啟用(`WS_EX_NOACTIVATE`)視窗的 `WM_MOUSEACTIVATE` 回傳 `MA_NOACTIVATEANDEAT`(只丟掉按下那一則訊息),確切觸發條件尚未定位。因應是讓所有互動都不依賴 mousedown:右鍵選單在 mouseup 開(也是 Windows 慣例);mousemove 的 `buttons` 帶著「左鍵按著」而沒有 pressed 時補一個 pressed,拖曳照常;沒有 mousedown 的左鍵 mouseup 當成單擊。
|
||||||
|
- **鎖定/解鎖後重新套用**:監聽 `powerMonitor` 的 `unlock-screen` 與 `resume`,解鎖或喚醒後重新套用目前的點穿狀態、`showInactive()` 並重宣告置頂(session 切換後 Windows 可能把視窗的輸入或 z-order 狀態弄掉)。
|
||||||
- **拖曳**:由主程序用 `screen.getCursorScreenPoint()` 每 16 ms 更新視窗位置,游標離開視窗也不會掉;方向由水平位移決定。
|
- **拖曳**:由主程序用 `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`,不會搶焦點。
|
- **置頂要定期重新宣告**: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-的對照)。
|
- **行為邏輯以 Codex 為準**,細節見[與 Codex 的對照](#與-codex-的對照)。
|
||||||
@@ -320,8 +323,9 @@ claude-pet
|
|||||||
| 方法 | 路徑 | 說明 |
|
| 方法 | 路徑 | 說明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `POST` | `/event` | 接收事件(JSON,≤ 64 KB),回 `204` |
|
| `POST` | `/event` | 接收事件(JSON,≤ 64 KB),回 `204` |
|
||||||
| `GET` | `/state` | 目前狀態:`{"anim","base","sessions","pet","scale"}` |
|
| `GET` | `/state` | 目前狀態:`{"anim","base","sessions","pet","scale","bounds","mouse","rendererMouse"}` |
|
||||||
| `GET` | `/health` | 回 `ok` |
|
| `GET` | `/health` | 回 `ok` |
|
||||||
|
| `POST` | `/restart` | 重啟寵物(與選單的「重啟」相同) |
|
||||||
|
|
||||||
`POST /event` 的 payload(hook 腳本送出的格式):
|
`POST /event` 的 payload(hook 腳本送出的格式):
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
const { app, BrowserWindow, Tray, Menu, screen, ipcMain, nativeImage, shell, dialog } = require("electron");
|
const { app, BrowserWindow, Tray, Menu, screen, ipcMain, nativeImage, shell, dialog, powerMonitor } = require("electron");
|
||||||
const http = require("node:http");
|
const http = require("node:http");
|
||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const os = require("node:os");
|
const os = require("node:os");
|
||||||
|
const { spawn } = require("node:child_process");
|
||||||
const autostart = require("./lib/autostart");
|
const autostart = require("./lib/autostart");
|
||||||
const hooksInstaller = require("./lib/hooks-installer");
|
const hooksInstaller = require("./lib/hooks-installer");
|
||||||
const { unpacked, portableDir, isPackaged } = require("./lib/paths");
|
const { unpacked, portableDir, portableExe, isPackaged } = require("./lib/paths");
|
||||||
|
|
||||||
const APP_DIR = __dirname;
|
const APP_DIR = __dirname;
|
||||||
const HOME = os.homedir();
|
const HOME = os.homedir();
|
||||||
@@ -69,6 +70,7 @@ const mouseDiag = {
|
|||||||
toggles: 0, lastToggleAt: null, polls: 0, lastPollAt: null,
|
toggles: 0, lastToggleAt: null, polls: 0, lastPollAt: null,
|
||||||
rel: null, inBox: false, inBubble: false,
|
rel: null, inBox: false, inBubble: false,
|
||||||
rendererErrors: 0, lastRendererError: null, dragEnds: [],
|
rendererErrors: 0, lastRendererError: null, dragEnds: [],
|
||||||
|
sessionEvents: [], // 鎖定/解鎖/睡眠/喚醒
|
||||||
};
|
};
|
||||||
let currentState = { anim: "idle", base: "idle", sessions: 0 };
|
let currentState = { anim: "idle", base: "idle", sessions: 0 };
|
||||||
|
|
||||||
@@ -374,6 +376,25 @@ function reassertTopmost() {
|
|||||||
} catch { /* 視窗正在關閉就算了 */ }
|
} catch { /* 視窗正在關閉就算了 */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 鎖定/解鎖與睡眠喚醒:session 切換後 Windows 可能把視窗的輸入或 z-order 狀態弄掉,
|
||||||
|
// 記下來並把目前的點穿狀態與置頂重新套用一次
|
||||||
|
function watchSessionChanges() {
|
||||||
|
for (const ev of ["lock-screen", "unlock-screen", "suspend", "resume"]) {
|
||||||
|
powerMonitor.on(ev, () => {
|
||||||
|
appendLog(`power: ${ev}`);
|
||||||
|
mouseDiag.sessionEvents.push({ at: Date.now(), ev });
|
||||||
|
if (mouseDiag.sessionEvents.length > 10) mouseDiag.sessionEvents.shift();
|
||||||
|
if (ev !== "unlock-screen" && ev !== "resume") return;
|
||||||
|
if (!win || win.isDestroyed()) return;
|
||||||
|
try {
|
||||||
|
win.setIgnoreMouseEvents(ignoring, { forward: true });
|
||||||
|
win.showInactive();
|
||||||
|
} catch { /* 視窗正在關閉 */ }
|
||||||
|
reassertTopmost();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function startTopmostGuard() {
|
function startTopmostGuard() {
|
||||||
topmostTimer = setInterval(reassertTopmost, TOPMOST_REASSERT_MS);
|
topmostTimer = setInterval(reassertTopmost, TOPMOST_REASSERT_MS);
|
||||||
// 螢幕配置變動最容易觸發降級,變動後立刻補一次
|
// 螢幕配置變動最容易觸發降級,變動後立刻補一次
|
||||||
@@ -487,6 +508,12 @@ function startServer() {
|
|||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (req.method === "POST" && req.url === "/restart") {
|
||||||
|
res.writeHead(204);
|
||||||
|
res.end();
|
||||||
|
setTimeout(restartApp, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (req.method === "GET" && req.url === "/health") {
|
if (req.method === "GET" && req.url === "/health") {
|
||||||
res.writeHead(200, { "content-type": "text/plain" });
|
res.writeHead(200, { "content-type": "text/plain" });
|
||||||
res.end("ok");
|
res.end("ok");
|
||||||
@@ -633,6 +660,7 @@ function buildMenu() {
|
|||||||
{ label: "開啟設定檔", click: () => { saveConfig(); shell.openPath(CONFIG_PATH); } },
|
{ label: "開啟設定檔", click: () => { saveConfig(); shell.openPath(CONFIG_PATH); } },
|
||||||
{ label: "開啟事件紀錄", click: () => { appendLog("open log"); shell.openPath(LOG_PATH); } },
|
{ label: "開啟事件紀錄", click: () => { appendLog("open log"); shell.openPath(LOG_PATH); } },
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
|
{ label: "重啟", click: restartApp },
|
||||||
{ label: "結束", click: () => app.quit() },
|
{ label: "結束", click: () => app.quit() },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -659,6 +687,23 @@ function setScale(scale) {
|
|||||||
refreshTray();
|
refreshTray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重啟。安裝版/開發模式用 app.relaunch()(等目前這個結束後才啟動新的)。
|
||||||
|
// portable 版不能這樣:relaunch 會重跑解壓到 %TEMP% 的那顆 exe,而 portable 外殼在程式結束時會把
|
||||||
|
// 整個解壓目錄刪掉、下次啟動再重新解壓;舊程序還在跑時檔案被鎖住,解壓也會失敗。
|
||||||
|
// 所以改成延遲兩秒後重新執行原本的 -portable.exe,然後結束自己。
|
||||||
|
function restartApp() {
|
||||||
|
appendLog("restart requested");
|
||||||
|
const exe = portableExe();
|
||||||
|
if (exe) {
|
||||||
|
const child = spawn("cmd.exe", ["/c", `ping -n 3 127.0.0.1 >nul && start "" "${exe}"`],
|
||||||
|
{ detached: true, stdio: "ignore", windowsHide: true, windowsVerbatimArguments: true });
|
||||||
|
child.unref();
|
||||||
|
} else {
|
||||||
|
app.relaunch();
|
||||||
|
}
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
function resetPosition() {
|
function resetPosition() {
|
||||||
if (!win) return;
|
if (!win) return;
|
||||||
const pos = defaultPosition(windowSizeFor(config.scale));
|
const pos = defaultPosition(windowSizeFor(config.scale));
|
||||||
@@ -735,6 +780,7 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
startServer();
|
startServer();
|
||||||
startCursorPolling();
|
startCursorPolling();
|
||||||
startTopmostGuard();
|
startTopmostGuard();
|
||||||
|
watchSessionChanges();
|
||||||
appendLog(`started pet=${activePet()?.id || "none"} scale=${config.scale} port=${config.port}`);
|
appendLog(`started pet=${activePet()?.id || "none"} scale=${config.scale} port=${config.port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-pet",
|
"name": "claude-pet",
|
||||||
"version": "2.0.11",
|
"version": "2.0.14",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "claude-pet",
|
"name": "claude-pet",
|
||||||
"version": "2.0.11",
|
"version": "2.0.14",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^43.4.1",
|
"electron": "^43.4.1",
|
||||||
"electron-builder": "^26.0.12"
|
"electron-builder": "^26.0.12"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-pet",
|
"name": "claude-pet",
|
||||||
"version": "2.0.11",
|
"version": "2.0.14",
|
||||||
"description": "Codex Pets 相容的 Claude Code 桌面寵物(spriteVersionNumber 2:9 個動作列 + 16 方向追視)",
|
"description": "Codex Pets 相容的 Claude Code 桌面寵物(spriteVersionNumber 2:9 個動作列 + 16 方向追視)",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
+33
-4
@@ -518,7 +518,11 @@ function handleTest(p) {
|
|||||||
|
|
||||||
function report() {
|
function report() {
|
||||||
const base = baseState();
|
const base = baseState();
|
||||||
const snapshot = JSON.stringify({ anim: player.anim, base, sessions: sessions.size, pet: petInfo?.id || null });
|
const snapshot = JSON.stringify({
|
||||||
|
anim: player.anim, base, sessions: sessions.size, pet: petInfo?.id || null,
|
||||||
|
// renderer 這邊的滑鼠狀態也露到 /state,查「點不到寵物」時才看得出是不是卡在這裡
|
||||||
|
rendererMouse: { pressed: !!pressed, drag: !!drag, hovered, overlay: overlay?.anim || null },
|
||||||
|
});
|
||||||
if (snapshot !== lastReported) {
|
if (snapshot !== lastReported) {
|
||||||
lastReported = snapshot;
|
lastReported = snapshot;
|
||||||
window.pet.send("pet:state", JSON.parse(snapshot));
|
window.pet.send("pet:state", JSON.parse(snapshot));
|
||||||
@@ -650,6 +654,8 @@ function overPetArea(x, y) {
|
|||||||
function setHovered(on) {
|
function setHovered(on) {
|
||||||
if (on === hovered) return;
|
if (on === hovered) return;
|
||||||
hovered = on;
|
hovered = on;
|
||||||
|
log(`hover ${on ? "enter" : "leave"} pressed=${!!pressed} drag=${!!drag} overlay=${overlay?.anim || "-"}`);
|
||||||
|
report();
|
||||||
if (on) {
|
if (on) {
|
||||||
// 只在「進入」時觸發一次;一次性動作或拖曳進行中不打斷
|
// 只在「進入」時觸發一次;一次性動作或拖曳進行中不打斷
|
||||||
if (!overlay && !drag) {
|
if (!overlay && !drag) {
|
||||||
@@ -670,6 +676,13 @@ function refreshHitTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("mousemove", (e) => {
|
window.addEventListener("mousemove", (e) => {
|
||||||
|
// 鎖定螢幕再解鎖後,Windows/Chromium 會把送給這個視窗的每一個 mousedown 吃掉,
|
||||||
|
// mouseup 與 mousemove 卻照送(實測見事件紀錄)。mousemove 的 buttons 位元仍然帶著
|
||||||
|
// 「左鍵按著」,用它補一個 pressed,拖曳才不會因為少了 down 就失效。
|
||||||
|
if (!pressed && !drag && (e.buttons & 1)) {
|
||||||
|
pressed = { x: e.clientX, y: e.clientY, sx: e.screenX, sy: e.screenY, moved: false, synthetic: true };
|
||||||
|
log(`mousedown missing; synthesized from buttons at ${e.clientX},${e.clientY}`);
|
||||||
|
}
|
||||||
if (pressed) {
|
if (pressed) {
|
||||||
if (!pressed.moved && Math.hypot(e.screenX - pressed.sx, e.screenY - pressed.sy) > 4) {
|
if (!pressed.moved && Math.hypot(e.screenX - pressed.sx, e.screenY - pressed.sy) > 4) {
|
||||||
pressed.moved = true;
|
pressed.moved = true;
|
||||||
@@ -689,16 +702,30 @@ document.addEventListener("mouseout", (e) => {
|
|||||||
setHovered(false);
|
setHovered(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 滑鼠按下/放開都寫進事件紀錄:這是「有沒有收到滑鼠事件」唯一的直接證據
|
||||||
window.addEventListener("mousedown", (e) => {
|
window.addEventListener("mousedown", (e) => {
|
||||||
|
log(`mousedown b=${e.button} at ${e.clientX},${e.clientY} pressed=${!!pressed} drag=${!!drag}`);
|
||||||
if (e.button === 0) {
|
if (e.button === 0) {
|
||||||
pressed = { x: e.clientX, y: e.clientY, sx: e.screenX, sy: e.screenY, moved: false };
|
pressed = { x: e.clientX, y: e.clientY, sx: e.screenX, sy: e.screenY, moved: false };
|
||||||
} else if (e.button === 2) {
|
|
||||||
window.pet.send("pet:context-menu");
|
|
||||||
}
|
}
|
||||||
|
report();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 點擊與右鍵都以 mouseup 為準:解鎖後 mousedown 會被吃掉(見上),mouseup 不會;
|
||||||
|
// 右鍵選單在放開時開也是 Windows 的慣例。
|
||||||
window.addEventListener("mouseup", (e) => {
|
window.addEventListener("mouseup", (e) => {
|
||||||
if (e.button !== 0 || !pressed) return;
|
log(`mouseup b=${e.button} at ${e.clientX},${e.clientY} pressed=${!!pressed} moved=${!!pressed?.moved}`);
|
||||||
|
if (e.button === 2) {
|
||||||
|
window.pet.send("pet:context-menu");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
if (!pressed) {
|
||||||
|
log("mouseup without mousedown; treating as click");
|
||||||
|
onClick();
|
||||||
|
report();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const wasDrag = pressed.moved;
|
const wasDrag = pressed.moved;
|
||||||
pressed = null;
|
pressed = null;
|
||||||
if (wasDrag) {
|
if (wasDrag) {
|
||||||
@@ -708,6 +735,7 @@ window.addEventListener("mouseup", (e) => {
|
|||||||
} else {
|
} else {
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
|
report();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("contextmenu", (e) => e.preventDefault());
|
window.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||||
@@ -728,6 +756,7 @@ window.pet.on("pet:drag-ended", () => {
|
|||||||
pressed = null;
|
pressed = null;
|
||||||
drag = null;
|
drag = null;
|
||||||
canvas.classList.remove("grabbing");
|
canvas.classList.remove("grabbing");
|
||||||
|
report();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 主程序切了點穿狀態:只負責換游標樣式
|
// 主程序切了點穿狀態:只負責換游標樣式
|
||||||
|
|||||||
Reference in New Issue
Block a user