Development Architecture - ShadobaDev/Platemaker-qt GitHub Wiki
Architecture
How the GUI code is organised, and the rules that keep it that way.
Platemaker GUI is a cross-platform Qt 6 desktop application for comic artists. It puts what
libplatemaker does behind a visual interface:
- workspaces (
.platemaker.jsonfiles) and the projects (chapters) in them; - source pages as scrollable tile grids;
- the strip pipeline, with live progress;
- canvas profiles and margin-overlay templates; output profiles (format, slice size, JPEG options);
- the pipeline's two optional steps, authored on a continuous strip of the chapter: a project-wide colour grade, and text, balloons and artwork (strip editor).
Separation of concerns
The GUI is a thin shell. All domain logic β the pipeline, the data models, serialisation,
profile matching β lives in libplatemaker.
Qt GUI (this repository)
βββ views, widgets, dialogs all QWidget-derived
βββ background workers render worker, thumbnail loading
βββ no domain logic
libplatemaker (shared library)
βββ WorkspaceSerializer, CanvasProfileMatcher
βββ Scaler, ScaledStrip, Slicer, MarginCropper, TemplateGenerator
βββ ThumbnailCache, ImageIO, CancellationToken
The main window
| File | Holds |
|---|---|
mainwindow/mainwindow.cpp |
Construction, menu wiring, shortcuts |
mainwindow/workspace.cpp |
Open, save, close, recent files |
mainwindow/projects.cpp |
Creating, renaming, removing projects |
mainwindow/render.cpp |
Starting, monitoring and cancelling renders; the render gate |
mainwindow/renderbatch.cpp |
Refresh all projects (F6): one chapter after another |
mainwindow/profiles.cpp |
Canvas and output profile actions |
mainwindow/templates.cpp |
Template generation actions |
mainwindow/advisories.cpp |
The conditions this window observes, and the status bar that reports them |
mainwindow/fonts.cpp |
Tools β Fontsβ¦: the workspace's own fonts and the ones its lettering misses |
mainwindow/package.cpp |
File β Export packageβ¦ / Open packageβ¦ |
mainwindow/about.cpp |
Version, authors and the manual, in one dialog |
Widgets
Each folder under widgets/ holds one widget or dialog, with its .h, .cpp and .ui
together. The names say what they are; project/ is the only one large enough to be split, into
input.cpp and output.cpp for its two tabs.
Application state
There is exactly one Workspace, held by MainWindow. Widgets do not keep copies β they
read it live.
Project therefore stores a project index, not a reference or pointer to a ProjectItem.
That is deliberate: the workspace holds projects in a vector, so removing a lower-indexed project
shifts everything after it and would leave any stored reference dangling. An index can be
corrected instead, which is what setProjectIndex() exists for.
The corollary is that no widget may cache workspace data across an operation that could reallocate the vector.
Dirty tracking
Anything that changes workspace data marks the workspace modified, which surfaces as a * after
the filename in the title bar and is cleared by saving.
Widgets do not set that flag directly β Project emits projectModified() and MainWindow owns
the consequence. Any new code path that mutates workspace data has to emit it too, or the change
will be silently lost on exit.
Background threads
| Operation | Mechanism | Notes |
|---|---|---|
| Render | RenderWorker on its own thread |
Owns copies of its inputs; never touches the live workspace |
| Thumbnails | Per-tile background load | ThumbnailCache is thread-safe |
| Cancellation | CancellationToken |
Atomic; checked between slices, so cancelling is cooperative rather than immediate |
Two rules hold the threading model together:
- A worker never touches a widget. Everything crosses back to the GUI thread as a queued signal.
- A worker never touches the live workspace.
RenderWorkeris constructed with copies of everything it needs β inputs, profiles, output directory β so a render cannot race against the user editing the project underneath it. Its result is read back on the main thread when it finishes.
Signals and slots
Slots that respond to a control are named for the event: onAddFiles(), onRenderClicked(),
onOutputProfileChanged(). Signals are named for what happened, in the past tense:
projectModified(), sliceSaved(), renderToggleRequested().
Connections use the pointer-to-member form (&Class::signal), never the string-based SIGNAL/
SLOT macros, so that a mismatch is a compile error rather than a silent runtime no-op.
Cross-platform notes
- Tested on Windows 10/11 (MSVC + MSYS2 MinGW) and Ubuntu 22.04.
platemaker.dlland the libvips runtime DLLs are copied next to the executable by the CMake post-build step β no manual PATH setup needed.- Linux: the installed binary carries an RPATH (
$ORIGIN,$ORIGIN/../lib), whichqt_standard_project_setup()adds; noLD_LIBRARY_PATHneeded after install. - macOS: not tested yet.
UI style
- The application is dark, whatever the OS is set to.
app/main.cppforces the dark colour scheme (QStyleHints::setColorScheme). Windows 11 keeps its nativewindows11style, which renders dark from that scheme; Windows 10 and Linux get Fusion with an explicit dark palette, because the Windows 10 native style has no dark theme and leaves controls, popups and menus light.PM_FORCE_FUSION(an environment variable) previews the Fusion path on Windows 11. - New widgets carry no colour stylesheet: they take their colours from that palette, so they look the
same on both paths. Several older dialogs still hardcode a dark stylesheet in their
.ui; that is the pattern not to copy. - Dialog buttons: standard
QDialogButtonBoxwith Ok / Cancel. - Destructive actions (delete profile, remove project): require a
QMessageBox::questionconfirmation. - Errors from libplatemaker (pipeline, serialiser): shown as
QMessageBox::critical. - Progress:
QProgressBarin the Action dock, 0β100 (percent of output slices written).
See also: Building Β· Contributing Β· Concepts