dsh-mini 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -12
- package/app/HudWindow.cs +4 -1
- package/app/SelectionWatcher.cs +155 -23
- package/app/Service.cs +9 -2
- package/app/bin/DSH/345/260/217/347/252/227.exe +0 -0
- package/lib/client.js +2 -0
- package/lib/index.js +5 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -177,20 +177,29 @@ cd app
|
|
|
177
177
|
在**任何程序**里用鼠标选中一段文字,松开后光标右下方会弹出蓝色小药丸「问大肥鱼」;点它,
|
|
178
178
|
小窗自动弹到最前,选中的内容以引用块(每行 `> ` 前缀)插进输入框,直接接着提问即可。
|
|
179
179
|
|
|
180
|
-
* 实现在 `app/SelectionWatcher.cs
|
|
180
|
+
* 实现在 `app/SelectionWatcher.cs`:全局鼠标轮询(90ms)→ 松开时若拖动距离 ≥6 逻辑像素、
|
|
181
181
|
且不在小窗/药丸自己身上 → 后台线程等 130ms(双击选词等 320ms)→ 校验仍在前台 → 取选区。
|
|
182
|
-
*
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
182
|
+
* **取选区三级阶梯**(`SelectionReader.ReadHit`),从最无侵入的一档开始:
|
|
183
|
+
1. **UI Automation**(`TextPattern`)—— 不碰键盘、不碰剪贴板,还能拿到选区最后一行/一个字的矩形,
|
|
184
|
+
药丸因此能精确贴在它右下角。Notepad、Word、WPS、VS Code、WinForms/WPF、DSH 自己都走这一档。
|
|
185
|
+
2. **`WM_COPY`** —— 发给焦点控件,不发按键。部分老式编辑框只认这条。
|
|
186
|
+
3. **合成一次 `Ctrl+C`** —— 只有前两档都拿不到时才用。**浏览器正文(Chromium)、部分 Electron 应用、
|
|
187
|
+
某些游戏/自绘控件根本没有 `TextPattern` 也没有 `WM_COPY`,这是唯一可行的手段**。
|
|
188
|
+
* **绝不做「备份剪贴板 → 复制 → 还原」**。之前那套是"选中后复制不上"的元凶:还原那一步会把你刚按的
|
|
189
|
+
`Ctrl+C` 覆盖回旧内容。现在第三档会**把选区留在剪贴板里**(你在浏览器里划一下就发现 `Ctrl+V` 有内容了),
|
|
190
|
+
代价是<u>占用</u>剪贴板,而不是<u>破坏</u>它 —— 这是读取 Chromium 选区的唯一代价,取舍写在明处。
|
|
191
|
+
* 设置页的「选区读取:辅助功能读不到时改用复制」**默认打开**;关掉后浏览器正文里就不会再弹按钮,
|
|
192
|
+
但剪贴板绝对不会被碰(适合只用 Office/记事本、或对剪贴板敏感的场景)。
|
|
193
|
+
* 保护:按住 Ctrl/Alt/Shift 时的拖动直接跳过(那是复制/扩选,不是重新选词);终端
|
|
194
|
+
(console/cascadia/mintty/putty)不发按键;资源管理器/桌面/任务栏(Progman/WorkerW/CabinetWClass…)
|
|
195
|
+
一律跳过(否则会真的复制文件、污染 `Win+V` 历史);用户按着修饰键期间完全不介入。
|
|
188
196
|
* 药丸 9 秒无操作自动消失;托盘菜单「划词问大肥鱼」可随时关掉,状态存在
|
|
189
197
|
`%LOCALAPPDATA%\dsh-hud\ask.txt`。
|
|
190
198
|
* 因此**标题条 ✕ 只是收进托盘**(不是退出)——watcher 活在这个进程里,退出了就没法划词。
|
|
191
199
|
彻底退出用托盘菜单「退出」。
|
|
192
|
-
* 诊断:`hud.log` 里 `[pill]` 开头的行会写每次 `kick / skip / read N chars
|
|
193
|
-
|
|
200
|
+
* 诊断:`hud.log` 里 `[pill]` 开头的行会写每次 `kick / skip / read N chars`,以及实际走了哪一档:
|
|
201
|
+
`copy: wm_copy N chars`、`copy: key_chord N chars seq A->B`(`0 chars` 且序号没动,说明那儿没有真选中
|
|
202
|
+
或目标程序忽略了合成按键);往 `%LOCALAPPDATA%\dsh-hud\seltest.txt` 写任意内容可触发一次「只读不弹」的自检。
|
|
194
203
|
|
|
195
204
|
## 常驻划词服务、全局快捷键与设置页
|
|
196
205
|
|
|
@@ -222,6 +231,8 @@ DSH小窗.exe --service --url <tokenized> # Mutex Local\DshHudService,单
|
|
|
222
231
|
主窗「设置 → DSH 小窗」:
|
|
223
232
|
|
|
224
233
|
* 截图自动复制到剪贴板(默认开,整屏/框选都复制);
|
|
234
|
+
* 兼容模式:读不到选区时模拟 `Ctrl+C`(**默认关**。关着时只用系统辅助功能读选区,
|
|
235
|
+
完全不碰剪贴板、不发按键;打开后老式控件才读得到,代价是可能和自己按的 `Ctrl+C` 抢剪贴板);
|
|
225
236
|
* 全屏截图快捷键、框选截图快捷键(失焦即保存)。
|
|
226
237
|
|
|
227
238
|
配置落在 `%LOCALAPPDATA%\dsh-hud\config.ini`(`key=value`,C# 壳、宿主插件、客户端三边都读),
|
|
@@ -256,9 +267,12 @@ cd ..; npm pack # → dsh-mini-0.1.0.tgz(约 600 KB)
|
|
|
256
267
|
* 截图只有「整屏」和「框选/吸附窗口」两种,没有滚动长截图;框选吸附依赖窗口标题非空,
|
|
257
268
|
无标题窗口(部分全屏游戏、自绘界面)会退化成「该点所在的整块屏幕」。
|
|
258
269
|
(滚动长截图评估过:要拼接滚动帧 + 去重 + 处理懒加载,工作量明显大于本轮其它几项,先没做。)
|
|
259
|
-
*
|
|
260
|
-
|
|
261
|
-
|
|
270
|
+
* 划词按三级阶梯取选区:UIA `TextPattern`(无侵入、带选区矩形)→ `WM_COPY`(发消息、不发按键)→
|
|
271
|
+
合成一次 `Ctrl+C`。**Chromium 正文没有 `TextPattern`、也不响应 `WM_COPY`**(我实测过 UIA 与
|
|
272
|
+
IAccessible2 两条路都拿不到选区),所以浏览器里必然走到第三档,会占用剪贴板;不想被碰可在设置页关掉
|
|
273
|
+
「选区读取:辅助功能读不到时改用复制」。终端(console/cascadia/mintty/putty)和桌面/资源管理器窗口
|
|
274
|
+
直接跳过,避免 Ctrl+C 变成 SIGINT 或真的复制文件。**任何情况都不再还原旧剪贴板内容**——那正是以前
|
|
275
|
+
「选中后 Ctrl+C 复制不上」的根因。
|
|
262
276
|
* 主题色与小窗边框:导航完成后同步两次,另外每 6 秒复查一次页面背景色;
|
|
263
277
|
跨显示器拖到不同 DPI 的屏会收到 `WM_DPICHANGED` 并重算标题条/图标/最小尺寸。
|
|
264
278
|
* 标题条图标是 `Segoe MDL2 Assets` 字体,字号偏小(E8BB 关闭键取 8.5f 才和其它图标一样大)。
|
package/app/HudWindow.cs
CHANGED
|
@@ -228,6 +228,7 @@ namespace DshHud
|
|
|
228
228
|
|
|
229
229
|
config = HudConfig.Load();
|
|
230
230
|
configStamp = HudConfig.Stamp();
|
|
231
|
+
SelectionReader.ClipboardFallback = config.TextFallback;
|
|
231
232
|
}
|
|
232
233
|
|
|
233
234
|
private Size Logical(int w, int h)
|
|
@@ -567,7 +568,9 @@ namespace DshHud
|
|
|
567
568
|
{
|
|
568
569
|
configStamp = stamp;
|
|
569
570
|
config = HudConfig.Load();
|
|
570
|
-
|
|
571
|
+
SelectionReader.ClipboardFallback = config.TextFallback;
|
|
572
|
+
Log("config reloaded copyShot=" + config.CopyShot +
|
|
573
|
+
" textFallback=" + (config.TextFallback ? "1" : "0"));
|
|
571
574
|
}
|
|
572
575
|
|
|
573
576
|
string quote = HudConfig.QuotePath;
|
package/app/SelectionWatcher.cs
CHANGED
|
@@ -52,15 +52,92 @@ namespace DshHud
|
|
|
52
52
|
|
|
53
53
|
internal const uint GA_ROOT = 2;
|
|
54
54
|
internal const int VK_LBUTTON = 0x01;
|
|
55
|
-
internal const
|
|
55
|
+
internal const int VK_SHIFT = 0x10;
|
|
56
|
+
internal const int VK_CONTROL = 0x11;
|
|
57
|
+
internal const int VK_MENU = 0x12;
|
|
56
58
|
internal const byte VK_C = 0x43;
|
|
57
59
|
internal const uint KEYEVENTF_KEYUP = 0x0002;
|
|
58
60
|
|
|
61
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
62
|
+
internal struct GUITHREADINFO
|
|
63
|
+
{
|
|
64
|
+
public int cbSize;
|
|
65
|
+
public uint flags;
|
|
66
|
+
public IntPtr hwndActive;
|
|
67
|
+
public IntPtr hwndFocus;
|
|
68
|
+
public IntPtr hwndCapture;
|
|
69
|
+
public IntPtr hwndMenuOwner;
|
|
70
|
+
public IntPtr hwndMoveSize;
|
|
71
|
+
public IntPtr hwndCaret;
|
|
72
|
+
public RECT rcCaret;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
76
|
+
internal struct RECT
|
|
77
|
+
{
|
|
78
|
+
public int left;
|
|
79
|
+
public int top;
|
|
80
|
+
public int right;
|
|
81
|
+
public int bottom;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
[DllImport("user32.dll")]
|
|
85
|
+
internal static extern bool GetGUIThreadInfo(uint idThread, ref GUITHREADINFO info);
|
|
86
|
+
|
|
87
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
|
88
|
+
internal static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint msg, IntPtr wParam,
|
|
89
|
+
IntPtr lParam, uint flags, uint timeout, out IntPtr result);
|
|
90
|
+
|
|
91
|
+
internal const uint WM_COPY = 0x0301;
|
|
92
|
+
internal const uint SMTO_ABORTIFHUNG = 0x0002;
|
|
93
|
+
|
|
59
94
|
internal static bool IsMouseDown()
|
|
60
95
|
{
|
|
61
96
|
return (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0;
|
|
62
97
|
}
|
|
63
98
|
|
|
99
|
+
internal static bool IsKeyDown(int vk)
|
|
100
|
+
{
|
|
101
|
+
return (GetAsyncKeyState(vk) & 0x8000) != 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// <summary>
|
|
105
|
+
/// True while the user holds Ctrl/Alt/Shift himself. Every probe is skipped then:
|
|
106
|
+
/// injecting our own Ctrl+C on top of a real one is what made the user's copies
|
|
107
|
+
/// land empty, and it can also cancel a drag-copy in Explorer.
|
|
108
|
+
/// </summary>
|
|
109
|
+
internal static bool IsModifierDown()
|
|
110
|
+
{
|
|
111
|
+
return IsKeyDown(VK_CONTROL) || IsKeyDown(VK_MENU) || IsKeyDown(VK_SHIFT);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// <summary>
|
|
115
|
+
/// The control that currently has the keyboard focus. WM_COPY has to be delivered to
|
|
116
|
+
/// the edit control, not the top-level window.
|
|
117
|
+
/// </summary>
|
|
118
|
+
internal static IntPtr FocusedControl()
|
|
119
|
+
{
|
|
120
|
+
try
|
|
121
|
+
{
|
|
122
|
+
IntPtr fg = GetForegroundWindow();
|
|
123
|
+
if (fg == IntPtr.Zero) return IntPtr.Zero;
|
|
124
|
+
uint pid;
|
|
125
|
+
uint tid = GetWindowThreadProcessId(fg, out pid);
|
|
126
|
+
GUITHREADINFO gti = new GUITHREADINFO();
|
|
127
|
+
gti.cbSize = Marshal.SizeOf(typeof(GUITHREADINFO));
|
|
128
|
+
if (GetGUIThreadInfo(tid, ref gti))
|
|
129
|
+
{
|
|
130
|
+
if (gti.hwndFocus != IntPtr.Zero) return gti.hwndFocus;
|
|
131
|
+
if (gti.hwndActive != IntPtr.Zero) return gti.hwndActive;
|
|
132
|
+
}
|
|
133
|
+
return fg;
|
|
134
|
+
}
|
|
135
|
+
catch
|
|
136
|
+
{
|
|
137
|
+
return IntPtr.Zero;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
64
141
|
/// <summary>
|
|
65
142
|
/// Terminals map Ctrl+C to an interrupt rather than to "copy", so the clipboard
|
|
66
143
|
/// fallback must never fire over one of them.
|
|
@@ -187,6 +264,28 @@ namespace DshHud
|
|
|
187
264
|
return false;
|
|
188
265
|
}
|
|
189
266
|
|
|
267
|
+
/// <summary>
|
|
268
|
+
/// Ask the focused control to copy through WM_COPY. This is the polite fallback:
|
|
269
|
+
/// no keystrokes are synthesised, so a real Ctrl+C pressed by the user is never
|
|
270
|
+
/// raced or cancelled, and nothing is written back over the clipboard.
|
|
271
|
+
/// </summary>
|
|
272
|
+
internal static bool SendCopyMessage()
|
|
273
|
+
{
|
|
274
|
+
try
|
|
275
|
+
{
|
|
276
|
+
IntPtr target = FocusedControl();
|
|
277
|
+
if (target == IntPtr.Zero) return false;
|
|
278
|
+
IntPtr result;
|
|
279
|
+
SendMessageTimeout(target, WM_COPY, IntPtr.Zero, IntPtr.Zero,
|
|
280
|
+
SMTO_ABORTIFHUNG, 300, out result);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
catch
|
|
284
|
+
{
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
190
289
|
internal static void SendCopy()
|
|
191
290
|
{
|
|
192
291
|
keybd_event(VK_CONTROL, 0, 0, UIntPtr.Zero);
|
|
@@ -218,6 +317,19 @@ namespace DshHud
|
|
|
218
317
|
|
|
219
318
|
internal static class SelectionReader
|
|
220
319
|
{
|
|
320
|
+
/// <summary>
|
|
321
|
+
/// On by default, but only ever consulted after UIA came back empty — so modern text
|
|
322
|
+
/// surfaces (Notepad, WinForms, WPF, Office, the browser's own chrome) never touch the
|
|
323
|
+
/// clipboard. Chromium content and legacy controls expose no TextPattern at all, and for
|
|
324
|
+
/// those a copy is the only way to read the selection.
|
|
325
|
+
///
|
|
326
|
+
/// Crucially the copy is *not* undone: the old implementation restored the previous
|
|
327
|
+
/// clipboard contents afterwards, and that restore raced the user's own Ctrl+C and
|
|
328
|
+
/// overwrote what they had just copied. Losing the previous clipboard contents is the
|
|
329
|
+
/// price of reading a selection here; breaking Ctrl+C is not.
|
|
330
|
+
/// </summary>
|
|
331
|
+
internal static bool ClipboardFallback = true;
|
|
332
|
+
|
|
221
333
|
/// <summary>
|
|
222
334
|
/// Read the current selection. UIA first: it is passive (no keystrokes, no clipboard
|
|
223
335
|
/// churn) and it also tells us where the selection ends, which is what the pill
|
|
@@ -275,7 +387,7 @@ namespace DshHud
|
|
|
275
387
|
{
|
|
276
388
|
}
|
|
277
389
|
|
|
278
|
-
hit.Text = Read();
|
|
390
|
+
if (ClipboardFallback) hit.Text = Read();
|
|
279
391
|
return hit;
|
|
280
392
|
}
|
|
281
393
|
|
|
@@ -338,16 +450,30 @@ namespace DshHud
|
|
|
338
450
|
|
|
339
451
|
private static string ReadOnStaThread()
|
|
340
452
|
{
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
453
|
+
// No clipboard backup/restore any more. Restoring was the actual damage: while
|
|
454
|
+
// we waited for our own copy to land, the user's real Ctrl+C could complete
|
|
455
|
+
// first, and our restore then threw that copy away ("复制不上"). Now the worst
|
|
456
|
+
// case is that the selection text stays on the clipboard — which is exactly
|
|
457
|
+
// what the user just asked for anyway.
|
|
458
|
+
if (ShellWin.IsModifierDown()) return "";
|
|
459
|
+
|
|
460
|
+
uint seq = ShellWin.GetClipboardSequenceNumber();
|
|
461
|
+
// WM_COPY is best effort only: Win32 edits answer it, Chromium and most
|
|
462
|
+
// modern renderers ignore it, and SendMessageTimeout also returns false
|
|
463
|
+
// when the target window is busy. Never treat "no answer" as "no selection".
|
|
464
|
+
ShellWin.SendCopyMessage();
|
|
465
|
+
|
|
466
|
+
string text = WaitForClipboardText(seq, 320);
|
|
467
|
+
if (!string.IsNullOrEmpty(text))
|
|
347
468
|
{
|
|
469
|
+
SelectionWatcher.Log("copy: wm_copy " + text.Length + " chars");
|
|
470
|
+
return Finish(text);
|
|
348
471
|
}
|
|
349
472
|
|
|
350
|
-
|
|
473
|
+
// A real key chord is the only thing that works for Chromium content and
|
|
474
|
+
// legacy controls. Only when the user has opted in, and never while they
|
|
475
|
+
// are holding a modifier themselves.
|
|
476
|
+
if (!ClipboardFallback || ShellWin.IsModifierDown()) return "";
|
|
351
477
|
try
|
|
352
478
|
{
|
|
353
479
|
ShellWin.SendCopy();
|
|
@@ -356,32 +482,35 @@ namespace DshHud
|
|
|
356
482
|
{
|
|
357
483
|
return "";
|
|
358
484
|
}
|
|
485
|
+
text = WaitForClipboardText(seq, 700);
|
|
486
|
+
SelectionWatcher.Log("copy: key_chord " + (text == null ? 0 : text.Length) +
|
|
487
|
+
" chars seq " + seq + "->" + ShellWin.GetClipboardSequenceNumber());
|
|
488
|
+
return string.IsNullOrEmpty(text) ? "" : Finish(text);
|
|
489
|
+
}
|
|
359
490
|
|
|
360
|
-
|
|
491
|
+
/// <summary>Wait until the clipboard sequence number moves, then read its text.</summary>
|
|
492
|
+
private static string WaitForClipboardText(uint seq, int budgetMs)
|
|
493
|
+
{
|
|
361
494
|
int waited = 0;
|
|
362
|
-
while (waited <
|
|
495
|
+
while (waited < budgetMs)
|
|
363
496
|
{
|
|
364
|
-
Thread.Sleep(
|
|
365
|
-
waited +=
|
|
497
|
+
Thread.Sleep(25);
|
|
498
|
+
waited += 25;
|
|
366
499
|
if (ShellWin.GetClipboardSequenceNumber() == seq) continue;
|
|
367
500
|
try
|
|
368
501
|
{
|
|
369
|
-
if (Clipboard.ContainsText())
|
|
502
|
+
if (Clipboard.ContainsText()) return Clipboard.GetText();
|
|
370
503
|
}
|
|
371
504
|
catch
|
|
372
505
|
{
|
|
373
506
|
}
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
try
|
|
378
|
-
{
|
|
379
|
-
if (backup != null) Clipboard.SetDataObject(backup, false);
|
|
380
|
-
}
|
|
381
|
-
catch
|
|
382
|
-
{
|
|
507
|
+
return "";
|
|
383
508
|
}
|
|
509
|
+
return "";
|
|
510
|
+
}
|
|
384
511
|
|
|
512
|
+
private static string Finish(string text)
|
|
513
|
+
{
|
|
385
514
|
if (string.IsNullOrEmpty(text)) return "";
|
|
386
515
|
text = text.Replace("\r\n", "\n").Replace('\r', '\n').Trim();
|
|
387
516
|
if (text.Length == 0) return "";
|
|
@@ -748,6 +877,9 @@ namespace DshHud
|
|
|
748
877
|
private void Kick(Point pt, int delay)
|
|
749
878
|
{
|
|
750
879
|
if (!enabled) { Log("skip: disabled"); return; }
|
|
880
|
+
// A drag with Ctrl/Shift/Alt held is a copy or an extension, not a fresh
|
|
881
|
+
// selection: probing it races the user's own shortcut.
|
|
882
|
+
if (ShellWin.IsModifierDown()) { Log("skip: modifier down"); return; }
|
|
751
883
|
|
|
752
884
|
if (ShellWin.IsShellSurfaceAt(pt)) { Log("skip: shell surface"); return; }
|
|
753
885
|
int id = ++probeId;
|
package/app/Service.cs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
using System;
|
|
2
2
|
using System.Diagnostics;
|
|
3
3
|
using System.Drawing;
|
|
4
4
|
using System.IO;
|
|
@@ -19,6 +19,7 @@ namespace DshHud
|
|
|
19
19
|
internal bool Ask;
|
|
20
20
|
internal string HotkeyFull;
|
|
21
21
|
internal string HotkeyRegion;
|
|
22
|
+
internal bool TextFallback;
|
|
22
23
|
|
|
23
24
|
internal HudConfig()
|
|
24
25
|
{
|
|
@@ -26,6 +27,7 @@ namespace DshHud
|
|
|
26
27
|
Ask = true;
|
|
27
28
|
HotkeyFull = "Ctrl+Shift+1";
|
|
28
29
|
HotkeyRegion = "Ctrl+Shift+2";
|
|
30
|
+
TextFallback = true;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
internal static string Dir
|
|
@@ -61,6 +63,7 @@ namespace DshHud
|
|
|
61
63
|
else if (key == "ask") cfg.Ask = val != "0";
|
|
62
64
|
else if (key == "hotkeyFull") cfg.HotkeyFull = val;
|
|
63
65
|
else if (key == "hotkeyRegion") cfg.HotkeyRegion = val;
|
|
66
|
+
else if (key == "textFallback") cfg.TextFallback = val != "0";
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
catch
|
|
@@ -79,6 +82,7 @@ namespace DshHud
|
|
|
79
82
|
sb.AppendLine("ask=" + (Ask ? "1" : "0"));
|
|
80
83
|
sb.AppendLine("hotkeyFull=" + HotkeyFull);
|
|
81
84
|
sb.AppendLine("hotkeyRegion=" + HotkeyRegion);
|
|
85
|
+
sb.AppendLine("textFallback=" + (TextFallback ? "1" : "0"));
|
|
82
86
|
File.WriteAllText(ConfigPath, sb.ToString(), Encoding.UTF8);
|
|
83
87
|
}
|
|
84
88
|
catch
|
|
@@ -250,6 +254,7 @@ namespace DshHud
|
|
|
250
254
|
watcher = new SelectionWatcher(host);
|
|
251
255
|
watcher.QuoteRequested += DeliverQuote;
|
|
252
256
|
watcher.Enabled = config.Ask;
|
|
257
|
+
SelectionReader.ClipboardFallback = config.TextFallback;
|
|
253
258
|
|
|
254
259
|
tray = new NotifyIcon();
|
|
255
260
|
tray.Icon = SystemIcons.Application;
|
|
@@ -313,9 +318,11 @@ namespace DshHud
|
|
|
313
318
|
config = HudConfig.Load();
|
|
314
319
|
watcher.Enabled = config.Ask;
|
|
315
320
|
askItem.Checked = config.Ask;
|
|
321
|
+
SelectionReader.ClipboardFallback = config.TextFallback;
|
|
316
322
|
host.Bind(config);
|
|
317
323
|
SelectionWatcher.Log("config reloaded: hotkeyFull=" + config.HotkeyFull +
|
|
318
|
-
" hotkeyRegion=" + config.HotkeyRegion
|
|
324
|
+
" hotkeyRegion=" + config.HotkeyRegion +
|
|
325
|
+
" textFallback=" + (config.TextFallback ? "1" : "0"));
|
|
319
326
|
}
|
|
320
327
|
|
|
321
328
|
private void OnHotkey(string kind)
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -551,6 +551,8 @@ window.__ModuleLoader__.load({
|
|
|
551
551
|
react.createElement('h3', { style: { fontSize: '14px', margin: '0 0 4px' } }, '小窗与截图'),
|
|
552
552
|
react.createElement('p', { style: hintStyle }, '设置对原生小窗和后台划词服务同时生效,保存后约 3 秒内应用。'),
|
|
553
553
|
checkbox('copyShot', '截图自动复制到剪贴板', '全屏、框选两种截图都会复制'),
|
|
554
|
+
checkbox('textFallback', '选区读取:辅助功能读不到时改用复制',
|
|
555
|
+
'默认打开。先走系统辅助功能(不发按键、不碰剪贴板);浏览器正文、老控件没有这个接口,只能复制一次来读选区 —— 会占用剪贴板,但不会再还原,所以不会覆盖你刚按的 Ctrl+C。关掉后这些地方就不出按钮。'),
|
|
554
556
|
hotkey('hotkeyFull', '全屏截图快捷键'),
|
|
555
557
|
hotkey('hotkeyRegion', '框选截图快捷键'),
|
|
556
558
|
react.createElement('p', { style: hintStyle }, '写法:Ctrl+Shift+1、Ctrl+Shift+S、Alt+PrintScreen、F8 等。组合里必须带 Ctrl/Alt/Shift/Win 之一。'),
|
package/lib/index.js
CHANGED
|
@@ -202,7 +202,7 @@ function advertise(ctx) {
|
|
|
202
202
|
|
|
203
203
|
const CONFIG_PATH = join(WORK_DIR, 'config.ini')
|
|
204
204
|
|
|
205
|
-
const CONFIG_DEFAULTS = { copyShot: true, ask: true, hotkeyFull: 'Ctrl+Shift+1', hotkeyRegion: 'Ctrl+Shift+2' }
|
|
205
|
+
const CONFIG_DEFAULTS = { copyShot: true, ask: true, textFallback: true, hotkeyFull: 'Ctrl+Shift+1', hotkeyRegion: 'Ctrl+Shift+2' }
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
208
|
* Settings live in a flat `key=value` file because the window shell (C# 5) and this
|
|
@@ -219,7 +219,7 @@ function readConfig() {
|
|
|
219
219
|
if (eq <= 0) continue
|
|
220
220
|
const key = line.slice(0, eq).trim()
|
|
221
221
|
const value = line.slice(eq + 1).trim()
|
|
222
|
-
if (key === 'copyShot' || key === 'ask') config[key] = value !== '0'
|
|
222
|
+
if (key === 'copyShot' || key === 'ask' || key === 'textFallback') config[key] = value !== '0'
|
|
223
223
|
else if (key === 'hotkeyFull' || key === 'hotkeyRegion') config[key] = value
|
|
224
224
|
}
|
|
225
225
|
} catch {
|
|
@@ -235,6 +235,7 @@ function writeConfig(config) {
|
|
|
235
235
|
`copyShot=${config.copyShot ? '1' : '0'}`,
|
|
236
236
|
`ask=${config.ask ? '1' : '0'}`,
|
|
237
237
|
`hotkeyFull=${config.hotkeyFull}`,
|
|
238
|
+
`textFallback=${config.textFallback ? '1' : '0'}`,
|
|
238
239
|
`hotkeyRegion=${config.hotkeyRegion}`,
|
|
239
240
|
]
|
|
240
241
|
writeFileSync(CONFIG_PATH, lines.join('\n') + '\n', 'utf8')
|
|
@@ -326,12 +327,13 @@ export function apply(ctx, config = {}) {
|
|
|
326
327
|
let body
|
|
327
328
|
try { body = await readJson(req) } catch (error) { sendJson(res, 400, { error: error.message }); return }
|
|
328
329
|
const next = readConfig()
|
|
329
|
-
for (const key of ['copyShot', 'ask', 'hotkeyFull', 'hotkeyRegion']) {
|
|
330
|
+
for (const key of ['copyShot', 'ask', 'textFallback', 'hotkeyFull', 'hotkeyRegion']) {
|
|
330
331
|
if (Object.prototype.hasOwnProperty.call(body, key)) next[key] = body[key]
|
|
331
332
|
}
|
|
332
333
|
// Normalize before writing: the shell matches these strings exactly when it
|
|
333
334
|
// registers the global hotkeys.
|
|
334
335
|
next.copyShot = Boolean(next.copyShot)
|
|
336
|
+
next.textFallback = Boolean(next.textFallback)
|
|
335
337
|
next.ask = Boolean(next.ask)
|
|
336
338
|
next.hotkeyFull = String(next.hotkeyFull ?? '').trim() || CONFIG_DEFAULTS.hotkeyFull
|
|
337
339
|
next.hotkeyRegion = String(next.hotkeyRegion ?? '').trim() || CONFIG_DEFAULTS.hotkeyRegion
|