Development Main Window - ShadobaDev/Platemaker-qt GitHub Wiki

Main window — the application shell

The main window, the project view, the image tile, the profile dialogs, the application state, thumbnails and threads.

The shell — MainWindow

Dock-based host window. Holds:

Area Widget Purpose
Workspace panel Left dock Lists projects in the open workspace, profile buttons
Project docks QDockWidget per open project Each holds a Project widget, tabbed with the workspace panel (opened via openProjectDock())
Action dock Right column Pipeline log, project status, progress bar, Stop
Menu bar File / Edit / View / Canvas / Output / Tools / Render / Help All application actions (below)
Status bar — Advisories (Advisories) and short notices

The window title names the workspace file, with an asterisk while it has unsaved changes.

One MainWindow instance = one open workspace. Switching workspace closes the current one (with save prompt) and re-opens with the new file.

The menu bar follows the standard categories (Microsoft's menu guidelines: File, Edit and View on the left, Tools and Help on the right, single-word names, flyouts one level deep):

File    New workspace… · Open workspace… · Open recent ▸ │ Save · Save as… │ Open package… ·
        Export package… │
        Reveal in Explorer │
        Close workspace · Exit
Edit    Undo · Redo
View    Workspace panel · Action panel
Canvas  New profile… · Edit active profile… · Manage profiles… │ Import ▸ · Export ▸ │
        Templates… · Open templates folder
Output  New profile… · Edit active profile… · Manage profiles… │ Import ▸ · Export ▸
Tools   Import bubble presets… · Export bubble presets… │ Fonts…
Render  Render current project (F5) · Refresh all projects (F6) │ Stop (Esc)
Help    Help │ Authors · Version

Templates sit under Canvas because a template is a canvas profile's file. Exit goes through close(), so it asks about unsaved changes as the title bar does.

One PresetStore for the application. MainWindow owns the bubble presets and hands the same store to every strip editor (StripEdit::Editor(PresetStore&, …)), so a preset saved, deleted or imported in one is offered in all at once — every picker follows the store's changed(). Presets follow the artist, not the chapter: they live in the application's settings, and opening or closing a workspace does not touch them. Importing and exporting a pack moves a file, so it is in Tools, not in the editor's panel, whose ⋯ holds only Delete preset.

Every dock — Workspace, project, strip and Action — wears the same custom title bar, the reusable DockTitleBar widget (widgets/docktitlebar/), installed via MainWindow::installDockTitleBar() (setTitleBarWidget): a live title label plus minimise, maximise and close buttons styled after the native window controls. DockTitleBar handles maximise ⇄ restore (fill the screen) itself — identical for every dock — and leaves the dock-specific actions to the owner via signals: minimise toggles dock ⇄ detach (docking tabs it beside the Workspace, except the Workspace anchor and the Right-only Action column, which just re-dock), close routes to closeDock() (Workspace / Action / strip hide — reopened via the Show … panel menu items / View strip; a project dock is destroyed). Qt hides a dock's title bar while it is tabified, so those buttons show only while a dock floats or is docked alone; a tabified dock is detached by double-clicking its tab. The Action dock keeps its fixed-width grip content underneath the bar. See the strip editor's shell for the shared dock tab-bar handling.

The project view — Project

A dock widget, tabbed alongside the workspace panel (one dock per open project). Represents one ProjectItem — created lazily by MainWindow::openProjectDock(), holding a Workspace& reference.

  • Scrollable grid of ImageTile widgets, one per InputFile in the project.
  • Tiles are ordered by InputFile::order, which is also the sequence the render builds the strip in (ProjectItem::inputsInOrder()).
  • Drag-and-drop and the ▲/▼ buttons reorder through Infrastructure::ProjectEditor (setInputOrder / moveInput), which rewrites only the order field — the stored input vector is never physically moved. A reorder marks the affected outputs out of sync via the lib's input-composition staleness axis (detectInputCompositionChange, surfaced by sanitize() at the next Refresh/Render/reopen), and marks the workspace dirty.
  • A toolbar row above the grid shows: project name, input directory, link/unlink canvas profile button, output profile selector.
  • A Workflow tab renders the pipeline as a read-only map of StageCards (widgets/stagecard/), built from the live ProjectItem: Inputs → Margin crop → Colour correction → Resize → Slice → Text & bubbles → Output. The two optional stages draw greyed with a dashed border until active, and carry their actions on the right: a green + on hover where a step can be activated in place, Edit (or a double-click) to open the strip editor (shell), and − to clear it. It is an honest map of a fixed, non-reorderable pipeline — deliberately not a node graph, because the library's steps are typed and ordered by design, and a draggable canvas would imply a freedom that does not exist.

The image tile — ImageTile

Single card in the project grid. Shows:

  • Thumbnail (loaded asynchronously via ThumbnailCache + QtConcurrent)
  • Filename
  • Processing status badge with a colour-coded left border, keyed on FileStatus plus, for a Processed input, whether a canvas profile was applied: Pending (grey) / Processed / Done (green) / Modified (orange) / Missing (red) / Desynchronized "Out of sync" (amber) / Skipped (violet) — the render did not include this page (missing / load error) / Unverified (rose) — FileStatus::Error, rendered but its hash could not be checked / "Processed (no canvas profile)" (cyan) — the page was rendered implicitly, without a matching canvas profile (no margins). The cyan state is not a FileStatus: it is derived from InputFile::canvasProfileId being empty on a Processed input, so it persists across reopen

ImageTile::setStatus(FileStatus, bool renderedWithoutProfile) repaints only the badge + border (no thumbnail reload), so the tile can be updated live during a render (see Run Pipeline in Workflows) without re-running the async thumbnail load. The renderedWithoutProfile flag drives the cyan "Processed (no canvas profile)" state.

Profile dialogs

Dialog Library type Trigger
CanvasProfileDialog CanvasProfile Add / Edit in ManageCanvasProfilesDialog
OutputProfileDialog OutputProfile Add / Edit in ManageOutputProfilesDialog
ManageCanvasProfilesDialog Workspace::canvasProfiles() Canvas → Manage profiles…
ManageOutputProfilesDialog Workspace::outputProfiles() Output → Manage profiles…

ManageCanvasProfilesDialog also emits generateTemplatesRequested(CanvasProfile) when the user asks for the selected profile's template PNG.

The dialogs edit copies; the workspace is mutated only on accept, and only through the lib's Infrastructure::WorkspaceEditor (the palettes are private in the model — see the library's Canvas Profiles page). The GUI does not mint ids, deduplicate, preserve templateInfo, or strip presets itself; the editor does.

Application state

All persistent state is stored in Platemaker::Models::Workspace — the GUI never has its own parallel data model. MainWindow owns the single live m_workspace (the whole model is loaded at open); each Project widget holds a reference to it and is a live view over m_workspace.projectItems[m_projectIndex], not a copy. Project widgets are created lazily, one per open dock.

MainWindow
  └── m_workspace : Workspace          // loaded from .platemaker.json; the source of truth
  └── m_workspacePath : QString        // current file path
  └── m_dirty : bool                   // unsaved changes
  └── m_overlayRecords : RecordStore   // parsed cache; the records live in the overlays SVGs

Project (one per open project dock)
  └── m_workspace : Workspace&         // reference to MainWindow's workspace
  └── m_projectIndex : int             // index into m_workspace.projectItems
  └── m_records : ObjectRecord::Map        // this project's slice of the store
  └── m_workspacePath : QString        // overlays/ lives beside this file

Bubble authoring records are a cache, not a store. What a bubble says — shape, text, font, colours — is information the workspace codec has no business carrying, so it lives inside the overlay's own SVG in the pm: namespace. MainWindow::m_overlayRecords holds the parsed form, keyed by project uid then overlay uid, so a populate() does not re-read and re-parse the whole chapter; it is repopulated from the assets by Svg::readOverlays() when a workspace opens, and nothing about it is written to disk. An authoring sidecar would be a second copy of what the asset already carries — one more file to keep in step, and one more thing to lose separately from the artwork it describes.

The assets live in an overlays/ folder beside the workspace file, which follows the storage split the rest of the app runs on (the library is a complete, OS-path-agnostic tool; the GUI decides where things live), so a folder holding the workspace and its overlays/ copies as one — a folder holds one workspace (Workspace folder ownership). Deliberately not in .platemaker-cache/, which is regenerable and safe to delete. Each Project holds its own slice and pushes changes back via Project::recordsChanged.

Undo covers both halves. An overlay step records OverlayState — the project's stripOverlays and these authoring records, together — so undoing a text edit restores what a bubble said and not merely where it sat. Without it the record would come back pointing at the new file, and the strip would show text the render does not bake. A project step (Project::fullSnapshot()) is the other kind and leaves both alone.

Mutation goes through the library. The workspace's profile palettes and the projects' profile-link fields are private in the model; the GUI edits them only through Infrastructure::WorkspaceEditor (replaceCanvasProfiles / replaceOutputProfiles, addCanvasProfileToProject / removeCanvasProfileFromProject, setProjectOutputProfile, setCanvasProfileTemplateInfo). It reads through the const accessors (canvasProfiles(), outputProfileId(), …).

Keeping open views in sync. A workspace-level profile edit (Manage/New/Edit) emits MainWindow::workspaceProfilesChanged, connected in openProjectDock() to Project::refreshProfileViews() on every open dock — so the output-profile combo and assigned-canvas list of all open projects update at once, without a manual refresh.

Dirty tracking rules:

  • Any profile edit → m_dirty = true → asterisk in title bar
  • Any image tile reorder → ProjectEditor::setInputOrder/moveInput (rewrites order) + m_dirty = true
  • Successful pipeline run → m_dirty = true (hashes updated)
  • File → Save → WorkspaceSerializer::save() → m_dirty = false

Thumbnail loading

  • Thumbnails are never loaded on the main thread.
  • Each ImageTile requests its thumbnail via QtConcurrent::run() as soon as it is given its file (setFileInfo()).
  • ThumbnailCache::getOrGenerate(filePath) is the only call made in the worker.
  • A QFutureWatcher brings the result back to the GUI thread, where the tile sets its pixmap.
  • A thumbnail that fails leaves the tile without one; the tile remains interactive.
  • During a render, output thumbnails are pre-warmed by the library, not read from the output file. The pipeline writes each slice's thumbnail into the same cache from the in-RAM slice before sliceSaved (Workflows), so the output tile's getOrGenerate() is a cache hit. File-reading generation (getOrGenerate opening the source) is therefore only for input tiles and at-rest output tiles (reopening a workspace) — never for an output while a render is writing it, which is what avoids the read/write race.
  • The strip editor (shell) reuses the input tiles' thumbnails as low-res proxies — an instant blurry fill drawn for each page while its full-resolution decode runs, so scrolling never shows a gap.

Background threads

Operation Mechanism Thread safety notes
Thumbnail loading QtConcurrent::run() per tile ThumbnailCache is thread-safe
Strip-editor page build QtConcurrent::run() per page (proxy + sharp) The sharp tier runs the library's page domain (decodePageToRgba); the proxy tier reads ThumbnailCache. Both are thread-safe; QPixmap is built on the GUI thread in the watcher, and a generation counter drops results from a superseded rebuild
Writing a bubble's file GUI thread Svg::write() on a settled edit, not per keystroke; a styled bubble's preview is rasterised by the library (rasterizeSvgRgba()), cached by document — a bubble is a few hundred pixels, not worth a thread
Pipeline run RenderWorker moved to its own QThread CancellationToken is atomic
Package export / unpack QtConcurrent::run() behind a QProgressDialog Cancellable; nothing in the open workspace is touched
Template generation GUI thread One PNG per request, generated in TemplatesDialog
All UI updates QMetaObject::invokeMethod() or signal/slot Never touch widgets from worker

See also: Architecture · Workflows · Main window (manual)