Development Strip Editor Panels - ShadobaDev/Platemaker-qt GitHub Wiki

Object state and tool options

The two panels that used to be one, the tree that used to be a list, and the rule that keeps them from becoming the same thing again.

Status β€” built; the OBJECT STACK nests the strip's pages and a bubble's tails. ObjectState, StripState, BubbleToolOptions, PresetStore and the tree exist; the strip is pinned last with its pages under it, and a bubble's tails under it. A bubble's text gets no row: it is a property group. A selected tail shows only its own section in OBJECT STATE β€” width, bend, Delete tail. See the framework overview; where each lives in the code, the code map.

Two questions, two panels

Answers Subject
Tool options, under the rail what will the next object be? the active tool's options
Object state, top right what is this object? the selection

They used to be one widget answering both, meaning the selection when there was one and the next placement when there was not, with nothing on screen saying which. That is why the preset picker was confusing: one control meant two things depending on state nobody could see.

Splitting them is not enough on its own. The first attempt put the same controls in both places on the grounds that a balloon's colours mean the same thing either way β€” which is true, and which produced two panels that looked identical. The two panels must differ in what they contain, not only in what they mean.

Object state

A breadcrumb naming the subject, then one collapsible section per property group the selection has. No tabs: there is only one kind of thing in this panel, so a tab strip would be navigation without a destination. Qt has no collapsible container, so widgets/collapsiblesection/ supplies one β€” a disclosure arrow over a content widget, rather than a checkable QGroupBox, whose checkbox means enabled and reads as "turn this off".

  • Collapsed by default. Expanding is manual and the state persists across selection changes of the same kind, because the editors are updated in place rather than rebuilt.
  • The breadcrumb is the whole answer to "what am I editing" β€” strip / bubble#7 / tail#2, or 3 objects with the list on hover.
  • Empty is a legitimate state. Nothing selected, or a selection with no roles in common, shows a short line saying so. It is not hidden and the panel does not become something else. A multi-selection reads N objects and shows the union of its sections β€” see Selection, below.
  • Mixed values are shown as Mixed rather than as one object's value standing in for all.

Which sections exist comes from the selected object's kind, and nothing else. A shapeless text object has no fill to edit and no tail to grow, so Skin, Style and Tails are absent β€” not greyed. A greyed control promises something deferred; these are not deferred, they are not part of that object. The same rule scales to a multi-selection: a section is there when at least one selected object carries its group, bound to the objects that carry it (Property groups).

Two things keep that from making the panel restless:

  • Group order is fixed (Shape Β· Skin Β· Style Β· Text Β· Tails), so a group present on both the old and the new selection stays where it was.
  • Editors are updated in place, never rebuilt, so expansion, scroll position and keyboard focus survive a selection change.

Together they mean the panel does not move when one balloon is selected after another β€” which is almost every selection β€” and moves exactly when the kind changes, which is worth seeing.

What it never does:

  • It is never hidden. A tool that authors nothing does not remove it. Hiding it resizes the canvas, so the strip jumps sideways whenever the tool changes.
  • It never belongs to a tool. Selecting the grade tool leaves the selected balloon's properties on screen, because the subject of an edit changed, not the object. Picking the Text tool does not strip a selected balloon down to its lettering β€” that was real, and it was an effect with no visible cause.

Tool options

The active tool's options. For a tool that creates objects, those options are "what the next object will be": a preset picker, the groups the preset fills, and the application's primary/secondary colour pair. For a tool that creates nothing β€” a transform mode β€” they are its own settings, such as whether scaling keeps aspect.

The colour pair is global to the tools, and it lives in the tool column, under the tiles β€” not on the TOOL OPTIONS page, which swaps with the tool: furniture that vanishes under the Grade tool is not furniture. It is entirely independent of the per-object colours in OBJECT STATE: setting a balloon's fill does not change the pair, and changing the pair does not touch any object. The eyedropper is how a colour moves from the canvas into it, and it takes what is drawn β€” through the grade, and from the balloons on the page as readily as from the paper. The colour tool is how a colour moves back out: the left button paints with the primary and Shift with the secondary, and what changes is what is under the pointer β€” the lettering, the outline, or the fill. The right button stays the context menu’s.

Presets populate; they do not lock

Choosing a preset fills the properties it covers. Nothing is locked and no unlock step exists β€” the artist carries straight on into fine-tuning, which is the point of starting from a preset.

The picker here sets what the next object starts from. Restyling one that already exists is a different act and has its own place: Apply preset β–Έ on the object's context menu. Both go through PresetStore::applied(), the one function that knows what a preset does not carry β€” the lettering, the box, the tails and the style seed all survive.

The picker therefore stops claiming anything, and a badge reports the truth:

Badge Meaning
Shout every property the preset covers still matches it
Custom at least one does not
Mixed a multi-selection disagrees

One changed field and it is Custom. The object does not remember which preset it came from β€” a value that has been edited has no meaningful genesis, and tracking one invites the ship of Theseus into the data model.

The badge is computed, not stored. Comparing the object's values against the preset library each time it is drawn needs no per-object field, cannot go stale, and self-heals if the artist happens to edit back to an exact match. It is also the only design with an honest answer for a multi-selection.

The same badge appears beside the group in OBJECT STATE and on the object's row in the OBJECT STACK.

This is what finally removes the original complaint β€” a combo box shows a name that stopped being true the moment you touched a control. The badge does not prevent the edit; it stops lying about it.

Object stack β€” a tree

One tree, in one place. An earlier draft had a tree inside OBJECT STATE and a flat list in the OBJECT STACK: two widgets showing overlapping truths, which is the duplicated-panel mistake one level up.

β–Έ πŸ—¨  "I don't know what's going on..." a bubble
   β€’ Tail 1
   β€’ Tail 2
β–ͺ logo.svg                              imported artwork
β–Έ β–¬ STRIP                               always at the bottom
   β–« page 01
   β–« page 02  …
  • Top-level rows are z-ordered, front at the top, and drag-reorderable. Composite order is z-order, in the GUI and in the library alike.
  • Nesting is structural and is not reorderable β€” a tail belongs to its balloon.
  • Everything is collapsed by default and expands manually. Forty pages under the strip would otherwise be forty rows of noise.
  • The Grade tool's TOOL OPTIONS page is an image editor's colour menu: the adjustments listed, the chosen one's controls below, applied live to the selection. It can act on the strip only; with anything else selected it says why and offers Select the strip, and picked with nothing selected it selects the strip itself.
  • Imported artwork is an ArtworkObject, not a bubble with nothing filled in. Import artwork… copies the file into overlays/ as art-<sha16>.<ext> and registers it with a picture record β€” no authoring sidecar, so its drawing is not ours to re-type β€” but it is otherwise placed, moved, re-anchored, muted, resized, lettered and rendered like any other object, and OBJECT STATE shows it the same ObjectState panel as everything else, gated by which property groups the record carries.

Selecting a row, clicking the object on the canvas, and the breadcrumb in OBJECT STATE are three views of one selection and always agree.

  • The object stack is a tree, and a way to pick existing objects rather than a structure of its own. Every row points at an object the editor already has β€” an overlay by uid, a page by input uid, a tail by its bubble's uid and its index β€” and holds nothing else. Overlays are top level, front-most first, each bubble with a Tail N row per tail; the strip is pinned last with its pages nested under it, collapsed until opened. A bubble's text has no row: it is a property group, not an object.
    • The strip and its pages are selectable subjects, not overlays. ObjectController::Subject says which kind of thing is selected β€” none, an overlay, a tail, the strip or a page β€” and selecting one lets go of every overlay through the same path an empty click takes. OBJECT STATE is a stack of two panels chosen by that subject: ObjectState for overlays, StripState for the strip and its pages.
    • The strip shows its page count, how many pages its grade skips, and the adjustments that grade applies β€” Curves, Brightness & contrast, Saturation, in the order the library runs them β€” each with its values, Edit (which opens it in the Grade tool) and Remove (one undo step, Remove …), the way an image editor lists the filters applied to a layer. Rows are rebuilt only when which adjustments apply changes; while a slider moves only their text is rewritten. The strip cannot be dragged, dropped on or muted: a strip that could be hidden would stop showing what renders.
    • A page shows its size in the strip and Excluded from colour correction, which adds or removes its uid in ColourCorrection::excludedInputUids β€” the one colour decision the library lets a page make. The toggle is made against the grade the editor last showed and goes out as one undo step, named Exclude page from colour correction or Include page in colour correction.
    • Every row wears the object it stands for. The glyph is the object's own silhouette β€” this balloon with its tails β€” scaled into k_rowGlyphPx points from Painter::silhouette(), filled in the object's colour and outlined in the palette's, so it survives both themes. There is no rendering and no library round-trip: the path is already geometry. The lettering is left out because the row's text is the lettering β€” the first k_labelChars characters of it. A shapeless object wears Aa (drawn as outlines, so hinting cannot clip it), a tail row wears a tail, and imported artwork wears itself, scaled; the strip and its pages wear glyphs of their own, so no row is shorter than its neighbours. A tail's glyph is a curved sliver rather than a V, because the tree already draws a chevron beside it. Every glyph is drawn at the view's devicePixelRatioF() β€” drawing at one and letting the view scale up is what makes an icon look soft on a scaled display β€” and cached per object against what it is made of: the shape, the tails, the box, the two colours and that ratio.
    • Every row reports what is true of its object, as chips after its name: unanchored (in the Warning tone) when the page it was placed on is not in the strip, the preset it still matches, and the blend mode by name whenever it is not Over (both neutral). They are facts nobody typed and that the row says nowhere else β€” muting is deliberately not among them, because the row's own checkbox already answers it and a chip repeating the control beside it is a second voice saying the same thing. The blend mode is lower-cased here and Title Case on the menu, also deliberately: a menu entry is a command and a chip is a remark.
      • The chips win the width. layOutBadges() drops a chip that would cross the row's edge, and silently, so a long line of lettering would have stopped the row reporting; the name is elided to whatever the chips leave. An unanchored object correspondingly lost the word orphan where its page number goes β€” the chip says that now, and saying it twice cost the row the width it needs.
    • What the artist opened stays open. A row is moved by taking it out and inserting it, which makes the view forget whether it was expanded, so that is carried across; and rows for deleted objects are removed before the strip is placed, so a deletion above it does not move β€” and fold β€” the strip.
    • Updated in place, never cleared and refilled. Every edit returns to the editor as a feed, so a tree rebuilt on each feed would lose what the artist had open, selected or scrolled to at exactly the moment they were using it; rows are matched to overlays by uid and only what changed is touched.
    • Drops land between rows, never onto one β€” no row accepts a drop β€” because nesting is structural and not something a drag may create. A tree moves a row by taking it out and inserting it again, so a drag can arrive as several model signals: they restart one zero-length timer, and the commit it fires compares the rows with the overlays and records nothing if the order did not change. The take also clears the row's selection, which is ignored until that commit has re-shown it.

Selection

  • The selection is a set with a primary. ObjectController holds the selected overlays in the order they were picked; the last is the primary, which is what selectedOverlay() returns and what every single-subject path still reads. The canvas gathers a set with Ctrl+click β€” the scene always multi-selected, the controller used to take the first item and drop the rest β€” and the tree gathers one with Ctrl and Shift (ExtendedSelection).
    • The strip and a page stay single, and so does a tail picked without Ctrl. Picking one collapses the set: there is one strip, a page is a row of it, and a tail belongs to one balloon. With Ctrl a tail joins the set instead (below).
    • OBJECT STATE binds the whole set through setRecords(): the sections shown are those at least one selected object carries, and a control whose value differs across the selection says Mixed rather than showing the first object's. Every control kind has a way to say it β€” a chequered swatch, a spin box using the value below its minimum with Qt's specialValueText, a combo with no current entry and a Mixed placeholder, and a tri-state check box β€” so fill, outline, stroke width, line style, its amount, font, size, bold, alignment and the lettering's colour are all editable across a selection.
    • Shape and the tails are absent for a set, and so is the lettering's own text: giving several objects one shape is a conversion, adding a tail to five balloons is five objects, and five balloons do not share one line. The rest of the Text section β€” font, size, bold, alignment, colour β€” is there.
    • A role is which objects a group is bound to. The skin editor is bound to the shaped objects only, so lettering with no balloon neither votes Mixed with a fill it never uses nor receives one.
    • Writes are property-granular for a set: PropertyGroupEditor::applyEditedTo() writes only what the artist touched since bind() and leaves every other property as each object had it. One subject still takes the whole-group applyTo(). The edits come back as changedMany() / committedMany(), in the selection's order, and land as one history step (Edit N objects).
    • The colour tool follows the same rule: poured onto an object that is part of a selection, it paints every selected object in the role it has, as one step, and the selection stays.
    • Delete acts on the whole set, as one history step (Delete N objects) β€” one gesture, one undo. Duplicate, Apply preset β–Έ and Re-anchor to β–Έ are disabled while several are selected: they act on one object and say so.
    • A feed re-applies the set and drops whatever no longer exists, so an undo that removed two of five leaves three selected rather than clearing the lot.
    • A selection may hold objects and tails at once, because position is the role they share: a tail of one balloon and the body of another are dragged as one thing. Ctrl gathers them β€” on a tail's row in the tree, or on its handle on the strip β€” and the balloon a selected tail belongs to is selected with it, because its handles only exist while it is. OBJECT STATE then shows no property sections: what such a selection has in common is where it sits, and that is edited by dragging rather than in a panel.
    • Dragging one of a selection moves all of it β€” position is the one property every object has. Each co-mover is placed at the position it held when the drag began plus the drag's delta, so a fast drag cannot make the formation drift; the whole move is one history step (Move N objects), and each object re-anchors to whatever page it lands on, exactly as a single drag does. Only a move carries the others: a resize is that object's own size.
  • Selection is the canvas's, not a tool's. Every object is selectable, movable and resizable under every tool; an unanchored one is the only exception, because it is not on the strip. A tool decides what a placement creates β€” armed in CanvasInput and drawn by Placement β€” so ObjectController knows nothing about the active tool; it is told only what TOOL OPTIONS says the next object is (setToolDefaults()). Anything more would put the same gate in two places and make the object panel depend on a cause the artist cannot see.

The object menu

  • The object's context menu is the widgets' own action list (Qt::ActionsContextMenu on both the canvas and the tree, so the two cannot drift and every entry keeps its shortcut; a right-click selects the object it points at unless that object is already part of the selection), in three sections: what the object looks like β€” Apply preset β–Έ, Blend β–Έ β€” then where it sits in the stack β€” Bring forward, Send back β€” then what happens to it whole: Convert to β–Έ, Re-anchor to β–Έ, Duplicate, Delete, and Import artwork… which needs no selection at all.
    • Blend is a property of every kind, so it is in OBJECT STATE as well as on the menu. BlendEditor (properties/) is the one row a balloon, a piece of lettering and imported artwork all show β€” deliberately not a PropertyGroupEditor, because blend lives on StripOverlay rather than on the authoring record: it belongs to the object, not to the drawing. A selection that disagrees shows Mixed (no current entry, a placeholder), exactly as a mixed swatch does. A selected tail has no row: its composite is its balloon's. The menu's tick and the row read one computation, selectionBlend(), so they cannot disagree.
    • Blend applies to the whole selection as one step, and the tick shows the mode only when the selection agrees β€” the same answer OBJECT STATE gives by saying Mixed. The mode has been in the model, the compositor and this preview since the library shipped it; this menu and the row are what reach it.
    • The colour entries spend the pair: Fill with primary colour and Outline with secondary colour apply it to every selected object that has that role, exactly as the colour tool does when poured onto one β€” the menu is what makes the gesture discoverable. They read the pair and never write it.
    • Apply from tool options β–Έ copies one property group from TOOL OPTIONS onto the selection β€” Shape, Fill & outline, Line style, Text style (everything about the lettering except the lettering). Shape lands only on objects that already have one: giving a silhouette to something that has none is a conversion, and that is elsewhere on this menu. This is the style applicator: as a rail tool it would have to carry a current style, which a stateless tool may not; as a menu entry the value is whatever the tool's options are set to. Applying a line style mints a styleSeed for an object that has none, through topUpStyleSeed() β€” the one place that mints one, which collect(), a preset and a fresh placement all call.
    • Save as preset… stores the selected balloon's look through PresetStore, which is the one place that knows a preset carries no lettering.
    • Convert to β–Έ crosses the one boundary there is: Text or Balloon. An object either has a silhouette β€” to fill, to roughen, to grow tails from β€” or it has not, and that decides which property groups it carries at all. Which silhouette is a property, so it is not in this menu; a conversion to Balloon arrives at whatever TOOL OPTIONS' tiles are set to, the same source Apply from tool options β–Έ spends. The whole selection goes in one step, and an object already of that kind passes through untouched β€” including its place in the stack, since nothing in the conversion reorders anything. Select two texts and a balloon, convert to Balloon, and the balloon is simply not in the diff. The tick is on the kind, so any silhouette ticks Balloon, and only when the selection agrees β€” as Blend's does.
      • The carry-over needs no per-pair code. Every kind is the same record, so a conversion writes one property, and what the new kind cannot draw is hidden rather than destroyed: a converted balloon's tails stay in its record β€” hasTail() is what decides they no longer count β€” and a conversion back brings them with it. The one thing the artist is told is what stopped being drawn.
      • A kind is something only an authored object has. Imported artwork has no record to convert and a tail is not a kind of object, so a selection containing either can reach nothing and the menu is greyed. Every authored object can reach every shape, which is why the intersection the menu offers is, today, either all of them or none.
      • Shapes are named and ordered in one place (shapeTitle() / shapeOrder()), so this menu and OBJECT STATE's shape tiles cannot come to offer different things or call them different names.
    • Bring forward / Send back move one object one place through stripOverlays, which is the composite order β€” the list has always shown it, reversed, and a drag has always committed it. They stay single-object: moving several needs a rule about their order among themselves.

The rule that has to hold

An editor never knows which surface it is in. If a group's editor must be more compact under the rail than on the right, the surface asks it to be compact; the editor does not ask where it is.

The previous attempt failed exactly here β€” the panel carried an enum naming its own seat β€” and the visible result was two panels a few hundred pixels apart showing the same large control grid. The enum was deleted rather than refined.


See also: Strip editor Β· Property groups Β· Advisories and badges

⚠️ **GitHub.com Fallback** ⚠️