TASK 59: the properties panel is a hierarchy, and the shape is a choice
The creator's ruling on the webcam's properties: a "Shape" title above the
Mirror/Rect row; the drag hint indented under that row "to show parentage";
the same for Geometry, Border Color, Border Thickness; Remove and Blur indented
under Background; Strength indented under Blur.
One rule covers it: a title owns its content and the content is indented one
level (10px) under it - two levels where a title is itself nested (Strength
under Blur under Background). The titles are SemiBold 11px #a0a0b0 like the
ones already there.
The shape also stops being a toggle button that shows the NEXT action ("Round"
when the camera was a rectangle) and becomes a CHOICE: two segment options,
Rect | Circle, with Rect selected by default - which is what the creator asked
for ("Rect should be selected as a default if none exists"). The old
ShapeButtonText is gone.
The mutual exclusion lives in the MODEL: IsRectShape/IsRoundShape on
SceneElement, each setter flipping the shape through ToggleClipShape() (so the
pre-round rect is still stored/restored) and always re-raising, so a refused
write - the vertical Chat layout is rect-only (TASK 57) - makes the UI snap
back instead of sticking. Deliberately NOT a WPF RadioButton group: a group
unchecks its siblings by setting IsChecked in code, which makes WPF drop the
binding (stackoverflow 7376373; the same trap measured in 50980687/57275046).
The Circle option stays hidden where CanToggleClipShape is false, so the circle
is still not offered in the vertical Chat layout.
Guard: ytLive.Tests/PropertiesHierarchyTests.cs - the model half (Rect by
default, pick circle, pick rect, the locked Chat layout refuses the circle) and
the panel half (the Shape title owns both options, the circle is the gated one,
the hint is indented and follows the row, every titled block's content is at
10px, Strength at 20px). 432/432 green, 0 warnings.
This commit is contained in:
@@ -2100,3 +2100,45 @@ wearing light-system chrome. Unstyled `CheckBox` is the same story.
|
||||
|
||||
**Side effect worth naming:** the sweep also exposed a typo'd foreground (`#e0d0e0` instead of
|
||||
`#e0e0e0`) that only a single source of truth can prevent from recurring.
|
||||
|
||||
## 2026-10-21 — A CHOICE IN WPF: MUTUAL EXCLUSION BELONGS IN THE MODEL, NOT IN A RADIOBUTTON GROUP (RECIPE — TASK 59)
|
||||
|
||||
A "pick one of these" control (Rect | Circle) bound in WPF has two classic traps, both
|
||||
of which break the panel silently — the control keeps showing what it last showed while
|
||||
the model says something else.
|
||||
|
||||
**Trap 1 — the RadioButton group kills the binding.** A `RadioButton` group unchecks its
|
||||
siblings by setting `IsChecked` **in code** (`RadioButton.UncheckRadioButton` →
|
||||
`SetCurrentValueInternal`). WPF sees the target property being set outside the binding
|
||||
and **drops the binding** — the options stop reflecting the model for good
|
||||
("Losing the binding for radio button after I set the property manually in code",
|
||||
stackoverflow 7376373; the same trap in a template, 57275046).
|
||||
|
||||
**Trap 2 — a ToggleButton unchecks itself when clicked while checked.** A plain
|
||||
`ToggleButton` (unlike `RadioButton`) toggles `IsChecked` off when you click it while it
|
||||
is on. With a TwoWay binding the model receives `false` — and if the model refuses that
|
||||
write, the UI must be told again, or the option stays visibly unchecked while the model
|
||||
holds the truth.
|
||||
|
||||
**The recipe:**
|
||||
1. **Give the model one bool per option** (`IsRectShape`, `IsRoundShape`) — each
|
||||
settable, each doing the real work through the existing `Toggle…()` path so the
|
||||
side-effects (store/restore the pre-round rect) are not duplicated.
|
||||
2. **The setter ALWAYS re-raises** its own property name (`Raise(nameof(IsRectShape))`),
|
||||
even when the write changed nothing. The binding re-reads the getter and the UI snaps
|
||||
back to the model's truth — that is what makes a refused write (a locked shape)
|
||||
visible instead of sticky.
|
||||
3. **Raise BOTH option properties whenever the underlying value changes** (in the
|
||||
`ClipShape` setter), so the pair can never disagree on screen.
|
||||
4. **Do NOT use a `RadioButton` `GroupName`** for a bound pair. Two plain
|
||||
`ToggleButton`s (styled as segments) with model-side mutual exclusion keep their
|
||||
bindings alive (stackoverflow 50980687 measures the same behaviour with
|
||||
`IsChecked` bound to separate bools).
|
||||
5. **Show the CURRENT state, not the next action.** A button labelled "Round" when the
|
||||
camera is a rectangle tells the user what will happen; a selected "Rect" option tells
|
||||
them what IS. The creator called this out: *"Rect should be selected as a default if
|
||||
none exists."*
|
||||
|
||||
**Bonus:** the same "title owns its content, content is indented one level" rule applies
|
||||
to any dense settings panel — one indent level per ownership level, two when a title is
|
||||
itself nested (Strength under Blur under Background).
|
||||
|
||||
Reference in New Issue
Block a user