pragma ComponentBehavior: Bound import QtQuick import Quickshell import qs.Common import qs.Modules.Plugins import qs.Services import qs.Widgets BasePill { id: root property bool isActive: false property var popoutTarget: null property var widgetData: null property string screenName: "" property string screenModel: "" property bool showNetworkIcon: widgetData?.showNetworkIcon !== undefined ? widgetData.showNetworkIcon : SettingsData.controlCenterShowNetworkIcon property bool showBluetoothIcon: widgetData?.showBluetoothIcon !== undefined ? widgetData.showBluetoothIcon : SettingsData.controlCenterShowBluetoothIcon property bool showAudioIcon: widgetData?.showAudioIcon !== undefined ? widgetData.showAudioIcon : SettingsData.controlCenterShowAudioIcon property bool showAudioPercent: widgetData?.showAudioPercent !== undefined ? widgetData.showAudioPercent : SettingsData.controlCenterShowAudioPercent property bool showVpnIcon: widgetData?.showVpnIcon !== undefined ? widgetData.showVpnIcon : SettingsData.controlCenterShowVpnIcon property bool showBrightnessIcon: widgetData?.showBrightnessIcon !== undefined ? widgetData.showBrightnessIcon : SettingsData.controlCenterShowBrightnessIcon property bool showBrightnessPercent: widgetData?.showBrightnessPercent !== undefined ? widgetData.showBrightnessPercent : SettingsData.controlCenterShowBrightnessPercent property bool showMicIcon: widgetData?.showMicIcon !== undefined ? widgetData.showMicIcon : SettingsData.controlCenterShowMicIcon property bool showMicPercent: widgetData?.showMicPercent !== undefined ? widgetData.showMicPercent : SettingsData.controlCenterShowMicPercent property bool showBatteryIcon: widgetData?.showBatteryIcon !== undefined ? widgetData.showBatteryIcon : SettingsData.controlCenterShowBatteryIcon property bool showPrinterIcon: widgetData?.showPrinterIcon !== undefined ? widgetData.showPrinterIcon : SettingsData.controlCenterShowPrinterIcon property bool showScreenSharingIcon: widgetData?.showScreenSharingIcon !== undefined ? widgetData.showScreenSharingIcon : SettingsData.controlCenterShowScreenSharingIcon property real touchpadThreshold: 100 property real micAccumulator: 0 property real volumeAccumulator: 0 property real brightnessAccumulator: 0 readonly property real vIconSize: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) property var _hRow: null property var _vCol: null property var _hAudio: null property var _hBrightness: null property var _hMic: null property var _vAudio: null property var _vBrightness: null property var _vMic: null property var _interactionDelegates: [] readonly property var defaultControlCenterGroupOrder: ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"] readonly property var effectiveControlCenterGroupOrder: getEffectiveControlCenterGroupOrder() readonly property var controlCenterRenderModel: getControlCenterRenderModel() onIsVerticalOrientationChanged: root.clearInteractionRefs() onWheel: function (wheelEvent) { const delta = wheelEvent.angleDelta.y; if (delta === 0) return; root.refreshInteractionRefs(); const rootX = wheelEvent.x - root.leftMargin; const rootY = wheelEvent.y - root.topMargin; if (root.isVerticalOrientation && _vCol) { const pos = root.mapToItem(_vCol, rootX, rootY); if (_vBrightness?.visible && pos.y >= _vBrightness.y && pos.y < _vBrightness.y + _vBrightness.height) { root.handleBrightnessWheel(delta); } else if (_vMic?.visible && pos.y >= _vMic.y && pos.y < _vMic.y + _vMic.height) { root.handleMicWheel(delta); } else { root.handleVolumeWheel(delta); } } else if (_hRow) { const pos = root.mapToItem(_hRow, rootX, rootY); if (_hBrightness?.visible && pos.x >= _hBrightness.x && pos.x < _hBrightness.x + _hBrightness.width) { root.handleBrightnessWheel(delta); } else if (_hMic?.visible && pos.x >= _hMic.x && pos.x < _hMic.x + _hMic.width) { root.handleMicWheel(delta); } else { root.handleVolumeWheel(delta); } } else { root.handleVolumeWheel(delta); } wheelEvent.accepted = true; } onRightClicked: function (rootX, rootY) { root.refreshInteractionRefs(); if (root.isVerticalOrientation && _vCol) { const pos = root.mapToItem(_vCol, rootX, rootY); if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) { AudioService.toggleMute(); return; } if (_vMic?.visible && pos.y >= _vMic.y && pos.y < _vMic.y + _vMic.height) { AudioService.toggleMicMute(); return; } } else if (_hRow) { const pos = root.mapToItem(_hRow, rootX, rootY); if (_hAudio?.visible && pos.x >= _hAudio.x && pos.x < _hAudio.x + _hAudio.width) { AudioService.toggleMute(); return; } if (_hMic?.visible && pos.x >= _hMic.x && pos.x < _hMic.x + _hMic.width) { AudioService.toggleMicMute(); return; } } } Loader { active: root.showPrinterIcon sourceComponent: Component { Ref { service: CupsService } } } function getNetworkIconName() { if (NetworkService.wifiToggling) return "sync"; switch (NetworkService.networkStatus) { case "ethernet": return "lan"; case "vpn": return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon; default: return NetworkService.wifiSignalIcon; } } function getNetworkIconColor() { if (NetworkService.wifiToggling) return Theme.primary; return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.surfaceText; } function getVolumeIconName() { if (!AudioService.sink?.audio) return "volume_up"; if (AudioService.sink.audio.muted) return "volume_off"; if (AudioService.sink.audio.volume === 0) return "volume_mute"; if (AudioService.sink.audio.volume * 100 < 33) return "volume_down"; return "volume_up"; } function getMicIconName() { if (!AudioService.source?.audio) return "mic"; if (AudioService.source.audio.muted || AudioService.source.audio.volume === 0) return "mic_off"; return "mic"; } function getMicIconColor() { if (!AudioService.source?.audio) return Theme.surfaceText; if (AudioService.source.audio.muted || AudioService.source.audio.volume === 0) return Theme.surfaceText; return Theme.widgetIconColor; } function getBrightnessIconName() { const deviceName = getPinnedBrightnessDevice(); if (!deviceName) return "brightness_medium"; const level = DisplayService.getDeviceBrightness(deviceName); if (level <= 33) return "brightness_low"; if (level <= 66) return "brightness_medium"; return "brightness_high"; } function getScreenPinKey() { if (!root.screenName) return ""; const screen = Quickshell.screens.find(s => s.name === root.screenName); if (screen) { return SettingsData.getScreenDisplayName(screen); } if (SettingsData.displayNameMode === "model" && root.screenModel && root.screenModel.length > 0) { return root.screenModel; } return root.screenName; } function getPinnedBrightnessDevice() { const pinKey = getScreenPinKey(); if (!pinKey) return ""; const pins = SettingsData.brightnessDevicePins || {}; return pins[pinKey] || ""; } function hasPinnedBrightnessDevice() { return getPinnedBrightnessDevice().length > 0; } function handleVolumeWheel(delta) { if (!AudioService.sink?.audio) return; var step = 5; const isMouseWheel = Math.abs(delta) >= 120 && (Math.abs(delta) % 120) === 0; if (!isMouseWheel) { step = 1; volumeAccumulator += delta; if (Math.abs(volumeAccumulator) < touchpadThreshold) return; delta = volumeAccumulator; volumeAccumulator = 0; } const maxVol = AudioService.sinkMaxVolume; const currentVolume = AudioService.sink.audio.volume * 100; const newVolume = delta > 0 ? Math.min(maxVol, currentVolume + step) : Math.max(0, currentVolume - step); AudioService.sink.audio.muted = false; AudioService.sink.audio.volume = newVolume / 100; AudioService.playVolumeChangeSoundIfEnabled(); } function handleMicWheel(delta) { if (!AudioService.source?.audio) return; var step = 5; const isMouseWheel = Math.abs(delta) >= 120 && (Math.abs(delta) % 120) === 0; if (!isMouseWheel) { step = 1; micAccumulator += delta; if (Math.abs(micAccumulator) < touchpadThreshold) return; delta = micAccumulator; micAccumulator = 0; } const currentVolume = AudioService.source.audio.volume * 100; const newVolume = delta > 0 ? Math.min(100, currentVolume + step) : Math.max(0, currentVolume - step); AudioService.source.audio.muted = false; AudioService.source.audio.volume = newVolume / 100; } function handleBrightnessWheel(delta) { const deviceName = getPinnedBrightnessDevice(); if (!deviceName) { return; } var step = 5; const isMouseWheel = Math.abs(delta) >= 120 && (Math.abs(delta) % 120) === 0; if (!isMouseWheel) { step = 1; brightnessAccumulator += delta; if (Math.abs(brightnessAccumulator) < touchpadThreshold) return; delta = brightnessAccumulator; brightnessAccumulator = 0; } const currentBrightness = DisplayService.getDeviceBrightness(deviceName); const newBrightness = delta > 0 ? Math.min(100, currentBrightness + step) : Math.max(1, currentBrightness - step); DisplayService.setBrightness(newBrightness, deviceName); } function getBrightness() { const deviceName = getPinnedBrightnessDevice(); if (!deviceName) { return; } return DisplayService.getDeviceBrightness(deviceName) / 100; } function getBatteryIconColor() { if (!BatteryService.batteryAvailable) return Theme.widgetIconColor; if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error; if (BatteryService.isCharging || BatteryService.isPluggedIn) return Theme.primary; return Theme.widgetIconColor; } function hasPrintJobs() { return CupsService.getTotalJobsNum() > 0; } function getControlCenterIconSize() { return Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale); } function getEffectiveControlCenterGroupOrder() { const knownIds = root.defaultControlCenterGroupOrder; const savedOrder = root.widgetData?.controlCenterGroupOrder; const result = []; const seen = {}; if (savedOrder && typeof savedOrder.length === "number") { for (let i = 0; i < savedOrder.length; ++i) { const groupId = savedOrder[i]; if (knownIds.indexOf(groupId) === -1 || seen[groupId]) continue; seen[groupId] = true; result.push(groupId); } } for (let i = 0; i < knownIds.length; ++i) { const groupId = knownIds[i]; if (seen[groupId]) continue; seen[groupId] = true; result.push(groupId); } return result; } function isGroupVisible(groupId) { switch (groupId) { case "screenSharing": return root.showScreenSharingIcon && NiriService.hasCasts; case "network": return root.showNetworkIcon && NetworkService.networkAvailable; case "vpn": return root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected; case "bluetooth": return root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled; case "audio": return root.showAudioIcon; case "microphone": return root.showMicIcon; case "brightness": return root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice(); case "battery": return root.showBatteryIcon && BatteryService.batteryAvailable; case "printer": return root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs(); default: return false; } } function isCompositeGroup(groupId) { return groupId === "audio" || groupId === "microphone" || groupId === "brightness"; } function getControlCenterRenderModel() { return root.effectiveControlCenterGroupOrder.map(groupId => ({ "id": groupId, "visible": root.isGroupVisible(groupId), "composite": root.isCompositeGroup(groupId) })); } function clearInteractionRefs() { root._hAudio = null; root._hBrightness = null; root._hMic = null; root._vAudio = null; root._vBrightness = null; root._vMic = null; } function registerInteractionDelegate(isVertical, item) { if (!item) return; for (let i = 0; i < root._interactionDelegates.length; ++i) { const entry = root._interactionDelegates[i]; if (entry && entry.item === item) { entry.isVertical = isVertical; return; } } root._interactionDelegates = root._interactionDelegates.concat([ { "isVertical": isVertical, "item": item } ]); } function unregisterInteractionDelegate(item) { if (!item) return; root._interactionDelegates = root._interactionDelegates.filter(entry => entry && entry.item !== item); } function refreshInteractionRefs() { root.clearInteractionRefs(); for (let i = 0; i < root._interactionDelegates.length; ++i) { const entry = root._interactionDelegates[i]; const item = entry?.item; if (!item || !item.visible) continue; const groupId = item.interactionGroupId; if (entry.isVertical) { if (groupId === "audio") root._vAudio = item; else if (groupId === "microphone") root._vMic = item; else if (groupId === "brightness") root._vBrightness = item; } else { if (groupId === "audio") root._hAudio = item; else if (groupId === "microphone") root._hMic = item; else if (groupId === "brightness") root._hBrightness = item; } } } function hasNoVisibleIcons() { return !root.controlCenterRenderModel.some(entry => entry.visible); } content: Component { Item { implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : controlIndicators.implicitWidth implicitHeight: root.isVerticalOrientation ? controlColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2) Component.onCompleted: { root._hRow = controlIndicators; root._vCol = controlColumn; root.clearInteractionRefs(); } Column { id: controlColumn visible: root.isVerticalOrientation width: parent.width anchors.horizontalCenter: parent.horizontalCenter spacing: Theme.spacingXS Repeater { model: root.controlCenterRenderModel Item { id: verticalGroupItem required property var modelData required property int index property string interactionGroupId: modelData.id width: parent.width height: { switch (modelData.id) { case "audio": return root.vIconSize + (audioPercentV.visible ? audioPercentV.implicitHeight + 2 : 0); case "microphone": return root.vIconSize + (micPercentV.visible ? micPercentV.implicitHeight + 2 : 0); case "brightness": return root.vIconSize + (brightnessPercentV.visible ? brightnessPercentV.implicitHeight + 2 : 0); default: return root.vIconSize; } } visible: modelData.visible Component.onCompleted: { root.registerInteractionDelegate(true, verticalGroupItem); root.refreshInteractionRefs(); } Component.onDestruction: { if (root) { root.unregisterInteractionDelegate(verticalGroupItem); root.refreshInteractionRefs(); } } onVisibleChanged: root.refreshInteractionRefs() onInteractionGroupIdChanged: { root.refreshInteractionRefs(); } DankIcon { anchors.centerIn: parent visible: !verticalGroupItem.modelData.composite name: { switch (verticalGroupItem.modelData.id) { case "screenSharing": return "screen_record"; case "network": return root.getNetworkIconName(); case "vpn": return "vpn_lock"; case "bluetooth": return "bluetooth"; case "battery": return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable); case "printer": return "print"; default: return "settings"; } } size: root.vIconSize color: { switch (verticalGroupItem.modelData.id) { case "screenSharing": return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText; case "network": return root.getNetworkIconColor(); case "vpn": return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText; case "bluetooth": return BluetoothService.connected ? Theme.primary : Theme.surfaceText; case "battery": return root.getBatteryIconColor(); case "printer": return Theme.primary; default: return Theme.widgetIconColor; } } } DankIcon { id: audioIconV visible: verticalGroupItem.modelData.id === "audio" name: root.getVolumeIconName() size: root.vIconSize color: Theme.widgetIconColor anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top } NumericText { id: audioPercentV visible: verticalGroupItem.modelData.id === "audio" && root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume) text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" reserveText: "100%" font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) color: Theme.widgetTextColor anchors.horizontalCenter: parent.horizontalCenter anchors.top: audioIconV.bottom anchors.topMargin: 2 } DankIcon { id: micIconV visible: verticalGroupItem.modelData.id === "microphone" name: root.getMicIconName() size: root.vIconSize color: root.getMicIconColor() anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top } NumericText { id: micPercentV visible: verticalGroupItem.modelData.id === "microphone" && root.showMicPercent && isFinite(AudioService.source?.audio?.volume) text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" reserveText: "100%" font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) color: Theme.widgetTextColor anchors.horizontalCenter: parent.horizontalCenter anchors.top: micIconV.bottom anchors.topMargin: 2 } DankIcon { id: brightnessIconV visible: verticalGroupItem.modelData.id === "brightness" name: root.getBrightnessIconName() size: root.vIconSize color: Theme.widgetIconColor anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top } NumericText { id: brightnessPercentV visible: verticalGroupItem.modelData.id === "brightness" && root.showBrightnessPercent && isFinite(getBrightness()) text: Math.round(getBrightness() * 100) + "%" reserveText: "100%" font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) color: Theme.widgetTextColor anchors.horizontalCenter: parent.horizontalCenter anchors.top: brightnessIconV.bottom anchors.topMargin: 2 } } } Item { width: parent.width height: root.vIconSize visible: root.hasNoVisibleIcons() DankIcon { name: "settings" size: root.vIconSize color: root.isActive ? Theme.primary : Theme.widgetIconColor anchors.centerIn: parent } } } Row { id: controlIndicators visible: !root.isVerticalOrientation anchors.centerIn: parent spacing: Theme.spacingXS Repeater { model: root.controlCenterRenderModel Item { id: horizontalGroupItem required property var modelData required property int index property string interactionGroupId: modelData.id width: { switch (modelData.id) { case "audio": return audioGroup.width; case "microphone": return micGroup.width; case "brightness": return brightnessGroup.width; default: return root.getControlCenterIconSize(); } } implicitWidth: width height: root.widgetThickness - root.horizontalPadding * 2 visible: modelData.visible Component.onCompleted: { root.registerInteractionDelegate(false, horizontalGroupItem); root.refreshInteractionRefs(); } Component.onDestruction: { if (root) { root.unregisterInteractionDelegate(horizontalGroupItem); root.refreshInteractionRefs(); } } onVisibleChanged: root.refreshInteractionRefs() onInteractionGroupIdChanged: { root.refreshInteractionRefs(); } DankIcon { id: iconOnlyItem anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left visible: !horizontalGroupItem.modelData.composite name: { switch (horizontalGroupItem.modelData.id) { case "screenSharing": return "screen_record"; case "network": return root.getNetworkIconName(); case "vpn": return "vpn_lock"; case "bluetooth": return "bluetooth"; case "battery": return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable); case "printer": return "print"; default: return "settings"; } } size: root.getControlCenterIconSize() color: { switch (horizontalGroupItem.modelData.id) { case "screenSharing": return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText; case "network": return root.getNetworkIconColor(); case "vpn": return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText; case "bluetooth": return BluetoothService.connected ? Theme.primary : Theme.surfaceText; case "battery": return root.getBatteryIconColor(); case "printer": return Theme.primary; default: return Theme.widgetIconColor; } } } Rectangle { id: audioGroup width: audioContent.implicitWidth + 2 implicitWidth: width height: parent.height color: "transparent" anchors.verticalCenter: parent.verticalCenter visible: horizontalGroupItem.modelData.id === "audio" Row { id: audioContent anchors.left: parent.left anchors.leftMargin: 1 anchors.verticalCenter: parent.verticalCenter spacing: 2 DankIcon { id: audioIcon name: root.getVolumeIconName() size: root.getControlCenterIconSize() color: Theme.widgetIconColor anchors.verticalCenter: parent.verticalCenter } NumericText { id: audioPercent visible: root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume) text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" reserveText: "100%" font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) color: Theme.widgetTextColor anchors.verticalCenter: parent.verticalCenter width: visible ? implicitWidth : 0 } } } Rectangle { id: micGroup width: micContent.implicitWidth + 2 implicitWidth: width height: parent.height color: "transparent" anchors.verticalCenter: parent.verticalCenter visible: horizontalGroupItem.modelData.id === "microphone" Row { id: micContent anchors.left: parent.left anchors.leftMargin: 1 anchors.verticalCenter: parent.verticalCenter spacing: 2 DankIcon { id: micIcon name: root.getMicIconName() size: root.getControlCenterIconSize() color: root.getMicIconColor() anchors.verticalCenter: parent.verticalCenter } NumericText { id: micPercent visible: root.showMicPercent && isFinite(AudioService.source?.audio?.volume) text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" reserveText: "100%" font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) color: Theme.widgetTextColor anchors.verticalCenter: parent.verticalCenter width: visible ? implicitWidth : 0 } } } Rectangle { id: brightnessGroup width: brightnessContent.implicitWidth + 2 implicitWidth: width height: parent.height color: "transparent" anchors.verticalCenter: parent.verticalCenter visible: horizontalGroupItem.modelData.id === "brightness" Row { id: brightnessContent anchors.left: parent.left anchors.leftMargin: 1 anchors.verticalCenter: parent.verticalCenter spacing: 2 DankIcon { id: brightnessIcon name: root.getBrightnessIconName() size: root.getControlCenterIconSize() color: Theme.widgetIconColor anchors.verticalCenter: parent.verticalCenter } NumericText { id: brightnessPercent visible: root.showBrightnessPercent && isFinite(getBrightness()) text: Math.round(getBrightness() * 100) + "%" reserveText: "100%" font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) color: Theme.widgetTextColor anchors.verticalCenter: parent.verticalCenter width: visible ? implicitWidth : 0 } } } } } DankIcon { name: "settings" size: root.getControlCenterIconSize() color: root.isActive ? Theme.primary : Theme.widgetIconColor anchors.verticalCenter: parent.verticalCenter visible: root.hasNoVisibleIcons() } } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor acceptedButtons: Qt.NoButton } } } }