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}