The Marquee, shapes and a moved selection snap to View › Snap To; Ctrl draws freely (13.5a)
This commit is contained in:
1 parent
64ebd1cdf9
commit
3992bc913d
11 files changed
+271
-11
No files matched your search
@@ -161,6 +161,7 @@ Platform adaptations, each forced by a missing Qt analog:
|
||||
- Select › Color Range (13.3b, Swift 1.3.4: `c3e360a`): `kernels/WandPixels.c/.h` are v1.3.4's, with `color_range_mask`. `Document/ColorRangeSelection.h/.cpp` twin Swift's file, `ColorRangeSelection+Session.h` its session half; `ColorRangeEdit` is a class in the session's `std::optional`, its `id` standing for Swift's object identity, its `image` the live composite at document size (`selectionSample` with every layer, premultiplied), so the kernel reads it directly where Swift draws it into a context first. `colorAt` averages the 3 × 3 pixels round the floored point, those past the picture clear as in Swift's context, and unpremultiplies with Swift's rounding; a point off the picture, clear or no number samples nothing. Each update is a worker of its own (`QtConcurrent::run` and a watcher a change, as Swift starts a task each) and lands only on the same edit, generation and document; it says so at once and again when its match lands, which sets the selection without a step (antialiased as the selection tools are) or, on a `MagicWandError` or `ExportError`, keeps the selection, shows the error and logs it on `omaphoto.app`. The preview is the mask as `Grayscale8` scaled smooth to twice the panel's fit, where Swift draws it at medium quality. OK applies the match as one `Color Range` step through `setSelection` (or `deselect` for a match of nothing) and always announces the close; with no colour or an error it keeps the old selection without a step; Cancel puts the old selection back. The gates (`canEditLayers`, `canUseHistory`, `canStartProjectOperation`) wait for the panel, and closing it resumes file requests (Swift's `didSet`; on opening it is a no-op, since an open panel blocks every waiter, and is left out). The sheet's writes (`setColorRangeSampleMode`, `setColorRangeHeld`, `setColorRangeFuzziness` clamped whole to 0–200, `setColorRangeInvert`) notify, an unchanged one silent. On the canvas Color Range is `picking`; a left press, Space aside, samples with Shift adding and Alt taking away (Alt first, as Swift's switch reads them) and refocuses the panel, after Levels' checks; the eyedropper cursor carries a black + or − on a white disc bottom right (`eyedropperCursor(ratio, badge)`) for the held key or the chosen eyedropper, and `syncPicking` redraws it when either changes; Shift and Alt reach `setColorRangeHeld` through the modifier monitor. Swift's `keepColorRangeCursor`, `pointerOverCanvas` and cursor-update monitor work round AppKit's cursor rects and have no twin. `UI/ColorRangeSheet` twins Swift's sheet: the three eyedroppers (`UI/SampleButton`, moved unchanged from the Hue/Saturation sheet so both share it), lit for the effective mode; the preview (black, the picture, a 20% white rim, the canvas's shape within 292 by 200); the caption in `PlaceholderText` (Alt for Option); Fuzziness as a scrubbable title, a slider and a 48-point field; Invert; the error in `BrightText`; Cancel and the default OK; 340 wide, 24 of padding. `ContentView` shows it in `colorRangePanel` while an edit is open, its close button and Escape cancelling, and the Select menu's Color Range… follows Subject, enabled by `canSelectColorRange`. Swift has no tests for this commit: `ColorRangeTests` pins the kernel, the sampling, the modes, Fuzziness, Invert, the preview, OK, Cancel, the gates, the announcements and an outline too detailed (a 2100-pixel checkerboard); `ColorRangeCanvasTests` the canvas's clicks and keys, the badged cursors, the sheet and the panel with its menu entry; `ColorRangeSheetTests` the sheet's layout, field, preview, Invert and error. Equivalent mutants: the no-colours branch's reset of the selection and preview (before any colour both hold those values already), the watcher's `deleteLater` (memory alone), the warning and `syncPicking`'s memory of the shown badge (it only spares cursor updates). Swift's `document` and `colorRange == nil` terms in `canSelectColorRange` are dead (`canEditLayers` reads both) and left out. Untested: the preview's two allocation checks and the worker's `ExportError` catch (only memory starvation reaches them), and the sheet's `releaseFocus` (without it the commit reads members that happen to live; no sanitizer runs).
|
||||
- Dialog colours (13.4a, Swift 1.3.5: `c580a38`): Export JPEG's background for transparency and Canvas Size's custom extension open the app's picker, where they opened the desktop's `QColorDialog`. `ColorPickerTarget` gains `dialog` with its `dialogTitle` (`Color Picker (JPEG Background)`); the change Swift keeps in the session as `dialogColorChange` rides in `ColorPickerState::dialogChange`, so it goes with its picker. `openDialogColorPicker` refuses while a picker is open; `previewDialogColor` hands the dialog the working colour and changes nothing of the session's, so it is silent, as Swift's `@ObservationIgnored` closure; `closeColorPicker` tells it the chosen colour or the original. `pickingForDialog` keeps the canvas from picking, and the picker's sheet leaves out "Click the canvas to sample" for a dialog. `DialogColorSwatch` lives in `UI/ColorPickerSheet+Dialog.h/.cpp`, where Swift keeps it in `ColorPickerSheet.swift`: it derives from `SwatchButton`, whose header includes the picker's. It is drawn as the brush's swatch (34 by 18, radius 4, a white ring, a black rim), named by its title, opens the picker on a click, hands it each new working colour on `changed()` (Swift's `onChange`, the last colour remembered), and closes it, keeping the colour, when hidden (Swift's `onDisappear`: Canvas Size's swatch hides when the extension leaves Custom). The sheets close it before Cancel, OK and Export, OK first so an open picker's colour is the fill, and in their destructors, before their members go, since the swatch's change writes into them. Both sheets take the session; Canvas Size reads the palette from it. A window-modal sheet blocks its window's other windows, the picker's panel among them, where a macOS sheet leaves the app's panels free: `FloatingPanel::show` parents a panel to `QApplication::activeModalWidget()` while one is up, else to its owner's window, moving a panel made earlier across; a panel under a sheet dies with it and the next is the window's again. Judged: `908bed9` (Qt delivers `applicationStateChanged` on the GUI thread, so `RecentProjects` hops nowhere) and `158cd53` (`ProjectDigest` already hashes a local's bytes). Tests: `DialogColorPickerTests` (the session, the swatch, the panel over a modal sheet, the canvas), the sheets' swatches in `JPEGExportSheetTests` and `CanvasSizeSheetTests`, the panel over the real Canvas Size sheet in `ProjectResizeTests`. Equivalent mutants: the swatch's memory of the picker's colour, kept or not (a sheet ignores a colour it holds).
|
||||
- Export JPEG's zoomable preview (13.4b, Swift 1.3.5: `ac6f309`): the exporter decodes the preview at full size, 8192 a side at most, so 100% shows the file's own pixels. `JPEGPreview` (declared in `UI/JPEGExportSheet.h`, defined in `JPEGExportSheet+Preview.cpp`, as Swift keeps it beside the sheet) is a `QAbstractScrollArea` of Swift's 560 by 330 frame: Fit, or a zoom from Swift's steps (25% to 800%, `step` a thousandth either side), 1 being one image pixel a device pixel (`shownSize` divides the export's size by the device pixel ratio, as Swift by `displayScale`, so a preview decoded smaller still spans it); an image smaller than the view sits in its middle; a new zoom keeps the view's middle on the same part of the image, or starts at the image's middle from Fit (Swift's `keepCentered`, whose `before.width > 0` guard is dead and left out); a left drag moves it (the open hand, closed while dragging, the arrow in Fit); the wheel scrolls; a left double click switches Fit and 100%; from 100% up it draws nearest, below 100% through an area-averaged copy kept per size (`QImage::scaled`, Swift's high quality; QPainter's bilinear filter aliases a big reduction), dropped only for a new image. Without mouse tracking Qt delivers no buttonless move, so a lost release cannot be seen, and a new press restarts the drag. The zoom reaches the sheet through a callback given at construction. The sheet's title row carries Fit (resting in Fit) and the two magnifiers (`LayerIcons`' `plusMagnifyingGlass` and `minusMagnifyingGlass`, redrawn on `PaletteChange`), each resting at its end, with Swift's help naming the key and the zoom; the title row sits 8 above the preview (Swift's `padding(.bottom, -8)`); the size, the result and the buttons share the bottom row, 12 apart. Swift's `previewZoom` hook routes the View menu's Fit Canvas, Actual Pixels, Zoom In and Zoom Out to the preview; a window-modal sheet blocks the window's menus, so the sheet binds those four keys itself as `QShortcut`s, as remapped (`ShortcutSettings::menu`), again on every remap, and no session hook is needed. The import error's second OK has no twin: the alert already holds one button. Tests: `JPEGPreviewTests` (each drawing against `QImage::scaled` references), `JPEGExportSheetTests`, `JPEGExportTests`' limit, the keys through the real menus in `ProjectExportTests`, device pixels in `HighDpiCanvasTests`. Equivalent mutants: the nearest branch from above 1 rather than from 1 (at 100% the averaged copy is the image itself), and dropping the averaged copy for an image already held (it only costs a new average).
|
||||
- Snapping while drawing (13.5a, Swift 1.3.5: `d6e3e93`, `8b1369a`): `snappedPoint` and `snappedSelectionOffset` live in `Crop.cpp`, Swift's file. A drawn point moves each axis to the nearest crop target within reach (the first of the nearest, reach inclusive) and shows the lines met; a moved selection snaps its rounded box's edges or middle through `TransformSnap::offset`, an axis Shift holds left out; with snapping off, or no move begun, the input comes back and the lines go. The canvas's `snappedCorner` snaps a Marquee's start (a Lasso's never), a Marquee's dragged corner, a shape's start and dragged corner, ten view points' reach, and the selection drag; Ctrl, Swift's Control as the crop and the Move tool read it, draws and moves freely and clears the lines. Every release clears them, as Swift's `mouseUp`. Tests: Swift's `drawnPointsSnapToTheSnapToTargets` in `GuideTests`; a moved selection's offsets (rounding, a locked axis, a turned ellipse's own box) and the gestures at 100% and 200% in `SnapDrawingCanvasTests`.
|
||||
- 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.
|
||||
|
||||
@@ -425,8 +425,10 @@ Reviews for this section ran on codex `gpt-6.1-sol` at `max` (agent `codex-sol`)
|
||||
- 13.4a review (Fable 5.1): round 1 FAIL (the swatch's minimize case, its white ring and radius untested); round 2 PASS.
|
||||
- [x] 13.4b Part two: Export JPEG's zoomable preview and tidier layout, the View menu's zoom on it, one OK on the import error (`ac6f309`).
|
||||
- 13.4b review (Fable 5.1): round 1 FAIL (the full-size preview passed Qt's 128 MB decode limit on large canvases, so they could not export; the kept middle's `min` untested); round 2 PASS.
|
||||
- [ ] 13.5 Part two: the marquee, shapes and a moved selection snap to View › Snap To (`d6e3e93`, `8b1369a`); View › Grid Settings… with Restore Defaults (`1c819d0`, `db5eafa`).
|
||||
- [ ] 13.6 Part three: Ctrl+Z while typing takes back what was typed (`ee68ba9`); font previews in the Type bar (`1bd1df4`).
|
||||
- [x] 13.5a Part three: the marquee, shapes and a moved selection snap to View › Snap To (`d6e3e93`, `8b1369a`).
|
||||
- 13.5a review (Fable 5.1): round 1 FAIL (test gaps: rounding, the targets without centres, a path's own box, the drag's reach when zoomed); round 2 FAIL (the nearer of two lines); round 3 FAIL (the nearer line neither first nor lowest). All fixed, each mutant caught; closed after the third round.
|
||||
- [ ] 13.5b Part four: View › Grid Settings… with Restore Defaults (`1c819d0`, `db5eafa`).
|
||||
- [ ] 13.6 Part five: Ctrl+Z while typing takes back what was typed (`ee68ba9`); font previews in the Type bar (`1bd1df4`).
|
||||
- [ ] 13.7 Versions 1.3.6–1.3.7: New Canvas presets in a More menu (`aaf3dc0`); the project's Quick Look preview (`14aad18`, `4868465`) judged: the package's preview file is format, Finder's thumbnail extension is macOS alone.
|
||||
- [ ] 13.8 Version 1.4, part one: the changes behind the GPU canvas that the CPU canvas and export share: a sharper canvas with adjustment layers (adjustment surfaces at device resolution, adjustment operations scaled with the surface, direct sampling for an upright layer at 1:1, tiled layers included) and the PSD adjustment and mask fixes (`8c0417b`); Photoshop's positive Saturation (`fe7a83d`); Levels and Hue/Saturation on all cores (`af3b568`); a blur's preview kept while the layer grows (`3ccc125`); the hierarchy kept until what shapes it changes (`d0135c8`, `dcf1c66`).
|
||||
- [ ] 13.9a Part two: blending. Clipping stacks and live-mask coverage at device resolution; an adjustment in any mode over the document and inside a stack, blended at full coverage, its alpha restored, then mixed through mask and opacity (`6dc2219`); a clipping stack composited in its base's mode in every mode, on the canvas, in export and in Copy Merged (`8ab32c9`; Qt keeps the stack's mode: a held judgment to prove); Soft Light by Photoshop's formula as Core Image computes it, on the canvas, in export and in Copy Merged (`5a8f6ce`: a 0.9 source over a 0.5 backdrop gives about 170; compare Qt's formula first); soft, partly clear painted layers blending alike in Soft Light, Overlay, Multiply and Color Dodge (`e6c964b`, `softEdgedLayersBlendTheSame`).
|
||||
|
||||
@@ -3,6 +3,10 @@ public:
|
||||
SnapGuides transformSnapTargets(const QSet<QUuid> &moving) const;
|
||||
LayerTransform snappedMove(const LayerTransform &draft, const QSet<QUuid> &moving, double tolerance);
|
||||
SnapGuides cropSnapTargets() const;
|
||||
// A drawn corner, each axis to a target within reach.
|
||||
QPointF snappedPoint(QPointF point, double tolerance);
|
||||
// A moved selection's edges or middle; a locked axis stays.
|
||||
QSizeF snappedSelectionOffset(QSizeF offset, double tolerance, bool horizontal = true, bool vertical = true);
|
||||
// The Crop tool's frame: the canvas until one is drawn.
|
||||
std::optional<QRectF> visibleCropRect() const;
|
||||
std::optional<double> cropRatio() const;
|
||||
|
||||
@@ -174,6 +174,41 @@ SnapGuides EditorSession::cropSnapTargets() const
|
||||
return alignmentSnapTargets({}, false);
|
||||
}
|
||||
|
||||
QPointF EditorSession::snappedPoint(QPointF point, double tolerance)
|
||||
{
|
||||
if (!m_snappingEnabled) {
|
||||
snapGuides = {};
|
||||
return point;
|
||||
}
|
||||
const SnapGuides targets = cropSnapTargets();
|
||||
// The first of the nearest, as Swift's `min`.
|
||||
const auto nearest = [tolerance](double value, const std::vector<double> &lines) {
|
||||
std::optional<double> best;
|
||||
for (const double line : lines)
|
||||
if (std::abs(line - value) <= tolerance && (!best || std::abs(line - value) < std::abs(*best - value)))
|
||||
best = line;
|
||||
return best;
|
||||
};
|
||||
const std::optional<double> x = nearest(point.x(), targets.xs), y = nearest(point.y(), targets.ys);
|
||||
snapGuides = {x ? std::vector<double>{*x} : std::vector<double>{}, y ? std::vector<double>{*y} : std::vector<double>{}};
|
||||
return QPointF(x.value_or(point.x()), y.value_or(point.y()));
|
||||
}
|
||||
|
||||
QSizeF EditorSession::snappedSelectionOffset(QSizeF offset, double tolerance, bool horizontal, bool vertical)
|
||||
{
|
||||
if (!m_snappingEnabled || !m_selectionMoveOrigin) {
|
||||
snapGuides = {};
|
||||
return offset;
|
||||
}
|
||||
const QSizeF whole(std::round(offset.width()), std::round(offset.height()));
|
||||
const QRectF box = m_selectionMoveOrigin->path.boundingRect().translated(whole.width(), whole.height());
|
||||
const SnapGuides targets = cropSnapTargets();
|
||||
const TransformSnap::Offset snap =
|
||||
TransformSnap::offset(box, horizontal ? targets.xs : std::vector<double>{}, vertical ? targets.ys : std::vector<double>{}, tolerance);
|
||||
snapGuides = {snap.x ? std::vector<double>{*snap.x} : std::vector<double>{}, snap.y ? std::vector<double>{*snap.y} : std::vector<double>{}};
|
||||
return whole + snap.offset;
|
||||
}
|
||||
|
||||
std::optional<QRectF> EditorSession::visibleCropRect() const
|
||||
{
|
||||
if (m_tool != NavigationTool::crop || !m_document)
|
||||
|
||||
@@ -183,7 +183,7 @@ void CanvasView::press(QMouseEvent *event, int clicks)
|
||||
} else if (m_session.tool() == NavigationTool::gradient) {
|
||||
beginGradientDrag(point);
|
||||
} else if (m_session.tool() == NavigationTool::shape) {
|
||||
m_session.beginShape(m_session.viewport.documentPoint(point, m_session.document()->size()));
|
||||
m_session.beginShape(snappedCorner(m_session.viewport.documentPoint(point, m_session.document()->size()), event->modifiers()));
|
||||
} else if (m_session.tool() == NavigationTool::crop) {
|
||||
beginCropDrag(point);
|
||||
} else if (m_session.tool() == NavigationTool::type) {
|
||||
@@ -271,7 +271,7 @@ void CanvasView::mouseMoveEvent(QMouseEvent *event)
|
||||
return;
|
||||
if (m_session.shapeDraft() && m_session.document()) {
|
||||
// Alt has no other job here: it grows from centre.
|
||||
m_session.dragShape(m_session.viewport.documentPoint(point, m_session.document()->size()),
|
||||
m_session.dragShape(snappedCorner(m_session.viewport.documentPoint(point, m_session.document()->size()), event->modifiers()),
|
||||
event->modifiers().testFlag(Qt::ShiftModifier), event->modifiers().testFlag(Qt::AltModifier));
|
||||
synchronizeDisplay();
|
||||
return;
|
||||
|
||||
@@ -67,7 +67,7 @@ void CanvasView::lassoMouseDown(QPointF point, Qt::KeyboardModifiers modifiers)
|
||||
m_session.magicWand(pixel, mode);
|
||||
return;
|
||||
}
|
||||
m_session.beginLasso(pixel, mode);
|
||||
m_session.beginLasso(m_session.tool() == NavigationTool::marquee ? snappedCorner(pixel, modifiers) : pixel, mode);
|
||||
synchronizeDisplay();
|
||||
return;
|
||||
}
|
||||
@@ -218,22 +218,41 @@ void CanvasView::dragSelection(QPointF point, Qt::KeyboardModifiers modifiers)
|
||||
return;
|
||||
const QPointF pixel = m_session.viewport.documentPoint(point, m_session.document()->size());
|
||||
QSizeF offset(pixel.x() - m_selectionDragStart->x(), pixel.y() - m_selectionDragStart->y());
|
||||
bool horizontal = true, vertical = true;
|
||||
if (modifiers.testFlag(Qt::ShiftModifier)) {
|
||||
if (std::abs(offset.width()) >= std::abs(offset.height()))
|
||||
if (std::abs(offset.width()) >= std::abs(offset.height())) {
|
||||
offset.setHeight(0);
|
||||
else
|
||||
vertical = false;
|
||||
} else {
|
||||
offset.setWidth(0);
|
||||
horizontal = false;
|
||||
}
|
||||
}
|
||||
// Snaps as a drawn Marquee does, unless Ctrl is held.
|
||||
if (modifiers.testFlag(Qt::ControlModifier))
|
||||
m_session.snapGuides = {};
|
||||
else
|
||||
offset = m_session.snappedSelectionOffset(offset, TransformSnap::distance / std::max(m_session.viewport.pointsPerPixel(), 0.0001), horizontal,
|
||||
vertical);
|
||||
m_session.moveSelection(offset);
|
||||
}
|
||||
|
||||
QPointF CanvasView::snappedCorner(QPointF pixel, Qt::KeyboardModifiers modifiers)
|
||||
{
|
||||
if (modifiers.testFlag(Qt::ControlModifier)) {
|
||||
m_session.snapGuides = {};
|
||||
return pixel;
|
||||
}
|
||||
return m_session.snappedPoint(pixel, TransformSnap::distance / std::max(m_session.viewport.pointsPerPixel(), 0.0001));
|
||||
}
|
||||
|
||||
// Alt subtracts, never from the centre; Shift squares once armed.
|
||||
void CanvasView::dragMarqueeDraft(QPointF pixel, Qt::KeyboardModifiers modifiers)
|
||||
{
|
||||
if (!modifiers.testFlag(Qt::ShiftModifier))
|
||||
m_marqueeConstrainArmed = true;
|
||||
m_marqueeDragPixel = pixel;
|
||||
m_session.dragMarquee(pixel, m_marqueeConstrainArmed && modifiers.testFlag(Qt::ShiftModifier), false);
|
||||
m_session.dragMarquee(snappedCorner(pixel, modifiers), m_marqueeConstrainArmed && modifiers.testFlag(Qt::ShiftModifier), false);
|
||||
}
|
||||
|
||||
// A frame's pan toward a pointer at the edge.
|
||||
|
||||
@@ -210,6 +210,8 @@ private:
|
||||
void endSelectionGestures();
|
||||
void dragSelection(QPointF point, Qt::KeyboardModifiers modifiers);
|
||||
void dragMarqueeDraft(QPointF pixel, Qt::KeyboardModifiers modifiers);
|
||||
// A Marquee or shape corner, snapped unless Ctrl is held.
|
||||
QPointF snappedCorner(QPointF pixel, Qt::KeyboardModifiers modifiers);
|
||||
QSizeF marqueeAutoscrollDelta(QPointF point) const;
|
||||
void updateMarqueeAutoscroll(QPointF point);
|
||||
void stepMarqueeAutoscroll();
|
||||
|
||||
@@ -53,6 +53,7 @@ private slots:
|
||||
void layoutGridLinesIncludeMajorsAndSubdivisions();
|
||||
void aDragCreatesMovesAndDeletesGuides();
|
||||
void aDragSnapsAndHitsTheNearestGuide();
|
||||
void drawnPointsSnapToTheSnapToTargets();
|
||||
};
|
||||
|
||||
void GuideTests::newGuidesUndoAndClear()
|
||||
@@ -427,5 +428,39 @@ void GuideTests::aDragSnapsAndHitsTheNearestGuide()
|
||||
QVERIFY(!session->hitGuide(near));
|
||||
}
|
||||
|
||||
// Swift's drawnPointsSnapToTheSnapToTargets, as written.
|
||||
void GuideTests::drawnPointsSnapToTheSnapToTargets()
|
||||
{
|
||||
const auto session = paintedSession();
|
||||
session->setSnapToLayers(false);
|
||||
session->setSnapToDocumentBounds(true);
|
||||
session->setShowsGrid(true);
|
||||
session->setSnapToGrid(true);
|
||||
// Each axis alone: grid every 8, the edge at 400.
|
||||
QCOMPARE(session->snappedPoint(QPointF(62, 20), 3), QPointF(64, 20));
|
||||
QCOMPARE(session->snappedPoint(QPointF(397.5, 9), 3), QPointF(400, 8));
|
||||
QVERIFY(session->snapGuides.xs == std::vector<double>{400} && session->snapGuides.ys == std::vector<double>{8});
|
||||
// A tie keeps the first line met; reach counts inclusively.
|
||||
QCOMPARE(session->snappedPoint(QPointF(4, 17), 4), QPointF(0, 16));
|
||||
// Two lines in reach: the nearer wins.
|
||||
QCOMPARE(session->snappedPoint(QPointF(3, 19), 5), QPointF(0, 16));
|
||||
QCOMPARE(session->snappedPoint(QPointF(5, 21), 5), QPointF(8, 24));
|
||||
QCOMPARE(session->snappedPoint(QPointF(63, 11), 1), QPointF(64, 11));
|
||||
// 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->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(false);
|
||||
QCOMPARE(session->snappedPoint(QPointF(62, 20), 3), QPointF(62, 20));
|
||||
QVERIFY(session->snapGuides.xs.empty() && session->snapGuides.ys.empty());
|
||||
session->setSnapEnabled(true);
|
||||
session->setShowsGrid(false);
|
||||
session->setSnapToGrid(false);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(GuideTests)
|
||||
#include "GuideTests.moc"
|
||||
@@ -212,13 +212,14 @@ void SelectionCanvasTests::focusLossEndsAMarqueeDraftAndAnOutlineMove()
|
||||
QVERIFY(session.lassoDraft().has_value());
|
||||
session.cancelLasso();
|
||||
session.selectAll();
|
||||
// Twenty across: out of the canvas edges' snapping reach.
|
||||
shown.press(QPointF(200, 150));
|
||||
shown.move(QPointF(210, 150));
|
||||
shown.move(QPointF(220, 150));
|
||||
shown.canvas->clearFocus();
|
||||
QCOMPARE(session.history.undoName(), QString("Move Selection"));
|
||||
QVERIFY(session.canUndo());
|
||||
shown.release(QPointF(210, 150));
|
||||
QCOMPARE(bounds(session), QRectF(10, 0, 400, 300));
|
||||
shown.release(QPointF(220, 150));
|
||||
QCOMPARE(bounds(session), QRectF(20, 0, 400, 300));
|
||||
}
|
||||
|
||||
void SelectionCanvasTests::shiftDeleteBeginsAContentAwareFill()
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
#include "SelectionCanvasFixtures.h"
|
||||
|
||||
// Swift 1.3.5's snapping while drawing and moving a selection.
|
||||
class SnapDrawingCanvasTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void initTestCase() { QStandardPaths::setTestModeEnabled(true); }
|
||||
void aMarqueeSnapsWhereItStartsAndEnds();
|
||||
void aShapeSnapsItsCorners();
|
||||
void aMovedSelectionSnapsUnlessCtrl();
|
||||
void theReachIsTenViewPoints();
|
||||
void aMovedSelectionSnapsItsEdges();
|
||||
};
|
||||
|
||||
namespace {
|
||||
// Only the canvas's edges are targets: 0 to 400, 300.
|
||||
struct Snapping : Canvas {
|
||||
Snapping()
|
||||
{
|
||||
session.setSnappingEnabled(true);
|
||||
session.setSnapEnabled(true);
|
||||
session.setSnapToDocumentBounds(true);
|
||||
session.setSnapToLayers(false);
|
||||
session.setSnapToGrid(false);
|
||||
}
|
||||
QRectF selected() const { return session.selection().value().path.boundingRect(); }
|
||||
};
|
||||
}
|
||||
|
||||
void SnapDrawingCanvasTests::aMarqueeSnapsWhereItStartsAndEnds()
|
||||
{
|
||||
Snapping shown;
|
||||
// Within ten points, press and drag meet the edges.
|
||||
shown.press(QPointF(6, 8));
|
||||
shown.move(QPointF(393, 291));
|
||||
QVERIFY(shown.session.snapGuides.xs == std::vector<double>{400} && shown.session.snapGuides.ys == std::vector<double>{300});
|
||||
shown.release(QPointF(393, 291));
|
||||
QCOMPARE(shown.selected(), QRectF(0, 0, 400, 300));
|
||||
QVERIFY(shown.session.snapGuides.xs.empty() && shown.session.snapGuides.ys.empty());
|
||||
// Ctrl draws freely, both ends.
|
||||
shown.session.deselect();
|
||||
shown.drag(QPointF(6, 8), QPointF(393, 291), Qt::ControlModifier);
|
||||
QCOMPARE(shown.selected(), QRectF(6, 8, 387, 283));
|
||||
// A Lasso starts where it is pressed.
|
||||
shown.session.deselect();
|
||||
shown.session.selectTool(NavigationTool::lasso);
|
||||
shown.press(QPointF(6, 8));
|
||||
QCOMPARE(shown.session.lassoDraft().value().points.front(), QPointF(6, 8));
|
||||
shown.release(QPointF(6, 8));
|
||||
}
|
||||
|
||||
void SnapDrawingCanvasTests::aShapeSnapsItsCorners()
|
||||
{
|
||||
Snapping shown;
|
||||
shown.session.selectTool(NavigationTool::shape);
|
||||
shown.press(QPointF(6, 8));
|
||||
shown.move(QPointF(393, 291));
|
||||
QCOMPARE(shown.session.shapeDraft().value().rect, QRectF(0, 0, 400, 300));
|
||||
QVERIFY(shown.session.snapGuides.xs == std::vector<double>{400});
|
||||
shown.move(QPointF(393, 291), Qt::ControlModifier);
|
||||
QCOMPARE(shown.session.shapeDraft().value().rect, QRectF(0, 0, 393, 291));
|
||||
QVERIFY(shown.session.snapGuides.xs.empty());
|
||||
shown.session.cancelShape();
|
||||
shown.press(QPointF(6, 8), Qt::ControlModifier);
|
||||
QCOMPARE(shown.session.shapeDraft().value().anchor, QPointF(6, 8));
|
||||
shown.session.cancelShape();
|
||||
}
|
||||
|
||||
void SnapDrawingCanvasTests::aMovedSelectionSnapsUnlessCtrl()
|
||||
{
|
||||
Snapping shown;
|
||||
// Drawn freely, five in from the top left.
|
||||
shown.drag(QPointF(5, 5), QPointF(55, 35), Qt::ControlModifier);
|
||||
QCOMPARE(shown.selected(), QRectF(5, 5, 50, 30));
|
||||
// Twelve up: both near edges meet the canvas.
|
||||
shown.press(QPointF(30, 20));
|
||||
shown.move(QPointF(30, 8));
|
||||
QCOMPARE(shown.selected(), QRectF(0, 0, 50, 30));
|
||||
QVERIFY(shown.session.snapGuides.xs == std::vector<double>{0} && shown.session.snapGuides.ys == std::vector<double>{0});
|
||||
// Shift keeps one axis: the locked one never snaps.
|
||||
shown.move(QPointF(30, 8), Qt::ShiftModifier);
|
||||
QCOMPARE(shown.selected(), QRectF(5, 0, 50, 30));
|
||||
QVERIFY(shown.session.snapGuides.xs.empty() && shown.session.snapGuides.ys == std::vector<double>{0});
|
||||
shown.move(QPointF(18, 19), Qt::ShiftModifier);
|
||||
QCOMPARE(shown.selected(), QRectF(0, 5, 50, 30));
|
||||
QVERIFY(shown.session.snapGuides.xs == std::vector<double>{0} && shown.session.snapGuides.ys.empty());
|
||||
// Ctrl moves freely and shows no line.
|
||||
shown.move(QPointF(30, 8), Qt::ControlModifier);
|
||||
QCOMPARE(shown.selected(), QRectF(5, -7, 50, 30));
|
||||
QVERIFY(shown.session.snapGuides.xs.empty() && shown.session.snapGuides.ys.empty());
|
||||
shown.release(QPointF(30, 8), Qt::ControlModifier);
|
||||
}
|
||||
|
||||
void SnapDrawingCanvasTests::theReachIsTenViewPoints()
|
||||
{
|
||||
Snapping shown;
|
||||
shown.session.addGuide({QUuid::createUuid(), CanvasGuide::Axis::vertical, 200});
|
||||
const QSizeF size = shown.documentSize();
|
||||
// Seven pixels off the guide: snapped at 100%, not 200%.
|
||||
shown.drag(shown.session.viewport.viewPoint(QPointF(207, 150), size), shown.session.viewport.viewPoint(QPointF(250, 180), size));
|
||||
QCOMPARE(shown.selected(), QRectF(200, 150, 50, 30));
|
||||
shown.session.deselect();
|
||||
shown.session.zoom(2);
|
||||
shown.drag(shown.session.viewport.viewPoint(QPointF(207, 150), size), shown.session.viewport.viewPoint(QPointF(250, 180), size));
|
||||
QCOMPARE(shown.selected(), QRectF(207, 150, 43, 30));
|
||||
// A moved selection's reach is ten view points too.
|
||||
QPainterPath box;
|
||||
box.addRect(210, 150, 40, 20);
|
||||
shown.session.setSelection(DocumentSelection{box}, QStringLiteral("Marquee"));
|
||||
shown.press(shown.session.viewport.viewPoint(QPointF(230, 160), size));
|
||||
shown.move(shown.session.viewport.viewPoint(QPointF(227, 160), size));
|
||||
QCOMPARE(shown.selected(), QRectF(207, 150, 40, 20));
|
||||
shown.release(shown.session.viewport.viewPoint(QPointF(227, 160), size));
|
||||
}
|
||||
|
||||
void SnapDrawingCanvasTests::aMovedSelectionSnapsItsEdges()
|
||||
{
|
||||
Snapping shown;
|
||||
EditorSession *const session = &shown.session;
|
||||
QPainterPath box;
|
||||
box.addRect(10, 10, 50, 30);
|
||||
// No move begun: the offset as it came, no lines.
|
||||
session->snapGuides = {{1}, {}};
|
||||
QCOMPARE(session->snappedSelectionOffset(QSizeF(-8.6, 0), 3), QSizeF(-8.6, 0));
|
||||
QVERIFY(session->snapGuides.xs.empty());
|
||||
session->setSelection(DocumentSelection{box}, QStringLiteral("Marquee"));
|
||||
QVERIFY(session->beginSelectionMove());
|
||||
// Whole pixels first; the left edge then meets the canvas.
|
||||
QCOMPARE(session->snappedSelectionOffset(QSizeF(-8.6, 0.4), 3), QSizeF(-10, 0));
|
||||
QVERIFY(session->snapGuides.xs == std::vector<double>{0} && session->snapGuides.ys.empty());
|
||||
// The far edge meets the far side; beyond reach, nothing.
|
||||
QCOMPARE(session->snappedSelectionOffset(QSizeF(338.6, 258.6), 3), QSizeF(340, 260));
|
||||
QVERIFY(session->snapGuides.xs == std::vector<double>{400} && session->snapGuides.ys == std::vector<double>{300});
|
||||
QCOMPARE(session->snappedSelectionOffset(QSizeF(-5, 0), 3), QSizeF(-5, 0));
|
||||
// Rounded half away from zero; the middle is no target.
|
||||
QCOMPARE(session->snappedSelectionOffset(QSizeF(8.6, 0), 3), QSizeF(9, 0));
|
||||
QCOMPARE(session->snappedSelectionOffset(QSizeF(2.5, 0), 3), QSizeF(3, 0));
|
||||
QCOMPARE(session->snappedSelectionOffset(QSizeF(163, 0), 3), QSizeF(163, 0));
|
||||
// An axis Shift holds does not snap.
|
||||
QCOMPARE(session->snappedSelectionOffset(QSizeF(-8.6, -8.6), 3, true, false), QSizeF(-10, -9));
|
||||
QCOMPARE(session->snappedSelectionOffset(QSizeF(-8.6, -8.6), 3, false, true), QSizeF(-9, -10));
|
||||
QVERIFY(session->snapGuides.xs.empty() && session->snapGuides.ys == std::vector<double>{0});
|
||||
// A turned ellipse snaps its own box, not control points.
|
||||
session->endSelectionMove();
|
||||
QPainterPath ellipse;
|
||||
ellipse.addEllipse(100, 100, 200, 60);
|
||||
ellipse = QTransform().translate(200, 130).rotate(45).translate(-200, -130).map(ellipse);
|
||||
const double left = ellipse.boundingRect().left();
|
||||
QVERIFY(left - ellipse.controlPointRect().left() > 6);
|
||||
session->setSelection(DocumentSelection{ellipse}, QStringLiteral("Marquee"));
|
||||
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);
|
||||
QCOMPARE(session->snappedSelectionOffset(QSizeF(-8.6, 0), 3), QSizeF(-8.6, 0));
|
||||
QVERIFY(session->snapGuides.xs.empty());
|
||||
}
|
||||
|
||||
QTEST_MAIN(SnapDrawingCanvasTests)
|
||||
#include "SnapDrawingCanvasTests.moc"
|
||||
@@ -180,6 +180,7 @@ compositor_test(ColorRangeCanvasTests)
|
||||
compositor_test(ColorRangeSheetTests)
|
||||
compositor_test(DialogColorPickerTests)
|
||||
compositor_test(JPEGPreviewTests)
|
||||
compositor_test(SnapDrawingCanvasTests)
|
||||
# It calls the colour-range kernel itself.
|
||||
target_include_directories(ColorRangeTests PRIVATE ${OMAPHOTO_KERNELS})
|
||||
compositor_test(DitherTests)
|
||||
|
||||
Reference in new issue
Block a user