|
|
|
@@ -139,12 +139,15 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
private readonly NumericUpDown _xInput = new() { DecimalPlaces = 2, Minimum = 0, Maximum = 100000m, Width = 80 }; |
|
|
|
private readonly NumericUpDown _yInput = new() { DecimalPlaces = 2, Minimum = 0, Maximum = 100000m, Width = 80 }; |
|
|
|
private readonly NumericUpDown _addressWidthInput = new() { DecimalPlaces = 2, Minimum = 1, Maximum = 100000m, Width = 80 }; |
|
|
|
// Gap between Address Control lines as a multiple of the previous line's font size |
|
|
|
// (AddressControlLayout.LineSpacingMultiplier, default 1.25); below 1.0 packs lines tighter. |
|
|
|
private readonly NumericUpDown _addressLineSpacingInput = new() { DecimalPlaces = 2, Increment = 0.05m, Minimum = 0.5m, Maximum = 5m, Width = 80 }; |
|
|
|
private readonly ListBox _addressLineList = new() { Width = 210, Height = 72, IntegralHeight = false }; |
|
|
|
private readonly Button _addAddressLineButton = new() { Text = "+", Width = 34, Height = 24 }; |
|
|
|
private readonly Button _removeAddressLineButton = new() { Text = "-", Width = 34, Height = 24 }; |
|
|
|
private readonly Button _moveAddressLineUpButton = new() { Text = "Up", Width = 48, Height = 24 }; |
|
|
|
private readonly Button _moveAddressLineDownButton = new() { Text = "Down", Width = 56, Height = 24 }; |
|
|
|
private readonly TextBox _fontFamilyInput = new() { Width = 160 }; |
|
|
|
private readonly Button _fontFamilyInput = new() { Width = 160, Height = 24, TextAlign = ContentAlignment.MiddleLeft }; |
|
|
|
private readonly NumericUpDown _fontSizeInput = new() { DecimalPlaces = 1, Minimum = 1, Maximum = 1000m, Width = 80 }; |
|
|
|
private readonly Button _colorButton = new() { Text = string.Empty, Width = 60, Height = 24, FlatStyle = FlatStyle.Popup }; |
|
|
|
private readonly NumericUpDown _zOrderInput = new() { DecimalPlaces = 0, Minimum = 0, Maximum = 100000m, Width = 80 }; |
|
|
|
@@ -196,7 +199,13 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
private readonly TextElementPropertiesEditor _propertiesEditor = new(); |
|
|
|
private readonly TemplateLayoutDocument _document; |
|
|
|
private readonly TemplateCanvasControl _canvas; |
|
|
|
private readonly TemplatePreviewControl _previewControl; |
|
|
|
private readonly CheckBox _showDataToggle = new() |
|
|
|
{ |
|
|
|
Text = "Show data", |
|
|
|
Appearance = Appearance.Button, |
|
|
|
AutoSize = true, |
|
|
|
Enabled = false, |
|
|
|
}; |
|
|
|
|
|
|
|
/// <summary>Guards against re-entrant control-event handling while this form is |
|
|
|
/// programmatically updating a control's value in response to another control's change |
|
|
|
@@ -214,7 +223,6 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
_document = document; |
|
|
|
_dimensionsEditor = new CanvasDimensionsEditor(document.Canvas); |
|
|
|
_canvas = new TemplateCanvasControl(document) { Dock = DockStyle.Fill, Margin = new Padding(12) }; |
|
|
|
_previewControl = new TemplatePreviewControl(document) { Dock = DockStyle.Fill, Margin = new Padding(12, 0, 12, 12) }; |
|
|
|
|
|
|
|
Text = "Template Designer"; |
|
|
|
// Sprint 4: the properties panel grew from 7 rows to 9 (added Angle and Collapse-if-blank |
|
|
|
@@ -266,11 +274,13 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
RefreshPropertiesPanel(); |
|
|
|
RefreshSelectionLabel(); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// Sprint 7 AC: the preview must auto-refresh after any layout edit. TemplatePreviewControl |
|
|
|
// re-resolves against the live, shared document on every repaint, so an Invalidate() is |
|
|
|
// all that's needed here — no separate "rebuild" step to keep in sync. |
|
|
|
_previewControl.Invalidate(); |
|
|
|
_showDataToggle.CheckedChanged += (_, _) => |
|
|
|
{ |
|
|
|
_canvas.ShowData = _showDataToggle.Checked; |
|
|
|
_showDataToggle.Text = _showDataToggle.Checked ? "Edit layout" : "Show data"; |
|
|
|
RefreshSelectionLabel(); |
|
|
|
}; |
|
|
|
|
|
|
|
// Post-Sprint-7-review fix (2026-10-19): auto-navigate on every change instead of |
|
|
|
@@ -286,7 +296,6 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
NavigateToRecord((int)_recordNumberInput.Value); |
|
|
|
} |
|
|
|
}; |
|
|
|
_previewControl.SetContext(Array.Empty<string>(), null); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>The current layout state, kept in sync with the dimensions controls and the |
|
|
|
@@ -411,7 +420,6 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
RefreshDimensionControls(); |
|
|
|
_canvas.ClearSelection(); |
|
|
|
_canvas.Invalidate(); |
|
|
|
_previewControl.Invalidate(); |
|
|
|
|
|
|
|
SetFileStatus( |
|
|
|
$"Opened '{dialog.FileName}' ({_document.Elements.Count} element(s), {_document.AddressControls.Count} address control(s)).", |
|
|
|
@@ -443,8 +451,40 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
return panel; |
|
|
|
} |
|
|
|
|
|
|
|
private string? _dragColumnName; |
|
|
|
private Point _dragStart; |
|
|
|
|
|
|
|
private Control BuildCsvPreviewGrid() |
|
|
|
{ |
|
|
|
// Drag a column header onto the canvas to place that field (or add it to an Address |
|
|
|
// Control): remember the header pressed, start the drag once the mouse moves far enough. |
|
|
|
_csvPreviewGrid.MouseDown += (_, e) => |
|
|
|
{ |
|
|
|
var hit = _csvPreviewGrid.HitTest(e.X, e.Y); |
|
|
|
_dragColumnName = e.Button == MouseButtons.Left && hit.Type == DataGridViewHitTestType.ColumnHeader |
|
|
|
? _csvPreviewGrid.Columns[hit.ColumnIndex].HeaderText |
|
|
|
: null; |
|
|
|
_dragStart = e.Location; |
|
|
|
}; |
|
|
|
_csvPreviewGrid.MouseMove += (_, e) => |
|
|
|
{ |
|
|
|
if (_dragColumnName is null || e.Button != MouseButtons.Left) |
|
|
|
{ |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
var drag = SystemInformation.DragSize; |
|
|
|
if (Math.Abs(e.X - _dragStart.X) < drag.Width && Math.Abs(e.Y - _dragStart.Y) < drag.Height) |
|
|
|
{ |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
var column = _dragColumnName; |
|
|
|
_dragColumnName = null; |
|
|
|
_csvPreviewGrid.DoDragDrop(new DataObject(TemplateCanvasControl.ColumnDragFormat, column), DragDropEffects.Copy); |
|
|
|
}; |
|
|
|
_csvPreviewGrid.MouseUp += (_, _) => _dragColumnName = null; |
|
|
|
|
|
|
|
var container = new Panel |
|
|
|
{ |
|
|
|
Dock = DockStyle.Fill, |
|
|
|
@@ -526,6 +566,7 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
} |
|
|
|
|
|
|
|
NavigateToRecord(1); |
|
|
|
_showDataToggle.Enabled = true; |
|
|
|
|
|
|
|
SetCsvStatus( |
|
|
|
$"Loaded '{path}' ({result.Headers.Count} column(s), {result.SampleRows.Count} sample row(s)).", |
|
|
|
@@ -591,82 +632,9 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
_csvStatusLabel.ForeColor = isError ? Color.Firebrick : Color.DarkGreen; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>Sprint 7: the canvas row's right side now holds two side-by-side areas — the new |
|
|
|
/// record-accurate preview panel and the existing properties panel — laid out via a |
|
|
|
/// <see cref="TableLayoutPanel"/> with two fixed-width columns rather than two independently |
|
|
|
/// Dock-ed controls, so their relative placement is deterministic by construction (row/column |
|
|
|
/// position) instead of depending on Controls-collection add order. This deliberately avoids |
|
|
|
/// the exact class of dock-order bug the Sprint 6 retrospective's live-verification action |
|
|
|
/// caught (a hidden properties panel from ambiguous Dock ordering).</summary> |
|
|
|
private Control BuildRightSidePanel() |
|
|
|
{ |
|
|
|
const int previewWidth = 340; |
|
|
|
const int propertiesWidth = 240; |
|
|
|
|
|
|
|
var panel = new TableLayoutPanel |
|
|
|
{ |
|
|
|
Dock = DockStyle.Right, |
|
|
|
Width = previewWidth + propertiesWidth, |
|
|
|
ColumnCount = 2, |
|
|
|
RowCount = 1, |
|
|
|
}; |
|
|
|
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, previewWidth)); |
|
|
|
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, propertiesWidth)); |
|
|
|
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); |
|
|
|
|
|
|
|
panel.Controls.Add(BuildPreviewArea(), 0, 0); |
|
|
|
panel.Controls.Add(BuildPropertiesPanel(), 1, 0); |
|
|
|
|
|
|
|
return panel; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>Sprint 7: the record-navigation toolbar (record number) stacked above the |
|
|
|
/// read-only preview surface, via row/column position rather than Dock ordering (see |
|
|
|
/// <see cref="BuildRightSidePanel"/>'s remarks). Post-Sprint-7-review fix (2026-10-19): the |
|
|
|
/// number input alone now drives navigation on every change (see the constructor's |
|
|
|
/// <c>_recordNumberInput.ValueChanged</c> wiring) — no separate "Go" button anymore.</summary> |
|
|
|
private Control BuildPreviewArea() |
|
|
|
{ |
|
|
|
var container = new TableLayoutPanel |
|
|
|
{ |
|
|
|
Dock = DockStyle.Fill, |
|
|
|
ColumnCount = 1, |
|
|
|
RowCount = 3, |
|
|
|
}; |
|
|
|
container.RowStyles.Add(new RowStyle(SizeType.AutoSize)); |
|
|
|
container.RowStyles.Add(new RowStyle(SizeType.AutoSize)); |
|
|
|
container.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); |
|
|
|
|
|
|
|
var toolbar = new FlowLayoutPanel |
|
|
|
{ |
|
|
|
AutoSize = true, |
|
|
|
Padding = new Padding(12, 12, 3, 3), |
|
|
|
FlowDirection = FlowDirection.LeftToRight, |
|
|
|
}; |
|
|
|
toolbar.Controls.Add(new Label |
|
|
|
{ |
|
|
|
Text = "Record #:", |
|
|
|
AutoSize = true, |
|
|
|
Anchor = AnchorStyles.Left, |
|
|
|
Margin = new Padding(3, 6, 3, 3), |
|
|
|
}); |
|
|
|
toolbar.Controls.Add(_recordNumberInput); |
|
|
|
_recordStatusLabel.Margin = new Padding(12, 6, 3, 3); |
|
|
|
toolbar.Controls.Add(_recordStatusLabel); |
|
|
|
|
|
|
|
var header = new Label |
|
|
|
{ |
|
|
|
Text = "Preview (resolved values, read-only):", |
|
|
|
AutoSize = true, |
|
|
|
Padding = new Padding(12, 0, 0, 4), |
|
|
|
}; |
|
|
|
|
|
|
|
container.Controls.Add(toolbar, 0, 0); |
|
|
|
container.Controls.Add(header, 0, 1); |
|
|
|
container.Controls.Add(_previewControl, 0, 2); |
|
|
|
|
|
|
|
return container; |
|
|
|
} |
|
|
|
/// <summary>The canvas row's right side holds the properties panel; record navigation and the |
|
|
|
/// "Show data" toggle live in the element toolbar (see <see cref="BuildElementToolbar"/>).</summary> |
|
|
|
private Control BuildRightSidePanel() => BuildPropertiesPanel(); |
|
|
|
|
|
|
|
/// <summary>Sprint 7, "Jump to a specific record number": fetches the requested 1-based |
|
|
|
/// record via <see cref="CsvRecordNavigator"/> (the full-file streaming reader, not the |
|
|
|
@@ -680,8 +648,8 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
{ |
|
|
|
if (string.IsNullOrEmpty(_currentCsvPath)) |
|
|
|
{ |
|
|
|
_previewControl.SetContext(Array.Empty<string>(), null); |
|
|
|
SetRecordStatus("No CSV loaded. Load a CSV to preview a record.", isError: false); |
|
|
|
_canvas.SetCsvPreviewContext(Array.Empty<string>(), null); |
|
|
|
SetRecordStatus("No CSV loaded. Load a CSV to view record data.", isError: false); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
@@ -692,7 +660,7 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
_previewControl.SetContext(_loadedCsvHeaders, result.Record); |
|
|
|
_canvas.SetCsvPreviewContext(_loadedCsvHeaders, result.Record); |
|
|
|
SetRecordStatus($"Showing record {recordNumber}.", isError: false); |
|
|
|
} |
|
|
|
|
|
|
|
@@ -717,8 +685,12 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
AddPropertyRow(1 * rowHeight, "X (pt):", _xInput, leftMargin, labelTop, inputTop); |
|
|
|
AddPropertyRow(2 * rowHeight, "Y (pt):", _yInput, leftMargin, labelTop, inputTop); |
|
|
|
AddPropertyRow(3 * rowHeight, "Address width (pt):", _addressWidthInput, leftMargin, labelTop, inputTop); |
|
|
|
var spacingLabel = new Label { Text = "Line spacing (x):", AutoSize = true, Location = new Point(leftMargin + 108, (3 * rowHeight) + labelTop) }; |
|
|
|
_addressLineSpacingInput.Location = new Point(leftMargin + 108, (3 * rowHeight) + inputTop); |
|
|
|
_propertiesPanel.Controls.Add(spacingLabel); |
|
|
|
_propertiesPanel.Controls.Add(_addressLineSpacingInput); |
|
|
|
AddAddressLineControls(4 * rowHeight, leftMargin, labelTop, inputTop); |
|
|
|
AddPropertyRow(7 * rowHeight, "Font family:", _fontFamilyInput, leftMargin, labelTop, inputTop); |
|
|
|
AddPropertyRow(7 * rowHeight, "Font (click to choose):", _fontFamilyInput, leftMargin, labelTop, inputTop); |
|
|
|
AddPropertyRow(8 * rowHeight, "Font size (pt):", _fontSizeInput, leftMargin, labelTop, inputTop); |
|
|
|
AddPropertyRow(9 * rowHeight, "Color:", _colorButton, leftMargin, labelTop, inputTop); |
|
|
|
// Sprint 3, Batch 4 ("Re-map an existing dynamic field"): only meaningful for the legacy |
|
|
|
@@ -760,7 +732,8 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
_xInput.ValueChanged += (_, _) => { if (!_suppressEvents) { SetSelectedX((double)_xInput.Value); } }; |
|
|
|
_yInput.ValueChanged += (_, _) => { if (!_suppressEvents) { SetSelectedY((double)_yInput.Value); } }; |
|
|
|
_addressWidthInput.ValueChanged += (_, _) => { if (!_suppressEvents && _canvas.SelectedAddressControl is not null) { _canvas.SelectedAddressControl.Width = (double)_addressWidthInput.Value; _canvas.NotifyElementChanged(); } }; |
|
|
|
_fontFamilyInput.TextChanged += (_, _) => { if (!_suppressEvents) { SetSelectedFontFamily(_fontFamilyInput.Text); } }; |
|
|
|
_addressLineSpacingInput.ValueChanged += (_, _) => { if (!_suppressEvents && _canvas.SelectedAddressControl is not null) { _canvas.SelectedAddressControl.LineSpacingMultiplier = (double)_addressLineSpacingInput.Value; _canvas.NotifyElementChanged(); } }; |
|
|
|
_fontFamilyInput.Click += (_, _) => OnFontButtonClick(); |
|
|
|
_fontSizeInput.ValueChanged += (_, _) => { if (!_suppressEvents) { SetSelectedFontSize((double)_fontSizeInput.Value); } }; |
|
|
|
_zOrderInput.ValueChanged += (_, _) => { if (!_suppressEvents) { SetSelectedZOrder((int)_zOrderInput.Value); } }; |
|
|
|
_angleInput.ValueChanged += (_, _) => { if (!_suppressEvents) { SetSelectedRotationAngle((double)_angleInput.Value); } }; |
|
|
|
@@ -985,6 +958,50 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
_canvas.NotifyElementChanged(); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>Opens the standard Windows font picker for the selected element/line, seeded with |
|
|
|
/// its current family and size, and applies both the chosen family and size.</summary> |
|
|
|
private void OnFontButtonClick() |
|
|
|
{ |
|
|
|
if (_propertiesEditor.Selected is null && _canvas.SelectedAddressLine is null) |
|
|
|
{ |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
var currentFamily = _fontFamilyInput.Text; |
|
|
|
var currentSize = (float)_fontSizeInput.Value; |
|
|
|
Font seed; |
|
|
|
try |
|
|
|
{ |
|
|
|
seed = new Font(string.IsNullOrWhiteSpace(currentFamily) ? "Arial" : currentFamily, currentSize, GraphicsUnit.Point); |
|
|
|
} |
|
|
|
catch (ArgumentException) |
|
|
|
{ |
|
|
|
seed = new Font("Arial", currentSize, GraphicsUnit.Point); |
|
|
|
} |
|
|
|
|
|
|
|
using (seed) |
|
|
|
{ |
|
|
|
using var dialog = new FontDialog { Font = seed, FontMustExist = true, ShowEffects = false }; |
|
|
|
if (dialog.ShowDialog(this) != DialogResult.OK) |
|
|
|
{ |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
SetSelectedFontFamily(dialog.Font.FontFamily.Name); |
|
|
|
SetSelectedFontSize(dialog.Font.SizeInPoints); |
|
|
|
_suppressEvents = true; |
|
|
|
try |
|
|
|
{ |
|
|
|
_fontFamilyInput.Text = dialog.Font.FontFamily.Name; |
|
|
|
_fontSizeInput.Value = ClampToNumericRange(_fontSizeInput, (decimal)dialog.Font.SizeInPoints); |
|
|
|
} |
|
|
|
finally |
|
|
|
{ |
|
|
|
_suppressEvents = false; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void OnColorButtonClick() |
|
|
|
{ |
|
|
|
if (_propertiesEditor.Selected is null && _canvas.SelectedAddressLine is null) |
|
|
|
@@ -1089,6 +1106,9 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
_addressWidthInput.Value = ClampToNumericRange( |
|
|
|
_addressWidthInput, (decimal)(selectedAddressControl?.Width ?? AddressControlLayout.DefaultWidth)); |
|
|
|
_addressWidthInput.Enabled = selectedAddressControl is not null; |
|
|
|
_addressLineSpacingInput.Value = ClampToNumericRange( |
|
|
|
_addressLineSpacingInput, (decimal)(selectedAddressControl?.LineSpacingMultiplier ?? AddressControlLayout.DefaultLineSpacingMultiplier)); |
|
|
|
_addressLineSpacingInput.Enabled = selectedAddressControl is not null; |
|
|
|
RefreshAddressLineList(selectedAddressControl); |
|
|
|
|
|
|
|
_fontFamilyInput.Text = selected?.FontFamily ?? selectedAddressLine?.FontFamily ?? string.Empty; |
|
|
|
@@ -1230,6 +1250,21 @@ public sealed class TemplateDesignerForm : Form |
|
|
|
_selectionLabel.Margin = new Padding(18, 6, 3, 3); |
|
|
|
panel.Controls.Add(_selectionLabel); |
|
|
|
|
|
|
|
// "Show data": swaps the canvas from the {Column} token view to the resolved values for |
|
|
|
// the record chosen here (replaces the old separate Preview panel). |
|
|
|
_showDataToggle.Margin = new Padding(18, 3, 3, 3); |
|
|
|
panel.Controls.Add(_showDataToggle); |
|
|
|
panel.Controls.Add(new Label |
|
|
|
{ |
|
|
|
Text = "Record #:", |
|
|
|
AutoSize = true, |
|
|
|
Anchor = AnchorStyles.Left, |
|
|
|
Margin = new Padding(6, 6, 3, 3), |
|
|
|
}); |
|
|
|
panel.Controls.Add(_recordNumberInput); |
|
|
|
_recordStatusLabel.Margin = new Padding(6, 6, 3, 3); |
|
|
|
panel.Controls.Add(_recordStatusLabel); |
|
|
|
|
|
|
|
// Sprint 10, "Align and distribute multiple elements": a new button group, enabled only |
|
|
|
// while 2+ items are multi-selected (RefreshAlignmentButtonsEnabled, called from every |
|
|
|
// place selection state already gets refreshed). Appended to this same toolbar rather than |
|
|
|
|