1using System; 2using System.Collections.Generic; 3using System.Drawing; 4using System.Drawing.Drawing2D; 5using System.Drawing.Text; 6using System.Runtime.InteropServices; 7using System.Windows.Forms; 8using WindowCapture.Helpers; 9using WindowCapture.Models; 10using WindowCapture.Native; 11 12namespace WindowCapture.UI 13{ 14 public class ClipboardForm : Form 15 { 16 // Don't steal focus from the text field when showing 17 protected override bool ShowWithoutActivation { get { return true; } } 18 19 public enum ClipItemType { Text, Image } 20 21 public class ClipItem 22 { 23 public ClipItemType Type; 24 public string Text; 25 public Bitmap Image; 26 public DateTime Time; 27 public string Preview; 28 public string ImageInfo; // "1920x1080 PNG 2.4MB 16:9" 29 } 30 31 private static string BuildImageInfo(System.Drawing.Image img) 32 { 33 int w = img.Width, h = img.Height; 34 // Format 35 string fmt = "BMP"; 36 if (img.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Png)) fmt = "PNG"; 37 else if (img.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Jpeg)) fmt = "JPEG"; 38 else if (img.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Gif)) fmt = "GIF"; 39 else if (img.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Bmp)) fmt = "BMP"; 40 else if (img.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Tiff)) fmt = "TIFF"; 41 // Size estimate (bitmap in memory) 42 long bytes = (long)w * h * 4; 43 string size = bytes < 1024 ? bytes + "B" : 44 bytes < 1048576 ? (bytes / 1024) + "KB" : 45 (bytes / 1048576f).ToString("0.#") + "MB"; 46 // Aspect ratio 47 int gcd = GCD(w, h); 48 string ratio = (w / gcd) + ":" + (h / gcd); 49 // Simplify large ratios 50 if (w / gcd > 32) ratio = ((float)w / h).ToString("0.##") + ":1"; 51 52 return w + "x" + h + " " + fmt + " " + size + " " + ratio; 53 } 54 55 private static int GCD(int a, int b) { while (b != 0) { int t = b; b = a % b; a = t; } return a; } 56 57 // Static history 58 private static List<ClipItem> history = new List<ClipItem>(); 59 private static int maxItems = 50; 60 private static Form listenerForm; 61 private static bool listenerInstalled; 62 private static bool ignoreNextCapture; // skip our own SetClipboard 63 64 // Instance 65 private int selectedIndex = -1; // -1 = nothing selected 66 private int scrollOffset = 0; 67 private const int ItemHeight = 48; 68 private const int HeaderHeight = 0; // no header 69 private const int FormW = 380; 70 private const int MaxVisibleItems = 8; 71 private PreviewForm previewPopup = null; // hover preview window 72 private float fadeProgress = 0f; 73 private float fadeTarget = 1f; 74 private Timer fadeTimer; 75 private bool closing; 76 77 // Drag 78 private bool dragStarted; 79 private Point dragStartPoint; 80 private int dragItemIndex = -1; 81 82 // GDI 83 private Font fontTitle; 84 private Font fontPreview; 85 private Font fontTime; 86 private SolidBrush brushText; 87 private SolidBrush brushDim; 88 private SolidBrush brushAccent; 89 private Pen penBorder; 90 91 [DllImport("user32.dll")] 92 private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); 93 private const int SW_SHOWNOACTIVATE = 4; 94 95 public ClipboardForm() 96 { 97 FormBorderStyle = FormBorderStyle.None; 98 ShowInTaskbar = false; 99 TopMost = true; 100 StartPosition = FormStartPosition.Manual; 101 BackColor = Color.FromArgb(Settings.BlurTintColor.R, Settings.BlurTintColor.G, Settings.BlurTintColor.B); 102 DoubleBuffered = true; 103 SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true); 104 105 int itemCount = Math.Min(history.Count, MaxVisibleItems); 106 if (itemCount < 1) itemCount = 1; 107 int h = itemCount * ItemHeight + HeaderHeight + 4; // 4px padding 108 Size = new Size(FormW, h); 109 110 Point pos = GetCaretScreenPos(); 111 var screen = Screen.FromPoint(pos).WorkingArea; 112 int x = pos.X + 8; 113 int y = pos.Y + 20; 114 if (x + FormW > screen.Right) x = screen.Right - FormW - 8; 115 if (y + h > screen.Bottom) y = pos.Y - h - 4; 116 if (x < screen.Left) x = screen.Left + 4; 117 if (y < screen.Top) y = screen.Top + 4; 118 Location = new Point(x, y); 119 120 Opacity = 0; 121 fadeTimer = new Timer { Interval = 16 }; 122 fadeTimer.Tick += FadeTimer_Tick; 123 124 Load += (s, e) => 125 { 126 BlurHelper.Apply(Handle); 127 WinApi.TryEnableRoundedCorners(Handle); 128 fadeTimer.Start(); 129 }; 130 } 131 132 // Show without stealing focus 133 public void ShowNoActivate() 134 { 135 Show(); 136 // Don't take focus away from the text field user is typing in 137 } 138 139 private void InitGdi() 140 { 141 if (fontTitle != null) return; 142 fontTitle = new Font("Segoe UI Semibold", 12f, FontStyle.Regular, GraphicsUnit.Pixel); 143 fontPreview = new Font("Segoe UI", 11f, FontStyle.Regular, GraphicsUnit.Pixel); 144 fontTime = new Font("Segoe UI", 10f, FontStyle.Regular, GraphicsUnit.Pixel); 145 brushText = new SolidBrush(Color.FromArgb(210, 218, 235)); 146 brushDim = new SolidBrush(Color.FromArgb(100, 110, 130)); 147 brushAccent = new SolidBrush(Color.FromArgb(70, 130, 200)); 148 penBorder = new Pen(Color.FromArgb(55, 60, 72)); 149 } 150 151 // ===== Clipboard listener ===== 152 public static void InstallListener() 153 { 154 if (listenerInstalled) return; 155 listenerInstalled = true; 156 listenerForm = new ClipboardListenerForm(); 157 listenerForm.ShowInTaskbar = false; 158 listenerForm.FormBorderStyle = FormBorderStyle.None; 159 listenerForm.Size = new Size(1, 1); 160 listenerForm.StartPosition = FormStartPosition.Manual; 161 listenerForm.Location = new Point(-2000, -2000); 162 listenerForm.Show(); 163 listenerForm.Hide(); 164 } 165 166 private class ClipboardListenerForm : Form 167 { 168 public ClipboardListenerForm() 169 { 170 Load += (s, e) => WinApi.AddClipboardFormatListener(Handle); 171 FormClosing += (s, e) => WinApi.RemoveClipboardFormatListener(Handle); 172 } 173 174 protected override void WndProc(ref Message m) 175 { 176 if (m.Msg == WinApi.WM_CLIPBOARDUPDATE) 177 { 178 if (!ignoreNextCapture) 179 CaptureClipboard(); 180 ignoreNextCapture = false; 181 m.Result = IntPtr.Zero; 182 return; 183 } 184 base.WndProc(ref m); 185 } 186 } 187 188 private static void CaptureClipboard() 189 { 190 try 191 { 192 IDataObject data = Clipboard.GetDataObject(); 193 if (data == null) return; 194 195 // Check image first (some apps set both text and image) 196 if (data.GetDataPresent(DataFormats.Bitmap)) 197 { 198 // 'using' so the clipboard-provided Image is always freed (it was leaked before, 199 // exhausting GDI handles under repeated copy/screenshot). 200 using (var img = data.GetData(DataFormats.Bitmap) as System.Drawing.Image) 201 if (img != null) 202 { 203 for (int di = 0; di < history.Count; di++) 204 if (history[di].Type == ClipItemType.Image && history[di].Image != null && 205 history[di].Image.Width == img.Width && history[di].Image.Height == img.Height) 206 return; 207 208 var item = new ClipItem(); 209 item.Type = ClipItemType.Image; 210 item.Image = new Bitmap(img); 211 item.Time = DateTime.Now; 212 item.ImageInfo = BuildImageInfo(img); 213 item.Preview = item.ImageInfo; 214 history.Insert(0, item); 215 TrimHistory(); 216 return; 217 } 218 } 219 220 // Then check DIB (screenshots often use this format) 221 if (data.GetDataPresent(DataFormats.Dib)) 222 { 223 try 224 { 225 using (var img = Clipboard.GetImage()) 226 if (img != null) 227 { 228 for (int di = 0; di < history.Count; di++) 229 if (history[di].Type == ClipItemType.Image && history[di].Image != null && 230 history[di].Image.Width == img.Width && history[di].Image.Height == img.Height) 231 return; 232 233 var item = new ClipItem(); 234 item.Type = ClipItemType.Image; 235 item.Image = new Bitmap(img); 236 item.Time = DateTime.Now; 237 item.ImageInfo = BuildImageInfo(img); 238 item.Preview = item.ImageInfo; 239 history.Insert(0, item); 240 TrimHistory(); 241 return; 242 } 243 } 244 catch { } 245 } 246 247 // Text 248 if (data.GetDataPresent(DataFormats.UnicodeText) || data.GetDataPresent(DataFormats.Text)) 249 { 250 string text = data.GetData(DataFormats.UnicodeText) as string; 251 if (text == null) text = data.GetData(DataFormats.Text) as string; 252 if (!string.IsNullOrEmpty(text)) 253 { 254 // Check for duplicate anywhere in history — skip if found 255 for (int di = 0; di < history.Count; di++) 256 if (history[di].Type == ClipItemType.Text && history[di].Text == text) 257 return; 258 259 string preview = text.Replace("\r\n", " ").Replace("\n", " "); 260 if (preview.Length > 80) preview = preview.Substring(0, 80) + "..."; 261 262 var item = new ClipItem(); 263 item.Type = ClipItemType.Text; 264 item.Text = text; 265 item.Time = DateTime.Now; 266 item.Preview = preview; 267 history.Insert(0, item); 268 TrimHistory(); 269 } 270 } 271 } 272 catch { } 273 } 274 275 private static void TrimHistory() 276 { 277 while (history.Count > maxItems) 278 { 279 var old = history[history.Count - 1]; 280 if (old.Image != null) old.Image.Dispose(); 281 history.RemoveAt(history.Count - 1); 282 } 283 } 284 285 private Point GetCaretScreenPos() 286 { 287 try 288 { 289 IntPtr fg = WinApi.GetForegroundWindow(); 290 if (fg != IntPtr.Zero) 291 { 292 uint pid; 293 uint tid = WinApi.GetWindowThreadProcessId(fg, out pid); 294 var info = new WinApi.GUITHREADINFO(); 295 info.cbSize = Marshal.SizeOf(info); 296 if (WinApi.GetGUIThreadInfo(tid, ref info) && info.hwndCaret != IntPtr.Zero) 297 { 298 var pt = new WinApi.POINT(info.rcCaret.Left, info.rcCaret.Bottom); 299 WinApi.ClientToScreen(info.hwndCaret, ref pt); 300 return new Point(pt.X, pt.Y); 301 } 302 } 303 } 304 catch { } 305 WinApi.POINT mp; 306 WinApi.GetCursorPos(out mp); 307 return new Point(mp.X, mp.Y); 308 } 309 310 // ===== Fade ===== 311 private void FadeTimer_Tick(object s, EventArgs e) 312 { 313 float d = fadeTarget - fadeProgress; 314 if (Math.Abs(d) > 0.01f) 315 { 316 fadeProgress += d * 0.22f; 317 if (Math.Abs(fadeTarget - fadeProgress) < 0.01f) 318 fadeProgress = fadeTarget; 319 float eased = fadeProgress * fadeProgress * (3f - 2f * fadeProgress); 320 Opacity = eased; 321 Invalidate(); 322 } 323 else 324 { 325 fadeProgress = fadeTarget; 326 Opacity = fadeTarget; 327 if (fadeTarget <= 0) { fadeTimer.Stop(); Close(); return; } 328 fadeTimer.Stop(); 329 Invalidate(); 330 } 331 } 332 333 public void FadeOut() 334 { 335 if (closing) return; 336 if (saveDialogOpen) return; // don't close while save dialog is open 337 closing = true; 338 HidePreview(); 339 if (previewDelayTimer != null) previewDelayTimer.Stop(); 340 fadeTarget = 0f; 341 fadeTimer.Start(); 342 } 343 344 // Re-show form that's in the middle of fading out 345 public void ReviveFromFadeOut() 346 { 347 closing = false; 348 fadeTarget = 1f; 349 if (!fadeTimer.Enabled) fadeTimer.Start(); 350 } 351 352 public bool IsFadingOut { get { return closing; } } 353 354 // ===== Keyboard ===== 355 protected override bool ProcessCmdKey(ref Message msg, Keys keyData) 356 { 357 if (keyData == Keys.Escape) { FadeOut(); return true; } 358 if (keyData == Keys.Up) 359 { 360 if (selectedIndex > 0) selectedIndex--; 361 EnsureVisible(); Invalidate(); return true; 362 } 363 if (keyData == Keys.Down) 364 { 365 if (selectedIndex < history.Count - 1) selectedIndex++; 366 EnsureVisible(); Invalidate(); return true; 367 } 368 if (keyData == Keys.Enter || keyData == Keys.Space) 369 { 370 SelectItem(selectedIndex); return true; 371 } 372 return base.ProcessCmdKey(ref msg, keyData); 373 } 374 375 private void EnsureVisible() 376 { 377 if (selectedIndex < scrollOffset) scrollOffset = selectedIndex; 378 if (selectedIndex >= scrollOffset + MaxVisibleItems) scrollOffset = selectedIndex - MaxVisibleItems + 1; 379 } 380 381 // ===== Mouse ===== 382 private bool IsSaveButtonHit(int idx, int mouseX, int mouseY) 383 { 384 if (idx < 0 || idx >= history.Count) return false; 385 if (history[idx].Type != ClipItemType.Image || history[idx].Image == null) return false; 386 int btnW = 40, btnH = 18; 387 int btnX = Width - btnW - 8; 388 int iy = HeaderHeight + 2 + (idx - scrollOffset) * ItemHeight; 389 int btnY = iy + ItemHeight - btnH - 6; 390 return mouseX >= btnX && mouseX <= btnX + btnW && mouseY >= btnY && mouseY <= btnY + btnH; 391 } 392 393 private bool saveDialogOpen = false; 394 private int saveFlashIdx = -1; // which item's Save button is flashing green 395 private long saveFlashTime = 0; 396 397 private void SaveImageQuick(int idx) 398 { 399 if (idx < 0 || idx >= history.Count) return; 400 var item = history[idx]; 401 if (item.Type != ClipItemType.Image || item.Image == null) return; 402 try 403 { 404 string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); 405 string name = "clipboard_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".png"; 406 string path = System.IO.Path.Combine(desktop, name); 407 item.Image.Save(path, System.Drawing.Imaging.ImageFormat.Png); 408 // Flash button green 409 saveFlashIdx = idx; 410 saveFlashTime = Environment.TickCount; 411 Invalidate(); 412 // Reset after 800ms 413 var resetTimer = new Timer { Interval = 800 }; 414 resetTimer.Tick += (s, e) => { resetTimer.Stop(); resetTimer.Dispose(); saveFlashIdx = -1; if (!IsDisposed) Invalidate(); }; 415 resetTimer.Start(); 416 } 417 catch { } 418 } 419 420 private void SaveImageDialog(int idx) 421 { 422 if (idx < 0 || idx >= history.Count) return; 423 var item = history[idx]; 424 if (item.Type != ClipItemType.Image || item.Image == null) return; 425 try 426 { 427 saveDialogOpen = true; 428 var dlg = new SaveFileDialog(); 429 dlg.Filter = "PNG|*.png|JPEG|*.jpg|BMP|*.bmp"; 430 dlg.FileName = "clipboard_" + DateTime.Now.ToString("yyyyMMdd_HHmmss"); 431 dlg.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); 432 if (dlg.ShowDialog() == DialogResult.OK) 433 { 434 var fmt = System.Drawing.Imaging.ImageFormat.Png; 435 if (dlg.FileName.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)) fmt = System.Drawing.Imaging.ImageFormat.Jpeg; 436 else if (dlg.FileName.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase)) fmt = System.Drawing.Imaging.ImageFormat.Bmp; 437 item.Image.Save(dlg.FileName, fmt); 438 saveFlashIdx = idx; 439 saveFlashTime = Environment.TickCount; 440 Invalidate(); 441 var resetTimer = new Timer { Interval = 800 }; 442 resetTimer.Tick += (s, e) => { resetTimer.Stop(); resetTimer.Dispose(); saveFlashIdx = -1; if (!IsDisposed) Invalidate(); }; 443 resetTimer.Start(); 444 } 445 saveDialogOpen = false; 446 // Close clipboard form after save dialog 447 FadeOut(); 448 } 449 catch { saveDialogOpen = false; } 450 } 451 452 protected override void OnMouseDown(MouseEventArgs e) 453 { 454 base.OnMouseDown(e); 455 if (e.Button == MouseButtons.Left) 456 { 457 int idx = HitTest(e.Y); 458 // Check Save button click 459 if (IsSaveButtonHit(idx, e.X, e.Y)) 460 { 461 SaveImageQuick(idx); 462 return; 463 } 464 if (idx >= 0 && idx < history.Count) 465 { 466 dragStarted = false; 467 dragStartPoint = e.Location; 468 dragItemIndex = idx; 469 selectedIndex = idx; 470 Invalidate(); 471 } 472 } 473 else if (e.Button == MouseButtons.Right) 474 { 475 int idx = HitTest(e.Y); 476 if (IsSaveButtonHit(idx, e.X, e.Y)) 477 { 478 SaveImageDialog(idx); 479 return; 480 } 481 } 482 } 483 484 protected override void OnMouseUp(MouseEventArgs e) 485 { 486 base.OnMouseUp(e); 487 if (e.Button == MouseButtons.Left && !dragStarted && dragItemIndex >= 0) 488 { 489 SelectItem(dragItemIndex); 490 } 491 dragItemIndex = -1; 492 dragStarted = false; 493 } 494 495 protected override void OnMouseMove(MouseEventArgs e) 496 { 497 base.OnMouseMove(e); 498 if (e.Button == MouseButtons.Left && dragItemIndex >= 0 && !dragStarted) 499 { 500 if (Math.Abs(e.X - dragStartPoint.X) > 6 || Math.Abs(e.Y - dragStartPoint.Y) > 6) 501 { 502 dragStarted = true; 503 StartDragDrop(dragItemIndex); 504 return; 505 } 506 } 507 int idx = HitTest(e.Y); 508 if (idx >= 0 && idx < history.Count) 509 { 510 if (idx != selectedIndex) 511 { 512 selectedIndex = idx; 513 ShowPreview(idx); 514 } 515 Invalidate(); // always repaint for Save button hover 516 } 517 else if (selectedIndex >= 0) 518 { 519 selectedIndex = -1; 520 Invalidate(); 521 HidePreview(); 522 } 523 } 524 525 protected override void OnMouseLeave(EventArgs e) 526 { 527 base.OnMouseLeave(e); 528 // Don't deselect/hide if mouse moved to preview popup 529 if (previewPopup != null && !previewPopup.IsDisposed) 530 { 531 WinApi.POINT mp; WinApi.GetCursorPos(out mp); 532 var r = previewPopup.Bounds; 533 if (mp.X >= r.Left && mp.X <= r.Right && mp.Y >= r.Top && mp.Y <= r.Bottom) 534 return; // mouse is over preview — keep selection 535 } 536 if (selectedIndex >= 0) { selectedIndex = -1; Invalidate(); } 537 HidePreview(); 538 } 539 540 private Timer previewDelayTimer; 541 private int pendingPreviewIdx = -1; 542 543 private void ShowPreview(int idx) 544 { 545 // Cancel any pending preview 546 if (previewDelayTimer != null) previewDelayTimer.Stop(); 547 HidePreview(); 548 pendingPreviewIdx = idx; 549 550 // Delay 400ms before showing 551 if (previewDelayTimer == null) 552 { 553 previewDelayTimer = new Timer { Interval = 250 }; 554 previewDelayTimer.Tick += (s, e) => 555 { 556 previewDelayTimer.Stop(); 557 ShowPreviewNow(pendingPreviewIdx); 558 }; 559 } 560 previewDelayTimer.Start(); 561 } 562 563 private void ShowPreviewNow(int idx) 564 { 565 HidePreview(); 566 if (idx < 0 || idx >= history.Count) return; 567 if (closing || IsDisposed) return; 568 var item = history[idx]; 569 570 var pop = new PreviewForm(); 571 pop.FormBorderStyle = FormBorderStyle.None; 572 pop.ShowInTaskbar = false; 573 pop.TopMost = true; 574 pop.StartPosition = FormStartPosition.Manual; 575 pop.BackColor = Color.FromArgb(30, 30, 35); 576 577 int pw, ph; 578 if (item.Type == ClipItemType.Image && item.Image != null) 579 { 580 float scale = Math.Min(300f / Math.Max(1, item.Image.Width), 300f / Math.Max(1, item.Image.Height)); 581 scale = Math.Min(scale, 1f); 582 pw = (int)(item.Image.Width * scale) + 8; 583 ph = (int)(item.Image.Height * scale) + 8; 584 } 585 else 586 { 587 pw = 360; 588 string text = item.Text ?? ""; 589 // Count wrapped lines (approx 50 chars per line at 9pt in 350px) 590 int charLines = (text.Length + 49) / 50; 591 int newlineLines = text.Split('\n').Length; 592 int lines = Math.Max(charLines, newlineLines); 593 lines = Math.Min(lines, 15); 594 ph = Math.Max(50, lines * 16 + 20); 595 ph = Math.Min(ph, 320); 596 } 597 598 pop.Size = new Size(pw, ph); 599 int px = this.Right + 4; 600 int py = this.Top + (idx - scrollOffset) * ItemHeight; 601 var scr = Screen.FromControl(this).WorkingArea; 602 if (px + pw > scr.Right) px = this.Left - pw - 4; 603 if (py + ph > scr.Bottom) py = scr.Bottom - ph; 604 if (py < scr.Top) py = scr.Top; 605 pop.Location = new Point(px, py); 606 607 pop.Paint += (s, pe) => 608 { 609 var g = pe.Graphics; 610 g.SmoothingMode = SmoothingMode.HighSpeed; 611 g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; 612 g.Clear(Color.FromArgb(30, 30, 35)); 613 using (var pen = new Pen(Color.FromArgb(60, 100, 100, 100))) 614 g.DrawRectangle(pen, 0, 0, pop.Width - 1, pop.Height - 1); 615 616 if (item.Type == ClipItemType.Image && item.Image != null) 617 { 618 try { g.DrawImage(item.Image, 4, 4, pop.Width - 8, pop.Height - 8); } catch { } 619 } 620 else 621 { 622 string text = item.Text ?? ""; 623 using (var f = new Font("Segoe UI", 9f)) 624 using (var b = new SolidBrush(Color.FromArgb(220, 220, 220))) 625 { 626 var sf = new StringFormat(); 627 sf.Trimming = StringTrimming.EllipsisWord; 628 g.DrawString(text, f, b, new RectangleF(8, 8, pop.Width - 16, pop.Height - 16), sf); 629 } 630 } 631 }; 632 633 previewPopup = pop; 634 try 635 { 636 if (IsDisposed || closing) { pop.Dispose(); previewPopup = null; return; } 637 pop.Location = new Point(px, py); 638 pop.Size = new Size(pw, ph); 639 pop.Show(); 640 pop.StartFadeIn(); 641 } 642 catch { try { pop.Dispose(); } catch { } previewPopup = null; } 643 } 644 645 private void HidePreview() 646 { 647 try { if (previewDelayTimer != null) previewDelayTimer.Stop(); } catch { } 648 pendingPreviewIdx = -1; 649 if (previewPopup != null) 650 { 651 try 652 { 653 if (!previewPopup.IsDisposed) 654 previewPopup.StartFadeOut(); // smooth fade out, auto-closes 655 } 656 catch { try { previewPopup.Dispose(); } catch { } } 657 previewPopup = null; 658 } 659 } 660 661 protected override void OnMouseWheel(MouseEventArgs e) 662 { 663 base.OnMouseWheel(e); 664 scrollOffset = Math.Max(0, Math.Min(history.Count - MaxVisibleItems, scrollOffset + (e.Delta > 0 ? -1 : 1))); 665 Invalidate(); 666 } 667 668 private int HitTest(int y) { return y < HeaderHeight + 2 ? -1 : (y - HeaderHeight - 2) / ItemHeight + scrollOffset; } 669 670 // ===== Select item ===== 671 private void SelectItem(int idx) 672 { 673 if (idx < 0 || idx >= history.Count) return; 674 SetClipboardFromItem(idx); 675 FadeOut(); 676 } 677 678 private void SetClipboardFromItem(int idx) 679 { 680 if (idx < 0 || idx >= history.Count) return; 681 var item = history[idx]; 682 try 683 { 684 ignoreNextCapture = true; 685 if (item.Type == ClipItemType.Text) 686 Clipboard.SetText(item.Text); 687 else if (item.Type == ClipItemType.Image && item.Image != null) 688 Clipboard.SetImage(item.Image); 689 } 690 catch { } 691 } 692 693 /// <summary>Set clipboard to whatever item the mouse is currently hovering over. 694 /// Returns true if an item was selected, false if mouse was not over any item.</summary> 695 public bool SelectHoveredItem() 696 { 697 // Use whatever is currently selected (highlighted by hover) 698 if (selectedIndex >= 0 && selectedIndex < history.Count) 699 { 700 SetClipboardFromItem(selectedIndex); 701 return true; 702 } 703 return false; 704 } 705 706 // ===== Drag & drop ===== 707 private void StartDragDrop(int idx) 708 { 709 if (idx < 0 || idx >= history.Count) return; 710 var item = history[idx]; 711 712 DataObject data = new DataObject(); 713 string tempFile = null; 714 715 if (item.Type == ClipItemType.Text && item.Text != null) 716 { 717 data.SetText(item.Text); 718 } 719 else if (item.Type == ClipItemType.Image && item.Image != null) 720 { 721 // Save image to temp file for proper drag & drop 722 try 723 { 724 tempFile = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "wc_clip_" + DateTime.Now.Ticks + ".png"); 725 item.Image.Save(tempFile, System.Drawing.Imaging.ImageFormat.Png); 726 var files = new System.Collections.Specialized.StringCollection(); 727 files.Add(tempFile); 728 data.SetFileDropList(files); 729 data.SetImage(item.Image); // also set image for apps that support it 730 } 731 catch { } 732 } 733 734 closing = true; 735 Hide(); 736 737 DoDragDrop(data, DragDropEffects.Copy | DragDropEffects.Move); 738 739 // Cleanup temp file after a delay 740 if (tempFile != null) 741 { 742 try 743 { 744 Timer cleanup = new Timer(); cleanup.Interval = 5000; 745 cleanup.Tick += delegate { cleanup.Stop(); cleanup.Dispose(); try { System.IO.File.Delete(tempFile); } catch { } }; 746 cleanup.Start(); 747 } 748 catch { } 749 } 750 751 if (!IsDisposed) Close(); 752 } 753 754 // ===== Paint ===== 755 protected override void OnPaintBackground(PaintEventArgs e) { } 756 757 protected override void OnPaint(PaintEventArgs e) 758 { 759 InitGdi(); 760 var g = e.Graphics; 761 g.SmoothingMode = SmoothingMode.HighSpeed; 762 g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; 763 int w = Width, h = Height; 764 765 g.Clear(Color.FromArgb(Settings.BlurTintAlpha, Settings.BlurTintColor)); 766 767 int ni = Settings.NoiseIntensity; 768 if (ni > 0) 769 { 770 var ns = GetNoise(ni); 771 if (ns != null) using (var tb = new TextureBrush(ns, WrapMode.Tile)) g.FillRectangle(tb, 0, 0, w, h); 772 } 773 774 g.DrawRectangle(penBorder, 0, 0, w - 1, h - 1); 775 776 if (history.Count == 0) 777 { 778 g.DrawString("Empty", fontPreview, brushDim, w / 2 - 20, 16); 779 return; 780 } 781 782 int yOff = HeaderHeight + 2; 783 for (int i = scrollOffset; i < Math.Min(history.Count, scrollOffset + MaxVisibleItems); i++) 784 { 785 var item = history[i]; 786 int iy = yOff + (i - scrollOffset) * ItemHeight; 787 788 if (i == selectedIndex) 789 { 790 using (var sb = new SolidBrush(Color.FromArgb(30, 70, 130, 200))) g.FillRectangle(sb, 1, iy, w - 2, ItemHeight); 791 using (var lp = new Pen(Color.FromArgb(80, 70, 130, 200), 2)) g.DrawLine(lp, 1, iy, 1, iy + ItemHeight); 792 } 793 794 int tx = 8; 795 string ts = FormatTime(item.Time); 796 var tsz = g.MeasureString(ts, fontTime); 797 798 if (item.Type == ClipItemType.Image && item.Image != null) 799 { 800 int thumbH = ItemHeight - 6; 801 int thumbW = Math.Min(56, (int)((float)item.Image.Width / Math.Max(1, item.Image.Height) * thumbH)); 802 try { g.DrawImage(item.Image, tx, iy + 3, thumbW, thumbH); } catch { } 803 tx += thumbW + 8; 804 var sf2 = new StringFormat { Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap }; 805 // Line 1: resolution 806 string info1 = item.Image.Width + "x" + item.Image.Height; 807 // Line 2: format + size + ratio 808 string info2 = item.ImageInfo ?? ""; 809 int sp = info2.IndexOf(" "); 810 if (sp > 0) info2 = info2.Substring(sp + 2); else info2 = ""; 811 g.DrawString(info1, fontPreview, brushText, new RectangleF(tx, iy + 4, w - tx - 70, 18), sf2); 812 if (info2.Length > 0) 813 g.DrawString(info2, fontTime, brushDim, new RectangleF(tx, iy + 22, w - tx - 70, 16), sf2); 814 g.DrawString(ts, fontTime, brushDim, w - tsz.Width - 8, iy + 4); 815 // Save button 816 int btnW = 40, btnH = 18; 817 int btnX = w - btnW - 8, btnY = iy + ItemHeight - btnH - 6; 818 bool hoverSave = false; 819 bool isFlash = (saveFlashIdx == i); 820 if (i == selectedIndex) 821 { 822 WinApi.POINT mpp; WinApi.GetCursorPos(out mpp); 823 Point cp2 = PointToClient(new Point(mpp.X, mpp.Y)); 824 hoverSave = cp2.X >= btnX && cp2.X <= btnX + btnW && cp2.Y >= btnY && cp2.Y <= btnY + btnH; 825 } 826 Color btnColor = isFlash ? Color.FromArgb(100, 40, 180, 60) : 827 hoverSave ? Color.FromArgb(60, 100, 180, 255) : 828 Color.FromArgb(30, 100, 150, 200); 829 Color borderColor = isFlash ? Color.FromArgb(120, 60, 220, 80) : 830 Color.FromArgb(80, 100, 150, 220); 831 using (var btnBrush = new SolidBrush(btnColor)) 832 g.FillRectangle(btnBrush, btnX, btnY, btnW, btnH); 833 using (var btnPen = new Pen(borderColor)) 834 g.DrawRectangle(btnPen, btnX, btnY, btnW, btnH); 835 string btnText = isFlash ? "OK!" : "Save"; 836 g.DrawString(btnText, fontTime, brushText, btnX + (isFlash ? 8 : 6), btnY + 2); 837 } 838 else 839 { 840 // Two lines: first line bold/main, second line dimmer continuation 841 string text = item.Text ?? item.Preview ?? ""; 842 string line1 = text.Length > 60 ? text.Substring(0, 60) : text; 843 string line2 = text.Length > 60 ? text.Substring(60, Math.Min(60, text.Length - 60)) : ""; 844 line1 = line1.Replace('\n', ' ').Replace('\r', ' '); 845 line2 = line2.Replace('\n', ' ').Replace('\r', ' '); 846 var sf = new StringFormat { Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap }; 847 g.DrawString(line1, fontPreview, brushText, new RectangleF(tx, iy + 4, w - tx - tsz.Width - 14, 18), sf); 848 if (line2.Length > 0) 849 g.DrawString(line2, fontTime, brushDim, new RectangleF(tx, iy + 22, w - tx - 14, 16), sf); 850 g.DrawString(ts, fontTime, brushDim, w - tsz.Width - 8, iy + 4); 851 } 852 853 if (i < scrollOffset + MaxVisibleItems - 1) 854 g.DrawLine(penBorder, 8, iy + ItemHeight - 1, w - 8, iy + ItemHeight - 1); 855 } 856 857 if (history.Count > MaxVisibleItems) 858 { 859 float ratio = (float)scrollOffset / Math.Max(1, history.Count - MaxVisibleItems); 860 int barH = Math.Max(20, h * MaxVisibleItems / Math.Max(1, history.Count)); 861 int barY = 36 + (int)(ratio * (h - 36 - barH)); 862 using (var sb = new SolidBrush(Color.FromArgb(40, 255, 255, 255))) g.FillRectangle(sb, w - 5, barY, 3, barH); 863 } 864 } 865 866 private string FormatTime(DateTime t) 867 { 868 var d = DateTime.Now - t; 869 if (d.TotalSeconds < 60) return "now"; 870 if (d.TotalMinutes < 60) return (int)d.TotalMinutes + "m"; 871 if (d.TotalHours < 24) return (int)d.TotalHours + "h"; 872 return t.ToString("dd.MM"); 873 } 874 875 private static Bitmap noiseBmp; 876 private static int noiseAlpha; 877 private static Bitmap GetNoise(int alpha) 878 { 879 if (noiseBmp != null && noiseAlpha == alpha) return noiseBmp; 880 noiseAlpha = alpha; 881 if (noiseBmp != null) noiseBmp.Dispose(); 882 noiseBmp = new Bitmap(64, 64); 883 var rng = new Random(42); 884 for (int y = 0; y < 64; y++) 885 for (int x = 0; x < 64; x++) { int v = rng.Next(256); noiseBmp.SetPixel(x, y, Color.FromArgb(alpha, v, v, v)); } 886 return noiseBmp; 887 } 888 889 // With WS_EX_NOACTIVATE, form never gets activated/deactivated 890 // Closing is handled by V key release in Controller.cs 891 892 protected override void Dispose(bool disposing) 893 { 894 if (disposing) 895 { 896 if (fadeTimer != null) fadeTimer.Dispose(); 897 if (fontTitle != null) fontTitle.Dispose(); 898 if (fontPreview != null) fontPreview.Dispose(); 899 if (fontTime != null) fontTime.Dispose(); 900 if (brushText != null) brushText.Dispose(); 901 if (brushDim != null) brushDim.Dispose(); 902 if (brushAccent != null) brushAccent.Dispose(); 903 if (penBorder != null) penBorder.Dispose(); 904 } 905 base.Dispose(disposing); 906 } 907 908 protected override CreateParams CreateParams 909 { 910 get 911 { 912 var cp = base.CreateParams; 913 cp.ExStyle |= 0x00000080 /*WS_EX_TOOLWINDOW*/ | 0x08000000 /*WS_EX_NOACTIVATE*/; 914 return cp; 915 } 916 } 917 } 918 919 // Helper form that doesn't steal focus when shown, with fade animation 920 internal class PreviewForm : Form 921 { 922 protected override bool ShowWithoutActivation { get { return true; } } 923 protected override CreateParams CreateParams 924 { 925 get 926 { 927 var cp = base.CreateParams; 928 cp.ExStyle |= 0x08000000 | 0x00000080 | 0x00000008; 929 return cp; 930 } 931 } 932 933 private Timer fadeTimer; 934 private float fadeProgress = 0f; 935 private float fadeTarget = 1f; 936 private bool fadingOut = false; 937 938 public void StartFadeIn() 939 { 940 Opacity = 0; 941 fadeProgress = 0f; 942 fadeTarget = 1f; 943 fadingOut = false; 944 if (fadeTimer == null) 945 { 946 fadeTimer = new Timer { Interval = 16 }; 947 fadeTimer.Tick += FadeTick; 948 } 949 fadeTimer.Start(); 950 } 951 952 public void StartFadeOut() 953 { 954 fadeTarget = 0f; 955 fadingOut = true; 956 if (fadeTimer == null) 957 { 958 fadeTimer = new Timer { Interval = 16 }; 959 fadeTimer.Tick += FadeTick; 960 } 961 fadeTimer.Start(); 962 } 963 964 private void FadeTick(object s, EventArgs e) 965 { 966 float d = fadeTarget - fadeProgress; 967 if (Math.Abs(d) > 0.02f) 968 { 969 fadeProgress += d * 0.45f; // ~75ms fade 970 Opacity = fadeProgress * fadeProgress * (3f - 2f * fadeProgress); // smooth ease 971 } 972 else 973 { 974 fadeProgress = fadeTarget; 975 Opacity = fadeTarget; 976 fadeTimer.Stop(); 977 if (fadingOut) { try { Close(); } catch { } } 978 } 979 } 980 981 protected override void Dispose(bool disposing) 982 { 983 if (disposing && fadeTimer != null) { fadeTimer.Stop(); fadeTimer.Dispose(); } 984 base.Dispose(disposing); 985 } 986 } 987}