Development Advisories - ShadobaDev/Platemaker-qt GitHub Wiki
How the application tells the artist that something is set up in a way that will not do what they meant β without a modal dialog, and without a warning nobody can clear.
Status β built.
widgets/badge/, the registry,AdvisoryBarand its two hosts, and the render gate. See Inputs and anchoring for the gate's prompt.
struct Advisory {
enum class Level { Info, Warning, Error };
Level level;
QString text; //!< four words, for the badge
QString detail; //!< the tooltip: what is wrong, and why
QString actionText; //!< optional β a way to look at it
std::function<void()> action;
QString resolveText; //!< optional β a way to make it go away, which may destroy something
std::function<void()> resolve;
QString projectUid; //!< which chapter it is about; empty = the application's
};A way to look and a way to resolve are kept apart, because they are offered differently. An action only takes the artist somewhere, so it is one click on a chip. A resolution can delete work β Delete the 3 objects β so it is only ever offered in a question, the render gate, and never sits on a chip one stray click away.
The action travels with the warning. β3 objects unanchoredβ is an annoyance; β3 objects unanchored β show themβ is help. An advisory that cannot say what to do about itself is usually not worth raising.
Two conditions stand today. N objects unanchored, an Error: an overlay whose anchor names no page, or a page not in the input list; its action opens the strip editor with them picked out, its resolution deletes them. N fonts missing, a Warning: lettering in a family this computer does not have. The render is unaffected β the words are outlines in the file β but an edit would set them in a stand-in; its action shows the objects, and it has no resolution. A third candidate β a grade is set but its render step is off β was raised, built, and then designed out: the library dropped
ColourCorrection::enabled, so a grade that is not neutral always runs and a neutral one is no grade. There is no state in between for anyone to be warned about. A warning deleted by making its condition impossible is worth more than a warning worded well.
class Advisories : public QObject { // owned by MainWindow
void raise(const QString& key, Advisory); // idempotent per key β re-raising replaces
void clear(const QString& key);
void clearProject(const QString& uid); // a project was removed
void clearAll(); // the workspace closed
QList<Advisory> forProject(const QString& uid) const; // + application-wide, worst first
bool hasError(const QString& uid) const; // the render gate's whole test
signals:
void changed();
};The key is a string, built by the emitter as "<condition>/<project uid>". By uid and never by
index, because removing a project shifts every index after it and an advisory would slide onto a
chapter it was never about.
A condition is derived, so it is re-evaluated and never remembered. MainWindow::refreshAdvisoriesFor()
recomputes everything it can observe about one project and raises or withdraws to match. It is called from
anything that changes a project, rather than from each place that could make a condition true β the
arrangement in which a condition is eventually forgotten and a badge outlives its cause.
Re-raising an advisory that has not changed says nothing. Conditions are re-evaluated on anything that touches a project, so without that the status bar would be rebuilt on every keystroke that reaches one.
It is not part of the tool framework, and the reason is concrete: a condition a tool notices in the
strip editor can have its cause in the Workflow tab, and neither window owns the other. Whoever can
observe the condition raises it β and for today's conditions that is MainWindow, which holds the whole
workspace and needs no dock open to read it.
An advisory says which project it is about. The status bar shows one chapter's at a time β the open
one in front (currentProjectIndex()), and none while no chapter has a window open β because "3 objects
unanchored" over three open chapters is a sentence with no subject. The render gate asks about one
project for the same reason.
Emitters: the workflow map, the strip editor's tools, the render preflight. The status bar subscribes and knows nothing about any of them. Keys make raising idempotent, so a condition re-evaluated on every change does not accumulate duplicates.
| Level | Tone | Example | Effect on a render |
|---|---|---|---|
| Error | red | 3 objects unanchored | stops and asks |
| Warning | yellow | 2 fonts missing | none |
| Info | blue | β | none |
| neutral | palette | multiply Β· Shout β the blend and preset chips on object rows | none |
"Stops and asks" is a property of the level rather than a special case, so the render gate is one rule β prompt while any Error stands β rather than a growing list of checks. It asks rather than refuses; see Inputs and anchoring for the prompt.
This is the distinction that decides what is a badge at all.
- A condition is derived from state, so whoever raised it re-evaluates and clears it. Objects unanchored is one: it disappears when the artist fixes it, and nothing has to remember to clear it.
- An event has no condition to re-evaluate. βConversion dropped 2 tailsβ already happened; nothing can make it stop being true, so nothing can clear it.
AdvisoryBar is a strip of chips for one project that keeps itself in step with the registry. The
registry always allowed more than one subscriber; what was missing was something a window could hold,
and the gap showed the moment the strip editor was dragged out: a floating dock is a top-level window
with no status bar, and maximised it covers the one behind it. The chapter's problems were invisible in
the very window they are worked on.
So there are two hosts, and neither knows about the other:
| Host | Where | When |
|---|---|---|
MainWindow |
the status bar, via addPermanentWidget()
|
always |
| the strip editor | its own bottom edge | only while its dock is floating |
The editor's bar stands down when the dock is back inside the main window, because the status bar is
already saying it and saying it twice teaches nothing. The rule belongs to the owner β MainWindow
connects QDockWidget::topLevelChanged to Editor::setAdvisoriesActive(), because the editor cannot
know what other surfaces its window has. Stands down, not is destroyed: drag the dock out again and
the bar is there.
The one case this leaves is a floating dock sitting beside a visible main window, where the same chip shows twice. That is redundant rather than wrong, and the alternative is asking which window the user is actually looking at, which no toolkit can answer.
The bar is as wide as its chips and no wider, so where it sits is the host's to say: a status bar
pins it right of its own accord, and the editor's layout is told to (Qt::AlignRight). Bottom right in
both, because that is the corner this application β and every other β keeps its permanent status items
in, and a chip that moves when you undock a window is a chip you have to look for.
A bar with nothing to say takes no room β not a row of pixels, not its own margins. An always-visible empty strip is chrome reporting that all is well, which nobody asked for.
Events are therefore not badges. They go to the status bar's temporary message area β
showMessage(text, k_noticeMs) β while badges live on the right through addPermanentWidget(). The
lifetime question answers itself: a message expires, a badge is recomputed. One thing raises an event
today: Convert to βΈ hiding tails, which the strip editor reports as noted() and MainWindow shows.
That is the whole mechanism; a thing that expires by itself has no registry to belong to.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Converted 2 texts to bubbles. (β 3 objects unanchored) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
temporary, expires permanent, recomputed
The chips go into one tray, not one by one β that tray is the AdvisoryBar. QStatusBar frames
every item it is handed, so adding chips separately draws a vertical rule between each pair; one widget
holding them all is one frame, and the gap between chips becomes that widget's layout spacing instead of
a piece of chrome. It is also why the empty bar collapses its margins rather than merely hiding: a status
bar re-shows its permanent widgets whenever it reformats, so a surface that can be shown from outside had
better be zero-sized when it is empty.
No memory or CPU readout. Qt offers neither portably, and an empty status bar is not a problem needing a solution.
Badge painting used to live only inside profilepickerdialog.cpp's own delegate β but a delegate paints
inside an item view, and a status bar needs a widget. widgets/badge/ serves both: BadgeItemDelegate
is now shared by every item view that shows badges (including profilepickerdialog.cpp), and
makeBadge() gives the status bar and floating editors a standalone widget from the same description:
struct Badge {
QString text; //!< four words at most
QColor fill; //!< the only one a caller must supply
QString detail{}; //!< the tooltip
QColor fill2{}; //!< invalid β flat; valid β a vertical gradient
QColor border{}; //!< invalid β fill.darker(150)
QColor textColour{}; //!< invalid β black or white, by the fill's luminance
Badge& withFill2(const QColor&); // chainable, so one colour can be
Badge& withBorder(const QColor&); // overridden without restating the
Badge& withTextColour(const QColor&); // others
};
enum class BadgeTone { Info, Warning, Error, Neutral };
Badge toneBadge(BadgeTone, const QString& text, const QString& detail, const QPalette&);
QSize badgeSize(const Badge&, const QFont& base);
void paintBadge(QPainter&, QRect, const Badge&, const QFont& base); // one chip
QList<QRect> layOutBadges(QPainter*, const QFont& base, const QList<Badge>&, // a run of them
int left, int top, int lineHeight, int right);
QWidget* makeBadge(const Badge&, QWidget* parent = nullptr, // for the status bar;
std::function<void()> onClick = {}); // onClick makes it a buttonFour colours, three of them optional, each defaulting from the fill β which is what keeps every
existing call site compiling and looking identical, since today's badge is exactly { text, colour }
and today's painter already derives the border as colour.darker(150). fill comes second for that
reason, ahead of detail, which is the only field order in which an old two-argument call still means
what it used to.
One delegate paints every badged row. BadgeItemDelegate draws a row as its text followed by its
chips, with an optional dimmed second line and chips of its own β the profile picker's shape and the
object stack's, from one class. It reads them from the model, at roles far above Qt::UserRole
(+900 to +902) so they cannot collide with what a host keeps there: indexing a flat vector by
index.row() is an assumption only a list can keep, and the object stack is a tree. A row with nothing
to report is handed straight back to QStyledItemDelegate, so a view gains the delegate without gaining
a new look. The style still draws the background, the selection, the check indicator and the icon; only
the text and the chips are ours.
layOutBadges() paints or measures from one code path β pass a null painter to measure. That is
what lets a view answer "which chip is under the cursor" without a second copy of the layout
arithmetic, and a second copy is exactly the thing that drifts and puts the wrong sentence on a chip.
The label is near-black or near-white, by the fill's perceived luminance (textColour left
invalid). It used to be a fixed dark grey, which was correct only while every chip stayed light: a red
error badge printed dark on dark.
The four semantic tones are defined in exactly one place, derived against QPalette::Base so they
read in both light and dark themes. Nothing else in the codebase names a badge colour.
A delegate-painted badge is not a widget, so setToolTip() has nothing to attach to and something has
to be asked instead. The hook is QStyledItemDelegate::helpEvent() β a virtual on the delegate, not
on the view β so a view grows per-badge tooltips by using a delegate that has them, and no view has to
learn anything. The delegate asks layOutBadges() with a null painter, which is why the chip that
answers cannot be a different chip from the one on screen.
A makeBadge() widget needs none of this: it carries its own tooltip like any other widget. Given an
onClick, it becomes a button with a pointing-hand cursor β how a status-bar chip that only reports a
problem also offers the way out of it.
A badge's text is four words; its detail is the sentence explaining what is misconfigured and what
to do. Neither is optional for an Error.
Tool::availability() β Available | Unavailable(reason)Would grey a tool and explain itself in a tooltip. It was planned as one line on the tool and is not declared: its obvious candidate β greying the grade tool when its render step is off β turns out to be unnecessary, because the preview is ungraded too and therefore nothing lies. A badge says it better than a disabled control, and greying would also remove the convenience that touching the grade turns its step on.
The first tool that genuinely cannot run will bring its own reason.
See also: Strip editor Β· Inputs and anchoring Β· Architecture