QuickSettings: editable widgets/button section in the ControlCenter

This commit is contained in:
ItsLemmy
2025-10-08 23:05:52 -04:00
parent c4d1a142ab
commit bff195309a
19 changed files with 542 additions and 357 deletions
+10
View File
@@ -246,6 +246,16 @@ Singleton {
property JsonObject controlCenter: JsonObject {
// Position: close_to_bar_button, center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
property string position: "close_to_bar_button"
property JsonObject widgets
widgets: JsonObject {
property list<var> quickSettings: [{
"id": "Bluetooth"
}, {
"id": "WiFi"
}, {
"id": "PowerProfile"
}]
}
}
// dock
+1 -1
View File
@@ -77,7 +77,7 @@ Item {
// Error handling
onWidgetIdChanged: {
if (widgetId && !BarWidgetRegistry.hasWidget(widgetId)) {
Logger.warn("BarWidgetLoader", "Widget not found in bar registry:", widgetId)
Logger.warn("BarWidgetLoader", "Widget not found in registry:", widgetId)
}
}
}
+24 -196
View File
@@ -4,9 +4,9 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import Quickshell.Services.UPower
import qs.Modules.Settings
import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.Extras
import qs.Commons
import qs.Services
import qs.Widgets
@@ -17,7 +17,6 @@ NBox {
property string uptimeText: "--"
property real spacing: Style.marginS * scaling
readonly property bool hasPP: PowerProfileService.available
ColumnLayout {
anchors.fill: parent
@@ -97,205 +96,34 @@ NBox {
}
}
RowLayout {
id: utilitiesRow
Layout.alignment: Qt.AlignVCenter
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
Layout.bottomMargin: Style.marginM * scaling
}
GridLayout {
id: grid
Layout.fillWidth: true
columns: 2
columnSpacing: Style.marginL * scaling
rowSpacing: Style.marginM * scaling
// Left group - Media & Display
Rectangle {
color: Color.mSurface
radius: Style.radiusM * scaling
Layout.preferredHeight: Style.baseWidgetSize * 1.2 * scaling
Layout.preferredWidth: childrenRect.width + (Style.marginS * scaling * 2)
RowLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
// Screen Recorder
NIconButton {
baseSize: Style.baseWidgetSize * 0.9
icon: "camera-video"
visible: ProgramCheckerService.gpuScreenRecorderAvailable
tooltipText: ScreenRecorderService.isRecording ? I18n.tr("tooltips.stop-screen-recording") : I18n.tr("tooltips.start-screen-recording")
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary
onClicked: {
ScreenRecorderService.toggleRecording()
if (!ScreenRecorderService.isRecording) {
var panel = PanelService.getPanel("controlCenterPanel")
panel?.close()
}
}
}
// Wallpaper
NIconButton {
baseSize: Style.baseWidgetSize * 0.9
visible: Settings.data.wallpaper.enabled
icon: "wallpaper-selector"
tooltipText: I18n.tr("tooltips.wallpaper-selector")
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
onRightClicked: WallpaperService.setRandomWallpaper()
}
// Night Light
NIconButton {
baseSize: Style.baseWidgetSize * 0.9
visible: ProgramCheckerService.wlsunsetAvailable
colorBg: Settings.data.nightLight.forced ? Color.mPrimary : Color.transparent
colorFg: Settings.data.nightLight.forced ? Color.mOnPrimary : Color.mPrimary
icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
tooltipText: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? I18n.tr("tooltips.night-light-forced") : I18n.tr("tooltips.night-light-enabled")) : I18n.tr("tooltips.night-light-disabled")
onClicked: {
if (!Settings.data.nightLight.enabled) {
Settings.data.nightLight.enabled = true
Settings.data.nightLight.forced = false
} else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) {
Settings.data.nightLight.forced = true
} else {
Settings.data.nightLight.enabled = false
Settings.data.nightLight.forced = false
}
}
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
}
}
}
}
// Spacer
Item {
Layout.fillWidth: true
}
// Center group - Network & Caffeine
Rectangle {
color: Color.mSurface
radius: Style.radiusM * scaling
Layout.preferredHeight: Style.baseWidgetSize * 1.2 * scaling
Layout.preferredWidth: childrenRect.width + (Style.marginS * scaling * 2)
RowLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
// Wifi
NIconButton {
id: wifiButton
baseSize: Style.baseWidgetSize * 0.9
tooltipText: I18n.tr("tooltips.manage-wifi")
icon: {
try {
if (NetworkService.ethernetConnected) {
return "ethernet"
}
let connected = false
let signalStrength = 0
for (const net in NetworkService.networks) {
if (NetworkService.networks[net].connected) {
connected = true
signalStrength = NetworkService.networks[net].signal
break
}
}
return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off"
} catch (error) {
Logger.error("Wi-Fi", "Error getting icon:", error)
return "signal_wifi_bad"
}
}
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
}
// Bluetooth
NIconButton {
baseSize: Style.baseWidgetSize * 0.9
tooltipText: I18n.tr("tooltips.bluetooth-devices")
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
}
// Caffeine (Keep Awake)
NIconButton {
baseSize: Style.baseWidgetSize * 0.9
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.disable-keep-awake") : I18n.tr("tooltips.enable-keep-awake")
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
onClicked: {
IdleInhibitorService.manualToggle()
}
}
}
}
// Spacer
Item {
Layout.fillWidth: true
}
// Right group - Power Profiles
Rectangle {
color: Color.mSurface
radius: Style.radiusM * scaling
Layout.preferredHeight: Style.baseWidgetSize * 1.2 * scaling
Layout.preferredWidth: childrenRect.width + (Style.marginS * scaling * 2)
RowLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
// Performance
NIconButton {
baseSize: Style.baseWidgetSize * 0.9
icon: PowerProfileService.getIcon(PowerProfile.Performance)
tooltipText: I18n.tr("tooltips.set-power-profile", {
"profile": PowerProfileService.getName(PowerProfile.Performance)
})
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mOnPrimary : Color.mPrimary
onClicked: PowerProfileService.setProfile(PowerProfile.Performance)
}
// Balanced
NIconButton {
baseSize: Style.baseWidgetSize * 0.9
icon: PowerProfileService.getIcon(PowerProfile.Balanced)
tooltipText: I18n.tr("tooltips.set-power-profile", {
"profile": PowerProfileService.getName(PowerProfile.Balanced)
})
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnPrimary : Color.mPrimary
onClicked: PowerProfileService.setProfile(PowerProfile.Balanced)
}
// Eco
NIconButton {
baseSize: Style.baseWidgetSize * 0.9
icon: PowerProfileService.getIcon(PowerProfile.PowerSaver)
tooltipText: I18n.tr("tooltips.set-power-profile", {
"profile": PowerProfileService.getName(PowerProfile.PowerSaver)
})
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mOnPrimary : Color.mPrimary
onClicked: PowerProfileService.setProfile(PowerProfile.PowerSaver)
Repeater {
model: Settings.data.controlCenter.widgets.quickSettings
delegate: ControlCenterWidgetLoader {
Layout.fillWidth: true
Layout.preferredWidth: (grid.width - grid.columnSpacing) / 2
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "quickSettings",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.controlCenter.widgets.quickSettings.length
}
Layout.alignment: Qt.AlignVCenter
}
}
}
-130
View File
@@ -1,130 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
// Weather overview card (placeholder data)
NBox {
id: root
readonly property bool weatherReady: (LocationService.data.weather !== null)
ColumnLayout {
id: content
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
clip: true
RowLayout {
spacing: Style.marginS * scaling
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : ""
pointSize: Style.fontSizeXXXL * 1.75 * scaling
color: Color.mPrimary
}
ColumnLayout {
spacing: Style.marginXXS * scaling
NText {
text: {
// Ensure the name is not too long if one had to specify the country
const chunks = Settings.data.location.name.split(",")
return chunks[0]
}
pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
RowLayout {
NText {
visible: weatherReady
text: {
if (!weatherReady) {
return ""
}
var temp = LocationService.data.weather.current_weather.temperature
var suffix = "C"
if (Settings.data.location.useFahrenheit) {
temp = LocationService.celsiusToFahrenheit(temp)
var suffix = "F"
}
temp = Math.round(temp)
return `${temp}°${suffix}`
}
pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
}
NText {
text: weatherReady ? `(${LocationService.data.weather.timezone_abbreviation})` : ""
pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
visible: LocationService.data.weather
}
}
}
}
NDivider {
visible: weatherReady
Layout.fillWidth: true
}
RowLayout {
visible: weatherReady
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
spacing: Style.marginL * scaling
Repeater {
model: weatherReady ? LocationService.data.weather.daily.time : []
delegate: ColumnLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginS * scaling
NText {
text: {
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"))
return Qt.locale().toString(weatherDate, "ddd")
}
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NIcon {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index])
pointSize: Style.fontSizeXXL * 1.6 * scaling
color: Color.mPrimary
}
NText {
Layout.alignment: Qt.AlignHCenter
text: {
var max = LocationService.data.weather.daily.temperature_2m_max[index]
var min = LocationService.data.weather.daily.temperature_2m_min[index]
if (Settings.data.location.useFahrenheit) {
max = LocationService.celsiusToFahrenheit(max)
min = LocationService.celsiusToFahrenheit(min)
}
max = Math.round(max)
min = Math.round(min)
return `${max}°/${min}°`
}
pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
}
}
}
}
RowLayout {
visible: !weatherReady
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
NBusyIndicator {}
}
}
}
+6 -12
View File
@@ -10,8 +10,8 @@ import qs.Widgets
NPanel {
id: root
preferredWidth: 480
preferredHeight: 580
preferredWidth: 440
preferredHeight: 540
panelKeyboardFocus: true
// Positioning
@@ -38,13 +38,7 @@ NPanel {
// Top Card: profile + utilities
TopCard {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(124 * scaling)
}
// Weather
WeatherCard {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(196 * scaling)
Layout.preferredHeight: Math.max(280 * scaling)
}
// Media + stats column
@@ -55,13 +49,13 @@ NPanel {
// Media card
MediaCard {
Layout.preferredWidth: Math.max(270 * scaling)
Layout.fillHeight: true
Layout.preferredWidth: Math.max(250 * scaling)
Layout.preferredHeight: Math.max(196 * scaling)
}
// System monitors combined in one card
SystemMonitorCard {
Layout.preferredWidth: Math.max(160 * scaling)
Layout.preferredWidth: Math.max(140 * scaling)
Layout.preferredHeight: Math.max(196 * scaling)
}
}
@@ -0,0 +1,74 @@
import QtQuick
import Quickshell
import qs.Services
import qs.Commons
Item {
id: root
property string widgetId: ""
property var widgetProps: ({})
property string screenName: widgetProps && widgetProps.screen ? widgetProps.screen.name : ""
property string section: widgetProps && widgetProps.section || ""
property int sectionIndex: widgetProps && widgetProps.sectionWidgetIndex || 0
// Don't reserve space unless the loaded widget is really visible
implicitWidth: getImplicitSize(loader.item, "implicitWidth")
implicitHeight: getImplicitSize(loader.item, "implicitHeight")
Connections {
target: ScalingService
enabled: loader.item && (loader.item.screen !== undefined)
function onScaleChanged(aScreenName, scale) {
if (loader.item && loader.item.screen && aScreenName === screenName) {
loader.item['scaling'] = scale
}
}
}
function getImplicitSize(item, prop) {
return (item && item.visible) ? item[prop] : 0
}
Loader {
id: loader
anchors.fill: parent
active: widgetId !== ""
asynchronous: false
sourceComponent: {
if (!active) {
return null
}
return ControlCenterWidgetRegistry.getWidget(widgetId)
}
onLoaded: {
if (item && widgetProps) {
// Apply properties to loaded widget
for (var prop in widgetProps) {
if (item.hasOwnProperty(prop)) {
item[prop] = widgetProps[prop]
}
}
}
if (item.hasOwnProperty("onLoaded")) {
item.onLoaded()
}
//Logger.log("ControlCenterWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
}
Component.onDestruction: {
// Explicitly clear references
widgetProps = null
}
}
// Error handling
onWidgetIdChanged: {
if (widgetId && !ControlCenterWidgetRegistry.hasWidget(widgetId)) {
Logger.warn("ControlCenterWidgetLoader", "Widget not found in registry:", widgetId)
}
}
}
@@ -0,0 +1,17 @@
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
NButton {
property ShellScreen screen
property real scaling: 1.0
outlined: true
text: "Bluetooth"
fontSize: Style.fontSizeS * scaling
fontWeight: Style.fontWeightRegular
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
}
@@ -0,0 +1,17 @@
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
NButton {
property ShellScreen screen
property real scaling: 1.0
outlined: true
text: IdleInhibitorService.isInhibited ? "Keep-awake" : "Keep-awake"
fontSize: Style.fontSizeS * scaling
fontWeight: Style.fontWeightRegular
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
onClicked: IdleInhibitorService.manualToggle()
}
@@ -0,0 +1,33 @@
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
NButton {
property ShellScreen screen
property real scaling: 1.0
outlined: true
enabled: ProgramCheckerService.wlsunsetAvailable
text: "Night Light"
fontSize: Style.fontSizeS * scaling
fontWeight: Style.fontWeightRegular
icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
onClicked: {
if (!Settings.data.nightLight.enabled) {
Settings.data.nightLight.enabled = true
Settings.data.nightLight.forced = false
} else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) {
Settings.data.nightLight.forced = true
} else {
Settings.data.nightLight.enabled = false
Settings.data.nightLight.forced = false
}
}
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
}
}
@@ -0,0 +1,23 @@
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.UPower
import qs.Commons
import qs.Services
import qs.Widgets
// Performance
NButton {
property ShellScreen screen
property real scaling: 1.0
readonly property bool hasPP: PowerProfileService.available
enabled: hasPP
outlined: true
text: PowerProfileService.getName()
fontSize: Style.fontSizeS * scaling
fontWeight: Style.fontWeightRegular
icon: PowerProfileService.getIcon()
onClicked: {
PowerProfileService.cycleProfile()
}
}
@@ -0,0 +1,24 @@
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
NButton {
property ShellScreen screen
property real scaling: 1.0
enabled: ProgramCheckerService.gpuScreenRecorderAvailable
outlined: true
icon: "camera-video"
text: "Screen Recorder"
fontWeight: Style.fontWeightRegular
onClicked: {
ScreenRecorderService.toggleRecording()
if (!ScreenRecorderService.isRecording) {
var panel = PanelService.getPanel("controlCenterPanel")
panel?.close()
}
}
}
@@ -0,0 +1,20 @@
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
NButton {
property ShellScreen screen
property real scaling: 1.0
enabled: Settings.data.wallpaper.enabled
outlined: true
icon: "wallpaper-selector"
text: "Wallpaper"
fontSize: Style.fontSizeS * scaling
fontWeight: Style.fontWeightRegular
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
onRightClicked: WallpaperService.setRandomWallpaper()
}
+42
View File
@@ -0,0 +1,42 @@
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
NButton {
property ShellScreen screen
property real scaling: 1.0
outlined: true
icon: {
try {
if (NetworkService.ethernetConnected) {
return "ethernet"
}
let connected = false
let signalStrength = 0
for (const net in NetworkService.networks) {
if (NetworkService.networks[net].connected) {
connected = true
signalStrength = NetworkService.networks[net].signal
break
}
}
return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off"
} catch (error) {
Logger.error("Wi-Fi", "Error getting icon:", error)
return "signal_wifi_bad"
}
}
text: {
if (NetworkService.ethernetConnected) {
return "Network"
}
return "Wi-Fi"
}
fontSize: Style.fontSizeS * scaling
fontWeight: Style.fontWeightRegular
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
}
@@ -13,6 +13,10 @@ NBox {
property string sectionId: ""
property var widgetModel: []
property var availableWidgets: []
property bool enableMoveBetweenSections: true
property var widgetRegistry: null
property string settingsDialogComponent: "BarWidgetSettingsDialog.qml"
readonly property real miniButtonSize: Style.baseWidgetSize * 0.65
@@ -154,7 +158,7 @@ NBox {
// Store the widget index for drag operations
property int widgetIndex: index
readonly property int buttonsWidth: Math.round(20 * scaling)
readonly property int buttonsCount: 1 + BarWidgetRegistry.widgetHasUserSettings(modelData.id)
readonly property int buttonsCount: 1 + (root.widgetRegistry ? root.widgetRegistry.widgetHasUserSettings(modelData.id) : 0)
// Visual feedback during drag
opacity: flowDragArea.draggedIndex === index ? 0.5 : 1.0
@@ -197,9 +201,10 @@ NBox {
onTriggered: action => root.moveWidget(root.sectionId, index, action)
}
// Update the MouseArea to use the new context menu
// MouseArea for the context menu
MouseArea {
id: contextMouseArea
enabled: enableMoveBetweenSections
anchors.fill: parent
acceptedButtons: Qt.RightButton
z: -1 // Below the buttons but above background
@@ -209,9 +214,7 @@ NBox {
// Check if click is not on the buttons area
const localX = mouse.x
const buttonsStartX = parent.width - (parent.buttonsCount * parent.buttonsWidth)
if (localX < buttonsStartX) {
// Use the helper function to open at mouse position
contextMenu.openAtItem(widgetItem, mouse.x, mouse.y)
}
}
@@ -236,7 +239,7 @@ NBox {
Layout.preferredWidth: buttonsCount * buttonsWidth
Loader {
active: BarWidgetRegistry.widgetHasUserSettings(modelData.id)
active: root.widgetRegistry && root.widgetRegistry.widgetHasUserSettings(modelData.id)
sourceComponent: NIconButton {
icon: "settings"
tooltipText: I18n.tr("tooltips.widget-settings")
@@ -247,7 +250,7 @@ NBox {
colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight)
colorFgHover: Color.mOnPrimary
onClicked: {
var component = Qt.createComponent(Qt.resolvedUrl("BarWidgetSettingsDialog.qml"))
var component = Qt.createComponent(Qt.resolvedUrl(root.settingsDialogComponent))
function instantiateAndOpen() {
var dialog = component.createObject(root, {
"widgetIndex": index,
@@ -258,19 +261,19 @@ NBox {
if (dialog) {
dialog.open()
} else {
Logger.error("BarSectionEditor", "Failed to create settings dialog instance")
Logger.error("WidgetSectionEditor", "Failed to create settings dialog instance")
}
}
if (component.status === Component.Ready) {
instantiateAndOpen()
} else if (component.status === Component.Error) {
Logger.error("BarSectionEditor", component.errorString())
Logger.error("WidgetSectionEditor", component.errorString())
} else {
component.statusChanged.connect(function () {
if (component.status === Component.Ready) {
instantiateAndOpen()
} else if (component.status === Component.Error) {
Logger.error("BarSectionEditor", component.errorString())
Logger.error("WidgetSectionEditor", component.errorString())
}
})
}
+10
View File
@@ -29,6 +29,7 @@ NPanel {
Audio,
Bar,
ColorScheme,
ControlCenter,
OSD,
Display,
Dock,
@@ -111,6 +112,10 @@ NPanel {
id: notificationsTab
NotificationsTab {}
}
Component {
id: controlCenterTab
ControlCenterTab {}
}
// Order *DOES* matter
function updateTabsModel() {
@@ -124,6 +129,11 @@ NPanel {
"label": "settings.bar.title",
"icon": "settings-bar",
"source": barTab
}, {
"id": SettingsPanel.Tab.ControlCenter,
"label": "settings.control-center.title",
"icon": "settings-bar",
"source": controlCenterTab
}, {
"id": SettingsPanel.Tab.Dock,
"label": "settings.dock.title",
+10 -4
View File
@@ -5,7 +5,7 @@ import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Bar
import qs.Modules.Settings.Extras
ColumnLayout {
id: root
@@ -201,9 +201,11 @@ ColumnLayout {
spacing: Style.marginM * scaling
// Left Section
BarSectionEditor {
SectionEditor {
sectionName: "Left"
sectionId: "left"
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.left
availableWidgets: availableWidgets
onAddWidget: (widgetId, section) => _addWidgetToSection(widgetId, section)
@@ -216,9 +218,11 @@ ColumnLayout {
}
// Center Section
BarSectionEditor {
SectionEditor {
sectionName: "Center"
sectionId: "center"
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.center
availableWidgets: availableWidgets
onAddWidget: (widgetId, section) => _addWidgetToSection(widgetId, section)
@@ -231,9 +235,11 @@ ColumnLayout {
}
// Right Section
BarSectionEditor {
SectionEditor {
sectionName: "Right"
sectionId: "right"
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.right
availableWidgets: availableWidgets
onAddWidget: (widgetId, section) => _addWidgetToSection(widgetId, section)
+140
View File
@@ -0,0 +1,140 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Extras
ColumnLayout {
id: root
spacing: Style.marginL * scaling
// Handler for drag start - disables panel background clicks
function handleDragStart() {
var panel = PanelService.getPanel("settingsPanel")
if (panel && panel.disableBackgroundClick) {
panel.disableBackgroundClick()
}
}
// Handler for drag end - re-enables panel background clicks
function handleDragEnd() {
var panel = PanelService.getPanel("settingsPanel")
if (panel && panel.enableBackgroundClick) {
panel.enableBackgroundClick()
}
}
// Widgets Management Section
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.controlCenter.widgets.section.label")
description: I18n.tr("settings.controlCenter.widgets.section.description")
}
// Bar Sections
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: Style.marginM * scaling
spacing: Style.marginM * scaling
// Quick Settings
SectionEditor {
sectionName: "Quick Settings"
sectionId: "quickSettings"
settingsDialogComponent: ""
widgetRegistry: ControlCenterWidgetRegistry
widgetModel: Settings.data.controlCenter.widgets["quickSettings"]
availableWidgets: availableWidgets
enableMoveBetweenSections: false
onAddWidget: (widgetId, section) => _addWidgetToSection(widgetId, section)
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// ---------------------------------
// Signal functions
// ---------------------------------
function _addWidgetToSection(widgetId, section) {
var newWidget = {
"id": widgetId
}
if (ControlCenterWidgetRegistry.widgetHasUserSettings(widgetId)) {
var metadata = ControlCenterWidgetRegistry.widgetMetadata[widgetId]
if (metadata) {
Object.keys(metadata).forEach(function (key) {
if (key !== "allowUserSettings") {
newWidget[key] = metadata[key]
}
})
}
}
Settings.data.controlCenter.widgets[section].push(newWidget)
}
function _removeWidgetFromSection(section, index) {
if (index >= 0 && index < Settings.data.controlCenter.widgets[section].length) {
var newArray = Settings.data.controlCenter.widgets[section].slice()
var removedWidgets = newArray.splice(index, 1)
Settings.data.controlCenter.widgets[section] = newArray
// Check that we still have a control center
if (removedWidgets[0].id === "ControlCenter" && BarService.lookupWidget("ControlCenter") === undefined) {
ToastService.showWarning(I18n.tr("toast.missing-control-center.label"), I18n.tr("toast.missing-control-center.description"), 12000)
}
}
}
function _reorderWidgetInSection(section, fromIndex, toIndex) {
if (fromIndex >= 0 && fromIndex < Settings.data.controlCenter.widgets[section].length && toIndex >= 0 && toIndex < Settings.data.controlCenter.widgets[section].length) {
// Create a new array to avoid modifying the original
var newArray = Settings.data.controlCenter.widgets[section].slice()
var item = newArray[fromIndex]
newArray.splice(fromIndex, 1)
newArray.splice(toIndex, 0, item)
Settings.data.controlCenter.widgets[section] = newArray
//Logger.log("BarTab", "Widget reordered. New array:", JSON.stringify(newArray))
}
}
function _updateWidgetSettingsInSection(section, index, settings) {
// Update the widget settings in the Settings data
Settings.data.controlCenter.widgets[section][index] = settings
//Logger.log("BarTab", `Updated widget settings for ${settings.id} in ${section} section`)
}
// Base list model for all combo boxes
ListModel {
id: availableWidgets
}
Component.onCompleted: {
// Fill out availableWidgets ListModel
availableWidgets.clear()
ControlCenterWidgetRegistry.getAvailableWidgets().forEach(entry => {
availableWidgets.append({
"key": entry,
"name": entry
})
})
}
}
+71
View File
@@ -0,0 +1,71 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Commons
import qs.Modules.ControlCenter.Widgets
Singleton {
id: root
// Widget registry object mapping widget names to components
property var widgets: ({
"Bluetooth": bluetoothComponent,
"KeepAwake": keepAwakeComponent,
"NightLight": nightLightComponent,
"PowerProfile": powerProfileComponent,
"ScreenRecorder": screenRecorderComponent,
"WiFi": wiFiComponent,
"WallpaperSelector": wallpaperSelectorComponent
})
property var widgetMetadata: ({})
// Component definitions - these are loaded once at startup
property Component bluetoothComponent: Component {
Bluetooth {}
}
property Component keepAwakeComponent: Component {
KeepAwake {}
}
property Component nightLightComponent: Component {
NightLight {}
}
property Component powerProfileComponent: Component {
PowerProfile {}
}
property Component screenRecorderComponent: Component {
ScreenRecorder {}
}
property Component wiFiComponent: Component {
WiFi {}
}
property Component wallpaperSelectorComponent: Component {
WallpaperSelector {}
}
function init() {
Logger.log("ControlCenterWidgetRegistry", "Service started")
}
// ------------------------------
// Helper function to get widget component by name
function getWidget(id) {
return widgets[id] || null
}
// Helper function to check if widget exists
function hasWidget(id) {
return id in widgets
}
// Get list of available widget id
function getAvailableWidgets() {
return Object.keys(widgets)
}
// Helper function to check if widget has user settings
function widgetHasUserSettings(id) {
return (widgetMetadata[id] !== undefined) && (widgetMetadata[id].allowUserSettings === true)
}
}
+8 -5
View File
@@ -19,6 +19,7 @@ Rectangle {
property int fontWeight: Style.fontWeightBold
property real iconSize: Style.fontSizeL * scaling
property bool outlined: false
property int horizontalAlignment: Qt.AlignHCenter
// Signals
signal clicked
@@ -27,7 +28,6 @@ Rectangle {
// Internal properties
property bool hovered: false
property bool pressed: false
// Dimensions
implicitWidth: contentRow.implicitWidth + (Style.marginL * 2 * scaling)
@@ -47,7 +47,7 @@ Rectangle {
border.color: {
if (!enabled)
return Color.mOutline
if (pressed || hovered)
if (hovered)
return backgroundColor
return outlined ? backgroundColor : Color.transparent
}
@@ -71,7 +71,10 @@ Rectangle {
// Content
RowLayout {
id: contentRow
anchors.centerIn: parent
anchors.verticalCenter: parent.verticalCenter
anchors.left: root.horizontalAlignment === Qt.AlignLeft ? parent.left : undefined
anchors.horizontalCenter: root.horizontalAlignment === Qt.AlignHCenter ? parent.horizontalCenter : undefined
anchors.leftMargin: root.horizontalAlignment === Qt.AlignLeft ? Style.marginL * scaling : 0
spacing: Style.marginXS * scaling
// Icon (optional)
@@ -84,8 +87,8 @@ Rectangle {
if (!root.enabled)
return Color.mOnSurfaceVariant
if (root.outlined) {
if (root.pressed || root.hovered)
return root.backgroundColor
if (root.hovered)
return root.textColor
return root.backgroundColor
}
return root.textColor