fix: 框选截图顺序与多覆盖层状态聚合\n\n- 主屏截到应用窗口: 之前框选结束后先恢复应用窗口再截图, 应用弹回盖住选区; 改为先销毁覆盖层并截图, 最后恢复应用窗口\n- 副屏拖拽无响应/Esc 无效: 轮询只读第一个(主屏)覆盖层状态; 改为聚合所有覆盖层, 任一屏 Esc 可取消、任一屏拖拽可识别\n- 多显示器截图源匹配增加按下→右排序对位回退(display_id 缺失时)
这个提交包含在:
1 父节点
5e6b7cd878
当前提交
257fdc7358
1 个文件已更改
+23
-11
+23
-11
@@ -742,20 +742,26 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width:
|
|||||||
let picked: { rect: { x: number; y: number; width: number; height: number }; display: (typeof displays)[number] } | null = null;
|
let picked: { rect: { x: number; y: number; width: number; height: number }; display: (typeof displays)[number] } | null = null;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
await new Promise(r => setTimeout(r, 100));
|
await new Promise(r => setTimeout(r, 100));
|
||||||
let state: { down: boolean; wasDown: boolean; cancelled: boolean } | null = null;
|
// 聚合所有覆盖层的状态: 任一屏按 Esc 取消, 任一屏的按下/松开都要识别
|
||||||
|
let down = false;
|
||||||
|
let wasDown = false;
|
||||||
|
let anyAlive = false;
|
||||||
for (const { win } of overlays) {
|
for (const { win } of overlays) {
|
||||||
if (win.isDestroyed()) continue;
|
if (win.isDestroyed()) continue;
|
||||||
try {
|
try {
|
||||||
const raw = await win.webContents.executeJavaScript("JSON.stringify({ down: !!window.__down, wasDown: !!window.__wasDown, cancelled: !!window.__cancelled })", true);
|
const raw = await win.webContents.executeJavaScript("JSON.stringify({ down: !!window.__down, wasDown: !!window.__wasDown, cancelled: !!window.__cancelled })", true);
|
||||||
state = JSON.parse(raw) as { down: boolean; wasDown: boolean; cancelled: boolean };
|
const s = JSON.parse(raw) as { down: boolean; wasDown: boolean; cancelled: boolean };
|
||||||
break;
|
anyAlive = true;
|
||||||
|
down = down || s.down;
|
||||||
|
wasDown = wasDown || s.wasDown;
|
||||||
|
if (s.cancelled) { cancelled = true; }
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
if (!state) continue;
|
if (cancelled) break;
|
||||||
if (state.cancelled) { cancelled = true; break; }
|
if (!anyAlive) continue;
|
||||||
const cursor = screen.getCursorScreenPoint();
|
const cursor = screen.getCursorScreenPoint();
|
||||||
if (state.down && !downPoint) { downPoint = { x: cursor.x, y: cursor.y }; sawDown = true; }
|
if (down && !downPoint) { downPoint = { x: cursor.x, y: cursor.y }; sawDown = true; }
|
||||||
if (!state.down && sawDown && downPoint) {
|
if (!down && sawDown && downPoint) {
|
||||||
const display = displayForPoint(downPoint.x, downPoint.y);
|
const display = displayForPoint(downPoint.x, downPoint.y);
|
||||||
const rx0 = Math.min(downPoint.x, cursor.x);
|
const rx0 = Math.min(downPoint.x, cursor.x);
|
||||||
const ry0 = Math.min(downPoint.y, cursor.y);
|
const ry0 = Math.min(downPoint.y, cursor.y);
|
||||||
@@ -765,11 +771,12 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width:
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const w of appWindows) { if (!w.isDestroyed()) { w.show(); w.restore(); w.focus(); } }
|
|
||||||
for (const { win } of overlays) if (!win.isDestroyed()) win.destroy();
|
|
||||||
if (cancelled) throw new RuntimeError("pick_cancelled", "已取消框选");
|
if (cancelled) throw new RuntimeError("pick_cancelled", "已取消框选");
|
||||||
if (!picked || picked.rect.width <= 2 || picked.rect.height <= 2) throw new RuntimeError("pick_cancelled", "未完成框选");
|
if (!picked || picked.rect.width <= 2 || picked.rect.height <= 2) throw new RuntimeError("pick_cancelled", "未完成框选");
|
||||||
const { rect, display } = picked;
|
const { rect, display } = picked;
|
||||||
|
// 关键顺序: 先销毁覆盖层、完成截图, 最后才恢复应用窗口——
|
||||||
|
// 之前是先恢复窗口再截图, 应用弹回盖住选区, 截到的全是应用窗口内容
|
||||||
|
for (const { win } of overlays) if (!win.isDestroyed()) win.destroy();
|
||||||
await new Promise(r => setTimeout(r, 300));
|
await new Promise(r => setTimeout(r, 300));
|
||||||
const outDir = path.join(app.getPath("userData"), "region-picks");
|
const outDir = path.join(app.getPath("userData"), "region-picks");
|
||||||
fs.mkdirSync(outDir, { recursive: true });
|
fs.mkdirSync(outDir, { recursive: true });
|
||||||
@@ -781,9 +788,12 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width:
|
|||||||
types: ["screen"],
|
types: ["screen"],
|
||||||
thumbnailSize: { width: Math.round(display.bounds.width * scale), height: Math.round(display.bounds.height * scale) }
|
thumbnailSize: { width: Math.round(display.bounds.width * scale), height: Math.round(display.bounds.height * scale) }
|
||||||
});
|
});
|
||||||
|
const physicalW = Math.round(display.bounds.width * scale);
|
||||||
|
// display_id 匹配 → 缩略图尺寸匹配 → 显示器数量一致时按下→右排序对位
|
||||||
|
const displaysByX = [...displays].sort((a, b) => a.bounds.x - b.bounds.x);
|
||||||
const source = sources.find(item => item.display_id === String(display.id))
|
const source = sources.find(item => item.display_id === String(display.id))
|
||||||
|| sources.find(item => item.thumbnail.getSize().width === Math.round(display.bounds.width * scale))
|
|| sources.find(item => item.thumbnail.getSize().width === physicalW)
|
||||||
|| sources[0];
|
|| (sources.length === displays.length ? sources[displaysByX.findIndex(d => d.id === display.id)] : sources[0]);
|
||||||
if (!source || source.thumbnail.isEmpty()) {
|
if (!source || source.thumbnail.isEmpty()) {
|
||||||
throw new RuntimeError("pick_capture_failed", "框选截图失败:未获取到屏幕画面");
|
throw new RuntimeError("pick_capture_failed", "框选截图失败:未获取到屏幕画面");
|
||||||
}
|
}
|
||||||
@@ -796,6 +806,8 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width:
|
|||||||
const cropW = Math.max(1, Math.min(thumbnail.getSize().width - cropX, Math.round(rect.width * actualScale)));
|
const cropW = Math.max(1, Math.min(thumbnail.getSize().width - cropX, Math.round(rect.width * actualScale)));
|
||||||
const cropH = Math.max(1, Math.min(thumbnail.getSize().height - cropY, Math.round(rect.height * actualScale)));
|
const cropH = Math.max(1, Math.min(thumbnail.getSize().height - cropY, Math.round(rect.height * actualScale)));
|
||||||
fs.writeFileSync(imagePath, thumbnail.crop({ x: cropX, y: cropY, width: cropW, height: cropH }).toPNG());
|
fs.writeFileSync(imagePath, thumbnail.crop({ x: cropX, y: cropY, width: cropW, height: cropH }).toPNG());
|
||||||
|
// 截图完成后才恢复应用窗口
|
||||||
|
for (const w of appWindows) { if (!w.isDestroyed()) { w.show(); w.restore(); w.focus(); } }
|
||||||
// 返回屏幕绝对 DIP 坐标(含负值/副屏偏移)+该屏缩放比, 运行时由此换算点击位置
|
// 返回屏幕绝对 DIP 坐标(含负值/副屏偏移)+该屏缩放比, 运行时由此换算点击位置
|
||||||
return {
|
return {
|
||||||
x: rect.x,
|
x: rect.x,
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户