Development Workspace Ownership - ShadobaDev/Platemaker-qt GitHub Wiki

Workspace folder ownership

How a workspace's folder is kept exclusive, safe to move or hand to someone else, and free of files nobody references any more.

The code is mostly in widgets/workspacefolder/ and mainwindow/workspace.cpp; fonts in mainwindow/fonts.cpp, the package in mainwindow/package.cpp and the lib's WorkspacePackager.

Where a bubble lives, and when it becomes pixels

Two layers, each with exactly one owner. For a workspace at D:\Comic\Chapter_002.platemaker.json:

layer what it holds where written by
library record uid, assetPath, sha256, anchorInputUid, xFrac/yFrac/wFrac, enabled, blend Chapter_002.platemaker.json β†’ projectItems[].stripOverlays[] lib
the asset resolved artwork plus the editor's pm: parameters D:\Comic\overlays\ovl-<sha16>.svg GUI
an imported picture the file as imported, byte for byte D:\Comic\overlays\art-<sha16>.<ext> GUI (copied, never rewritten)

Two and not three, because what a bubble says and what it looks like belong in one file. Holding shapes, fills, strokes and glyphs in a bespoke schema beside a bitmap is inventing a worse SVG; here the artwork every renderer can draw carries, in a private pm: namespace every renderer ignores, the parameters the editor re-solves from. Losing those attributes degrades a bubble to flat-but-still-rendering art rather than to nothing.

The GUI resolves a bubble's geometry; the library composites the file. Painter turns a record into paths β€” silhouette() and textOutline() β€” and everything that shows a bubble draws from them:

  • Preview β€” BubbleObject resolves the paths when its record changes and draws them on every repaint (Painter::paintPaths()), so typing updates the strip with no file I/O at all.
  • The file β€” Svg::write() writes the same paths, the lettering as outlines, when an edit settles (the panel debounces typing by 300 ms; a drag reports on mouse release), never per keystroke.
  • The shape tiles draw through Painter::render(), which is why a tile cannot show a shape that placing it does not give you.

Because the scene is the strip at 1:1, the preview and the file are the same geometry at the same scale β€” the preview is not consistent with the render, it is the same drawing. A styled bubble is the exception (below).

A bubble owns one file for its lifetime. Svg::writeFile() names a new overlay by the content hash of the bytes about to be written (so identical bubbles share one file, matching the library's own dedup), and thereafter overwrites that same path. Naming every revision by its content instead would leave one file per settled edit β€” a dozen in a single lettering session. Undo does not need them: an overlay step's OverlayState carries the complete authoring record, and restoreOverlayState() calls Project::rewriteOverlayAssets() to re-emit the files of the overlays the step changed (and any whose file is missing) from the restored records β€” not every bubble, since re-emitting re-outlines the lettering and a bubble whose font is not installed would be re-set in a stand-in. An overlay sharing a path with another (addOverlay() dedups identical content at creation) forks to a fresh file rather than re-lettering its twin. A lettered picture is the exception: its wrapper is named by content on every write, so each settled edit to its words leaves a new ovl-*.svg.

A text or styling edit therefore rewrites the asset and the record's sha256; a move or a reorder rewrites neither, only the placement. Project::applyOverlays() re-emits exactly the overlays whose authoring record actually changed.

A styled bubble is previewed by the library. Marker and Ink are SVG filters, which Qt cannot draw, so ObjectController rasterises the bubble's document with the render's own StripOverlayCompositor::rasterizeSvgRgba() and BubbleObject shows that image while at rest, falling back to its own paths mid-drag. It renders from the document in hand, not from the file just written: on a synced drive a file read back immediately after a write may still return the previous content, which would pin pre-edit artwork on screen for a whole session.

A folder holds one workspace

Everything a workspace writes for itself β€” overlays/, templates/, .platemaker-cache/ β€” sits beside its .platemaker.json. A second workspace file in the same folder would share all of that, and anything that later tidies one workspace's unused files would delete the other's β€” the failure Cubase documents for Remove unused media, and the reason Cubase and Ableton both ask for one project per folder.

workspacesInFolder() counts *.platemaker.json files directly in a folder β€” not a profile bundle, not the retired sidecar, not a subfolder (a workspace in a subfolder owns that subfolder). Two places enforce it:

  • New and Save As (MainWindow::askWorkspaceFile()) refuse a folder that already holds a different workspace β€” including Save As into the same folder under another name β€” and offer to create a subfolder named after the file instead.
  • Open (MainWindow::resolveSharedFolder(), the first thing loadWorkspace() does, before the currently open workspace is closed) refuses to open a folder holding more than one. It lists them β€” file name, last saved, projects, objects, so a synced drive's conflict copy can be told from the original β€” and asks which one to keep; the others go to the Recycle Bin. Cancelling changes nothing and leaves whatever was already open alone. The folder is never opened as it is: a warning the artist could dismiss would leave every later cleanup unsafe.

The lock

A folder is written by one Platemaker at a time. WorkspaceLock is taken before anything loads or moves β€” before the shared-folder check above, too, so a folder in use elsewhere is never tidied β€” and released when the workspace closes or the application quits.

It is two files, because one cannot do both jobs:

What Where Answers
Process lock a QLockFile, named by a hash of the folder the application's local data (locks/), never synced "is this open in another window on this machine?"
Marker .platemaker.lock β€” host, pid, since inside the workspace folder, visible "is this open on another computer?", and β€” re-read before every write β€” "is it still mine?"

A held QLockFile keeps its file open, and an open file can be neither written nor deleted by anyone else β€” measured on a local disk and on a Google Drive letter alike. Putting the lock itself in the workspace folder would therefore stop a synced drive from ever delivering a takeover: the holder would keep reading its own, unchanged file. The marker is the file that actually travels between computers; the QLockFile only ever answers "here, now" β€” and, because it tells a live holder from a dead one, a lock left by a crash or a reset clears itself. The marker is written and closed at once, so a synced drive carries it both ways. The one case nothing here can detect is the other computer being off, or Platemaker there having crashed β€” which is why a takeover is offered rather than refused.

The marker says Outcome The dialog
nobody, or a dead process on this machine Acquired none
a live process on this machine OpenHere "Already open in another Platemaker window." β†’ OK
another computer OpenElsewhere "Open on <host> since <time>. Take it over?" β†’ Take over / Cancel
the folder cannot be marked (read-only media, full disk) Unavailable none β€” opens unguarded for the session

stillOurs() is asked again before every write to the folder (saving, an overlay file, a template, a render) and whenever the application is activated. Once the marker names someone else, nothing more is written β€” Project asks through a write guard MainWindow hands it, and refuses an overlay edit whole rather than applying it with no file to hold it β€” and the artist is offered Save As… (to another folder) or Close workspace, which discards. A marker deleted by hand is written again rather than read as a takeover. The CLI never takes the lock.

Moving the folder is safe

An overlay's path is written twice: absolute, and relative to the workspace file when it lies inside its folder (WorkspaceSerializer, lib 0.6.0). Loading prefers the relative path when it resolves to a file that exists. The model the GUI works with holds absolute paths either way, so nothing above the codec resolves anything β€” a folder moved, renamed, zipped and unzipped elsewhere simply loads with every overlay found, because the relative form travelled with it.

Save As collects the workspace's own files

Before a byte is written in the new location, MainWindow::collectWorkspaceFiles() copies in every project's overlay files, every canvas profile's template and every file in fonts/, and points the model at the copies β€” so the new workspace references only its own folder and the old one can be moved, zipped or deleted without breaking it.

  • collectOverlayFiles() (widgets/workspacefolder/) also carries the picture behind a lettered picture β€” named only by its record's artwork, beside the generated wrapper, so following assetPath alone would leave it behind.
  • A name already taken by a different file in the target is never overwritten: an overlay's own file is copied under its content hash instead; a picture, whose name its record depends on, fails the Save As outright.
  • All or nothing. Every file is copied before the model changes; a failure names the file and leaves the workspace exactly where it was.
  • Open strip editors are re-fed afterwards β€” each keeps its own copy of the overlays, and its next edit would otherwise write the old paths straight back.
  • Input pages are never copied; they are the artist's own files.
  • Undo follows. A history step recorded before the Save As still names the old folder, so Project::restoreOverlayState() runs the same collect before restoring it β€” undo never points the workspace back at the old folder, nor writes into it through rewriteOverlayAssets(). A balloon whose file is missing is pointed at the new folder anyway and regenerated there from its record.

Nothing is deleted mid-session; the sweep runs at open

Deleting an object, re-lettering a picture, forking a shared file, discarding unsaved work, removing a chapter and deleting a template all leave their files behind while a workspace is open β€” the undo history may still need them, and a picture's art-* file is its only copy. MainWindow::sweepWorkspaceFolder() clears what turned out unused, but only at open: that is the one moment the in-memory model is the file on disk and no undo history exists. At close, or after Discard, memory is not what is on disk β€” a sweep from memory could delete a picture the saved file still points to.

  • Referenced (referencedWorkspaceFiles()): every overlay's assetPath, the picture behind every lettered picture, and every canvas profile's template.
  • Candidates (unusedWorkspaceFiles()): only Platemaker's own names β€” overlays/ovl-*.svg, overlays/art-*, and everything under templates/. A file the artist placed there by hand, fonts/, the lock and the cache are never touched.
  • Runs only with both the folder locked by this process and holding exactly this one workspace; otherwise it is skipped and the Action log says why.
  • To the Recycle Bin (QFile::moveToTrash()), restorable β€” measured on a Google Drive letter too. Each file is listed in the Action log, the status bar reports a count, and the log's context menu offers Show Recycle Bin. It is an event, not a standing condition, so it is not an advisory (see Advisories and badges). Where there is no trash (a network share, removable media), the artist is asked once per open whether to delete permanently; declining asks again next time.
  • Delete template no longer deletes the file β€” it only clears the reference, keeping the edit undoable, and the next open's sweep removes the file if it stayed unused.

Missing fonts never change lettering silently

Outlining text goes through QFont, which substitutes a stand-in for a family that is not installed β€” silently, every time.

  • The record keeps the family it was meant to have. TextEditor writes a family only when the artist picks one; the file's pm:fontFamily is always that.
  • A file always names a font. An empty family is written as the application's own default (Painter::defaultFamily() β€” Segoe UI on Windows 11), never as whatever the platform happens to substitute (Tahoma on Windows), so "no font chosen" and "the chosen font is missing" read identically as a missing font on a machine with a different default. Presets keep an empty family β€” a preset that picks no font means the default, wherever it is used β€” and PresetStore::matching() treats an empty family and the default's name as the same font (setDefaultFamily(), handed in by MainWindow), so a bubble reloaded from its file still matches the preset it came from.
  • A stand-in bake says so. When the record's family is not available, Svg::write() also writes pm:fontFallback naming the family Qt actually used (QFontInfo) β€” without it, a stand-in bake and a real one would carry the same pm:fontFamily and be indistinguishable.
  • Only what a step changed is re-emitted. rewriteOverlayAssets(uids) takes the uids an undo step actually touched, plus any overlay whose file is missing β€” re-emitting every bubble on every undo used to re-set the lettering of bubbles the step never touched.
  • Healed at open, before the sweep, with the folder held: MainWindow::healFontFallbacks() re-writes, from its record, every file carrying pm:fontFallback whose font is now available β€” installed, or brought by the workspace β€” updates its hash and marks the workspace modified; saving is still the artist's call. This is not an undo step; it repairs a file to match what its record already says. It runs before the sweep because a lettered picture's re-set wrapper gets a new name, leaving the old one unused.
  • Reported as a condition, not per edit. Each project with lettering in a missing font carries a font(s) missing advisory (Warning) naming the families, with Show them β€” in the status bar, and in the strip editor's own bar for that project. Editing such a bubble does not ask, because the stand-in is recorded and the next open with the font undoes it. The render is unaffected until an edit, since the file holds outlines, not text.

The workspace's own fonts

A workspace can carry fonts that are active only while it is open β€” InDesign's Document Fonts, not a system install. Every .ttf, .otf and .ttc directly in <folder>/fonts/ (workspaceFontFiles()) is registered with QFontDatabase::addApplicationFont() right after the model loads β€” before anything is drawn or checked, and before the missing-fonts advisory can fire β€” and unregistered when the workspace closes. Nothing is installed and nothing needs administrator rights.

  • Wins over an installed copy of the same family (measured on Qt 6.11/Windows): once registered, QFont(family) draws with the workspace's file, and falls back the moment it is unregistered. A family stays listed once, with no foundry suffix.
  • fonts/ is a workspace folder like any other: the one-workspace-per-folder rule protects it, and it is carried whole by Save As β€” but it is never a sweep candidate (unusedWorkspaceFiles() only looks at overlays/ and templates/), because it is the artist's own, not something Platemaker generated. Licence files beside the fonts travel with them.
  • Files dropped in by hand count from the next open, not while running β€” Qt's font database has no watcher for it. A file that will not load is named in the Action log.
  • ObjectController listens to QGuiApplication::fontDatabaseChanged (emitted synchronously by add/removeApplicationFont()) and rebuilds every bubble's outline and rasterisation, so adding or removing a font changes what is drawn without touching any record.

Tools β†’ Fonts… (mainwindow/fonts.cpp, MainWindow::onFonts()) is the one dialog onto all of this:

  • The workspace's own files β€” family, style, file name, whether each is active now or "active from the next open" (added by hand while the workspace was open) or not a usable font, and whether the same family is also installed on this machine β€” "installed" meaning the font database as it stood at start-up, before any workspace added to it, so this never claims to know which copy is actually drawn (the workspace's always wins).
  • The families bubbles use but that are missing β€” the same set missingFonts/ advisories report (objectsInStandIns()), with how many objects each.
  • Add font… asks the write guard (canWriteWorkspace()) first, copies the files into fonts/ (never overwriting a different file of the same name), activates them at once, re-evaluates the advisories and runs healFontFallbacks() immediately β€” not as an undo step, since it repairs files to match records they already hold rather than editing anything. A step recorded before it still names the stand-in's bytes, which the next open heals again. Show folder opens fonts/.
  • Install copies the selected, not-yet-installed fonts into the per-user font location with no admin rights (%LOCALAPPDATA%\Microsoft\Windows\Fonts + a HKCU value + AddFontResource and a WM_FONTCHANGE broadcast on Windows; QStandardPaths::FontsLocation + fc-cache elsewhere) β€” Explorer's own Install, not Install for all users. A different file already installed under the same name is never overwritten. Measured: a process already running, Platemaker included, does not see a per-user install until it restarts, so the dialog says so; the open workspace keeps using its own copy regardless.

Export and Open package

File β†’ Export package… hands a workspace to another machine or person as one <name>.platemaker.zip that opens there with nothing to fix β€” pages included. File β†’ Open package… is the other end. Both are GUI (mainwindow/package.cpp) wrapped around the lib's WorkspacePackager.

The package mirrors the workspace folder. WorkspacePackager::plan() builds the copy the library already knows how to make β€” every path the model references, rewritten and rootless, uids and sha256 kept (the bytes are copied, not re-encoded), no output directory and no render trace. overlays/, templates/ and fonts/ keep their places in the zip because that is where the GUI put them; the library knows none of them by name. Only files from outside the workspace folder come in: pages under inputs/, anything else under external/. An unpacked package is therefore the GUI's own layout again. The GUI then appends what the model alone cannot name:

  • the picture behind every lettered picture, beside its wrapper, under the name its own record gives it (the same rule collectOverlayFiles() already follows for Save As);
  • fonts: the workspace's fonts/ folder whole, plus β€” for every family a bubble names explicitly β€” the installed files of every style of it (installedFontFiles(), via DirectWrite on Windows; not yet resolved on Linux, where such a family is simply reported as not packed). A font belongs to the family when any name it carries says so β€” its Win32, DirectWrite or typographic family β€” because Qt lists a font by its Win32 family, which DirectWrite's own lookup does not know (measured: Wieynk Fraktur's five files share the Win32 family "Wieynk Fraktur", which DirectWrite splits into "WieynkFraktur", "WieynkFrakturZier", …). The platform default (a record with an empty family) is never packed: it was not chosen, so a machine without it is already handled by the missing-fonts advisory.

Nothing is blocked; gaps are declared. Before a byte is written, whatever cannot go in β€” missing pages, a lettered picture's missing source, a font no file could be found for β€” is listed, and the artist can still choose Export anyway: missing pages travel as declared and Replace file… fixes them after opening. The manifest (package.json, written last) records what the GUI packed and what it could not, plus editable (false when anything is missing) and the exporting Platemaker/libplatemaker versions.

The archive is the lib's job. WorkspacePackager::write()/unpack() stream through libarchive β€” already linked for libvips, so only its headers needed fetching β€” with ZIP64, a cancellable progress callback, and unpacking that refuses any entry trying to escape the target folder. Export writes on a worker behind a cancellable QProgressDialog, by default beside the workspace's folder rather than in it; nothing in the workspace is touched, so it needs no lock. Unpacking is whole or not at all, through <folder>.partial, and the manifest must name a workspace file at the root. Open unpacks into a new folder named after the package, in a location the artist picks, and refuses a location where that folder already exists β€” so the one-workspace-per-folder rule holds by construction, without needing its own check. The unpacked result is then opened through loadWorkspace() exactly like any other workspace: the lock, font activation, the font heal and the sweep all run as usual, and the zip itself is never touched again.

Afterwards, once: a version mismatch between the exporting and the opening Platemaker (or libplatemaker) is reported in the Action log and the status bar β€” informational, renders may differ slightly, nothing is blocked; a package whose manifest says it is not fully editable (a CLI export, a font that could not be packed) says so; missing pages arrive declared at their place in inputs/, so the Input tab shows them missing and Replace file… β€” or dropping the file there β€” fixes them; and fonts the package brought that are not installed on this machine are named, with an offer straight into Tools β†’ Fonts… to install them.


See also: Advisories and badges Β· Inputs and anchoring Β· Workspaces Β· Architecture

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