mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-20 02:04:01 -04:00
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:
+1
-1
Submodule dank-qml-common updated: 9594c1e3bd...660b04470b
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user