dsh-mini 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,850 @@
1
+ using System;
2
+ using System.Drawing;
3
+ using System.Drawing.Drawing2D;
4
+ using System.Runtime.InteropServices;
5
+ using System.Text;
6
+ using System.Threading;
7
+ using System.Windows.Automation;
8
+ using System.Windows.Automation.Text;
9
+ using System.Windows.Forms;
10
+
11
+ namespace DshHud
12
+ {
13
+ /// <summary>Small Win32 surface used by the selection watcher.</summary>
14
+ internal static class ShellWin
15
+ {
16
+ [StructLayout(LayoutKind.Sequential)]
17
+ internal struct POINT
18
+ {
19
+ public int X;
20
+ public int Y;
21
+
22
+ internal POINT(int x, int y)
23
+ {
24
+ X = x;
25
+ Y = y;
26
+ }
27
+ }
28
+
29
+ [DllImport("user32.dll")]
30
+ internal static extern short GetAsyncKeyState(int vKey);
31
+
32
+ [DllImport("user32.dll")]
33
+ internal static extern IntPtr WindowFromPoint(POINT p);
34
+
35
+ [DllImport("user32.dll")]
36
+ internal static extern IntPtr GetForegroundWindow();
37
+
38
+ [DllImport("user32.dll")]
39
+ internal static extern IntPtr GetAncestor(IntPtr hWnd, uint flags);
40
+
41
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
42
+ internal static extern int GetClassName(IntPtr hWnd, StringBuilder sb, int max);
43
+
44
+ [DllImport("user32.dll")]
45
+ internal static extern void keybd_event(byte vk, byte scan, uint flags, UIntPtr extra);
46
+
47
+ [DllImport("user32.dll")]
48
+ internal static extern uint GetClipboardSequenceNumber();
49
+
50
+ [DllImport("user32.dll")]
51
+ internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
52
+
53
+ internal const uint GA_ROOT = 2;
54
+ internal const int VK_LBUTTON = 0x01;
55
+ internal const byte VK_CONTROL = 0x11;
56
+ internal const byte VK_C = 0x43;
57
+ internal const uint KEYEVENTF_KEYUP = 0x0002;
58
+
59
+ internal static bool IsMouseDown()
60
+ {
61
+ return (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0;
62
+ }
63
+
64
+ /// <summary>
65
+ /// Terminals map Ctrl+C to an interrupt rather than to "copy", so the clipboard
66
+ /// fallback must never fire over one of them.
67
+ /// </summary>
68
+ internal static bool IsTerminalAt(Point p)
69
+ {
70
+ try
71
+ {
72
+ IntPtr h = WindowFromPoint(new POINT(p.X, p.Y));
73
+ if (h == IntPtr.Zero) return false;
74
+ StringBuilder sb = new StringBuilder(256);
75
+ if (GetClassName(h, sb, sb.Capacity) <= 0) return false;
76
+ string cls = sb.ToString().ToLowerInvariant();
77
+ return cls.IndexOf("console") >= 0
78
+ || cls.IndexOf("cascadia") >= 0
79
+ || cls.IndexOf("mintty") >= 0
80
+ || cls.IndexOf("putty") >= 0
81
+ || cls.IndexOf("terminal") >= 0;
82
+ }
83
+ catch
84
+ {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /// <summary>Same terminal test as <see cref="IsTerminalAt"/> but for a known window handle.</summary>
90
+ internal static bool IsTerminalWindow(IntPtr h)
91
+ {
92
+ try
93
+ {
94
+ if (h == IntPtr.Zero) return false;
95
+ StringBuilder sb = new StringBuilder(256);
96
+ if (GetClassName(h, sb, sb.Capacity) <= 0) return false;
97
+ string cls = sb.ToString().ToLowerInvariant();
98
+ return cls.IndexOf("console") >= 0
99
+ || cls.IndexOf("cascadia") >= 0
100
+ || cls.IndexOf("mintty") >= 0
101
+ || cls.IndexOf("putty") >= 0
102
+ || cls.IndexOf("terminal") >= 0;
103
+ }
104
+ catch
105
+ {
106
+ return false;
107
+ }
108
+ }
109
+ internal static IntPtr RootWindowAt(Point p)
110
+ {
111
+ try
112
+ {
113
+ return GetAncestor(WindowFromPoint(new POINT(p.X, p.Y)), GA_ROOT);
114
+ }
115
+ catch
116
+ {
117
+ return IntPtr.Zero;
118
+ }
119
+ }
120
+
121
+ /// <summary>
122
+ /// Explorer, the desktop and the taskbar read Ctrl+C as "copy the selected file",
123
+ /// so probing them would litter the clipboard history with file copies. Those
124
+ /// surfaces are skipped outright.
125
+ /// </summary>
126
+ internal static bool IsShellSurfaceAt(Point p)
127
+ {
128
+ try
129
+ {
130
+ IntPtr root = RootWindowAt(p);
131
+ if (root == IntPtr.Zero) return true;
132
+ StringBuilder sb = new StringBuilder(256);
133
+ if (GetClassName(root, sb, sb.Capacity) <= 0) return false;
134
+ string cls = sb.ToString().ToLowerInvariant();
135
+ return cls == "progman"
136
+ || cls == "workerw"
137
+ || cls == "shell_traywnd"
138
+ || cls == "shell_secondarytraywnd"
139
+ || cls == "cabinetwclass"
140
+ || cls == "explorewclass"
141
+ || cls == "desktopwindowxamlsource";
142
+ }
143
+ catch
144
+ {
145
+ return false;
146
+ }
147
+ }
148
+
149
+ /// <summary>
150
+ /// Game clients (Unity/Unreal shells) paint their own UI and expose no text, so the
151
+ /// Ctrl+C fallback below would be delivered to them as a stray "C". In 原神 "C" is the
152
+ /// character screen, so a double-click anywhere on the game silently opened it. Game
153
+ /// surfaces are never probed: there is no selection to read there anyway.
154
+ /// </summary>
155
+ internal static bool IsGameWindow(IntPtr h)
156
+ {
157
+ try
158
+ {
159
+ if (h == IntPtr.Zero) return false;
160
+ IntPtr root = GetAncestor(h, GA_ROOT);
161
+ if (root == IntPtr.Zero) root = h;
162
+
163
+ StringBuilder sb = new StringBuilder(256);
164
+ if (GetClassName(root, sb, sb.Capacity) > 0)
165
+ {
166
+ string cls = sb.ToString().ToLowerInvariant();
167
+ if (cls == "unitywndclass" || cls == "unrealwindow") return true;
168
+ }
169
+
170
+ uint pid;
171
+ GetWindowThreadProcessId(root, out pid);
172
+ if (pid != 0)
173
+ {
174
+ string name = "";
175
+ try { name = System.Diagnostics.Process.GetProcessById((int)pid).ProcessName.ToLowerInvariant(); }
176
+ catch { }
177
+ string[] games = { "yuanshen", "genshinimpact", "starrail", "zenlesszonezero", "bh3" };
178
+ foreach (string g in games)
179
+ {
180
+ if (name.IndexOf(g) >= 0) return true;
181
+ }
182
+ }
183
+ }
184
+ catch
185
+ {
186
+ }
187
+ return false;
188
+ }
189
+
190
+ internal static void SendCopy()
191
+ {
192
+ keybd_event(VK_CONTROL, 0, 0, UIntPtr.Zero);
193
+ keybd_event(VK_C, 0, 0, UIntPtr.Zero);
194
+ keybd_event(VK_C, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
195
+ keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
196
+ }
197
+ }
198
+
199
+ /// <summary>
200
+ /// Grabs the text of a live selection. Windows has no global "give me the selection"
201
+ /// API, so the shell sends Ctrl+C, watches the clipboard sequence number to know when
202
+ /// the copy actually landed, then puts the previous clipboard back.
203
+ /// </summary>
204
+ /// <summary>What was selected and where its last line sits (physical screen pixels).</summary>
205
+ internal struct SelectionHit
206
+ {
207
+ internal string Text;
208
+ internal Rectangle Rect;
209
+ internal bool HasRect;
210
+
211
+ /// <summary>Where to hang the pill when we have no real rectangle.</summary>
212
+ internal Rectangle AnchorPoint(Point fallback)
213
+ {
214
+ if (HasRect) return Rect;
215
+ return new Rectangle(fallback.X, fallback.Y, 0, 0);
216
+ }
217
+ }
218
+
219
+ internal static class SelectionReader
220
+ {
221
+ /// <summary>
222
+ /// Read the current selection. UIA first: it is passive (no keystrokes, no clipboard
223
+ /// churn) and it also tells us where the selection ends, which is what the pill
224
+ /// anchors to. Ctrl+C is only the fallback for apps that do not expose TextPattern.
225
+ ///
226
+ /// `hwnd` is the window the user dragged in. Targeting it beats asking for the
227
+ /// focused element: the helper process often is not the foreground app by the
228
+ /// time we look, and a window can keep a selection while unfocused.
229
+ /// </summary>
230
+ internal static SelectionHit ReadHit(IntPtr hwnd)
231
+ {
232
+ SelectionHit hit = new SelectionHit();
233
+ hit.Text = "";
234
+
235
+ try
236
+ {
237
+ AutomationElement el = hwnd != IntPtr.Zero ? AutomationElement.FromHandle(hwnd) : null;
238
+ if (el == null) el = AutomationElement.FocusedElement;
239
+ AutomationElement text = FindTextElement(el, 0);
240
+ if (text != null)
241
+ {
242
+ object pattern;
243
+ if (text.TryGetCurrentPattern(TextPattern.Pattern, out pattern))
244
+ {
245
+ TextPattern tp = (TextPattern)pattern;
246
+ TextPatternRange[] ranges = tp.GetSelection();
247
+ if (ranges != null && ranges.Length > 0)
248
+ {
249
+ string text2 = ranges[0].GetText(-1);
250
+ if (!string.IsNullOrEmpty(text2))
251
+ {
252
+ hit.Text = Normalize(text2);
253
+ // UIA hands back one rect per line in physical screen pixels;
254
+ // the last one is the end of the selection, which is where
255
+ // the pill wants to sit.
256
+ System.Windows.Rect[] rects = ranges[0].GetBoundingRectangles();
257
+ if (rects != null && rects.Length > 0)
258
+ {
259
+ System.Windows.Rect last = rects[rects.Length - 1];
260
+ if (last.Width > 0.5 && last.Height > 0.5)
261
+ {
262
+ hit.Rect = Rectangle.Round(new RectangleF(
263
+ (float)last.X, (float)last.Y,
264
+ (float)last.Width, (float)last.Height));
265
+ hit.HasRect = true;
266
+ }
267
+ }
268
+ return hit;
269
+ }
270
+ }
271
+ }
272
+ }
273
+ }
274
+ catch
275
+ {
276
+ }
277
+
278
+ hit.Text = Read();
279
+ return hit;
280
+ }
281
+
282
+ internal static SelectionHit ReadHit()
283
+ {
284
+ return ReadHit(IntPtr.Zero);
285
+ }
286
+
287
+ /// <summary>
288
+ /// Breadth-first walk for the first element that supports TextPattern. Editors are
289
+ /// usually one or two levels below the top window (Notepad: window → pane →
290
+ /// document), so a shallow bound is enough and keeps this fast.
291
+ /// </summary>
292
+ private static AutomationElement FindTextElement(AutomationElement root, int depth)
293
+ {
294
+ if (root == null || depth > 4) return null;
295
+ try
296
+ {
297
+ object pattern;
298
+ if (root.TryGetCurrentPattern(TextPattern.Pattern, out pattern)) return root;
299
+
300
+ TreeWalker walker = TreeWalker.RawViewWalker;
301
+ AutomationElement child = walker.GetFirstChild(root);
302
+ int guard = 0;
303
+ while (child != null && guard < 40)
304
+ {
305
+ guard += 1;
306
+ AutomationElement found = FindTextElement(child, depth + 1);
307
+ if (found != null) return found;
308
+ child = walker.GetNextSibling(child);
309
+ }
310
+ }
311
+ catch
312
+ {
313
+ }
314
+ return null;
315
+ }
316
+
317
+ private static string Normalize(string text)
318
+ {
319
+ if (string.IsNullOrEmpty(text)) return "";
320
+ text = text.Replace("\r\n", "\n").Replace('\r', '\n').Trim();
321
+ if (text.Length > 4000) text = text.Substring(0, 4000);
322
+ return text;
323
+ }
324
+
325
+ internal static string Read()
326
+ {
327
+ string result = "";
328
+ Thread worker = new Thread(delegate()
329
+ {
330
+ result = ReadOnStaThread();
331
+ });
332
+ worker.SetApartmentState(ApartmentState.STA);
333
+ worker.IsBackground = true;
334
+ worker.Start();
335
+ if (!worker.Join(2000)) return "";
336
+ return result;
337
+ }
338
+
339
+ private static string ReadOnStaThread()
340
+ {
341
+ IDataObject backup = null;
342
+ try
343
+ {
344
+ backup = Clipboard.GetDataObject();
345
+ }
346
+ catch
347
+ {
348
+ }
349
+
350
+ uint seq = ShellWin.GetClipboardSequenceNumber();
351
+ try
352
+ {
353
+ ShellWin.SendCopy();
354
+ }
355
+ catch
356
+ {
357
+ return "";
358
+ }
359
+
360
+ string text = null;
361
+ int waited = 0;
362
+ while (waited < 900)
363
+ {
364
+ Thread.Sleep(30);
365
+ waited += 30;
366
+ if (ShellWin.GetClipboardSequenceNumber() == seq) continue;
367
+ try
368
+ {
369
+ if (Clipboard.ContainsText()) text = Clipboard.GetText();
370
+ }
371
+ catch
372
+ {
373
+ }
374
+ break;
375
+ }
376
+
377
+ try
378
+ {
379
+ if (backup != null) Clipboard.SetDataObject(backup, false);
380
+ }
381
+ catch
382
+ {
383
+ }
384
+
385
+ if (string.IsNullOrEmpty(text)) return "";
386
+ text = text.Replace("\r\n", "\n").Replace('\r', '\n').Trim();
387
+ if (text.Length == 0) return "";
388
+ if (text.Length > 4000) text = text.Substring(0, 4000);
389
+ return text;
390
+ }
391
+ }
392
+
393
+ /// <summary>The little pill that follows a selection: "问大肥鱼".</summary>
394
+ internal sealed class AskButton : Form
395
+ {
396
+ internal event EventHandler Clicked;
397
+
398
+ private static readonly Color BaseBg = Color.FromArgb(255, 255, 255);
399
+ private static readonly Color HoverBg = Color.FromArgb(243, 244, 246);
400
+ private static readonly Color TextColor = Color.FromArgb(24, 24, 27);
401
+ private static readonly Color BorderColor = Color.FromArgb(206, 208, 214);
402
+
403
+ private readonly Label label;
404
+ private float scale;
405
+
406
+ internal AskButton(float scale)
407
+ {
408
+ this.scale = scale > 0f ? scale : 1f;
409
+
410
+ FormBorderStyle = FormBorderStyle.None;
411
+ StartPosition = FormStartPosition.Manual;
412
+ ShowInTaskbar = false;
413
+ TopMost = true;
414
+ AutoScaleMode = AutoScaleMode.None;
415
+ BackColor = BaseBg;
416
+ DoubleBuffered = true;
417
+ KeyPreview = false;
418
+
419
+ Fingerprint();
420
+
421
+ label = new Label();
422
+ label.Text = "\u95EE\u5927\u80A5\u9C7C";
423
+ label.AutoSize = false;
424
+ label.Dock = DockStyle.Fill;
425
+ label.TextAlign = ContentAlignment.MiddleCenter;
426
+ label.ForeColor = TextColor;
427
+ label.BackColor = Color.Transparent;
428
+ label.Cursor = Cursors.Hand;
429
+ label.Click += delegate { Fire(); };
430
+ label.MouseEnter += delegate { SetHover(true); };
431
+ label.MouseLeave += delegate { SetHover(false); };
432
+ Controls.Add(label);
433
+
434
+ MouseEnter += delegate { SetHover(true); };
435
+ MouseLeave += delegate { SetHover(false); };
436
+ Click += delegate { Fire(); };
437
+
438
+ ApplyMetrics();
439
+ }
440
+
441
+ protected override void OnHandleCreated(EventArgs e)
442
+ {
443
+ base.OnHandleCreated(e);
444
+ // The monitor DPI is only reliable through the window itself; take it from here
445
+ // so the pill is never a shrunken 96-dpi rectangle on a 150% screen.
446
+ int dpi = DeviceDpi;
447
+ if (dpi >= 96)
448
+ {
449
+ float s = dpi / 96f;
450
+ if (Math.Abs(s - scale) > 0.01f)
451
+ {
452
+ scale = s;
453
+ ApplyMetrics();
454
+ }
455
+ }
456
+ }
457
+
458
+ private void ApplyMetrics()
459
+ {
460
+ label.Font = new Font("Microsoft YaHei UI", 8.5f, FontStyle.Bold);
461
+ Width = TextRenderer.MeasureText(label.Text, label.Font).Width + U(28);
462
+ Height = U(32);
463
+ ApplyRoundedRegion();
464
+ }
465
+
466
+ internal void Fingerprint()
467
+ {
468
+ SelectionWatcher.Log("pill scale=" + scale.ToString("0.00"));
469
+ }
470
+
471
+ /// <summary>Never steal focus from the document the user is selecting in.</summary>
472
+ protected override bool ShowWithoutActivation
473
+ {
474
+ get { return true; }
475
+ }
476
+
477
+ protected override CreateParams CreateParams
478
+ {
479
+ get
480
+ {
481
+ CreateParams cp = base.CreateParams;
482
+ cp.ExStyle |= 0x08000000; // WS_EX_NOACTIVATE
483
+ cp.ExStyle |= 0x00000080; // WS_EX_TOOLWINDOW
484
+ return cp;
485
+ }
486
+ }
487
+
488
+ internal void ShowNear(Point cursor)
489
+ {
490
+ Rectangle wa = Native.WorkArea();
491
+ int x = cursor.X + U(14);
492
+ int y = cursor.Y + U(18);
493
+ if (x + Width > wa.Right) x = cursor.X - Width - U(10);
494
+ if (y + Height > wa.Bottom) y = cursor.Y - Height - U(10);
495
+ if (x < wa.Left) x = wa.Left + U(4);
496
+ if (y < wa.Top) y = wa.Top + U(4);
497
+
498
+ Location = new Point(x, y);
499
+ if (!Visible) Show();
500
+ BringToFront();
501
+ SelectionWatcher.Log("pill at " + x + "," + y + " " + Width + "x" + Height);
502
+ }
503
+
504
+ /// <summary>
505
+ /// Hangs the pill at the end of the last selected line: to its right when there is
506
+ /// room, otherwise right-aligned underneath the selection. Clamped to the work area.
507
+ /// </summary>
508
+ internal void ShowNearSelection(SelectionHit hit, Point fallback)
509
+ {
510
+ Rectangle wa = Native.WorkArea();
511
+ int x;
512
+ int y;
513
+ string mode;
514
+
515
+ if (hit.HasRect && hit.Rect.Width > 0)
516
+ {
517
+ // bottom-right of the last selected rect: keeps the text itself uncovered
518
+ Rectangle sel = hit.Rect;
519
+ x = sel.Right + U(10);
520
+ y = sel.Bottom + U(8);
521
+ mode = "right-below";
522
+ if (x + Width > wa.Right - U(6))
523
+ {
524
+ // not enough room to the right: slide left, still under the last line
525
+ x = sel.Right - Width;
526
+ y = sel.Bottom + U(8);
527
+ mode = "below";
528
+ }
529
+ if (y + Height > wa.Bottom - U(6))
530
+ {
531
+ // bottom of the screen: flip above the last line
532
+ y = sel.Top - Height - U(8);
533
+ mode = "above";
534
+ }
535
+ }
536
+ else
537
+ {
538
+ x = fallback.X + U(14);
539
+ y = fallback.Y + U(18);
540
+ mode = "cursor";
541
+ }
542
+
543
+ if (x < wa.Left + U(6)) x = wa.Left + U(6);
544
+ if (x + Width > wa.Right - U(6)) x = wa.Right - Width - U(6);
545
+ if (y < wa.Top + U(6)) y = wa.Top + U(6);
546
+ if (y + Height > wa.Bottom - U(6)) y = wa.Bottom - Height - U(6);
547
+
548
+ Location = new Point(x, y);
549
+ if (!Visible) Show();
550
+ BringToFront();
551
+ SelectionWatcher.Log("pill at " + x + "," + y + " " + Width + "x" + Height +
552
+ " mode=" + mode + " sel=" + (hit.HasRect ? hit.Rect.ToString() : "none"));
553
+ }
554
+
555
+ internal void Dismiss()
556
+ {
557
+ if (Visible) Hide();
558
+ }
559
+
560
+ private int U(int v)
561
+ {
562
+ return (int)Math.Round(v * scale);
563
+ }
564
+
565
+ private void SetHover(bool on)
566
+ {
567
+ BackColor = on ? HoverBg : BaseBg;
568
+ label.ForeColor = TextColor;
569
+ Invalidate();
570
+ }
571
+
572
+ private void Fire()
573
+ {
574
+ EventHandler handler = Clicked;
575
+ if (handler != null) handler(this, EventArgs.Empty);
576
+ }
577
+
578
+ /// <summary>Capsule outline: the corner radius is half the height, so both ends are fully round.</summary>
579
+ private GraphicsPath BuildPath(Rectangle b)
580
+ {
581
+ GraphicsPath path = new GraphicsPath();
582
+ int d = Math.Max(2, Math.Min(b.Height, b.Width));
583
+ path.AddArc(b.X, b.Y, d, d, 180, 90);
584
+ path.AddArc(b.Right - d, b.Y, d, d, 270, 90);
585
+ path.AddArc(b.Right - d, b.Bottom - d, d, d, 0, 90);
586
+ path.AddArc(b.X, b.Bottom - d, d, d, 90, 90);
587
+ path.CloseFigure();
588
+ return path;
589
+ }
590
+
591
+ private void ApplyRoundedRegion()
592
+ {
593
+ using (GraphicsPath path = BuildPath(new Rectangle(0, 0, Width, Height)))
594
+ {
595
+ Region = new Region(path);
596
+ }
597
+ }
598
+
599
+ /// <summary>A hairline border keeps the white pill readable on white pages.</summary>
600
+ protected override void OnPaint(PaintEventArgs e)
601
+ {
602
+ base.OnPaint(e);
603
+ e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
604
+ int inset = Math.Max(1, U(1));
605
+ Rectangle b = new Rectangle(inset, inset, Width - inset * 2 - 1, Height - inset * 2 - 1);
606
+ using (GraphicsPath path = BuildPath(b))
607
+ using (Pen pen = new Pen(BorderColor, Math.Max(1f, U(1))))
608
+ {
609
+ e.Graphics.DrawPath(pen, path);
610
+ }
611
+ }
612
+ }
613
+
614
+ /// <summary>
615
+ /// Polls the mouse button (a low-level hook buys nothing here), notices drag-selects
616
+ /// and double-clicks anywhere on the desktop, reads the selection and offers the pill.
617
+ /// </summary>
618
+ internal sealed class SelectionWatcher : IDisposable
619
+ {
620
+ internal event Action<string> QuoteRequested;
621
+
622
+ private readonly Form host;
623
+ private readonly AskButton button;
624
+ private readonly System.Windows.Forms.Timer timer;
625
+ private readonly float scale;
626
+
627
+ private bool down;
628
+ private Point downPt;
629
+ private Point lastClickPt;
630
+ private int downTick;
631
+ private int lastClickTick;
632
+ private int shownTick;
633
+ private int probeId;
634
+ private bool enabled = true;
635
+ private string text = "";
636
+
637
+ internal SelectionWatcher(Form host)
638
+ {
639
+ this.host = host;
640
+ scale = Native.EffectiveScale();
641
+
642
+ button = new AskButton(scale);
643
+ button.Clicked += OnButtonClicked;
644
+
645
+ timer = new System.Windows.Forms.Timer();
646
+ timer.Interval = 90;
647
+ timer.Tick += Poll;
648
+ timer.Start();
649
+ }
650
+
651
+ internal bool Enabled
652
+ {
653
+ get { return enabled; }
654
+ set
655
+ {
656
+ enabled = value;
657
+ if (!value) HideButton();
658
+ }
659
+ }
660
+
661
+ public void Dispose()
662
+ {
663
+ try
664
+ {
665
+ timer.Stop();
666
+ timer.Tick -= Poll;
667
+ timer.Dispose();
668
+ }
669
+ catch
670
+ {
671
+ }
672
+ try
673
+ {
674
+ button.Clicked -= OnButtonClicked;
675
+ button.Dispose();
676
+ }
677
+ catch
678
+ {
679
+ }
680
+ }
681
+
682
+ private int U(int v)
683
+ {
684
+ return (int)Math.Round(v * scale);
685
+ }
686
+
687
+ private bool OverOwnWindows(Point p)
688
+ {
689
+ if (host != null && host.Visible && host.Bounds.Contains(p)) return true;
690
+ if (button != null && button.Visible && button.Bounds.Contains(p)) return true;
691
+ return false;
692
+ }
693
+
694
+ private void Poll(object sender, EventArgs e)
695
+ {
696
+ int now = Environment.TickCount;
697
+ Point p = Cursor.Position;
698
+ bool isDown = ShellWin.IsMouseDown();
699
+
700
+ if (isDown && !down)
701
+ {
702
+ down = true;
703
+ downPt = p;
704
+ downTick = now;
705
+ if (button.Visible && !button.Bounds.Contains(p)) HideButton();
706
+ }
707
+ else if (!isDown && down)
708
+ {
709
+ down = false;
710
+ int elapsed = now - downTick;
711
+ int dist = Math.Abs(p.X - downPt.X) + Math.Abs(p.Y - downPt.Y);
712
+
713
+ if (elapsed < 8000 && dist >= U(6))
714
+ {
715
+ if (!OverOwnWindows(p)) Kick(p, 130);
716
+ }
717
+ else if (dist < U(4))
718
+ {
719
+ // double-click / triple-click selects a word or a paragraph
720
+ bool second = now - lastClickTick < 520
721
+ && Math.Abs(p.X - lastClickPt.X) < U(24)
722
+ && Math.Abs(p.Y - lastClickPt.Y) < U(24);
723
+ if (second && !OverOwnWindows(p)) Kick(p, 320);
724
+ lastClickTick = now;
725
+ lastClickPt = p;
726
+ }
727
+ }
728
+
729
+ if (button.Visible && now - shownTick > 9000) HideButton();
730
+ }
731
+
732
+ internal static void Log(string line)
733
+ {
734
+ try
735
+ {
736
+ string dir = System.IO.Path.Combine(
737
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "dsh-hud");
738
+ System.IO.Directory.CreateDirectory(dir);
739
+ System.IO.File.AppendAllText(System.IO.Path.Combine(dir, "hud.log"),
740
+ DateTime.Now.ToString("HH:mm:ss") + " [pill] " + line + "\r\n",
741
+ System.Text.Encoding.UTF8);
742
+ }
743
+ catch
744
+ {
745
+ }
746
+ }
747
+
748
+ private void Kick(Point pt, int delay)
749
+ {
750
+ if (!enabled) { Log("skip: disabled"); return; }
751
+
752
+ if (ShellWin.IsShellSurfaceAt(pt)) { Log("skip: shell surface"); return; }
753
+ int id = ++probeId;
754
+ bool terminal = ShellWin.IsTerminalAt(pt);
755
+ IntPtr root = ShellWin.RootWindowAt(pt);
756
+ if (ShellWin.IsGameWindow(root)) { Log("skip: game window"); return; }
757
+ Log("kick at " + pt.X + "," + pt.Y + " terminal=" + terminal + " root=" + root);
758
+
759
+ ThreadPool.QueueUserWorkItem(delegate(object state)
760
+ {
761
+ try
762
+ {
763
+ Thread.Sleep(delay);
764
+ if (id != probeId) return;
765
+ if (terminal) { Log("skip: terminal"); return; }
766
+ // the user may have switched apps while we waited; never copy there
767
+ // prefer whatever the user is actually looking at; the window under the
768
+ // cursor may be a stale/overlapped one (explorer behind a maximized app)
769
+ IntPtr fg = ShellWin.GetForegroundWindow();
770
+ if (fg != IntPtr.Zero && ShellWin.IsTerminalWindow(fg)) { Log("skip: foreground terminal"); return; }
771
+ if (ShellWin.IsGameWindow(fg)) { Log("skip: foreground game"); return; }
772
+
773
+ SelectionHit hit = SelectionReader.ReadHit(fg != IntPtr.Zero ? fg : root);
774
+ if (string.IsNullOrEmpty(hit.Text) && fg != IntPtr.Zero && fg != root)
775
+ {
776
+ hit = SelectionReader.ReadHit(root);
777
+ }
778
+ string got = hit.Text;
779
+ Log("read " + (got == null ? -1 : got.Length) + " chars" +
780
+ (hit.HasRect ? " rect=" + hit.Rect.ToString() : " rect=none"));
781
+ if (string.IsNullOrEmpty(got)) return;
782
+ if (id != probeId) return;
783
+
784
+ host.BeginInvoke(new MethodInvoker(delegate
785
+ {
786
+ ShowButton(hit, pt);
787
+ }));
788
+ }
789
+ catch
790
+ {
791
+ }
792
+ });
793
+ }
794
+
795
+ /// <summary>Diagnostic hook: show the pill for a sample text at the cursor.</summary>
796
+ internal void DebugPill()
797
+ {
798
+ SelectionHit hit = new SelectionHit();
799
+ hit.Text = "\u8C03\u8BD5\uFF1A\u9884\u89C8\u836F\u4E38\u5916\u89C2\u7528\u7684\u4E00\u6BB5\u6587\u5B57";
800
+ ShowButton(hit, Cursor.Position);
801
+ }
802
+
803
+ /// <summary>Diagnostic hook: place the pill as if <paramref name="rect"/> were the last selected rect.</summary>
804
+ internal void DebugPillAt(Rectangle rect)
805
+ {
806
+ SelectionHit hit = new SelectionHit();
807
+ hit.HasRect = true;
808
+ hit.Rect = rect;
809
+ hit.Text = "debug";
810
+ shownTick = Environment.TickCount;
811
+ button.ShowNearSelection(hit, Cursor.Position);
812
+ }
813
+ /// <summary>Diagnostic hook: read the current selection without any mouse work.</summary>
814
+ internal void TestProbe()
815
+ {
816
+ ThreadPool.QueueUserWorkItem(delegate(object state)
817
+ {
818
+ SelectionHit hit = SelectionReader.ReadHit();
819
+ string got = hit.Text;
820
+ int len = got == null ? -1 : got.Length;
821
+ string head = len > 0 ? got.Substring(0, Math.Min(40, got.Length)) : "";
822
+ Log("test read " + len + " chars" + (hit.HasRect ? " rect=" + hit.Rect.ToString() : " rect=none") +
823
+ ": " + head.Replace("\n", " ").Replace("\r", " "));
824
+ });
825
+ }
826
+
827
+ private void ShowButton(SelectionHit hit, Point pt)
828
+ {
829
+ Log("show " + (hit.Text == null ? 0 : hit.Text.Length) + " chars");
830
+ text = hit.Text;
831
+ shownTick = Environment.TickCount;
832
+ button.ShowNearSelection(hit, pt);
833
+ }
834
+
835
+ private void HideButton()
836
+ {
837
+ text = "";
838
+ button.Dismiss();
839
+ }
840
+
841
+ private void OnButtonClicked(object sender, EventArgs e)
842
+ {
843
+ string got = text;
844
+ HideButton();
845
+ if (string.IsNullOrEmpty(got)) return;
846
+ Action<string> handler = QuoteRequested;
847
+ if (handler != null) handler(got);
848
+ }
849
+ }
850
+ }