Files
DankMaterialShell/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml
T
08dde7f79a add: Cellular Network support (rebase of #2765) (#3096)
* feat: add cellular support

* fix(settings): resolve cellular tab index collision

* fix: address review feedback for cellular support

Bump the network API version, drop the deleted term freeze file,
regenerate the settings search index, and reuse existing translation
terms where they already cover the new cellular UI.

* fix: do not assign readonly userPreference when switching network type

Assigning SettingsData.networkPreference through the readonly alias
threw on click, so the Cellular segment in Control Center did nothing.

* fix: make Cellular selectable and the modem row clickable

The Control Center type switcher was bound only to preference, so
Cellular snapped back to Ethernet. The settings modem row highlighted
on hover but had no click handler. Show the modem as a connect target
when no APN profiles exist.

* fix: create a GSM profile when connecting cellular with none saved

Clicking Mobile broadband only activated an existing profile, so a
modem with no APN did nothing useful. Fall back to AddAndActivate
like ethernet connect.

* fix: address second round of cellular review feedback

- Guard cellularDevices map with devMutex via snapshot/by-iface/set/remove
  helpers, mirroring the existing ethernet and wifi accessors
- Regenerate mocks with mockery v3 instead of hand-editing mock_Backend.go
- Reuse refreshCellularState() in SetCellularEnabled, DisconnectCellular,
  DisconnectCellularDevice, ActivateCellularConnection and device removal
- Wrap retryErr rather than the first error when the auto-config retry fails
- Use Theme.surfaceTextSecondary/primaryHover/primaryHoverLight in the
  cellular-off state instead of hand-rolled Qt.rgba values
- Drop the duplicate "No devices found" label so it can no longer render
  twice with no modem and no saved config

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* fix: address remaining cellular review comments

- Toast "Failed to toggle cellular" instead of always "enable"
- Remove unused enableCellularRadio() wrappers
- Store Control Center network type as a value, not a list index

* fix: pass nmcli connection selector as separate argument

`nmcli con mod uuid:<UUID>` is not valid syntax -- nmcli expects the
selector keyword as its own argument, so every priority update failed
with "no such connection profile". Because practically every NM profile
has a UUID, this silently disabled autoconnect-priority and route-metric
updates for ethernet and WiFi as well as cellular.

Also fix the Cellular settings status text, which said "Connecting..."
for a flag that only tracks radio enable/disable, and add the seven
cellular terms that were missing from the translation catalogs.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* apply review comments

* fix lints

* update debug doc

* fix style inconsistencies

---------

Co-authored-by: Yechiel Worenklein <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: bbedward <[email protected]>
2026-08-29 15:37:36 -04:00

1287 lines
50 KiB
QML

import QtQuick
import qs.Common
import qs.Services
import qs.Modules.ControlCenter.Widgets
import qs.Modules.ControlCenter.Components
import "../utils/detailHeight.js" as DetailHeightUtils
import "../utils/layout.js" as LayoutUtils
Column {
id: root
readonly property var log: Log.scoped("DragDropGrid")
property bool editMode: false
property string expandedSection: ""
property int expandedWidgetIndex: -1
property var model: null
property var expandedWidgetData: null
property var bluetoothCodecSelector: null
property var audioPortSelector: 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
signal configRequested(int index, var widgetData, var anchor)
spacing: editMode ? Theme.spacingL : Theme.spacingS
property real maxPopoutHeight: 9999
property var currentRowWidgets: []
property real currentRowWidth: 0
property int expandedRowIndex: -1
property var colorPickerModal: null
property var activePluginDetailInstance: null
readonly property real _maxDetailHeight: {
const rows = layoutResult.rows;
let totalRowHeight = 0;
for (let i = 0; i < rows.length; i++) {
const sliderOnly = rows[i].every(w => {
const id = w.id || "";
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider";
});
totalRowHeight += sliderOnly ? 36 : 60;
}
const rowSpacing = Math.max(0, rows.length - 1) * spacing;
return Math.max(100, maxPopoutHeight - totalRowHeight - rowSpacing);
}
readonly property real targetImplicitHeight: {
if (editMode)
return editModeGrid.implicitHeight;
const rows = layoutResult.rows;
let totalHeight = 0;
for (let i = 0; i < rows.length; i++) {
const widgets = rows[i] || [];
const sliderOnly = widgets.length > 0 && widgets.every(w => {
const id = w.id || "";
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider";
});
totalHeight += sliderOnly ? (editMode ? 56 : 36) : 60;
if (expandedSection !== "" && i === expandedRowIndex)
totalHeight += detailHeightForSection(expandedSection) + Theme.spacingS;
}
totalHeight += Math.max(0, rows.length - 1) * spacing;
return totalHeight;
}
function detailHeightForSection(section) {
return DetailHeightUtils.detailHeightForSection(section, _maxDetailHeight, activePluginDetailInstance);
}
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;
}
function getCompoundPillIconBlinking(id) {
if (id === "wifi")
return NetworkService.isWifiConnecting;
if (id === "bluetooth")
return BluetoothService.connecting;
return false;
}
function getCompoundPillIconName(id, widgetDef) {
switch (id) {
case "wifi":
{
if (NetworkService.wifiToggling)
return "sync";
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
return NetworkService.wifiSignalIcon;
const status = NetworkService.networkStatus;
if (status === "ethernet")
return "settings_ethernet";
if (status === "cellular")
return "network_cell";
if (status === "vpn")
return NetworkService.ethernetConnected ? "settings_ethernet" : (NetworkService.cellularConnected ? "network_cell" : NetworkService.wifiSignalIcon);
if (status === "wifi")
return NetworkService.wifiSignalIcon;
return "wifi";
}
case "bluetooth":
{
return "bluetooth";
}
case "audioOutput":
return AudioService.sinkVolumeIconName;
case "audioInput":
{
if (!AudioService.source?.audio)
return "mic_off";
return AudioService.source.audio.muted ? "mic_off" : "mic";
}
default:
return widgetDef?.icon || "help";
}
}
function getCompoundPillIsActive(id) {
switch (id) {
case "wifi":
{
if (NetworkService.wifiToggling)
return false;
const status = NetworkService.networkStatus;
if (status === "ethernet")
return true;
if (status === "cellular")
return true;
if (status === "vpn")
return NetworkService.ethernetConnected || NetworkService.wifiConnected || NetworkService.cellularConnected;
if (status === "wifi")
return true;
return NetworkService.wifiEnabled;
}
case "bluetooth":
return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled);
case "audioOutput":
return !!(AudioService.sink?.audio && !AudioService.sink.audio.muted);
case "audioInput":
return !!(AudioService.source?.audio && !AudioService.source.audio.muted);
default:
return false;
}
}
function handleCompoundPillToggled(id) {
switch (id) {
case "wifi":
{
if (NetworkService.networkStatus !== "ethernet" && NetworkService.networkStatus !== "cellular" && !NetworkService.wifiToggling) {
NetworkService.toggleWifiRadio();
}
break;
}
case "bluetooth":
{
BluetoothService.setBluetoothEnabled(!BluetoothService.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;
}
}
}
function handleCompoundPillWheelEvent(id, wheelEvent) {
if (id === "audioOutput") {
if (!AudioService.sink || !AudioService.sink.audio)
return;
let delta = wheelEvent.angleDelta.y;
let maxVol = AudioService.sinkMaxVolume;
let currentVolume = AudioService.sink.audio.volume * 100;
let newVolume;
if (delta > 0)
newVolume = Math.min(maxVol, 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;
}
}
function componentForWidget(widgetData) {
const id = widgetData.id || "";
const widgetWidth = widgetData.width || 50;
if (id.startsWith("builtin_"))
return builtinPluginWidgetComponent;
if (id.startsWith("plugin_"))
return pluginWidgetComponent;
switch (id) {
case "wifi":
case "bluetooth":
case "audioOutput":
case "audioInput":
return widgetWidth <= 25 ? smallCompoundComponent : compoundPillComponent;
case "volumeSlider":
return audioSliderComponent;
case "brightnessSlider":
return brightnessSliderComponent;
case "inputVolumeSlider":
return inputAudioSliderComponent;
case "battery":
return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent;
case "diskUsage":
return widgetWidth <= 25 ? smallDiskUsageComponent : diskUsagePillComponent;
case "colorPicker":
return widgetWidth <= 25 ? smallColorPickerComponent : colorPickerPillComponent;
case "doNotDisturb":
return widgetWidth <= 25 ? smallToggleComponent : dndPillComponent;
default:
return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent;
}
}
Repeater {
model: root.editMode ? [] : 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: root.componentForWidget(modelData)
onWidgetMoved: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
onRemoveWidget: index => root.removeWidget(index)
onToggleWidgetSize: index => root.toggleWidgetSize(index)
onConfigRequested: (idx, data, anchor) => root.configRequested(idx, data, anchor)
}
}
}
DetailHost {
id: detailHost
width: parent.width
height: active ? (root.detailHeightForSection(root.expandedSection) + Theme.spacingS) : 0
clip: true
property string retainedSection: ""
property var retainedWidgetData: null
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 || height > 0.5
expandedSection: active ? root.expandedSection : retainedSection
expandedWidgetData: active ? root.expandedWidgetData : retainedWidgetData
bluetoothCodecSelector: root.bluetoothCodecSelector
audioPortSelector: root.audioPortSelector
widgetModel: root.model
collapseCallback: root.collapseRequested
screenName: root.screenName
screenModel: root.screenModel
function retainActiveDetail() {
if (!active || !root.expandedSection)
return;
retainedSection = root.expandedSection;
retainedWidgetData = root.expandedWidgetData;
}
function syncActivePluginDetail() {
if (active) {
root.activePluginDetailInstance = pluginDetailInstance;
} else if (root.activePluginDetailInstance === pluginDetailInstance) {
root.activePluginDetailInstance = null;
}
}
onActiveChanged: {
retainActiveDetail();
syncActivePluginDetail();
}
onPluginDetailInstanceChanged: syncActivePluginDetail()
onHeightChanged: {
if (!active && height <= 0.5) {
retainedSection = "";
retainedWidgetData = null;
}
}
Connections {
target: root
function onExpandedSectionChanged() {
detailHost.retainActiveDetail();
}
function onExpandedWidgetDataChanged() {
detailHost.retainActiveDetail();
}
}
// targetValue-based direction: `active` can be stale when the behavior triggers
Behavior on height {
id: detailHeightBehavior
NumberAnimation {
readonly property bool growing: (detailHeightBehavior.targetValue ?? 0) >= detailHost.height
duration: Theme.variantDuration(Theme.popoutAnimationDuration, growing)
easing.type: Easing.BezierSpline
easing.bezierCurve: growing ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
}
}
}
EditModeGrid {
id: editModeGrid
width: root.width
visible: root.editMode
active: root.editMode
model: root.model
componentProvider: root
onRemoveWidget: index => root.removeWidget(index)
onToggleWidgetSize: index => root.toggleWidgetSize(index)
onConfigRequested: (idx, data, anchor) => root.configRequested(idx, data, anchor)
}
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
iconBlinking: root.getCompoundPillIconBlinking(widgetData.id || "")
iconName: root.getCompoundPillIconName(widgetData.id || "", widgetDef)
primaryText: {
switch (widgetData.id || "") {
case "wifi":
{
if (NetworkService.wifiToggling)
return NetworkService.wifiEnabled ? I18n.tr("Disabling WiFi...", "network status") : I18n.tr("Enabling WiFi...", "network status");
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
return NetworkService.connectingSSID || I18n.tr("Connecting...", "network status");
const status = NetworkService.networkStatus;
if (status === "ethernet")
return I18n.tr("Ethernet", "network status");
if (status === "cellular")
return I18n.tr("Cellular", "network status");
if (status === "vpn") {
if (NetworkService.ethernetConnected)
return I18n.tr("Ethernet", "network status");
if (NetworkService.cellularConnected)
return I18n.tr("Cellular", "network status");
if (NetworkService.wifiConnected && NetworkService.currentWifiSSID)
return NetworkService.currentWifiSSID;
}
if (status === "wifi" && NetworkService.currentWifiSSID)
return NetworkService.currentWifiSSID;
if (NetworkService.wifiEnabled)
return I18n.tr("Not connected", "network status");
return I18n.tr("WiFi off", "network status");
}
case "bluetooth":
{
if (!BluetoothService.available)
return I18n.tr("Bluetooth", "bluetooth status");
if (!BluetoothService.adapter)
return I18n.tr("No adapter", "bluetooth status");
if (!BluetoothService.adapter.enabled)
return I18n.tr("Disabled", "bluetooth status");
return I18n.tr("Enabled", "bluetooth status");
}
case "audioOutput":
return AudioService.sink?.description || I18n.tr("No output device", "audio status");
case "audioInput":
return AudioService.source?.description || I18n.tr("No input device", "audio status");
default:
return widgetDef?.text || I18n.tr("Unknown", "widget status");
}
}
secondaryText: {
switch (widgetData.id || "") {
case "wifi":
{
if (NetworkService.wifiToggling)
return I18n.tr("Please wait...", "network status");
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
return I18n.tr("Connecting...", "network status");
const status = NetworkService.networkStatus;
if (status === "ethernet")
return I18n.tr("Connected", "network status");
if (status === "cellular")
return NetworkService.cellularIP || I18n.tr("Connected", "network status");
if (status === "vpn") {
if (NetworkService.ethernetConnected)
return I18n.tr("Connected", "network status");
if (NetworkService.cellularConnected)
return NetworkService.cellularIP || I18n.tr("Connected", "network status");
if (NetworkService.wifiConnected)
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : I18n.tr("Connected", "network status");
}
if (status === "wifi")
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : I18n.tr("Connected", "network status");
if (NetworkService.wifiEnabled)
return I18n.tr("Select network", "network status");
return "";
}
case "bluetooth":
{
if (!BluetoothService.available)
return I18n.tr("No adapters", "bluetooth status");
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
return I18n.tr("Off", "bluetooth status");
if (BluetoothService.connecting)
return I18n.tr("Connecting...", "bluetooth status");
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) {
const name = primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || I18n.tr("Connected Device", "bluetooth status");
// A reported 0% is valid; batteryAvailable distinguishes missing data.
// https://quickshell.org/docs/v0.3.1/types/Quickshell.Bluetooth/BluetoothDevice/
// https://bluez.readthedocs.io/en/latest/battery-api/
if (primaryDevice.batteryAvailable)
return `${name} • ${Math.round(primaryDevice.battery * 100)}%`;
const btBattery = BatteryService.bluetoothDevices.find(dev => dev.name === name || dev.name.toLowerCase().includes(name.toLowerCase()) || name.toLowerCase().includes(dev.name.toLowerCase()));
if (btBattery)
return `${name} • ${btBattery.percentage}%`;
return name;
}
return I18n.tr("No devices", "bluetooth status");
}
case "audioOutput":
{
if (!AudioService.sink?.audio)
return I18n.tr("Select device", "audio status");
if (AudioService.sink.audio.muted)
return I18n.tr("Muted", "audio status");
const volume = AudioService.sink.audio.volume;
if (typeof volume !== "number" || isNaN(volume))
return "0%";
return Math.round(volume * 100) + "%";
}
case "audioInput":
{
if (!AudioService.source?.audio)
return I18n.tr("Select device", "audio status");
if (AudioService.source.audio.muted)
return I18n.tr("Muted", "audio status");
const volume = AudioService.source.audio.volume;
if (typeof volume !== "number" || isNaN(volume))
return "0%";
return Math.round(volume * 100) + "%";
}
default:
return widgetDef?.description || "";
}
}
isActive: root.getCompoundPillIsActive(widgetData.id || "")
enabled: widgetDef?.enabled ?? true
onToggled: {
if (root.editMode)
return;
root.handleCompoundPillToggled(widgetData.id || "");
}
onExpandClicked: {
if (root.editMode)
return;
root.expandClicked(widgetData, widgetIndex);
}
onWheelEvent: function (wheelEvent) {
if (root.editMode)
return;
root.handleCompoundPillWheelEvent(widgetData.id || "", wheelEvent);
}
}
}
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
sliderTrackColor: Theme.ccSliderTrackColor
sliderTrackOpacity: Theme.ccSliderTrackOpacity
}
}
}
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
sliderTrackColor: Theme.ccSliderTrackColor
sliderTrackOpacity: Theme.ccSliderTrackOpacity
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
sliderTrackColor: Theme.ccSliderTrackColor
sliderTrackOpacity: Theme.ccSliderTrackOpacity
}
}
}
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: dndPillComponent
DndPill {
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 "idleInhibitor":
return "motion_sensor_active";
default:
return "help";
}
}
text: {
switch (widgetData.id || "") {
case "nightMode":
return I18n.tr("Night Mode");
case "darkMode":
{
if (SettingsData.matugenSmartMode && Theme.currentTheme === Theme.dynamic)
return SessionData.isLightMode ? I18n.tr("Auto (Light Mode)", "dark mode toggle label when matugen smart mode resolved light") : I18n.tr("Auto (Dark Mode)", "dark mode toggle label when matugen smart mode resolved dark");
return I18n.tr("Dark Mode");
}
case "idleInhibitor":
return SessionService.idleInhibited ? I18n.tr("Keeping Awake") : I18n.tr("Keep Awake");
default:
return I18n.tr("Unknown", "widget status");
}
}
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 "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 "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 "do_not_disturb_on";
case "idleInhibitor":
return "motion_sensor_active";
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 || ""
showMountPath: widgetData.showMountPath !== undefined ? widgetData.showMountPath : true
onExpandClicked: {
if (!root.editMode) {
root.expandClicked(widgetData, widgetIndex);
}
}
}
}
Component {
id: smallDiskUsageComponent
SmallDiskUsageButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 48
mountPath: widgetData.mountPath || "/"
instanceId: widgetData.instanceId || ""
showMountPath: widgetData.showMountPath !== undefined ? widgetData.showMountPath : true
onClicked: {
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);
}
if (id === "builtin_tailscale") {
if (root.model?.tailscaleLoader) {
root.model.tailscaleLoader.active = true;
}
builtinInstance = Qt.binding(() => root.model?.tailscaleBuiltinInstance);
}
if (id === "builtin_display_profiles") {
if (root.model?.displayProfilesLoader) {
root.model.displayProfilesLoader.active = true;
}
builtinInstance = Qt.binding(() => root.model?.displayProfilesBuiltinInstance);
}
}
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;
}
}
function tryCreatePluginInstance() {
const pluginComponent = PluginService.pluginWidgetComponents[pluginId];
if (!pluginComponent)
return false;
try {
const instance = pluginComponent.createObject(null, {
"pluginId": pluginId,
"pluginService": PluginService,
"visible": false,
"width": 0,
"height": 0
});
if (instance) {
pluginInstance = instance;
return true;
}
} catch (e) {
log.warn("stale plugin component for", pluginId, "- reloading");
PluginService.reloadPlugin(pluginId);
}
return false;
}
Component.onCompleted: {
Qt.callLater(() => tryCreatePluginInstance());
}
Connections {
target: PluginService
function onPluginDataChanged(changedPluginId) {
if (changedPluginId === pluginId && pluginInstance) {
pluginInstance.loadPluginData();
}
}
function onPluginLoaded(loadedPluginId) {
if (loadedPluginId !== pluginId || pluginInstance)
return;
Qt.callLater(() => tryCreatePluginInstance());
}
}
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();
}
}
}
}
Component {
id: smallCompoundComponent
SmallCompoundButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
width: parent.width
height: 48
iconBlinking: root.getCompoundPillIconBlinking(widgetData.id || "")
iconName: root.getCompoundPillIconName(widgetData.id || "", widgetDef)
isActive: root.getCompoundPillIsActive(widgetData.id || "")
enabled: (widgetDef?.enabled ?? true) && !root.editMode
onToggled: {
if (root.editMode)
return;
root.handleCompoundPillToggled(widgetData.id || "");
}
onExpandClicked: {
if (root.editMode)
return;
root.expandClicked(widgetData, widgetIndex);
}
onWheelEvent: function (wheelEvent) {
if (root.editMode)
return;
root.handleCompoundPillWheelEvent(widgetData.id || "", wheelEvent);
}
}
}
Component {
id: smallColorPickerComponent
SmallColorPickerButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 48
colorPickerModal: root.colorPickerModal
onClicked: {
if (root.editMode)
return;
if (root.colorPickerModal)
root.colorPickerModal.show();
}
}
}
}