import QtQuick import qs.Common import qs.Services import qs.Modules.ControlCenter.Widgets import qs.Modules.ControlCenter.Components import "../utils/layout.js" as LayoutUtils Column { id: root property bool editMode: false property string expandedSection: "" property int expandedWidgetIndex: -1 property var model: null property var expandedWidgetData: null property var bluetoothCodecSelector: null property bool darkModeTransitionPending: false property string screenName: "" property string screenModel: "" property var parentScreen: null signal expandClicked(var widgetData, int globalIndex) signal removeWidget(int index) signal moveWidget(int fromIndex, int toIndex) signal toggleWidgetSize(int index) signal collapseRequested function requestCollapse() { collapseRequested(); } spacing: editMode ? Theme.spacingL : Theme.spacingS property var currentRowWidgets: [] property real currentRowWidth: 0 property int expandedRowIndex: -1 property var colorPickerModal: null function calculateRowsAndWidgets() { return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex); } property var layoutResult: { const dummy = [expandedSection, expandedWidgetIndex, model?.controlCenterWidgets]; return calculateRowsAndWidgets(); } onLayoutResultChanged: { expandedRowIndex = layoutResult.expandedRowIndex; } function moveToTop(item) { const children = root.children; for (var i = 0; i < children.length; i++) { if (children[i] === item) continue; if (children[i].z) children[i].z = Math.min(children[i].z, 999); } item.z = 1000; } Repeater { model: root.layoutResult.rows Column { width: root.width spacing: 0 property int rowIndex: index property var rowWidgets: modelData property bool isSliderOnlyRow: { const widgets = rowWidgets || []; if (widgets.length === 0) return false; return widgets.every(w => w.id === "volumeSlider" || w.id === "brightnessSlider" || w.id === "inputVolumeSlider"); } topPadding: isSliderOnlyRow ? (root.editMode ? 4 : -6) : 0 bottomPadding: isSliderOnlyRow ? (root.editMode ? 4 : -6) : 0 Flow { width: parent.width spacing: Theme.spacingS Repeater { model: rowWidgets || [] DragDropWidgetWrapper { widgetData: modelData property int globalWidgetIndex: { const widgets = SettingsData.controlCenterWidgets || []; for (var i = 0; i < widgets.length; i++) { if (widgets[i].id === modelData.id) { if (modelData.id === "diskUsage" || modelData.id === "brightnessSlider") { if (widgets[i].instanceId === modelData.instanceId) { return i; } } else { return i; } } } return -1; } property int widgetWidth: modelData.width || 50 width: { const baseWidth = root.width; const spacing = Theme.spacingS; if (widgetWidth <= 25) { return (baseWidth - spacing * 3) / 4; } else if (widgetWidth <= 50) { return (baseWidth - spacing) / 2; } else if (widgetWidth <= 75) { return (baseWidth - spacing * 2) * 0.75; } else { return baseWidth; } } height: isSliderOnlyRow ? 48 : 60 editMode: root.editMode widgetIndex: globalWidgetIndex gridCellWidth: width gridCellHeight: height gridColumns: 4 gridLayout: root isSlider: { const id = modelData.id || ""; return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider"; } widgetComponent: { const id = modelData.id || ""; if (id.startsWith("builtin_")) { return builtinPluginWidgetComponent; } else if (id.startsWith("plugin_")) { return pluginWidgetComponent; } else if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") { return compoundPillComponent; } else if (id === "volumeSlider") { return audioSliderComponent; } else if (id === "brightnessSlider") { return brightnessSliderComponent; } else if (id === "inputVolumeSlider") { return inputAudioSliderComponent; } else if (id === "battery") { return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent; } else if (id === "diskUsage") { return diskUsagePillComponent; } else if (id === "colorPicker") { return colorPickerPillComponent; } else { return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent; } } onWidgetMoved: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) onRemoveWidget: index => root.removeWidget(index) onToggleWidgetSize: index => root.toggleWidgetSize(index) } } } DetailHost { id: detailHost width: parent.width height: active ? (getDetailHeight(root.expandedSection) + Theme.spacingS) : 0 property bool active: { if (root.expandedSection === "") return false; if (root.expandedSection.startsWith("diskUsage_") && root.expandedWidgetData) { const expandedInstanceId = root.expandedWidgetData.instanceId; return rowWidgets.some(w => w.id === "diskUsage" && w.instanceId === expandedInstanceId); } if (root.expandedSection.startsWith("brightnessSlider_") && root.expandedWidgetData) { const expandedInstanceId = root.expandedWidgetData.instanceId; return rowWidgets.some(w => w.id === "brightnessSlider" && w.instanceId === expandedInstanceId); } return rowIndex === root.expandedRowIndex; } visible: active expandedSection: root.expandedSection expandedWidgetData: root.expandedWidgetData bluetoothCodecSelector: root.bluetoothCodecSelector widgetModel: root.model collapseCallback: root.requestCollapse screenName: root.screenName screenModel: root.screenModel } } } Component { id: errorPillComponent ErrorPill { property var widgetData: parent.widgetData || {} width: parent.width height: 60 primaryMessage: { if (!DMSService.dmsAvailable) { return I18n.tr("DMS_SOCKET not available"); } return I18n.tr("NM not supported"); } secondaryMessage: I18n.tr("update dms for NM integration.") } } Component { id: compoundPillComponent CompoundPill { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") width: parent.width height: 60 iconName: { switch (widgetData.id || "") { case "wifi": { if (NetworkService.wifiToggling) return "sync"; const status = NetworkService.networkStatus; if (status === "ethernet") return "settings_ethernet"; if (status === "vpn") return NetworkService.ethernetConnected ? "settings_ethernet" : NetworkService.wifiSignalIcon; if (status === "wifi") return NetworkService.wifiSignalIcon; if (NetworkService.wifiEnabled) return "wifi_off"; return "wifi_off"; } case "bluetooth": { if (!BluetoothService.available) return "bluetooth_disabled"; if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) return "bluetooth_disabled"; return "bluetooth"; } case "audioOutput": { if (!AudioService.sink) return "volume_off"; let volume = AudioService.sink.audio.volume; let muted = AudioService.sink.audio.muted; if (muted || volume === 0.0) return "volume_off"; if (volume <= 0.33) return "volume_down"; if (volume <= 0.66) return "volume_up"; return "volume_up"; } case "audioInput": { if (!AudioService.source) return "mic_off"; let muted = AudioService.source.audio.muted; return muted ? "mic_off" : "mic"; } default: return widgetDef?.icon || "help"; } } primaryText: { switch (widgetData.id || "") { case "wifi": { if (NetworkService.wifiToggling) return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."; const status = NetworkService.networkStatus; if (status === "ethernet") return "Ethernet"; if (status === "vpn") { if (NetworkService.ethernetConnected) return "Ethernet"; if (NetworkService.wifiConnected && NetworkService.currentWifiSSID) return NetworkService.currentWifiSSID; } if (status === "wifi" && NetworkService.currentWifiSSID) return NetworkService.currentWifiSSID; if (NetworkService.wifiEnabled) return "Not connected"; return "WiFi off"; } case "bluetooth": { if (!BluetoothService.available) return "Bluetooth"; if (!BluetoothService.adapter) return "No adapter"; if (!BluetoothService.adapter.enabled) return "Disabled"; return "Enabled"; } case "audioOutput": return AudioService.sink?.description || "No output device"; case "audioInput": return AudioService.source?.description || "No input device"; default: return widgetDef?.text || "Unknown"; } } secondaryText: { switch (widgetData.id || "") { case "wifi": { if (NetworkService.wifiToggling) return "Please wait..."; const status = NetworkService.networkStatus; if (status === "ethernet") return "Connected"; if (status === "vpn") { if (NetworkService.ethernetConnected) return "Connected"; if (NetworkService.wifiConnected) return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"; } if (status === "wifi") return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"; if (NetworkService.wifiEnabled) return "Select network"; return ""; } case "bluetooth": { if (!BluetoothService.available) return "No adapters"; if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) return "Off"; const primaryDevice = (() => { if (!BluetoothService.adapter || !BluetoothService.adapter.devices) return null; let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]; for (let device of devices) { if (device && device.connected) return device; } return null; })(); if (primaryDevice) return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device"; return "No devices"; } case "audioOutput": { if (!AudioService.sink) return "Select device"; if (AudioService.sink.audio.muted) return "Muted"; const volume = AudioService.sink.audio.volume; if (typeof volume !== "number" || isNaN(volume)) return "0%"; return Math.round(volume * 100) + "%"; } case "audioInput": { if (!AudioService.source) return "Select device"; if (AudioService.source.audio.muted) return "Muted"; const volume = AudioService.source.audio.volume; if (typeof volume !== "number" || isNaN(volume)) return "0%"; return Math.round(volume * 100) + "%"; } default: return widgetDef?.description || ""; } } isActive: { switch (widgetData.id || "") { case "wifi": { if (NetworkService.wifiToggling) return false; const status = NetworkService.networkStatus; if (status === "ethernet") return true; if (status === "vpn") return NetworkService.ethernetConnected || NetworkService.wifiConnected; if (status === "wifi") return true; return NetworkService.wifiEnabled; } case "bluetooth": return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled); case "audioOutput": return !!(AudioService.sink && !AudioService.sink.audio.muted); case "audioInput": return !!(AudioService.source && !AudioService.source.audio.muted); default: return false; } } enabled: widgetDef?.enabled ?? true onToggled: { if (root.editMode) return; switch (widgetData.id || "") { case "wifi": { if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) { NetworkService.toggleWifiRadio(); } break; } case "bluetooth": { if (BluetoothService.available && BluetoothService.adapter) { BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled; } break; } case "audioOutput": { if (AudioService.sink && AudioService.sink.audio) { AudioService.sink.audio.muted = !AudioService.sink.audio.muted; } break; } case "audioInput": { if (AudioService.source && AudioService.source.audio) { AudioService.source.audio.muted = !AudioService.source.audio.muted; } break; } } } onExpandClicked: { if (root.editMode) return; root.expandClicked(widgetData, widgetIndex); } onWheelEvent: function (wheelEvent) { if (root.editMode) return; const id = widgetData.id || ""; if (id === "audioOutput") { if (!AudioService.sink || !AudioService.sink.audio) return; let delta = wheelEvent.angleDelta.y; let currentVolume = AudioService.sink.audio.volume * 100; let newVolume; if (delta > 0) newVolume = Math.min(100, currentVolume + 5); else newVolume = Math.max(0, currentVolume - 5); AudioService.sink.audio.muted = false; AudioService.sink.audio.volume = newVolume / 100; wheelEvent.accepted = true; } else if (id === "audioInput") { if (!AudioService.source || !AudioService.source.audio) return; let delta = wheelEvent.angleDelta.y; let currentVolume = AudioService.source.audio.volume * 100; let newVolume; if (delta > 0) newVolume = Math.min(100, currentVolume + 5); else newVolume = Math.max(0, currentVolume - 5); AudioService.source.audio.muted = false; AudioService.source.audio.volume = newVolume / 100; wheelEvent.accepted = true; } } } } Component { id: audioSliderComponent Item { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 width: parent.width height: 16 AudioSliderRow { anchors.centerIn: parent width: parent.width height: 14 property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) } } } Component { id: brightnessSliderComponent Item { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 width: parent.width height: 16 BrightnessSliderRow { id: brightnessSliderRow anchors.centerIn: parent width: parent.width height: 14 deviceName: widgetData.deviceName || "" instanceId: widgetData.instanceId || "" screenName: root.screenName parentScreen: root.parentScreen property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) onIconClicked: { if (!root.editMode && DisplayService.devices && DisplayService.devices.length > 1) { root.expandClicked(widgetData, widgetIndex); } } } } } Component { id: inputAudioSliderComponent Item { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 width: parent.width height: 16 InputAudioSliderRow { anchors.centerIn: parent width: parent.width height: 14 property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) } } } Component { id: batteryPillComponent BatteryPill { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 width: parent.width height: 60 onExpandClicked: { if (!root.editMode) { root.expandClicked(widgetData, widgetIndex); } } } } Component { id: smallBatteryComponent SmallBatteryButton { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 width: parent.width height: 48 onClicked: { if (!root.editMode) { root.expandClicked(widgetData, widgetIndex); } } } } Component { id: toggleButtonComponent ToggleButton { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 width: parent.width height: 60 iconName: { switch (widgetData.id || "") { case "nightMode": return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"; case "darkMode": return "contrast"; case "doNotDisturb": return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off"; case "idleInhibitor": return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"; default: return "help"; } } text: { switch (widgetData.id || "") { case "nightMode": return I18n.tr("Night Mode"); case "darkMode": return I18n.tr("Dark Mode"); case "doNotDisturb": return I18n.tr("Do Not Disturb"); case "idleInhibitor": return SessionService.idleInhibited ? I18n.tr("Keeping Awake") : I18n.tr("Keep Awake"); default: return "Unknown"; } } iconRotation: { if (widgetData.id !== "darkMode") return 0; if (darkModeTransitionPending) { return SessionData.isLightMode ? 180 : 0; } return SessionData.isLightMode ? 180 : 0; } isActive: { switch (widgetData.id || "") { case "nightMode": return DisplayService.nightModeEnabled || false; case "darkMode": return !SessionData.isLightMode; case "doNotDisturb": return SessionData.doNotDisturb || false; case "idleInhibitor": return SessionService.idleInhibited || false; default: return false; } } enabled: !root.editMode onClicked: { if (root.editMode) return; switch (widgetData.id || "") { case "nightMode": { if (DisplayService.automationAvailable) DisplayService.toggleNightMode(); break; } case "darkMode": { const newMode = !SessionData.isLightMode; Theme.screenTransition(); Theme.setLightMode(newMode); break; } case "doNotDisturb": { SessionData.setDoNotDisturb(!SessionData.doNotDisturb); break; } case "idleInhibitor": { SessionService.toggleIdleInhibit(); break; } } } } } Component { id: smallToggleComponent SmallToggleButton { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 width: parent.width height: 48 iconName: { switch (widgetData.id || "") { case "nightMode": return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"; case "darkMode": return "contrast"; case "doNotDisturb": return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off"; case "idleInhibitor": return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"; default: return "help"; } } iconRotation: { if (widgetData.id !== "darkMode") return 0; if (darkModeTransitionPending) { return SessionData.isLightMode ? 180 : 0; } return SessionData.isLightMode ? 180 : 0; } isActive: { switch (widgetData.id || "") { case "nightMode": return DisplayService.nightModeEnabled || false; case "darkMode": return !SessionData.isLightMode; case "doNotDisturb": return SessionData.doNotDisturb || false; case "idleInhibitor": return SessionService.idleInhibited || false; default: return false; } } enabled: !root.editMode onClicked: { if (root.editMode) return; switch (widgetData.id || "") { case "nightMode": { if (DisplayService.automationAvailable) DisplayService.toggleNightMode(); break; } case "darkMode": { const newMode = !SessionData.isLightMode; Theme.screenTransition(); Theme.setLightMode(newMode); break; } case "doNotDisturb": { SessionData.setDoNotDisturb(!SessionData.doNotDisturb); break; } case "idleInhibitor": { SessionService.toggleIdleInhibit(); break; } } } } } Component { id: diskUsagePillComponent DiskUsagePill { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 width: parent.width height: 60 mountPath: widgetData.mountPath || "/" instanceId: widgetData.instanceId || "" onExpandClicked: { if (!root.editMode) { root.expandClicked(widgetData, widgetIndex); } } } } Component { id: colorPickerPillComponent ColorPickerPill { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 width: parent.width height: 60 colorPickerModal: root.colorPickerModal } } Component { id: builtinPluginWidgetComponent Loader { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 property int widgetWidth: widgetData.width || 50 width: parent.width height: 60 property var builtinInstance: null Component.onCompleted: { const id = widgetData.id || ""; if (id === "builtin_vpn") { if (root.model?.vpnLoader) { root.model.vpnLoader.active = true; } builtinInstance = Qt.binding(() => root.model?.vpnBuiltinInstance); } if (id === "builtin_cups") { if (root.model?.cupsLoader) { root.model.cupsLoader.active = true; } builtinInstance = Qt.binding(() => root.model?.cupsBuiltinInstance); } } sourceComponent: { if (!builtinInstance) return null; const hasDetail = builtinInstance.ccDetailContent !== null; if (widgetWidth <= 25) { return builtinSmallToggleComponent; } else if (hasDetail) { return builtinCompoundPillComponent; } else { return builtinToggleComponent; } } } } Component { id: builtinCompoundPillComponent CompoundPill { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 property var builtinInstance: parent.builtinInstance iconName: builtinInstance?.ccWidgetIcon || "extension" primaryText: builtinInstance?.ccWidgetPrimaryText || "Built-in" secondaryText: builtinInstance?.ccWidgetSecondaryText || "" isActive: builtinInstance?.ccWidgetIsActive || false onToggled: { if (root.editMode) return; if (builtinInstance) { builtinInstance.ccWidgetToggled(); } } onExpandClicked: { if (root.editMode) return; if (builtinInstance) { builtinInstance.ccWidgetExpanded(); } root.expandClicked(widgetData, widgetIndex); } } } Component { id: builtinToggleComponent ToggleButton { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 property var builtinInstance: parent.builtinInstance iconName: builtinInstance?.ccWidgetIcon || "extension" text: builtinInstance?.ccWidgetPrimaryText || "Built-in" isActive: builtinInstance?.ccWidgetIsActive || false enabled: !root.editMode onClicked: { if (root.editMode) return; if (builtinInstance) { builtinInstance.ccWidgetToggled(); } } } } Component { id: builtinSmallToggleComponent SmallToggleButton { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 property var builtinInstance: parent.builtinInstance iconName: builtinInstance?.ccWidgetIcon || "extension" isActive: builtinInstance?.ccWidgetIsActive || false enabled: !root.editMode onClicked: { if (root.editMode) return; if (builtinInstance) { builtinInstance.ccWidgetToggled(); } } } } Component { id: pluginWidgetComponent Loader { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 property int widgetWidth: widgetData.width || 50 width: parent.width height: 60 property var pluginInstance: null property string pluginId: widgetData.id?.replace("plugin_", "") || "" sourceComponent: { if (!pluginInstance) return null; const hasDetail = pluginInstance.ccDetailContent !== null; if (widgetWidth <= 25) { return pluginSmallToggleComponent; } else if (hasDetail) { return pluginCompoundPillComponent; } else { return pluginToggleComponent; } } Component.onCompleted: { Qt.callLater(() => { const pluginComponent = PluginService.pluginWidgetComponents[pluginId]; if (pluginComponent) { const instance = pluginComponent.createObject(null, { "pluginId": pluginId, "pluginService": PluginService, "visible": false, "width": 0, "height": 0 }); if (instance) { pluginInstance = instance; } } }); } Connections { target: PluginService function onPluginDataChanged(changedPluginId) { if (changedPluginId === pluginId && pluginInstance) { pluginInstance.loadPluginData(); } } } Component.onDestruction: { if (pluginInstance) { pluginInstance.destroy(); } } } } Component { id: pluginCompoundPillComponent CompoundPill { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 property var pluginInstance: parent.pluginInstance iconName: pluginInstance?.ccWidgetIcon || "extension" primaryText: pluginInstance?.ccWidgetPrimaryText || "Plugin" secondaryText: pluginInstance?.ccWidgetSecondaryText || "" isActive: pluginInstance?.ccWidgetIsActive || false onToggled: { if (root.editMode) return; if (pluginInstance) { pluginInstance.ccWidgetToggled(); } } onExpandClicked: { if (root.editMode) return; if (pluginInstance) { pluginInstance.ccWidgetExpanded(); } root.expandClicked(widgetData, widgetIndex); } } } Component { id: pluginToggleComponent ToggleButton { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 property var pluginInstance: parent.pluginInstance property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") iconName: pluginInstance?.ccWidgetIcon || widgetDef?.icon || "extension" text: pluginInstance?.ccWidgetPrimaryText || widgetDef?.text || "Plugin" secondaryText: pluginInstance?.ccWidgetSecondaryText || "" isActive: pluginInstance?.ccWidgetIsActive || false enabled: !root.editMode onClicked: { if (root.editMode) return; if (pluginInstance) { pluginInstance.ccWidgetToggled(); } } } } Component { id: pluginSmallToggleComponent SmallToggleButton { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 property var pluginInstance: parent.pluginInstance property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") iconName: pluginInstance?.ccWidgetIcon || widgetDef?.icon || "extension" isActive: pluginInstance?.ccWidgetIsActive || false enabled: !root.editMode onClicked: { if (root.editMode) return; if (pluginInstance && pluginInstance.ccDetailContent) { root.expandClicked(widgetData, widgetIndex); } else if (pluginInstance) { pluginInstance.ccWidgetToggled(); } } } } }