From 1289a6c79009adb172811b2ade0f140b909416b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 11 Oct 2026 04:24:36 -0400 Subject: [PATCH] =?UTF-8?q?14.5:=20one=20View=20=E2=80=BA=20Snap=20for=20a?= =?UTF-8?q?ll=20snapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 4 ++-- src/Document/Crop.cpp | 8 ++++---- src/Document/EditorSession.cpp | 8 -------- src/Document/EditorSession.h | 6 ++---- src/Document/Guides.cpp | 2 ++ src/UI/CompositorMenus.cpp | 3 --- tests/CompositorMenusTests.cpp | 4 ++-- tests/CropCanvasTests.cpp | 2 +- tests/GuideTests.cpp | 4 ++-- tests/ResizeSnapCanvasTests.cpp | 4 ++-- tests/ResizeSnapTests.cpp | 4 ++-- tests/SnapDrawingCanvasTests.cpp | 4 ++-- tests/TransformSnapTests.cpp | 4 ++-- tests/ViewMenuTests.cpp | 19 ++++++++++--------- 14 files changed, 33 insertions(+), 43 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3d7da64..1f50b72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -122,9 +122,9 @@ Platform adaptations, each forced by a missing Qt analog: - File and picture drops (10.4): `IO/ImageFileDrop` twins Swift's enum as a namespace. `importProviders` takes the drop's `QMimeData`, where Swift takes item providers: each local file URL is a file, in order; a URL that is no file counts as unreadable, unless no file came at all and the drop holds a picture (a browser's or a screenshot's), whose bytes are copied to `Dropped-` in the temporary folder, as Swift's `temporaryFile` copies a promised file, the format chosen in Swift's order (PNG, JPEG, HEIC, TIFF, then any `image/` type) and the suffix from Qt's MIME database. The files go to the workspace's `receive` or, without a workspace, to the session's `importImages`; once that has run, a drop with anything unreadable adds Swift's words to the session's import error, after the importer's own ("from the file manager" where Swift says "from Finder"). Swift's `projects:` parameter has no caller and is left out. `holdsImages` is Swift's drop types but the layer row's: a local file or a picture. `receiveProviders` copies layer rows as before and hands anything else to `importProviders` one provider at a time (`providers`: a URL each, or, when no file came, one item holding the preferred picture alone, so a lazy drag is asked for nothing else, as Swift asks for one format; its web addresses are left behind, since alone they read as unreadable either way), each after the last has run, for the tab then in front, whose session hears what could not be read and stays alive until its import has run; so an import error holds the next file until it is dismissed, Swift's gate between providers. A copied picture that cannot be written whole (its flush checked, since `QFile` buffers) is removed and counts as unreadable; a failed flush fails `QFile::remove` too, so the copy is closed and unlinked by path. An open alert shows its message as it stands (`showAlert` updates it), as SwiftUI's alert re-reads it. The canvas, the tab buttons and the New canvas button take files and pictures as they take rows (a canvas without a workspace under Swift's session guards, Levels, busy, the new-canvas sheet, the importer and a rename, importing into its own session); Alt refuses only a row, as Swift's `validateDrop`. Tests: `ImageFileDropTests`. - Slider snapping (10.5): `UI/SliderSnap` twins Swift's enum as a namespace. Swift swaps `NSSliderCell.startTracking` so that a press on the track sets the value whose knob is centred there before tracking begins; the twin's `install` puts one application event filter in place, once, as Swift's lazy static swaps once: a left press on an enabled horizontal `QSlider` with a range, outside its knob and with travel for it, sets the slider down, then sets the value under the press (`QStyle::sliderValueFromPosition` over the groove less the knob, the knob's half-width off, flipped for an inverted slider), and presses where the knob now is, the original press consumed, so Qt drags the knob from there even when the slider's owner rounds the value and moves it (a filter's field); a press made with another button held is left to Qt, which refuses it. So editing begins before the value, Swift's order (the opacity slider's drag opens its one step first), and a press on the knob drags it from where it is. QSlider keeps its style option protected, so the filter builds one as `initStyleOption` does. Qt's own absolute set (`SH_Slider_AbsoluteSetButtons`) would set the value before the press and recentre a pressed knob, so it is not used, but it is the test's oracle for where the knob lands. `main` installs it after the theme. Tests: `SliderSnapTests` (Swift's `clickingTheTrackSnapsTheKnobAndStillEditsTheValue`: the value under the press at once, editing begun first, a drag going on, the release; the knob, other buttons, a disabled, vertical, empty, inverted and narrow slider). Equivalent mutant: the groove's own offset, zero under Fusion, the app's one style. The call in `main` is not under test, as the theme's construction there is not. - Selection modifiers, Select Subject, Soft Light and the font menu (12.1, Swift 1.1.1–1.1.4): `DocumentSelection` gains `feather`, the fade's reach in document pixels. Its coverage is filled antialiased and blurred by `PixelAdjust::gaussianBlur` at sigma feather / 2 with the edge repeated, Core Image's `clampedToExtent`; `coverageBounds` grows the outline's box by `ceil(2 × feather)`, and the clip, the copy region and a mask made from the selection (now drawn through `PixelAdjust::coverage` from the clip) take it. A feather stacks as blurs do (the root of the squares, 250 at most) and travels through moves, Expand, Contract and Inverse. The Select menu's Expand…, Contract… and Feather… ask for an amount (`promptSelectionAmount`); `selectionAmountOperation` is a getter and a setter that resumes file requests, read first by `canEditLayers`, `canUseHistory` and `canStartProjectOperation`. `SelectionAmountSheet` (in `LassoControls`, Swift's file) is Swift's sheet in a `FloatingPanel` named `selectionAmountPanel`, titled by the operation; its note keeps its room while hidden, as SwiftUI's opacity; `QString::toInt` trims spaces as Swift's `trimmingCharacters` does. The selection bar gains Feather and its amount (1–250). Select ▸ Subject (Ctrl+Alt+A) draws the live composite, runs `SubjectRemoval::subjectMask` in a worker with the project busy (a completion callback, as the other jobs), and on the same document applies `MaskTracing::whitePixels` (at least half gray) as the selection, one `Select Subject` step, or sets `brushError`; `canSelectSubject` leaves out Swift's document and busy terms, which `canEditLayers` reads. Soft Light joins `LayerBlendMode` after Overlay (since 13.9a hand-blended by Photoshop's formula, see that bullet). `TypeFontPicker` is Swift's: the style's own name alone until the menu opens, a missing name added at the end, an open menu left alone, the face already shown writing nothing, the current name elided at its end within 210 points (Fusion's label draws a pixel in from each side of the edit field). Point text caches its measure by style in the inline editor, and `setTextDraft` resumes file requests only when a draft ends, as Swift 1.1.3's. Swift's `Geometry` cache, which skips AppKit's frame work, has no twin: the canvas draws the editor. Equivalent mutants: the editor's box branch (`textBoxSize` returns the box), `setTextDraft`'s ended test (`resumeFileRequests` asks `canStartProjectOperation`, which a draft refuses). Tests: `SelectionFeatherTests` (Swift's, then stacking, travel, bounds and the sheet's session half), `SelectionAmountSheetTests`, `SelectSubjectTests`, `TypeFontPickerTests`, and the menus, blend modes and Type bar in their files. -- Folders, opacity and Snap (12.2a, Swift 1.1.5–1.1.6): a folder takes an opacity that multiplies into each layer inside it, never composited as a unit, since folders pass through. `LayerOpacity::effective` (in `LayerGroups.h`) walks up to 64 folders, a lookup standing for Swift's closure; `ImageLayer::effectiveOpacity`, `ProjectLayerRecord::effectiveOpacity` (declared in `ProjectStore.h`, defined in `LayerGroups.cpp`, as Swift's extension) and `CanvasDocument::effectiveOpacities` twin Swift's, the last refusing a repeated id with a `logic_error` where Swift's dictionary traps. Every drawing path takes it: the export, `drawLiveComposite`, the canvas (its every draw, adjustments included, and `DisplayState`, so a folder's change redraws) and the live-mask baker's sources, whose records are pointers, so it calls `LayerOpacity::effective` itself. `canEditOpacity` is one layer, a folder included; `canEditAppearance` stays for blending, so `LayerAppearanceControls` rests with `canEditOpacity` and its Blend caption and picker with `canEditAppearance`; `setSelectedLayersOpacity` dims selected folders too. The format moves to version 8 here (Swift's guides arrive under it in 12.3; until then a version-8 project's `guides` key is ignored as unknown), because 1.2.0's store rule (`d9b3f9e`) lets a folder dim from version 8 on, its blend mode always normal. View ▸ Snap is `snappingEnabled`, a getter and a setter that clears the guides and notifies (Swift's `didSet`); `snappedMove` and the crop drag read it. The transform box draws its accent alone. The effects sheet's sliders span Swift's new ranges (Size 0–20, Distance 0–100, the inner shadow's 0–50, Blur 0–100) while typing and steps reach the effect's own limits (500, 5,000, 500), clamped by `apply`; `QSlider` pins a larger value at its end by itself, so Swift's clamp of the thumb has no twin; the swatch takes a white ring inside a black rim, where SwiftUI's `contentShape` has no need of a twin (a Qt button takes its whole rect). The layer list takes T for Type, as 1.1.6's table does. Equivalent mutant: `canEditOpacity`'s `activeLayer()` term (one selected layer is the active one). `drawPixelGrid` moved to `EditorCanvas+Grid.cpp`, keeping the canvas file under 500 lines. Tests: `FolderOpacityTests` (the walk, the export, the composite, adjustments, effects, the canvas in each way it draws, the baker, Swift's `aDimmedFolderSavesAndReopens`), folder duplication in `LayerDuplicateTests` (moved out of `EditorSessionTests` at 500 lines), `TransformTests` and `LayerDragTests`, the controls in `LayerAppearanceControlsTests`, Snap in `TransformSnapTests`, `CropCanvasTests` and `ViewMenuTests` (the View menu tests, moved out of `CompositorMenusTests`), the ranges and swatch in `EffectsPanelTests`. +- Folders, opacity and Snap (12.2a, Swift 1.1.5–1.1.6): a folder takes an opacity that multiplies into each layer inside it, never composited as a unit, since folders pass through. `LayerOpacity::effective` (in `LayerGroups.h`) walks up to 64 folders, a lookup standing for Swift's closure; `ImageLayer::effectiveOpacity`, `ProjectLayerRecord::effectiveOpacity` (declared in `ProjectStore.h`, defined in `LayerGroups.cpp`, as Swift's extension) and `CanvasDocument::effectiveOpacities` twin Swift's, the last refusing a repeated id with a `logic_error` where Swift's dictionary traps. Every drawing path takes it: the export, `drawLiveComposite`, the canvas (its every draw, adjustments included, and `DisplayState`, so a folder's change redraws) and the live-mask baker's sources, whose records are pointers, so it calls `LayerOpacity::effective` itself. `canEditOpacity` is one layer, a folder included; `canEditAppearance` stays for blending, so `LayerAppearanceControls` rests with `canEditOpacity` and its Blend caption and picker with `canEditAppearance`; `setSelectedLayersOpacity` dims selected folders too. The format moves to version 8 here (Swift's guides arrive under it in 12.3; until then a version-8 project's `guides` key is ignored as unknown), because 1.2.0's store rule (`d9b3f9e`) lets a folder dim from version 8 on, its blend mode always normal. View ▸ Snap was `snappingEnabled`, a getter and a setter that cleared the guides and notified (Swift's `didSet`); `snappedMove` and the crop drag read it (one Snap since 14.5). The transform box draws its accent alone. The effects sheet's sliders span Swift's new ranges (Size 0–20, Distance 0–100, the inner shadow's 0–50, Blur 0–100) while typing and steps reach the effect's own limits (500, 5,000, 500), clamped by `apply`; `QSlider` pins a larger value at its end by itself, so Swift's clamp of the thumb has no twin; the swatch takes a white ring inside a black rim, where SwiftUI's `contentShape` has no need of a twin (a Qt button takes its whole rect). The layer list takes T for Type, as 1.1.6's table does. Equivalent mutant: `canEditOpacity`'s `activeLayer()` term (one selected layer is the active one). `drawPixelGrid` moved to `EditorCanvas+Grid.cpp`, keeping the canvas file under 500 lines. Tests: `FolderOpacityTests` (the walk, the export, the composite, adjustments, effects, the canvas in each way it draws, the baker, Swift's `aDimmedFolderSavesAndReopens`), folder duplication in `LayerDuplicateTests` (moved out of `EditorSessionTests` at 500 lines), `TransformTests` and `LayerDragTests`, the controls in `LayerAppearanceControlsTests`, Snap in `TransformSnapTests`, `CropCanvasTests` and `ViewMenuTests` (the View menu tests, moved out of `CompositorMenusTests`), the ranges and swatch in `EffectsPanelTests`. - Keyboard shortcuts (12.2b, Swift 1.1.6): `UI/KeyboardShortcuts.h/.cpp` twin Swift's file, `KeyboardShortcuts+Sheet.cpp` its sheet and recorder. `ShortcutChord` keeps Swift's key strings (Backspace 0x7f, Return, Escape, Tab, Space, the arrows as Cocoa's F700s) and modifier bits, Ctrl for Command, Alt for Option, Meta for Control, then Shift; a key event reads Backspace and Delete alike, Enter as Return, Backtab as Tab, a shifted bracket or sign as its own key, anything else by its lowered key name, so a function key's name is long and refused (Swift takes it as a private-use character; known difference); labels are `QKeySequence`'s native text (Ctrl+Alt+K), where Swift prints symbols. `ShortcutDefinition::all` is Swift's list less Hide Compositor, which has no twin; `problem` keeps Swift's rules less its three chords reserved by macOS (the desktop takes its own before the app does). `ShortcutSettings` keeps the overrides in `QSettings` under Swift's `keyboardShortcuts.v1`, as Swift's JSON, a stored set that fails `problem` ignored with a warning on `omaphoto.app` where Swift is silent; `changed()` stands for its observation, and `reload` reads the store again, as a launch does. The menus remember each entry's own key in a property and take `menu` of it on every change. `canvasEvent` and `textEvent` hand back the press to handle, or none to swallow: the canvas (its key handling moved to `EditorCanvas+Keys.cpp`, the navigation file at 500 lines), the layer list and the window's key filter read `canvasEvent`, open text `textEvent`; a translated press the canvas or list leaves passes the original on, as `super.keyDown` does. Swift's `panPhysicalKey` is `m_panKey`: the key whose release ends a remapped pan. `NativeShortcut` is `configuredNativeShortcut`: a `QShortcut` over the sheet and its children clicks its button when that shows (a disabled button refuses `click` by itself); its own plain Return or Escape rests it, leaving the dialogs' default and escape handling, so a field's Return still commits first, while a button moved onto the other's key takes it; a moved original key is swallowed at the sheet; a synthesized press carries Swift's shifted text (`}` for Shift+]), which the brush keys read; `NewCanvasSheet`'s own Return shortcuts rest while Apply is moved; Levels' Preview takes its chord this way, in place of the checkbox's shortcut. The sheet is Swift's: 660 wide, the hint, a search, the three groups of rows with 150-by-26 recorders, the contextual words in Linux's keys, the problem in `BrightText` at 12 pixels only while there is one, Restore Defaults, Cancel and a default Save resting while recording or wrong; a modifier alone keeps recording, and while recording the recorder claims every key, Swift's `performKeyEquivalent`: a `Qt::Tool` panel keeps its parent window's menu shortcuts live, and `QWidget::event` would move the focus on Tab. It opens from the Edit menu, after Paste and a separator, in the `keyboardShortcuts` panel `CompositorMenus` holds. Equivalent mutants: `menu`'s and the Shift branch's `isMenu` terms (menu chords all carry modifiers), `canvasEvent`'s early return without overrides, `m_panKey`'s reset (every pan sets it) and Save's explicit default (its only auto-default button). Tests: `KeyboardShortcutsTests`, `KeyboardShortcutsSheetTests`, `RemappedKeysTests`. -- Guides and the layout grid (12.3a, Swift 1.1.7): `Document/Guides.h/.cpp` twin Swift's `Guides.swift`: `CanvasGuide` (its axis by Swift's raw values, `rawValue` and `guideAxis`), `LayoutGrid` (a struct since 13.5b), `GuideDrag`, and the session extension, declared in `Guides+Session.h` with the accessors of Swift's view flags (`showsGrid`, `showsGuides`, `showsRulers`, `snapEnabled`, the four Snap To flags, `locksGuides`, each a getter and a setter that notifies, and `guideDrag`), whose state stays in `EditorSession.h`; `guideHitDistance` and `guideColor` are Swift's statics. `finishGuideDrag` takes `removing` where Swift names it `delete`. `canEditGuides` leaves out Swift's `_ = showsBusy`, which only registers an observation. 1.1.7's `snapEnabled` (Snap To's master switch, read by `alignmentSnapTargets` and `snappedGuidePosition`) stands beside 1.1.6's `snappingEnabled` (read by `snappedMove` and the crop drag), as it does in Swift through 1.3.3, with a View entry each. `transformSnapTargets` and `cropSnapTargets` are `alignmentSnapTargets` with and without centres, the canvas's middle after its edges and a layer's between its edges, Swift's order; `TransformSnap::box` (in `Crop.cpp`) is the upright box both files read. The document holds `guides`, equality and undo included; the manifest's optional `guides` is Swift's `Codable` (an array of `id`, `axis` and `position`, left out when there are none, the snapshot writing none for an empty list), and the store refuses guides before version 8 (invalid), more than 1,000 (too large), a repeated id or a position past a million (invalid). `installProject` and `clearProject` drop a drag; `CanvasDocument(snapshot)` loads the guides, so `applyDocumentSize` and `applyImageSize` keep them; the canvas resizer offsets them, the image resizer scales them (a same-size resize keeps them), the canvas flip mirrors those across it. The View menu gains Swift's Show (Grid Ctrl+', Guides Ctrl+;), Rulers (Ctrl+R), Snap (Ctrl+Shift+;), Snap To (Guides, Grid, Layers, Document Bounds), Lock Guides (Ctrl+Alt+;) and Clear Guides, resting without a document or, for Clear, without guides; the five new shortcut definitions join the list. Equivalent mutants: `finishGuideDrag`'s `isNew` test on removal and its `original != position` test (the history drops a step that changes nothing). The rulers, the grid and guides on the canvas and their drags are 12.3b's. Tests: `GuideTests` (Swift's but the canvas drag and the ruler's steps), `ViewMenuTests`, the targets' order in `TransformSnapTests`. +- Guides and the layout grid (12.3a, Swift 1.1.7): `Document/Guides.h/.cpp` twin Swift's `Guides.swift`: `CanvasGuide` (its axis by Swift's raw values, `rawValue` and `guideAxis`), `LayoutGrid` (a struct since 13.5b), `GuideDrag`, and the session extension, declared in `Guides+Session.h` with the accessors of Swift's view flags (`showsGrid`, `showsGuides`, `showsRulers`, `snapEnabled`, the four Snap To flags, `locksGuides`, each a getter and a setter that notifies, and `guideDrag`), whose state stays in `EditorSession.h`; `guideHitDistance` and `guideColor` are Swift's statics. `finishGuideDrag` takes `removing` where Swift names it `delete`. `canEditGuides` leaves out Swift's `_ = showsBusy`, which only registers an observation. 1.1.7's `snapEnabled` (Snap To's master switch, read by `alignmentSnapTargets` and `snappedGuidePosition`) is since 14.5 the one View › Snap: Swift 1.4.6 (`bba92dc`) made 1.1.6's `snappingEnabled` an alias of it and dropped its own entry, so `snappingEnabled()` reads `m_snapEnabled` for the moves, crops and drawing that read it, and `setSnapEnabled` clears the snap guides when it turns off (Swift's moved `didSet`); the alias's setter has no caller and is left out. `transformSnapTargets` and `cropSnapTargets` are `alignmentSnapTargets` with and without centres, the canvas's middle after its edges and a layer's between its edges, Swift's order; `TransformSnap::box` (in `Crop.cpp`) is the upright box both files read. The document holds `guides`, equality and undo included; the manifest's optional `guides` is Swift's `Codable` (an array of `id`, `axis` and `position`, left out when there are none, the snapshot writing none for an empty list), and the store refuses guides before version 8 (invalid), more than 1,000 (too large), a repeated id or a position past a million (invalid). `installProject` and `clearProject` drop a drag; `CanvasDocument(snapshot)` loads the guides, so `applyDocumentSize` and `applyImageSize` keep them; the canvas resizer offsets them, the image resizer scales them (a same-size resize keeps them), the canvas flip mirrors those across it. The View menu gains Swift's Show (Grid Ctrl+', Guides Ctrl+;), Rulers (Ctrl+R), Snap (Ctrl+Shift+;), Snap To (Guides, Grid, Layers, Document Bounds), Lock Guides (Ctrl+Alt+;) and Clear Guides, resting without a document or, for Clear, without guides; the five new shortcut definitions join the list. Equivalent mutants: `finishGuideDrag`'s `isNew` test on removal and its `original != position` test (the history drops a step that changes nothing). The rulers, the grid and guides on the canvas and their drags are 12.3b's. Tests: `GuideTests` (Swift's but the canvas drag and the ruler's steps), `ViewMenuTests`, the targets' order in `TransformSnapTests`. - Rulers, and guides on the canvas (12.3b, Swift 1.1.7): `UI/CanvasRulers.h/.cpp` twin Swift's file: `CanvasRuler::thickness`, `CanvasRulerCorner` and `CanvasRulerView` (a widget per strip, repainting on every `changed()`, where Swift's canvas asks each ruler to redraw), with Swift's `majorStep` and `label`. The canvas is the rulers' sibling, so a ruler maps its points through the screen (`inCanvas`), where Swift reads its own bounds as the canvas's. A press hands the canvas the keys and begins a guide from that ruler (horizontal from the top one), a drag moves it, a release over a ruler drops it; a ruler has no mouse tracking, so it sees no buttonless move and leaves a lost release to the canvas's next press. `ContentView` frames the canvas slot in a grid with the corner and the two strips, shown while `showsRulers` holds and a document is open. `TransformOverlay` draws the layout grid (majors solid gray 0.7 at 45%, subdivisions dotted one point on and two off in gray 0.55 at 28% once four points apart, both a hairline) and the guides (`EditorSession::guideColor`, a hairline across the whole view) first, as Swift's `draw`; the canvas redraws the view when the grid's flag, the guides' flag or the displayed guides change (`GuidesShown`), Swift's observation. Guide drags live in `EditorCanvas+Guides.cpp`: with the Move tool a press on a guide (`canEditGuides` and `hitGuide`) moves it ahead of any layer under the resize cursor of its axis, a drag follows, a release over a ruler (`isOverRuler`: a point left of or above the canvas while the rulers show) deletes it; Escape cancels it ahead of a transform edit's keys; wheels and pinches rest while one is dragged; the Move tool's hover shows the guide's resize cursor after the handles and before a layer's move cursor. Qt can lose a release: a buttonless move and a lost focus finish the drag where it is. Equivalent mutant: the ruler press's `canEditGuides` test (`beginGuideCreation` asks it again). Tests: `GuideCanvasTests` (Swift's `moveToolDragsAGuideAndRulerDropDeletesIt` and `rulerStepUsesNicePixelIntervals`; the drawing, repaints, cursors, keys, lost releases, the rulers' layout, ticks, repaints, focus and drags), the canvas's place in `ContentViewTests`. - The Magic tool's Object mode (12.4, Swift 1.1.8): `Document/ObjectSelection.h/.cpp` twin Swift's file, `ObjectSelection+Session.h` its session half; `WandMode` (Swift's raw values, `allWandModes`) sits in `Selection.h`. Vision's foreground instances have no Linux twin (the user's decision): `SubjectRemoval::foreground` hands back U²-Net's mask at the image's size (the Remove Background model and cache, 9.7), which `select` scales to the model's 320 grid, cuts at half, and splits into 8-connected regions (`region`, the neighbours a traced outline joins); the region under the clicked cell is the instance, background none, and a picture where the model finds no subject none, as Vision's empty result. Its cells are enlarged smoothly to the image and refined by `GuidedMatte::refine` (radius 5) on the image, for Swift's edge-preserving upsample, then cut at half; `adjusted` erodes or dilates a pixel ring a step (eight neighbours, ten steps at most, the image's edge counting as inside, Swift's `eroded` and `dilated`), `MagicWand::outline` traces it and `smoothed` rounds the stairs as Swift's (the start Qt's close repeats dropped first, since a CGPath's close adds no point, then a subpath under three points dropped, Douglas–Peucker at 1.6 from the leftmost point, Chaikin three times, the fill rule kept; Swift's later drops of a repeated end and Chaikin's guard under three points are dead after that and left out). A `bad_alloc` is `ExportError(render)`; Swift's `unsupported` has no twin. The worker catches `SubjectRemovalError`, `MagicWandError` (the trace's refusals) and `ExportError`, each landing in `brushError` as Swift's `catch`. `selectionSample` is the wand's sample made public as Swift's; `selectObject` runs through the wand's watcher and landing (`Wanding` carries the step's name, `Object Selection`), with a completion callback, the offset clamped to ten either way and the anti-alias setting as its smoothing. `pressWandKey` chooses the tool, W on the canvas and in the list, a repeat ignored; Tab steps the mode; the tool's label, its shortcut definition (`Magic`) and the bar's title say Magic. The bar (`LassoControls`) shows Wand and Object as checkable buttons for Magic alone, the colour wand's controls in Wand mode, and in Object mode This Layer and All Layers and an Edge field of −10 to 10 px (`SelectionAmountField` takes a sign when its range goes below zero; a lone minus changes nothing, an overflow reads as its end), with Swift's help; the anti-alias help follows the mode. The canvas's press runs `selectObject` ahead of the colour wand, a click inside the selection picks afresh, the cursor is the crosshair with `SelectionIcon::objectSelection` (haloed by eight offsets as every selection icon, where Swift strokes it white), and the rail draws the icon for Object mode (`ToolIconKind::object`); the status hint follows the mode. Known difference: U²-Net finds one salient mask, so objects touching in it are one region where Vision may tell instances apart. Left out as dead: the mode picker's `cancelLasso` (Magic never holds a draft: `beginLasso` refuses it and `selectTool` cancels one), `selectObject`'s busy term (`canEditSelection` reads it) and its clamp of the offset (`adjusted` clamps), and a null check of the enlarged cells (`GuidedMatte::refine` checks its own). Equivalent mutants: the grid's cut at exactly 128, `simplifyClosed`'s guard under four points (the fallback returns the same points), `finish`'s three-point threshold at four (a traced loop has four corners), the Chaikin reserve, the edge's rounding (typing and arrows give whole numbers), W calling `selectTool` (`pressWandKey` is that call). Untested failure paths: the region's own 320-cell image, 100 KB, which no limit starves without starving the grid before it, and the worker's `MagicWandError`: a refined mask never nears eight million edges, and the trace's image-sized buffer asks less than the guided filter before it. Tests: `ObjectSelectionTests` (regions, edges, smoothing, the model on red discs, the session, keys, the canvas), the bar in `LassoControlsTests`, the icon sheet, the rail and hints in `ContentViewTests` and `ContentViewStatusTests`. - Photoshop import (12.5a, Swift 1.2.0): `src/IO/PSD/` twins Swift's `IO/PSD` folder. `PSDTypes` holds `PSDError` (Swift's five cases and words), `PSDConversion`, `PSDRecord`, `PSDDocument` and `fromPSD` (Swift's `LayerBlendMode.fromPSD`); Swift's `PSDLayerKind.other` is never assigned, so the case and its note are left out. `PSDChannelCoder` decodes raw and PackBits planes (a no-op run skipped, every overrun `truncated`) and builds `RGBA8888_Premultiplied` pixels tagged sRGB, rounding as Swift's `(c × a + 127) / 255`, and `Grayscale8` masks; a surface that cannot be allocated is `ExportError(render)` through `BrushRaster::context`, where Swift says `truncated`. `PSDReader` reads Swift's fields in Swift's order over a `Cursor` whose bounds check cannot overflow (an 8B64 length near `qint64`'s top reads as truncated, where Swift's `offset + count` traps), maps the file where it can and reads it where it cannot (a pipe, a mount without `mmap`), as `mappedIfSafe` does (a file that cannot be opened or read is `unreadable`, where Swift passes on the Cocoa error), logs every refusal on `omaphoto.io` and rethrows it. Pascal names are Mac OS Roman through a table of the 128 high code points taken from Python's `mac_roman` (Qt decodes it only through ICU); keys are ASCII or nothing, as Swift's `.ascii`. `PSDAdjustments::parse` (in `PSDReader+Adjustments.cpp`) reads Levels, Curves and Hue/Saturation as Swift does. `PSDVector` draws vector masks with `QPainterPath` (tight `boundingRect`, a path empty only without elements as a `CGPath`), fills by nonzero winding and strokes with `Qt::SvgMiterJoin` at 10, SVG's miter rule being CoreGraphics' (probed: kept to a ratio of 9.57, bevelled from 10.43; `Qt::MiterJoin` measures from the join point and clips instead); `integral` stays in doubles, so a wild box reaches the size check instead of an overflowing `QRect`. `PSDDocumentBuilder` makes the layers and conversions; a shape's identity is its asset's. `ImageImporter::loadPhotoshop` and `photoshopAssets` run on the import's worker, `makeImport` on the UI thread. In the session (`EditorSession+Imports.cpp`) a file whose first bytes are `8BPS` takes the Photoshop path: `beginPSDReading` shows the sheet while a `QFutureWatcher` reads it, `finishPSDReading` fills it or takes it away, `finishConversion` answers from the event loop (a resumed task), Cancel while reading drops the file silently, a failure joins the import's failures, and a confirmed file lands through `insertPhotoshop` (a new canvas at the file's size and resolution, or one folder named for the file inside the active folder, centred on a drop point, 10,000 layers at most). A request of Photoshop files alone is one `Import Photoshop File` step. Fixes beyond Swift: `insertPhotoshop` validates the hierarchy first, so folders nested past its depth refuse the file (`truncated`), and a folder clipped to the layer below takes no link but the base note, where Swift links it; either way Swift admits a document its store refuses. Left out as dead: the gates' `showsConversionSheet` terms and its `didSet` (the sheet shows only while importing, which the gates refuse already), Swift's test hook `confirmConversions`, `confirmPSDConversions` and `NativeLayerList.moveLayer` (no caller), the request's `id`, the builder's folder and adjustment checks on a clipping base (only pixel layers become bases), the reader's `layerSectionEnd`, the default mask colour, the reader's plane-size checks (a plane decodes to its size), resolution's finiteness test and its floor, the vector code's finiteness tests on boxes built from finite numbers and the origination's notes (never set), Curves' two length checks past the first, `pixelSize`'s area in doubles (its boxes are whole, so the whole pixels decide alone), the session's catch around `makeImport` (every image arrives with its asset, so nothing there allocates), and the new folder's removal from the collapsed set (a fresh id is in no set). `UI/PSDConversionSheet` is Swift's sheet in a window-modal `QDialog` (`conversionSheet`) that `ContentView` shows from the session's request: the title at 17 pixels bold, the words, a spinner while reading or the list of layer names over their notes, Cancel and the default confirm button, resting while reading; Escape and the close button cancel, and a sheet the session takes away answers nothing; `open()` makes the dialog window-modal. The request shown is kept, so an unrelated change keeps the content, and replaced content hides at once and is deleted later. The importer's filter and the desktop entry take `.psd` (`image/vnd.adobe.photoshop`); a dropped picture is copied as `Dropped.` into a folder of its own, as Swift 1.2.0 keeps a dropped file's name. Equivalent mutants: the cursor's own bounds refusal and `skip`'s (every read past the end meets another refusal: a short signature, a short plane), PackBits' count and row-end checks (the branches refuse the same bytes; they spare reading past the buffer), the `reserve` calls, `painter.end()` before the raster returns, the drop copy's `close()` before its static removal, and the logs. Untested failure path: a read that fails after the file opened (an I/O error; Qt 6.4 refuses to open a directory). Tests: `PSDRoundTripTests` and `PSDVectorTests` (Swift's, through `tests/PSDFixture.h`, Swift's writer, and `PSDVectorFixtures.h`), `PSDReaderTests` and `PSDReaderLayerTests` (the reader's edges, every cut before the last layer byte, budgets and vector layers read whole, built by `PSDReaderFixtures.h` and `PSDVectorBuilders.h`), `PSDImportTests` (the session and the sheet), and the drop, importer, message and desktop checks in their files. diff --git a/src/Document/Crop.cpp b/src/Document/Crop.cpp index e883e53..cf5e451 100644 --- a/src/Document/Crop.cpp +++ b/src/Document/Crop.cpp @@ -154,7 +154,7 @@ SnapGuides EditorSession::transformSnapTargets(const QSet &moving) const // `draft` nudged onto a nearby edge or middle, within `tolerance`. LayerTransform EditorSession::snappedMove(const LayerTransform &draft, const QSet &moving, double tolerance) { - if (!m_snappingEnabled) { + if (!snappingEnabled()) { snapGuides = {}; return draft; } @@ -172,7 +172,7 @@ LayerTransform EditorSession::snappedMove(const LayerTransform &draft, const QSe QPointF EditorSession::snappedResizePoint(QPointF point, const TransformDrag &drag, bool proportional, const QSet &moving, double tolerance, const std::function &update) { - if (!m_snappingEnabled || drag.mode.kind != TransformDrag::Kind::resize || drag.original.radians() != 0) { + if (!snappingEnabled() || drag.mode.kind != TransformDrag::Kind::resize || drag.original.radians() != 0) { snapGuides = {}; return point; } @@ -228,7 +228,7 @@ SnapGuides EditorSession::cropSnapTargets() const QPointF EditorSession::snappedPoint(QPointF point, double tolerance) { - if (!m_snappingEnabled) { + if (!snappingEnabled()) { snapGuides = {}; return point; } @@ -248,7 +248,7 @@ QPointF EditorSession::snappedPoint(QPointF point, double tolerance) QSizeF EditorSession::snappedSelectionOffset(QSizeF offset, double tolerance, bool horizontal, bool vertical) { - if (!m_snappingEnabled || !m_selectionMoveOrigin) { + if (!snappingEnabled() || !m_selectionMoveOrigin) { snapGuides = {}; return offset; } diff --git a/src/Document/EditorSession.cpp b/src/Document/EditorSession.cpp index 290a8d7..6756896 100644 --- a/src/Document/EditorSession.cpp +++ b/src/Document/EditorSession.cpp @@ -368,14 +368,6 @@ void EditorSession::setShowsPixelGrid(bool shows) notify(); } -void EditorSession::setSnappingEnabled(bool enabled) -{ - m_snappingEnabled = enabled; - if (!enabled) - snapGuides = {}; - notify(); -} - void EditorSession::setCropRect(std::optional rect) { if (std::exchange(m_cropRect, rect) != rect) diff --git a/src/Document/EditorSession.h b/src/Document/EditorSession.h index b50cc7b..2e59acc 100644 --- a/src/Document/EditorSession.h +++ b/src/Document/EditorSession.h @@ -243,9 +243,8 @@ public: // The View menu's grid, shown from 800%. bool showsPixelGrid() const { return m_showsPixelGrid; } void setShowsPixelGrid(bool shows); - // View > Snap: moves and crops line up with edges. - bool snappingEnabled() const { return m_snappingEnabled; } - void setSnappingEnabled(bool enabled); + // Swift's alias of View › Snap: moves, crops, drawing. + bool snappingEnabled() const { return m_snapEnabled; } // The Eyedropper's ring: the sampled colour over the original. bool showsSampleRing() const { return m_showsSampleRing; } void setShowsSampleRing(bool shows); @@ -345,7 +344,6 @@ private: std::optional m_renamingLayerID; std::optional m_projectPath; bool m_showsPixelGrid = ToolDefaults::boolean(QStringLiteral("pixelGrid"), true); - bool m_snappingEnabled = true; // The layout grid starts off; guides show; rulers hide. bool m_showsGrid = ToolDefaults::boolean(QStringLiteral("grid"), false); LayoutGrid m_layoutGrid = LayoutGrid::stored(); diff --git a/src/Document/Guides.cpp b/src/Document/Guides.cpp index 28ed5c8..e0efcc4 100644 --- a/src/Document/Guides.cpp +++ b/src/Document/Guides.cpp @@ -220,6 +220,8 @@ void EditorSession::setSnapEnabled(bool enabled) { m_snapEnabled = enabled; ToolDefaults::set(enabled, QStringLiteral("snap")); + if (!enabled) + snapGuides = {}; notify(); } diff --git a/src/UI/CompositorMenus.cpp b/src/UI/CompositorMenus.cpp index 28ca211..eb9d44f 100644 --- a/src/UI/CompositorMenus.cpp +++ b/src/UI/CompositorMenus.cpp @@ -135,8 +135,6 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi add(view, QStringLiteral("zoomOut"), QStringLiteral("Zoom Out"), QKeySequence(Qt::CTRL | Qt::Key_Minus), [zoomBy] { zoomBy(-1); }); add(view, QStringLiteral("pixelGrid"), QStringLiteral("Pixel Grid (800% and above)"), QKeySequence(), [this] { session().setShowsPixelGrid(!session().showsPixelGrid()); }) ->setCheckable(true); - add(view, QStringLiteral("snap"), QStringLiteral("Snap"), QKeySequence(), [this] { session().setSnappingEnabled(!session().snappingEnabled()); }) - ->setCheckable(true); // Ctrl+H: the Move tool's box; the desktop hides apps. add(view, QStringLiteral("transformControls"), QStringLiteral("Show Transform Controls"), QKeySequence(Qt::CTRL | Qt::Key_H), [this] { session().setShowsTransformControls(!session().showsTransformControls()); }) @@ -356,7 +354,6 @@ void CompositorMenus::synchronize() for (const char *name : {"fit", "actualPixels", "zoomIn", "zoomOut"}) action(QString::fromLatin1(name))->setEnabled(drawn); action(QStringLiteral("pixelGrid"))->setChecked(s.showsPixelGrid()); - action(QStringLiteral("snap"))->setChecked(s.snappingEnabled()); const bool opened = s.document().has_value(); for (const auto &[name, value] : std::initializer_list>{ {"showGrid", s.showsGrid()}, {"showGuides", s.showsGuides()}, {"showRulers", s.showsRulers()}, {"snapEnabled", s.snapEnabled()}, diff --git a/tests/CompositorMenusTests.cpp b/tests/CompositorMenusTests.cpp index 740c5a5..64cc674 100644 --- a/tests/CompositorMenusTests.cpp +++ b/tests/CompositorMenusTests.cpp @@ -29,7 +29,7 @@ void CompositorMenusTests::everyEntryHasSwiftsShortcutWithCtrlForCommand() {"newCanvas", "Ctrl+N"}, {"openProject", "Ctrl+O"}, {"importImages", ""}, {"save", "Ctrl+S"}, {"saveAs", "Ctrl+Shift+S"}, {"exportPNG", "Ctrl+Shift+E"}, {"exportJPEG", "Ctrl+Alt+Shift+S"}, {"closeProject", "Ctrl+W"}, {"undo", "Ctrl+Z"}, {"redo", "Ctrl+Shift+Z"}, {"fit", "Ctrl+0"}, {"actualPixels", "Ctrl+1"}, - {"zoomIn", "Ctrl+="}, {"zoomOut", "Ctrl+-"}, {"pixelGrid", ""}, {"snap", ""}, {"showGrid", "Ctrl+'"}, {"showGuides", "Ctrl+;"}, {"gridSettings", ""}, {"showRulers", "Ctrl+R"}, {"snapEnabled", "Ctrl+Shift+;"}, {"snapToGuides", ""}, {"snapToGrid", ""}, {"snapToLayers", ""}, {"snapToDocumentBounds", ""}, {"lockGuides", "Ctrl+Alt+;"}, {"clearGuides", ""}, {"transformControls", "Ctrl+H"}, {"transformLayer", "Ctrl+T"}, + {"zoomIn", "Ctrl+="}, {"zoomOut", "Ctrl+-"}, {"pixelGrid", ""}, {"showGrid", "Ctrl+'"}, {"showGuides", "Ctrl+;"}, {"gridSettings", ""}, {"showRulers", "Ctrl+R"}, {"snapEnabled", "Ctrl+Shift+;"}, {"snapToGuides", ""}, {"snapToGrid", ""}, {"snapToLayers", ""}, {"snapToDocumentBounds", ""}, {"lockGuides", "Ctrl+Alt+;"}, {"clearGuides", ""}, {"transformControls", "Ctrl+H"}, {"transformLayer", "Ctrl+T"}, {"layerViaCopy", "Ctrl+J"}, {"cut", "Ctrl+X"}, {"copy", "Ctrl+C"}, {"copyMerged", "Ctrl+Shift+C"}, {"paste", "Ctrl+V"}, {"keyboardShortcuts", ""}, {"fillForeground", "Alt+Backspace"}, {"fillBackground", "Ctrl+Backspace"}, {"clearSelectionPixels", ""}, {"contentAwareFill", "Shift+Backspace"}, {"clippingMask", "Ctrl+Alt+G"}, {"groupLayers", "Ctrl+G"}, {"ungroupLayers", "Ctrl+Shift+G"}, {"moveOutOfFolder", ""}, {"newBlankLayer", "Ctrl+Shift+N"}, {"renameLayer", ""}, {"layerVisibility", ""}, {"moveLayerUp", "Ctrl+]"}, {"moveLayerDown", "Ctrl+["}, {"mergeLayers", "Ctrl+E"}, {"flipHorizontal", ""}, {"flipVertical", ""}, {"deleteLayer", ""}, @@ -51,7 +51,7 @@ void CompositorMenusTests::everyEntryHasSwiftsShortcutWithCtrlForCommand() QCOMPARE(titles, (QStringList{"&File", "&Edit", "&View", "&Select", "&Image", "Fil&ter", "&Layer"})); QCOMPARE(menus[0]->actions().size(), 12); QCOMPARE(menus[1]->actions().size(), 13); - QCOMPARE(menus[2]->actions().size(), 17); + QCOMPARE(menus[2]->actions().size(), 16); QCOMPARE(menus[3]->actions().size(), 11); // Swift 1.3.4's Color Range… follows Subject. const QList select = menus[3]->actions(); diff --git a/tests/CropCanvasTests.cpp b/tests/CropCanvasTests.cpp index ac45858..824b532 100644 --- a/tests/CropCanvasTests.cpp +++ b/tests/CropCanvasTests.cpp @@ -200,7 +200,7 @@ void CropCanvasTests::snappingReachesEightViewPoints() shown.drag(from, to); QCOMPARE(shown.frame(), std::optional(QRectF(100, 50, 300, 120))); // Snap off: the edges stay where the pointer puts them. - shown.session.setSnappingEnabled(false); + shown.session.setSnapEnabled(false); shown.session.setCropRect(std::nullopt); shown.drag(from, to); QCOMPARE(shown.frame(), std::optional(QRectF(100, 50, 290, 120))); diff --git a/tests/GuideTests.cpp b/tests/GuideTests.cpp index edcb842..693778b 100644 --- a/tests/GuideTests.cpp +++ b/tests/GuideTests.cpp @@ -451,11 +451,11 @@ void GuideTests::drawnPointsSnapToTheSnapToTargets() // The canvas's middle is no target for a drawn point. QCOMPARE(session->snappedPoint(QPointF(62, 149), 2), QPointF(64, 149)); QVERIFY(session->snapGuides.ys.empty()); - session->setSnappingEnabled(false); + session->setSnapEnabled(false); session->snapGuides = {{1}, {2}}; QCOMPARE(session->snappedPoint(QPointF(62, 20), 3), QPointF(62, 20)); QVERIFY(session->snapGuides.xs.empty() && session->snapGuides.ys.empty()); - session->setSnappingEnabled(true); + session->setSnapEnabled(true); session->setSnapEnabled(false); QCOMPARE(session->snappedPoint(QPointF(62, 20), 3), QPointF(62, 20)); QVERIFY(session->snapGuides.xs.empty() && session->snapGuides.ys.empty()); diff --git a/tests/ResizeSnapCanvasTests.cpp b/tests/ResizeSnapCanvasTests.cpp index ba6b75f..ca3a215 100644 --- a/tests/ResizeSnapCanvasTests.cpp +++ b/tests/ResizeSnapCanvasTests.cpp @@ -25,13 +25,13 @@ void ResizeSnapCanvasTests::resizedEdgesSnapUnlessCtrl() QCOMPARE(layerWith(session, shown.red).transform.size, QSizeF(246, 100)); // Snapping off, nothing snaps. session.undo(); - session.setSnappingEnabled(false); + session.setSnapEnabled(false); shown.drag(QPointF(250, 150), QPointF(396, 150)); QCOMPARE(layerWith(session, shown.red).transform.size, QSizeF(246, 100)); QCOMPARE(session.snapGuides, SnapGuides{}); // Locked, Shift frees the lock: a corner snaps both edges. session.undo(); - session.setSnappingEnabled(true); + session.setSnapEnabled(true); session.setLocksTransformRatio(true); shown.press(QPointF(250, 200)); shown.move(QPointF(396, 296), Qt::ShiftModifier); diff --git a/tests/ResizeSnapTests.cpp b/tests/ResizeSnapTests.cpp index bf18206..23ce1bd 100644 --- a/tests/ResizeSnapTests.cpp +++ b/tests/ResizeSnapTests.cpp @@ -171,10 +171,10 @@ void ResizeSnapTests::aTieTakesTheLowEdge() void ResizeSnapTests::snappingOffOrAnotherDragLeavesThePoint() { Resized resized; - resized.session.setSnappingEnabled(false); + resized.session.setSnapEnabled(false); const LayerTransform result = resized.resize(3, QPointF(110, 60), QPointF(147, 60), false); QCOMPARE(result.origin.x() + result.size.width(), 147.0); - resized.session.setSnappingEnabled(true); + resized.session.setSnapEnabled(true); const TransformDrag move{layerWith(resized.session, resized.layer).transform, QPointF(110, 60), {TransformDrag::Kind::move, 3}, std::nullopt}; QCOMPARE(resized.session.snappedResizePoint(QPointF(147, 60), move, false, {resized.layer}, 5, [&](QPointF to) { return move.updated(to, false, false); }), QPointF(147, 60)); diff --git a/tests/SnapDrawingCanvasTests.cpp b/tests/SnapDrawingCanvasTests.cpp index 2f449e3..ccb846f 100644 --- a/tests/SnapDrawingCanvasTests.cpp +++ b/tests/SnapDrawingCanvasTests.cpp @@ -17,7 +17,7 @@ namespace { struct Snapping : Canvas { Snapping() { - session.setSnappingEnabled(true); + session.setSnapEnabled(true); session.setSnapEnabled(true); session.setSnapToDocumentBounds(true); session.setSnapToLayers(false); @@ -151,7 +151,7 @@ void SnapDrawingCanvasTests::aMovedSelectionSnapsItsEdges() QVERIFY(session->beginSelectionMove()); const double whole = std::round(2 - left); QCOMPARE(session->snappedSelectionOffset(QSizeF(whole, 0), 3), QSizeF(whole - (left + whole), 0)); - session->setSnappingEnabled(false); + session->setSnapEnabled(false); QCOMPARE(session->snappedSelectionOffset(QSizeF(-8.6, 0), 3), QSizeF(-8.6, 0)); QVERIFY(session->snapGuides.xs.empty()); } diff --git a/tests/TransformSnapTests.cpp b/tests/TransformSnapTests.cpp index 3e9e7ae..b1ca3d2 100644 --- a/tests/TransformSnapTests.cpp +++ b/tests/TransformSnapTests.cpp @@ -77,13 +77,13 @@ void TransformSnapTests::snappedMoveNudgesWithinToleranceAndKeepsTheGuides() const LayerTransform snapped = session.snappedMove(draft, {a}, 5); QVERIFY(snapped != draft && !session.snapGuides.xs.empty()); QSignalSpy changes(&session, &EditorSession::changed); - session.setSnappingEnabled(false); + session.setSnapEnabled(false); QVERIFY(!session.snappingEnabled() && changes.count() == 1); QCOMPARE(session.snapGuides, SnapGuides{}); session.snapGuides = SnapGuides{{1}, {2}}; QCOMPARE(session.snappedMove(draft, {a}, 5), draft); QCOMPARE(session.snapGuides, SnapGuides{}); - session.setSnappingEnabled(true); + session.setSnapEnabled(true); QCOMPARE(session.snappedMove(draft, {a}, 5), snapped); } diff --git a/tests/ViewMenuTests.cpp b/tests/ViewMenuTests.cpp index ec6b5fa..ab475b8 100644 --- a/tests/ViewMenuTests.cpp +++ b/tests/ViewMenuTests.cpp @@ -71,15 +71,16 @@ void ViewMenuTests::thePixelGridEntryTogglesTheSession() QVERIFY(!grid.isChecked()); bar.workspace.newCanvas(); QVERIFY(grid.isChecked() && grid.isEnabled()); - // Snap, beneath it, follows the session the same way. - QAction &snap = bar.action("snap"); - QVERIFY(snap.isCheckable() && snap.isChecked() && snap.isEnabled() && snap.text() == "Snap"); - snap.trigger(); - QVERIFY(!bar.session().snappingEnabled() && !snap.isChecked()); - snap.trigger(); - QVERIFY(bar.session().snappingEnabled() && snap.isChecked()); - bar.session().setSnappingEnabled(false); - QVERIFY(!snap.isChecked()); + // One Snap for all snapping (Swift 1.4.6). + QVERIFY(!bar.window.findChild("snap")); + QStringList titles; + for (const QAction *entry : bar.window.findChildren()) + titles << entry->text(); + QCOMPARE(titles.count("Snap"), qsizetype(1)); + bar.session().setSnapEnabled(false); + QVERIFY(!bar.session().snappingEnabled() && !bar.action("snapEnabled").isChecked()); + bar.session().setSnapEnabled(true); + QVERIFY(bar.session().snappingEnabled() && bar.action("snapEnabled").isChecked()); } void ViewMenuTests::guideEntriesFollowTheSession()