diff --git a/AGENTS.md b/AGENTS.md index f547209..d5b93b6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,7 +86,7 @@ Platform adaptations, each forced by a missing Qt analog: - Desktop integration (11.2): the app id is `io.github.ZacharyZhang_NY.OmaPhoto` (Compositor's was its bundle identifier, `com.wonderassembly.compositor`), which names the desktop entry, the icon and the MIME package, and which `main` gives Qt as the desktop file name, so a Wayland compositor matches the window to its entry and icon; on X11 Qt 6.4 names the window class after the application (`omaphoto`, `OmaPhoto`), so the entry's `StartupWMClass` is `OmaPhoto`. `packaging/io.github.ZacharyZhang_NY.OmaPhoto.desktop` launches `omaphoto %F` (files named at launch reach `ProjectWorkspace::receive`, as Finder's Open With reaches the delegate) and lists Swift's two document types: its projects (`application/x-compositor-project`) and JPEG, PNG, HEIC and TIFF. `io.github.ZacharyZhang_NY.OmaPhoto.xml` declares the project type for shared-mime-info, a folder (`inode/directory`) named `*.comp`, commented "OmaPhoto Project", where Swift says "Compositor Project"; a file manager may still open such a folder as a folder, since a project is a directory on Linux. `cmake --install` lays down the app, the entry, the MIME package, Compositor's app icon (`packaging/icons`) at six hicolor sizes (16 to 512) and U²-Net's licence under `share/licenses/omaphoto`; `OMAPHOTO_MODEL`, a path, installs the model as `share/omaphoto/u2net.onnx` whatever its own name, the file itself when given a link (`file(REAL_PATH)`), where `SubjectRemoval` looks, and packages pass it (11.3). A direct install (no `DESTDIR`) runs `update-mime-database` on its prefix, required, so the project type registers at once; a staged install leaves that to the package's hooks. `DesktopIntegrationTests` reads both files, stages an install and compares the whole file list (the model included when the build was configured with one, read from its cache), and installs directly with a stand-in `update-mime-database` first on the path and any `DESTDIR` of the caller cleared; the call in `main` is not under test. - The acceptance run (11.5): `AcceptanceTests`, `AcceptanceColorTests` and `AcceptanceProjectTests` drive the app's own window as `main` builds it (`AcceptanceFixtures.h`'s `App`: an `OmarchyTheme` on a folder of the test's own, the workspace, the window at 1180 × 780), one scenario for each row of the README's feature list, through menu entries, canvas gestures, sheets and the real panels (`DialogDesk`), and check pixels, layers, sizes and files; `AcceptanceNewTests`, `AcceptanceEditTests` and `AcceptanceFinishTests` (13.19h) do the same for each row new in 1.4.5. With `ACCEPTANCE_SHOTS` naming a folder, each scenario saves the window with its shown floating panels drawn over it at their places (offscreen draws no title bars), after 300 ms of events, since posted geometry and previews land from the event loop; `docs/acceptance/` holds the committed shots and `docs/acceptance.md` the table. A fix the run found: `~CompositorMenus` disconnects from `QApplication::focusChanged` first, because its Keyboard Shortcuts panel, a member, hides as it goes and moves the focus into the half-destroyed menus (quitting with that panel active crashed); `KeyboardShortcutsSheetTests::theWindowGoesWhileThePanelHoldsTheKeys` holds it. So does `~ProjectWorkspaceView` with its session watch: a floating panel whose field holds typing (the colour picker, Levels, Hue/Saturation, a filter) commits as it hides, and the session's signal reached the window's toolbar and tab strip, already gone; `WindowTeardownTests` holds all four. - Packages (11.3): one CMake install serves every package. It lays down the app, its entry, MIME package, icons and both licences (the app's MIT `LICENSE` and U²-Net's, under `share/licenses/omaphoto`), and `OMAPHOTO_MODEL` adds the model. `OMAPHOTO_BUNDLE_ORT` (off by default) installs ONNX Runtime beside the app in `lib/omaphoto` (under the library folder) with the app's run path set there, and Microsoft's `LICENSE` and `ThirdPartyNotices.txt` with it (`OMAPHOTO_ORT_NOTICES`, where the images keep them from the release; a configure that cannot find them fails). Every package bundles (11.4d, issue #5): ONNX Runtime stamps each release's symbols with its version (`VERS_1.20.1`), so a package linked against a system's runtime refuses to install, or to start, once that system updates it. The DEB, the RPM and the Arch package carry Microsoft's 1.21.0 build, pinned by checksum, which the dev, Fedora and Arch images install into `/usr/local` too, so the tests run against the runtime that ships; the flake builds from nixpkgs' own, since Nix rebuilds its closure with it. `DesktopIntegrationTests` expects the runtime (in the configured library folder), its version link and its notices in a bundling build, as it expects the model in a build given one, reading what CMake itself evaluated through compile definitions (both checked by hand in a second build). CPack makes the DEB (Ubuntu) and the RPM (Fedora), each on its own system: the DEB's library dependencies come from `dpkg-shlibdeps`, which finds the bundled runtime through the run path, and it names what no library does (`libheif-plugin-libde265` for HEVC, `qt6-image-formats-plugins` for TIFF, `shared-mime-info`, `hicolor-icon-theme`); the RPM asks for `qt6-qtimageformats`, `shared-mime-info` and `hicolor-icon-theme`, its libraries found by rpmbuild but the bundled runtime left out of what it provides and requires (`__provides_exclude_from`, `__requires_exclude` in `CPACK_RPM_SPEC_MORE_DEFINE`), and suggests RPM Fusion's `libheif-freeworld`, since Fedora's own libheif decodes no HEVC. Packages stage their install (`DESTDIR`), so their systems' hooks and triggers refresh the MIME and icon caches. `Dockerfile.dev` gains `dpkg-dev`, `file` and `fakeroot` for the DEB. `packaging/arch/PKGBUILD` builds this checkout for Arch and Omarchy (`makepkg` there), fetching the model and Microsoft's runtime by their checksums; its `prepare()` lays the runtime out as that build's CMake config expects (`lib64`, `include/onnxruntime` beside `lib/cmake`) and the package bundles it. `flake.nix` sits at the repository root; it builds the app alone and installs staged (`DESTDIR=/`) so the store holds no MIME cache. The DEB was built and read back in the dev image; the RPM, the PKGBUILD and the flake are built on their own systems in 11.4. -- Distro checks (11.4): `scripts/distro-check.sh arch|fedora|fedora44|nixos|resolute` builds `scripts/distro//Dockerfile` (for `resolute`, `Dockerfile.dev` on Ubuntu 26.04; `fedora` is 43 since 11.4d, `fedora44` the same file on 44 since 12.16) and runs dev.sh's configure, build and full test run in it, warnings as errors, as uid 1000 (root ignores the read-only folders and unreadable files the tests make), the tree mounted read-only and `HOME` and `TMPDIR` on a tmpfs (a sync on the container's overlay waits on the host's disk); NixOS builds the flake's package (nixpkgs pinned by the committed `flake.lock`) as root, since Nix writes its store, then runs the tests in its `devShells.default` as uid 1000 (`chroot --userspec`, with `HOME` and `TMPDIR` put back, since `nix develop` points `TMPDIR` at a folder root made); the shell sets the plugin path, the model's data path with shared-mime-info's beside it (a NixOS system has the database in its profile through `xdg.mime.enable`; without it Qt knows no camera RAW type, missed until 12.16) and a fontconfig file of DejaVu with fontconfig's own rules from the store, since NixOS keeps them in `/etc/fonts/conf.d`, which a container lacks. Every image carries DejaVu Sans, Serif and Mono, the faces the tests name (Fedora splits them into three packages). Ubuntu 24.04 is the dev image itself. What newer Qt and GCC asked for: `QImage::flipped` from Qt 6.9 where 6.4 has `mirrored`; `Q_OBJECT` on every class a test finds with `findChild` (Qt 6.11 refuses one without); `DistortWarp` projects points itself (`project` in `Distort.cpp`), since `QTransform::map` clamps w for points past the horizon from Qt 6.10 (6.10.2 and 6.11.2 probed; 6.4 divides), where Swift divides whatever its sign, and `DistortTests` pins it past the horizon (on 6.4 the two agree, so a mutant there is equivalent); a local pragma for GCC 16's false `-Wmismatched-new-delete` in `DownsampleTests`' replaced operators. Tests follow the systems where only a message or a count differs: libheif 1.23's longer messages, Qt 6.9's TIFF plugin logging libtiff's errors as criticals, fonts named rather than counted, the Type icon's letters in the system's own sans (Fedora's is Noto: the icon sheet compares them only under DejaVu Sans), and `QT_FORCE_STDERR_LOGGING` for the launched app, since a journald-built Qt logs to the journal when stderr is no console. The five starvation tests place their failure by Qt 6.4's own allocations and skip on any other Qt (`placesStarvation` in `AddressSpaceLimit.h`); the limit itself holds there. `ProjectExportFailureTests` opens a panel and an alert once in `initTestCase`, before any limit, as the app's window has before any export: a snapshot that fails opens its alert while the limit holds, and on every system that alert was the process's first font lookup and the panel's first open, costs the app never meets there. In the NixOS shell, which reads fontconfig's whole `conf.d` without a cache, the lookup's failed allocation segfaulted inside fontconfig (`FcHashTableFind`, found under gdb); on the dev image a panel test run alone ran out inside the panel's first open. `theJPEGRenderTakesTheSnapshotWithoutACopy` holds the JPEG render's snapshot to a move under a 2 MB limit, as `thePanelOpensWithoutCopyingTheSnapshot` holds the PNG panel's. The flake leaves out `dev.sh`'s `build/`, which nixpkgs' CMake hook would take for its own build folder, and the `Compositor-Ref` spec, which a `path:` flake would copy whole. +- Distro checks (11.4): `scripts/distro-check.sh arch|fedora|fedora44|nixos|resolute` builds `scripts/distro//Dockerfile` (for `resolute`, `Dockerfile.dev` on Ubuntu 26.04; `fedora` is 43 since 11.4d, `fedora44` the same file on 44 since 12.16) and runs dev.sh's configure, build and full test run in it, warnings as errors, as uid 1000 (root ignores the read-only folders and unreadable files the tests make), the tree mounted read-only and `HOME` and `TMPDIR` on a tmpfs (a sync on the container's overlay waits on the host's disk); NixOS builds the flake's package (nixpkgs pinned by the committed `flake.lock`) as root, since Nix writes its store, then runs the tests in its `devShells.default` as uid 1000 (`chroot --userspec`, with `HOME` and `TMPDIR` put back, since `nix develop` points `TMPDIR` at a folder root made); the shell sets the plugin path, the model's data path with shared-mime-info's beside it (a NixOS system has the database in its profile through `xdg.mime.enable`; without it Qt knows no camera RAW type, missed until 12.16) and a fontconfig file of DejaVu with fontconfig's own rules from the store, since NixOS keeps them in `/etc/fonts/conf.d`, which a container lacks. Every image carries DejaVu Sans, Serif and Mono, the faces the tests name (Fedora splits them into three packages). Ubuntu 24.04 is the dev image itself. What newer Qt and GCC asked for: `QImage::flipped` from Qt 6.9 where 6.4 has `mirrored`; `Q_OBJECT` on every class a test finds with `findChild` (Qt 6.11 refuses one without); `DistortWarp` projects points itself (`project` in `Distort.cpp`), since `QTransform::map` clamps w for points past the horizon from Qt 6.10 (6.10.2 and 6.11.2 probed; 6.4 divides), where Swift divides whatever its sign, and `DistortTests` pins it past the horizon (on 6.4 the two agree, so a mutant there is equivalent); a local pragma for GCC 16's false `-Wmismatched-new-delete` in `DownsampleTests`' replaced operators; from Qt 6.10 a `QComboBox` emits `highlighted` when its current item changes with the popup closed and closes on Escape without calling `hidePopup` (both probed in 13.19i), so `TypeFontPicker` and `BlendModePicker` preview only while the popup shows and end the preview on the popup window's `Hide` event, which every version sends on every close (on 6.10 a programmatic highlight had saved an original that a later close put back over typed text, and Escape had kept the preview). Tests follow the systems where only a message or a count differs (`GridSettingsSheetTests` sets an English default locale, since Qt 6.10's C locale groups no digits): libheif 1.23's longer messages, Qt 6.9's TIFF plugin logging libtiff's errors as criticals, fonts named rather than counted, the Type icon's letters in the system's own sans (Fedora's is Noto: the icon sheet compares them only under DejaVu Sans), and `QT_FORCE_STDERR_LOGGING` for the launched app, since a journald-built Qt logs to the journal when stderr is no console. The six starvation tests place their failure by Qt 6.4's own allocations and skip on any other Qt (Dither's since 13.19i, where Qt 6.10 converted on threads the limit starved first) (`placesStarvation` in `AddressSpaceLimit.h`); the limit itself holds there. `ProjectExportFailureTests` opens a panel and an alert once in `initTestCase`, before any limit, as the app's window has before any export: a snapshot that fails opens its alert while the limit holds, and on every system that alert was the process's first font lookup and the panel's first open, costs the app never meets there. In the NixOS shell, which reads fontconfig's whole `conf.d` without a cache, the lookup's failed allocation segfaulted inside fontconfig (`FcHashTableFind`, found under gdb); on the dev image a panel test run alone ran out inside the panel's first open. `theJPEGRenderTakesTheSnapshotWithoutACopy` holds the JPEG render's snapshot to a move under a 2 MB limit, as `thePanelOpensWithoutCopyingTheSnapshot` holds the PNG panel's. The flake leaves out `dev.sh`'s `build/`, which nixpkgs' CMake hook would take for its own build folder, and the `Compositor-Ref` spec, which a `path:` flake would copy whole. - Spot Healing (8.4): `HealPixels.c` compiles in place beside the other kernels. `SpotHealingMode`, `allSpotHealingModes` and `rawValue` (Swift's names) sit in `BrushStroke.h`, and `BrushSettings` gains `healing` and `healingMode`. While the stroke lasts its tiles show Swift's wash, gray 0.12 at 45% through the coverage, never erasing. `heal` takes the painted bounds from the kernel's `heal_coverage_bounds`, draws the source into a region about three spots wider, and hands the kernel the coverage packed row by row, as Swift's gray context holds it (the twin copies each tile's rows where Swift draws each coverage image); the seed is `QRandomGenerator::global()`, Swift's `UInt32.random`. Each touched tile is then its base with the healed region copied over, blended with the base through `PixelAdjust::blend` under a selection, where Swift clips the context. The kernel's failure is `ProjectError(tooLarge)`; pixels or packed coverage that cannot be allocated are `ExportError(render)`, as Swift's contexts. The session's `beginBrush` takes Spot Healing on pixels alone, never on a mask, as Swift's guard; `finishBrushImmediately` heals before the commit, one `Spot Healing` step; `spotHealingMode` is a getter and a setter that notifies; Tab steps the type. `isBrushTool` is Swift's four tools: the circle, the right-drag tip, the brackets, the digits and `BrushControls` serve each, and a press with Clone Stamp or the Smear begins nothing until 8.5 and 8.6 bring them. `ContentView` keeps one bar across the four, as Swift's single `isBrushTool` branch keeps its view; the bar titles itself by tool (Spot Healing, Clone Stamp, Smear, Eraser, Brush), shows Paint and Erase for the Brush alone and the three types for Spot Healing alone, and names opacity Strength for the Smear; the clone's Aligned and Sample arrive with 8.5, the Smear's modes with 8.6. Equivalent mutant: the healed tile starting from its context rather than its base, since the region holds the whole wash and the copy replaces it. - Clone Stamp (8.5): `Document/CloneStamp.h` holds `CloneSettings`, `CloneStamp.cpp` Swift's extension: `setCloneSource` (a point that is no number is refused; a new source restarts the alignment), `cloneStrokeOffset`, `cloneSamplePoint` and `cloneSample`, whose context that cannot be allocated logs on `omaphoto.app` where Swift's `try?` is silent. The session keeps `cloneSource`, `cloneSettings` (a setter that notifies), `cloneOffset` and `parkedBrushTips` by `tipFamily` (Clone Stamp 1, the Smear 2, the rest 0; the two parked tips start at Swift's 40, 0, 1). `selectTool` parks size, hardness and opacity when the family changes and reads the tip it restores with `at`: the session starts in family 0 and leaving a family parks it, so Swift's `if let` always finds one. `beginTransform` and Transform Selection set the Move tool directly, as Swift's do, and park nothing. `beginBrush` takes Clone Stamp on pixels alone, asks for a source first (`brushError`: "Alt-click where Clone Stamp should copy from first."), samples the document as the stroke starts and fixes the offset; Tab toggles the sample; the step is `Clone Stamp`. `BrushStroke::Clone` is Swift's tuple. `publish` makes each tile's view of the sample on the calling thread (`clonePixels`): under a scale or turn QPainter draws it bilinear, Swift's medium quality; a whole translation copies it; a fractional one is drawn at the floor and shifted by `BrushRaster::shiftedByFraction`, since QPainter snaps an unscaled image. `fillClone` then composites it source-over through coverage × opacity × selection, each byte rounded once, where Swift clips the context to the coverage. Swift's `!isMask || isBlur` holds since 8.6: a mask takes a clone only from Blur. The bar shows Aligned (a `QCheckBox` written on `clicked`, so the session's own changes write nothing back), This Layer and All Layers, and "Alt-click to set the source" in `PlaceholderText` while there is no source. On the canvas an Alt-press with Clone Stamp sets the source and paints nothing; Swift's cursor refresh there is dead (Alt is held, so the crosshair stays) and left out. With a source the pointer hides unless Alt or Space is held. `updateBrushCursor` gives the overlay the source crosshair in view points and, between strokes and without Alt, `clonePreview` (the source around the stamped point at screen resolution, at most 1024 pixels, kept under Swift's key; Swift's `.medium` there is overridden by `LayerRenderer`'s own quality and left out) with `cloneTip` (one click painted by the brush engine, kept by size and hardness). `BrushCursorOverlay` gains `marker`, `preview`, `previewOpacity` and `tip`, images compared by `cacheKey` as Swift's `!==`; under an antialiased ellipse clip at the brush's opacity it draws the preview and cuts it to the tip inside `AdjustmentSurface::draw`, Swift's transparency layer, so the tip applies at the drawing's resolution however far a capped preview is enlarged, and draws the crosshair white 3 under black 1 with round caps. A modifier change refreshes the brush cursor, as Swift's monitor does. Equivalent mutants: `displayedTransform` in `cloneSample` and `clonePreview` (it differs from the stored transform only in a transform edit, which leaves Clone Stamp for Move, or a filter edit, which refuses strokes; the preview would show it only with a filter sheet open over Clone Stamp). - The Smear (8.6): `SmudgeLiquify.h/.cpp` hold `BlurToolMode` and `WarpStroke`, Swift's float arithmetic to the letter (the positive guards of `weight` included) on a document-sized copy drawn through the layer's displayed transform; `image()` is that copy itself, where Swift snapshots it after each append, since the twin reads it only between appends. `beginWarp` and `finishWarp` twin the extension: a warp takes pixels alone (`brushError`: "Smudge and Liquify work on a layer's pixels, not its mask."), and the commit paints the copy along the stroke with a hard tip four pixels wider, `replacesWithClone` so clear pixels clear, one step named by the mode. `BlurTool.cpp` holds `blurSample`, in the stroke's own grid since 13.11 and in pieces since 13.16 (see those bullets). Core Image's Gaussian is `PixelAdjust::gaussianBlur`: separable, weights exp(−i²/2σ²) over three sigmas, normalised, in float on the stored premultiplied values (Swift's Core Image context is unmanaged), each byte rounded once, rows in parallel. Where Core Image approximates a large blur, the twin's is exact. `BrushStroke` gains `isBlur` (a mask takes a clone only from Blur), `replacesWithClone` (a copy by coverage: `fillClone` keeps 1 − coverage of the old pixel) and `editName` (the step's name when set). `clonePixels` resamples every clone, a gray one too, into premultiplied colour, so a sample's edge keeps the share of the pixel the image covers, and `fillClone` composites that alpha into a mask as CoreGraphics draws a gray image's edge: a white mask half a pixel past the canvas stays white. The session keeps `blurMode` and `warpStroke`; `beginBrush` hands Liquify and Smudge to `beginWarp`; `continueBrush`, `cancelBrush` and `finishBrushImmediately` take the warp first; exactly Swift's six places read it: `canEditLayers`, `canUseHistory`, `canStartProjectOperation`, `selectLayer`, `selectTool`, `cycleToolMode` (`selectLayers` and `selectLayerTarget` read the brush stroke alone, as Swift's). Tab steps the Smear's modes. The bar shows the three modes with Swift's help; the status line says "Drag to soften", "Drag to smudge" or "Drag to push pixels" (`hint` takes the Smear's mode). On the canvas the warp's copy stands in for its layer across the canvas, through the layer's mask at 2048, its opacity and blend mode, and a step repaints the view (no dirty rect, as Swift's); drags, releases, Escape, wheels, pinches and the tip drag treat a warp as a stroke. Equivalent mutants: `displayedTransform` in `beginWarp` (painting refuses transform and filter edits), the radius rounded to nearest (pixels past its floor lie outside the brush), `u >= h` for `u > h` (weight 1 either way), a warp without dabs going on to commit (it appends nothing), copy mode weighing by coverage times opacity (a warp commits at full opacity), `fillClone`'s format guard (a clone is always sampled in colour), and the Gaussian's branch-free interior moved by a pixel (it reads one tap weighing under 1% of the centre). diff --git a/TASKS.md b/TASKS.md index b428056..8c4d015 100644 --- a/TASKS.md +++ b/TASKS.md @@ -497,7 +497,10 @@ Reviews for this section ran on codex `gpt-6.1-sol` at `max` (agent `codex-sol`) - 13.19h review: round 1 FAIL: the font scenario emitted the menu's signals itself, Dither and Blur's Radius were set past their controls, the ASCII check took blocks for letters, the mask-alone view was not looked at, and seven rows said more than their scenarios (my rewrite of the table had not landed). Fixed: the font popup opened, hovered and clicked (`popupRow`, `choose`); Dither's style, characters and text size through its sheet and OK, Camera Raw through OK, the Radius typed in the bar, Ctrl+Shift+G and Ctrl+Z as keys, Color Range's OK, Save As from the menu; ASCII of \"@\" must keep holes (inked share under three quarters) and grow about twice at 28; the mask-alone canvas checked black and white; each row says what its scenario does. The reviewer's mutants (no popup, no style handler, blocks for letters, no mask-alone drawing) and a Radius field that writes nothing are each caught. - 13.19h round 2: FAIL: the committed Camera Raw shot showed the zero-exposure preview, since the scenario waited for any preview. It now waits for the preview whose ground has brightened; the shot is retaken. - 13.19h round 3: PASS. - - [ ] 13.19i Distro checks: arch, fedora, fedora44, nixos, resolute. + - [x] 13.19i Distro checks: arch, fedora, fedora44, nixos, resolute. + - 13.19i work: first run failed on every newer Qt. Arch (6.11), Fedora 43 and 44, Ubuntu 26.04 (6.10) and NixOS (6.11) found two real bugs in the pickers: Qt 6.10 emits `highlighted` when a closed combo box follows its value, so the font menu started a preview as the caret moved and a later close put back text typed since; and it closes the popup on Escape without `hidePopup`, so Escape kept a font or blend preview. Fixed by previewing only while the popup shows and ending on the popup window's `Hide`, tested on both Qts with mutants caught. Two test fixes: `GridSettingsSheetTests` sets an English default locale (6.10's C locale groups nothing), and Dither's starvation test skips off Qt 6.4 as the other five do. + - 13.19i second run: all five pass, 334 of 334 each; the full suite passes on the dev image. + - 13.19i review: round 1 PASS (the Qt behaviour probed on 6.4.2, 6.10.2 and 6.11.2, six mutants caught). - [ ] 13.19j Tag `v1.4.5`; the release workflow publishes every package. - [ ] 13.19k Live installs from GitHub in fresh containers. - [ ] 13.19l The acceptance checklist. diff --git a/docs/acceptance.md b/docs/acceptance.md index acc7e6a..da6e841 100644 --- a/docs/acceptance.md +++ b/docs/acceptance.md @@ -31,7 +31,7 @@ The screenshots come from Qt's offscreen platform, so floating panels appear wit | Keep working while a project saves | Another layer is added while a save runs. The file holds the state the save began with. | — | `ProjectSaveWhileEditingTests` | pass | | Reload a project changed on disk | With unsaved edits open, another program renames a layer on disk. The app's alert asks, Revert is chosen, and the saved state shows with the new name. | [15](acceptance/15-shortcuts.png) | `ExternalChangeTests` | pass | | Follows the Omarchy theme | Starts in the built-in dark. A Catppuccin Latte `colors.toml` written while the app runs retints the window. | [16](acceptance/16-omarchy-light.png) | `OmarchyThemeTests` | pass | -| Builds and tests on each system | The full suite runs on Ubuntu 24.04 (Qt 6.4.2), Ubuntu 26.04, Arch, Fedora 43 and 44, and NixOS. Warnings count as errors. | — | `scripts/distro-check.sh` | pending | +| Builds and tests on each system | The full suite, 334 binaries, passes on Ubuntu 24.04 (Qt 6.4.2), Ubuntu 26.04 (6.10.2), Arch (6.11.2), Fedora 43 and 44, and NixOS (6.11.2). Warnings count as errors. | — | `scripts/distro-check.sh` | pass | | Packages and install scripts | Each install script runs from GitHub in a fresh container. On Ubuntu 24.04 and 26.04, Fedora 43 and 44 and Arch, it checks its package against `SHA256SUMS`, installs it and starts 1.4.5. On NixOS, `nixos.sh` resolves `v1.4.5`, builds its flake and starts 1.4.5. Debian 13 is refused, as intended. | — | `DesktopIntegrationTests`, `.github/workflows/release.yml` | pending | ## New in 1.4.5 diff --git a/src/UI/BlendModePicker.cpp b/src/UI/BlendModePicker.cpp index 0bc0ac3..4567324 100644 --- a/src/UI/BlendModePicker.cpp +++ b/src/UI/BlendModePicker.cpp @@ -1,4 +1,6 @@ #include "UI/BlendModePicker.h" +#include +#include BlendModePicker::BlendModePicker(EditorSession &session, QWidget *parent) : QComboBox(parent), m_session(session) { @@ -13,6 +15,8 @@ BlendModePicker::BlendModePicker(EditorSession &session, QWidget *parent) : QCom connect(this, &QComboBox::highlighted, this, &BlendModePicker::highlight); connect(this, &QComboBox::activated, this, &BlendModePicker::choose); connect(&m_session, &EditorSession::changed, this, &BlendModePicker::synchronize); + // Qt 6.10 closes on Escape without calling hidePopup. + view()->window()->installEventFilter(this); synchronize(); } @@ -24,9 +28,10 @@ void BlendModePicker::showPopup() QComboBox::showPopup(); } -void BlendModePicker::hidePopup() +bool BlendModePicker::eventFilter(QObject *watched, QEvent *event) { - QComboBox::hidePopup(); + if (watched != view()->window() || event->type() != QEvent::Hide) + return QComboBox::eventFilter(watched, event); m_tracking = false; // A click closes the menu, then chooses: clear after. QMetaObject::invokeMethod(this, [this] { @@ -34,6 +39,7 @@ void BlendModePicker::hidePopup() m_layerID = std::nullopt; m_highlighted = std::nullopt; }, Qt::QueuedConnection); + return false; } void BlendModePicker::synchronize() @@ -47,6 +53,9 @@ void BlendModePicker::synchronize() void BlendModePicker::highlight(int index) { + // Qt 6.10 signals too, as the closed menu follows. + if (!m_tracking) + return; const std::optional mode = layerBlendMode(itemText(index)); if (mode) m_highlighted = mode; diff --git a/src/UI/BlendModePicker.h b/src/UI/BlendModePicker.h index be1aef7..ed87589 100644 --- a/src/UI/BlendModePicker.h +++ b/src/UI/BlendModePicker.h @@ -9,7 +9,10 @@ public: explicit BlendModePicker(EditorSession &session, QWidget *parent = nullptr); void showPopup() override; - void hidePopup() override; + +protected: + // The popup closed, however Qt closed it. + bool eventFilter(QObject *watched, QEvent *event) override; private: void synchronize(); diff --git a/src/UI/TypeControls.cpp b/src/UI/TypeControls.cpp index f7b6246..1812dae 100644 --- a/src/UI/TypeControls.cpp +++ b/src/UI/TypeControls.cpp @@ -1,5 +1,6 @@ #include "UI/TypeControls.h" #include +#include #include #include #include @@ -147,18 +148,22 @@ TypeFontPicker::TypeFontPicker(std::function { setAccessibleName(QStringLiteral("Font")); prepareOwnNames(); - // Only a face previews; (Multiple) leaves the last one showing. + // An open menu's face previews; Qt 6.10 signals closed too. connect(this, &QComboBox::highlighted, this, [this](int index) { - if (!isMultiple(index)) + if (view()->isVisible() && !isMultiple(index)) m_preview(PreviewStep::show, itemText(index)); }); + // Qt 6.10 closes on Escape without calling hidePopup. + view()->window()->installEventFilter(this); } // Qt reports a choice after the close: the revert waits. -void TypeFontPicker::hidePopup() +bool TypeFontPicker::eventFilter(QObject *watched, QEvent *event) { - QComboBox::hidePopup(); + if (watched != view()->window() || event->type() != QEvent::Hide) + return QComboBox::eventFilter(watched, event); QMetaObject::invokeMethod(this, [this] { m_preview(PreviewStep::revert, QString()); }, Qt::QueuedConnection); + return false; } bool TypeFontPicker::isMultiple(int index) const diff --git a/src/UI/TypeControls.h b/src/UI/TypeControls.h index fdd66af..5b32d5c 100644 --- a/src/UI/TypeControls.h +++ b/src/UI/TypeControls.h @@ -45,14 +45,14 @@ public: // Shows `name`, or (Multiple) when empty; an open menu stays. void sync(const QString &name); void showPopup() override; - // Closed, the text goes back; a choice already kept it. - void hidePopup() override; // Swift's isMultiple: the top item that is no font. bool isMultiple(int index) const; protected: // A long name is cut short, never widening the control. void paintEvent(QPaintEvent *event) override; + // Closed, the text goes back; a choice already kept it. + bool eventFilter(QObject *watched, QEvent *event) override; private: void showMultiple(); diff --git a/tests/AcceptanceEditTests.cpp b/tests/AcceptanceEditTests.cpp index 0fbba75..c565f7a 100644 --- a/tests/AcceptanceEditTests.cpp +++ b/tests/AcceptanceEditTests.cpp @@ -69,7 +69,7 @@ void AcceptanceEditTests::textPaintingSmearAndMasks() // Pointing at a face previews it; Escape puts it back. QTest::mouseMove(faces->view()->viewport(), popupRow(*faces, mono)); QTRY_COMPARE(app.session().textDraft().value().style.fontNameAt(6), mono); - QTest::keyClick(faces->view(), Qt::Key_Escape); + QTest::keyClick(faces->view()->window()->windowHandle(), Qt::Key_Escape); QTRY_VERIFY(app.session().textDraft().value().style.fontNameAt(6) != mono); choose(*faces, mono); QTRY_COMPARE(app.session().textDraft().value().style.fontRuns.value(), (std::vector{{6, 5, mono}})); diff --git a/tests/BlendModePickerTests.cpp b/tests/BlendModePickerTests.cpp index f11616c..c2959b7 100644 --- a/tests/BlendModePickerTests.cpp +++ b/tests/BlendModePickerTests.cpp @@ -20,6 +20,7 @@ private slots: void hoveringTriesAModeOnAndChoosingKeepsIt(); void aMenuClosedWithoutAChoiceLeavesTheLayerAlone(); void theMaskButtonAddsAWhiteMaskOnce(); + void escapeAndAClosedHighlightLeaveNoPreview(); }; void BlendModePickerTests::theMenuListsEveryModeAndFollowsTheActiveLayer() @@ -165,5 +166,31 @@ void BlendModePickerTests::theMaskButtonAddsAWhiteMaskOnce() QVERIFY(button.isEnabled()); } +void BlendModePickerTests::escapeAndAClosedHighlightLeaveNoPreview() +{ + EditorSession session; + session.createDocument(4, 4); + session.insert(white()); + BlendModePicker picker(session); + picker.show(); + QVERIFY(QTest::qWaitForWindowExposed(&picker)); + // Qt 6.10 signals as the closed menu follows the layer. + emit picker.highlighted(3); + QVERIFY(!session.blendPreview().has_value()); + // So a key choice when closed keeps its mode. + emit picker.activated(8); + QCOMPARE(session.activeLayer().value().blendMode, LayerBlendMode::screen); + session.undo(); + // Escape closes without hidePopup on Qt 6.10: the preview goes. + picker.showPopup(); + QTRY_VERIFY(picker.view()->isVisible()); + emit picker.highlighted(3); + QVERIFY(session.blendPreview().has_value()); + QTest::keyClick(picker.view(), Qt::Key_Escape); + QVERIFY(!picker.view()->isVisible()); + QTRY_VERIFY(!session.blendPreview().has_value()); + QCOMPARE(session.activeLayer().value().blendMode, LayerBlendMode::normal); +} + QTEST_MAIN(BlendModePickerTests) #include "BlendModePickerTests.moc" diff --git a/tests/FilterFailureTests.cpp b/tests/FilterFailureTests.cpp index 56db939..982c768 100644 --- a/tests/FilterFailureTests.cpp +++ b/tests/FilterFailureTests.cpp @@ -47,6 +47,9 @@ void FilterFailureTests::aTrimThatFindsNoMemoryIsARenderError() void FilterFailureTests::aDitherThatFindsNoMemoryIsARenderError() { + // Qt 6.10 converts on threads the limit starves first. + if (!placesStarvation()) + QSKIP("the failure point is placed for Qt 6.4's allocations"); DitherSettings dither; dither.pixelSize = 1; // Another format: its conversion finds no memory. diff --git a/tests/GridSettingsSheetTests.cpp b/tests/GridSettingsSheetTests.cpp index 7470a62..0de1b94 100644 --- a/tests/GridSettingsSheetTests.cpp +++ b/tests/GridSettingsSheetTests.cpp @@ -14,6 +14,8 @@ class GridSettingsSheetTests : public QObject { Q_OBJECT private slots: + // A user's English locale: Qt 6.10's C locale groups nothing. + void initTestCase() { QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedStates)); } void theSheetLaysOutAsSwifts(); void eachChangePreviewsTheGrid(); void aPickedColourBecomesCustom(); diff --git a/tests/TypeFontPreviewTests.cpp b/tests/TypeFontPreviewTests.cpp index 2393ad6..db1a968 100644 --- a/tests/TypeFontPreviewTests.cpp +++ b/tests/TypeFontPreviewTests.cpp @@ -35,6 +35,8 @@ private slots: void hoveringTriesAFaceAndClosingPutsItBack(); void aChoiceKeepsWhatShows(); void onlyOpenTextPreviews(); + void aClosedMenusHighlightTriesNothing(); + void escapePutsTheFaceBack(); }; void TypeFontPreviewTests::theMenuDrawsEachFaceInItself() @@ -133,5 +135,37 @@ void TypeFontPreviewTests::onlyOpenTextPreviews() QCOMPARE(typed.session.activeLayer().value().liveText().value().style, kept); } +void TypeFontPreviewTests::aClosedMenusHighlightTriesNothing() +{ + Typed typed; + // Qt 6.10 signals as the menu follows the caret, closed. + QSignalSpy changed(&typed.session, &EditorSession::changed); + typed.hover(mono); + QVERIFY(changed.isEmpty() && typed.face() == sans); + // So a later close puts back nothing typed meanwhile. + QTest::keyClick(typed.canvas, Qt::Key_End); + QTest::keyClicks(typed.canvas, QStringLiteral("!")); + typed.menu().showPopup(); + typed.menu().hidePopup(); + QCoreApplication::processEvents(); + QCOMPARE(typed.session.textDraft().value().style.content, QString("Hello!")); +} + +void TypeFontPreviewTests::escapePutsTheFaceBack() +{ + Typed typed; + typed.menu().showPopup(); + QTRY_VERIFY(typed.menu().view()->isVisible()); + typed.hover(mono); + // The open popup's own events leave the preview showing. + typed.menu().view()->window()->update(); + QTest::qWait(50); + QCOMPARE(typed.face(), mono); + // Qt 6.10 closes on Escape without calling hidePopup. + QTest::keyClick(typed.menu().view(), Qt::Key_Escape); + QVERIFY(!typed.menu().view()->isVisible()); + QTRY_COMPARE(typed.face(), sans); +} + QTEST_MAIN(TypeFontPreviewTests) #include "TypeFontPreviewTests.moc"