windowcapture
исходный код / UI/EditorForm.cs

EditorForm.cs

419 строк · 17,840 байт · модуль UI
  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}