mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-15 15:54:00 -04:00
refactor(control center): redesign update #8
This commit is contained in:
@@ -94,36 +94,41 @@ function firstFit(taken, steps, w, h) {
|
||||
|
||||
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)
|
||||
return null;
|
||||
const px = cell.col * (colW + gap);
|
||||
const pw = cell.cols * colW + (cell.cols - 1) * gap;
|
||||
return {
|
||||
"x": mirror ? width - px - pw : px,
|
||||
"y": cell.row * (rowUnit + gap),
|
||||
"w": pw,
|
||||
"h": cell.rows * rowUnit + (cell.rows - 1) * gap,
|
||||
"col": cell.col,
|
||||
"row": cell.row,
|
||||
"cols": cell.cols,
|
||||
"rows": cell.rows
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
"slots": slots,
|
||||
const layout = {
|
||||
"slots": [],
|
||||
"rows": packed.rows,
|
||||
"totalHeight": packed.rows > 0 ? packed.rows * rowUnit + (packed.rows - 1) * gap : 0,
|
||||
"columns": columns,
|
||||
"width": width,
|
||||
"colW": colW,
|
||||
"colW": (width - gap * (columns - 1)) / columns,
|
||||
"rowUnit": rowUnit,
|
||||
"gap": gap,
|
||||
"step": step,
|
||||
"mirror": mirror
|
||||
};
|
||||
layout.slots = packed.cells.map(cell => cell ? Object.assign(slotRect(layout, cell.col, cell.row, cell.cols, cell.rows), cell) : null);
|
||||
return layout;
|
||||
}
|
||||
|
||||
function slotRect(layout, col, row, cols, rows) {
|
||||
const px = col * (layout.colW + layout.gap);
|
||||
const pw = cols * layout.colW + (cols - 1) * layout.gap;
|
||||
return {
|
||||
"x": layout.mirror ? layout.width - px - pw : px,
|
||||
"y": row * (layout.rowUnit + layout.gap),
|
||||
"w": pw,
|
||||
"h": rows * layout.rowUnit + (rows - 1) * layout.gap
|
||||
};
|
||||
}
|
||||
|
||||
function cellOccupied(layout, col, row) {
|
||||
const taken = layout.slots.filter(Boolean).map(slot => ({
|
||||
"x": slot.col,
|
||||
"y": slot.row,
|
||||
"w": slot.cols,
|
||||
"h": slot.rows
|
||||
}));
|
||||
return overlaps(taken, col, row, 1, 1);
|
||||
}
|
||||
|
||||
function cellAt(layout, x, y, cols, rows) {
|
||||
|
||||
@@ -21,7 +21,7 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("SettingsData")
|
||||
|
||||
readonly property int settingsConfigVersion: 35
|
||||
readonly property int settingsConfigVersion: 36
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
|
||||
@@ -446,19 +446,22 @@ var LOCAL_SPEC = {
|
||||
id: "settings",
|
||||
enabled: true,
|
||||
w: 1,
|
||||
h: 1
|
||||
h: 1,
|
||||
small: true
|
||||
},
|
||||
{
|
||||
id: "lock",
|
||||
enabled: true,
|
||||
w: 1,
|
||||
h: 1
|
||||
h: 1,
|
||||
small: true
|
||||
},
|
||||
{
|
||||
id: "power",
|
||||
enabled: true,
|
||||
w: 1,
|
||||
h: 1
|
||||
h: 1,
|
||||
small: true
|
||||
},
|
||||
{
|
||||
id: "brightnessSlider",
|
||||
@@ -531,6 +534,13 @@ var LOCAL_SPEC = {
|
||||
enabled: true,
|
||||
w: 4,
|
||||
h: 1
|
||||
},
|
||||
{
|
||||
id: "runningApps",
|
||||
enabled: true,
|
||||
w: 7,
|
||||
h: 1,
|
||||
small: true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ var STALE_WIDGET_KEYS = ["desktopClockEnabled", "desktopClockStyle", "desktopClo
|
||||
var BAR_WIDGET_LIST_KEYS = ["leftWidgets", "centerWidgets", "rightWidgets"];
|
||||
var CC_HEADER_IDS = ["userCard", "quickActions", "header"];
|
||||
var CC_ACTION_TILE_IDS = ["settings", "lock", "power"];
|
||||
var CC_USER_KEYS = ["w", "h", "col", "row", "hostname", "compositor", "uptime", "badge"];
|
||||
var CC_USER_KEYS = ["w", "h", "col", "row", "hostname", "compositor", "uptime", "badge", "background"];
|
||||
|
||||
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"];
|
||||
|
||||
@@ -707,6 +707,20 @@ function migrateToVersion(obj, targetVersion) {
|
||||
settings.configVersion = 35;
|
||||
}
|
||||
|
||||
if (currentVersion < 36 && targetVersion >= 36) {
|
||||
// The footer's running-apps chip became a grid tile; layouts saved before then never mention it.
|
||||
const widgets = settings.controlCenterWidgets;
|
||||
if (Array.isArray(widgets) && !widgets.some(widget => widget?.id === "runningApps"))
|
||||
settings.controlCenterWidgets = widgets.concat([{
|
||||
id: "runningApps",
|
||||
enabled: true,
|
||||
w: Math.max(1, (settings.controlCenterColumns ?? 8) - 1),
|
||||
h: 1,
|
||||
small: true
|
||||
}]);
|
||||
settings.configVersion = 36;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -721,7 +735,8 @@ function migrateControlCenterHeader(widgets, fixedHeader, columns) {
|
||||
id: id,
|
||||
enabled: true,
|
||||
w: 1,
|
||||
h: 1
|
||||
h: 1,
|
||||
small: true
|
||||
}));
|
||||
const identity = header.length === 0 ? {} : header.find(widget => widget.id === "userCard" || (widget.id === "header" && widget.showUser !== false));
|
||||
if (!identity || rest.some(widget => widget?.id === "user"))
|
||||
|
||||
@@ -56,8 +56,10 @@ Singleton {
|
||||
readonly property real tileTextGap: Theme.spacingM
|
||||
readonly property real footerHeight: iconBoxSize
|
||||
readonly property real footerGap: Theme.spacingM
|
||||
readonly property real footerAppIconSize: Theme.iconSize * iconScale
|
||||
readonly property int footerMaxAppIcons: 3
|
||||
readonly property real runningAppsIconSize: Theme.iconSize * iconScale
|
||||
readonly property int runningAppsMaxIcons: 3
|
||||
// Small tile body height as a fraction of a row; the resize corner snaps to the nearer of this and a full row.
|
||||
readonly property real smallRowFraction: 0.5
|
||||
readonly property real shapeButtonSize: Theme.iconSize
|
||||
|
||||
readonly property real detailDialogInset: Theme.spacingL
|
||||
@@ -120,6 +122,10 @@ Singleton {
|
||||
readonly property color tileInactiveSubtitle: Theme.surfaceVariantText
|
||||
readonly property color tileInactiveIcon: Theme.primary
|
||||
|
||||
function actionIconColor(id) {
|
||||
return id === "power" ? Theme.error : tileInactiveIcon;
|
||||
}
|
||||
|
||||
readonly property bool animationsEnabled: !SettingsData.reduceMotion && Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
||||
|
||||
function preferredDetailHeight(section, pluginHeight) {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modules.ControlCenter
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
@@ -14,28 +12,20 @@ Item {
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
property bool editMode: false
|
||||
property var toplevels: []
|
||||
property bool showEdit: true
|
||||
property string pendingAction: ""
|
||||
|
||||
signal runningAppsRequested
|
||||
signal addWidgetRequested
|
||||
signal resetRequested
|
||||
signal clearRequested
|
||||
signal editToggled
|
||||
signal cancelRequested
|
||||
|
||||
readonly property var appIds: {
|
||||
const ids = [];
|
||||
for (const toplevel of toplevels) {
|
||||
const id = toplevel?.appId ?? "";
|
||||
if (!ids.includes(id))
|
||||
ids.push(id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
readonly property real leadingWidth: Math.max(0, width - trailing.width - CcMetrics.footerGap)
|
||||
readonly property bool occupied: editMode || showEdit
|
||||
|
||||
implicitHeight: CcMetrics.footerHeight
|
||||
implicitHeight: occupied ? CcMetrics.footerHeight : 0
|
||||
visible: occupied
|
||||
|
||||
function cancelConfirmation() {
|
||||
pendingAction = "";
|
||||
@@ -52,86 +42,6 @@ Item {
|
||||
|
||||
onEditModeChanged: cancelConfirmation()
|
||||
|
||||
StyledButton {
|
||||
id: appsChip
|
||||
|
||||
readonly property real iconBox: height - Theme.spacingS * 2
|
||||
|
||||
anchors.left: parent.left
|
||||
height: parent.height
|
||||
width: Math.min(Theme.spacingS + icons.width + Theme.spacingS + countLabel.implicitWidth + Theme.spacingXS + chevron.width + Theme.spacingM, root.leadingWidth)
|
||||
radius: Theme.fullRadius(width, height)
|
||||
color: CcMetrics.tileInactiveColor
|
||||
border.width: Theme.layerOutlineWidth
|
||||
border.color: Theme.outlineMedium
|
||||
visible: !root.editMode && root.toplevels.length > 0
|
||||
Accessible.name: countLabel.text
|
||||
onClicked: root.runningAppsRequested()
|
||||
|
||||
Row {
|
||||
id: icons
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.appIds.slice(0, CcMetrics.footerMaxAppIcons)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
required property string modelData
|
||||
|
||||
width: appsChip.iconBox
|
||||
height: width
|
||||
radius: Theme.fullRadius(width, height)
|
||||
color: CcMetrics.iconBoxInactiveColor
|
||||
|
||||
CcAppIcon {
|
||||
anchors.centerIn: parent
|
||||
appId: parent.modelData
|
||||
iconSize: CcMetrics.footerAppIconSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: countLabel
|
||||
anchors.left: icons.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: chevron.left
|
||||
anchors.rightMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.toplevels.length === 1 ? I18n.tr("%1 window", "singular, %1 is 1, open windows in the control center footer").arg(1) : I18n.tr("%1 windows", "plural, %1 is a count of open windows in the control center footer").arg(root.toplevels.length)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: CcMetrics.tileInactiveContent
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: chevron
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: I18n.isRtl ? "chevron_left" : "chevron_right"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.onSurfaceVariant
|
||||
}
|
||||
|
||||
FocusRing {
|
||||
visible: parent.visualFocus
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
control: parent
|
||||
stateColor: CcMetrics.tileInactiveContent
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: editActions
|
||||
|
||||
@@ -230,7 +140,7 @@ Item {
|
||||
border.width: Theme.layerOutlineWidth
|
||||
border.color: Theme.outlineMedium
|
||||
tooltipText: I18n.tr("Edit")
|
||||
visible: !root.editMode
|
||||
visible: !root.editMode && root.showEdit
|
||||
onClicked: root.editToggled()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,23 @@ DankEditableGrid {
|
||||
signal lockRequested
|
||||
signal powerRequested
|
||||
signal settingsRequested
|
||||
signal accountsRequested
|
||||
signal editRequested
|
||||
signal closeRequested
|
||||
property bool tapToClose: false
|
||||
property var runningToplevels: []
|
||||
property bool dockEdit: false
|
||||
readonly property var editDock: {
|
||||
if (!dockEdit)
|
||||
return null;
|
||||
const layout = slotLayout;
|
||||
const col = layout.columns - 1;
|
||||
const row = layout.rows - 1;
|
||||
if (row < 0 || GridUtils.cellOccupied(layout, col, row))
|
||||
return null;
|
||||
const rect = GridUtils.slotRect(layout, col, row, 1, 1);
|
||||
return Qt.point(rect.x, rect.y);
|
||||
}
|
||||
|
||||
readonly property real gridHeight: layoutHeight
|
||||
readonly property real cellWidth: (width + CcMetrics.gridGap) / columns
|
||||
@@ -36,7 +51,7 @@ DankEditableGrid {
|
||||
|
||||
sourceItems: shownIndices.map(i => Object.assign({}, savedWidgets[i], sizeWithHiddenTwin(i)))
|
||||
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)
|
||||
placeholderRadius: draggingSlot?.small ? Theme.fullRadius(draggingSlot.width, draggingSlot.height) : (draggingSlot?.tileItem?.bodyRadius ?? Theme.fullRadius(width, CcMetrics.tileHeight))
|
||||
|
||||
onLayoutCommitted: items => model.setLayout(withHidden(items))
|
||||
|
||||
@@ -68,6 +83,21 @@ DankEditableGrid {
|
||||
return widgets;
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
x: (root.editDock?.x ?? 0) + root.contentPadding + (root.slotLayout.colW - width) / 2
|
||||
y: (root.editDock?.y ?? 0) + root.contentPadding + (root.slotLayout.rowUnit - height) / 2
|
||||
visible: root.editDock !== null
|
||||
buttonSize: CcMetrics.footerHeight
|
||||
iconName: "edit"
|
||||
iconSize: CcMetrics.iconBoxIconSize
|
||||
iconColor: CcMetrics.tileInactiveContent
|
||||
backgroundColor: CcMetrics.tileInactiveColor
|
||||
border.width: Theme.layerOutlineWidth
|
||||
border.color: Theme.outlineMedium
|
||||
tooltipText: I18n.tr("Edit")
|
||||
onClicked: root.editRequested()
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: tileRepeater
|
||||
|
||||
|
||||
@@ -13,10 +13,26 @@ DankEditableGridSlot {
|
||||
readonly property real cols: slot?.cols ?? 1
|
||||
readonly property real rows: slot?.rows ?? 1
|
||||
readonly property bool compact: cols <= 2 && rows === 1
|
||||
readonly property bool small: WidgetUtils.isSmall(grid.layoutItems[index] ?? widgetData, rows)
|
||||
readonly property var tileItem: tileLoader.item
|
||||
// Small tiles resize from a virtual half cell so growing them passes through full size first.
|
||||
property real biasW: 0
|
||||
property real biasH: 0
|
||||
readonly property real smallSpanLimit: (1 + CcMetrics.smallRowFraction) / 2
|
||||
|
||||
passthrough: tileItem?.passthrough ?? null
|
||||
|
||||
function reanchor(small) {
|
||||
const shift = ((small ? CcMetrics.smallRowFraction : 1) - smallSpanLimit) * grid.cellWidth;
|
||||
biasH += shift;
|
||||
if (cols === 1)
|
||||
biasW += shift;
|
||||
}
|
||||
|
||||
function spanOf(requested, bias) {
|
||||
return (requested + bias + CcMetrics.gridGap) / grid.cellWidth;
|
||||
}
|
||||
|
||||
onPressAndHold: {
|
||||
if (!editChrome.hasOptions)
|
||||
return;
|
||||
@@ -25,8 +41,13 @@ DankEditableGridSlot {
|
||||
|
||||
onResizeRequested: (requestedWidth, requestedHeight) => {
|
||||
const step = sizeSpec.step;
|
||||
let width = GridUtils.dimension(Math.round((requestedWidth + CcMetrics.gridGap) / grid.cellWidth / step) * step, sizeSpec.minW, sizeSpec.maxW, sizeSpec.w, step);
|
||||
let height = GridUtils.dimension(Math.round((requestedHeight + CcMetrics.gridGap) / grid.cellWidth / step) * step, sizeSpec.minH, sizeSpec.maxH, sizeSpec.h, step);
|
||||
const canShrink = WidgetUtils.canShrink(widgetData.id);
|
||||
if (canShrink && root.small !== (spanOf(requestedHeight, biasH) < smallSpanLimit))
|
||||
reanchor(!root.small);
|
||||
const spanW = spanOf(requestedWidth, biasW);
|
||||
const spanH = spanOf(requestedHeight, biasH);
|
||||
let width = GridUtils.dimension(Math.round(spanW / step) * step, sizeSpec.minW, sizeSpec.maxW, sizeSpec.w, step);
|
||||
let height = GridUtils.dimension(Math.round(spanH / step) * step, sizeSpec.minH, sizeSpec.maxH, sizeSpec.h, step);
|
||||
const current = WidgetUtils.clampSize(widgetData, grid.columns, grid.maximumRows);
|
||||
if (WidgetUtils.isSliderWidget(widgetData.id) && width < 2 && height < 2) {
|
||||
if (width !== current.w && sizeSpec.maxH > 1)
|
||||
@@ -39,6 +60,9 @@ DankEditableGridSlot {
|
||||
changes.w = width;
|
||||
if (height !== current.h)
|
||||
changes.h = height;
|
||||
const small = canShrink && height === 1 && spanH < smallSpanLimit;
|
||||
if (small !== (widgetData.small === true))
|
||||
changes.small = small;
|
||||
grid.previewSize(index, changes);
|
||||
}
|
||||
|
||||
@@ -104,6 +128,13 @@ DankEditableGridSlot {
|
||||
when: root.tileItem !== null
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: root.tileItem
|
||||
property: "small"
|
||||
value: root.small
|
||||
when: root.tileItem !== null && "small" in root.tileItem
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.tileItem
|
||||
ignoreUnknownSignals: true
|
||||
@@ -131,9 +162,13 @@ DankEditableGridSlot {
|
||||
passthrough: root.passthrough
|
||||
dragging: root.dragging
|
||||
resizing: root.resizing
|
||||
cornerRadius: root.tileItem?.bodyRadius ?? Theme.fullRadius(root.width, root.height)
|
||||
sizeText: root.cols + "×" + root.rows
|
||||
onResizeStarted: (px, py) => root.beginResize(px, py)
|
||||
cornerRadius: root.small ? Theme.fullRadius(root.width, root.height) : (root.tileItem?.bodyRadius ?? Theme.fullRadius(root.width, root.height))
|
||||
sizeText: (root.small && root.cols === 1 ? CcMetrics.smallRowFraction : root.cols) + "×" + (root.small ? CcMetrics.smallRowFraction : root.rows)
|
||||
onResizeStarted: (px, py) => {
|
||||
root.biasH = root.small ? (CcMetrics.smallRowFraction - 1) * root.grid.cellWidth : 0;
|
||||
root.biasW = root.small && root.cols === 1 ? root.biasH : 0;
|
||||
root.beginResize(px, py);
|
||||
}
|
||||
onResizeMoved: (px, py) => root.resizeTo(px, py)
|
||||
onResizeEnded: root.finishResize()
|
||||
onResizeCanceled: root.cancelResize()
|
||||
|
||||
@@ -15,6 +15,8 @@ Item {
|
||||
id: root
|
||||
|
||||
property int widgetIndex: -1
|
||||
property var model: null
|
||||
property int columns: CcMetrics.gridColumns
|
||||
property var transientSurfaceTracker: null
|
||||
property Item _anchor: null
|
||||
|
||||
@@ -29,6 +31,8 @@ Item {
|
||||
readonly property bool isDisk: widgetId === "diskUsage"
|
||||
readonly property bool isIdleInhibitor: widgetId === "idleInhibitor"
|
||||
readonly property bool isUser: widgetId === "user"
|
||||
readonly property bool isRunningApps: widgetId === "runningApps"
|
||||
readonly property var cardActions: WidgetUtils.cardActions(widgetData)
|
||||
|
||||
visible: widgetIndex >= 0 || contextMenu.renderActive
|
||||
|
||||
@@ -86,53 +90,96 @@ Item {
|
||||
}
|
||||
|
||||
customContent: Component {
|
||||
CcGroup {
|
||||
CcListRow {
|
||||
visible: root.isPlugin
|
||||
iconName: "settings"
|
||||
title: I18n.tr("Plugin settings")
|
||||
clickable: true
|
||||
onClicked: {
|
||||
PopoutService.openSettingsWithTab(SettingsTabs.pluginPrefix + root.widgetId.replace("plugin_", ""));
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
Column {
|
||||
width: parent?.width ?? 0
|
||||
|
||||
Repeater {
|
||||
model: root.isUser ? DashRegistry.sheetOptionSpecs("user") : []
|
||||
CcGroup {
|
||||
width: parent.width
|
||||
|
||||
CcListRow {
|
||||
visible: root.isPlugin
|
||||
iconName: "settings"
|
||||
title: I18n.tr("Plugin settings")
|
||||
clickable: true
|
||||
onClicked: {
|
||||
PopoutService.openSettingsWithTab(SettingsTabs.pluginPrefix + root.widgetId.replace("plugin_", ""));
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
CcToggleRow {
|
||||
required property var modelData
|
||||
visible: root.isUser
|
||||
text: I18n.tr("Background")
|
||||
checked: root.widgetData?.background === true
|
||||
onToggled: checked => root.persistOption("background", checked)
|
||||
}
|
||||
|
||||
text: modelData.text
|
||||
checked: DashRegistry.optionValue(modelData, root.widgetData?.[modelData.key])
|
||||
onToggled: checked => root.persistOption(modelData.key, checked)
|
||||
Repeater {
|
||||
model: root.isUser ? 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)
|
||||
}
|
||||
}
|
||||
|
||||
CcToggleRow {
|
||||
visible: root.isRunningApps
|
||||
text: I18n.tr("Show window count", "toggle in control center running apps widget options")
|
||||
checked: root.widgetData?.showCount !== false
|
||||
onToggled: checked => root.persistOption("showCount", 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")
|
||||
checked: root.widgetData?.showMountPath !== false
|
||||
onToggled: checked => root.persistOption("showMountPath", checked)
|
||||
}
|
||||
|
||||
CcListRow {
|
||||
visible: root.isIdleInhibitor
|
||||
iconName: "timer"
|
||||
title: I18n.tr("Duration")
|
||||
body: DankDropdown {
|
||||
readonly property var presets: IdleInhibitPresets.presetOptions
|
||||
|
||||
compactMode: true
|
||||
dropdownWidth: parent.width
|
||||
transientSurfaceTracker: contextMenu.transientSurfaceTracker
|
||||
currentValue: presets.find(p => p.minutes === (root.widgetData?.durationMinutes ?? 0))?.label ?? ""
|
||||
options: presets.map(p => p.label)
|
||||
onValueChanged: value => {
|
||||
const preset = presets.find(p => p.label === value);
|
||||
if (preset)
|
||||
root.persistOption("durationMinutes", preset.minutes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
checked: root.widgetData?.showMountPath !== false
|
||||
onToggled: checked => root.persistOption("showMountPath", checked)
|
||||
CcSectionLabel {
|
||||
width: parent.width
|
||||
visible: root.isUser
|
||||
text: I18n.tr("Actions")
|
||||
}
|
||||
|
||||
CcListRow {
|
||||
visible: root.isIdleInhibitor
|
||||
iconName: "timer"
|
||||
title: I18n.tr("Duration")
|
||||
body: DankDropdown {
|
||||
readonly property var presets: IdleInhibitPresets.presetOptions
|
||||
CcGroup {
|
||||
width: parent.width
|
||||
visible: root.isUser
|
||||
|
||||
compactMode: true
|
||||
dropdownWidth: parent.width
|
||||
transientSurfaceTracker: contextMenu.transientSurfaceTracker
|
||||
currentValue: presets.find(p => p.minutes === (root.widgetData?.durationMinutes ?? 0))?.label ?? ""
|
||||
options: presets.map(p => p.label)
|
||||
onValueChanged: value => {
|
||||
const preset = presets.find(p => p.label === value);
|
||||
if (preset)
|
||||
root.persistOption("durationMinutes", preset.minutes);
|
||||
Repeater {
|
||||
model: root.isUser ? WidgetUtils.BUTTON_IDS : []
|
||||
|
||||
CcToggleRow {
|
||||
required property string modelData
|
||||
|
||||
text: root.model?.getWidgetForId(modelData)?.text ?? modelData
|
||||
checked: root.cardActions.includes(modelData)
|
||||
onToggled: checked => root.widgetIndex = WidgetUtils.setCardAction(root.widgetIndex, modelData, checked, root.columns)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import qs.Modules.ControlCenter.Models
|
||||
import qs.Modules.ControlCenter.Details
|
||||
import qs.Widgets
|
||||
import "./utils/sections.js" as Sections
|
||||
import "./utils/widgets.js" as WidgetUtils
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
@@ -19,7 +20,7 @@ FocusScope {
|
||||
|
||||
readonly property bool pageOpen: (host.expandedSection ?? "") !== ""
|
||||
readonly property real gridHeight: widgetGrid.gridHeight
|
||||
readonly property real chromeHeight: Theme.spacingS + footer.height
|
||||
readonly property real chromeHeight: footer.occupied ? Theme.spacingS + footer.height : 0
|
||||
readonly property bool widgetSheetOpen: widgetSheetLoader.item?.active ?? false
|
||||
readonly property real coveredAmount: Math.max(detailPage.opacity, widgetSheetLoader.item?.progress ?? 0)
|
||||
property bool widgetSheetRequested: false
|
||||
@@ -30,7 +31,8 @@ FocusScope {
|
||||
const current = CompositorService.filterCurrentWorkspace(all, host.triggerScreen?.name) || [];
|
||||
return current.concat(all.filter(toplevel => !current.includes(toplevel)));
|
||||
}
|
||||
readonly property string placedWidgetIds: (SettingsData.controlCenterWidgets || []).map(w => w.id).sort().join(",")
|
||||
readonly property string placedWidgetIds: WidgetUtils.placedIds(SettingsData.controlCenterWidgets).sort().join(",")
|
||||
readonly property bool editPlaced: placedWidgetIds.split(",").includes("edit")
|
||||
readonly property real targetImplicitHeight: {
|
||||
const total = CcMetrics.sheetPadding * 2 + gridHeight + chromeHeight;
|
||||
if (detailPage.shownSection === "")
|
||||
@@ -45,7 +47,8 @@ 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 - chromeHeight
|
||||
// Always reserve the footer: the row cap stays the same in edit mode and cannot loop through the docked edit button.
|
||||
readonly property real availableGridHeight: (host.availableHeight ?? (host.triggerScreen?.height ?? CcMetrics.fallbackScreenHeight) - CcMetrics.maxHeightInset) - CcMetrics.sheetPadding * 2 - Theme.spacingS - CcMetrics.footerHeight
|
||||
readonly property vector4d chromeRoom: host.chromeRoom ?? Qt.vector4d(Infinity, Infinity, Infinity, Infinity)
|
||||
readonly property DankPanelResizer panelResizer: DankPanelResizer {
|
||||
popout: root.host
|
||||
@@ -287,7 +290,7 @@ FocusScope {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: footer.top
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
anchors.bottomMargin: footer.occupied ? Theme.spacingS : 0
|
||||
clip: contentHeight > height
|
||||
contentWidth: width
|
||||
contentHeight: Math.max(height, mainColumn.implicitHeight + CcMetrics.sheetPadding)
|
||||
@@ -320,12 +323,16 @@ FocusScope {
|
||||
live: root.host.shouldBeVisible && detailPage.shownSection === ""
|
||||
screenName: root.host.triggerScreen?.name || ""
|
||||
tapToClose: root.host.headerTogglesClose ?? false
|
||||
runningToplevels: root.runningToplevels
|
||||
dockEdit: !root.editPlaced && !root.host.editMode
|
||||
onExpandClicked: widgetData => root.openWidgetPage(widgetData)
|
||||
onRemoveWidget: index => widgetModel.removeWidget(index)
|
||||
onConfigRequested: (index, widgetData, anchor) => root.openConfigOverlay(index, widgetData, anchor)
|
||||
onColorPickerRequested: root.host.openColorPicker()
|
||||
onCloseRequested: root.host.close()
|
||||
onSettingsRequested: root.host.openSettings()
|
||||
onAccountsRequested: root.host.openAccounts()
|
||||
onEditRequested: root.host.editMode = true
|
||||
onLockRequested: {
|
||||
root.host.close();
|
||||
root.host.lockRequested();
|
||||
@@ -353,9 +360,8 @@ FocusScope {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: CcMetrics.sheetPadding
|
||||
editMode: root.host.editMode
|
||||
toplevels: root.runningToplevels
|
||||
showEdit: !root.editPlaced && widgetGrid.editDock === null
|
||||
opacity: body.opacity
|
||||
onRunningAppsRequested: root.navigateTo("runningApps")
|
||||
onAddWidgetRequested: root.openWidgetSheet()
|
||||
onResetRequested: widgetModel.resetToDefault()
|
||||
onClearRequested: widgetModel.clearAll()
|
||||
@@ -459,6 +465,8 @@ FocusScope {
|
||||
|
||||
active: false
|
||||
sourceComponent: WidgetConfigOverlay {
|
||||
model: widgetModel
|
||||
columns: root.gridColumns
|
||||
transientSurfaceTracker: root.host.transientSurfaceTracker
|
||||
onVisibleChanged: {
|
||||
if (visible)
|
||||
|
||||
@@ -36,6 +36,11 @@ DankPopout {
|
||||
close();
|
||||
}
|
||||
|
||||
function openAccounts() {
|
||||
requestedWindow = "accounts";
|
||||
close();
|
||||
}
|
||||
|
||||
function openColorPicker() {
|
||||
if (!colorPickerModal)
|
||||
return;
|
||||
@@ -65,6 +70,9 @@ DankPopout {
|
||||
case "settings":
|
||||
PopoutService.focusOrToggleSettings();
|
||||
break;
|
||||
case "accounts":
|
||||
PopoutService.openSettingsWithTab("user_accounts");
|
||||
break;
|
||||
case "colorPicker":
|
||||
root.colorPickerModal?.show();
|
||||
break;
|
||||
|
||||
@@ -110,6 +110,9 @@ QtObject {
|
||||
readonly property Component actionTile: Component {
|
||||
ActionTile {}
|
||||
}
|
||||
readonly property Component runningAppsTile: Component {
|
||||
RunningAppsTile {}
|
||||
}
|
||||
readonly property Component pluginTile: Component {
|
||||
PluginTile {}
|
||||
}
|
||||
@@ -168,7 +171,10 @@ QtObject {
|
||||
case "lock":
|
||||
case "power":
|
||||
case "settings":
|
||||
case "edit":
|
||||
return actionTile;
|
||||
case "runningApps":
|
||||
return runningAppsTile;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -223,6 +229,24 @@ QtObject {
|
||||
"category": "system",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "edit",
|
||||
"text": I18n.tr("Edit"),
|
||||
"description": "",
|
||||
"icon": "edit",
|
||||
"type": "action",
|
||||
"category": "system",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "runningApps",
|
||||
"text": I18n.tr("Running apps"),
|
||||
"description": "",
|
||||
"icon": "apps",
|
||||
"type": "action",
|
||||
"category": "system",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "nightMode",
|
||||
"text": I18n.tr("Night mode"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modules.ControlCenter
|
||||
import "../utils/widgets.js" as WidgetUtils
|
||||
|
||||
CcTile {
|
||||
id: root
|
||||
@@ -10,19 +10,7 @@ CcTile {
|
||||
toggle: false
|
||||
iconName: widgetDef?.icon ?? ""
|
||||
title: widgetDef?.text ?? ""
|
||||
restIconColor: action === "power" ? Theme.error : CcMetrics.tileInactiveIcon
|
||||
restIconColor: CcMetrics.actionIconColor(action)
|
||||
|
||||
onClicked: {
|
||||
switch (action) {
|
||||
case "lock":
|
||||
host?.lockRequested();
|
||||
return;
|
||||
case "power":
|
||||
host?.powerRequested();
|
||||
return;
|
||||
case "settings":
|
||||
host?.settingsRequested();
|
||||
return;
|
||||
}
|
||||
}
|
||||
onClicked: WidgetUtils.triggerButton(host, action)
|
||||
}
|
||||
|
||||
@@ -20,17 +20,24 @@ Item {
|
||||
property real columns: 4
|
||||
property real rows: 1
|
||||
property bool compact: columns <= 2 && rows === 1
|
||||
property bool small: false
|
||||
readonly property real bodyInset: small ? Math.max(0, (Math.min(width, height) - CcMetrics.iconBoxSize) / 2) : 0
|
||||
readonly property real bodyWidth: width - bodyInset * 2
|
||||
readonly property real bodyHeight: height - bodyInset * 2
|
||||
property bool toggle: !opensPage
|
||||
property Component expandedContent: null
|
||||
// Replaces the icon and labels inside the body; the chrome, focus and activation stay.
|
||||
property Component bodyContent: null
|
||||
property real expandedMinimumHeight: Theme.listItemHeight
|
||||
readonly property Item expandedItem: expandedLoader.item
|
||||
readonly property bool expanded: expandedContent !== null && width >= CcMetrics.expandedTileMinWidth && height >= headerHeight + expandedMinimumHeight + tilePadding * 2 + Theme.spacingM
|
||||
readonly property real tilePadding: tall && !narrow ? Theme.spacingM : Theme.spacingS
|
||||
readonly property real baseIconExtent: Math.min(Math.max(Theme.minimumTouchTargetSize, CcMetrics.iconBoxSize), height - tilePadding * 2, width - tilePadding * 2)
|
||||
readonly property real baseIconExtent: Math.min(Math.max(Theme.minimumTouchTargetSize, CcMetrics.iconBoxSize), bodyHeight - tilePadding * 2, bodyWidth - tilePadding * 2)
|
||||
readonly property real iconExtent: stacked ? Math.max(0, Math.min(baseIconExtent, height - tilePadding * 2 - Theme.spacingS - titleLabel.implicitHeight)) : baseIconExtent
|
||||
readonly property real headerHeight: Math.max(baseIconExtent, Theme.fontSizeLarge + Theme.fontSizeMedium + Theme.spacingS)
|
||||
readonly property bool stacked: tall && !expanded && width <= height
|
||||
readonly property bool narrow: width < CcMetrics.expandedTileMinWidth
|
||||
readonly property bool denseText: narrow || small
|
||||
readonly property real labelHeight: titleLabel.implicitHeight + (showSubtitle ? Theme.spacingXXS + subtitleLabel.implicitHeight : 0)
|
||||
readonly property bool showSubtitle: subtitle !== "" && (!stacked || height - tilePadding * 2 >= iconExtent + Theme.spacingS + titleLabel.implicitHeight + Theme.spacingXXS + subtitleLabel.implicitHeight)
|
||||
property bool showExpand: false
|
||||
@@ -50,10 +57,10 @@ Item {
|
||||
readonly property bool showsActive: active && interactive
|
||||
readonly property real restRadius: {
|
||||
if (showsActive)
|
||||
return Math.min(CcMetrics.tileActiveRadius, width / 2, height / 2);
|
||||
return tall ? Math.min(CcMetrics.tallTileRadius, width / 2, height / 2) : Theme.fullRadius(width, height);
|
||||
return Math.min(CcMetrics.tileActiveRadius, bodyWidth / 2, bodyHeight / 2);
|
||||
return tall ? Math.min(CcMetrics.tallTileRadius, width / 2, height / 2) : Theme.fullRadius(bodyWidth, bodyHeight);
|
||||
}
|
||||
readonly property bool acceptsInput: interactive && enabled
|
||||
property bool acceptsInput: interactive && enabled
|
||||
readonly property bool bodyActive: showsActive && !hasIconBox
|
||||
readonly property color bodyColor: {
|
||||
if (!enabled)
|
||||
@@ -83,7 +90,7 @@ Item {
|
||||
return showsActive ? CcMetrics.tileActiveColor : CcMetrics.iconBoxInactiveColor;
|
||||
}
|
||||
|
||||
property real bodyRadius: bodyLayer.pressed ? Math.min(Theme.cornerRadiusM, width / 2, height / 2) : restRadius
|
||||
property real bodyRadius: bodyLayer.pressed ? Math.min(Theme.cornerRadiusM, bodyWidth / 2, bodyHeight / 2) : restRadius
|
||||
property real iconBoxRadius: {
|
||||
if (boxLayer.pressed)
|
||||
return Math.min(Theme.cornerRadiusS, CcMetrics.iconBoxSize / 2);
|
||||
@@ -151,6 +158,7 @@ Item {
|
||||
id: body
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.bodyInset
|
||||
radius: root.bodyRadius
|
||||
color: root.bodyColor
|
||||
border.width: Theme.layerOutlineWidth
|
||||
@@ -197,11 +205,11 @@ Item {
|
||||
id: compactIcon
|
||||
anchors.centerIn: parent
|
||||
name: root.iconName
|
||||
size: CcMetrics.tileIconSize
|
||||
size: root.small ? CcMetrics.iconBoxIconSize : CcMetrics.tileIconSize
|
||||
color: root.iconColor
|
||||
filled: root.showsActive
|
||||
rotation: root.iconRotation
|
||||
visible: root.compact
|
||||
visible: root.compact && root.bodyContent === null
|
||||
|
||||
DankBlink {
|
||||
target: compactIcon
|
||||
@@ -209,11 +217,17 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: root.bodyContent !== null
|
||||
sourceComponent: root.bodyContent
|
||||
}
|
||||
|
||||
Item {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.tilePadding
|
||||
visible: !root.compact
|
||||
visible: !root.compact && root.bodyContent === null
|
||||
|
||||
Rectangle {
|
||||
id: iconBox
|
||||
@@ -311,7 +325,7 @@ Item {
|
||||
width: parent.width
|
||||
text: root.title
|
||||
color: root.contentColor
|
||||
font.pixelSize: root.narrow ? Theme.fontSizeMedium : Theme.fontSizeLarge
|
||||
font.pixelSize: root.denseText ? Theme.fontSizeMedium : Theme.fontSizeLarge
|
||||
font.weight: Theme.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: root.stacked ? Text.Wrap : Text.NoWrap
|
||||
@@ -326,7 +340,7 @@ Item {
|
||||
width: parent.width
|
||||
text: root.subtitle
|
||||
color: root.subtitleColor
|
||||
font.pixelSize: root.narrow ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||
font.pixelSize: root.denseText ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
horizontalAlignment: root.stacked && root.narrow ? Text.AlignHCenter : Text.AlignLeft
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modules.ControlCenter
|
||||
import qs.Widgets
|
||||
|
||||
CcTile {
|
||||
id: root
|
||||
|
||||
readonly property var toplevels: host?.runningToplevels ?? []
|
||||
readonly property var appIds: {
|
||||
const ids = [];
|
||||
for (const toplevel of toplevels) {
|
||||
const id = toplevel?.appId ?? "";
|
||||
if (!ids.includes(id))
|
||||
ids.push(id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
readonly property bool showCount: widgetData?.showCount !== false
|
||||
readonly property string countText: toplevels.length === 1 ? I18n.tr("%1 window", "singular, %1 is 1, open windows in the control center footer").arg(1) : I18n.tr("%1 windows", "plural, %1 is a count of open windows in the control center footer").arg(toplevels.length)
|
||||
|
||||
opensPage: true
|
||||
title: widgetDef?.text ?? ""
|
||||
subtitle: countText
|
||||
acceptsInput: interactive && toplevels.length > 0
|
||||
onClicked: expandClicked()
|
||||
|
||||
bodyContent: Component {
|
||||
Item {
|
||||
id: body
|
||||
|
||||
readonly property real iconBox: Math.max(0, Math.min(CcMetrics.iconBoxSize, height - Theme.spacingS * 2))
|
||||
readonly property int iconCount: Math.max(1, Math.min(root.appIds.length, CcMetrics.runningAppsMaxIcons, Math.floor((width - Theme.spacingS * 2 + Theme.spacingXS) / (iconBox + Theme.spacingXS))))
|
||||
readonly property real chevronRoom: chevron.width + Theme.spacingXS
|
||||
readonly property real textRoom: width - Theme.spacingS * 2 - icons.width - Theme.spacingM
|
||||
// Drop the wording, then the chevron, then the count itself as the pill narrows.
|
||||
readonly property string label: {
|
||||
if (!root.showCount || root.toplevels.length === 0)
|
||||
return "";
|
||||
if (fullMetrics.advanceWidth + chevronRoom <= textRoom)
|
||||
return root.countText;
|
||||
return shortMetrics.advanceWidth <= textRoom ? String(root.toplevels.length) : "";
|
||||
}
|
||||
readonly property bool showChevron: label !== "" && (label === root.countText || shortMetrics.advanceWidth + chevronRoom <= textRoom)
|
||||
|
||||
StyledTextMetrics {
|
||||
id: fullMetrics
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Theme.fontWeightMedium
|
||||
text: root.countText
|
||||
}
|
||||
|
||||
StyledTextMetrics {
|
||||
id: shortMetrics
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Theme.fontWeightMedium
|
||||
text: String(root.toplevels.length)
|
||||
}
|
||||
|
||||
Row {
|
||||
id: icons
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: body.label === "" ? (parent.width - width) / 2 : Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.appIds.length > 0 ? root.appIds.slice(0, body.iconCount) : [""]
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
required property string modelData
|
||||
|
||||
width: body.iconBox
|
||||
height: width
|
||||
radius: Theme.fullRadius(width, height)
|
||||
color: CcMetrics.iconBoxInactiveColor
|
||||
|
||||
CcAppIcon {
|
||||
anchors.centerIn: parent
|
||||
appId: parent.modelData
|
||||
iconSize: Math.min(CcMetrics.runningAppsIconSize, parent.width)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
objectName: "runningAppsLabel"
|
||||
anchors.left: icons.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: body.showChevron ? chevron.left : parent.right
|
||||
anchors.rightMargin: body.showChevron ? Theme.spacingXS : Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: body.label
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: root.contentColor
|
||||
elide: Text.ElideRight
|
||||
visible: body.label !== ""
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: chevron
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: I18n.isRtl ? "chevron_left" : "chevron_right"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.onSurfaceVariant
|
||||
visible: body.showChevron
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modules.ControlCenter
|
||||
import qs.Modules.DankDash
|
||||
@@ -21,13 +22,35 @@ Item {
|
||||
|
||||
signal optionChanged(string key, var value)
|
||||
|
||||
readonly property bool wide: width >= height * 2
|
||||
readonly property real avatarSide: Math.min(width, height)
|
||||
readonly property var savedOrder: WidgetUtils.cardOrder(widgetData)
|
||||
property var previewOrder: null
|
||||
property string draggedAction: ""
|
||||
property point dragPosition: Qt.point(0, 0)
|
||||
property point grabOffset: Qt.point(0, 0)
|
||||
readonly property var order: previewOrder ?? savedOrder
|
||||
readonly property int split: order.indexOf(WidgetUtils.CARD_IDENTITY)
|
||||
readonly property int leadingColumns: WidgetUtils.actionColumns(rows, split)
|
||||
readonly property int trailingColumns: WidgetUtils.actionColumns(rows, order.length - split - 1)
|
||||
// Grid pitch keeps card buttons in line with small tiles outside the card.
|
||||
readonly property real pitch: host?.cellWidth ?? CcMetrics.columnWidth + CcMetrics.gridGap
|
||||
readonly property real buttonInset: Math.max(0, (pitch - CcMetrics.gridGap - CcMetrics.iconBoxSize) / 2)
|
||||
readonly property real identityStart: leadingColumns * pitch
|
||||
readonly property real identityWidth: Math.max(0, width - (leadingColumns + trailingColumns) * pitch)
|
||||
readonly property bool wide: identityWidth >= height * 2
|
||||
readonly property bool background: widgetData?.background === true
|
||||
readonly property bool tall: height >= CcMetrics.gridRowUnit * 2
|
||||
readonly property real inset: background ? (tall ? Theme.spacingM : Theme.spacingS) : 0
|
||||
readonly property real avatarSide: Math.min(identityWidth, height) - inset * 2
|
||||
readonly property string shape: widgetData?.shape ?? WidgetUtils.USER_SHAPES[0]
|
||||
readonly property bool editMode: host?.editMode ?? false
|
||||
readonly property bool tapToClose: interactive && (host?.tapToClose ?? false)
|
||||
readonly property Item passthrough: shapeButton
|
||||
readonly property real bodyRadius: Theme.fullRadius(avatarSide, avatarSide)
|
||||
readonly property Item passthrough: passthroughArea
|
||||
// Only a bare avatar gets the circle outline; any button beside it turns the card into a pill.
|
||||
readonly property real bodyRadius: {
|
||||
if (!background && savedOrder.length === 1)
|
||||
return Theme.fullRadius(avatarSide, avatarSide);
|
||||
return tall ? Math.min(CcMetrics.tallTileRadius, width / 2, height / 2) : Theme.fullRadius(width, height);
|
||||
}
|
||||
readonly property real fallbackGlyphRatio: 0.5
|
||||
readonly property string avatarSource: PortalService.profileImageUrl
|
||||
readonly property bool hasImage: picture.status === Image.Ready
|
||||
@@ -39,12 +62,99 @@ Item {
|
||||
Accessible.role: Accessible.StaticText
|
||||
Accessible.name: UserInfoService.fullName || UserInfoService.username
|
||||
|
||||
UserIdentity {
|
||||
function actionSlot(id) {
|
||||
const index = order.indexOf(id);
|
||||
const leading = index < split;
|
||||
const count = leading ? split : order.length - split - 1;
|
||||
const position = leading ? index : index - split - 1;
|
||||
const perLine = WidgetUtils.actionColumns(rows, count);
|
||||
const lines = Math.ceil(count / perLine);
|
||||
const size = CcMetrics.iconBoxSize;
|
||||
const start = leading ? 0 : width + CcMetrics.gridGap - trailingColumns * pitch;
|
||||
const x = start + (position % perLine) * pitch + buttonInset;
|
||||
return Qt.point(I18n.isRtl ? width - x - size : x, (height - lines * pitch + pitch - size) / 2 + Math.floor(position / perLine) * pitch);
|
||||
}
|
||||
|
||||
// Splitting at the identity's centre stops a swap across it from flickering back.
|
||||
function reorderTarget(point) {
|
||||
const identityX = I18n.isRtl ? width - identityStart - identityWidth : identityStart;
|
||||
if (point.x >= identityX && point.x < identityX + identityWidth) {
|
||||
const leading = (point.x < identityX + identityWidth / 2) !== I18n.isRtl;
|
||||
return (order.indexOf(draggedAction) < split) === leading ? "" : WidgetUtils.CARD_IDENTITY;
|
||||
}
|
||||
const half = pitch / 2;
|
||||
const center = CcMetrics.iconBoxSize / 2;
|
||||
return order.find(id => {
|
||||
if (id === draggedAction || id === WidgetUtils.CARD_IDENTITY)
|
||||
return false;
|
||||
const slot = actionSlot(id);
|
||||
return Math.abs(point.x - slot.x - center) < half && Math.abs(point.y - slot.y - center) < half;
|
||||
}) ?? "";
|
||||
}
|
||||
|
||||
function beginReorder(id, scenePoint) {
|
||||
const point = mapFromItem(null, scenePoint.x, scenePoint.y);
|
||||
const slot = actionSlot(id);
|
||||
grabOffset = Qt.point(point.x - slot.x, point.y - slot.y);
|
||||
dragPosition = slot;
|
||||
previewOrder = savedOrder.slice();
|
||||
draggedAction = id;
|
||||
}
|
||||
|
||||
function moveReorder(scenePoint) {
|
||||
if (draggedAction === "")
|
||||
return;
|
||||
const point = mapFromItem(null, scenePoint.x, scenePoint.y);
|
||||
dragPosition = Qt.point(point.x - grabOffset.x, point.y - grabOffset.y);
|
||||
const target = reorderTarget(point);
|
||||
if (target !== "")
|
||||
previewOrder = WidgetUtils.moveInOrder(previewOrder, draggedAction, target);
|
||||
}
|
||||
|
||||
function finishReorder() {
|
||||
if (draggedAction === "")
|
||||
return;
|
||||
if (JSON.stringify(previewOrder) !== JSON.stringify(savedOrder))
|
||||
optionChanged("actions", previewOrder);
|
||||
draggedAction = "";
|
||||
previewOrder = null;
|
||||
}
|
||||
|
||||
function passesThrough(point) {
|
||||
const targets = [shapeButton];
|
||||
for (let i = 0; i < actionRepeater.count; i++)
|
||||
targets.push(actionRepeater.itemAt(i));
|
||||
return targets.some(item => item?.visible && item.contains(mapToItem(item, point.x, point.y)));
|
||||
}
|
||||
|
||||
onEditModeChanged: finishReorder()
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.bodyRadius
|
||||
color: CcMetrics.tileInactiveColor
|
||||
border.width: Theme.layerOutlineWidth
|
||||
border.color: Theme.outlineMedium
|
||||
visible: root.background
|
||||
}
|
||||
|
||||
Item {
|
||||
id: identityArea
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.identityStart
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.identityWidth
|
||||
}
|
||||
|
||||
UserIdentity {
|
||||
anchors.fill: identityArea
|
||||
anchors.margins: root.inset
|
||||
visible: root.wide
|
||||
live: root.live && root.wide
|
||||
options: DashRegistry.resolvedOptions("user", root.widgetData)
|
||||
avatarSize: root.height
|
||||
avatarSize: height
|
||||
stacked: root.rows >= 2
|
||||
contentColor: Theme.onSurface
|
||||
mutedColor: Theme.onSurfaceVariant
|
||||
@@ -53,7 +163,7 @@ Item {
|
||||
Item {
|
||||
id: avatar
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.centerIn: identityArea
|
||||
width: root.avatarSide
|
||||
height: root.avatarSide
|
||||
visible: !root.wide
|
||||
@@ -111,6 +221,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
objectName: "userCloseButton"
|
||||
anchors.fill: parent
|
||||
visible: root.tapToClose
|
||||
radius: root.bodyRadius
|
||||
@@ -127,6 +238,102 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: actionRepeater
|
||||
|
||||
model: ScriptModel {
|
||||
values: WidgetUtils.BUTTON_IDS.filter(id => root.savedOrder.includes(id))
|
||||
}
|
||||
|
||||
Item {
|
||||
id: action
|
||||
|
||||
required property string modelData
|
||||
readonly property var definition: root.host?.model?.getWidgetForId(modelData) ?? null
|
||||
readonly property point slot: root.actionSlot(modelData)
|
||||
readonly property bool held: root.draggedAction === modelData
|
||||
|
||||
x: held ? root.dragPosition.x : slot.x
|
||||
y: held ? root.dragPosition.y : slot.y
|
||||
z: held ? 1 : 0
|
||||
width: CcMetrics.iconBoxSize
|
||||
height: width
|
||||
|
||||
Behavior on x {
|
||||
enabled: !action.held && CcMetrics.animationsEnabled
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveFastSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
enabled: !action.held && CcMetrics.animationsEnabled
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveFastSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.fill: parent
|
||||
buttonSize: CcMetrics.iconBoxSize
|
||||
iconName: action.definition?.icon ?? ""
|
||||
iconSize: CcMetrics.iconBoxIconSize
|
||||
iconColor: CcMetrics.actionIconColor(action.modelData)
|
||||
backgroundColor: root.background ? "transparent" : CcMetrics.tileInactiveColor
|
||||
border.width: root.background ? 0 : Theme.layerOutlineWidth
|
||||
border.color: Theme.outlineMedium
|
||||
tooltipText: action.definition?.text ?? ""
|
||||
onClicked: {
|
||||
if (root.interactive)
|
||||
WidgetUtils.triggerButton(root.host, action.modelData);
|
||||
}
|
||||
}
|
||||
|
||||
DragHandler {
|
||||
target: null
|
||||
enabled: root.editMode
|
||||
cursorShape: Qt.ClosedHandCursor
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
root.beginReorder(action.modelData, centroid.scenePressPosition);
|
||||
return;
|
||||
}
|
||||
root.finishReorder();
|
||||
}
|
||||
onCentroidChanged: {
|
||||
if (active)
|
||||
root.moveReorder(centroid.scenePosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
objectName: "userAvatarButton"
|
||||
x: identityArea.x + (!root.wide ? (identityArea.width - width) / 2 : I18n.isRtl ? identityArea.width - root.inset - width : root.inset)
|
||||
y: (root.height - height) / 2
|
||||
width: root.avatarSide
|
||||
height: root.avatarSide
|
||||
radius: Theme.fullRadius(width, height)
|
||||
visible: root.interactive
|
||||
Accessible.name: I18n.tr("Users & accounts", "settings sidebar category")
|
||||
onClicked: root.host?.accountsRequested()
|
||||
|
||||
FocusRing {
|
||||
visible: parent.visualFocus
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
control: parent
|
||||
cornerRadius: parent.radius
|
||||
tooltipText: parent.Accessible.name
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: shapeButton
|
||||
|
||||
@@ -141,4 +348,16 @@ Item {
|
||||
visible: root.editMode && avatar.visible
|
||||
onClicked: root.optionChanged("shape", WidgetUtils.nextUserShape(root.shape))
|
||||
}
|
||||
|
||||
// Edit-mode grid input skips these points so the shape and card buttons stay clickable.
|
||||
Item {
|
||||
id: passthroughArea
|
||||
|
||||
anchors.fill: parent
|
||||
containmentMask: QtObject {
|
||||
function contains(point: point): bool {
|
||||
return root.passesThrough(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,81 @@
|
||||
.import qs.Modules.ControlCenter as ControlCenter
|
||||
.import "../../../Common/GridLayout.js" as GridLayout
|
||||
|
||||
var OPTION_IDS = ["diskUsage", "brightnessSlider", "idleInhibitor", "user"];
|
||||
var OPTION_IDS = ["diskUsage", "brightnessSlider", "idleInhibitor", "user", "runningApps"];
|
||||
var ACTION_IDS = ["settings", "lock", "power"];
|
||||
var BUTTON_IDS = ACTION_IDS.concat(["edit"]);
|
||||
var SMALL_BY_DEFAULT = BUTTON_IDS.concat(["runningApps"]);
|
||||
var CARD_IDENTITY = "user";
|
||||
var USER_SHAPES = ["circle", "cookie4", "cookie7", "cookie12", "clover4", "clover8", "sunny", "pentagon", "arch", "slanted", "gem", "square"];
|
||||
|
||||
function isSliderWidget(id) {
|
||||
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider";
|
||||
}
|
||||
|
||||
function isButton(id) {
|
||||
return BUTTON_IDS.includes(id);
|
||||
}
|
||||
|
||||
// A "user" entry marks where the identity sits among the card's buttons.
|
||||
function cardOrder(widget) {
|
||||
const saved = Array.isArray(widget?.actions) ? widget.actions : [];
|
||||
const order = saved.filter((id, i) => (id === CARD_IDENTITY || BUTTON_IDS.includes(id)) && saved.indexOf(id) === i);
|
||||
return order.includes(CARD_IDENTITY) ? order : [CARD_IDENTITY].concat(order);
|
||||
}
|
||||
|
||||
function cardActions(widget) {
|
||||
return cardOrder(widget).filter(id => id !== CARD_IDENTITY);
|
||||
}
|
||||
|
||||
function actionColumns(rows, count) {
|
||||
return count > 0 ? Math.ceil(count / Math.max(1, Math.min(count, Math.floor(Number(rows) || 1)))) : 0;
|
||||
}
|
||||
|
||||
function cardColumns(order, rows) {
|
||||
const split = order.indexOf(CARD_IDENTITY);
|
||||
return actionColumns(rows, split) + actionColumns(rows, order.length - split - 1);
|
||||
}
|
||||
|
||||
function moveInOrder(order, id, targetId) {
|
||||
const from = order.indexOf(id);
|
||||
const to = order.indexOf(targetId);
|
||||
if (from < 0 || to < 0 || from === to)
|
||||
return order;
|
||||
const moved = order.slice();
|
||||
moved.splice(from, 1);
|
||||
moved.splice(to, 0, id);
|
||||
return moved;
|
||||
}
|
||||
|
||||
function placedIds(widgets) {
|
||||
return (widgets || []).reduce((ids, widget) => ids.concat([widget?.id], cardActions(widget)), []);
|
||||
}
|
||||
|
||||
function triggerButton(host, id) {
|
||||
switch (id) {
|
||||
case "lock":
|
||||
host?.lockRequested();
|
||||
return;
|
||||
case "power":
|
||||
host?.powerRequested();
|
||||
return;
|
||||
case "settings":
|
||||
host?.settingsRequested();
|
||||
return;
|
||||
case "edit":
|
||||
host?.editRequested();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function canShrink(id) {
|
||||
return id !== "user" && !isSliderWidget(id);
|
||||
}
|
||||
|
||||
function isSmall(widget, rows) {
|
||||
return widget?.small === true && rows === 1 && canShrink(widget.id);
|
||||
}
|
||||
|
||||
function isShown(widget) {
|
||||
switch (widget?.id) {
|
||||
case "battery":
|
||||
@@ -60,9 +127,13 @@ function sizeSpec(widget, columns, rows = Infinity) {
|
||||
"maxH": rows,
|
||||
"step": ControlCenter.CcMetrics.gridStep
|
||||
};
|
||||
if (widget?.id === "user")
|
||||
if (widget?.id === "user") {
|
||||
spec.w = (Number.isFinite(columns) ? columns : ControlCenter.CcMetrics.defaultColumns) - ACTION_IDS.length;
|
||||
if (ACTION_IDS.includes(widget?.id))
|
||||
spec.minW = Math.min(columns, 1 + cardColumns(cardOrder(widget), widget.h));
|
||||
}
|
||||
if (widget?.id === "runningApps")
|
||||
spec.w = Number.isFinite(columns) ? columns : ControlCenter.CcMetrics.defaultColumns;
|
||||
if (isButton(widget?.id))
|
||||
spec.w = 1;
|
||||
return spec;
|
||||
}
|
||||
@@ -89,6 +160,8 @@ function clampSize(widget, columns, rows = Infinity) {
|
||||
function addWidget(widgetId, columns) {
|
||||
const widgets = Common.SettingsData.controlCenterWidgets.slice();
|
||||
const widget = defaultWidget(widgetId, columns);
|
||||
if (SMALL_BY_DEFAULT.includes(widgetId))
|
||||
widget.small = true;
|
||||
|
||||
if (widgetId === "diskUsage") {
|
||||
widget.instanceId = generateUniqueId();
|
||||
@@ -127,6 +200,50 @@ function setOption(index, key, value) {
|
||||
Common.SettingsData.set("controlCenterWidgets", widgets);
|
||||
}
|
||||
|
||||
// A button lives in the card or the grid, never both; returns the card's index since tiles moving in or out shift it.
|
||||
function setCardAction(index, id, enabled, columns) {
|
||||
const widgets = Common.SettingsData.controlCenterWidgets.slice();
|
||||
const card = widgets[index];
|
||||
if (!card)
|
||||
return index;
|
||||
const before = cardOrder(card);
|
||||
const order = before.filter(action => action !== id).concat(enabled ? [id] : []);
|
||||
const width = clampSize(card, columns).w;
|
||||
const w = Math.max(1, Math.min(columns, width + cardColumns(order, card.h) - cardColumns(before, card.h)));
|
||||
const updated = Object.assign({}, card, {
|
||||
"actions": order,
|
||||
"w": w
|
||||
});
|
||||
widgets[index] = updated;
|
||||
if (enabled)
|
||||
return commitCard(widgets.filter(widget => widget?.id !== id), updated);
|
||||
if (!before.includes(id) || placedIds(widgets).includes(id))
|
||||
return commitCard(widgets, updated);
|
||||
|
||||
const tile = Object.assign(defaultWidget(id, columns), {
|
||||
"small": true
|
||||
});
|
||||
const leading = before.indexOf(id) < before.indexOf(CARD_IDENTITY);
|
||||
const freed = width - w;
|
||||
if (isUnplaced(card)) {
|
||||
widgets.splice(leading ? index : index + 1, 0, tile);
|
||||
} else if (freed > 0) {
|
||||
tile.col = leading ? card.col : card.col + w;
|
||||
tile.row = card.row;
|
||||
if (leading)
|
||||
updated.col = card.col + freed;
|
||||
widgets.splice(index + 1, 0, tile);
|
||||
} else {
|
||||
widgets.push(tile);
|
||||
}
|
||||
return commitCard(widgets, updated);
|
||||
}
|
||||
|
||||
function commitCard(widgets, card) {
|
||||
Common.SettingsData.set("controlCenterWidgets", widgets);
|
||||
return widgets.indexOf(card);
|
||||
}
|
||||
|
||||
function setLayout(widgets) {
|
||||
Common.SettingsData.set("controlCenterWidgets", widgets);
|
||||
}
|
||||
|
||||
@@ -88,6 +88,10 @@ FocusScope {
|
||||
root.windowRequested("settings");
|
||||
}
|
||||
|
||||
function openAccounts() {
|
||||
root.windowRequested("accounts");
|
||||
}
|
||||
|
||||
function openColorPicker() {
|
||||
if (!PopoutService.colorPickerModal)
|
||||
return;
|
||||
|
||||
@@ -163,6 +163,9 @@ Item {
|
||||
case "settings":
|
||||
PopoutService.focusOrToggleSettings();
|
||||
break;
|
||||
case "accounts":
|
||||
PopoutService.openSettingsWithTab("user_accounts");
|
||||
break;
|
||||
case "colorPicker":
|
||||
PopoutService.showColorPicker();
|
||||
break;
|
||||
|
||||
@@ -87,8 +87,7 @@ Item {
|
||||
const target = root.passthrough;
|
||||
if (!target?.visible)
|
||||
return true;
|
||||
const local = root.mapToItem(target, point.x, point.y);
|
||||
return local.x < 0 || local.y < 0 || local.x >= target.width || local.y >= target.height;
|
||||
return !target.contains(root.mapToItem(target, point.x, point.y));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -231,8 +231,7 @@ Item {
|
||||
const target = root.passthrough;
|
||||
if (!target?.visible)
|
||||
return true;
|
||||
const local = band.mapToItem(target, point.x, point.y);
|
||||
return local.x < 0 || local.y < 0 || local.x >= target.width || local.y >= target.height;
|
||||
return !target.contains(band.mapToItem(target, point.x, point.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,20 @@ ShellRoot {
|
||||
WidgetModel {
|
||||
id: registry
|
||||
}
|
||||
QtObject {
|
||||
id: userHost
|
||||
property bool editMode: false
|
||||
property bool tapToClose: true
|
||||
property var model: registry
|
||||
property int closes: 0
|
||||
property int accounts: 0
|
||||
function closeRequested() {
|
||||
closes++;
|
||||
}
|
||||
function accountsRequested() {
|
||||
accounts++;
|
||||
}
|
||||
}
|
||||
IslandController {
|
||||
id: island
|
||||
dashboardAvailableWidth: CcMetrics.sheetWidthFor(20) + controlCenterSheetInset + PopoutMetrics.editOverflow * 2
|
||||
@@ -256,9 +270,241 @@ ShellRoot {
|
||||
sliderSlot.resizeRequested(grid.cellWidth * 3.5 - CcMetrics.gridGap, grid.cellWidth - CcMetrics.gridGap);
|
||||
grid.commitSize();
|
||||
check(SettingsData.controlCenterWidgets[0].w === 3.5 && sliderSlot.tileItem.columns === 3.5, "resizing snaps to half columns");
|
||||
SettingsData.controlCenterWidgets = [
|
||||
{
|
||||
id: "lock",
|
||||
w: 1,
|
||||
h: 1
|
||||
}
|
||||
];
|
||||
const buttonSlot = grid.children.find(child => typeof child.beginResize === "function");
|
||||
const buttonChrome = buttonSlot.children.find(child => typeof child.resizeStarted === "function");
|
||||
const resizeButton = delta => {
|
||||
buttonChrome.resizeStarted(0, 0, 1);
|
||||
buttonSlot.resizeTo(delta, delta);
|
||||
buttonSlot.finishResize();
|
||||
return SettingsData.controlCenterWidgets[0];
|
||||
};
|
||||
let button = resizeButton(-grid.cellWidth / 2);
|
||||
check(button.small === true && button.w === 1 && button.h === 1 && buttonSlot.tileItem.small, "shrinking a 1x1 below its cell keeps the cell and makes the button small " + JSON.stringify(button));
|
||||
button = resizeButton(0);
|
||||
check(button.small === true, "grabbing a small button without moving keeps it small");
|
||||
button = resizeButton(grid.cellWidth * 0.3);
|
||||
check(button.small === false && button.w === 1 && button.h === 1, "growing a small button passes through a full 1x1 " + JSON.stringify(button));
|
||||
button = resizeButton(-grid.cellWidth / 2);
|
||||
button = resizeButton(grid.cellWidth);
|
||||
check(button.small === false && button.w === 2 && button.h === 2, "past full size a small button grows a cell per cell of travel like a full one " + JSON.stringify(button));
|
||||
SettingsData.controlCenterWidgets = [
|
||||
{
|
||||
id: "darkMode",
|
||||
w: 4,
|
||||
h: 1
|
||||
}
|
||||
];
|
||||
const wideSlot = grid.children.find(child => typeof child.beginResize === "function");
|
||||
const wideChrome = wideSlot.children.find(child => typeof child.resizeStarted === "function");
|
||||
const resizeWide = delta => {
|
||||
wideChrome.resizeStarted(0, 0, 1);
|
||||
wideSlot.resizeTo(0, delta);
|
||||
wideSlot.finishResize();
|
||||
return SettingsData.controlCenterWidgets[0];
|
||||
};
|
||||
let wide = resizeWide(-grid.cellWidth / 2);
|
||||
check(wide.small === true && wide.w === 4 && wide.h === 1 && wideSlot.tileItem.small, "wide tiles take the half-height step too " + JSON.stringify(wide));
|
||||
wide = resizeWide(grid.cellWidth * 0.3);
|
||||
check(wide.small === false && wide.w === 4 && wide.h === 1, "a half-height tile grows back to a full row " + JSON.stringify(wide));
|
||||
SettingsData.controlCenterWidgets = [
|
||||
{
|
||||
id: "user",
|
||||
w: 5,
|
||||
h: 1
|
||||
},
|
||||
{
|
||||
id: "lock",
|
||||
w: 1,
|
||||
h: 1,
|
||||
small: true
|
||||
},
|
||||
{
|
||||
id: "edit",
|
||||
w: 1,
|
||||
h: 1,
|
||||
small: true
|
||||
}
|
||||
];
|
||||
WidgetUtils.setCardAction(0, "lock", true, 8);
|
||||
WidgetUtils.setCardAction(0, "edit", true, 8);
|
||||
let card = SettingsData.controlCenterWidgets[0];
|
||||
check(SettingsData.controlCenterWidgets.length === 1 && JSON.stringify(card.actions) === JSON.stringify(["user", "lock", "edit"]) && card.w === 7, "card actions take their tiles out of the grid and widen the card by their columns " + JSON.stringify(SettingsData.controlCenterWidgets));
|
||||
check(WidgetUtils.sizeSpec(Object.assign({}, card, {
|
||||
h: 2
|
||||
}), 8).minW === 2 && WidgetUtils.sizeSpec(card, 8).minW === 3, "card buttons wrap into rows before they claim columns");
|
||||
check(WidgetUtils.sizeSpec(Object.assign({}, card, {
|
||||
h: 2,
|
||||
actions: ["lock", "user", "edit"]
|
||||
}), 8).minW === 3, "buttons on both sides of the identity claim a column each");
|
||||
const cardTile = grid.children.find(child => typeof child.beginResize === "function").tileItem;
|
||||
const sceneOf = point => cardTile.mapToItem(null, point.x, point.y);
|
||||
const edit = cardTile.actionSlot("edit");
|
||||
cardTile.beginReorder("edit", sceneOf(Qt.point(edit.x + 1, edit.y + 1)));
|
||||
cardTile.moveReorder(sceneOf(Qt.point(cardTile.identityStart + cardTile.identityWidth * 0.75, cardTile.height / 2)));
|
||||
check(JSON.stringify(cardTile.order) === JSON.stringify(["user", "lock", "edit"]), "a button stays put on its own side of the identity");
|
||||
cardTile.moveReorder(sceneOf(Qt.point(cardTile.identityStart + cardTile.identityWidth * 0.25, cardTile.height / 2)));
|
||||
cardTile.moveReorder(sceneOf(Qt.point(cardTile.identityStart + cardTile.identityWidth * 0.4, cardTile.height / 2)));
|
||||
cardTile.finishReorder();
|
||||
card = SettingsData.controlCenterWidgets[0];
|
||||
check(JSON.stringify(card.actions) === JSON.stringify(["edit", "user", "lock"]), "dragging a button past the identity's centre saves it on the other side " + JSON.stringify(card));
|
||||
check(Math.abs(cardTile.actionSlot("edit").x - cardTile.buttonInset) < 0.5 && cardTile.identityStart === cardTile.pitch, "the identity makes room for leading buttons");
|
||||
const lock = cardTile.actionSlot("lock");
|
||||
const lockCenter = Qt.point(lock.x + CcMetrics.iconBoxSize / 2, lock.y + CcMetrics.iconBoxSize / 2);
|
||||
const dropPoint = Qt.point(cardTile.identityStart + cardTile.identityWidth * 0.25, cardTile.height / 2);
|
||||
mousePress(cardTile, lockCenter.x, lockCenter.y);
|
||||
for (let i = 1; i <= 8; i++)
|
||||
mouseMove(cardTile, lockCenter.x + (dropPoint.x - lockCenter.x) * i / 8, lockCenter.y + (dropPoint.y - lockCenter.y) * i / 8);
|
||||
check(grid.draggingSourceIndex === -1 && cardTile.draggedAction === "lock", "dragging a card button reorders it instead of moving the card");
|
||||
mouseRelease(cardTile, dropPoint.x, dropPoint.y);
|
||||
check(JSON.stringify(SettingsData.controlCenterWidgets[0].actions) === JSON.stringify(["edit", "lock", "user"]), "a mouse drop saves the new order " + JSON.stringify(SettingsData.controlCenterWidgets[0]));
|
||||
const corner = Qt.point(cardTile.width - 2, cardTile.height - 2);
|
||||
mousePress(cardTile, corner.x, corner.y);
|
||||
mouseMove(cardTile, corner.x, corner.y + grid.cellWidth / 2);
|
||||
mouseMove(cardTile, corner.x, corner.y + grid.cellWidth);
|
||||
check(grid.sizePreview?.index === 0 && grid.draggingSourceIndex === -1, "the card's resize corner still works around its buttons");
|
||||
mouseRelease(cardTile, corner.x, corner.y + grid.cellWidth);
|
||||
SettingsData.controlCenterWidgets = [
|
||||
{
|
||||
id: "user",
|
||||
w: 7,
|
||||
h: 1,
|
||||
col: 0,
|
||||
row: 0,
|
||||
actions: ["settings", "user", "lock"]
|
||||
},
|
||||
{
|
||||
id: "wifi",
|
||||
w: 4,
|
||||
h: 1,
|
||||
col: 0,
|
||||
row: 1
|
||||
}
|
||||
];
|
||||
let cardIndex = WidgetUtils.setCardAction(0, "settings", false, 8);
|
||||
cardIndex = WidgetUtils.setCardAction(cardIndex, "lock", false, 8);
|
||||
const returned = SettingsData.controlCenterWidgets;
|
||||
const at = id => grid.slotLayout.slots[SettingsData.controlCenterWidgets.findIndex(widget => widget.id === id)];
|
||||
check(returned[cardIndex].id === "user" && returned[cardIndex].w === 5 && at("user").col === 1 && at("settings").col === 0 && at("lock").col === 6 && at("settings").row === 0 && at("lock").row === 0 && at("wifi").row === 1 && returned.every(widget => widget.id === "user" || widget.id === "wifi" || widget.small), "buttons taken off a placed card return as small tiles where they sat " + JSON.stringify(returned));
|
||||
SettingsData.controlCenterWidgets = [
|
||||
{
|
||||
id: "wifi",
|
||||
w: 2,
|
||||
h: 1
|
||||
},
|
||||
{
|
||||
id: "user",
|
||||
w: 6,
|
||||
h: 1,
|
||||
actions: ["power", "user"]
|
||||
}
|
||||
];
|
||||
cardIndex = WidgetUtils.setCardAction(1, "power", false, 8);
|
||||
check(cardIndex === 2 && SettingsData.controlCenterWidgets[1].id === "power" && at("power").col === 2 && at("user").col === 3 && at("user").row === 0, "a button taken off an unplaced card packs next to it and the card index follows " + JSON.stringify(SettingsData.controlCenterWidgets));
|
||||
check(WidgetUtils.setCardAction(cardIndex, "power", true, 8) === 1, "adding a tile before the card to it reports the card's new index");
|
||||
grid.dockEdit = true;
|
||||
SettingsData.controlCenterWidgets = [
|
||||
{
|
||||
id: "wifi",
|
||||
w: 8,
|
||||
h: 1
|
||||
},
|
||||
{
|
||||
id: "runningApps",
|
||||
w: 7,
|
||||
h: 1,
|
||||
small: true
|
||||
}
|
||||
];
|
||||
const dock = grid.editDock;
|
||||
check(dock !== null && Math.abs(dock.x - grid.cellWidth * 7) < 0.5 && Math.abs(dock.y - grid.cellWidth) < 0.5, "the edit button joins a last row with a free trailing cell " + JSON.stringify(dock));
|
||||
SettingsData.controlCenterWidgets = [
|
||||
{
|
||||
id: "wifi",
|
||||
w: 4,
|
||||
h: 1
|
||||
},
|
||||
{
|
||||
id: "runningApps",
|
||||
w: 8,
|
||||
h: 1
|
||||
}
|
||||
];
|
||||
check(grid.editDock === null, "a full last row keeps the edit button in the footer");
|
||||
grid.dockEdit = false;
|
||||
grid.destroy();
|
||||
wait(0);
|
||||
const userTile = registry.componentForWidget({
|
||||
id: "user"
|
||||
}).createObject(scene);
|
||||
size(userTile, 6, 2);
|
||||
userTile.widgetData = {
|
||||
id: "user",
|
||||
actions: ["user", "lock", "power"],
|
||||
background: true
|
||||
};
|
||||
const cardRadius = userTile.bodyRadius;
|
||||
userTile.widgetData = {
|
||||
id: "user",
|
||||
actions: ["user", "lock", "power"]
|
||||
};
|
||||
check(userTile.bodyRadius === cardRadius && cardRadius < userTile.height / 2, "a tall card with buttons keeps its card outline without a background");
|
||||
userTile.host = userHost;
|
||||
const avatarButton = named(userTile, "userAvatarButton");
|
||||
const closeButton = named(userTile, "userCloseButton");
|
||||
const shownAvatar = item => {
|
||||
if (item !== userTile && item.visible && (item.imageSource !== undefined || item.shape !== undefined))
|
||||
return item;
|
||||
for (const child of item.children || []) {
|
||||
const found = shownAvatar(child);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
for (const layout of [[5, 1, {}], [6, 2, {
|
||||
background: true,
|
||||
actions: ["settings", "user", "lock"]
|
||||
}], [1, 1, {}], [3, 3, {
|
||||
background: true
|
||||
}]]) {
|
||||
size(userTile, layout[0], layout[1]);
|
||||
userTile.widgetData = Object.assign({
|
||||
id: "user"
|
||||
}, layout[2]);
|
||||
settle();
|
||||
const shown = shownAvatar(userTile);
|
||||
const at = shown.mapToItem(userTile, 0, 0);
|
||||
const hit = avatarButton.mapToItem(userTile, 0, 0);
|
||||
check(Math.abs(at.x - hit.x) < 1 && Math.abs(at.y - hit.y) < 1 && Math.abs(shown.width - avatarButton.width) < 1, "the avatar's click area covers the picture at " + layout[0] + "x" + layout[1] + " " + JSON.stringify([at.x, at.y, shown.width, hit.x, hit.y, avatarButton.width]));
|
||||
}
|
||||
size(userTile, 5, 1);
|
||||
userTile.widgetData = {
|
||||
id: "user"
|
||||
};
|
||||
settle();
|
||||
const avatarCenter = avatarButton.mapToItem(userTile, avatarButton.width / 2, avatarButton.height / 2);
|
||||
mouseMove(userTile, avatarCenter.x, avatarCenter.y);
|
||||
check(avatarButton.hovered && !closeButton.hovered, "hovering the avatar lights only the avatar");
|
||||
mouseClick(userTile, avatarCenter.x, avatarCenter.y);
|
||||
check(userHost.accounts === 1 && userHost.closes === 0, "clicking the avatar opens accounts without closing through the card");
|
||||
mouseClick(userTile, userTile.width - Theme.spacingL, userTile.height / 2);
|
||||
check(userHost.accounts === 1 && userHost.closes === 1, "clicking the rest of the card only closes");
|
||||
userTile.destroy();
|
||||
wait(0);
|
||||
const tile = tileComponent.createObject(scene);
|
||||
size(tile, 4, 1);
|
||||
tile.small = true;
|
||||
settle();
|
||||
const smallBox = named(tile, "tileIconBox");
|
||||
check(smallBox.height <= tile.bodyHeight - tile.tilePadding * 2 && named(tile, "tileLabels").height <= tile.bodyHeight, "a half-height tile keeps its icon and labels inside its body");
|
||||
tile.small = false;
|
||||
size(tile, 4, 3);
|
||||
settle();
|
||||
check(tile.expanded && tile.expandedItem !== null, "large tile exposes inline actions");
|
||||
|
||||
@@ -30,3 +30,10 @@ test("header entries become a user row and lock, power and settings tiles", () =
|
||||
const released = store.migrateToVersion({ configVersion: 18, controlCenterWidgets: [{ id: "wifi", width: 50 }] }, 35);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(released.controlCenterWidgets.map(w => [w.id, w.w, w.h]))), [["user", 5, 1], ["settings", 1, 1], ["lock", 1, 1], ["power", 1, 1], ["wifi", 4, 1]]);
|
||||
});
|
||||
|
||||
test("layouts saved before the running apps tile get one appended", () => {
|
||||
const migrated = store.migrateToVersion({ configVersion: 35, controlCenterColumns: 6, controlCenterWidgets: [{ id: "wifi", w: 4, h: 1 }] }, 36);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(migrated.controlCenterWidgets)), [{ id: "wifi", w: 4, h: 1 }, { id: "runningApps", enabled: true, w: 5, h: 1, small: true }]);
|
||||
const kept = store.migrateToVersion({ configVersion: 35, controlCenterWidgets: [{ id: "runningApps", w: 2, h: 1, col: 0, row: 3 }] }, 36);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(kept.controlCenterWidgets)), [{ id: "runningApps", w: 2, h: 1, col: 0, row: 3 }]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user