Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

224 linhas
8.4KB

  1. using System.Drawing;
  2. using System.Windows.Forms;
  3. using EnvelopeRenderer.Desktop.Core.Launch;
  4. using EnvelopeRenderer.Desktop.Views;
  5. namespace EnvelopeRenderer.Desktop;
  6. /// <summary>
  7. /// Operator-facing shell: pick a template, CSV, and output PDF path, then launch a render and
  8. /// watch it through to completion. Sprint 1, Batch 4 ("Launch a text-only render from the
  9. /// desktop app") proved the process could be launched without freezing the UI; Batch 5 ("Show
  10. /// render progress and completion summary") adds reading the CLI's `PROGRESS` stream while it
  11. /// runs, showing a non-technical completion summary once it exits, and — since the button is now
  12. /// only re-enabled after the process actually exits rather than as soon as it starts — closes the
  13. /// double-launch gap previously logged in logs/technical_debt_log.md.
  14. ///
  15. /// All non-UI logic (input validation, argument construction, CLI discovery, process launch,
  16. /// progress parsing, and summary formatting) lives in EnvelopeRenderer.Desktop.Core.Launch and is
  17. /// unit tested there — this class is intentionally thin event-handler wiring over that logic.
  18. /// </summary>
  19. public sealed class MainForm : Form
  20. {
  21. private readonly TextBox _templatePathTextBox = new();
  22. private readonly TextBox _csvPathTextBox = new();
  23. private readonly TextBox _outputPathTextBox = new();
  24. private readonly Button _renderButton = new() { Text = "&Render", AutoSize = true };
  25. private readonly Button _designTemplateButton = new() { Text = "&Design Template...", AutoSize = true };
  26. private readonly Label _statusLabel = new()
  27. {
  28. AutoSize = false,
  29. Height = 48,
  30. TextAlign = ContentAlignment.TopLeft,
  31. };
  32. private readonly CliProcessLauncher _launcher;
  33. public MainForm() : this(new CliProcessLauncher())
  34. {
  35. }
  36. /// <summary>Internal constructor allowing a launcher to be injected (used by tests/composition).</summary>
  37. internal MainForm(CliProcessLauncher launcher)
  38. {
  39. _launcher = launcher;
  40. Text = "Envelope Renderer";
  41. MinimumSize = new Size(640, 260);
  42. StartPosition = FormStartPosition.CenterScreen;
  43. Controls.Add(BuildLayout());
  44. }
  45. private Control BuildLayout()
  46. {
  47. var layout = new TableLayoutPanel
  48. {
  49. Dock = DockStyle.Fill,
  50. ColumnCount = 3,
  51. RowCount = 5,
  52. Padding = new Padding(12),
  53. AutoSize = true,
  54. };
  55. layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
  56. layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
  57. layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
  58. AddPickerRow(
  59. layout,
  60. row: 0,
  61. labelText: "Template (XML):",
  62. textBox: _templatePathTextBox,
  63. browse: () => BrowseForOpenFile(_templatePathTextBox, "XML template files (*.xml)|*.xml|All files (*.*)|*.*"));
  64. AddPickerRow(
  65. layout,
  66. row: 1,
  67. labelText: "CSV data:",
  68. textBox: _csvPathTextBox,
  69. browse: () => BrowseForOpenFile(_csvPathTextBox, "CSV files (*.csv)|*.csv|All files (*.*)|*.*"));
  70. AddPickerRow(
  71. layout,
  72. row: 2,
  73. labelText: "Output PDF:",
  74. textBox: _outputPathTextBox,
  75. browse: () => BrowseForSaveFile(_outputPathTextBox, "PDF files (*.pdf)|*.pdf|All files (*.*)|*.*"));
  76. _renderButton.Click += OnRenderClick;
  77. _designTemplateButton.Click += (_, _) => OnDesignTemplateClick();
  78. var buttonPanel = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft, AutoSize = true };
  79. buttonPanel.Controls.Add(_renderButton);
  80. buttonPanel.Controls.Add(_designTemplateButton);
  81. layout.Controls.Add(buttonPanel, 1, 3);
  82. layout.SetColumnSpan(buttonPanel, 2);
  83. _statusLabel.Dock = DockStyle.Fill;
  84. layout.Controls.Add(_statusLabel, 0, 4);
  85. layout.SetColumnSpan(_statusLabel, 3);
  86. return layout;
  87. }
  88. private static void AddPickerRow(TableLayoutPanel layout, int row, string labelText, TextBox textBox, Action browse)
  89. {
  90. var label = new Label
  91. {
  92. Text = labelText,
  93. AutoSize = true,
  94. Anchor = AnchorStyles.Left,
  95. Margin = new Padding(3, 9, 3, 3),
  96. };
  97. textBox.Dock = DockStyle.Fill;
  98. textBox.Margin = new Padding(3, 6, 3, 3);
  99. var browseButton = new Button { Text = "Browse...", AutoSize = true, Margin = new Padding(3, 3, 3, 3) };
  100. browseButton.Click += (_, _) => browse();
  101. layout.Controls.Add(label, 0, row);
  102. layout.Controls.Add(textBox, 1, row);
  103. layout.Controls.Add(browseButton, 2, row);
  104. }
  105. private static void BrowseForOpenFile(TextBox target, string filter)
  106. {
  107. using var dialog = new OpenFileDialog { Filter = filter, CheckFileExists = true, Title = "Select a file" };
  108. if (dialog.ShowDialog() == DialogResult.OK)
  109. {
  110. target.Text = dialog.FileName;
  111. }
  112. }
  113. private static void BrowseForSaveFile(TextBox target, string filter)
  114. {
  115. using var dialog = new SaveFileDialog { Filter = filter, OverwritePrompt = false, Title = "Choose where to save the PDF" };
  116. if (dialog.ShowDialog() == DialogResult.OK)
  117. {
  118. target.Text = dialog.FileName;
  119. }
  120. }
  121. private async void OnRenderClick(object? sender, EventArgs e)
  122. {
  123. var inputs = new RenderLaunchInputs(_templatePathTextBox.Text, _csvPathTextBox.Text, _outputPathTextBox.Text);
  124. var validation = RenderLaunchValidator.Validate(inputs);
  125. if (!validation.IsValid)
  126. {
  127. SetStatus(string.Join(" ", validation.Errors), isError: true);
  128. return;
  129. }
  130. var cliPath = CliExecutablePathResolver.Resolve(AppContext.BaseDirectory);
  131. if (!cliPath.IsFound)
  132. {
  133. SetStatus(cliPath.Error!, isError: true);
  134. ShowLaunchFailure(cliPath.Error!);
  135. return;
  136. }
  137. // Disabled here and only re-enabled once RunAsync's awaited Task completes below (i.e.
  138. // once the child process has actually exited) — not the moment it starts — so an
  139. // operator can't launch a second render against the same output while one is in flight.
  140. _renderButton.Enabled = false;
  141. SetStatus("Starting render...", isError: false);
  142. try
  143. {
  144. var arguments = CliArgumentListBuilder.Build(inputs);
  145. // Constructed on the UI thread so Progress<T> captures this thread's
  146. // SynchronizationContext and marshals each Report() call back to it automatically —
  147. // OnRenderProgress can update controls directly without a manual Invoke.
  148. var progress = new Progress<ProgressEvent>(OnRenderProgress);
  149. var result = await _launcher.RunAsync(cliPath.Path!, arguments, progress);
  150. var summary = RenderCompletionSummaryFormatter.Format(result);
  151. if (result.Succeeded)
  152. {
  153. SetStatus(summary, isError: false);
  154. }
  155. else if (!result.Started)
  156. {
  157. // A launch failure (the process never started) keeps the message-box treatment
  158. // from Batch 4 — it means something is wrong with the desktop app's own setup,
  159. // not with the operator's data, and deserves an explicit acknowledgement.
  160. SetStatus(summary, isError: true);
  161. ShowLaunchFailure(summary);
  162. }
  163. else
  164. {
  165. SetStatus(summary, isError: true);
  166. }
  167. }
  168. finally
  169. {
  170. _renderButton.Enabled = true;
  171. }
  172. }
  173. private void OnDesignTemplateClick()
  174. {
  175. using var designer = new TemplateDesignerForm();
  176. designer.ShowDialog(this);
  177. }
  178. private void OnRenderProgress(ProgressEvent progressEvent)
  179. {
  180. SetStatus(RenderProgressStatusFormatter.Format(progressEvent), isError: progressEvent.Kind == ProgressEventKind.Failure);
  181. }
  182. private void ShowLaunchFailure(string message)
  183. {
  184. MessageBox.Show(this, message, "Could not start render", MessageBoxButtons.OK, MessageBoxIcon.Error);
  185. }
  186. private void SetStatus(string message, bool isError)
  187. {
  188. _statusLabel.Text = message;
  189. _statusLabel.ForeColor = isError ? Color.Firebrick : Color.DarkGreen;
  190. }
  191. }

Powered by TurnKey Linux.