Block B2 of the miniplayer/tray contract, and the block that settles the premise the whole port rests on: a second top-level Window in the same QQmlApplicationEngine reads the SAME bridge singletons as the shell. Proven by driving it — a probe window with one Text bound to QbzPlayer.npTitle tracked the main window across a track change with no mirror code. Slint needs ~155 lines of mirroring for this; Qt needs none. qml/miniplayer/ (new, six files) — MiniWindow (frameless, transparent, transientParent null, its own key host and its own onClosing), MiniShell (the card, the artwork backdrop, the surface router), MiniCoverArt and MiniExplicitBadge (the two components MiniBits.slint exports, which cannot share one .qml file), and the compact and artwork surfaces. The card is the window minus a 6 px gutter, its radius is 9 for the condensed surfaces and 10 otherwise, and switching the surface RESIZES THE WINDOW — 540 to 178 and back, driven and measured, not inferred. The backdrop lives inside a masked container. `clip: true` on a rounded Rectangle does NOT round its children on this Qt build — theme/RoundedImage.qml records that as measured fact — so without the mask the scrim and the atmosphere's own opaque root would paint square corners over the card the moment the backdrop is on. That is reachable before its toggle exists, because the pref is co-owned with the shipping Slint app. src/mini_bridge.rs — enter/exit, the mini's own key dispatcher, and toggle(). Re-entering an open mini re-raises it rather than doing nothing, which is what the reference does and what present() will want. src/hotkeys_qt.rs — action_for_mini_key, a SIBLING of action_for_key with the context arm inverted. The main window still refuses Context::Mini and the test that pins it is untouched; a new test pins the mirror. src/hotkeys_bridge.rs — the ui.miniPlayer arm stops logging and toggles. qml/shell/ViewModeMenu.qml — the Miniplayer row goes live with its own dispatch arm, and it is hidden under the kiosk profile, 1:1 with PlayerBarSmall.slint:819. The kiosk port landed first, so that gate was owed here. Three comments claiming the row is inert are corrected. Surfaces 0, 3 and 4 are clamped to the artwork surface until their content exists (B3 and B4), because mini_surface and mini_default_view live in the prefs file shared with the Slint app: a user whose Slint mini was last on Queue would otherwise open onto a blank card with no visible way out. Each block deletes its own entry from that clamp. Enter does not hide the main window yet — window.hideToTray() is B6's, and open-coding a bare hide() here is what the one-visibility-owner rule forbids. So the main window stays visible behind the mini in this commit, by design. cc
177 lines
7.6 KiB
QML
177 lines
7.6 KiB
QML
// Now-Playing-view mode menu (PlayerBar.slint layout-menu, phase 18) —
|
|
// one QbzContextMenu shared by the full PlayerBar and the Small bar:
|
|
// New / Classic / Small / Large (with the current mode checked), then the
|
|
// window-mode rows. All three window-mode rows are LIVE: Immersive since the
|
|
// 2026-08-02 immersive-port §7 B2, Kiosk since the 2026-08-02 kiosk-port §8.1,
|
|
// Miniplayer since the 2026-08-03 miniplayer/tray-port §4.10 B2.
|
|
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
import com.blitzfc.qbz
|
|
import "../controls"
|
|
import "../theme"
|
|
|
|
QbzContextMenu {
|
|
id: root
|
|
menuWidth: 196
|
|
|
|
// The theme tokens. Declared HERE even though the BASE type
|
|
// (controls/QbzContextMenu.qml) already has a `QbzTheme { id: theme }`:
|
|
// a QML id belongs to the document that declares it, and a derived
|
|
// document is a DIFFERENT scope — the base's context is created as a
|
|
// child of this one, never as its parent, so `theme` was resolving to
|
|
// nothing here and every binding below threw a silent ReferenceError
|
|
// (leaving the row Rectangles at their default WHITE fill). The four
|
|
// sibling menus built on the same base — CardMenu, AudioSettingsMenu,
|
|
// PmFolderMenu, SidebarRowMenu — all declare their own; this file was
|
|
// the lone outlier.
|
|
QbzTheme { id: theme }
|
|
|
|
Repeater {
|
|
model: [
|
|
{ "label": QbzSession.tr("New", QbzSession.trRev), "icon": "panel-left", "mode": 0 },
|
|
{ "label": QbzSession.tr("Classic", QbzSession.trRev), "icon": "panel-right-close", "mode": 1 },
|
|
{ "label": QbzSession.tr("Small", QbzSession.trRev), "icon": "rows-3", "mode": 2 },
|
|
{ "label": QbzSession.tr("Large", QbzSession.trRev), "icon": "layout-grid", "mode": 3 },
|
|
]
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
width: parent ? parent.width : 0
|
|
height: 33
|
|
radius: 5
|
|
color: miArea.containsMouse ? theme.surfaceHover : "transparent"
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 8
|
|
spacing: 8
|
|
QbzIcon {
|
|
name: modelData.icon
|
|
width: 15
|
|
height: 15
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
tintName: "secondary"
|
|
}
|
|
Text {
|
|
height: parent.height
|
|
width: parent.width - 23 - 22
|
|
text: modelData.label
|
|
color: theme.textSecondary
|
|
font.pixelSize: 13
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
QbzIcon {
|
|
visible: QbzShell.npbMode === modelData.mode
|
|
name: "check"
|
|
width: 13
|
|
height: 13
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
tintName: "accent"
|
|
}
|
|
}
|
|
MouseArea {
|
|
id: miArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
root.close()
|
|
QbzShell.npbSetMode(modelData.mode)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Divider + the window-mode rows (Miniplayer / Immersive / Kiosk), all
|
|
// three live.
|
|
Item {
|
|
width: parent ? parent.width : 0
|
|
height: 7
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
height: 1
|
|
color: theme.borderSubtle
|
|
}
|
|
}
|
|
Repeater {
|
|
// Window-mode rows. `live` flips per-row inside the shared delegate
|
|
// (2026-08-02 immersive-port §7); Immersive went live in that
|
|
// contract's B2, Kiosk in the 2026-08-02-kiosk-port §8.1, Miniplayer
|
|
// in the 2026-08-03 miniplayer/tray-port §4.10 B2.
|
|
//
|
|
// Each row carries its own `action` because the delegate's MouseArea
|
|
// used to call QbzImmersive.openFromMenu() unconditionally: flipping
|
|
// any other row's `live` would have made it open Immersive.
|
|
//
|
|
// The Kiosk label mirrors the profile, 1:1 with the reference
|
|
// (`shell/PlayerBar.slint:797`): in kiosk it reads "Desktop mode",
|
|
// because the row is the way back out.
|
|
//
|
|
// THE MINIPLAYER ROW IS ABSENT UNDER THE KIOSK PROFILE, not dimmed —
|
|
// 1:1 with `crates/qbz-ui/ui/shell/PlayerBarSmall.slint:819`, which
|
|
// wraps its ContextMenuItem in `if !ShellState.kiosk-profile`. The
|
|
// reference's reason is the appliance shell: a second floating,
|
|
// draggable, always-on-top window has no way back on a touch-only
|
|
// kiosk. Building the model in JS (not a ternary inside one row) is
|
|
// what makes the row genuinely absent; the binding re-runs when
|
|
// QbzShell.kioskProfile changes, which the live desktop/kiosk toggle
|
|
// does. This closes ticket T-13 (contract §4.10.2): the kiosk contract
|
|
// landed first, so this diff owes the gate and pays it here.
|
|
model: {
|
|
var rows = []
|
|
if (!QbzShell.kioskProfile)
|
|
rows.push({ "label": QbzSession.tr("Miniplayer", QbzSession.trRev),
|
|
"icon": "picture-in-picture-2", "live": true, "action": "miniplayer" })
|
|
rows.push({ "label": QbzSession.tr("Immersive", QbzSession.trRev),
|
|
"icon": "maximize-2", "live": true, "action": "immersive" })
|
|
rows.push({ "label": QbzShell.kioskProfile
|
|
? QbzSession.tr("Desktop mode", QbzSession.trRev)
|
|
: QbzSession.tr("Kiosk mode", QbzSession.trRev),
|
|
"icon": "hard-drive", "live": true, "action": "kiosk" })
|
|
return rows
|
|
}
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
width: parent ? parent.width : 0
|
|
height: 33
|
|
radius: 5
|
|
opacity: modelData.live ? 1.0 : 0.45
|
|
color: (modelData.live && modeArea.containsMouse)
|
|
? theme.surfaceHover : "transparent"
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 8
|
|
spacing: 8
|
|
QbzIcon { name: modelData.icon; width: 15; height: 15; anchors.verticalCenter: parent.verticalCenter; tintName: "secondary" }
|
|
Text {
|
|
height: parent.height
|
|
text: modelData.label
|
|
color: theme.textSecondary
|
|
font.pixelSize: 13
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
MouseArea {
|
|
id: modeArea
|
|
anchors.fill: parent
|
|
enabled: modelData.live
|
|
hoverEnabled: true
|
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
onClicked: {
|
|
root.close()
|
|
if (modelData.action === "immersive")
|
|
QbzImmersive.openFromMenu()
|
|
else if (modelData.action === "kiosk")
|
|
QbzSession.toggleProfile()
|
|
else if (modelData.action === "miniplayer")
|
|
// Suppressed under gamescope INSIDE enter() — one
|
|
// predicate for this row and for Shift+M (§4.10.1).
|
|
QbzMini.enter()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|