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

EditorForm.Editing.cs

283 строк · 11,190 байт · модуль 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        // ===== Editing operations (undo, copy, save, AI) =====
 24
 25        public void Undo()
 26        {
 27            if (canvas != null)
 28            {
 29                canvas.Undo();
 30            }
 31        }
 32
 33        public void AddUndoAction(string type, object item)
 34        {
 35            undoStack.Add(new UndoAction(type, item));
 36        }
 37
 38        private void CopyToClipboard()
 39        {
 40            // Use GetFullCompositeImage (same as Word export) to include all annotations
 41            var fullImage = canvas.GetFullCompositeImage();
 42            if (fullImage == null)
 43            {
 44                System.Diagnostics.Debug.WriteLine("GetFullCompositeImage returned null");
 45                return;
 46            }
 47
 48            // Apply crop if needed
 49            Bitmap finalImage;
 50            if (cropRect.X == 0 && cropRect.Y == 0 &&
 51                cropRect.Width == fullImage.Width && cropRect.Height == fullImage.Height)
 52            {
 53                // No crop needed
 54                finalImage = fullImage;
 55            }
 56            else
 57            {
 58                // Crop to cropRect
 59                finalImage = new Bitmap(cropRect.Width, cropRect.Height, PixelFormat.Format32bppArgb);
 60                using (var g = Graphics.FromImage(finalImage))
 61                {
 62                    g.DrawImage(fullImage,
 63                        new Rectangle(0, 0, cropRect.Width, cropRect.Height),
 64                        cropRect,
 65                        GraphicsUnit.Pixel);
 66                }
 67                fullImage.Dispose();
 68            }
 69
 70            Clipboard.SetImage(finalImage);
 71            finalImage.Dispose();
 72        }
 73
 74        /// <summary>
 75        /// Quick AI insert: generate description via Gemini, insert to Word, close editor
 76        /// Uses automatic model fallback on 429 errors
 77        /// </summary>
 78        private void QuickAIInsert()
 79        {
 80            // Prevent duplicate calls (KeyDown can fire multiple times)
 81            if (quickAIInProgress)
 82                return;
 83            quickAIInProgress = true;
 84
 85            // Check if Gemini is configured
 86            if (!GeminiIntegration.IsConfigured)
 87            {
 88                TrayApp.Instance.ShowNotification("Ошибка", "API ключ Gemini не настроен", ToolTipIcon.Error);
 89                quickAIInProgress = false;
 90                return;
 91            }
 92
 93            // Check if Word document is available
 94            var docs = WordIntegration.GetOpenDocuments();
 95            if (docs.Count == 0)
 96            {
 97                TrayApp.Instance.ShowNotification("Ошибка", "Word документ не найден", ToolTipIcon.Error);
 98                quickAIInProgress = false;
 99                return;
100            }
101
102            // Use first document or previously selected
103            var doc = WordIntegration.SelectedDocument ?? docs[0];
104            WordIntegration.SelectedDocument = doc;
105
106            // Get the image
107            var fullImage = canvas.GetFullCompositeImage();
108            if (fullImage == null)
109            {
110                TrayApp.Instance.ShowNotification("Ошибка", "Не удалось получить изображение", ToolTipIcon.Error);
111                quickAIInProgress = false;
112                return;
113            }
114
115            // Apply crop if needed
116            Bitmap imageToSend;
117            if (cropRect.X == 0 && cropRect.Y == 0 &&
118                cropRect.Width == fullImage.Width && cropRect.Height == fullImage.Height)
119            {
120                imageToSend = fullImage;
121            }
122            else
123            {
124                imageToSend = new Bitmap(cropRect.Width, cropRect.Height, PixelFormat.Format32bppArgb);
125                using (var g = Graphics.FromImage(imageToSend))
126                {
127                    g.DrawImage(fullImage,
128                        new Rectangle(0, 0, cropRect.Width, cropRect.Height),
129                        cropRect,
130                        GraphicsUnit.Pixel);
131                }
132                fullImage.Dispose();
133            }
134
135            // Show "processing" notification
136            TrayApp.Instance.ShowNotification("Обработка", "Генерация описания...", ToolTipIcon.Info);
137
138            // Get previous context from Word (last description before the last image)
139            string previousContext = null;
140            try
141            {
142                previousContext = WordIntegration.GetLastDescription();
143                if (!string.IsNullOrEmpty(previousContext))
144                {
145                    System.Diagnostics.Debug.WriteLine("Context from Word: " + previousContext.Substring(0, Math.Min(50, previousContext.Length)) + "...");
146                }
147            }
148            catch { }
149
150            // Generate description with automatic fallback on 429
151            GeminiIntegration.GenerateDescriptionWithFallbackAsync(imageToSend,
152                (result) =>
153                {
154                    // On success - insert to Word
155                    string description = result != null ? result.Item1 : "";
156                    string caption = result != null ? result.Item2 : "";
157                    string usedModel = result != null ? result.Item3 : "";
158
159                    bool insertResult = WordIntegration.InsertImageWithCaption(imageToSend, description, caption);
160
161                    if (insertResult)
162                    {
163                        string msg = "Добавлено в Word";
164                        if (!string.IsNullOrEmpty(usedModel))
165                            msg += " (" + usedModel + ")";
166                        TrayApp.Instance.ShowNotification("Успех", msg, ToolTipIcon.Info);
167
168                        // Close form only on success
169                        if (this.IsHandleCreated && !this.IsDisposed)
170                        {
171                            this.BeginInvoke(new Action(() => this.Close()));
172                        }
173                    }
174                    else
175                    {
176                        TrayApp.Instance.ShowNotification("Ошибка", "Не удалось вставить в Word", ToolTipIcon.Error);
177                        quickAIInProgress = false;
178                    }
179
180                    imageToSend.Dispose();
181                },
182                (model, ex) =>
183                {
184                    // On error
185                    string errorMsg = ex.Message;
186
187                    if (errorMsg.Contains("Все API ключи"))
188                    {
189                        errorMsg = "Все API ключи исчерпаны";
190                    }
191                    else if (errorMsg.Contains("Все модели недоступны"))
192                    {
193                        errorMsg = "Все модели недоступны (лимиты)";
194                    }
195                    else if (errorMsg.Contains("401") || errorMsg.Contains("API key"))
196                    {
197                        errorMsg = "Неверный API ключ";
198                    }
199                    else if (errorMsg.Contains("timeout") || errorMsg.Contains("Timeout"))
200                    {
201                        errorMsg = "Таймаут запроса";
202                    }
203                    else if (errorMsg.Length > 80)
204                    {
205                        errorMsg = errorMsg.Substring(0, 80) + "...";
206                    }
207
208                    TrayApp.Instance.ShowNotification("Ошибка AI", errorMsg, ToolTipIcon.Error, 5000);
209                    imageToSend.Dispose();
210                    quickAIInProgress = false;
211                },
212                (model) =>
213                {
214                    // On model switch - show notification
215                    TrayApp.Instance.ShowNotification("Смена модели", "Пробую: " + model, ToolTipIcon.Info);
216                }
217            , previousContext);
218        }
219
220        private void OpenFileDialog()
221        {
222            using (var dlg = new OpenFileDialog())
223            {
224                dlg.Filter = "Images & Videos|*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tiff;*.webp;*.mp4;*.avi;*.webm;*.mkv;*.mov;*.wmv|" +
225                             "Images|*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tiff;*.webp|" +
226                             "Videos|*.mp4;*.avi;*.webm;*.mkv;*.mov;*.wmv|" +
227                             "All Files|*.*";
228                dlg.Title = "Open Media File";
229                if (dlg.ShowDialog() == DialogResult.OK)
230                {
231                    // Open in new viewer window
232                    var form = new EditorForm(dlg.FileName);
233                    form.Show();
234                }
235            }
236        }
237
238        private void SaveImage()
239        {
240            using (var dlg = new SaveFileDialog())
241            {
242                dlg.Filter = "PNG Image|*.png|JPEG Image|*.jpg|Bitmap|*.bmp";
243                dlg.DefaultExt = "png";
244                dlg.FileName = "capture_" + DateTime.Now.ToString("yyyyMMdd_HHmmss");
245
246                if (dlg.ShowDialog() == DialogResult.OK)
247                {
248                    // Use GetFullCompositeImage to include all annotations
249                    var fullImage = canvas.GetFullCompositeImage();
250                    if (fullImage == null) return;
251
252                    // Apply crop if needed
253                    Bitmap finalImage;
254                    if (cropRect.X == 0 && cropRect.Y == 0 &&
255                        cropRect.Width == fullImage.Width && cropRect.Height == fullImage.Height)
256                    {
257                        finalImage = fullImage;
258                    }
259                    else
260                    {
261                        finalImage = new Bitmap(cropRect.Width, cropRect.Height, PixelFormat.Format32bppArgb);
262                        using (var g = Graphics.FromImage(finalImage))
263                        {
264                            g.DrawImage(fullImage,
265                                new Rectangle(0, 0, cropRect.Width, cropRect.Height),
266                                cropRect,
267                                GraphicsUnit.Pixel);
268                        }
269                        fullImage.Dispose();
270                    }
271
272                    ImageFormat fmt = ImageFormat.Png;
273                    if (dlg.FileName.ToLower().EndsWith(".jpg") || dlg.FileName.ToLower().EndsWith(".jpeg"))
274                        fmt = ImageFormat.Jpeg;
275                    else if (dlg.FileName.ToLower().EndsWith(".bmp"))
276                        fmt = ImageFormat.Bmp;
277                    finalImage.Save(dlg.FileName, fmt);
278                    finalImage.Dispose();
279                }
280            }
281        }
282    }
283}