Files
DankMaterialShell/quickshell/Modules/ControlCenter/Components/CcDetailBand.qml
T
2026-10-27 13:02:12 -04:00

336 lines
11 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.Details
import qs.DCommon.Widgets
import "../utils/sections.js" as Sections
FocusScope {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string section: ""
property var model: null
property string screenName: ""
property string screenModel: ""
property var transientSurfaceTracker: null
property real maximumHeight: 0
property real coverage: 0
property var runningToplevels: []
signal dismissed
signal backRequested
signal collapseRequested
signal closeRequested
signal codecSelectorRequested(var device)
signal portSelectorRequested(var node)
property string shownSection: ""
readonly property var pageItem: pageLoader.item
readonly property bool pluginPage: shownSection.startsWith("plugin_") || shownSection.startsWith("builtin_")
// A page measures itself a tick after it loads; waiting for that keeps the first target the final one.
readonly property real pageHeight: {
const own = pageItem?.implicitHeight ?? 0;
if (own > 0)
return own;
if (!pluginPage)
return 0;
const preferred = pageItem?.preferredHeight ?? 0;
return preferred > 0 ? preferred : CcMetrics.detailHeightDefault;
}
// The band's extent includes the gap above and below its card, so they grow and shrink with it.
readonly property real chromeHeight: CcMetrics.gridGap * 2 + CcMetrics.detailHeaderHeight + CcMetrics.detailDialogPadding * 2
readonly property real contentCap: Math.max(CcMetrics.detailHeightFloor(shownSection, pageItem?.preferredHeight ?? 0), maximumHeight - chromeHeight)
readonly property real targetHeight: section === "" || pageHeight <= 0 ? 0 : chromeHeight + Math.min(pageHeight, contentCap)
readonly property bool transitioning: heightAnimation.running
// Only IPC can ask for a plugin section whose tile is not placed, so there is nothing to show.
readonly property bool pageEmpty: pageItem?.empty === true
readonly property string title: {
const own = pageItem?.title ?? "";
if (own)
return own;
const parsed = Sections.parse(shownSection);
return model?.getWidgetForId(parsed.base)?.text ?? "";
}
height: targetHeight
visible: shownSection !== ""
clip: true
Accessible.role: Accessible.Grouping
Accessible.name: title
// Same timing as the popout's own resize, so the surface and the content move as one.
Behavior on height {
enabled: CcMetrics.animationsEnabled
NumberAnimation {
id: heightAnimation
duration: CcMetrics.transitionDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
onPageEmptyChanged: {
if (pageEmpty && section !== "")
Qt.callLater(collapseRequested);
}
function finishClose() {
if (section !== "" || shownSection === "")
return;
pageLoader.sourceComponent = null;
shownSection = "";
dismissed();
}
function dismissTransient() {
const item = pageItem;
if (!item || typeof item.dismissTransient !== "function")
return false;
return item.dismissTransient() === true;
}
function _componentFor(sectionId) {
const parsed = Sections.parse(sectionId);
switch (parsed.base) {
case "network":
case "wifi":
return networkComponent;
case "bluetooth":
return bluetoothComponent;
case "audioOutput":
return audioOutputComponent;
case "audioInput":
return audioInputComponent;
case "battery":
return batteryComponent;
case "doNotDisturb":
return dndComponent;
case "idleInhibitor":
return idleInhibitComponent;
case "diskUsage":
return diskUsageComponent;
case "brightnessSlider":
return brightnessComponent;
case "runningApps":
return runningAppsComponent;
}
if (sectionId.startsWith("builtin_") || sectionId.startsWith("plugin_"))
return pluginComponent;
return null;
}
onSectionChanged: {
if (section === "") {
if (!CcMetrics.animationsEnabled)
finishClose();
return;
}
shownSection = section;
pageLoader.sourceComponent = _componentFor(section);
(pageItem ?? root).forceActiveFocus();
}
onHeightChanged: {
if (section === "" && height <= 0.5)
finishClose();
}
Rectangle {
y: CcMetrics.gridGap
width: parent.width
height: Math.max(0, parent.height - CcMetrics.gridGap * 2)
radius: CcMetrics.tallTileRadius
color: CcMetrics.dialogColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Item {
id: panel
anchors.fill: parent
anchors.margins: CcMetrics.detailDialogPadding
opacity: root.section !== "" ? (CcMetrics.hideCoveredContent ? 1 - root.coverage : 1) : 0
Behavior on opacity {
enabled: CcMetrics.animationsEnabled
NumberAnimation {
duration: CcMetrics.fadeDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
Item {
id: header
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: CcMetrics.detailHeaderHeight
StyledText {
anchors.left: parent.left
anchors.leftMargin: CcMetrics.rowPaddingH
anchors.right: headerSlot.left
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: root.title
font.pixelSize: CcMetrics.detailTitleSize
font.weight: Theme.fontWeightMedium
color: Theme.surfaceText
elide: Text.ElideRight
horizontalAlignment: Text.AlignLeft
}
Item {
id: headerSlot
anchors.right: closeButton.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: childrenRect.width
height: childrenRect.height
}
DActionButton {
id: closeButton
anchors.right: parent.right
anchors.rightMargin: CcMetrics.headerEdgeInset
anchors.verticalCenter: parent.verticalCenter
buttonSize: CcMetrics.headerActionSize
iconName: "close"
iconSize: CcMetrics.headerActionIconSize
iconColor: Theme.surfaceText
Accessible.name: I18n.tr("Close")
onClicked: root.backRequested()
}
}
Loader {
id: pageLoader
anchors.left: parent.left
anchors.right: parent.right
anchors.top: header.bottom
anchors.bottom: parent.bottom
active: root.shownSection !== ""
onLoaded: {
const actions = item.headerActions ?? null;
if (actions)
actions.parent = headerSlot;
}
}
}
}
Connections {
target: root.pageItem
ignoreUnknownSignals: true
function onMountPathChanged(newMountPath) {
Sections.updateWidgetForSection(root.shownSection, {
"mountPath": newMountPath
});
root.collapseRequested();
}
function onDeviceNameChanged(newDeviceName) {
Sections.updateWidgetForSection(root.shownSection, {
"deviceName": newDeviceName
});
}
function onShowCodecSelector(device) {
root.codecSelectorRequested(device);
}
function onShowPortSelector(node) {
root.portSelectorRequested(node);
}
function onDismissRequested() {
root.backRequested();
}
function onCloseRequested() {
root.closeRequested();
}
}
Component {
id: networkComponent
NetworkDetail {
transientSurfaceTracker: root.transientSurfaceTracker
transitioning: root.transitioning
}
}
Component {
id: bluetoothComponent
BluetoothDetail {
transientSurfaceTracker: root.transientSurfaceTracker
}
}
Component {
id: audioOutputComponent
AudioOutputDetail {}
}
Component {
id: audioInputComponent
AudioInputDetail {}
}
Component {
id: batteryComponent
BatteryDetail {}
}
Component {
id: dndComponent
DoNotDisturbDetail {}
}
Component {
id: idleInhibitComponent
IdleInhibitorDetail {}
}
Component {
id: diskUsageComponent
DiskUsageDetail {
readonly property var widgetEntry: Sections.widgetForSection(root.shownSection)
currentMountPath: widgetEntry?.mountPath || "/"
}
}
Component {
id: brightnessComponent
BrightnessDetail {
readonly property var widgetEntry: Sections.widgetForSection(root.shownSection)
initialDeviceName: widgetEntry?.deviceName || ""
instanceId: widgetEntry?.instanceId || ""
screenName: root.screenName
screenModel: root.screenModel
}
}
Component {
id: runningAppsComponent
RunningAppsDetail {
toplevels: root.runningToplevels
}
}
Component {
id: pluginComponent
PluginDetailShell {
pluginId: root.shownSection.startsWith("plugin_") ? root.shownSection.replace("plugin_", "") : ""
builtinInstance: root.shownSection.startsWith("builtin_") ? (root.model?.builtinInstances[root.shownSection] ?? null) : null
}
}
}