1using System; 2using System.Collections.Generic; 3using System.Drawing; 4using System.Drawing.Drawing2D; 5using System.Threading; 6using System.Windows.Forms; 7using WindowCapture.Integration; 8using WindowCapture.Models; 9 10namespace WindowCapture.UI 11{ 12 /// <summary> 13 /// Side panel for Word integration - shows document preview and export controls 14 /// </summary> 15 public class WordSidePanel : Panel 16 { 17 private static readonly Color DarkBg = Color.FromArgb(30, 30, 34); 18 private static readonly Color DarkPanel = Color.FromArgb(40, 40, 45); 19 private static readonly Color DarkField = Color.FromArgb(50, 50, 55); 20 private static readonly Color AccentColor = Color.FromArgb(86, 156, 214); 21 private static readonly Color TextColor = Color.FromArgb(230, 230, 230); 22 private static readonly Color DimTextColor = Color.FromArgb(140, 140, 140); 23 private static readonly Color BorderColor = Color.FromArgb(60, 60, 65); 24 private static readonly Color SuccessColor = Color.FromArgb(78, 154, 78); 25 26 private PictureBox previewBox; 27 private Label lblDocName; 28 private Label lblStatus; 29 private ComboBox cmbDocuments; 30 private TextBox txtDescription; 31 private TextBox txtCaption; 32 private Button btnInsert; 33 private Button btnRefresh; 34 private Button btnClose; 35 private System.Windows.Forms.Timer refreshTimer; 36 37 // Title page section 38 private GlassScrollPanel glassScroll; 39 private Panel scrollPanel; 40 private Panel titlePageSection; 41 private Button btnTitlePageToggle; 42 private bool titlePageExpanded = false; 43 private TextBox txtTemplatePath; 44 private TextBox txtWorkNumber; 45 private ComboBox cmbDiscipline; 46 private ComboBox cmbTopic; 47 private ComboBox cmbStudent; 48 private ComboBox cmbGroup; 49 private ComboBox cmbTeacher; 50 private Button btnInsertTitlePage; 51 private Button btnGenerateDescription; 52 private TextBox txtGeminiApiKey; 53 54 // Formatting section 55 private Panel formattingSection; 56 private Button btnFormattingToggle; 57 private bool formattingExpanded = false; 58 private Button btnAddPageNumbering; 59 private Button btnOriginalize; 60 61 // Gemini section 62 private Panel geminiSection; 63 private Button btnGeminiToggle; 64 private bool geminiExpanded = false; 65 private ListBox lstGeminiModels; 66 private Button btnResetModelStatuses; 67 private TextBox txtGeminiThinkingBudget; 68 private ComboBox cmbGeminiMediaResolution; 69 private TextBox txtGeminiInstruction; 70 71 private Bitmap imageToInsert; 72 private EditorForm parentEditor; 73 private bool isAnimating; 74 private int targetWidth; 75 private const int PanelWidth = 320; 76 private const int AnimationStep = 40; 77 private bool ignoreSpaceUntilRelease = true; // Ignore space key until released 78 private bool pendingFocus = false; // Set focus after space is released 79 80 public event EventHandler InsertSuccess; 81 public event EventHandler PanelClosed; 82 83 public WordSidePanel(EditorForm editor) 84 { 85 parentEditor = editor; 86 InitializePanel(); 87 SetupRefreshTimer(); 88 } 89 90 private void InitializePanel() 91 { 92 Width = 0; // Start collapsed 93 Dock = DockStyle.Right; 94 BackColor = Color.Transparent; 95 96 // Handle Ctrl+Space at panel level 97 this.KeyDown += Panel_KeyDown; 98 99 int margin = 12; 100 int fieldWidth = PanelWidth - margin * 2; 101 102 // Scrollable container — glass scroll panel (same style as settings) 103 glassScroll = new GlassScrollPanel(); 104 glassScroll.Dock = DockStyle.Fill; 105 glassScroll.BackColor = Color.Transparent; 106 Controls.Add(glassScroll); 107 scrollPanel = glassScroll.Content; // All controls go into inner panel 108 109 int y = 10; 110 111 // Header 112 var lblHeader = new Label(); 113 lblHeader.Text = "Word Export"; 114 lblHeader.Font = new Font("Segoe UI Semibold", 11f); 115 lblHeader.ForeColor = TextColor; 116 lblHeader.Location = new Point(margin, y); 117 lblHeader.AutoSize = true; 118 scrollPanel.Controls.Add(lblHeader); 119 120 // Close button 121 btnClose = new Button(); 122 btnClose.Text = "x"; 123 btnClose.Size = new Size(28, 28); 124 btnClose.Location = new Point(PanelWidth - margin - 28 - 20, y - 2); // -20 for scrollbar 125 btnClose.FlatStyle = FlatStyle.Flat; 126 btnClose.FlatAppearance.BorderSize = 0; 127 btnClose.BackColor = Color.Transparent; 128 btnClose.ForeColor = DimTextColor; 129 btnClose.Font = new Font("Segoe UI", 10f); 130 btnClose.Cursor = Cursors.Hand; 131 btnClose.TabStop = false; // Don't receive focus via Tab 132 btnClose.Click += (s, e) => Hide(); 133 btnClose.MouseEnter += (s, e) => { btnClose.ForeColor = TextColor; }; 134 btnClose.MouseLeave += (s, e) => { btnClose.ForeColor = DimTextColor; }; 135 // Ignore space key on close button (prevents accidental close) 136 btnClose.KeyDown += (s, e) => 137 { 138 if (e.KeyCode == Keys.Space) 139 { 140 e.SuppressKeyPress = true; 141 e.Handled = true; 142 } 143 }; 144 scrollPanel.Controls.Add(btnClose); 145 y += 35; 146 147 // Document selector 148 AddLabelToScroll("Документ:", margin, y); 149 y += 22; 150 151 cmbDocuments = new ComboBox(); 152 cmbDocuments.Location = new Point(margin, y); 153 cmbDocuments.Size = new Size(fieldWidth - 40, 28); 154 cmbDocuments.DropDownStyle = ComboBoxStyle.DropDownList; 155 cmbDocuments.BackColor = DarkField; 156 cmbDocuments.ForeColor = TextColor; 157 cmbDocuments.FlatStyle = FlatStyle.Flat; 158 cmbDocuments.Font = new Font("Segoe UI", 9f); 159 cmbDocuments.SelectedIndexChanged += CmbDocuments_SelectedIndexChanged; 160 scrollPanel.Controls.Add(cmbDocuments); 161 162 btnRefresh = new Button(); 163 btnRefresh.Text = "R"; 164 btnRefresh.Size = new Size(32, 28); 165 btnRefresh.Location = new Point(cmbDocuments.Right + 4, y); 166 btnRefresh.FlatStyle = FlatStyle.Flat; 167 btnRefresh.FlatAppearance.BorderColor = BorderColor; 168 btnRefresh.BackColor = DarkField; 169 btnRefresh.ForeColor = TextColor; 170 btnRefresh.Font = new Font("Segoe UI", 9f); 171 btnRefresh.Cursor = Cursors.Hand; 172 btnRefresh.TabStop = false; 173 btnRefresh.Click += (s, e) => LoadDocuments(); 174 scrollPanel.Controls.Add(btnRefresh); 175 y += 38; 176 177 // Document preview 178 AddLabelToScroll("Превью документа:", margin, y); 179 y += 22; 180 181 var previewPanel = new Panel(); 182 previewPanel.Location = new Point(margin, y); 183 previewPanel.Size = new Size(fieldWidth, 140); 184 previewPanel.BackColor = DarkField; 185 previewPanel.Paint += (s, e) => 186 { 187 using (var pen = new Pen(BorderColor)) 188 e.Graphics.DrawRectangle(pen, 0, 0, previewPanel.Width - 1, previewPanel.Height - 1); 189 }; 190 scrollPanel.Controls.Add(previewPanel); 191 192 previewBox = new PictureBox(); 193 previewBox.Dock = DockStyle.Fill; 194 previewBox.SizeMode = PictureBoxSizeMode.Zoom; 195 previewBox.BackColor = Color.Transparent; 196 previewPanel.Controls.Add(previewBox); 197 198 lblDocName = new Label(); 199 lblDocName.Text = ""; 200 lblDocName.ForeColor = DimTextColor; 201 lblDocName.Font = new Font("Segoe UI", 8f); 202 lblDocName.Location = new Point(margin, y + 142); 203 lblDocName.Size = new Size(fieldWidth, 16); 204 scrollPanel.Controls.Add(lblDocName); 205 y += 165; 206 207 // Description 208 AddLabelToScroll("Описание:", margin, y); 209 y += 22; 210 211 txtDescription = new TextBox(); 212 txtDescription.Location = new Point(margin, y); 213 txtDescription.Size = new Size(fieldWidth, 60); 214 txtDescription.Multiline = true; 215 txtDescription.BackColor = DarkField; 216 txtDescription.ForeColor = TextColor; 217 txtDescription.BorderStyle = BorderStyle.FixedSingle; 218 txtDescription.Font = new Font("Segoe UI", 9f); 219 txtDescription.ScrollBars = ScrollBars.Vertical; 220 scrollPanel.Controls.Add(txtDescription); 221 y += 70; 222 223 // Caption 224 AddLabelToScroll("Название рисунка:", margin, y); 225 y += 22; 226 227 txtCaption = new TextBox(); 228 txtCaption.Location = new Point(margin, y); 229 txtCaption.Size = new Size(fieldWidth, 26); 230 txtCaption.BackColor = DarkField; 231 txtCaption.ForeColor = TextColor; 232 txtCaption.BorderStyle = BorderStyle.FixedSingle; 233 txtCaption.Font = new Font("Segoe UI", 9f); 234 scrollPanel.Controls.Add(txtCaption); 235 y += 34; 236 237 // Generate description button (AI) 238 btnGenerateDescription = new Button(); 239 btnGenerateDescription.Text = "Сгенерировать описание (AI)"; 240 btnGenerateDescription.Location = new Point(margin, y); 241 btnGenerateDescription.Size = new Size(fieldWidth, 28); 242 btnGenerateDescription.FlatStyle = FlatStyle.Flat; 243 btnGenerateDescription.FlatAppearance.BorderColor = BorderColor; 244 btnGenerateDescription.BackColor = DarkField; 245 btnGenerateDescription.ForeColor = TextColor; 246 btnGenerateDescription.Font = new Font("Segoe UI", 9f); 247 btnGenerateDescription.Cursor = Cursors.Hand; 248 btnGenerateDescription.TabStop = false; 249 btnGenerateDescription.Click += BtnGenerateDescription_Click; 250 scrollPanel.Controls.Add(btnGenerateDescription); 251 y += 38; 252 253 // Status 254 lblStatus = new Label(); 255 lblStatus.Location = new Point(margin, y); 256 lblStatus.Size = new Size(fieldWidth, 20); 257 lblStatus.ForeColor = DimTextColor; 258 lblStatus.Font = new Font("Segoe UI", 8f); 259 scrollPanel.Controls.Add(lblStatus); 260 y += 25; 261 262 // Insert button 263 btnInsert = new Button(); 264 btnInsert.Text = "Вставить (Enter)"; 265 btnInsert.Location = new Point(margin, y); 266 btnInsert.Size = new Size(fieldWidth, 34); 267 btnInsert.FlatStyle = FlatStyle.Flat; 268 btnInsert.FlatAppearance.BorderColor = AccentColor; 269 btnInsert.BackColor = AccentColor; 270 btnInsert.ForeColor = TextColor; 271 btnInsert.Font = new Font("Segoe UI", 10f); 272 btnInsert.Cursor = Cursors.Hand; 273 btnInsert.TabStop = false; 274 btnInsert.Click += BtnInsert_Click; 275 scrollPanel.Controls.Add(btnInsert); 276 y += 48; 277 278 // Title page section toggle 279 btnTitlePageToggle = new Button(); 280 btnTitlePageToggle.Text = "Титульный лист ▶"; 281 btnTitlePageToggle.Location = new Point(margin, y); 282 btnTitlePageToggle.Size = new Size(fieldWidth, 30); 283 btnTitlePageToggle.FlatStyle = FlatStyle.Flat; 284 btnTitlePageToggle.FlatAppearance.BorderColor = BorderColor; 285 btnTitlePageToggle.BackColor = Color.FromArgb(25, 255, 255, 255); 286 btnTitlePageToggle.ForeColor = TextColor; 287 btnTitlePageToggle.Font = new Font("Segoe UI", 9f); 288 btnTitlePageToggle.Cursor = Cursors.Hand; 289 btnTitlePageToggle.TabStop = false; 290 btnTitlePageToggle.TextAlign = ContentAlignment.MiddleLeft; 291 btnTitlePageToggle.Click += BtnTitlePageToggle_Click; 292 scrollPanel.Controls.Add(btnTitlePageToggle); 293 y += 35; 294 295 // Title page collapsible section 296 titlePageSection = new Panel(); 297 titlePageSection.Location = new Point(margin, y); 298 titlePageSection.Size = new Size(fieldWidth, 0); // Start collapsed 299 titlePageSection.BackColor = Color.FromArgb(20, 255, 255, 255); 300 titlePageSection.Visible = false; 301 scrollPanel.Controls.Add(titlePageSection); 302 303 InitializeTitlePageSection(fieldWidth, margin); 304 305 // Formatting section toggle (positioned after title page section) 306 btnFormattingToggle = new Button(); 307 btnFormattingToggle.Text = "Форматирование ▶"; 308 btnFormattingToggle.Size = new Size(fieldWidth, 30); 309 btnFormattingToggle.FlatStyle = FlatStyle.Flat; 310 btnFormattingToggle.FlatAppearance.BorderColor = BorderColor; 311 btnFormattingToggle.BackColor = Color.FromArgb(25, 255, 255, 255); 312 btnFormattingToggle.ForeColor = TextColor; 313 btnFormattingToggle.Font = new Font("Segoe UI", 9f); 314 btnFormattingToggle.Cursor = Cursors.Hand; 315 btnFormattingToggle.TabStop = false; 316 btnFormattingToggle.TextAlign = ContentAlignment.MiddleLeft; 317 btnFormattingToggle.Click += BtnFormattingToggle_Click; 318 scrollPanel.Controls.Add(btnFormattingToggle); 319 320 // Formatting collapsible section 321 formattingSection = new Panel(); 322 formattingSection.Size = new Size(fieldWidth, 0); 323 formattingSection.BackColor = Color.FromArgb(20, 255, 255, 255); 324 formattingSection.Visible = false; 325 scrollPanel.Controls.Add(formattingSection); 326 327 InitializeFormattingSection(fieldWidth, margin); 328 329 // Gemini section toggle 330 btnGeminiToggle = new Button(); 331 btnGeminiToggle.Text = "Gemini AI ▶"; 332 btnGeminiToggle.Size = new Size(fieldWidth, 30); 333 btnGeminiToggle.FlatStyle = FlatStyle.Flat; 334 btnGeminiToggle.FlatAppearance.BorderColor = BorderColor; 335 btnGeminiToggle.BackColor = Color.FromArgb(25, 255, 255, 255); 336 btnGeminiToggle.ForeColor = TextColor; 337 btnGeminiToggle.Font = new Font("Segoe UI", 9f); 338 btnGeminiToggle.Cursor = Cursors.Hand; 339 btnGeminiToggle.TabStop = false; 340 btnGeminiToggle.TextAlign = ContentAlignment.MiddleLeft; 341 btnGeminiToggle.Click += BtnGeminiToggle_Click; 342 scrollPanel.Controls.Add(btnGeminiToggle); 343 344 // Gemini collapsible section 345 geminiSection = new Panel(); 346 geminiSection.Size = new Size(fieldWidth, 0); 347 geminiSection.BackColor = Color.FromArgb(20, 255, 255, 255); 348 geminiSection.Visible = false; 349 scrollPanel.Controls.Add(geminiSection); 350 351 InitializeGeminiSection(fieldWidth, margin); 352 353 // Update positions based on title page expansion 354 UpdateSectionPositions(margin); 355 356 // Hook mouse wheel from child controls to glass scroll 357 glassScroll.HookChildWheelEvents(scrollPanel); 358 359 // Paint border 360 Paint += (s, e) => 361 { 362 using (var pen = new Pen(BorderColor)) 363 e.Graphics.DrawLine(pen, 0, 0, 0, Height); 364 }; 365 366 // Handle keys - ignore space until released after panel opens 367 txtCaption.KeyDown += TextBox_KeyDown; 368 txtCaption.KeyUp += TextBox_KeyUp; 369 txtCaption.PreviewKeyDown += TextBox_PreviewKeyDown; 370 371 txtDescription.KeyDown += TextBox_KeyDown; 372 txtDescription.KeyUp += TextBox_KeyUp; 373 txtDescription.PreviewKeyDown += TextBox_PreviewKeyDown; 374 } 375 376 private void InitializeTitlePageSection(int fieldWidth, int margin) 377 { 378 int innerMargin = 8; 379 int innerWidth = fieldWidth - innerMargin * 2; 380 int yy = 10; 381 382 // Template path 383 AddLabelToPanel(titlePageSection, "Шаблон:", innerMargin, yy); 384 yy += 20; 385 386 txtTemplatePath = new TextBox(); 387 txtTemplatePath.Location = new Point(innerMargin, yy); 388 txtTemplatePath.Size = new Size(innerWidth - 35, 24); 389 txtTemplatePath.BackColor = DarkField; 390 txtTemplatePath.ForeColor = TextColor; 391 txtTemplatePath.BorderStyle = BorderStyle.FixedSingle; 392 txtTemplatePath.Font = new Font("Segoe UI", 8f); 393 txtTemplatePath.ReadOnly = true; 394 titlePageSection.Controls.Add(txtTemplatePath); 395 396 var btnBrowse = new Button(); 397 btnBrowse.Text = "..."; 398 btnBrowse.Location = new Point(txtTemplatePath.Right + 3, yy); 399 btnBrowse.Size = new Size(30, 24); 400 btnBrowse.FlatStyle = FlatStyle.Flat; 401 btnBrowse.FlatAppearance.BorderColor = BorderColor; 402 btnBrowse.BackColor = DarkField; 403 btnBrowse.ForeColor = TextColor; 404 btnBrowse.TabStop = false; 405 btnBrowse.Click += BtnBrowseTemplate_Click; 406 titlePageSection.Controls.Add(btnBrowse); 407 yy += 32; 408 409 // Work number 410 AddLabelToPanel(titlePageSection, "Номер работы:", innerMargin, yy); 411 yy += 20; 412 413 txtWorkNumber = new TextBox(); 414 txtWorkNumber.Location = new Point(innerMargin, yy); 415 txtWorkNumber.Size = new Size(60, 24); 416 txtWorkNumber.BackColor = DarkField; 417 txtWorkNumber.ForeColor = TextColor; 418 txtWorkNumber.BorderStyle = BorderStyle.FixedSingle; 419 txtWorkNumber.Font = new Font("Segoe UI", 9f); 420 txtWorkNumber.Text = "1"; 421 titlePageSection.Controls.Add(txtWorkNumber); 422 yy += 32; 423 424 // Discipline 425 yy = AddComboBoxWithAdd(titlePageSection, "Дисциплина:", innerMargin, yy, innerWidth, 426 out cmbDiscipline, "discipline"); 427 428 // Topic 429 yy = AddComboBoxWithAdd(titlePageSection, "Тема:", innerMargin, yy, innerWidth, 430 out cmbTopic, "topic"); 431 432 // Student 433 yy = AddComboBoxWithAdd(titlePageSection, "Студент:", innerMargin, yy, innerWidth, 434 out cmbStudent, "student"); 435 436 // Group 437 yy = AddComboBoxWithAdd(titlePageSection, "Группа:", innerMargin, yy, innerWidth, 438 out cmbGroup, "group"); 439 440 // Teacher 441 yy = AddComboBoxWithAdd(titlePageSection, "Преподаватель:", innerMargin, yy, innerWidth, 442 out cmbTeacher, "teacher"); 443 444 // Insert title page button 445 btnInsertTitlePage = new Button(); 446 btnInsertTitlePage.Text = "Вставить титульный лист"; 447 btnInsertTitlePage.Location = new Point(innerMargin, yy); 448 btnInsertTitlePage.Size = new Size(innerWidth, 30); 449 btnInsertTitlePage.FlatStyle = FlatStyle.Flat; 450 btnInsertTitlePage.FlatAppearance.BorderColor = SuccessColor; 451 btnInsertTitlePage.BackColor = SuccessColor; 452 btnInsertTitlePage.ForeColor = TextColor; 453 btnInsertTitlePage.Font = new Font("Segoe UI", 9f); 454 btnInsertTitlePage.Cursor = Cursors.Hand; 455 btnInsertTitlePage.TabStop = false; 456 btnInsertTitlePage.Click += BtnInsertTitlePage_Click; 457 titlePageSection.Controls.Add(btnInsertTitlePage); 458 yy += 40; 459 460 // Store total height 461 titlePageSection.Tag = yy; // Store expanded height 462 } 463 464 private ListBox lstApiKeys; 465 private Button btnAddApiKey; 466 private Button btnRemoveApiKey; 467 468 private void InitializeGeminiSection(int fieldWidth, int margin) 469 { 470 int innerMargin = 8; 471 int innerWidth = fieldWidth - innerMargin * 2; 472 int yy = 10; 473 var settings = TitlePageData.Instance.Gemini; 474 475 // API Keys label with add button 476 AddLabelToPanel(geminiSection, "API ключи:", innerMargin, yy); 477 478 // Add key button 479 btnAddApiKey = new Button(); 480 btnAddApiKey.Text = "+"; 481 btnAddApiKey.Location = new Point(innerMargin + 70, yy - 3); 482 btnAddApiKey.Size = new Size(24, 20); 483 btnAddApiKey.FlatStyle = FlatStyle.Flat; 484 btnAddApiKey.FlatAppearance.BorderColor = BorderColor; 485 btnAddApiKey.BackColor = DarkField; 486 btnAddApiKey.ForeColor = TextColor; 487 btnAddApiKey.Font = new Font("Segoe UI", 8f); 488 btnAddApiKey.Click += BtnAddApiKey_Click; 489 geminiSection.Controls.Add(btnAddApiKey); 490 491 // Remove key button 492 btnRemoveApiKey = new Button(); 493 btnRemoveApiKey.Text = "-"; 494 btnRemoveApiKey.Location = new Point(innerMargin + 96, yy - 3); 495 btnRemoveApiKey.Size = new Size(24, 20); 496 btnRemoveApiKey.FlatStyle = FlatStyle.Flat; 497 btnRemoveApiKey.FlatAppearance.BorderColor = BorderColor; 498 btnRemoveApiKey.BackColor = DarkField; 499 btnRemoveApiKey.ForeColor = TextColor; 500 btnRemoveApiKey.Font = new Font("Segoe UI", 8f); 501 btnRemoveApiKey.Click += BtnRemoveApiKey_Click; 502 geminiSection.Controls.Add(btnRemoveApiKey); 503 yy += 20; 504 505 // API Keys list 506 lstApiKeys = new ListBox(); 507 lstApiKeys.Location = new Point(innerMargin, yy); 508 lstApiKeys.Size = new Size(innerWidth, 50); 509 lstApiKeys.BackColor = DarkField; 510 lstApiKeys.ForeColor = TextColor; 511 lstApiKeys.BorderStyle = BorderStyle.FixedSingle; 512 lstApiKeys.Font = new Font("Segoe UI", 8f); 513 lstApiKeys.DrawMode = DrawMode.OwnerDrawFixed; 514 lstApiKeys.ItemHeight = 16; 515 lstApiKeys.DrawItem += LstApiKeys_DrawItem; 516 lstApiKeys.SelectedIndexChanged += LstApiKeys_SelectedIndexChanged; 517 RefreshApiKeysList(); 518 geminiSection.Controls.Add(lstApiKeys); 519 yy += 55; 520 521 // New API key input 522 txtGeminiApiKey = new TextBox(); 523 txtGeminiApiKey.Location = new Point(innerMargin, yy); 524 txtGeminiApiKey.Size = new Size(innerWidth, 24); 525 txtGeminiApiKey.BackColor = DarkField; 526 txtGeminiApiKey.ForeColor = TextColor; 527 txtGeminiApiKey.BorderStyle = BorderStyle.FixedSingle; 528 txtGeminiApiKey.Font = new Font("Segoe UI", 8f); 529 txtGeminiApiKey.Text = ""; 530 // Placeholder text 531 txtGeminiApiKey.GotFocus += (s, e) => { if (txtGeminiApiKey.Text == "Новый ключ...") { txtGeminiApiKey.Text = ""; txtGeminiApiKey.ForeColor = TextColor; } }; 532 txtGeminiApiKey.LostFocus += (s, e) => { if (string.IsNullOrEmpty(txtGeminiApiKey.Text)) { txtGeminiApiKey.Text = "Новый ключ..."; txtGeminiApiKey.ForeColor = DimTextColor; } }; 533 txtGeminiApiKey.Text = "Новый ключ..."; 534 txtGeminiApiKey.ForeColor = DimTextColor; 535 geminiSection.Controls.Add(txtGeminiApiKey); 536 yy += 30; 537 538 // Model selection with status colors 539 AddLabelToPanel(geminiSection, "Модель:", innerMargin, yy); 540 541 // Reset button 542 btnResetModelStatuses = new Button(); 543 btnResetModelStatuses.Text = "Сброс"; 544 btnResetModelStatuses.Location = new Point(innerMargin + 50, yy - 3); 545 btnResetModelStatuses.Size = new Size(50, 20); 546 btnResetModelStatuses.FlatStyle = FlatStyle.Flat; 547 btnResetModelStatuses.FlatAppearance.BorderColor = BorderColor; 548 btnResetModelStatuses.BackColor = DarkField; 549 btnResetModelStatuses.ForeColor = TextColor; 550 btnResetModelStatuses.Font = new Font("Segoe UI", 7f); 551 btnResetModelStatuses.Click += BtnResetModelStatuses_Click; 552 geminiSection.Controls.Add(btnResetModelStatuses); 553 yy += 20; 554 555 // ListBox for models with colored status indicators 556 lstGeminiModels = new ListBox(); 557 lstGeminiModels.Location = new Point(innerMargin, yy); 558 lstGeminiModels.Size = new Size(innerWidth, 120); 559 lstGeminiModels.BackColor = DarkField; 560 lstGeminiModels.ForeColor = TextColor; 561 lstGeminiModels.BorderStyle = BorderStyle.FixedSingle; 562 lstGeminiModels.Font = new Font("Segoe UI", 8f); 563 lstGeminiModels.DrawMode = DrawMode.OwnerDrawFixed; 564 lstGeminiModels.ItemHeight = 18; 565 lstGeminiModels.DrawItem += LstGeminiModels_DrawItem; 566 lstGeminiModels.SelectedIndexChanged += LstGeminiModels_SelectedIndexChanged; 567 // Add fallback models 568 foreach (var model in GeminiIntegration.FallbackModels) 569 { 570 lstGeminiModels.Items.Add(model); 571 } 572 // Select current model 573 int idx = lstGeminiModels.Items.IndexOf(settings.Model); 574 if (idx >= 0) lstGeminiModels.SelectedIndex = idx; 575 geminiSection.Controls.Add(lstGeminiModels); 576 yy += 125; 577 578 // Thinking Budget 579 AddLabelToPanel(geminiSection, "Thinking Budget:", innerMargin, yy); 580 txtGeminiThinkingBudget = new TextBox(); 581 txtGeminiThinkingBudget.Location = new Point(innerMargin + 100, yy - 2); 582 txtGeminiThinkingBudget.Size = new Size(50, 24); 583 txtGeminiThinkingBudget.BackColor = DarkField; 584 txtGeminiThinkingBudget.ForeColor = TextColor; 585 txtGeminiThinkingBudget.BorderStyle = BorderStyle.FixedSingle; 586 txtGeminiThinkingBudget.Font = new Font("Segoe UI", 8f); 587 txtGeminiThinkingBudget.Text = settings.ThinkingBudget.ToString(); 588 txtGeminiThinkingBudget.Leave += (s, e) => { 589 int val; 590 if (int.TryParse(txtGeminiThinkingBudget.Text, out val)) 591 { 592 settings.ThinkingBudget = val; 593 TitlePageData.Instance.Save(); 594 } 595 }; 596 geminiSection.Controls.Add(txtGeminiThinkingBudget); 597 yy += 28; 598 599 // Media Resolution 600 AddLabelToPanel(geminiSection, "Media Resolution:", innerMargin, yy); 601 yy += 20; 602 603 cmbGeminiMediaResolution = new ComboBox(); 604 cmbGeminiMediaResolution.Location = new Point(innerMargin, yy); 605 cmbGeminiMediaResolution.Size = new Size(innerWidth, 24); 606 cmbGeminiMediaResolution.BackColor = DarkField; 607 cmbGeminiMediaResolution.ForeColor = TextColor; 608 cmbGeminiMediaResolution.FlatStyle = FlatStyle.Flat; 609 cmbGeminiMediaResolution.Font = new Font("Segoe UI", 8f); 610 cmbGeminiMediaResolution.DropDownStyle = ComboBoxStyle.DropDownList; 611 cmbGeminiMediaResolution.Items.Add("MEDIA_RESOLUTION_LOW"); 612 cmbGeminiMediaResolution.Items.Add("MEDIA_RESOLUTION_MEDIUM"); 613 cmbGeminiMediaResolution.Items.Add("MEDIA_RESOLUTION_HIGH"); 614 string mediaRes = settings.MediaResolution ?? "MEDIA_RESOLUTION_LOW"; 615 int resIdx = cmbGeminiMediaResolution.Items.IndexOf(mediaRes); 616 cmbGeminiMediaResolution.SelectedIndex = resIdx >= 0 ? resIdx : 0; 617 cmbGeminiMediaResolution.SelectedIndexChanged += (s, e) => { 618 settings.MediaResolution = cmbGeminiMediaResolution.SelectedItem.ToString(); 619 TitlePageData.Instance.Save(); 620 }; 621 geminiSection.Controls.Add(cmbGeminiMediaResolution); 622 yy += 30; 623 624 // System Instruction 625 AddLabelToPanel(geminiSection, "Системная инструкция:", innerMargin, yy); 626 yy += 20; 627 628 txtGeminiInstruction = new TextBox(); 629 txtGeminiInstruction.Location = new Point(innerMargin, yy); 630 txtGeminiInstruction.Size = new Size(innerWidth, 120); 631 txtGeminiInstruction.BackColor = DarkField; 632 txtGeminiInstruction.ForeColor = TextColor; 633 txtGeminiInstruction.BorderStyle = BorderStyle.FixedSingle; 634 txtGeminiInstruction.Font = new Font("Segoe UI", 8f); 635 txtGeminiInstruction.Multiline = true; 636 txtGeminiInstruction.ScrollBars = ScrollBars.Vertical; 637 txtGeminiInstruction.Text = settings.SystemInstruction; 638 txtGeminiInstruction.TextChanged += (s, e) => { TitlePageData.Instance.Gemini.SystemInstruction = txtGeminiInstruction.Text; TitlePageData.Instance.Save(); }; 639 geminiSection.Controls.Add(txtGeminiInstruction); 640 yy += 130; 641 642 // Export/Import buttons 643 var btnExport = new Button(); 644 btnExport.Text = "Экспорт"; 645 btnExport.Location = new Point(innerMargin, yy); 646 btnExport.Size = new Size((innerWidth - 5) / 2, 26); 647 btnExport.FlatStyle = FlatStyle.Flat; 648 btnExport.FlatAppearance.BorderColor = BorderColor; 649 btnExport.BackColor = DarkField; 650 btnExport.ForeColor = TextColor; 651 btnExport.Font = new Font("Segoe UI", 8f); 652 btnExport.Cursor = Cursors.Hand; 653 btnExport.Click += BtnExportSettings_Click; 654 geminiSection.Controls.Add(btnExport); 655 656 var btnImport = new Button(); 657 btnImport.Text = "Импорт"; 658 btnImport.Location = new Point(btnExport.Right + 5, yy); 659 btnImport.Size = new Size((innerWidth - 5) / 2, 26); 660 btnImport.FlatStyle = FlatStyle.Flat; 661 btnImport.FlatAppearance.BorderColor = BorderColor; 662 btnImport.BackColor = DarkField; 663 btnImport.ForeColor = TextColor; 664 btnImport.Font = new Font("Segoe UI", 8f); 665 btnImport.Cursor = Cursors.Hand; 666 btnImport.Click += BtnImportSettings_Click; 667 geminiSection.Controls.Add(btnImport); 668 yy += 32; 669 670 // Store total height 671 geminiSection.Tag = yy; 672 } 673 674 private void BtnGeminiToggle_Click(object sender, EventArgs e) 675 { 676 geminiExpanded = !geminiExpanded; 677 678 if (geminiExpanded) 679 { 680 btnGeminiToggle.Text = "Gemini AI ▼"; 681 geminiSection.Visible = true; 682 int expandedHeight = (int)geminiSection.Tag; 683 geminiSection.Size = new Size(geminiSection.Width, expandedHeight); 684 // Refresh model list colors 685 if (lstGeminiModels != null) 686 lstGeminiModels.Invalidate(); 687 } 688 else 689 { 690 btnGeminiToggle.Text = "Gemini AI ▶"; 691 geminiSection.Size = new Size(geminiSection.Width, 0); 692 geminiSection.Visible = false; 693 } 694 695 UpdateSectionPositions(12); // margin 696 } 697 698 private void LstGeminiModels_DrawItem(object sender, DrawItemEventArgs e) 699 { 700 if (e.Index < 0) return; 701 702 var listBox = (ListBox)sender; 703 string model = listBox.Items[e.Index].ToString(); 704 var settings = TitlePageData.Instance.Gemini; 705 var status = settings.GetModelStatus(model); 706 707 // Background 708 e.DrawBackground(); 709 710 // Status indicator color 711 Color statusColor; 712 switch (status) 713 { 714 case ModelStatusType.Working: 715 statusColor = Color.FromArgb(50, 200, 50); // Green 716 break; 717 case ModelStatusType.RateLimited: 718 statusColor = Color.FromArgb(220, 180, 50); // Yellow 719 break; 720 case ModelStatusType.Broken: 721 statusColor = Color.FromArgb(200, 50, 50); // Red 722 break; 723 default: // Unknown 724 statusColor = Color.FromArgb(100, 100, 100); // Gray 725 break; 726 } 727 728 // Draw status circle 729 using (var brush = new SolidBrush(statusColor)) 730 { 731 e.Graphics.FillEllipse(brush, e.Bounds.X + 4, e.Bounds.Y + 4, 10, 10); 732 } 733 734 // Draw text 735 Color textColor = (e.State & DrawItemState.Selected) != 0 ? Color.White : TextColor; 736 if (status == ModelStatusType.Broken) 737 textColor = Color.FromArgb(180, 100, 100); // Dimmed for broken models 738 739 using (var brush = new SolidBrush(textColor)) 740 { 741 e.Graphics.DrawString(model, e.Font, brush, e.Bounds.X + 18, e.Bounds.Y + 2); 742 } 743 744 // Focus rectangle 745 e.DrawFocusRectangle(); 746 } 747 748 private void LstGeminiModels_SelectedIndexChanged(object sender, EventArgs e) 749 { 750 if (lstGeminiModels.SelectedIndex < 0) return; 751 string model = lstGeminiModels.SelectedItem.ToString(); 752 TitlePageData.Instance.Gemini.Model = model; 753 TitlePageData.Instance.Save(); 754 } 755 756 private void BtnResetModelStatuses_Click(object sender, EventArgs e) 757 { 758 TitlePageData.Instance.Gemini.ResetModelStatuses(); 759 TitlePageData.Instance.Save(); 760 lstGeminiModels.Invalidate(); // Refresh colors 761 App.TrayApp.Instance.ShowNotification("Статусы сброшены", "Все модели снова доступны", ToolTipIcon.Info); 762 } 763 764 private void RefreshApiKeysList() 765 { 766 lstApiKeys.Items.Clear(); 767 var settings = TitlePageData.Instance.Gemini; 768 769 // Migrate legacy key if needed 770 if ((settings.ApiKeys == null || settings.ApiKeys.Count == 0) && !string.IsNullOrEmpty(TitlePageData.Instance.GeminiApiKey)) 771 { 772 settings.AddApiKey(TitlePageData.Instance.GeminiApiKey); 773 TitlePageData.Instance.Save(); 774 } 775 776 if (settings.ApiKeys != null) 777 { 778 foreach (var key in settings.ApiKeys) 779 { 780 lstApiKeys.Items.Add(key); 781 } 782 } 783 784 // Select current key 785 if (settings.CurrentApiKeyIndex >= 0 && settings.CurrentApiKeyIndex < lstApiKeys.Items.Count) 786 lstApiKeys.SelectedIndex = settings.CurrentApiKeyIndex; 787 else if (lstApiKeys.Items.Count > 0) 788 lstApiKeys.SelectedIndex = 0; 789 } 790 791 private void LstApiKeys_DrawItem(object sender, DrawItemEventArgs e) 792 { 793 if (e.Index < 0) return; 794 795 var listBox = (ListBox)sender; 796 string key = listBox.Items[e.Index].ToString(); 797 var settings = TitlePageData.Instance.Gemini; 798 799 e.DrawBackground(); 800 801 // Current key indicator 802 bool isCurrent = e.Index == settings.CurrentApiKeyIndex; 803 string displayText = MaskApiKey(key); 804 if (isCurrent) 805 displayText = "► " + displayText; 806 807 Color textColor = (e.State & DrawItemState.Selected) != 0 ? Color.White : TextColor; 808 using (var brush = new SolidBrush(textColor)) 809 { 810 e.Graphics.DrawString(displayText, e.Font, brush, e.Bounds.X + 2, e.Bounds.Y + 1); 811 } 812 813 e.DrawFocusRectangle(); 814 } 815 816 private string MaskApiKey(string key) 817 { 818 if (string.IsNullOrEmpty(key) || key.Length < 8) 819 return "****"; 820 return key.Substring(0, 4) + "..." + key.Substring(key.Length - 4); 821 } 822 823 private void LstApiKeys_SelectedIndexChanged(object sender, EventArgs e) 824 { 825 if (lstApiKeys.SelectedIndex < 0) return; 826 var settings = TitlePageData.Instance.Gemini; 827 settings.CurrentApiKeyIndex = lstApiKeys.SelectedIndex; 828 TitlePageData.Instance.Save(); 829 lstApiKeys.Invalidate(); // Refresh to show current indicator 830 } 831 832 private void BtnAddApiKey_Click(object sender, EventArgs e) 833 { 834 string key = txtGeminiApiKey.Text.Trim(); 835 if (string.IsNullOrEmpty(key) || key == "Новый ключ...") 836 { 837 App.TrayApp.Instance.ShowNotification("Ошибка", "Введите API ключ", ToolTipIcon.Warning); 838 return; 839 } 840 841 var settings = TitlePageData.Instance.Gemini; 842 if (settings.AddApiKey(key)) 843 { 844 TitlePageData.Instance.Save(); 845 RefreshApiKeysList(); 846 txtGeminiApiKey.Text = "Новый ключ..."; 847 txtGeminiApiKey.ForeColor = DimTextColor; 848 App.TrayApp.Instance.ShowNotification("Ключ добавлен", "API ключ сохранён", ToolTipIcon.Info); 849 } 850 else 851 { 852 App.TrayApp.Instance.ShowNotification("Ошибка", "Ключ уже существует", ToolTipIcon.Warning); 853 } 854 } 855 856 private void BtnRemoveApiKey_Click(object sender, EventArgs e) 857 { 858 if (lstApiKeys.SelectedIndex < 0) return; 859 860 var settings = TitlePageData.Instance.Gemini; 861 if (settings.ApiKeys != null && settings.ApiKeys.Count > 0) 862 { 863 settings.ApiKeys.RemoveAt(lstApiKeys.SelectedIndex); 864 if (settings.CurrentApiKeyIndex >= settings.ApiKeys.Count) 865 settings.CurrentApiKeyIndex = Math.Max(0, settings.ApiKeys.Count - 1); 866 TitlePageData.Instance.Save(); 867 RefreshApiKeysList(); 868 } 869 } 870 871 private void BtnExportSettings_Click(object sender, EventArgs e) 872 { 873 using (var sfd = new SaveFileDialog()) 874 { 875 sfd.Filter = "JSON файл|*.json"; 876 sfd.Title = "Экспорт настроек"; 877 sfd.FileName = "windowcapture_settings.json"; 878 879 if (sfd.ShowDialog() == DialogResult.OK) 880 { 881 if (TitlePageData.Instance.ExportSettings(sfd.FileName)) 882 { 883 App.TrayApp.Instance.ShowNotification("Экспорт", "Настройки сохранены (без API ключей)", ToolTipIcon.Info); 884 } 885 else 886 { 887 App.TrayApp.Instance.ShowNotification("Ошибка", "Не удалось экспортировать настройки", ToolTipIcon.Error); 888 } 889 } 890 } 891 } 892 893 private void BtnImportSettings_Click(object sender, EventArgs e) 894 { 895 using (var ofd = new OpenFileDialog()) 896 { 897 ofd.Filter = "JSON файл|*.json"; 898 ofd.Title = "Импорт настроек"; 899 900 if (ofd.ShowDialog() == DialogResult.OK) 901 { 902 if (TitlePageData.Instance.ImportSettings(ofd.FileName)) 903 { 904 App.TrayApp.Instance.ShowNotification("Импорт", "Настройки загружены", ToolTipIcon.Info); 905 // Refresh UI 906 RefreshSettingsUI(); 907 } 908 else 909 { 910 App.TrayApp.Instance.ShowNotification("Ошибка", "Не удалось импортировать настройки", ToolTipIcon.Error); 911 } 912 } 913 } 914 } 915 916 private void RefreshSettingsUI() 917 { 918 var settings = TitlePageData.Instance.Gemini; 919 920 // Refresh model list 921 if (lstGeminiModels != null) 922 { 923 int idx = lstGeminiModels.Items.IndexOf(settings.Model); 924 if (idx >= 0) lstGeminiModels.SelectedIndex = idx; 925 lstGeminiModels.Invalidate(); 926 } 927 928 // Refresh thinking budget 929 if (txtGeminiThinkingBudget != null) 930 txtGeminiThinkingBudget.Text = settings.ThinkingBudget.ToString(); 931 932 // Refresh media resolution 933 if (cmbGeminiMediaResolution != null) 934 { 935 int resIdx = cmbGeminiMediaResolution.Items.IndexOf(settings.MediaResolution ?? "MEDIA_RESOLUTION_LOW"); 936 if (resIdx >= 0) cmbGeminiMediaResolution.SelectedIndex = resIdx; 937 } 938 939 // Refresh system instruction 940 if (txtGeminiInstruction != null) 941 txtGeminiInstruction.Text = settings.SystemInstruction; 942 943 // Refresh title page data 944 LoadTitlePageData(); 945 } 946 947 private void InitializeFormattingSection(int fieldWidth, int margin) 948 { 949 int innerMargin = 8; 950 int innerWidth = fieldWidth - innerMargin * 2; 951 int yy = 10; 952 953 // Page numbering button 954 btnAddPageNumbering = new Button(); 955 btnAddPageNumbering.Text = "Добавить нумерацию страниц"; 956 btnAddPageNumbering.Location = new Point(innerMargin, yy); 957 btnAddPageNumbering.Size = new Size(innerWidth, 28); 958 btnAddPageNumbering.FlatStyle = FlatStyle.Flat; 959 btnAddPageNumbering.FlatAppearance.BorderColor = AccentColor; 960 btnAddPageNumbering.BackColor = DarkField; 961 btnAddPageNumbering.ForeColor = TextColor; 962 btnAddPageNumbering.Font = new Font("Segoe UI", 9f); 963 btnAddPageNumbering.Cursor = Cursors.Hand; 964 btnAddPageNumbering.TabStop = false; 965 btnAddPageNumbering.Click += BtnAddPageNumbering_Click; 966 formattingSection.Controls.Add(btnAddPageNumbering); 967 yy += 32; 968 969 // Description label 970 var lblDesc = new Label(); 971 lblDesc.Text = "(Первая страница без номера,\n со 2-й: снизу по центру, TNR 14)"; 972 lblDesc.Location = new Point(innerMargin, yy); 973 lblDesc.Size = new Size(innerWidth, 35); 974 lblDesc.ForeColor = DimTextColor; 975 lblDesc.Font = new Font("Segoe UI", 8f); 976 formattingSection.Controls.Add(lblDesc); 977 yy += 42; 978 979 // Originalize button 980 btnOriginalize = new Button(); 981 btnOriginalize.Text = "Оригинализировать описания"; 982 btnOriginalize.Location = new Point(innerMargin, yy); 983 btnOriginalize.Size = new Size(innerWidth, 28); 984 btnOriginalize.FlatStyle = FlatStyle.Flat; 985 btnOriginalize.FlatAppearance.BorderColor = AccentColor; 986 btnOriginalize.BackColor = DarkField; 987 btnOriginalize.ForeColor = TextColor; 988 btnOriginalize.Font = new Font("Segoe UI", 9f); 989 btnOriginalize.Cursor = Cursors.Hand; 990 btnOriginalize.TabStop = false; 991 btnOriginalize.Click += BtnOriginalize_Click; 992 formattingSection.Controls.Add(btnOriginalize); 993 yy += 30; 994 995 var lblOrigDesc = new Label(); 996 lblOrigDesc.Text = "(Перефразирует описания и\n названия рисунков через AI)"; 997 lblOrigDesc.Location = new Point(innerMargin, yy); 998 lblOrigDesc.Size = new Size(innerWidth, 35); 999 lblOrigDesc.ForeColor = DimTextColor; 1000 lblOrigDesc.Font = new Font("Segoe UI", 8f); 1001 formattingSection.Controls.Add(lblOrigDesc); 1002 yy += 40; 1003 1004 // Store total height 1005 formattingSection.Tag = yy; 1006 } 1007 1008 private void BtnAddPageNumbering_Click(object sender, EventArgs e) 1009 { 1010 if (cmbDocuments.SelectedItem == null) 1011 { 1012 lblStatus.Text = "Выберите документ"; 1013 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1014 return; 1015 } 1016 1017 var doc = cmbDocuments.SelectedItem as WordDocumentInfo; 1018 if (doc == null) return; 1019 1020 WordIntegration.SelectedDocument = doc; 1021 1022 lblStatus.Text = "Добавление нумерации..."; 1023 lblStatus.ForeColor = DimTextColor; 1024 btnAddPageNumbering.Enabled = false; 1025 Application.DoEvents(); 1026 1027 bool result = WordIntegration.AddPageNumbering(); 1028 1029 if (result) 1030 { 1031 lblStatus.Text = "Нумерация добавлена!"; 1032 lblStatus.ForeColor = SuccessColor; 1033 } 1034 else 1035 { 1036 lblStatus.Text = "Ошибка нумерации"; 1037 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1038 } 1039 1040 btnAddPageNumbering.Enabled = true; 1041 } 1042 1043 private void UpdateSectionPositions(int margin) 1044 { 1045 // Calculate Y position after title page section 1046 int y = btnTitlePageToggle.Bottom + 5; 1047 1048 if (titlePageExpanded) 1049 { 1050 titlePageSection.Location = new Point(margin, y); 1051 y += (int)titlePageSection.Tag; 1052 } 1053 1054 // Position formatting toggle 1055 btnFormattingToggle.Location = new Point(margin, y); 1056 y += btnFormattingToggle.Height + 5; 1057 1058 if (formattingExpanded) 1059 { 1060 formattingSection.Location = new Point(margin, y); 1061 y += (int)formattingSection.Tag; 1062 } 1063 1064 // Position Gemini toggle 1065 btnGeminiToggle.Location = new Point(margin, y); 1066 y += btnGeminiToggle.Height + 5; 1067 1068 if (geminiExpanded) 1069 { 1070 geminiSection.Location = new Point(margin, y); 1071 } 1072 1073 // Refresh glass scroll after layout changes 1074 if (glassScroll != null) glassScroll.SetupScroll(); 1075 } 1076 1077 private int AddComboBoxWithAdd(Panel parent, string labelText, int x, int y, int width, 1078 out ComboBox combo, string fieldName) 1079 { 1080 AddLabelToPanel(parent, labelText, x, y); 1081 y += 20; 1082 1083 combo = new ComboBox(); 1084 combo.Location = new Point(x, y); 1085 combo.Size = new Size(width - 35, 24); 1086 combo.DropDownStyle = ComboBoxStyle.DropDown; // Allow editing 1087 combo.BackColor = DarkField; 1088 combo.ForeColor = TextColor; 1089 combo.FlatStyle = FlatStyle.Flat; 1090 combo.Font = new Font("Segoe UI", 9f); 1091 combo.Tag = fieldName; // Store field name for lookup 1092 // Auto-save when leaving the combo (on lost focus or selection change) 1093 combo.Leave += ComboBox_Leave; 1094 combo.SelectedIndexChanged += ComboBox_SelectedIndexChanged; 1095 parent.Controls.Add(combo); 1096 1097 var btnAdd = new Button(); 1098 btnAdd.Text = "+"; 1099 btnAdd.Location = new Point(combo.Right + 3, y); 1100 btnAdd.Size = new Size(30, 24); 1101 btnAdd.FlatStyle = FlatStyle.Flat; 1102 btnAdd.FlatAppearance.BorderColor = BorderColor; 1103 btnAdd.BackColor = DarkField; 1104 btnAdd.ForeColor = TextColor; 1105 btnAdd.TabStop = false; 1106 btnAdd.Tag = combo; // Reference to associated combo 1107 btnAdd.Click += BtnAddToList_Click; 1108 parent.Controls.Add(btnAdd); 1109 1110 return y + 32; 1111 } 1112 1113 private void ComboBox_Leave(object sender, EventArgs e) 1114 { 1115 // Auto-save value when leaving the combo box 1116 var combo = sender as ComboBox; 1117 if (combo == null) return; 1118 1119 string value = combo.Text.Trim(); 1120 if (string.IsNullOrEmpty(value)) return; 1121 1122 string fieldName = combo.Tag as string; 1123 if (string.IsNullOrEmpty(fieldName)) return; 1124 1125 var data = TitlePageData.Instance; 1126 if (data.AddToList(fieldName, value)) 1127 { 1128 // Refresh combo items to include the new value 1129 var list = data.GetList(fieldName); 1130 if (list != null) 1131 { 1132 int idx = list.IndexOf(value); 1133 LoadComboItems(combo, list, idx); 1134 } 1135 } 1136 } 1137 1138 private void ComboBox_SelectedIndexChanged(object sender, EventArgs e) 1139 { 1140 // Save selected index when changed 1141 var combo = sender as ComboBox; 1142 if (combo == null || combo.SelectedIndex < 0) return; 1143 1144 string fieldName = combo.Tag as string; 1145 if (string.IsNullOrEmpty(fieldName)) return; 1146 1147 var data = TitlePageData.Instance; 1148 data.SetLastSelectedIndex(fieldName, combo.SelectedIndex); 1149 } 1150 1151 private void BtnFormattingToggle_Click(object sender, EventArgs e) 1152 { 1153 formattingExpanded = !formattingExpanded; 1154 1155 if (formattingExpanded) 1156 { 1157 btnFormattingToggle.Text = "Форматирование ▼"; 1158 formattingSection.Visible = true; 1159 int expandedHeight = (int)formattingSection.Tag; 1160 formattingSection.Size = new Size(formattingSection.Width, expandedHeight); 1161 } 1162 else 1163 { 1164 btnFormattingToggle.Text = "Форматирование ▶"; 1165 formattingSection.Size = new Size(formattingSection.Width, 0); 1166 formattingSection.Visible = false; 1167 } 1168 1169 UpdateSectionPositions(12); // margin 1170 } 1171 1172 private void BtnTitlePageToggle_Click(object sender, EventArgs e) 1173 { 1174 titlePageExpanded = !titlePageExpanded; 1175 1176 if (titlePageExpanded) 1177 { 1178 btnTitlePageToggle.Text = "Титульный лист ▼"; 1179 titlePageSection.Visible = true; 1180 int expandedHeight = (int)titlePageSection.Tag; 1181 titlePageSection.Size = new Size(titlePageSection.Width, expandedHeight); 1182 LoadTitlePageData(); 1183 } 1184 else 1185 { 1186 btnTitlePageToggle.Text = "Титульный лист ▶"; 1187 titlePageSection.Size = new Size(titlePageSection.Width, 0); 1188 titlePageSection.Visible = false; 1189 } 1190 1191 UpdateSectionPositions(12); // margin 1192 } 1193 1194 private void LoadTitlePageData() 1195 { 1196 var data = TitlePageData.Instance; 1197 1198 txtTemplatePath.Text = data.TemplatePath ?? ""; 1199 txtWorkNumber.Text = data.LastWorkNumber ?? "1"; 1200 1201 LoadComboItems(cmbDiscipline, data.Disciplines, data.LastSelected.Discipline); 1202 LoadComboItems(cmbTopic, data.Topics, data.LastSelected.Topic); 1203 LoadComboItems(cmbStudent, data.Students, data.LastSelected.Student); 1204 LoadComboItems(cmbGroup, data.Groups, data.LastSelected.Group); 1205 LoadComboItems(cmbTeacher, data.Teachers, data.LastSelected.Teacher); 1206 } 1207 1208 private void LoadComboItems(ComboBox combo, List<string> items, int selectedIndex) 1209 { 1210 combo.Items.Clear(); 1211 if (items != null) 1212 { 1213 foreach (var item in items) 1214 combo.Items.Add(item); 1215 } 1216 1217 if (selectedIndex >= 0 && selectedIndex < combo.Items.Count) 1218 combo.SelectedIndex = selectedIndex; 1219 else if (combo.Items.Count > 0) 1220 combo.SelectedIndex = 0; 1221 } 1222 1223 private void BtnBrowseTemplate_Click(object sender, EventArgs e) 1224 { 1225 using (var ofd = new OpenFileDialog()) 1226 { 1227 ofd.Filter = "Word Documents|*.docx;*.doc|All Files|*.*"; 1228 ofd.Title = "Выберите шаблон титульного листа"; 1229 1230 if (ofd.ShowDialog() == DialogResult.OK) 1231 { 1232 txtTemplatePath.Text = ofd.FileName; 1233 var data = TitlePageData.Instance; 1234 data.TemplatePath = ofd.FileName; 1235 data.Save(); 1236 } 1237 } 1238 } 1239 1240 private void BtnAddToList_Click(object sender, EventArgs e) 1241 { 1242 var btn = sender as Button; 1243 if (btn == null) return; 1244 1245 var combo = btn.Tag as ComboBox; 1246 if (combo == null) return; 1247 1248 string value = combo.Text.Trim(); 1249 if (string.IsNullOrEmpty(value)) return; 1250 1251 string fieldName = combo.Tag as string; 1252 if (string.IsNullOrEmpty(fieldName)) return; 1253 1254 var data = TitlePageData.Instance; 1255 if (data.AddToList(fieldName, value)) 1256 { 1257 // Refresh combo items 1258 var list = data.GetList(fieldName); 1259 if (list != null) 1260 { 1261 int idx = list.IndexOf(value); 1262 LoadComboItems(combo, list, idx); 1263 } 1264 } 1265 } 1266 1267 private void BtnInsertTitlePage_Click(object sender, EventArgs e) 1268 { 1269 if (cmbDocuments.SelectedItem == null) 1270 { 1271 lblStatus.Text = "Выберите документ"; 1272 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1273 return; 1274 } 1275 1276 if (string.IsNullOrEmpty(txtTemplatePath.Text) || !System.IO.File.Exists(txtTemplatePath.Text)) 1277 { 1278 lblStatus.Text = "Выберите шаблон"; 1279 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1280 return; 1281 } 1282 1283 var doc = cmbDocuments.SelectedItem as WordDocumentInfo; 1284 if (doc == null) return; 1285 1286 WordIntegration.SelectedDocument = doc; 1287 1288 // Save current selections 1289 SaveTitlePageSelections(); 1290 1291 // Prepare replacements 1292 var replacements = new Dictionary<string, string> 1293 { 1294 { "%WORKNUMBER%", txtWorkNumber.Text.Trim() }, 1295 { "%DISCIPLINE%", cmbDiscipline.Text.Trim() }, 1296 { "%TOPIC%", cmbTopic.Text.Trim() }, 1297 { "%STUDENT%", cmbStudent.Text.Trim() }, 1298 { "%GROUP%", cmbGroup.Text.Trim() }, 1299 { "%TEACHER%", cmbTeacher.Text.Trim() } 1300 }; 1301 1302 lblStatus.Text = "Вставка титульного листа..."; 1303 lblStatus.ForeColor = DimTextColor; 1304 btnInsertTitlePage.Enabled = false; 1305 Application.DoEvents(); 1306 1307 bool result = WordIntegration.InsertTitlePage(txtTemplatePath.Text, replacements); 1308 1309 if (result) 1310 { 1311 lblStatus.Text = "Титульный лист вставлен!"; 1312 lblStatus.ForeColor = SuccessColor; 1313 } 1314 else 1315 { 1316 lblStatus.Text = "Ошибка вставки"; 1317 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1318 } 1319 1320 btnInsertTitlePage.Enabled = true; 1321 } 1322 1323 private void SaveTitlePageSelections() 1324 { 1325 var data = TitlePageData.Instance; 1326 data.LastWorkNumber = txtWorkNumber.Text; 1327 1328 // Add current values to lists if not present 1329 data.AddToList("discipline", cmbDiscipline.Text.Trim()); 1330 data.AddToList("topic", cmbTopic.Text.Trim()); 1331 data.AddToList("student", cmbStudent.Text.Trim()); 1332 data.AddToList("group", cmbGroup.Text.Trim()); 1333 data.AddToList("teacher", cmbTeacher.Text.Trim()); 1334 1335 // Save selected indices 1336 data.SetLastSelectedIndex("discipline", cmbDiscipline.SelectedIndex); 1337 data.SetLastSelectedIndex("topic", cmbTopic.SelectedIndex); 1338 data.SetLastSelectedIndex("student", cmbStudent.SelectedIndex); 1339 data.SetLastSelectedIndex("group", cmbGroup.SelectedIndex); 1340 data.SetLastSelectedIndex("teacher", cmbTeacher.SelectedIndex); 1341 } 1342 1343 private Label AddLabelToScroll(string text, int x, int y) 1344 { 1345 var lbl = new Label(); 1346 lbl.Text = text; 1347 lbl.Location = new Point(x, y); 1348 lbl.AutoSize = true; 1349 lbl.ForeColor = TextColor; 1350 lbl.Font = new Font("Segoe UI", 9f); 1351 scrollPanel.Controls.Add(lbl); 1352 return lbl; 1353 } 1354 1355 private Label AddLabelToPanel(Panel panel, string text, int x, int y) 1356 { 1357 var lbl = new Label(); 1358 lbl.Text = text; 1359 lbl.Location = new Point(x, y); 1360 lbl.AutoSize = true; 1361 lbl.ForeColor = TextColor; 1362 lbl.Font = new Font("Segoe UI", 9f); 1363 panel.Controls.Add(lbl); 1364 return lbl; 1365 } 1366 1367 private void TextBox_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e) 1368 { 1369 // Block space at preview level before it reaches the control 1370 if (e.KeyCode == Keys.Space && ignoreSpaceUntilRelease) 1371 { 1372 e.IsInputKey = false; 1373 } 1374 } 1375 1376 private void TextBox_KeyDown(object sender, KeyEventArgs e) 1377 { 1378 // Ctrl+Space to insert (check BEFORE ignoreSpaceUntilRelease) 1379 if (e.KeyCode == Keys.Space && e.Control) 1380 { 1381 e.SuppressKeyPress = true; 1382 e.Handled = true; 1383 BtnInsert_Click(null, null); 1384 return; 1385 } 1386 1387 if (e.KeyCode == Keys.Space && ignoreSpaceUntilRelease) 1388 { 1389 e.SuppressKeyPress = true; 1390 e.Handled = true; 1391 return; 1392 } 1393 1394 // Enter to insert 1395 if (e.KeyCode == Keys.Enter) 1396 { 1397 bool isDescription = (sender == txtDescription); 1398 // In description, require Ctrl+Enter; in caption, just Enter 1399 if (!isDescription || e.Control) 1400 { 1401 e.SuppressKeyPress = true; 1402 e.Handled = true; 1403 BtnInsert_Click(null, null); 1404 } 1405 } 1406 } 1407 1408 private void TextBox_KeyUp(object sender, KeyEventArgs e) 1409 { 1410 if (e.KeyCode == Keys.Space) 1411 { 1412 ignoreSpaceUntilRelease = false; 1413 if (pendingFocus) 1414 { 1415 pendingFocus = false; 1416 txtDescription.Focus(); 1417 } 1418 } 1419 } 1420 1421 private void Panel_KeyDown(object sender, KeyEventArgs e) 1422 { 1423 // Ctrl+Space to insert from anywhere in panel 1424 if (e.KeyCode == Keys.Space && e.Control) 1425 { 1426 e.SuppressKeyPress = true; 1427 e.Handled = true; 1428 BtnInsert_Click(null, null); 1429 } 1430 } 1431 1432 /// <summary> 1433 /// Called from EditorForm when space key is released 1434 /// </summary> 1435 public void OnSpaceReleased() 1436 { 1437 ignoreSpaceUntilRelease = false; 1438 spaceWasReleased = true; 1439 TrySetFocus(); 1440 } 1441 1442 private bool spaceWasReleased = false; 1443 1444 private void TrySetFocus() 1445 { 1446 // Only set focus if: space was released AND animation finished AND we have pending focus 1447 if (pendingFocus && spaceWasReleased && !isAnimating) 1448 { 1449 pendingFocus = false; 1450 spaceWasReleased = false; 1451 // Small delay to ensure everything is settled 1452 var focusTimer = new System.Windows.Forms.Timer(); 1453 focusTimer.Interval = 50; 1454 focusTimer.Tick += (s, e) => 1455 { 1456 focusTimer.Stop(); 1457 focusTimer.Dispose(); 1458 if (Width > 0) // Panel is still visible 1459 { 1460 txtDescription.Focus(); 1461 txtDescription.SelectAll(); 1462 } 1463 }; 1464 focusTimer.Start(); 1465 } 1466 } 1467 1468 private Label AddLabel(string text, int x, int y) 1469 { 1470 // Legacy method - now redirects to scrollPanel 1471 return AddLabelToScroll(text, x, y); 1472 } 1473 1474 private void SetupRefreshTimer() 1475 { 1476 // Timer disabled to prevent interference with Word context menus 1477 // Preview is updated manually when document is selected or on Show() 1478 refreshTimer = new System.Windows.Forms.Timer(); 1479 refreshTimer.Interval = 60000; // 60 seconds - disabled effectively 1480 refreshTimer.Tick += RefreshTimer_Tick; 1481 } 1482 1483 private void RefreshTimer_Tick(object sender, EventArgs e) 1484 { 1485 // Disabled - was causing Word context menus to close due to COM automation calls 1486 // Preview now updates only on document selection change 1487 } 1488 1489 public void Show(Bitmap screenshotImage) 1490 { 1491 imageToInsert = screenshotImage; 1492 ignoreSpaceUntilRelease = true; // Ignore space until user releases it 1493 pendingFocus = true; // Will focus after space is released 1494 spaceWasReleased = false; // Reset 1495 LoadDocuments(); 1496 1497 if (cmbDocuments.Items.Count > 0) 1498 { 1499 targetWidth = PanelWidth; 1500 isAnimating = true; 1501 var animTimer = new System.Windows.Forms.Timer(); 1502 animTimer.Interval = 16; 1503 animTimer.Tick += (s, e) => 1504 { 1505 if (Width < targetWidth) 1506 { 1507 Width = Math.Min(Width + AnimationStep, targetWidth); 1508 parentEditor.Invalidate(); 1509 } 1510 else 1511 { 1512 isAnimating = false; 1513 animTimer.Stop(); 1514 animTimer.Dispose(); 1515 if (glassScroll != null) glassScroll.SetupScroll(); 1516 // Try to set focus (if space was already released) 1517 TrySetFocus(); 1518 } 1519 }; 1520 animTimer.Start(); 1521 // refreshTimer.Start(); // Disabled to prevent Word interference 1522 } 1523 } 1524 1525 public new void Hide() 1526 { 1527 // Already hiding — don't create another timer 1528 if (isAnimating && targetWidth == 0) return; 1529 1530 // If docked Fill inside a separate window, close that window instead 1531 // (Width animation can't work with Dock=Fill — layout overrides it) 1532 if (Dock == DockStyle.Fill && Parent is Form) 1533 { 1534 ((Form)Parent).Close(); 1535 return; 1536 } 1537 1538 targetWidth = 0; 1539 isAnimating = true; 1540 var animTimer = new System.Windows.Forms.Timer(); 1541 animTimer.Interval = 16; 1542 animTimer.Tick += (s, e) => 1543 { 1544 if (Width > 0) 1545 { 1546 Width = Math.Max(Width - AnimationStep, 0); 1547 parentEditor.Invalidate(); 1548 } 1549 else 1550 { 1551 isAnimating = false; 1552 animTimer.Stop(); 1553 animTimer.Dispose(); 1554 if (PanelClosed != null) 1555 PanelClosed(this, EventArgs.Empty); 1556 } 1557 }; 1558 animTimer.Start(); 1559 } 1560 1561 public bool IsExpanded 1562 { 1563 get { return Width > 0 || isAnimating; } 1564 } 1565 1566 private void LoadDocuments() 1567 { 1568 cmbDocuments.Items.Clear(); 1569 var docs = WordIntegration.GetOpenDocuments(); 1570 1571 if (docs.Count == 0) 1572 { 1573 lblStatus.Text = "Word не найден"; 1574 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1575 btnInsert.Enabled = false; 1576 } 1577 else 1578 { 1579 foreach (var doc in docs) 1580 { 1581 cmbDocuments.Items.Add(doc); 1582 } 1583 1584 // Select previously selected or first 1585 var prev = WordIntegration.SelectedDocument; 1586 int selIdx = 0; 1587 if (prev != null) 1588 { 1589 for (int i = 0; i < cmbDocuments.Items.Count; i++) 1590 { 1591 var item = cmbDocuments.Items[i] as WordDocumentInfo; 1592 if (item != null && item.FullPath == prev.FullPath) 1593 { 1594 selIdx = i; 1595 break; 1596 } 1597 } 1598 } 1599 cmbDocuments.SelectedIndex = selIdx; 1600 1601 lblStatus.Text = "Готово к вставке"; 1602 lblStatus.ForeColor = SuccessColor; 1603 btnInsert.Enabled = true; 1604 } 1605 } 1606 1607 private void CmbDocuments_SelectedIndexChanged(object sender, EventArgs e) 1608 { 1609 var doc = cmbDocuments.SelectedItem as WordDocumentInfo; 1610 if (doc != null) 1611 { 1612 WordIntegration.SelectedDocument = doc; 1613 lblDocName.Text = doc.Name; 1614 UpdatePreview(); 1615 } 1616 } 1617 1618 private void UpdatePreview() 1619 { 1620 try 1621 { 1622 // Method 1: Try CopyAsPicture from Word (works from memory, no file needed) 1623 var docPreview = WordIntegration.GetDocumentPreview(); 1624 if (docPreview != null) 1625 { 1626 SetPreviewImage(ScalePreview(docPreview)); 1627 return; 1628 } 1629 1630 // Method 2: Try to render docx file directly 1631 var selectedDoc = WordIntegration.SelectedDocument; 1632 if (selectedDoc != null && !string.IsNullOrEmpty(selectedDoc.FullPath) && 1633 System.IO.File.Exists(selectedDoc.FullPath)) 1634 { 1635 string tempPath = System.IO.Path.Combine( 1636 System.IO.Path.GetTempPath(), 1637 "wc_preview_" + Guid.NewGuid().ToString("N") + ".docx"); 1638 1639 try 1640 { 1641 using (var source = new System.IO.FileStream(selectedDoc.FullPath, 1642 System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite)) 1643 using (var dest = new System.IO.FileStream(tempPath, 1644 System.IO.FileMode.Create, System.IO.FileAccess.Write)) 1645 { 1646 source.CopyTo(dest); 1647 } 1648 1649 var docxPreview = DocxRenderer.RenderDocx(tempPath, 140); 1650 try { System.IO.File.Delete(tempPath); } catch { } 1651 1652 if (docxPreview != null) 1653 { 1654 SetPreviewImage(docxPreview); 1655 return; 1656 } 1657 } 1658 catch 1659 { 1660 try { System.IO.File.Delete(tempPath); } catch { } 1661 } 1662 } 1663 1664 // Method 3: Fallback to window screenshot 1665 var screenshot = WordIntegration.GetWordWindowScreenshot(); 1666 if (screenshot != null) 1667 { 1668 SetPreviewImage(ScalePreview(screenshot)); 1669 } 1670 else 1671 { 1672 ShowPlaceholder(); 1673 } 1674 } 1675 catch { } 1676 } 1677 1678 private void SetPreviewImage(Bitmap bmp) 1679 { 1680 if (previewBox.Image != null) 1681 { 1682 previewBox.Image.Dispose(); 1683 } 1684 previewBox.Image = bmp; 1685 } 1686 1687 private Bitmap ScalePreview(Bitmap source) 1688 { 1689 // Scale to fit preview box 1690 int maxWidth = previewBox.Width > 0 ? previewBox.Width : 280; 1691 int maxHeight = previewBox.Height > 0 ? previewBox.Height : 140; 1692 1693 float scale = Math.Min((float)maxWidth / source.Width, (float)maxHeight / source.Height); 1694 if (scale >= 1f) return source; 1695 1696 int newW = (int)(source.Width * scale); 1697 int newH = (int)(source.Height * scale); 1698 1699 var scaled = new Bitmap(newW, newH); 1700 using (var g = Graphics.FromImage(scaled)) 1701 { 1702 g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBilinear; 1703 g.DrawImage(source, 0, 0, newW, newH); 1704 } 1705 source.Dispose(); 1706 return scaled; 1707 } 1708 1709 private void ShowPlaceholder() 1710 { 1711 var bmp = new Bitmap(previewBox.Width > 0 ? previewBox.Width : 280, 1712 previewBox.Height > 0 ? previewBox.Height : 140); 1713 using (var g = Graphics.FromImage(bmp)) 1714 { 1715 g.Clear(DarkField); 1716 using (var brush = new SolidBrush(DimTextColor)) 1717 using (var font = new Font("Segoe UI", 9f)) 1718 { 1719 var text = "Документ не сохранён"; 1720 var size = g.MeasureString(text, font); 1721 g.DrawString(text, font, brush, 1722 (bmp.Width - size.Width) / 2, 1723 (bmp.Height - size.Height) / 2); 1724 } 1725 } 1726 if (previewBox.Image != null) 1727 previewBox.Image.Dispose(); 1728 previewBox.Image = bmp; 1729 } 1730 1731 private void BtnGenerateDescription_Click(object sender, EventArgs e) 1732 { 1733 if (imageToInsert == null) 1734 { 1735 lblStatus.Text = "Нет изображения"; 1736 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1737 return; 1738 } 1739 1740 if (!GeminiIntegration.IsConfigured) 1741 { 1742 lblStatus.Text = "API ключ не настроен (GEMINI_API_KEY)"; 1743 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1744 return; 1745 } 1746 1747 // Disable button and show progress 1748 btnGenerateDescription.Enabled = false; 1749 btnGenerateDescription.Text = "Генерация..."; 1750 lblStatus.Text = "Запрос к AI..."; 1751 lblStatus.ForeColor = DimTextColor; 1752 1753 // Use async method with callbacks 1754 GeminiIntegration.GenerateDescriptionAsync(imageToInsert, 1755 // On success 1756 (result) => 1757 { 1758 if (result != null && (!string.IsNullOrEmpty(result.Item1) || !string.IsNullOrEmpty(result.Item2))) 1759 { 1760 // Fill in the fields 1761 if (!string.IsNullOrEmpty(result.Item1)) 1762 txtDescription.Text = result.Item1; 1763 if (!string.IsNullOrEmpty(result.Item2)) 1764 txtCaption.Text = result.Item2; 1765 1766 lblStatus.Text = "Описание сгенерировано"; 1767 lblStatus.ForeColor = SuccessColor; 1768 } 1769 else 1770 { 1771 // Show detailed error 1772 string error = GeminiIntegration.LastError ?? "Неизвестная ошибка"; 1773 lblStatus.Text = "Ошибка: " + error; 1774 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1775 System.Diagnostics.Debug.WriteLine("Gemini generation failed: " + error); 1776 1777 // Show response in debug if available 1778 if (!string.IsNullOrEmpty(GeminiIntegration.LastResponse)) 1779 { 1780 System.Diagnostics.Debug.WriteLine("Last response: " + GeminiIntegration.LastResponse); 1781 } 1782 } 1783 btnGenerateDescription.Enabled = true; 1784 btnGenerateDescription.Text = "Сгенерировать описание (AI)"; 1785 }, 1786 // On error 1787 (ex) => 1788 { 1789 string errorMsg = ex.Message; 1790 if (!string.IsNullOrEmpty(GeminiIntegration.LastError)) 1791 errorMsg = GeminiIntegration.LastError; 1792 lblStatus.Text = "Ошибка: " + errorMsg; 1793 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1794 System.Diagnostics.Debug.WriteLine("Generate description error: " + ex); 1795 btnGenerateDescription.Enabled = true; 1796 btnGenerateDescription.Text = "Сгенерировать описание (AI)"; 1797 } 1798 ); 1799 } 1800 1801 /// <summary> 1802 /// Public method to trigger insert (called from EditorForm on Ctrl+Space) 1803 /// </summary> 1804 public void DoInsert() 1805 { 1806 BtnInsert_Click(null, null); 1807 } 1808 1809 private void BtnInsert_Click(object sender, EventArgs e) 1810 { 1811 if (cmbDocuments.SelectedItem == null || imageToInsert == null) 1812 { 1813 lblStatus.Text = "Выберите документ"; 1814 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1815 return; 1816 } 1817 1818 var doc = cmbDocuments.SelectedItem as WordDocumentInfo; 1819 if (doc == null) return; 1820 1821 WordIntegration.SelectedDocument = doc; 1822 1823 lblStatus.Text = "Вставка..."; 1824 lblStatus.ForeColor = DimTextColor; 1825 btnInsert.Enabled = false; 1826 Application.DoEvents(); 1827 1828 bool result = WordIntegration.InsertImageWithCaption( 1829 imageToInsert, 1830 txtDescription.Text.Trim(), 1831 txtCaption.Text.Trim()); 1832 1833 if (result) 1834 { 1835 lblStatus.Text = "Вставлено!"; 1836 lblStatus.ForeColor = SuccessColor; 1837 1838 if (InsertSuccess != null) 1839 InsertSuccess(this, EventArgs.Empty); 1840 1841 // Close the editor after successful insert 1842 parentEditor.Close(); 1843 } 1844 else 1845 { 1846 lblStatus.Text = "Ошибка вставки"; 1847 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1848 btnInsert.Enabled = true; 1849 } 1850 } 1851 1852 private void BtnOriginalize_Click(object sender, EventArgs e) 1853 { 1854 if (cmbDocuments.SelectedItem == null) 1855 { 1856 lblStatus.Text = "Выберите документ"; 1857 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1858 return; 1859 } 1860 1861 if (!GeminiIntegration.IsConfigured) 1862 { 1863 lblStatus.Text = "API ключ не настроен"; 1864 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1865 return; 1866 } 1867 1868 var doc = cmbDocuments.SelectedItem as WordDocumentInfo; 1869 if (doc == null) return; 1870 1871 WordIntegration.SelectedDocument = doc; 1872 1873 // Step 1: Extract descriptions and captions 1874 lblStatus.Text = "Сканирование документа..."; 1875 lblStatus.ForeColor = DimTextColor; 1876 btnOriginalize.Enabled = false; 1877 Application.DoEvents(); 1878 1879 var items = WordIntegration.ExtractDescriptionsAndCaptions(doc.DocumentObject); 1880 1881 if (items == null || items.Count == 0) 1882 { 1883 lblStatus.Text = "Описания не найдены"; 1884 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1885 btnOriginalize.Enabled = true; 1886 return; 1887 } 1888 1889 lblStatus.Text = "Найдено " + items.Count + " текстов. Перефразирование..."; 1890 lblStatus.ForeColor = DimTextColor; 1891 Application.DoEvents(); 1892 1893 // Step 2: Collect texts for rephrasing 1894 var textsToRephrase = new List<string>(); 1895 foreach (var item in items) 1896 textsToRephrase.Add(item.TextToRephrase); 1897 1898 // Step 3: Send to Gemini async 1899 GeminiIntegration.RephraseTextsAsync(textsToRephrase, 1900 // On success 1901 (rephrasedTexts) => 1902 { 1903 // Populate NewText in items 1904 for (int i = 0; i < items.Count && i < rephrasedTexts.Count; i++) 1905 items[i].NewText = rephrasedTexts[i]; 1906 1907 lblStatus.Text = "Перефразировано. Открытие превью..."; 1908 lblStatus.ForeColor = SuccessColor; 1909 btnOriginalize.Enabled = true; 1910 1911 // Step 4: Show preview dialog 1912 using (var dialog = new OriginalizePreviewDialog(items)) 1913 { 1914 if (dialog.ShowDialog(parentEditor) == DialogResult.OK) 1915 { 1916 // Step 5: Apply replacements 1917 lblStatus.Text = "Применение замен..."; 1918 lblStatus.ForeColor = DimTextColor; 1919 Application.DoEvents(); 1920 1921 int count = WordIntegration.ApplyReplacements(doc.DocumentObject, items); 1922 lblStatus.Text = "Заменено: " + count + " текстов"; 1923 lblStatus.ForeColor = SuccessColor; 1924 } 1925 else 1926 { 1927 lblStatus.Text = "Отменено"; 1928 lblStatus.ForeColor = DimTextColor; 1929 } 1930 } 1931 }, 1932 // On error 1933 (error) => 1934 { 1935 lblStatus.Text = "Ошибка: " + error; 1936 lblStatus.ForeColor = Color.FromArgb(200, 85, 85); 1937 btnOriginalize.Enabled = true; 1938 } 1939 ); 1940 } 1941 1942 protected override void Dispose(bool disposing) 1943 { 1944 if (disposing) 1945 { 1946 if (refreshTimer != null) 1947 { 1948 refreshTimer.Stop(); 1949 refreshTimer.Dispose(); 1950 } 1951 if (previewBox.Image != null) 1952 { 1953 previewBox.Image.Dispose(); 1954 } 1955 } 1956 base.Dispose(disposing); 1957 } 1958 } 1959}