diff --git a/AGENTS.md b/AGENTS.md index bf11ca4..3010bb3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -163,6 +163,7 @@ Platform adaptations, each forced by a missing Qt analog: - 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`. - Grid Settings (13.5b, Swift 1.3.5: `1c819d0`, `db5eafa`): `LayoutGrid` is Swift's struct: spacing 2–4096 and subdivisions 1–64, never finer than a pixel, clamped as it is made, plain members where Swift has `let`, so it stays assignable (as `CanvasSizeDraft`); its lines are counted from the origin, so an uneven step stays on the majors. `GridAppearance` holds Photoshop's presets by Swift's raw values (`allGridPresets`, `rawValue`, `presetColor`, none for Custom), the styles (`allGridStyles`, `dashes` in screen points), Custom's colour and the majors' opacity (1% at least when drawn; subdivisions at 28/45 of it). Both are the person's: `ToolDefaults` gains `integer`, `text` (empty when nothing is stored) and their `set`s, and the session reads `LayoutGrid::stored()` and `GridAppearance::stored()` as its members are made, under Swift's keys (`gridSpacing`, `gridSubdivisions`, `gridColor`, `gridCustomColor` as hex, `gridStyle`, `gridOpacity`); a stored value that cannot be read is warned of on `omaphoto.app` and left at its default, where Swift falls back in silence. `setLayoutGrid` and `setGridAppearance` store and notify, silent when unchanged. Snap To Grid, the guide snapping and the overlay read the session's grid; the overlay draws the majors in the chosen colour, opacity and dashes (counted in pen widths for Qt) and the dotted subdivisions in that colour, and `GuidesShown` carries both, so a change repaints. `ProjectController::gridSettings` lives in `ProjectController+Resize.cpp` beside the other sheets, since the controller's own file would pass 500 lines: refused without a window or while a modal sheet is up (Swift's `attachedSheet`), it shows the grid, opens `GridSettingsSheet` in the window-modal `sheet` titled Grid, previews every change through the session, and on its answer puts back the grid's visibility and either keeps the settings or restores them; nothing is undone or saved, and the project is not busy. `UI/GridSettingsSheet` twins Swift's: the title, Color (a preset menu and the `DialogColorSwatch` "Grid Color"), Style, Opacity (a half-point scrub, a slider and a 48-point field with "%", stepped by arrows, clamped), a rule, "Gridline every" with "pixels" (a point's scrub) and Subdivisions (a fifth of a point's), the note (the step to two places, grouped as Swift's `formatted()`, or the limits in `BrightText`), Cancel, Restore Defaults (Custom's colour kept) and the default OK, resting while invalid; 360 wide, 24 in, 18 apart. A field takes any whole number, as Swift's binding, so OK rests until the grid is valid, and previews only valid grids. A picked colour becomes Custom's, remembering the preset it left so picking that colour again returns to it; a preset chosen from the menu ends that. A fix beyond Swift: the picker reports 8-bit colours, so Swift's comparisons miss presets that are no byte (Light Gray's 0.7): opening the picker on Light Gray turns the grid Custom at once. The twin compares both sides in the picker's 8 bits, since its Cancel hands back the exact original, so opening and Cancel both keep a preset. Left out as dead: Swift's reset of the pick in its return branch (returning to a preset ends the pick through the preset's own change, in Swift too) and `ToolDefaults::text`'s fallback (Swift's callers pass ""). Untested: the destructor's `releaseFocus` and picker close (without them the swatch's hide writes into members already gone; no sanitizer runs). Tests: `GuideGridTests` (Swift's four grid tests, the settings kept), `GridSettingsSheetTests`, the drawing in `GuideCanvasTests`, the flow in `ProjectResizeTests`, the entry in `ViewMenuTests` and `CompositorMenusTests`. +- Typing's undo and font previews (13.6, Swift 1.3.5: `ee68ba9`, `1bd1df4`). Swift's first commit lets the text view answer Undo and Redo itself, since the window's history took them; the twin's editor has answered them since 8.9b (the keys win `ShortcutOverride`, the menus reach the focused canvas's editor), so only Swift's case is added as a test: typing in a new box and Ctrl+Z through the real menus clears it in one step. The session's `previewFont`, `keepFontPreview` and `endFontPreview` twin Swift's: open text only, each face tried from the style as it was before the first (`m_fontPreviewOriginal`, Swift's `@ObservationIgnored` member) on the draft's selection, refused when invalid or unchanged, kept on a choice, put back on a close without one. `TypeFontPicker` draws each face's name in that face through `Qt::FontRole`, which the combo's menu delegate honours, at the menu's pixel size; a face that cannot draw the letters of its own name (`QRawFont`, the face itself and not fontconfig's fallbacks) keeps the menu's font, as Swift's symbol fonts do. Which faces can is worked out once on a worker when the first picker is made (Swift's `prepareStyledNames`), joined when the menu loads, a name still missing worked out then. The picker takes Swift's preview closure at construction: a highlighted face shows (the `(Multiple)` row does not), and every close posts the revert to the event loop, since Qt reports a choice after `hidePopup`; a choice keeps the preview first, which empties the original, so the revert finds nothing. Swift's `chose` flag does the same work twice and is left out as dead. Equivalent mutants: each preview built from the draft rather than the original (the selection holds still while the menu is open), `previewFont`'s validity term, Swift's too (`setFont` keeps a valid style valid), the worker's start-once guard and its join when the menu loads (without either the same answers are worked out again, costing only time), and the letters-only test of a face's name (every installed name in the dev image is letters and hyphens DejaVu covers); untested in the dev image: a face that cannot draw its own name (only DejaVu is installed). Tests: `TypeFontPreviewTests`, the Ctrl+Z case in `InlineTextDrawTests`. - 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 "), 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. diff --git a/TASKS.md b/TASKS.md index f40d3b5..6820881 100644 --- a/TASKS.md +++ b/TASKS.md @@ -429,7 +429,8 @@ Reviews for this section ran on codex `gpt-6.1-sol` at `max` (agent `codex-sol`) - 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. - [x] 13.5b Part four: View › Grid Settings… with Restore Defaults (`1c819d0`, `db5eafa`). - 13.5b review (Fable 5.1): round 1 FAIL (the picker's Cancel turned Light Gray, Light Blue and Green into Custom, since only one side was compared in 8 bits; the drawing's dots, dashes, caps and 4-point threshold loosely tested); round 2 PASS. The CPU cap was lifted to all 32 cores during this item (scripts/mutate.py follows). -- [ ] 13.6 Part five: Ctrl+Z while typing takes back what was typed (`ee68ba9`); font previews in the Type bar (`1bd1df4`). +- [x] 13.6 Part five: Ctrl+Z while typing takes back what was typed (`ee68ba9`); font previews in the Type bar (`1bd1df4`). +- 13.6 review (Fable 5.1): round 1 FAIL (four survivors unrecorded; the code judged right against Swift); round 2 PASS. - [ ] 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`). diff --git a/src/Document/EditorSession.h b/src/Document/EditorSession.h index d4b495d..4e237ea 100644 --- a/src/Document/EditorSession.h +++ b/src/Document/EditorSession.h @@ -85,12 +85,10 @@ public: // Where the project lives on disk, once opened or saved. const std::optional<QString> &projectPath() const { return m_projectPath; } void setProjectPath(std::optional<QString> path); - bool canStartProjectOperation() const; // `ready` runs at once, or when the session allows. void waitForFileRequest(std::function<void()> ready); void waitForProjectAccess(std::function<void()> ready); - void selectLayer(std::optional<QUuid> id); void selectTool(NavigationTool value); // Tab: the current tool's next mode. @@ -432,6 +430,8 @@ private: mutable std::map<QUuid, ShapePreview> m_shapeTransformPreviewCache; std::optional<TextDraft> m_textDraft; LayerTextStyle m_textDefaults; + // The text's style before the font menu previewed faces. + std::optional<LayerTextStyle> m_fontPreviewOriginal; std::optional<QPointF> m_cloneSource; CloneSettings m_cloneSettings; // The offset aligned strokes keep, fixed as a stroke begins. diff --git a/src/Document/TypeTool+Session.h b/src/Document/TypeTool+Session.h index 5199eb3..f318ec4 100644 --- a/src/Document/TypeTool+Session.h +++ b/src/Document/TypeTool+Session.h @@ -17,6 +17,12 @@ public: bool recolorText(QUuid id, const PaletteColor &color); LayerTextStyle currentTextStyle() const; void changeTextStyle(const std::function<void(LayerTextStyle &)> &change); + // The open font menu tries the hovered face on text. + void previewFont(const QString &name); + // Chosen: the text stays as it shows. + void keepFontPreview(); + // Closed without a choice: the text goes back. + void endFontPreview(); // A text layer's name: its first words on one line. static QString layerName(const QString &content); // Point text's size: what it measures, plus padding. diff --git a/src/Document/TypeTool.cpp b/src/Document/TypeTool.cpp index f39543f..170754c 100644 --- a/src/Document/TypeTool.cpp +++ b/src/Document/TypeTool.cpp @@ -255,6 +255,39 @@ LayerTextStyle EditorSession::currentTextStyle() const return active && active->liveText() ? active->liveText()->style : m_textDefaults; } +// Only text being edited; a selected layer is not opened. +void EditorSession::previewFont(const QString &name) +{ + if (!m_textDraft) + return; + TextDraft draft = *m_textDraft; + if (!m_fontPreviewOriginal) + m_fontPreviewOriginal = draft.style; + LayerTextStyle style = *m_fontPreviewOriginal; + style.setFont(name, draft.selection); + if (!style.isValid() || style == draft.style) + return; + draft.style = style; + setTextDraft(draft); +} + +void EditorSession::keepFontPreview() +{ + m_fontPreviewOriginal.reset(); +} + +void EditorSession::endFontPreview() +{ + if (!m_fontPreviewOriginal) + return; + const LayerTextStyle original = *std::exchange(m_fontPreviewOriginal, std::nullopt); + if (m_textDraft && m_textDraft->style != original) { + TextDraft draft = *m_textDraft; + draft.style = original; + setTextDraft(draft); + } +} + void EditorSession::changeTextStyle(const std::function<void(LayerTextStyle &)> &change) { const std::optional<ImageLayer> active = activeLayer(); diff --git a/src/UI/TypeControls.cpp b/src/UI/TypeControls.cpp index 28733da..f7b6246 100644 --- a/src/UI/TypeControls.cpp +++ b/src/UI/TypeControls.cpp @@ -1,5 +1,8 @@ #include "UI/TypeControls.h" #include <QAbstractItemView> +#include <QFontInfo> +#include <QRawFont> +#include <QtConcurrent> #include <QStylePainter> #include <QSignalBlocker> #include "UI/ColorPaletteControls.h" @@ -91,13 +94,71 @@ void TextStyleField::focusOutEvent(QFocusEvent *event) QLineEdit::focusOutEvent(event); } -TypeFontPicker::TypeFontPicker(QWidget *parent) : QComboBox(parent) -{ - setAccessibleName(QStringLiteral("Font")); -} - namespace { const QString multiple = QStringLiteral("(Multiple)"); + +// A face draws its own name: each letter, its glyph. +bool drawsOwnName(const QString &name) +{ + const QRawFont face = QRawFont::fromFont(TextLayout::font(LayerTextStyle(), name)); + for (const char32_t letter : name.toUcs4()) + if (QChar::isLetter(letter) && !face.supportsCharacter(letter)) + return false; + return true; +} + +// Swift's styledNames: made once, ahead, off the UI thread. +QHash<QString, bool> &ownNames() +{ + static QHash<QString, bool> names; + return names; +} + +QFuture<QHash<QString, bool>> &preparing() +{ + static QFuture<QHash<QString, bool>> future; + return future; +} + +void prepareOwnNames() +{ + if (preparing().isValid()) + return; + preparing() = QtConcurrent::run([names = TextLayout::availableFonts()] { + QHash<QString, bool> made; + for (const QString &name : names) + made.insert(name, drawsOwnName(name)); + return made; + }); +} + +// Finished names join; one still missing is made now. +bool ownName(const QString &name) +{ + if (preparing().isFinished() && ownNames().isEmpty()) + ownNames() = preparing().result(); + if (!ownNames().contains(name)) + ownNames().insert(name, drawsOwnName(name)); + return ownNames().value(name); +} +} + +TypeFontPicker::TypeFontPicker(std::function<void(PreviewStep, const QString &)> preview, QWidget *parent) : QComboBox(parent), m_preview(std::move(preview)) +{ + setAccessibleName(QStringLiteral("Font")); + prepareOwnNames(); + // Only a face previews; (Multiple) leaves the last one showing. + connect(this, &QComboBox::highlighted, this, [this](int index) { + if (!isMultiple(index)) + m_preview(PreviewStep::show, itemText(index)); + }); +} + +// Qt reports a choice after the close: the revert waits. +void TypeFontPicker::hidePopup() +{ + QComboBox::hidePopup(); + QMetaObject::invokeMethod(this, [this] { m_preview(PreviewStep::revert, QString()); }, Qt::QueuedConnection); } bool TypeFontPicker::isMultiple(int index) const @@ -144,6 +205,15 @@ void TypeFontPicker::showPopup() const QSignalBlocker quiet(this); clear(); addItems(names); + // Each face's name in that face, at the menu's size. + const int pixels = QFontInfo(font()).pixelSize(); + for (int index = 0; index < count(); ++index) { + if (!ownName(itemText(index))) + continue; + QFont face = TextLayout::font(LayerTextStyle(), itemText(index)); + face.setPixelSize(pixels); + setItemData(index, face, Qt::FontRole); + } if (selected.isEmpty()) showMultiple(); else @@ -166,7 +236,14 @@ void TypeFontPicker::paintEvent(QPaintEvent *) } TypeControls::TypeControls(EditorSession &session, QWidget *parent) - : ToolHeaderBar(QStringLiteral("Type"), parent), m_session(session), m_font(new TypeFontPicker), + : ToolHeaderBar(QStringLiteral("Type"), parent), m_session(session), + m_font(new TypeFontPicker([this](TypeFontPicker::PreviewStep step, const QString &name) { + switch (step) { + case TypeFontPicker::PreviewStep::show: m_session.previewFont(name); break; + case TypeFontPicker::PreviewStep::revert: m_session.endFontPreview(); break; + case TypeFontPicker::PreviewStep::keep: m_session.keepFontPreview(); break; + } + })), m_size(new TextStyleField( [this] { return number(m_session.currentTextStyle().fontSize, locale()); }, [this](const QString &text) { @@ -222,6 +299,8 @@ TypeControls::TypeControls(EditorSession &session, QWidget *parent) m_font->setFixedWidth(210); m_font->setToolTip(QStringLiteral("Font face, including bold and italic variants")); connect(m_font, &QComboBox::activated, this, [this](int index) { + // The text already shows the face under the pointer: kept. + m_session.keepFontPreview(); const QString name = m_font->itemText(index); // (Multiple), or the face already shown, changes nothing. if (m_font->isMultiple(index) || name == shownFont()) diff --git a/src/UI/TypeControls.h b/src/UI/TypeControls.h index d9ee16a..fdd66af 100644 --- a/src/UI/TypeControls.h +++ b/src/UI/TypeControls.h @@ -39,10 +39,14 @@ private: class TypeFontPicker : public QComboBox { Q_OBJECT public: - explicit TypeFontPicker(QWidget *parent = nullptr); + // The open menu trying faces on the text, Swift's PreviewStep. + enum class PreviewStep { show, revert, keep }; + explicit TypeFontPicker(std::function<void(PreviewStep, const QString &)> preview, QWidget *parent = nullptr); // Shows `name`, or (Multiple) when empty; an open menu stays. void sync(const QString &name); void showPopup() override; + // Closed, the text goes back; a choice already kept it. + void hidePopup() override; // Swift's isMultiple: the top item that is no font. bool isMultiple(int index) const; @@ -53,6 +57,7 @@ protected: private: void showMultiple(); void hideMultiple(); + const std::function<void(PreviewStep, const QString &)> m_preview; bool m_loaded = false; }; diff --git a/tests/InlineTextDrawTests.cpp b/tests/InlineTextDrawTests.cpp index 582a2ec..0d03ac5 100644 --- a/tests/InlineTextDrawTests.cpp +++ b/tests/InlineTextDrawTests.cpp @@ -322,6 +322,11 @@ void InlineTextDrawTests::theEditMenuReachesOpenText() QTRY_VERIFY(canvas->hasFocus()); QTest::keyClicks(canvas, QStringLiteral("menu")); const auto content = [&] { return session.textDraft().value().style.content; }; + // Swift 1.3.5: Ctrl+Z takes back the typing in one step. + QTest::keyClick(canvas, Qt::Key_Z, Qt::ControlModifier); + QCOMPARE(content(), QString()); + QTest::keyClick(canvas, Qt::Key_Z, Qt::ControlModifier | Qt::ShiftModifier); + QCOMPARE(content(), QString("menu")); // Swift's text branch: bare entries, enabled, the text view's. QCOMPARE(bar.action("undo").text(), QString("Undo")); QVERIFY(bar.action("undo").isEnabled() && bar.action("redo").isEnabled()); diff --git a/tests/Tests.cmake b/tests/Tests.cmake index c6939f2..80012e2 100644 --- a/tests/Tests.cmake +++ b/tests/Tests.cmake @@ -183,6 +183,7 @@ compositor_test(JPEGPreviewTests) compositor_test(SnapDrawingCanvasTests) compositor_test(GuideGridTests) compositor_test(GridSettingsSheetTests) +compositor_test(TypeFontPreviewTests) # It calls the colour-range kernel itself. target_include_directories(ColorRangeTests PRIVATE ${OMAPHOTO_KERNELS}) compositor_test(DitherTests) diff --git a/tests/TypeFontPreviewTests.cpp b/tests/TypeFontPreviewTests.cpp new file mode 100644 index 0000000..2393ad6 --- /dev/null +++ b/tests/TypeFontPreviewTests.cpp @@ -0,0 +1,137 @@ +#include "InlineTextDrawFixtures.h" +#include "Rendering/TextLayout.h" +#include "UI/TypeControls.h" +#include <QAbstractItemView> +#include <QFontInfo> + +// Swift 1.3.5's font previews: faces in themselves, tried on text. +namespace { +const QString sans = QStringLiteral("DejaVuSans"); +const QString mono = QStringLiteral("DejaVuSansMono"); +const QString serif = QStringLiteral("DejaVuSerif"); + +struct Typed : DrawnText { + TypeControls *const bar = new TypeControls(session, &window); + TypeFontPicker &menu() const { return *bar->findChild<TypeFontPicker *>(QStringLiteral("typeFont")); } + Typed() + { + bar->setGeometry(0, 300, 400, 40); + bar->show(); + session.changeTextStyle([](LayerTextStyle &style) { style.fontName = sans; }); + canvas->setFocus(); + beginTextAt(session, QPointF(20, 30)); + QTest::keyClicks(canvas, QStringLiteral("Hello")); + QTest::keyClick(canvas, Qt::Key_A, Qt::ControlModifier); + } + QString face() const { return session.textDraft().value().style.uniformFontName({0, 5}).value_or(QString()); } + void hover(const QString &name) { emit menu().highlighted(menu().findText(name)); } +}; +} + +class TypeFontPreviewTests : public QObject { + Q_OBJECT +private slots: + void theMenuDrawsEachFaceInItself(); + void hoveringTriesAFaceAndClosingPutsItBack(); + void aChoiceKeepsWhatShows(); + void onlyOpenTextPreviews(); +}; + +void TypeFontPreviewTests::theMenuDrawsEachFaceInItself() +{ + Typed typed; + typed.session.changeTextStyle([](LayerTextStyle &style) { style.setFont(mono, {0, 2}); }); + // A desktop font in points: the shown pixel size counts. + QFont points = typed.menu().font(); + points.setPointSize(11); + typed.menu().setFont(points); + QCOMPARE(typed.menu().font().pixelSize(), -1); + typed.menu().showPopup(); + const int pixels = QFontInfo(typed.menu().font()).pixelSize(); + QVERIFY(pixels > 0); + QVERIFY(typed.menu().isMultiple(0) && !typed.menu().itemData(0, Qt::FontRole).isValid()); + QVERIFY(typed.menu().count() > 3); + for (int index = 1; index < typed.menu().count(); ++index) { + const QString name = typed.menu().itemText(index); + const QFont face = typed.menu().itemData(index, Qt::FontRole).value<QFont>(); + QCOMPARE(face.family(), TextLayout::font(LayerTextStyle(), name).family()); + QCOMPARE(face.styleName(), TextLayout::font(LayerTextStyle(), name).styleName()); + QCOMPARE(face.pixelSize(), pixels); + } + typed.menu().hidePopup(); +} + +void TypeFontPreviewTests::hoveringTriesAFaceAndClosingPutsItBack() +{ + Typed typed; + typed.menu().showPopup(); + typed.hover(mono); + QCOMPARE(typed.face(), mono); + // Each face from the text as it was, never stacked. + typed.hover(serif); + QCOMPARE(typed.face(), serif); + typed.menu().hidePopup(); + QCOMPARE(typed.face(), serif); + QTRY_COMPARE(typed.face(), sans); + // The next preview starts from the text as it stands. + QTest::keyClick(typed.canvas, Qt::Key_End); + QTest::keyClicks(typed.canvas, QStringLiteral("!")); + QTest::keyClick(typed.canvas, Qt::Key_A, Qt::ControlModifier); + typed.menu().showPopup(); + typed.hover(mono); + QCOMPARE(typed.session.textDraft().value().style.content, QString("Hello!")); + typed.menu().hidePopup(); + QTRY_COMPARE(typed.face(), sans); + // Only the selected letters take the face tried. + typed.session.setTextDraft([&] { + TextDraft draft = typed.session.textDraft().value(); + draft.selection = {0, 2}; + return draft; + }()); + typed.session.previewFont(mono); + QVERIFY(typed.session.textDraft().value().style.uniformFontName({0, 2}) == mono && typed.session.textDraft().value().style.fontNameAt(3) == sans); + // Tried and put back unchanged, the text is no news. + typed.session.previewFont(sans); + typed.session.endFontPreview(); + QSignalSpy changed(&typed.session, &EditorSession::changed); + typed.session.previewFont(sans); + typed.session.endFontPreview(); + QVERIFY(changed.isEmpty()); +} + +void TypeFontPreviewTests::aChoiceKeepsWhatShows() +{ + Typed typed; + typed.menu().showPopup(); + typed.hover(mono); + // Qt closes the menu, then reports the choice. + typed.menu().hidePopup(); + emit typed.menu().activated(typed.menu().findText(mono)); + QCoreApplication::processEvents(); + QCOMPARE(typed.face(), mono); + // Kept: the preview's original is gone, so no later revert. + QSignalSpy changed(&typed.session, &EditorSession::changed); + typed.session.endFontPreview(); + QVERIFY(changed.isEmpty() && typed.face() == mono); + // The face already showing changes nothing, silently. + typed.session.previewFont(mono); + QVERIFY(changed.isEmpty()); + typed.session.keepFontPreview(); + typed.session.endFontPreview(); + QVERIFY(changed.isEmpty() && typed.face() == mono); +} + +void TypeFontPreviewTests::onlyOpenTextPreviews() +{ + Typed typed; + QVERIFY(typed.session.finishText()); + const LayerTextStyle kept = typed.session.activeLayer().value().liveText().value().style; + QSignalSpy changed(&typed.session, &EditorSession::changed); + typed.session.previewFont(mono); + typed.session.endFontPreview(); + QVERIFY(changed.isEmpty() && !typed.session.textDraft()); + QCOMPARE(typed.session.activeLayer().value().liveText().value().style, kept); +} + +QTEST_MAIN(TypeFontPreviewTests) +#include "TypeFontPreviewTests.moc"