Development Advisories - ShadobaDev/Platemaker-qt GitHub Wiki

Advisories and badges

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, AdvisoryBar and its two hosts, and the render gate. See Inputs and anchoring for the gate's prompt.

An advisory

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.

The registry belongs to the application

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.

Levels

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.

Conditions, and things that merely happened

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.

The surface is a widget, not a status bar

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.

widgets/badge/

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 button

Four 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.

Tooltips

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

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

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