Skip to content

Development Architecture

Bartłomiej Mucha edited this page Oct 4, 2026 · 4 revisions

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.json files) 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. RenderWorker is 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.dll and 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), which qt_standard_project_setup() adds; no LD_LIBRARY_PATH needed after install.
  • macOS: not tested yet.

UI style

  • The application is dark, whatever the OS is set to. app/main.cpp forces the dark colour scheme (QStyleHints::setColorScheme). Windows 11 keeps its native windows11 style, 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 QDialogButtonBox with Ok / Cancel.
  • Destructive actions (delete profile, remove project): require a QMessageBox::question confirmation.
  • Errors from libplatemaker (pipeline, serialiser): shown as QMessageBox::critical.
  • Progress: QProgressBar in the Action dock, 0–100 (percent of output slices written).

See also: Building · Contributing · Concepts

Clone this wiki locally