Files
qbz/crates/qbz-qt/qml/controls/QbzConfirmModal.qml
T
vicrodh 8ff0adc149 fix(qt): convert the Slint colour literals that made scrims and shadows invisible
Slint writes #RRGGBBAA, Qt parses #AARRGGBB, so a literal copied verbatim does
not come out the wrong colour — it comes out transparent, because the alpha byte
is whatever the red byte used to be.

Seven sites across five modals. Every faked 32px drop shadow in the port was
#00000080 (alpha 00), and the scrims of LogViewerModal and LibFolderEditModal
were #000000bf, so those two modals had been opening with no dark backdrop at
all. The rest of the tree already spells the same scrim #bf000000.

Found by grepping for 8-digit literals of the shape #00xxxxxx. The gradient
stops in MixArtwork and LocalAlbumView that are legitimately transparent, and
the comments in the scene components that warn about this exact trap, were
checked and left alone.

cc
2026-08-15 00:51:18 -06:00

249 lines
9.9 KiB
QML

// QbzConfirmModal — THE port's confirm dialog. Before this file the tree had
// no confirm surface at all (grep -rl confirm qml/ found only LyricsPanel and
// ArtistView, neither a dialog), so every destructive action either mutated
// straight away or was not shipped. It is written generic on purpose: the
// Blacklist manager's Clear-All is the first caller, and MyQBZ delete,
// playlist delete and the offline-cache purge all need the same surface.
//
// Geometry is 1:1 with the Slint Clear-All modal
// (settings/BlacklistManagerView.slint:992-1113), which itself mirrors
// PlaylistDuplicateConfirmModal:
// scrim full-bleed #000000bf, click dismisses
// panel width min(host - 80, 400), height = content, centered, Radius.md,
// surface-main, 1px border-subtle, drop-shadow blur 32 #00000080
// padding 24, spacing 16; title heading/semibold, body 15 text-secondary
// buttons right-aligned, spacing 10, both 36px tall, Radius.sm,
// width = label + 32
// danger fill Theme.danger, hover Theme.danger.darker(0.12) — NOT
// `danger-hover`, which is a translucent tint for GHOST danger
// surfaces and would wash a filled button out (the Slint comment at
// :1080-1089 spells this out at length); label hard #ffffff
//
// MOUNT IT LAST inside the view root with `anchors.fill: parent`. ADR-009's
// ">= 3000" is satisfied both structurally (declaration order IS z-order) and
// by the explicit z: 3100 below — the same layer the port's only other in-pane
// modal scrim sits on (controls/DiscoverConfigModal.qml:56), so the two never
// fight.
//
// The shadow is FAKED (a translucent rounded rect behind the panel) — the
// LoginScreen.qml:30-49 precedent, including its 2026-07-29 note: the old
// "effects render nothing here" doctrine is RETIRED (both effect modules are
// installed and this port runs on the GPU; RoundedImage.qml detects the
// software path with GraphicsInfo.api instead of assuming it). A real blurred
// drop shadow is a visual change owing its own parity pass, so the fake stays
// for now, exactly as LoginScreen's does. The port's only other in-pane modal
// (controls/DiscoverConfigModal.qml) draws no shadow at all.
import QtQuick
import com.blitzfc.qbz
import "../theme"
Item {
id: root
property string title: ""
property string body: ""
/// "" is not a valid label — the default is the shared msgid, as a BINDING
/// so a live language switch re-translates it (QbzSession.trRev).
property string cancelLabel: QbzSession.tr("Cancel", QbzSession.trRev)
property string confirmLabel: ""
/// Filled-red confirm button (the Clear-All / delete voice). false gives
/// the accent fill instead, for a non-destructive confirmation.
///
/// NAMED `danger`, not `destructive`: spec 03 §6.10/§10 specifies `danger`,
/// and `controls/SettingsButton.qml:15` already carries `property bool
/// danger` for exactly this concept (red border + red label + a `favorite`
/// glyph tint). Two names for one idea across two neighbouring controls is
/// how a call site ends up setting a property that does not exist — which in
/// QML is not an error, just a silently-ignored line.
property bool danger: true
readonly property bool opened: root._open
property bool _open: false
signal confirmed()
signal cancelled()
QbzTheme { id: theme }
function open() {
root._open = true
// AFTER _open, so `visible` is already true — an invisible item cannot
// take active focus and the Esc handler would be dead.
keyScope.forceActiveFocus()
}
function close() {
root._open = false
root._restoreShellFocus()
}
// §1.4.3 (2026-08-03 hotkeys-port contract): keyScope GRABS focus on open
// and Qt strands activeFocus on the now-invisible item at close, which
// kills the AppShell key dispatcher until the next click. Restore the
// shell root — the dispatcher's fallback focus item — on every close
// path. Duck-walk, because this modal mounts under several parents (the
// Blacklist manager, FolderModals, MyQBZ delete).
function _restoreShellFocus() {
var p = root
while (p.parent) {
if (p.parent.isQbzShellRoot === true) {
p.parent.forceActiveFocus()
return
}
p = p.parent
}
}
function _cancel() {
root.close()
root.cancelled()
}
function _confirm() {
root.close()
root.confirmed()
}
visible: root._open
enabled: root._open
z: 3100
// Esc dismiss (the Slint esc-scope FocusScope, :1006-1017). No visual and
// no MouseArea, so it never intercepts a press meant for the scrim.
FocusScope {
id: keyScope
anchors.fill: parent
Keys.onEscapePressed: function (event) {
root._cancel()
event.accepted = true
}
}
// Scrim. `radius` is load-bearing, not decoration: this Item fills the
// view root, which fills AppShell's rounded content frame, and Qt Quick's
// `clip` is a rectangular scissor that does not follow `radius` — so an
// opaque full-bleed child paints into the frame's four bezel corners
// (AppShell.qml:252-259, which names DiscoverConfigModal's scrim as the
// one that had to be fixed exactly this way).
Rectangle {
anchors.fill: parent
radius: theme.radiusMd
color: "#bf000000"
MouseArea {
anchors.fill: parent
onClicked: root._cancel()
}
}
// Faked drop shadow (blur 32 / #00000080) — see the header note.
Rectangle {
x: panel.x
y: panel.y + 8
width: panel.width
height: panel.height
radius: theme.radiusMd
color: "#80000000"
opacity: 0.5
}
Rectangle {
id: panel
width: Math.min(root.width - 80, 400)
// The Slint panel is `height: panel.preferred-height` over a
// VerticalLayout with padding 24 -> content + 48.
height: panelCol.implicitHeight + 48
x: Math.round((root.width - width) / 2)
y: Math.round((root.height - height) / 2)
radius: theme.radiusMd
color: theme.surfaceMain
border.width: 1
border.color: theme.borderSubtle
// Swallow clicks so they never reach the scrim (the Slint's empty
// panel TouchArea, :1030).
MouseArea { anchors.fill: parent }
Column {
id: panelCol
x: 24
y: 24
width: parent.width - 48
spacing: 16
Text {
width: parent.width
text: root.title
color: theme.textPrimary
font.pixelSize: theme.fontHeading
font.weight: theme.weightSemibold
wrapMode: Text.WordWrap
}
Text {
visible: root.body !== ""
width: parent.width
text: root.body
color: theme.textSecondary
font.pixelSize: theme.fontBody
wrapMode: Text.WordWrap
}
Item {
width: parent.width
height: 36
Row {
anchors.right: parent.right
spacing: 10
Rectangle {
width: cancelLbl.implicitWidth + 32
height: 36
radius: theme.radiusSm
color: cancelArea.containsMouse ? theme.surfaceHover
: theme.surfaceElevated
Text {
id: cancelLbl
anchors.centerIn: parent
text: root.cancelLabel
color: theme.textPrimary
font.pixelSize: theme.fontBody
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root._cancel()
}
}
Rectangle {
width: confirmLbl.implicitWidth + 32
height: 36
radius: theme.radiusSm
color: root.danger
? (confirmArea.containsMouse ? Qt.darker(theme.danger, 1.12)
: theme.danger)
: (confirmArea.containsMouse ? theme.accentHover : theme.accent)
Text {
id: confirmLbl
anchors.centerIn: parent
text: root.confirmLabel
// The Slint hard-codes #ffffff on the filled danger
// button and that is correct here — `danger` is a
// fixed red under every palette. The accent arm goes
// through the theme's on-accent selector instead
// (QbzTheme.qml:314-317), which is the port's
// owner-approved WCAG deviation.
color: root.danger ? "#ffffff" : theme.accentGlyphColor
font.pixelSize: theme.fontBody
font.weight: theme.weightMedium
}
MouseArea {
id: confirmArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root._confirm()
}
}
}
}
}
}
}