Conversation
…n a ContainerGroup (#40) - Add value_changed_event to AbstractAttribute for untyped notification - Add attribute_added and attribute_removed events to AttributeContainer - Add shared attribute detection (shared_attributes, is_shared) to ContainerGroup - Add synchronization controls (set_synchronized, is_synchronized, synchronize_all, clear_synchronizations, sync_attribute) - Support real-time 2-way value synchronization with recursion guard - Support dynamic container/attribute addition and removal - Support JSON serialization/deserialization of synchronized attributes - Add comprehensive unit tests in test_container_group_sync.cpp
…onize-shared-attributes-across-containers-in-a-groupcontainer feat: add option to synchronize shared attributes across containers i…
Adds a design layer under meta_qt so a host can supply an alternative look
for attribute rows without forking the renderer, and so a second look costs
a registration rather than an edit to shared dispatch.
Today a row is produced by three hardcoded cascades: an if-chain on typeid,
a WidgetRenderer<T> specialisation, then an if-chain on the widget_type
string. WidgetRenderer<float>::render() also does five jobs at once -- read
metadata, pick a control, build it, wire control->attribute, wire
attribute->control -- and the last two are copy-pasted into every branch.
A second visual variant therefore means duplicating the model wiring, which
is where the subtle bugs live (a sync landing mid-drag, signal feedback
loops, subscription lifetime).
Four layers, all additive:
- ui/theme.hpp Theme as a value type plus a registry, not process-wide
globals; derived colours are exposed as formulas so an
accent change propagates. Two colourways ship so the
mechanism has more than one occupant.
- ui/control.hpp ControlBase / Control<T>. Wheel events are ignored unless
focused (wheelEvent is final; override handle_wheel), and
the editing flag is only settable via begin_edit/end_edit
alongside the matching signal.
- ui/binding.hpp bind<T>() -- written once per type, never per design.
Owns the sync-vs-edit guard, signal blocking and
subscription lifetime for every control of that type.
- ui/design_registry.hpp
(design, type, widget_type) -> factory, resolving exact,
then wildcard, then falling back to the stock renderer.
Controls provide can_render() so an attribute a design
cannot honour (a rail with no min/max) degrades to stock
rather than rendering a [0, 0] range.
designs/industrial/ supplies two controls as proof the seam is real: a float
slider and a bool toggle. An unregistered design name resolves nothing and
every row falls back, so selecting a design that does not exist yields the
unmodified stock panel.
container_widget gains one ContainerRenderOptions field, row_renderer,
threaded to the three row-building sites. Left empty it calls qt::render()
exactly as before, so behaviour is unchanged unless a host opts in.
Follow-up to the design layer: "stock" becomes a flavour registered like any
other rather than a fallback hardcoded into the dispatcher, so every design
sits at the same level in the hierarchy.
designs/stock wraps the existing WidgetRenderer<T> for each supported type,
registered under the wildcard widget_type since WidgetRenderer<T> already
resolves widget_type itself. No stock widget is rewritten and
WidgetRenderer<T> stays a public compile-time extension point -- only its use
as a dispatcher goes away. Splitting the inner widget_type if-chains into
separate entries would remove those too, but that is a rewrite rather than a
registration and is left for later.
render_row() now reduces to a single DesignRegistry::render() call with no
hardcoded renderer behind it.
Resolution gains a fallback chain, expressed as data rather than as a special
case in code: set_fallback("industrial", "stock") means a widget type the
industrial design has not covered yet resolves through stock instead of
rendering nothing. Without it a design under construction would drop every
unported row, which is what the old hardcoded fallback existed to prevent.
Leave the fallback unset and an unregistered type renders nothing, as
expected. Cycles are ignored rather than followed, and a factory returning
nullptr -- can_render() declining -- resumes the same walk.
The design only worked against the one colour scheme it was sampled from: every surface, hairline and ink value was a hardcoded hex. Theme::from_palette() derives them from a QPalette instead, so the look sits on top of whatever scheme the host runs. Two derivation rules, picked so the same code works on light and dark schemes without branching on which one it is: - Surfaces blend towards black or white. A recessed rail well is darker and a raised bevel lighter in both schemes, whereas darker() on a near-black window barely moves. - Dimmed ink blends towards the window colour, so "less prominent" resolves to darker on a light scheme and lighter on a dark one by construction. Accent comes from QPalette::Highlight rather than being imposed. Group accents stay hardcoded on purpose: they encode which family an operation belongs to, so they have to stay distinguishable from each other rather than track a host accent. ThemeRegistry now defaults to "palette", built lazily from the application palette because the registry is a static singleton that may be constructed before QApplication exists. The sampled colourway remains available as "industrial-dark" for pinning the reference look. The struct's member initialisers still hold the reference values. They document what was sampled and keep a default-constructed Theme paintable, but they are no longer the intended source of colour.
SliderInt covers roughly 13% of the rows in a Hesiod node panel once Seed is counted, and was the largest remaining stock element after float and bool. Rather than copy ParamSlider, the row geometry and painting move into slider_chrome and both sliders call it. The two rows cannot drift apart, and the duplication is limited to value semantics, which genuinely differ: the int rail quantises to whole values, the glide drives the painted position only, and the readout is set up front. A float slider derives its value from the animation instead, which for an int would briefly show something the model cannot hold. One wheel notch is one unit rather than a percentage of the range. Section chrome needs a different seam. A section is not bound to an attribute, so it cannot resolve through the design registry; ContainerRenderOptions gains a section_factory alongside row_renderer, empty meaning the stock section. The industrial factory returns a stock CollapsibleSection wearing a theme-derived stylesheet rather than a subclass, which restyles the header without widening CollapsibleSection's interface. That header is a checked QToolButton, which most styles paint in the platform highlight colour. It is why an unstyled panel showed blue bars between grey rows.
Combo boxes now use a popup of our own rather than QComboBox. The stock popup composites a frame and an item view that are styled separately, so mid-open you see one surface and once it settles another, and the two rarely agree on colour. Painting the whole surface in one pass is the only way to make it read as a single thing. Covers EnumComboBox, ComboBox and ButtonGrid, roughly 6% of the rows. The four Qt popup traps are handled explicitly, since each looks like a different bug: it opens on release, because opening on press means the matching release lands outside and dismisses it; an outside press is closed by hand, because overriding mousePressEvent suppresses Qt's built-in dismissal; the close is timestamped in hideEvent rather than destroyed(), which fires an event-loop pass too late; and a click arriving within 200ms of a close is swallowed, otherwise clicking an open combo closes it and immediately reopens it so it never appears to close. Sections gain a real collapse animation. CollapsibleSection::set_expanded becomes virtual and its members protected so a design can animate without that class anticipating how. The animation drives setFixedHeight rather than maximumHeight, which Qt clamps upward so the body springs to full size for a frame; disables the body layout for the duration, since a live layout reads a shrinking parent as a squeeze and compresses the rows instead of clipping them; and measures the expanded height while the body is actually laid out, because sizeHint() on a hidden widget overestimates and the animation overshoots. Also: bind() is renamed bind_control(). An unqualified bind<T>() call with a std type pulls std::bind in through ADL and fails deep inside <functional>, nowhere near the mistake. ui_font() probes for a neutral grotesque instead of taking whatever the platform hands out, falling through to the platform default when none is installed.
Sections are drawn as a card: the header and the body each paint the same background, with the section painting one underneath to fill the few pixels between them. All three name the colour explicitly. Nothing else works here. Painting only the card is invisible, because both children are opaque and cover it -- a magenta test card showed through as a three-pixel line and nothing more. Making the header "transparent" so the card shows is equally unreliable: Qt falls back to painting a QToolButton itself for any state a stylesheet does not name, and :checked is every expanded section, so the header came out a different shade no matter what the card was set to. Collapsing moves a ClipBox's reveal rather than resizing anything. The body is kept at its natural size and cropped, because a child is clipped to its parent for free. Constraining the body instead fails three different ways: a fixed height has to land exactly on the final size hint or it snaps when released, a maximumHeight is clamped back up by the body's own Fixed policy, and leaving the body Preferred lets the parent layout redistribute space across every section, so one animating section nudges all of them -- worse the further down the column they sit, because the error accumulates. The clip box also watches the body for LayoutRequest. A widget whose height follows its width settles after first layout, and without this it stays cropped at whatever it measured first. Combo popups reveal by clipping inside a fixed translucent window rather than resizing the window itself: the platform enforces a minimum window size and coalesces rapid resizes, so an animated geometry simply snaps. WA_TranslucentBackground has to be set before the native window exists, and without WA_NoSystemBackground beside it, or the surface is left undefined rather than clear. PointsCanvas derives its height from its width instead of a hardcoded 220. The point domain is square, so a fixed height only matched it at one panel width. This changes the stock widget for every consumer, not only this design.
…emove header templates
The factory read kPaletteTheme directly instead of the theme set for the design, so set_theme() had no effect on section chrome. A host that picks a colourway got it on the rows but not on the section cards, and the two drew different greys. Resolved per section rather than captured, since the theme can be set after the design registers.
feat(qt): pluggable widget designs for attribute rows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.