// One theme, drawn as the desktop it produces. // // A row of flat swatches would name a theme's colors without showing what any // of them does. So the card is a miniature of the real thing: the ground, a // window sitting on it wearing the accent as a border, the two text weights // the shell actually reads with, and only then the four remaining colors as // dots. Clicking applies it immediately -- there is no preview mode here, // because the whole shell is the preview. import QtQuick import qs.config import qs.services Rectangle { id: root // A catalog record or a saved profile -- the same shape either way. required property var theme signal chosen signal removed // A custom profile saved before palettes existed carries none of its own // and inherits its scheme's default, exactly as ThemeProfiles resolves it. readonly property var palette: root.theme.palette ?? ThemeCatalog.defaultPalette(root.theme.scheme) readonly property bool active: root.theme.id === ThemeProfiles.activeId readonly property bool shipped: root.theme.shipped === true readonly property bool isDefault: root.theme.id === (root.theme.scheme === "light" ? ThemeCatalog.defaultLight : ThemeCatalog.defaultDark) // Delete arms in place rather than through ConfirmAction. The ✕ badge is a // 22px corner of a card whose whole body is the apply target roughly ten // pixels away -- there is nowhere in that caption to put a Keep beside a // Delete it without the pair becoming the card. So the badge itself is the // two presses: the first turns it into a labelled danger pill and reddens // the card's border, the second deletes. The token is ShellState's, the // same one ConfirmAction uses, so one confirm is armed app-wide. readonly property string removeActionId: "theme-card-delete:" + root.theme.id readonly property bool removeArmed: ShellState.armedConfirm === root.removeActionId function pressRemove(): void { if (!root.removeArmed) { ShellState.armedConfirm = root.removeActionId; return; } root.disarmRemove(); root.removed(); } function disarmRemove(): void { if (root.removeArmed) ShellState.armedConfirm = ""; } // Armed, the card body is the way out: a press anywhere on it takes the // arming back instead of applying the theme, because the miss this guards // against is a press landing next to the badge rather than on it. function press(): void { if (root.removeArmed) { root.disarmRemove(); return; } root.chosen(); } // A card that scrolls away, or a gallery that rebuilds, must not leave the // token claiming a theme nothing is showing. Component.onDestruction: root.disarmRemove() implicitHeight: preview.height + caption.height + 4 radius: Theme.cardRadius + 2 color: Theme.alpha(Theme.fg, 0.04) border.width: 2 border.color: root.removeArmed ? Theme.danger : (root.activeFocus ? Theme.accentSecondary : (root.active ? Theme.accent : Theme.alpha(Theme.fg, 0.12))) clip: true activeFocusOnTab: true Accessible.role: Accessible.Button Accessible.name: root.theme.name Accessible.description: (root.theme.scheme === "light" ? "Light theme" : "Dark theme") + (root.active ? ", selected" : "") + (root.removeArmed ? ", delete is armed — press this card to keep it" : "") Accessible.focusable: true Accessible.focused: root.activeFocus Keys.onReturnPressed: root.press() Keys.onSpacePressed: root.press() Keys.onEscapePressed: root.disarmRemove() Rectangle { id: preview anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: 2 height: 84 color: root.palette.bg border.width: 0 Rectangle { anchors.fill: parent anchors.margins: 11 radius: 7 color: root.palette.bgPanel border.width: 2 border.color: root.theme.accent Column { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: 8 spacing: 7 Rectangle { width: parent.width * 0.64 height: 4 radius: 2 color: root.palette.fg border.width: 0 } Row { spacing: 4 Repeater { model: [root.theme.accent, root.theme.secondary, root.palette.fgDim, root.palette.fgMuted] Rectangle { required property var modelData width: 9 height: 9 radius: 5 color: modelData border.width: 0 } } } } } } Item { id: caption anchors.left: parent.left anchors.right: parent.right anchors.top: preview.bottom anchors.leftMargin: 12 anchors.rightMargin: 10 height: 30 Text { id: name anchors.left: parent.left anchors.right: marks.left anchors.rightMargin: 6 anchors.verticalCenter: parent.verticalCenter text: root.theme.name + (root.isDefault ? " · today's default" : "") color: Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall font.weight: Font.DemiBold elide: Text.ElideRight } Row { id: marks anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 6 Text { anchors.verticalCenter: parent.verticalCenter visible: root.active text: "✓" color: Theme.accent font.family: Theme.fontFamily font.pixelSize: Theme.fontSize font.weight: Font.DemiBold } // Only saved themes can be removed, and the affordance is a // separate focus stop so Tab never lands on "delete" when the // person meant "select". // // At rest the badge is neutral: danger marks the press that // confirms, never the one that initiates, which is SettingsButton's // contract and holds here too. Armed, it stops being a glyph and // becomes a labelled pill wide enough to read. Rectangle { id: remove anchors.verticalCenter: parent.verticalCenter visible: !root.shipped width: root.removeArmed ? removeLabel.implicitWidth + 16 : 22 height: 22 radius: 7 color: root.removeArmed ? Theme.alpha(Theme.danger, 0.3) : (removeHover.hovered ? Theme.alpha(Theme.fg, 0.14) : Theme.alpha(Theme.fg, 0.07)) border.width: root.removeArmed || remove.activeFocus ? 2 : 1 border.color: root.removeArmed || remove.activeFocus ? Theme.danger : Theme.alpha(Theme.fg, 0.08) activeFocusOnTab: visible Accessible.role: Accessible.Button Accessible.name: root.removeArmed ? "Delete " + root.theme.name + ", press again to confirm" : "Delete " + root.theme.name Accessible.description: root.removeArmed ? "This theme's palette, terminal colors and effects go with it" : "" Accessible.focusable: true Accessible.focused: remove.activeFocus Keys.onReturnPressed: root.pressRemove() Keys.onSpacePressed: root.pressRemove() Keys.onEscapePressed: root.disarmRemove() Text { id: removeLabel anchors.centerIn: parent text: root.removeArmed ? "Delete it" : "✕" color: root.removeArmed ? Theme.danger : Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall font.weight: root.removeArmed ? Font.DemiBold : Font.Normal } HoverHandler { id: removeHover cursorShape: Qt.PointingHandCursor } // An exclusive grab, so the delete press does not also reach // the card's own tap handler underneath and select the theme // on its way out. TapHandler { gesturePolicy: TapHandler.ReleaseWithinBounds onTapped: root.pressRemove() } } } } HoverHandler { id: cardHover cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: root.press() } }