diff --git a/jetbrains b/jetbrains index cf4c7bc..a7fa3d7 160000 --- a/jetbrains +++ b/jetbrains @@ -1 +1 @@ -Subproject commit cf4c7bc7156800886c7d4f6568c05ffca57a5640 +Subproject commit a7fa3d7061fe51bd12c50f81da2cbf13f782efdc diff --git a/media/annotationPanel/app.js b/media/annotationPanel/app.js index e3e3970..bb55803 100644 --- a/media/annotationPanel/app.js +++ b/media/annotationPanel/app.js @@ -72,6 +72,9 @@ nextImage: 'ArrowRight' }; + // 复制坐标时逗号后加空格(个人偏好,application 作用域;由扩展端经 config 消息下发) + let copyCoordsSpace = true; + function parseKeybinding(kb) { const parts = kb.toLowerCase().split('+'); const key = parts.pop(); @@ -643,7 +646,8 @@ } function formatNormalizedBox(box) { - return [box.x, box.y, box.tox, box.toy].map((v) => v.toFixed(COORD_DECIMALS)).join(','); + return [box.x, box.y, box.tox, box.toy].map((v) => v.toFixed(COORD_DECIMALS)) + .join(copyCoordsSpace ? ', ' : ','); } /** 拖拽过程中在颜色栏实时显示即将复制的坐标。 */ @@ -1011,10 +1015,13 @@ window.addEventListener('message', (e) => { const msg = e.data; if (msg.type === 'config') { - // 接收快捷键配置 + // 接收扩展端配置:快捷键 + 坐标分隔偏好 if (msg.keybindings) { Object.assign(keybindings, msg.keybindings); } + if (typeof msg.copyCoordsSpace === 'boolean') { + copyCoordsSpace = msg.copyCoordsSpace; + } updateButtonTexts(); return; } diff --git a/media/tempScreenshots/app.js b/media/tempScreenshots/app.js index 636f0a1..9f833fa 100644 --- a/media/tempScreenshots/app.js +++ b/media/tempScreenshots/app.js @@ -58,6 +58,8 @@ /* ---------- 常量 ---------- */ const CAROUSEL_INTERVAL_MS = 100; // 需求:0.1s 轮播 const COORD_DECIMALS = 4; // 归一化坐标小数位 + // 复制坐标时逗号后加空格(个人偏好,application 作用域;由扩展端经 config 消息下发) + let copyCoordsSpace = true; const MIN_BOX_PX = 3; const MAX_ZOOM = 20; @@ -101,7 +103,8 @@ } function formatCoords(x1, y1, x2, y2) { - return [x1, y1, x2, y2].map((v) => v.toFixed(COORD_DECIMALS)).join(','); + return [x1, y1, x2, y2].map((v) => v.toFixed(COORD_DECIMALS)) + .join(copyCoordsSpace ? ', ' : ','); } function activeFrame() { @@ -719,6 +722,13 @@ } return; } + if (msg.type === 'config') { + // 扩展端配置:复制坐标分隔偏好(个人偏好,application 作用域) + if (typeof msg.copyCoordsSpace === 'boolean') { + copyCoordsSpace = msg.copyCoordsSpace; + } + return; + } if (msg.type === 'notice') { toast(msg.text, msg.level === 'error'); } diff --git a/package.json b/package.json index a3e2b68..a05ad21 100644 --- a/package.json +++ b/package.json @@ -293,6 +293,12 @@ "default": "ok_templates", "description": "%config.okTemplatesDirectory%" }, + "okScriptToolkit.copyCoordsSpace": { + "type": "boolean", + "default": true, + "scope": "application", + "description": "%config.copyCoordsSpace%" + }, "okScriptToolkit.annotationKeybindings": { "type": "object", "default": { diff --git a/package.nls.es.json b/package.nls.es.json index b030229..bda8794 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -37,6 +37,7 @@ "config.okScriptProjectPath": "Raíz del proyecto ok-script usada por el lanzador de tareas.", "config.okScriptPython": "Intérprete de Python usado para ejecutar tareas ok-script.", "config.captureMethod": "Método de captura de la ventana del juego. auto prueba Windows Graphics Capture y recurre a BitBlt si falla. Usa foreground si la pantalla de inicio o de acceso sale en blanco: activa la ventana y lee la pantalla, lo que roba el foco.", + "config.copyCoordsSpace": "Añade un espacio después de cada coma al copiar coordenadas (x, y, tox, toy). Es una preferencia personal: solo ámbito global (application), no se puede configurar por espacio de trabajo.", "config.characterProjectPath": "Raíz del proyecto usada por el gestor de personajes y habilidades.", "config.characterMasterFile": "JSON maestro de personajes.", "config.characterSkillsDirectory": "Directorio JSON de habilidades de personajes.", diff --git a/package.nls.ja.json b/package.nls.ja.json index 25dcb36..a905440 100644 --- a/package.nls.ja.json +++ b/package.nls.ja.json @@ -37,6 +37,7 @@ "config.okScriptProjectPath": "タスク起動で使用する ok-script プロジェクトルート。", "config.okScriptPython": "ok-script タスク実行に使用する Python。", "config.captureMethod": "ゲームウィンドウのキャプチャ方式。auto は Windows Graphics Capture を試し、失敗したら BitBlt に戻します。ランチャーやログイン画面が空白になる場合は foreground を使ってください。ウィンドウを前面に出して画面を読み取るため、フォーカスが奪われます。", + "config.copyCoordsSpace": "座標をコピーするときにカンマの後にスペースを入れます(x, y, tox, toy)。個人設定のため、グローバル(application スコープ)のみで、ワークスペース単位では設定できません。", "config.characterProjectPath": "キャラクター・スキル管理で使用するプロジェクトルート。", "config.characterMasterFile": "キャラクターマスター JSON。", "config.characterSkillsDirectory": "キャラクタースキル JSON ディレクトリ。", diff --git a/package.nls.json b/package.nls.json index 827c745..fcbf0d8 100644 --- a/package.nls.json +++ b/package.nls.json @@ -37,6 +37,7 @@ "config.okScriptProjectPath": "ok-script project root containing src/config.py. Used by the task launcher.", "config.okScriptPython": "Python interpreter used to run ok-script tasks. Empty uses the target project's .venv on Windows.", "config.captureMethod": "Game window capture method. auto tries Windows Graphics Capture and falls back to BitBlt. Use foreground for launcher or login screens that capture blank otherwise: it activates the window and reads the screen, which steals focus.", + "config.copyCoordsSpace": "Add a space after each comma when copying box coordinates (x, y, tox, toy). A personal habit — application scope only, cannot be set per workspace.", "config.characterProjectPath": "Project root used by the character and skill manager. Empty falls back to okScriptProjectPath and the current workspace.", "config.characterMasterFile": "Character master JSON relative to the character project root.", "config.characterSkillsDirectory": "Character skill JSON directory relative to the character project root.", diff --git a/package.nls.ko.json b/package.nls.ko.json index aabff69..55f72c3 100644 --- a/package.nls.ko.json +++ b/package.nls.ko.json @@ -37,6 +37,7 @@ "config.okScriptProjectPath": "작업 실행기에서 사용할 ok-script 프로젝트 루트입니다.", "config.okScriptPython": "ok-script 작업 실행에 사용할 Python 인터프리터입니다.", "config.captureMethod": "게임 창 캡처 방식입니다. auto는 Windows Graphics Capture를 시도하고 실패하면 BitBlt로 대체합니다. 런처/로그인 화면이 비어 있게 찍힌다면 foreground를 사용하세요. 창을 전면으로 활성화한 뒤 화면을 읽으므로 포커스를 가져갑니다.", + "config.copyCoordsSpace": "좌표를 복사할 때 쉼표 뒤에 공백을 추가합니다(x, y, tox, toy). 개인 취향에 해당하며 전역(application 범위) 설정으로만 가능하고 작업 영역별로는 설정할 수 없습니다.", "config.characterProjectPath": "캐릭터·스킬 관리자가 사용할 프로젝트 루트입니다.", "config.characterMasterFile": "캐릭터 마스터 JSON입니다.", "config.characterSkillsDirectory": "캐릭터 스킬 JSON 디렉터리입니다.", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 6abff75..e9c2e22 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -37,6 +37,7 @@ "config.okScriptProjectPath": "包含 src/config.py 的 ok-script 项目根目录,供任务启动器使用。", "config.okScriptPython": "运行 ok-script 任务的 Python 解释器;留空时 Windows 优先使用目标项目 .venv。", "config.captureMethod": "游戏窗口截图方式。auto 先尝试 Windows Graphics Capture,失败回退 BitBlt。若启动器/登录界面截出来是空白,改用 foreground:它会把窗口激活到前台再读取屏幕(会抢焦点)。", + "config.copyCoordsSpace": "复制框选坐标时在逗号后加空格(x, y, tox, toy)。属个人习惯,仅全局设置(application 作用域),不能按工作区覆盖。", "config.characterProjectPath": "角色技能管理器使用的项目根目录;留空时回退到 okScriptProjectPath 和当前工作区。", "config.characterMasterFile": "角色主表 JSON(相对于角色项目根目录)。", "config.characterSkillsDirectory": "角色技能 JSON 目录(相对于角色项目根目录)。", diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index 249e4e1..84eb04f 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -37,6 +37,7 @@ "config.okScriptProjectPath": "包含 src/config.py 的 ok-script 專案根目錄,供任務啟動器使用。", "config.okScriptPython": "執行 ok-script 任務的 Python 直譯器;留空時 Windows 優先使用目標專案 .venv。", "config.captureMethod": "遊戲視窗截圖方式。auto 先嘗試 Windows Graphics Capture,失敗時退回 BitBlt。若啟動器/登入介面截出來是空白,改用 foreground:它會把視窗啟用到前景再讀取畫面(會搶走焦點)。", + "config.copyCoordsSpace": "複製框選座標時在逗號後加空格(x, y, tox, toy)。屬個人習慣,僅全域設定(application 作用域),不能按工作區覆蓋。", "config.characterProjectPath": "角色技能管理器使用的專案根目錄;留空時回退到 okScriptProjectPath 和目前工作區。", "config.characterMasterFile": "角色主表 JSON(相對於角色專案根目錄)。", "config.characterSkillsDirectory": "角色技能 JSON 目錄(相對於角色專案根目錄)。", diff --git a/scripts/test_annotation_coords.js b/scripts/test_annotation_coords.js index 8ba55ef..3c5b1bb 100644 --- a/scripts/test_annotation_coords.js +++ b/scripts/test_annotation_coords.js @@ -160,7 +160,7 @@ function mouse(type, target, x, y, button = 0) { const copy = lastPost('copyText'); assert(copy, 'box-select in coord mode must post a copyText message'); - assert(copy.text === '0.1250,0.1000,0.6250,0.6000', + assert(copy.text === '0.1250, 0.1000, 0.6250, 0.6000', 'expected normalized x,y,tox,toy, got ' + copy.text); // 框留在画布上供继续调整,所以坐标模式不会自动退出 assert(coordBtn.classList.contains('active'), @@ -174,7 +174,7 @@ function mouse(type, target, x, y, button = 0) { mouse('mouseup', canvas, 350, 260); await flush(); assert(post('copyText').length === before + 1, 'moving the box must copy again'); - assert(lastPost('copyText').text === '0.1875,0.1667,0.6875,0.6667', + assert(lastPost('copyText').text === '0.1875, 0.1667, 0.6875, 0.6667', 'moved box must copy the updated coords, got ' + lastPost('copyText').text); /* ---------- 拖动手柄:缩放并重新复制 ---------- */ @@ -186,7 +186,7 @@ function mouse(type, target, x, y, button = 0) { await flush(); assert(post('copyText').length === before + 1, 'resizing the box must copy again'); // dx=60,dy=30 → 图像 +144,+72 → 宽 1104 高 612 - assert(lastPost('copyText').text === '0.1875,0.1667,0.7625,0.7333', + assert(lastPost('copyText').text === '0.1875, 0.1667, 0.7625, 0.7333', 'resized box must copy the updated coords, got ' + lastPost('copyText').text); /* ---------- 点击非交互部分清除坐标框 ---------- */ @@ -203,7 +203,7 @@ function mouse(type, target, x, y, button = 0) { mouse('mousemove', canvas, 500, 345); mouse('mouseup', canvas, 500, 345); await flush(); - assert(lastPost('copyText').text === '0.1250,0.1000,0.6250,0.6000', + assert(lastPost('copyText').text === '0.1250, 0.1000, 0.6250, 0.6000', 'a fresh box can be created after clearing, got ' + lastPost('copyText').text); /* ---------- 坐标框不进入标注数据(不落盘) ---------- */ @@ -219,7 +219,7 @@ function mouse(type, target, x, y, button = 0) { mouse('mousemove', canvas, 100, 120); mouse('mouseup', canvas, 100, 120); await flush(); - assert(lastPost('copyText').text === '0.1250,0.1000,0.6250,0.6000', + assert(lastPost('copyText').text === '0.1250, 0.1000, 0.6250, 0.6000', 'reverse drag must normalize to the same box, got ' + lastPost('copyText').text); /* ---------- 越界框选必须 clamp ---------- */ @@ -227,7 +227,7 @@ function mouse(type, target, x, y, button = 0) { mouse('mousemove', canvas, 5000, 5000); mouse('mouseup', canvas, 5000, 5000); await flush(); - assert(lastPost('copyText').text === '0.0000,0.0000,1.0000,1.0000', + assert(lastPost('copyText').text === '0.0000, 0.0000, 1.0000, 1.0000', 'out-of-range selection must clamp to 0..1, got ' + lastPost('copyText').text); /* ---------- 退出坐标模式应丢弃坐标框 ---------- */ diff --git a/scripts/test_temp_screenshots.js b/scripts/test_temp_screenshots.js index 102fdfa..18218d0 100644 --- a/scripts/test_temp_screenshots.js +++ b/scripts/test_temp_screenshots.js @@ -197,8 +197,8 @@ function mouse(type, target, x, y, button = 0) { const copy = lastPost('copyText'); assert(copy, 'box-select must post a copyText message'); - assert(copy.text === '0.2500,0.2000,0.7500,0.6667', - 'normalized coords must be x,y,tox,toy with 4 decimals, got ' + copy.text); + assert(copy.text === '0.2500, 0.2000, 0.7500, 0.6667', + 'default normalized coords must be "x, y, tox, toy" with 4 decimals (copyCoordsSpace=true), got ' + copy.text); assert(document.getElementById('selBox').classList.contains('visible'), 'the selection box must stay visible after copying so it can be compared against moving frames'); @@ -210,7 +210,7 @@ function mouse(type, target, x, y, button = 0) { mouse('mouseup', window, 250, 165); await flush(); assert(post('copyText').length === copyBefore + 1, 'moving the box must copy again'); - assert(lastPost('copyText').text === '0.3750,0.3333,0.8750,0.8000', + assert(lastPost('copyText').text === '0.3750, 0.3333, 0.8750, 0.8000', 'moved box must copy the updated coords, got ' + lastPost('copyText').text); /* ---------- 3b. 拖动手柄缩放并重新复制 ---------- */ @@ -221,7 +221,7 @@ function mouse(type, target, x, y, button = 0) { mouse('mouseup', window, 390, 247.5); await flush(); assert(post('copyText').length === copyBefore + 1, 'resizing the box must copy again'); - assert(lastPost('copyText').text === '0.3750,0.3333,0.9750,0.9333', + assert(lastPost('copyText').text === '0.3750, 0.3333, 0.9750, 0.9333', 'resized box must copy the updated coords, got ' + lastPost('copyText').text); /* ---------- 3c. 点击非交互部分清除坐标框 ---------- */ @@ -235,12 +235,46 @@ function mouse(type, target, x, y, button = 0) { assert(!document.getElementById('selBox').classList.contains('visible'), 'the box must disappear after clicking empty area'); + /* ---------- 3c-2. config 消息切换「逗号后加空格」偏好 ---------- */ + // 关掉空格 → 复制格式回退为 x,y,tox,toy;再打开 → 恢复默认("x, y, tox, toy") + window.postMessage({ type: 'config', copyCoordsSpace: false }, '*'); + await flush(); + copyBefore = post('copyText').length; + mouse('mousedown', overlay, 100, 50); + mouse('mousemove', window, 200, 150); + mouse('mouseup', window, 200, 150); + await flush(); + assert(post('copyText').length === copyBefore + 1, 'box-select must copy after toggling space off'); + assert(lastPost('copyText').text === '0.2500,0.0556,0.5000,0.5000', + // 几何值带前面步骤遗留的舞台 y 变换,这里只关心分隔符 + 'copyCoordsSpace=false must join coords with bare commas, got ' + lastPost('copyText').text); + // 清掉旧框:否则 (100,50) 落在框的 tl 手柄上,会触发缩放而不是新建框 + mouse('mousedown', overlay, 30, 30); + mouse('mouseup', overlay, 30, 30); + await flush(); + window.postMessage({ type: 'config', copyCoordsSpace: true }, '*'); + await flush(); + copyBefore = post('copyText').length; + mouse('mousedown', overlay, 100, 50); + mouse('mousemove', window, 200, 150); + mouse('mouseup', window, 200, 150); + await flush(); + assert(post('copyText').length === copyBefore + 1, 'box-select must copy after toggling space on'); + assert(lastPost('copyText').text === '0.2500, 0.0556, 0.5000, 0.5000', + 'copyCoordsSpace=true must join coords with ", ", got ' + lastPost('copyText').text); + // 清除,避免影响后面的越界 clamp 用例 + mouse('mousedown', overlay, 30, 30); + mouse('mouseup', overlay, 30, 30); + await flush(); + assert(!document.getElementById('selBox').classList.contains('visible'), + 'the box must disappear after the space-toggle cleanup click'); + // 越界框选必须 clamp 到 0..1(放在清除之后,避免复用上一次留下的框) mouse('mousedown', overlay, -50, -50); mouse('mousemove', window, 900, 900); mouse('mouseup', window, 900, 900); await flush(); - assert(lastPost('copyText').text === '0.0000,0.0000,1.0000,1.0000', + assert(lastPost('copyText').text === '0.0000, 0.0000, 1.0000, 1.0000', 'out-of-range selection must clamp to 0..1, got ' + lastPost('copyText').text); // 误触(极小框)不应产生复制 @@ -275,7 +309,7 @@ function mouse(type, target, x, y, button = 0) { await flush(); assert(carouselBtn.classList.contains('active'), 'the carousel must still be running after the box-selection completes'); - assert(lastPost('copyText').text === '0.3000,0.2333,0.8000,0.7000', + assert(lastPost('copyText').text === '0.3000, 0.2333, 0.8000, 0.7000', 'coords taken against a cycling frame must still normalize correctly, got ' + lastPost('copyText').text); carouselBtn.dispatchEvent(new window.MouseEvent('click', { bubbles: true })); diff --git a/src/annotationPanel.ts b/src/annotationPanel.ts index 88f11cb..6ad9e2b 100644 --- a/src/annotationPanel.ts +++ b/src/annotationPanel.ts @@ -35,6 +35,13 @@ class AnnotationController { ) { this.disposables.push( webview.onDidReceiveMessage((msg) => { void this.onMessage(msg); }), + // 面板开着时改设置也要热更新(review 意见:不要靠「切一下视图」触发) + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('okScriptToolkit.copyCoordsSpace') || + e.affectsConfiguration('okScriptToolkit.annotationKeybindings')) { + this.sendConfig(); + } + }), ); } @@ -42,16 +49,16 @@ class AnnotationController { attachHtml(): void { this.webview.html = annotationHtml(this.webview.cspSource, this.extensionUri, this.webview); - this.sendKeybindings(); + this.sendConfig(); } - /** 读取扩展设置并发送快捷键配置到 webview */ - private sendKeybindings(): void { + /** 读取扩展设置并发送面板配置到 webview(快捷键 + 坐标分隔偏好) */ + private sendConfig(): void { const cfg = vscode.workspace.getConfiguration('okScriptToolkit'); const kb = cfg.get>('annotationKeybindings'); - if (kb) { - void this.webview.postMessage({ type: 'config', keybindings: kb }); - } + // 坐标逗号后加空格是个人习惯 → application 作用域,只在用户设置里存在 + const copyCoordsSpace = cfg.get('copyCoordsSpace', true); + void this.webview.postMessage({ type: 'config', keybindings: kb, copyCoordsSpace }); } open(imagePath: string, imageList: string[]): void { @@ -128,6 +135,8 @@ class AnnotationController { }): Promise { switch (msg.type) { case 'ready': + // ready 重发一次配置:attachHtml 时机太早、webview 脚本可能还没挂监听 + this.sendConfig(); if (this._currentImage) { await this.loadImage(this._currentImage); } diff --git a/src/tempScreenshotPanel.ts b/src/tempScreenshotPanel.ts index e0b66e0..18f5d17 100644 --- a/src/tempScreenshotPanel.ts +++ b/src/tempScreenshotPanel.ts @@ -80,11 +80,30 @@ class TempScreenshotController { this.disposables.push( webview.onDidReceiveMessage((msg) => { void this.onMessage(msg); }), store.onChange(() => { void this.refresh(); }), + // 面板开着时改设置也要热更新(review 意见:不要靠「切一下视图」触发) + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('okScriptToolkit.copyCoordsSpace')) { + this.pushCoordFormat(); + } + }), ); } attachHtml(): void { this.webview.html = tempScreenshotHtml(this.webview, this.extensionUri); + this.pushCoordFormat(); + } + + /** + * 把「复制坐标逗号后加空格」偏好推给 webview。 + * application 作用域(个人习惯,不随工作区),读一次默认 true。 + * attach 与每次 refresh 都推:refresh 覆盖 ready / 视图可见 / 列表变化, + * 用户改设置后切一下视图就能生效,不用重开面板。 + */ + private pushCoordFormat(): void { + const copyCoordsSpace = vscode.workspace.getConfiguration('okScriptToolkit') + .get('copyCoordsSpace', true); + void this.webview.postMessage({ type: 'config', copyCoordsSpace }); } /* ---------- 推送列表 ---------- */ @@ -92,6 +111,7 @@ class TempScreenshotController { async refresh(): Promise { if (this.disposed) return; if (!this.isVisible()) return; + this.pushCoordFormat(); const items = this.store.list(); const metas: TempMeta[] = [];