Files
Panama/config/dot/quickshell/modules/settings/ThemeCard.qml
T

267 lines
9.3 KiB
QML

// 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()
}
}