diff --git a/code/src/EnvelopeRenderer.Desktop/Views/TemplateCanvasControl.cs b/code/src/EnvelopeRenderer.Desktop/Views/TemplateCanvasControl.cs
index a258e8b..1737d27 100644
--- a/code/src/EnvelopeRenderer.Desktop/Views/TemplateCanvasControl.cs
+++ b/code/src/EnvelopeRenderer.Desktop/Views/TemplateCanvasControl.cs
@@ -66,6 +66,7 @@ public sealed class TemplateCanvasControl : Control
_editor = new CanvasElementEditor(document, MeasureElement);
DoubleBuffered = true;
+ AllowDrop = true;
BackColor = SystemColors.ControlDark;
SetStyle(ControlStyles.ResizeRedraw, true);
// Post-Sprint-9: a plain Control (unlike UserControl) is not selectable/focusable by
@@ -227,6 +228,56 @@ public sealed class TemplateCanvasControl : Control
}
}
+ /// Drag-and-drop data format carrying a CSV column header name from the form's CSV
+ /// grid onto this canvas.
+ public const string ColumnDragFormat = "EnvelopeRenderer.CsvColumn";
+
+ /// Drops a CSV column onto the canvas at a client-space pixel position: onto an
+ /// Address Control it appends a new line bound to the column; anywhere else it places a new
+ /// dynamic placeholder there. Ignored in show-data (read-only) mode.
+ public bool DropColumn(string columnName, Point clientPoint)
+ {
+ if (_showData || string.IsNullOrEmpty(columnName))
+ {
+ return false;
+ }
+
+ var (x, y) = CurrentTransform().ToPoints(clientPoint.X, clientPoint.Y);
+ var (control, _) = HitTestAddressControl(x, y);
+ if (control is not null)
+ {
+ control.Lines.Add(AddressControlLineLayout.CreateField(columnName));
+ SelectAddressControl(control, control.Lines.Count - 1);
+ Invalidate();
+ SelectionChanged?.Invoke(this, EventArgs.Empty);
+ ElementsChanged?.Invoke(this, EventArgs.Empty);
+ return true;
+ }
+
+ _editor.AddDynamicPlaceholder(x, y, columnName);
+ Invalidate();
+ SelectionChanged?.Invoke(this, EventArgs.Empty);
+ ElementsChanged?.Invoke(this, EventArgs.Empty);
+ return true;
+ }
+
+ protected override void OnDragOver(DragEventArgs drgevent)
+ {
+ base.OnDragOver(drgevent);
+ drgevent.Effect = !_showData && drgevent.Data?.GetDataPresent(ColumnDragFormat) == true
+ ? DragDropEffects.Copy
+ : DragDropEffects.None;
+ }
+
+ protected override void OnDragDrop(DragEventArgs drgevent)
+ {
+ base.OnDragDrop(drgevent);
+ if (drgevent.Data?.GetData(ColumnDragFormat) is string column)
+ {
+ DropColumn(column, PointToClient(new Point(drgevent.X, drgevent.Y)));
+ }
+ }
+
public TextElementLayout AddDynamicPlaceholderElement(string columnName = "Column")
{
var (x, y) = DefaultNewElementPosition();
@@ -248,6 +299,37 @@ public sealed class TemplateCanvasControl : Control
Invalidate();
}
+ private bool _showData;
+
+ /// "Show data" view mode: the canvas draws each element's text resolved against the
+ /// current record (the one supplied via ) instead of the
+ /// literal {Column} tokens with their blue placeholder fills. Read-only while on — hit
+ /// testing and measuring work from the token text, so editing gestures are ignored until the
+ /// operator switches back to design view.
+ [System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)]
+ public bool ShowData
+ {
+ get => _showData;
+ set
+ {
+ if (_showData == value)
+ {
+ return;
+ }
+
+ _showData = value;
+ Invalidate();
+ }
+ }
+
+ /// The text an element is drawn with: its resolved value for the current record in
+ /// show-data mode (falling back to the token text if it cannot be resolved), otherwise the
+ /// token text.
+ private string DisplayTextFor(TextElementLayout element) =>
+ _showData
+ ? TextResolver.TryResolve(element.Runs, _csvHeaders, _csvSampleRecord) ?? element.DisplayText
+ : element.DisplayText;
+
/// Re-selects the given element (e.g. after the properties panel changes it) and
/// redraws — used so external edits stay visually in sync with the canvas.
public void NotifyElementChanged()
@@ -776,7 +858,13 @@ public sealed class TemplateCanvasControl : Control
// the pixel position to draw the string's top-left corner at is the transform of
// (X, effectiveY + height). `effectiveY` is the same as `element.Y` unless Sprint 4's
// address-line collapsing has shifted it (see AddressBlockPreviewCalculator).
- var (drawX, drawY) = transform.ToPixels(element.X, effectiveY + height);
+ var (drawX, boxTopY) = transform.ToPixels(element.X, effectiveY + height);
+ using var drawFont = ResolveDrawFont(element.FontFamily, (float)element.FontSize, transform.Scale);
+ // Plain (non-box) text is positioned by baseline like the PDF; a boxed element's text
+ // hangs from the top of its box (PDF DrawTextBox).
+ var drawY = element.HasBox
+ ? boxTopY
+ : transform.ToPixels(element.X, effectiveY).Y - BaselineOffsetPx(drawFont);
GraphicsState? savedState = null;
if (element.RotationAngle != 0)
@@ -813,7 +901,7 @@ public sealed class TemplateCanvasControl : Control
// approximation gap for this story; a per-run-aware wrapped highlight is polish-
// tier scope for a later story, not required here.
var boxRect = new RectangleF((float)drawX, (float)drawY, (float)(width * transform.Scale), (float)boxHeight);
- g.DrawString(element.DisplayText, font, brush, boxRect);
+ g.DrawString(DisplayTextFor(element), drawFont, brush, boxRect);
// Sprint 10, "Add a live wrap/clip indicator for text elements": a dashed-orange
// outline, the same visual language as the unmapped-column warning below, drawn
@@ -839,7 +927,7 @@ public sealed class TemplateCanvasControl : Control
// (see AddressBlockPreviewCalculator's per-run ElementPreviewState.UnmappedRuns)
// rather than for the whole element, so a mixed element with one bad token among
// several good ones only flags that one segment.
- var segments = MeasureRunSegments(element, font);
+ var segments = _showData ? new List<(TextRun Run, double Width)>() : MeasureRunSegments(element, font);
var cumulativeWidth = 0.0;
for (var i = 0; i < segments.Count; i++)
{
@@ -859,7 +947,7 @@ public sealed class TemplateCanvasControl : Control
cumulativeWidth += segmentWidth;
}
- g.DrawString(element.DisplayText, font, brush, (float)drawX, (float)drawY);
+ g.DrawString(DisplayTextFor(element), drawFont, brush, (float)drawX, (float)drawY);
}
if (state.IsUnmappedColumn)
@@ -1027,9 +1115,11 @@ public sealed class TemplateCanvasControl : Control
using var font = ResolveFont(line.FontFamily, (float)line.FontSize);
var text = ResolveAddressLinePreviewText(line) ?? line.DisplayText;
var size = MeasureText(text, font);
- var (drawX, drawY) = transform.ToPixels(control.X, lineStates[i].EffectiveY + size.Height);
+ using var drawFont = ResolveDrawFont(line.FontFamily, (float)line.FontSize, transform.Scale);
+ var drawX = transform.ToPixels(control.X, lineStates[i].EffectiveY).X;
+ var drawY = transform.ToPixels(control.X, lineStates[i].EffectiveY).Y - BaselineOffsetPx(drawFont);
- if (line.IsDynamic)
+ if (line.IsDynamic && !_showData)
{
using var fill = new SolidBrush(System.Drawing.Color.FromArgb(45, System.Drawing.Color.DodgerBlue));
g.FillRectangle(
@@ -1041,7 +1131,7 @@ public sealed class TemplateCanvasControl : Control
}
using var brush = new SolidBrush(System.Drawing.Color.FromArgb(line.Color.R, line.Color.G, line.Color.B));
- g.DrawString(text, font, brush, (float)drawX, (float)drawY);
+ g.DrawString(text, drawFont, brush, (float)drawX, (float)drawY);
if (isPrimarySelected && i == _selectedAddressLineIndex)
{
@@ -1123,7 +1213,7 @@ public sealed class TemplateCanvasControl : Control
/// and the new preview panel) instead of keeping
/// its own identical copy of this per-run resolution rule.
private string? ResolveAddressLinePreviewText(AddressControlLineLayout line) =>
- TextResolver.TryResolve(line.Runs, _csvHeaders, _csvSampleRecord);
+ _showData ? TextResolver.TryResolve(line.Runs, _csvHeaders, _csvSampleRecord) : null;
/// Sprint 6 defect fix (record-accurate render/preview only, not this canvas — see
/// below): static text rotates around its measured center, while dynamic/mixed content rotates
@@ -1248,6 +1338,33 @@ public sealed class TemplateCanvasControl : Control
/// so a missing font only affects the design-time preview's appearance — it does not crash
/// the designer. This is independent of, and does not relax, the render-time rule that a
/// missing font is a blocking error (`TEMPLATE_FORMAT.md`'s "Known gaps").
+ /// The font used to actually paint text: sized in pixels (point size times the
+ /// canvas zoom) so glyphs scale with the page exactly like positions and boxes do. A
+ /// points-unit font would be converted at the screen DPI regardless of zoom, so text would
+ /// not track the PDF's size. Measurement helpers keep using (points).
+ private static Font ResolveDrawFont(string familyName, float sizePoints, double scale)
+ {
+ var px = (float)(Math.Max(sizePoints <= 0 ? 12f : sizePoints, 0.1f) * scale);
+ try
+ {
+ return new Font(familyName, Math.Max(px, 1f), FontStyle.Regular, GraphicsUnit.Pixel);
+ }
+ catch (ArgumentException)
+ {
+ return new Font(FontFamily.GenericSansSerif, Math.Max(px, 1f), FontStyle.Regular, GraphicsUnit.Pixel);
+ }
+ }
+
+ /// Distance in pixels from the top of a GDI+ text box to the baseline for a
+ /// pixel-unit font. The PDF places text by baseline (the element's Y), so the canvas draws the
+ /// box this far above the baseline.
+ private static float BaselineOffsetPx(Font pixelFont)
+ {
+ var family = pixelFont.FontFamily;
+ var style = pixelFont.Style;
+ return pixelFont.Size * family.GetCellAscent(style) / family.GetEmHeight(style);
+ }
+
private static Font ResolveFont(string familyName, float size)
{
try
@@ -1266,7 +1383,7 @@ public sealed class TemplateCanvasControl : Control
// Post-Sprint-9: needed so this control actually receives OnKeyDown (Delete key) at all —
// see the constructor's ControlStyles.Selectable remarks.
Focus();
- if (e.Button != MouseButtons.Left)
+ if (e.Button != MouseButtons.Left || _showData)
{
return;
}
@@ -1618,7 +1735,7 @@ public sealed class TemplateCanvasControl : Control
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
- if ((e.KeyCode is Keys.Delete or Keys.Back) && !IsInteracting)
+ if ((e.KeyCode is Keys.Delete or Keys.Back) && !IsInteracting && !_showData)
{
RemoveSelectedElement();
e.Handled = true;
diff --git a/code/src/EnvelopeRenderer.Desktop/Views/TemplateDesignerForm.cs b/code/src/EnvelopeRenderer.Desktop/Views/TemplateDesignerForm.cs
index b8efbde..bcf9e43 100644
--- a/code/src/EnvelopeRenderer.Desktop/Views/TemplateDesignerForm.cs
+++ b/code/src/EnvelopeRenderer.Desktop/Views/TemplateDesignerForm.cs
@@ -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,
+ };
/// 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(), null);
}
/// 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;
}
- /// 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
- /// 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).
- 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;
- }
-
- /// Sprint 7: the record-navigation toolbar (record number) stacked above the
- /// read-only preview surface, via row/column position rather than Dock ordering (see
- /// 's remarks). Post-Sprint-7-review fix (2026-10-19): the
- /// number input alone now drives navigation on every change (see the constructor's
- /// _recordNumberInput.ValueChanged wiring) — no separate "Go" button anymore.
- 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;
- }
+ /// The canvas row's right side holds the properties panel; record navigation and the
+ /// "Show data" toggle live in the element toolbar (see ).
+ private Control BuildRightSidePanel() => BuildPropertiesPanel();
/// Sprint 7, "Jump to a specific record number": fetches the requested 1-based
/// record via (the full-file streaming reader, not the
@@ -680,8 +648,8 @@ public sealed class TemplateDesignerForm : Form
{
if (string.IsNullOrEmpty(_currentCsvPath))
{
- _previewControl.SetContext(Array.Empty(), null);
- SetRecordStatus("No CSV loaded. Load a CSV to preview a record.", isError: false);
+ _canvas.SetCsvPreviewContext(Array.Empty(), 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();
}
+ /// 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.
+ 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