1using System; 2using System.Collections.Generic; 3using System.Drawing; 4using System.Drawing.Drawing2D; 5using System.Drawing.Imaging; 6using System.Drawing.Text; 7using System.IO; 8using System.Linq; 9using System.Runtime.InteropServices; 10using System.Windows.Forms; 11using WindowCapture.App; 12using WindowCapture.Detection; 13using WindowCapture.Effects; 14using WindowCapture.Helpers; 15using WindowCapture.Integration; 16using WindowCapture.Models; 17using WindowCapture.Native; 18 19namespace WindowCapture.UI 20{ 21 public partial class EditorForm : Form 22 { 23 [DllImport("winmm.dll")] 24 private static extern int waveOutSetVolume(IntPtr hwo, uint dwVolume); 25 26 public static bool IsOpen { get; private set; } 27 28 // Image 29 private Bitmap captured; 30 private Bitmap blurredBitmap; 31 private GraphicsPath clipPath; 32 private Rectangle bounds; 33 private bool isRectangular; 34 35 // Zoom 36 private float zoomLevel = 1.0f; 37 private const float zoomMin = 0.05f; 38 private const float zoomMax = 50.0f; // Increased max zoom 39 private const float zoomFactor = 1.25f; // Exponential zoom factor (25% change per step) 40 41 // Smooth zoom animation 42 private System.Windows.Forms.Timer zoomTimer; 43 private float targetZoom; 44 private const float ZoomInterpolationFactor = 0.35f; // Smoother interpolation (35% per frame) 45 46 // UI 47 private Panel scrollContainer; 48 private AnnotationCanvas canvas; 49 50 // Glass hover buttons (static position, cursor-proximity edge lighting) 51 private Action[] hoverBtnActions; 52 private float[] hoverBtnGlow; // 0.0=invisible, 1.0=full edge glow per button 53 private bool[] hoverBtnHovered; // mouse directly over button 54 private Point lastCursorClientPt; // last cursor position in scrollContainer coords 55 private int hoverBtnClickIndex = -1; // button being clicked (-1 = none) 56 private System.Windows.Forms.Timer glassLeaveTimer; // checks if cursor left window 57 private const int HoverBtnW = 34; 58 private const int HoverBtnH = 24; 59 private const int HoverBtnGap = 1; 60 private const float GlowMaxRadius = 170f; // distance where glow starts appearing 61 private const float GlowFullRadius = 12f; // distance where glow is 100% 62 63 // Glass zoom indicator (top-left corner, fades after zoom) 64 private float zoomIndicatorAlpha; // 0=hidden, 1=fully visible 65 private System.Windows.Forms.Timer zoomFadeTimer; 66 private DateTime zoomLastChangeTime; 67 private const int ZoomFadeDelayMs = 900; // ms before fading starts 68 private const int ZoomFadeDurationMs = 400; // ms to fade out 69 70 // Form dragging 71 private bool formDragging; 72 private Point formDragStart; 73 74 // Undo stack 75 private List<UndoAction> undoStack = new List<UndoAction>(); 76 77 // Auto-detect with Alt 78 private Detector editorDetector; 79 private bool altPressed; 80 private Rectangle autoDetectedRect; 81 private bool altWasReleasedOnce; 82 83 // Crop 84 private Rectangle cropRect; 85 86 // Word export (long-press space) 87 private System.Windows.Forms.Timer spaceHoldTimer; 88 private bool spaceHeld; 89 private bool wordDialogOpen; 90 private DateTime spaceDownTime; 91 private const int SpaceHoldThreshold = 250; // milliseconds 92 private WordSidePanel wordSidePanel; 93 private bool quickAIInProgress; // Prevent duplicate Ctrl+Space calls 94 95 // Viewer mode (photo/video browsing) 96 private bool viewerMode; 97 private string currentFilePath; 98 private string[] folderFiles; // all media files in directory, sorted 99 private int currentFileIndex; 100 private bool isVideoFile; 101 private bool isAnimatedGif; 102 private Image gifImage; // kept alive for ImageAnimator 103 private WebBrowser videoBrowser; 104 105 private System.Windows.Forms.Timer fileNameFadeTimer; 106 private float fileNameAlpha; 107 108 private static string[] ImageExtensions { get { return MediaTypes.ImageExtensions; } } 109 private static string[] VideoExtensions { get { return MediaTypes.VideoExtensions; } } 110 private static string[] AudioExtensions { get { return MediaTypes.AudioExtensions; } } 111 112 // Video/audio playback controls (glass-styled) 113 private bool isAudioFile; 114 private System.Windows.Forms.Timer videoUpdateTimer; 115 private bool videoPlaying = true; 116 private double videoDuration; 117 private double videoCurrentTime; 118 private double prevVideoTime; // for smooth interpolation 119 private DateTime lastVideoUpdateTime; 120 private float seekBarProgress; // 0.0 - 1.0 121 private float videoVolume = 1.0f; 122 private bool videoMuted; 123 private float videoControlsAlpha = 0f; 124 private DateTime videoLastMouseMove; 125 private System.Windows.Forms.Timer videoControlsFadeTimer; 126 private const int VideoControlBarH = 50; 127 private bool videoControlBarHovered; // mouse in control bar area 128 private DoubleBufferedPanel videoOverlay; // full-size overlay for controls + events 129 private bool rmbSeeking; // right-click drag seeking 130 private float[] waveformPeaks; // normalized 0-1 peaks for waveform display 131 private string waveformLoadedPath; // path of file whose waveform is loaded 132 private int rmbSeekStartX; 133 private double rmbSeekStartTime; 134 135 // Glass volume indicator (temporary popup on wheel) 136 private float volumeIndicatorAlpha; 137 private DateTime volumeLastChangeTime; 138 139 // Image preload cache 140 private Dictionary<int, Bitmap> imageCache; 141 private const int CacheRadius = 5; 142 public bool IsVideoFile { get { return isVideoFile; } } 143 public bool IsAudioFile { get { return isAudioFile; } } 144 145 // Public properties for canvas access 146 public bool IsViewerMode { get { return viewerMode; } } 147 public float ZoomLevel { get { return zoomLevel; } } 148 public Bitmap CapturedImage { get { return captured; } } 149 public Bitmap BlurredBitmap { get { return blurredBitmap; } } 150 public Rectangle AutoDetectedRect { get { return autoDetectedRect; } } 151 public bool AltPressed { get { return altPressed; } } 152 public Rectangle CropRect { get { return cropRect; } } 153 public new Rectangle Bounds { get { return bounds; } } 154 155 // Annotations (lists are managed by canvas but we expose them) 156 public List<HighlightRect> Highlights { get { return canvas != null ? canvas.Highlights : new List<HighlightRect>(); } } 157 public List<ArrowAnnotation> Arrows { get { return canvas != null ? canvas.Arrows : new List<ArrowAnnotation>(); } } 158 public List<NumberMarker> Markers { get { return canvas != null ? canvas.Markers : new List<NumberMarker>(); } } 159 public List<TextBlock> TextBlocks { get { return canvas != null ? canvas.TextBlocks : new List<TextBlock>(); } } 160 public List<CommentBubble> CommentBubbles { get { return canvas != null ? canvas.CommentBubbles : new List<CommentBubble>(); } } 161 162 public EditorForm(Bitmap fullScreen, GraphicsPath path) 163 { 164 var pathBounds = Rectangle.Ceiling(path.GetBounds()); 165 isRectangular = IsPathRectangular(path, pathBounds); 166 167 bounds = pathBounds; 168 clipPath = (GraphicsPath)path.Clone(); 169 170 var matrix = new Matrix(); 171 matrix.Translate(-bounds.X, -bounds.Y); 172 clipPath.Transform(matrix); 173 174 captured = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb); 175 using (var g = Graphics.FromImage(captured)) 176 { 177 g.Clear(Color.Transparent); 178 if (!isRectangular) 179 g.SetClip(clipPath); 180 g.DrawImage(fullScreen, new Rectangle(0, 0, bounds.Width, bounds.Height), pathBounds, GraphicsUnit.Pixel); 181 } 182 183 // Setup detector for Alt auto-selection 184 editorDetector = new Detector(); 185 editorDetector.SetScreen(captured); 186 187 cropRect = new Rectangle(0, 0, bounds.Width, bounds.Height); 188 189 CreateBlurredBitmap(); 190 FontHelper.EnsureLoaded(); 191 InitializeForm(); 192 FinalizeInit(); 193 194 // If Alt is currently held (from capture), wait for release before allowing Alt in editor 195 bool altCurrentlyHeld = (Control.ModifierKeys & Keys.Alt) == Keys.Alt; 196 altWasReleasedOnce = !altCurrentlyHeld; 197 } 198 199 // Constructor for viewer mode (open file) 200 public EditorForm(string filePath) 201 { 202 viewerMode = true; 203 currentFilePath = Path.GetFullPath(filePath); 204 isRectangular = true; 205 imageCache = new Dictionary<int, Bitmap>(); 206 207 // Scan directory for all media files 208 ScanFolderFiles(currentFilePath); 209 210 string ext = Path.GetExtension(filePath).ToLowerInvariant(); 211 isVideoFile = VideoExtensions.Contains(ext); 212 isAudioFile = AudioExtensions.Contains(ext); 213 214 // Detect animated GIF (handled separately — canvas + ImageAnimator) 215 isAnimatedGif = MediaTypes.IsAnimatedGif(currentFilePath); 216 217 if (isVideoFile || isAudioFile) 218 { 219 // For video/audio: create dummy captured so the editor initializes 220 // Use a moderate initial size — will be adjusted after video dimensions are known 221 var wa = Screen.PrimaryScreen.WorkingArea; 222 int initW = isAudioFile ? wa.Width : (int)(wa.Width * 0.7); 223 int initH = isAudioFile ? wa.Height : (int)(wa.Height * 0.7); 224 bounds = new Rectangle(0, 0, initW, initH); 225 captured = new Bitmap(initW, initH, PixelFormat.Format32bppArgb); 226 using (var g = Graphics.FromImage(captured)) 227 g.Clear(Settings.BlurTintColor); 228 clipPath = new GraphicsPath(); 229 clipPath.AddRectangle(bounds); 230 cropRect = bounds; 231 } 232 else if (isAnimatedGif) 233 { 234 // Load GIF without cloning (preserves animation frames) 235 gifImage = LoadGifImage(filePath); 236 bounds = new Rectangle(0, 0, gifImage.Width, gifImage.Height); 237 captured = new Bitmap(gifImage.Width, gifImage.Height, PixelFormat.Format32bppArgb); 238 using (var g = Graphics.FromImage(captured)) 239 g.DrawImage(gifImage, 0, 0); 240 clipPath = new GraphicsPath(); 241 clipPath.AddRectangle(bounds); 242 cropRect = bounds; 243 } 244 else 245 { 246 // Load image 247 var img = LoadImageFromFile(filePath); 248 bounds = new Rectangle(0, 0, img.Width, img.Height); 249 captured = img; 250 clipPath = new GraphicsPath(); 251 clipPath.AddRectangle(bounds); 252 cropRect = new Rectangle(0, 0, bounds.Width, bounds.Height); 253 254 // Cache the loaded image 255 lock (imageCache) { imageCache[currentFileIndex] = (Bitmap)img.Clone(); } 256 } 257 258 CreateBlurredBitmap(); 259 FontHelper.EnsureLoaded(); 260 InitializeForm(); 261 262 // Audio: maximize window; Video: FitWindowToVideo will handle sizing after getting dimensions 263 if (isAudioFile) 264 WindowState = FormWindowState.Maximized; 265 266 FinalizeInit(); 267 altWasReleasedOnce = true; 268 269 // Setup player 270 if (isAnimatedGif) 271 { 272 // Start GIF animation on canvas (no WebBrowser needed) 273 canvas.SetAnimatedGif(gifImage); 274 } 275 else if (isVideoFile || isAudioFile) 276 { 277 SetupVideoPlayer(currentFilePath); 278 } 279 else 280 { 281 // Start preloading neighboring images 282 PreloadNeighborImages(); 283 } 284 285 SetupFileNameOverlay(); 286 ShowFileName(); 287 } 288 289 private void InitializeForm() 290 { 291 Text = ""; 292 FormBorderStyle = FormBorderStyle.None; 293 StartPosition = FormStartPosition.CenterScreen; 294 BackColor = Color.FromArgb(Settings.BlurTintColor.R, Settings.BlurTintColor.G, Settings.BlurTintColor.B); 295 MinimumSize = new Size(250, 200); 296 DoubleBuffered = true; 297 SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw, true); 298 299 // Auto-fit to window 300 int maxW = Screen.PrimaryScreen.WorkingArea.Width - 100; 301 int maxH = Screen.PrimaryScreen.WorkingArea.Height - 100; 302 303 if (Settings.AutoFitToWindow && (bounds.Width > maxW || bounds.Height > maxH)) 304 { 305 float scaleW = (float)maxW / bounds.Width; 306 float scaleH = (float)maxH / bounds.Height; 307 zoomLevel = Math.Min(1.0f, Math.Min(scaleW, scaleH)); 308 } 309 310 int formW = Math.Min((int)(bounds.Width * zoomLevel) + 40, maxW); 311 int formH = Math.Min((int)(bounds.Height * zoomLevel) + 40, maxH); 312 formW = Math.Max(formW, 350); 313 formH = Math.Max(formH, 250); 314 Size = new Size(formW, formH); 315 316 // Scroll container with double buffering (no title bar - fully borderless) 317 scrollContainer = new DoubleBufferedPanel(); 318 scrollContainer.Dock = DockStyle.Fill; 319 scrollContainer.AutoScroll = false; // No scrollbars 320 scrollContainer.BackColor = Color.FromArgb(Settings.BlurTintColor.R, Settings.BlurTintColor.G, Settings.BlurTintColor.B); 321 ((DoubleBufferedPanel)scrollContainer).TransparentBackground = true; 322 323 canvas = new AnnotationCanvas(this); 324 canvas.Dock = DockStyle.Fill; // Static viewport: fills the window permanently 325 326 scrollContainer.Controls.Add(canvas); 327 scrollContainer.Resize += delegate { 328 if (viewerMode && !isVideoFile && !isAudioFile && bounds.Width > 0) 329 FitZoomToViewport(); 330 CenterImage(); 331 }; 332 scrollContainer.MouseDown += delegate(object s, MouseEventArgs me) 333 { 334 if (me.Button == MouseButtons.Left) 335 { 336 int btnIdx = HitTestGlassButton(me.Location); 337 if (btnIdx >= 0 && hoverBtnGlow[btnIdx] > 0.15f) 338 { 339 hoverBtnClickIndex = btnIdx; 340 return; 341 } 342 formDragging = true; 343 formDragStart = me.Location; 344 } 345 }; 346 scrollContainer.MouseMove += delegate(object s, MouseEventArgs me) 347 { 348 if (formDragging) 349 { 350 Location = new Point(Location.X + me.X - formDragStart.X, Location.Y + me.Y - formDragStart.Y); 351 } 352 UpdateGlassButtons(me.Location); 353 }; 354 scrollContainer.MouseUp += delegate(object s, MouseEventArgs me) 355 { 356 if (hoverBtnClickIndex >= 0 && me.Button == MouseButtons.Left) 357 { 358 int idx = hoverBtnClickIndex; 359 hoverBtnClickIndex = -1; 360 if (HitTestGlassButton(me.Location) == idx) 361 hoverBtnActions[idx](); 362 return; 363 } 364 formDragging = false; 365 }; 366 scrollContainer.Paint += ScrollContainer_Paint; 367 368 // Word side panel (initially hidden) 369 wordSidePanel = new WordSidePanel(this); 370 wordSidePanel.InsertSuccess += WordSidePanel_InsertSuccess; 371 wordSidePanel.PanelClosed += WordSidePanel_Closed; 372 373 // Initialize glass button actions + zoom indicator 374 InitGlassButtons(); 375 zoomFadeTimer = new System.Windows.Forms.Timer(); 376 zoomFadeTimer.Interval = 16; // ~60fps for smooth fade 377 zoomFadeTimer.Tick += ZoomFadeTimer_Tick; 378 379 Controls.Add(wordSidePanel); 380 Controls.Add(scrollContainer); 381 382 CenterImage(); 383 384 // Check for Word on startup and show panel if available 385 CheckWordAvailability(); 386 } 387 388 /// <summary> 389 /// Shared post-initialization: keyboard, D2D, zoom timer, Shown handler. 390 /// Called at the end of both constructors after InitializeForm(). 391 /// </summary> 392 private void FinalizeInit() 393 { 394 KeyPreview = true; 395 KeyDown += EditorForm_KeyDown; 396 KeyUp += EditorForm_KeyUp; 397 398 IsOpen = true; 399 400 canvas.SetOriginalBitmap(captured); 401 InitializeDirect2D(); 402 403 Shown += delegate 404 { 405 ApplyRoundedCorners(); 406 EnableBlurBackground(); 407 // Fit image to actual viewport after window is fully shown/maximized 408 if (viewerMode && !isVideoFile && !isAudioFile && bounds.Width > 0) 409 FitZoomToViewport(); 410 CenterImage(); 411 }; 412 413 targetZoom = zoomLevel; 414 zoomTimer = new System.Windows.Forms.Timer(); 415 zoomTimer.Interval = 16; // ~60 FPS 416 zoomTimer.Tick += ZoomTimer_Tick; 417 } 418 } 419}