12.3b: rulers, the grid and guides on the canvas, guide drags (Compositor 1.1.7)
This commit is contained in:
1 parent
338451fc35
commit
78bfa2445d
19 files changed
+718
-10
No files matched your search
@@ -114,7 +114,8 @@ Platform adaptations, each forced by a missing Qt analog:
|
||||
- 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 as QPainter's `CompositionMode_SoftLight`, which follows the PDF formula `BlendModeTests` holds. `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`.
|
||||
- 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 `compositor.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 namespace), `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` is Swift's static; `guideColor` arrives with 12.3b, its reader. `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 namespace), `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`.
|
||||
- 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`.
|
||||
- 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 the mean of its edge pixels, as Swift's `edgeTone`. `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.
|
||||
|
||||
@@ -79,6 +79,7 @@ add_library(compositor_core STATIC
|
||||
src/UI/BlendModePicker.cpp
|
||||
src/UI/BrushControls.cpp
|
||||
src/UI/CropControls.cpp
|
||||
src/UI/CanvasRulers.cpp
|
||||
src/UI/CanvasSizeSheet.cpp
|
||||
src/UI/ImageSizeSheet.cpp
|
||||
src/UI/JPEGExportSheet.cpp
|
||||
@@ -138,6 +139,7 @@ add_library(compositor_core STATIC
|
||||
src/Rendering/EditorCanvas+Crop.cpp
|
||||
src/Rendering/EditorCanvas+Effects.cpp
|
||||
src/Rendering/EditorCanvas+Grid.cpp
|
||||
src/Rendering/EditorCanvas+Guides.cpp
|
||||
src/Rendering/EditorCanvas+Keys.cpp
|
||||
src/Rendering/EditorCanvas+Gradient.cpp
|
||||
src/Rendering/EditorCanvas+Cursors.cpp
|
||||
@@ -462,6 +464,7 @@ compositor_test(FilterControlsTests)
|
||||
compositor_test(FloatingSelectionTests)
|
||||
compositor_test(CropTests)
|
||||
compositor_test(GuideTests)
|
||||
compositor_test(GuideCanvasTests)
|
||||
compositor_test(CropSessionTests)
|
||||
compositor_test(CropCanvasTests)
|
||||
compositor_test(CropControlsTests)
|
||||
|
||||
@@ -313,7 +313,10 @@ The panel of 4.6 shows and edits what the session holds so far. Each item adds i
|
||||
- 12.3a work: guides in the document, undo, the store (version 8's `guides` with Swift's refusals), the resizers and the canvas flip; the layout grid; guide drags in the session with snapping; View > Show, Rulers, Snap, Snap To, Lock Guides and Clear Guides; the five shortcut definitions; moves and crops snapping through Snap To. Evidence: every test green in the dev image, lint clean; mutants over the model, the session, the store, the codec, the resizers, the flip, the projects and the menus: all caught but the two equivalent ones AGENTS.md names.
|
||||
- 12.3a round 1: FAIL, two findings. Five behaviours passed their tests broken: a guide scaled by the wrong axis (a 200 by 50 resize now), `canEditGuides`' Levels, Hue/Saturation and filter terms (each edit opened now), the five View shortcut definitions (each remapped and read back now), the grid's lines taken along the wrong edge (392 in xs, no 304 in ys), a guide cleared mid-drag still shown (tested now); each mutant is caught. `guideColor` had no reader yet: it waits for 12.3b.
|
||||
- 12.3a round 2: PASS, no findings.
|
||||
- [ ] 12.3b Version 1.1.7, part two: the rulers beside the canvas, the grid and guides drawn over it, guides dragged from the rulers and with the Move tool.
|
||||
- [x] 12.3b Version 1.1.7, part two: the rulers beside the canvas, the grid and guides drawn over it, guides dragged from the rulers and with the Move tool.
|
||||
- 12.3b work: the rulers beside the canvas (corner, strips, ticks and labels, drags that make and drop guides), the grid and guides drawn over the canvas, guide drags with the Move tool (cursor, Escape, rests for wheels and pinches, lost releases). Evidence: every test green in the dev image, lint clean; mutants over the rulers, the overlay, the canvas's drags, keys, cursor and repaint, and the layout: all caught but the equivalent one AGENTS.md names.
|
||||
- 12.3b round 1: FAIL, three findings. Six drawn details passed their tests broken: the ruler's half-step tick, its labels' place, presence and gray, the guides' hairline width, the grid majors' ink, and Escape taking a guide drag ahead of an open transform edit (each pinned now, its mutant caught); a screenshot two tests saved in the working directory (they save beside the test binary now, the stray file removed); a no-op assertion (gone).
|
||||
- 12.3b round 2: PASS, no findings.
|
||||
- [ ] 12.4 Version 1.1.8: the Magic tool's Object mode (U²-Net and the clicked region).
|
||||
- [ ] 12.5a Version 1.2.0, part one: Photoshop PSD import (the reader, channel coder, vectors, document builder, the conversion report sheet).
|
||||
- [ ] 12.5b Version 1.2.0, part two: the Outer Glow effect, brush Smoothing, and the release's other changes to what the twin holds.
|
||||
|
||||
+15
-1
@@ -1,4 +1,5 @@
|
||||
#include "ContentView.h"
|
||||
#include "UI/CanvasRulers.h"
|
||||
#include "UI/ColorPaletteControls.h"
|
||||
#include "Logging.h"
|
||||
#include "Document/ProjectWorkspace.h"
|
||||
@@ -245,7 +246,18 @@ ContentView::ContentView(EditorSession &session, ProjectController *projects, QW
|
||||
middle->setSpacing(0);
|
||||
middle->addWidget(rail);
|
||||
middle->addWidget(divider(QFrame::VLine, this));
|
||||
middle->addWidget(canvas, 1);
|
||||
// The rulers frame the canvas above and left, Swift's stacks.
|
||||
auto *framed = new QWidget(this);
|
||||
auto *rulers = new QGridLayout(framed);
|
||||
rulers->setContentsMargins(0, 0, 0, 0);
|
||||
rulers->setSpacing(0);
|
||||
m_rulers = {new CanvasRulerCorner(framed), new CanvasRulerView(session, CanvasGuide::Axis::horizontal, *m_canvas, framed),
|
||||
new CanvasRulerView(session, CanvasGuide::Axis::vertical, *m_canvas, framed)};
|
||||
rulers->addWidget(m_rulers[0], 0, 0);
|
||||
rulers->addWidget(m_rulers[1], 0, 1);
|
||||
rulers->addWidget(m_rulers[2], 1, 0);
|
||||
rulers->addWidget(canvas, 1, 1);
|
||||
middle->addWidget(framed, 1);
|
||||
m_layersPanel->setFixedWidth(int(layersPanelWidth()));
|
||||
middle->addWidget(new PanelResizeEdge(*m_layersPanel, this));
|
||||
middle->addWidget(m_layersPanel);
|
||||
@@ -372,6 +384,8 @@ void ContentView::synchronize()
|
||||
}
|
||||
const std::optional<CanvasDocument> &document = m_session.document();
|
||||
showWelcome(!document);
|
||||
for (QWidget *ruler : m_rulers)
|
||||
ruler->setVisible(m_session.showsRulers() && document);
|
||||
m_zoom->setVisible(document.has_value());
|
||||
m_dimensions->setVisible(document.has_value());
|
||||
if (document) {
|
||||
|
||||
@@ -69,6 +69,8 @@ private:
|
||||
LayersPanel *const m_layersPanel;
|
||||
// An accent ring while a drop may land.
|
||||
QWidget *const m_dropRing;
|
||||
// Swift 1.1.7's rulers: the corner, then the two strips.
|
||||
std::vector<QWidget *> m_rulers;
|
||||
ProjectWorkspace *m_workspace = nullptr;
|
||||
// A document that appears hands the canvas the keys.
|
||||
bool m_hadDocument = false;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Swift's Guides extension, and the View menu's guide state.
|
||||
public:
|
||||
// Cyan, Photoshop's default guide colour.
|
||||
static inline const QColor guideColor = QColor::fromRgbF(0, 1, 1, 0.9f);
|
||||
static constexpr double guideHitDistance = 5;
|
||||
bool showsGrid() const { return m_showsGrid; }
|
||||
void setShowsGrid(bool shows);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "Rendering/EditorCanvas.h"
|
||||
|
||||
// Swift's isOverRuler: rulers sit above and left.
|
||||
bool CanvasView::isOverRuler(QPointF point) const
|
||||
{
|
||||
return m_session.showsRulers() && (point.x() < 0 || point.y() < 0);
|
||||
}
|
||||
|
||||
std::optional<double> CanvasView::documentPosition(CanvasGuide::Axis axis, QPointF point) const
|
||||
{
|
||||
const std::optional<CanvasDocument> &document = m_session.document();
|
||||
if (!document)
|
||||
return std::nullopt;
|
||||
const QPointF pixel = m_session.viewport.documentPoint(point, document->size());
|
||||
return axis == CanvasGuide::Axis::vertical ? pixel.x() : pixel.y();
|
||||
}
|
||||
|
||||
// A press on a guide moves it, before any layer.
|
||||
bool CanvasView::beginGuideDrag(QPointF point)
|
||||
{
|
||||
const std::optional<CanvasGuide> guide = m_session.canEditGuides() ? m_session.hitGuide(point) : std::nullopt;
|
||||
if (!guide)
|
||||
return false;
|
||||
m_session.beginGuideMove(*guide);
|
||||
m_guideDragging = true;
|
||||
m_dragCursor = QCursor(guide->axis == CanvasGuide::Axis::vertical ? Qt::SizeHorCursor : Qt::SizeVerCursor);
|
||||
setCursor(*m_dragCursor);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A move without the left button: Qt lost the release.
|
||||
bool CanvasView::guideMove(QPointF point, Qt::MouseButtons buttons)
|
||||
{
|
||||
if (!m_guideDragging)
|
||||
return false;
|
||||
if (!buttons.testFlag(Qt::LeftButton)) {
|
||||
endGuideDrag(point);
|
||||
return true;
|
||||
}
|
||||
const std::optional<GuideDrag> &drag = m_session.guideDrag();
|
||||
if (const std::optional<double> position = drag ? documentPosition(drag->axis, point) : std::nullopt)
|
||||
m_session.moveGuideDrag(*position);
|
||||
synchronizeDisplay();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Released over a ruler, the guide goes.
|
||||
void CanvasView::endGuideDrag(QPointF point)
|
||||
{
|
||||
if (!m_guideDragging)
|
||||
return;
|
||||
m_guideDragging = false;
|
||||
m_dragCursor.reset();
|
||||
m_session.finishGuideDrag(isOverRuler(point));
|
||||
updateCursor();
|
||||
}
|
||||
@@ -111,6 +111,11 @@ void CanvasView::pressKey(QKeyEvent *event, int physical)
|
||||
m_session.cancelGradient();
|
||||
} else if (cropKey(*event)) {
|
||||
return;
|
||||
} else if (event->key() == Qt::Key_Escape && m_session.guideDrag()) {
|
||||
m_session.cancelGuideDrag();
|
||||
m_guideDragging = false;
|
||||
m_dragCursor.reset();
|
||||
updateCursor();
|
||||
} else if ((event->key() == Qt::Key_Escape || enter) && m_session.transformEdit()) {
|
||||
// The keys end a drag with its edit, cursor included.
|
||||
m_transformDrag.reset();
|
||||
|
||||
@@ -29,7 +29,8 @@ bool CanvasView::event(QEvent *event)
|
||||
if (event->type() == QEvent::NativeGesture) {
|
||||
const auto *gesture = static_cast<QNativeGestureEvent *>(event);
|
||||
// A pinch, as Swift's magnify; not mid-drag or mid-stroke.
|
||||
if (gesture->gestureType() == Qt::ZoomNativeGesture && !m_transformDrag && !m_cropDrag && !m_session.brushStroke() && !m_session.warpStroke()) {
|
||||
if (gesture->gestureType() == Qt::ZoomNativeGesture && !m_transformDrag && !m_cropDrag && !m_guideDragging && !m_session.brushStroke()
|
||||
&& !m_session.warpStroke()) {
|
||||
m_session.zoom(m_session.viewport.zoom() * (1 + gesture->value()), gesture->position());
|
||||
event->accept();
|
||||
return true;
|
||||
@@ -97,7 +98,7 @@ void CanvasView::consumeFocusRequest(int request)
|
||||
|
||||
void CanvasView::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
if (!m_session.document() || m_transformDrag || m_cropDrag || m_session.brushStroke() || m_session.warpStroke())
|
||||
if (!m_session.document() || m_transformDrag || m_cropDrag || m_guideDragging || m_session.brushStroke() || m_session.warpStroke())
|
||||
return;
|
||||
// Trackpads report pixels; a wheel reports notches, lines each.
|
||||
const bool precise = !event->pixelDelta().isNull();
|
||||
@@ -146,7 +147,8 @@ void CanvasView::mousePressEvent(QMouseEvent *event)
|
||||
m_lastDragPoint = point;
|
||||
updateCursor();
|
||||
} else if (m_session.tool() == NavigationTool::move) {
|
||||
beginTransformDrag(point, event->modifiers());
|
||||
if (!beginGuideDrag(point))
|
||||
beginTransformDrag(point, event->modifiers());
|
||||
} else if (isSelectionTool(m_session.tool())) {
|
||||
lassoMouseDown(point, event->modifiers());
|
||||
updateCursor();
|
||||
@@ -211,7 +213,7 @@ void CanvasView::mouseMoveEvent(QMouseEvent *event)
|
||||
if (m_inlineTextEditor && !held)
|
||||
m_inlineTextEditor->release();
|
||||
if (pickingMove(point, event->buttons()) || targetingMove(point, event->buttons(), event->modifiers())
|
||||
|| cropMove(point, event->buttons(), event->modifiers()))
|
||||
|| cropMove(point, event->buttons(), event->modifiers()) || guideMove(point, event->buttons()))
|
||||
return;
|
||||
if (m_transformDrag) {
|
||||
dragTransform(point, event->modifiers());
|
||||
@@ -280,6 +282,7 @@ void CanvasView::mouseReleaseEvent(QMouseEvent *event)
|
||||
return;
|
||||
}
|
||||
m_session.snapGuides = {};
|
||||
endGuideDrag(event->position());
|
||||
pickingRelease();
|
||||
targetingRelease();
|
||||
brushMouseUp(event->position());
|
||||
@@ -323,6 +326,8 @@ void CanvasView::focusOutEvent(QFocusEvent *event)
|
||||
updateBrushCursor();
|
||||
cancelTransformDrag();
|
||||
endCropDrag();
|
||||
// A lost release: the guide stays where it was dragged.
|
||||
endGuideDrag(QPointF(0, 0));
|
||||
m_gradientDrag.reset();
|
||||
m_session.cancelShape();
|
||||
endSelectionGestures();
|
||||
|
||||
@@ -178,6 +178,9 @@ QCursor CanvasView::transformCursor(QPointF point, Qt::KeyboardModifiers modifie
|
||||
const std::optional<TransformOverlayGeometry> geometry = m_overlay.geometry();
|
||||
const std::optional<TransformDrag::Mode> hit = geometry ? geometry->hit(point) : std::nullopt;
|
||||
if (!hit) {
|
||||
// Guides lie under the handles, over a layer's drag.
|
||||
if (const std::optional<CanvasGuide> guide = m_session.hitGuide(point))
|
||||
return QCursor(guide->axis == CanvasGuide::Axis::vertical ? Qt::SizeHorCursor : Qt::SizeVerCursor);
|
||||
if (!pressMovesLayer(point, modifiers))
|
||||
return QCursor(Qt::ArrowCursor);
|
||||
return duplicate ? duplicateCursor(ratio) : moveCursor(ratio);
|
||||
|
||||
@@ -161,6 +161,11 @@ bool CanvasView::synchronizeDisplay()
|
||||
} else if (m_displayedTool != m_session.tool() || m_displayedBlocked != blocked || m_displayedSelectionCursor != cursorState) {
|
||||
updateCursor();
|
||||
}
|
||||
// The grid and guides redraw the view, as Swift's observation.
|
||||
if (const GuidesShown lines{m_session.showsGrid(), m_session.showsGuides(), m_session.displayedGuides()}; m_displayedGuideLines != lines) {
|
||||
update();
|
||||
m_displayedGuideLines = lines;
|
||||
}
|
||||
// A shape draft redraws the view, as Swift's observation does.
|
||||
if (const std::optional<ShapeShown> shape = shownShape(); m_displayedShape != shape) {
|
||||
update();
|
||||
|
||||
@@ -58,6 +58,9 @@ public:
|
||||
const SampleRingOverlay &sampleRing() const { return m_sampleRing; }
|
||||
// The text typed on the canvas, while one is open.
|
||||
InlineTextEditor *inlineTextEditor() const { return m_inlineTextEditor.get(); }
|
||||
// Released on the rulers, just outside the canvas.
|
||||
bool isOverRuler(QPointF point) const;
|
||||
std::optional<double> documentPosition(CanvasGuide::Axis axis, QPointF point) const;
|
||||
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
@@ -234,6 +237,10 @@ private:
|
||||
void dragCrop(QPointF point, Qt::KeyboardModifiers modifiers);
|
||||
void endCropDrag();
|
||||
bool cropKey(const QKeyEvent &key);
|
||||
// The Move tool's guide drags (EditorCanvas+Guides.cpp).
|
||||
bool beginGuideDrag(QPointF point);
|
||||
bool guideMove(QPointF point, Qt::MouseButtons buttons);
|
||||
void endGuideDrag(QPointF point);
|
||||
// Swift's targeted drag: right raises; Ctrl drags the hue.
|
||||
bool targetingMove(QPointF point, Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers);
|
||||
void targetingRelease();
|
||||
@@ -348,4 +355,13 @@ private:
|
||||
std::optional<CropSnap> m_cropSnap;
|
||||
// The crop frame as last shown, in view points.
|
||||
std::optional<QRectF> m_displayedCrop;
|
||||
bool m_guideDragging = false;
|
||||
// The grid and guides as drawn: a change redraws.
|
||||
struct GuidesShown {
|
||||
bool grid;
|
||||
bool guides;
|
||||
std::vector<CanvasGuide> lines;
|
||||
friend bool operator==(const GuidesShown &, const GuidesShown &) = default;
|
||||
};
|
||||
std::optional<GuidesShown> m_displayedGuideLines;
|
||||
};
|
||||
@@ -181,6 +181,8 @@ std::optional<std::pair<QPointF, QPointF>> TransformOverlay::gradientLine() cons
|
||||
|
||||
void TransformOverlay::draw(QPainter &context, const QPalette &palette) const
|
||||
{
|
||||
drawLayoutGrid(context);
|
||||
drawGuides(context);
|
||||
if (m_session.tool() == NavigationTool::crop)
|
||||
drawCrop(context);
|
||||
else if (const std::optional<std::pair<QPointF, QPointF>> line = gradientLine())
|
||||
@@ -359,6 +361,70 @@ void TransformOverlay::drawTransformHandles(QPainter &context, const QPalette &p
|
||||
context.restore();
|
||||
}
|
||||
|
||||
// The layout grid: solid majors, dotted subdivisions when roomy.
|
||||
void TransformOverlay::drawLayoutGrid(QPainter &context) const
|
||||
{
|
||||
const std::optional<CanvasDocument> &document = m_session.document();
|
||||
if (!m_session.showsGrid() || !document)
|
||||
return;
|
||||
const QSizeF size = document->size();
|
||||
const QTransform map = documentToView();
|
||||
const double hairline = 1 / std::max(m_session.viewport.backingScale, 1.0);
|
||||
const auto lines = [&](bool majors) {
|
||||
QPainterPath path;
|
||||
for (const double x : LayoutGrid::lines(size.width())) {
|
||||
if (LayoutGrid::isMajor(x) == majors) {
|
||||
path.moveTo(map.map(QPointF(x, 0)));
|
||||
path.lineTo(map.map(QPointF(x, size.height())));
|
||||
}
|
||||
}
|
||||
for (const double y : LayoutGrid::lines(size.height())) {
|
||||
if (LayoutGrid::isMajor(y) == majors) {
|
||||
path.moveTo(map.map(QPointF(0, y)));
|
||||
path.lineTo(map.map(QPointF(size.width(), y)));
|
||||
}
|
||||
}
|
||||
return path;
|
||||
};
|
||||
context.save();
|
||||
context.setRenderHint(QPainter::Antialiasing, true);
|
||||
context.setBrush(Qt::NoBrush);
|
||||
if (LayoutGrid::step * m_session.viewport.pointsPerPixel() >= 4) {
|
||||
QPen dotted(QColor::fromRgbF(0.55, 0.55, 0.55, 0.28), hairline, Qt::CustomDashLine, Qt::FlatCap);
|
||||
// One point on, two off: Qt counts in pen widths.
|
||||
dotted.setDashPattern({1 / hairline, 2 / hairline});
|
||||
context.setPen(dotted);
|
||||
context.drawPath(lines(false));
|
||||
}
|
||||
context.setPen(QPen(QColor::fromRgbF(0.7, 0.7, 0.7, 0.45), hairline, Qt::SolidLine, Qt::FlatCap));
|
||||
context.drawPath(lines(true));
|
||||
context.restore();
|
||||
}
|
||||
|
||||
// Guides span the whole view, past the document too.
|
||||
void TransformOverlay::drawGuides(QPainter &context) const
|
||||
{
|
||||
const std::optional<CanvasDocument> &document = m_session.document();
|
||||
if (!m_session.showsGuides() || !document)
|
||||
return;
|
||||
const std::vector<CanvasGuide> guides = m_session.displayedGuides();
|
||||
if (guides.empty())
|
||||
return;
|
||||
const CanvasViewport &viewport = m_session.viewport;
|
||||
const QSizeF view = viewport.viewSize;
|
||||
context.save();
|
||||
context.setRenderHint(QPainter::Antialiasing, true);
|
||||
context.setPen(QPen(EditorSession::guideColor, 1 / std::max(viewport.backingScale, 1.0), Qt::SolidLine, Qt::FlatCap));
|
||||
for (const CanvasGuide &guide : guides) {
|
||||
const QPointF at = viewport.viewPoint(QPointF(guide.position, guide.position), document->size());
|
||||
if (guide.axis == CanvasGuide::Axis::vertical)
|
||||
context.drawLine(QPointF(at.x(), 0), QPointF(at.x(), view.height()));
|
||||
else
|
||||
context.drawLine(QPointF(0, at.y()), QPointF(view.width(), at.y()));
|
||||
}
|
||||
context.restore();
|
||||
}
|
||||
|
||||
// While a move snaps, a line along what it met.
|
||||
void TransformOverlay::drawSnapGuides(QPainter &context, const QPalette &palette) const
|
||||
{
|
||||
|
||||
@@ -51,6 +51,8 @@ public:
|
||||
|
||||
private:
|
||||
QTransform documentToView() const;
|
||||
void drawLayoutGrid(QPainter &context) const;
|
||||
void drawGuides(QPainter &context) const;
|
||||
void drawTransformHandles(QPainter &context, const QPalette &palette) const;
|
||||
void drawGradientLine(QPainter &context, QPointF start, QPointF end) const;
|
||||
void drawCrop(QPainter &context) const;
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "UI/CanvasRulers.h"
|
||||
#include "Rendering/EditorCanvas.h"
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
CanvasRulerCorner::CanvasRulerCorner(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setFixedSize(CanvasRuler::thickness, CanvasRuler::thickness);
|
||||
}
|
||||
|
||||
void CanvasRulerCorner::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.fillRect(rect(), QColor::fromRgbF(0.2, 0.2, 0.2));
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
painter.setPen(QPen(QColor::fromRgbF(1, 1, 1, 0.28), 1));
|
||||
const double side = CanvasRuler::thickness;
|
||||
painter.drawLine(QPointF(5, side - 4), QPointF(side - 4, 5));
|
||||
}
|
||||
|
||||
CanvasRulerView::CanvasRulerView(EditorSession &session, CanvasGuide::Axis axis, CanvasView &canvas, QWidget *parent)
|
||||
: QWidget(parent), m_session(session), m_axis(axis), m_canvas(canvas)
|
||||
{
|
||||
const bool vertical = axis == CanvasGuide::Axis::vertical;
|
||||
setAccessibleName(vertical ? QStringLiteral("Vertical ruler") : QStringLiteral("Horizontal ruler"));
|
||||
setCursor(vertical ? Qt::SizeHorCursor : Qt::SizeVerCursor);
|
||||
if (vertical)
|
||||
setFixedWidth(CanvasRuler::thickness);
|
||||
else
|
||||
setFixedHeight(CanvasRuler::thickness);
|
||||
connect(&session, &EditorSession::changed, this, qOverload<>(&QWidget::update));
|
||||
}
|
||||
|
||||
double CanvasRulerView::majorStep(double pointsPerPixel)
|
||||
{
|
||||
const double target = 70 / std::max(pointsPerPixel, 0.0001);
|
||||
static constexpr std::array<double, 18> nice{1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1'000, 2'000, 2'500, 5'000, 10'000, 20'000, 25'000};
|
||||
for (const double step : nice) {
|
||||
if (step >= target)
|
||||
return step;
|
||||
}
|
||||
return 50'000;
|
||||
}
|
||||
|
||||
QString CanvasRulerView::label(double value)
|
||||
{
|
||||
return QString::number(qint64(std::round(value)));
|
||||
}
|
||||
|
||||
void CanvasRulerView::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.fillRect(rect(), QColor::fromRgbF(0.2, 0.2, 0.2));
|
||||
const std::optional<CanvasDocument> &document = m_session.document();
|
||||
if (!document)
|
||||
return;
|
||||
const bool horizontal = m_axis == CanvasGuide::Axis::horizontal;
|
||||
const QSizeF size = document->size();
|
||||
const CanvasViewport &viewport = m_session.viewport;
|
||||
const double step = majorStep(viewport.pointsPerPixel());
|
||||
const double minor = step / 10;
|
||||
const double hairline = 1 / std::max(devicePixelRatioF(), 1.0);
|
||||
// The canvas's own points, wherever the ruler sits beside it.
|
||||
const QPointF offset = inCanvas(QPointF(0, 0));
|
||||
const QPointF from = viewport.documentPoint(offset, size), to = viewport.documentPoint(offset + QPointF(width(), height()), size);
|
||||
const double start = horizontal ? from.x() : from.y(), end = horizontal ? to.x() : to.y();
|
||||
const double first = std::floor(std::min(start, end) / minor) * minor, last = std::ceil(std::max(start, end) / minor) * minor;
|
||||
if (!(minor > 0) || !std::isfinite(first) || !std::isfinite(last))
|
||||
return;
|
||||
QFont font = painter.font();
|
||||
font.setPixelSize(8);
|
||||
painter.setFont(font);
|
||||
const QColor tick = QColor::fromRgbF(0.62, 0.62, 0.62), words = QColor::fromRgbF(0.78, 0.78, 0.78);
|
||||
for (double value = first; value <= last + 0.001; value += minor) {
|
||||
const QPointF at = viewport.viewPoint(QPointF(value, value), size) - offset;
|
||||
const double view = horizontal ? at.x() : at.y();
|
||||
const double remainder = std::abs(std::remainder(value, step));
|
||||
const bool major = remainder < 0.001 || std::abs(remainder - step) < 0.001;
|
||||
const bool middle = !major && std::abs(std::remainder(value, step / 2)) < 0.001;
|
||||
const double length = major ? 8 : middle ? 5 : 3;
|
||||
painter.fillRect(horizontal ? QRectF(view - hairline / 2, height() - length, hairline, length)
|
||||
: QRectF(width() - length, view - hairline / 2, length, hairline),
|
||||
tick);
|
||||
if (!major)
|
||||
continue;
|
||||
const QString text = label(value);
|
||||
const QSizeF drawn = painter.fontMetrics().size(Qt::TextSingleLine, text);
|
||||
painter.setPen(words);
|
||||
if (horizontal) {
|
||||
painter.drawText(QRectF(QPointF(view + 2, 0), drawn), Qt::AlignLeft | Qt::AlignTop, text);
|
||||
} else {
|
||||
// Turned along the tick, as Swift draws it.
|
||||
painter.save();
|
||||
painter.translate(1, view + 2);
|
||||
painter.rotate(-90);
|
||||
painter.drawText(QRectF(QPointF(-drawn.width(), 0), drawn), Qt::AlignLeft | Qt::AlignTop, text);
|
||||
painter.restore();
|
||||
}
|
||||
}
|
||||
const QColor edge = QColor::fromRgbF(0.08, 0.08, 0.08);
|
||||
painter.fillRect(horizontal ? QRectF(0, height() - hairline, width(), hairline) : QRectF(width() - hairline, 0, hairline, height()), edge);
|
||||
}
|
||||
|
||||
// The canvas is a sibling: points go by the screen.
|
||||
QPointF CanvasRulerView::inCanvas(QPointF point) const
|
||||
{
|
||||
return m_canvas.mapFromGlobal(mapToGlobal(point));
|
||||
}
|
||||
|
||||
std::optional<double> CanvasRulerView::documentPosition(QPointF point) const
|
||||
{
|
||||
return m_canvas.documentPosition(m_axis, inCanvas(point));
|
||||
}
|
||||
|
||||
void CanvasRulerView::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() != Qt::LeftButton)
|
||||
return;
|
||||
m_canvas.setFocus(Qt::MouseFocusReason);
|
||||
const std::optional<double> position = documentPosition(event->position());
|
||||
if (!m_session.canEditGuides() || !position)
|
||||
return;
|
||||
m_session.beginGuideCreation(m_axis, *position);
|
||||
}
|
||||
|
||||
void CanvasRulerView::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (!m_session.guideDrag())
|
||||
return;
|
||||
if (const std::optional<double> position = documentPosition(event->position()))
|
||||
m_session.moveGuideDrag(*position);
|
||||
}
|
||||
|
||||
void CanvasRulerView::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() != Qt::LeftButton || !m_session.guideDrag())
|
||||
return;
|
||||
m_session.finishGuideDrag(m_canvas.isOverRuler(inCanvas(event->position())));
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include "Document/EditorSession.h"
|
||||
#include <QWidget>
|
||||
|
||||
class CanvasView;
|
||||
|
||||
namespace CanvasRuler {
|
||||
inline constexpr int thickness = 18;
|
||||
}
|
||||
|
||||
// The square where the two rulers meet.
|
||||
class CanvasRulerCorner : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CanvasRulerCorner(QWidget *parent = nullptr);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
};
|
||||
|
||||
// Swift's CanvasRulerNSView: ticks in document pixels; drag out a guide.
|
||||
class CanvasRulerView : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
CanvasRulerView(EditorSession &session, CanvasGuide::Axis axis, CanvasView &canvas, QWidget *parent = nullptr);
|
||||
// Numbered ticks about 70 points apart, 1-2-5 steps.
|
||||
static double majorStep(double pointsPerPixel);
|
||||
static QString label(double value);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
QPointF inCanvas(QPointF point) const;
|
||||
std::optional<double> documentPosition(QPointF point) const;
|
||||
|
||||
EditorSession &m_session;
|
||||
const CanvasGuide::Axis m_axis;
|
||||
CanvasView &m_canvas;
|
||||
};
|
||||
@@ -86,7 +86,7 @@ void ContentViewTests::testCreateCanvasAndNavigation()
|
||||
QCOMPARE(find<QLabel>(view, "zoomStatus").text(), QString("125%"));
|
||||
// Layouts settle in the event loop; then the picture holds.
|
||||
QCoreApplication::processEvents();
|
||||
QVERIFY(view.grab().save(QStringLiteral("EditorFoundation.png")));
|
||||
QVERIFY(view.grab().save(QCoreApplication::applicationDirPath() + QStringLiteral("/EditorFoundation.png")));
|
||||
// A project that is cleared brings a fresh welcome back.
|
||||
auto *copied = new QMimeData;
|
||||
QImage small(400, 300, QImage::Format_RGBA8888);
|
||||
@@ -354,7 +354,8 @@ void ContentViewTests::theCanvasSitsUnderTheWelcomeAndTakesTheKeysWithADocument(
|
||||
QVERIFY(canvas.isVisible());
|
||||
// The rail and the Layers panel take theirs; 480 stays.
|
||||
QVERIFY(canvas.width() >= 480 && canvas.height() >= 300);
|
||||
QCOMPARE(canvas.parentWidget()->x() + canvas.parentWidget()->width(), view.layersPanel().x() - 8);
|
||||
// Hidden rulers leave the canvas touching the resize grip.
|
||||
QCOMPARE(canvas.mapTo(&view, QPoint(canvas.width(), 0)).x(), view.layersPanel().x() - 8);
|
||||
QCOMPARE(canvas.geometry(), canvas.parentWidget()->rect());
|
||||
QTRY_VERIFY(find<QLineEdit>(view, "widthInput").hasFocus());
|
||||
session.createNewProject(300, 200);
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
#include "CanvasFixtures.h"
|
||||
#include "ContentView.h"
|
||||
#include "UI/CanvasRulers.h"
|
||||
#include "UI/LayersPanel.h"
|
||||
#include <QNativeGestureEvent>
|
||||
|
||||
// Swift 1.1.7 on the canvas: grid, guides, rulers, drags.
|
||||
namespace {
|
||||
// A 400 by 300 document filling the canvas at 100%.
|
||||
struct GuideCanvas : Shown {
|
||||
GuideCanvas() : Shown(QSize(400, 300), QSize(400, 300))
|
||||
{
|
||||
settle();
|
||||
session.zoom(1);
|
||||
session.setShowsTransformControls(false);
|
||||
QImage image(100, 60, QImage::Format_RGBA8888_Premultiplied);
|
||||
image.fill(Qt::red);
|
||||
session.insert(ImportedImage(image, image, QStringLiteral("Red")));
|
||||
session.selectTool(NavigationTool::move);
|
||||
canvas->synchronizeDisplay();
|
||||
}
|
||||
QPointF at(double x, double y) const { return session.viewport.viewPoint(QPointF(x, y), QSizeF(400, 300)); }
|
||||
QColor pixel(QPointF point) { return canvas->grab().toImage().pixelColor(point.toPoint()); }
|
||||
void press(QPointF point) { QTest::mousePress(canvas, Qt::LeftButton, Qt::NoModifier, point.toPoint()); }
|
||||
void drag(QPointF point) { ::drag(*canvas, point); }
|
||||
void release(QPointF point) { QTest::mouseRelease(canvas, Qt::LeftButton, Qt::NoModifier, point.toPoint()); }
|
||||
};
|
||||
|
||||
void hover(QWidget &widget, QPointF at)
|
||||
{
|
||||
QMouseEvent event(QEvent::MouseMove, at, at, widget.mapToGlobal(at), Qt::NoButton, Qt::NoButton, Qt::NoModifier);
|
||||
QApplication::sendEvent(&widget, &event);
|
||||
}
|
||||
|
||||
struct Painted : QObject {
|
||||
int &count;
|
||||
explicit Painted(int &count) : count(count) {}
|
||||
bool eventFilter(QObject *, QEvent *event) override
|
||||
{
|
||||
count += event->type() == QEvent::Paint;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// A hairline on a whole point spans two rows.
|
||||
bool cyan(const QColor &colour)
|
||||
{
|
||||
return colour.green() - colour.red() > 80 && colour.blue() - colour.red() > 80;
|
||||
}
|
||||
}
|
||||
|
||||
class GuideCanvasTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void moveToolDragsAGuideAndRulerDropDeletesIt();
|
||||
void rulerStepUsesNicePixelIntervals();
|
||||
void guidesAndTheGridDrawOverTheCanvas();
|
||||
void theCursorKeysAndLostReleasesEndADrag();
|
||||
void theRulersFrameTheCanvasAndDragOutGuides();
|
||||
};
|
||||
|
||||
void GuideCanvasTests::moveToolDragsAGuideAndRulerDropDeletesIt()
|
||||
{
|
||||
GuideCanvas shown;
|
||||
const CanvasGuide guide{QUuid::createUuid(), CanvasGuide::Axis::vertical, 40};
|
||||
shown.session.addGuide(guide);
|
||||
shown.session.setShowsRulers(true);
|
||||
shown.press(shown.at(40, 20));
|
||||
shown.drag(shown.at(70, 20));
|
||||
QCOMPARE(shown.session.guideDrag().value().position, 70.0);
|
||||
shown.release(shown.at(70, 20));
|
||||
QCOMPARE(shown.session.document().value().guides.front().position, 70.0);
|
||||
QVERIFY(!shown.session.guideDrag());
|
||||
QCOMPARE(shown.session.history.undoName(), QString("Move Guide"));
|
||||
// The layer under the guide never moved.
|
||||
QCOMPARE(shown.session.activeLayer().value().origin(), QPointF(150, 120));
|
||||
shown.press(shown.at(70, 20));
|
||||
shown.release(shown.at(-10, 20));
|
||||
QVERIFY2(shown.session.document().value().guides.empty(), "dropping on the ruler deletes the guide");
|
||||
// So does the top ruler, for a horizontal guide.
|
||||
shown.session.addGuide({QUuid::createUuid(), CanvasGuide::Axis::horizontal, 200});
|
||||
shown.press(shown.at(300, 200));
|
||||
shown.release(shown.at(300, -5));
|
||||
QVERIFY(shown.session.document().value().guides.empty());
|
||||
shown.session.undo();
|
||||
shown.session.undo();
|
||||
// Without rulers a drop out there deletes nothing.
|
||||
shown.session.undo();
|
||||
shown.session.setShowsRulers(false);
|
||||
shown.press(shown.at(70, 20));
|
||||
shown.release(shown.at(-10, 20));
|
||||
QCOMPARE(shown.session.document().value().guides.front().position, 70.0);
|
||||
}
|
||||
|
||||
void GuideCanvasTests::rulerStepUsesNicePixelIntervals()
|
||||
{
|
||||
QCOMPARE(CanvasRulerView::majorStep(1), 100.0);
|
||||
QCOMPARE(CanvasRulerView::majorStep(8), 10.0);
|
||||
QCOMPARE(CanvasRulerView::majorStep(0.7), 100.0);
|
||||
QCOMPARE(CanvasRulerView::majorStep(0.0001), 50'000.0);
|
||||
QCOMPARE(CanvasRulerView::label(0), QString("0"));
|
||||
QCOMPARE(CanvasRulerView::label(250), QString("250"));
|
||||
QCOMPARE(CanvasRulerView::label(-0.4), QString("0"));
|
||||
QCOMPARE(CanvasRulerView::label(-99.6), QString("-100"));
|
||||
}
|
||||
|
||||
void GuideCanvasTests::guidesAndTheGridDrawOverTheCanvas()
|
||||
{
|
||||
GuideCanvas shown;
|
||||
// A shown flag or a new guide repaints the canvas.
|
||||
QTest::qWait(50);
|
||||
int paints = 0;
|
||||
Painted counter(paints);
|
||||
shown.canvas->installEventFilter(&counter);
|
||||
shown.session.setShowsGrid(true);
|
||||
shown.canvas->synchronizeDisplay();
|
||||
QTRY_VERIFY(paints > 0);
|
||||
shown.session.setShowsGrid(false);
|
||||
shown.canvas->synchronizeDisplay();
|
||||
paints = 0;
|
||||
shown.session.addGuide({QUuid::createUuid(), CanvasGuide::Axis::vertical, 390});
|
||||
shown.canvas->synchronizeDisplay();
|
||||
QTRY_VERIFY(paints > 0);
|
||||
shown.canvas->removeEventFilter(&counter);
|
||||
// A guide spans the view in cyan; hidden, it goes.
|
||||
shown.session.addGuide({QUuid::createUuid(), CanvasGuide::Axis::horizontal, 20});
|
||||
shown.canvas->synchronizeDisplay();
|
||||
const QPointF line = shown.at(10, 20);
|
||||
QVERIFY2(cyan(shown.pixel(line)), qPrintable(shown.pixel(line).name()));
|
||||
QVERIFY(cyan(shown.pixel(line + QPointF(350, 0))));
|
||||
shown.session.addGuide({QUuid::createUuid(), CanvasGuide::Axis::vertical, 300});
|
||||
QVERIFY(cyan(shown.pixel(shown.at(300, 290))));
|
||||
// A hairline: two columns at most show it at 100%.
|
||||
const QImage hair = shown.canvas->grab().toImage();
|
||||
int columns = 0;
|
||||
for (int x = int(shown.at(300, 0).x()) - 5; x <= int(shown.at(300, 0).x()) + 5; ++x) {
|
||||
const QColor seen = hair.pixelColor(x, int(shown.at(0, 290).y()));
|
||||
columns += seen.green() - seen.red() > 30;
|
||||
}
|
||||
QVERIFY2(columns >= 1 && columns <= 2, qPrintable(QString::number(columns)));
|
||||
shown.session.setShowsGuides(false);
|
||||
QVERIFY(!cyan(shown.pixel(line)));
|
||||
// The grid's majors every 64 pixels, subdivisions dotted between.
|
||||
const QColor plain = shown.pixel(shown.at(64.5, 10.5));
|
||||
shown.session.setShowsGrid(true);
|
||||
const QColor major = shown.pixel(shown.at(64, 10) + QPointF(0.2, 0));
|
||||
QVERIFY2(major != plain, qPrintable(major.name() + plain.name()));
|
||||
// The major ink: gray 0.7 at 45%, over two columns.
|
||||
const QImage majors = shown.canvas->grab().toImage();
|
||||
const int column = int(shown.at(64, 0).x()), rowAt = int(shown.at(0, 10).y());
|
||||
const double spread = majors.pixelColor(column - 1, rowAt).red() + majors.pixelColor(column, rowAt).red() - 2.0 * plain.red();
|
||||
QVERIFY2(std::abs(spread - 0.45 * (0.7 * 255 - plain.red())) <= 4, qPrintable(QString::number(spread)));
|
||||
// The subdivision at 32 is dotted: some rows inked.
|
||||
const QImage grid = shown.canvas->grab().toImage();
|
||||
int inked = 0;
|
||||
for (int y = 0; y < 30; ++y)
|
||||
inked += grid.pixelColor(shown.at(32, y).toPoint()) != grid.pixelColor(shown.at(36, y).toPoint());
|
||||
QVERIFY2(inked > 3 && inked < 27, qPrintable(QString::number(inked)));
|
||||
// At 25% the subdivisions are closer than four points: none.
|
||||
shown.session.zoom(0.25);
|
||||
shown.canvas->synchronizeDisplay();
|
||||
const QImage quarter = shown.canvas->grab().toImage();
|
||||
QCOMPARE(quarter.pixelColor(shown.at(8, 150).toPoint()), quarter.pixelColor(shown.at(12, 150).toPoint()));
|
||||
// The major at 64 shows on a column it straddles.
|
||||
const int major64 = int(std::floor(shown.at(64, 150).x()));
|
||||
const QColor between = quarter.pixelColor(shown.at(40, 150).toPoint());
|
||||
QVERIFY(quarter.pixelColor(major64, 150) != between || quarter.pixelColor(major64 - 1, 150) != between);
|
||||
}
|
||||
|
||||
void GuideCanvasTests::theCursorKeysAndLostReleasesEndADrag()
|
||||
{
|
||||
GuideCanvas shown;
|
||||
shown.session.addGuide({QUuid::createUuid(), CanvasGuide::Axis::vertical, 40});
|
||||
shown.session.addGuide({QUuid::createUuid(), CanvasGuide::Axis::horizontal, 250});
|
||||
hover(*shown.canvas, shown.at(41, 20));
|
||||
QCOMPARE(shown.canvas->cursor().shape(), Qt::SizeHorCursor);
|
||||
hover(*shown.canvas, shown.at(60, 251));
|
||||
QCOMPARE(shown.canvas->cursor().shape(), Qt::SizeVerCursor);
|
||||
shown.session.setLocksGuides(true);
|
||||
hover(*shown.canvas, shown.at(60, 251));
|
||||
QVERIFY(shown.canvas->cursor().shape() != Qt::SizeVerCursor);
|
||||
shown.session.setLocksGuides(false);
|
||||
// Escape puts the guide back; the cursor lets go.
|
||||
shown.press(shown.at(40, 20));
|
||||
shown.drag(shown.at(90, 20));
|
||||
QCOMPARE(shown.canvas->cursor().shape(), Qt::SizeHorCursor);
|
||||
// A guide drag takes Escape before a transform edit.
|
||||
QTest::keyClick(shown.canvas, Qt::Key_Escape);
|
||||
shown.release(shown.at(90, 20));
|
||||
shown.session.beginTransform();
|
||||
QVERIFY(shown.session.transformEdit());
|
||||
shown.press(shown.at(40, 20));
|
||||
QVERIFY(shown.session.guideDrag());
|
||||
QTest::keyClick(shown.canvas, Qt::Key_Escape);
|
||||
QVERIFY(!shown.session.guideDrag() && shown.session.transformEdit());
|
||||
shown.release(shown.at(40, 20));
|
||||
shown.session.cancelTransform();
|
||||
shown.press(shown.at(40, 20));
|
||||
shown.drag(shown.at(90, 20));
|
||||
// A wheel or pinch rests while a guide is dragged.
|
||||
const CanvasViewport before = shown.session.viewport;
|
||||
QWheelEvent wheel(shown.at(90, 20), shown.canvas->mapToGlobal(shown.at(90, 20)), QPoint(0, 30), QPoint(0, 0), Qt::LeftButton,
|
||||
Qt::NoModifier, Qt::NoScrollPhase, false);
|
||||
QApplication::sendEvent(shown.canvas, &wheel);
|
||||
QNativeGestureEvent pinch(Qt::ZoomNativeGesture, QPointingDevice::primaryPointingDevice(), 2, shown.at(90, 20), shown.at(90, 20),
|
||||
shown.canvas->mapToGlobal(shown.at(90, 20)), 0.5, {}, {});
|
||||
QApplication::sendEvent(shown.canvas, &pinch);
|
||||
QCOMPARE(shown.session.viewport, before);
|
||||
QTest::keyClick(shown.canvas, Qt::Key_Escape);
|
||||
QVERIFY(!shown.session.guideDrag());
|
||||
QCOMPARE(shown.session.document().value().guides.front().position, 40.0);
|
||||
hover(*shown.canvas, shown.at(200, 150));
|
||||
QVERIFY(shown.canvas->cursor().shape() != Qt::SizeHorCursor);
|
||||
shown.release(shown.at(90, 20));
|
||||
QCOMPARE(shown.session.document().value().guides.front().position, 40.0);
|
||||
// A lost release: a buttonless move lands it.
|
||||
shown.press(shown.at(40, 20));
|
||||
shown.drag(shown.at(95, 20));
|
||||
hover(*shown.canvas, shown.at(95, 20));
|
||||
QVERIFY(!shown.session.guideDrag());
|
||||
QCOMPARE(shown.session.document().value().guides.front().position, 95.0);
|
||||
// The drag's cursor let go with it.
|
||||
hover(*shown.canvas, shown.at(300, 50));
|
||||
QVERIFY(shown.canvas->cursor().shape() != Qt::SizeHorCursor);
|
||||
// So does a lost focus.
|
||||
shown.press(shown.at(95, 20));
|
||||
shown.drag(shown.at(120, 20));
|
||||
shown.canvas->clearFocus();
|
||||
QVERIFY(!shown.session.guideDrag());
|
||||
QCOMPARE(shown.session.document().value().guides.front().position, 120.0);
|
||||
// A rename in progress leaves the guides alone.
|
||||
shown.session.setRenamingLayerID(shown.session.activeLayerID());
|
||||
shown.press(shown.at(120, 20));
|
||||
QVERIFY(!shown.session.guideDrag());
|
||||
// No drag began: a wheel still pans.
|
||||
const CanvasViewport resting = shown.session.viewport;
|
||||
QApplication::sendEvent(shown.canvas, &wheel);
|
||||
QVERIFY(!(shown.session.viewport == resting));
|
||||
shown.release(shown.at(120, 20));
|
||||
shown.session.setRenamingLayerID(std::nullopt);
|
||||
// Away from guides a press moves the layer, as before.
|
||||
shown.press(shown.at(200, 150));
|
||||
shown.drag(shown.at(230, 150));
|
||||
shown.release(shown.at(230, 150));
|
||||
QCOMPARE(shown.session.activeLayer().value().origin(), QPointF(180, 120));
|
||||
}
|
||||
|
||||
void GuideCanvasTests::theRulersFrameTheCanvasAndDragOutGuides()
|
||||
{
|
||||
EditorSession session;
|
||||
ContentView view(session);
|
||||
view.resize(900, 600);
|
||||
view.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
const QList<CanvasRulerView *> rulers = view.findChildren<CanvasRulerView *>();
|
||||
QCOMPARE(rulers.size(), 2);
|
||||
CanvasRulerView *top = rulers[0]->accessibleName() == "Horizontal ruler" ? rulers[0] : rulers[1];
|
||||
CanvasRulerView *side = top == rulers[0] ? rulers[1] : rulers[0];
|
||||
QCOMPARE(side->accessibleName(), QString("Vertical ruler"));
|
||||
session.setShowsRulers(true);
|
||||
QVERIFY(!top->isVisible());
|
||||
session.createDocument(400, 300);
|
||||
QTRY_VERIFY(top->isVisible() && side->isVisible());
|
||||
QVERIFY(view.findChild<CanvasRulerCorner *>()->isVisible());
|
||||
QVERIFY(top->height() == CanvasRuler::thickness && side->width() == CanvasRuler::thickness);
|
||||
QVERIFY(top->cursor().shape() == Qt::SizeVerCursor && side->cursor().shape() == Qt::SizeHorCursor);
|
||||
CanvasView &canvas = *view.findChild<CanvasView *>();
|
||||
// The top ruler sits above the canvas, the side left.
|
||||
QCOMPARE(top->mapTo(&view, QPoint(0, top->height())).y(), canvas.mapTo(&view, QPoint(0, 0)).y());
|
||||
QCOMPARE(side->mapTo(&view, QPoint(side->width(), 0)).x(), canvas.mapTo(&view, QPoint(0, 0)).x());
|
||||
QTRY_COMPARE(session.viewport.viewSize, QSizeF(canvas.size()));
|
||||
session.zoom(1);
|
||||
// The corner's diagonal, and rulers that follow the view.
|
||||
const QImage corner = view.findChild<CanvasRulerCorner *>()->grab().toImage();
|
||||
QVERIFY(corner.pixelColor(9, 9).lightness() > corner.pixelColor(3, 3).lightness() + 10);
|
||||
int paints = 0;
|
||||
Painted counter(paints);
|
||||
top->installEventFilter(&counter);
|
||||
session.zoom(2);
|
||||
QTRY_VERIFY(paints > 0);
|
||||
top->removeEventFilter(&counter);
|
||||
session.zoom(1);
|
||||
// A press on a ruler hands the canvas the keys.
|
||||
view.layersPanel().setFocus();
|
||||
QTRY_VERIFY(!canvas.hasFocus());
|
||||
QTest::mousePress(side, Qt::LeftButton, Qt::NoModifier, QPoint(9, 100));
|
||||
QVERIFY(canvas.hasFocus());
|
||||
QTest::mouseRelease(side, Qt::LeftButton, Qt::NoModifier, QPoint(9, 100));
|
||||
// A drag from the top ruler makes a guide.
|
||||
const QPointF target = session.viewport.viewPoint(QPointF(0, 150), QSizeF(400, 300));
|
||||
QTest::mousePress(top, Qt::LeftButton, Qt::NoModifier, QPoint(50, 9));
|
||||
QVERIFY(session.guideDrag() && session.guideDrag().value().axis == CanvasGuide::Axis::horizontal);
|
||||
const QPoint inRuler = top->mapFromGlobal(canvas.mapToGlobal(target.toPoint()));
|
||||
drag(*top, QPointF(inRuler));
|
||||
QCOMPARE(session.guideDrag().value().position, 150.0);
|
||||
QTest::mouseRelease(top, Qt::LeftButton, Qt::NoModifier, inRuler);
|
||||
QCOMPARE(session.document().value().guides.size(), size_t(1));
|
||||
QCOMPARE(session.document().value().guides.front().position, 150.0);
|
||||
QCOMPARE(session.history.undoName(), QString("New Guide"));
|
||||
// Dropped back on the ruler, no guide is made.
|
||||
QTest::mousePress(side, Qt::LeftButton, Qt::NoModifier, QPoint(9, 100));
|
||||
QTest::mouseRelease(side, Qt::LeftButton, Qt::NoModifier, QPoint(9, 100));
|
||||
QCOMPARE(session.document().value().guides.size(), size_t(1));
|
||||
// Locked guides take no drag; rulers follow the flag.
|
||||
session.setLocksGuides(true);
|
||||
QTest::mousePress(side, Qt::LeftButton, Qt::NoModifier, QPoint(9, 100));
|
||||
QVERIFY(!session.guideDrag());
|
||||
QTest::mouseRelease(side, Qt::LeftButton, Qt::NoModifier, QPoint(9, 100));
|
||||
session.setShowsRulers(false);
|
||||
QVERIFY(!top->isVisible() && !side->isVisible());
|
||||
// The ruler ticks: a labelled major every 100 pixels.
|
||||
session.setShowsRulers(true);
|
||||
const QImage strip = top->grab().toImage();
|
||||
const int zero = top->mapFromGlobal(canvas.mapToGlobal(session.viewport.viewPoint(QPointF(0, 0), QSizeF(400, 300)).toPoint())).x();
|
||||
const int row = CanvasRuler::thickness - 2;
|
||||
// A half-point hairline lands on one of two columns.
|
||||
QVERIFY(std::max(strip.pixelColor(zero, row).lightness(), strip.pixelColor(zero - 1, row).lightness()) > 120);
|
||||
// Majors reach eight points up, halves five, minors three.
|
||||
const auto lit = [&](int x, int y) { return std::max(strip.pixelColor(x, y).lightness(), strip.pixelColor(x - 1, y).lightness()) > 120; };
|
||||
QVERIFY(lit(zero + 50, CanvasRuler::thickness - 5) && !lit(zero + 50, CanvasRuler::thickness - 6));
|
||||
// A label two points past the major, in gray 0.78.
|
||||
int brightest = 0;
|
||||
for (int y = 0; y < 10; ++y) {
|
||||
for (int x = zero + 2; x < zero + 12; ++x)
|
||||
brightest = std::max(brightest, strip.pixelColor(x, y).lightness());
|
||||
}
|
||||
// Gray 0.78 at partial coverage stays well below white.
|
||||
QVERIFY2(brightest > 150 && brightest < 180, qPrintable(QString::number(brightest)));
|
||||
int between = 0;
|
||||
for (int y = 0; y < 8; ++y) {
|
||||
for (int x = zero + 30; x < zero + 45; ++x)
|
||||
between = std::max(between, strip.pixelColor(x, y).lightness());
|
||||
}
|
||||
QVERIFY(between < 80);
|
||||
QVERIFY(lit(zero, CanvasRuler::thickness - 7) && lit(zero + 10, row) && !lit(zero + 10, CanvasRuler::thickness - 5));
|
||||
QVERIFY(strip.pixelColor(zero + 5, CanvasRuler::thickness - 2).lightness() < 80);
|
||||
}
|
||||
|
||||
QTEST_MAIN(GuideCanvasTests)
|
||||
#include "GuideCanvasTests.moc"
|
||||
@@ -85,7 +85,7 @@ void ProjectWorkspaceViewTests::testCreateCanvasAndNavigation()
|
||||
action(window, "fitCanvas").trigger();
|
||||
QVERIFY(workspace.current().session.viewport.followsFit());
|
||||
QCoreApplication::processEvents();
|
||||
QVERIFY(window.grab().save(QStringLiteral("EditorFoundation.png")));
|
||||
QVERIFY(window.grab().save(QCoreApplication::applicationDirPath() + QStringLiteral("/EditorFoundation.png")));
|
||||
// New canvas opens a tab unasked; the canvas stays behind.
|
||||
action(window, "newCanvasToolbar").trigger();
|
||||
QCOMPARE(int(workspace.tabs().size()), 2);
|
||||
|
||||
Reference in new issue
Block a user