267 lines
9.3 KiB
QML
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()
|
|
}
|
|
}
|