1using System; 2using System.Drawing; 3using System.Drawing.Drawing2D; 4using System.Runtime.InteropServices; 5using System.Windows.Forms; 6using WindowCapture.Detection; 7using WindowCapture.Models; 8using WindowCapture.Native; 9using WindowCapture.UI; 10using WindowCapture.Recording; 11 12namespace WindowCapture.App 13{ 14 /// <summary>Which concerns this Controller's global hooks handle. Lets each app process 15 /// (screenshot / textassist / clipboard) install and run only the hooks it needs.</summary> 16 [Flags] 17 public enum ControllerMode 18 { 19 None = 0, 20 Capture = 1, // primary+Win activation -> freeze overlay -> editor/recording 21 TextAssist = 2, // T9/autocorrect keystroke feed + Ctrl+Shift+Space full correction 22 Clipboard = 4, // Ctrl+V hold -> clipboard history 23 Gestures = 8, // MMB-drag -> open search / soundpad panels 24 All = Capture | TextAssist | Clipboard | Gestures 25 } 26 27 public class Controller : IDisposable 28 { 29 private ControllerMode mode; // mutable: the unified MediaCore app toggles modules live 30 private IntPtr hook; 31 private IntPtr mouseHook; 32 private WinApi.LowLevelKeyboardProc proc; 33 private WinApi.LowLevelMouseProc mouseProc; 34 private bool mmbDown; 35 private int mmbStartX; 36 private bool searchTriggered; 37 private bool soundpadTriggered; 38 private FrozenOverlay overlay; 39 private Detector detector; 40 private Bitmap frozen; 41 private Rectangle captureBounds; // virtual-screen rect the frozen bitmap was captured from (origin may be negative) 42 private bool active; 43 private bool isRecordingMode; 44 private VideoRecorder recorder; 45 private RecordingOverlay recordingOverlay; 46 private bool ignoreActivationUntilRelease; // Ignore activation key until it's released 47 private Point lastCur; 48 private GraphicsPath currentPath; 49 private int expandLevel = 0; 50 51 // Clipboard hold detection 52 private Timer clipboardHoldTimer; 53 private bool ctrlVHeld; 54 private bool ctrlVEaten; // suppressed real V keydown 55 private ClipboardForm clipboardForm; 56 private const uint LLKHF_INJECTED = 0x10; 57 58 [DllImport("user32.dll")] 59 private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo); 60 61 public event Action<string> StatusChanged; 62 63 public Controller() : this(ControllerMode.All) { } 64 65 public Controller(ControllerMode mode) 66 { 67 this.mode = mode; 68 proc = HookCallback; 69 mouseProc = MouseHookCallback; 70 71 // Capture concern needs the freeze overlay + region detector. 72 if ((mode & ControllerMode.Capture) != 0) 73 { 74 overlay = new FrozenOverlay(); 75 overlay.MouseClick += Overlay_MouseClick; 76 overlay.WheelScrolled += Overlay_WheelScrolled; 77 overlay.ManualRectSelected += Overlay_ManualRectSelected; 78 overlay.WindowSelected += Overlay_WindowSelected; 79 overlay.AddRegionRequested += Overlay_AddRegionRequested; 80 overlay.MediaParseRequested += Overlay_MediaParseRequested; 81 overlay.MediaParseRegionRequested += Overlay_MediaParseRegionRequested; 82 detector = new Detector(); 83 } 84 85 // Install only the hooks this mode actually uses. 86 bool needKeyboard = (mode & (ControllerMode.Capture | ControllerMode.TextAssist | ControllerMode.Clipboard)) != 0; 87 bool needMouse = (mode & (ControllerMode.TextAssist | ControllerMode.Gestures)) != 0; 88 if (needKeyboard) 89 hook = WinApi.SetWindowsHookEx(WinApi.WH_KEYBOARD_LL, proc, WinApi.GetModuleHandle(null), 0); 90 if (needMouse) 91 mouseHook = WinApi.SetWindowsHookEx(WinApi.WH_MOUSE_LL, mouseProc, WinApi.GetModuleHandle(null), 0); 92 93 if (StatusChanged != null) 94 StatusChanged("Press " + Settings.ActivationKeyName + "+Win to capture"); 95 } 96 97 /// <summary>Enable/disable one module's behavior at runtime. The unified MediaCore app 98 /// constructs with All (so every hook + the capture overlay are installed up front), then 99 /// gates which concerns actually act by flipping mode bits here. Setup/teardown beyond the 100 /// hooks (clipboard listener, SAGE warmup, TSF bridge) is the caller's responsibility.</summary> 101 public void SetModuleEnabled(ControllerMode bit, bool on) 102 { 103 if (on) mode = (ControllerMode)((int)mode | (int)bit); 104 else mode = (ControllerMode)((int)mode & ~(int)bit); 105 } 106 107 private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) 108 { 109 if (nCode >= 0) 110 { 111 // Ignore Alt when editor is open (editor handles Alt itself) 112 if (EditorForm.IsOpen) 113 return WinApi.CallNextHookEx(hook, nCode, wParam, lParam); 114 115 var kb = (WinApi.KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(WinApi.KBDLLHOOKSTRUCT)); 116 int wmsg = wParam.ToInt32(); 117 118 // --- Ctrl+V: suppress real keydown, paste on keyup via injected keybd_event --- 119 int vkc = (int)kb.vkCode; 120 bool isInjected = (kb.flags & LLKHF_INJECTED) != 0; 121 122 if (vkc == 0x56 && (mode & ControllerMode.Clipboard) != 0) // V key 123 { 124 bool ctrlHeld = (WinApi.GetAsyncKeyState(0xA2) & 0x8000) != 0 125 || (WinApi.GetAsyncKeyState(0xA3) & 0x8000) != 0; 126 127 if (wmsg == WinApi.WM_KEYDOWN || wmsg == WinApi.WM_SYSKEYDOWN) 128 { 129 if (isInjected) 130 { 131 // Our own keybd_event paste → let through 132 } 133 else if (ctrlHeld) 134 { 135 if (!ctrlVHeld && Settings.ClipboardHistory) 136 { 137 ctrlVHeld = true; 138 ctrlVEaten = true; 139 if (clipboardHoldTimer == null) 140 { 141 clipboardHoldTimer = new Timer { Interval = 120 }; 142 clipboardHoldTimer.Tick += ClipboardHoldTimer_Tick; 143 } 144 clipboardHoldTimer.Start(); 145 } 146 return (IntPtr)1; // suppress real V keydown 147 } 148 } 149 else if (wmsg == WinApi.WM_KEYUP || wmsg == WinApi.WM_SYSKEYUP) 150 { 151 if (isInjected) 152 { 153 // Our own keybd_event → let through 154 } 155 else if (ctrlVEaten) 156 { 157 ctrlVEaten = false; 158 ctrlVHeld = false; 159 if (clipboardHoldTimer != null) clipboardHoldTimer.Stop(); 160 161 // Select hovered item, then paste 162 bool hasForm = clipboardForm != null && !clipboardForm.IsDisposed; 163 if (hasForm) 164 { 165 // Use ManualResetEvent to wait until clipboard is set 166 var clipReady = new System.Threading.ManualResetEvent(false); 167 bool itemSelected = false; 168 TrayApp.Instance.BeginInvoke(new Action(() => 169 { 170 try 171 { 172 if (clipboardForm != null && !clipboardForm.IsDisposed) 173 { 174 itemSelected = clipboardForm.SelectHoveredItem(); 175 clipboardForm.FadeOut(); 176 } 177 } 178 catch { } 179 clipReady.Set(); 180 })); 181 182 // Paste ONLY if user hovered over an item 183 System.Threading.ThreadPool.QueueUserWorkItem(delegate 184 { 185 clipReady.WaitOne(500); 186 if (itemSelected) 187 { 188 System.Threading.Thread.Sleep(50); 189 // Always inject full Ctrl+V (user may have released Ctrl already) 190 keybd_event(0xA2, 0, 0, UIntPtr.Zero); // Ctrl down 191 keybd_event(0x56, 0, 0, UIntPtr.Zero); // V down 192 System.Threading.Thread.Sleep(5); 193 keybd_event(0x56, 0, 0x0002, UIntPtr.Zero); // V up 194 keybd_event(0xA2, 0, 0x0002, UIntPtr.Zero); // Ctrl up 195 } 196 }); 197 } 198 else 199 { 200 // No form — quick paste (normal Ctrl+V passthrough) 201 System.Threading.ThreadPool.QueueUserWorkItem(delegate 202 { 203 System.Threading.Thread.Sleep(15); 204 keybd_event(0xA2, 0, 0, UIntPtr.Zero); 205 keybd_event(0x56, 0, 0, UIntPtr.Zero); 206 System.Threading.Thread.Sleep(5); 207 keybd_event(0x56, 0, 0x0002, UIntPtr.Zero); 208 keybd_event(0xA2, 0, 0x0002, UIntPtr.Zero); 209 }); 210 } 211 return (IntPtr)1; // suppress real V keyup 212 } 213 } 214 } 215 if ((vkc == 0xA2 || vkc == 0xA3) && (wmsg == WinApi.WM_KEYUP || wmsg == WinApi.WM_SYSKEYUP) && (mode & ControllerMode.Clipboard) != 0) 216 { 217 if (ctrlVHeld || ctrlVEaten) 218 { 219 bool wasEaten = ctrlVEaten; 220 ctrlVHeld = false; 221 ctrlVEaten = false; 222 if (clipboardHoldTimer != null) clipboardHoldTimer.Stop(); 223 // Select + paste + close (same as V keyup) 224 bool hasForm = clipboardForm != null && !clipboardForm.IsDisposed; 225 if (hasForm && wasEaten) 226 { 227 var clipReady2 = new System.Threading.ManualResetEvent(false); 228 bool itemSelected2 = false; 229 TrayApp.Instance.BeginInvoke(new Action(() => 230 { 231 try 232 { 233 if (clipboardForm != null && !clipboardForm.IsDisposed) 234 { 235 itemSelected2 = clipboardForm.SelectHoveredItem(); 236 clipboardForm.FadeOut(); 237 } 238 } 239 catch { } 240 clipReady2.Set(); 241 })); 242 System.Threading.ThreadPool.QueueUserWorkItem(delegate 243 { 244 clipReady2.WaitOne(500); 245 if (itemSelected2) 246 { 247 // Re-press Ctrl for paste since user just released it 248 System.Threading.Thread.Sleep(30); 249 keybd_event(0xA2, 0, 0, UIntPtr.Zero); // Ctrl down 250 keybd_event(0x56, 0, 0, UIntPtr.Zero); // V down 251 System.Threading.Thread.Sleep(5); 252 keybd_event(0x56, 0, 0x0002, UIntPtr.Zero); // V up 253 keybd_event(0xA2, 0, 0x0002, UIntPtr.Zero); // Ctrl up 254 } 255 }); 256 } 257 else if (hasForm) 258 { 259 TrayApp.Instance.BeginInvoke(new Action(() => 260 { 261 if (clipboardForm != null && !clipboardForm.IsDisposed) 262 clipboardForm.FadeOut(); 263 })); 264 } 265 } 266 } 267 268 // --- Ctrl+Shift+Space: full text correction --- 269 if (vkc == 0x20 && (wmsg == WinApi.WM_KEYDOWN || wmsg == WinApi.WM_SYSKEYDOWN) && !isInjected && (mode & ControllerMode.TextAssist) != 0) 270 { 271 bool ctrlH = (WinApi.GetAsyncKeyState(0xA2) & 0x8000) != 0 || (WinApi.GetAsyncKeyState(0xA3) & 0x8000) != 0; 272 bool shiftH = (WinApi.GetAsyncKeyState(0xA0) & 0x8000) != 0 || (WinApi.GetAsyncKeyState(0xA1) & 0x8000) != 0; 273 if (ctrlH && shiftH) 274 { 275 System.Threading.ThreadPool.QueueUserWorkItem(delegate 276 { 277 System.Threading.Thread.Sleep(30); 278 Helpers.TextProcessor.CorrectEntireText(); 279 }); 280 return (IntPtr)1; // suppress the space 281 } 282 } 283 284 // --- Feed keystrokes to TextProcessor (skip injected from our own replacements) --- 285 if ((wmsg == WinApi.WM_KEYDOWN || wmsg == WinApi.WM_SYSKEYDOWN) && !isInjected && (mode & ControllerMode.TextAssist) != 0) 286 { 287 bool suppress = Helpers.TextProcessor.OnKeyDown((int)kb.vkCode, (int)kb.scanCode); 288 if (suppress) return (IntPtr)1; 289 } 290 291 // ===== Activation COMBO: primary key (default Alt) + Win held together ===== 292 // A deliberate two-key chord avoids accidental triggering on a casual Alt tap. 293 // Hold Ctrl too (Alt+Win+Ctrl) for video-recording mode. Skip our own injected keys. 294 int vk = (int)kb.vkCode; 295 if (!isInjected && (mode & ControllerMode.Capture) != 0 && IsActivationComboKey(vk)) 296 { 297 int msg = wParam.ToInt32(); 298 bool isKeyDown = msg == WinApi.WM_KEYDOWN || msg == WinApi.WM_SYSKEYDOWN; 299 bool isKeyUp = msg == WinApi.WM_KEYUP || msg == WinApi.WM_SYSKEYUP; 300 301 bool vkIsWin = (vk == 0x5B || vk == 0x5C); 302 bool vkIsPrimary = IsPrimaryActivationVk(vk); 303 304 if (isKeyUp) 305 { 306 // The released key is a chord key, so the chord is necessarily broken now → 307 // end capture and clear the re-arm latch. (Don't query GetAsyncKeyState for the 308 // just-released key — it isn't reliably cleared yet inside the LL hook.) 309 ignoreActivationUntilRelease = false; 310 if (active) Deactivate(); 311 } 312 else if (isKeyDown && !active && !ignoreActivationUntilRelease && !EditorForm.IsOpen) 313 { 314 // IMPORTANT: the just-pressed key's GetAsyncKeyState is NOT updated yet inside the 315 // LL keyboard hook, so treat the current event's key as "down" by event type and 316 // only query the OTHER chord key via GetAsyncKeyState (it was pressed earlier). 317 bool primaryDown = vkIsPrimary || PrimaryActivationHeld(); 318 bool winDown = vkIsWin || WinHeld(); 319 if (primaryDown && winDown) 320 { 321 isRecordingMode = CtrlHeld(); // +Ctrl = video mode 322 Activate(); 323 } 324 } 325 326 // Swallow the Win key while it is part of our chord (primary held, or capture active) 327 // so Windows doesn't pop the Start menu on Win release. Win alone is left untouched. 328 if (vkIsWin && (PrimaryActivationHeld() || active)) 329 return (IntPtr)1; 330 } 331 } 332 return WinApi.CallNextHookEx(hook, nCode, wParam, lParam); 333 } 334 335 private bool searchOpen; // tracks whether search panel is currently visible 336 337 private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) 338 { 339 if (nCode >= 0) 340 { 341 int msg = wParam.ToInt32(); 342 // Left mouse click: reset word buffer (cursor moved) 343 if (msg == 0x0202 && (mode & ControllerMode.TextAssist) != 0) // WM_LBUTTONUP 344 { 345 Helpers.TextProcessor.OnCursorMoved(); 346 } 347 if (msg == WinApi.WM_MBUTTONDOWN && (mode & ControllerMode.Gestures) != 0) 348 { 349 bool altHeld = (WinApi.GetAsyncKeyState(0xA4) & 0x8000) != 0 350 || (WinApi.GetAsyncKeyState(0xA5) & 0x8000) != 0; 351 // Don't arm the explorer/soundpad slide gesture while the editor is open — 352 // there the middle button is used for panning and placing markers. 353 if (!altHeld && !active && !EditorForm.IsOpen) 354 { 355 mmbDown = true; 356 mmbStartX = Marshal.ReadInt32(lParam, 0); 357 searchTriggered = false; 358 soundpadTriggered = false; 359 } 360 } 361 else if (msg == WinApi.WM_MBUTTONUP) 362 { 363 mmbDown = false; 364 } 365 else if (mmbDown && msg == WinApi.WM_MOUSEMOVE && !EditorForm.IsOpen) 366 { 367 int curX = Marshal.ReadInt32(lParam, 0); 368 int dx = curX - mmbStartX; 369 370 if (!searchOpen && !searchTriggered && dx > 60) 371 { 372 // Drag right with search closed → open 373 searchTriggered = true; 374 searchOpen = true; 375 if (TrayApp.Instance != null) 376 TrayApp.Instance.BeginInvoke(new Action(() => TrayApp.Instance.ShowSearchPanel())); 377 } 378 else if (searchOpen && !searchTriggered && dx < -60) 379 { 380 // Drag left with search open → close search 381 searchTriggered = true; 382 searchOpen = false; 383 mmbDown = false; 384 if (TrayApp.Instance != null) 385 TrayApp.Instance.BeginInvoke(new Action(() => TrayApp.Instance.HideSearchPanel())); 386 } 387 else if (!searchOpen && !soundpadTriggered && dx < -60) 388 { 389 // Drag left with nothing open → open soundpad from right 390 soundpadTriggered = true; 391 mmbDown = false; 392 if (TrayApp.Instance != null) 393 TrayApp.Instance.BeginInvoke(new Action(() => TrayApp.Instance.ShowSoundpad())); 394 } 395 else if (searchOpen && searchTriggered && dx > 60) 396 { 397 // Continue dragging right after open → widen 398 int extraW = dx - 60; 399 if (TrayApp.Instance != null) 400 TrayApp.Instance.BeginInvoke(new Action(() => TrayApp.Instance.WidenSearchPanel(extraW))); 401 } 402 } 403 } 404 return WinApi.CallNextHookEx(mouseHook, nCode, wParam, lParam); 405 } 406 407 private void ClipboardHoldTimer_Tick(object s, EventArgs e) 408 { 409 clipboardHoldTimer.Stop(); 410 if (!ctrlVHeld) return; 411 if (TrayApp.Instance != null && !TrayApp.Instance.IsDisposed) 412 { 413 TrayApp.Instance.BeginInvoke(new Action(() => 414 { 415 // If form exists and is fading out — revive it 416 if (clipboardForm != null && !clipboardForm.IsDisposed) 417 { 418 if (clipboardForm.IsFadingOut) 419 clipboardForm.ReviveFromFadeOut(); 420 return; 421 } 422 clipboardForm = new ClipboardForm(); 423 clipboardForm.FormClosed += (ss, ee) => { clipboardForm = null; }; 424 clipboardForm.Show(); 425 })); 426 } 427 } 428 429 // ===== Activation chord helpers ===== 430 private static bool KeyHeld(int vk) { return (WinApi.GetAsyncKeyState(vk) & 0x8000) != 0; } 431 private static bool ShiftHeld() { return KeyHeld(0xA0) || KeyHeld(0xA1) || KeyHeld(0x10); } 432 private static bool CtrlHeld() { return KeyHeld(0xA2) || KeyHeld(0xA3) || KeyHeld(0x11); } 433 private static bool WinHeld() { return KeyHeld(0x5B) || KeyHeld(0x5C); } // VK_LWIN / VK_RWIN 434 435 /// <summary>Is the configured primary activation key currently physically held?</summary> 436 private static bool PrimaryActivationHeld() 437 { 438 int k = Settings.ActivationKey; 439 if (k == 0xA4 || k == 0xA5) return KeyHeld(0xA4) || KeyHeld(0xA5) || KeyHeld(0x12); // Alt 440 if (k == 0xA2 || k == 0xA3) return CtrlHeld(); // Ctrl 441 if (k == 0xA0 || k == 0xA1) return ShiftHeld(); // Shift 442 return KeyHeld(k); // F-keys etc. 443 } 444 445 /// <summary>Does this vkCode match the configured primary activation key (L/R/generic variants)?</summary> 446 private static bool IsPrimaryActivationVk(int vk) 447 { 448 int k = Settings.ActivationKey; 449 if (k == 0xA4 || k == 0xA5) return (vk == 0xA4 || vk == 0xA5 || vk == 0x12); // Alt 450 if (k == 0xA2 || k == 0xA3) return (vk == 0xA2 || vk == 0xA3 || vk == 0x11); // Ctrl 451 if (k == 0xA0 || k == 0xA1) return (vk == 0xA0 || vk == 0xA1 || vk == 0x10); // Shift 452 return (vk == k); // F-keys etc. 453 } 454 455 /// <summary>Is this vkCode one of the chord keys (primary or Win)? Used to scope the hook work.</summary> 456 private static bool IsActivationComboKey(int vk) 457 { 458 bool isWin = (vk == 0x5B || vk == 0x5C); // VK_LWIN / VK_RWIN 459 return IsPrimaryActivationVk(vk) || isWin; 460 } 461 462 private void Activate() 463 { 464 // Don't activate if editor is already open 465 if (EditorForm.IsOpen) return; 466 467 active = true; 468 expandLevel = 0; 469 // Capture the entire virtual desktop (all monitors), not just the primary screen. 470 // For a single-monitor setup this equals PrimaryScreen.Bounds, so behavior is unchanged. 471 var bounds = SystemInformation.VirtualScreen; 472 captureBounds = bounds; 473 frozen = new Bitmap(bounds.Width, bounds.Height); 474 using (var g = Graphics.FromImage(frozen)) 475 g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size); 476 477 detector.SetScreen(frozen); 478 overlay.IsVideoMode = isRecordingMode; 479 overlay.ShowWithScreenshot((Bitmap)frozen.Clone()); 480 481 Application.Idle += OnIdle; 482 483 if (StatusChanged != null) 484 StatusChanged("Capturing..."); 485 } 486 487 private void Deactivate() 488 { 489 Application.Idle -= OnIdle; 490 active = false; 491 expandLevel = 0; 492 currentPath = null; 493 if (overlay != null) overlay.Reset(); 494 if (frozen != null) { frozen.Dispose(); frozen = null; } 495 496 if (StatusChanged != null) 497 StatusChanged("Press " + Settings.ActivationKeyName + "+Win to capture"); 498 } 499 500 // Selection rects are in capture-local (frozen) coords; recording captures the live screen, 501 // which needs real screen coords. On a single monitor captureBounds origin is (0,0) → no-op. 502 private Rectangle ToScreenRect(Rectangle frozenRect) 503 { 504 return new Rectangle(frozenRect.X + captureBounds.X, frozenRect.Y + captureBounds.Y, 505 frozenRect.Width, frozenRect.Height); 506 } 507 508 private void StartVideoRecording(Rectangle area) 509 { 510 string tempPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "WindowCapture_" + Guid.NewGuid().ToString("N") + ".mp4"); 511 512 try 513 { 514 recorder = new VideoRecorder(); 515 recorder.Start(area, tempPath); 516 517 recordingOverlay = new RecordingOverlay(); 518 recordingOverlay.StopRequested += () => { 519 recorder.Stop(); 520 recordingOverlay.Close(); 521 522 // Determine actual output file (mp4, wmv, or avi fallback) 523 string actualTemp = tempPath; 524 string ext = ".mp4"; 525 if (!System.IO.File.Exists(actualTemp)) 526 { 527 // Check if WMV was used 528 string wmvTemp = tempPath.Substring(0, tempPath.Length - 4) + ".wmv"; 529 if (System.IO.File.Exists(wmvTemp)) 530 { 531 actualTemp = wmvTemp; 532 ext = ".wmv"; 533 } 534 else 535 { 536 // Check if AVI fallback was used 537 string aviTemp = tempPath.Substring(0, tempPath.Length - 4) + ".avi"; 538 if (System.IO.File.Exists(aviTemp)) 539 { 540 actualTemp = aviTemp; 541 ext = ".avi"; 542 } 543 } 544 } 545 546 string filter; 547 if (ext == ".wmv") 548 filter = "WMV Video|*.wmv|All Files|*.*"; 549 else if (ext == ".mp4") 550 filter = "MP4 Video|*.mp4|All Files|*.*"; 551 else 552 filter = "AVI Video|*.avi|All Files|*.*"; 553 554 var sfd = new SaveFileDialog 555 { 556 Filter = filter, 557 Title = "Save Video Recording", 558 FileName = "Recording_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ext 559 }; 560 561 if (sfd.ShowDialog() == DialogResult.OK) 562 { 563 try 564 { 565 if (System.IO.File.Exists(sfd.FileName)) System.IO.File.Delete(sfd.FileName); 566 System.IO.File.Move(actualTemp, sfd.FileName); 567 } 568 catch (Exception ex) 569 { 570 MessageBox.Show("Failed to save video: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 571 } 572 } 573 else 574 { 575 try { if (System.IO.File.Exists(actualTemp)) System.IO.File.Delete(actualTemp); } catch { } 576 } 577 578 recorder.Dispose(); 579 recorder = null; 580 recordingOverlay = null; 581 }; 582 recordingOverlay.Start(); 583 } 584 catch (Exception ex) 585 { 586 MessageBox.Show("Failed to start recording: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 587 if (recorder != null) { recorder.Dispose(); recorder = null; } 588 } 589 } 590 591 private void OnIdle(object s, EventArgs e) 592 { 593 if (!active) return; 594 595 // Dynamic Ctrl detection to toggle video-mode colors while the chord is held. 596 // (Shift is part of the activation chord now, so Ctrl selects photo vs video.) 597 bool currentCtrl = CtrlHeld(); 598 if (currentCtrl != isRecordingMode) 599 { 600 isRecordingMode = currentCtrl; 601 overlay.IsVideoMode = isRecordingMode; 602 } 603 604 // Don't update auto-detect while in other modes 605 if (overlay.IsDrawingManualRect || overlay.IsWindowSelectMode || overlay.IsMediaParseMode) return; 606 607 // Cursor.Position is in screen coords; the detector/frozen bitmap are in capture-local 608 // coords (origin = captureBounds top-left, which is negative on left/top secondary monitors). 609 var curScreen = Cursor.Position; 610 var cur = new Point(curScreen.X - captureBounds.X, curScreen.Y - captureBounds.Y); 611 if (cur == lastCur) return; 612 lastCur = cur; 613 expandLevel = 0; // Reset expand level on mouse move 614 615 var path = detector.Detect(cur, expandLevel); 616 if (currentPath != null) currentPath.Dispose(); 617 currentPath = path; 618 overlay.SetRegion(path, expandLevel); 619 } 620 621 private void Overlay_WheelScrolled(object sender, int delta) 622 { 623 if (!active) return; 624 625 if (delta > 0) 626 { 627 expandLevel++; 628 } 629 else if (delta < 0 && expandLevel > 0) 630 { 631 expandLevel--; 632 } 633 634 var path = detector.Detect(lastCur, expandLevel); 635 if (currentPath != null) currentPath.Dispose(); 636 currentPath = path; 637 overlay.SetRegion(path, expandLevel); 638 } 639 640 private void Overlay_ManualRectSelected(object sender, Rectangle rect) 641 { 642 if (!active || frozen == null) return; 643 644 // Capture data before Deactivate() clears it 645 var frozenCopy = (Bitmap)frozen.Clone(); 646 647 // Ignore activation key until released 648 ignoreActivationUntilRelease = true; 649 Deactivate(); 650 651 if (isRecordingMode) 652 { 653 frozenCopy.Dispose(); 654 StartVideoRecording(ToScreenRect(rect)); 655 ignoreActivationUntilRelease = false; 656 } 657 else 658 { 659 // Create path from manually selected rectangle 660 var manualPath = new GraphicsPath(); 661 manualPath.AddRectangle(rect); 662 663 var editor = new EditorForm(frozenCopy, manualPath); 664 editor.FormClosed += delegate { 665 frozenCopy.Dispose(); 666 manualPath.Dispose(); 667 ignoreActivationUntilRelease = false; // Reset so next Alt works immediately 668 }; 669 editor.Show(); 670 } 671 } 672 673 private void Overlay_WindowSelected(object sender, Rectangle rect) 674 { 675 if (!active || frozen == null) return; 676 677 // Capture data before Deactivate() clears it 678 var frozenCopy = (Bitmap)frozen.Clone(); 679 680 // Ignore activation key until released 681 ignoreActivationUntilRelease = true; 682 Deactivate(); 683 684 if (isRecordingMode) 685 { 686 frozenCopy.Dispose(); 687 StartVideoRecording(ToScreenRect(rect)); 688 ignoreActivationUntilRelease = false; 689 } 690 else 691 { 692 // Create path from window rectangle 693 var windowPath = new GraphicsPath(); 694 windowPath.AddRectangle(rect); 695 696 var editor = new EditorForm(frozenCopy, windowPath); 697 editor.FormClosed += delegate { 698 frozenCopy.Dispose(); 699 windowPath.Dispose(); 700 ignoreActivationUntilRelease = false; // Reset so next Alt works immediately 701 }; 702 editor.Show(); 703 } 704 } 705 706 private void Overlay_AddRegionRequested(Point pt) 707 { 708 if (!active) return; 709 710 // Detect region at this point and add it 711 var path = detector.Detect(pt, 0); 712 if (path != null) 713 { 714 overlay.AddPath(path); 715 } 716 } 717 718 private GraphicsPath Overlay_MediaParseRegionRequested(Point pt) 719 { 720 // Use the same detector to find region at point for media parser 721 if (!active) return null; 722 return detector.Detect(pt, 0); 723 } 724 725 private void Overlay_MediaParseRequested(Point pt) 726 { 727 if (!active) return; 728 729 // Parse media at this point 730 var mediaItems = MediaParser.ParseMediaAt(pt, overlay.Handle); 731 732 // Deactivate overlay first 733 Deactivate(); 734 735 // Show media download dialog 736 if (mediaItems.Count > 0) 737 { 738 var dialog = new MediaDownloadDialog(mediaItems); 739 dialog.Show(); 740 } 741 else 742 { 743 MessageBox.Show("No media found at this location.\n\nTry clicking directly on an image, video, or audio element.", 744 "Media Parser", MessageBoxButtons.OK, MessageBoxIcon.Information); 745 } 746 } 747 748 private void Overlay_MouseClick(object sender, MouseEventArgs e) 749 { 750 // Ignore clicks if editor is already open 751 if (EditorForm.IsOpen) return; 752 753 // Ignore clicks if we just finished drawing a manual rectangle 754 if (e.Button != MouseButtons.Left) return; 755 if (!active || currentPath == null || frozen == null) return; 756 757 // Capture data before Deactivate() clears it 758 var pathCopy = (GraphicsPath)currentPath.Clone(); 759 var bounds = Rectangle.Round(pathCopy.GetBounds()); 760 var frozenCopy = (Bitmap)frozen.Clone(); 761 762 // Ignore activation key until released (prevents re-activation while key still held) 763 ignoreActivationUntilRelease = true; 764 Deactivate(); 765 766 if (isRecordingMode) 767 { 768 pathCopy.Dispose(); 769 frozenCopy.Dispose(); 770 StartVideoRecording(ToScreenRect(bounds)); 771 ignoreActivationUntilRelease = false; 772 } 773 else 774 { 775 var editor = new EditorForm(frozenCopy, pathCopy); 776 editor.FormClosed += delegate { 777 frozenCopy.Dispose(); 778 pathCopy.Dispose(); 779 ignoreActivationUntilRelease = false; // Reset so next Alt works immediately 780 }; 781 editor.Show(); 782 } 783 } 784 785 public void ResetSearchOpen() { searchOpen = false; } 786 787 public void ShowSettings() 788 { 789 var dialog = new SettingsDialog(); 790 dialog.ShowDialog(); 791 } 792 793 public void Dispose() 794 { 795 Deactivate(); 796 if (hook != IntPtr.Zero) WinApi.UnhookWindowsHookEx(hook); 797 if (mouseHook != IntPtr.Zero) WinApi.UnhookWindowsHookEx(mouseHook); 798 if (overlay != null) overlay.Dispose(); 799 } 800 } 801}