diff --git a/dux-qml-common b/dux-qml-common index c6e4a6bce..e261e339c 160000 --- a/dux-qml-common +++ b/dux-qml-common @@ -1 +1 @@ -Subproject commit c6e4a6bce24b804688225b0889410d2ec0ae06d7 +Subproject commit e261e339c4eaf5118c6c66ec66b8cf4e703ce3e3 diff --git a/quickshell/Common/GridLayout.js b/quickshell/Common/GridLayout.js index 7d4625145..0e71470f0 100644 --- a/quickshell/Common/GridLayout.js +++ b/quickshell/Common/GridLayout.js @@ -171,6 +171,37 @@ function shrinkRows(layout, shrinkable, unit) { }); } +// Opens a full-width band of `extent` at the cut under the slot at `index`, or under the last row when there is +// none, pushing every slot below the cut down by exactly that much. The extent carries the band's own gaps, so an +// animated extent moves the rows without a step at either end. The cut sits below any taller neighbour straddling +// the slot's bottom edge. +function insertBand(layout, index, extent) { + if (extent <= 0) + return layout; + const slots = layout.slots; + const anchor = slots[index] ?? null; + let cut = anchor ? anchor.y + anchor.h : layout.totalHeight; + for (let moved = true; moved && anchor; ) { + moved = false; + for (const slot of slots) { + if (!slot || slot.y >= cut || slot.y + slot.h <= cut) + continue; + cut = slot.y + slot.h; + moved = true; + } + } + const shifted = slots.map(slot => slot && slot.y >= cut ? Object.assign({}, slot, { + "y": slot.y + extent + }) : slot); + const below = slots.some(slot => slot && slot.y >= cut); + return Object.assign({}, layout, { + "slots": shifted, + "bandY": cut, + "bandExtent": extent, + "totalHeight": (below ? layout.totalHeight : cut) + extent + }); +} + function rowAt(layout, y) { const pitch = (layout.rowUnit + layout.gap) * layout.step; const tops = layout.bandTops; diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 2333163e8..b34969bdd 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -335,6 +335,10 @@ Singleton { property real foregroundLayerTransparency: Spec.SPEC.foregroundLayerTransparency.def property real blurLayerOutlineOpacity: Spec.SPEC.blurLayerOutlineOpacity.def onBlurLayerOutlineOpacityChanged: saveSettings() + property string tooltipSurfaceColor: Spec.SPEC.tooltipSurfaceColor.def + onTooltipSurfaceColorChanged: saveSettings() + property string tooltipSurfaceCustomColor: Spec.SPEC.tooltipSurfaceCustomColor.def + onTooltipSurfaceCustomColorChanged: saveSettings() property bool blurBorderEnabled: Spec.SPEC.blurBorderEnabled.def onBlurBorderEnabledChanged: saveSettings() property string blurBorderColor: Spec.SPEC.blurBorderColor.def diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 51a8cd603..bdfe94e0a 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -578,13 +578,15 @@ var LOCAL_SPEC = { id: "brightnessSlider", enabled: true, w: 4, - h: 1 + h: 1, + small: true }, { id: "volumeSlider", enabled: true, w: 4, - h: 1 + h: 1, + small: true }, { id: "wifi", diff --git a/quickshell/Common/settings/SettingsStore.js b/quickshell/Common/settings/SettingsStore.js index 8a741df2a..40d54d59a 100644 --- a/quickshell/Common/settings/SettingsStore.js +++ b/quickshell/Common/settings/SettingsStore.js @@ -190,9 +190,8 @@ function parse(root, jsonObj) { // would wipe values set by detection processes on every reload. if (SPEC[k].persist === false) continue; - if (!(k in jsonObj)) { - root[k] = Util.cloneDef(SPEC[k].def); - } + if (!(k in jsonObj)) + assign(root, k, Util.cloneDef(SPEC[k].def)); } for (var k in jsonObj) { @@ -203,7 +202,16 @@ function parse(root, jsonObj) { var raw = jsonObj[k]; var spec = SPEC[k]; var coerce = spec.coerce; - root[k] = coerce ? (coerce(raw) !== undefined ? coerce(raw) : root[k]) : raw; + assign(root, k, coerce ? (coerce(raw) !== undefined ? coerce(raw) : root[k]) : raw); + } +} + +// A QML root throws on an undeclared property; a shared spec key the shell has not declared must not abort the load. +function assign(root, key, value) { + try { + root[key] = value; + } catch (e) { + console.warn("settings: no SettingsData property for spec key, skipped:", key); } } @@ -216,6 +224,8 @@ function toJson(root, setKeys) { if (k === "pluginSettings") continue; var value = root[k]; + if (value === undefined) + continue; if (k === "desktopWidgetInstances") value = withoutInstancePositions(value); if (k === "builtInPluginSettings") diff --git a/quickshell/Modules/ControlCenter/CcMetrics.qml b/quickshell/Modules/ControlCenter/CcMetrics.qml index 5e2c141ca..4f7af1ef0 100644 --- a/quickshell/Modules/ControlCenter/CcMetrics.qml +++ b/quickshell/Modules/ControlCenter/CcMetrics.qml @@ -68,8 +68,9 @@ Singleton { readonly property real smallRowFraction: 0.5 readonly property real shapeButtonSize: Theme.iconSize - readonly property real detailDialogInset: Theme.spacingL readonly property real detailDialogPadding: Theme.spacingS + readonly property real detailHeaderHeight: Theme.iconButtonSize + readonly property real detailTitleSize: Theme.fontSizeLarge readonly property real detailMinContentHeight: Theme.listItemTwoLineHeight * 3 readonly property real headerActionSize: Theme.buttonHeightXS readonly property real headerActionIconSize: Theme.iconSizeMedium @@ -134,9 +135,8 @@ Singleton { readonly property bool animationsEnabled: !SettingsData.reduceMotion && Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None - function preferredDetailHeight(section, pluginHeight) { - if (!section) - return 0; + // The 1.6 detail heights: a detail never shows less than it did there, whatever the grid leaves free. + function detailHeightFloor(section, pluginHeight) { if (section.startsWith("plugin_")) return pluginHeight > 0 ? pluginHeight : detailHeightDefault; if (section.startsWith("brightnessSlider_")) @@ -145,13 +145,8 @@ Singleton { case "wifi": case "network": case "bluetooth": - case "battery": case "builtin_vpn": case "builtin_tailscale": - case "audioOutput": - case "audioInput": - case "diskUsage": - case "runningApps": return detailHeightList; default: return detailHeightDefault; diff --git a/quickshell/Modules/ControlCenter/Components/CcDetailPage.qml b/quickshell/Modules/ControlCenter/Components/CcDetailBand.qml similarity index 58% rename from quickshell/Modules/ControlCenter/Components/CcDetailPage.qml rename to quickshell/Modules/ControlCenter/Components/CcDetailBand.qml index 6639032db..c918d9009 100644 --- a/quickshell/Modules/ControlCenter/Components/CcDetailPage.qml +++ b/quickshell/Modules/ControlCenter/Components/CcDetailBand.qml @@ -18,8 +18,7 @@ FocusScope { property string screenName: "" property string screenModel: "" property var transientSurfaceTracker: null - property real topInset: 0 - property vector4d cornerRadii: Qt.vector4d(Theme.windowRadius, Theme.windowRadius, Theme.windowRadius, Theme.windowRadius) + property real maximumHeight: 0 property real coverage: 0 property var runningToplevels: [] @@ -31,17 +30,21 @@ FocusScope { signal portSelectorRequested(var node) property string shownSection: "" - property bool enterPending: false - readonly property bool animationsEnabled: CcMetrics.animationsEnabled && !SettingsData.reduceMotion - readonly property bool transitioning: enterPending || enterAnimation.running readonly property var pageItem: pageLoader.item - readonly property real pageHeight: (pageItem?.implicitHeight ?? 0) > 0 ? pageItem.implicitHeight : CcMetrics.preferredDetailHeight(shownSection, pageItem?.preferredHeight ?? 0) - readonly property real contentHeight: pageHeight - readonly property real chromeHeight: header.height + CcMetrics.detailDialogPadding * 2 - readonly property real maximumHeight: height - topInset - CcMetrics.detailDialogInset - // plugin detail content may not scroll itself, so the panel grows to fit it - readonly property bool pageScrollsItself: !shownSection.startsWith("plugin_") - readonly property real minimumHeight: chromeHeight + (pageScrollsItself ? Math.min(contentHeight, CcMetrics.detailMinContentHeight) : pageHeight) + readonly property real pageHeight: { + const own = pageItem?.implicitHeight ?? 0; + if (own > 0) + return own; + 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 === "" ? 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) @@ -49,48 +52,37 @@ FocusScope { const parsed = Sections.parse(shownSection); return model?.getWidgetForId(parsed.base)?.text ?? ""; } + + height: targetHeight visible: shownSection !== "" - opacity: 0 - layer.enabled: enterPending || enterAnimation.running || exitAnimation.running - layer.smooth: true - Accessible.role: Accessible.Dialog + 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 !== "") + if (section !== "" || shownSection === "") return; pageLoader.sourceComponent = null; shownSection = ""; dismissed(); } - function containsItem(item) { - for (let ancestor = item; ancestor; ancestor = ancestor.parent) { - if (ancestor === root) - return true; - } - return false; - } - - function moveFocus(backwards) { - const current = root.Window.window?.activeFocusItem ?? root; - const next = current.nextItemInFocusChain(!backwards); - const target = root.containsItem(next) ? next : backwards ? closeButton : root.nextItemInFocusChain(true); - (root.containsItem(target) ? target : closeButton).forceActiveFocus(backwards ? Qt.BacktabFocusReason : Qt.TabFocusReason); - } - - Shortcut { - sequence: "Tab" - enabled: root.section !== "" && root.activeFocus - onActivated: root.moveFocus(false) - } - - Shortcut { - sequences: ["Backtab", "Shift+Tab"] - enabled: root.section !== "" && root.activeFocus - onActivated: root.moveFocus(true) - } - function dismissTransient() { const item = pageItem; if (!item || typeof item.dismissTransient !== "function") @@ -129,132 +121,52 @@ FocusScope { } onSectionChanged: { - enterPending = false; - enterAnimation.stop(); - exitAnimation.stop(); if (section === "") { - if (!animationsEnabled) { - root.opacity = 0; + if (!CcMetrics.animationsEnabled) finishClose(); - return; - } - exitBlocker.forceActiveFocus(); - exitAnimation.start(); return; } - const opening = shownSection === ""; - enterPending = opening && animationsEnabled; - root.opacity = enterPending ? 0 : 1; - dialogSurface.entryScale = enterPending ? CcMetrics.popupEnterScale : 1; shownSection = section; pageLoader.sourceComponent = _componentFor(section); (pageItem ?? root).forceActiveFocus(); } - Connections { - target: root.Window.window - enabled: root.enterPending - - function onFrameSwapped() { - root.enterPending = false; - enterAnimation.start(); - } - } - - ParallelAnimation { - id: enterAnimation - - NumberAnimation { - target: root - property: "opacity" - to: 1 - duration: CcMetrics.fadeDuration - easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.expressiveEffects - } - NumberAnimation { - target: dialogSurface - property: "entryScale" - to: 1 - duration: Theme.expressiveDurations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial - } - } - - NumberAnimation { - id: exitAnimation - target: root - property: "opacity" - to: 0 - duration: CcMetrics.fadeDuration - easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.expressiveEffects - onFinished: root.finishClose() + onHeightChanged: { + if (section === "" && height <= 0.5) + finishClose(); } Rectangle { - anchors.fill: parent - topLeftRadius: root.cornerRadii.x - topRightRadius: root.cornerRadii.y - bottomRightRadius: root.cornerRadii.z - bottomLeftRadius: root.cornerRadii.w - color: Theme.scrimColor - opacity: Theme.scrimAlpha - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.AllButtons - onClicked: { - if (root.section !== "") - root.backRequested(); - } - onWheel: wheel => wheel.accepted = true - } - - Rectangle { - id: dialogSurface - - x: CcMetrics.detailDialogInset - y: root.topInset + Math.max(0, (root.maximumHeight - height) / 2) - width: Math.max(0, root.width - CcMetrics.detailDialogInset * 2) - height: Math.max(0, Math.min(root.chromeHeight + root.contentHeight, root.maximumHeight)) - radius: Theme.cornerRadiusXL - - Behavior on height { - enabled: root.animationsEnabled && !root.transitioning && root.section !== "" - NumberAnimation { - duration: Theme.expressiveDurations.expressiveFastSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.standard - } - } + 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 - property real entryScale: 1 - scale: Math.min(1, entryScale) - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.AllButtons - } Item { id: panel anchors.fill: parent anchors.margins: CcMetrics.detailDialogPadding - opacity: CcMetrics.hideCoveredContent ? 1 - root.coverage : 1 + 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.pageHeaderHeight + height: CcMetrics.detailHeaderHeight StyledText { anchors.left: parent.left @@ -263,7 +175,8 @@ FocusScope { anchors.rightMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter text: root.title - font.pixelSize: CcMetrics.pageTitleSize + font.pixelSize: CcMetrics.detailTitleSize + font.weight: Theme.fontWeightMedium color: Theme.surfaceText elide: Text.ElideRight horizontalAlignment: Text.AlignLeft @@ -308,18 +221,6 @@ FocusScope { } } - MouseArea { - id: exitBlocker - - anchors.fill: parent - visible: root.section === "" - hoverEnabled: true - acceptedButtons: Qt.AllButtons - onWheel: wheel => wheel.accepted = true - Keys.onPressed: event => event.accepted = true - Keys.onReleased: event => event.accepted = true - } - Connections { target: root.pageItem ignoreUnknownSignals: true diff --git a/quickshell/Modules/ControlCenter/Components/CcTileGrid.qml b/quickshell/Modules/ControlCenter/Components/CcTileGrid.qml index 4e7f7427b..06a7cb38e 100644 --- a/quickshell/Modules/ControlCenter/Components/CcTileGrid.qml +++ b/quickshell/Modules/ControlCenter/Components/CcTileGrid.qml @@ -4,6 +4,7 @@ import QtQuick import qs.Common import qs.Widgets import qs.Modules.ControlCenter +import "../utils/sections.js" as Sections import "../utils/widgets.js" as WidgetUtils import "../../../Common/GridLayout.js" as GridUtils @@ -16,6 +17,17 @@ DEditableGrid { property int columns: CcMetrics.gridColumns property real availableHeight: CcMetrics.fallbackScreenHeight - CcMetrics.maxHeightInset readonly property int maximumRows: CcMetrics.rowCapFor(availableHeight, cellWidth - CcMetrics.gridGap) + // The detail band opens under this tile's row and pushes the rows below it down. While a band is shown the + // slots follow its animated height frame by frame instead of running their own motion, so one animation drives + // both; the section is the band's own, which it keeps until it has fully collapsed. + property string expandedSection: "" + property real bandHeight: 0 + property real bandTargetHeight: 0 + property bool bandMoving: false + readonly property int expandedIndex: indexForSection(expandedSection) + readonly property real bandY: slotLayout.bandY ?? 0 + readonly property real baseHeight: baseLayout.totalHeight + readonly property real targetHeight: GridUtils.insertBand(baseLayout, expandedIndex, expandedSection !== "" ? bandTargetHeight : 0).totalHeight signal expandClicked(var widgetData) signal removeWidget(int index) @@ -44,11 +56,14 @@ DEditableGrid { readonly property var shownIndices: savedWidgets.reduce((indices, widget, i) => !WidgetUtils.inGroup(widget, groupIds) && WidgetUtils.isShown(widget) && model?.componentForWidget(widget) ? indices.concat([i]) : indices, []) sourceItems: shownIndices.map(i => Object.assign({}, savedWidgets[i], sizeWithHiddenTwin(i))) - slotLayout: { + readonly property var baseLayout: { const items = clamped(layoutItems); const layout = GridUtils.packCards(items, placementOrder, columns, width, CcMetrics.gridGap, cellWidth - CcMetrics.gridGap, I18n.isRtl, null, CcMetrics.gridStep, true); return GridUtils.shrinkRows(layout, items.map(item => WidgetUtils.isSmall(item, item.h)), CcMetrics.smallRowHeight); } + // Whole pixels keep the rows crisp while they ride the band's animation. + slotLayout: GridUtils.insertBand(baseLayout, expandedIndex, Math.round(bandHeight)) + holdLayout: bandMoving gravity: true rowStep: CcMetrics.gridStep packer: items => GridUtils.packCells(clamped(items), placementOrder, columns, null, CcMetrics.gridStep, true).cells @@ -60,6 +75,17 @@ DEditableGrid { memberDragging = false; } + // A grouped member expands under its group's tile. + function indexForSection(section) { + if (section === "") + return -1; + const widget = Sections.widgetForSection(section); + const target = widget?.group ? savedWidgets.find(other => other?.id === WidgetUtils.GROUP_ID && other.instanceId === widget.group) : widget; + if (!target) + return -1; + return sourceItems.findIndex(item => item.id === target.id && (item.instanceId || "") === (target.instanceId || "")); + } + function clamped(items) { return items.map(widget => Object.assign({}, widget, WidgetUtils.clampSize(widget, columns, maximumRows))); } diff --git a/quickshell/Modules/ControlCenter/Components/PluginDetailShell.qml b/quickshell/Modules/ControlCenter/Components/PluginDetailShell.qml index a24dfe87a..cd19acf8e 100644 --- a/quickshell/Modules/ControlCenter/Components/PluginDetailShell.qml +++ b/quickshell/Modules/ControlCenter/Components/PluginDetailShell.qml @@ -10,6 +10,7 @@ Item { readonly property var instance: builtinInstance ?? pluginHost.instance readonly property real preferredHeight: instance?.ccDetailHeight ?? 0 + readonly property bool empty: (instance?.ccDetailContent ?? null) === null readonly property string title: instance?.ccWidgetPrimaryText || PluginService.loadedPlugins[pluginId]?.name || PluginService.availablePluginsList.find(p => p.id === pluginId)?.name || "" PluginInstanceHost { diff --git a/quickshell/Modules/ControlCenter/ControlCenterContent.qml b/quickshell/Modules/ControlCenter/ControlCenterContent.qml index 3903b319a..e8ec7889b 100644 --- a/quickshell/Modules/ControlCenter/ControlCenterContent.qml +++ b/quickshell/Modules/ControlCenter/ControlCenterContent.qml @@ -24,12 +24,13 @@ FocusScope { readonly property bool pageOpen: (host.expandedSection ?? "") !== "" readonly property real gridHeight: widgetGrid.gridHeight + readonly property real gridTargetHeight: widgetGrid.targetHeight // Reserved from the grid in both modes so the island growing into edit mode never reflows tiles. readonly property real editRoom: host.editRoom ?? 0 readonly property real chromeHeight: (host.editRoomShown ?? false) ? editRoom : 0 readonly property bool stripAbove: host.stripAbove ?? false readonly property bool widgetSheetOpen: widgetSheetLoader.item?.active ?? false - readonly property real coveredAmount: Math.max(detailPage.opacity, widgetSheetLoader.item?.progress ?? 0) + readonly property real coveredAmount: Math.max(widgetSheetLoader.item?.progress ?? 0, codecSelectorLoader.item?.presence ?? 0, portSelectorLoader.item?.presence ?? 0) property bool widgetSheetRequested: false property int toplevelRevision: 0 readonly property var runningToplevels: { @@ -46,13 +47,8 @@ FocusScope { readonly property real dragRoom: widgetGrid.dragging && (host.growsFromTop ?? true) && widgetGrid.pinnedHeight + newRowUnit <= availableGridHeight ? newRowUnit : 0 readonly property real newRowUnit: CcMetrics.gridGap + widgetGrid.slotLayout.rowUnit // Held during a drag so the trash never moves under the pointer. - readonly property real heldGridHeight: widgetGrid.dragging ? widgetGrid.pinnedHeight + dragRoom : gridHeight - readonly property real targetImplicitHeight: { - const total = CcMetrics.sheetPadding * 2 + heldGridHeight; - if (detailPage.shownSection === "") - return total; - return Math.max(total, detailPage.topInset + detailPage.minimumHeight + CcMetrics.detailDialogInset); - } + readonly property real heldGridHeight: widgetGrid.dragging ? widgetGrid.pinnedHeight + dragRoom : gridTargetHeight + readonly property real targetImplicitHeight: CcMetrics.sheetPadding * 2 + heldGridHeight property Item detailReturnFocus: null property var pageHistory: [] // One string so Discard and undo can compare and restore the whole session state at once. @@ -115,13 +111,13 @@ FocusScope { return; if (host.expandedSection) pageHistory = pageHistory.concat([host.expandedSection]); - else if (detailPage.shownSection === "") + else if (detailBand.shownSection === "") detailReturnFocus = root.Window.window?.activeFocusItem ?? null; host.expandedSection = section; } function goBack() { - if (detailPage.dismissTransient()) + if (detailBand.dismissTransient()) return; if (pageHistory.length > 0) { const previous = pageHistory[pageHistory.length - 1]; @@ -132,6 +128,14 @@ FocusScope { host.collapseAll(); } + // Followed while the band grows, so it stays in view however the popout and the grid settle around it. + function revealDetail() { + if (detailBand.section === "") + return; + const top = mainColumn.y + body.y + detailBand.y; + contentFlickable.revealRange(top, top + detailBand.height); + } + function openWidgetPage(widgetData) { const section = Sections.sectionFor(widgetData); if (host.expandedSection === section) { @@ -366,7 +370,7 @@ FocusScope { anchors.bottomMargin: -contentInset - (editActions.joined && !root.stripAbove ? editActions.height : 0) z: 1 visible: root.host.editMode - enabled: detailPage.shownSection === "" && !root.widgetSheetOpen + enabled: detailBand.shownSection === "" && !root.widgetSheetOpen edgeResize: root.panelResizing ? resizeSide < 0 : root.panelResizer.sideMovable(-1, root.gridColumns) cornerResize: root.panelResizing ? resizeSide > 0 : root.panelResizer.sideMovable(1, root.gridColumns) horizontalResize: true @@ -451,7 +455,11 @@ FocusScope { anchors.top: parent.top editMode: root.host.editMode model: widgetModel - live: root.host.shouldBeVisible && detailPage.shownSection === "" + live: root.host.shouldBeVisible + expandedSection: detailBand.shownSection + bandHeight: detailBand.height + bandTargetHeight: detailBand.targetHeight + bandMoving: detailBand.shownSection !== "" screenName: root.host.triggerScreen?.name || "" tapToClose: root.host.headerTogglesClose ?? false runningToplevels: root.runningToplevels @@ -481,6 +489,37 @@ FocusScope { loader.item.openFromControlCenter(bounds, root.host.screen); } } + + CcDetailBand { + id: detailBand + + x: 0 + y: widgetGrid.bandY + width: parent.width + section: root.host.expandedSection ?? "" + maximumHeight: root.availableGridHeight - widgetGrid.baseHeight + coverage: Math.max(codecSelectorLoader.item?.presence ?? 0, portSelectorLoader.item?.presence ?? 0) + model: widgetModel + screenName: root.host.triggerScreen?.name || "" + screenModel: root.host.triggerScreen?.model || "" + transientSurfaceTracker: root.host.transientSurfaceTracker + runningToplevels: root.runningToplevels + onCodecSelectorRequested: device => root.showCodecSelector(device) + onPortSelectorRequested: node => root.showPortSelector(node) + onBackRequested: root.goBack() + onCollapseRequested: root.host.collapseAll() + onCloseRequested: root.host.close() + onHeightChanged: root.revealDetail() + onDismissed: { + const target = root.detailReturnFocus; + root.detailReturnFocus = null; + if (target?.visible && target.enabled) { + target.forceActiveFocus(); + return; + } + root.forceActiveFocus(); + } + } } } } @@ -495,7 +534,7 @@ FocusScope { // Below the edit rings so the shoulder fill never covers them. z: -1 editMode: root.host.editMode - enabled: detailPage.shownSection === "" && !root.widgetSheetOpen + enabled: detailBand.shownSection === "" && !root.widgetSheetOpen dragging: root.gridDragWidget !== null dragRemovable: WidgetUtils.isRemovable(root.gridDragWidget) overTrash: root.overTrash @@ -514,36 +553,6 @@ FocusScope { onCancelRequested: root.cancelEdit() } - CcDetailPage { - id: detailPage - - z: 1 - anchors.fill: parent - section: root.host.expandedSection ?? "" - topInset: CcMetrics.sheetPadding - cornerRadii: root.surfaceCornerRadii - coverage: Math.max(codecSelectorLoader.item?.presence ?? 0, portSelectorLoader.item?.presence ?? 0) - model: widgetModel - screenName: root.host.triggerScreen?.name || "" - screenModel: root.host.triggerScreen?.model || "" - transientSurfaceTracker: root.host.transientSurfaceTracker - onCodecSelectorRequested: device => root.showCodecSelector(device) - onPortSelectorRequested: node => root.showPortSelector(node) - runningToplevels: root.runningToplevels - onBackRequested: root.goBack() - onCollapseRequested: root.host.collapseAll() - onCloseRequested: root.host.close() - onDismissed: { - const target = root.detailReturnFocus; - root.detailReturnFocus = null; - if (target?.visible && target.enabled) { - target.forceActiveFocus(); - return; - } - root.forceActiveFocus(); - } - } - Loader { id: codecSelectorLoader diff --git a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml index ced393d4b..eb7082e65 100644 --- a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml +++ b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml @@ -10,7 +10,7 @@ DPopout { layerNamespace: "dms:control-center" fullHeightSurface: true resizeCurve: Theme.expressiveCurves.standard - resizeDuration: Theme.expressiveDurations.expressiveFastSpatial + resizeDuration: CcMetrics.transitionDuration resizeMotion: true resizing: contentLoader.item?.panelResizing ?? false surfacePadding: CcMetrics.editOverflow diff --git a/quickshell/Modules/ControlCenter/Widgets/CcListRow.qml b/quickshell/Modules/ControlCenter/Widgets/CcListRow.qml index 87f1f170b..10d5f82d0 100644 --- a/quickshell/Modules/ControlCenter/Widgets/CcListRow.qml +++ b/quickshell/Modules/ControlCenter/Widgets/CcListRow.qml @@ -4,6 +4,7 @@ import qs.Modules.ControlCenter import qs.Modules.Settings.Widgets SettingsRow { + minHeight: Theme.listItemHeight paddingH: CcMetrics.rowPaddingH paddingV: CcMetrics.rowPaddingV rowColor: CcMetrics.rowColor diff --git a/quickshell/Modules/ControlCenter/utils/widgets.js b/quickshell/Modules/ControlCenter/utils/widgets.js index 4afc86f06..a7d151223 100644 --- a/quickshell/Modules/ControlCenter/utils/widgets.js +++ b/quickshell/Modules/ControlCenter/utils/widgets.js @@ -137,7 +137,7 @@ function clampSize(widget, columns, rows = Infinity) { function addWidget(widgetId, columns) { const widgets = Common.SettingsData.controlCenterWidgets.slice(); const widget = defaultWidget(widgetId, columns); - if (BUTTON_IDS.includes(widgetId)) + if (BUTTON_IDS.includes(widgetId) || isSliderWidget(widgetId)) widget.small = true; if (widgetId === "diskUsage") { diff --git a/quickshell/Widgets/DEditableGrid.qml b/quickshell/Widgets/DEditableGrid.qml index 13ba3b990..a9c241993 100644 --- a/quickshell/Widgets/DEditableGrid.qml +++ b/quickshell/Widgets/DEditableGrid.qml @@ -16,6 +16,8 @@ Item { property real placeholderRadius: Theme.cornerRadiusXL property bool animationsEnabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None property bool animateLayout: false + // Slots take layout changes as they come, for a layout that an animation elsewhere already drives frame by frame. + property bool holdLayout: false property int draggingSourceIndex: -1 property var dragCell: null property var sizePreview: null diff --git a/quickshell/Widgets/DEditableGridSlot.qml b/quickshell/Widgets/DEditableGridSlot.qml index d9cd47a94..fc55eda71 100644 --- a/quickshell/Widgets/DEditableGridSlot.qml +++ b/quickshell/Widgets/DEditableGridSlot.qml @@ -112,7 +112,7 @@ Item { } Behavior on width { - enabled: !root.dragging && root.resizeOrigin === null && root.grid.animationsEnabled && root.grid.animateLayout + enabled: !root.dragging && root.resizeOrigin === null && root.grid.animationsEnabled && root.grid.animateLayout && !root.grid.holdLayout NumberAnimation { duration: Theme.expressiveDurations.expressiveFastSpatial easing.type: Easing.BezierSpline @@ -121,7 +121,7 @@ Item { } Behavior on height { - enabled: !root.dragging && root.resizeOrigin === null && root.grid.animationsEnabled && root.grid.animateLayout + enabled: !root.dragging && root.resizeOrigin === null && root.grid.animationsEnabled && root.grid.animateLayout && !root.grid.holdLayout NumberAnimation { duration: Theme.expressiveDurations.expressiveFastSpatial easing.type: Easing.BezierSpline @@ -130,7 +130,7 @@ Item { } Behavior on x { - enabled: !root.dragging && root.resizeOrigin === null && root.grid.animationsEnabled && root.grid.animateLayout + enabled: !root.dragging && root.resizeOrigin === null && root.grid.animationsEnabled && root.grid.animateLayout && !root.grid.holdLayout NumberAnimation { duration: Theme.expressiveDurations.expressiveDefaultSpatial easing.type: Easing.BezierSpline @@ -139,7 +139,7 @@ Item { } Behavior on y { - enabled: !root.dragging && root.resizeOrigin === null && root.grid.animationsEnabled && root.grid.animateLayout + enabled: !root.dragging && root.resizeOrigin === null && root.grid.animationsEnabled && root.grid.animateLayout && !root.grid.holdLayout NumberAnimation { duration: Theme.expressiveDurations.expressiveDefaultSpatial easing.type: Easing.BezierSpline