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

EditorForm.ViewerMode.cs

362 строк · 14,961 байт · модуль 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
 22    {
 23        // ===== Viewer mode (image/file browsing) =====
 24
 25        /// <summary>Load GIF without cloning — preserves animation frames for ImageAnimator.</summary>
 26        private Image LoadGifImage(string path)
 27        {
 28            // Must copy to MemoryStream — Image.FromFile locks the file
 29            var data = File.ReadAllBytes(path);
 30            var ms = new MemoryStream(data);
 31            return Image.FromStream(ms); // ms must stay alive while Image is alive
 32        }
 33
 34        private Bitmap LoadImageFromFile(string path)
 35        {
 36            try
 37            {
 38                using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
 39                {
 40                    // Read entire file into memory first to avoid stream contention
 41                    var ms = new MemoryStream();
 42                    fs.CopyTo(ms);
 43                    ms.Position = 0;
 44                    using (var temp = new Bitmap(ms))
 45                        return BitmapHelper.Clone32(temp);
 46                }
 47            }
 48            catch
 49            {
 50                // Return a small placeholder on failure (corrupted file, race condition, etc.)
 51                var placeholder = new Bitmap(1, 1, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
 52                return placeholder;
 53            }
 54        }
 55
 56        private void ScanFolderFiles(string filePath)
 57        {
 58            string dir = Path.GetDirectoryName(filePath);
 59            string ext = Path.GetExtension(filePath).ToLowerInvariant();
 60
 61            // Only show files of the same type as the opened file
 62            string[] allowedExts;
 63            if (VideoExtensions.Contains(ext))
 64                allowedExts = VideoExtensions;
 65            else if (AudioExtensions.Contains(ext))
 66                allowedExts = AudioExtensions;
 67            else
 68                allowedExts = ImageExtensions;
 69
 70            var files = Directory.GetFiles(dir)
 71                .Where(f => allowedExts.Contains(Path.GetExtension(f).ToLowerInvariant()))
 72                .OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
 73                .ToArray();
 74            folderFiles = files;
 75            currentFileIndex = Array.FindIndex(folderFiles, f =>
 76                string.Equals(f, filePath, StringComparison.OrdinalIgnoreCase));
 77            if (currentFileIndex < 0) currentFileIndex = 0;
 78        }
 79
 80        private void SetupFileNameOverlay()
 81        {
 82            // No more Label — file name is now painted in glass style via PaintFileNameIndicator
 83            fileNameFadeTimer = new System.Windows.Forms.Timer();
 84            fileNameFadeTimer.Interval = 16;
 85            fileNameFadeTimer.Tick += FileNameFadeTimer_Tick;
 86        }
 87
 88        private DateTime fileNameShowTime;
 89
 90        private void ShowFileName()
 91        {
 92            if (currentFilePath == null) return;
 93            fileNameAlpha = 1f;
 94            fileNameShowTime = DateTime.Now;
 95            fileNameFadeTimer.Start();
 96            // Trigger repaint on the appropriate surface
 97            if (videoOverlay != null) videoOverlay.Invalidate(new Rectangle(0, 0, 400, 80));
 98            else if (scrollContainer != null && scrollContainer.IsHandleCreated) scrollContainer.Invalidate(new Rectangle(0, 0, 400, 80));
 99            if (canvas != null && canvas.IsHandleCreated) canvas.Invalidate(new Rectangle(0, 0, 400, 80));
100        }
101
102        private void FileNameFadeTimer_Tick(object sender, EventArgs e)
103        {
104            double elapsed = (DateTime.Now - fileNameShowTime).TotalMilliseconds;
105            if (elapsed < 2000) return;
106            double fadeMs = elapsed - 2000;
107            float alpha = 1f - (float)(fadeMs / 1000.0);
108            if (alpha <= 0)
109            {
110                fileNameAlpha = 0;
111                fileNameFadeTimer.Stop();
112            }
113            else
114                fileNameAlpha = alpha;
115            // Repaint
116            if (videoOverlay != null) videoOverlay.Invalidate(new Rectangle(0, 0, 400, 80));
117            else if (scrollContainer != null && scrollContainer.IsHandleCreated) scrollContainer.Invalidate(new Rectangle(0, 0, 400, 80));
118            if (canvas != null && canvas.IsHandleCreated) canvas.Invalidate(new Rectangle(0, 0, 400, 80));
119        }
120
121        // Viewer state
122        private DateTime lastBrowseTime;
123        private System.Windows.Forms.Timer delayedNameTimer;
124
125        private bool browsing; // guard against reentrant calls
126
127        public void BrowseFile(int direction)
128        {
129            if (folderFiles == null || folderFiles.Length <= 1) return;
130            if (browsing) return;
131            browsing = true;
132            try { BrowseFileInternal(direction); } finally { browsing = false; }
133        }
134
135        private void BrowseFileInternal(int direction)
136        {
137
138            int newIndex = currentFileIndex + direction;
139            if (newIndex < 0) newIndex = folderFiles.Length - 1;
140            if (newIndex >= folderFiles.Length) newIndex = 0;
141
142            if (newIndex == currentFileIndex) return;
143            currentFileIndex = newIndex;
144            currentFilePath = folderFiles[currentFileIndex];
145
146            PerformanceLogger.Log("BrowseFile", -1, string.Format("Index: {0}, Path: {1}", currentFileIndex, currentFilePath));
147
148            string ext = Path.GetExtension(currentFilePath).ToLowerInvariant();
149            bool nowVideo = VideoExtensions.Contains(ext);
150            bool nowAudio = AudioExtensions.Contains(ext);
151
152            bool nowGif = !nowVideo && !nowAudio && MediaTypes.IsAnimatedGif(currentFilePath);
153
154            if (nowGif)
155            {
156                isAnimatedGif = true;
157                isVideoFile = false;
158                isAudioFile = false;
159
160                // Clean up video player if it was active
161                if (dsMedia != null || dsPanel != null || videoBrowser != null)
162                {
163                    DsCleanup();
164                    if (videoUpdateTimer != null) { videoUpdateTimer.Stop(); videoUpdateTimer.Dispose(); videoUpdateTimer = null; }
165                    if (videoControlsFadeTimer != null) { videoControlsFadeTimer.Stop(); videoControlsFadeTimer.Dispose(); videoControlsFadeTimer = null; }
166                    if (videoOverlayForm != null) { videoOverlayForm.Close(); videoOverlayForm.Dispose(); videoOverlayForm = null; }
167                    if (videoOverlay != null) { scrollContainer.Controls.Remove(videoOverlay); videoOverlay.Dispose(); videoOverlay = null; }
168                    if (dsPanel != null) { scrollContainer.Controls.Remove(dsPanel); dsPanel.Dispose(); dsPanel = null; }
169                    if (canvas != null) canvas.Visible = true;
170                }
171
172                // Load and animate GIF on canvas
173                if (gifImage != null) { gifImage.Dispose(); gifImage = null; }
174                gifImage = LoadGifImage(currentFilePath);
175                bounds = new Rectangle(0, 0, gifImage.Width, gifImage.Height);
176                cropRect = bounds;
177                if (clipPath != null) clipPath.Dispose();
178                clipPath = new GraphicsPath();
179                clipPath.AddRectangle(bounds);
180                if (captured != null) captured.Dispose();
181                captured = new Bitmap(gifImage.Width, gifImage.Height, PixelFormat.Format32bppArgb);
182                using (var g = Graphics.FromImage(captured)) g.DrawImage(gifImage, 0, 0);
183
184                canvas.SetAnimatedGif(gifImage);
185                D2DRenderer.UploadBitmap(captured);
186                FitZoomToViewport();
187                CenterImage();
188                ShowFileName();
189                return;
190            }
191
192            isAnimatedGif = false;
193            if (gifImage != null) { canvas.StopGifAnimation(); gifImage.Dispose(); gifImage = null; }
194
195            if (nowVideo || nowAudio)
196            {
197                isVideoFile = nowVideo;
198                isAudioFile = nowAudio;
199                // isAnimatedGif already set above
200
201                // Switch to new video/audio
202                if (dsMedia != null)
203                {
204                    NavigateVideo(currentFilePath); // DsCleanup inside
205                }
206                else
207                {
208                    // First time switching from image to video
209                    if (canvas != null) canvas.Visible = false;
210                    SetupVideoPlayer(currentFilePath);
211                }
212                ShowFileName();
213                return;
214            }
215            else
216            {
217                // Switch to image
218                isVideoFile = false;
219                isAudioFile = false;
220                isAnimatedGif = false;
221
222                // Clean up video player if it was active
223                if (dsMedia != null || dsPanel != null || videoBrowser != null)
224                {
225                    DsCleanup();
226                    if (videoUpdateTimer != null) { videoUpdateTimer.Stop(); videoUpdateTimer.Dispose(); videoUpdateTimer = null; }
227                    if (videoControlsFadeTimer != null) { videoControlsFadeTimer.Stop(); videoControlsFadeTimer.Dispose(); videoControlsFadeTimer = null; }
228                    if (videoOverlayForm != null) { videoOverlayForm.Close(); videoOverlayForm.Dispose(); videoOverlayForm = null; }
229                    if (videoOverlay != null) { scrollContainer.Controls.Remove(videoOverlay); videoOverlay.Dispose(); videoOverlay = null; }
230                    if (dsPanel != null) { scrollContainer.Controls.Remove(dsPanel); dsPanel.Dispose(); dsPanel = null; }
231                    if (canvas != null) canvas.Visible = true;
232                }
233                
234                Bitmap img = null;
235                lock (imageCache)
236                {
237                    if (imageCache.ContainsKey(currentFileIndex))
238                        img = (Bitmap)imageCache[currentFileIndex].Clone();
239                }
240
241                if (img == null)
242                    img = LoadImageFromFile(currentFilePath);
243
244                UpdateImageContent(img);
245                
246                // Preload neighbors in background
247                PreloadNeighborImages();
248            }
249
250            // Delayed name show
251            if (delayedNameTimer == null)
252            {
253                delayedNameTimer = new System.Windows.Forms.Timer { Interval = 400 };
254                delayedNameTimer.Tick += (s, e) => { delayedNameTimer.Stop(); ShowFileName(); };
255            }
256            delayedNameTimer.Stop();
257            delayedNameTimer.Start();
258        }
259
260        private void UpdateImageContent(Bitmap img)
261        {
262            using (PerformanceLogger.Measure("UpdateImageContent"))
263            {
264                if (captured != null) captured.Dispose();
265                captured = img;
266                bounds = new Rectangle(0, 0, img.Width, img.Height);
267                cropRect = bounds;
268
269                if (clipPath != null) clipPath.Dispose();
270                clipPath = new GraphicsPath();
271                clipPath.AddRectangle(bounds);
272
273                // Optimization: Don't recreate blur bitmap during fast browsing
274                if (DateTime.Now.Subtract(lastBrowseTime).TotalMilliseconds > 200)
275                    CreateBlurredBitmap();
276
277                if (canvas != null)
278                    canvas.SetOriginalBitmap(captured);
279
280                D2DRenderer.UploadBitmap(captured);
281
282                // Fit zoom to current viewport (preserves aspect ratio)
283                FitZoomToViewport();
284                CenterImage();
285                lastBrowseTime = DateTime.Now;
286            }
287        }
288
289        /// <summary>Calculate zoom so the image fits entirely within the current viewport, preserving aspect ratio.</summary>
290        private void FitZoomToViewport()
291        {
292            int vw, vh;
293            if (scrollContainer != null && scrollContainer.IsHandleCreated)
294            {
295                vw = scrollContainer.ClientSize.Width;
296                vh = scrollContainer.ClientSize.Height;
297            }
298            else
299            {
300                vw = Width - 40;
301                vh = Height - 40;
302            }
303            if (vw < 10) vw = Width;
304            if (vh < 10) vh = Height;
305
306            float scaleW = (float)vw / bounds.Width;
307            float scaleH = (float)vh / bounds.Height;
308            float fit = Math.Min(scaleW, scaleH);
309            if (fit > 1.0f) fit = 1.0f; // Don't upscale small images
310
311            zoomLevel = fit;
312            targetZoom = fit;
313        }
314
315        // Background preloading of neighboring images for instant browsing
316        private void PreloadNeighborImages()
317        {
318            if (folderFiles == null || imageCache == null) return;
319            int curIdx = currentFileIndex;
320            System.Threading.ThreadPool.QueueUserWorkItem(delegate {
321                for (int offset = 1; offset <= CacheRadius; offset++)
322                {
323                    foreach (int dir in new[] { -1, 1 })
324                    {
325                        int idx = (curIdx + offset * dir + folderFiles.Length) % folderFiles.Length;
326                        lock (imageCache) { if (imageCache.ContainsKey(idx)) continue; }
327                        string ext2 = Path.GetExtension(folderFiles[idx]).ToLowerInvariant();
328                        if (VideoExtensions.Contains(ext2) || AudioExtensions.Contains(ext2)) continue;
329                        try
330                        {
331                            var bmp = LoadImageFromFile(folderFiles[idx]);
332                            lock (imageCache) { imageCache[idx] = bmp; }
333                        }
334                        catch { }
335                    }
336                }
337                // Evict distant entries
338                lock (imageCache)
339                {
340                    var keys = new List<int>(imageCache.Keys);
341                    foreach (var k in keys)
342                    {
343                        int dist = Math.Abs(k - curIdx);
344                        dist = Math.Min(dist, folderFiles.Length - dist);
345                        if (dist > CacheRadius + 2)
346                        {
347                            imageCache[k].Dispose();
348                            imageCache.Remove(k);
349                        }
350                    }
351                }
352            });
353        }
354
355        // Static helper to open a file in viewer mode
356        public static void OpenFile(string path)
357        {
358            var form = new EditorForm(path);
359            form.Show();
360        }
361    }
362}