This commit is contained in:
2026-05-28 21:45:21 +02:00
parent 653439ac24
commit a356a3d369
610 changed files with 205679 additions and 0 deletions
@@ -0,0 +1,385 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.Compositor
import qs.Services.UI
import qs.Widgets
Item {
id: root
Layout.preferredHeight: isVerticalBar ? -1 : Style.getBarHeightForScreen(screenName)
Layout.preferredWidth: isVerticalBar ? Style.getBarHeightForScreen(screenName) : -1
Layout.fillHeight: false
Layout.fillWidth: false
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] || {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length && widgets[sectionWidgetIndex]) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
// Widget settings - matching MediaMini pattern
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : (widgetMetadata.showIcon || false)
readonly property bool showText: (widgetSettings.showText !== undefined) ? widgetSettings.showText : (widgetMetadata.showText || false)
readonly property string hideMode: (widgetSettings.hideMode !== undefined) ? widgetSettings.hideMode : (widgetMetadata.hideMode || "hidden")
readonly property string scrollingMode: (widgetSettings.scrollingMode !== undefined) ? widgetSettings.scrollingMode : (widgetMetadata.scrollingMode || "hover")
// Maximum widget width with user settings support
readonly property real maxWidth: (widgetSettings.maxWidth !== undefined) ? widgetSettings.maxWidth : Math.max(widgetMetadata.maxWidth || 0, screen ? screen.width * 0.06 : 0)
readonly property bool useFixedWidth: (widgetSettings.useFixedWidth !== undefined) ? widgetSettings.useFixedWidth : (widgetMetadata.useFixedWidth || false)
readonly property string textColorKey: (widgetSettings.textColor !== undefined) ? widgetSettings.textColor : widgetMetadata.textColor
readonly property color textColor: Color.resolveColorKey(textColorKey)
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
readonly property real barHeight: Style.getBarHeightForScreen(screenName)
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
readonly property bool hasFocusedWindow: CompositorService.getFocusedWindow() !== null
readonly property string windowTitle: CompositorService.getFocusedWindowTitle() || "No active window"
readonly property string fallbackIcon: "user-desktop"
readonly property int iconSize: Style.toOdd(capsuleHeight * 0.75)
readonly property int verticalSize: Style.toOdd(capsuleHeight * 0.85)
// For horizontal bars, height is always barHeight (no animation needed)
// For vertical bars, collapse to 0 when hidden
implicitHeight: isVerticalBar ? (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : verticalSize) : barHeight
implicitWidth: isVerticalBar ? (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : verticalSize) : (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : dynamicWidth)
// "visible": Always Visible, "hidden": Hide When Empty, "transparent": Transparent When Empty
visible: (hideMode !== "hidden" || hasFocusedWindow) || opacity > 0
opacity: ((hideMode !== "hidden" || hasFocusedWindow) && (hideMode !== "transparent" || hasFocusedWindow)) ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
}
function calculateContentWidth() {
// Calculate the actual content width based on visible elements
var contentWidth = 0;
var margins = Style.margin2S; // Left and right margins
// Icon width (if visible)
if (showIcon) {
contentWidth += iconSize;
if (showText) {
contentWidth += Style.marginS; // Spacing after icon
}
}
// Text width (use the measured width)
if (showText) {
contentWidth += titleContainer.measuredWidth;
// Additional small margin for text
contentWidth += Style.margin2XXS;
}
// Add container margins
contentWidth += margins;
return Math.ceil(contentWidth);
}
// Dynamic width: adapt to content but respect maximum width setting
readonly property real dynamicWidth: {
// If using fixed width mode, always use maxWidth
if (useFixedWidth) {
return maxWidth;
}
// Otherwise, adapt to content
if (!hasFocusedWindow) {
return Math.min(calculateContentWidth(), maxWidth);
}
// Use content width but don't exceed user-set maximum width
return Math.min(calculateContentWidth(), maxWidth);
}
function getAppIcon() {
try {
// Try CompositorService first
const focusedWindow = CompositorService.getFocusedWindow();
if (focusedWindow && focusedWindow.appId) {
try {
const idValue = focusedWindow.appId;
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue);
const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase());
if (iconResult && iconResult !== "") {
return iconResult;
}
} catch (iconError) {
Logger.w("ActiveWindow", "Error getting icon from CompositorService:", iconError);
}
}
if (CompositorService.isHyprland) {
// Fallback to ToplevelManager
if (ToplevelManager && ToplevelManager.activeToplevel) {
try {
const activeToplevel = ToplevelManager.activeToplevel;
if (activeToplevel.appId) {
const idValue2 = activeToplevel.appId;
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2);
const iconResult2 = ThemeIcons.iconForAppId(normalizedId2.toLowerCase());
if (iconResult2 && iconResult2 !== "") {
return iconResult2;
}
}
} catch (fallbackError) {
Logger.w("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError);
}
}
}
return ThemeIcons.iconFromName(fallbackIcon);
} catch (e) {
Logger.w("ActiveWindow", "Error in getAppIcon:", e);
return ThemeIcons.iconFromName(fallbackIcon);
}
}
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
Rectangle {
id: windowActiveRect
visible: root.visible
x: isVerticalBar ? Style.pixelAlignCenter(parent.width, width) : 0
y: isVerticalBar ? 0 : Style.pixelAlignCenter(parent.height, height)
width: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : verticalSize) : ((!hasFocusedWindow) && (hideMode === "hidden") ? 0 : dynamicWidth)
height: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : verticalSize) : capsuleHeight
radius: Style.radiusM
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
// Smooth width transition
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
}
Item {
id: mainContainer
anchors.fill: parent
anchors.leftMargin: isVerticalBar ? 0 : Style.marginS
anchors.rightMargin: isVerticalBar ? 0 : Style.marginS
// Horizontal layout for top/bottom bars
RowLayout {
id: rowLayout
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS
visible: !isVerticalBar
z: 1
// Window icon
Item {
Layout.preferredWidth: iconSize
Layout.preferredHeight: iconSize
Layout.alignment: Qt.AlignVCenter
visible: showIcon
IconImage {
id: windowIcon
anchors.fill: parent
source: getAppIcon()
asynchronous: true
smooth: true
visible: source !== ""
// Apply dock shader to active window icon (always themed)
layer.enabled: widgetSettings.colorizeIcons !== false
layer.effect: ShaderEffect {
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
property real colorizeMode: 0.0 // Dock mode (grayscale)
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
}
}
}
NScrollText {
id: titleContainer
text: windowTitle
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: root.capsuleHeight
fadeRoundLeftCorners: !showIcon
visible: showText
maxWidth: {
// Calculate available width based on other elements
var iconWidth = (showIcon && windowIcon.visible ? (iconSize + Style.marginS) : 0);
var totalMargins = Style.margin2XXS;
var availableWidth = mainContainer.width - iconWidth - totalMargins;
return Math.max(20, availableWidth);
}
scrollMode: {
if (scrollingMode === "always")
return NScrollText.ScrollMode.Always;
if (scrollingMode === "hover")
return NScrollText.ScrollMode.Hover;
return NScrollText.ScrollMode.Never;
}
forcedHover: mainMouseArea.containsMouse
fadeExtent: 0.1
fadeCornerRadius: Style.radiusM
NText {
text: windowTitle
pointSize: barFontSize
applyUiScale: false
font.weight: Style.fontWeightMedium
color: root.textColor
}
}
}
// Vertical layout for left/right bars - icon only
Item {
id: verticalLayout
width: parent.width - Style.margin2M
height: parent.height - Style.margin2M
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
visible: isVerticalBar
z: 1
// Window icon
Item {
id: verticalIconContainer
width: root.iconSize
height: width
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
visible: windowTitle !== ""
IconImage {
id: windowIconVertical
anchors.fill: parent
source: getAppIcon()
asynchronous: true
smooth: true
visible: source !== ""
// Apply dock shader to active window icon (always themed)
layer.enabled: widgetSettings.colorizeIcons !== false
layer.effect: ShaderEffect {
property color targetColor: Color.mOnSurface
property real colorizeMode: 0.0 // Dock mode (grayscale)
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
}
}
}
}
// Mouse area moved to root
}
}
// Mouse area for hover detection
MouseArea {
id: mainMouseArea
anchors.fill: parent
// Extend click area to screen edge if widget is at the start/end
anchors.leftMargin: (!isVerticalBar && section === "left" && sectionWidgetIndex === 0) ? -Style.marginS : 0
anchors.rightMargin: (!isVerticalBar && section === "right" && sectionWidgetIndex === sectionWidgetsCount - 1) ? -Style.marginS : 0
anchors.topMargin: (isVerticalBar && section === "left" && sectionWidgetIndex === 0) ? -Style.marginM : 0
anchors.bottomMargin: (isVerticalBar && section === "right" && sectionWidgetIndex === sectionWidgetsCount - 1) ? -Style.marginM : 0
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onEntered: {
if ((windowTitle !== "") && isVerticalBar || (scrollingMode === "never")) {
TooltipService.show(root, windowTitle, BarService.getTooltipDirection(root.screen?.name));
}
}
onExited: {
TooltipService.hide();
}
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
PanelService.showContextMenu(contextMenu, root, screen);
}
}
}
Connections {
target: CompositorService
function onActiveWindowChanged() {
try {
windowIcon.source = Qt.binding(getAppIcon);
windowIconVertical.source = Qt.binding(getAppIcon);
} catch (e) {
Logger.w("ActiveWindow", "Error in onActiveWindowChanged:", e);
}
}
function onWindowListChanged() {
try {
windowIcon.source = Qt.binding(getAppIcon);
windowIconVertical.source = Qt.binding(getAppIcon);
} catch (e) {
Logger.w("ActiveWindow", "Error in onWindowListChanged:", e);
}
}
}
}
@@ -0,0 +1,216 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.Media
import qs.Services.UI
import qs.Widgets
import qs.Widgets.AudioSpectrum
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property int visualizerWidth: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width
readonly property bool hideWhenIdle: widgetSettings.hideWhenIdle !== undefined ? widgetSettings.hideWhenIdle : widgetMetadata.hideWhenIdle
readonly property string colorName: widgetSettings.colorName !== undefined ? widgetSettings.colorName : widgetMetadata.colorName
readonly property color fillColor: Color.resolveColorKey(colorName)
readonly property bool shouldShow: (currentVisualizerType !== "" && currentVisualizerType !== "none") && (!hideWhenIdle || MediaService.isPlaying)
// Register/unregister with SpectrumService based on visibility (use screenName — screen can be null after DPMS/output changes)
readonly property string spectrumComponentId: "bar:audiovisualizer:" + screenName + ":" + root.section + ":" + root.sectionWidgetIndex
onShouldShowChanged: {
if (root.shouldShow) {
SpectrumService.registerComponent(root.spectrumComponentId);
} else {
SpectrumService.unregisterComponent(root.spectrumComponentId);
}
}
Component.onDestruction: {
if (root.shouldShow) {
SpectrumService.unregisterComponent(root.spectrumComponentId);
}
}
// Content dimensions for implicit sizing
readonly property real contentWidth: !shouldShow ? 0 : isVerticalBar ? capsuleHeight : visualizerWidth
readonly property real contentHeight: !shouldShow ? 0 : isVerticalBar ? visualizerWidth : capsuleHeight
implicitWidth: contentWidth
implicitHeight: contentHeight
visible: shouldShow
opacity: shouldShow ? 1.0 : 0.0
Behavior on implicitWidth {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
}
// Store visualizer type to force re-evaluation
readonly property string currentVisualizerType: Settings.data.audio.visualizerType
// Visual capsule centered in parent
Rectangle {
id: background
width: root.contentWidth
height: root.contentHeight
anchors.centerIn: parent
radius: Style.radiusS
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
// When visualizer type or playback changes, shouldShow updates automatically
// The Loader dynamically loads the appropriate visualizer based on settings
Loader {
id: visualizerLoader
anchors.fill: parent
anchors.margins: Style.marginS
active: shouldShow
asynchronous: true
sourceComponent: {
switch (currentVisualizerType) {
case "linear":
return linearComponent;
case "mirrored":
return mirroredComponent;
case "wave":
return waveComponent;
default:
return null;
}
}
}
}
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.cycle-visualizer"),
"action": "cycle-visualizer",
"icon": "chart-column"
},
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
if (screen) {
PanelService.closeContextMenu(screen);
}
if (action === "cycle-visualizer") {
const types = ["linear", "mirrored", "wave"];
const currentIndex = types.indexOf(currentVisualizerType);
const nextIndex = (currentIndex + 1) % types.length;
Settings.data.audio.visualizerType = types[nextIndex];
} else if (action === "widget-settings" && screen) {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
// Click to cycle through visualizer types
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (screen) {
PanelService.showContextMenu(contextMenu, root, screen);
}
} else {
const types = ["linear", "mirrored", "wave"];
const currentIndex = types.indexOf(currentVisualizerType);
const nextIndex = (currentIndex + 1) % types.length;
Settings.data.audio.visualizerType = types[nextIndex];
}
}
}
Component {
id: linearComponent
NLinearSpectrum {
anchors.fill: parent
values: SpectrumService.values
fillColor: root.fillColor
showMinimumSignal: true
vertical: root.isVerticalBar
barPosition: root.barPosition
mirrored: Settings.data.audio.spectrumMirrored
}
}
Component {
id: mirroredComponent
NMirroredSpectrum {
anchors.fill: parent
values: SpectrumService.values
fillColor: root.fillColor
showMinimumSignal: true
vertical: root.isVerticalBar
mirrored: Settings.data.audio.spectrumMirrored
}
}
Component {
id: waveComponent
NWaveSpectrum {
anchors.fill: parent
values: SpectrumService.values
fillColor: root.fillColor
showMinimumSignal: true
vertical: root.isVerticalBar
mirrored: Settings.data.audio.spectrumMirrored
}
}
}
@@ -0,0 +1,263 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.UPower
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.Hardware
import qs.Services.Networking
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
readonly property bool useGraphicMode: displayMode === "graphic" || displayMode === "graphic-clean"
readonly property bool hideIfNotDetected: widgetSettings.hideIfNotDetected !== undefined ? widgetSettings.hideIfNotDetected : widgetMetadata.hideIfNotDetected
readonly property bool hideIfIdle: widgetSettings.hideIfIdle !== undefined ? widgetSettings.hideIfIdle : widgetMetadata.hideIfIdle
// Check if selected device is actually present/connected
readonly property bool isReady: BatteryService.isDeviceReady(selectedDevice)
readonly property bool isPresent: BatteryService.isDevicePresent(selectedDevice)
readonly property real percent: isReady ? BatteryService.getPercentage(selectedDevice) : -1
readonly property bool isCharging: isReady ? BatteryService.isCharging(selectedDevice) : false
readonly property bool isPluggedIn: isReady ? BatteryService.isPluggedIn(selectedDevice) : false
readonly property bool isLowBattery: isReady ? BatteryService.isLowBattery(selectedDevice) : false
readonly property bool isCriticalBattery: isReady ? BatteryService.isCriticalBattery(selectedDevice) : false
// Visibility: show if hideIfNotDetected is false, or if battery is ready
readonly property bool shouldShow: !hideIfNotDetected || (isReady && (hideIfIdle ? !isPluggedIn : true))
readonly property string deviceNativePath: widgetSettings.deviceNativePath !== undefined ? widgetSettings.deviceNativePath : widgetMetadata.deviceNativePath
readonly property var selectedDevice: BatteryService.isDevicePresent(BatteryService.findDevice(deviceNativePath)) ? BatteryService.findDevice(deviceNativePath) : null
readonly property var tooltipContent: {
if (!isReady || !isPresent) {
return I18n.tr("battery.no-battery-detected");
}
let rows = [];
const isInternal = selectedDevice.isLaptopBattery;
if (isInternal) {
// Show charge percentage
rows.push([I18n.tr("battery.battery-level"), `${percent}%`]);
let timeText = BatteryService.getTimeRemainingText(selectedDevice);
if (timeText) {
const colonIdx = timeText.indexOf(":");
if (colonIdx >= 0) {
rows.push([timeText.substring(0, colonIdx).trim(), timeText.substring(colonIdx + 1).trim()]);
} else {
rows.push([timeText, ""]);
}
}
let rateText = BatteryService.getRateText(selectedDevice);
if (!isPluggedIn && rateText) {
const colonIdx = rateText.indexOf(":");
if (colonIdx >= 0) {
rows.push([rateText.substring(0, colonIdx).trim(), rateText.substring(colonIdx + 1).trim()]);
} else {
rows.push([rateText, ""]);
}
}
// Show battery health if supported (check actual battery, not DisplayDevice)
let healthDevice = selectedDevice.healthSupported ? selectedDevice : (BatteryService.laptopBatteries.length > 0 ? BatteryService.laptopBatteries[0] : null);
if (healthDevice && healthDevice.healthSupported) {
rows.push([I18n.tr("battery.battery-health"), `${Math.round(healthDevice.healthPercentage)}%`]);
}
} else if (selectedDevice) {
// External / Peripheral Device (Phone, Keyboard, Mouse, Gamepad, Headphone etc.)
let name = BatteryService.getDeviceName(selectedDevice);
rows.push([name, `${percent}%`]);
}
// If we are showing the main laptop battery, append external devices
if (isInternal) {
var external = BatteryService.bluetoothBatteries;
if (external.length > 0) {
if (rows.length > 0) {
rows.push(["---", "---"]); // Separator
}
for (var j = 0; j < external.length; j++) {
var dev = external[j];
var dName = BatteryService.getDeviceName(dev);
var dPct = BatteryService.getPercentage(dev);
rows.push([dName, `${dPct}%`]);
}
}
}
return rows;
}
visible: shouldShow
opacity: shouldShow ? 1.0 : 0.0
implicitWidth: useGraphicMode ? capsule.width : pill.width
implicitHeight: useGraphicMode ? capsule.height : pill.height
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
// ==================== GRAPHIC MODE ====================
// Capsule background (graphic mode only)
Rectangle {
id: capsule
visible: root.useGraphicMode
anchors.centerIn: nBattery
width: root.isBarVertical ? root.capsuleHeight : nBattery.width + Style.margin2S
height: root.isBarVertical ? nBattery.height + Style.margin2S : root.capsuleHeight
radius: Math.min(Style.radiusL, width / 2)
color: graphicMouseArea.containsMouse ? Color.mHover : Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
}
NBattery {
id: nBattery
visible: root.useGraphicMode
anchors.centerIn: parent
baseSize: (Style.getBarHeightForScreen(root.screenName) / root.capsuleHeight) * Style.fontSizeXXS
showPercentageText: root.displayMode !== "graphic-clean"
vertical: root.isBarVertical
percentage: root.percent
ready: root.isReady
charging: root.isCharging
pluggedIn: root.isPluggedIn
low: root.isLowBattery
critical: root.isCriticalBattery
baseColor: graphicMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface
textColor: graphicMouseArea.containsMouse ? Color.mHover : Color.mSurface
}
MouseArea {
id: graphicMouseArea
visible: root.useGraphicMode
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onEntered: {
if (!getBatteryPanel()?.isPanelOpen && root.tooltipContent) {
TooltipService.show(root, root.tooltipContent, BarService.getTooltipDirection(root.screen?.name));
tooltipRefreshTimer.start();
}
}
onExited: {
tooltipRefreshTimer.stop();
TooltipService.hide();
}
onClicked: mouse => {
TooltipService.hide();
if (mouse.button === Qt.RightButton) {
PanelService.showContextMenu(contextMenu, nBattery, screen);
} else {
toggleBatteryPanel();
}
}
}
Timer {
id: tooltipRefreshTimer
interval: 1000
repeat: true
onTriggered: {
if (graphicMouseArea.containsMouse) {
TooltipService.updateText(root.tooltipContent);
}
}
}
// ==================== ICON MODE ====================
BarPill {
id: pill
visible: !root.useGraphicMode
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
icon: BatteryService.getIcon(root.percent, root.isCharging, root.isPluggedIn, root.isReady)
text: root.isReady ? root.percent : "-"
suffix: "%"
autoHide: false
forceOpen: root.isReady && root.displayMode === "icon-always"
forceClose: root.displayMode === "icon-only" || !root.isReady
customBackgroundColor: root.isCharging ? Color.mPrimary : ((root.isLowBattery || root.isCriticalBattery) ? Color.mError : "transparent")
customTextIconColor: root.isCharging ? Color.mOnPrimary : ((root.isLowBattery || root.isCriticalBattery) ? Color.mOnError : "transparent")
tooltipText: !getBatteryPanel()?.isPanelOpen ? root.tooltipContent : ""
onClicked: toggleBatteryPanel()
onRightClicked: PanelService.showContextMenu(contextMenu, pill, screen)
}
// ==================== SHARED ====================
function getBatteryPanel() {
var panel = PanelService.getPanel("batteryPanel", screen);
if (panel) {
panel.panelID = {
showPowerProfiles: widgetSettings.showPowerProfiles !== undefined ? widgetSettings.showPowerProfiles : widgetMetadata.showPowerProfiles,
showNoctaliaPerformance: widgetSettings.showNoctaliaPerformance !== undefined ? widgetSettings.showNoctaliaPerformance : widgetMetadata.showNoctaliaPerformance
};
}
return panel;
}
function toggleBatteryPanel() {
getBatteryPanel()?.toggle(root);
}
}
@@ -0,0 +1,122 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings // For SettingsPanel
import qs.Services.Networking
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
implicitWidth: pill.width
implicitHeight: pill.height
NPopupContextMenu {
id: contextMenu
model: [
{
"label": BluetoothService.enabled ? I18n.tr("actions.disable-bluetooth") : I18n.tr("actions.enable-bluetooth"),
"action": "toggle-bluetooth",
"icon": BluetoothService.enabled ? "bluetooth-off" : "bluetooth",
"enabled": !NetworkService.airplaneModeEnabled && BluetoothService.bluetoothAvailable
},
{
"label": I18n.tr("common.bluetooth") + " " + I18n.tr("tooltips.open-settings"),
"action": "bluetooth-settings",
"icon": "settings"
},
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "toggle-bluetooth") {
BluetoothService.setBluetoothEnabled(!BluetoothService.enabled);
} else if (action === "bluetooth-settings") {
SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 1, screen);
} else if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
BarPill {
id: pill
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
icon: !BluetoothService.enabled ? "bluetooth-off" : ((BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 0) ? "bluetooth-connected" : "bluetooth")
text: {
if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 0) {
const firstDevice = BluetoothService.connectedDevices[0];
return firstDevice.name || firstDevice.deviceName;
}
return "";
}
suffix: {
if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 1) {
return ` + ${BluetoothService.connectedDevices.length - 1}`;
}
return "";
}
autoHide: false
forceOpen: !isBarVertical && root.displayMode === "alwaysShow"
forceClose: isBarVertical || root.displayMode === "alwaysHide" || text === ""
onClicked: {
var p = PanelService.getPanel("bluetoothPanel", screen);
if (p)
p.toggle(this);
}
onRightClicked: {
PanelService.showContextMenu(contextMenu, pill, screen);
}
tooltipText: {
if (PanelService.getPanel("bluetoothPanel", screen)?.isPanelOpen) {
return "";
}
if (pill.text !== "") {
return pill.text;
}
return I18n.tr("tooltips.bluetooth-devices");
}
}
}
@@ -0,0 +1,211 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings
import qs.Services.Hardware
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
readonly property bool applyToAllMonitors: widgetSettings.applyToAllMonitors !== undefined ? widgetSettings.applyToAllMonitors : (Settings.data.brightness.syncAllMonitors !== undefined ? Settings.data.brightness.syncAllMonitors : widgetMetadata.applyToAllMonitors)
readonly property bool reverseScroll: Settings.data.general.reverseScroll
// Used to avoid opening the pill on Quickshell startup
property bool firstBrightnessReceived: false
implicitWidth: pill.width
implicitHeight: pill.height
// Track the brightness monitor reactively via declarative binding so it
// updates atomically when monitors change, avoiding a transient undefined
// state that occurs when Monitor QtObjects are destroyed before the
// imperative updateMonitor() call would run.
property var brightnessMonitor: {
var _ = BrightnessService.monitors; // reactive dependency
var __ = BrightnessService.ddcMonitors; // reactive dependency
if (!screen)
return null;
return BrightnessService.getMonitorForScreen(screen) ?? null;
}
function getControllableMonitorCount() {
var monitors = BrightnessService.monitors || [];
var count = 0;
for (var i = 0; i < monitors.length; i++) {
if (monitors[i] && monitors[i].brightnessControlAvailable)
count++;
}
return count;
}
visible: brightnessMonitor !== null
opacity: brightnessMonitor !== null ? 1.0 : 0.0
function getIcon() {
var monitor = brightnessMonitor;
if (!monitor || !monitor.brightnessControlAvailable || isNaN(monitor.brightness))
return "sun-off";
var brightness = monitor.brightness;
if (brightness <= 0.001)
return "sun-off";
return brightness <= 0.5 ? "brightness-low" : "brightness-high";
}
// Connection used to open the pill when brightness changes
Connections {
target: brightnessMonitor
ignoreUnknownSignals: true
function onBrightnessUpdated() {
// Ignore if this is the first time we receive an update.
// Most likely service just kicked off.
if (!firstBrightnessReceived) {
firstBrightnessReceived = true;
return;
}
pill.show();
hideTimerAfterChange.restart();
}
}
Timer {
id: hideTimerAfterChange
interval: 2500
running: false
repeat: false
onTriggered: pill.hide()
}
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.open-display-settings"),
"action": "open-display-settings",
"icon": "sun"
},
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "open-display-settings") {
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
settingsPanel.requestedTab = SettingsPanel.Tab.Display;
settingsPanel.open();
} else if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
BarPill {
id: pill
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
icon: getIcon()
autoHide: false // Important to be false so we can hover as long as we want
text: {
var monitor = brightnessMonitor;
if (!monitor || !monitor.brightnessControlAvailable || isNaN(monitor.brightness))
return "";
return Math.round(monitor.brightness * 100);
}
suffix: text.length > 0 ? "%" : "-"
forceOpen: displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide"
tooltipText: {
var monitor = brightnessMonitor;
var panel = PanelService.getPanel("brightnessPanel", screen);
if (panel?.isPanelOpen || !monitor || !monitor.brightnessControlAvailable || isNaN(monitor.brightness))
return "";
return I18n.tr("tooltips.brightness-at", {
"brightness": Math.round(monitor.brightness * 100)
});
}
onWheel: function (angle) {
var monitor = brightnessMonitor;
if (!monitor || !monitor.brightnessControlAvailable)
return;
if (root.reverseScroll)
angle *= -1;
if (angle === 0)
return;
var shouldApplyToAll = root.applyToAllMonitors && root.getControllableMonitorCount() > 1;
if (shouldApplyToAll) {
var direction = angle > 0 ? 1 : -1;
var baseValue = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness;
var step = monitor.stepSize;
var minValue = monitor.minBrightnessValue;
if (direction > 0 && Settings.data.brightness.enforceMinimum && baseValue < minValue) {
baseValue = Math.max(step, minValue);
} else {
baseValue = baseValue + direction * step;
}
var targetValue = Math.max(minValue, Math.min(1, baseValue));
BrightnessService.monitors.forEach(function (m) {
if (m && m.brightnessControlAvailable) {
m.setBrightnessDebounced(targetValue);
}
});
} else if (angle > 0) {
monitor.increaseBrightness();
} else if (angle < 0) {
monitor.decreaseBrightness();
}
}
onClicked: PanelService.getPanel("brightnessPanel", screen)?.toggle(this)
onRightClicked: {
PanelService.showContextMenu(contextMenu, pill, screen);
}
}
}
@@ -0,0 +1,219 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
readonly property var now: Time.now
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property string clockColor: widgetSettings.clockColor !== undefined ? widgetSettings.clockColor : widgetMetadata.clockColor
readonly property bool useCustomFont: widgetSettings.useCustomFont !== undefined ? widgetSettings.useCustomFont : widgetMetadata.useCustomFont
readonly property string customFont: widgetSettings.customFont !== undefined ? widgetSettings.customFont : widgetMetadata.customFont
readonly property string formatHorizontal: widgetSettings.formatHorizontal !== undefined ? widgetSettings.formatHorizontal : widgetMetadata.formatHorizontal
readonly property string formatVertical: widgetSettings.formatVertical !== undefined ? widgetSettings.formatVertical : widgetMetadata.formatVertical
readonly property string tooltipFormat: widgetSettings.tooltipFormat !== undefined ? widgetSettings.tooltipFormat : widgetMetadata.tooltipFormat
readonly property color textColor: Color.resolveColorKey(clockColor)
// Content dimensions for implicit sizing
readonly property real contentWidth: isBarVertical ? capsuleHeight : Math.round((isBarVertical ? verticalLoader.implicitWidth : horizontalLoader.implicitWidth) + Style.margin2M)
readonly property real contentHeight: isBarVertical ? Math.round(verticalLoader.implicitHeight + Style.margin2S) : capsuleHeight
// Size: use implicit width/height
// BarWidgetLoader sets explicit width/height to extend click area
implicitWidth: contentWidth
implicitHeight: contentHeight
// Visual clock capsule - stays at content size, centered in parent
Rectangle {
id: visualClock
width: root.contentWidth
height: root.contentHeight
anchors.centerIn: parent
radius: Style.radiusL
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
Item {
id: clockContainer
anchors.centerIn: parent
// Horizontal
Loader {
id: horizontalLoader
active: !isBarVertical
anchors.centerIn: parent
sourceComponent: ColumnLayout {
anchors.centerIn: parent
spacing: Settings.data.bar.showCapsule ? -5 : -3
Repeater {
id: repeater
model: I18n.locale.toString(now, formatHorizontal.trim()).split("\\n")
NText {
visible: text !== ""
text: modelData
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
Binding on pointSize {
value: {
if (repeater.model.length == 1) {
// Single line: Full size
return barFontSize;
} else if (repeater.model.length == 2) {
// Two lines: First line is bigger than the second
return (index == 0) ? Math.round(barFontSize * 0.9) : Math.round(barFontSize * 0.75);
} else {
// More than two lines: Make it small!
return Math.round(barFontSize * 0.75);
}
}
}
applyUiScale: false
color: textColor
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
features: ({
"tnum": 1
})
}
}
}
}
// Vertical
Loader {
id: verticalLoader
active: isBarVertical
anchors.centerIn: parent // Now this works without layout conflicts
sourceComponent: ColumnLayout {
anchors.centerIn: parent
spacing: -2
Repeater {
model: I18n.locale.toString(now, formatVertical.trim()).split(" ")
delegate: NText {
visible: text !== ""
text: modelData
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
pointSize: barFontSize
applyUiScale: false
color: textColor
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
features: ({
"tnum": 1
})
}
}
}
}
}
}
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.open-calendar"),
"action": "open-calendar",
"icon": "calendar"
},
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
// Close the context menu
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "open-calendar") {
PanelService.getPanel("clockPanel", screen)?.toggle(root);
} else if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
// Build tooltip text with formatted time/date
function buildTooltipText() {
if (tooltipFormat && tooltipFormat.trim() !== "") {
return I18n.locale.toString(now, tooltipFormat.trim());
}
// Fallback to default if no format is set
return I18n.tr("common.calendar"); // Defaults to "Calendar"
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onEntered: {
if (!PanelService.getPanel("clockPanel", screen)?.isPanelOpen) {
TooltipService.show(root, buildTooltipText(), BarService.getTooltipDirection(root.screen?.name));
tooltipRefreshTimer.start();
}
}
onExited: {
tooltipRefreshTimer.stop();
TooltipService.hide();
}
onClicked: mouse => {
TooltipService.hide();
if (mouse.button === Qt.RightButton) {
PanelService.showContextMenu(contextMenu, root, screen);
} else {
PanelService.getPanel("clockPanel", screen)?.toggle(this);
}
}
}
Timer {
id: tooltipRefreshTimer
interval: 1000
repeat: true
onTriggered: {
if (clockMouseArea.containsMouse && !PanelService.getPanel("clockPanel", screen)?.isPanelOpen) {
TooltipService.updateText(buildTooltipText());
}
}
}
}
@@ -0,0 +1,144 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings
import qs.Services.System
import qs.Services.UI
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string customIcon: widgetSettings.icon !== undefined ? widgetSettings.icon : widgetMetadata.icon
readonly property bool useDistroLogo: widgetSettings.useDistroLogo !== undefined ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo
readonly property string customIconPath: widgetSettings.customIconPath !== undefined ? widgetSettings.customIconPath : widgetMetadata.customIconPath
readonly property bool enableColorization: widgetSettings.enableColorization !== undefined ? widgetSettings.enableColorization : widgetMetadata.enableColorization
readonly property string colorizeSystemIcon: widgetSettings.colorizeSystemIcon !== undefined ? widgetSettings.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon
readonly property color iconColor: {
if (!enableColorization)
return Color.mOnSurface;
return Color.resolveColorKey(colorizeSystemIcon);
}
// If we have a custom path and not using distro logo, use the theme icon.
// If using distro logo, don't use theme icon.
icon: (customIconPath === "" && !useDistroLogo) ? customIcon : ""
tooltipText: {
if (!screen || PanelService.getPanel("controlCenterPanel", screen)?.isPanelOpen) {
return "";
} else {
return I18n.tr("tooltips.open-control-center");
}
}
tooltipDirection: BarService.getTooltipDirection(screen?.name)
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
applyUiScale: false
customRadius: Style.radiusL
colorBg: Style.capsuleColor
colorFg: iconColor
colorBgHover: Color.mHover
colorFgHover: Color.mOnHover
colorBorder: Style.capsuleBorderColor
colorBorderHover: Style.capsuleBorderColor
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.open-launcher"),
"action": "open-launcher",
"icon": "search"
},
{
"label": I18n.tr("actions.open-settings"),
"action": "open-settings",
"icon": "adjustments"
},
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "open-launcher") {
PanelService.toggleLauncher(screen);
} else if (action === "open-settings") {
var panel = PanelService.getPanel("settingsPanel", screen);
panel.requestedTab = SettingsPanel.Tab.General;
panel.toggle();
} else if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
onClicked: {
var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen);
if (Settings.data.controlCenter.position === "close_to_bar_button") {
// Will open the panel next to the bar button.
controlCenterPanel?.toggle(this);
} else {
controlCenterPanel?.toggle();
}
}
onRightClicked: {
PanelService.showContextMenu(contextMenu, root, screen);
}
onMiddleClicked: PanelService.toggleLauncher(screen)
IconImage {
id: customOrDistroLogo
anchors.centerIn: parent
width: root.buttonSize * 0.8
height: width
source: {
if (useDistroLogo)
return HostService.osLogo;
if (customIconPath !== "")
return customIconPath.startsWith("file://") ? customIconPath : "file://" + customIconPath;
return "";
}
visible: source !== ""
smooth: true
asynchronous: true
layer.enabled: (enableColorization) && (useDistroLogo || customIconPath !== "")
layer.effect: ShaderEffect {
property color targetColor: !hovering ? iconColor : Color.mOnHover
property real colorizeMode: 2.0
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
}
}
}
@@ -0,0 +1,676 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings
import qs.Services.Control
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon
readonly property string leftClickExec: widgetSettings.leftClickExec || widgetMetadata.leftClickExec
readonly property bool leftClickUpdateText: widgetSettings.leftClickUpdateText ?? widgetMetadata.leftClickUpdateText
readonly property string rightClickExec: widgetSettings.rightClickExec || widgetMetadata.rightClickExec
readonly property bool rightClickUpdateText: widgetSettings.rightClickUpdateText ?? widgetMetadata.rightClickUpdateText
readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec
readonly property bool middleClickUpdateText: widgetSettings.middleClickUpdateText ?? widgetMetadata.middleClickUpdateText
readonly property string ipcIdentifier: widgetSettings.ipcIdentifier !== undefined ? widgetSettings.ipcIdentifier : (widgetMetadata.ipcIdentifier || "")
readonly property string wheelExec: widgetSettings.wheelExec || widgetMetadata.wheelExec
readonly property string wheelUpExec: widgetSettings.wheelUpExec || widgetMetadata.wheelUpExec
readonly property string wheelDownExec: widgetSettings.wheelDownExec || widgetMetadata.wheelDownExec
readonly property string wheelMode: widgetSettings.wheelMode || widgetMetadata.wheelMode
readonly property bool wheelUpdateText: widgetSettings.wheelUpdateText ?? widgetMetadata.wheelUpdateText
readonly property bool wheelUpUpdateText: widgetSettings.wheelUpUpdateText ?? widgetMetadata.wheelUpUpdateText
readonly property bool wheelDownUpdateText: widgetSettings.wheelDownUpdateText ?? widgetMetadata.wheelDownUpdateText
readonly property string textCommand: widgetSettings.textCommand !== undefined ? widgetSettings.textCommand : (widgetMetadata.textCommand || "")
readonly property bool textStream: widgetSettings.textStream !== undefined ? widgetSettings.textStream : (widgetMetadata.textStream || false)
readonly property int textIntervalMs: widgetSettings.textIntervalMs !== undefined ? widgetSettings.textIntervalMs : (widgetMetadata.textIntervalMs || 3000)
readonly property string textCollapse: widgetSettings.textCollapse !== undefined ? widgetSettings.textCollapse : (widgetMetadata.textCollapse || "")
readonly property bool parseJson: widgetSettings.parseJson !== undefined ? widgetSettings.parseJson : (widgetMetadata.parseJson || false)
readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec || (wheelMode === "unified" && wheelExec) || (wheelMode === "separate" && (wheelUpExec || wheelDownExec)))
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : true
readonly property bool showExecTooltip: widgetSettings.showExecTooltip !== undefined ? widgetSettings.showExecTooltip : (widgetMetadata.showExecTooltip !== undefined ? widgetMetadata.showExecTooltip : true)
readonly property bool showTextTooltip: widgetSettings.showTextTooltip !== undefined ? widgetSettings.showTextTooltip : (widgetMetadata.showTextTooltip !== undefined ? widgetMetadata.showTextTooltip : true)
readonly property string generalTooltipText: widgetSettings.generalTooltipText !== undefined ? widgetSettings.generalTooltipText : (widgetMetadata.generalTooltipText || "")
readonly property bool _hasCustomTooltip: generalTooltipText.trim() !== ""
readonly property string hideMode: widgetSettings.hideMode || "alwaysExpanded"
readonly property bool hasOutput: _dynamicText !== ""
readonly property bool shouldForceOpen: textStream && (hideMode === "alwaysExpanded" || hideMode === "maxTransparent")
readonly property bool _useNewHideLogic: textCommand && textCommand.length > 0 && textStream
readonly property bool _useTextCommandLogic: textCommand && textCommand.length > 0 && textStream
readonly property bool _pillVisible: {
if (!_useTextCommandLogic) {
return true;
}
if (hideMode === "alwaysExpanded" || hideMode === "maxTransparent") {
return true;
}
var hasActualIcon = (_dynamicIcon !== "" || customIcon !== "");
return hasOutput || (showIcon && hasActualIcon);
}
readonly property real _pillOpacity: {
if (!_useTextCommandLogic) {
return 1.0;
}
if (hideMode === "maxTransparent" && !hasOutput) {
return 0.0;
}
return 1.0;
}
readonly property bool _pillForceOpen: {
if (!_useTextCommandLogic) {
return _dynamicText !== "" || (textStream && currentMaxTextLength > 0);
}
if (currentMaxTextLength <= 0) {
return false;
}
if (hideMode === "alwaysExpanded" || hideMode === "maxTransparent") {
return true;
}
return hasOutput;
}
readonly property string _pillIcon: {
if (!_useTextCommandLogic) {
if (textCommand && textCommand.length > 0 && showIcon) {
return _dynamicIcon !== "" ? _dynamicIcon : customIcon;
} else if (!(textCommand && textCommand.length > 0)) {
return _dynamicIcon !== "" ? _dynamicIcon : customIcon;
} else {
return "";
}
}
if (!showIcon)
return "";
var actualIcon = _dynamicIcon !== "" ? _dynamicIcon : customIcon;
if (hideMode === "expandWithOutput" && actualIcon === "") {
return "question-mark";
}
return actualIcon;
}
readonly property string _pillText: {
if (!_useTextCommandLogic) {
return (!isVerticalBar || currentMaxTextLength > 0) ? _dynamicText : "";
}
if (currentMaxTextLength <= 0) {
return "";
}
if (hasOutput) {
return _dynamicText;
}
if (hideMode === "expandWithOutput") {
return "";
}
return " ".repeat(currentMaxTextLength);
}
readonly property bool enableColorization: widgetSettings.enableColorization || false
readonly property string colorizeSystemIcon: {
if (widgetSettings.colorizeSystemIcon !== undefined)
return widgetSettings.colorizeSystemIcon;
return widgetMetadata.colorizeSystemIcon !== undefined ? widgetMetadata.colorizeSystemIcon : "none";
}
readonly property bool isColorizing: enableColorization && colorizeSystemIcon !== "none"
// Get color value from color name (returns null for invalid names)
function _getColorValue(colorName, forHover) {
const baseColor = (function () {
switch (colorName) {
case "primary":
return Color.mPrimary;
case "secondary":
return Color.mSecondary;
case "tertiary":
return Color.mTertiary;
case "error":
return Color.mError;
default:
return null;
}
})();
return baseColor !== null ? (forHover ? Qt.darker(baseColor, 1.2) : baseColor) : null;
}
// Resolve icon color with priority: dynamic > static > default
function _resolveIconColor(dynamicColorName, staticColorName, isHover) {
if (dynamicColorName && dynamicColorName !== "") {
if (dynamicColorName === "none") {
return isHover ? Color.mOnHover : Color.mOnSurface;
}
const color = _getColorValue(dynamicColorName, isHover);
if (color !== null)
return color;
}
if (staticColorName && staticColorName !== "" && isColorizing) {
const color = _getColorValue(staticColorName, isHover);
if (color !== null)
return color;
}
return isHover ? Color.mOnHover : Color.mOnSurface;
}
readonly property color iconColor: _resolveIconColor(_dynamicColor, colorizeSystemIcon, false)
readonly property color iconHoverColor: _resolveIconColor(_dynamicColor, colorizeSystemIcon, true)
implicitWidth: pill.width
implicitHeight: pill.height
BarPill {
id: pill
visible: _pillVisible
opacity: _pillOpacity
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
icon: _pillIcon
text: _pillText
rotateText: isVerticalBar && currentMaxTextLength > 0
autoHide: false
forceOpen: _pillForceOpen
forceClose: !_pillForceOpen
customTextIconColor: iconColor
// Helper function to build tooltip content
function _buildTooltipContent() {
var lines = [];
// Add custom tooltip if set
if (_hasCustomTooltip) {
lines.push(generalTooltipText);
}
// Add command details if enabled and available
if (showExecTooltip && hasExec) {
if (leftClickExec !== "") {
lines.push(I18n.tr("bar.custom-button.left-click-label") + `: ${leftClickExec}`);
} else if (!leftClickUpdateText) {
lines.push(I18n.tr("bar.custom-button.left-click-label") + ": " + I18n.tr("actions.widget-settings"));
}
if (rightClickExec !== "") {
lines.push(I18n.tr("bar.custom-button.right-click-label") + `: ${rightClickExec}`);
} else if (!rightClickUpdateText) {
lines.push(I18n.tr("bar.custom-button.right-click-label") + ": " + I18n.tr("actions.widget-settings"));
}
if (middleClickExec !== "") {
lines.push(I18n.tr("bar.custom-button.middle-click-label") + `: ${middleClickExec}`);
} else if (!middleClickUpdateText) {
lines.push(I18n.tr("bar.custom-button.middle-click-label") + ": " + I18n.tr("actions.widget-settings"));
}
if (wheelMode === "unified" && wheelExec !== "") {
lines.push(I18n.tr("bar.custom-button.wheel-label") + `: ${wheelExec}`);
} else if (wheelMode === "separate") {
if (wheelUpExec !== "") {
lines.push(I18n.tr("bar.custom-button.wheel-up") + `: ${wheelUpExec}`);
}
if (wheelDownExec !== "") {
lines.push(I18n.tr("bar.custom-button.wheel-down") + `Wheel down: ${wheelDownExec}`);
}
}
}
// Add dynamic text tooltip if enabled and available
if (showTextTooltip && _dynamicTooltip !== "") {
lines.push(_dynamicTooltip);
}
return lines;
}
tooltipText: {
var lines = _buildTooltipContent();
// If no custom tooltip and both switches are off, show default tooltip
if (!_hasCustomTooltip && !showExecTooltip && !showTextTooltip) {
return I18n.tr("bar.custom-button.default-tooltip");
}
// If there's content, join with newlines
if (lines.length > 0) {
return lines.join("\n");
}
// Fallback (shouldn't reach here normally)
return I18n.tr("bar.custom-button.default-tooltip");
}
onClicked: root.clicked()
onRightClicked: root.rightClicked()
onMiddleClicked: root.middleClicked()
onWheel: delta => root.wheeled(delta)
}
// Internal state for dynamic text
property string _dynamicText: ""
property string _dynamicIcon: ""
property string _dynamicTooltip: ""
property string _dynamicColor: ""
// Maximum length for text display before scrolling (different values for horizontal and vertical)
readonly property var maxTextLength: {
"horizontal": ((widgetSettings && widgetSettings.maxTextLength && widgetSettings.maxTextLength.horizontal !== undefined) ? widgetSettings.maxTextLength.horizontal : ((widgetMetadata && widgetMetadata.maxTextLength && widgetMetadata.maxTextLength.horizontal !== undefined) ? widgetMetadata.maxTextLength.horizontal : 10)),
"vertical": ((widgetSettings && widgetSettings.maxTextLength && widgetSettings.maxTextLength.vertical !== undefined) ? widgetSettings.maxTextLength.vertical : ((widgetMetadata && widgetMetadata.maxTextLength && widgetMetadata.maxTextLength.vertical !== undefined) ? widgetMetadata.maxTextLength.vertical : 10))
}
readonly property int _staticDuration: 6 // How many cycles to stay static at start/end
// Encapsulated state for scrolling text implementation
property var _scrollState: {
"originalText": "",
"needsScrolling": false,
"offset": 0,
"phase": 0 // 0=static start, 1=scrolling, 2=static end
,
"phaseCounter": 0
}
// Current max text length based on bar orientation
readonly property int currentMaxTextLength: isVerticalBar ? maxTextLength.vertical : maxTextLength.horizontal
// Periodically run the text command (if set)
Timer {
id: refreshTimer
interval: Math.max(250, textIntervalMs)
repeat: true
running: (!isVerticalBar || currentMaxTextLength > 0) && !textStream && textCommand && textCommand.length > 0
triggeredOnStart: true
onTriggered: root.runTextCommand()
}
// Restart exited text stream commands after a delay
Timer {
id: restartTimer
interval: 1000
running: (!isVerticalBar || currentMaxTextLength > 0) && textStream && !textProc.running
onTriggered: root.runTextCommand()
}
// Timer for scrolling text display
Timer {
id: scrollTimer
interval: 300
repeat: true
running: false
onTriggered: {
if (_scrollState.needsScrolling && _scrollState.originalText.length > currentMaxTextLength) {
// Traditional marquee with pause at beginning and end
if (_scrollState.phase === 0) {
// Static at beginning
_dynamicText = _scrollState.originalText.substring(0, Math.min(currentMaxTextLength, _scrollState.originalText.length));
_scrollState.phaseCounter++;
if (_scrollState.phaseCounter >= _staticDuration) {
_scrollState.phaseCounter = 0;
_scrollState.phase = 1; // Move to scrolling
}
} else if (_scrollState.phase === 1) {
// Scrolling
_scrollState.offset++;
var start = _scrollState.offset;
var end = start + currentMaxTextLength;
if (start >= _scrollState.originalText.length - currentMaxTextLength) {
// Reached or passed the end, ensure we show the last part
var textEnd = _scrollState.originalText.length;
var textStart = Math.max(0, textEnd - currentMaxTextLength);
_dynamicText = _scrollState.originalText.substring(textStart, textEnd);
_scrollState.phase = 2; // Move to static end phase
_scrollState.phaseCounter = 0;
} else {
_dynamicText = _scrollState.originalText.substring(start, end);
}
} else if (_scrollState.phase === 2) {
// Static at end
// Ensure end text is displayed correctly
var textEnd = _scrollState.originalText.length;
var textStart = Math.max(0, textEnd - currentMaxTextLength);
_dynamicText = _scrollState.originalText.substring(textStart, textEnd);
_scrollState.phaseCounter++;
if (_scrollState.phaseCounter >= _staticDuration) {
// Do NOT loop back to start, just stop scrolling
scrollTimer.stop();
}
}
} else {
scrollTimer.stop();
}
}
}
SplitParser {
id: textStdoutSplit
onRead: line => root.parseDynamicContent(line)
}
StdioCollector {
id: textStdoutCollect
onStreamFinished: () => root.parseDynamicContent(this.text)
}
Process {
id: textProc
stdout: textStream ? textStdoutSplit : textStdoutCollect
stderr: StdioCollector {}
onExited: (exitCode, exitStatus) => {
if (textStream) {
Logger.w("CustomButton", `Streaming text command exited (code: ${exitCode}), restarting...`);
return;
}
}
}
function parseDynamicContent(content) {
var contentStr = String(content || "").trim();
if (parseJson && contentStr) {
// Handle multi-line JSON by filtering empty lines and joining
var jsonStr = contentStr;
if (contentStr.includes('\n')) {
const lines = contentStr.split('\n').filter(line => line.trim() !== '');
jsonStr = lines.join('');
}
try {
const parsed = JSON.parse(jsonStr);
const text = parsed.text || "";
const icon = parsed.icon || "";
let tooltip = parsed.tooltip || "";
const color = parsed.color || "";
// Validate color value
const validColors = ["primary", "secondary", "tertiary", "error", "none"];
const validColor = (color && validColors.includes(color)) ? color : "";
if (checkCollapse(text)) {
_scrollState.originalText = "";
_dynamicText = "";
_dynamicIcon = "";
_dynamicTooltip = "";
_dynamicColor = "";
_scrollState.needsScrolling = false;
_scrollState.phase = 0;
_scrollState.phaseCounter = 0;
return;
}
_scrollState.originalText = text;
_scrollState.needsScrolling = text.length > currentMaxTextLength && currentMaxTextLength > 0;
if (_scrollState.needsScrolling) {
_dynamicText = text.substring(0, currentMaxTextLength);
_scrollState.phase = 0;
_scrollState.phaseCounter = 0;
_scrollState.offset = 0;
scrollTimer.start();
} else {
_dynamicText = text;
scrollTimer.stop();
}
_dynamicIcon = icon;
_dynamicColor = validColor;
_dynamicTooltip = toHtml(tooltip);
_scrollState.offset = 0;
return;
} catch (e) {
Logger.w("CustomButton", `Failed to parse JSON. Content: "${contentStr}"`);
}
}
if (checkCollapse(contentStr)) {
_scrollState.originalText = "";
_dynamicText = "";
_dynamicIcon = "";
_dynamicTooltip = "";
_dynamicColor = "";
_scrollState.needsScrolling = false;
_scrollState.phase = 0;
_scrollState.phaseCounter = 0;
return;
}
_scrollState.originalText = contentStr;
_scrollState.needsScrolling = contentStr.length > currentMaxTextLength && currentMaxTextLength > 0;
if (_scrollState.needsScrolling) {
_dynamicText = contentStr.substring(0, currentMaxTextLength);
_scrollState.phase = 0;
_scrollState.phaseCounter = 0;
_scrollState.offset = 0;
scrollTimer.start();
} else {
_dynamicText = contentStr;
scrollTimer.stop();
}
_dynamicIcon = "";
_dynamicColor = "";
_dynamicTooltip = toHtml(contentStr);
_scrollState.offset = 0;
}
function checkCollapse(text) {
if (!textCollapse || textCollapse.length === 0) {
return false;
}
if (textCollapse.startsWith("/") && textCollapse.endsWith("/") && textCollapse.length > 1) {
// Treat as regex
var pattern = textCollapse.substring(1, textCollapse.length - 1);
try {
var regex = new RegExp(pattern);
return regex.test(text);
} catch (e) {
Logger.w("CustomButton", `Invalid regex for textCollapse: ${textCollapse} - ${e.message}`);
return (textCollapse === text); // Fallback to exact match on invalid regex
}
} else {
// Treat as plain string
return (textCollapse === text);
}
}
function clicked() {
if (leftClickExec) {
Quickshell.execDetached(["sh", "-lc", leftClickExec]);
Logger.i("CustomButton", `Executing command: ${leftClickExec}`);
} else if (!leftClickUpdateText) {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
if (!textStream && leftClickUpdateText) {
runTextCommand();
}
}
function rightClicked() {
if (rightClickExec) {
Quickshell.execDetached(["sh", "-lc", rightClickExec]);
Logger.i("CustomButton", `Executing command: ${rightClickExec}`);
} else if (!rightClickUpdateText) {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
if (!textStream && rightClickUpdateText) {
runTextCommand();
}
}
function middleClicked() {
if (middleClickExec) {
Quickshell.execDetached(["sh", "-lc", middleClickExec]);
Logger.i("CustomButton", `Executing command: ${middleClickExec}`);
} else if (!middleClickUpdateText) {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
if (!textStream && middleClickUpdateText) {
runTextCommand();
}
}
function toHtml(str) {
const htmlTagRegex = /<\/?[a-zA-Z][^>]*>/g;
const placeholders = [];
let i = 0;
const protectedStr = str.replace(htmlTagRegex, tag => {
placeholders.push(tag);
return `___HTML_TAG_${i++}___`;
});
let escaped = protectedStr.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/\r\n|\r|\n/g, "<br/>");
escaped = escaped.replace(/___HTML_TAG_(\d+)___/g, (_, index) => placeholders[Number(index)]);
return escaped;
}
function runTextCommand() {
if (!textCommand || textCommand.length === 0)
return;
if (textProc.running)
return;
textProc.command = ["sh", "-lc", textCommand];
textProc.running = true;
}
function wheeled(delta) {
if (wheelMode === "unified" && wheelExec) {
let normalizedDelta = delta > 0 ? 1 : -1;
let command = wheelExec.replace(/\$delta([+\-*/]\d+)?/g, function (match, operation) {
if (operation) {
try {
let operator = operation.charAt(0);
let operand = parseInt(operation.substring(1));
let result;
switch (operator) {
case '+':
result = normalizedDelta + operand;
break;
case '-':
result = normalizedDelta - operand;
break;
case '*':
result = normalizedDelta * operand;
break;
case '/':
result = Math.floor(normalizedDelta / operand);
break;
default:
result = normalizedDelta;
}
return result.toString();
} catch (e) {
Logger.w("CustomButton", `Error evaluating expression: ${match}, using normalized value ${normalizedDelta}`);
return normalizedDelta.toString();
}
} else {
return normalizedDelta.toString();
}
});
Quickshell.execDetached(["sh", "-lc", command]);
Logger.i("CustomButton", `Executing command: ${command}`);
} else if (wheelMode === "separate") {
if ((delta > 0 && wheelUpExec) || (delta < 0 && wheelDownExec)) {
let commandExec = delta > 0 ? wheelUpExec : wheelDownExec;
let normalizedDelta = delta > 0 ? 1 : -1;
let command = commandExec.replace(/\$delta([+\-*/]\d+)?/g, function (match, operation) {
if (operation) {
try {
let operator = operation.charAt(0);
let operand = parseInt(operation.substring(1));
let result;
switch (operator) {
case '+':
result = normalizedDelta + operand;
break;
case '-':
result = normalizedDelta - operand;
break;
case '*':
result = normalizedDelta * operand;
break;
case '/':
result = Math.floor(normalizedDelta / operand);
break;
default:
result = normalizedDelta;
}
return result.toString();
} catch (e) {
Logger.w("CustomButton", `Error evaluating expression: ${match}, using normalized value ${normalizedDelta}`);
return normalizedDelta.toString();
}
} else {
return normalizedDelta.toString();
}
});
Quickshell.execDetached(["sh", "-lc", command]);
Logger.i("CustomButton", `Executing command: ${command}`);
}
}
if (!textStream) {
if (wheelMode === "unified" && wheelUpdateText) {
runTextCommand();
} else if (wheelMode === "separate") {
if ((delta > 0 && wheelUpUpdateText) || (delta < 0 && wheelDownUpdateText)) {
runTextCommand();
}
}
}
}
// Register IPC button in onLoaded (called by BarWidgetLoader after properties are set).
// Component.onCompleted is too early — widgetSettings depends on section/index/screen
// which are set by BarWidgetLoader.onLoaded after the component is created.
function onLoaded() {
if (ipcIdentifier && ipcIdentifier.trim() !== "")
CustomButtonIPCService.registerButton(root);
}
Component.onDestruction: {
if (ipcIdentifier && ipcIdentifier.trim() !== "")
CustomButtonIPCService.unregisterButton(root);
}
}
@@ -0,0 +1,68 @@
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
icon: "dark-mode"
tooltipText: Settings.data.colorSchemes.darkMode ? I18n.tr("tooltips.switch-to-light-mode") : I18n.tr("tooltips.switch-to-dark-mode")
tooltipDirection: BarService.getTooltipDirection(screen?.name)
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
applyUiScale: false
customRadius: Style.radiusL
colorBg: Style.capsuleColor
colorFg: Color.resolveColorKey(iconColorKey)
onClicked: Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
onRightClicked: {
PanelService.showContextMenu(contextMenu, root, screen);
}
}
@@ -0,0 +1,96 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.Power
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
implicitWidth: pill.width
implicitHeight: pill.height
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
BarPill {
id: pill
screen: root.screen
text: IdleInhibitorService.timeout == null ? "" : Time.formatVagueHumanReadableDuration(IdleInhibitorService.timeout)
oppositeDirection: BarService.getPillDirection(root)
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.keep-awake") : I18n.tr("tooltips.keep-awake")
onClicked: IdleInhibitorService.manualToggle()
onRightClicked: {
PanelService.showContextMenu(contextMenu, pill, screen);
}
forceOpen: IdleInhibitorService.timeout !== null
forceClose: IdleInhibitorService.timeout == null
onWheel: function (delta) {
var sign = delta > 0 ? 1 : -1;
// the offset makes scrolling down feel symmetrical to scrolling up
var timeout = IdleInhibitorService.timeout - (delta < 0 ? 60 : 0);
if (timeout == null || timeout < 600) {
delta = 60; // <= 10m, increment at 1m interval
} else if (timeout >= 600 && timeout < 1800) {
delta = 300; // >= 10m, increment at 5m interval
} else if (timeout >= 1800 && timeout < 3600) {
delta = 600; // >= 30m, increment at 10m interval
} else if (timeout >= 3600) {
delta = 1800; // > 1h, increment at 30m interval
}
IdleInhibitorService.changeTimeout(delta * sign);
}
}
}
@@ -0,0 +1,92 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.Compositor
import qs.Services.Keyboard
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
// Use the shared service for keyboard layout
property string currentLayout: KeyboardLayoutService.currentLayout
implicitWidth: pill.width
implicitHeight: pill.height
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
BarPill {
id: pill
anchors.verticalCenter: parent.verticalCenter
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
icon: root.showIcon ? "keyboard" : ""
autoHide: false // Important to be false so we can hover as long as we want
text: isBarVertical ? currentLayout.substring(0, 3).toUpperCase() : currentLayout
tooltipText: KeyboardLayoutService.fullLayoutName
// When icon is disabled, always show the layout text
forceOpen: !root.showIcon || root.displayMode === "forceOpen"
forceClose: root.showIcon && root.displayMode === "alwaysHide"
onClicked: CompositorService.cycleKeyboardLayout()
onRightClicked: {
PanelService.showContextMenu(contextMenu, pill, screen);
}
}
}
@@ -0,0 +1,119 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.Commons
import qs.Modules.Panels.Settings
import qs.Services.System
import qs.Services.UI
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string customIcon: widgetSettings.icon || (widgetMetadata ? widgetMetadata.icon : "rocket")
readonly property bool useDistroLogo: widgetSettings.useDistroLogo !== undefined ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo
readonly property string customIconPath: widgetSettings.customIconPath !== undefined ? widgetSettings.customIconPath : widgetMetadata.customIconPath
readonly property bool enableColorization: widgetSettings.enableColorization !== undefined ? widgetSettings.enableColorization : widgetMetadata.enableColorization
readonly property string colorizeSystemIcon: widgetSettings.colorizeSystemIcon !== undefined ? widgetSettings.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon
readonly property color iconColor: {
if (!enableColorization)
return Color.mOnSurface;
return Color.resolveColorKey(colorizeSystemIcon);
}
// If we have a custom path or are using distro logo, don't show the theme icon.
icon: (customIconPath === "" && !useDistroLogo) ? customIcon : ""
tooltipText: I18n.tr("actions.open-launcher")
tooltipDirection: BarService.getTooltipDirection(screenName)
baseSize: Style.getCapsuleHeightForScreen(screenName)
applyUiScale: false
customRadius: Style.radiusL
colorBg: Style.capsuleColor
colorFg: iconColor
colorBgHover: Color.mHover
colorFgHover: Color.mOnHover
colorBorder: Style.capsuleBorderColor
colorBorderHover: Style.capsuleBorderColor
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.launcher-settings"),
"action": "launcher-settings",
"icon": "adjustments"
},
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
}
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "launcher-settings") {
var panel = PanelService.getPanel("settingsPanel", screen);
panel.requestedTab = SettingsPanel.Tab.Launcher;
panel.toggle();
} else if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
onClicked: PanelService.toggleLauncher(screen)
onMiddleClicked: PanelService.toggleLauncher(screen)
onRightClicked: {
PanelService.showContextMenu(contextMenu, root, screen);
}
IconImage {
id: customOrDistroLogo
anchors.centerIn: parent
width: root.buttonSize * 0.8
height: width
source: {
if (useDistroLogo)
return HostService.osLogo;
if (customIconPath !== "")
return customIconPath.startsWith("file://") ? customIconPath : "file://" + customIconPath;
return "";
}
visible: source !== ""
smooth: true
asynchronous: true
layer.enabled: (enableColorization) && (useDistroLogo || customIconPath !== "")
layer.effect: ShaderEffect {
property color targetColor: !hovering ? iconColor : Color.mOnHover
property real colorizeMode: 2.0
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
}
}
}
@@ -0,0 +1,165 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings
import qs.Services.Keyboard
import qs.Services.UI
import qs.Widgets
//test
Item {
id: root
property ShellScreen screen
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Settings
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
// Content dimensions for implicit sizing
readonly property real contentWidth: isVertical ? capsuleHeight : Math.round(layout.implicitWidth + Style.margin2M)
readonly property real contentHeight: isVertical ? Math.round(layout.implicitHeight + Style.margin2M) : capsuleHeight
readonly property bool hideWhenOff: (widgetSettings.hideWhenOff !== undefined) ? widgetSettings.hideWhenOff : (widgetMetadata.hideWhenOff !== undefined ? widgetMetadata.hideWhenOff : false)
readonly property string capsIcon: widgetSettings.capsLockIcon !== undefined ? widgetSettings.capsLockIcon : widgetMetadata.capsLockIcon
readonly property string numIcon: widgetSettings.numLockIcon !== undefined ? widgetSettings.numLockIcon : widgetMetadata.numLockIcon
readonly property string scrollIcon: widgetSettings.scrollLockIcon !== undefined ? widgetSettings.scrollLockIcon : widgetMetadata.scrollLockIcon
readonly property bool showCaps: (widgetSettings.showCapsLock !== undefined) ? widgetSettings.showCapsLock : widgetMetadata.showCapsLock
readonly property bool showNum: (widgetSettings.showNumLock !== undefined) ? widgetSettings.showNumLock : widgetMetadata.showNumLock
readonly property bool showScroll: (widgetSettings.showScrollLock !== undefined) ? widgetSettings.showScrollLock : widgetMetadata.showScrollLock
visible: !root.hideWhenOff || (root.showCaps && LockKeysService.capsLockOn) || (root.showNum && LockKeysService.numLockOn) || (root.showScroll && LockKeysService.scrollLockOn)
Component.onCompleted: LockKeysService.registerComponent("bar-lockkeys:" + (screen?.name || "unknown"))
Component.onDestruction: LockKeysService.unregisterComponent("bar-lockkeys:" + (screen?.name || "unknown"))
implicitHeight: contentHeight
implicitWidth: contentWidth
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
// Visual capsule centered in parent
Rectangle {
id: visualCapsule
anchors.centerIn: parent
color: Style.capsuleColor
width: root.contentWidth
height: root.contentHeight
radius: Style.radiusM
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
Item {
id: layout
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
implicitHeight: rowLayout.visible ? rowLayout.implicitHeight : colLayout.implicitHeight
implicitWidth: rowLayout.visible ? rowLayout.implicitWidth : colLayout.implicitWidth
RowLayout {
id: rowLayout
spacing: 0
visible: !root.isVertical
NIcon {
color: LockKeysService.capsLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
icon: root.capsIcon
visible: root.showCaps && (!root.hideWhenOff || LockKeysService.capsLockOn)
}
NIcon {
color: LockKeysService.numLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
icon: root.numIcon
visible: root.showNum && (!root.hideWhenOff || LockKeysService.numLockOn)
}
NIcon {
color: LockKeysService.scrollLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
icon: root.scrollIcon
visible: root.showScroll && (!root.hideWhenOff || LockKeysService.scrollLockOn)
}
}
ColumnLayout {
id: colLayout
spacing: 0
visible: root.isVertical
NIcon {
color: LockKeysService.capsLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
icon: root.capsIcon
visible: root.showCaps && (!root.hideWhenOff || LockKeysService.capsLockOn)
}
NIcon {
color: LockKeysService.numLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
icon: root.numIcon
visible: root.showNum && (!root.hideWhenOff || LockKeysService.numLockOn)
}
NIcon {
color: LockKeysService.scrollLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
icon: root.scrollIcon
visible: root.showScroll && (!root.hideWhenOff || LockKeysService.scrollLockOn)
}
}
}
}
// MouseArea at root level for extended click area
MouseArea {
acceptedButtons: Qt.RightButton
anchors.fill: parent
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
PanelService.showContextMenu(contextMenu, root, screen);
}
}
}
}
@@ -0,0 +1,510 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.Media
import qs.Services.UI
import qs.Widgets
import qs.Widgets.AudioSpectrum
Item {
id: root
property ShellScreen screen
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Settings
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
// Bar orientation (per-screen)
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
// Widget settings
readonly property string hideMode: widgetSettings.hideMode !== undefined ? widgetSettings.hideMode : widgetMetadata.hideMode
readonly property bool hideWhenIdle: widgetSettings.hideWhenIdle !== undefined ? widgetSettings.hideWhenIdle : widgetMetadata.hideWhenIdle
readonly property bool showAlbumArt: widgetSettings.showAlbumArt !== undefined ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt
readonly property bool showArtistFirst: widgetSettings.showArtistFirst !== undefined ? widgetSettings.showArtistFirst : widgetMetadata.showArtistFirst
readonly property bool showVisualizer: widgetSettings.showVisualizer !== undefined ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
readonly property string visualizerType: widgetSettings.visualizerType !== undefined ? widgetSettings.visualizerType : widgetMetadata.visualizerType
readonly property string scrollingMode: widgetSettings.scrollingMode !== undefined ? widgetSettings.scrollingMode : widgetMetadata.scrollingMode
readonly property bool showProgressRing: widgetSettings.showProgressRing !== undefined ? widgetSettings.showProgressRing : widgetMetadata.showProgressRing
readonly property bool useFixedWidth: widgetSettings.useFixedWidth !== undefined ? widgetSettings.useFixedWidth : widgetMetadata.useFixedWidth
readonly property real maxWidth: widgetSettings.maxWidth !== undefined ? widgetSettings.maxWidth : Math.max(widgetMetadata.maxWidth, screen ? screen.width * 0.06 : 0)
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
readonly property color textColor: Color.resolveColorKey(textColorKey)
// Dimensions
readonly property int artSize: Style.toOdd(capsuleHeight * 0.75)
readonly property int iconSize: Style.toOdd(capsuleHeight * 0.75)
readonly property int verticalSize: Style.toOdd(capsuleHeight * 0.85)
readonly property int progressWidth: 2
// State
readonly property bool hasPlayer: MediaService.currentPlayer !== null
readonly property bool shouldHideIdle: (hideMode === "idle" || hideWhenIdle) && !MediaService.isPlaying
readonly property bool shouldHideEmpty: !hasPlayer && hideMode === "hidden"
readonly property bool isHidden: shouldHideIdle || shouldHideEmpty
// Title
readonly property string title: {
if (!hasPlayer)
return I18n.tr("bar.media-mini.no-active-player");
var artist = MediaService.trackArtist;
var track = MediaService.trackTitle;
return showArtistFirst ? (artist ? `${artist} - ${track}` : track) : (artist ? `${track} - ${artist}` : track);
}
// SpectrumService registration for visualizer
readonly property string spectrumComponentId: "bar:mediamini:" + root.screen?.name + ":" + root.section + ":" + root.sectionWidgetIndex
readonly property bool needsSpectrum: root.showVisualizer && root.visualizerType !== "" && root.visualizerType !== "none" && !root.isHidden
Layout.preferredHeight: isVertical ? -1 : Style.getBarHeightForScreen(screenName)
Layout.preferredWidth: isVertical ? Style.getBarHeightForScreen(screenName) : -1
Layout.fillHeight: false
Layout.fillWidth: false
onNeedsSpectrumChanged: {
if (root.needsSpectrum) {
SpectrumService.registerComponent(root.spectrumComponentId);
} else {
SpectrumService.unregisterComponent(root.spectrumComponentId);
}
}
Component.onCompleted: {
if (root.needsSpectrum) {
SpectrumService.registerComponent(root.spectrumComponentId);
}
}
Component.onDestruction: {
SpectrumService.unregisterComponent(root.spectrumComponentId);
}
// Layout
// For horizontal bars, height is always capsuleHeight (no animation needed to prevent jitter)
// For vertical bars, collapse to 0 when hidden
implicitWidth: isVertical ? (isHidden ? 0 : verticalSize) : (isHidden ? 0 : contentWidth)
implicitHeight: isVertical ? (isHidden ? 0 : verticalSize) : capsuleHeight
visible: !shouldHideIdle && (hideMode !== "hidden" || opacity > 0)
opacity: isHidden ? 0.0 : ((hideMode === "transparent" && !hasPlayer) ? 0.0 : 1.0)
property real mainContentWidth: 0
readonly property real contentWidth: {
if (useFixedWidth)
return maxWidth;
// Calculate icon/art width (must match RowLayout visibility)
var iconWidth = 0;
if (!hasPlayer) {
iconWidth = iconSize;
} else if (showAlbumArt || showProgressRing) {
iconWidth = artSize;
}
var margins = isVertical ? 0 : Style.margin2S;
// Add spacing and text width
var textWidth = 0;
if (titleContainer.measuredWidth > 0) {
if (iconWidth > 0)
margins += Style.marginS;
textWidth = titleContainer.measuredWidth + Style.margin2XXS;
}
var total = iconWidth + textWidth + margins;
// calculate the width of all elements except the scrolling text
mainContentWidth = total - textWidth;
return hasPlayer ? Math.min(total, maxWidth) : total;
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
}
// Context menu
NPopupContextMenu {
id: contextMenu
model: {
var items = [];
if (hasPlayer && MediaService.canPlay) {
items.push({
"label": MediaService.isPlaying ? I18n.tr("common.pause") : I18n.tr("common.play"),
"action": "play-pause",
"icon": MediaService.isPlaying ? "media-pause" : "media-play"
});
}
if (hasPlayer && MediaService.canGoPrevious) {
items.push({
"label": I18n.tr("common.previous"),
"action": "previous",
"icon": "media-prev"
});
}
if (hasPlayer && MediaService.canGoNext) {
items.push({
"label": I18n.tr("common.next"),
"action": "next",
"icon": "media-next"
});
}
// Append available players (like in Control Center) so user can switch from the bar
var players = MediaService.getAvailablePlayers ? MediaService.getAvailablePlayers() : [];
if (players && players.length > 1) {
for (var i = 0; i < players.length; i++) {
var isCurrent = (i === MediaService.selectedPlayerIndex);
items.push({
"label": players[i].identity,
"action": "player-" + i,
"icon": isCurrent ? "check" : "disc",
"enabled": true,
"visible": true
});
}
}
items.push({
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
});
return items;
}
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "play-pause")
MediaService.playPause();
else if (action === "previous")
MediaService.previous();
else if (action === "next")
MediaService.next();
else if (action && action.indexOf("player-") === 0) {
var idx = parseInt(action.split("-")[1]);
if (!isNaN(idx)) {
MediaService.switchToPlayer(idx);
}
} else if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
// Main container - stays at content size, pixel-perfect centered in parent
Rectangle {
id: container
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: Style.toOdd(isVertical ? (isHidden ? 0 : verticalSize) : (isHidden ? 0 : contentWidth))
height: Style.toOdd(isVertical ? (isHidden ? 0 : verticalSize) : capsuleHeight)
radius: Style.radiusM
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
}
Item {
anchors.fill: parent
anchors.leftMargin: isVertical ? 0 : Style.marginS
anchors.rightMargin: isVertical ? 0 : Style.marginS
// Visualizer
Loader {
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: Style.toOdd(parent.width)
height: Style.toOdd(parent.height)
active: showVisualizer
z: 0
sourceComponent: {
if (!showVisualizer)
return null;
if (visualizerType === "linear")
return linearSpectrum;
if (visualizerType === "mirrored")
return mirroredSpectrum;
if (visualizerType === "wave")
return waveSpectrum;
return null;
}
}
// Horizontal layout
RowLayout {
anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS
visible: !isVertical
z: 1
// Album art / Progress ring
Item {
visible: hasPlayer && (showAlbumArt || showProgressRing)
Layout.preferredWidth: visible ? artSize : 0
Layout.preferredHeight: visible ? artSize : 0
Layout.alignment: Qt.AlignVCenter
ProgressRing {
id: progressRing
anchors.fill: parent
visible: showProgressRing
progress: MediaService.trackLength > 0 ? MediaService.currentPosition / MediaService.trackLength : 0
lineWidth: root.progressWidth
}
NImageRounded {
visible: showAlbumArt && hasPlayer
anchors.fill: parent
anchors.margins: showProgressRing ? root.progressWidth * 2 : 0
radius: width / 2
imagePath: MediaService.trackArtUrl
borderWidth: 0
imageFillMode: Image.PreserveAspectCrop
}
}
// Scrolling title
NScrollText {
id: titleContainer
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: capsuleHeight
fadeRoundLeftCorners: !(showAlbumArt || showProgressRing)
text: title
scrollMode: {
if (scrollingMode === "always")
return NScrollText.ScrollMode.Always;
if (scrollingMode === "hover")
return NScrollText.ScrollMode.Hover;
return NScrollText.ScrollMode.Never;
}
cursorShape: hasPlayer ? Qt.PointingHandCursor : Qt.ArrowCursor
maxWidth: root.maxWidth - root.mainContentWidth
forcedHover: mainMouseArea.containsMouse
fadeExtent: 0.1
fadeCornerRadius: Style.radiusM
NText {
color: hasPlayer ? root.textColor : Color.mOnSurfaceVariant
pointSize: barFontSize
elide: Text.ElideNone
}
}
}
// Vertical layout
Item {
id: verticalLayout
visible: isVertical
width: Style.toOdd(verticalSize)
height: Style.toOdd(width)
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
z: 1
ProgressRing {
anchors.fill: parent
visible: showProgressRing
progress: MediaService.trackLength > 0 ? MediaService.currentPosition / MediaService.trackLength : 0
lineWidth: root.progressWidth
}
NImageRounded {
visible: showAlbumArt && hasPlayer
anchors.fill: parent
anchors.margins: showProgressRing ? root.progressWidth * 2 : 0
radius: width / 2
imagePath: MediaService.trackArtUrl
borderWidth: 0
imageFillMode: Image.PreserveAspectCrop
}
}
// Mouse interaction moved to root
}
}
// Mouse interaction
MouseArea {
id: mainMouseArea
anchors.fill: parent
// Extend click area to screen edge if widget is at the start/end
anchors.leftMargin: (!isVertical && section === "left" && sectionWidgetIndex === 0) ? -Style.marginS : 0
anchors.rightMargin: (!isVertical && section === "right" && sectionWidgetIndex === sectionWidgetsCount - 1) ? -Style.marginS : 0
anchors.topMargin: (isVertical && section === "left" && sectionWidgetIndex === 0) ? -Style.marginM : 0
anchors.bottomMargin: (isVertical && section === "right" && sectionWidgetIndex === sectionWidgetsCount - 1) ? -Style.marginM : 0
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton | Qt.ForwardButton | Qt.BackButton
onClicked: mouse => {
TooltipService.hide();
if (mouse.button === Qt.LeftButton) {
PanelService.getPanel("mediaPlayerPanel", screen)?.toggle(container);
} else if (mouse.button === Qt.RightButton) {
PanelService.showContextMenu(contextMenu, container, screen);
} else if (mouse.button === Qt.MiddleButton && hasPlayer) {
MediaService.playPause();
} else if (mouse.button === Qt.ForwardButton && hasPlayer) {
MediaService.next();
} else if (mouse.button === Qt.BackButton && hasPlayer) {
MediaService.previous();
}
}
onEntered: {
if (!root || !screen) {
return;
}
var scrollMode = scrollingMode;
if ((isVertical || scrollMode === "never")) {
var panel = PanelService.getPanel("mediaPlayerPanel", screen);
if (panel && !panel.isPanelOpen) {
TooltipService.show(root, title, BarService.getTooltipDirection(root.screen?.name));
}
}
}
onExited: TooltipService.hide()
}
// Components
Component {
id: linearSpectrum
NLinearSpectrum {
width: parent.width - Style.marginS
height: 20
values: SpectrumService.values
fillColor: Color.mPrimary
opacity: 0.4
barPosition: root.barPosition
mirrored: Settings.data.audio.spectrumMirrored
}
}
Component {
id: mirroredSpectrum
NMirroredSpectrum {
width: parent.width - Style.marginS
height: parent.height - Style.marginS
values: SpectrumService.values
fillColor: Color.mPrimary
opacity: 0.4
mirrored: Settings.data.audio.spectrumMirrored
}
}
Component {
id: waveSpectrum
NWaveSpectrum {
width: parent.width - Style.marginS
height: parent.height - Style.marginS
values: SpectrumService.values
fillColor: Color.mPrimary
opacity: 0.4
mirrored: Settings.data.audio.spectrumMirrored
}
}
// Progress Ring Component
component ProgressRing: Canvas {
property real progress: 0
property real lineWidth: 2
function repaint() {
if (this.visible && this.opacity > 0)
requestPaint();
}
onProgressChanged: repaint()
Component.onCompleted: repaint()
Connections {
target: Color
function onMPrimaryChanged() {
repaint();
}
}
onPaint: {
if (width <= 0 || height <= 0)
return;
var ctx = getContext("2d");
var centerX = width / 2;
var centerY = height / 2;
var radius = Math.min(width, height) / 2 - lineWidth;
ctx.reset();
// Background
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.lineWidth = lineWidth;
ctx.strokeStyle = Qt.alpha(Color.mOnSurface, 0.4);
ctx.stroke();
// Progress
ctx.beginPath();
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI);
ctx.lineWidth = lineWidth;
ctx.strokeStyle = Color.mPrimary;
ctx.lineCap = "round";
ctx.stroke();
}
}
}
@@ -0,0 +1,185 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings
import qs.Services.Media
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
readonly property string middleClickCommand: (widgetSettings.middleClickCommand !== undefined) ? widgetSettings.middleClickCommand : widgetMetadata.middleClickCommand
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
// Used to avoid opening the pill on Quickshell startup
property bool firstInputVolumeReceived: false
property int wheelAccumulator: 0
implicitWidth: pill.width
implicitHeight: pill.height
// Connection used to open the pill when input volume changes
Connections {
target: AudioService.source?.audio ? AudioService.source?.audio : null
function onVolumeChanged() {
// Logger.i("Bar:Microphone", "onInputVolumeChanged")
if (!firstInputVolumeReceived) {
// Ignore the first volume change
firstInputVolumeReceived = true;
} else {
// If a tooltip is visible while we show the pill
// hide it so it doesn't overlap the volume slider.
TooltipService.hide();
pill.show();
externalHideTimer.restart();
}
}
}
// Connection used to open the pill when input mute state changes
Connections {
target: AudioService.source?.audio ? AudioService.source?.audio : null
function onMutedChanged() {
// Logger.i("Bar:Microphone", "onInputMutedChanged")
if (!firstInputVolumeReceived) {
// Ignore the first mute change
firstInputVolumeReceived = true;
} else {
TooltipService.hide();
pill.show();
externalHideTimer.restart();
}
}
}
Timer {
id: externalHideTimer
running: false
interval: 1500
onTriggered: {
pill.hide();
}
}
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.toggle-mute"),
"action": "toggle-mute",
"icon": AudioService.inputMuted ? "microphone-off" : "microphone"
},
{
"label": I18n.tr("actions.run-custom-command"),
"action": "custom-command",
"icon": "adjustments"
},
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "toggle-mute") {
AudioService.setInputMuted(!AudioService.inputMuted);
} else if (action === "custom-command") {
Quickshell.execDetached(["sh", "-c", middleClickCommand]);
} else if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
BarPill {
id: pill
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
icon: AudioService.getInputIcon()
autoHide: false // Important to be false so we can hover as long as we want
text: {
const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0;
const displayVolume = Math.min(maxVolume, AudioService.inputVolume);
return Math.round(displayVolume * 100);
}
suffix: "%"
forceOpen: displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide"
tooltipText: {
if (PanelService.getPanel("audioPanel", screen)?.isPanelOpen) {
return "";
} else {
const nick = AudioService.source?.nickname ?? "";
const volumeText = I18n.tr("tooltips.microphone-volume-at", {
"volume": (() => {
const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0;
const displayVolume = Math.min(maxVolume, AudioService.inputVolume);
return Math.round(displayVolume * 100);
})()
});
return nick ? volumeText + "\n" + nick : volumeText;
}
}
onWheel: function (delta) {
// As soon as we start scrolling to adjust volume, hide the tooltip
TooltipService.hide();
wheelAccumulator += delta;
if (wheelAccumulator >= 120) {
wheelAccumulator = 0;
AudioService.setInputVolume(AudioService.inputVolume + AudioService.stepVolume);
} else if (wheelAccumulator <= -120) {
wheelAccumulator = 0;
AudioService.setInputVolume(AudioService.inputVolume - AudioService.stepVolume);
}
}
onClicked: {
PanelService.getPanel("audioPanel", screen)?.toggle(this);
}
onRightClicked: {
PanelService.showContextMenu(contextMenu, pill, screen);
}
onMiddleClicked: {
Quickshell.execDetached(["sh", "-c", middleClickCommand]);
}
}
}
@@ -0,0 +1,105 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings // For SettingsPanel
import qs.Services.Networking
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
implicitWidth: pill.width
implicitHeight: pill.height
NPopupContextMenu {
id: contextMenu
model: [
{
"label": NetworkService.wifiEnabled ? I18n.tr("actions.disable-wifi") : I18n.tr("actions.enable-wifi"),
"action": "toggle-wifi",
"icon": NetworkService.wifiEnabled ? "wifi-off" : "wifi",
"enabled": !NetworkService.airplaneModeEnabled && NetworkService.wifiAvailable
},
{
"label": I18n.tr("common.wifi") + " " + I18n.tr("tooltips.open-settings"),
"action": "wifi-settings",
"icon": "settings"
},
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "toggle-wifi") {
NetworkService.setWifiEnabled(!NetworkService.wifiEnabled);
} else if (action === "wifi-settings") {
SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 0, screen);
} else if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
BarPill {
id: pill
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
icon: NetworkService.getIcon()
text: NetworkService.getStatusText(false)
autoHide: false
forceOpen: !isBarVertical && root.displayMode === "alwaysShow"
forceClose: isBarVertical || root.displayMode === "alwaysHide" || text === ""
onClicked: {
var panel = PanelService.getPanel("networkPanel", screen);
panel?.toggle(this);
}
onRightClicked: {
PanelService.showContextMenu(contextMenu, pill, screen);
}
tooltipText: {
if (PanelService.getPanel("networkPanel", screen)?.isPanelOpen) {
return "";
}
return NetworkService.getStatusText(true);
}
}
}
@@ -0,0 +1,90 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.Panels.Settings
import qs.Services.System
import qs.Services.UI
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
applyUiScale: false
customRadius: Style.radiusL
colorBg: Settings.data.nightLight.enabled ? Color.mPrimary : Style.capsuleColor
colorFg: Settings.data.nightLight.enabled ? Color.mOnPrimary : Color.resolveColorKey(iconColorKey)
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
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("common.night-light") : I18n.tr("common.night-light")) : I18n.tr("common.night-light")
tooltipDirection: BarService.getTooltipDirection(screen?.name)
onClicked: {
// Check if wlsunset is available before enabling night light
if (!ProgramCheckerService.wlsunsetAvailable) {
ToastService.showWarning(I18n.tr("common.night-light"), I18n.tr("toast.night-light.not-installed"));
return;
}
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;
}
}
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
onRightClicked: {
PanelService.showContextMenu(contextMenu, root, screen);
}
}
@@ -0,0 +1,74 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.Panels.Settings
import qs.Services.Power
import qs.Services.UI
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
applyUiScale: false
customRadius: Style.radiusL
colorBg: PowerProfileService.noctaliaPerformanceMode ? Color.mPrimary : Style.capsuleColor
colorFg: PowerProfileService.noctaliaPerformanceMode ? Color.mOnPrimary : Color.resolveColorKey(iconColorKey)
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
icon: PowerProfileService.noctaliaPerformanceMode ? "rocket" : "rocket-off"
tooltipText: PowerProfileService.noctaliaPerformanceMode ? I18n.tr("tooltips.noctalia-performance-enabled") : I18n.tr("tooltips.noctalia-performance-enabled")
tooltipDirection: BarService.getTooltipDirection(screen?.name)
onClicked: PowerProfileService.toggleNoctaliaPerformance()
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
onRightClicked: {
PanelService.showContextMenu(contextMenu, root, screen);
}
}
@@ -0,0 +1,139 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.System
import qs.Services.UI
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property bool showUnreadBadge: widgetSettings.showUnreadBadge !== undefined ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge
readonly property bool hideWhenZero: widgetSettings.hideWhenZero !== undefined ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero
readonly property bool hideWhenZeroUnread: widgetSettings.hideWhenZeroUnread !== undefined ? widgetSettings.hideWhenZeroUnread : widgetMetadata.hideWhenZeroUnread
readonly property string unreadBadgeColor: widgetSettings.unreadBadgeColor !== undefined ? widgetSettings.unreadBadgeColor : widgetMetadata.unreadBadgeColor
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
readonly property color badgeColor: Color.resolveColorKey(unreadBadgeColor)
function computeUnreadCount() {
var since = NotificationService.lastSeenTs;
var count = 0;
var model = NotificationService.historyModel;
for (var i = 0; i < model.count; i++) {
var item = model.get(i);
var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp;
if (ts > since)
count++;
}
return count;
}
readonly property int count: computeUnreadCount()
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
applyUiScale: false
customRadius: Style.radiusL
icon: NotificationService.doNotDisturb ? "bell-off" : "bell"
tooltipText: {
if (PanelService.getPanel("notificationHistoryPanel", screen)?.isPanelOpen) {
return "";
} else {
return I18n.tr("tooltips.open-notification-history-enable-dnd");
}
}
tooltipDirection: BarService.getTooltipDirection(screen?.name)
colorBg: Style.capsuleColor
colorFg: Color.resolveColorKey(iconColorKey)
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
visible: !((hideWhenZero && NotificationService.historyModel.count === 0) || (hideWhenZeroUnread && count === 0))
opacity: !((hideWhenZero && NotificationService.historyModel.count === 0) || (hideWhenZeroUnread && count === 0)) ? 1.0 : 0.0
NPopupContextMenu {
id: contextMenu
model: [
{
"label": NotificationService.doNotDisturb ? I18n.tr("actions.disable-dnd") : I18n.tr("actions.enable-dnd"),
"action": "toggle-dnd",
"icon": NotificationService.doNotDisturb ? "bell" : "bell-off"
},
{
"label": I18n.tr("actions.clear-history"),
"action": "clear-history",
"icon": "trash"
},
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "toggle-dnd") {
NotificationService.doNotDisturb = !NotificationService.doNotDisturb;
} else if (action === "clear-history") {
NotificationService.clearHistory();
} else if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
onClicked: {
var panel = PanelService.getPanel("notificationHistoryPanel", screen);
panel?.toggle(this);
}
onRightClicked: {
PanelService.showContextMenu(contextMenu, root, screen);
}
Loader {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenterOffset: parent.baseSize / 4
anchors.verticalCenterOffset: -parent.baseSize / 4
z: 2
active: showUnreadBadge
sourceComponent: Rectangle {
id: badge
height: 7
width: height
radius: Style.radiusXS
color: root.hovering ? Color.mOnHover : (root.badgeColor || Color.mError)
border.color: Color.mSurface
border.width: Style.borderS
visible: count > 0
}
}
}
@@ -0,0 +1,74 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.UPower
import qs.Commons
import qs.Services.Power
import qs.Services.UI
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
applyUiScale: false
customRadius: Style.radiusL
visible: PowerProfileService.available
icon: PowerProfileService.getIcon()
tooltipText: I18n.tr("tooltips.power-profile", {
"profile": PowerProfileService.getName()
})
tooltipDirection: BarService.getTooltipDirection(screen?.name)
colorBg: (PowerProfileService.profile === PowerProfile.Balanced) ? Style.capsuleColor : Color.mPrimary
colorFg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.resolveColorKey(iconColorKey) : Color.mOnPrimary
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
onClicked: PowerProfileService.cycleProfile()
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
onRightClicked: {
PanelService.showContextMenu(contextMenu, root, screen);
}
}
@@ -0,0 +1,77 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.UI
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string iconColorKey: (widgetSettings.iconColor !== undefined) ? widgetSettings.iconColor : widgetMetadata.iconColor
baseSize: Style.getCapsuleHeightForScreen(screenName)
applyUiScale: false
customRadius: Style.radiusL
icon: "power"
tooltipText: {
if (PanelService.getPanel("sessionMenuPanel", screen)?.isPanelOpen)
return "";
else
return I18n.tr("tooltips.session-menu");
}
tooltipDirection: BarService.getTooltipDirection(screenName)
colorBg: Style.capsuleColor
colorFg: Color.resolveColorKey(iconColorKey)
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
onClicked: PanelService.getPanel("sessionMenuPanel", screen)?.toggle()
onRightClicked: {
PanelService.showContextMenu(contextMenu, root, screen);
}
}
@@ -0,0 +1,86 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string valueIconColor: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
readonly property color iconColor: Color.resolveColorKey(valueIconColor)
icon: "settings"
tooltipText: {
if (PanelService.getPanel("settingsPanel", screen)?.isPanelOpen) {
return "";
} else {
return I18n.tr("tooltips.open-settings");
}
}
tooltipDirection: BarService.getTooltipDirection(screen?.name)
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
applyUiScale: false
customRadius: Style.radiusL
colorBg: Style.capsuleColor
colorFg: iconColor
colorBgHover: Color.mHover
colorFgHover: Color.mOnHover
colorBorder: Style.capsuleBorderColor
colorBorderHover: Style.capsuleBorderColor
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
onClicked: {
if (Settings.data.ui.settingsPanelMode === "attached") {
PanelService.getPanel("settingsPanel", screen)?.toggle(this);
} else {
PanelService.getPanel("settingsPanel", screen)?.toggle();
}
}
onRightClicked: {
PanelService.showContextMenu(contextMenu, root, screen);
}
}
@@ -0,0 +1,41 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property real barHeight: Style.getBarHeightForScreen(screenName)
readonly property int spacerSize: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width
implicitWidth: isBarVertical ? barHeight : spacerSize
implicitHeight: isBarVertical ? spacerSize : barHeight
width: implicitWidth
height: implicitHeight
}
@@ -0,0 +1,967 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings
import qs.Services.System
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
readonly property bool compactMode: widgetSettings.compactMode !== undefined ? widgetSettings.compactMode : widgetMetadata.compactMode
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
readonly property bool useMonospaceFont: widgetSettings.useMonospaceFont !== undefined ? widgetSettings.useMonospaceFont : widgetMetadata.useMonospaceFont
readonly property bool usePadding: !compactMode && !isVertical && useMonospaceFont && ((widgetSettings.usePadding !== undefined) ? widgetSettings.usePadding : widgetMetadata.usePadding)
readonly property bool showCpuUsage: (widgetSettings.showCpuUsage !== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage
readonly property bool showCpuCores: (widgetSettings.showCpuCores !== undefined) ? widgetSettings.showCpuCores : widgetMetadata.showCpuCores
readonly property bool showCpuFreq: (widgetSettings.showCpuFreq !== undefined) ? widgetSettings.showCpuFreq : widgetMetadata.showCpuFreq
readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp
readonly property bool showGpuTemp: (widgetSettings.showGpuTemp !== undefined) ? widgetSettings.showGpuTemp : widgetMetadata.showGpuTemp
readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage
readonly property bool showMemoryAsPercent: (widgetSettings.showMemoryAsPercent !== undefined) ? widgetSettings.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
readonly property bool showSwapUsage: (widgetSettings.showSwapUsage !== undefined) ? widgetSettings.showSwapUsage : widgetMetadata.showSwapUsage
readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats
readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage
readonly property bool showDiskUsageAsPercent: (widgetSettings.showDiskUsageAsPercent !== undefined) ? widgetSettings.showDiskUsageAsPercent : widgetMetadata.showDiskUsageAsPercent
readonly property bool showDiskAvailable: (widgetSettings.showDiskAvailable !== undefined) ? widgetSettings.showDiskAvailable : widgetMetadata.showDiskAvailable
readonly property bool showLoadAverage: (widgetSettings.showLoadAverage !== undefined) ? widgetSettings.showLoadAverage : widgetMetadata.showLoadAverage
readonly property string diskPath: (widgetSettings.diskPath !== undefined) ? widgetSettings.diskPath : widgetMetadata.diskPath
readonly property string fontFamily: useMonospaceFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault
readonly property int paddingPercent: usePadding ? String("100%").length : 0
readonly property int paddingTemp: usePadding ? String("999°").length : 0
readonly property int paddingCpuFreq: usePadding ? String("9.9").length : 0
readonly property int paddingSpeed: usePadding ? String("9999G").length : 0
readonly property real iconSize: Style.toOdd(capsuleHeight * 0.48)
readonly property real miniGaugeWidth: Math.max(3, Style.toOdd(root.iconSize * 0.25))
// Content dimensions for implicit sizing
readonly property real contentWidth: isVertical ? capsuleHeight : Math.round(mainGrid.implicitWidth + Style.margin2M)
readonly property real contentHeight: isVertical ? Math.round(mainGrid.implicitHeight + Style.margin2M) : capsuleHeight
readonly property color iconColor: Color.resolveColorKey(iconColorKey)
readonly property color textColor: Color.resolveColorKey(textColorKey)
// Size: use implicit width/height
// BarWidgetLoader sets explicit width/height to extend click area
implicitWidth: contentWidth
implicitHeight: contentHeight
Component.onCompleted: SystemStatService.registerComponent("bar-sysmon:" + (screen?.name || "unknown"))
Component.onDestruction: SystemStatService.unregisterComponent("bar-sysmon:" + (screen?.name || "unknown"))
function openExternalMonitor() {
Quickshell.execDetached(["sh", "-c", Settings.data.systemMonitor.externalMonitor]);
}
// Build comprehensive tooltip text with all stats
function buildTooltipContent() {
let rows = [];
// CPU
rows.push([I18n.tr("system-monitor.cpu-usage"), `${Math.round(SystemStatService.cpuUsage)}% (${SystemStatService.cpuFreq.replace(/[^0-9.]/g, "")} GHz)`]);
if (showCpuCores) {
SystemStatService.coresUsage.forEach((usage, core) => rows.push([" " + I18n.tr("system-monitor.core-usage", {
"id": core
}), `${Math.round(usage)}%`]));
}
if (SystemStatService.cpuTemp > 0) {
rows.push([I18n.tr("system-monitor.cpu-temp"), `${Math.round(SystemStatService.cpuTemp)}°C`]);
}
// GPU (if available)
if (SystemStatService.gpuAvailable) {
rows.push([I18n.tr("system-monitor.gpu-temp"), `${Math.round(SystemStatService.gpuTemp)}°C`]);
}
// Load Average
if (SystemStatService.loadAvg1 >= 0) {
rows.push([I18n.tr("system-monitor.load-average"), `${SystemStatService.loadAvg1.toFixed(2)} · ${SystemStatService.loadAvg5.toFixed(2)} · ${SystemStatService.loadAvg15.toFixed(2)}`]);
}
// Memory
rows.push([I18n.tr("common.memory"), `${Math.round(SystemStatService.memPercent)}% (${(SystemStatService.memGb).toFixed(1)} GiB)`]);
// Swap (if available)
if (SystemStatService.swapTotalGb > 0) {
rows.push([I18n.tr("bar.system-monitor.swap-usage-label"), `${Math.round(SystemStatService.swapPercent)}% (${(SystemStatService.swapGb).toFixed(1)} GiB)`]);
}
// Network
rows.push([I18n.tr("system-monitor.download-speed"), `${SystemStatService.formatSpeed(SystemStatService.rxSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}` + "/s"]);
rows.push([I18n.tr("system-monitor.upload-speed"), `${SystemStatService.formatSpeed(SystemStatService.txSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}` + "/s"]);
// Disk
const diskPercent = SystemStatService.diskPercents[diskPath];
if (diskPercent !== undefined) {
const usedGb = SystemStatService.diskUsedGb[diskPath] || 0;
const sizeGb = SystemStatService.diskSizeGb[diskPath] || 0;
const availGb = SystemStatService.diskAvailableGb[diskPath] || 0;
rows.push([I18n.tr("system-monitor.disk"), `${diskPercent}% (${usedGb.toFixed(1)} / ${sizeGb.toFixed(1)} GB)`]);
rows.push([I18n.tr("common.available"), `${availGb.toFixed(1)} GB`]);
}
return rows;
}
// Visibility-aware warning/critical states (delegates to service)
readonly property bool cpuWarning: showCpuUsage && SystemStatService.cpuWarning
readonly property bool cpuCritical: showCpuUsage && SystemStatService.cpuCritical
readonly property bool tempWarning: showCpuTemp && SystemStatService.tempWarning
readonly property bool tempCritical: showCpuTemp && SystemStatService.tempCritical
readonly property bool gpuWarning: showGpuTemp && SystemStatService.gpuWarning
readonly property bool gpuCritical: showGpuTemp && SystemStatService.gpuCritical
readonly property bool memWarning: showMemoryUsage && SystemStatService.memWarning
readonly property bool memCritical: showMemoryUsage && SystemStatService.memCritical
readonly property bool swapWarning: showSwapUsage && SystemStatService.swapWarning
readonly property bool swapCritical: showSwapUsage && SystemStatService.swapCritical
readonly property bool diskWarning: showDiskUsage && SystemStatService.isDiskWarning(diskPath)
readonly property bool diskCritical: showDiskUsage && SystemStatService.isDiskCritical(diskPath)
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("system-monitor.title"),
"action": "sysmon-settings",
"icon": "settings"
},
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "sysmon-settings") {
let monitorCmd = Settings.data.systemMonitor.externalMonitor;
if (monitorCmd && monitorCmd.trim() !== "") {
openExternalMonitor();
} else {
SettingsPanelService.openToTab(SettingsPanel.Tab.System, 0, screen);
}
} else if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
// Visual capsule centered in parent
Rectangle {
id: visualCapsule
width: root.contentWidth
height: root.contentHeight
anchors.centerIn: parent
radius: Style.radiusM
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
// Mini gauge component for compact mode, vertical gauge that fills from bottom
Component {
id: miniGaugeComponent
NLinearGauge {
ratio: 0
orientation: Qt.Vertical
fillColor: Color.mPrimary
width: miniGaugeWidth
height: iconSize
}
}
GridLayout {
id: mainGrid
anchors.centerIn: parent
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: isVertical ? -1 : 1
columns: isVertical ? 1 : -1
rowSpacing: isVertical ? (compactMode ? Style.marginL : Style.marginXL) : 0
columnSpacing: isVertical ? 0 : Style.marginM
// CPU Usage Component
Item {
id: cpuUsageContainer
implicitWidth: cpuUsageContent.implicitWidth
implicitHeight: cpuUsageContent.implicitHeight
Layout.preferredWidth: isVertical ? root.width : implicitWidth
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showCpuUsage
GridLayout {
id: cpuUsageContent
anchors.centerIn: parent
property bool verticalDisplay: isVertical && (!compactMode || showCpuCores)
flow: verticalDisplay ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: verticalDisplay ? -1 : 1
columns: verticalDisplay ? 1 : -1
rowSpacing: compactMode ? 3 : Style.marginXS
columnSpacing: compactMode ? 3 : Style.marginXS
Item {
Layout.preferredWidth: iconSize
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
Layout.alignment: Qt.AlignCenter
Layout.row: (isVertical && !compactMode) ? 1 : 0
Layout.column: 0
NIcon {
icon: "cpu-usage"
pointSize: iconSize
applyUiScale: false
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, contentHeight)
color: (cpuWarning || cpuCritical) ? SystemStatService.cpuColor : root.iconColor
}
}
// Text mode
NText {
visible: !compactMode
text: `${Math.round(SystemStatService.cpuUsage)}%`.padStart(paddingPercent, " ")
family: fontFamily
pointSize: barFontSize
applyUiScale: false
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: (cpuWarning || cpuCritical) ? SystemStatService.cpuColor : root.textColor
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// Compact mode general cpu
Loader {
active: compactMode && !showCpuCores
visible: compactMode && !showCpuCores
sourceComponent: miniGaugeComponent
Layout.alignment: Qt.AlignCenter
Layout.row: 0
Layout.column: 1
onLoaded: {
item.ratio = Qt.binding(() => SystemStatService.cpuUsage / 100);
item.fillColor = Qt.binding(() => SystemStatService.cpuColor);
}
}
// Compact mode for cores
Repeater {
model: (compactMode && showCpuCores) ? SystemStatService.coresUsage : []
delegate: NLinearGauge {
required property var modelData
width: isVertical ? iconSize : miniGaugeWidth
height: isVertical ? miniGaugeWidth : iconSize
orientation: isVertical ? Qt.Horizontal : Qt.Vertical
ratio: modelData / 100
fillColor: SystemStatService.getCoreUsageColor(modelData)
}
}
}
}
// CPU Frequency Component
Item {
id: cpuFreqContainer
implicitWidth: cpuFreqContent.implicitWidth
implicitHeight: cpuFreqContent.implicitHeight
Layout.preferredWidth: isVertical ? root.width : implicitWidth
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showCpuFreq && (!isVertical || compactMode)
GridLayout {
id: cpuFreqContent
anchors.centerIn: parent
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: (isVertical && !compactMode) ? 2 : 1
columns: (isVertical && !compactMode) ? 1 : 2
rowSpacing: Style.marginXXS
columnSpacing: compactMode ? 3 : Style.marginXS
Item {
Layout.preferredWidth: iconSize
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
Layout.alignment: Qt.AlignCenter
Layout.row: (isVertical && !compactMode) ? 1 : 0
Layout.column: 0
NIcon {
icon: "cpu-usage"
pointSize: iconSize
applyUiScale: false
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, contentHeight)
color: root.iconColor
}
}
// Text mode
NText {
visible: !compactMode
text: SystemStatService.cpuFreq.replace("Hz", "").replace(" ", "").padStart(paddingCpuFreq, " ")
family: fontFamily
pointSize: barFontSize
applyUiScale: false
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: root.textColor
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// Compact mode
Loader {
active: compactMode
visible: compactMode
sourceComponent: miniGaugeComponent
Layout.alignment: Qt.AlignCenter
Layout.row: 0
Layout.column: 1
onLoaded: {
item.ratio = Qt.binding(() => SystemStatService.cpuFreqRatio);
}
}
}
}
// CPU Temperature Component
Item {
id: cpuTempContainer
implicitWidth: cpuTempContent.implicitWidth
implicitHeight: cpuTempContent.implicitHeight
Layout.preferredWidth: isVertical ? root.width : implicitWidth
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showCpuTemp
GridLayout {
id: cpuTempContent
anchors.centerIn: parent
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: (isVertical && !compactMode) ? 2 : 1
columns: (isVertical && !compactMode) ? 1 : 2
rowSpacing: Style.marginXXS
columnSpacing: compactMode ? 3 : Style.marginXS
Item {
Layout.preferredWidth: iconSize
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
Layout.alignment: Qt.AlignCenter
Layout.row: (isVertical && !compactMode) ? 1 : 0
Layout.column: 0
NIcon {
icon: "cpu-temperature"
pointSize: iconSize
applyUiScale: false
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, contentHeight)
color: (tempWarning || tempCritical) ? SystemStatService.tempColor : root.iconColor
}
}
// Text mode
NText {
visible: !compactMode
text: `${Math.round(SystemStatService.cpuTemp)}°`.padStart(paddingTemp, " ")
family: fontFamily
pointSize: barFontSize
applyUiScale: false
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: (tempWarning || tempCritical) ? SystemStatService.tempColor : root.textColor
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// Compact mode, mini gauge (to the right of icon)
Loader {
active: compactMode
visible: compactMode
sourceComponent: miniGaugeComponent
Layout.alignment: Qt.AlignCenter
Layout.row: 0
Layout.column: 1
onLoaded: {
item.ratio = Qt.binding(() => SystemStatService.cpuTemp / 100);
item.fillColor = Qt.binding(() => SystemStatService.tempColor);
}
}
}
}
// GPU Temperature Component
Item {
id: gpuTempContainer
implicitWidth: gpuTempContent.implicitWidth
implicitHeight: gpuTempContent.implicitHeight
Layout.preferredWidth: isVertical ? root.width : implicitWidth
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showGpuTemp && SystemStatService.gpuAvailable
GridLayout {
id: gpuTempContent
anchors.centerIn: parent
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: (isVertical && !compactMode) ? 2 : 1
columns: (isVertical && !compactMode) ? 1 : 2
rowSpacing: Style.marginXXS
columnSpacing: compactMode ? 3 : Style.marginXS
Item {
Layout.preferredWidth: iconSize
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
Layout.alignment: Qt.AlignCenter
Layout.row: (isVertical && !compactMode) ? 1 : 0
Layout.column: 0
NIcon {
icon: "gpu-temperature"
pointSize: iconSize
applyUiScale: false
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, contentHeight)
color: (gpuWarning || gpuCritical) ? SystemStatService.gpuColor : root.iconColor
}
}
// Text mode
NText {
visible: !compactMode
text: `${Math.round(SystemStatService.gpuTemp)}°`.padStart(paddingTemp, " ")
family: fontFamily
pointSize: barFontSize
applyUiScale: false
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: (gpuWarning || gpuCritical) ? SystemStatService.gpuColor : root.textColor
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// Compact mode
Loader {
active: compactMode
visible: compactMode
sourceComponent: miniGaugeComponent
Layout.alignment: Qt.AlignCenter
Layout.row: 0
Layout.column: 1
onLoaded: {
item.ratio = Qt.binding(() => SystemStatService.gpuTemp / 100);
item.fillColor = Qt.binding(() => SystemStatService.gpuColor);
}
}
}
}
// Load Average Component
Item {
id: loadAvgContainer
implicitWidth: loadAvgContent.implicitWidth
implicitHeight: loadAvgContent.implicitHeight
Layout.preferredWidth: isVertical ? root.width : implicitWidth
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showLoadAverage && SystemStatService.nproc > 0 && SystemStatService.loadAvg1 > 0
GridLayout {
id: loadAvgContent
anchors.centerIn: parent
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: (isVertical && !compactMode) ? 2 : 1
columns: (isVertical && !compactMode) ? 1 : 2
rowSpacing: Style.marginXXS
columnSpacing: compactMode ? 3 : Style.marginXS
Item {
Layout.preferredWidth: iconSize
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
Layout.alignment: Qt.AlignCenter
Layout.row: (isVertical && !compactMode) ? 1 : 0
Layout.column: 0
NIcon {
icon: "weight"
pointSize: iconSize
applyUiScale: false
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, contentHeight)
color: root.iconColor
}
}
// Text mode
NText {
visible: !compactMode
text: `${SystemStatService.loadAvg1.toFixed(1)}`.padStart(usePadding ? `${SystemStatService.nproc.toFixed(1)}`.length : 0, " ")
family: fontFamily
pointSize: barFontSize
applyUiScale: false
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: root.textColor
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// Compact mode
Loader {
active: compactMode
visible: compactMode
sourceComponent: miniGaugeComponent
Layout.alignment: Qt.AlignCenter
Layout.row: 0
Layout.column: 1
onLoaded: {
item.ratio = Qt.binding(() => Math.min(1, SystemStatService.loadAvg1 / SystemStatService.nproc));
}
}
}
}
// Memory Usage Component
Item {
id: memoryContainer
implicitWidth: memoryContent.implicitWidth
implicitHeight: memoryContent.implicitHeight
Layout.preferredWidth: isVertical ? root.width : implicitWidth
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showMemoryUsage
GridLayout {
id: memoryContent
anchors.centerIn: parent
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: (isVertical && !compactMode) ? 2 : 1
columns: (isVertical && !compactMode) ? 1 : 2
rowSpacing: Style.marginXXS
columnSpacing: compactMode ? 3 : Style.marginXS
Item {
Layout.preferredWidth: iconSize
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
Layout.alignment: Qt.AlignCenter
Layout.row: (isVertical && !compactMode) ? 1 : 0
Layout.column: 0
NIcon {
icon: "memory"
pointSize: iconSize
applyUiScale: false
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, contentHeight)
color: (memWarning || memCritical) ? SystemStatService.memColor : root.iconColor
}
}
// Text mode
NText {
visible: !compactMode
text: SystemStatService.formatRamDisplay({
percent: showMemoryAsPercent,
padding: usePadding
})
family: fontFamily
pointSize: barFontSize
applyUiScale: false
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: (memWarning || memCritical) ? SystemStatService.memColor : root.textColor
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// Compact mode
Loader {
active: compactMode
visible: compactMode
sourceComponent: miniGaugeComponent
Layout.alignment: Qt.AlignCenter
Layout.row: 0
Layout.column: 1
onLoaded: {
item.ratio = Qt.binding(() => SystemStatService.memPercent / 100);
item.fillColor = Qt.binding(() => SystemStatService.memColor);
}
}
}
}
// Swap Usage Component
Item {
id: swapContainer
implicitWidth: swapContent.implicitWidth
implicitHeight: swapContent.implicitHeight
Layout.preferredWidth: isVertical ? root.width : implicitWidth
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showSwapUsage && SystemStatService.swapTotalGb > 0
GridLayout {
id: swapContent
anchors.centerIn: parent
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: (isVertical && !compactMode) ? 2 : 1
columns: (isVertical && !compactMode) ? 1 : 2
rowSpacing: Style.marginXXS
columnSpacing: compactMode ? 3 : Style.marginXS
Item {
Layout.preferredWidth: iconSize
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
Layout.alignment: Qt.AlignCenter
Layout.row: (isVertical && !compactMode) ? 1 : 0
Layout.column: 0
NIcon {
icon: "exchange"
pointSize: iconSize
applyUiScale: false
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, contentHeight)
color: (swapWarning || swapCritical) ? SystemStatService.swapColor : root.iconColor
}
}
// Text mode
NText {
visible: !compactMode
text: SystemStatService.formatRamDisplay({
swap: true,
percent: true,
padding: usePadding
})
family: fontFamily
pointSize: barFontSize
applyUiScale: false
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: (swapWarning || swapCritical) ? SystemStatService.swapColor : root.textColor
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// Compact mode
Loader {
active: compactMode
visible: compactMode
sourceComponent: miniGaugeComponent
Layout.alignment: Qt.AlignCenter
Layout.row: 0
Layout.column: 1
onLoaded: {
item.ratio = Qt.binding(() => SystemStatService.swapPercent / 100);
item.fillColor = Qt.binding(() => SystemStatService.swapColor);
}
}
}
}
// Network Download Speed Component
Item {
implicitWidth: downloadContent.implicitWidth
implicitHeight: downloadContent.implicitHeight
Layout.preferredWidth: isVertical ? root.width : implicitWidth
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showNetworkStats
GridLayout {
id: downloadContent
anchors.centerIn: parent
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: (isVertical && !compactMode) ? 2 : 1
columns: (isVertical && !compactMode) ? 1 : 2
rowSpacing: Style.marginXXS
columnSpacing: compactMode ? 3 : Style.marginXS
Item {
Layout.preferredWidth: iconSize
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
Layout.alignment: Qt.AlignCenter
Layout.row: (isVertical && !compactMode) ? 1 : 0
Layout.column: 0
NIcon {
icon: "download-speed"
pointSize: iconSize
applyUiScale: false
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, contentHeight)
color: root.iconColor
}
}
// Text mode
NText {
visible: !compactMode
text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.rxSpeed) : SystemStatService.formatSpeed(SystemStatService.rxSpeed).padStart(paddingSpeed, " ")
family: fontFamily
pointSize: barFontSize
applyUiScale: false
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: root.textColor
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// Compact mode
Loader {
active: compactMode
visible: compactMode
sourceComponent: miniGaugeComponent
Layout.alignment: Qt.AlignCenter
Layout.row: 0
Layout.column: 1
onLoaded: {
item.ratio = Qt.binding(() => SystemStatService.rxRatio);
}
}
}
}
// Network Upload Speed Component
Item {
implicitWidth: uploadContent.implicitWidth
implicitHeight: uploadContent.implicitHeight
Layout.preferredWidth: isVertical ? root.width : implicitWidth
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showNetworkStats
GridLayout {
id: uploadContent
anchors.centerIn: parent
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: (isVertical && !compactMode) ? 2 : 1
columns: (isVertical && !compactMode) ? 1 : 2
rowSpacing: Style.marginXXS
columnSpacing: compactMode ? 3 : Style.marginXS
Item {
Layout.preferredWidth: iconSize
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
Layout.alignment: Qt.AlignCenter
Layout.row: (isVertical && !compactMode) ? 1 : 0
Layout.column: 0
NIcon {
icon: "upload-speed"
pointSize: iconSize
applyUiScale: false
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, contentHeight)
color: root.iconColor
}
}
// Text mode
NText {
visible: !compactMode
text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.txSpeed) : SystemStatService.formatSpeed(SystemStatService.txSpeed).padStart(paddingSpeed, " ")
family: fontFamily
pointSize: barFontSize
applyUiScale: false
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: root.textColor
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// Compact mode
Loader {
active: compactMode
visible: compactMode
sourceComponent: miniGaugeComponent
Layout.alignment: Qt.AlignCenter
Layout.row: 0
Layout.column: 1
onLoaded: {
item.ratio = Qt.binding(() => SystemStatService.txRatio);
}
}
}
}
// Disk Usage Component (primary drive)
Item {
id: diskContainer
implicitWidth: diskContent.implicitWidth
implicitHeight: diskContent.implicitHeight
Layout.preferredWidth: isVertical ? root.width : implicitWidth
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showDiskUsage
GridLayout {
id: diskContent
anchors.centerIn: parent
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: (isVertical && !compactMode) ? 2 : 1
columns: (isVertical && !compactMode) ? 1 : 2
rowSpacing: Style.marginXXS
columnSpacing: compactMode ? 3 : Style.marginXS
Item {
Layout.preferredWidth: iconSize
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
Layout.alignment: Qt.AlignCenter
Layout.row: (isVertical && !compactMode) ? 1 : 0
Layout.column: 0
NIcon {
icon: "storage"
pointSize: iconSize
applyUiScale: false
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, contentHeight)
color: (diskWarning || diskCritical) ? SystemStatService.getDiskColor(diskPath) : root.iconColor
}
}
// Text mode
NText {
visible: !compactMode
text: SystemStatService.formatDiskDisplay(diskPath, {
percent: showDiskUsageAsPercent,
available: showDiskAvailable,
padding: usePadding
})
family: fontFamily
pointSize: barFontSize
applyUiScale: false
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: (diskWarning || diskCritical) ? SystemStatService.getDiskColor(diskPath) : root.textColor
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// Compact mode
Loader {
active: compactMode
visible: compactMode
sourceComponent: miniGaugeComponent
Layout.alignment: Qt.AlignCenter
Layout.row: 0
Layout.column: 1
onLoaded: {
item.ratio = Qt.binding(() => (showDiskAvailable ? SystemStatService.diskAvailPercents[diskPath] : SystemStatService.diskPercents[diskPath] ?? 0) / 100);
item.fillColor = Qt.binding(() => SystemStatService.getDiskColor(diskPath, showDiskAvailable));
}
}
}
}
}
}
// MouseArea at root level for extended click area
MouseArea {
id: tooltipArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
PanelService.getPanel("systemStatsPanel", screen)?.toggle(root);
TooltipService.hide();
} else if (mouse.button === Qt.RightButton) {
TooltipService.hide();
PanelService.showContextMenu(contextMenu, root, screen);
} else if (mouse.button === Qt.MiddleButton) {
TooltipService.hide();
openExternalMonitor();
}
}
onEntered: {
if (!PanelService.getPanel("systemStatsPanel", screen).isPanelOpen) {
TooltipService.show(root, buildTooltipContent(), BarService.getTooltipDirection(root.screen?.name));
tooltipRefreshTimer.start();
}
}
onExited: {
tooltipRefreshTimer.stop();
TooltipService.hide();
}
}
Timer {
id: tooltipRefreshTimer
interval: 1000
repeat: true
onTriggered: {
if (tooltipArea.containsMouse) {
TooltipService.updateText(buildTooltipContent());
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,580 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Trigger re-evaluation when window is registered
property int popupMenuUpdateTrigger: 0
// Get shared popup menu window from PanelService (reactive to trigger changes)
readonly property var popupMenuWindow: {
// Reference trigger to force re-evaluation
var popupMenuUpdateTriggerRef = popupMenuUpdateTrigger;
return PanelService.getPopupMenuWindow(screen);
}
readonly property var trayMenu: popupMenuWindow ? popupMenuWindow.trayMenuLoader : null
Connections {
target: PanelService
function onPopupMenuWindowRegistered(registeredScreen) {
if (registeredScreen === screen) {
root.popupMenuUpdateTrigger++;
}
}
}
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
readonly property real barHeight: Style.getBarHeightForScreen(screenName)
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property bool density: Settings.data.bar.density
readonly property int iconSize: Style.toOdd(capsuleHeight * 0.65)
property var blacklist: widgetSettings.blacklist || widgetMetadata.blacklist || [] // Read from settings
property var pinned: widgetSettings.pinned || widgetMetadata.pinned || [] // Pinned items (shown inline)
property bool drawerEnabled: widgetSettings.drawerEnabled !== undefined ? widgetSettings.drawerEnabled : (widgetMetadata.drawerEnabled !== undefined ? widgetMetadata.drawerEnabled : true) // Enable drawer panel
property bool hidePassive: widgetSettings.hidePassive !== undefined ? widgetSettings.hidePassive : true // Hide passive status items
readonly property string chevronColorKey: widgetSettings.chevronColor !== undefined ? widgetSettings.chevronColor : widgetMetadata.chevronColor
readonly property color chevronColor: Color.resolveColorKey(chevronColorKey)
property var filteredItems: [] // Items to show inline (pinned)
property var dropdownItems: [] // Items to show in drawer (unpinned)
property int hoveredItemIndex: -1 // Track hovered item for dot indicator
Timer {
id: updateDebounceTimer
interval: 100 // milliseconds
running: false
repeat: false
onTriggered: _performFilteredItemsUpdate()
}
readonly property var statusSignature: {
if (!SystemTray.items || !SystemTray.items.values) {
return "";
}
var sig = "";
var items = SystemTray.items.values;
for (var i = 0; i < items.length; i++) {
var item = items[i];
if (item) {
// Direct property access creates reactive binding
var s = item.status;
sig += (item.id || i) + ":" + (s !== undefined ? s : -1);
}
}
// Trigger update when signature changes (status changed)
if (root.hidePassive) {
Qt.callLater(root.updateFilteredItems);
}
return sig;
}
Repeater {
id: statusConnectionsRepeater
model: SystemTray.items && SystemTray.items.values ? SystemTray.items.values : []
delegate: Item {
Connections {
target: modelData
enabled: modelData !== null && modelData !== undefined
function onStatusChanged() {
if (root.hidePassive) {
root.updateFilteredItems();
}
}
}
}
}
function _performFilteredItemsUpdate() {
// Force a fresh read of settings to ensure we have the latest blacklist
var currentSettings = {};
if (section && sectionWidgetIndex >= 0 && screenName) {
var w = Settings.getBarWidgetsForScreen(screenName)[section];
if (w && sectionWidgetIndex < w.length) {
currentSettings = w[sectionWidgetIndex];
}
}
// Update local properties with fresh data
if (currentSettings.blacklist !== undefined)
root.blacklist = currentSettings.blacklist;
if (currentSettings.pinned !== undefined)
root.pinned = currentSettings.pinned;
let newItems = [];
if (SystemTray.items && SystemTray.items.values) {
const trayItems = SystemTray.items.values;
for (var i = 0; i < trayItems.length; i++) {
const item = trayItems[i];
if (!item) {
continue;
}
const title = item.tooltipTitle || item.name || item.id || "";
// Skip passive items if hidePassive is enabled
if (root.hidePassive && item.status !== undefined && (item.status === SystemTray.Passive || item.status === 0)) {
continue;
}
// Check if blacklisted
let isBlacklisted = false;
if (root.blacklist && root.blacklist.length > 0) {
for (var j = 0; j < root.blacklist.length; j++) {
const rule = root.blacklist[j];
if (wildCardMatch(title, rule)) {
isBlacklisted = true;
break;
}
}
}
if (!isBlacklisted) {
newItems.push(item);
}
}
}
// If drawer is disabled, show all items inline
if (!root.drawerEnabled) {
filteredItems = newItems;
dropdownItems = [];
} else {
// Build inline (pinned) and drawer (unpinned) lists
// If pinned list is empty, all items go to drawer (none inline)
// If pinned list has items, pinned items are inline, rest go to drawer
if (pinned && pinned.length > 0) {
let pinnedItems = [];
for (var k = 0; k < newItems.length; k++) {
const item2 = newItems[k];
const title2 = item2.tooltipTitle || item2.name || item2.id || "";
for (var m = 0; m < pinned.length; m++) {
const rule2 = pinned[m];
if (wildCardMatch(title2, rule2)) {
pinnedItems.push(item2);
break;
}
}
}
filteredItems = pinnedItems;
// Unpinned items go to drawer
let unpinnedItems = [];
for (var v = 0; v < newItems.length; v++) {
const cand = newItems[v];
let isPinned = false;
for (var f = 0; f < filteredItems.length; f++) {
if (filteredItems[f] === cand) {
isPinned = true;
break;
}
}
if (!isPinned)
unpinnedItems.push(cand);
}
dropdownItems = unpinnedItems;
} else {
// No pinned items: all items go to drawer (none inline)
filteredItems = [];
dropdownItems = newItems;
}
}
}
function updateFilteredItems() {
updateDebounceTimer.restart();
}
function wildCardMatch(str, rule) {
if (!str || !rule) {
return false;
}
// First, convert '*' to a placeholder to preserve it, then escape other special regex characters
// Use a unique placeholder that won't appear in normal strings
const placeholder = '\uE000'; // Private use character
let processedRule = rule.replace(/\*/g, placeholder);
// Escape all special regex characters (but placeholder won't match this)
let escapedRule = processedRule.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
// Convert placeholder back to '.*' for wildcard matching
let pattern = escapedRule.replace(new RegExp(placeholder, 'g'), '.*');
// Add ^ and $ to match the entire string
pattern = '^' + pattern + '$';
try {
const regex = new RegExp(pattern, 'i');
// 'i' for case-insensitive
return regex.test(str);
} catch (e) {
Logger.w("Tray", "Invalid regex pattern for wildcard match:", rule, e.message);
return false; // If regex is invalid, it won't match
}
}
function toggleDrawer(button) {
TooltipService.hideImmediately();
// Close the popup menu if it's open
if (popupMenuWindow && popupMenuWindow.visible) {
popupMenuWindow.close();
}
const panel = PanelService.getPanel("trayDrawerPanel", root.screen);
if (panel) {
panel.widgetSection = root.section;
panel.widgetIndex = root.sectionWidgetIndex;
panel.toggle(this);
}
}
function onLoaded() {
// When the widget is fully initialized with its props set the screen for the trayMenu
if (trayMenu && trayMenu.item) {
trayMenu.item.screen = screen;
}
}
Connections {
target: SystemTray.items
function onValuesChanged() {
root.updateFilteredItems();
// Repeater will automatically update when items change
}
}
Connections {
target: Settings
function onSettingsSaved() {
root.updateFilteredItems();
}
}
// Watch for hidePassive changes to update filtering immediately
onHidePassiveChanged: {
root.updateFilteredItems();
}
Component.onCompleted: {
root.updateFilteredItems(); // Initial update
}
// Content dimensions for implicit sizing
readonly property int visibleItemCount: (root.drawerEnabled && dropdownItems.length > 0 ? 1 : 0) + filteredItems.length
readonly property real capsulePadding: 0
readonly property real capsuleWidth: isVertical ? capsuleHeight : Math.round(trayFlow.implicitWidth + capsulePadding * 2)
readonly property real capsuleContentHeight: isVertical ? Math.round(trayFlow.implicitHeight + capsulePadding * 2) : capsuleHeight
implicitWidth: isVertical ? barHeight : Math.round(trayFlow.implicitWidth + capsulePadding * 2)
implicitHeight: isVertical ? Math.round(trayFlow.implicitHeight + capsulePadding * 2) : barHeight
visible: filteredItems.length > 0 || dropdownItems.length > 0
opacity: (filteredItems.length > 0 || dropdownItems.length > 0) ? 1.0 : 0.0
// Visual capsule centered in parent
Rectangle {
id: visualCapsule
width: capsuleWidth
height: capsuleContentHeight
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
radius: Style.radiusM
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
}
NPopupContextMenu {
id: chevronContextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
chevronContextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
Flow {
id: trayFlow
spacing: 0
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
// Position centered in capsule
anchors.centerIn: visualCapsule
// Drawer opener (before items if opposite direction)
NIconButton {
id: chevronIconBefore
visible: root.drawerEnabled && dropdownItems.length > 0 && BarService.getPillDirection(root)
width: isVertical ? barHeight : capsuleHeight
height: isVertical ? capsuleHeight : barHeight
tooltipText: {
if (PanelService.getPanel("trayDrawerPanel", root.screen)?.isPanelOpen) {
return "";
} else {
return I18n.tr("tooltips.open-tray-dropdown");
}
}
tooltipDirection: BarService.getTooltipDirection(root.screen?.name)
baseSize: capsuleHeight
applyUiScale: false
customRadius: Style.radiusL
colorBg: "transparent"
colorFg: root.chevronColor
colorBorder: "transparent"
colorBorderHover: "transparent"
icon: {
switch (barPosition) {
case "bottom":
return "caret-up";
case "left":
return "caret-right";
case "right":
return "caret-left";
case "top":
default:
return "caret-down";
}
}
onClicked: toggleDrawer(this)
onRightClicked: PanelService.showContextMenu(chevronContextMenu, this, screen)
}
// Pinned items
Repeater {
id: repeater
model: root.filteredItems
delegate: Item {
id: trayDelegate
required property var modelData
required property int index
width: isVertical ? barHeight : capsuleHeight
height: isVertical ? capsuleHeight : barHeight
visible: modelData
readonly property bool isHovered: root.hoveredItemIndex === index
// Tooltip anchor representing the visual area (for proper tooltip positioning)
Item {
id: tooltipAnchor
width: capsuleHeight
height: capsuleHeight
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
}
IconImage {
id: trayIcon
width: iconSize
height: iconSize
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
asynchronous: true
backer.fillMode: Image.PreserveAspectFit
source: {
let icon = modelData?.icon || "";
if (!icon) {
return "";
}
// Process icon path
if (icon.includes("?path=")) {
const chunks = icon.split("?path=");
const name = chunks[0];
const path = chunks[1];
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
return icon;
}
opacity: status === Image.Ready ? 1 : 0
layer.enabled: widgetSettings.colorizeIcons !== false
layer.effect: ShaderEffect {
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
property real colorizeMode: 1.0
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
}
}
Rectangle {
id: hoverIndicator
anchors.bottom: trayIcon.bottom
anchors.bottomMargin: -2
anchors.horizontalCenter: trayIcon.horizontalCenter
width: Style.toOdd(iconSize * 0.25)
height: 4
color: trayDelegate.isHovered ? Color.mHover : "transparent"
radius: Math.min(Style.radiusXXS, width / 2)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
MouseArea {
id: itemMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onContainsMouseChanged: {
if (containsMouse) {
if (popupMenuWindow) {
popupMenuWindow.close();
}
root.hoveredItemIndex = trayDelegate.index;
TooltipService.show(tooltipAnchor, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection(root.screen?.name));
} else if (root.hoveredItemIndex === trayDelegate.index) {
root.hoveredItemIndex = -1;
TooltipService.hide(tooltipAnchor);
}
}
onClicked: mouse => {
if (!modelData) {
return;
}
if (mouse.button === Qt.LeftButton) {
// Close any open menu first
if (popupMenuWindow) {
popupMenuWindow.close();
}
if (!modelData.onlyMenu) {
modelData.activate();
}
} else if (mouse.button === Qt.MiddleButton) {
// Close the menu if it was visible
if (popupMenuWindow && popupMenuWindow.visible) {
popupMenuWindow.close();
return;
}
modelData.secondaryActivate && modelData.secondaryActivate();
} else if (mouse.button === Qt.RightButton) {
TooltipService.hideImmediately();
// Close the menu if it was visible
if (popupMenuWindow && popupMenuWindow.visible) {
popupMenuWindow.close();
return;
}
// Close any opened panel
if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) {
PanelService.openedPanel.close();
}
if (modelData.hasMenu && modelData.menu && trayMenu && trayMenu.item) {
// Calculate menu position after ensuring menu is loaded
const calculateAndShow = () => {
// Position menu based on bar position, using tooltipAnchor for proper positioning
// Increased spacing for better alignment with other context menus
let menuX, menuY;
if (barPosition === "left") {
// For left bar: position menu to the right of the visual area
menuX = tooltipAnchor.width + Style.marginL;
menuY = 0;
} else if (barPosition === "right") {
// For right bar: position menu to the left of the visual area
menuX = -trayMenu.item.implicitWidth - Style.marginL;
menuY = 0;
} else {
// For horizontal bars: center horizontally and position below visual area
menuX = (tooltipAnchor.width / 2) - (trayMenu.item.implicitWidth / 2);
menuY = tooltipAnchor.height + Style.marginS;
}
PanelService.showTrayMenu(root.screen, modelData, trayMenu.item, tooltipAnchor, menuX, menuY, root.section, root.sectionWidgetIndex);
};
// Use Qt.callLater to ensure menu dimensions are calculated
Qt.callLater(calculateAndShow);
} else {
Logger.d("Tray", "No menu available for", modelData.id, "or trayMenu not set");
}
}
}
}
}
}
// Drawer opener (after items if normal direction)
NIconButton {
id: chevronIconAfter
visible: root.drawerEnabled && dropdownItems.length > 0 && !BarService.getPillDirection(root)
width: isVertical ? barHeight : capsuleHeight
height: isVertical ? capsuleHeight : barHeight
tooltipText: I18n.tr("tooltips.open-tray-dropdown")
tooltipDirection: BarService.getTooltipDirection(root.screen?.name)
baseSize: capsuleHeight
applyUiScale: false
customRadius: Style.radiusL
colorBg: "transparent"
colorFg: root.chevronColor
colorBorder: "transparent"
colorBorderHover: "transparent"
icon: {
switch (barPosition) {
case "bottom":
return "caret-up";
case "left":
return "caret-right";
case "right":
return "caret-left";
case "top":
default:
return "caret-down";
}
}
onClicked: toggleDrawer(this)
onRightClicked: PanelService.showContextMenu(chevronContextMenu, this, screen)
}
} // closes Flow
}
@@ -0,0 +1,139 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.Networking
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
implicitWidth: pill.width
implicitHeight: pill.height
NPopupContextMenu {
id: contextMenu
model: {
const items = [];
const active = VPNService.activeConnections;
for (let i = 0; i < active.length; ++i) {
const conn = active[i];
items.push({
"label": I18n.tr("actions.disconnect-vpn", {
"name": conn.name
}),
"action": "disconnect:" + conn.uuid,
"icon": "shield-off"
});
}
const inactive = VPNService.inactiveConnections;
for (let i = 0; i < inactive.length; ++i) {
const conn = inactive[i];
items.push({
"label": I18n.tr("actions.connect-vpn", {
"name": conn.name
}),
"action": "connect:" + conn.uuid,
"icon": "shield-lock"
});
}
items.push({
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
});
return items;
}
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (!action) {
return;
}
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
return;
}
if (action.startsWith("connect:")) {
const uuid = action.substring("connect:".length);
VPNService.connect(uuid);
return;
}
if (action.startsWith("disconnect:")) {
const uuid = action.substring("disconnect:".length);
VPNService.disconnect(uuid);
}
}
}
BarPill {
id: pill
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
icon: VPNService.hasActiveConnection ? "shield-lock" : "shield"
text: {
if (VPNService.activeConnections.length > 0) {
return VPNService.activeConnections[0].name;
}
if (VPNService.connectingUuid) {
const pending = VPNService.connections[VPNService.connectingUuid];
if (pending) {
return pending.name;
}
}
return "";
}
suffix: {
if (VPNService.activeConnections.length > 1) {
return ` + ${VPNService.activeConnections.length - 1}`;
}
return "";
}
autoHide: false
forceOpen: !isBarVertical && root.displayMode === "alwaysShow"
forceClose: isBarVertical || root.displayMode === "alwaysHide" || !pill.text
onRightClicked: {
PanelService.showContextMenu(contextMenu, pill, screen);
}
tooltipText: {
if (pill.text !== "") {
return pill.text;
}
return I18n.tr("tooltips.manage-vpn");
}
}
}
@@ -0,0 +1,203 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings
import qs.Services.Media
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
readonly property string middleClickCommand: (widgetSettings.middleClickCommand !== undefined) ? widgetSettings.middleClickCommand : widgetMetadata.middleClickCommand
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
readonly property bool reverseScroll: Settings.data.general.reverseScroll
// Used to avoid opening the pill on Quickshell startup
property bool firstVolumeReceived: false
property int wheelAccumulator: 0
implicitWidth: pill.width
implicitHeight: pill.height
// Connection used to open the pill when volume changes
Connections {
target: AudioService
function onVolumeChanged() {
// Logger.i("Bar:Volume", "onVolumeChanged")
if (!firstVolumeReceived) {
// Ignore the first volume change
firstVolumeReceived = true;
} else {
// Hide any tooltip while the pill is visible / being updated
TooltipService.hide();
pill.show();
externalHideTimer.restart();
}
}
function onMutedChanged() {
if (!firstVolumeReceived) {
firstVolumeReceived = true;
} else {
TooltipService.hide();
pill.show();
externalHideTimer.restart();
}
}
function onVolumeAtMaximum() {
if (!firstVolumeReceived) {
firstVolumeReceived = true;
} else {
// Hide any tooltip while the pill is visible / being updated
TooltipService.hide();
pill.show();
externalHideTimer.restart();
}
}
function onVolumeAtMinimum() {
if (!firstVolumeReceived) {
firstVolumeReceived = true;
} else {
// Hide any tooltip while the pill is visible / being updated
TooltipService.hide();
pill.show();
externalHideTimer.restart();
}
}
}
Timer {
id: externalHideTimer
running: false
interval: 1500
onTriggered: {
pill.hide();
}
}
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.toggle-mute"),
"action": "toggle-mute",
"icon": AudioService.muted ? "volume-off" : "volume"
},
{
"label": I18n.tr("actions.run-custom-command"),
"action": "custom-command",
"icon": "adjustments"
},
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "toggle-mute") {
AudioService.setOutputMuted(!AudioService.muted);
} else if (action === "custom-command") {
Quickshell.execDetached(["sh", "-c", middleClickCommand]);
} else if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
BarPill {
id: pill
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
icon: AudioService.getOutputIcon()
autoHide: false // Important to be false so we can hover as long as we want
text: {
const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0;
const displayVolume = Math.min(maxVolume, AudioService.volume);
return Math.round(displayVolume * 100);
}
suffix: "%"
forceOpen: displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide"
tooltipText: {
if (PanelService.getPanel("audioPanel", screen)?.isPanelOpen) {
return "";
} else {
const nick = AudioService.sink?.nickname ?? "";
const volumeText = I18n.tr("tooltips.volume-at", {
"volume": (() => {
const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0;
const displayVolume = Math.min(maxVolume, AudioService.volume);
return Math.round(displayVolume * 100);
})()
});
return nick ? volumeText + "\n" + nick : volumeText;
}
}
onWheel: function (delta) {
// Hide tooltip as soon as the user starts scrolling to adjust volume
TooltipService.hide();
if (root.reverseScroll)
delta *= -1;
wheelAccumulator += delta;
if (wheelAccumulator >= 120) {
wheelAccumulator = 0;
AudioService.increaseVolume();
} else if (wheelAccumulator <= -120) {
wheelAccumulator = 0;
AudioService.decreaseVolume();
}
}
onClicked: {
PanelService.getPanel("audioPanel", screen)?.toggle(this);
}
onRightClicked: {
PanelService.showContextMenu(contextMenu, pill, screen);
}
onMiddleClicked: {
Quickshell.execDetached(["sh", "-c", middleClickCommand]);
}
}
}
@@ -0,0 +1,92 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.UI
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
enabled: Settings.data.wallpaper.enabled
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
applyUiScale: false
customRadius: Style.radiusL
icon: "wallpaper-selector"
tooltipText: {
if (PanelService.getPanel("wallpaperPanel", screen)?.isPanelOpen) {
return "";
} else {
return I18n.tr("tooltips.wallpaper-selector");
}
}
tooltipDirection: BarService.getTooltipDirection(screen?.name)
colorBg: Style.capsuleColor
colorFg: Color.resolveColorKey(iconColorKey)
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.random-wallpaper"),
"action": "random-wallpaper",
"icon": "dice"
},
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "random-wallpaper") {
WallpaperService.setRandomWallpaper();
} else if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
onClicked: {
var wallpaperPanel = PanelService.getPanel("wallpaperPanel", screen);
if (Settings.data.wallpaper.panelPosition === "follow_bar") {
wallpaperPanel?.toggle(this);
} else {
wallpaperPanel?.toggle();
}
}
onRightClicked: {
PanelService.showContextMenu(contextMenu, root, screen);
}
}
File diff suppressed because it is too large Load Diff