1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-14 17:52:10 -04:00

feat: add configurable control center group ordering (#2006)

* Add grouped element reordering to control center setting popup.

Reorganize the control center widget menu into grouped rows and add drag handles for reordering.
Introduce controlCenterGroups to drive the grouped popup layout, along with dynamic content width calculation.
Disable dependent options when their parent icon is turned off, and refine DankToggle disabled colors to better distinguish checked and unchecked states.

* Apply Control Center group order to live widget rendering.

Apply persisted `controlCenterGroupOrder` to the actual control center button rendering path instead of only using it in the settings UI.
This refactors `ControlCenterButton.qml` to derive a normalized effective group order, build a small render model from that order, and use model-driven rendering for both vertical and horizontal layouts.

Highlights:
- add a default control center group order and normalize saved order data
- ignore unknown ids, deduplicate duplicates, and append missing known groups
- add shared group visibility helpers and derive a render model from them
- render both vertical and horizontal indicators from the effective order
- preserve existing icon, color, percent text, and visibility behavior
- keep the fallback settings icon outside persisted ordering
- reconnect cached interaction refs for audio, mic, and brightness to the real rendered group containers so wheel and right-click behavior still work
- clear and refresh interaction refs on orientation, visibility, and delegate lifecycle changes
- tighten horizontal composite group sizing by measuring actual rendered content, fixing extra spacing around the audio indicator

Also updates the settings widgets UI to persist and restore control center group ordering consistently with the live control center rendering.
This commit is contained in:
Ron Harel
2026-03-16 17:11:26 +02:00
committed by GitHub
parent 7f392acc54
commit 7826d827dd
4 changed files with 976 additions and 433 deletions

View File

@@ -1,3 +1,4 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
@@ -38,12 +39,20 @@ BasePill {
property var _vAudio: null property var _vAudio: null
property var _vBrightness: null property var _vBrightness: null
property var _vMic: null property var _vMic: null
property var _interactionDelegates: []
readonly property var defaultControlCenterGroupOrder: ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"]
readonly property var effectiveControlCenterGroupOrder: getEffectiveControlCenterGroupOrder()
readonly property var controlCenterRenderModel: getControlCenterRenderModel()
onIsVerticalOrientationChanged: root.clearInteractionRefs()
onWheel: function (wheelEvent) { onWheel: function (wheelEvent) {
const delta = wheelEvent.angleDelta.y; const delta = wheelEvent.angleDelta.y;
if (delta === 0) if (delta === 0)
return; return;
root.refreshInteractionRefs();
const rootX = wheelEvent.x - root.leftMargin; const rootX = wheelEvent.x - root.leftMargin;
const rootY = wheelEvent.y - root.topMargin; const rootY = wheelEvent.y - root.topMargin;
@@ -72,6 +81,8 @@ BasePill {
} }
onRightClicked: function (rootX, rootY) { onRightClicked: function (rootX, rootY) {
root.refreshInteractionRefs();
if (root.isVerticalOrientation && _vCol) { if (root.isVerticalOrientation && _vCol) {
const pos = root.mapToItem(_vCol, rootX, rootY); const pos = root.mapToItem(_vCol, rootX, rootY);
if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) { if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) {
@@ -279,26 +290,142 @@ BasePill {
return CupsService.getTotalJobsNum() > 0; return CupsService.getTotalJobsNum() > 0;
} }
function getControlCenterIconSize() {
return Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale);
}
function getEffectiveControlCenterGroupOrder() {
const knownIds = root.defaultControlCenterGroupOrder;
const savedOrder = root.widgetData?.controlCenterGroupOrder;
const result = [];
const seen = {};
if (savedOrder && typeof savedOrder.length === "number") {
for (let i = 0; i < savedOrder.length; ++i) {
const groupId = savedOrder[i];
if (knownIds.indexOf(groupId) === -1 || seen[groupId])
continue;
seen[groupId] = true;
result.push(groupId);
}
}
for (let i = 0; i < knownIds.length; ++i) {
const groupId = knownIds[i];
if (seen[groupId])
continue;
seen[groupId] = true;
result.push(groupId);
}
return result;
}
function isGroupVisible(groupId) {
switch (groupId) {
case "screenSharing":
return root.showScreenSharingIcon && NiriService.hasCasts;
case "network":
return root.showNetworkIcon && NetworkService.networkAvailable;
case "vpn":
return root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected;
case "bluetooth":
return root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled;
case "audio":
return root.showAudioIcon;
case "microphone":
return root.showMicIcon;
case "brightness":
return root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice();
case "battery":
return root.showBatteryIcon && BatteryService.batteryAvailable;
case "printer":
return root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs();
default:
return false;
}
}
function isCompositeGroup(groupId) {
return groupId === "audio" || groupId === "microphone" || groupId === "brightness";
}
function getControlCenterRenderModel() {
return root.effectiveControlCenterGroupOrder.map(groupId => ({
"id": groupId,
"visible": root.isGroupVisible(groupId),
"composite": root.isCompositeGroup(groupId)
}));
}
function clearInteractionRefs() {
root._hAudio = null;
root._hBrightness = null;
root._hMic = null;
root._vAudio = null;
root._vBrightness = null;
root._vMic = null;
}
function registerInteractionDelegate(isVertical, item) {
if (!item)
return;
for (let i = 0; i < root._interactionDelegates.length; ++i) {
const entry = root._interactionDelegates[i];
if (entry && entry.item === item) {
entry.isVertical = isVertical;
return;
}
}
root._interactionDelegates = root._interactionDelegates.concat([
{
"isVertical": isVertical,
"item": item
}
]);
}
function unregisterInteractionDelegate(item) {
if (!item)
return;
root._interactionDelegates = root._interactionDelegates.filter(entry => entry && entry.item !== item);
}
function refreshInteractionRefs() {
root.clearInteractionRefs();
for (let i = 0; i < root._interactionDelegates.length; ++i) {
const entry = root._interactionDelegates[i];
const item = entry?.item;
if (!item || !item.visible)
continue;
const groupId = item.interactionGroupId;
if (entry.isVertical) {
if (groupId === "audio")
root._vAudio = item;
else if (groupId === "microphone")
root._vMic = item;
else if (groupId === "brightness")
root._vBrightness = item;
} else {
if (groupId === "audio")
root._hAudio = item;
else if (groupId === "microphone")
root._hMic = item;
else if (groupId === "brightness")
root._hBrightness = item;
}
}
}
function hasNoVisibleIcons() { function hasNoVisibleIcons() {
if (root.showScreenSharingIcon && NiriService.hasCasts) return !root.controlCenterRenderModel.some(entry => entry.visible);
return false;
if (root.showNetworkIcon && NetworkService.networkAvailable)
return false;
if (root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected)
return false;
if (root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled)
return false;
if (root.showAudioIcon)
return false;
if (root.showMicIcon)
return false;
if (root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice())
return false;
if (root.showBatteryIcon && BatteryService.batteryAvailable)
return false;
if (root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs())
return false;
return true;
} }
content: Component { content: Component {
@@ -309,12 +436,7 @@ BasePill {
Component.onCompleted: { Component.onCompleted: {
root._hRow = controlIndicators; root._hRow = controlIndicators;
root._vCol = controlColumn; root._vCol = controlColumn;
root._hAudio = audioIcon.parent; root.clearInteractionRefs();
root._hBrightness = brightnessIcon.parent;
root._hMic = micIcon.parent;
root._vAudio = audioIconV.parent;
root._vBrightness = brightnessIconV.parent;
root._vMic = micIconV.parent;
} }
Column { Column {
@@ -324,162 +446,151 @@ BasePill {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXS spacing: Theme.spacingXS
Item { Repeater {
width: parent.width model: root.controlCenterRenderModel
height: root.vIconSize Item {
visible: root.showScreenSharingIcon && NiriService.hasCasts id: verticalGroupItem
required property var modelData
required property int index
property string interactionGroupId: modelData.id
DankIcon { width: parent.width
name: "screen_record" height: {
size: root.vIconSize switch (modelData.id) {
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText case "audio":
anchors.centerIn: parent return root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0);
} case "microphone":
} return root.vIconSize + (root.showMicPercent ? micPercentV.implicitHeight + 2 : 0);
case "brightness":
return root.vIconSize + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight + 2 : 0);
default:
return root.vIconSize;
}
}
visible: modelData.visible
Item { Component.onCompleted: {
width: parent.width root.registerInteractionDelegate(true, verticalGroupItem);
height: root.vIconSize root.refreshInteractionRefs();
visible: root.showNetworkIcon && NetworkService.networkAvailable }
Component.onDestruction: {
if (root) {
root.unregisterInteractionDelegate(verticalGroupItem);
root.refreshInteractionRefs();
}
}
onVisibleChanged: root.refreshInteractionRefs()
onInteractionGroupIdChanged: {
root.refreshInteractionRefs();
}
DankIcon { DankIcon {
name: root.getNetworkIconName() anchors.centerIn: parent
size: root.vIconSize visible: !verticalGroupItem.modelData.composite
color: root.getNetworkIconColor() name: {
anchors.centerIn: parent switch (verticalGroupItem.modelData.id) {
} case "screenSharing":
} return "screen_record";
case "network":
return root.getNetworkIconName();
case "vpn":
return "vpn_lock";
case "bluetooth":
return "bluetooth";
case "battery":
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
case "printer":
return "print";
default:
return "settings";
}
}
size: root.vIconSize
color: {
switch (verticalGroupItem.modelData.id) {
case "screenSharing":
return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
case "network":
return root.getNetworkIconColor();
case "vpn":
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
case "bluetooth":
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
case "battery":
return root.getBatteryIconColor();
case "printer":
return Theme.primary;
default:
return Theme.widgetIconColor;
}
}
}
Item { DankIcon {
width: parent.width id: audioIconV
height: root.vIconSize visible: verticalGroupItem.modelData.id === "audio"
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected name: root.getVolumeIconName()
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
DankIcon { NumericText {
name: "vpn_lock" id: audioPercentV
size: root.vIconSize visible: verticalGroupItem.modelData.id === "audio" && root.showAudioPercent
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
anchors.centerIn: parent reserveText: "100%"
} font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
} color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: audioIconV.bottom
anchors.topMargin: 2
}
Item { DankIcon {
width: parent.width id: micIconV
height: root.vIconSize visible: verticalGroupItem.modelData.id === "microphone"
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled name: root.getMicIconName()
size: root.vIconSize
color: root.getMicIconColor()
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
DankIcon { NumericText {
name: "bluetooth" id: micPercentV
size: root.vIconSize visible: verticalGroupItem.modelData.id === "microphone" && root.showMicPercent
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
anchors.centerIn: parent reserveText: "100%"
} font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
} color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: micIconV.bottom
anchors.topMargin: 2
}
Item { DankIcon {
width: parent.width id: brightnessIconV
height: root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0) visible: verticalGroupItem.modelData.id === "brightness"
visible: root.showAudioIcon name: root.getBrightnessIconName()
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
DankIcon { NumericText {
id: audioIconV id: brightnessPercentV
name: root.getVolumeIconName() visible: verticalGroupItem.modelData.id === "brightness" && root.showBrightnessPercent
size: root.vIconSize text: Math.round(getBrightness() * 100) + "%"
color: Theme.widgetIconColor reserveText: "100%"
anchors.horizontalCenter: parent.horizontalCenter font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
anchors.top: parent.top color: Theme.widgetTextColor
} anchors.horizontalCenter: parent.horizontalCenter
anchors.top: brightnessIconV.bottom
NumericText { anchors.topMargin: 2
id: audioPercentV }
visible: root.showAudioPercent
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: audioIconV.bottom
anchors.topMargin: 2
}
}
Item {
width: parent.width
height: root.vIconSize + (root.showMicPercent ? micPercentV.implicitHeight + 2 : 0)
visible: root.showMicIcon
DankIcon {
id: micIconV
name: root.getMicIconName()
size: root.vIconSize
color: root.getMicIconColor()
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
NumericText {
id: micPercentV
visible: root.showMicPercent
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: micIconV.bottom
anchors.topMargin: 2
}
}
Item {
width: parent.width
height: root.vIconSize + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight + 2 : 0)
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
DankIcon {
id: brightnessIconV
name: root.getBrightnessIconName()
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
NumericText {
id: brightnessPercentV
visible: root.showBrightnessPercent
text: Math.round(getBrightness() * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: brightnessIconV.bottom
anchors.topMargin: 2
}
}
Item {
width: parent.width
height: root.vIconSize
visible: root.showBatteryIcon && BatteryService.batteryAvailable
DankIcon {
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: root.vIconSize
color: root.getBatteryIconColor()
anchors.centerIn: parent
}
}
Item {
width: parent.width
height: root.vIconSize
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
DankIcon {
name: "print"
size: root.vIconSize
color: Theme.primary
anchors.centerIn: parent
} }
} }
@@ -503,157 +614,206 @@ BasePill {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankIcon { Repeater {
name: "screen_record" model: root.controlCenterRenderModel
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: root.showScreenSharingIcon && NiriService.hasCasts
}
DankIcon { Item {
id: networkIcon id: horizontalGroupItem
name: root.getNetworkIconName() required property var modelData
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) required property int index
color: root.getNetworkIconColor() property string interactionGroupId: modelData.id
anchors.verticalCenter: parent.verticalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable
}
DankIcon { width: {
id: vpnIcon switch (modelData.id) {
name: "vpn_lock" case "audio":
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) return audioGroup.width;
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText case "microphone":
anchors.verticalCenter: parent.verticalCenter return micGroup.width;
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected case "brightness":
} return brightnessGroup.width;
default:
return root.getControlCenterIconSize();
}
}
implicitWidth: width
height: root.widgetThickness - root.horizontalPadding * 2
visible: modelData.visible
DankIcon { Component.onCompleted: {
id: bluetoothIcon root.registerInteractionDelegate(false, horizontalGroupItem);
name: "bluetooth" root.refreshInteractionRefs();
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) }
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText Component.onDestruction: {
anchors.verticalCenter: parent.verticalCenter if (root) {
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled root.unregisterInteractionDelegate(horizontalGroupItem);
} root.refreshInteractionRefs();
}
}
onVisibleChanged: root.refreshInteractionRefs()
onInteractionGroupIdChanged: {
root.refreshInteractionRefs();
}
Rectangle { DankIcon {
width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.reservedWidth : 0) + 4 id: iconOnlyItem
implicitWidth: width anchors.verticalCenter: parent.verticalCenter
height: root.widgetThickness - root.horizontalPadding * 2 anchors.left: parent.left
color: "transparent" visible: !horizontalGroupItem.modelData.composite
anchors.verticalCenter: parent.verticalCenter name: {
visible: root.showAudioIcon switch (horizontalGroupItem.modelData.id) {
case "screenSharing":
return "screen_record";
case "network":
return root.getNetworkIconName();
case "vpn":
return "vpn_lock";
case "bluetooth":
return "bluetooth";
case "battery":
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
case "printer":
return "print";
default:
return "settings";
}
}
size: root.getControlCenterIconSize()
color: {
switch (horizontalGroupItem.modelData.id) {
case "screenSharing":
return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
case "network":
return root.getNetworkIconColor();
case "vpn":
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
case "bluetooth":
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
case "battery":
return root.getBatteryIconColor();
case "printer":
return Theme.primary;
default:
return Theme.widgetIconColor;
}
}
}
DankIcon { Rectangle {
id: audioIcon id: audioGroup
name: root.getVolumeIconName() width: audioContent.implicitWidth + 2
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) implicitWidth: width
color: Theme.widgetIconColor height: parent.height
anchors.verticalCenter: parent.verticalCenter color: "transparent"
anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 2 visible: horizontalGroupItem.modelData.id === "audio"
Row {
id: audioContent
anchors.left: parent.left
anchors.leftMargin: 1
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankIcon {
id: audioIcon
name: root.getVolumeIconName()
size: root.getControlCenterIconSize()
color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
}
NumericText {
id: audioPercent
visible: root.showAudioPercent
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
width: visible ? implicitWidth : 0
}
}
}
Rectangle {
id: micGroup
width: micContent.implicitWidth + 2
implicitWidth: width
height: parent.height
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: horizontalGroupItem.modelData.id === "microphone"
Row {
id: micContent
anchors.left: parent.left
anchors.leftMargin: 1
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankIcon {
id: micIcon
name: root.getMicIconName()
size: root.getControlCenterIconSize()
color: root.getMicIconColor()
anchors.verticalCenter: parent.verticalCenter
}
NumericText {
id: micPercent
visible: root.showMicPercent
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
width: visible ? implicitWidth : 0
}
}
}
Rectangle {
id: brightnessGroup
width: brightnessContent.implicitWidth + 2
implicitWidth: width
height: parent.height
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: horizontalGroupItem.modelData.id === "brightness"
Row {
id: brightnessContent
anchors.left: parent.left
anchors.leftMargin: 1
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankIcon {
id: brightnessIcon
name: root.getBrightnessIconName()
size: root.getControlCenterIconSize()
color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
}
NumericText {
id: brightnessPercent
visible: root.showBrightnessPercent
text: Math.round(getBrightness() * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
width: visible ? implicitWidth : 0
}
}
}
} }
NumericText {
id: audioPercent
visible: root.showAudioPercent
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: audioIcon.right
anchors.leftMargin: 2
width: reservedWidth
}
}
Rectangle {
width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.reservedWidth : 0) + 4
implicitWidth: width
height: root.widgetThickness - root.horizontalPadding * 2
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: root.showMicIcon
DankIcon {
id: micIcon
name: root.getMicIconName()
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.getMicIconColor()
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 2
}
NumericText {
id: micPercent
visible: root.showMicPercent
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: micIcon.right
anchors.leftMargin: 2
width: reservedWidth
}
}
Rectangle {
width: brightnessIcon.implicitWidth + (root.showBrightnessPercent ? brightnessPercent.reservedWidth : 0) + 4
height: root.widgetThickness - root.horizontalPadding * 2
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
DankIcon {
id: brightnessIcon
name: root.getBrightnessIconName()
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 2
}
NumericText {
id: brightnessPercent
visible: root.showBrightnessPercent
text: Math.round(getBrightness() * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: brightnessIcon.right
anchors.leftMargin: 2
width: reservedWidth
}
}
DankIcon {
id: batteryIcon
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.getBatteryIconColor()
anchors.verticalCenter: parent.verticalCenter
visible: root.showBatteryIcon && BatteryService.batteryAvailable
}
DankIcon {
id: printerIcon
name: "print"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
} }
DankIcon { DankIcon {
name: "settings" name: "settings"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) size: root.getControlCenterIconSize()
color: root.isActive ? Theme.primary : Theme.widgetIconColor color: root.isActive ? Theme.primary : Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.hasNoVisibleIcons() visible: root.hasNoVisibleIcons()

View File

@@ -391,6 +391,7 @@ Item {
widgetObj.showBatteryIcon = SettingsData.controlCenterShowBatteryIcon; widgetObj.showBatteryIcon = SettingsData.controlCenterShowBatteryIcon;
widgetObj.showPrinterIcon = SettingsData.controlCenterShowPrinterIcon; widgetObj.showPrinterIcon = SettingsData.controlCenterShowPrinterIcon;
widgetObj.showScreenSharingIcon = SettingsData.controlCenterShowScreenSharingIcon; widgetObj.showScreenSharingIcon = SettingsData.controlCenterShowScreenSharingIcon;
widgetObj.controlCenterGroupOrder = ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"];
} }
if (widgetId === "runningApps") { if (widgetId === "runningApps") {
widgetObj.runningAppsCompactMode = SettingsData.runningAppsCompactMode; widgetObj.runningAppsCompactMode = SettingsData.runningAppsCompactMode;
@@ -429,7 +430,7 @@ Item {
"id": widget.id, "id": widget.id,
"enabled": widget.enabled "enabled": widget.enabled
}; };
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"]; var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined) if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]]; result[keys[i]] = widget[keys[i]];
@@ -498,6 +499,32 @@ Item {
return; return;
var newWidget = cloneWidgetData(widgets[widgetIndex]); var newWidget = cloneWidgetData(widgets[widgetIndex]);
newWidget[settingName] = value; newWidget[settingName] = value;
if (!value) {
switch (settingName) {
case "showAudioIcon":
newWidget.showAudioPercent = false;
break;
case "showMicIcon":
newWidget.showMicPercent = false;
break;
case "showBrightnessIcon":
newWidget.showBrightnessPercent = false;
break;
}
}
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
}
function handleControlCenterGroupOrderChanged(sectionId, widgetIndex, groupOrder) {
var widgets = getWidgetsForSection(sectionId).slice();
if (widgetIndex < 0 || widgetIndex >= widgets.length)
return;
var previousWidget = widgets[widgetIndex];
var newWidget = cloneWidgetData(previousWidget);
newWidget.controlCenterGroupOrder = groupOrder.slice();
widgets[widgetIndex] = newWidget; widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets); setWidgetsForSection(sectionId, widgets);
} }
@@ -655,6 +682,8 @@ Item {
item.showPrinterIcon = widget.showPrinterIcon; item.showPrinterIcon = widget.showPrinterIcon;
if (widget.showScreenSharingIcon !== undefined) if (widget.showScreenSharingIcon !== undefined)
item.showScreenSharingIcon = widget.showScreenSharingIcon; item.showScreenSharingIcon = widget.showScreenSharingIcon;
if (widget.controlCenterGroupOrder !== undefined)
item.controlCenterGroupOrder = widget.controlCenterGroupOrder;
if (widget.minimumWidth !== undefined) if (widget.minimumWidth !== undefined)
item.minimumWidth = widget.minimumWidth; item.minimumWidth = widget.minimumWidth;
if (widget.showSwap !== undefined) if (widget.showSwap !== undefined)
@@ -948,6 +977,9 @@ Item {
onControlCenterSettingChanged: (sectionId, index, setting, value) => { onControlCenterSettingChanged: (sectionId, index, setting, value) => {
widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value); widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value);
} }
onControlCenterGroupOrderChanged: (sectionId, index, groupOrder) => {
widgetsTab.handleControlCenterGroupOrderChanged(sectionId, index, groupOrder);
}
onPrivacySettingChanged: (sectionId, index, setting, value) => { onPrivacySettingChanged: (sectionId, index, setting, value) => {
widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value); widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value);
} }
@@ -1012,6 +1044,9 @@ Item {
onControlCenterSettingChanged: (sectionId, index, setting, value) => { onControlCenterSettingChanged: (sectionId, index, setting, value) => {
widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value); widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value);
} }
onControlCenterGroupOrderChanged: (sectionId, index, groupOrder) => {
widgetsTab.handleControlCenterGroupOrderChanged(sectionId, index, groupOrder);
}
onPrivacySettingChanged: (sectionId, index, setting, value) => { onPrivacySettingChanged: (sectionId, index, setting, value) => {
widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value); widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value);
} }
@@ -1076,6 +1111,9 @@ Item {
onControlCenterSettingChanged: (sectionId, index, setting, value) => { onControlCenterSettingChanged: (sectionId, index, setting, value) => {
widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value); widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value);
} }
onControlCenterGroupOrderChanged: (sectionId, index, groupOrder) => {
widgetsTab.handleControlCenterGroupOrderChanged(sectionId, index, groupOrder);
}
onPrivacySettingChanged: (sectionId, index, setting, value) => { onPrivacySettingChanged: (sectionId, index, setting, value) => {
widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value); widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value);
} }

View File

@@ -27,6 +27,7 @@ Column {
signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex) signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex)
signal diskMountSelectionChanged(string sectionId, int widgetIndex, string mountPath) signal diskMountSelectionChanged(string sectionId, int widgetIndex, string mountPath)
signal controlCenterSettingChanged(string sectionId, int widgetIndex, string settingName, bool value) signal controlCenterSettingChanged(string sectionId, int widgetIndex, string settingName, bool value)
signal controlCenterGroupOrderChanged(string sectionId, int widgetIndex, var groupOrder)
signal privacySettingChanged(string sectionId, int widgetIndex, string settingName, bool value) signal privacySettingChanged(string sectionId, int widgetIndex, string settingName, bool value)
signal minimumWidthChanged(string sectionId, int widgetIndex, bool enabled) signal minimumWidthChanged(string sectionId, int widgetIndex, bool enabled)
signal showSwapChanged(string sectionId, int widgetIndex, bool enabled) signal showSwapChanged(string sectionId, int widgetIndex, bool enabled)
@@ -39,7 +40,7 @@ Column {
"id": widget.id, "id": widget.id,
"enabled": widget.enabled "enabled": widget.enabled
}; };
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"]; var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined) if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]]; result[keys[i]] = widget[keys[i]];
@@ -90,7 +91,6 @@ Column {
height: 70 height: 70
z: held ? 2 : 1 z: held ? 2 : 1
Rectangle { Rectangle {
id: itemBackground id: itemBackground
@@ -587,6 +587,7 @@ Column {
controlCenterContextMenu.widgetData = modelData; controlCenterContextMenu.widgetData = modelData;
controlCenterContextMenu.sectionId = root.sectionId; controlCenterContextMenu.sectionId = root.sectionId;
controlCenterContextMenu.widgetIndex = index; controlCenterContextMenu.widgetIndex = index;
controlCenterContextMenu.controlCenterGroups = controlCenterContextMenu.getOrderedControlCenterGroups();
var buttonPos = ccMenuButton.mapToItem(root, 0, 0); var buttonPos = ccMenuButton.mapToItem(root, 0, 0);
var popupWidth = controlCenterContextMenu.width; var popupWidth = controlCenterContextMenu.width;
@@ -1054,13 +1055,236 @@ Column {
property string sectionId: "" property string sectionId: ""
property int widgetIndex: -1 property int widgetIndex: -1
width: 220 readonly property real minimumContentWidth: controlCenterContentMetrics.implicitWidth + Theme.spacingS * 2
height: menuColumn.implicitHeight + Theme.spacingS * 2 readonly property real controlCenterRowHeight: 32
readonly property real controlCenterRowSpacing: 1
readonly property real controlCenterGroupVerticalPadding: Theme.spacingXS * 2
readonly property real controlCenterMenuSpacing: 2
width: Math.max(220, minimumContentWidth)
height: getControlCenterPopupHeight(controlCenterGroups)
padding: 0 padding: 0
modal: true modal: true
focus: true focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onClosed: {
cancelControlCenterDrag();
}
readonly property var defaultControlCenterGroups: [
{
id: "network",
rows: [
{
icon: "lan",
label: I18n.tr("Network"),
setting: "showNetworkIcon"
}
]
},
{
id: "vpn",
rows: [
{
icon: "vpn_lock",
label: I18n.tr("VPN"),
setting: "showVpnIcon"
}
]
},
{
id: "bluetooth",
rows: [
{
icon: "bluetooth",
label: I18n.tr("Bluetooth"),
setting: "showBluetoothIcon"
}
]
},
{
id: "audio",
rows: [
{
icon: "volume_up",
label: I18n.tr("Audio"),
setting: "showAudioIcon"
},
{
icon: "percent",
label: I18n.tr("Volume"),
setting: "showAudioPercent"
}
]
},
{
id: "microphone",
rows: [
{
icon: "mic",
label: I18n.tr("Microphone"),
setting: "showMicIcon"
},
{
icon: "percent",
label: I18n.tr("Microphone Volume"),
setting: "showMicPercent"
}
]
},
{
id: "brightness",
rows: [
{
icon: "brightness_high",
label: I18n.tr("Brightness"),
setting: "showBrightnessIcon"
},
{
icon: "percent",
label: I18n.tr("Brightness Value"),
setting: "showBrightnessPercent"
}
]
},
{
id: "battery",
rows: [
{
icon: "battery_full",
label: I18n.tr("Battery"),
setting: "showBatteryIcon"
}
]
},
{
id: "printer",
rows: [
{
icon: "print",
label: I18n.tr("Printer"),
setting: "showPrinterIcon"
}
]
},
{
id: "screenSharing",
rows: [
{
icon: "screen_record",
label: I18n.tr("Screen Sharing"),
setting: "showScreenSharingIcon"
}
]
}
]
property var controlCenterGroups: defaultControlCenterGroups
property int draggedControlCenterGroupIndex: -1
property int controlCenterGroupDropIndex: -1
function updateControlCenterGroupDropIndex(draggedIndex, localY) {
const totalGroups = controlCenterGroups.length;
let dropIndex = totalGroups;
for (let i = 0; i < totalGroups; i++) {
const delegate = groupRepeater.itemAt(i);
if (!delegate)
continue;
const midpoint = delegate.y + delegate.height / 2;
if (localY < midpoint) {
dropIndex = i;
break;
}
}
controlCenterGroupDropIndex = Math.max(0, Math.min(totalGroups, dropIndex));
draggedControlCenterGroupIndex = draggedIndex;
}
function finishControlCenterDrag() {
if (draggedControlCenterGroupIndex < 0) {
controlCenterGroupDropIndex = -1;
return;
}
const fromIndex = draggedControlCenterGroupIndex;
let toIndex = controlCenterGroupDropIndex;
draggedControlCenterGroupIndex = -1;
controlCenterGroupDropIndex = -1;
if (toIndex < 0 || toIndex > controlCenterGroups.length || toIndex === fromIndex || toIndex === fromIndex + 1)
return;
const groups = controlCenterGroups.slice();
const moved = groups.splice(fromIndex, 1)[0];
if (toIndex > fromIndex)
toIndex -= 1;
groups.splice(toIndex, 0, moved);
controlCenterGroups = groups;
const reorderedGroupIds = groups.map(group => group.id);
root.controlCenterGroupOrderChanged(sectionId, widgetIndex, reorderedGroupIds);
}
function cancelControlCenterDrag() {
draggedControlCenterGroupIndex = -1;
controlCenterGroupDropIndex = -1;
}
function getControlCenterGroupHeight(group) {
const rowCount = group?.rows?.length ?? 0;
if (rowCount <= 0)
return controlCenterGroupVerticalPadding;
return rowCount * controlCenterRowHeight + Math.max(0, rowCount - 1) * controlCenterRowSpacing + controlCenterGroupVerticalPadding;
}
function getControlCenterPopupHeight(groups) {
const orderedGroups = groups || [];
let totalHeight = Theme.spacingS * 2;
for (let i = 0; i < orderedGroups.length; i++) {
totalHeight += getControlCenterGroupHeight(orderedGroups[i]);
if (i < orderedGroups.length - 1)
totalHeight += controlCenterMenuSpacing;
}
return totalHeight;
}
function getOrderedControlCenterGroups() {
const baseGroups = defaultControlCenterGroups.slice();
const currentWidget = contentItem.getCurrentWidgetData();
const savedOrder = currentWidget?.controlCenterGroupOrder;
if (!savedOrder || !savedOrder.length)
return baseGroups;
const groupMap = {};
for (let i = 0; i < baseGroups.length; i++)
groupMap[baseGroups[i].id] = baseGroups[i];
const orderedGroups = [];
for (let i = 0; i < savedOrder.length; i++) {
const groupId = savedOrder[i];
const group = groupMap[groupId];
if (group) {
orderedGroups.push(group);
delete groupMap[groupId];
}
}
for (let i = 0; i < baseGroups.length; i++) {
const group = baseGroups[i];
if (groupMap[group.id])
orderedGroups.push(group);
}
return orderedGroups;
}
background: Rectangle { background: Rectangle {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -1069,83 +1293,64 @@ Column {
} }
contentItem: Item { contentItem: Item {
function getCurrentWidgetData() {
const widgets = root.items || [];
if (controlCenterContextMenu.widgetIndex >= 0 && controlCenterContextMenu.widgetIndex < widgets.length)
return widgets[controlCenterContextMenu.widgetIndex];
return controlCenterContextMenu.widgetData;
}
Column { Column {
id: menuColumn id: menuColumn
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS anchors.margins: Theme.spacingS
spacing: 2 spacing: 2
Repeater { Item {
model: [ id: controlCenterContentMetrics
{ visible: false
icon: "lan", implicitWidth: 16 + Theme.spacingS + 16 + Theme.spacingS + longestControlCenterLabelMetrics.advanceWidth + Theme.spacingM + 40 + Theme.spacingS * 2 + Theme.spacingM
label: I18n.tr("Network"), }
setting: "showNetworkIcon"
}, TextMetrics {
{ id: longestControlCenterLabelMetrics
icon: "vpn_lock", font.pixelSize: Theme.fontSizeSmall
label: I18n.tr("VPN"), text: {
setting: "showVpnIcon" const labels = [
}, I18n.tr("Network"),
{ I18n.tr("VPN"),
icon: "bluetooth", I18n.tr("Bluetooth"),
label: I18n.tr("Bluetooth"), I18n.tr("Audio"),
setting: "showBluetoothIcon" I18n.tr("Volume"),
}, I18n.tr("Microphone"),
{ I18n.tr("Microphone Volume"),
icon: "volume_up", I18n.tr("Brightness"),
label: I18n.tr("Audio"), I18n.tr("Brightness Value"),
setting: "showAudioIcon" I18n.tr("Battery"),
}, I18n.tr("Printer"),
{ I18n.tr("Screen Sharing")
icon: "percent", ];
label: I18n.tr("Volume"), let longest = "";
setting: "showAudioPercent" for (let i = 0; i < labels.length; i++) {
}, if (labels[i].length > longest.length)
{ longest = labels[i];
icon: "mic", }
label: I18n.tr("Microphone"), return longest;
setting: "showMicIcon" }
}, }
{
icon: "percent", Repeater {
label: I18n.tr("Microphone Volume"), model: controlCenterContextMenu.controlCenterGroups
setting: "showMicPercent"
}, delegate: Item {
{ id: delegateRoot
icon: "brightness_high",
label: I18n.tr("Brightness"),
setting: "showBrightnessIcon"
},
{
icon: "percent",
label: I18n.tr("Brightness Value"),
setting: "showBrightnessPercent"
},
{
icon: "battery_full",
label: I18n.tr("Battery"),
setting: "showBatteryIcon"
},
{
icon: "print",
label: I18n.tr("Printer"),
setting: "showPrinterIcon"
},
{
icon: "screen_record",
label: I18n.tr("Screen Sharing"),
setting: "showScreenSharingIcon"
}
]
delegate: Rectangle {
required property var modelData required property var modelData
required property int index required property int index
function getCheckedState() { function getCheckedState(settingName) {
var wd = controlCenterContextMenu.widgetData; const wd = controlCenterContextMenu.contentItem.getCurrentWidgetData();
switch (modelData.setting) { switch (settingName) {
case "showNetworkIcon": case "showNetworkIcon":
return wd?.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon; return wd?.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
case "showVpnIcon": case "showVpnIcon":
@@ -1175,57 +1380,197 @@ Column {
} }
} }
readonly property string rootSetting: modelData.rows[0]?.setting ?? ""
readonly property bool rootEnabled: rootSetting ? getCheckedState(rootSetting) : true
readonly property bool isDragged: controlCenterContextMenu.draggedControlCenterGroupIndex === index
readonly property bool showDropIndicatorAbove: controlCenterContextMenu.controlCenterGroupDropIndex === index
readonly property bool showDropIndicatorBelow: controlCenterContextMenu.controlCenterGroupDropIndex === controlCenterContextMenu.controlCenterGroups.length && index === controlCenterContextMenu.controlCenterGroups.length - 1
width: menuColumn.width width: menuColumn.width
height: 32 height: groupBackground.height
radius: Theme.cornerRadius
color: toggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row { Rectangle {
id: groupBackground
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: modelData.icon
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.label
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: toggle
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.spacingS anchors.top: parent.top
anchors.verticalCenter: parent.verticalCenter height: groupContent.implicitHeight + Theme.spacingXS * 2
width: 40 radius: Theme.cornerRadius
height: 20 color: isDragged ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.18) : (groupHoverArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent")
checked: getCheckedState() opacity: isDragged ? 0.75 : 1.0
onToggled: {
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggled);
}
} }
MouseArea { Rectangle {
id: toggleArea anchors.left: parent.left
anchors.fill: parent anchors.right: parent.right
hoverEnabled: true anchors.top: parent.top
cursorShape: Qt.PointingHandCursor anchors.topMargin: -1
onPressed: { height: 2
toggle.checked = !toggle.checked; radius: 1
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggle.checked); color: Theme.primary
visible: showDropIndicatorAbove
z: 3
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: -1
height: 2
radius: 1
color: Theme.primary
visible: showDropIndicatorBelow
z: 3
}
Item {
id: groupContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: Theme.spacingXS
implicitHeight: groupColumn.implicitHeight
Column {
id: groupColumn
anchors.left: parent.left
anchors.right: parent.right
spacing: 1
Repeater {
id: groupColumnRepeater
model: modelData.rows
delegate: Rectangle {
required property var modelData
required property int index
readonly property var rowData: modelData
readonly property bool isFirstRow: index === 0
readonly property bool rowEnabled: isFirstRow ? true : delegateRoot.rootEnabled
readonly property bool computedCheckedState: rowEnabled ? getCheckedState(rowData.setting) : false
readonly property bool rowHovered: rowEnabled && (toggleArea.containsMouse || (isFirstRow && groupDragHandleArea.containsMouse))
width: groupColumn.width
height: 32
radius: Theme.cornerRadius
opacity: rowEnabled ? 1.0 : 0.5
color: rowHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: toggle.left
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Item {
width: 16
height: 16
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "drag_indicator"
size: 16
color: groupDragHandleArea.pressed || isDragged ? Theme.primary : Theme.outline
visible: isFirstRow
}
MouseArea {
id: groupDragHandleArea
anchors.fill: parent
hoverEnabled: true
preventStealing: true
enabled: isFirstRow
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
onPressed: mouse => {
mouse.accepted = true;
const point = mapToItem(menuColumn, mouse.x, mouse.y);
controlCenterContextMenu.updateControlCenterGroupDropIndex(delegateRoot.index, point.y);
}
onPositionChanged: mouse => {
if (!pressed)
return;
mouse.accepted = true;
const point = mapToItem(menuColumn, mouse.x, mouse.y);
controlCenterContextMenu.updateControlCenterGroupDropIndex(delegateRoot.index, point.y);
}
onReleased: mouse => {
mouse.accepted = true;
const point = mapToItem(menuColumn, mouse.x, mouse.y);
controlCenterContextMenu.updateControlCenterGroupDropIndex(delegateRoot.index, point.y);
controlCenterContextMenu.finishControlCenterDrag();
}
onCanceled: {
controlCenterContextMenu.cancelControlCenterDrag();
}
}
}
DankIcon {
name: rowData.icon
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: rowData.label
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: toggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
enabled: rowEnabled
checked: computedCheckedState
onToggled: {
if (!rowEnabled)
return;
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, rowData.setting, toggled);
}
}
MouseArea {
id: toggleArea
anchors.fill: parent
anchors.leftMargin: 16 + Theme.spacingS * 2
hoverEnabled: true
cursorShape: rowEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: rowEnabled && controlCenterContextMenu.draggedControlCenterGroupIndex < 0
onPressed: {
if (!rowEnabled)
return;
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, rowData.setting, !computedCheckedState);
}
}
}
}
}
MouseArea {
id: groupHoverArea
anchors.fill: parent
hoverEnabled: true
enabled: false
} }
} }
} }
id: groupRepeater
} }
} }
} }

View File

@@ -99,8 +99,8 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius radius: Theme.cornerRadius
// M3 disabled track: on surface 12% opacity // Distinguish disabled checked vs unchecked so unchecked disabled switches don't look enabled
color: !toggle.enabled ? Qt.alpha(Theme.surfaceText, 0.12) : (toggle.checked ? Theme.primary : Theme.surfaceVariantAlpha) color: !toggle.enabled ? (toggle.checked ? Qt.alpha(Theme.surfaceText, 0.12) : "transparent") : (toggle.checked ? Theme.primary : Theme.surfaceVariantAlpha)
opacity: toggle.toggling ? 0.6 : 1 opacity: toggle.toggling ? 0.6 : 1
// M3 disabled unchecked border: on surface 12% opacity // M3 disabled unchecked border: on surface 12% opacity
@@ -119,8 +119,8 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
// M3 disabled thumb: // M3 disabled thumb:
// checked = solid surface | unchecked = on surface 38% // checked = solid surface | unchecked = outlined off-state thumb
color: !toggle.enabled ? (toggle.checked ? Theme.surface : Qt.alpha(Theme.surfaceText, 0.38)) : (toggle.checked ? Theme.surface : Theme.outline) color: !toggle.enabled ? (toggle.checked ? Theme.surface : "transparent") : (toggle.checked ? Theme.surface : Theme.outline)
border.color: !toggle.enabled ? (toggle.checked ? "transparent" : Qt.alpha(Theme.surfaceText, 0.38)) : Theme.outline border.color: !toggle.enabled ? (toggle.checked ? "transparent" : Qt.alpha(Theme.surfaceText, 0.38)) : Theme.outline
border.width: (toggle.checked && toggle.enabled) ? 1 : 2 border.width: (toggle.checked && toggle.enabled) ? 1 : 2
@@ -165,8 +165,8 @@ Item {
// M3 disabled icon: on surface 38% // M3 disabled icon: on surface 38%
color: toggle.enabled ? Theme.surfaceText : Qt.alpha(Theme.surfaceText, 0.38) color: toggle.enabled ? Theme.surfaceText : Qt.alpha(Theme.surfaceText, 0.38)
filled: true filled: true
opacity: toggle.checked ? 1 : 0 opacity: (toggle.checked && toggle.enabled) ? 1 : 0
scale: toggle.checked ? 1 : 0.6 scale: (toggle.checked && toggle.enabled) ? 1 : 0.6
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {