14.11: the Layers footer's buttons in the primary ink, dimmed as the menus

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 07:01:32 -04:00
1 parent e0ba02707e
commit 9611cfa11c
6 files changed
+88 -9

No files matched your search

+2 -1
View File
@@ -65,7 +65,7 @@ Platform adaptations, each forced by a missing Qt analog:
- `EditorSession::drawLiveComposite` lives in `LiveLayerMask.cpp`, as in Swift, and draws the document as the export does, in document pixels, through the displayed transforms, placements and blend modes; `LayerMaskSessionTests` compares it with the export on every render.
- `ContentView` is one project's editor area, a `QWidget` that compares what it shows with what the session holds on every `changed()`; the window around it is `ProjectWorkspaceView` (4.3c), which owns the toolbar that SwiftUI lets `ContentView` declare. `ToolHeaderBar` carries `Q_OBJECT`: Qt 6.11's `findChild` refuses a class without one at compile time. Every tool has a bar of `ToolHeaderStyle::height`, so the canvas never jumps: a tool whose bar has not arrived yet shows an empty `ToolHeaderBar`, the idle and Eyedropper bars are titles alone, Hand and Zoom share one `NavigationToolHeader`, and a bar put in late is shown at once (a layout shows a late child only from the event loop). The welcome is made anew each time the document goes, as Swift's `if document == nil` makes a fresh view, so its fields and its clipboard suggestion start over. A rail button is a `QToolButton` that toggles itself on a click before the session answers, so the click's slot sets every button again from the session, which a refused `selectTool` leaves silent. `NewCanvasSheet` takes Return and Enter from anywhere on the sheet through widget shortcuts, as SwiftUI's `.defaultAction` does: `setDefault` alone reaches only a `QDialog`. Fonts are set in pixels, Swift's points on a display of one: `setPointSize` would grow them by the screen's DPI. Swift's `.secondary` text is the palette's `PlaceholderText`, its orange warning the palette's `BrightText`, which the theme of 4.5 fills; dividers are `Mid`. Labels and hints say Ctrl and Alt for ⌘ and ⌥. The status bar's hint is an eliding label and the one item that gives way when the window is narrow, as SwiftUI truncates it; zoom and size print through the English locale, as Swift's interpolation groups digits (`1,200 × 800 px`). Swift's last branch gives the Eyedropper the Zoom tool's hint; the twin says the same. Hints and icons that follow tool state (marquee and lasso kinds, brush and smear modes, shape kinds) arrive with that state. `NavigationToolHeader` applies on Return and Escape directly and again when focus leaves, where Swift applies only on the loss of focus: a Qt widget in a window that is not active loses no focus. Text that is no number reads as zero through `toDouble`, which the `> 0` test refuses, so no `ok` flag is read; `inf` reads as a number and the viewport refuses it. `NewCanvasSheet::clipboardDimensions` reads a copied picture's size from its header through `QImageReader` (no pixel is decoded), PNG before TIFF as in Swift, sides swapped for a stored quarter turn. The importer is a `QFileDialog` for existing files, and the two alerts are `QMessageBox`es opened over the view; each is taken down again when the session's flag or error is cleared from elsewhere, as a SwiftUI binding would dismiss it.
- `LayersPanel` twins `LayersPanel.swift`: the heading with its count, `LayerAppearanceControls`, the rows or the empty words, and the footer's `QToolButton`s (new layer, folder, `LayerMaskMenu`, the effects menu since 9.6d, the adjustment menu since 9.5d, delete). Its width is `ContentView`'s `layersPanelWidth`, kept in `QSettings` under Swift's `AppStorage` key; a stored width outside `minimumWidth…maximumWidth` is the default, logged. `PanelResizeEdge` lives in `ContentView.cpp` as in Swift: an 8-point grip with the split cursor over a `Mid` hairline, dragged left to widen, whole points, within range. Tests enable `QStandardPaths::setTestModeEnabled`, which sends `QSettings` to `~/.qttest`. The delete button is Swift's `deleteLayerOrMask` (a chosen effect goes first since 9.6a, then a selected mask, then the selection; since 9.6d its words name a chosen effect first).
- `NativeLayerList` twins the `NSTableView` with a `QScrollArea` of `LayerCell`s, one a row, made anew only when the ids change (a selection or rename reconfigures the cells in place, Swift's "no reload" rule; `nativeSelectionDoesNotReloadRowsAndReorderKeepsIdentity` compares the cell pointers) and shown at once, since a layout shows late children from the event loop. Each cell lays its controls out by hand from Swift's constraints (eye, disclosure, thumbnail, mask link, mask thumbnail, name, size, the effect rows since 9.6e; the depth and a clipping mask each step 24 points in) and keeps a `ThumbnailKey` (asset identity, transform, canvas size) so a picture is redrawn only when what it shows changes. `EyeSwipeButton` and `LayerThumbnailButton` are `QToolButton`s that answer the press themselves. The eye begins a swipe and sets the rows under the drag, scrolling the list by the pointer's overshoot past the viewport as `autoscroll(with:)` does; Swift tracks the drag inside `mouseDown` with a `defer`, so the twin watches the left button's release application-wide (a menu opened mid-press takes it; another button's release leaves the swipe) and a row rebuilt mid-swipe takes the button with it, so its destructor ends the swipe from the event loop, never inside the rebuild that deletes it. The thumbnail follows Swift's `mouseDown` order: Alt on a mask chooses the mask on a release without a drag, Shift toggling it as `selectMask` does (the copy's drag arrives with 4.7), Alt elsewhere is the row's, Ctrl waits for 7.1's selection loading, Shift on a mask targets and toggles it, a plain press targets the pixels or the mask, and every press but Ctrl's then goes to the row with its modifiers, so Shift ranges from the active row without targeting first. A double click with a modifier is a second press of the button's own (Swift's `mouseDown` runs twice), and Alt's on the row's strip clips again; a plain one is left to the row, which renames as Swift's `renameClickedLayer` does for a pixel layer (text opens since 8.9a, an adjustment's settings since 9.5d). `QTest::mouseDClick` on a widget sends the double click alone; the window overload sends the press, release, double click and release a real click brings. The row's own press is `clickRow` (it first lets go of a chosen effect since 9.6e; Alt in the bottom 10 points of the whole row clips to the layer below, Ctrl extends, Shift ranges from the active row, a name click on a lone mask-selected layer commits and targets the pixels, a press on a selected row keeps the selection). A rename is typed in the row's `QLineEdit` (Return keeps, Escape cancels, leaving keeps) and the session's `renamingLayerID` is cleared before the rename, because the rename's own signal would reopen the editor; then the list takes focus, as Swift's `window.makeFirstResponder(table)`. A focus lost to the menu bar or a popup (`Qt::MenuBarFocusReason`, `Qt::PopupFocusReason`) is no leaving, for the editor and the opacity field alike: an `NSMenu` never ends editing. The name and size labels are `Qt::PlainText`, so a name is never read as markup. The context menu is the list's since 12.10a (below). Hidden rows fade through a `QGraphicsOpacityEffect`. The list's keys mirror the canvas's (tool letters, Escape and Return around a transform edit, arrows nudging under Move, where every edit sits) and Up and Down walk the rows otherwise, Shift ranging from the anchor as the table's (the far end of the selected run around the active row; the range shrinks back towards it), the row brought into view. A palette change redraws every icon from the event loop, once the widgets carry the new palette: the list's `update`, the panel's and the mask button's icons; a folder's icon keeps its ink in its `ThumbnailKey`. The canvas's application event filter already takes Shift-+ and Shift-− from the list; `BlendShortcutTests` holds both and the text-field exception.
- `NativeLayerList` twins the `NSTableView` with a `QScrollArea` of `LayerCell`s, one a row, made anew only when the ids change (a selection or rename reconfigures the cells in place, Swift's "no reload" rule; `nativeSelectionDoesNotReloadRowsAndReorderKeepsIdentity` compares the cell pointers) and shown at once, since a layout shows late children from the event loop. Each cell lays its controls out by hand from Swift's constraints (eye, disclosure, thumbnail, mask link, mask thumbnail, name, size, the effect rows since 9.6e; the depth and a clipping mask each step 24 points in) and keeps a `ThumbnailKey` (asset identity, transform, canvas size) so a picture is redrawn only when what it shows changes. `EyeSwipeButton` and `LayerThumbnailButton` are `QToolButton`s that answer the press themselves. The eye begins a swipe and sets the rows under the drag, scrolling the list by the pointer's overshoot past the viewport as `autoscroll(with:)` does; Swift tracks the drag inside `mouseDown` with a `defer`, so the twin watches the left button's release application-wide (a menu opened mid-press takes it; another button's release leaves the swipe) and a row rebuilt mid-swipe takes the button with it, so its destructor ends the swipe from the event loop, never inside the rebuild that deletes it. The thumbnail follows Swift's `mouseDown` order: Alt on a mask chooses the mask on a release without a drag, Shift toggling it as `selectMask` does (the copy's drag arrives with 4.7), Alt elsewhere is the row's, Ctrl waits for 7.1's selection loading, Shift on a mask targets and toggles it, a plain press targets the pixels or the mask, and every press but Ctrl's then goes to the row with its modifiers, so Shift ranges from the active row without targeting first. A double click with a modifier is a second press of the button's own (Swift's `mouseDown` runs twice), and Alt's on the row's strip clips again; a plain one is left to the row, which renames as Swift's `renameClickedLayer` does for a pixel layer (text opens since 8.9a, an adjustment's settings since 9.5d). `QTest::mouseDClick` on a widget sends the double click alone; the window overload sends the press, release, double click and release a real click brings. The row's own press is `clickRow` (it first lets go of a chosen effect since 9.6e; Alt in the bottom 10 points of the whole row clips to the layer below, Ctrl extends, Shift ranges from the active row, a name click on a lone mask-selected layer commits and targets the pixels, a press on a selected row keeps the selection). A rename is typed in the row's `QLineEdit` (Return keeps, Escape cancels, leaving keeps) and the session's `renamingLayerID` is cleared before the rename, because the rename's own signal would reopen the editor; then the list takes focus, as Swift's `window.makeFirstResponder(table)`. A focus lost to the menu bar or a popup (`Qt::MenuBarFocusReason`, `Qt::PopupFocusReason`) is no leaving, for the editor and the opacity field alike: an `NSMenu` never ends editing. The name and size labels are `Qt::PlainText`, so a name is never read as markup. The context menu is the list's since 12.10a (below). Hidden rows fade through a `QGraphicsOpacityEffect`. The list's keys mirror the canvas's (tool letters, Escape and Return around a transform edit, arrows nudging under Move, where every edit sits) and Up and Down walk the rows otherwise, Shift ranging from the anchor as the table's (the far end of the selected run around the active row; the range shrinks back towards it), the row brought into view. A palette change redraws every icon from the event loop, once the widgets carry the new palette: the list's `update`, the panel's icons (since 14.11) and the mask button's; a folder's icon keeps its ink in its `ThumbnailKey`. The canvas's application event filter already takes Shift-+ and Shift-− from the list; `BlendShortcutTests` holds both and the text-field exception.
- Drags in the list (`NativeLayerList+Drag.cpp`): a row pressed and moved past `startDragDistance` starts a `QDrag` carrying the row's id, or its whole selection's, one id a line under `ProjectWorkspace::layerType` (Swift writes one pasteboard item a row); Alt at the press offers `CopyAction` alone under `CanvasView::duplicateCursor`, as the table's source mask does, and the drop then duplicates. `dropTarget` is Swift's `validateDrop`: a folder's middle half takes the drop in, the rest lands above the nearer row edge or below every row, every row must pass `canPlaceLayer` (a copied folder carries its contents since 12.2a); `acceptDrop` is `acceptDrop`, the place worked out once, the order reversed into a folder, the moved rows kept selected, one `Move Layer(s)` or `Duplicate Layer(s)` step. Swift refuses a drag from another table by its source; the twin's drag carries its list's token under `sourceType` and a drop takes only its own (Qt's synthetic drop events carry no `source()`, and two projects opened from one file share their layers' ids, so neither would do). A press arms the drag with its modifiers, on the row or on a thumbnail (a thumbnail press that targets goes on to arm the row, as Swift's `mouseDown` falls through to the table); every left press first drops whatever an earlier press armed, then arms unless it clipped (Swift returns after clipping) or was Ctrl's or Shift's; only a move with the left button held past `startDragDistance` starts the drag. An Alt press on a mask watches the left button's release application-wide (a popup opened mid-press takes it), as the eye does; a release seen on a `QWindow` is not counted, since the window sees the button's own release before the button does, and another button's release leaves the press, as Swift waits for the left one. A plain drag offers Move and Copy with Move preferred, so a project target can take the Copy Swift proposes (a tab, the canvas and the New button set `CopyAction` on a valid enter, move and drop and ignore the rest); the list itself duplicates only when a copy is offered alone (Alt at the press), whatever Ctrl makes Qt propose, and sets Move or Copy itself. A drag held near the viewport's edge scrolls the list by the overshoot into a 16-point margin and keeps scrolling on a timer until the pointer moves back inside or the drag ends (a leave or a drop stops it, so no tick reads a drag that is gone), as the table autoscrolls (Wayland sends no moves while the pointer holds still); `autoscrolling()` says whether the timer runs, for the tests. An Alt click that clips sets the cursor for the next click at once, as Swift's refresh after toggling. `LayerColumn` paints the indicator: a two-point accent line between rows or the accent-filled folder row. A mask's Alt-drag carries its layer's id under `maskType`, read back through `ProjectWorkspace::layerID` (a payload that is no id names no mask, an all-zero id included), and lands on the row under the pointer through `canCopyMask`. `QDrag::exec` needs the platform's drag loop, which `offscreen` has not: `LayerDragGestureTests` defines both `QDrag::exec` overloads in the test executable, where they take libQt6Gui's place (as `ProjectReplaceTests` does with libc), record what the drag offered and return `IgnoreAction`, so presses, thresholds and actions are tested to the call; `LayerDragTests` holds the drops and `LayerDragFixtures.h` what both share. The eye and thumbnail buttons live in `NativeLayerList+Buttons.cpp`. With Alt held the viewport's cursor follows the pointer (`cursorFor`): the clipping cursor over a row's strip (create or release, drawn as paths like the symbols, refused where `canToggleClippingMask` says no), the duplicate cursor over the rest of a row (a folder's strip included, since a folder clips nothing, 12.2a) and over a mask thumbnail, the arrow otherwise and with Ctrl; Alt or Ctrl pressed anywhere, and hover over any row or control (every cell and its children track the mouse), reach an application event filter installed while the list is shown, as Swift's `flagsChanged` monitor and tracking area; the list keeps the last hover point, so a modifier change is judged where the pointer sits (`QCursor::pos` is no witness offscreen), and a hover's own modifiers are the event's.
- `EditorSession::duplicateActiveLayer` and `duplicateLayer` (`Document/SelectionClipboard.cpp`, the rest of that extension arrives with 7.4) came forward from 5.1 for the Alt-drag: the copy is the layer with a new id and " copy" after its name, inserted above it (`insertCopy`); since 12.10b `duplicateActiveLayer` is `duplicateLayers` of every selected layer (none inside a selected folder, in document order: `copiedLayerIDs`; Swift's union with the active layer is dead and left out, since `selectLayers` always keeps the active layer selected), one step, where several copies stack together above the topmost original in the panel's order, in its parent, and end up selected, the active one's copy active; since 12.2a a folder copies with every layer inside it, new ids throughout, parents and clipping links inside the copy pointing inside it and those outside kept, a collapsed folder's copy collapsed, and nothing past 10,000 layers; `duplicateLayer` selects, duplicates and places the copy inside one transaction, refusing a place `canPlaceLayer` refuses (which reads `canEditLayers`, Swift's first term) and answering false when the limit made no copy.
- `LayerMerge.cpp`, `LayerFlip.cpp` and `Filters.cpp` twin the Swift files of those names (`Filters.cpp` holds `PixelFilter::trimmed` alone until 9.x). `trimmed` returns a `Trimmed` struct where Swift returns a tuple, and reads the alpha bounds straight from a premultiplied `RGBA8888` image, drawing any other format into one first, where Swift always draws into a context. `mergePlan` is a private `MergePlan` struct. `mergeLayers` renders through `drawLiveComposite` into a document-sized context and takes the trimmed image's own size through `ImageLayer(asset, origin)`, where Swift also assigns `trimmed.transform`: at 1:1 the two are equal by construction. Its insertion index counts the kept layers before the anchor, so Swift's `?? layers.count` and the clamp are dead and left out; the hierarchy check stays (a folder selected together with its own child would put the result in a removed folder), and Swift's beeps are warnings on `omaphoto.rendering`, as is a context that cannot be allocated (Swift returns in silence). `flipLayers` reads the group box and the active layer through `value()`, since `canTransform` has proved both, and drops Swift's `ids.isEmpty` guard for the same reason; `commitTransform` has already run `finishOpacityEdit`, so Swift's second call in `mergeLayers` and `flipLayers` is left out. `LayerTransform::mirrored` is declared in `LayerTransform.h` and defined in `LayerFlip.cpp`, as Swift's extension. `beginDuplicateTransform` keeps a private `TransformDuplicate`; a selected folder carries its selected contents, which are no targets of their own, and since the layer limit can refuse a copy, Swift's `copies.isEmpty` branch (the step closed, the selection back) is kept (12.2a; the copies are found by the selection since 12.10b); a cancel removes each copy with its contents and their collapsed marks. `commitTransform` ends the duplicate after its own transaction (`endDuplicateTransform`, Swift's `defer`) on the layer path only: `selectLayers` has just made a copy active, so no mask is selected and the mask path never carries one. `cancelTransform` removes the copies, restores the selection and ends it. The Layer menu's Duplicate Layer entry calls `duplicateActiveLayer`, what Swift's `layerViaCopy` does without a selection (7.4 brings the selection); Merge shows `mergeTitle` and Flip Layer follows `canTransform`.
@@ -190,6 +190,7 @@ Platform adaptations, each forced by a missing Qt analog:
- Camera Raw's colour noise reduction (14.8, Swift 1.4.6: `a2718d9`, `42a559b`): `kernels/AdjustPixels.c` is 42a559b's, unchanged: Color smooths each pixel's red and blue differences from its Rec. 709 brightness toward the area's (each weighted by coverage, so clear pixels add no colour and pull none toward gray), green following from the brightness kept, over a reach that grows with the amount and Color Smoothness; Color Detail keeps edges where the smoothed colour itself changes across the blur's width. Tests: `CameraRawColorNoiseTests` (Swift's two: colour grain under a quarter with brightness within a level, and a run that reads nothing clear pixels left behind, made sharp by `mallopt`: the planes on the heap, `M_PERTURB` filling what is handed out, so a plane left unwritten differs from run to run; then half the amount, a red beside clear pixels kept, an edge kept by Color Detail down and across, a speck taking its surround's colour at its own brightness, translucent colour kept, and exact pixels of the upstream kernel for a faint neighbour's coverage, Color Smoothness at 0 and 100, a partial amount, a reduced preview, a lone pixel among clear ones, edges in the red or the blue difference alone, down and across, and half the Color Detail).
- Last Filter (14.9, Swift 1.4.7: `0415aa4`, `6b81d4a`): `repeatsAsLastFilter` (the Filter menu's own kinds), `FilterEdit::repeating`, and in the session `lastFilter` (a getter and a setter that notifies, Swift's public var, which its tests write), `canRepeatLastFilter` and `repeatLastFilter`, which takes a completion callback: a pending gradient lands first (`commitGradient`'s continuation), then `beginFilter(kind, true)` opens the edit without a preview unless the kind is automatic, `commitFilter` applies it with the settings it had, and its callback cancels an edit still open, so nothing is left without a panel. `commitFilter` records the kind. Swift's `edit.repeating` check after `beginFilter` is dead and left out: the repeat commits the gradient first, so `beginFilter` either opens a repeating edit or none. Filter › Last Filter is first, then a separator, titled `Last Filter: <kind>` once one is recorded, on Ctrl+Meta+F (Swift's Command-Control F), resting while `canRepeatLastFilter` does not hold; `ShortcutDefinition::all` gains it after Select Subject. `synchronizePanels` keeps the filter panel shut for a repeating edit. Equivalent mutant: `canRepeatLastFilter`'s `m_filterEdit` term (`canAdjust` refuses an open filter; Swift's too). Tests: `LastFilterTests` (Swift's two; an Image adjustment not recorded, the setter's signal, the gates, Vignette's on an empty layer, a pending gradient as its own step, Remove Background through its preview, the menu's title, key and panel, a preview only when automatic, a failed preview closed, a blur that cannot grow, a newer edit outlasting the repeat), the key and Swift's unique menu keys in `KeyboardShortcutsTests`, the Filter menu in `CompositorMenusTests`.
- The closed hand (14.10, Swift 1.4.7: `c0a1cd0`) was held already: Swift sets the hand once a drag and lets Space's key repeats put the open hand back, so its fix holds the drag's cursor; the twin's `updateCursor` ranks a pan (`m_lastDragPoint`) above Space and the Hand tool, a repeated Space release is no release, and the release asks the cursor again, so the hand opens while Space is held. `CanvasNavigationTests::theHandToolAndSpaceDragTheView` holds it through repeats, and through Space let go mid-pan, the drag going on closed until the button comes up.
- The Layers footer's ink (14.11, Swift 1.4.7: `7fb40a9`, `790195f`): `footerIcon` (in `UI/LayersPanel`, Swift's `FooterIcon`) gives the plain buttons (new layer, folder, mask, delete) the primary ink, `WindowText`, and for their disabled state the menus' own: the shape in `PlaceholderText` dimmed by the style's `generatedIconPixmap`, as Qt dims the effects and adjustment menus' icons, so a disabled button is exactly as dim as a disabled menu (Swift measures a quarter-strength white for the same end). Both inks are read from the active group, since a disabled widget's palette answers with its disabled group. The menus keep `PlaceholderText`. The panel redraws its icons from the event loop on a palette change, once its buttons carry the new palette. Tests: `LayersPanelTests::footerButtonsTakeThePrimaryInkAndDimAsTheMenus`, and the inks after a new palette in `LayerListFocusTests::aNewPaletteRedrawsEveryIcon`.
- Drops between projects: `ProjectWorkspace::draggedLayerSource` finds the tab whose document holds the dragged id (Swift reads the drag pasteboard; the twin reads the drag's `QMimeData`, which every drop target has), `canReceiveDrag(data, destination)` refuses only a layer's own tab, and `receiveProviders` copies several ids one after another, as Swift awaits each provider, so a two-row drop on the New button opens two tabs as Swift's does. `ContentView` takes such a drop over its whole area (`setWorkspace` hands it the workspace; `acceptsDrop` keeps Swift's guards: busy, the sheet, the importer, a rename; `levels` arrives with 9.x), shows the 3-point accent ring inside the canvas while targeted and hands the drop's document point on when it fell on the canvas. `ProjectTabButton` takes drops for its tab (accent fill and rim, "Add to <title>"), and `NewCanvasButton`, the toolbar's New canvas button with the action as its default, for a fresh tab ("Open in a new project tab"), both refusing an Alt drag, which duplicates in the panel; Swift's transient `NewTabDropSlot` in the strip has no twin, the New canvas button is `NewProjectDropTarget` already. Files and pictures arrived with 10.4.
- `CanvasThumbnail` draws a layer or a mask inside the canvas's box at `backingScale` 2 (the panel's pixmaps carry that ratio); a mask's ground is `LayerMask::background`, white or black by its edge's majority, as the canvas treats it (Swift's `edgeTone` mean went with 12.12b). `LayerIcons` draws the panel's SF Symbols as paths, in one palette colour, like `ToolIcons`. `BlendModePicker` is a `QComboBox` whose popup previews the highlighted mode through `previewBlendMode` for the layer it opened on (Qt calls `hidePopup` before `activated`, so the preview, the target and the highlight are cleared from the event loop, after a choice had its chance) and refuses a choice once another layer is active. `LayerAppearanceControls` is Swift's blend and opacity row: the slider's press and release are one opacity drag (the release is watched application-wide while the slider is down, so a popup opened mid-drag cannot keep the step open), the percent field applies once through its `FocusOut`, which Return and Escape cause (Swift's `releasesFocusOnCommit`), and steps with Up and Down (Swift's `isFinite` test and the steps' clamp are left out: `setLayerOpacity` refuses a value that is no finite number and clamps the rest), and another active layer makes the controls anew, as Swift's `.id` does.
- `ProjectWorkspaceView` is the `QMainWindow`: the front tab's `ContentView` as its central widget, made anew when another tab comes to the front, as SwiftUI's `.id` rebuilds it, and disabled while the workspace manages; the window holds the shown tab (`std::shared_ptr`) until the old editor is deleted, as Swift's views hold their session, so a replaced or closed tab's session never ends under its editor (`ProjectWorkspace::tab` is public for this); a `QToolBar` with the New canvas action, the `ProjectTabStrip` (which takes the toolbar's free width) 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 plain `QWidget` whose buttons are kept for the tabs that stay, made for new tabs and deleted for tabs that went, and placed by hand from `projectTabOverflow` (13.14): every button at its own size hint, so no title is squeezed below Swift's 35 points, laid out again when the strip is resized, the workspace changes or any tab's session changes; it fills no background, so the toolbar shows through (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.
+2 -1
View File
@@ -1,5 +1,6 @@
#include "UI/LayerMaskMenu.h"
#include "UI/LayerIcons.h"
#include "UI/LayersPanel.h"
#include <QEvent>
#include <QGuiApplication>
#include <QHBoxLayout>
@@ -23,7 +24,7 @@ void LayerMaskMenu::synchronize()
setEnabled(m_session.canEditMask() && !(active && active->mask));
setToolTip(m_session.selection() ? QStringLiteral("Add layer mask revealing the selection (Alt-click to hide it)")
: QStringLiteral("Add layer mask (Alt-click for a black mask)"));
setIcon(LayerIcons::pixmap(LayerIcon::addMask, 16, palette().color(QPalette::PlaceholderText), devicePixelRatio()));
setIcon(footerIcon(LayerIcon::addMask, *this));
}
void LayerMaskMenu::changeEvent(QEvent *event)
+19 -4
View File
@@ -1,6 +1,8 @@
#include "UI/LayersPanel.h"
#include "UI/LayerAppearanceControls.h"
#include "UI/LayerIcons.h"
#include <QStyle>
#include <QStyleOption>
#include "UI/LayerMaskMenu.h"
#include "UI/NativeLayerList.h"
#include <QEvent>
@@ -107,23 +109,36 @@ LayersPanel::LayersPanel(EditorSession &session, QWidget *parent)
synchronize();
}
QIcon footerIcon(LayerIcon icon, const QWidget &button)
{
// Disabled, the menus' own dimmed ink: Qt dims their icons.
QStyleOption option;
option.initFrom(&button);
const QPixmap menuInk = LayerIcons::pixmap(icon, 16, button.palette().color(QPalette::Active, QPalette::PlaceholderText), button.devicePixelRatio());
QIcon made(LayerIcons::pixmap(icon, 16, button.palette().color(QPalette::Active, QPalette::WindowText), button.devicePixelRatio()));
made.addPixmap(button.style()->generatedIconPixmap(QIcon::Disabled, menuInk, &option), QIcon::Disabled);
return made;
}
// The icons take the palette's ink, again after each theme.
void LayersPanel::applyIcons()
{
const QColor ink = palette().color(QPalette::PlaceholderText);
m_emptyIcon->setPixmap(LayerIcons::pixmap(LayerIcon::layers, 25, ink, devicePixelRatio()));
m_addBlankLayer->setIcon(LayerIcons::pixmap(LayerIcon::newLayer, 16, ink, devicePixelRatio()));
m_group->setIcon(LayerIcons::pixmap(LayerIcon::newFolder, 16, ink, devicePixelRatio()));
// Buttons in the primary ink; the two menus keep theirs.
m_addBlankLayer->setIcon(footerIcon(LayerIcon::newLayer, *m_addBlankLayer));
m_group->setIcon(footerIcon(LayerIcon::newFolder, *m_group));
m_effects->setIcon(LayerIcons::pixmap(LayerIcon::sparkles, 16, ink, devicePixelRatio()));
m_adjustments->setIcon(LayerIcons::pixmap(LayerIcon::halfFilledCircle, 16, ink, devicePixelRatio()));
m_delete->setIcon(LayerIcons::pixmap(LayerIcon::trash, 16, ink, devicePixelRatio()));
m_delete->setIcon(footerIcon(LayerIcon::trash, *m_delete));
}
void LayersPanel::changeEvent(QEvent *event)
{
QWidget::changeEvent(event);
// Once the buttons carry the new palette too.
if (event->type() == QEvent::PaletteChange)
applyIcons();
QMetaObject::invokeMethod(this, &LayersPanel::applyIcons, Qt::QueuedConnection);
}
QToolButton *LayersPanel::footerButton(const QString &name, const QString &label, const QString &tip, const std::function<void()> &run)
+6
View File
@@ -1,5 +1,7 @@
#pragma once
#include "Document/EditorSession.h"
#include "UI/LayerIcons.h"
#include <QIcon>
#include <QLabel>
#include <QStackedWidget>
#include <QToolButton>
@@ -7,7 +9,11 @@
class NativeLayerList;
// Swift's FooterIcon: full ink; disabled, as dim as menus.
QIcon footerIcon(LayerIcon icon, const QWidget &button);
// The layers panel: heading, appearance, the rows, the footer.
class LayersPanel : public QWidget {
Q_OBJECT
public:
+5 -3
View File
@@ -169,13 +169,15 @@ void LayerListFocusTests::aNewPaletteRedrawsEveryIcon()
QApplication::setPalette(changed);
const double ratio = panel.devicePixelRatio();
auto *add = panel.findChild<QToolButton *>("addBlankLayer");
QTRY_COMPARE(iconImage(add->icon(), 16), iconImage(LayerIcons::pixmap(LayerIcon::newLayer, 16, QColor(10, 200, 30), ratio), 16));
QCOMPARE(iconImage(panel.findChild<QToolButton *>("addLayerMask")->icon(), 16), iconImage(LayerIcons::pixmap(LayerIcon::addMask, 16, QColor(10, 200, 30), ratio), 16));
// Buttons take the primary ink since 14.11; menus the secondary.
QTRY_COMPARE(iconImage(add->icon(), 16), iconImage(LayerIcons::pixmap(LayerIcon::newLayer, 16, QColor(200, 10, 30), ratio), 16));
QCOMPARE(iconImage(panel.findChild<QToolButton *>("addLayerMask")->icon(), 16), iconImage(LayerIcons::pixmap(LayerIcon::addMask, 16, QColor(200, 10, 30), ratio), 16));
QCOMPARE(iconImage(panel.findChild<QToolButton *>("layerEffects")->icon(), 16), iconImage(LayerIcons::pixmap(LayerIcon::sparkles, 16, QColor(10, 200, 30), ratio), 16));
LayerCell &folder = *panel.list().cells().at(0);
QTRY_COMPARE(iconImage(folder.findChild<QToolButton *>("layerEye")->icon(), 16), iconImage(LayerIcons::pixmap(LayerIcon::eye, 16, QColor(200, 10, 30), ratio), 16));
QCOMPARE(iconImage(folder.thumbnail().icon(), 36), iconImage(LayerIcons::pixmap(LayerIcon::folder, 36 * 0.8, QColor(200, 10, 30), ratio), 36));
QApplication::setPalette(before);
QTRY_COMPARE(iconImage(add->icon(), 16), iconImage(LayerIcons::pixmap(LayerIcon::newLayer, 16, before.color(QPalette::PlaceholderText), ratio), 16));
QTRY_COMPARE(iconImage(add->icon(), 16), iconImage(LayerIcons::pixmap(LayerIcon::newLayer, 16, before.color(QPalette::Active, QPalette::WindowText), ratio), 16));
}
void LayerListFocusTests::aSwipeReleasedOverAPopupStillEnds()
+54
View File
@@ -7,6 +7,8 @@
#include <QStackedWidget>
#include <QToolButton>
#include <QMenu>
#include <QStyle>
#include <QStyleOption>
#include <QtTest>
#include "SessionFixtures.h"
@@ -20,6 +22,18 @@ template <typename Widget> Widget &find(QWidget &root, const char *name)
return *found;
}
// The brightest ink an icon draws in a mode.
QColor brightest(const QIcon &icon, QIcon::Mode mode)
{
const QImage drawn = icon.pixmap(16, mode).toImage().convertToFormat(QImage::Format_ARGB32);
QColor best(0, 0, 0, 0);
for (int y = 0; y < drawn.height(); ++y)
for (int x = 0; x < drawn.width(); ++x)
if (drawn.pixelColor(x, y).alpha() > best.alpha())
best = drawn.pixelColor(x, y);
return best;
}
void drag(QWidget &edge, int fromX, int toX)
{
const QPoint start(fromX, 20), end(toX, 20);
@@ -40,6 +54,7 @@ private slots:
void theEdgeResizesThePanelWithinRangeAndRemembersIt();
void aStoredWidthOutOfRangeIsTheDefault();
void theMaskButtonUsesTheSelection();
void footerButtonsTakeThePrimaryInkAndDimAsTheMenus();
};
void LayersPanelTests::initTestCase()
@@ -174,6 +189,45 @@ void LayersPanelTests::aStoredWidthOutOfRangeIsTheDefault()
QCOMPARE(ContentView::layersPanelWidth(), 252.0);
}
void LayersPanelTests::footerButtonsTakeThePrimaryInkAndDimAsTheMenus()
{
EditorSession session;
LayersPanel panel(session);
// Opaque inks, each group its own: disabled reads the active.
const QColor primary(230, 230, 230), secondary(150, 150, 150);
QPalette inks = panel.palette();
inks.setColor(QPalette::Active, QPalette::WindowText, primary);
inks.setColor(QPalette::Inactive, QPalette::WindowText, primary);
inks.setColor(QPalette::Active, QPalette::PlaceholderText, secondary);
inks.setColor(QPalette::Inactive, QPalette::PlaceholderText, secondary);
inks.setColor(QPalette::Disabled, QPalette::WindowText, QColor(60, 20, 20));
inks.setColor(QPalette::Disabled, QPalette::PlaceholderText, QColor(20, 60, 20));
panel.setPalette(inks);
QVERIFY(!find<QToolButton>(panel, "addLayerMask").isEnabled());
QTRY_COMPARE(brightest(find<QToolButton>(panel, "addBlankLayer").icon(), QIcon::Normal).rgb(), primary.rgb());
// Swift 1.4.7: primary ink; disabled, dimmed as a menu's.
const std::vector<std::pair<const char *, LayerIcon>> buttons{
{"addBlankLayer", LayerIcon::newLayer}, {"groupLayers", LayerIcon::newFolder}, {"addLayerMask", LayerIcon::addMask}, {"deleteLayer", LayerIcon::trash}};
for (const auto &[name, shape] : buttons) {
QToolButton &button = find<QToolButton>(panel, name);
QVERIFY2(brightest(button.icon(), QIcon::Normal).rgb() == primary.rgb(), name);
QStyleOption option;
option.initFrom(&button);
const QPixmap menuInk = LayerIcons::pixmap(shape, 16, secondary, button.devicePixelRatio());
const QImage dimmed = button.style()->generatedIconPixmap(QIcon::Disabled, menuInk, &option).toImage();
QVERIFY2(button.icon().pixmap(menuInk.size(), QIcon::Disabled).toImage() == dimmed, name);
QVERIFY2(button.icon().pixmap(menuInk.size(), QIcon::Disabled).toImage() != button.icon().pixmap(menuInk.size(), QIcon::Normal).toImage(), name);
}
for (const char *name : {"layerEffects", "addAdjustment"})
QVERIFY2(brightest(find<QToolButton>(panel, name).icon(), QIcon::Normal).rgb() == secondary.rgb(), name);
// A theme's new ink reaches both.
QPalette next = panel.palette();
next.setColor(QPalette::WindowText, QColor(250, 200, 40));
panel.setPalette(next);
QTRY_COMPARE(brightest(find<QToolButton>(panel, "addBlankLayer").icon(), QIcon::Normal).rgb(), QColor(250, 200, 40).rgb());
QTRY_COMPARE(brightest(find<QToolButton>(panel, "addLayerMask").icon(), QIcon::Normal).rgb(), QColor(250, 200, 40).rgb());
}
QTEST_MAIN(LayersPanelTests)
#include "LayersPanelTests.moc"