Procházet zdrojové kódy

Designer: Show data toggle, column drag-drop, line spacing, font picker, canvas text fidelity

- Replace the side Preview panel with a Show data toggle and record picker on
  the element toolbar; the canvas shows resolved values (read-only) or the
  {Column} tokens for editing, including Address Control lines.
- Drag a CSV column header onto the canvas to place a field, or onto an
  Address Control to add a bound line.
- Add a Line spacing field for Address Controls.
- Font is chosen through the standard Windows font dialog (family and size).
- Canvas text now scales with zoom and sits on the element's baseline, so
  size and position match the PDF.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
master
Daniel Covington před 4 dny
rodič
revize
d59ab47bc2
2 změnil soubory, kde provedl 252 přidání a 100 odebrání
  1. +127
    -10
      code/src/EnvelopeRenderer.Desktop/Views/TemplateCanvasControl.cs
  2. +125
    -90
      code/src/EnvelopeRenderer.Desktop/Views/TemplateDesignerForm.cs

+ 127
- 10
code/src/EnvelopeRenderer.Desktop/Views/TemplateCanvasControl.cs Zobrazit soubor

@@ -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
}
}

/// <summary>Drag-and-drop data format carrying a CSV column header name from the form's CSV
/// grid onto this canvas.</summary>
public const string ColumnDragFormat = "EnvelopeRenderer.CsvColumn";

/// <summary>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.</summary>
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;

/// <summary>"Show data" view mode: the canvas draws each element's text resolved against the
/// current record (the one supplied via <see cref="SetCsvPreviewContext"/>) instead of the
/// literal <c>{Column}</c> 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.</summary>
[System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)]
public bool ShowData
{
get => _showData;
set
{
if (_showData == value)
{
return;
}

_showData = value;
Invalidate();
}
}

/// <summary>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.</summary>
private string DisplayTextFor(TextElementLayout element) =>
_showData
? TextResolver.TryResolve(element.Runs, _csvHeaders, _csvSampleRecord) ?? element.DisplayText
: element.DisplayText;

/// <summary>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.</summary>
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
/// <see cref="AddressBlockPreviewCalculator"/> and the new preview panel) instead of keeping
/// its own identical copy of this per-run resolution rule.</summary>
private string? ResolveAddressLinePreviewText(AddressControlLineLayout line) =>
TextResolver.TryResolve(line.Runs, _csvHeaders, _csvSampleRecord);
_showData ? TextResolver.TryResolve(line.Runs, _csvHeaders, _csvSampleRecord) : null;

/// <summary>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").</summary>
/// <summary>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 <see cref="ResolveFont"/> (points).</summary>
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);
}
}

/// <summary>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.</summary>
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;


+ 125
- 90
code/src/EnvelopeRenderer.Desktop/Views/TemplateDesignerForm.cs Zobrazit soubor

@@ -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


Načítá se…
Zrušit
Uložit

Powered by TurnKey Linux.