1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00

Compare commits

...

12 Commits

Author SHA1 Message Date
bbedward
ad0f3fa33b dankbar: convert center section to use WidgetHost 2025-12-04 19:37:21 -05:00
bbedward
63d121b796 proc: ability to run command with noTimeout 2025-12-04 16:09:38 -05:00
bbedward
4291cfe82f settings: fix launcher tab sizing 2025-12-04 16:01:07 -05:00
bbedward
f312868154 lock: respect confirmation mode power actions 2025-12-04 14:58:36 -05:00
bbedward
5b42d34ac8 expose iconSize helpers to plugins 2025-12-04 14:40:38 -05:00
bbedward
397a8c275d settings: add IPCs to open specific settings tabs 2025-12-04 14:31:35 -05:00
purian23
2aabee453b Remove hyprpicker requirement for DMS Copr 2025-12-04 14:25:40 -05:00
bbedward
185333a615 brightness: default IPCs to pinned devices per-display
fixes #875
2025-12-04 13:55:14 -05:00
bbedward
7d177eb1d4 greeter: fix mango config override
fixes #904
2025-12-04 13:46:11 -05:00
Givani Boekestijn
705a84051d feat(dankdash): add vim keybindings (hjkl) to wallpaper picker navigation (#903) 2025-12-04 13:22:30 -05:00
bbedward
f6821f80e1 dankslideout: convert to Rectangle 2025-12-04 12:54:15 -05:00
bbedward
e7a6f5228d widgets: fix binding loop in button 2025-12-04 12:50:06 -05:00
25 changed files with 1302 additions and 857 deletions

View File

@@ -517,7 +517,6 @@ jobs:
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: hyprpicker
Recommends: matugen
Recommends: wl-clipboard
Recommends: NetworkManager

View File

@@ -33,7 +33,6 @@ Requires: dgop
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: hyprpicker
Recommends: matugen
Recommends: quickshell-git
Recommends: wl-clipboard

View File

@@ -3,122 +3,139 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
readonly property int noTimeout: -1
property int defaultDebounceMs: 50
property int defaultTimeoutMs: 10000
property var _procDebouncers: ({})
function runCommand(id, command, callback, debounceMs, timeoutMs) {
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs
const timeout = (typeof timeoutMs === "number" && timeoutMs > 0) ? timeoutMs : defaultTimeoutMs
let procId = id ? id : Math.random()
const isRandomId = !id
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs;
const timeout = (typeof timeoutMs === "number") ? timeoutMs : defaultTimeoutMs;
let procId = id ? id : Math.random();
const isRandomId = !id;
if (!_procDebouncers[procId]) {
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
t.triggered.connect(function() { _launchProc(procId, isRandomId) })
_procDebouncers[procId] = { timer: t, command: command, callback: callback, waitMs: wait, timeoutMs: timeout, isRandomId: isRandomId }
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root);
t.triggered.connect(function () {
_launchProc(procId, isRandomId);
});
_procDebouncers[procId] = {
timer: t,
command: command,
callback: callback,
waitMs: wait,
timeoutMs: timeout,
isRandomId: isRandomId
};
} else {
_procDebouncers[procId].command = command
_procDebouncers[procId].callback = callback
_procDebouncers[procId].waitMs = wait
_procDebouncers[procId].timeoutMs = timeout
_procDebouncers[procId].command = command;
_procDebouncers[procId].callback = callback;
_procDebouncers[procId].waitMs = wait;
_procDebouncers[procId].timeoutMs = timeout;
}
const entry = _procDebouncers[procId]
entry.timer.interval = entry.waitMs
entry.timer.restart()
const entry = _procDebouncers[procId];
entry.timer.interval = entry.waitMs;
entry.timer.restart();
}
function _launchProc(id, isRandomId) {
const entry = _procDebouncers[id]
if (!entry) return
const entry = _procDebouncers[id];
if (!entry)
return;
const proc = Qt.createQmlObject('import Quickshell.Io; Process { running: false }', root);
const out = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc);
const err = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc);
const timeoutTimer = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root);
const proc = Qt.createQmlObject('import Quickshell.Io; Process { running: false }', root)
const out = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
const err = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
const timeoutTimer = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
proc.stdout = out;
proc.stderr = err;
proc.command = entry.command;
proc.stdout = out
proc.stderr = err
proc.command = entry.command
let capturedOut = "";
let capturedErr = "";
let exitSeen = false;
let exitCodeValue = -1;
let outSeen = false;
let errSeen = false;
let timedOut = false;
let capturedOut = ""
let capturedErr = ""
let exitSeen = false
let exitCodeValue = -1
let outSeen = false
let errSeen = false
let timedOut = false
timeoutTimer.interval = entry.timeoutMs
timeoutTimer.triggered.connect(function() {
timeoutTimer.interval = entry.timeoutMs;
timeoutTimer.triggered.connect(function () {
if (!exitSeen) {
timedOut = true
proc.running = false
exitSeen = true
exitCodeValue = 124
maybeComplete()
timedOut = true;
proc.running = false;
exitSeen = true;
exitCodeValue = 124;
maybeComplete();
}
})
});
out.streamFinished.connect(function() {
out.streamFinished.connect(function () {
try {
capturedOut = out.text || ""
capturedOut = out.text || "";
} catch (e) {
capturedOut = ""
capturedOut = "";
}
outSeen = true
maybeComplete()
})
outSeen = true;
maybeComplete();
});
err.streamFinished.connect(function() {
err.streamFinished.connect(function () {
try {
capturedErr = err.text || ""
capturedErr = err.text || "";
} catch (e) {
capturedErr = ""
capturedErr = "";
}
errSeen = true
maybeComplete()
})
errSeen = true;
maybeComplete();
});
proc.exited.connect(function(code) {
timeoutTimer.stop()
exitSeen = true
exitCodeValue = code
maybeComplete()
})
proc.exited.connect(function (code) {
timeoutTimer.stop();
exitSeen = true;
exitCodeValue = code;
maybeComplete();
});
function maybeComplete() {
if (!exitSeen || !outSeen || !errSeen) return
timeoutTimer.stop()
if (!exitSeen || !outSeen || !errSeen)
return;
timeoutTimer.stop();
if (entry && entry.callback && typeof entry.callback === "function") {
try {
const safeOutput = capturedOut !== null && capturedOut !== undefined ? capturedOut : ""
const safeExitCode = exitCodeValue !== null && exitCodeValue !== undefined ? exitCodeValue : -1
entry.callback(safeOutput, safeExitCode)
const safeOutput = capturedOut !== null && capturedOut !== undefined ? capturedOut : "";
const safeExitCode = exitCodeValue !== null && exitCodeValue !== undefined ? exitCodeValue : -1;
entry.callback(safeOutput, safeExitCode);
} catch (e) {
console.warn("runCommand callback error for command:", entry.command, "Error:", e)
console.warn("runCommand callback error for command:", entry.command, "Error:", e);
}
}
try { proc.destroy() } catch (_) {}
try { timeoutTimer.destroy() } catch (_) {}
try {
proc.destroy();
} catch (_) {}
try {
timeoutTimer.destroy();
} catch (_) {}
if (isRandomId || entry.isRandomId) {
Qt.callLater(function() {
Qt.callLater(function () {
if (_procDebouncers[id]) {
try { _procDebouncers[id].timer.destroy() } catch (_) {}
delete _procDebouncers[id]
try {
_procDebouncers[id].timer.destroy();
} catch (_) {}
delete _procDebouncers[id];
}
})
});
}
}
proc.running = true
timeoutTimer.start()
proc.running = true;
if (entry.timeoutMs !== noTimeout)
timeoutTimer.start();
}
}

View File

@@ -648,6 +648,13 @@ Item {
return "SETTINGS_OPEN_SUCCESS";
}
function openWith(tab: string): string {
if (!tab)
return "SETTINGS_OPEN_FAILED: No tab specified";
PopoutService.openSettingsWithTab(tab);
return `SETTINGS_OPEN_SUCCESS: ${tab}`;
}
function close(): string {
PopoutService.closeSettings();
return "SETTINGS_CLOSE_SUCCESS";
@@ -658,11 +665,47 @@ Item {
return "SETTINGS_TOGGLE_SUCCESS";
}
function toggleWith(tab: string): string {
if (!tab)
return "SETTINGS_TOGGLE_FAILED: No tab specified";
PopoutService.toggleSettingsWithTab(tab);
return `SETTINGS_TOGGLE_SUCCESS: ${tab}`;
}
function focusOrToggle(): string {
PopoutService.focusOrToggleSettings();
return "SETTINGS_FOCUS_OR_TOGGLE_SUCCESS";
}
function focusOrToggleWith(tab: string): string {
if (!tab)
return "SETTINGS_FOCUS_OR_TOGGLE_FAILED: No tab specified";
PopoutService.focusOrToggleSettingsWithTab(tab);
return `SETTINGS_FOCUS_OR_TOGGLE_SUCCESS: ${tab}`;
}
function tabs(): string {
if (!PopoutService.settingsModal)
return "wallpaper\ntheme\ntypography\ntime_weather\nsounds\ndankbar\ndankbar_settings\ndankbar_widgets\nworkspaces\nmedia_player\nnotifications\nosd\nrunning_apps\nupdater\ndock\nlauncher\nkeybinds\ndisplays\nnetwork\nprinters\nlock_screen\npower_sleep\nplugins\nabout";
var modal = PopoutService.settingsModal;
var ids = [];
var structure = modal.sidebar?.categoryStructure ?? [];
for (var i = 0; i < structure.length; i++) {
var cat = structure[i];
if (cat.separator)
continue;
if (cat.id)
ids.push(cat.id);
if (cat.children) {
for (var j = 0; j < cat.children.length; j++) {
if (cat.children[j].id)
ids.push(cat.children[j].id);
}
}
}
return ids.join("\n");
}
function get(key: string): string {
return JSON.stringify(SettingsData?.[key]);
}

View File

@@ -110,7 +110,7 @@ DankModal {
console.warn("Failed to parse dms color pick JSON:", e);
root.show();
}
});
}, 0, Proc.noTimeout);
}
modalWidth: 680

View File

@@ -10,6 +10,7 @@ FloatingWindow {
property alias profileBrowser: profileBrowser
property alias wallpaperBrowser: wallpaperBrowser
property alias sidebar: sidebar
property int currentTabIndex: 0
property bool shouldHaveFocus: visible
property bool allowFocusOverride: false
@@ -32,6 +33,23 @@ FloatingWindow {
visible = !visible;
}
function showWithTab(tabIndex: int) {
if (tabIndex >= 0)
currentTabIndex = tabIndex;
visible = true;
}
function showWithTabName(tabName: string) {
var idx = sidebar.resolveTabIndex(tabName);
if (idx >= 0)
currentTabIndex = idx;
visible = true;
}
function resolveTabIndex(tabName: string): int {
return sidebar.resolveTabIndex(tabName);
}
function toggleMenu() {
enableAnimations = true;
menuVisible = !menuVisible;

View File

@@ -21,26 +21,31 @@ Rectangle {
"icon": "palette",
"children": [
{
"id": "wallpaper",
"text": I18n.tr("Wallpaper"),
"icon": "wallpaper",
"tabIndex": 0
},
{
"id": "theme",
"text": I18n.tr("Theme & Colors"),
"icon": "format_paint",
"tabIndex": 10
},
{
"id": "typography",
"text": I18n.tr("Typography & Motion"),
"icon": "text_fields",
"tabIndex": 14
},
{
"id": "time_weather",
"text": I18n.tr("Time & Weather"),
"icon": "schedule",
"tabIndex": 1
},
{
"id": "sounds",
"text": I18n.tr("Sounds"),
"icon": "volume_up",
"tabIndex": 15,
@@ -54,11 +59,13 @@ Rectangle {
"icon": "toolbar",
"children": [
{
"id": "dankbar_settings",
"text": I18n.tr("Settings"),
"icon": "tune",
"tabIndex": 3
},
{
"id": "dankbar_widgets",
"text": I18n.tr("Widgets"),
"icon": "widgets",
"tabIndex": 22
@@ -72,32 +79,38 @@ Rectangle {
"collapsedByDefault": true,
"children": [
{
"id": "workspaces",
"text": I18n.tr("Workspaces"),
"icon": "view_module",
"tabIndex": 4
},
{
"id": "media_player",
"text": I18n.tr("Media Player"),
"icon": "music_note",
"tabIndex": 16
},
{
"id": "notifications",
"text": I18n.tr("Notifications"),
"icon": "notifications",
"tabIndex": 17
},
{
"id": "osd",
"text": I18n.tr("On-screen Displays"),
"icon": "tune",
"tabIndex": 18
},
{
"id": "running_apps",
"text": I18n.tr("Running Apps"),
"icon": "apps",
"tabIndex": 19,
"hyprlandNiriOnly": true
},
{
"id": "updater",
"text": I18n.tr("System Updater"),
"icon": "refresh",
"tabIndex": 20
@@ -111,11 +124,13 @@ Rectangle {
"collapsedByDefault": true,
"children": [
{
"id": "dock",
"text": I18n.tr("Dock"),
"icon": "dock_to_bottom",
"tabIndex": 5
},
{
"id": "launcher",
"text": I18n.tr("Launcher"),
"icon": "grid_view",
"tabIndex": 9
@@ -123,7 +138,7 @@ Rectangle {
]
},
{
"id": "input",
"id": "keybinds",
"text": I18n.tr("Keyboard Shortcuts"),
"icon": "keyboard",
"tabIndex": 2,
@@ -156,11 +171,13 @@ Rectangle {
"collapsedByDefault": true,
"children": [
{
"id": "lock_screen",
"text": I18n.tr("Lock Screen"),
"icon": "lock",
"tabIndex": 11
},
{
"id": "power_sleep",
"text": I18n.tr("Power & Sleep"),
"icon": "power_settings_new",
"tabIndex": 21
@@ -338,6 +355,37 @@ Rectangle {
return items;
}
function resolveTabIndex(name: string): int {
if (!name)
return -1;
var normalized = name.toLowerCase().replace(/[_\-\s]/g, "");
for (var i = 0; i < categoryStructure.length; i++) {
var cat = categoryStructure[i];
if (cat.separator)
continue;
var catId = (cat.id || "").toLowerCase().replace(/[_\-\s]/g, "");
if (catId === normalized) {
if (cat.tabIndex !== undefined)
return cat.tabIndex;
if (cat.children && cat.children.length > 0)
return cat.children[0].tabIndex;
}
if (cat.children) {
for (var j = 0; j < cat.children.length; j++) {
var child = cat.children[j];
var childId = (child.id || "").toLowerCase().replace(/[_\-\s]/g, "");
if (childId === normalized)
return child.tabIndex;
}
}
}
return -1;
}
width: 270
height: parent.height
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)

View File

@@ -312,7 +312,7 @@ Item {
Row {
id: viewModeButtons
spacing: Theme.spacingXS
visible: searchMode === "apps" && appLauncher.model.count > 0
visible: searchMode === "apps"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter

View File

@@ -1,6 +1,5 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
@@ -17,20 +16,18 @@ PluginComponent {
ccWidgetPrimaryText: I18n.tr("Printers")
ccWidgetSecondaryText: {
if (CupsService.cupsAvailable && CupsService.getPrintersNum() > 0) {
return I18n.tr("Printers: ") + CupsService.getPrintersNum() + " - " + I18n.tr("Jobs: ") + CupsService.getTotalJobsNum()
return I18n.tr("Printers: ") + CupsService.getPrintersNum() + " - " + I18n.tr("Jobs: ") + CupsService.getTotalJobsNum();
} else {
if (!CupsService.cupsAvailable) {
return I18n.tr("Print Server not available")
return I18n.tr("Print Server not available");
} else {
return I18n.tr("No printer found")
return I18n.tr("No printer found");
}
}
}
ccWidgetIsActive: CupsService.cupsAvailable && CupsService.getTotalJobsNum() > 0
onCcWidgetToggled: {
}
onCcWidgetToggled: {}
ccDetailContent: Component {
Rectangle {
@@ -39,6 +36,21 @@ PluginComponent {
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
DankActionButton {
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
iconName: "settings"
buttonSize: 24
iconSize: 14
iconColor: Theme.surfaceVariantText
onClicked: {
PopoutService.closeControlCenter();
PopoutService.openSettingsWithTab("printers");
}
}
Column {
visible: !CupsService.cupsAvailable || CupsService.getPrintersNum() == 0
anchors.centerIn: parent
@@ -58,7 +70,7 @@ PluginComponent {
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
id: detailColumn
anchors.fill: parent
@@ -78,12 +90,12 @@ PluginComponent {
Layout.maximumWidth: parent.width - 180
description: ""
currentValue: {
CupsService.getSelectedPrinter()
CupsService.getSelectedPrinter();
}
options: CupsService.getPrintersNames()
onValueChanged: value => {
CupsService.setSelectedPrinter(value)
}
CupsService.setSelectedPrinter(value);
}
}
Column {
@@ -135,11 +147,11 @@ PluginComponent {
cursorShape: Qt.PointingHandCursor
enabled: true
onClicked: {
const selected = CupsService.getSelectedPrinter()
const selected = CupsService.getSelectedPrinter();
if (CupsService.getCurrentPrinterState() === "stopped") {
CupsService.resumePrinter(selected)
CupsService.resumePrinter(selected);
} else {
CupsService.pausePrinter(selected)
CupsService.pausePrinter(selected);
}
}
}
@@ -180,8 +192,8 @@ PluginComponent {
cursorShape: Qt.PointingHandCursor
enabled: true
onClicked: {
const selected = CupsService.getSelectedPrinter()
CupsService.purgeJobs(selected)
const selected = CupsService.getSelectedPrinter();
CupsService.purgeJobs(selected);
}
}
}
@@ -275,8 +287,8 @@ PluginComponent {
StyledText {
text: {
var date = new Date(modelData.timeCreated)
return date.toLocaleString(Qt.locale(), Locale.ShortFormat)
var date = new Date(modelData.timeCreated);
return date.toLocaleString(Qt.locale(), Locale.ShortFormat);
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
@@ -296,7 +308,7 @@ PluginComponent {
iconName: "delete"
buttonSize: 36
onClicked: {
CupsService.cancelJob(CupsService.getSelectedPrinter(), modelData.id)
CupsService.cancelJob(CupsService.getSelectedPrinter(), modelData.id);
}
}
}

View File

@@ -69,8 +69,8 @@ Rectangle {
height: 40
StyledText {
id: headerText
text: I18n.tr("Network Settings")
id: headerLeft
text: I18n.tr("Network")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
@@ -79,7 +79,7 @@ Rectangle {
Item {
height: 1
width: parent.width - headerText.width - rightControls.width
width: parent.width - headerLeft.width - rightControls.width
}
Row {
@@ -115,6 +115,8 @@ Rectangle {
id: preferenceControls
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.backend === "networkmanager" && DMSService.apiVersion > 10
buttonHeight: 28
textSize: Theme.fontSizeSmall
model: ["Ethernet", "WiFi"]
currentIndex: currentPreferenceIndex
@@ -125,6 +127,18 @@ Rectangle {
NetworkService.setNetworkPreference(index === 0 ? "ethernet" : "wifi");
}
}
DankActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "settings"
buttonSize: 28
iconSize: 16
iconColor: Theme.surfaceVariantText
onClicked: {
PopoutService.closeControlCenter();
PopoutService.openSettingsWithTab("network");
}
}
}
}

View File

@@ -1,6 +1,5 @@
import QtQuick
import qs.Common
import qs.Services
Item {
id: root
@@ -19,7 +18,7 @@ Item {
property bool forceVerticalLayout: false
readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false)
readonly property real spacing: {
readonly property real widgetSpacing: {
const baseSpacing = noBackground ? 2 : Theme.spacingXS;
const outlineThickness = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0;
return baseSpacing + (outlineThickness * 2);
@@ -33,36 +32,32 @@ Item {
if (SettingsData.centeringMode === "geometric") {
applyGeometricLayout();
} else {
// Default to index layout or if value is not 'geometric'
applyIndexLayout();
}
}
function applyGeometricLayout() {
if ((isVertical ? height : width) <= 0 || !visible) {
if ((isVertical ? height : width) <= 0 || !visible)
return;
}
centerWidgets = [];
totalWidgets = 0;
totalSize = 0;
for (var i = 0; i < centerRepeater.count; i++) {
const item = centerRepeater.itemAt(i);
if (item && item.active && item.item && getWidgetVisible(item.widgetId)) {
centerWidgets.push(item.item);
const loader = centerRepeater.itemAt(i);
if (loader && loader.active && loader.item) {
centerWidgets.push(loader.item);
totalWidgets++;
totalSize += isVertical ? item.item.height : item.item.width;
totalSize += isVertical ? loader.item.height : loader.item.width;
}
}
if (totalWidgets === 0) {
if (totalWidgets === 0)
return;
}
if (totalWidgets > 1) {
totalSize += spacing * (totalWidgets - 1);
}
if (totalWidgets > 1)
totalSize += widgetSpacing * (totalWidgets - 1);
positionWidgetsGeometric();
}
@@ -70,7 +65,6 @@ Item {
function positionWidgetsGeometric() {
const parentLength = isVertical ? height : width;
const parentCenter = parentLength / 2;
let currentPos = parentCenter - (totalSize / 2);
centerWidgets.forEach(widget => {
@@ -81,67 +75,53 @@ Item {
widget.anchors.horizontalCenter = undefined;
widget.x = currentPos;
}
const widgetSize = isVertical ? widget.height : widget.width;
currentPos += widgetSize + spacing;
currentPos += widgetSize + widgetSpacing;
});
}
function applyIndexLayout() {
if ((isVertical ? height : width) <= 0 || !visible) {
if ((isVertical ? height : width) <= 0 || !visible)
return;
}
centerWidgets = [];
totalWidgets = 0;
totalSize = 0;
let configuredWidgets = 0;
let configuredMiddleWidget = null;
let configuredLeftWidget = null;
let configuredRightWidget = null;
for (var i = 0; i < centerRepeater.count; i++) {
const item = centerRepeater.itemAt(i);
if (item && getWidgetVisible(item.widgetId)) {
configuredWidgets++;
}
}
const configuredWidgets = centerRepeater.count;
const isOddConfigured = configuredWidgets % 2 === 1;
const configuredMiddlePos = Math.floor(configuredWidgets / 2);
const configuredLeftPos = isOddConfigured ? -1 : ((configuredWidgets / 2) - 1);
const configuredRightPos = isOddConfigured ? -1 : (configuredWidgets / 2);
let currentConfigIndex = 0;
for (var i = 0; i < centerRepeater.count; i++) {
const item = centerRepeater.itemAt(i);
if (item && getWidgetVisible(item.widgetId)) {
if (isOddConfigured && currentConfigIndex === configuredMiddlePos && item.active && item.item) {
configuredMiddleWidget = item.item;
}
if (!isOddConfigured && currentConfigIndex === configuredLeftPos && item.active && item.item) {
configuredLeftWidget = item.item;
}
if (!isOddConfigured && currentConfigIndex === configuredRightPos && item.active && item.item) {
configuredRightWidget = item.item;
}
if (item.active && item.item) {
centerWidgets.push(item.item);
totalWidgets++;
totalSize += isVertical ? item.item.height : item.item.width;
}
currentConfigIndex++;
const wrapper = centerRepeater.itemAt(i);
if (!wrapper)
continue;
if (isOddConfigured && i === configuredMiddlePos && wrapper.active && wrapper.item)
configuredMiddleWidget = wrapper.item;
if (!isOddConfigured && i === configuredLeftPos && wrapper.active && wrapper.item)
configuredLeftWidget = wrapper.item;
if (!isOddConfigured && i === configuredRightPos && wrapper.active && wrapper.item)
configuredRightWidget = wrapper.item;
if (wrapper.active && wrapper.item) {
centerWidgets.push(wrapper.item);
totalWidgets++;
totalSize += isVertical ? wrapper.item.height : wrapper.item.width;
}
}
if (totalWidgets === 0) {
if (totalWidgets === 0)
return;
}
if (totalWidgets > 1) {
totalSize += spacing * (totalWidgets - 1);
}
if (totalWidgets > 1)
totalSize += widgetSpacing * (totalWidgets - 1);
positionWidgetsByIndex(configuredWidgets, configuredMiddleWidget, configuredLeftWidget, configuredRightWidget);
}
@@ -151,11 +131,10 @@ Item {
const isOddConfigured = configuredWidgets % 2 === 1;
centerWidgets.forEach(widget => {
if (isVertical) {
if (isVertical)
widget.anchors.verticalCenter = undefined;
} else {
else
widget.anchors.horizontalCenter = undefined;
}
});
if (isOddConfigured && configuredMiddleWidget) {
@@ -163,222 +142,154 @@ Item {
const middleIndex = centerWidgets.indexOf(middleWidget);
const middleSize = isVertical ? middleWidget.height : middleWidget.width;
if (isVertical) {
if (isVertical)
middleWidget.y = parentCenter - (middleSize / 2);
} else {
else
middleWidget.x = parentCenter - (middleSize / 2);
}
let currentPos = isVertical ? middleWidget.y : middleWidget.x;
for (var i = middleIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width;
currentPos -= (spacing + size);
if (isVertical) {
currentPos -= (widgetSpacing + size);
if (isVertical)
centerWidgets[i].y = currentPos;
} else {
else
centerWidgets[i].x = currentPos;
}
}
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize;
for (var i = middleIndex + 1; i < totalWidgets; i++) {
currentPos += spacing;
if (isVertical) {
currentPos += widgetSpacing;
if (isVertical)
centerWidgets[i].y = currentPos;
} else {
else
centerWidgets[i].x = currentPos;
}
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width;
}
} else {
if (totalWidgets === 1) {
const widget = centerWidgets[0];
const size = isVertical ? widget.height : widget.width;
if (isVertical) {
widget.y = parentCenter - (size / 2);
} else {
widget.x = parentCenter - (size / 2);
return;
}
if (totalWidgets === 1) {
const widget = centerWidgets[0];
const size = isVertical ? widget.height : widget.width;
if (isVertical)
widget.y = parentCenter - (size / 2);
else
widget.x = parentCenter - (size / 2);
return;
}
if (!configuredLeftWidget || !configuredRightWidget) {
if (totalWidgets % 2 === 1) {
const middleIndex = Math.floor(totalWidgets / 2);
const middleWidget = centerWidgets[middleIndex];
if (!middleWidget)
return;
const middleSize = isVertical ? middleWidget.height : middleWidget.width;
if (isVertical)
middleWidget.y = parentCenter - (middleSize / 2);
else
middleWidget.x = parentCenter - (middleSize / 2);
let currentPos = isVertical ? middleWidget.y : middleWidget.x;
for (var i = middleIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width;
currentPos -= (widgetSpacing + size);
if (isVertical)
centerWidgets[i].y = currentPos;
else
centerWidgets[i].x = currentPos;
}
return;
}
if (!configuredLeftWidget || !configuredRightWidget) {
if (totalWidgets % 2 === 1) {
const middleIndex = Math.floor(totalWidgets / 2);
const middleWidget = centerWidgets[middleIndex];
if (!middleWidget) {
return;
}
const middleSize = isVertical ? middleWidget.height : middleWidget.width;
if (isVertical) {
middleWidget.y = parentCenter - (middleSize / 2);
} else {
middleWidget.x = parentCenter - (middleSize / 2);
}
let currentPos = isVertical ? middleWidget.y : middleWidget.x;
for (var i = middleIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width;
currentPos -= (spacing + size);
if (isVertical) {
centerWidgets[i].y = currentPos;
} else {
centerWidgets[i].x = currentPos;
}
}
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize;
for (var i = middleIndex + 1; i < totalWidgets; i++) {
currentPos += spacing;
if (isVertical) {
centerWidgets[i].y = currentPos;
} else {
centerWidgets[i].x = currentPos;
}
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width;
}
} else {
const leftIndex = (totalWidgets / 2) - 1;
const rightIndex = totalWidgets / 2;
const fallbackLeft = centerWidgets[leftIndex];
const fallbackRight = centerWidgets[rightIndex];
if (!fallbackLeft || !fallbackRight) {
return;
}
const halfSpacing = spacing / 2;
const leftSize = isVertical ? fallbackLeft.height : fallbackLeft.width;
if (isVertical) {
fallbackLeft.y = parentCenter - halfSpacing - leftSize;
fallbackRight.y = parentCenter + halfSpacing;
} else {
fallbackLeft.x = parentCenter - halfSpacing - leftSize;
fallbackRight.x = parentCenter + halfSpacing;
}
let currentPos = isVertical ? fallbackLeft.y : fallbackLeft.x;
for (var i = leftIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width;
currentPos -= (spacing + size);
if (isVertical) {
centerWidgets[i].y = currentPos;
} else {
centerWidgets[i].x = currentPos;
}
}
currentPos = (isVertical ? fallbackRight.y + fallbackRight.height : fallbackRight.x + fallbackRight.width);
for (var i = rightIndex + 1; i < totalWidgets; i++) {
currentPos += spacing;
if (isVertical) {
centerWidgets[i].y = currentPos;
} else {
centerWidgets[i].x = currentPos;
}
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width;
}
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize;
for (var i = middleIndex + 1; i < totalWidgets; i++) {
currentPos += widgetSpacing;
if (isVertical)
centerWidgets[i].y = currentPos;
else
centerWidgets[i].x = currentPos;
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width;
}
return;
}
const leftWidget = configuredLeftWidget;
const rightWidget = configuredRightWidget;
const leftIndex = centerWidgets.indexOf(leftWidget);
const rightIndex = centerWidgets.indexOf(rightWidget);
const halfSpacing = spacing / 2;
const leftSize = isVertical ? leftWidget.height : leftWidget.width;
if (isVertical) {
leftWidget.y = parentCenter - halfSpacing - leftSize;
rightWidget.y = parentCenter + halfSpacing;
} else {
leftWidget.x = parentCenter - halfSpacing - leftSize;
rightWidget.x = parentCenter + halfSpacing;
}
const leftIndex = (totalWidgets / 2) - 1;
const rightIndex = totalWidgets / 2;
const fallbackLeft = centerWidgets[leftIndex];
const fallbackRight = centerWidgets[rightIndex];
if (!fallbackLeft || !fallbackRight)
return;
const halfSpacing = widgetSpacing / 2;
const leftSize = isVertical ? fallbackLeft.height : fallbackLeft.width;
let currentPos = isVertical ? leftWidget.y : leftWidget.x;
for (var i = leftIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width;
currentPos -= (spacing + size);
if (isVertical) {
centerWidgets[i].y = currentPos;
fallbackLeft.y = parentCenter - halfSpacing - leftSize;
fallbackRight.y = parentCenter + halfSpacing;
} else {
centerWidgets[i].x = currentPos;
fallbackLeft.x = parentCenter - halfSpacing - leftSize;
fallbackRight.x = parentCenter + halfSpacing;
}
let currentPos = isVertical ? fallbackLeft.y : fallbackLeft.x;
for (var i = leftIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width;
currentPos -= (widgetSpacing + size);
if (isVertical)
centerWidgets[i].y = currentPos;
else
centerWidgets[i].x = currentPos;
}
currentPos = (isVertical ? fallbackRight.y + fallbackRight.height : fallbackRight.x + fallbackRight.width);
for (var i = rightIndex + 1; i < totalWidgets; i++) {
currentPos += widgetSpacing;
if (isVertical)
centerWidgets[i].y = currentPos;
else
centerWidgets[i].x = currentPos;
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width;
}
}
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width);
for (var i = rightIndex + 1; i < totalWidgets; i++) {
currentPos += spacing;
if (isVertical) {
centerWidgets[i].y = currentPos;
} else {
centerWidgets[i].x = currentPos;
}
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width;
}
}
}
function getWidgetVisible(widgetId) {
const widgetVisibility = {
"cpuUsage": DgopService.dgopAvailable,
"memUsage": DgopService.dgopAvailable,
"cpuTemp": DgopService.dgopAvailable,
"gpuTemp": DgopService.dgopAvailable,
"network_speed_monitor": DgopService.dgopAvailable
};
return widgetVisibility[widgetId] ?? true;
}
function getWidgetComponent(widgetId) {
// Build dynamic component map including plugins
let baseMap = {
"launcherButton": "launcherButtonComponent",
"workspaceSwitcher": "workspaceSwitcherComponent",
"focusedWindow": "focusedWindowComponent",
"runningApps": "runningAppsComponent",
"clock": "clockComponent",
"music": "mediaComponent",
"weather": "weatherComponent",
"systemTray": "systemTrayComponent",
"privacyIndicator": "privacyIndicatorComponent",
"clipboard": "clipboardComponent",
"cpuUsage": "cpuUsageComponent",
"memUsage": "memUsageComponent",
"diskUsage": "diskUsageComponent",
"cpuTemp": "cpuTempComponent",
"gpuTemp": "gpuTempComponent",
"notificationButton": "notificationButtonComponent",
"battery": "batteryComponent",
"controlCenterButton": "controlCenterButtonComponent",
"idleInhibitor": "idleInhibitorComponent",
"spacer": "spacerComponent",
"separator": "separatorComponent",
"network_speed_monitor": "networkComponent",
"keyboard_layout_name": "keyboardLayoutNameComponent",
"vpn": "vpnComponent",
"notepadButton": "notepadButtonComponent",
"colorPicker": "colorPickerComponent",
"systemUpdate": "systemUpdateComponent"
};
// For built-in components, get from components property
const componentKey = baseMap[widgetId];
if (componentKey && root.components[componentKey]) {
return root.components[componentKey];
return;
}
// For plugin components, get from PluginService
var parts = widgetId.split(":");
var pluginId = parts[0];
let pluginComponents = PluginService.getWidgetComponents();
return pluginComponents[pluginId] || null;
const leftWidget = configuredLeftWidget;
const rightWidget = configuredRightWidget;
const leftIndex = centerWidgets.indexOf(leftWidget);
const rightIndex = centerWidgets.indexOf(rightWidget);
const halfSpacing = widgetSpacing / 2;
const leftSize = isVertical ? leftWidget.height : leftWidget.width;
if (isVertical) {
leftWidget.y = parentCenter - halfSpacing - leftSize;
rightWidget.y = parentCenter + halfSpacing;
} else {
leftWidget.x = parentCenter - halfSpacing - leftSize;
rightWidget.x = parentCenter + halfSpacing;
}
let currentPos = isVertical ? leftWidget.y : leftWidget.x;
for (var i = leftIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width;
currentPos -= (widgetSpacing + size);
if (isVertical)
centerWidgets[i].y = currentPos;
else
centerWidgets[i].x = currentPos;
}
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width);
for (var i = rightIndex + 1; i < totalWidgets; i++) {
currentPos += widgetSpacing;
if (isVertical)
centerWidgets[i].y = currentPos;
else
centerWidgets[i].x = currentPos;
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width;
}
}
height: parent.height
@@ -392,177 +303,71 @@ Item {
onTriggered: root.updateLayout()
}
Component.onCompleted: {
layoutTimer.restart();
}
Component.onCompleted: layoutTimer.restart()
onWidthChanged: {
if (width > 0) {
if (width > 0)
layoutTimer.restart();
}
}
onHeightChanged: {
if (height > 0) {
if (height > 0)
layoutTimer.restart();
}
}
onVisibleChanged: {
if (visible && (isVertical ? height : width) > 0) {
if (visible && (isVertical ? height : width) > 0)
layoutTimer.restart();
}
}
Repeater {
id: centerRepeater
model: root.widgetsModel
Loader {
onCountChanged: layoutTimer.restart()
Item {
property var itemData: modelData
property string widgetId: itemData.widgetId
property var widgetData: itemData
property int spacerSize: itemData.size || 20
readonly property real itemSpacing: root.widgetSpacing
anchors.verticalCenter: !root.isVertical ? parent.verticalCenter : undefined
anchors.horizontalCenter: root.isVertical ? parent.horizontalCenter : undefined
active: root.getWidgetVisible(itemData.widgetId) && (itemData.widgetId !== "music" || MprisController.activePlayer !== null)
sourceComponent: root.getWidgetComponent(itemData.widgetId)
opacity: (itemData.enabled !== false) ? 1 : 0
asynchronous: false
width: widgetLoader.item ? widgetLoader.item.width : 0
height: widgetLoader.item ? widgetLoader.item.height : 0
onLoaded: {
if (!item) {
return;
}
item.widthChanged.connect(() => {
if (layoutTimer)
layoutTimer.restart();
});
item.heightChanged.connect(() => {
if (layoutTimer)
layoutTimer.restart();
});
if (root.axis && "axis" in item) {
item.axis = Qt.binding(() => root.axis);
}
if (root.axis && "isVertical" in item) {
try {
item.isVertical = Qt.binding(() => root.axis.isVertical);
} catch (e) {}
readonly property bool active: widgetLoader.active
readonly property var item: widgetLoader.item
WidgetHost {
id: widgetLoader
anchors.verticalCenter: !root.isVertical ? parent.verticalCenter : undefined
anchors.horizontalCenter: root.isVertical ? parent.horizontalCenter : undefined
widgetId: itemData.widgetId
widgetData: itemData
spacerSize: itemData.size || 20
components: root.components
isInColumn: root.isVertical
axis: root.axis
section: "center"
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
barSpacing: root.barSpacing
barConfig: root.barConfig
isFirst: index === 0
isLast: index === centerRepeater.count - 1
sectionSpacing: parent.itemSpacing
isLeftBarEdge: false
isRightBarEdge: false
isTopBarEdge: false
isBottomBarEdge: false
onContentItemReady: contentItem => {
contentItem.widthChanged.connect(() => layoutTimer.restart());
contentItem.heightChanged.connect(() => layoutTimer.restart());
}
// Inject properties for plugin widgets
if ("section" in item) {
item.section = root.section;
}
if ("parentScreen" in item) {
item.parentScreen = Qt.binding(() => root.parentScreen);
}
if ("widgetThickness" in item) {
item.widgetThickness = Qt.binding(() => root.widgetThickness);
}
if ("barThickness" in item) {
item.barThickness = Qt.binding(() => root.barThickness);
}
if ("barSpacing" in item) {
item.barSpacing = Qt.binding(() => root.barSpacing);
}
if ("barConfig" in item) {
item.barConfig = Qt.binding(() => root.barConfig);
}
if ("sectionSpacing" in item) {
item.sectionSpacing = Qt.binding(() => root.spacing);
}
if ("widgetData" in item) {
item.widgetData = Qt.binding(() => widgetData);
}
if ("isFirst" in item) {
item.isFirst = Qt.binding(() => {
for (var i = 0; i < centerRepeater.count; i++) {
const checkItem = centerRepeater.itemAt(i);
if (checkItem && checkItem.active && checkItem.item) {
return checkItem.item === item;
}
}
return false;
});
}
if ("isLast" in item) {
item.isLast = Qt.binding(() => {
for (var i = centerRepeater.count - 1; i >= 0; i--) {
const checkItem = centerRepeater.itemAt(i);
if (checkItem && checkItem.active && checkItem.item) {
return checkItem.item === item;
}
}
return false;
});
}
if ("isLeftBarEdge" in item) {
item.isLeftBarEdge = false;
}
if ("isRightBarEdge" in item) {
item.isRightBarEdge = false;
}
if ("isTopBarEdge" in item) {
item.isTopBarEdge = false;
}
if ("isBottomBarEdge" in item) {
item.isBottomBarEdge = false;
}
if (item.pluginService !== undefined) {
var parts = model.widgetId.split(":");
var pluginId = parts[0];
var variantId = parts.length > 1 ? parts[1] : null;
if (item.pluginId !== undefined) {
item.pluginId = pluginId;
}
if (item.variantId !== undefined) {
item.variantId = variantId;
}
if (item.variantData !== undefined && variantId) {
item.variantData = PluginService.getPluginVariantData(pluginId, variantId);
}
item.pluginService = PluginService;
}
if (item.popoutService !== undefined) {
item.popoutService = PopoutService;
}
layoutTimer.restart();
}
onActiveChanged: {
layoutTimer.restart();
}
}
}
Connections {
target: PluginService
function onPluginLoaded(pluginId) {
// Force refresh of component lookups
for (var i = 0; i < centerRepeater.count; i++) {
var item = centerRepeater.itemAt(i);
if (item && item.widgetId.startsWith(pluginId)) {
item.sourceComponent = root.getWidgetComponent(item.widgetId);
}
}
}
function onPluginUnloaded(pluginId) {
// Force refresh of component lookups
for (var i = 0; i < centerRepeater.count; i++) {
var item = centerRepeater.itemAt(i);
if (item && item.widgetId.startsWith(pluginId)) {
item.sourceComponent = root.getWidgetComponent(item.widgetId);
}
onActiveChanged: layoutTimer.restart()
}
}
}

View File

@@ -94,7 +94,7 @@ Item {
return true;
}
if (event.key === Qt.Key_Right) {
if (event.key === Qt.Key_Right || event.key === Qt.Key_L) {
if (gridIndex + 1 < visibleCount) {
gridIndex++;
} else if (currentPage < totalPages - 1) {
@@ -104,7 +104,7 @@ Item {
return true;
}
if (event.key === Qt.Key_Left) {
if (event.key === Qt.Key_Left || event.key === Qt.Key_H) {
if (gridIndex > 0) {
gridIndex--;
} else if (currentPage > 0) {
@@ -115,7 +115,7 @@ Item {
return true;
}
if (event.key === Qt.Key_Down) {
if (event.key === Qt.Key_Down || event.key === Qt.Key_J) {
if (gridIndex + columns < visibleCount) {
gridIndex += columns;
} else if (currentPage < totalPages - 1) {
@@ -125,7 +125,7 @@ Item {
return true;
}
if (event.key === Qt.Key_Up) {
if (event.key === Qt.Key_Up || event.key === Qt.Key_K) {
if (gridIndex >= columns) {
gridIndex -= columns;
} else if (currentPage > 0) {

View File

@@ -208,14 +208,11 @@ SWAY_EOF
;;
mangowc)
TEMP_DIR=$(mktemp -d)
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
cp "$COMPOSITOR_CONFIG" "$TEMP_DIR/config.conf"
exec mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
else
touch "$TEMP_DIR/config.conf"
exec mango -s "$QS_CMD && mmsg -d quit"
fi
export MANGOCONFIG="$TEMP_DIR"
exec mango -s "$QS_CMD && mmsg -d quit"
;;
*)

View File

@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
@@ -18,262 +19,365 @@ Rectangle {
property int gridRows: 2
property bool useGridLayout: false
signal closed()
property string holdAction: ""
property int holdActionIndex: -1
property real holdProgress: 0
property bool showHoldHint: false
readonly property bool needsConfirmation: SettingsData.powerActionConfirm
readonly property int holdDurationMs: SettingsData.powerActionHoldDuration * 1000
signal closed
function updateVisibleActions() {
const allActions = (typeof SettingsData !== "undefined" && SettingsData.powerMenuActions)
? SettingsData.powerMenuActions
: ["logout", "suspend", "hibernate", "reboot", "poweroff"]
const hibernateSupported = (typeof SessionService !== "undefined" && SessionService.hibernateSupported) || false
const allActions = (typeof SettingsData !== "undefined" && SettingsData.powerMenuActions) ? SettingsData.powerMenuActions : ["logout", "suspend", "hibernate", "reboot", "poweroff"];
const hibernateSupported = (typeof SessionService !== "undefined" && SessionService.hibernateSupported) || false;
let filtered = allActions.filter(action => {
if (action === "hibernate" && !hibernateSupported) return false
if (action === "lock") return false
if (action === "restart") return false
if (action === "logout" && !showLogout) return false
return true
})
if (action === "hibernate" && !hibernateSupported)
return false;
if (action === "lock")
return false;
if (action === "restart")
return false;
if (action === "logout" && !showLogout)
return false;
return true;
});
visibleActions = filtered
visibleActions = filtered;
useGridLayout = (typeof SettingsData !== "undefined" && SettingsData.powerMenuGridLayout !== undefined)
? SettingsData.powerMenuGridLayout
: false
if (!useGridLayout) return
const count = visibleActions.length
useGridLayout = (typeof SettingsData !== "undefined" && SettingsData.powerMenuGridLayout !== undefined) ? SettingsData.powerMenuGridLayout : false;
if (!useGridLayout)
return;
const count = visibleActions.length;
if (count === 0) {
gridColumns = 1
gridRows = 1
return
gridColumns = 1;
gridRows = 1;
return;
}
if (count <= 3) {
gridColumns = 1
gridRows = count
return
gridColumns = 1;
gridRows = count;
return;
}
if (count === 4) {
gridColumns = 2
gridRows = 2
return
gridColumns = 2;
gridRows = 2;
return;
}
gridColumns = 3
gridRows = Math.ceil(count / 3)
gridColumns = 3;
gridRows = Math.ceil(count / 3);
}
function getDefaultActionIndex() {
const defaultAction = (typeof SettingsData !== "undefined" && SettingsData.powerMenuDefaultAction)
? SettingsData.powerMenuDefaultAction
: "suspend"
const index = visibleActions.indexOf(defaultAction)
return index >= 0 ? index : 0
const defaultAction = (typeof SettingsData !== "undefined" && SettingsData.powerMenuDefaultAction) ? SettingsData.powerMenuDefaultAction : "suspend";
const index = visibleActions.indexOf(defaultAction);
return index >= 0 ? index : 0;
}
function getActionAtIndex(index) {
if (index < 0 || index >= visibleActions.length) return ""
return visibleActions[index]
if (index < 0 || index >= visibleActions.length)
return "";
return visibleActions[index];
}
function getActionData(action) {
switch (action) {
case "reboot":
return { "icon": "restart_alt", "label": I18n.tr("Reboot"), "key": "R" }
return {
"icon": "restart_alt",
"label": I18n.tr("Reboot"),
"key": "R"
};
case "logout":
return { "icon": "logout", "label": I18n.tr("Log Out"), "key": "X" }
return {
"icon": "logout",
"label": I18n.tr("Log Out"),
"key": "X"
};
case "poweroff":
return { "icon": "power_settings_new", "label": I18n.tr("Power Off"), "key": "P" }
return {
"icon": "power_settings_new",
"label": I18n.tr("Power Off"),
"key": "P"
};
case "suspend":
return { "icon": "bedtime", "label": I18n.tr("Suspend"), "key": "S" }
return {
"icon": "bedtime",
"label": I18n.tr("Suspend"),
"key": "S"
};
case "hibernate":
return { "icon": "ac_unit", "label": I18n.tr("Hibernate"), "key": "H" }
return {
"icon": "ac_unit",
"label": I18n.tr("Hibernate"),
"key": "H"
};
default:
return { "icon": "help", "label": action, "key": "?" }
return {
"icon": "help",
"label": action,
"key": "?"
};
}
}
function selectOption(action) {
if (!action) return
if (typeof SessionService === "undefined") return
hide()
function actionNeedsConfirm(action) {
return action !== "lock" && action !== "restart";
}
function startHold(action, actionIndex) {
if (!needsConfirmation || !actionNeedsConfirm(action)) {
executeAction(action);
return;
}
holdAction = action;
holdActionIndex = actionIndex;
holdProgress = 0;
showHoldHint = false;
holdTimer.start();
}
function cancelHold() {
if (holdAction === "")
return;
const wasHolding = holdProgress > 0;
holdTimer.stop();
if (wasHolding && holdProgress < 1) {
showHoldHint = true;
hintTimer.restart();
}
holdAction = "";
holdActionIndex = -1;
holdProgress = 0;
}
function completeHold() {
if (holdProgress < 1) {
cancelHold();
return;
}
const action = holdAction;
holdTimer.stop();
holdAction = "";
holdActionIndex = -1;
holdProgress = 0;
executeAction(action);
}
function executeAction(action) {
if (!action)
return;
if (typeof SessionService === "undefined")
return;
hide();
switch (action) {
case "logout":
SessionService.logout()
break
SessionService.logout();
break;
case "suspend":
SessionService.suspend()
break
SessionService.suspend();
break;
case "hibernate":
SessionService.hibernate()
break
SessionService.hibernate();
break;
case "reboot":
SessionService.reboot()
break
SessionService.reboot();
break;
case "poweroff":
SessionService.poweroff()
break
SessionService.poweroff();
break;
}
}
function selectOption(action, actionIndex) {
startHold(action, actionIndex !== undefined ? actionIndex : -1);
}
function show() {
updateVisibleActions()
const defaultIndex = getDefaultActionIndex()
holdAction = "";
holdActionIndex = -1;
holdProgress = 0;
showHoldHint = false;
updateVisibleActions();
const defaultIndex = getDefaultActionIndex();
if (useGridLayout) {
selectedRow = Math.floor(defaultIndex / gridColumns)
selectedCol = defaultIndex % gridColumns
selectedIndex = defaultIndex
selectedRow = Math.floor(defaultIndex / gridColumns);
selectedCol = defaultIndex % gridColumns;
selectedIndex = defaultIndex;
} else {
selectedIndex = defaultIndex
selectedIndex = defaultIndex;
}
isVisible = true
Qt.callLater(() => powerMenuFocusScope.forceActiveFocus())
isVisible = true;
Qt.callLater(() => powerMenuFocusScope.forceActiveFocus());
}
function hide() {
isVisible = false
closed()
cancelHold();
isVisible = false;
closed();
}
function handleListNavigation(event) {
function handleListNavigation(event, isPressed) {
if (!isPressed) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_S || event.key === Qt.Key_H || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
cancelHold();
event.accepted = true;
}
return;
}
switch (event.key) {
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length
event.accepted = true
break
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
event.accepted = true;
break;
case Qt.Key_Down:
case Qt.Key_Tab:
selectedIndex = (selectedIndex + 1) % visibleActions.length
event.accepted = true
break
selectedIndex = (selectedIndex + 1) % visibleActions.length;
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
selectOption(getActionAtIndex(selectedIndex))
event.accepted = true
break
startHold(getActionAtIndex(selectedIndex), selectedIndex);
event.accepted = true;
break;
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % visibleActions.length
event.accepted = true
selectedIndex = (selectedIndex + 1) % visibleActions.length;
event.accepted = true;
}
break
break;
case Qt.Key_P:
if (!(event.modifiers & Qt.ControlModifier)) {
selectOption("poweroff")
event.accepted = true
const idx = visibleActions.indexOf("poweroff");
startHold("poweroff", idx);
event.accepted = true;
} else {
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length
event.accepted = true
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
event.accepted = true;
}
break
break;
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % visibleActions.length
event.accepted = true
selectedIndex = (selectedIndex + 1) % visibleActions.length;
event.accepted = true;
}
break
break;
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length
event.accepted = true
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
event.accepted = true;
}
break
break;
case Qt.Key_R:
selectOption("reboot")
event.accepted = true
break
startHold("reboot", visibleActions.indexOf("reboot"));
event.accepted = true;
break;
case Qt.Key_X:
selectOption("logout")
event.accepted = true
break
startHold("logout", visibleActions.indexOf("logout"));
event.accepted = true;
break;
case Qt.Key_S:
selectOption("suspend")
event.accepted = true
break
startHold("suspend", visibleActions.indexOf("suspend"));
event.accepted = true;
break;
case Qt.Key_H:
selectOption("hibernate")
event.accepted = true
break
startHold("hibernate", visibleActions.indexOf("hibernate"));
event.accepted = true;
break;
}
}
function handleGridNavigation(event) {
function handleGridNavigation(event, isPressed) {
if (!isPressed) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_S || event.key === Qt.Key_H || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
cancelHold();
event.accepted = true;
}
return;
}
switch (event.key) {
case Qt.Key_Left:
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
break
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
break;
case Qt.Key_Right:
selectedCol = (selectedCol + 1) % gridColumns
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
break
selectedCol = (selectedCol + 1) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
break;
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedRow = (selectedRow - 1 + gridRows) % gridRows
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
break
selectedRow = (selectedRow - 1 + gridRows) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
break;
case Qt.Key_Down:
case Qt.Key_Tab:
selectedRow = (selectedRow + 1) % gridRows
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
break
selectedRow = (selectedRow + 1) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
selectOption(getActionAtIndex(selectedIndex))
event.accepted = true
break
startHold(getActionAtIndex(selectedIndex), selectedIndex);
event.accepted = true;
break;
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
selectedCol = (selectedCol + 1) % gridColumns
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
selectedCol = (selectedCol + 1) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
}
break
break;
case Qt.Key_P:
if (!(event.modifiers & Qt.ControlModifier)) {
selectOption("poweroff")
event.accepted = true
const idx = visibleActions.indexOf("poweroff");
startHold("poweroff", idx);
event.accepted = true;
} else {
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
}
break
break;
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
selectedRow = (selectedRow + 1) % gridRows
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
selectedRow = (selectedRow + 1) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
}
break
break;
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
selectedRow = (selectedRow - 1 + gridRows) % gridRows
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
selectedRow = (selectedRow - 1 + gridRows) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
}
break
break;
case Qt.Key_R:
selectOption("reboot")
event.accepted = true
break
startHold("reboot", visibleActions.indexOf("reboot"));
event.accepted = true;
break;
case Qt.Key_X:
selectOption("logout")
event.accepted = true
break
startHold("logout", visibleActions.indexOf("logout"));
event.accepted = true;
break;
case Qt.Key_S:
selectOption("suspend")
event.accepted = true
break
startHold("suspend", visibleActions.indexOf("suspend"));
event.accepted = true;
break;
case Qt.Key_H:
selectOption("hibernate")
event.accepted = true
break
startHold("hibernate", visibleActions.indexOf("hibernate"));
event.accepted = true;
break;
}
}
@@ -287,29 +391,62 @@ Rectangle {
onClicked: root.hide()
}
Timer {
id: holdTimer
interval: 16
repeat: true
onTriggered: {
root.holdProgress = Math.min(1, root.holdProgress + (interval / root.holdDurationMs));
if (root.holdProgress >= 1) {
stop();
root.completeHold();
}
}
}
Timer {
id: hintTimer
interval: 2000
onTriggered: root.showHoldHint = false
}
FocusScope {
id: powerMenuFocusScope
anchors.fill: parent
focus: root.isVisible
onVisibleChanged: {
if (visible) Qt.callLater(() => forceActiveFocus())
if (visible)
Qt.callLater(() => forceActiveFocus());
}
Keys.onEscapePressed: root.hide()
Keys.onPressed: event => {
if (event.isAutoRepeat) {
event.accepted = true;
return;
}
if (useGridLayout) {
handleGridNavigation(event)
handleGridNavigation(event, true);
} else {
handleListNavigation(event)
handleListNavigation(event, true);
}
}
Keys.onReleased: event => {
if (event.isAutoRepeat) {
event.accepted = true;
return;
}
if (useGridLayout) {
handleGridNavigation(event, false);
} else {
handleListNavigation(event, false);
}
}
Rectangle {
anchors.centerIn: parent
width: useGridLayout
? Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2)
: 320
width: useGridLayout ? Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2) : 320
height: contentItem.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
@@ -320,7 +457,7 @@ Rectangle {
id: contentItem
anchors.fill: parent
anchors.margins: Theme.spacingL
implicitHeight: headerRow.height + Theme.spacingM + (useGridLayout ? buttonGrid.implicitHeight : buttonColumn.implicitHeight)
implicitHeight: headerRow.height + Theme.spacingM + (useGridLayout ? buttonGrid.implicitHeight : buttonColumn.implicitHeight) + (root.needsConfirmation ? hintRow.height + Theme.spacingM : 0)
Row {
id: headerRow
@@ -363,48 +500,86 @@ Rectangle {
model: root.visibleActions
Rectangle {
id: gridButtonRect
required property int index
required property string modelData
readonly property var actionData: root.getActionData(modelData)
readonly property bool isSelected: root.selectedIndex === index
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
width: (contentItem.width - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
height: 100
radius: Theme.cornerRadius
color: {
if (isSelected) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
if (mouseArea.containsMouse) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
if (isSelected)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
if (mouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: isSelected ? Theme.primary : "transparent"
border.width: isSelected ? 2 : 0
Rectangle {
id: gridProgressMask
anchors.fill: parent
radius: parent.radius
visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
visible: gridButtonRect.isHolding
layer.enabled: gridButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: gridProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * root.holdProgress
color: {
if (gridButtonRect.modelData === "poweroff")
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
if (gridButtonRect.modelData === "reboot")
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.3);
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
}
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: parent.parent.actionData.icon
name: gridButtonRect.actionData.icon
size: Theme.iconSize + 8
color: {
if (parent.parent.showWarning && mouseArea.containsMouse) {
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning
if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) {
return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
}
return Theme.surfaceText
return Theme.surfaceText;
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: parent.parent.actionData.label
text: gridButtonRect.actionData.label
font.pixelSize: Theme.fontSizeMedium
color: {
if (parent.parent.showWarning && mouseArea.containsMouse) {
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning
if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) {
return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
}
return Theme.surfaceText
return Theme.surfaceText;
}
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
@@ -418,7 +593,7 @@ Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: parent.parent.parent.actionData.key
text: gridButtonRect.actionData.key
font.pixelSize: Theme.fontSizeSmall - 1
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
@@ -432,11 +607,14 @@ Rectangle {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.selectedRow = Math.floor(index / root.gridColumns)
root.selectedCol = index % root.gridColumns
root.selectOption(modelData)
onPressed: {
root.selectedRow = Math.floor(index / root.gridColumns);
root.selectedCol = index % root.gridColumns;
root.selectedIndex = index;
root.startHold(modelData, index);
}
onReleased: root.cancelHold()
onCanceled: root.cancelHold()
}
}
}
@@ -455,24 +633,62 @@ Rectangle {
model: root.visibleActions
Rectangle {
id: listButtonRect
required property int index
required property string modelData
readonly property var actionData: root.getActionData(modelData)
readonly property bool isSelected: root.selectedIndex === index
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
if (isSelected) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
if (listMouseArea.containsMouse) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
if (isSelected)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
if (listMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: isSelected ? Theme.primary : "transparent"
border.width: isSelected ? 2 : 0
Rectangle {
id: listProgressMask
anchors.fill: parent
radius: parent.radius
visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
visible: listButtonRect.isHolding
layer.enabled: listButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: listProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * root.holdProgress
color: {
if (listButtonRect.modelData === "poweroff")
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
if (listButtonRect.modelData === "reboot")
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.3);
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
}
}
}
Row {
anchors.left: parent.left
anchors.right: parent.right
@@ -482,25 +698,25 @@ Rectangle {
spacing: Theme.spacingM
DankIcon {
name: parent.parent.actionData.icon
name: listButtonRect.actionData.icon
size: Theme.iconSize + 4
color: {
if (parent.parent.showWarning && listMouseArea.containsMouse) {
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning
if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) {
return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
}
return Theme.surfaceText
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: parent.parent.actionData.label
text: listButtonRect.actionData.label
font.pixelSize: Theme.fontSizeMedium
color: {
if (parent.parent.showWarning && listMouseArea.containsMouse) {
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning
if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) {
return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
}
return Theme.surfaceText
return Theme.surfaceText;
}
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
@@ -517,7 +733,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: parent.parent.actionData.key
text: listButtonRect.actionData.key
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
@@ -530,14 +746,53 @@ Rectangle {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.selectedIndex = index
root.selectOption(modelData)
onPressed: {
root.selectedIndex = index;
root.startHold(modelData, index);
}
onReleased: root.cancelHold()
onCanceled: root.cancelHold()
}
}
}
}
Row {
id: hintRow
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingS
spacing: Theme.spacingXS
visible: root.needsConfirmation
opacity: root.showHoldHint ? 1 : 0.5
Behavior on opacity {
NumberAnimation {
duration: 150
}
}
DankIcon {
name: root.showHoldHint ? "warning" : "touch_app"
size: Theme.fontSizeSmall
color: root.showHoldHint ? Theme.warning : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
readonly property int remainingSeconds: Math.ceil(SettingsData.powerActionHoldDuration * (1 - root.holdProgress))
text: {
if (root.showHoldHint)
return I18n.tr("Hold longer to confirm");
if (root.holdProgress > 0)
return I18n.tr("Hold to confirm (%1s)").arg(remainingSeconds);
return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration);
}
font.pixelSize: Theme.fontSizeSmall
color: root.showHoldHint ? Theme.warning : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}

View File

@@ -44,6 +44,9 @@ Item {
readonly property bool hasVerticalPill: verticalBarPill !== null
readonly property bool hasPopout: popoutContent !== null
readonly property int iconSize: Theme.barIconSize(barThickness, -4)
readonly property int iconSizeLarge: Theme.barIconSize(barThickness)
Component.onCompleted: {
loadPluginData();
}

View File

@@ -45,10 +45,14 @@ Item {
Item {
width: parent.width
height: logoModeGroup.implicitHeight
clip: true
DankButtonGroup {
id: logoModeGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 480 ? 44 : 64
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: {
const modes = [I18n.tr("Apps Icon"), I18n.tr("OS Logo"), I18n.tr("Dank")];
if (CompositorService.isNiri) {
@@ -153,78 +157,88 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Item {
width: parent.width
height: colorOverrideRow.implicitHeight
clip: true
DankButtonGroup {
id: colorModeGroup
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
currentIndex: {
const override = SettingsData.launcherLogoColorOverride;
if (override === "")
return 0;
if (override === "primary")
return 1;
if (override === "surface")
return 2;
return 3;
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
switch (index) {
case 0:
SettingsData.set("launcherLogoColorOverride", "");
break;
case 1:
SettingsData.set("launcherLogoColorOverride", "primary");
break;
case 2:
SettingsData.set("launcherLogoColorOverride", "surface");
break;
case 3:
const currentOverride = SettingsData.launcherLogoColorOverride;
const isPreset = currentOverride === "" || currentOverride === "primary" || currentOverride === "surface";
if (isPreset) {
SettingsData.set("launcherLogoColorOverride", "#ffffff");
}
break;
Row {
id: colorOverrideRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
DankButtonGroup {
id: colorModeGroup
buttonPadding: parent.parent.width < 480 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.parent.width < 480 ? 44 : 64
textSize: parent.parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
currentIndex: {
const override = SettingsData.launcherLogoColorOverride;
if (override === "")
return 0;
if (override === "primary")
return 1;
if (override === "surface")
return 2;
return 3;
}
}
}
Rectangle {
visible: {
const override = SettingsData.launcherLogoColorOverride;
return override !== "" && override !== "primary" && override !== "surface";
}
width: 36
height: 36
radius: 18
color: {
const override = SettingsData.launcherLogoColorOverride;
if (override !== "" && override !== "primary" && override !== "surface") {
return override;
}
return "#ffffff";
}
border.color: Theme.outline
border.width: 1
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!PopoutService.colorPickerModal)
onSelectionChanged: (index, selected) => {
if (!selected)
return;
PopoutService.colorPickerModal.selectedColor = SettingsData.launcherLogoColorOverride;
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Launcher Logo Color");
PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) {
SettingsData.set("launcherLogoColorOverride", selectedColor);
};
PopoutService.colorPickerModal.show();
switch (index) {
case 0:
SettingsData.set("launcherLogoColorOverride", "");
break;
case 1:
SettingsData.set("launcherLogoColorOverride", "primary");
break;
case 2:
SettingsData.set("launcherLogoColorOverride", "surface");
break;
case 3:
const currentOverride = SettingsData.launcherLogoColorOverride;
const isPreset = currentOverride === "" || currentOverride === "primary" || currentOverride === "surface";
if (isPreset) {
SettingsData.set("launcherLogoColorOverride", "#ffffff");
}
break;
}
}
}
Rectangle {
id: colorPickerCircle
visible: {
const override = SettingsData.launcherLogoColorOverride;
return override !== "" && override !== "primary" && override !== "surface";
}
width: 36
height: 36
radius: 18
color: {
const override = SettingsData.launcherLogoColorOverride;
if (override !== "" && override !== "primary" && override !== "surface")
return override;
return "#ffffff";
}
border.color: Theme.outline
border.width: 1
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!PopoutService.colorPickerModal)
return;
PopoutService.colorPickerModal.selectedColor = SettingsData.launcherLogoColorOverride;
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Launcher Logo Color");
PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) {
SettingsData.set("launcherLogoColorOverride", selectedColor);
};
PopoutService.colorPickerModal.show();
}
}
}
}
@@ -329,28 +343,32 @@ Item {
SettingsToggleRow {
text: I18n.tr("Close Overview on Launch")
description: I18n.tr("When enabled, launching an app from the launcher will automatically close the Niri overview if it's open.")
description: I18n.tr("Auto-close Niri overview when launching apps.")
checked: SettingsData.spotlightCloseNiriOverview
onToggled: checked => SettingsData.set("spotlightCloseNiriOverview", checked)
}
SettingsToggleRow {
text: I18n.tr("Enable Overview Overlay")
description: I18n.tr("When enabled, shows the launcher overlay when typing in Niri overview mode. Disable this if you prefer to not have the launcher when typing on Niri overview or want to use other launcher in the overview.")
description: I18n.tr("Show launcher overlay when typing in Niri overview. Disable to use another launcher.")
checked: SettingsData.niriOverviewOverlayEnabled
onToggled: checked => SettingsData.set("niriOverviewOverlayEnabled", checked)
}
}
SettingsCard {
id: recentAppsCard
width: parent.width
iconName: "history"
title: I18n.tr("Recently Used Apps")
property var rankedAppsModel: {
var ranking = AppUsageHistoryData.appUsageRanking;
if (!ranking)
return [];
var apps = [];
for (var appId in (AppUsageHistoryData.appUsageRanking || {})) {
var appData = (AppUsageHistoryData.appUsageRanking || {})[appId];
for (var appId in ranking) {
var appData = ranking[appId];
apps.push({
"id": appId,
"name": appData.name,
@@ -401,7 +419,7 @@ Item {
spacing: Theme.spacingS
Repeater {
model: parent.parent.rankedAppsModel
model: recentAppsCard.rankedAppsModel
delegate: Rectangle {
width: rankedAppsList.width
@@ -496,11 +514,11 @@ Item {
StyledText {
width: parent.width
text: parent.parent.rankedAppsModel.length === 0 ? "No apps have been launched yet." : ""
text: "No apps have been launched yet."
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: parent.parent.rankedAppsModel.length === 0
visible: recentAppsCard.rankedAppsModel.length === 0
}
}
}

View File

@@ -130,7 +130,7 @@ Item {
{
"id": "gpuTemp",
"text": I18n.tr("GPU Temperature"),
"description": I18n.tr("GPU temperature display"),
"description": "",
"icon": "auto_awesome_mosaic",
"warning": !DgopService.dgopAvailable ? I18n.tr("Requires 'dgop' tool") : I18n.tr("This widget prevents GPU power off states, which can significantly impact battery life on laptops. It is not recommended to use this on laptops with hybrid graphics."),
"enabled": DgopService.dgopAvailable

View File

@@ -168,7 +168,17 @@ Column {
}
StyledText {
text: modelData.description
text: {
if (modelData.id === "gpuTemp") {
var selectedIdx = modelData.selectedGpuIndex !== undefined ? modelData.selectedGpuIndex : 0;
if (DgopService.availableGpus && DgopService.availableGpus.length > selectedIdx) {
var gpu = DgopService.availableGpus[selectedIdx];
return gpu.driver ? gpu.driver.toUpperCase() : "";
}
return I18n.tr("No GPU detected");
}
return modelData.description;
}
font.pixelSize: Theme.fontSizeSmall
color: modelData.enabled ? Theme.outline : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
elide: Text.ElideRight
@@ -185,39 +195,37 @@ Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Item {
width: 60
height: 32
DankActionButton {
id: gpuMenuButton
visible: modelData.id === "gpuTemp"
buttonSize: 32
iconName: "more_vert"
iconSize: 18
iconColor: Theme.outline
onClicked: {
gpuContextMenu.widgetData = modelData;
gpuContextMenu.sectionId = root.sectionId;
gpuContextMenu.widgetIndex = index;
DankDropdown {
id: gpuDropdown
anchors.fill: parent
popupWidth: -1
currentValue: {
var selectedIndex = modelData.selectedGpuIndex !== undefined ? modelData.selectedGpuIndex : 0;
if (DgopService.availableGpus && DgopService.availableGpus.length > selectedIndex && selectedIndex >= 0) {
var gpu = DgopService.availableGpus[selectedIndex];
return gpu.driver.toUpperCase();
}
return DgopService.availableGpus && DgopService.availableGpus.length > 0 ? DgopService.availableGpus[0].driver.toUpperCase() : "";
var buttonPos = gpuMenuButton.mapToItem(root, 0, 0);
var popupWidth = gpuContextMenu.width;
var popupHeight = gpuContextMenu.height;
var xPos = buttonPos.x - popupWidth - Theme.spacingS;
if (xPos < 0) {
xPos = buttonPos.x + gpuMenuButton.width + Theme.spacingS;
}
options: {
var gpuOptions = [];
if (DgopService.availableGpus && DgopService.availableGpus.length > 0) {
for (var i = 0; i < DgopService.availableGpus.length; i++) {
var gpu = DgopService.availableGpus[i];
gpuOptions.push(gpu.driver.toUpperCase());
}
}
return gpuOptions;
}
onValueChanged: value => {
var gpuIndex = options.indexOf(value);
if (gpuIndex >= 0) {
root.gpuSelectionChanged(root.sectionId, index, gpuIndex);
}
var yPos = buttonPos.y - popupHeight / 2 + gpuMenuButton.height / 2;
if (yPos < 0) {
yPos = Theme.spacingS;
} else if (yPos + popupHeight > root.height) {
yPos = root.height - popupHeight - Theme.spacingS;
}
gpuContextMenu.x = xPos;
gpuContextMenu.y = yPos;
gpuContextMenu.open();
}
}
@@ -1123,4 +1131,112 @@ Column {
}
}
}
Popup {
id: gpuContextMenu
property var widgetData: null
property string sectionId: ""
property int widgetIndex: -1
width: 250
height: gpuMenuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
}
contentItem: Item {
Column {
id: gpuMenuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 2
Repeater {
model: DgopService.availableGpus || []
delegate: Rectangle {
required property var modelData
required property int index
width: gpuMenuColumn.width
height: 40
radius: Theme.cornerRadius
color: gpuOptionArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
property bool isSelected: {
var selectedIdx = gpuContextMenu.widgetData ? (gpuContextMenu.widgetData.selectedGpuIndex !== undefined ? gpuContextMenu.widgetData.selectedGpuIndex : 0) : 0;
return index === selectedIdx;
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: checkIcon.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "memory"
size: 18
color: isSelected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: modelData.driver ? modelData.driver.toUpperCase() : ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: isSelected ? Theme.primary : Theme.surfaceText
}
StyledText {
text: modelData.displayName || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: 180
}
}
}
DankIcon {
id: checkIcon
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
name: "check"
size: 18
color: Theme.primary
visible: isSelected
}
MouseArea {
id: gpuOptionArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.gpuSelectionChanged(gpuContextMenu.sectionId, gpuContextMenu.widgetIndex, index);
gpuContextMenu.close();
}
}
}
}
}
}
}
}

View File

@@ -357,6 +357,35 @@ Singleton {
return devices.length > 0 ? devices[0].id : "";
}
function getPinnedDeviceForFocusedScreen() {
const focusedScreen = CompositorService.getFocusedScreen();
if (!focusedScreen)
return "";
const pins = SettingsData.brightnessDevicePins || {};
const screenKey = SettingsData.getScreenDisplayName(focusedScreen);
if (!screenKey)
return "";
const pinnedDevice = pins[screenKey];
if (!pinnedDevice)
return "";
const deviceExists = devices.some(d => d.id === pinnedDevice);
if (!deviceExists)
return "";
return pinnedDevice;
}
function getPreferredDevice() {
const pinned = getPinnedDeviceForFocusedScreen();
if (pinned)
return pinned;
return getDefaultDevice();
}
function getCurrentDeviceInfo() {
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice);
if (!deviceToUse) {
@@ -809,110 +838,95 @@ Singleton {
// IPC Handler for external control
IpcHandler {
function set(percentage: string, device: string): string {
if (!root.brightnessAvailable) {
if (!root.brightnessAvailable)
return "Brightness control not available";
}
const value = parseInt(percentage);
if (isNaN(value)) {
if (isNaN(value))
return "Invalid brightness value: " + percentage;
}
const targetDevice = device || "";
const actualDevice = device || root.getPreferredDevice();
if (targetDevice && !root.devices.some(d => d.id === targetDevice)) {
return "Device not found: " + targetDevice;
}
if (actualDevice && !root.devices.some(d => d.id === actualDevice))
return "Device not found: " + actualDevice;
const deviceInfo = targetDevice ? root.getCurrentDeviceInfoByName(targetDevice) : null;
const deviceInfo = actualDevice ? root.getCurrentDeviceInfoByName(actualDevice) : null;
const minValue = (deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc")) ? 1 : 0;
const clampedValue = Math.max(minValue, Math.min(100, value));
root.lastIpcDevice = targetDevice;
if (targetDevice && targetDevice !== root.currentDevice) {
root.setCurrentDevice(targetDevice, false);
}
root.setBrightness(clampedValue, targetDevice);
root.lastIpcDevice = actualDevice;
if (actualDevice && actualDevice !== root.currentDevice)
root.setCurrentDevice(actualDevice, false);
if (targetDevice) {
return "Brightness set to " + clampedValue + "% on " + targetDevice;
} else {
return "Brightness set to " + clampedValue + "%";
}
root.setBrightness(clampedValue, actualDevice);
return actualDevice ? "Brightness set to " + clampedValue + "% on " + actualDevice : "Brightness set to " + clampedValue + "%";
}
function increment(step: string, device: string): string {
if (!root.brightnessAvailable) {
if (!root.brightnessAvailable)
return "Brightness control not available";
}
const targetDevice = device || "";
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice;
const actualDevice = device || root.getPreferredDevice();
if (actualDevice && !root.devices.some(d => d.id === actualDevice)) {
if (actualDevice && !root.devices.some(d => d.id === actualDevice))
return "Device not found: " + actualDevice;
}
const stepValue = parseInt(step || "5");
root.lastIpcDevice = actualDevice;
if (actualDevice && actualDevice !== root.currentDevice) {
if (actualDevice && actualDevice !== root.currentDevice)
root.setCurrentDevice(actualDevice, false);
}
const isExponential = SessionData.getBrightnessExponential(actualDevice);
const currentBrightness = root.getDeviceBrightness(actualDevice);
const deviceInfo = root.getCurrentDeviceInfoByName(actualDevice);
let maxValue = 100;
if (isExponential) {
maxValue = 100;
} else {
maxValue = deviceInfo?.displayMax || 100;
}
const maxValue = isExponential ? 100 : (deviceInfo?.displayMax || 100);
const newBrightness = Math.min(maxValue, currentBrightness + stepValue);
root.setBrightness(newBrightness, actualDevice);
return "Brightness increased by " + stepValue + "%" + (targetDevice ? " on " + targetDevice : "");
return "Brightness increased by " + stepValue + "%" + (device ? " on " + actualDevice : "");
}
function decrement(step: string, device: string): string {
if (!root.brightnessAvailable) {
if (!root.brightnessAvailable)
return "Brightness control not available";
}
const targetDevice = device || "";
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice;
const actualDevice = device || root.getPreferredDevice();
if (actualDevice && !root.devices.some(d => d.id === actualDevice)) {
if (actualDevice && !root.devices.some(d => d.id === actualDevice))
return "Device not found: " + actualDevice;
}
const stepValue = parseInt(step || "5");
root.lastIpcDevice = actualDevice;
if (actualDevice && actualDevice !== root.currentDevice) {
if (actualDevice && actualDevice !== root.currentDevice)
root.setCurrentDevice(actualDevice, false);
}
const isExponential = SessionData.getBrightnessExponential(actualDevice);
const currentBrightness = root.getDeviceBrightness(actualDevice);
const deviceInfo = root.getCurrentDeviceInfoByName(actualDevice);
let minValue = 0;
if (isExponential) {
switch (true) {
case isExponential:
minValue = 1;
} else {
minValue = (deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc")) ? 1 : 0;
break;
case deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc"):
minValue = 1;
break;
default:
minValue = 0;
break;
}
const newBrightness = Math.max(minValue, currentBrightness - stepValue);
root.setBrightness(newBrightness, actualDevice);
return "Brightness decreased by " + stepValue + "%" + (targetDevice ? " on " + targetDevice : "");
return "Brightness decreased by " + stepValue + "%" + (device ? " on " + actualDevice : "");
}
function status(): string {

View File

@@ -197,6 +197,8 @@ Singleton {
property bool _settingsWantsOpen: false
property bool _settingsWantsToggle: false
property string _settingsPendingTab: ""
function openSettings() {
if (settingsModal) {
settingsModal.show();
@@ -207,6 +209,19 @@ Singleton {
}
}
function openSettingsWithTab(tabName: string) {
if (settingsModal) {
settingsModal.showWithTabName(tabName);
return;
}
if (settingsModalLoader) {
_settingsPendingTab = tabName;
_settingsWantsOpen = true;
_settingsWantsToggle = false;
settingsModalLoader.activeAsync = true;
}
}
function closeSettings() {
settingsModal?.close();
}
@@ -221,6 +236,22 @@ Singleton {
}
}
function toggleSettingsWithTab(tabName: string) {
if (settingsModal) {
var idx = settingsModal.resolveTabIndex(tabName);
if (idx >= 0)
settingsModal.currentTabIndex = idx;
settingsModal.toggle();
return;
}
if (settingsModalLoader) {
_settingsPendingTab = tabName;
_settingsWantsToggle = true;
_settingsWantsOpen = false;
settingsModalLoader.activeAsync = true;
}
}
function focusOrToggleSettings() {
if (settingsModal?.visible) {
const settingsTitle = I18n.tr("Settings", "settings window title");
@@ -238,6 +269,26 @@ Singleton {
openSettings();
}
function focusOrToggleSettingsWithTab(tabName: string) {
if (settingsModal?.visible) {
const settingsTitle = I18n.tr("Settings", "settings window title");
for (const toplevel of ToplevelManager.toplevels.values) {
if (toplevel.title !== "Settings" && toplevel.title !== settingsTitle)
continue;
if (toplevel.activated) {
settingsModal.hide();
return;
}
var idx = settingsModal.resolveTabIndex(tabName);
if (idx >= 0)
settingsModal.currentTabIndex = idx;
toplevel.activate();
return;
}
}
openSettingsWithTab(tabName);
}
function unloadSettings() {
if (settingsModalLoader) {
settingsModal = null;
@@ -248,9 +299,22 @@ Singleton {
function _onSettingsModalLoaded() {
if (_settingsWantsOpen) {
_settingsWantsOpen = false;
settingsModal?.show();
} else if (_settingsWantsToggle) {
if (_settingsPendingTab) {
settingsModal?.showWithTabName(_settingsPendingTab);
_settingsPendingTab = "";
} else {
settingsModal?.show();
}
return;
}
if (_settingsWantsToggle) {
_settingsWantsToggle = false;
if (_settingsPendingTab) {
var idx = settingsModal?.resolveTabIndex(_settingsPendingTab) ?? -1;
if (idx >= 0)
settingsModal.currentTabIndex = idx;
_settingsPendingTab = "";
}
settingsModal?.toggle();
}
}

View File

@@ -37,6 +37,6 @@ StyledRect {
onClicked: root.clicked()
onEntered: root.entered()
onExited: root.exited()
tooltipText: tooltipText
tooltipText: root.tooltipText
}
}

View File

@@ -122,9 +122,10 @@ PanelWindow {
width: parent.width
x: Theme.snap(slideContainer.slideOffset, root.dpr)
DankRectangle {
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainer
radius: Theme.cornerRadius
}
Column {

View File

@@ -11,7 +11,17 @@ Item {
if (!item)
return;
const windowContentItem = item.Window.window?.contentItem;
let windowContentItem = item.Window?.window?.contentItem;
if (!windowContentItem) {
let current = item;
while (current) {
if (current.Window?.window?.contentItem) {
windowContentItem = current.Window.window.contentItem;
break;
}
current = current.parent;
}
}
if (!windowContentItem)
return;

View File

@@ -21,29 +21,29 @@ MouseArea {
color: Qt.rgba(stateColor.r, stateColor.g, stateColor.b, stateOpacity)
}
Timer {
id: hoverDelay
interval: 1000
repeat: false
onTriggered: {
const p = root.mapToItem(null, parent.width / 2, parent.height + Theme.spacingXS)
tooltip.show(I18n.tr(""), p.x, p.y, null)
tooltip.show(root.tooltipText, root, 0, 0, "bottom");
}
}
onEntered: {
if (!tooltipText) { return }
hoverDelay.restart()
if (!tooltipText)
return;
hoverDelay.restart();
}
onExited: {
if (!tooltipText) { return }
hoverDelay.stop()
tooltip.hide()
if (!tooltipText)
return;
hoverDelay.stop();
tooltip.hide();
}
DankTooltip {
DankTooltipV2 {
id: tooltip
}
}

View File

@@ -133,6 +133,18 @@ Rectangle {
onClicked: DMSNetworkService.disconnectAllActive()
}
}
DankActionButton {
Layout.alignment: Qt.AlignVCenter
iconName: "settings"
buttonSize: 28
iconSize: 16
iconColor: Theme.surfaceVariantText
onClicked: {
PopoutService.closeControlCenter();
PopoutService.openSettingsWithTab("network");
}
}
}
Rectangle {