cc: redesign 7 with a persistent footer and individually tiled buttons

This commit is contained in:
bbedward
2026-09-29 14:52:40 -04:00
parent 2fb0cfee60
commit aced05157b
32 changed files with 1137 additions and 718 deletions
Generated
+3 -3
View File
@@ -3,11 +3,11 @@
"dank-qml-common": {
"flake": false,
"locked": {
"lastModified": 1790691843,
"narHash": "sha256-ZoqLNYx8vgjFV18mI3l8vvkG9SufqZD2qr7q/j+aCns=",
"lastModified": 1790707225,
"narHash": "sha256-EIM8uHjiGEhXQnwWkpCLPsM3khAVLOq8amaWVLlvkLg=",
"owner": "AvengeMedia",
"repo": "dank-qml-common",
"rev": "7092ee89ba7bab4afe74a215f31b999ae1546b9f",
"rev": "4bd5171013a256ebf66b7720564a58411172843c",
"type": "github"
},
"original": {
+1 -1
View File
@@ -21,7 +21,7 @@ Singleton {
id: root
readonly property var log: Log.scoped("SettingsData")
readonly property int settingsConfigVersion: 34
readonly property int settingsConfigVersion: 35
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
+45 -9
View File
@@ -434,25 +434,37 @@ var LOCAL_SPEC = {
controlCenterWidgets: {
def: [
{
id: "userCard",
id: "user",
enabled: true,
w: 6.5,
h: 1.5
w: 5,
h: 1
},
{
id: "quickActions",
id: "settings",
enabled: true,
w: 1.5,
h: 1.5
w: 1,
h: 1
},
{
id: "volumeSlider",
id: "lock",
enabled: true,
w: 1,
h: 1
},
{
id: "power",
enabled: true,
w: 1,
h: 1
},
{
id: "brightnessSlider",
enabled: true,
w: 4,
h: 1
},
{
id: "brightnessSlider",
id: "volumeSlider",
enabled: true,
w: 4,
h: 1
@@ -481,14 +493,38 @@ var LOCAL_SPEC = {
w: 4,
h: 1
},
{
id: "darkMode",
enabled: true,
w: 2,
h: 1
},
{
id: "nightMode",
enabled: true,
w: 2,
h: 1
},
{
id: "doNotDisturb",
enabled: true,
w: 2,
h: 1
},
{
id: "idleInhibitor",
enabled: true,
w: 2,
h: 1
},
{
id: "battery",
enabled: true,
w: 4,
h: 1
},
{
id: "darkMode",
id: "diskUsage",
enabled: true,
w: 4,
h: 1
+29 -65
View File
@@ -15,8 +15,9 @@ 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_LEGACY_HEADER_IDS = ["userCard", "quickActions"];
var CC_HEADER_OPTION_KEYS = ["hostname", "compositor", "uptime", "badge", "actions", "background", "powerAccent"];
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 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"];
@@ -700,79 +701,42 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 31;
}
if (currentVersion < 33 && targetVersion >= 33) {
if (currentVersion < 35 && targetVersion >= 35) {
if (Array.isArray(settings.controlCenterWidgets))
settings.controlCenterWidgets = migrateControlCenterHeader(settings.controlCenterWidgets);
settings.configVersion = 33;
}
if (currentVersion < 34 && targetVersion >= 34) {
if (Array.isArray(settings.controlCenterWidgets))
settings.controlCenterWidgets = splitControlCenterHeader(settings.controlCenterWidgets, settings.controlCenterColumns ?? SpecModule.SPEC.controlCenterColumns.def);
settings.configVersion = 34;
settings.controlCenterWidgets = migrateControlCenterHeader(settings.controlCenterWidgets, currentVersion < 33, settings.controlCenterColumns ?? 8);
settings.configVersion = 35;
}
return settings;
}
function migrateControlCenterHeader(widgets) {
const rest = widgets.filter(widget => !CC_LEGACY_HEADER_IDS.includes(widget?.id));
if (rest.some(widget => widget?.id === "header"))
return rest;
const header = {
id: "header",
function migrateControlCenterHeader(widgets, fixedHeader, columns) {
const header = widgets.filter(widget => CC_HEADER_IDS.includes(widget?.id));
if (!fixedHeader && header.length === 0)
return widgets;
const rest = widgets.filter(widget => !CC_HEADER_IDS.includes(widget?.id));
const actions = header.find(widget => Array.isArray(widget?.actions))?.actions ?? [];
const wanted = id => !rest.some(widget => widget?.id === id) && actions.find(action => action?.id === id)?.enabled !== false;
const tiles = CC_ACTION_TILE_IDS.filter(wanted).map(id => ({
id: id,
enabled: true,
w: 1,
h: 1
}));
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"))
return tiles.concat(rest);
const user = {
id: "user",
enabled: true,
w: 8,
w: Math.max(1, columns - tiles.length),
h: 1
};
for (const legacy of widgets) {
if (!CC_LEGACY_HEADER_IDS.includes(legacy?.id))
continue;
for (const key of CC_HEADER_OPTION_KEYS) {
if (key in legacy)
header[key] = legacy[key];
}
for (const key of CC_USER_KEYS) {
if (key in identity)
user[key] = identity[key];
}
return [header].concat(rest);
}
function splitControlCenterHeader(widgets, columns) {
const userDefaults = SpecModule.SPEC.controlCenterWidgets.def.find(widget => widget.id === "userCard");
const actionDefaults = SpecModule.SPEC.controlCenterWidgets.def.find(widget => widget.id === "quickActions");
const result = [];
for (const widget of widgets) {
if (widget?.id !== "header") {
result.push(widget);
continue;
}
const actions = Object.assign({}, actionDefaults);
const user = Object.assign({}, userDefaults, {
w: Math.max(1, Math.min(Number(widget.w) || columns, columns) - actions.w),
h: Math.max(userDefaults.h, Number(widget.h) || userDefaults.h)
});
for (const key of ["hostname", "compositor", "uptime", "badge", "background"]) {
if (key in widget)
user[key] = widget[key];
}
for (const key of ["actions", "powerAccent"]) {
if (key in widget)
actions[key] = widget[key];
}
if (Number.isFinite(widget.row)) {
user.row = widget.row;
actions.row = widget.row;
}
if (Number.isFinite(widget.col)) {
user.col = widget.col;
actions.col = widget.col + (widget.showUser === false ? 0 : user.w);
}
if (widget.showUser !== false)
result.push(user);
result.push(actions);
}
if (!result.some(widget => widget?.id === "quickActions"))
result.unshift(Object.assign({}, actionDefaults));
return result;
return [user].concat(tiles, rest);
}
function migrateBarWidgetGlobals(settings) {
+1 -1
View File
@@ -246,7 +246,7 @@ Item {
osdStartupTimer.start();
if (SettingsData.controlCenterWidgets.some(widget => widget.id === "diskUsage" && widget.enabled !== false))
DgopService.initializeDiskMounts();
if (SettingsData.controlCenterWidgets.some(widget => widget.id === "userCard" && widget.enabled !== false && widget.uptime !== false))
if (SettingsData.controlCenterWidgets.some(widget => widget.id === "user" && widget.enabled !== false && widget.uptime !== false))
DgopService.dgopAvailable;
// These are dummy references just to trigger the singletons onCompleted to trigger
+9 -16
View File
@@ -14,7 +14,6 @@ Singleton {
readonly property real gridStep: 0.5
property int columnPreview: 0
readonly property int gridColumns: columnPreview > 0 ? columnPreview : clampColumns(SettingsData.controlCenterColumns)
readonly property real sheetWidth: sheetWidthFor(gridColumns)
function clampColumns(value) {
const columns = Math.round(Number(value));
@@ -45,9 +44,7 @@ Singleton {
readonly property real sliderRowHeight: Theme.minimumTouchTargetSize
readonly property real stripTrackHeight: 32
readonly property real stripHandleHeight: stripTrackHeight + Theme.sliderHandleGap * 2
readonly property real stripIconSize: Theme.iconSize
readonly property real gridGap: Theme.spacingS
readonly property real tilePaddingH: Theme.spacingL
readonly property real iconScale: SettingsData.controlCenterIconScale
readonly property real tileIconSize: Theme.iconSizeLarge * iconScale
readonly property real iconBoxSize: Theme.minimumTouchTargetSize * iconScale
@@ -57,19 +54,11 @@ Singleton {
readonly property real iconBoxIconSize: Theme.iconSize * iconScale
readonly property real tallMeterThickness: 28
readonly property real tileTextGap: Theme.spacingM
readonly property real actionSize: Theme.minimumTouchTargetSize
readonly property real actionIconSize: Theme.iconSizeMedium
readonly property real actionGap: Theme.spacingXS
readonly property real headerHeight: actionSize * 2 + actionGap
function actionSpan(count) {
return Math.ceil((actionSize * count + actionGap * Math.max(0, count - 1) + gridGap) / (columnWidth + gridGap) / gridStep) * gridStep;
}
function actionCapacity(span) {
const size = span * (columnWidth + gridGap) - gridGap;
return Math.max(1, Math.floor((size + actionGap) / (actionSize + actionGap)));
}
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 shapeButtonSize: Theme.iconSize
readonly property real detailDialogInset: Theme.spacingL
readonly property real detailDialogPadding: Theme.spacingS
@@ -98,6 +87,9 @@ Singleton {
readonly property real dialogWidth: 320
readonly property real libraryPanelWidth: 400
readonly property real libraryPanelHeight: 400
readonly property real widgetSheetMinHeight: libraryPanelHeight
readonly property real previewMinWidth: columnWidth * 1.5
readonly property real previewTileHeight: Theme.minimumTouchTargetSize
readonly property real configMenuWidth: 260
readonly property real vpnPopoutListHeight: 200
readonly property real headerDropdownWidth: 120
@@ -144,6 +136,7 @@ Singleton {
case "audioOutput":
case "audioInput":
case "diskUsage":
case "runningApps":
return detailHeightList;
default:
return detailHeightDefault;
@@ -22,10 +22,12 @@ FocusScope {
property real minimumContentHeight: 0
property vector4d cornerRadii: Qt.vector4d(Theme.windowRadius, Theme.windowRadius, Theme.windowRadius, Theme.windowRadius)
property real coverage: 0
property var runningToplevels: []
signal dismissed
signal backRequested
signal collapseRequested
signal closeRequested
signal codecSelectorRequested(var device)
signal portSelectorRequested(var node)
@@ -119,6 +121,8 @@ FocusScope {
return diskUsageComponent;
case "brightnessSlider":
return brightnessComponent;
case "runningApps":
return runningAppsComponent;
}
if (sectionId.startsWith("builtin_") || sectionId.startsWith("plugin_"))
return pluginComponent;
@@ -338,6 +342,10 @@ FocusScope {
function onDismissRequested() {
root.backRequested();
}
function onCloseRequested() {
root.closeRequested();
}
}
Component {
@@ -399,6 +407,13 @@ FocusScope {
}
}
Component {
id: runningAppsComponent
RunningAppsDetail {
toplevels: root.runningToplevels
}
}
Component {
id: pluginComponent
PluginDetailShell {
@@ -12,7 +12,6 @@ DankGridEditChrome {
signal configRequested(var anchor)
hasOptions: WidgetUtils.hasOptions(widgetData.id)
removable: !WidgetUtils.isPinned(widgetData.id)
buttonSize: Theme.iconSize
hitOverflow: CcMetrics.gridGap / 2
iconSize: PopoutMetrics.chromeIconSize
@@ -0,0 +1,213 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.Widgets
import qs.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property bool editMode: false
property var toplevels: []
property bool resetArmed: false
signal runningAppsRequested
signal addWidgetRequested
signal resetRequested
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)
implicitHeight: CcMetrics.footerHeight
function cancelConfirmation() {
resetArmed = false;
}
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: Theme.surfaceContainerHighest
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
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
visible: root.editMode
DankButton {
id: addButton
buttonHeight: CcMetrics.footerHeight
maximumWidth: root.leadingWidth
iconName: "add"
text: I18n.tr("Add widget")
backgroundColor: Theme.secondaryContainer
textColor: Theme.onSecondaryContainer
onClicked: {
root.cancelConfirmation();
root.addWidgetRequested();
}
}
DankButton {
buttonHeight: CcMetrics.footerHeight
maximumWidth: Math.max(0, root.leadingWidth - addButton.width - editActions.spacing)
readonly property string label: I18n.tr("Defaults", "noun, control center edit button restoring the default layout")
readonly property string confirmLabel: I18n.tr("Confirm")
iconName: "settings_backup_restore"
text: root.resetArmed ? confirmLabel : label
reserveText: root.resetArmed ? label : confirmLabel
backgroundColor: root.resetArmed ? Theme.primary : Theme.surfaceContainerHighest
textColor: root.resetArmed ? Theme.onPrimary : Theme.onSurface
onClicked: {
if (!root.resetArmed) {
root.resetArmed = true;
return;
}
root.resetArmed = false;
root.resetRequested();
}
}
}
Row {
id: trailing
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
buttonSize: CcMetrics.footerHeight
iconName: "close"
iconSize: CcMetrics.iconBoxIconSize
iconColor: CcMetrics.tileInactiveContent
backgroundColor: CcMetrics.tileInactiveColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
tooltipText: I18n.tr("Discard")
visible: root.editMode
onClicked: root.cancelRequested()
}
DankActionButton {
buttonSize: CcMetrics.footerHeight
iconName: "check"
iconSize: CcMetrics.iconBoxIconSize
iconColor: Theme.onPrimary
backgroundColor: Theme.primary
tooltipText: I18n.tr("Save")
visible: root.editMode
onClicked: root.editToggled()
}
DankActionButton {
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")
visible: !root.editMode
onClicked: root.editToggled()
}
}
}
@@ -24,24 +24,54 @@ DankEditableGrid {
signal lockRequested
signal powerRequested
signal settingsRequested
signal editRequested
signal headerTapped
signal closeRequested
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)))
readonly property var savedWidgets: SettingsData.controlCenterWidgets || []
readonly property var shownIndices: savedWidgets.reduce((indices, widget, i) => WidgetUtils.isShown(widget) ? indices.concat([i]) : indices, [])
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)
onLayoutCommitted: items => model.setLayout(items)
onLayoutCommitted: items => model.setLayout(withHidden(items))
function sizeWithHiddenTwin(index) {
const widget = savedWidgets[index];
const size = WidgetUtils.clampSize(widget, Infinity);
if (!WidgetUtils.isUnplaced(widget))
return size;
for (const neighbor of [index + 1, index - 1]) {
const twin = savedWidgets[neighbor];
if (!twin || shownIndices.includes(neighbor) || !WidgetUtils.isUnplaced(twin))
continue;
const twinSize = WidgetUtils.clampSize(twin, Infinity);
if (twinSize.w !== size.w || twinSize.h !== size.h)
continue;
size.w += twinSize.w;
return size;
}
return size;
}
function savedIndex(index) {
return shownIndices[index] ?? -1;
}
function withHidden(items) {
const widgets = savedWidgets.slice();
shownIndices.forEach((saved, i) => widgets[saved] = items[i]);
return widgets;
}
function displayedItems() {
return GridUtils.placedItems(sourceItems.map(widget => Object.assign({}, widget, {
return withHidden(GridUtils.placedItems(sourceItems.map(widget => Object.assign({}, widget, {
"w": WidgetUtils.clampSize(widget, columns).w
})), slotLayout.slots);
})), slotLayout.slots));
}
Repeater {
@@ -20,7 +20,7 @@ DankEditableGridSlot {
onPressAndHold: {
if (!editChrome.hasOptions)
return;
root.grid.configRequested(root.index, root.widgetData, editChrome);
root.grid.configRequested(root.grid.savedIndex(root.index), root.widgetData, editChrome);
}
onResizeRequested: (requestedWidth, requestedHeight) => {
@@ -34,17 +34,6 @@ DankEditableGridSlot {
else
width = Math.min(2, sizeSpec.maxW);
}
if (widgetData.id === "quickActions") {
const count = WidgetUtils.enabledQuickActions(widgetData).length;
if (height !== current.h)
width = Math.max(width, CcMetrics.actionSpan(Math.ceil(count / CcMetrics.actionCapacity(height))));
const size = WidgetUtils.clampSize(Object.assign({}, widgetData, {
w: width,
h: height
}), grid.columns, grid.maximumRows);
width = size.w;
height = size.h;
}
const changes = {};
if (width !== current.w)
changes.w = width;
@@ -124,6 +113,10 @@ DankEditableGridSlot {
return;
root.grid.expandClicked(root.widgetData);
}
function onOptionChanged(key, value) {
root.grid.model?.setOption(root.grid.savedIndex(root.index), key, value);
}
}
CcEditChrome {
@@ -135,7 +128,7 @@ DankEditableGridSlot {
visible: root.grid.editMode
enabled: root.interactionEnabled
widgetData: root.widgetData
resizeEdgeWidth: root.widgetData.id === "quickActions" ? Theme.spacingL : -1
passthrough: root.passthrough
dragging: root.dragging
resizing: root.resizing
cornerRadius: root.tileItem?.bodyRadius ?? Theme.fullRadius(root.width, root.height)
@@ -144,7 +137,7 @@ DankEditableGridSlot {
onResizeMoved: (px, py) => root.resizeTo(px, py)
onResizeEnded: root.finishResize()
onResizeCanceled: root.cancelResize()
onRemoveRequested: root.grid.removeWidget(root.index)
onConfigRequested: anchor => root.grid.configRequested(root.index, root.widgetData, anchor)
onRemoveRequested: root.grid.removeWidget(root.grid.savedIndex(root.index))
onConfigRequested: anchor => root.grid.configRequested(root.grid.savedIndex(root.index), root.widgetData, anchor)
}
}
@@ -0,0 +1,192 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Modules.ControlCenter
import qs.Widgets
import "../utils/widgets.js" as WidgetUtils
DankBottomSheet {
id: root
property var widgets: []
readonly property string query: searchField.text.trim()
readonly property var matches: WidgetUtils.filterWidgets(widgets, query)
readonly property var categories: [
{
"id": "user",
"title": I18n.tr("User")
},
{
"id": "network",
"title": I18n.tr("Network")
},
{
"id": "audio",
"title": I18n.tr("Audio")
},
{
"id": "display",
"title": I18n.tr("Display")
},
{
"id": "system",
"title": I18n.tr("System")
},
{
"id": "plugins",
"title": I18n.tr("Plugins")
}
]
readonly property var shownCategories: categories.filter(category => widgets.some(widget => (widget.category ?? "plugins") === category.id))
signal chosen(string widgetId)
signal clearRequested
title: I18n.tr("Widgets")
initialFocusItem: searchField
onOpenedChanged: {
if (opened)
searchField.text = "";
}
headerActions: [
DankActionButton {
buttonSize: Theme.buttonHeightS
iconName: "clear_all"
iconColor: Theme.error
tooltipText: I18n.tr("Clear All")
onClicked: root.clearRequested()
}
]
component PreviewGrid: Grid {
id: grid
property var entries: []
signal chosen(string widgetId)
columns: Math.max(1, Math.floor((width + spacing) / (CcMetrics.previewMinWidth + spacing)))
spacing: Theme.spacingS
Repeater {
model: grid.entries
StyledButton {
id: preview
required property var modelData
readonly property bool isUser: modelData.id === "user"
readonly property bool available: modelData.enabled !== false
width: (grid.width - grid.spacing * (grid.columns - 1)) / grid.columns
height: CcMetrics.previewTileHeight + Theme.spacingXS + label.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadiusL
Accessible.name: modelData.text
Accessible.description: modelData.warning ?? modelData.description ?? ""
onClicked: {
if (!available)
return;
grid.chosen(modelData.id);
}
Rectangle {
id: miniTile
anchors.horizontalCenter: parent.horizontalCenter
y: Theme.spacingS
width: preview.isUser ? height : parent.width - Theme.spacingS * 2
height: CcMetrics.previewTileHeight
radius: Theme.fullRadius(width, height)
color: preview.available ? CcMetrics.tileInactiveColor : Theme.onSurface_12
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
DankIcon {
anchors.centerIn: parent
name: preview.modelData.icon
size: CcMetrics.iconBoxIconSize
color: preview.available ? CcMetrics.tileInactiveIcon : Theme.onSurface_38
}
}
StyledText {
id: label
anchors.top: miniTile.bottom
anchors.topMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - Theme.spacingXS * 2
text: preview.modelData.text
font.pixelSize: Theme.fontSizeSmall
color: preview.available ? Theme.surfaceText : Theme.onSurface_38
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
}
FocusRing {
visible: preview.visualFocus
}
StateLayer {
control: preview
tooltipText: preview.modelData.warning ?? ""
}
}
}
}
DankSearchField {
id: searchField
width: parent.width
height: Theme.fieldHeightLarge
placeholderText: I18n.tr("Search widgets...")
onAccepted: {
const first = root.matches.find(widget => widget.enabled !== false);
if (first)
root.chosen(first.id);
}
}
Repeater {
model: root.query === "" ? root.shownCategories : []
DankCollapsibleSection {
id: section
required property var modelData
required property int index
width: parent?.width ?? 0
title: modelData.title
expanded: index === 0
PreviewGrid {
Layout.fillWidth: true
entries: root.widgets.filter(widget => (widget.category ?? "plugins") === section.modelData.id)
onChosen: widgetId => root.chosen(widgetId)
}
}
}
PreviewGrid {
width: parent.width
visible: root.query !== ""
entries: root.query !== "" ? root.matches : []
onChosen: widgetId => root.chosen(widgetId)
}
StyledText {
width: parent.width
visible: root.matches.length === 0
topPadding: Theme.spacingL
bottomPadding: Theme.spacingL
text: I18n.tr("No widgets available")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
}
}
@@ -1,165 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.Widgets
import qs.Widgets
Row {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var availableWidgets: []
property var popupScreen: null
property real popoutX: 0
property real popoutY: 0
property real popoutWidth: 0
property real popoutHeight: 0
signal addWidget(string widgetId)
signal resetToDefault
signal clearAll
signal finishRequested
signal cancelRequested
readonly property real buttonWidth: (width - spacing * 4 - Theme.buttonHeightS * 2) / 3
height: Theme.buttonHeightS
spacing: Theme.spacingS
function openWidgetLibrary() {
libraryLoader.active = true;
const window = libraryLoader.item;
if (!window)
return;
if (popupScreen)
window.screen = popupScreen;
window.visible = true;
}
function closeWidgetLibrary() {
if (!libraryLoader.item)
return;
libraryLoader.item.visible = false;
}
onAddWidget: closeWidgetLibrary()
onVisibleChanged: {
if (visible)
return;
closeWidgetLibrary();
libraryLoader.active = false;
}
LazyLoader {
id: libraryLoader
active: false
PanelWindow {
id: addWidgetWindow
screen: root.popupScreen
visible: false
color: "transparent"
WlrLayershell.namespace: "dms:control-center-widget-library"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: PopoutManager.screenshotActive ? WlrKeyboardFocus.None : (visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None)
anchors {
top: true
left: true
right: true
bottom: true
}
WindowBlur {
targetWindow: addWidgetWindow
surfaceColor: widgetLibraryPanel.color
blurX: widgetLibraryPanel.x
blurY: widgetLibraryPanel.y
blurWidth: addWidgetWindow.visible ? widgetLibraryPanel.width : 0
blurHeight: addWidgetWindow.visible ? widgetLibraryPanel.height : 0
blurRadius: Theme.windowRadius
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.closeWidgetLibrary()
}
CcWidgetLibrary {
id: widgetLibraryPanel
width: Math.min(implicitWidth, addWidgetWindow.width - Theme.spacingL * 2)
height: Math.min(implicitHeight, addWidgetWindow.height - Theme.spacingL * 2)
x: Math.round(Math.max(Theme.spacingL, Math.min(addWidgetWindow.width - width - Theme.spacingL, root.popoutWidth > 0 ? root.popoutX + (root.popoutWidth - width) / 2 : (addWidgetWindow.width - width) / 2)))
y: Math.round(Math.max(Theme.spacingL, Math.min(addWidgetWindow.height - height - Theme.spacingL, root.popoutHeight > 0 ? root.popoutY + (root.popoutHeight - height) / 2 : (addWidgetWindow.height - height) / 2)))
widgets: root.availableWidgets
onChosen: widgetId => root.addWidget(widgetId)
onDismissed: root.closeWidgetLibrary()
}
onVisibleChanged: {
if (visible)
widgetLibraryPanel.reset();
}
}
}
DankButton {
width: root.buttonWidth
buttonHeight: Theme.buttonHeightS
iconName: "add"
text: I18n.tr("Add widget")
backgroundColor: Theme.secondaryContainer
textColor: Theme.onSecondaryContainer
onClicked: root.openWidgetLibrary()
}
DankButton {
width: root.buttonWidth
buttonHeight: Theme.buttonHeightS
iconName: "settings_backup_restore"
text: I18n.tr("Defaults", "noun, control center edit button restoring the default layout")
backgroundColor: Theme.chipSurface
textColor: Theme.surfaceText
onClicked: root.resetToDefault()
}
DankButton {
width: root.buttonWidth
buttonHeight: Theme.buttonHeightS
iconName: "clear_all"
text: I18n.tr("Reset")
backgroundColor: Theme.errorHover
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()
}
}
@@ -7,7 +7,6 @@ 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
@@ -29,9 +28,7 @@ Item {
readonly property bool isPlugin: widgetId.startsWith("plugin_")
readonly property bool isDisk: widgetId === "diskUsage"
readonly property bool isIdleInhibitor: widgetId === "idleInhibitor"
readonly property bool isUser: widgetId === "userCard"
readonly property bool isQuickActions: widgetId === "quickActions"
readonly property var quickActions: isQuickActions ? WidgetUtils.quickActions(widgetData) : []
readonly property bool isUser: widgetId === "user"
visible: widgetIndex >= 0 || contextMenu.renderActive
@@ -67,20 +64,8 @@ Item {
contextMenu.hide();
}
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)
return;
widgets[widgetIndex] = Object.assign({}, widgets[widgetIndex], {
[key]: value
});
SettingsData.set("controlCenterWidgets", widgets);
WidgetUtils.setOption(widgetIndex, key, value);
}
DankContextMenu {
@@ -125,58 +110,6 @@ Item {
}
}
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)
}
}
}
CcToggleRow {
visible: root.isUser || root.isQuickActions
text: I18n.tr("Background")
checked: root.isUser ? root.widgetData?.background !== false : root.widgetData?.background === true
onToggled: checked => root.persistOption("background", checked)
}
CcToggleRow {
visible: root.isQuickActions && root.widgetData?.background === true
text: I18n.tr("Button backgrounds")
checked: root.widgetData?.buttonBackgrounds === true
onToggled: checked => root.persistOption("buttonBackgrounds", 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")
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Modules.ControlCenter.Components
import qs.Modules.ControlCenter.Models
import qs.Modules.ControlCenter.Details
@@ -18,10 +19,22 @@ FocusScope {
readonly property bool pageOpen: (host.expandedSection ?? "") !== ""
readonly property real gridHeight: widgetGrid.gridHeight
readonly property real chromeHeight: Theme.spacingS + footer.height
readonly property bool widgetSheetOpen: widgetSheetLoader.item?.active ?? false
readonly property real coveredAmount: Math.max(detailPage.opacity, widgetSheetLoader.item?.progress ?? 0)
property bool widgetSheetRequested: false
property int toplevelRevision: 0
readonly property var runningToplevels: {
toplevelRevision;
const all = CompositorService.sortedToplevels ?? [];
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 real targetImplicitHeight: {
let total = CcMetrics.sheetPadding * 2 + gridHeight;
if (host.editMode)
total += Theme.spacingS + editControls.height;
const total = CcMetrics.sheetPadding * 2 + gridHeight + chromeHeight;
if (widgetSheetOpen)
return Math.max(total, CcMetrics.widgetSheetMinHeight);
if (detailPage.shownSection === "")
return total;
return Math.max(total, detailPage.topInset + detailPage.minimumHeight + CcMetrics.detailDialogInset);
@@ -34,7 +47,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 - Theme.spacingS - editControls.height
readonly property real availableGridHeight: (host.availableHeight ?? (host.triggerScreen?.height ?? CcMetrics.fallbackScreenHeight) - CcMetrics.maxHeightInset) - CcMetrics.sheetPadding * 2 - chromeHeight
readonly property real editGutter: host.editGutter ?? 0
readonly property DankPanelResizer panelResizer: DankPanelResizer {
popout: root.host
@@ -141,7 +154,32 @@ FocusScope {
SettingsData.set("controlCenterColumns", snapshot.columns);
}
function openWidgetSheet() {
const sheet = widgetSheetLoader.item;
if (!sheet) {
widgetSheetRequested = true;
return;
}
sheet.opened = true;
}
function closeWidgetSheet() {
if (!widgetSheetLoader.item)
return;
widgetSheetLoader.item.opened = false;
}
Keys.onEscapePressed: event => {
if (footer.resetArmed) {
footer.cancelConfirmation();
event.accepted = true;
return;
}
if (widgetSheetLoader.item?.opened) {
closeWidgetSheet();
event.accepted = true;
return;
}
if (configOverlayLoader.item?.visible) {
configOverlayLoader.item.close();
event.accepted = true;
@@ -180,8 +218,10 @@ FocusScope {
} else {
panelResizer.cancel();
editSnapshot = null;
widgetSheetRequested = false;
}
forceActiveFocus();
if (!activeFocus)
forceActiveFocus();
}
DankGridEditChrome {
@@ -191,7 +231,7 @@ FocusScope {
anchors.margins: -(contentInset + Theme.spacingS)
z: 1
visible: root.host.editMode
enabled: detailPage.shownSection === ""
enabled: detailPage.shownSection === "" && !root.widgetSheetOpen
edgeResize: root.panelResizing || root.panelResizer.sideMovable(-1, root.gridColumns)
cornerResize: root.panelResizing || root.panelResizer.sideMovable(1, root.gridColumns)
horizontalResize: true
@@ -257,7 +297,7 @@ FocusScope {
width: parent.width
height: root.gridHeight
opacity: CcMetrics.hideCoveredContent ? 1 - detailPage.opacity : 1
opacity: CcMetrics.hideCoveredContent ? 1 - root.coveredAmount : 1
CcTileGrid {
id: widgetGrid
@@ -275,8 +315,7 @@ FocusScope {
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
onCloseRequested: root.host.close()
onSettingsRequested: root.host.openSettings()
onLockRequested: {
root.host.close();
@@ -295,27 +334,17 @@ FocusScope {
}
}
EditControls {
id: editControls
CcFooter {
id: footer
width: parent.width
visible: root.host.editMode
popupScreen: root.host.screen
popoutX: root.host.alignedX
popoutY: root.host.alignedY
popoutWidth: root.host.alignedWidth
popoutHeight: root.host.alignedHeight
availableWidgets: {
if (!root.host.editMode)
return [];
const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id);
const allWidgets = widgetModel.baseWidgetDefinitions.concat(widgetModel.getPluginWidgets());
return allWidgets.filter(w => w.allowMultiple || !existingIds.includes(w.id));
}
onAddWidget: widgetId => widgetModel.addWidget(widgetId)
onResetToDefault: () => widgetModel.resetToDefault()
onClearAll: () => widgetModel.clearAll()
onFinishRequested: root.host.editMode = false
editMode: root.host.editMode
toplevels: root.runningToplevels
opacity: body.opacity
onRunningAppsRequested: root.navigateTo("runningApps")
onAddWidgetRequested: root.openWidgetSheet()
onResetRequested: widgetModel.resetToDefault()
onEditToggled: root.host.editMode = !root.host.editMode
onCancelRequested: root.cancelEdit()
}
}
@@ -337,8 +366,10 @@ FocusScope {
transientSurfaceTracker: root.host.transientSurfaceTracker
onCodecSelectorRequested: device => root.showCodecSelector(device)
onPortSelectorRequested: node => root.showPortSelector(node)
runningToplevels: root.runningToplevels
onBackRequested: root.goBack()
onCollapseRequested: root.host.collapseAll()
onCloseRequested: root.host.close()
onDismissed: {
const target = root.detailReturnFocus;
root.detailReturnFocus = null;
@@ -374,6 +405,47 @@ FocusScope {
}
}
Connections {
target: CompositorService
function onToplevelsChanged() {
root.toplevelRevision++;
}
}
Loader {
id: widgetSheetLoader
anchors.fill: parent
z: CcMetrics.overlayZ
active: root.host.editMode
asynchronous: true
onLoaded: {
if (!root.widgetSheetRequested)
return;
root.widgetSheetRequested = false;
item.opened = true;
}
sourceComponent: CcWidgetSheet {
scrimRadii: root.surfaceCornerRadii
surfaceColor: CcMetrics.dialogColor
widgets: {
const existingIds = root.placedWidgetIds.split(",");
const allWidgets = widgetModel.baseWidgetDefinitions.concat(widgetModel.getPluginWidgets());
return allWidgets.filter(w => w.allowMultiple || !existingIds.includes(w.id));
}
onDismissRequested: opened = false
onChosen: widgetId => {
widgetModel.addWidget(widgetId);
opened = false;
}
onClearRequested: {
widgetModel.clearAll();
opened = false;
}
}
}
Loader {
id: configOverlayLoader
@@ -0,0 +1,81 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.Widgets
import qs.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
readonly property string title: I18n.tr("Running apps")
property var toplevels: []
readonly property int count: toplevels.length
signal closeRequested
signal dismissRequested
onCountChanged: {
if (count === 0)
dismissRequested();
}
DankFlickable {
anchors.fill: parent
contentHeight: column.height
clip: true
Column {
id: column
width: parent.width
spacing: CcMetrics.detailContentGap
CcGroup {
Repeater {
model: ScriptModel {
values: root.toplevels
objectProp: CompositorService.toplevelKey
}
CcListRow {
id: row
required property var modelData
readonly property string appId: modelData?.appId ?? ""
readonly property string appName: Paths.getAppName(appId, DesktopEntries.heuristicLookup(appId))
readonly property var placement: CompositorService.toplevelPlacement(modelData)
readonly property string workspaceLabel: placement.workspace ? I18n.tr("Workspace %1", "control center running app row, %1 is a workspace number or name").arg(placement.workspace) : ""
readonly property string outputLabel: Quickshell.screens.length > 1 ? (SettingsData.getScreenDisplayName(Quickshell.screens.find(screen => screen.name === placement.output)) || placement.output) : ""
title: modelData?.title || appName
subtitle: [modelData?.title ? appName : "", workspaceLabel, outputLabel].filter(text => text !== "").join(" · ")
clickable: true
onClicked: {
modelData?.activate();
root.closeRequested();
}
leading: CcAppIcon {
appId: row.appId
iconSize: Theme.iconSizeLarge
}
DankActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "close"
iconColor: Theme.onSurfaceVariant
Accessible.name: I18n.tr("Close")
onClicked: row.modelData?.close()
}
}
}
}
}
}
}
@@ -104,11 +104,11 @@ QtObject {
readonly property Component colorPickerTile: Component {
ColorPickerTile {}
}
readonly property Component userCard: Component {
UserCard {}
readonly property Component userTile: Component {
UserTile {}
}
readonly property Component quickActions: Component {
QuickActions {}
readonly property Component actionTile: Component {
ActionTile {}
}
readonly property Component pluginTile: Component {
PluginTile {}
@@ -163,10 +163,12 @@ QtObject {
return diskUsageTile;
case "colorPicker":
return colorPickerTile;
case "userCard":
return userCard;
case "quickActions":
return quickActions;
case "user":
return userTile;
case "lock":
case "power":
case "settings":
return actionTile;
default:
return null;
}
@@ -186,11 +188,39 @@ QtObject {
readonly property var baseWidgetDefinitions: [
{
"id": "userCard",
"id": "user",
"text": I18n.tr("User"),
"description": I18n.tr("Show hostname"),
"description": I18n.tr("Profile"),
"icon": "person",
"type": "info",
"category": "user",
"enabled": true
},
{
"id": "settings",
"text": I18n.tr("Settings"),
"description": "",
"icon": "settings",
"type": "action",
"category": "system",
"enabled": true
},
{
"id": "lock",
"text": I18n.tr("Lock"),
"description": I18n.tr("Lock screen"),
"icon": "lock",
"type": "action",
"category": "system",
"enabled": true
},
{
"id": "power",
"text": I18n.tr("Power"),
"description": I18n.tr("Power menu"),
"icon": "power_settings_new",
"type": "action",
"category": "system",
"enabled": true
},
{
@@ -199,6 +229,7 @@ QtObject {
"description": I18n.tr("Blue light filter"),
"icon": "nightlight",
"type": "toggle",
"category": "display",
"enabled": NightModeService.automationAvailable,
"warning": !NightModeService.automationAvailable ? I18n.tr("Requires night mode support") : undefined
},
@@ -208,6 +239,7 @@ QtObject {
"description": I18n.tr("System theme toggle"),
"icon": "contrast",
"type": "toggle",
"category": "display",
"enabled": true
},
{
@@ -216,6 +248,7 @@ QtObject {
"description": I18n.tr("Block notifications"),
"icon": "do_not_disturb_on",
"type": "toggle",
"category": "system",
"enabled": true
},
{
@@ -224,6 +257,7 @@ QtObject {
"description": I18n.tr("Prevent screen timeout"),
"icon": "motion_sensor_active",
"type": "toggle",
"category": "system",
"enabled": true
},
{
@@ -232,6 +266,7 @@ QtObject {
"description": I18n.tr("Wi-Fi and Ethernet connection"),
"icon": "wifi",
"type": "connection",
"category": "network",
"enabled": NetworkService.wifiAvailable,
"warning": !NetworkService.wifiAvailable ? I18n.tr("Wi-Fi not available") : undefined
},
@@ -241,6 +276,7 @@ QtObject {
"description": I18n.tr("Device connections"),
"icon": "bluetooth",
"type": "connection",
"category": "network",
"enabled": BluetoothService.available,
"warning": !BluetoothService.available ? I18n.tr("Bluetooth not available") : undefined
},
@@ -250,6 +286,7 @@ QtObject {
"description": I18n.tr("Speaker settings"),
"icon": "volume_up",
"type": "connection",
"category": "audio",
"enabled": true
},
{
@@ -258,6 +295,7 @@ QtObject {
"description": I18n.tr("Microphone settings"),
"icon": "mic",
"type": "connection",
"category": "audio",
"enabled": true
},
{
@@ -266,6 +304,7 @@ QtObject {
"description": I18n.tr("Audio volume control"),
"icon": "volume_up",
"type": "slider",
"category": "audio",
"enabled": true
},
{
@@ -274,6 +313,7 @@ QtObject {
"description": I18n.tr("Display brightness control"),
"icon": "brightness_6",
"type": "slider",
"category": "display",
"enabled": BrightnessService.brightnessAvailable,
"warning": !BrightnessService.brightnessAvailable ? I18n.tr("Brightness control not available") : undefined,
"allowMultiple": true
@@ -284,15 +324,18 @@ QtObject {
"description": I18n.tr("Microphone volume control"),
"icon": "mic",
"type": "slider",
"category": "audio",
"enabled": true
},
{
"id": "battery",
"text": I18n.tr("Battery"),
"text": BatteryService.batteryAvailable || !PowerProfileWatcher.available ? I18n.tr("Battery") : I18n.tr("Power profile"),
"description": I18n.tr("Battery and power management"),
"icon": "battery_std",
"type": "action",
"enabled": true
"category": "system",
"enabled": BatteryService.batteryAvailable || PowerProfileWatcher.available,
"warning": !BatteryService.batteryAvailable && !PowerProfileWatcher.available ? I18n.tr("No battery") : undefined
},
{
"id": "diskUsage",
@@ -300,6 +343,7 @@ QtObject {
"description": I18n.tr("Filesystem usage monitoring"),
"icon": "storage",
"type": "action",
"category": "system",
"enabled": DgopService.dgopAvailable,
"warning": !DgopService.dgopAvailable ? I18n.tr("Requires 'dgop' tool") : undefined,
"allowMultiple": true
@@ -310,6 +354,7 @@ QtObject {
"description": I18n.tr("Choose colors from palette"),
"icon": "palette",
"type": "action",
"category": "display",
"enabled": true
},
{
@@ -318,6 +363,7 @@ QtObject {
"description": I18n.tr("VPN Connections"),
"icon": "vpn_key",
"type": "builtin_plugin",
"category": "network",
"enabled": DMSNetworkService.available,
"warning": !DMSNetworkService.available ? I18n.tr("VPN not available") : undefined,
"isBuiltinPlugin": true
@@ -328,6 +374,7 @@ QtObject {
"description": I18n.tr("Print Server Management"),
"icon": "Print",
"type": "builtin_plugin",
"category": "system",
"enabled": CupsService.cupsAvailable,
"warning": !CupsService.cupsAvailable ? I18n.tr("CUPS not available") : undefined,
"isBuiltinPlugin": true
@@ -338,6 +385,7 @@ QtObject {
"description": I18n.tr("Tailscale Network", "Tailscale control center widget description"),
"icon": "device_hub",
"type": "builtin_plugin",
"category": "network",
"enabled": TailscaleService.available,
"warning": !TailscaleService.available ? I18n.tr("Tailscale not available", "Warning when Tailscale service is not running") : undefined,
"isBuiltinPlugin": true
@@ -348,6 +396,7 @@ QtObject {
"description": I18n.tr("Switch between display configurations"),
"icon": "monitor",
"type": "builtin_plugin",
"category": "display",
"enabled": true,
"isBuiltinPlugin": true
}
@@ -394,6 +443,7 @@ QtObject {
"description": plugin.description || "",
"icon": plugin.icon || "extension",
"type": "plugin",
"category": "plugins",
"enabled": true,
"isPlugin": true
});
@@ -419,11 +469,15 @@ QtObject {
WidgetUtils.setLayout(widgets);
}
function setOption(index, key, value) {
WidgetUtils.setOption(index, key, value);
}
function resetToDefault() {
WidgetUtils.resetToDefault(columns);
WidgetUtils.resetToDefault();
}
function clearAll() {
WidgetUtils.clearAll(columns);
WidgetUtils.clearAll();
}
}
@@ -0,0 +1,28 @@
import QtQuick
import qs.Common
import qs.Modules.ControlCenter
CcTile {
id: root
readonly property string action: widgetData?.id ?? ""
toggle: false
iconName: widgetDef?.icon ?? ""
title: widgetDef?.text ?? ""
restIconColor: action === "power" ? Theme.error : CcMetrics.tileInactiveIcon
onClicked: {
switch (action) {
case "lock":
host?.lockRequested();
return;
case "power":
host?.powerRequested();
return;
case "settings":
host?.settingsRequested();
return;
}
}
}
@@ -8,10 +8,17 @@ CcTile {
id: root
readonly property bool available: BatteryService.batteryAvailable
readonly property bool profileMode: !available && PowerProfileWatcher.available
iconName: BatteryService.getBatteryIcon()
title: available ? I18n.tr("Battery") : I18n.tr("No battery")
iconName: profileMode ? Theme.getPowerProfileIcon(PowerProfileWatcher.currentProfile) : BatteryService.getBatteryIcon()
title: {
if (profileMode)
return I18n.tr("Power profile");
return available ? I18n.tr("Battery") : I18n.tr("No battery");
}
subtitle: {
if (profileMode)
return Theme.getPowerProfileLabel(PowerProfileWatcher.currentProfile);
if (!available)
return I18n.tr("Not available");
if (BatteryService.isCharging)
@@ -21,7 +28,8 @@ CcTile {
return `${BatteryService.batteryLevel}%`;
}
active: available && (BatteryService.isCharging || BatteryService.isPluggedIn)
opensPage: true
opensPage: !profileMode
showExpand: profileMode
tallContent: Component {
Item {
BatteryMeter {
@@ -35,7 +43,13 @@ CcTile {
}
}
onClicked: expandClicked()
onClicked: {
if (!profileMode) {
expandClicked();
return;
}
PowerProfileWatcher.cycleProfile();
}
expandedContent: Component {
CcTileActions {
actions: PowerProfileWatcher.availableProfiles.map(profile => ({
@@ -0,0 +1,33 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Widgets
Item {
id: root
property string appId: ""
property real iconSize: Theme.iconSize
implicitWidth: iconSize
implicitHeight: iconSize
IconImage {
id: image
anchors.fill: parent
source: root.appId ? Paths.getAppIcon(root.appId, DesktopEntries.heuristicLookup(root.appId)) : ""
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready
}
DankIcon {
anchors.centerIn: parent
name: "apps"
size: root.iconSize
color: Theme.onSurfaceVariant
visible: image.status === Image.Error || image.source.toString() === ""
}
}
@@ -70,7 +70,6 @@ Item {
iconName: root.iconName
iconSize: CcMetrics.iconBoxIconSize
iconColor: CcMetrics.tileInactiveContent
visible: root.tall
enabled: root.sliderEnabled && root.interactive
tooltipText: root.iconTooltip
Accessible.name: root.iconLabel
@@ -110,6 +109,7 @@ Item {
id: trackArea
objectName: "sliderTrackArea"
anchors.left: parent.left
anchors.leftMargin: root.tall ? 0 : action.width + Theme.spacingS
anchors.right: parent.right
y: root.vertical ? (root.showNumber ? labels.height + Theme.spacingS : 0) : root.tall ? action.height + Theme.spacingM : 0
height: Math.max(0, (root.vertical ? action.y - Theme.spacingS : parent.height) - y)
@@ -131,14 +131,8 @@ Item {
return "l";
return "m";
}
insetIcon: root.tall ? "" : root.iconName
insetIconPosition: "end"
insetIconClickable: true
insetIconTooltip: root.iconTooltip
insetIconLabel: root.iconLabel
Accessible.name: root.sliderLabel
showValue: !root.vertical
onInsetIconClicked: root.iconClicked()
onSliderValueChanged: newValue => root.sliderValueChanged(newValue)
Binding on trackHeight {
@@ -152,12 +146,6 @@ Item {
value: CcMetrics.stripHandleHeight
restoreMode: Binding.RestoreBinding
}
Binding on insetIconSize {
when: !root.tall
value: CcMetrics.stripIconSize
restoreMode: Binding.RestoreBinding
}
}
}
}
@@ -35,6 +35,7 @@ Item {
readonly property bool showSubtitle: subtitle !== "" && (!stacked || height - tilePadding * 2 >= iconExtent + Theme.spacingS + titleLabel.implicitHeight + Theme.spacingXXS + subtitleLabel.implicitHeight)
property bool showExpand: false
property bool opensPage: false
property color restIconColor: CcMetrics.tileInactiveIcon
property Component tallContent: null
property bool interactive: true
property bool iconBlinking: false
@@ -73,7 +74,7 @@ Item {
return Theme.onSurface_38;
if (hasIconBox)
return active ? CcMetrics.tileActiveContent : CcMetrics.tileInactiveContent;
return bodyActive ? CcMetrics.tileActiveContent : CcMetrics.tileInactiveIcon;
return bodyActive ? CcMetrics.tileActiveContent : root.restIconColor;
}
readonly property color iconBoxColor: {
if (!enabled)
@@ -2,17 +2,13 @@ import QtQuick
import qs.Common
import qs.Modules.ControlCenter
import qs.Widgets
import "../utils/widgets.js" as WidgetUtils
Rectangle {
id: root
property var widgets: []
readonly property var matches: {
const query = searchField.text.trim().toLowerCase();
if (!query)
return widgets;
return widgets.filter(widget => [widget.text, widget.description, widget.id].some(value => (value || "").toLowerCase().includes(query)));
}
readonly property var matches: WidgetUtils.filterWidgets(widgets, searchField.text)
signal chosen(string widgetId)
signal dismissed
@@ -1,91 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.ControlCenter
import qs.Widgets
import "../utils/widgets.js" as WidgetUtils
Item {
id: root
property var widgetData: ({})
property var widgetDef: null
property var host: null
property bool live: true
property bool interactive: true
property real columns: 1.5
property real rows: 1.5
property bool compact: false
readonly property Item passthrough: actionGrid
readonly property bool editMode: host?.editMode ?? false
readonly property var actions: WidgetUtils.enabledQuickActions(widgetData)
readonly property bool background: widgetData?.background === true
readonly property bool buttonBackgrounds: widgetData?.buttonBackgrounds === true
readonly property real bodyRadius: background ? Theme.cornerRadiusXL : Theme.cornerRadiusL
readonly property int actionColumns: Math.max(1, Math.min(actions.length, Math.floor((width + CcMetrics.actionGap) / (CcMetrics.actionSize + CcMetrics.actionGap))))
width: parent?.width ?? 0
height: CcMetrics.headerHeight
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
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;
}
}
Rectangle {
anchors.fill: parent
radius: root.bodyRadius
color: Theme.foregroundColor(Theme.cardSurface)
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
visible: root.background
}
Grid {
id: actionGrid
anchors.centerIn: parent
columns: root.actionColumns
spacing: CcMetrics.actionGap
Repeater {
model: root.actions
DankActionButton {
required property var modelData
readonly property string actionId: modelData.id
readonly property bool destructive: root.widgetData?.powerAccent === true && actionId === "power"
readonly property bool editing: actionId === "edit" && root.editMode
buttonSize: CcMetrics.actionSize
iconSize: CcMetrics.actionIconSize
circular: root.background
iconName: WidgetUtils.quickActionIcon(actionId)
iconColor: editing ? Theme.onPrimary : destructive ? Theme.error : Theme.onSurface
backgroundColor: editing ? Theme.primary : root.background ? (root.buttonBackgrounds ? Theme.foregroundColor(Theme.chipSurface) : "transparent") : Theme.foregroundColor(Theme.cardSurface)
tooltipText: I18n.tr(WidgetUtils.quickActionLabel(actionId))
checkable: actionId === "edit"
checked: editing
Accessible.checkable: checkable
Accessible.checked: checked
onClicked: root.trigger(actionId)
}
}
}
}
@@ -1,72 +0,0 @@
import QtQuick
import qs.Common
import qs.Modules.ControlCenter
import qs.Modules.DankDash
import qs.Widgets
Item {
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.5
property bool compact: false
readonly property bool tapToClose: interactive && (host?.tapToClose ?? false)
readonly property bool background: widgetData?.background !== false
readonly property real bodyRadius: Theme.cornerRadiusXL
width: parent?.width ?? 0
height: CcMetrics.headerHeight
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
Rectangle {
anchors.fill: parent
radius: root.bodyRadius
color: Theme.foregroundColor(Theme.cardSurface)
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
visible: root.background
}
UserIdentity {
id: identity
anchors.fill: parent
anchors.leftMargin: root.background ? Theme.spacingL : 0
anchors.rightMargin: anchors.leftMargin
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
live: root.live
options: DashRegistry.resolvedOptions("user", root.widgetData)
avatarSize: narrow ? height : Math.min(height - Theme.spacingM, width / 3)
narrow: width < Theme.minimumTouchTargetSize + Theme.spacingM + Theme.buttonHeightM
stacked: root.height >= CcMetrics.headerHeight
contentColor: Theme.onSurface
mutedColor: Theme.onSurfaceVariant
}
StyledButton {
anchors.fill: parent
visible: root.tapToClose
radius: root.bodyRadius
Accessible.name: I18n.tr("Close")
onClicked: root.host?.headerTapped()
FocusRing {
anchors.fill: parent
radius: parent.radius
visible: parent.visualFocus
}
StateLayer {
control: parent
tooltipText: I18n.tr("Close")
}
}
}
@@ -0,0 +1,149 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Modules.ControlCenter
import qs.Modules.DankDash
import qs.Services
import qs.Widgets
import "../utils/widgets.js" as WidgetUtils
Item {
id: root
property var widgetData: ({})
property var widgetDef: null
property var host: null
property bool live: true
property bool interactive: true
property real columns: 2
property real rows: 2
property bool compact: false
signal optionChanged(string key, var value)
readonly property bool wide: width >= height * 2
readonly property real avatarSide: Math.min(width, height)
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 real fallbackGlyphRatio: 0.5
readonly property string avatarSource: {
const image = PortalService.profileImage;
if (image === "")
return "";
return image.startsWith("/") ? "file://" + image : image;
}
readonly property bool hasImage: picture.status === Image.Ready
width: parent?.width ?? 0
height: parent?.height ?? 0
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
Accessible.role: Accessible.StaticText
Accessible.name: UserInfoService.fullName || UserInfoService.username
UserIdentity {
anchors.fill: parent
visible: root.wide
live: root.live && root.wide
options: DashRegistry.resolvedOptions("user", root.widgetData)
avatarSize: root.height
stacked: root.rows >= 2
contentColor: Theme.onSurface
mutedColor: Theme.onSurfaceVariant
}
Item {
id: avatar
anchors.centerIn: parent
width: root.avatarSide
height: root.avatarSide
visible: !root.wide
DankMaterialShape {
anchors.fill: parent
shape: root.shape
color: Theme.primaryContainer
visible: !root.hasImage
}
DankIcon {
anchors.centerIn: parent
name: "person"
size: Math.round(root.avatarSide * root.fallbackGlyphRatio)
color: Theme.onPrimaryContainer
filled: true
visible: !root.hasImage
}
Item {
id: avatarMask
anchors.fill: parent
layer.enabled: root.hasImage
visible: false
DankMaterialShape {
anchors.fill: parent
shape: root.shape
}
}
Image {
id: picture
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
retainWhileLoading: true
visible: root.hasImage
sourceSize.width: Math.max(1, Math.ceil(width * Screen.devicePixelRatio))
sourceSize.height: Math.max(1, Math.ceil(height * Screen.devicePixelRatio))
source: avatar.visible ? root.avatarSource : ""
layer.enabled: root.hasImage
layer.effect: MultiEffect {
maskEnabled: true
maskSource: avatarMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
}
StyledButton {
anchors.fill: parent
visible: root.tapToClose
radius: root.bodyRadius
Accessible.name: I18n.tr("Close")
onClicked: root.host?.closeRequested()
FocusRing {
visible: parent.visualFocus
}
StateLayer {
control: parent
cornerRadius: root.bodyRadius
}
}
DankActionButton {
id: shapeButton
anchors.bottom: avatar.bottom
anchors.left: avatar.left
buttonSize: CcMetrics.shapeButtonSize
iconSize: Theme.iconSizeSmall
iconName: "shuffle"
iconColor: Theme.onSurface
backgroundColor: Theme.surfaceContainerHighest
Accessible.name: I18n.tr("Shuffle")
visible: root.editMode && avatar.visible
onClicked: root.optionChanged("shape", WidgetUtils.nextUserShape(root.shape))
}
}
@@ -1,71 +1,44 @@
.import qs.Common as Common
.import qs.Services as Services
.import qs.Modules.ControlCenter as ControlCenter
.import "../../../Common/GridLayout.js" as GridLayout
var PINNED_IDS = ["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);
var OPTION_IDS = ["diskUsage", "brightnessSlider", "idleInhibitor", "user"];
var ACTION_IDS = ["settings", "lock", "power"];
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 isPinned(id) {
return PINNED_IDS.includes(id);
function isShown(widget) {
switch (widget?.id) {
case "battery":
return Services.BatteryService.batteryAvailable || Services.PowerProfileWatcher.available;
case "diskUsage":
return Services.DgopService.dgopAvailable;
default:
return true;
}
}
function isUnplaced(widget) {
return !Number.isFinite(widget?.col) || !Number.isFinite(widget?.row);
}
function hasOptions(id) {
return OPTION_IDS.includes(id) || String(id ?? "").startsWith("plugin_");
}
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;
function filterWidgets(widgets, query) {
const needle = query.trim().toLowerCase();
if (!needle)
return widgets;
return widgets.filter(widget => [widget.text, widget.description, widget.id].some(value => (value || "").toLowerCase().includes(needle)));
}
function enabledQuickActions(widgetData) {
return quickActions(widgetData).filter(action => action.enabled);
}
function quickActionIcon(id) {
return QUICK_ACTIONS.find(action => action.id === id)?.icon ?? "";
}
function quickActionLabel(id) {
return QUICK_ACTIONS.find(action => action.id === id)?.label ?? "";
function nextUserShape(shape) {
return USER_SHAPES[(USER_SHAPES.indexOf(shape) + 1) % USER_SHAPES.length];
}
function defaultWidget(id, columns) {
@@ -77,10 +50,6 @@ function defaultWidget(id, columns) {
}, columns));
}
function pinnedWidgets(columns) {
return PINNED_IDS.map(id => defaultWidget(id, columns));
}
function sizeSpec(widget, columns, rows = Infinity) {
const spec = {
"w": 4,
@@ -91,18 +60,11 @@ function sizeSpec(widget, columns, rows = Infinity) {
"maxH": rows,
"step": ControlCenter.CcMetrics.gridStep
};
switch (widget?.id) {
case "userCard":
spec.w = Math.max(1, (Number.isFinite(columns) ? columns : ControlCenter.CcMetrics.defaultColumns) - ControlCenter.CcMetrics.actionSpan(2));
spec.h = ControlCenter.CcMetrics.actionSpan(2);
return spec;
case "quickActions":
spec.w = ControlCenter.CcMetrics.actionSpan(2);
spec.h = ControlCenter.CcMetrics.actionSpan(2);
return spec;
default:
return spec;
}
if (widget?.id === "user")
spec.w = (Number.isFinite(columns) ? columns : ControlCenter.CcMetrics.defaultColumns) - ACTION_IDS.length;
if (ACTION_IDS.includes(widget?.id))
spec.w = 1;
return spec;
}
function clampSize(widget, columns, rows = Infinity) {
@@ -111,18 +73,6 @@ function clampSize(widget, columns, rows = Infinity) {
"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)
};
if (widget.id === "quickActions") {
const count = enabledQuickActions(widget).length;
const capacity = ControlCenter.CcMetrics.actionCapacity(size.w);
const neededRows = ControlCenter.CcMetrics.actionSpan(Math.ceil(count / capacity));
if (neededRows <= rows) {
size.h = Math.max(size.h, neededRows);
return size;
}
size.h = rows;
size.w = Math.min(columns, Math.max(size.w, ControlCenter.CcMetrics.actionSpan(Math.ceil(count / ControlCenter.CcMetrics.actionCapacity(rows)))));
return size;
}
if (!isSliderWidget(widget.id) || size.w >= 2 || size.h >= 2)
return size;
if (rows > 1)
@@ -161,21 +111,30 @@ function generateUniqueId() {
function removeWidget(index) {
const widgets = Common.SettingsData.controlCenterWidgets.slice();
if (index < 0 || index >= widgets.length || isPinned(widgets[index]?.id))
if (index < 0 || index >= widgets.length)
return;
widgets.splice(index, 1);
Common.SettingsData.set("controlCenterWidgets", widgets);
}
function setOption(index, key, value) {
const widgets = Common.SettingsData.controlCenterWidgets.slice();
if (index < 0 || index >= widgets.length)
return;
widgets[index] = Object.assign({}, widgets[index], {
[key]: value
});
Common.SettingsData.set("controlCenterWidgets", widgets);
}
function setLayout(widgets) {
Common.SettingsData.set("controlCenterWidgets", widgets);
}
function resetToDefault(columns) {
const ids = ["userCard", "quickActions", "volumeSlider", "brightnessSlider", "wifi", "bluetooth", "audioOutput", "audioInput", "nightMode", "darkMode"];
Common.SettingsData.set("controlCenterWidgets", ids.map(id => defaultWidget(id, columns)));
function resetToDefault() {
Common.SettingsData.resetToDefault(["controlCenterWidgets", "controlCenterColumns"]);
}
function clearAll(columns) {
Common.SettingsData.set("controlCenterWidgets", pinnedWidgets(columns));
function clearAll() {
Common.SettingsData.set("controlCenterWidgets", []);
}
@@ -74,20 +74,7 @@ BasePill {
readonly property bool _currentWorkspace: SettingsData.widgetOption("runningApps", widgetData, "runningAppsCurrentWorkspace")
readonly property bool _currentMonitor: SettingsData.widgetOption("runningApps", widgetData, "runningAppsCurrentMonitor")
readonly property bool _groupByApp: SettingsData.widgetOption("runningApps", widgetData, "runningAppsGroupByApp")
readonly property string windowModelKey: {
if (_groupByApp)
return "appId";
switch (CompositorService.compositor) {
case "aqueous":
return AqueousService.available ? "aqueousKey" : "address";
case "niri":
return "niriWindowId";
case "mango":
return "mangoWindowId";
default:
return "address";
}
}
readonly property string windowModelKey: _groupByApp ? "appId" : CompositorService.toplevelKey
readonly property var sortedToplevels: {
_toplevelsUpdateTrigger;
+51
View File
@@ -790,6 +790,57 @@ Singleton {
return toplevels;
}
readonly property string toplevelKey: {
switch (compositor) {
case "aqueous":
return AqueousService.available ? "aqueousKey" : "address";
case "niri":
return "niriWindowId";
case "mango":
return "mangoWindowId";
default:
return "address";
}
}
function toplevelPlacement(toplevel) {
if (!toplevel)
return {
"workspace": "",
"output": ""
};
if (isAqueous && AqueousService.available) {
const workspace = AqueousService.workspaces.find(w => w.id === toplevel.aqueousWorkspaceId);
return {
"workspace": String(workspace?.name || workspace?.number || ""),
"output": AqueousService.outputs.find(o => o.id === toplevel.aqueousOutputId)?.name ?? ""
};
}
if (useNiriSorting) {
const workspace = NiriService.allWorkspaces.find(w => w.id === toplevel.niriWorkspaceId);
return {
"workspace": String(workspace?.name || workspace?.idx || ""),
"output": workspace?.output ?? ""
};
}
if (useMangoSorting)
return {
"workspace": (toplevel.mangoTags || []).join(", "),
"output": toplevel.mangoMonitor ?? ""
};
if (isHyprland) {
const workspace = WindowModel.hyprlandToplevelFor(Array.from(Hyprland.toplevels?.values || []), toplevel)?.workspace;
return {
"workspace": workspace?.name ?? "",
"output": workspace?.monitor?.name ?? ""
};
}
return {
"workspace": "",
"output": toplevel.screens?.[0]?.name ?? ""
};
}
function fullscreenToplevelOnScreen(screenOrName) {
const screenName = _screenName(screenOrName);
if (isAqueous && AqueousService.available)
+1 -12
View File
@@ -185,7 +185,7 @@ ShellRoot {
h: 12
},
{
id: "diskUsage",
id: "colorPicker",
w: 2,
h: 2
}
@@ -295,17 +295,6 @@ ShellRoot {
verticalSlider.destroy();
wait(0);
const stripSlider = sliderComponent.createObject(scene);
let iconClicks = 0;
stripSlider.iconClicked.connect(() => iconClicks++);
size(stripSlider, 4, 1);
settle();
check(stripSlider.slider.mapToItem(stripSlider, 0, 0).x === 0 && stripSlider.slider.width === stripSlider.width, "strip slider track spans the row");
mouseClick(stripSlider.slider, stripSlider.slider.width - Theme.spacingXS - CcMetrics.stripIconSize / 2, stripSlider.slider.height / 2);
check(iconClicks === 1 && stripSlider.slider.value === 50, "strip slider inset icon takes the click without moving the value");
stripSlider.destroy();
wait(0);
SessionData.recentColors = Array.from({
length: 18
}, (_, i) => Qt.rgba(i / 18, 0.5, 0.5, 1).toString());
+3 -26
View File
@@ -26,30 +26,7 @@ test("sparse settings migrate without inventing grid keys", () => {
assert.equal(sparse.controlCenterWidgets, undefined);
});
test("split header tiles collapse into one pinned header ahead of the layout", () => {
const migrated = store.migrateToVersion({ configVersion: 32, controlCenterWidgets: [{ id: "userCard", w: 4, h: 1, hostname: false }, { id: "wifi", w: 4, h: 1, col: 0, row: 0 }, { id: "quickActions", w: 4, h: 1, powerAccent: true, actions: [{ id: "power", enabled: true }] }] }, 33);
assert.deepEqual(JSON.parse(JSON.stringify(migrated.controlCenterWidgets)), [{ id: "header", enabled: true, w: 8, h: 1, hostname: false, actions: [{ id: "power", enabled: true }], powerAccent: true }, { id: "wifi", w: 4, h: 1, col: 0, row: 0 }]);
assert.equal(migrated.configVersion, 33);
assert.deepEqual(JSON.parse(JSON.stringify(store.migrateToVersion({ configVersion: 31, controlCenterWidgets: [{ id: "wifi", w: 4, h: 1 }] }, 33).controlCenterWidgets.map(w => w.id))), ["header", "wifi"]);
assert.equal(store.migrateToVersion({ configVersion: 31 }, 33).controlCenterWidgets, undefined);
});
test("header splits into independently placed user and action widgets with their options", () => {
const wifi = { id: "wifi", w: 4, h: 1, col: 0, row: 0 };
const actions = [{ id: "settings", enabled: false }, { id: "edit", enabled: true }];
const migrated = store.migrateToVersion({ configVersion: 33, controlCenterColumns: 10, controlCenterWidgets: [wifi, { id: "header", w: 8, h: 2, col: 1, row: 3, hostname: false, compositor: false, uptime: false, badge: false, background: false, powerAccent: true, actions }] }, 34);
assert.deepEqual(JSON.parse(JSON.stringify(migrated.controlCenterWidgets)), [wifi,
{ id: "userCard", enabled: true, w: 6.5, h: 2, hostname: false, compositor: false, uptime: false, badge: false, background: false, col: 1, row: 3 },
{ id: "quickActions", enabled: true, w: 1.5, h: 1.5, actions, powerAccent: true, col: 7.5, row: 3 }
]);
assert.equal(migrated.configVersion, 34);
assert.equal(store.migrateToVersion(migrated, 34), null);
});
test("header migration respects hidden identity and retains an edit entry point", () => {
const hidden = store.migrateToVersion({ configVersion: 33, controlCenterWidgets: [{ id: "header", showUser: false, col: 2, row: 1 }] }, 34);
assert.deepEqual(JSON.parse(JSON.stringify(hidden.controlCenterWidgets)), [{ id: "quickActions", enabled: true, w: 1.5, h: 1.5, col: 2, row: 1 }]);
const absent = store.migrateToVersion({ configVersion: 33, controlCenterWidgets: [] }, 34);
assert.equal(absent.controlCenterWidgets[0].id, "quickActions");
assert.equal(store.migrateToVersion({ configVersion: 33 }, 34).controlCenterWidgets, undefined);
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]]);
});