Keep editing while a project saves; saves, reloads and opens queue behind one writer (12.13b)
This commit is contained in:
1 parent
838d5b1459
commit
d4c065fa77
11 files changed
+1027
-85
No files matched your search
@@ -49,7 +49,7 @@ Platform adaptations, each forced by a missing Qt analog:
|
||||
- Projects in the session: Swift's `projectURL` is `projectPath`, a `QString`, since files are paths here; it gets a setter when `ProjectController` needs one. Swift builds layers and the document through memberwise initialisers in two places (`installProject`, `applyDocumentSize`); the twin has two constructors for that, `ImageLayer(record, asset, mask)` and `CanvasDocument(snapshot)`, so both places agree by construction; shapes and text set them apart: `installProject` loads both after `CanvasDocument(snapshot)`, `applyDocumentSize` does not, as Swift's rebuild leaves them out. `applyImageSize` and `applyDocumentSize` live in `IO/ImageResizer.cpp`, as in Swift; they build the new document and record the step on a copy of the history before either is published (`DocumentHistory::adopt` takes the copy's steps: its past, its future and its revision, all a closed step changes), then fit the view and signal once, so memory that runs out on the way changes nothing (Swift begins, sets, fits and ends, signalling twice; since 10.1). The twin's document is read-only from outside: a test that needs a state no session call can make (a painted mask, a record with every field set) installs a hand-built `ProjectSnapshot`, where Swift's tests write into `session.document`.
|
||||
- Masks in the session (`LayerMask.cpp`, after the model): `isMaskSelected` has a getter only, since every Swift write of it sits in a session extension. `TransformEdit` gains `mask`. `commitTransform` hands a mask-alone edit to `commitMaskTransform`, which writes the placement inside one transaction and lets the history drop a change that changes nothing, where Swift compares placements first. In `copyMask` and `toggleMaskLink` Swift's `finishOpacityEdit()` after `commitTransform()` is left out: `commitTransform` has already finished the drag. `tests/SessionFixtures.h` holds `rewrite(session, change)`, which installs a changed snapshot where Swift's tests write a mask, a flip or a sampling into `session.document`; it resets the history as any install does, so steps are counted from there. `tests/SessionRecord.h` holds `described()`, the text of everything an observer can read, shared by the signal test files.
|
||||
- Live masks in the session (`LiveLayerMask.cpp`): `adoptClipping` and `releaseDetachedClipping` are static members, as Swift's `static func`s. `removeLiveMask` and `releaseDetachedClipping` walk the layers once per parent in list order, where Swift builds sibling arrays; the runs they free are the same. `LiveMaskGraph::validate` already refuses a link to oneself, a folder at either end and a missing source, so `canLinkMask` asks only for editable layers, an existing target and a valid graph; Swift's own checks of those cases, both folder checks of `toggleClippingMask` and `canToggleClippingMask` (the layer's own and its lower sibling's: the store validates the graph on load and `canLinkMask` on every link, so no folder is ever clipped and none becomes a base), and the folder case of the run's base in `releaseDetachedClipping` (nothing is ever clipped to a folder) are dead and left out; both toggle functions take the base from one helper, `clippingBase`. `adoptClipping` keeps its folder check: it writes the link without asking the graph, and `LiveMaskEditTests` calls it directly, because `placeLayer`'s release hides the side of the rule that looks below. `deleteWithLiveMaskChoice` shows a `QMessageBox` from the session, because Swift's session shows an `NSAlert` there: three buttons with Swift's texts (accept, reject and destructive roles), and `LiveMaskDeleteTests` drives the real alert from a zero timer under `QTEST_MAIN`. The bake runs through `QtConcurrent::run` and a `QFutureWatcher` with the project busy; layers are deleted first and the project freed last, as Swift's `defer` does; a failure sets `brushError` (a getter and a setter, arriving here because this is its first writer) and logs a warning. `LiveMaskBaker::bake` draws the target in its own pixel grid and each source through the inverse of the target's pixel-to-document map; the target's own mask, opacity and blend mode stay on the layer. A snapshot that repeats a layer id is a `logic_error` once the target and its pixels are found, where Swift's `Dictionary(uniqueKeysWithValues:)` traps. The bake's worker catches `ExportError` alone, as the import's worker catches `ImageImportError`: anything else is a bug and is left to crash. `releaseDetachedClipping` keys its runs by the optional parent itself, because all zeros is a valid folder id. `PixelAdjust::thumbnail` came forward from 9.2 for the baker. Over the renderer's pixel budget a bake draws nothing, on macOS too.
|
||||
- `ProjectController` is a `QObject`. Its `async` calls take a completion callback, posted to the event loop like the session's; steps inside continue inline. `newCanvas` and `close` take none: no caller awaits them. The store runs through `QtConcurrent::run` with one `QFutureWatcher` a call, a child of the controller, so an answer dies with it; the worker catches `std::runtime_error` (both of the store's error types) and anything else crashes. Panels are `QFileDialog`s and alerts `QMessageBox`es shown with `open()` over `window` (a `QPointer<QWidget>`), so nothing blocks; without a window they stand alone, where Swift runs the alert modally. A project is a folder: the open panel is in `Directory` mode and takes one project (no `QFileDialog` mode takes several folders, as Swift's workspace panel takes several packages); the save panel is a file panel with the `comp` suffix, and a typed name that is an existing project enters that folder, as any save dialog does (probed; GTK's does too), so Save As cannot land on an existing project. An accepted panel always holds a choice, read as `selectedFiles().value(0)`. A chosen name that does not end in `comp` (in any case) gets `.comp` appended, where `NSSavePanel` enforces the project type: `setDefaultSuffix` only fills in a missing suffix. The panel never saw that completed name, so if anything exists there (a link that leads nowhere counts) the save is refused with an alert and nothing is replaced. `open` trims the slashes at the end of its path, supplied or chosen in the panel: a shell completes a folder's name with a slash, and `QFileInfo` finds no name behind one, where Swift's `lastPathComponent` does. It trims only the end: `QDir::cleanPath` would also collapse `link/..`, which names another folder once `link` is a symbolic link. Callbacks that the session keeps (`waitForFileRequest`, `importImages`) hold the controller through a `QPointer`, because the session may outlive it; everything else hangs on the controller itself (connections, watchers, posted calls) and ends with it. The alert's roles carry the keys: Return saves, Escape cancels. Swift's `session.projectURL = …` is `setProjectPath`, which notifies; `markSaved()` needs no signal of its own, because every path from a save goes on to free the project, and that notifies. `samePlace` compares `canonicalFilePath()`s where Swift compares `resolvingSymlinksInPath()`. There are no recent documents (Qt has no API for the desktop's list) and no security scopes. `receive` keeps Swift's queue: requests drain one at a time from the event loop, each after `waitForFileRequest`; a project is known by its suffix in any case, a trailing slash aside. `tests/DialogDesk.h` answers the real dialogs from a timer: it types into a panel's name field (`selectFile` does nothing while that field has focus) and clicks or keys an alert. While a desk lives the working directory is a temporary folder of its own, so a panel never starts in the source tree (a mutant once saved `Untitled.comp` there and skewed the runs after it).
|
||||
- `ProjectController` is a `QObject`. Its `async` calls take a completion callback, posted to the event loop like the session's; steps inside continue inline. `newCanvas` and `close` take none: no caller awaits them. The store runs through `QtConcurrent::run` with one `QFutureWatcher` a call, a child of the controller, so an answer dies with it; the worker catches `std::runtime_error` (both of the store's error types) and anything else crashes. Panels are `QFileDialog`s and alerts `QMessageBox`es shown with `open()` over `window` (a `QPointer<QWidget>`), so nothing blocks; without a window they stand alone, where Swift runs the alert modally. A project is a folder: the open panel is in `Directory` mode and takes one project (no `QFileDialog` mode takes several folders, as Swift's workspace panel takes several packages); the save panel is a file panel with the `comp` suffix, and a typed name that is an existing project enters that folder, as any save dialog does (probed; GTK's does too), so Save As cannot land on an existing project. An accepted panel always holds a choice, read as `selectedFiles().value(0)`. A chosen name that does not end in `comp` (in any case) gets `.comp` appended, where `NSSavePanel` enforces the project type: `setDefaultSuffix` only fills in a missing suffix. The panel never saw that completed name, so if anything exists there (a link that leads nowhere counts) the save is refused with an alert and nothing is replaced. `open` trims the slashes at the end of its path, supplied or chosen in the panel: a shell completes a folder's name with a slash, and `QFileInfo` finds no name behind one, where Swift's `lastPathComponent` does. It trims only the end: `QDir::cleanPath` would also collapse `link/..`, which names another folder once `link` is a symbolic link. Callbacks that the session keeps (`waitForFileRequest`, `importImages`) hold the controller through a `QPointer`, because the session may outlive it; everything else hangs on the controller itself (connections, watchers, posted calls) and ends with it. The alert's roles carry the keys: Return saves, Escape cancels. Swift's `session.projectURL = …` is `setProjectPath`, which notifies; a save marks the saved revision before `setProjectPath`, whose signal then shows the saved state (12.13b: the write lands after the project is free again). `samePlace` compares `canonicalFilePath()`s where Swift compares `resolvingSymlinksInPath()`. There are no recent documents (Qt has no API for the desktop's list) and no security scopes. `receive` keeps Swift's queue: requests drain one at a time from the event loop, each after `waitForFileRequest`; a project is known by its suffix in any case, a trailing slash aside. `tests/DialogDesk.h` answers the real dialogs from a timer: it types into a panel's name field (`selectFile` does nothing while that field has focus) and clicks or keys an alert. While a desk lives the working directory is a temporary folder of its own, so a panel never starts in the source tree (a mutant once saved `Untitled.comp` there and skewed the runs after it).
|
||||
- `ProjectWorkspace` is a `QObject` with one signal, `changed()`, as the session has: it mutates itself, so the rule for `@Observable` classes that only the session writes does not cover it. `tabs`, `selectedID` and `isManaging` are read through getters. A `ProjectTab` holds its session and controller by value, the session first, and lives in a `std::shared_ptr`, as Swift's tabs are references: a flow that awaits a tab's controller or session (a load, a close, a quit, an import, a copy) keeps that tab alive in its callback until the answer has run, as Swift's `await` does, so a tab removed meanwhile still ends its flow. Every callback the workspace hands to a tab's controller or session holds the workspace through a `QPointer`. Swift's `async` calls take completion callbacks posted to the event loop; `close` takes one too, because its tests await it. The open panel takes one project (see the controller) and is titled `Open Project` like the controller's, where Swift's bare `NSOpenPanel` says `Open`: Qt's default for a folder panel is `Find Directory`. Managing ends last in every flow, after the sessions are free again, so that the workspace's last signal sees `canSwitch` as the call leaves it. A title cuts the suffix as Swift's `deletingPathExtension` does (`nameWithoutSuffix`: a name's first or last dot cuts nothing, so `.comp` stays whole), where `completeBaseName` would leave nothing. `samePlace` falls back on the absolute path (`absoluteFilePath`, which also cleans it) when a place no longer resolves, as `resolvingSymlinksInPath` does, because two missing places both canonicalise to the empty string. A loaded tab's default name is never shown (its title comes from the session's path, which the controller trims), so `loadProject` names it from the path as given. `receive` looks again every 30 ms while the workspace cannot switch, as Swift sleeps; `Task.isCancelled` has no twin. `receiveProviders` takes the drag's `QMimeData`: the layer-row type carries a UUID, judged by its text so that all zeros stays a valid id (a payload under another type is no row and goes to `ImageFileDrop` since 10.4, which reads nothing there); files and pictures arrive with 10.4. `copyLayers` (since 12.10b; `copyLayer` hands it one id, and a drop still copies row by row, as Swift's) bakes every mask that comes from outside the copy in one worker task (Swift awaits them one by one), then places the layers from a friend's seat in the target session, because Swift writes that document in place; `ProjectController::isProject` and `samePlace` are public statics so that both classes read paths alike. The anchor is the first layer's centre, or with several the centre of the box round every copied layer that is no folder; the `?? centre` fallback is dead (the first id is always among the copies) and is a checked read instead. The copies end up selected, the first active; an id the source tab lacks (two tabs of one project share ids, and the first holding the first id is the source) is left out of the selection, as Swift's `compactMap`.
|
||||
- `CanvasView` twins `EditorCanvas.swift`, split as `EditorCanvas.cpp` (the display) and `EditorCanvas+Navigation.cpp` (events, cursors, focus; keys in `EditorCanvas+Keys.cpp` since 12.2b). It follows Swift's `updateNSView`: `ContentView` calls `consumeFocusRequest` and `synchronizeDisplay` on every `changed()`, and a canvas alone follows nothing, as Swift's alone observes nothing. `paintEvent` clips to the dirty rect (the region's union, as `draw(_:)` receives it) and paints it through `AdjustmentSurface::draw`, so hand-blended modes and clip coverage meet an image-backed device and device-sized coverage costs only the dirty area; the smooth path draws its layers through a second surface, Swift's transparency layer, so blend modes never meet the checkerboard. `DisplayState` holds what Swift's holds (`brushRevision` came with 8.2, adjustments with 9.5, effects with 9.6c, the crop union in `renderBounds` with 10.1); with a live mask every layer counts, hidden ones included, since a hidden source still clips. The surround outside the document is the palette's `Base`, which the built-in dark sets to Swift's 0.105 gray and a light Omarchy theme to its light `darker_background`, so the welcome reads over it. The shadow is fourteen rings of black stacking to 35% at the edge (Qt has no blur); the border is antialiased, half in and half out, as CoreGraphics strokes it. A wheel without pixel deltas is read in lines, `QApplication::wheelScrollLines()` a notch, twelve points a line as Swift's non-precise scroll; `Qt::ZoomNativeGesture` is `magnify`. Geometry reaches the viewport from the event loop (`syncGeometry` is posted, as Swift defers past SwiftUI's layout) and notifies the session; `QWindow::screenChanged` stands in for `viewDidChangeBackingProperties`. A shown canvas takes the keys when a document exists and no sheet, importer or modal dialog holds them; `EditorSession::requestCanvasFocus` counts Swift's `canvasFocusRequest`, `NavigationToolHeader` asks on Return and Escape, and `ContentView` posts one when a document appears (Swift's `onChange`). Cursors: the open and closed hands for Hand and Space, a drawn magnifier for Zoom (Alt shows the minus), the I-beam for Type, the arrow for Idle and Move (the transform cursors arrive with 6.1), the cross for the rest; auto-repeat releases of Space and Alt are no release. Keys: the tool letters, Space, Escape and Return around a transform edit, arrows nudging in Move; the rest arrive with their tools. Swift's local monitors (`viewDidMoveToWindow`) are an application event filter installed while the canvas is shown: Shift-+ and Shift-− step the blend mode from any control of the window but a text field (`QLineEdit`) or the canvas holding open text (Swift's monitor stands aside for any `NSText` first responder, the inline text view included; since 8.9b), and Alt reaches the Zoom cursor from any window, an open menu's included (Swift's modifier monitor is application-wide); a blend key from another window, a dialog included, is that window's own.
|
||||
- `BrushRaster::visibleRect` and `AdjustmentSurface::draw` work on widget painters as on images: a widget's `QPainter::device()` measures itself in logical pixels where an image's measures device pixels, `combinedTransform()` carries the device pixel ratio, and `deviceTransform()` also the widget's offset in its window's backing store, whose image is the engine's `paintDevice()`. `HighDpiCanvasTests` holds this at ratio 2 through a `main` of its own that sets `QT_SCALE_FACTOR`.
|
||||
@@ -142,6 +142,7 @@ Platform adaptations, each forced by a missing Qt analog:
|
||||
- Scrubbable labels (12.12c1, Swift 1.2.11: `2d36d6e`, `680a522`): `UI/NumericScrub` twins Swift's modifier as a `QObject` living as its label's child and filtering its events; `scrubbable` and `unitSuffix(_:scrubValue:)` are its constructor on the label or unit (`Options`: sensitivity, range, optional step, value, set, onStart, onEnd). A left press on an enabled label, a double click's second press included, is taken (a disabled label's presses reach its filter before Qt drops them, so the filter asks), so the label keeps the drag past its edge; the first move off the press begins (Swift's `minimumDistance` of one, reached by any move of whole points), keeps the start value and calls `onStart`; each move sets the start plus the sideways distance (in the label's own points, which stay exact, where Qt's `notify` rounds global ones) times the sensitivity, snapped to the step when positive, then clamped; the left release calls `onEnd`. Swift's Int overload is a step of one. The cursor is the label's own `SizeHorCursor`: Qt keeps a grabbing widget's cursor through the drag and shows a disabled label's parent's, so Swift's hover and restore dance has no twin. Qt can lose a release: the left release is watched application-wide while pressed, and a move without the left button ends the drag (the label tracks the mouse, so such a move arrives). The tool bars scrub as Swift's: the Brush's Size, Hardness, Opacity or Strength and Smoothing, the Gradient's Opacity, the Magic Wand's Tolerance, Object Select's Edge, the Expand, Contract and Feather units (Expand's and Contract's rest with their rows; Feather's amount and unit stay enabled without a selection, its button alone resting, as Swift's, which the twin's amount had missed), the amount sheet's title (writing its input), the Shape's Width and Radius (a scrub writes its field's text over any typing, as a step does: `showValue` on `SelectionAmountField` and the Type bar's `TextStyleField`), the Type bar's size unit, Tracking and Leading (whole numbers), the Move bar's labels (whole numbers in Swift's new ranges: ±30,000, sides 1 to 30,000, Scale 0.1 to 30,000%, rotation ±360; the scrub writes its field's text over any typing, then the value, so an unfocused field shows what the session keeps; another active layer ends each field's drag under way, `TransformValueField::endScrub`, as it ends the opacity's), the zoom's unit (through the field's `step`) and the layer opacity (one opacity drag, `beginOpacityEdit` to `finishOpacityEdit`; another active layer ends a drag under way through `NumericScrub::end`, since the twin keeps the controls where Swift's `.id` remakes them, and later held moves wait for a fresh press). The sheets and panels follow in 12.12c2. `075a71b`'s narrowing fix in `AdjustPixels.c` is taken (the kernels match Swift's); its other changes and `725b934`'s are Swift's concurrency, plist and CI, with nothing to twin. Swift's `emptyCanvasLeavesTheScrubberCursorAloneAndRestoresTrackingForADocument` has no twin: the canvas never sets a global cursor, only its own. Equivalent mutant: removing the application filter when a drag ends (it then sees nothing to end). Tests: `NumericScrubTests`, `ScrubbableLabelsTests`, `ScrubbableTransformTests` (the Move bar, the zoom unit, the layer opacity), `ScrubFixtures.h`.
|
||||
- Scrubbable titles in the sheets and panels (12.12c2, the rest of `2d36d6e` and `680a522`): each title (or channel letter) is a `NumericScrub` with Swift's sensitivity, range and step, and every scrub replaces its field's typing (`setModified(false)`, as the row's slider does) before the sheet shows the value. Effects' rows scrub to the typed limit (Swift's `inputRange`, the stroke to 500); Hue/Saturation's titles take their range from Colorize, the Canvas and Image Size titles theirs and their sensitivity from the unit, Relative, Lock, Resample and the resolution, so the owner hands new limits to `NumericScrub::reshape` on every synchronize, as SwiftUI's next render hands the modifier new ones. Levels' captions move a tone a level a point and gamma a hundredth, within 0 to 255 and 0.1 to 9.99; a filter's title moves the field's last decimal a point (`1 / 10^decimals`) over the linear range, logarithmic sliders included, as Swift's; the colour picker's R, G and B move whole levels. Canvas Size snaps to whole units in pixels, percent, inches or centimetres, its range Swift's `scrubRange` (a locked ratio keeps both sides within 1 and 30,000; Relative counts from the current size); Image Size's titles rest while a physical unit has no positive resolution (Swift's `canScrubDimensions`), resampling spans Swift's pixel and hundred-megapixel limits, a print size moves a hundredth of an inch (0.0254 cm) a point down to the pixels at 9,600 per inch, and Resolution scrubs whole pixels per inch from 1 to 9,600, through the same `setResolution` a typed one takes (a physical size keeps, its pixels scale). Camera Raw's rows opt in through `CameraRawRow::Spec::scrub` (the sensitivity), scrubbing through the row's typed setter, unrounded and unclipped, as Swift's bindings do: the main rows a point per last decimal, the curve's amounts and Refine Saturation, the HSL family titles (the Color and Point Color rows stay passive, as Swift's `colorSlider` and `pointSlider`), Detail, Optics, Geometry and Calibration a unit a point; Color Grading's Blending and Balance do not scrub, as in Swift. A title's double-click reset runs first and lets the press through, so the second press still scrubs, as Swift's simultaneous gesture does. Known: a sheet destroyed while one of its fields holds the focus has that field commit into the sheet's destroyed members as Qt moves the focus; the app always hides a panel's content before deleting it, so the focus leaves first, and `ScrubFixtures`' typing helper leaves the field before the sheet goes. Equivalent mutant: the colour channels' step of one (the picker's colour is quantized to 8 bits, so a channel lands on whole levels anyway). Tests: `ScrubbableSheetsTests`, `ScrubbableCameraRawTests` (each row group's value, setter, sensitivity and limits; the passive titles).
|
||||
- Projects changed on disk (12.13a, Swift 1.3: `ed5ddea`, `cd4061a`, `cba6efc`): `IO/ProjectWatcher` twins Swift's class with a `QFileSystemWatcher` (inotify) on the package folder, its manifest and its images folder, where Swift opens kernel vnode sources: an event restarts a 300 ms single-shot delivery (Swift's coalescing) and re-arms every path from scratch each 100 ms, up to 20 times, until all three are watched (both timers are named children, `delivery` and `rearm`, so tests read them, and precise, since a coarse timer may fire 5% early where Swift's sleeps never end early; so is the controller's recheck); Qt's inotify engine subscribes to no `IN_MOVE_SELF`, where Swift's sources watch `.rename`, so a package exchanged for another one kept aside would pass unseen: the watcher also watches the package's parent folder and counts an event there only when the package path's device and inode differ from the ones it armed on, because a package swapped in (our store's `renameat2` exchange, another app's rename) retires the watched inodes. `IO/ProjectDigest` is a namespace: SHA-256 (`QCryptographicHash`) over the manifest's bytes, then each regular file in `images/` (links and folders skipped, names sorted by UTF-16 units) as its UTF-8 name and its size as a little-endian 64-bit number; a manifest that cannot be opened, or reads short of its size (a failed read, as Swift's `Data(contentsOf:)` throws), or an image gone mid-listing, throws, which the callers read as no digest (Swift's `try?`). `ProjectController+ExternalChanges.cpp` holds Swift's extension, its state `ExternalChangeState` (`ProjectController+ExternalChanges.h`) a public member, as Swift's: the watcher, the known digest, `checking`, `pending`, `saving`, the recheck `QTimer` (single-shot, a child of the controller), `recheckAttempt` and `reloadCount`. Swift's async loop runs as callbacks: each pass takes the pending flag, stops without a path, a document or while our own save writes, computes the digest off the UI thread (a `QFutureWatcher` child of the controller, so an answer dies with it), passes over a digest that matches, backs off at 250 ms × 2ⁿ (n to 7) while the project cannot start an operation, a transform is open or the workspace manages, reloads an unmodified document, and for unsaved work asks Swift's alert (Revert, Keep Mine) over the window, only for the front tab; without a window, or behind another tab, the change waits for `resumeExternalChangeCheck`, which the workspace's `select` calls. Keep Mine remembers the new digest. A reload (`reloadFromDisk`) marks the project busy, loads through the store off the UI thread, lands only on the same path with a document still open, calls `EditorSession::reloadProject`, remembers the digest as loaded and then counts the reload; a package that fails to load leaves the document alone and logs on `omaphoto.io`. `reloadProject` installs the snapshot and puts back the viewport, the collapsed folders that still exist, and the active layer with the selection's surviving members when the active layer still exists, as Swift's; history starts over. An open (the controller's own, which the workspace's tabs use) and a save remember the digest, then watch the package; a save marks `saving` from before its worker starts until the digest of what it wrote is remembered, or, when it fails, until its alert is answered (Swift's `defer`), so its own events pass; a new canvas and a close stop the watch. Swift's decoding from bytes in memory (`cba6efc`) has no twin: `QImageReader` decodes a PNG into memory the image owns, never mapped from the file, so a later save that replaces the file cannot reach an image kept for undo. Equivalent mutants: the try count's reset on a new event (a missing folder's creation is itself an event on the package, which re-arms), and the reload's two log lines. Tests: `ExternalChangeTests` (Swift's six; the revert question both ways, Keep Mine remembering, a background tab asking when brought forward, a busy project backing off then reloading, new canvas and close stopping the watch and its recheck, one check at a time, a check ending without a project or during our save), `ExternalChangeGateTests` (a failed save's guard, a managing workspace, the back-off's intervals, a load landing elsewhere, the reload's whole view and last signal, an unreadable manifest, Save As and open moving the watch, a second change while the question waits), `ExternalChangeFixtures.h`, `ProjectDigestRaceTests` (a binary of its own: `readdir` and `readdir64` take libc's place and remove an image as its folder's listing ends, so the digest's throw is ordered, not raced), `ProjectWatcherTests` (one report a burst, the quiet time from the last event, twenty re-arm tries, each watch speaking alone, an exchange keeping the old package, precise timing, re-arming and giving up, the digest's exact input and order, an unreadable manifest).
|
||||
- Editing while a project saves (12.13b, Swift 1.3: `940e188`): a save holds the tools only while it captures the document (`prepareSave`, Swift's: the revision, the snapshot, and the Save panel when there is no path yet or for Save As), then reserves the writer and only then frees the project, so no save asked in that signal slips in beside it, and writes in the background; a capture finds no document when Undo took it while the save waited (Swift's `guard let snapshot`), and the save answers false; `DocumentHistory::currentRevision` and `markSaved(QUuid)` twin Swift's, so only the captured version counts as saved: edits made while it writes stay modified, and undoing back to it is unmodified. Swift's `writing` task is `m_writing` with a list of waiters: `finishWriting` runs its callback at once when nothing writes, else when the write lands, each waiter posted to the event loop and passed through `finishWriting` again, so one writer runs at a time where Swift's store actor serialises the disk; `save` waits there before `begin`, so a second save writes what changed since, and `confirmReplacement` waits there first, so a close, a quit, a new canvas or an open never cuts a file short and asks only once the write is whole; an open also waits before its first read, and so does an external reload (12.13a's `reloadFromDisk`), since the twin's store is no actor and could read a package mid-save: a Revert answered while our save writes loads what we wrote. A digest that comes back while our save writes is looked at again once the write lands (pending, through `finishWriting`): after a save it matches what we wrote, after a failed one the change is taken up; Keep Mine remembers its digest only when no save landed while the question was up. A save queued behind a quit that already holds the tools is refused, as Swift's `begin` refuses it. A save that fails frees its waiters after its alert is answered. The refusal of a completed Save As name that exists sits in `prepareSave`, beside the panel. Swift has no tests for this commit: `ProjectSaveWhileEditingTests` pins it (edits while writing, a second save, a new canvas waiting, the panel alone holding the tools, a failed write, the last signal), and, over `tests/SaveHooks.h` (`fsync` slowed and counting overlapping writes, `renameat2` held at a package's swap, `readdir` and `readdir64` held at one folder, the store's loads counted from its log, every hold let go on cleanup), `ProjectSaveOrderTests` (a reopen reading nothing while the write is held, a Revert mid-save, the guard through the save's digest, Canvas Size and Image Size each committed before and after the held write lands, a digest out as our save begins, a failed save keeping an external change in line, a second save waiting through the first save's digest, Keep Mine after a save) and `ProjectSaveQueueTests` ( three saves one at a time, a save asked as the tools free, a quit waiting, a document undone away, a failed write holding its waiters, a reopen, a tab closing, a controller gone mid-write, an export beside a write).
|
||||
- Drops between projects: `ProjectWorkspace::draggedLayerSource` finds the tab whose document holds the dragged id (Swift reads the drag pasteboard; the twin reads the drag's `QMimeData`, which every drop target has), `canReceiveDrag(data, destination)` refuses only a layer's own tab, and `receiveProviders` copies several ids one after another, as Swift awaits each provider, so a two-row drop on the New button opens two tabs as Swift's does. `ContentView` takes such a drop over its whole area (`setWorkspace` hands it the workspace; `acceptsDrop` keeps Swift's guards: busy, the sheet, the importer, a rename; `levels` arrives with 9.x), shows the 3-point accent ring inside the canvas while targeted and hands the drop's document point on when it fell on the canvas. `ProjectTabButton` takes drops for its tab (accent fill and rim, "Add to <title>"), and `NewCanvasButton`, the toolbar's New canvas button with the action as its default, for a fresh tab ("Open in a new project tab"), both refusing an Alt drag, which duplicates in the panel; Swift's transient `NewTabDropSlot` in the strip has no twin, the New canvas button is `NewProjectDropTarget` already. Files and pictures arrived with 10.4.
|
||||
- `CanvasThumbnail` draws a layer or a mask inside the canvas's box at `backingScale` 2 (the panel's pixmaps carry that ratio); a mask's ground is `LayerMask::background`, white or black by its edge's majority, as the canvas treats it (Swift's `edgeTone` mean went with 12.12b). `LayerIcons` draws the panel's SF Symbols as paths, in one palette colour, like `ToolIcons`. `BlendModePicker` is a `QComboBox` whose popup previews the highlighted mode through `previewBlendMode` for the layer it opened on (Qt calls `hidePopup` before `activated`, so the preview, the target and the highlight are cleared from the event loop, after a choice had its chance) and refuses a choice once another layer is active. `LayerAppearanceControls` is Swift's blend and opacity row: the slider's press and release are one opacity drag (the release is watched application-wide while the slider is down, so a popup opened mid-drag cannot keep the step open), the percent field applies once through its `FocusOut`, which Return and Escape cause (Swift's `releasesFocusOnCommit`), and steps with Up and Down (Swift's `isFinite` test and the steps' clamp are left out: `setLayerOpacity` refuses a value that is no finite number and clamps the rest), and another active layer makes the controls anew, as Swift's `.id` does.
|
||||
- `ProjectWorkspaceView` is the `QMainWindow`: the front tab's `ContentView` as its central widget, made anew when another tab comes to the front, as SwiftUI's `.id` rebuilds it, and disabled while the workspace manages; the window holds the shown tab (`std::shared_ptr`) until the old editor is deleted, as Swift's views hold their session, so a replaced or closed tab's session never ends under its editor (`ProjectWorkspace::tab` is public for this); a `QToolBar` with the New canvas action, the `ProjectTabStrip`, a spacer and the four zoom actions, since SwiftUI's `.toolbar` modifier lives in `ContentView` but the toolbar belongs to the window. Its actions follow the front session's `changed()`, and on that signal the window syncs every tab's button as well, because a busy front tab dims the other tabs' buttons, which their own sessions' signals do not reach; the strip syncs its buttons on the workspace's signal, each button on its own session's. The window's title is the front tab's, with Qt's `[*]` placeholder, so `setWindowModified` can show the mark that `isDocumentEdited` shows; `setWindowFilePath` carries `representedURL`. The editor's own minimum size is cleared inside the window, which owns the 800 × 520 minimum for the whole, toolbar included. Closing the window ignores the event and asks the workspace (`closeWindow`), which confirms tab by tab and then closes the window again with the property `closeConfirmed` set, since the workspace may not include `UI/` headers; a confirmed close is accepted and the mark cleared, so a window shown again asks again. `ProjectTabStrip` is a `QScrollArea` whose buttons are kept for the tabs that stay, moved for the tabs that moved, made for new tabs and deleted for tabs that went; its row is sized to its tabs by hand (a resizable scroll area would squeeze every title below Swift's 35 points) and resized again whenever a title changes, and the front tab scrolls into view at once; it and its viewport fill no background, so the toolbar shows through as Swift's mask lets it (a stylesheet would do the same but freezes the widgets' palette, so a theme switch would pass them by); the unsaved dot is a character before the title. Files named on the command line go to `ProjectWorkspace::receive`, as Finder's Open With reaches the delegate. Layer drops on tabs and the New canvas button are 4.7's; file and picture drops arrived with 10.4.
|
||||
|
||||
Reference in new issue
Block a user