Reload a project changed on disk, fingerprinted by its manifest and image sizes (12.13a)

This commit is contained in:
雷电芽衣 committed 2026-09-29 20:13:20 -04:00
1 parent aa253d9715
commit 838d5b1459
20 files changed
+1388 -8

No files matched your search

+1
View File
@@ -141,6 +141,7 @@ Platform adaptations, each forced by a missing Qt analog:
- Masks painted anywhere (12.12b, Swift 1.2.11: `e753f26`, `8d2eade`, `2ffb092`, `a247676`, taken as their net change from 1.2.10): `BrushStroke` takes `growsMask`, which the Brush, the Gradient and fills (`applyPixelEdit`) pass, so on a mask their grid reaches the canvas as a pixel stroke's does; Spot Healing, Clone Stamp and the Smear keep the mask's grid. A solid placed mask (two pixels a side at most) is painted at a pixel per document pixel of its placement rather than grown from its pixel. `maskBackground` (`LayerMask::background` of the old mask's thumbnail, white or black, 1 without a mask) fills each new tile, the committed snapshot past its base and patches (`RasterSnapshot::fill`, drawn where neither reaches, the box of a mask grown by distant tiles holding such corners) and the asynchronous commit's context (`BrushCommit::Input::fill`). A mask stroke's snapshot keeps every touched tile (`committedBounds`). Committed, a mask grown past its old bounds, or placed already, takes the stroke's transform as its placement (`placedMask`, for the snapshot and the raster commit alike), keeping its place on the document while a linked one still moves with its layer. On the canvas an unplaced mask stroke draws in its possibly grown grid (`paintTransform`), a placed one in the layer's place, and a placed mask's preview and surface map through the stroke's `paintTransform`. `CanvasThumbnail::mask` fills past the mask with `LayerMask::background`, so a stroke touching the edge leaves no gray mean; the thumbnail's own `edgeTone` is gone. `8d2eade`'s canvas fix (the distortion's effects asked with the mask as stored) needs no twin: the twin asks for a layer's effects once, with the mask as shown (9.6c), and its y-flip in `LayerEffectsSurface` has no Qt counterpart. The linked mask surface's white fill (12.12a) stays dead: the layer's pixels lie only within the old mask's rect, however far the grid grows. Equivalent mutant: `placedMask`'s `mask.placement` term (a placed mask keeps its placement through `replacing`, and its transform over unchanged bounds is that placement). Tests: `MaskPaintAnywhereTests` (the brush grows a mask and keeps it linked and in place, a hide-all mask's new area hides, the Gradient and a fill grow it, the Smear's blur does not, a solid placed mask's grid (two pixels a side, a box not square, grown and not), the snapshot's and the commit's fill in uncovered corners, a grown stroke drawn in place, a grown placed mask's tiles mapped under effects), `CanvasThumbnailTests` (Swift's edge case), and `MaskEffectsCanvasTests`' finer placed mask through the stroke's transform.
- 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).
- 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.