feat(control center): configurable user card & power control action tiles

- Quick actions are reorderable with button color roles and optional power accents
- Updated dank-qml-common
This commit is contained in:
purian23
2026-09-27 20:55:54 -04:00
parent a4e500bc00
commit 64b4d0d235
35 changed files with 837 additions and 373 deletions
+25 -6
View File
@@ -1,4 +1,4 @@
function packCells(cards, order, columns, isAvailable, step = 1) {
function packCells(cards, order, columns, isAvailable, step = 1, gravity = false) {
const steps = Math.round(columns / step);
const cells = [];
const taken = [];
@@ -18,12 +18,21 @@ function packCells(cards, order, columns, isAvailable, step = 1) {
const h = Math.max(1, Math.round((card.h || 1) / step));
const cell = positioned(card) ? settle(taken, Math.max(0, Math.min(steps - w, Math.round(card.col / step))), Math.max(0, Math.round(card.row / step)), w, h) : firstFit(taken, steps, w, h);
taken.push(cell);
cells[sourceIndex] = cell;
}
if (gravity)
compact(taken);
for (let i = 0; i < cells.length; i++) {
const cell = cells[i];
if (!cell)
continue;
rows = Math.max(rows, cell.y + cell.h);
cells[sourceIndex] = {
cells[i] = {
"col": cell.x * step,
"row": cell.y * step,
"cols": w * step,
"rows": h * step
"cols": cell.w * step,
"rows": cell.h * step
};
}
@@ -33,6 +42,16 @@ function packCells(cards, order, columns, isAvailable, step = 1) {
};
}
// Saved positions only settle downward, so a shrink or removal above leaves a hole; gravity pulls everything up into it.
function compact(taken) {
const ordered = taken.slice().sort((a, b) => a.y - b.y || a.x - b.x);
for (const cell of ordered) {
const others = taken.filter(other => other !== cell);
while (cell.y > 0 && !overlaps(others, cell.x, cell.y - 1, cell.w, cell.h))
cell.y--;
}
}
function positioned(card) {
return Number.isFinite(card.col) && Number.isFinite(card.row);
}
@@ -73,8 +92,8 @@ function firstFit(taken, steps, w, h) {
};
}
function packCards(cards, order, columns, width, gap, rowUnit, mirror, isAvailable, step = 1) {
const packed = packCells(cards, order, columns, isAvailable, step);
function packCards(cards, order, columns, width, gap, rowUnit, mirror, isAvailable, step = 1, gravity = false) {
const packed = packCells(cards, order, columns, isAvailable, step, gravity);
const colW = (width - gap * (columns - 1)) / columns;
const slots = packed.cells.map(cell => {
if (!cell)
+2 -1
View File
@@ -21,7 +21,7 @@ Singleton {
id: root
readonly property var log: Log.scoped("SettingsData")
readonly property int settingsConfigVersion: 31
readonly property int settingsConfigVersion: 32
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -452,6 +452,7 @@ Singleton {
property bool dankLauncherV2IncludeFoldersInAll: Spec.SPEC.dankLauncherV2IncludeFoldersInAll.def
property bool launcherUseOverlayLayer: Spec.SPEC.launcherUseOverlayLayer.def
property string launcherStyle: Spec.SPEC.launcherStyle.def
property string avatarRing: Spec.SPEC.avatarRing.def
property bool spotlightBarShowModeChips: Spec.SPEC.spotlightBarShowModeChips.def
property bool keybindsFloatingWindow: Spec.SPEC.keybindsFloatingWindow.def
onKeybindsFloatingWindowChanged: saveSettings()
+3
View File
@@ -578,6 +578,9 @@ Singleton {
}
}
readonly property real avatarRingWidth: SettingsData.avatarRing === "none" ? 0 : outlineWidth
readonly property color avatarRingColor: SettingsData.avatarRing === "outline" ? surfaceVariant : roleColor(SettingsData.avatarRing)
function roleColor(mode) {
switch (mode) {
case "primary":
@@ -430,6 +430,18 @@ var LOCAL_SPEC = {
},
controlCenterWidgets: {
def: [
{
id: "userCard",
enabled: true,
w: 4,
h: 1
},
{
id: "quickActions",
enabled: true,
w: 4,
h: 1
},
{
id: "volumeSlider",
enabled: true,
@@ -635,6 +647,9 @@ var LOCAL_SPEC = {
launcherStyle: {
def: "full"
},
avatarRing: {
def: "outline"
},
spotlightBarShowModeChips: {
def: false
},
@@ -15,6 +15,7 @@ var SESSION_BACKED_PLUGIN_IDS = ["dankNotepadModule"];
var STALE_WIDGET_KEYS = ["desktopClockEnabled", "desktopClockStyle", "desktopClockTransparency", "desktopClockColorMode", "desktopClockCustomColor", "desktopClockShowDate", "desktopClockShowAnalogNumbers", "desktopClockShowAnalogSeconds", "desktopClockX", "desktopClockY", "desktopClockWidth", "desktopClockHeight", "desktopClockDisplayPreferences", "systemMonitorEnabled", "systemMonitorShowHeader", "systemMonitorTransparency", "systemMonitorColorMode", "systemMonitorCustomColor", "systemMonitorShowCpu", "systemMonitorShowCpuGraph", "systemMonitorShowCpuTemp", "systemMonitorShowGpuTemp", "systemMonitorGpuPciId", "systemMonitorShowMemory", "systemMonitorShowMemoryGraph", "systemMonitorShowNetwork", "systemMonitorShowNetworkGraph", "systemMonitorShowDisk", "systemMonitorShowTopProcesses", "systemMonitorTopProcessCount", "systemMonitorTopProcessSortBy", "systemMonitorGraphInterval", "systemMonitorLayoutMode", "systemMonitorX", "systemMonitorY", "systemMonitorWidth", "systemMonitorHeight", "systemMonitorDisplayPreferences", "systemMonitorVariants", "desktopWidgetPositions"];
var BAR_WIDGET_LIST_KEYS = ["leftWidgets", "centerWidgets", "rightWidgets"];
var CC_PINNED_WIDGETS = ["userCard", "quickActions"];
var REMOVED_KEYS_V21 = ["showBattery", "showCapsLockIndicator", "showClipboard", "showClock", "showControlCenterButton", "showCpuUsage", "showFocusedWindow", "showLauncherButton", "showMemUsage", "showMusic", "showNotificationButton", "showPrivacyButton", "showSystemTray", "showWeather", "showWorkspaceSwitcher", "hideBrightnessSlider", "updaterHideWidget", "workspaceScrolling", "appLauncherViewMode", "spotlightModalViewMode", "audioDeviceScrollVolumeEnabled", "desktopClockX", "desktopClockY", "desktopClockWidth", "desktopClockHeight", "desktopClockDisplayPreferences", "systemMonitorX", "systemMonitorY", "systemMonitorWidth", "systemMonitorHeight", "systemMonitorDisplayPreferences", "systemMonitorVariants"];
@@ -698,6 +699,20 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 31;
}
if (currentVersion < 32 && targetVersion >= 32) {
if (Array.isArray(settings.controlCenterWidgets)) {
const ids = settings.controlCenterWidgets.map(widget => widget?.id);
const pinned = CC_PINNED_WIDGETS.filter(id => !ids.includes(id)).map(id => ({
id: id,
enabled: true,
w: 4,
h: 1
}));
settings.controlCenterWidgets = pinned.concat(settings.controlCenterWidgets);
}
settings.configVersion = 32;
}
return settings;
}
@@ -21,6 +21,8 @@ SettingsNavRow {
DankCircularImage {
width: Theme.minimumTouchTargetSize
ringWidth: Theme.avatarRingWidth
ringColor: Theme.avatarRingColor
height: width
anchors.left: parent.left
anchors.leftMargin: (SettingsMetrics.navIconSize - width) / 2
@@ -54,11 +54,13 @@ Singleton {
readonly property real iconBoxIconSize: Theme.iconSize * iconScale
readonly property real tallMeterThickness: 28
readonly property real tileTextGap: Theme.spacingM
readonly property real headerActionSize: Theme.iconButtonSize * iconScale
readonly property real headerActionIconSize: Theme.iconSize * iconScale
readonly property real headerAvatarSize: 56
readonly property real headerHeight: tileHeight
readonly property real actionSize: Theme.iconButtonSize * iconScale
readonly property real actionIconSize: Theme.iconSize * iconScale
readonly property real actionGap: Theme.spacingS
// Buttons never shrink, so the actions tile cannot go narrower than its pills plus padding.
function quickActionsMinColumns(count) {
return Math.ceil((actionSize * count + actionGap * Math.max(0, count - 1) + Theme.spacingS * 2 + gridGap) / (columnWidth + gridGap) / gridStep) * gridStep;
}
readonly property real detailDialogInset: Theme.spacingL
readonly property real detailDialogPadding: Theme.spacingS
@@ -105,6 +107,7 @@ Singleton {
readonly property int overlayZ: 10000
readonly property real popupEnterScale: 0.92
readonly property color dialogColor: Theme.foregroundColor(Theme.hostSurface)
readonly property int backdropBlurRadius: 32
readonly property bool hideCoveredContent: BlurService.enabled && Theme.connectedSurfaceBlurEnabled && dialogColor.a < 1
readonly property color tileActiveColor: Theme.ccTileActiveBg
@@ -1,6 +1,7 @@
import QtQuick
import qs.Common
import qs.Widgets
import "../utils/widgets.js" as WidgetUtils
import qs.Modules.ControlCenter
DankGridEditChrome {
@@ -10,7 +11,8 @@ DankGridEditChrome {
signal configRequested(var anchor)
hasOptions: widgetData.id === "diskUsage" || widgetData.id === "brightnessSlider" || widgetData.id === "idleInhibitor" || String(widgetData.id ?? "").startsWith("plugin_")
hasOptions: WidgetUtils.hasOptions(widgetData.id)
removable: !WidgetUtils.isPinned(widgetData.id)
buttonSize: Theme.iconSize
hitOverflow: CcMetrics.gridGap / 2
iconSize: PopoutMetrics.chromeIconSize
@@ -21,13 +21,19 @@ DankEditableGrid {
signal removeWidget(int index)
signal configRequested(int index, var widgetData, var anchor)
signal colorPickerRequested
signal lockRequested
signal powerRequested
signal settingsRequested
signal editRequested
signal headerTapped
property bool tapToClose: false
readonly property real gridHeight: layoutHeight
readonly property real cellWidth: (width + CcMetrics.gridGap) / columns
readonly property CcTileSlot draggingSlot: tileRepeater.itemAt(draggingSourceIndex) as CcTileSlot
sourceItems: (SettingsData.controlCenterWidgets || []).map(widget => Object.assign({}, widget, WidgetUtils.clampSize(widget, Infinity)))
slotLayout: GridUtils.packCards(layoutItems.map(widget => Object.assign({}, widget, WidgetUtils.clampSize(widget, columns, maximumRows))), placementOrder, columns, width, CcMetrics.gridGap, cellWidth - CcMetrics.gridGap, I18n.isRtl, null, CcMetrics.gridStep)
slotLayout: GridUtils.packCards(layoutItems.map(widget => Object.assign({}, widget, WidgetUtils.clampSize(widget, columns, maximumRows))), placementOrder, columns, width, CcMetrics.gridGap, cellWidth - CcMetrics.gridGap, I18n.isRtl, null, CcMetrics.gridStep, true)
placeholderRadius: draggingSlot?.tileItem?.bodyRadius ?? Theme.fullRadius(width, CcMetrics.tileHeight)
onLayoutCommitted: items => model.setLayout(items)
@@ -9,7 +9,7 @@ DankEditableGridSlot {
id: root
readonly property var widgetData: JSON.parse(json)
readonly property var sizeSpec: WidgetUtils.sizeSpec(widgetData.id || "", grid.columns, grid.maximumRows)
readonly property var sizeSpec: WidgetUtils.sizeSpec(widgetData, grid.columns, grid.maximumRows)
readonly property real cols: slot?.cols ?? 1
readonly property real rows: slot?.rows ?? 1
readonly property bool compact: cols <= 2 && rows === 1
@@ -24,8 +24,10 @@ Row {
signal addWidget(string widgetId)
signal resetToDefault
signal clearAll
signal finishRequested
signal cancelRequested
readonly property real buttonWidth: (width - spacing * 2) / 3
readonly property real buttonWidth: (width - spacing * 4 - Theme.buttonHeightS * 2) / 3
height: Theme.buttonHeightS
spacing: Theme.spacingS
@@ -142,4 +144,22 @@ Row {
textColor: Theme.error
onClicked: root.clearAll()
}
DankActionButton {
buttonSize: Theme.buttonHeightS
iconName: "close"
iconColor: Theme.onSecondaryContainer
backgroundColor: Theme.secondaryContainer
tooltipText: I18n.tr("Cancel")
onClicked: root.cancelRequested()
}
DankActionButton {
buttonSize: Theme.buttonHeightS
iconName: "check"
iconColor: Theme.primaryText
backgroundColor: Theme.primary
tooltipText: I18n.tr("Finish")
onClicked: root.finishRequested()
}
}
@@ -1,149 +0,0 @@
import QtQuick
import qs.Common
import qs.Modules.ControlCenter
import qs.Services
import qs.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property bool editMode: false
property bool tapToClose: false
property bool live: true
signal powerButtonClicked
signal lockRequested
signal editModeToggled
signal editCancelled
signal settingsButtonClicked
signal headerTapped
Ref {
service: DgopService
modules: "system"
active: root.live && root.visible && (root.Window.window?.visible ?? false)
}
implicitHeight: CcMetrics.headerHeight
height: implicitHeight
MouseArea {
anchors.fill: parent
enabled: root.tapToClose
acceptedButtons: Qt.LeftButton
onClicked: root.headerTapped()
}
Item {
anchors.left: parent.left
anchors.right: actionButtonsRow.left
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
height: CcMetrics.headerAvatarSize
DankCircularImage {
id: avatar
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: CcMetrics.headerAvatarSize
height: CcMetrics.headerAvatarSize
imageSource: {
if (PortalService.profileImage === "")
return "";
if (PortalService.profileImage.startsWith("/"))
return "file://" + PortalService.profileImage;
return PortalService.profileImage;
}
fallbackIcon: "person"
}
Item {
anchors.left: avatar.right
anchors.leftMargin: Theme.spacingM
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
implicitHeight: userLabel.implicitHeight + Theme.spacingXXS + uptimeLabel.implicitHeight
StyledText {
id: userLabel
width: parent.width
text: UserInfoService.fullName || UserInfoService.username || I18n.tr("User")
font.pixelSize: Theme.fontSizeLarge
font.weight: Theme.fontWeightMedium
color: Theme.surfaceText
elide: Text.ElideRight
horizontalAlignment: Text.AlignLeft
}
StyledText {
id: uptimeLabel
y: userLabel.implicitHeight + Theme.spacingXXS
width: parent.width
text: DgopService.uptime ? I18n.tr("up", "uptime prefix, e.g. 'up 4h 2m'") + " " + DgopService.uptime.slice(3) : I18n.tr("Unknown")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
horizontalAlignment: Text.AlignLeft
}
}
}
Row {
id: actionButtonsRow
height: CcMetrics.headerActionSize
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankActionButton {
buttonSize: CcMetrics.headerActionSize
iconSize: CcMetrics.headerActionIconSize
iconName: "lock"
iconColor: Theme.surfaceText
Accessible.name: I18n.tr("Lock")
onClicked: root.lockRequested()
}
DankActionButton {
buttonSize: CcMetrics.headerActionSize
iconSize: CcMetrics.headerActionIconSize
iconName: "power_settings_new"
iconColor: Theme.surfaceText
Accessible.name: I18n.tr("Power")
onClicked: root.powerButtonClicked()
}
DankActionButton {
buttonSize: CcMetrics.headerActionSize
iconSize: CcMetrics.headerActionIconSize
iconName: "settings"
iconColor: Theme.surfaceText
Accessible.name: I18n.tr("Settings")
onClicked: root.settingsButtonClicked()
}
DankActionButton {
buttonSize: CcMetrics.headerActionSize
iconSize: CcMetrics.headerActionIconSize
iconName: "close"
iconColor: Theme.surfaceText
visible: root.editMode
Accessible.name: I18n.tr("Cancel")
onClicked: root.editCancelled()
}
DankActionButton {
buttonSize: CcMetrics.headerActionSize
iconSize: CcMetrics.headerActionIconSize
iconName: root.editMode ? "done" : "edit"
iconColor: root.editMode ? Theme.onSecondaryContainer : Theme.surfaceText
backgroundColor: root.editMode ? Theme.secondaryContainer : "transparent"
Accessible.name: root.editMode ? I18n.tr("Finish") : I18n.tr("Edit")
onClicked: root.editModeToggled()
}
}
}
@@ -1,14 +1,22 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import Quickshell.Widgets
import qs.Common
import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.Widgets
import qs.Modules.DankBar.Widgets
import qs.Modules.DankDash
import qs.Modules.Settings.Widgets
import "../utils/widgets.js" as WidgetUtils
import qs.Services
import qs.Widgets
Item {
id: root
property Item backdrop: null
property int widgetIndex: -1
property real anchorX: 0
property real anchorY: 0
@@ -25,6 +33,9 @@ Item {
readonly property bool isPlugin: widgetId.startsWith("plugin_")
readonly property bool isDisk: widgetId === "diskUsage"
readonly property bool isIdleInhibitor: widgetId === "idleInhibitor"
readonly property bool isUserCard: widgetId === "userCard"
readonly property bool isQuickActions: widgetId === "quickActions"
readonly property var quickActions: isQuickActions ? WidgetUtils.quickActions(widgetData) : []
visible: widgetIndex >= 0
z: CcMetrics.overlayZ
@@ -43,6 +54,12 @@ Item {
widgetIndex = -1;
}
function toggleAction(id, enabled) {
persistOption("actions", quickActions.map(action => action.id === id ? Object.assign({}, action, {
"enabled": enabled
}) : action));
}
function persistOption(key, value) {
const widgets = (SettingsData.controlCenterWidgets || []).slice();
if (widgetIndex < 0 || widgetIndex >= widgets.length)
@@ -105,6 +122,48 @@ Item {
}
}
// The popout is not a real subsurface, so fake compositor blur the same way the detail dialog does, but from the sheet content it covers.
Loader {
anchors.fill: parent
z: -1
active: root.visible && root.backdrop !== null && CcMetrics.hideCoveredContent
sourceComponent: ClippingRectangle {
id: clipArea
// Children land in an internal content item, so `parent` never reaches this pad.
readonly property real pad: CcMetrics.backdropBlurRadius
radius: panel.radius
color: "transparent"
ShaderEffectSource {
id: backdropSource
x: -clipArea.pad
y: -clipArea.pad
width: clipArea.width + clipArea.pad * 2
height: clipArea.height + clipArea.pad * 2
visible: false
sourceItem: root.backdrop
// mapFromItem is not reactive, so the capture is rebuilt from the panel's own geometry.
sourceRect: {
const origin = root.backdrop.mapFromItem(root, panel.x - clipArea.pad, panel.y - clipArea.pad);
return Qt.rect(origin.x, origin.y, width, height);
}
}
MultiEffect {
anchors.fill: backdropSource
source: backdropSource
blurEnabled: true
blur: 1
blurMax: CcMetrics.backdropBlurRadius
autoPaddingEnabled: false
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.AllButtons
@@ -129,6 +188,115 @@ Item {
}
}
Repeater {
model: root.isUserCard ? DashRegistry.sheetOptionSpecs("user") : []
CcToggleRow {
required property var modelData
text: modelData.text
checked: DashRegistry.optionValue(modelData, root.widgetData?.[modelData.key])
onToggled: checked => root.persistOption(modelData.key, checked)
}
}
SettingsReorderList {
id: actionList
visible: root.isQuickActions
model: root.quickActions
onReordered: indices => root.persistOption("actions", indices.map(i => root.quickActions[i]))
delegate: SettingsReorderRow {
required property var modelData
readonly property bool locked: modelData.id === "edit"
reorderList: actionList
paddingH: CcMetrics.rowPaddingH
paddingV: CcMetrics.rowPaddingV
rowColor: dragging ? Theme.blend(CcMetrics.rowColor, Theme.onSurface, Theme.stateLayerDrag) : CcMetrics.rowColor
iconName: WidgetUtils.quickActionIcon(modelData.id)
title: I18n.tr(WidgetUtils.quickActionLabel(modelData.id))
clickable: !locked
onClicked: root.toggleAction(modelData.id, !modelData.enabled)
DankToggle {
hideText: true
text: parent.title
activeFocusOnTab: false
checked: modelData.enabled
enabled: !locked
onToggled: value => root.toggleAction(modelData.id, value)
}
}
}
CcListRow {
id: colorRow
readonly property var roles: [{
"value": "default",
"label": I18n.tr("Default"),
"color": Theme.secondaryContainer
}, {
"value": "primary",
"label": I18n.tr("Primary"),
"color": Theme.primary
}, {
"value": "primaryContainer",
"label": I18n.tr("Primary Container"),
"color": Theme.primaryContainer
}, {
"value": "secondary",
"label": I18n.tr("Secondary"),
"color": Theme.secondary
}, {
"value": "surfaceVariant",
"label": I18n.tr("Surface Variant"),
"color": Theme.surfaceVariant
}, {
"value": "surfaceText",
"label": I18n.tr("Text Color"),
"color": Theme.surfaceText
}]
visible: root.isQuickActions
iconName: "palette"
title: I18n.tr("Button color")
body: DankDropdown {
compactMode: true
dropdownWidth: parent.width
currentValue: colorRow.roles.find(role => role.value === WidgetUtils.quickActionRole(root.widgetData))?.label ?? ""
options: colorRow.roles.map(role => role.label)
// QV4 has no Object.fromEntries.
optionColorMap: {
const map = {};
for (const role of colorRow.roles)
map[role.label] = role.color;
return map;
}
onValueChanged: value => {
const role = colorRow.roles.find(role => role.label === value);
if (role)
root.persistOption("buttonColor", role.value);
}
}
}
CcToggleRow {
visible: root.isQuickActions
text: I18n.tr("Background")
checked: root.widgetData?.background === true
onToggled: checked => root.persistOption("background", checked)
}
CcToggleRow {
visible: root.isQuickActions
text: I18n.tr("Highlight power", "toggle that gives the control center power button the error color")
checked: root.widgetData?.powerAccent === true
onToggled: checked => root.persistOption("powerAccent", checked)
}
CcToggleRow {
visible: root.isDisk
text: I18n.tr("Show mount path", "toggle in control center disk usage widget to turn mount path display on or off")
@@ -19,7 +19,7 @@ FocusScope {
readonly property bool pageOpen: (host.expandedSection ?? "") !== ""
readonly property real gridHeight: widgetGrid.gridHeight
readonly property real targetImplicitHeight: {
let total = CcMetrics.sheetPadding * 2 + headerPane.height + Theme.spacingS + gridHeight;
let total = CcMetrics.sheetPadding * 2 + gridHeight;
if (host.editMode)
total += Theme.spacingS + editControls.height;
if (detailPage.shownSection === "")
@@ -34,7 +34,7 @@ FocusScope {
readonly property vector4d surfaceCornerRadii: host.surfaceCornerRadii ?? Qt.vector4d(Theme.windowRadius, Theme.windowRadius, Theme.windowRadius, Theme.windowRadius)
readonly property int gridColumnCap: host.gridColumnCap ?? CcMetrics.columnCapFor((host.triggerScreen?.width ?? CcMetrics.sheetWidthDefault + Theme.spacingL * 2) - Theme.spacingL * 2)
readonly property int gridColumns: host.gridColumns ?? Math.min(CcMetrics.gridColumns, gridColumnCap)
readonly property real availableGridHeight: (host.availableHeight ?? (host.triggerScreen?.height ?? CcMetrics.fallbackScreenHeight) - CcMetrics.maxHeightInset) - CcMetrics.sheetPadding * 2 - CcMetrics.headerHeight - Theme.spacingS * 2 - editControls.height
readonly property real availableGridHeight: (host.availableHeight ?? (host.triggerScreen?.height ?? CcMetrics.fallbackScreenHeight) - CcMetrics.maxHeightInset) - CcMetrics.sheetPadding * 2 - Theme.spacingS - editControls.height
readonly property real editGutter: host.editGutter ?? 0
readonly property DankPanelResizer panelResizer: DankPanelResizer {
popout: root.host
@@ -252,33 +252,6 @@ FocusScope {
y: CcMetrics.sheetPadding
spacing: Theme.spacingS
HeaderPane {
id: headerPane
width: parent.width
editMode: root.host.editMode
live: root.host.shouldBeVisible
tapToClose: root.host.headerTogglesClose ?? false
onHeaderTapped: root.host.close()
onEditModeToggled: root.host.editMode = !root.host.editMode
onEditCancelled: root.cancelEdit()
onPowerButtonClicked: {
const loader = root.host.powerMenuModalLoader;
if (!loader)
return;
loader.active = true;
if (!loader.item)
return;
const bounds = Qt.rect(root.host.alignedX, root.host.alignedY, root.host.popupWidth, root.host.popupHeight);
loader.item.openFromControlCenter(bounds, root.host.screen);
}
onLockRequested: {
root.host.close();
root.host.lockRequested();
}
onSettingsButtonClicked: root.host.openSettings()
}
Item {
id: body
@@ -297,10 +270,28 @@ FocusScope {
model: widgetModel
live: root.host.shouldBeVisible && detailPage.shownSection === ""
screenName: root.host.triggerScreen?.name || ""
tapToClose: root.host.headerTogglesClose ?? false
onExpandClicked: widgetData => root.openWidgetPage(widgetData)
onRemoveWidget: index => widgetModel.removeWidget(index)
onConfigRequested: (index, widgetData, anchor) => root.openConfigOverlay(index, widgetData, anchor)
onColorPickerRequested: root.host.openColorPicker()
onHeaderTapped: root.host.close()
onEditRequested: root.host.editMode = !root.host.editMode
onSettingsRequested: root.host.openSettings()
onLockRequested: {
root.host.close();
root.host.lockRequested();
}
onPowerRequested: {
const loader = root.host.powerMenuModalLoader;
if (!loader)
return;
loader.active = true;
if (!loader.item)
return;
const bounds = Qt.rect(root.host.alignedX, root.host.alignedY, root.host.popupWidth, root.host.popupHeight);
loader.item.openFromControlCenter(bounds, root.host.screen);
}
}
}
@@ -324,6 +315,8 @@ FocusScope {
onAddWidget: widgetId => widgetModel.addWidget(widgetId)
onResetToDefault: () => widgetModel.resetToDefault()
onClearAll: () => widgetModel.clearAll()
onFinishRequested: root.host.editMode = false
onCancelRequested: root.cancelEdit()
}
}
}
@@ -334,7 +327,7 @@ FocusScope {
z: 1
anchors.fill: parent
section: root.host.expandedSection ?? ""
topInset: CcMetrics.sheetPadding + headerPane.height + Theme.spacingS
topInset: CcMetrics.sheetPadding
minimumContentHeight: Math.max(0, root.gridHeight - CcMetrics.pageHeaderHeight)
cornerRadii: root.surfaceCornerRadii
coverage: Math.max(codecSelectorLoader.item?.presence ?? 0, portSelectorLoader.item?.presence ?? 0)
@@ -387,6 +380,7 @@ FocusScope {
z: CcMetrics.overlayZ
active: false
sourceComponent: WidgetConfigOverlay {
backdrop: contentFlickable
onVisibleChanged: {
if (visible)
return;
@@ -104,6 +104,12 @@ QtObject {
readonly property Component colorPickerTile: Component {
ColorPickerTile {}
}
readonly property Component userCardTile: Component {
UserCardTile {}
}
readonly property Component quickActionsTile: Component {
QuickActionsTile {}
}
readonly property Component pluginTile: Component {
PluginTile {}
}
@@ -157,6 +163,10 @@ QtObject {
return diskUsageTile;
case "colorPicker":
return colorPickerTile;
case "userCard":
return userCardTile;
case "quickActions":
return quickActionsTile;
default:
return null;
}
@@ -406,6 +416,6 @@ QtObject {
}
function clearAll() {
WidgetUtils.clearAll();
WidgetUtils.clearAll(columns);
}
}
@@ -0,0 +1,36 @@
import QtQuick
import qs.Common
import qs.Modules.ControlCenter
Rectangle {
id: root
property var widgetData: ({})
property var widgetDef: null
property var host: null
property bool live: true
property bool interactive: true
property real columns: 4
property real rows: 1
property bool compact: columns <= 2 && rows === 1
readonly property bool tall: height >= CcMetrics.gridRowUnit * 2
readonly property bool narrow: width < CcMetrics.expandedTileMinWidth
readonly property real pad: tall && !narrow ? Theme.spacingM : Theme.spacingS
readonly property real bodyRadius: tall ? Math.min(CcMetrics.tallTileRadius, width / 2, height / 2) : Theme.fullRadius(width, height)
width: parent?.width ?? 0
height: CcMetrics.tileHeight
radius: bodyRadius
color: CcMetrics.tileInactiveColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Behavior on radius {
enabled: CcMetrics.animationsEnabled && !SettingsData.reduceMotion
NumberAnimation {
duration: Theme.expressiveDurations.expressiveEffects
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
}
@@ -0,0 +1,89 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.ControlCenter
import "../utils/widgets.js" as WidgetUtils
CcCardTile {
id: root
readonly property bool editMode: host?.editMode ?? false
readonly property var actions: WidgetUtils.quickActions(widgetData).filter(action => action.enabled)
readonly property string role: WidgetUtils.quickActionRole(widgetData)
readonly property bool filled: widgetData?.background === true
readonly property bool powerAccent: widgetData?.powerAccent === true
readonly property color roleBg: role === "default" ? Theme.secondaryContainer : Theme.roleColor(role)
readonly property color roleFg: {
switch (role) {
case "primary":
return Theme.primaryText;
case "primaryContainer":
return Theme.onPrimaryContainer;
case "secondary":
case "surfaceVariant":
return Theme.surfaceText;
case "surfaceText":
return Theme.surface;
}
return Theme.onSecondaryContainer;
}
// Without a pill the icon carries the role itself, so container roles fall back to their accent.
readonly property color roleIcon: {
switch (role) {
case "primary":
case "primaryContainer":
return Theme.primary;
case "secondary":
return Theme.secondary;
}
return Theme.surfaceText;
}
// The edit toggle lights up in primary, so a primary role needs a different active colour.
readonly property color activeBg: role === "primary" ? Theme.primaryContainer : Theme.primary
readonly property color activeFg: role === "primary" ? Theme.onPrimaryContainer : Theme.primaryText
function trigger(id) {
switch (id) {
case "lock":
host?.lockRequested();
return;
case "power":
host?.powerRequested();
return;
case "settings":
host?.settingsRequested();
return;
case "edit":
host?.editRequested();
return;
}
}
Row {
anchors.centerIn: parent
spacing: CcMetrics.actionGap
Repeater {
model: root.actions
DankActionButton {
required property var modelData
readonly property string actionId: modelData.id
readonly property bool destructive: root.powerAccent && actionId === "power"
readonly property bool active: actionId === "edit" && root.editMode
readonly property color bg: active ? root.activeBg : destructive ? Theme.errorContainer : root.roleBg
readonly property color fg: active ? root.activeFg : destructive ? Theme.onErrorContainer : root.roleFg
buttonSize: CcMetrics.actionSize
iconSize: CcMetrics.actionIconSize
iconName: WidgetUtils.quickActionIcon(actionId)
iconColor: root.filled || active ? fg : destructive ? Theme.error : root.roleIcon
backgroundColor: root.filled || active ? bg : "transparent"
Accessible.name: I18n.tr(WidgetUtils.quickActionLabel(actionId))
onClicked: root.trigger(actionId)
}
}
}
}
@@ -0,0 +1,37 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter
import qs.Modules.DankDash
CcCardTile {
id: root
// Keys and defaults come from the dashboard user card so both surfaces expose the same toggles.
readonly property var options: DashRegistry.resolvedOptions("user", widgetData)
readonly property bool tapToClose: interactive && (host?.tapToClose ?? false)
Accessible.role: tapToClose ? Accessible.Button : Accessible.Pane
Accessible.name: UserInfoService.username
UserIdentity {
anchors.fill: parent
anchors.margins: root.pad
anchors.leftMargin: root.narrow ? root.pad : root.pad + Theme.spacingXS
live: root.live
options: root.options
narrow: root.width < CcMetrics.columnWidth * 2
tall: root.tall
avatarSize: root.tall ? DashMetrics.avatarSizeHero : DashMetrics.avatarSize
contentColor: CcMetrics.tileInactiveContent
mutedColor: CcMetrics.tileInactiveSubtitle
}
MouseArea {
anchors.fill: parent
enabled: root.tapToClose
acceptedButtons: Qt.LeftButton
onClicked: root.host?.headerTapped()
}
}
@@ -2,12 +2,87 @@
.import qs.Modules.ControlCenter as ControlCenter
.import "../../../Common/GridLayout.js" as GridLayout
var PINNED_IDS = ["userCard", "quickActions"];
var OPTION_IDS = ["diskUsage", "brightnessSlider", "idleInhibitor", "userCard", "quickActions"];
var QUICK_ACTIONS = [{
"id": "lock",
"icon": "lock",
"label": "Lock"
}, {
"id": "power",
"icon": "power_settings_new",
"label": "Power"
}, {
"id": "settings",
"icon": "settings",
"label": "Settings"
}, {
"id": "edit",
"icon": "edit",
"label": "Edit"
}];
var QUICK_ACTION_IDS = QUICK_ACTIONS.map(action => action.id);
function isSliderWidget(id) {
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider";
}
function sizeSpec(id, columns, rows = Infinity) {
return {
function isPinned(id) {
return PINNED_IDS.includes(id);
}
function hasOptions(id) {
return OPTION_IDS.includes(id) || String(id ?? "").startsWith("plugin_");
}
// Saved order wins, unknown ids drop, missing ones append so new actions show up without a migration.
function quickActions(widgetData) {
const saved = Array.isArray(widgetData?.actions) ? widgetData.actions : [];
const out = saved.filter(action => QUICK_ACTION_IDS.includes(action?.id)).map(action => ({
"id": action.id,
"enabled": action.id === "edit" || action.enabled !== false
}));
for (const id of QUICK_ACTION_IDS) {
if (!out.some(action => action.id === id))
out.push({
"id": id,
"enabled": true
});
}
return out;
}
var QUICK_ACTION_ROLES = ["primary", "primaryContainer", "secondary", "surfaceVariant", "surfaceText"];
function quickActionRole(widgetData) {
return QUICK_ACTION_ROLES.includes(widgetData?.buttonColor) ? widgetData.buttonColor : "default";
}
function quickActionIcon(id) {
return QUICK_ACTIONS.find(action => action.id === id)?.icon ?? "";
}
// Untranslated catalog term; callers wrap it in I18n.tr.
function quickActionLabel(id) {
return QUICK_ACTIONS.find(action => action.id === id)?.label ?? "";
}
function defaultWidget(id, columns) {
return Object.assign({
"id": id,
"enabled": true
}, clampSize({
"id": id
}, columns));
}
function pinnedWidgets(columns) {
return PINNED_IDS.map(id => defaultWidget(id, columns));
}
function sizeSpec(widget, columns, rows = Infinity) {
const id = widget?.id || "";
const spec = {
"w": 4,
"h": 1,
"minW": 1,
@@ -16,10 +91,22 @@ function sizeSpec(id, columns, rows = Infinity) {
"maxH": rows,
"step": ControlCenter.CcMetrics.gridStep
};
switch (id) {
case "userCard":
spec.w = Number.isFinite(columns) ? Math.max(1, columns - 4) : 4;
spec.maxH = Math.min(2, rows);
break;
case "quickActions":
spec.minW = Math.min(ControlCenter.CcMetrics.quickActionsMinColumns(quickActions(widget).filter(action => action.enabled).length), columns);
spec.w = Math.max(spec.minW, 4);
spec.maxH = Math.min(2, rows);
break;
}
return spec;
}
function clampSize(widget, columns, rows = Infinity) {
const spec = sizeSpec(widget.id || "", columns, rows);
const spec = sizeSpec(widget, columns, rows);
const size = {
"w": GridLayout.dimension(widget.w, spec.minW, spec.maxW, spec.w, spec.step),
"h": GridLayout.dimension(widget.h, spec.minH, spec.maxH, spec.h, spec.step)
@@ -39,12 +126,7 @@ function clampSize(widget, columns, rows = Infinity) {
function addWidget(widgetId, columns) {
const widgets = Common.SettingsData.controlCenterWidgets.slice();
const widget = Object.assign({
"id": widgetId,
"enabled": true
}, clampSize({
"id": widgetId
}, columns));
const widget = defaultWidget(widgetId, columns);
if (widgetId === "diskUsage") {
widget.instanceId = generateUniqueId();
@@ -67,7 +149,7 @@ function generateUniqueId() {
function removeWidget(index) {
const widgets = Common.SettingsData.controlCenterWidgets.slice();
if (index < 0 || index >= widgets.length)
if (index < 0 || index >= widgets.length || isPinned(widgets[index]?.id))
return;
widgets.splice(index, 1);
Common.SettingsData.set("controlCenterWidgets", widgets);
@@ -79,14 +161,9 @@ function setLayout(widgets) {
function resetToDefault(columns) {
const ids = ["volumeSlider", "brightnessSlider", "wifi", "bluetooth", "audioOutput", "audioInput", "nightMode", "darkMode"];
Common.SettingsData.set("controlCenterWidgets", ids.map(id => Object.assign({
"id": id,
"enabled": true
}, clampSize({
"id": id
}, columns))));
Common.SettingsData.set("controlCenterWidgets", pinnedWidgets(columns).concat(ids.map(id => defaultWidget(id, columns))));
}
function clearAll() {
Common.SettingsData.set("controlCenterWidgets", []);
function clearAll(columns) {
Common.SettingsData.set("controlCenterWidgets", pinnedWidgets(columns));
}
@@ -140,10 +140,6 @@ Singleton {
readonly property real avatarSize: Theme.buttonHeightM
readonly property real avatarSizeHero: 72
readonly property real userBadgeSize: 26
readonly property real userBadgeOverhang: 0.2
readonly property real userBadgeIconSize: Theme.iconSizeSmall
readonly property real userChipHeight: 28
readonly property int historyLength: 60
readonly property real tileTrendRatio: 0.45
+1 -2
View File
@@ -488,9 +488,8 @@ Singleton {
return optionSpecs(id).some(spec => spec.key in stored);
}
function resolvedOptions(id) {
function resolvedOptions(id, stored = storedOptions(id)) {
const out = {};
const stored = storedOptions(id);
for (const spec of optionSpecs(id))
out[spec.key] = optionValue(spec, stored[spec.key]);
return out;
@@ -1,158 +1,27 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.DankDash
Card {
id: root
property bool live: Window.window?.visible ?? false
property bool dgopRefHeld: false
readonly property bool narrow: width < DashMetrics.gridRowUnit * 2
readonly property bool tall: height >= DashMetrics.gridRowUnit * 2
readonly property bool showHostname: options.hostname !== false && UserInfoService.hostname !== ""
readonly property bool showCompositor: options.compositor !== false && compositorName !== ""
readonly property bool showUptime: options.uptime !== false
readonly property bool showBadge: options.badge !== false
readonly property real avatarSize: Math.min(tall ? DashMetrics.avatarSizeHero : DashMetrics.avatarSize, height - pad * 2, width - pad * 2)
readonly property color badgeColor: tinted ? contentColor : Theme.primaryContainer
readonly property color badgeContentColor: tinted ? containerColor : Theme.onPrimaryContainer
readonly property string compositorName: CompositorService.displayName
readonly property string uptimeText: {
const prefix = I18n.tr("up", "uptime prefix, e.g. 'up 4h 2m'");
return DgopService.shortUptime ? prefix + DgopService.shortUptime.slice(2) : prefix;
}
readonly property string avatarSource: {
if (PortalService.profileImage === "")
return "";
if (PortalService.profileImage.startsWith("/"))
return "file://" + PortalService.profileImage;
return PortalService.profileImage;
}
entryId: "user"
function syncDgopRef(wanted) {
if (wanted === dgopRefHeld)
return;
dgopRefHeld = wanted;
if (wanted) {
DgopService.addRef("system");
return;
}
DgopService.removeRef("system");
}
onLiveChanged: syncDgopRef(live && showUptime)
onShowUptimeChanged: syncDgopRef(live && showUptime)
Component.onCompleted: syncDgopRef(live && showUptime)
Component.onDestruction: syncDgopRef(false)
Item {
id: avatarBox
x: root.narrow ? (parent.width - width) / 2 : 0
anchors.verticalCenter: parent.verticalCenter
width: root.avatarSize
height: root.avatarSize
DankCircularImage {
anchors.fill: parent
imageSource: root.avatarSource
fallbackIcon: "material:person"
}
DankMaterialShape {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: -DashMetrics.userBadgeSize * DashMetrics.userBadgeOverhang
anchors.bottomMargin: -DashMetrics.userBadgeSize * DashMetrics.userBadgeOverhang
width: DashMetrics.userBadgeSize
height: DashMetrics.userBadgeSize
shape: "gem"
color: root.badgeColor
visible: root.showBadge
SystemLogo {
anchors.centerIn: parent
width: DashMetrics.userBadgeIconSize
height: DashMetrics.userBadgeIconSize
colorOverride: root.badgeContentColor
}
}
}
Column {
anchors.left: avatarBox.right
anchors.leftMargin: Theme.spacingM
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
visible: !root.narrow
StyledText {
width: parent.width
text: root.showHostname ? UserInfoService.username + " @ " + UserInfoService.hostname : UserInfoService.username
font.pixelSize: root.tall ? Theme.fontSizeXLarge : Theme.fontSizeLarge
font.weight: Theme.fontWeightMedium
color: root.contentColor
elide: Text.ElideRight
}
Row {
id: detailRow
width: parent.width
height: DashMetrics.userChipHeight
spacing: Theme.spacingS
Row {
id: compositorDetails
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
visible: root.showCompositor
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "select_window"
size: Theme.iconSizeSmall
color: root.mutedColor
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: root.compositorName
font.pixelSize: Theme.fontSizeSmall
color: root.mutedColor
}
}
Row {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (compositorDetails.visible ? compositorDetails.width + parent.spacing : 0)
spacing: Theme.spacingXS
visible: root.showUptime && DgopService.shortUptime !== ""
DankIcon {
id: uptimeIcon
anchors.verticalCenter: parent.verticalCenter
name: "schedule"
size: Theme.iconSizeSmall
color: root.mutedColor
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - uptimeIcon.width - parent.spacing
text: root.uptimeText
font.pixelSize: Theme.fontSizeSmall
color: root.mutedColor
wrapMode: Text.NoWrap
elide: Text.ElideRight
}
}
}
UserIdentity {
anchors.fill: parent
live: root.Window.window?.visible ?? false
options: root.options
narrow: root.narrow
tall: root.tall
avatarSize: root.tall ? DashMetrics.avatarSizeHero : DashMetrics.avatarSize
contentColor: root.contentColor
mutedColor: root.mutedColor
badgeColor: root.tinted ? root.contentColor : Theme.primaryContainer
badgeContentColor: root.tinted ? root.containerColor : Theme.onPrimaryContainer
ringColor: root.tinted ? root.containerColor : Theme.avatarRingColor
}
}
@@ -833,6 +833,8 @@ Item {
DankCircularImage {
anchors.fill: parent
ringWidth: Theme.avatarRingWidth
ringColor: Theme.avatarRingColor
imageSource: {
const displayUser = GreeterState.username || root.pickerThemeUsername;
if (displayUser) {
@@ -134,6 +134,8 @@ Item {
DankCircularImage {
anchors.fill: parent
ringWidth: Theme.avatarRingWidth
ringColor: Theme.avatarRingColor
imageSource: root.profileImageSource(userRow.modelData.username)
fallbackIcon: "person"
}
@@ -600,6 +600,8 @@ Item {
DankCircularImage {
Layout.preferredWidth: LockMetrics.avatarSize
ringWidth: Theme.avatarRingWidth
ringColor: Theme.avatarRingColor
Layout.preferredHeight: LockMetrics.fieldHeight
imageSource: {
if (PortalService.profileImage === "")
@@ -15,7 +15,13 @@ Column {
}
readonly property var barStore: ({
"isDefault": keys => keys.every(key => (root.barConfig?.[key] ?? SettingsData.barConfigDefault(key)) === SettingsData.barConfigDefault(key)),
"resetToDefault": keys => SettingsData.updateBarConfig(root.page.barId, Object.fromEntries(keys.map(key => [key, SettingsData.barConfigDefault(key)])))
"resetToDefault": keys => {
// QV4 has no Object.fromEntries.
const patch = {};
for (const key of keys)
patch[key] = SettingsData.barConfigDefault(key);
SettingsData.updateBarConfig(root.page.barId, patch);
}
})
width: parent?.width ?? 0
@@ -24,6 +24,8 @@ Column {
leading: DankCircularImage {
width: SettingsMetrics.avatarSize
ringWidth: Theme.avatarRingWidth
ringColor: Theme.avatarRingColor
height: width
imageSource: PortalService.profileImage
fallbackIcon: "person"
@@ -42,5 +44,46 @@ Column {
onClicked: PortalService.setProfileImage("")
}
}
SettingsDropdownRow {
id: ringRow
readonly property var rings: [{
"value": "none",
"label": I18n.tr("None")
}, {
"value": "outline",
"label": I18n.tr("Outline")
}, {
"value": "primary",
"label": I18n.tr("Primary")
}, {
"value": "secondary",
"label": I18n.tr("Secondary")
}, {
"value": "tertiary",
"label": I18n.tr("Tertiary")
}]
settingKey: "avatarRing"
tags: ["user", "account", "profile", "avatar", "ring", "border", "color"]
text: I18n.tr("Avatar ring")
options: rings.map(ring => ring.label)
optionColorMap: {
const map = {};
for (const ring of rings) {
if (ring.value === "none")
continue;
map[ring.label] = ring.value === "outline" ? Theme.surfaceVariant : Theme.roleColor(ring.value);
}
return map;
}
currentValue: rings.find(ring => ring.value === SettingsData.avatarRing)?.label ?? ""
onValueChanged: value => {
const ring = rings.find(ring => ring.label === value);
if (ring)
SettingsData.set("avatarRing", ring.value);
}
}
}
}
+6 -3
View File
@@ -24,6 +24,7 @@ Item {
readonly property real touchTargetSize: Math.max(Theme.minimumTouchTargetSize, buttonSize)
readonly property real contentInset: touchTargetSize / 2
readonly property bool showOptionsButton: hasOptions && width - contentInset * 2 >= touchTargetSize * (horizontalResize ? 3 : 2)
readonly property int chromeButtons: (removable ? 1 : 0) + (showOptionsButton ? 1 : 0)
readonly property rect hitBounds: Qt.rect(contentInset - hitOverflow, contentInset - hitOverflow, width - (contentInset - hitOverflow) * 2, height - (contentInset - hitOverflow) * 2)
signal removeRequested
@@ -56,13 +57,13 @@ Item {
Rectangle {
x: I18n.isRtl ? root.width - root.contentInset - width - (root.touchTargetSize - root.buttonSize) / 2 : root.contentInset + (root.touchTargetSize - root.buttonSize) / 2
y: root.contentInset - height / 2
width: root.buttonSize + (root.showOptionsButton ? root.touchTargetSize : 0)
width: root.buttonSize + root.touchTargetSize * Math.max(0, root.chromeButtons - 1)
height: root.buttonSize
radius: Theme.fullRadius(width, height)
color: Theme.chipSurface
border.color: Theme.primary
border.width: Theme.outlineWidth
visible: root.removable
visible: root.chromeButtons > 0
}
DankActionButton {
@@ -100,7 +101,9 @@ Item {
DankActionButton {
id: optionsButton
x: I18n.isRtl ? root.width - root.contentInset - root.touchTargetSize - width : root.contentInset + root.touchTargetSize
readonly property real slotOffset: root.removable ? root.touchTargetSize : 0
x: I18n.isRtl ? root.width - root.contentInset - slotOffset - width : root.contentInset + slotOffset
y: 0
width: root.touchTargetSize
height: root.touchTargetSize
+3 -2
View File
@@ -1834,12 +1834,13 @@ Item {
visible: !root._surfaceSwitching
readonly property bool shouldClip: Theme.isDirectionalEffect || root.usesConnectedSurfaceChrome
readonly property real clipOversize: 1000
// inputMargin is only non-zero while a grid is being edited; let its chrome cross the connected edge instead of clipping it.
readonly property real connectedClipAllowance: {
if (!root.usesConnectedSurfaceChrome)
return 0;
if (root.frameOwnsConnectedChrome)
return 0;
return -Theme.connectedCornerRadius;
return root.inputMargin;
return -Theme.connectedCornerRadius + root.inputMargin;
}
clip: shouldClip
+156
View File
@@ -0,0 +1,156 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool live: Window.window?.visible ?? false
property var options: ({})
property real avatarSize: Theme.buttonHeightM
property bool narrow: false
property bool tall: false
property color contentColor: Theme.surfaceText
property color mutedColor: Theme.surfaceVariantText
property color badgeColor: Theme.primaryContainer
property color badgeContentColor: Theme.onPrimaryContainer
property color ringColor: Theme.avatarRingColor
readonly property real avatarDiameter: Math.min(avatarSize, width, height)
readonly property real badgeSize: Math.round(Math.max(Theme.iconSizeSmall + Theme.spacingXS, avatarDiameter / 3))
readonly property real badgeOverhang: 0.15
readonly property real badgeIconRatio: 0.6
readonly property bool showHostname: options.hostname !== false && UserInfoService.hostname !== ""
readonly property bool showCompositor: options.compositor !== false && compositorName !== ""
readonly property bool showUptime: options.uptime !== false
readonly property bool showBadge: options.badge !== false
readonly property string compositorName: CompositorService.displayName
readonly property string uptimeText: {
const prefix = I18n.tr("up", "uptime prefix, e.g. 'up 4h 2m'");
return DgopService.shortUptime ? prefix + DgopService.shortUptime.slice(2) : prefix;
}
readonly property string avatarSource: {
if (PortalService.profileImage === "")
return "";
if (PortalService.profileImage.startsWith("/"))
return "file://" + PortalService.profileImage;
return PortalService.profileImage;
}
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
Ref {
service: DgopService
modules: ["system"]
active: root.live && root.showUptime
}
Item {
id: avatarBox
x: root.narrow ? (parent.width - width) / 2 : 0
anchors.verticalCenter: parent.verticalCenter
width: root.avatarDiameter
height: root.avatarDiameter
DankCircularImage {
anchors.fill: parent
imageSource: root.avatarSource
fallbackIcon: "material:person"
ringWidth: Theme.avatarRingWidth
ringColor: root.ringColor
}
Rectangle {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: -root.badgeSize * root.badgeOverhang
anchors.bottomMargin: -root.badgeSize * root.badgeOverhang
width: root.badgeSize
height: root.badgeSize
radius: Theme.fullRadius(width, height)
color: root.badgeColor
border.width: Theme.avatarRingWidth
border.color: root.ringColor
visible: root.showBadge
SystemLogo {
anchors.centerIn: parent
width: Math.round(parent.width * root.badgeIconRatio)
height: width
colorOverride: String(root.badgeContentColor)
}
}
}
Column {
anchors.left: avatarBox.right
anchors.leftMargin: Theme.spacingM
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: !root.narrow
StyledText {
width: parent.width
text: root.showHostname ? UserInfoService.username + " @ " + UserInfoService.hostname : UserInfoService.username
font.pixelSize: root.tall ? Theme.fontSizeXLarge : Theme.fontSizeLarge
font.weight: Theme.fontWeightMedium
color: root.contentColor
elide: Text.ElideRight
}
// Tall cards have room for a second line, so uptime wraps there and elides everywhere else.
Flow {
width: parent.width
spacing: Theme.spacingS
Row {
id: compositorDetails
spacing: Theme.spacingXS
visible: root.showCompositor
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "select_window"
size: Theme.iconSizeSmall
color: root.mutedColor
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: root.compositorName
font.pixelSize: Theme.fontSizeSmall
color: root.mutedColor
}
}
Row {
readonly property real remaining: parent.width - (compositorDetails.visible ? compositorDetails.width + parent.spacing : 0)
width: root.tall ? Math.min(implicitWidth, parent.width) : remaining
spacing: Theme.spacingXS
visible: root.showUptime && DgopService.shortUptime !== ""
DankIcon {
id: uptimeIcon
anchors.verticalCenter: parent.verticalCenter
name: "schedule"
size: Theme.iconSizeSmall
color: root.mutedColor
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
width: root.tall ? implicitWidth : Math.max(0, parent.width - uptimeIcon.width - parent.spacing)
text: root.uptimeText
font.pixelSize: Theme.fontSizeSmall
color: root.mutedColor
wrapMode: Text.NoWrap
elide: Text.ElideRight
}
}
}
}
}
+8
View File
@@ -59,6 +59,14 @@ test("cards without a cell take the first free one and cells clamp to the column
assert.deepEqual(plain(grid.packCells([{ id: "a", w: 1, h: 1, col: 0.5, row: 1.5 }], [0], 4, null, 0.5).cells[0]), { col: 0.5, row: 1.5, cols: 1, rows: 1 });
});
test("gravity pulls positioned cards up into holes without jumping over blockers", () => {
const board = [{ id: "a", w: 2, h: 1, col: 0, row: 0 }, { id: "b", w: 2, h: 1, col: 2, row: 0 }, { id: "c", w: 2, h: 1, col: 0, row: 2 }, { id: "d", w: 2, h: 1, col: 2, row: 3 }];
assert.deepEqual(plain(grid.packCells(board, [0, 1, 2, 3], 6).cells.map(c => [c.col, c.row])), [[0, 0], [2, 0], [0, 2], [2, 3]]);
assert.deepEqual(plain(grid.packCells(board, [0, 1, 2, 3], 6, null, 1, true).cells.map(c => [c.col, c.row])), [[0, 0], [2, 0], [0, 1], [2, 1]]);
const blocked = [{ id: "a", w: 2, h: 1, col: 0, row: 1 }, { id: "b", w: 2, h: 1, col: 0, row: 3 }];
assert.deepEqual(plain(grid.packCells(blocked, [1, 0], 6, null, 1, true).cells.map(c => [c.col, c.row])), [[0, 0], [0, 1]]);
});
test("a dragged tile snaps to the cell under its corner", () => {
const layout = grid.packCards(cards, order, 6, 600, 0, 100, false);
assert.deepEqual(plain(grid.cellAt(layout, 240, 160, 2, 1)), { col: 2, row: 2 });
@@ -201,10 +201,14 @@ ShellRoot {
check(Math.abs(grid.slotLayout.slots[1].w - grid.slotLayout.slots[1].h) < 0.01, "host inset preserves square geometry");
grid.availableHeight = CcMetrics.gridRowUnit * 5 + CcMetrics.gridGap * 4;
check(grid.slotLayout.slots[0].rows <= 5, "tile height fits the available screen");
grid.beginDrag(0);
grid.updateDragTarget(0, grid.cellWidth);
grid.beginDrag(1);
grid.updateDragTarget(0, 0);
grid.endDrag();
check(SettingsData.controlCenterWidgets[0].w === 10 && SettingsData.controlCenterWidgets[0].h === 12 && SettingsData.controlCenterWidgets[0].row === 1 && SettingsData.controlCenterWidgets[1].row === grid.maximumRows + 1, "moving on a smaller screen preserves saved spans and pushes the collided tile " + JSON.stringify(SettingsData.controlCenterWidgets));
check(SettingsData.controlCenterWidgets[0].w === 10 && SettingsData.controlCenterWidgets[0].h === 12 && SettingsData.controlCenterWidgets[0].row === 2 && SettingsData.controlCenterWidgets[1].row === 0, "moving on a smaller screen preserves saved spans and pushes the collided tile " + JSON.stringify(SettingsData.controlCenterWidgets));
grid.beginDrag(0);
grid.updateDragTarget(0, grid.cellWidth * 4);
grid.endDrag();
check(SettingsData.controlCenterWidgets[0].row === 2, "gravity closes the gap a drop below free space would leave " + JSON.stringify(SettingsData.controlCenterWidgets));
grid.previewSize(0, {
w: 6
});
@@ -25,3 +25,10 @@ test("sparse settings migrate without inventing grid keys", () => {
assert.equal(sparse.controlCenterColumns, undefined);
assert.equal(sparse.controlCenterWidgets, undefined);
});
test("pinned header tiles are prepended once to a saved layout", () => {
const migrated = store.migrateToVersion({ configVersion: 31, controlCenterWidgets: [{ id: "wifi", w: 4, h: 1, col: 0, row: 0 }, { id: "quickActions", w: 4, h: 1 }] }, 32);
assert.deepEqual(JSON.parse(JSON.stringify(migrated.controlCenterWidgets)).map(w => w.id), ["userCard", "wifi", "quickActions"]);
assert.equal(migrated.configVersion, 32);
assert.equal(store.migrateToVersion({ configVersion: 31 }, 32).controlCenterWidgets, undefined);
});
@@ -16188,6 +16188,24 @@
],
"parentLabel": "Dock"
},
{
"section": "avatarRing",
"label": "Avatar ring",
"tabIndex": 60,
"category": "Users & accounts",
"keywords": [
"account",
"accounts",
"avatar",
"border",
"color",
"profile",
"ring",
"user",
"users"
],
"page": "user_accounts"
},
{
"section": "userProfile",
"label": "Profile",