Editable properties
Editable properties
Expose per-tool settings — a fill colour, a flag, a number — with one attribute, and the generic properties dialog renders an editor for it automatically.
What it is
Beyond the inherited Color, LineWidthPx and LineStyle (already
annotated on the base class), you surface any extra setting by tagging a public, read/write property with
[DrawingToolProperty]. There is no registration step: the discovery helper finds the attribute
reflectively and the properties dialog renders an editor row for it.
It is opt-in: infrastructure properties (IsPlaced, PanelId,
Anchors, …) stay hidden, and a tool controls exactly which knobs it exposes. The
attribute lives in TradeStrike.Pipeline.Drawing.
DrawingToolPropertyAttribute.cs[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class DrawingToolPropertyAttribute : Attribute
{
public string? DisplayName { get; set; } // defaults to the property name
public string? Description { get; set; } // one-line tooltip
public string Category { get; set; } = "General";
public int Order { get; set; } // lower renders first
public object? MinValue { get; set; } // advisory, for numeric editors
public object? MaxValue { get; set; } // advisory
public object? Step { get; set; } // spinner increment
}
MinValue / MaxValue /
Step guide the dialog's numeric editor, but programmatic writes are not silently
clamped — enforce hard invariants in your setter (as the base does for LineWidthPx,
which throws on a non-positive value).Adding a property
Give each property a real default so a freshly placed tool looks right before the user touches anything.
The example below adds an interior fill colour to a rectangle. The default fill uses an alpha of
0x33 so it is translucent; an alpha of 0 means "no fill", which the render code
checks before painting.
Rectangle.csusing System;
using TradeStrike.Pipeline.Drawing;
using TradeStrike.Pipeline.Plots;
[DrawingToolMetadata("Rectangle", Category = "Shapes", AnchorCount = 2)]
public sealed class Rectangle : DrawingToolBase
{
public Rectangle() : base(anchorCount: 2) { }
[DrawingToolProperty(DisplayName = "Fill Color", Category = "Style", Order = 50,
Description = "Interior fill. Alpha 0 = no fill.")]
public ChartColor FillColor { get; set; } = new(0x1E, 0x90, 0xFF, 0x33);
public override void OnRender(IDrawingToolRenderContext ctx)
{
var a = ctx.AnchorToPixel(Anchors[0]);
var b = ctx.AnchorToPixel(Anchors[1]);
double x = Math.Min(a.X, b.X), y = Math.Min(a.Y, b.Y);
double w = Math.Abs(b.X - a.X), h = Math.Abs(b.Y - a.Y);
if (FillColor.A > 0) ctx.Renderer.FillRect(x, y, w, h, FillColor); // fill first
ctx.Renderer.DrawLine(x, y, x + w, y, Color, LineWidthPx); // then outline
ctx.Renderer.DrawLine(x + w, y, x + w, y + h, Color, LineWidthPx);
ctx.Renderer.DrawLine(x + w, y + h, x, y + h, Color, LineWidthPx);
ctx.Renderer.DrawLine(x, y + h, x, y, Color, LineWidthPx);
if (ctx.IsSelected) { ctx.DrawHandle(a.X, a.Y); ctx.DrawHandle(b.X, b.Y); }
}
public override bool HitTest(double px, double py, IDrawingToolRenderContext ctx, double tol = 6)
{
var a = ctx.AnchorToPixel(Anchors[0]);
var b = ctx.AnchorToPixel(Anchors[1]);
return DrawingGeometry.DistanceToRectangleOutline(px, py, a.X, a.Y, b.X, b.Y) <= tol;
}
}
DisplayName, Category
(groups rows under a heading), Order (sequence within the group) and Description
(tooltip) work the same way they do on [DrawingToolMetadata], so the dialog stays consistent
across tools.How discovery works
The host enumerates editors via DrawingToolPropertyDiscovery.Discover(tool), which reflects
over the tool type for [DrawingToolProperty] attributes and returns
DrawingToolPropertyDescriptor entries sorted by (Category, Order, Name). Results
are cached per type, so the per-dialog-open cost is a dictionary lookup. Each descriptor exposes the editor
metadata plus Read(tool) / Write(tool, value) so the dialog and any headless code
path share one value-coercion contract.
Because [DrawingToolProperty] is declared Inherited = true, the annotations on
DrawingToolBase.Color / LineWidthPx / LineStyle flow to every
subclass automatically — built-ins and plugin tools alike surface those three editors with zero
boilerplate. Discovery skips indexers and any property whose getter or setter is not public, so a tool can
opt a property out simply by making its setter non-public.
DrawingToolPropertyDescriptor.cspublic sealed class DrawingToolPropertyDescriptor
{
public string Name { get; } // CLR property name (stable across DisplayName renames)
public string DisplayName { get; }
public string Description { get; }
public string Category { get; }
public int Order { get; }
public Type PropertyType { get; } // double, LineStyle, ChartColor, ...
public object? MinValue { get; }
public object? MaxValue { get; }
public object? Step { get; }
public object? Read(IDrawingTool tool);
public void Write(IDrawingTool tool, object? value);
}