1using System; 2using System.Runtime.InteropServices; 3using System.Text; 4using WindowCapture.Native; 5 6namespace WindowCapture.Helpers 7{ 8 /// <summary> 9 /// CursorReader: reads text around the caret in the active application. 10 /// Uses UI Automation TextPattern (Chrome, WPF, UWP) with WM_GETTEXT fallback (Notepad, Win32). 11 /// Non-invasive: doesn't touch clipboard, doesn't move cursor. 12 /// </summary> 13 public static class CursorReader 14 { 15 // Cached context (updated on STA thread) 16 private static string cachedLine = ""; // current line text 17 private static string cachedWord = ""; // word under cursor 18 private static string[] cachedPrevWords; // previous N words 19 private static int cachedCaretInLine = -1; // caret position within line 20 private static long lastUpdateTick = 0; 21 private static volatile bool updating = false; 22 23 const int UPDATE_INTERVAL_MS = 200; // refresh every 200ms max 24 25 // P/Invoke for WM_GETTEXT approach 26 const int WM_GETTEXTLENGTH = 0x000E; 27 const int WM_GETTEXT_MSG = 0x000D; 28 const int EM_GETSEL = 0x00B0; 29 const int EM_LINEFROMCHAR = 0x00C9; 30 const int EM_LINEINDEX = 0x00BB; 31 const int EM_LINELENGTH = 0x00C1; 32 const int EM_GETLINE = 0x00C4; 33 34 [DllImport("user32.dll", CharSet = CharSet.Unicode)] 35 static extern IntPtr SendMessageW(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); 36 [DllImport("user32.dll", CharSet = CharSet.Unicode)] 37 static extern IntPtr SendMessageW(IntPtr hWnd, int msg, IntPtr wParam, StringBuilder lParam); 38 39 /// <summary>Result of cursor context analysis.</summary> 40 public class CursorContext 41 { 42 public string CurrentWord; // word at/before caret (possibly partial) 43 public string CurrentLine; // full text of current line 44 public string[] PrevWords; // previous words (up to 5) 45 public int CaretInLine; // caret position within line 46 public bool IsValid; // whether we got meaningful data 47 } 48 49 /// <summary> 50 /// Get cursor context. Called from any thread. 51 /// Returns cached result and schedules background refresh. 52 /// </summary> 53 public static CursorContext GetContext() 54 { 55 long now = Environment.TickCount; 56 if (!updating && (now - lastUpdateTick > UPDATE_INTERVAL_MS)) 57 { 58 updating = true; 59 var t = new System.Threading.Thread(DoUpdate); 60 t.SetApartmentState(System.Threading.ApartmentState.STA); 61 t.IsBackground = true; 62 t.Start(); 63 } 64 65 return new CursorContext 66 { 67 CurrentWord = cachedWord, 68 CurrentLine = cachedLine, 69 PrevWords = cachedPrevWords, 70 CaretInLine = cachedCaretInLine, 71 IsValid = !string.IsNullOrEmpty(cachedLine) 72 }; 73 } 74 75 /// <summary>Force synchronous update (for Ctrl+Shift+Space).</summary> 76 public static CursorContext GetContextSync() 77 { 78 var t = new System.Threading.Thread(DoUpdate); 79 t.SetApartmentState(System.Threading.ApartmentState.STA); 80 t.IsBackground = true; 81 t.Start(); 82 t.Join(500); // wait up to 500ms 83 return GetContext(); 84 } 85 86 private static void DoUpdate() 87 { 88 try 89 { 90 lastUpdateTick = Environment.TickCount; 91 string line = null; 92 int caretPos = -1; 93 94 // Method 1: UI Automation TextPattern 95 try 96 { 97 var focused = System.Windows.Automation.AutomationElement.FocusedElement; 98 if (focused != null) 99 { 100 object patObj; 101 if (focused.TryGetCurrentPattern(System.Windows.Automation.TextPattern.Pattern, out patObj)) 102 { 103 var tp = patObj as System.Windows.Automation.TextPattern; 104 if (tp != null) 105 { 106 var sels = tp.GetSelection(); 107 if (sels != null && sels.Length > 0) 108 { 109 // Get current line 110 var lineRange = sels[0].Clone(); 111 lineRange.ExpandToEnclosingUnit(System.Windows.Automation.Text.TextUnit.Line); 112 line = lineRange.GetText(1000); 113 114 // Get position within line: count chars from line start to caret 115 var beforeCaret = sels[0].Clone(); 116 beforeCaret.MoveEndpointByUnit( 117 System.Windows.Automation.Text.TextPatternRangeEndpoint.Start, 118 System.Windows.Automation.Text.TextUnit.Line, -1); 119 string beforeText = beforeCaret.GetText(1000); 120 // caretPos = chars from start of line to caret 121 if (line != null && beforeText != null) 122 { 123 int lineStart = beforeText.LastIndexOf('\n'); 124 caretPos = lineStart >= 0 ? beforeText.Length - lineStart - 1 : beforeText.Length; 125 } 126 } 127 } 128 } 129 } 130 } 131 catch { } 132 133 // Method 2: WM_GETTEXT + EM_GETSEL fallback (Notepad, Win32 edit) 134 if (string.IsNullOrEmpty(line)) 135 { 136 try 137 { 138 IntPtr fg = WinApi.GetForegroundWindow(); 139 if (fg != IntPtr.Zero) 140 { 141 uint pid; uint tid = WinApi.GetWindowThreadProcessId(fg, out pid); 142 var info = new WinApi.GUITHREADINFO(); 143 info.cbSize = Marshal.SizeOf(typeof(WinApi.GUITHREADINFO)); 144 if (WinApi.GetGUIThreadInfo(tid, ref info)) 145 { 146 IntPtr hwnd = info.hwndFocus != IntPtr.Zero ? info.hwndFocus : fg; 147 148 // Get caret position 149 IntPtr selResult = SendMessageW(hwnd, EM_GETSEL, IntPtr.Zero, IntPtr.Zero); 150 int selStart = (int)(selResult.ToInt64() & 0xFFFF); 151 152 // Get line number from caret 153 int lineNum = (int)SendMessageW(hwnd, EM_LINEFROMCHAR, (IntPtr)selStart, IntPtr.Zero); 154 155 // Get line start index 156 int lineIdx = (int)SendMessageW(hwnd, EM_LINEINDEX, (IntPtr)lineNum, IntPtr.Zero); 157 158 // Get line length 159 int lineLen = (int)SendMessageW(hwnd, EM_LINELENGTH, (IntPtr)lineIdx, IntPtr.Zero); 160 161 if (lineLen > 0 && lineLen < 5000) 162 { 163 // Get line text 164 var sb = new StringBuilder(lineLen + 4); 165 sb.Append((char)lineLen); // EM_GETLINE requires first word = buffer size 166 sb.Append('\0', lineLen + 2); 167 SendMessageW(hwnd, EM_GETLINE, (IntPtr)lineNum, sb); 168 line = sb.ToString().Substring(0, Math.Min(sb.Length, lineLen)); 169 caretPos = selStart - lineIdx; 170 } 171 else 172 { 173 // Fallback: WM_GETTEXT full text 174 int textLen = (int)SendMessageW(hwnd, WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); 175 if (textLen > 0 && textLen < 50000) 176 { 177 var sb2 = new StringBuilder(textLen + 1); 178 SendMessageW(hwnd, WM_GETTEXT_MSG, (IntPtr)(textLen + 1), sb2); 179 string fullText = sb2.ToString(); 180 // Find current line 181 if (selStart >= 0 && selStart <= fullText.Length) 182 { 183 int lineStart = fullText.LastIndexOf('\n', Math.Max(0, selStart - 1)); 184 if (lineStart < 0) lineStart = 0; else lineStart++; 185 int lineEnd = fullText.IndexOf('\n', selStart); 186 if (lineEnd < 0) lineEnd = fullText.Length; 187 line = fullText.Substring(lineStart, lineEnd - lineStart); 188 caretPos = selStart - lineStart; 189 } 190 } 191 } 192 } 193 } 194 } 195 catch { } 196 } 197 198 // Parse results 199 if (!string.IsNullOrEmpty(line)) 200 { 201 cachedLine = line.TrimEnd('\r', '\n'); 202 cachedCaretInLine = Math.Max(0, Math.Min(caretPos, cachedLine.Length)); 203 204 // Extract current word at caret 205 string textBeforeCaret = cachedCaretInLine <= cachedLine.Length 206 ? cachedLine.Substring(0, cachedCaretInLine) 207 : cachedLine; 208 209 // Find word boundaries 210 var words = new System.Collections.Generic.List<string>(); 211 var wb = new StringBuilder(); 212 foreach (char c in textBeforeCaret) 213 { 214 if (char.IsLetter(c) || c == '-' || c == '\'') 215 wb.Append(c); 216 else if (wb.Length > 0) 217 { words.Add(wb.ToString()); wb.Clear(); } 218 } 219 if (wb.Length > 0) words.Add(wb.ToString()); 220 221 cachedWord = words.Count > 0 ? words[words.Count - 1].ToLower() : ""; 222 int prevCount = Math.Min(words.Count - 1, 5); 223 cachedPrevWords = new string[prevCount]; 224 for (int i = 0; i < prevCount; i++) 225 cachedPrevWords[i] = words[words.Count - 1 - prevCount + i].ToLower(); 226 } 227 } 228 catch { } 229 finally { updating = false; } 230 } 231 } 232}