Merge branch 'github-main'

This commit is contained in:
Never Gude 2025-09-08 13:32:06 +02:00
commit 6e009d3551
72 changed files with 2510 additions and 1137 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View file

@ -9,3 +9,24 @@ for i in {1..8}; do
done
echo "All notifications sent!"
# Additional tests for icon/image handling
if command -v notify-send >/dev/null 2>&1; then
echo "Sending icon/image tests..."
# 1) Themed icon name
notify-send -i dialog-information "Icon name test" "Should resolve from theme (dialog-information)"
# 2) Absolute path if a sample image exists
SAMPLE_IMG="/usr/share/pixmaps/debian-logo.png"
if [ -f "$SAMPLE_IMG" ]; then
notify-send -i "$SAMPLE_IMG" "Absolute path test" "Should show the provided image path"
fi
# 3) file:// URL form
if [ -f "$SAMPLE_IMG" ]; then
notify-send -i "file://$SAMPLE_IMG" "file:// URL test" "Should display after stripping scheme"
fi
echo "Icon/image tests sent!"
fi

View file

@ -102,7 +102,7 @@ Singleton {
// FileView to load custom colors data from colors.json
FileView {
id: customColorsFile
path: Settings.configDir + "colors.json"
path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : ""
watchChanges: true
onFileChanged: {
Logger.log("Color", "Reloading colors from disk")
@ -112,6 +112,13 @@ Singleton {
Logger.log("Color", "Writing colors to disk")
writeAdapter()
}
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path === Settings.configDir + "colors.json") {
reload()
}
}
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) {
// File doesn't exist, create it with default values

View file

@ -26,11 +26,13 @@ Singleton {
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos"
property string defaultLocation: "Tokyo"
property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
// Used to access via Settings.data.xxx.yyy
readonly property alias data: adapter
property bool isLoaded: false
property bool directoriesCreated: false
// Signal emitted when settings are loaded after startupcale changes
signal settingsLoaded
@ -71,34 +73,93 @@ Singleton {
// -----------------------------------------------------
// If the settings structure has changed, ensure
// backward compatibility
// backward compatibility by upgrading the settings
function upgradeSettingsData() {
for (var i = 0; i < adapter.bar.widgets.left.length; i++) {
var obj = adapter.bar.widgets.left[i]
if (typeof obj === "string") {
adapter.bar.widgets.left[i] = {
"id": obj
const sections = ["left", "center", "right"]
// -----------------
// 1st. check our settings are not super old, when we only had the widget type as a plain string
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
if (typeof widget === "string") {
adapter.bar.widgets[sectionName][i] = {
"id": widget
}
}
}
}
for (var i = 0; i < adapter.bar.widgets.center.length; i++) {
var obj = adapter.bar.widgets.center[i]
if (typeof obj === "string") {
adapter.bar.widgets.center[i] = {
"id": obj
// -----------------
// 2nd. migrate global settings to user settings
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
// Check if widget registry supports user settings, if it does not, then there is nothing to do
const reg = BarWidgetRegistry.widgetMetadata[widget.id]
if ((reg === undefined) || (reg.allowUserSettings === undefined) || !reg.allowUserSettings) {
continue
}
}
}
for (var i = 0; i < adapter.bar.widgets.right.length; i++) {
var obj = adapter.bar.widgets.right[i]
if (typeof obj === "string") {
adapter.bar.widgets.right[i] = {
"id": obj
// Check that the widget was not previously migrated and skip if necessary
const keys = Object.keys(widget)
if (keys.length > 1) {
continue
}
migrateWidget(widget)
Logger.log("Settings", JSON.stringify(widget))
}
}
}
// -----------------------------------------------------
function migrateWidget(widget) {
Logger.log("Settings", `Migrating '${widget.id}' widget`)
switch (widget.id) {
case "ActiveWindow":
widget.showIcon = adapter.bar.showActiveWindowIcon
break
case "Battery":
widget.alwaysShowPercentage = adapter.bar.alwaysShowBatteryPercentage
break
case "Brightness":
widget.alwaysShowPercentage = BarWidgetRegistry.widgetMetadata[widget.id].alwaysShowPercentage
break
case "Clock":
widget.showDate = adapter.location.showDateWithClock
widget.use12HourClock = adapter.location.use12HourClock
widget.reverseDayMonth = adapter.location.reverseDayMonth
widget.showSeconds = BarWidgetRegistry.widgetMetadata[widget.id].showSeconds
break
case "MediaMini":
widget.showAlbumArt = adapter.audio.showMiniplayerAlbumArt
widget.showVisualizer = adapter.audio.showMiniplayerCava
widget.visualizerType = BarWidgetRegistry.widgetMetadata[widget.id].visualizerType
break
case "NotificationHistory":
widget.showUnreadBadge = BarWidgetRegistry.widgetMetadata[widget.id].showUnreadBadge
widget.hideWhenZero = BarWidgetRegistry.widgetMetadata[widget.id].hideWhenZero
break
case "SidePanelToggle":
widget.useDistroLogo = adapter.bar.useDistroLogo
break
case "SystemMonitor":
widget.showNetworkStats = adapter.bar.showNetworkStats
break
case "Volume":
widget.alwaysShowPercentage = BarWidgetRegistry.widgetMetadata[widget.id].alwaysShowPercentage
break
case "Workspace":
widget.labelMode = adapter.bar.showWorkspaceLabel
break
}
}
// -----------------------------------------------------
// Kickoff essential services
function kickOffServices() {
@ -117,14 +178,15 @@ Singleton {
}
// -----------------------------------------------------
Item {
Component.onCompleted: {
// Ensure directories exist before FileView tries to read files
Component.onCompleted: {
// ensure settings dir exists
Quickshell.execDetached(["mkdir", "-p", configDir])
Quickshell.execDetached(["mkdir", "-p", cacheDir])
Quickshell.execDetached(["mkdir", "-p", cacheDirImages])
// ensure settings dir exists
Quickshell.execDetached(["mkdir", "-p", configDir])
Quickshell.execDetached(["mkdir", "-p", cacheDir])
Quickshell.execDetached(["mkdir", "-p", cacheDirImages])
}
// Mark directories as created and trigger file loading
directoriesCreated = true
}
// Don't write settings to disk immediately
@ -138,12 +200,16 @@ Singleton {
FileView {
id: settingsFileView
path: settingsFile
path: directoriesCreated ? settingsFile : ""
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: saveTimer.start()
Component.onCompleted: function () {
reload()
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path === settingsFile) {
reload()
}
}
onLoaded: function () {
if (!isLoaded) {
@ -174,15 +240,16 @@ Singleton {
// bar
property JsonObject bar: JsonObject {
property string position: "top" // Possible values: "top", "bottom"
property bool showActiveWindowIcon: true
property bool alwaysShowBatteryPercentage: false
property bool showNetworkStats: false
property string position: "top" // "top" or "bottom"
property real backgroundOpacity: 1.0
property bool useDistroLogo: false
property string showWorkspaceLabel: "none"
property list<string> monitors: []
property bool showActiveWindowIcon: true // TODO: delete
property bool alwaysShowBatteryPercentage: false // TODO: delete
property bool showNetworkStats: false // TODO: delete
property bool useDistroLogo: false // TODO: delete
property string showWorkspaceLabel: "none" // TODO: delete
// Widget configuration for modular bar system
property JsonObject widgets
widgets: JsonObject {
@ -236,9 +303,10 @@ Singleton {
property JsonObject location: JsonObject {
property string name: defaultLocation
property bool useFahrenheit: false
property bool reverseDayMonth: false
property bool use12HourClock: false
property bool showDateWithClock: false
property bool reverseDayMonth: false // TODO: delete
property bool use12HourClock: false // TODO: delete
property bool showDateWithClock: false // TODO: delete
}
// screen recorder
@ -267,6 +335,7 @@ Singleton {
property int transitionDuration: 1500 // 1500 ms
property string transitionType: "random"
property real transitionEdgeSmoothness: 0.05
property string defaultWallpaper: root.defaultWallpaper
property list<var> monitors: []
}
@ -299,25 +368,27 @@ Singleton {
property JsonObject notifications: JsonObject {
property bool doNotDisturb: false
property list<string> monitors: []
// Last time the user opened the notification history (ms since epoch)
property real lastSeenTs: 0
}
// audio
property JsonObject audio: JsonObject {
property bool showMiniplayerAlbumArt: false
property bool showMiniplayerCava: false
property string visualizerType: "linear"
property int volumeStep: 5
property int cavaFrameRate: 60
// MPRIS controls
property string visualizerType: "linear"
property list<string> mprisBlacklist: []
property string preferredPlayer: ""
property bool showMiniplayerAlbumArt: false // TODO: delete
property bool showMiniplayerCava: false // TODO: delete
}
// ui
property JsonObject ui: JsonObject {
property string fontDefault: "Roboto" // Default font for all text
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
property string fontDefault: "Roboto"
property string fontFixed: "DejaVu Sans Mono"
property string fontBillboard: "Inter"
property list<var> monitorsScaling: []
property bool idleInhibitorEnabled: false
}

View file

@ -60,6 +60,7 @@ Singleton {
property int animationFast: Math.round(150 * Settings.data.general.animationSpeed)
property int animationNormal: Math.round(300 * Settings.data.general.animationSpeed)
property int animationSlow: Math.round(450 * Settings.data.general.animationSpeed)
property int animationSlowest: Math.round(750 * Settings.data.general.animationSpeed)
// Dimensions
property int barHeight: 36

View file

@ -9,52 +9,38 @@ Singleton {
id: root
property var date: new Date()
property string time: {
let timeFormat = Settings.data.location.use12HourClock ? "h:mm AP" : "HH:mm"
let timeString = Qt.formatDateTime(date, timeFormat)
if (Settings.data.location.showDateWithClock) {
let dayName = date.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
let day = date.getDate()
let month = date.toLocaleDateString(Qt.locale(), "MMM")
return timeString + " - " + (Settings.data.location.reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
}
return timeString
// Returns a Unix Timestamp (in seconds)
readonly property int timestamp: {
return Math.floor(date / 1000)
}
readonly property string dateString: {
function formatDate(reverseDayMonth = true) {
let now = date
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
let day = now.getDate()
let suffix
if (day > 3 && day < 21)
suffix = 'th'
suffix = 'th'
else
switch (day % 10) {
switch (day % 10) {
case 1:
suffix = "st"
break
suffix = "st"
break
case 2:
suffix = "nd"
break
suffix = "nd"
break
case 3:
suffix = "rd"
break
suffix = "rd"
break
default:
suffix = "th"
}
suffix = "th"
}
let month = now.toLocaleDateString(Qt.locale(), "MMMM")
let year = now.toLocaleDateString(Qt.locale(), "yyyy")
return `${dayName}, `
+ (Settings.data.location.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
}
// Returns a Unix Timestamp (in seconds)
readonly property int timestamp: {
return Math.floor(date / 1000)
return `${dayName}, ` + (reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
}

View file

@ -3,6 +3,8 @@ import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Modules.SettingsPanel
import qs.Widgets
Variants {
id: backgroundVariants
@ -20,6 +22,8 @@ Variants {
// Internal state management
property string transitionType: "fade"
property real transitionProgress: 0
// Scaling support for widgets that rely on it
property real scaling: ScalingService.getScreenScale(screen)
readonly property real edgeSmoothness: Settings.data.wallpaper.transitionEdgeSmoothness
readonly property var allTransitions: WallpaperService.allTransitions
@ -87,6 +91,15 @@ Variants {
left: true
}
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if ((screen !== null) && (screenName === screen.name)) {
scaling = scale
}
}
}
Timer {
id: debounceTimer
interval: 333

View file

@ -76,6 +76,7 @@ Variants {
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"barSection": parent.objectName,
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
@ -103,6 +104,7 @@ Variants {
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"barSection": parent.objectName,
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
@ -131,6 +133,7 @@ Variants {
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"barSection": parent.objectName,
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length

View file

@ -12,6 +12,27 @@ RowLayout {
id: root
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
readonly property real minWidth: 160
readonly property real maxWidth: 400
Layout.alignment: Qt.AlignVCenter
@ -74,7 +95,7 @@ RowLayout {
Layout.preferredWidth: Style.fontSizeL * scaling * 1.2
Layout.preferredHeight: Style.fontSizeL * scaling * 1.2
Layout.alignment: Qt.AlignVCenter
visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon
visible: getTitle() !== "" && showIcon
IconImage {
id: windowIcon

View file

@ -11,11 +11,42 @@ Item {
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: 0
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Track if we've already notified to avoid spam
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property bool alwaysShowPercentage: widgetSettings.alwaysShowPercentage
!== undefined ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
readonly property real warningThreshold: widgetSettings.warningThreshold
!== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold
// Test mode
readonly property bool testMode: false
readonly property int testPercent: 50
readonly property bool testCharging: true
// Main properties
readonly property var battery: UPower.displayDevice
readonly property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery
&& battery.isPresent)
readonly property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
readonly property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
property bool hasNotifiedLowBattery: false
implicitWidth: pill.width
@ -23,15 +54,14 @@ Item {
// Helper to evaluate and possibly notify
function maybeNotify(percent, charging) {
const p = Math.round(percent)
// Only notify exactly at 15%, not at 0% or any other percentage
if (!charging && p === 15 && !root.hasNotifiedLowBattery) {
// Only notify once we are a below threshold
if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) {
root.hasNotifiedLowBattery = true
// Maybe go with toast ?
Quickshell.execDetached(
["notify-send", "-u", "critical", "-i", "battery-caution", "Low Battery", `Battery is at ${p}%. Please connect charger.`])
root.hasNotifiedLowBattery = true
}
// Reset when charging starts or when battery recovers above 20%
if (charging || p > 20) {
} else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) {
// Reset when charging starts or when battery recovers 5% above threshold
root.hasNotifiedLowBattery = false
}
}
@ -40,19 +70,10 @@ Item {
Connections {
target: UPower.displayDevice
function onPercentageChanged() {
let battery = UPower.displayDevice
let isReady = battery && battery.ready && battery.isLaptopBattery && battery.isPresent
let percent = isReady ? (battery.percentage * 100) : 0
let charging = isReady ? battery.state === UPowerDeviceState.Charging : false
root.maybeNotify(percent, charging)
}
function onStateChanged() {
let battery = UPower.displayDevice
let isReady = battery && battery.ready && battery.isLaptopBattery && battery.isPresent
let charging = isReady ? battery.state === UPowerDeviceState.Charging : false
// Reset notification flag when charging starts
if (charging) {
root.hasNotifiedLowBattery = false
@ -63,76 +84,44 @@ Item {
NPill {
id: pill
// Test mode
property bool testMode: false
property int testPercent: 20
property bool testCharging: false
property var battery: UPower.displayDevice
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
// Choose icon based on charge and charging state
function batteryIcon() {
if (!isReady || !battery.isLaptopBattery)
return "battery_android_alert"
if (charging)
return "battery_android_bolt"
if (percent >= 95)
return "battery_android_full"
// Hardcoded battery symbols
if (percent >= 85)
return "battery_android_6"
if (percent >= 70)
return "battery_android_5"
if (percent >= 55)
return "battery_android_4"
if (percent >= 40)
return "battery_android_3"
if (percent >= 25)
return "battery_android_2"
if (percent >= 10)
return "battery_android_1"
if (percent >= 0)
return "battery_android_0"
}
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: batteryIcon()
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent,
charging, isReady)
iconRotation: -90
text: ((isReady && battery.isLaptopBattery) || testMode) ? Math.round(percent) + "%" : "-"
textColor: charging ? Color.mPrimary : Color.mOnSurface
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false
forceOpen: isReady && (testMode || battery.isLaptopBattery) && Settings.data.bar.alwaysShowBatteryPercentage
forceOpen: isReady && (testMode || battery.isLaptopBattery) && alwaysShowPercentage
disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery))
tooltipText: {
let lines = []
if (testMode) {
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345))
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(12345)}.`)
return lines.join("\n")
}
if (!isReady || !battery.isLaptopBattery) {
return "No battery detected"
return "No battery detected."
}
if (battery.timeToEmpty > 0) {
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty))
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(battery.timeToEmpty)}.`)
}
if (battery.timeToFull > 0) {
lines.push("Time until full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull))
lines.push(`Time until full: ${Time.formatVagueHumanReadableDuration(battery.timeToFull)}.`)
}
if (battery.changeRate !== undefined) {
const rate = battery.changeRate
if (rate > 0) {
lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed(
2) + " W")
lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W." : "Discharging rate: " + rate.toFixed(
2) + " W.")
} else if (rate < 0) {
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W")
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W.")
} else {
lines.push("Estimating...")
}
} else {
lines.push(charging ? "Charging" : "Discharging")
lines.push(charging ? "Charging." : "Discharging.")
}
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
lines.push("Health: " + Math.round(battery.healthPercentage) + "%")

View file

@ -10,10 +10,28 @@ Item {
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: 0
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool userAlwaysShowPercentage: (widgetSettings.alwaysShowPercentage
!== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
// Used to avoid opening the pill on Quickshell startup
property bool firstBrightnessReceived: false
@ -37,28 +55,26 @@ Item {
target: getMonitor()
ignoreUnknownSignals: true
function onBrightnessUpdated() {
Logger.log("Bar-Brightness", "OnBrightnessUpdated")
var monitor = getMonitor()
if (!monitor)
return
var currentBrightness = monitor.brightness
// Ignore if this is the first time or if brightness hasn't actually changed
// Ignore if this is the first time we receive an update.
// Most likely service just kicked off.
if (!firstBrightnessReceived) {
firstBrightnessReceived = true
monitor.lastBrightness = currentBrightness
return
}
// Only show pill if brightness actually changed (not just loaded from settings)
if (Math.abs(currentBrightness - monitor.lastBrightness) > 0.1) {
pill.show()
}
monitor.lastBrightness = currentBrightness
pill.show()
hideTimerAfterChange.restart()
}
}
Timer {
id: hideTimerAfterChange
interval: 2500
running: false
repeat: false
onTriggered: pill.hide()
}
NPill {
id: pill
@ -71,6 +87,7 @@ Item {
var monitor = getMonitor()
return monitor ? (Math.round(monitor.brightness * 100) + "%") : ""
}
forceOpen: userAlwaysShowPercentage
tooltipText: {
var monitor = getMonitor()
if (!monitor)

View file

@ -10,24 +10,70 @@ Rectangle {
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property bool showDate: widgetSettings.showDate !== undefined ? widgetSettings.showDate : widgetMetadata.showDate
readonly property bool use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock
readonly property bool showSeconds: widgetSettings.showSeconds !== undefined ? widgetSettings.showSeconds : widgetMetadata.showSeconds
readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth
!== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth
implicitWidth: clock.width + Style.marginM * 2 * scaling
implicitHeight: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
// Clock Icon with attached calendar
NClock {
NText {
id: clock
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
text: {
const now = Time.date
const timeFormat = use12h ? (showSeconds ? "h:mm:ss AP" : "h:mm AP") : (showSeconds ? "HH:mm:ss" : "HH:mm")
const timeString = Qt.formatDateTime(now, timeFormat)
NTooltip {
id: tooltip
text: `${Time.dateString}.`
target: clock
positionAbove: Settings.data.bar.position === "bottom"
if (showDate) {
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
let day = now.getDate()
let month = now.toLocaleDateString(Qt.locale(), "MMM")
return timeString + " - " + (reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
}
return timeString
}
anchors.centerIn: parent
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
}
NTooltip {
id: tooltip
text: `${Time.formatDate(reverseDayMonth)}.`
target: clock
positionAbove: Settings.data.bar.position === "bottom"
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
if (!PanelService.getPanel("calendarPanel")?.active) {
tooltip.show()

View file

@ -13,11 +13,13 @@ NIconButton {
property var screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Get user settings from Settings data
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
@ -30,30 +32,27 @@ NIconButton {
}
// Use settings or defaults from BarWidgetRegistry
readonly property string userIcon: widgetSettings.icon || BarWidgetRegistry.widgetMetadata["CustomButton"].icon
readonly property string userLeftClickExec: widgetSettings.leftClickExec
|| BarWidgetRegistry.widgetMetadata["CustomButton"].leftClickExec
readonly property string userRightClickExec: widgetSettings.rightClickExec
|| BarWidgetRegistry.widgetMetadata["CustomButton"].rightClickExec
readonly property string userMiddleClickExec: widgetSettings.middleClickExec
|| BarWidgetRegistry.widgetMetadata["CustomButton"].middleClickExec
readonly property bool hasExec: (userLeftClickExec || userRightClickExec || userMiddleClickExec)
readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon
readonly property string leftClickExec: widgetSettings.leftClickExec || widgetMetadata.leftClickExec
readonly property string rightClickExec: widgetSettings.rightClickExec || widgetMetadata.rightClickExec
readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec
readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec)
sizeRatio: 0.8
icon: userIcon
icon: customIcon
tooltipText: {
if (!hasExec) {
return "Custom Button - Configure in settings"
} else {
var lines = []
if (userLeftClickExec !== "") {
lines.push(`Left click: <i>${userLeftClickExec}</i>.`)
if (leftClickExec !== "") {
lines.push(`Left click: <i>${leftClickExec}</i>.`)
}
if (userRightClickExec !== "") {
lines.push(`Right click: <i>${userRightClickExec}</i>.`)
if (rightClickExec !== "") {
lines.push(`Right click: <i>${rightClickExec}</i>.`)
}
if (userMiddleClickExec !== "") {
lines.push(`Middle click: <i>${userMiddleClickExec}</i>.`)
if (middleClickExec !== "") {
lines.push(`Middle click: <i>${middleClickExec}</i>.`)
}
return lines.join("<br/>")
}
@ -61,9 +60,9 @@ NIconButton {
opacity: hasExec ? Style.opacityFull : Style.opacityMedium
onClicked: {
if (userLeftClickExec) {
Quickshell.execDetached(["sh", "-c", userLeftClickExec])
Logger.log("CustomButton", `Executing command: ${userLeftClickExec}`)
if (leftClickExec) {
Quickshell.execDetached(["sh", "-c", leftClickExec])
Logger.log("CustomButton", `Executing command: ${leftClickExec}`)
} else if (!hasExec) {
// No script was defined, open settings
var settingsPanel = PanelService.getPanel("settingsPanel")
@ -73,16 +72,16 @@ NIconButton {
}
onRightClicked: {
if (userRightClickExec) {
Quickshell.execDetached(["sh", "-c", userRightClickExec])
Logger.log("CustomButton", `Executing command: ${userRightClickExec}`)
if (rightClickExec) {
Quickshell.execDetached(["sh", "-c", rightClickExec])
Logger.log("CustomButton", `Executing command: ${rightClickExec}`)
}
}
onMiddleClicked: {
if (userMiddleClickExec) {
Quickshell.execDetached(["sh", "-c", userMiddleClickExec])
Logger.log("CustomButton", `Executing command: ${userMiddleClickExec}`)
if (middleClickExec) {
Quickshell.execDetached(["sh", "-c", middleClickExec])
Logger.log("CustomButton", `Executing command: ${middleClickExec}`)
}
}
}

View file

@ -12,9 +12,6 @@ Item {
property ShellScreen screen
property real scaling: 1.0
property string barSection: ""
property int sectionWidgetIndex: 0
property int sectionWidgetsCount: 0
// Use the shared service for keyboard layout
property string currentLayout: KeyboardLayoutService.currentLayout

View file

@ -12,18 +12,44 @@ RowLayout {
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool showAlbumArt: (widgetSettings.showAlbumArt
!== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt
readonly property bool showVisualizer: (widgetSettings.showVisualizer
!== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType
!== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType
readonly property real minWidth: 160
readonly property real maxWidth: 400
function getTitle() {
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
}
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
visible: MediaService.currentPlayer !== null && MediaService.canPlay
Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
function getTitle() {
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
}
// A hidden text element to safely measure the full title width
NText {
id: fullTitleMetrics
@ -58,8 +84,7 @@ RowLayout {
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear"
&& MediaService.isPlaying
active: showVisualizer && visualizerType == "linear" && MediaService.isPlaying
z: 0
sourceComponent: LinearSpectrum {
@ -74,8 +99,7 @@ RowLayout {
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored"
&& MediaService.isPlaying
active: showVisualizer && visualizerType == "mirrored" && MediaService.isPlaying
z: 0
sourceComponent: MirroredSpectrum {
@ -90,8 +114,7 @@ RowLayout {
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave"
&& MediaService.isPlaying
active: showVisualizer && visualizerType == "wave" && MediaService.isPlaying
z: 0
sourceComponent: WaveSpectrum {
@ -115,12 +138,12 @@ RowLayout {
font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter
visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible
visible: !showAlbumArt && getTitle() !== "" && !trackArt.visible
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
visible: Settings.data.audio.showMiniplayerAlbumArt
visible: showAlbumArt
spacing: 0
Item {

View file

@ -12,10 +12,28 @@ Item {
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: 0
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool alwaysShowPercentage: (widgetSettings.alwaysShowPercentage
!== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
// Used to avoid opening the pill on Quickshell startup
property bool firstInputVolumeReceived: false
property int wheelAccumulator: 0
@ -78,6 +96,7 @@ Item {
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.inputVolume * 100) + "%"
forceOpen: alwaysShowPercentage
tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100)
+ "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."

View file

@ -4,6 +4,7 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Services
import qs.Widgets
@ -14,7 +15,6 @@ NIconButton {
property real scaling: 1.0
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
@ -26,7 +26,7 @@ NIconButton {
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.requestedTab = SettingsPanel.Tab.Brightness
settingsPanel.open(screen)
}
}

View file

@ -13,6 +13,45 @@ NIconButton {
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[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
function lastSeenTs() {
return Settings.data.notifications?.lastSeenTs || 0
}
function computeUnreadCount() {
var since = 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
}
sizeRatio: 0.8
icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications"
tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'."
@ -21,7 +60,40 @@ NIconButton {
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this)
onClicked: {
var panel = PanelService.getPanel("notificationHistoryPanel")
panel?.toggle(screen, this)
Settings.data.notifications.lastSeenTs = Time.timestamp * 1000
}
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
Loader {
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -4 * scaling
anchors.topMargin: -4 * scaling
z: 2
active: showUnreadBadge && (!hideWhenZero || computeUnreadCount() > 0)
sourceComponent: Rectangle {
id: badge
readonly property int count: computeUnreadCount()
readonly property string label: count <= 99 ? String(count) : "99+"
readonly property real pad: 8 * scaling
height: 16 * scaling
width: Math.max(height, textNode.implicitWidth + pad)
radius: height / 2
color: Color.mError
border.color: Color.mSurface
border.width: 1
visible: count > 0 || !hideWhenZero
NText {
id: textNode
anchors.centerIn: parent
text: badge.label
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnError
}
}
}
}

View file

@ -1,3 +1,4 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import QtQuick.Effects
@ -11,7 +12,28 @@ NIconButton {
property ShellScreen screen
property real scaling: 1.0
icon: Settings.data.bar.useDistroLogo ? "" : "widgets"
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool useDistroLogo: (widgetSettings.useDistroLogo
!== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo
icon: useDistroLogo ? "" : "widgets"
tooltipText: "Open side panel."
sizeRatio: 0.8
@ -24,14 +46,13 @@ NIconButton {
onClicked: PanelService.getPanel("sidePanel")?.toggle(screen, this)
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle(screen)
// When enabled, draw the distro logo instead of the icon glyph
IconImage {
id: logo
anchors.centerIn: parent
width: root.width * 0.6
height: width
source: Settings.data.bar.useDistroLogo ? DistroLogoService.osLogo : ""
visible: false //Settings.data.bar.useDistroLogo && source !== ""
source: useDistroLogo ? DistroLogoService.osLogo : ""
visible: useDistroLogo && source !== ""
smooth: true
}

View file

@ -12,11 +12,13 @@ Item {
property var screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Get user settings from Settings data - make it reactive
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
@ -29,19 +31,10 @@ Item {
}
// Use settings or defaults from BarWidgetRegistry
readonly property int userWidth: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex].width || BarWidgetRegistry.widgetMetadata["Spacer"].width
}
}
return BarWidgetRegistry.widgetMetadata["Spacer"].width
}
readonly property int spacerWidth: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width
// Set the width based on user settings
implicitWidth: userWidth * scaling
implicitWidth: spacerWidth * scaling
implicitHeight: Style.barHeight * scaling
width: implicitWidth
height: implicitHeight
@ -51,6 +44,6 @@ Item {
anchors.fill: parent
color: Qt.rgba(1, 0, 0, 0.1) // Very subtle red tint
visible: Settings.data.general.debugMode || false
radius: 2 * scaling
radius: Style.radiusXXS * scaling
}
}

View file

@ -11,6 +11,34 @@ RowLayout {
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool showCpuUsage: (widgetSettings.showCpuUsage
!== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage
readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp
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 showNetworkStats: (widgetSettings.showNetworkStats
!== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
@ -34,6 +62,7 @@ RowLayout {
id: cpuUsageLayout
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
visible: showCpuUsage
NIcon {
id: cpuUsageIcon
@ -59,6 +88,7 @@ RowLayout {
// spacing is thin here to compensate for the vertical thermometer icon
spacing: Style.marginXXS * scaling
Layout.alignment: Qt.AlignVCenter
visible: showCpuTemp
NIcon {
text: "thermometer"
@ -81,6 +111,7 @@ RowLayout {
id: memoryUsageLayout
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
visible: showMemoryUsage
NIcon {
text: "memory"
@ -88,7 +119,7 @@ RowLayout {
}
NText {
text: `${SystemStatService.memoryUsageGb}G`
text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${SystemStatService.memGb}G`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
@ -103,7 +134,7 @@ RowLayout {
id: networkDownloadLayout
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
visible: Settings.data.bar.showNetworkStats
visible: showNetworkStats
NIcon {
text: "download"
@ -126,7 +157,7 @@ RowLayout {
id: networkUploadLayout
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
visible: Settings.data.bar.showNetworkStats
visible: showNetworkStats
NIcon {
text: "upload"

View file

@ -15,6 +15,7 @@ Rectangle {
property ShellScreen screen
property real scaling: 1.0
readonly property real itemSize: 24 * scaling
function onLoaded() {

View file

@ -12,10 +12,28 @@ Item {
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: 0
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool alwaysShowPercentage: (widgetSettings.alwaysShowPercentage
!== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
// Used to avoid opening the pill on Quickshell startup
property bool firstVolumeReceived: false
property int wheelAccumulator: 0
@ -63,6 +81,7 @@ Item {
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.volume * 100) + "%"
forceOpen: alwaysShowPercentage
tooltipText: "Volume: " + Math.round(AudioService.volume * 100)
+ "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."

View file

@ -40,6 +40,6 @@ NIconButton {
return "signal_wifi_bad"
}
}
tooltipText: "Network / Wi-Fi."
tooltipText: "Manage Wi-Fi."
onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this)
}

View file

@ -14,6 +14,26 @@ Item {
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
property bool isDestroying: false
property bool hovered: false
@ -22,8 +42,8 @@ Item {
property bool effectsActive: false
property color effectColor: Color.mPrimary
property int horizontalPadding: Math.round(16 * scaling)
property int spacingBetweenPills: Math.round(8 * scaling)
property int horizontalPadding: Math.round(Style.marginS * scaling)
property int spacingBetweenPills: Math.round(Style.marginXS * scaling)
signal workspaceChanged(int workspaceId, color accentColor)
@ -124,7 +144,7 @@ Item {
Rectangle {
id: workspaceBackground
width: parent.width - Style.marginS * scaling * 2
width: parent.width
height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
@ -145,7 +165,7 @@ Item {
model: localWorkspaces
Item {
id: workspacePillContainer
height: (Settings.data.bar.showWorkspaceLabel !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling)
height: (labelMode !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling)
width: root.calculatedWsWidth(model)
Rectangle {
@ -153,15 +173,13 @@ Item {
anchors.fill: parent
Loader {
active: (Settings.data.bar.showWorkspaceLabel !== "none")
active: (labelMode !== "none")
sourceComponent: Component {
Text {
// Center horizontally
x: (pill.width - width) / 2
// Center vertically accounting for font metrics
y: (pill.height - height) / 2 + (height - contentHeight) / 2
text: {
if (Settings.data.bar.showWorkspaceLabel === "name" && model.name && model.name.length > 0) {
if (labelMode === "name" && model.name && model.name.length > 0) {
return model.name.substring(0, 2)
} else {
return model.idx.toString()

View file

@ -34,24 +34,28 @@ Variants {
WlrLayershell.namespace: "noctalia-dock"
property bool autoHide: Settings.data.dock.autoHide
property bool hidden: autoHide
property int hideDelay: 500
property int showDelay: 100
property int hideAnimationDuration: Style.animationFast
property int showAnimationDuration: Style.animationFast
property int peekHeight: 7 * scaling
property int fullHeight: dockContainer.height
property int iconSize: 36
readonly property bool autoHide: Settings.data.dock.autoHide
readonly property int hideDelay: 500
readonly property int showDelay: 100
readonly property int hideAnimationDuration: Style.animationFast
readonly property int showAnimationDuration: Style.animationFast
readonly property int peekHeight: 7 * scaling
readonly property int fullHeight: dockContainer.height
readonly property int iconSize: 36 * scaling
readonly property int floatingMargin: 12 * scaling // Margin to make dock float
// Bar positioning properties
property bool barAtBottom: Settings.data.bar.position === "bottom"
property int barHeight: barAtBottom ? (Settings.data.bar.height || 30) * scaling : 0
property int dockSpacing: 4 * scaling // Space between dock and bar
// Bar detection and positioning properties
readonly property bool hasBar: modelData.name ? (Settings.data.bar.monitors.includes(modelData.name)
|| (Settings.data.bar.monitors.length === 0)) : false
readonly property bool barAtBottom: hasBar && Settings.data.bar.position === "bottom"
readonly property bool barAtTop: hasBar && Settings.data.bar.position === "top"
readonly property int barHeight: (barAtBottom || barAtTop) ? (Settings.data.bar.height || 30) * scaling : 0
readonly property int dockSpacing: 8 * scaling // Space between dock and bar/edge
// Track hover state
property bool dockHovered: false
property bool anyAppHovered: false
property bool hidden: autoHide
// Dock is positioned at the bottom
anchors.bottom: true
@ -63,11 +67,11 @@ Variants {
// Make the window transparent
color: Color.transparent
// Set the window size - always include space for peek area when auto-hide is enabled
implicitWidth: dockContainer.width
implicitHeight: fullHeight + (barAtBottom ? barHeight + dockSpacing : 0)
// Set the window size - include extra height only if bar is at bottom
implicitWidth: dockContainer.width + (floatingMargin * 2)
implicitHeight: fullHeight + floatingMargin + (barAtBottom ? barHeight + dockSpacing : 0)
// Position the entire window above the bar when bar is at bottom
// Position the entire window above the bar only when bar is at bottom
margins.bottom: barAtBottom ? barHeight : 0
// Watch for autoHide setting changes
@ -111,7 +115,7 @@ Variants {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: peekHeight + dockSpacing
height: peekHeight + floatingMargin + (barAtBottom ? dockSpacing : 0)
hoverEnabled: autoHide
visible: autoHide
@ -130,24 +134,32 @@ Variants {
Rectangle {
id: dockContainer
width: dockLayout.implicitWidth + 48 * scaling
height: iconSize * 1.4 * scaling
width: dockLayout.implicitWidth + Style.marginL * scaling * 2
height: Math.round(iconSize * 1.6)
color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: dockSpacing
topLeftRadius: Style.radiusL * scaling
topRightRadius: Style.radiusL * scaling
anchors.bottomMargin: floatingMargin + (barAtBottom ? dockSpacing : 0)
radius: Style.radiusL * scaling
border.width: Math.max(1, Style.borderS * scaling)
border.color: Color.mOutline
// Animate the dock sliding up and down
transform: Translate {
y: hidden ? (fullHeight - peekHeight) : 0
// Fade and zoom animation properties
opacity: hidden ? 0 : 1
scale: hidden ? 0.85 : 1
Behavior on y {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: Easing.InOutQuad
}
Behavior on opacity {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: Easing.InOutQuad
}
}
Behavior on scale {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: hidden ? Easing.InQuad : Easing.OutBack
easing.overshoot: hidden ? 0 : 1.05
}
}
@ -179,15 +191,9 @@ Variants {
Item {
id: dock
width: dockLayout.implicitWidth
height: parent.height - (20 * scaling)
height: parent.height - (Style.marginM * 2 * scaling)
anchors.centerIn: parent
NTooltip {
id: appTooltip
visible: false
positionAbove: true
}
function getAppIcon(toplevel: Toplevel): string {
if (!toplevel)
return ""
@ -203,39 +209,48 @@ Variants {
Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null
delegate: Rectangle {
delegate: Item {
id: appButton
Layout.preferredWidth: iconSize * scaling
Layout.preferredHeight: iconSize * scaling
Layout.preferredWidth: iconSize
Layout.preferredHeight: iconSize
Layout.alignment: Qt.AlignCenter
color: Color.transparent
radius: Style.radiusM * scaling
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
property bool hovered: appMouseArea.containsMouse
property string appId: modelData ? modelData.appId : ""
property string appTitle: modelData ? modelData.title : ""
// The icon
// Individual tooltip for this app
NTooltip {
id: appTooltip
target: appButton
positionAbove: true
visible: false
}
// The icon with better quality settings
Image {
id: appIcon
width: iconSize * scaling
height: iconSize * scaling
width: iconSize
height: iconSize
anchors.centerIn: parent
source: dock.getAppIcon(modelData)
visible: source.toString() !== ""
sourceSize.width: iconSize * 2
sourceSize.height: iconSize * 2
smooth: true
mipmap: false
antialiasing: false
mipmap: true
antialiasing: true
fillMode: Image.PreserveAspectFit
cache: true
scale: appButton.hovered ? 1.1 : 1.0
scale: appButton.hovered ? 1.15 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
duration: Style.animationNormal
easing.type: Easing.OutBack
easing.overshoot: 1.2
}
}
}
@ -246,15 +261,15 @@ Variants {
visible: !appIcon.visible
text: "question_mark"
font.family: "Material Symbols Rounded"
font.pointSize: iconSize * 0.7 * scaling
font.pointSize: iconSize * 0.7
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
scale: appButton.hovered ? 1.1 : 1.0
scale: appButton.hovered ? 1.15 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutBack
easing.overshoot: 1.2
}
}
}
@ -270,7 +285,6 @@ Variants {
anyAppHovered = true
const appName = appButton.appTitle || appButton.appId || "Unknown"
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
appTooltip.target = appButton
appTooltip.isVisible = true
if (autoHide) {
showTimer.stop()
@ -300,15 +314,32 @@ Variants {
}
}
// Active indicator
Rectangle {
visible: isActive
width: iconSize * 0.25
height: 4 * scaling
width: iconSize * 0.2
height: iconSize * 0.1
color: Color.mPrimary
radius: Style.radiusXS
radius: Style.radiusXS * scaling
anchors.top: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Style.marginXXS * scaling
anchors.topMargin: Style.marginXXS * 1.5 * scaling
// Pulse animation for active indicator
SequentialAnimation on opacity {
running: isActive
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: Style.animationSlowest
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: Style.animationSlowest
easing.type: Easing.InOutQuad
}
}
}
}
}

View file

@ -58,29 +58,6 @@ Loader {
property real percent: isReady ? (battery.percentage * 100) : 0
property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false
property bool batteryVisible: isReady && percent > 0
function getIcon() {
if (!batteryVisible)
return ""
if (charging)
return "battery_android_bolt"
if (percent >= 95)
return "battery_android_full"
if (percent >= 85)
return "battery_android_6"
if (percent >= 70)
return "battery_android_5"
if (percent >= 55)
return "battery_android_4"
if (percent >= 40)
return "battery_android_3"
if (percent >= 25)
return "battery_android_2"
if (percent >= 10)
return "battery_android_1"
if (percent >= 0)
return "battery_android_0"
}
}
Item {
@ -420,7 +397,7 @@ Loader {
anchors.bottomMargin: Style.marginM * scaling
anchors.leftMargin: Style.marginL * scaling
anchors.rightMargin: Style.marginL * scaling
spacing: Style.marginM * scaling
spacing: Style.marginL * scaling
NText {
text: "SECURE TERMINAL"
@ -431,23 +408,6 @@ Loader {
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginS * scaling
visible: batteryIndicator.batteryVisible
NIcon {
text: batteryIndicator.getIcon()
font.pointSize: Style.fontSizeM * scaling
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
}
NText {
text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
}
RowLayout {
spacing: Style.marginS * scaling
NText {
@ -463,6 +423,25 @@ Loader {
color: Color.mOnSurface
}
}
RowLayout {
spacing: Style.marginS * scaling
visible: batteryIndicator.batteryVisible
NIcon {
text: BatteryService.getIcon(batteryIndicator.percent, batteryIndicator.charging,
batteryIndicator.isReady)
font.pointSize: Style.fontSizeM * scaling
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
rotation: -90
}
NText {
text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
}
}
}

View file

@ -39,7 +39,7 @@ NBox {
const totalSum = JSON.stringify(widget).split('').reduce((acc, character) => {
return acc + character.charCodeAt(0)
}, 0)
switch (totalSum % 10) {
switch (totalSum % 5) {
case 0:
return Color.mPrimary
case 1:
@ -50,16 +50,6 @@ NBox {
return Color.mError
case 4:
return Color.mOnSurface
case 5:
return Qt.darker(Color.mPrimary, 1.3)
case 6:
return Qt.darker(Color.mSecondary, 1.3)
case 7:
return Qt.darker(Color.mTertiary, 1.3)
case 8:
return Qt.darker(Color.mError, 1.3)
case 9:
return Qt.darker(Color.mOnSurface, 1.3)
}
}
@ -75,7 +65,7 @@ NBox {
text: sectionName + " Section"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
color: Color.mOnSurface
Layout.alignment: Qt.AlignVCenter
}
@ -89,7 +79,7 @@ NBox {
description: ""
placeholder: "Select a widget to add..."
onSelected: key => comboBox.currentKey = key
popupHeight: 240 * scaling
popupHeight: 340 * scaling
Layout.alignment: Qt.AlignVCenter
}
@ -188,13 +178,33 @@ NBox {
colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight)
colorFgHover: Color.mOnPrimary
onClicked: {
var dialog = Qt.createComponent("BarWidgetSettingsDialog.qml").createObject(root, {
"widgetIndex": index,
"widgetData": modelData,
"widgetId": modelData.id,
"parent": Overlay.overlay
})
dialog.open()
var component = Qt.createComponent(Qt.resolvedUrl("BarWidgetSettingsDialog.qml"))
function instantiateAndOpen() {
var dialog = component.createObject(root, {
"widgetIndex": index,
"widgetData": modelData,
"widgetId": modelData.id,
"parent": Overlay.overlay
})
if (dialog) {
dialog.open()
} else {
Logger.error("BarSectionEditor", "Failed to create settings dialog instance")
}
}
if (component.status === Component.Ready) {
instantiateAndOpen()
} else if (component.status === Component.Error) {
Logger.error("BarSectionEditor", component.errorString())
} else {
component.statusChanged.connect(function () {
if (component.status === Component.Ready) {
instantiateAndOpen()
} else if (component.status === Component.Error) {
Logger.error("BarSectionEditor", component.errorString())
}
})
}
}
}
}
@ -221,7 +231,7 @@ NBox {
MouseArea {
id: flowDragArea
anchors.fill: parent
z: 999 // Above all widgets to ensure it gets events first
z: -1 // Ensure this mouse area is below the Settings and Close buttons
// Critical properties for proper event handling
acceptedButtons: Qt.LeftButton

View file

@ -0,0 +1,134 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
import "./WidgetSettings" as WidgetSettings
// Widget Settings Dialog Component
Popup {
id: settingsPopup
property int widgetIndex: -1
property var widgetData: null
property string widgetId: ""
// Center popup in parent
x: (parent.width - width) * 0.5
y: (parent.height - height) * 0.5
width: 420 * scaling
height: content.implicitHeight + padding * 2
padding: Style.marginXL * scaling
modal: true
background: Rectangle {
id: bgRect
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Style.borderM * scaling
}
// Load settings when popup opens with data
onOpened: {
if (widgetData && widgetId) {
loadWidgetSettings()
}
}
function loadWidgetSettings() {
const widgetSettingsMap = {
"ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml",
"Battery": "WidgetSettings/BatterySettings.qml",
"Brightness": "WidgetSettings/BrightnessSettings.qml",
"Clock": "WidgetSettings/ClockSettings.qml",
"CustomButton": "WidgetSettings/CustomButtonSettings.qml",
"MediaMini": "WidgetSettings/MediaMiniSettings.qml",
"Microphone": "WidgetSettings/MicrophoneSettings.qml",
"NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml",
"Workspace": "WidgetSettings/WorkspaceSettings.qml",
"SidePanelToggle": "WidgetSettings/SidePanelToggleSettings.qml",
"Spacer": "WidgetSettings/SpacerSettings.qml",
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
"Volume": "WidgetSettings/VolumeSettings.qml"
}
const source = widgetSettingsMap[widgetId]
if (source) {
// Use setSource to pass properties at creation time
settingsLoader.setSource(source, {
"widgetData": widgetData,
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
})
}
}
ColumnLayout {
id: content
width: parent.width
spacing: Style.marginM * scaling
// Title
RowLayout {
Layout.fillWidth: true
NText {
text: `${settingsPopup.widgetId} Settings`
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: settingsPopup.close()
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
// Settings based on widget type
// Will be triggered via settingsLoader.setSource()
Loader {
id: settingsLoader
Layout.fillWidth: true
}
// Action buttons
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
Item {
Layout.fillWidth: true
}
NButton {
text: "Cancel"
outlined: true
onClicked: settingsPopup.close()
}
NButton {
text: "Apply"
icon: "check"
onClicked: {
if (settingsLoader.item && settingsLoader.item.saveSettings) {
var newSettings = settingsLoader.item.saveSettings()
root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings)
settingsPopup.close()
}
}
}
}
}
}

View file

@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showIcon = valueShowIcon
return settings
}
NToggle {
id: showIcon
Layout.fillWidth: true
label: "Show app icon"
checked: root.valueShowIcon
onToggled: checked => root.valueShowIcon = checked
}
}

View file

@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage
!== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage
return settings
}
NToggle {
label: "Always show percentage"
checked: root.valueAlwaysShowPercentage
onToggled: checked => root.valueAlwaysShowPercentage = checked
}
}

View file

@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage
!== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage
return settings
}
NToggle {
label: "Always show percentage"
checked: valueAlwaysShowPercentage
onToggled: checked => valueAlwaysShowPercentage = checked
}
}

View file

@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueShowDate: widgetData.showDate !== undefined ? widgetData.showDate : widgetMetadata.showDate
property bool valueUse12h: widgetData.use12HourClock !== undefined ? widgetData.use12HourClock : widgetMetadata.use12HourClock
property bool valueShowSeconds: widgetData.showSeconds !== undefined ? widgetData.showSeconds : widgetMetadata.showSeconds
property bool valueReverseDayMonth: widgetData.reverseDayMonth !== undefined ? widgetData.reverseDayMonth : widgetMetadata.reverseDayMonth
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showDate = valueShowDate
settings.use12HourClock = valueUse12h
settings.showSeconds = valueShowSeconds
settings.reverseDayMonth = valueReverseDayMonth
return settings
}
NToggle {
label: "Show date"
checked: valueShowDate
onToggled: checked => valueShowDate = checked
}
NToggle {
label: "Use 12-hour clock"
checked: valueUse12h
onToggled: checked => valueUse12h = checked
}
NToggle {
label: "Show seconds"
checked: valueShowSeconds
onToggled: checked => valueShowSeconds = checked
}
NToggle {
label: "Reverse day and month"
checked: valueReverseDayMonth
onToggled: checked => valueReverseDayMonth = checked
}
}

View file

@ -0,0 +1,58 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.icon = iconInput.text
settings.leftClickExec = leftClickExecInput.text
settings.rightClickExec = rightClickExecInput.text
settings.middleClickExec = middleClickExecInput.text
return settings
}
// Icon setting
NTextInput {
id: iconInput
Layout.fillWidth: true
label: "Icon Name"
description: "Choose a name from the Material Icon set."
placeholderText: "Enter icon name (e.g., favorite, home, settings)"
text: widgetData?.icon || widgetMetadata.icon
}
NTextInput {
id: leftClickExecInput
Layout.fillWidth: true
label: "Left Click Command"
placeholderText: "Enter command to execute (app or custom script)"
text: widgetData?.leftClickExec || widgetMetadata.leftClickExec
}
NTextInput {
id: rightClickExecInput
Layout.fillWidth: true
label: "Right Click Command"
placeholderText: "Enter command to execute (app or custom script)"
text: widgetData?.rightClickExec || widgetMetadata.rightClickExec
}
NTextInput {
id: middleClickExecInput
Layout.fillWidth: true
label: "Middle Click Command"
placeholderText: "Enter command to execute (app or custom script)"
text: widgetData.middleClickExec || widgetMetadata.middleClickExec
}
}

View file

@ -0,0 +1,62 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt
property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer
property string valueVisualizerType: widgetData.visualizerType || widgetMetadata.visualizerType
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showAlbumArt = valueShowAlbumArt
settings.showVisualizer = valueShowVisualizer
settings.visualizerType = valueVisualizerType
return settings
}
NToggle {
label: "Show album art"
checked: valueShowAlbumArt
onToggled: checked => valueShowAlbumArt = checked
}
NToggle {
label: "Show visualizer"
checked: valueShowVisualizer
onToggled: checked => valueShowVisualizer = checked
}
NComboBox {
visible: valueShowVisualizer
label: "Visualizer type"
model: ListModel {
ListElement {
key: "linear"
name: "Linear"
}
ListElement {
key: "mirrored"
name: "Mirrored"
}
ListElement {
key: "wave"
name: "Wave"
}
}
currentKey: valueVisualizerType
onSelected: key => valueVisualizerType = key
minimumWidth: 200 * scaling
}
}

View file

@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage
!== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage
return settings
}
NToggle {
label: "Always show percentage"
checked: valueAlwaysShowPercentage
onToggled: checked => valueAlwaysShowPercentage = checked
}
}

View file

@ -0,0 +1,38 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueShowUnreadBadge: widgetData.showUnreadBadge !== undefined ? widgetData.showUnreadBadge : widgetMetadata.showUnreadBadge
property bool valueHideWhenZero: widgetData.hideWhenZero !== undefined ? widgetData.hideWhenZero : widgetMetadata.hideWhenZero
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showUnreadBadge = valueShowUnreadBadge
settings.hideWhenZero = valueHideWhenZero
return settings
}
NToggle {
label: "Show unread badge"
checked: valueShowUnreadBadge
onToggled: checked => valueShowUnreadBadge = checked
}
NToggle {
label: "Hide badge when zero"
checked: valueHideWhenZero
onToggled: checked => valueHideWhenZero = checked
}
}

View file

@ -0,0 +1,30 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.useDistroLogo = valueUseDistroLogo
return settings
}
NToggle {
label: "Use distro logo instead of icon"
checked: valueUseDistroLogo
onToggled: checked => valueUseDistroLogo = checked
}
}

View file

@ -0,0 +1,30 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.width = parseInt(widthInput.text) || widgetMetadata.width
return settings
}
NTextInput {
id: widthInput
Layout.fillWidth: true
label: "Width"
description: "Spacing width in pixels"
text: widgetData.width || widgetMetadata.width
placeholderText: "Enter width in pixels"
}
}

View file

@ -0,0 +1,74 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local, editable state for checkboxes
property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage
property bool valueShowCpuTemp: widgetData.showCpuTemp !== undefined ? widgetData.showCpuTemp : widgetMetadata.showCpuTemp
property bool valueShowMemoryUsage: widgetData.showMemoryUsage !== undefined ? widgetData.showMemoryUsage : widgetMetadata.showMemoryUsage
property bool valueShowMemoryAsPercent: widgetData.showMemoryAsPercent
!== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
property bool valueShowNetworkStats: widgetData.showNetworkStats
!== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showCpuUsage = valueShowCpuUsage
settings.showCpuTemp = valueShowCpuTemp
settings.showMemoryUsage = valueShowMemoryUsage
settings.showMemoryAsPercent = valueShowMemoryAsPercent
settings.showNetworkStats = valueShowNetworkStats
return settings
}
NToggle {
id: showCpuUsage
Layout.fillWidth: true
label: "CPU usage"
checked: valueShowCpuUsage
onToggled: checked => valueShowCpuUsage = checked
}
NToggle {
id: showCpuTemp
Layout.fillWidth: true
label: "CPU temperature"
checked: valueShowCpuTemp
onToggled: checked => valueShowCpuTemp = checked
}
NToggle {
id: showMemoryUsage
Layout.fillWidth: true
label: "Memory usage"
checked: valueShowMemoryUsage
onToggled: checked => valueShowMemoryUsage = checked
}
NToggle {
id: showMemoryAsPercent
Layout.fillWidth: true
label: "Show memory as percentage"
checked: valueShowMemoryAsPercent
onToggled: checked => valueShowMemoryAsPercent = checked
}
NToggle {
id: showNetworkStats
Layout.fillWidth: true
label: "Network traffic"
checked: valueShowNetworkStats
onToggled: checked => valueShowNetworkStats = checked
}
}

View file

@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage
!== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage
return settings
}
NToggle {
label: "Always show percentage"
checked: valueAlwaysShowPercentage
onToggled: checked => valueAlwaysShowPercentage = checked
}
}

View file

@ -0,0 +1,44 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.labelMode = labelModeCombo.currentKey
return settings
}
NComboBox {
id: labelModeCombo
label: "Label Mode"
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "index"
name: "Index"
}
ListElement {
key: "name"
name: "Name"
}
}
currentKey: widgetData.labelMode || widgetMetadata.labelMode
onSelected: key => labelModeCombo.currentKey = key
minimumWidth: 200 * scaling
}
}

View file

@ -1,186 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
// Widget Settings Dialog Component
Popup {
id: settingsPopup
property int widgetIndex: -1
property var widgetData: null
property string widgetId: ""
// Center popup in parent
x: (parent.width - width) * 0.5
y: (parent.height - height) * 0.5
width: 420 * scaling
height: content.implicitHeight + padding * 2
padding: Style.marginXL * scaling
modal: true
background: Rectangle {
id: bgRect
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Style.borderM * scaling
}
ColumnLayout {
id: content
width: parent.width
spacing: Style.marginM * scaling
// Title
RowLayout {
Layout.fillWidth: true
NText {
text: "Widget Settings: " + settingsPopup.widgetId
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: settingsPopup.close()
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
// Settings based on widget type
Loader {
id: settingsLoader
Layout.fillWidth: true
sourceComponent: {
if (settingsPopup.widgetId === "CustomButton") {
return customButtonSettings
} else if (settingsPopup.widgetId === "Spacer") {
return spacerSettings
}
// Add more widget settings components here as needed
return null
}
}
// Action buttons
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
Item {
Layout.fillWidth: true
}
NButton {
text: "Cancel"
outlined: true
onClicked: settingsPopup.close()
}
NButton {
text: "Save"
onClicked: {
if (settingsLoader.item && settingsLoader.item.saveSettings) {
var newSettings = settingsLoader.item.saveSettings()
root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings)
settingsPopup.close()
}
}
}
}
}
// CustomButton settings component
Component {
id: customButtonSettings
ColumnLayout {
spacing: Style.marginM * scaling
function saveSettings() {
var settings = Object.assign({}, settingsPopup.widgetData)
settings.icon = iconInput.text
settings.leftClickExec = leftClickExecInput.text
settings.rightClickExec = rightClickExecInput.text
settings.middleClickExec = middleClickExecInput.text
return settings
}
// Icon setting
NTextInput {
id: iconInput
Layout.fillWidth: true
Layout.bottomMargin: Style.marginXL * scaling
label: "Icon Name"
description: "Use Material Icon names from the icon set."
text: settingsPopup.widgetData.icon || ""
placeholderText: "Enter icon name (e.g., favorite, home, settings)"
}
NTextInput {
id: leftClickExecInput
Layout.fillWidth: true
label: "Left Click Command"
description: "Command or application to run when left clicked."
text: settingsPopup.widgetData.leftClickExec || ""
placeholderText: "Enter command to execute (app or custom script)"
}
NTextInput {
id: rightClickExecInput
Layout.fillWidth: true
label: "Right Click Command"
description: "Command or application to run when right clicked."
text: settingsPopup.widgetData.rightClickExec || ""
placeholderText: "Enter command to execute (app or custom script)"
}
NTextInput {
id: middleClickExecInput
Layout.fillWidth: true
label: "Middle Click Command"
description: "Command or application to run when middle clicked."
text: settingsPopup.widgetData.middleClickExec || ""
placeholderText: "Enter command to execute (app or custom script)"
}
}
}
// Spacer settings component
Component {
id: spacerSettings
ColumnLayout {
spacing: Style.marginM * scaling
function saveSettings() {
var settings = Object.assign({}, settingsPopup.widgetData)
settings.width = parseInt(widthInput.text) || 20
return settings
}
NTextInput {
id: widthInput
Layout.fillWidth: true
label: "Width (pixels)"
description: "Width of the spacer in pixels."
text: settingsPopup.widgetData.width || "20"
placeholderText: "Enter width in pixels"
}
}
}
}

View file

@ -39,7 +39,7 @@ NPanel {
General,
Network,
ScreenRecorder,
TimeWeather,
Weather,
Wallpaper,
WallpaperSelector
}
@ -90,8 +90,8 @@ NPanel {
Tabs.NetworkTab {}
}
Component {
id: timeWeatherTab
Tabs.TimeWeatherTab {}
id: weatherTab
Tabs.WeatherTab {}
}
Component {
id: colorSchemeTab
@ -156,10 +156,10 @@ NPanel {
"icon": "brightness_6",
"source": brightnessTab
}, {
"id": SettingsPanel.Tab.TimeWeather,
"label": "Time & Weather",
"icon": "schedule",
"source": timeWeatherTab
"id": SettingsPanel.Tab.Weather,
"label": "Weather",
"icon": "partly_cloudy_day",
"source": weatherTab
}, {
"id": SettingsPanel.Tab.ColorScheme,
"label": "Color Scheme",
@ -368,7 +368,7 @@ NPanel {
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginXS * 1.5 * scaling
spacing: Style.marginXS * scaling
Repeater {
id: sections
@ -398,7 +398,8 @@ NPanel {
RowLayout {
id: tabEntryRow
anchors.fill: parent
anchors.margins: Style.marginS * scaling
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginS * scaling
// Tab icon

View file

@ -242,21 +242,7 @@ ColumnLayout {
Layout.bottomMargin: Style.marginS * scaling
}
// Miniplayer section
NToggle {
label: "Show Album Art In Bar Media Player"
description: "Show the album art of the currently playing song next to the title."
checked: Settings.data.audio.showMiniplayerAlbumArt
onToggled: checked => Settings.data.audio.showMiniplayerAlbumArt = checked
}
NToggle {
label: "Show Audio Visualizer In Bar Media Player"
description: "Shows an audio visualizer in the background of the miniplayer."
checked: Settings.data.audio.showMiniplayerCava
onToggled: checked => Settings.data.audio.showMiniplayerCava = checked
}
// Preferred player (persistent)
// Preferred player
NTextInput {
label: "Preferred Player"
description: "Substring to match MPRIS player (identity/bus/desktop)."

View file

@ -4,7 +4,7 @@ import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.SettingsPanel.Extras
import qs.Modules.SettingsPanel.Bar
ColumnLayout {
id: root
@ -70,57 +70,6 @@ ColumnLayout {
}
}
}
NToggle {
label: "Show Active Window's Icon"
description: "Display the app icon next to the title of the currently focused window."
checked: Settings.data.bar.showActiveWindowIcon
onToggled: checked => Settings.data.bar.showActiveWindowIcon = checked
}
NToggle {
label: "Show Battery Percentage"
description: "Display battery percentage at all times."
checked: Settings.data.bar.alwaysShowBatteryPercentage
onToggled: checked => Settings.data.bar.alwaysShowBatteryPercentage = checked
}
NToggle {
label: "Show Network Statistics"
description: "Display network upload and download speeds in the system monitor."
checked: Settings.data.bar.showNetworkStats
onToggled: checked => Settings.data.bar.showNetworkStats = checked
}
NToggle {
label: "Replace SidePanel toggle with distro logo"
description: "Show distro logo instead of the SidePanel toggle button in the bar."
checked: Settings.data.bar.useDistroLogo
onToggled: checked => {
Settings.data.bar.useDistroLogo = checked
}
}
NComboBox {
label: "Show Workspaces Labels"
description: "Show the workspace name or index within the workspace indicator."
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "index"
name: "Index"
}
ListElement {
key: "name"
name: "Name"
}
}
currentKey: Settings.data.bar.showWorkspaceLabel
onSelected: key => Settings.data.bar.showWorkspaceLabel = key
}
}
NDivider {
@ -138,7 +87,7 @@ ColumnLayout {
text: "Widgets Positioning"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
}

View file

@ -299,8 +299,7 @@ ColumnLayout {
currentKey: Settings.data.nightLight.manualSunrise
placeholder: "Select start time"
onSelected: key => Settings.data.nightLight.manualSunrise = key
preferredWidth: 120 * scaling
minimumWidth: 120 * scaling
}
Item {// add a little more spacing
@ -316,8 +315,7 @@ ColumnLayout {
currentKey: Settings.data.nightLight.manualSunset
placeholder: "Select stop time"
onSelected: key => Settings.data.nightLight.manualSunset = key
preferredWidth: 120 * scaling
minimumWidth: 120 * scaling
}
}
}

View file

@ -8,6 +8,7 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: 0
// Cache for scheme JSON (can be flat or {dark, light})
property var schemeColorsCache: ({})
@ -103,225 +104,218 @@ ColumnLayout {
}
}
// Main Toggles - Dark Mode / Matugen
ColumnLayout {
spacing: 0
spacing: Style.marginL * scaling
Layout.fillWidth: true
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
NToggle {
label: "Dark Mode"
description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available."
checked: Settings.data.colorSchemes.darkMode
enabled: true
onToggled: checked => Settings.data.colorSchemes.darkMode = checked
}
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
// Use Matugen
NToggle {
label: "Enable Matugen"
description: "Automatically generate colors based on your active wallpaper."
checked: Settings.data.colorSchemes.useWallpaperColors
onToggled: checked => {
if (checked) {
// Check if matugen is installed
matugenCheck.running = true
} else {
Settings.data.colorSchemes.useWallpaperColors = false
ToastService.showNotice("Matugen", "Disabled")
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
NToggle {
label: "Dark Mode"
description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available."
checked: Settings.data.colorSchemes.darkMode
enabled: true
onToggled: checked => Settings.data.colorSchemes.darkMode = checked
}
if (Settings.data.colorSchemes.predefinedScheme) {
// Use Matugen
NToggle {
label: "Enable Matugen"
description: "Automatically generate colors based on your active wallpaper."
checked: Settings.data.colorSchemes.useWallpaperColors
onToggled: checked => {
if (checked) {
// Check if matugen is installed
matugenCheck.running = true
} else {
Settings.data.colorSchemes.useWallpaperColors = false
ToastService.showNotice("Matugen", "Disabled")
if (Settings.data.colorSchemes.predefinedScheme) {
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
}
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
}
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
// Predefined Color Schemes
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NText {
text: "Predefined Color Schemes"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: "Predefined Color Schemes"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
// Color Schemes Grid
GridLayout {
columns: 3
rowSpacing: Style.marginM * scaling
columnSpacing: Style.marginM * scaling
Layout.fillWidth: true
Repeater {
model: ColorSchemeService.schemes
Rectangle {
id: schemeCard
property string schemePath: modelData
NText {
text: "These color schemes are only active when 'Use Matugen' is turned off. With Matugen enabled, colors will be automatically generated from your wallpaper. You can still switch between light and dark themes while using Matugen."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
Layout.preferredHeight: 120 * scaling
radius: Style.radiusM * scaling
color: getSchemeColor(modelData, "mSurface")
border.width: Math.max(1, Style.borderL * scaling)
border.color: (!Settings.data.colorSchemes.useWallpaperColors
&& (Settings.data.colorSchemes.predefinedScheme === modelData)) ? Color.mPrimary : Color.mOutline
scale: root.cardScaleLow
// Color Schemes Grid
GridLayout {
columns: 3
rowSpacing: Style.marginM * scaling
columnSpacing: Style.marginM * scaling
Layout.fillWidth: true
// Mouse area for selection
MouseArea {
anchors.fill: parent
onClicked: {
// Disable useWallpaperColors when picking a predefined color scheme
Settings.data.colorSchemes.useWallpaperColors = false
Logger.log("ColorSchemeTab", "Disabled matugen setting")
Repeater {
model: ColorSchemeService.schemes
Settings.data.colorSchemes.predefinedScheme = schemePath
ColorSchemeService.applyScheme(schemePath)
}
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
Rectangle {
id: schemeCard
property string schemePath: modelData
Layout.fillWidth: true
Layout.preferredHeight: 120 * scaling
radius: Style.radiusM * scaling
color: getSchemeColor(modelData, "mSurface")
border.width: Math.max(1, Style.borderL * scaling)
border.color: (!Settings.data.colorSchemes.useWallpaperColors
&& (Settings.data.colorSchemes.predefinedScheme === modelData)) ? Color.mPrimary : Color.mOutline
scale: root.cardScaleLow
// Mouse area for selection
MouseArea {
anchors.fill: parent
onClicked: {
// Disable useWallpaperColors when picking a predefined color scheme
Settings.data.colorSchemes.useWallpaperColors = false
Logger.log("ColorSchemeTab", "Disabled matugen setting")
Settings.data.colorSchemes.predefinedScheme = schemePath
ColorSchemeService.applyScheme(schemePath)
}
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
schemeCard.scale = root.cardScaleHigh
}
onExited: {
schemeCard.scale = root.cardScaleLow
}
onEntered: {
schemeCard.scale = root.cardScaleHigh
}
// Card content
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginXL * scaling
onExited: {
schemeCard.scale = root.cardScaleLow
}
}
// Card content
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginXL * scaling
spacing: Style.marginS * scaling
// Scheme name
NText {
text: {
// Remove json and the full path
var chunks = schemePath.replace(".json", "").split("/")
return chunks[chunks.length - 1]
}
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: getSchemeColor(modelData, "mOnSurface")
Layout.fillWidth: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
// Color swatches
RowLayout {
id: swatches
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
// Scheme name
NText {
text: {
// Remove json and the full path
var chunks = schemePath.replace(".json", "").split("/")
return chunks[chunks.length - 1]
}
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: getSchemeColor(modelData, "mOnSurface")
Layout.fillWidth: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
readonly property int swatchSize: 20 * scaling
// Primary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mPrimary")
}
// Color swatches
RowLayout {
id: swatches
// Secondary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mSecondary")
}
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
// Tertiary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mTertiary")
}
readonly property int swatchSize: 20 * scaling
// Primary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mPrimary")
}
// Secondary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mSecondary")
}
// Tertiary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mTertiary")
}
// Error color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mError")
}
// Error color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mError")
}
}
}
// Selection indicator (Checkmark)
Rectangle {
visible: !Settings.data.colorSchemes.useWallpaperColors
&& (Settings.data.colorSchemes.predefinedScheme === schemePath)
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginS * scaling
width: 24 * scaling
height: 24 * scaling
radius: width * 0.5
color: Color.mPrimary
// Selection indicator (Checkmark)
Rectangle {
visible: !Settings.data.colorSchemes.useWallpaperColors
&& (Settings.data.colorSchemes.predefinedScheme === schemePath)
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginS * scaling
width: 24 * scaling
height: 24 * scaling
radius: width * 0.5
color: Color.mPrimary
NText {
anchors.centerIn: parent
text: "✓"
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
NText {
anchors.centerIn: parent
text: "✓"
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
}
// Smooth animations
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
// Smooth animations
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationNormal
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationNormal
}
}
Behavior on border.width {
NumberAnimation {
duration: Style.animationFast
}
Behavior on border.width {
NumberAnimation {
duration: Style.animationFast
}
}
}

View file

@ -70,52 +70,6 @@ ColumnLayout {
onToggled: checked => Settings.data.general.dimDesktop = checked
}
NToggle {
label: "Auto-hide Dock"
description: "Automatically hide the dock when not in use."
checked: Settings.data.dock.autoHide
onToggled: checked => Settings.data.dock.autoHide = checked
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Dock Background Opacity"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Adjust the background opacity of the dock."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: Settings.data.dock.backgroundOpacity
onMoved: Settings.data.dock.backgroundOpacity = value
cutoutColor: Color.mSurface
}
NText {
text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
@ -175,7 +129,70 @@ ColumnLayout {
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Dock
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Dock"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
}
NToggle {
label: "Auto-hide Dock"
description: "Automatically hide the dock when not in use."
checked: Settings.data.dock.autoHide
onToggled: checked => Settings.data.dock.autoHide = checked
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Dock Background Opacity"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Adjust the background opacity of the dock."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: Settings.data.dock.backgroundOpacity
onMoved: Settings.data.dock.backgroundOpacity = value
cutoutColor: Color.mSurface
}
NText {
text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
@ -206,6 +223,7 @@ ColumnLayout {
currentKey: Settings.data.ui.fontDefault
placeholder: "Select default font..."
popupHeight: 420 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) {
Settings.data.ui.fontDefault = key
}
@ -218,6 +236,7 @@ ColumnLayout {
currentKey: Settings.data.ui.fontFixed
placeholder: "Select monospace font..."
popupHeight: 320 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) {
Settings.data.ui.fontFixed = key
}
@ -230,6 +249,7 @@ ColumnLayout {
currentKey: Settings.data.ui.fontBillboard
placeholder: "Select display font..."
popupHeight: 320 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) {
Settings.data.ui.fontBillboard = key
}

View file

@ -52,20 +52,6 @@ ColumnLayout {
}
}
NToggle {
label: "Enable Clipboard History"
description: "Show clipboard history in the launcher."
checked: Settings.data.appLauncher.enableClipboardHistory
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked
}
NToggle {
label: "Use App2Unit for Launching"
description: "Use app2unit -- 'desktop-entry' when launching applications for better systemd integration."
checked: Settings.data.appLauncher.useApp2Unit
onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
@ -105,6 +91,20 @@ ColumnLayout {
}
}
}
NToggle {
label: "Enable Clipboard History"
description: "Show clipboard history in the launcher."
checked: Settings.data.appLauncher.enableClipboardHistory
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked
}
NToggle {
label: "Use App2Unit for Launching"
description: "Use app2unit -- 'desktop-entry' when launching applications for better systemd integration."
checked: Settings.data.appLauncher.useApp2Unit
onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
}
}
NDivider {

View file

@ -52,46 +52,6 @@ ColumnLayout {
Layout.bottomMargin: Style.marginXL * scaling
}
// Time section
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Time Format"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NToggle {
label: "Use 12-Hour Clock"
description: "Display time in 12-hour format (AM/PM) instead of 24-hour."
checked: Settings.data.location.use12HourClock
onToggled: checked => Settings.data.location.use12HourClock = checked
}
NToggle {
label: "Reverse Day/Month"
description: "Display date as dd/mm instead of mm/dd."
checked: Settings.data.location.reverseDayMonth
onToggled: checked => Settings.data.location.reverseDayMonth = checked
}
NToggle {
label: "Show Date with Clock"
description: "Display date alongside time (e.g., 18:12 - Sat, 23 Aug)."
checked: Settings.data.location.showDateWithClock
onToggled: checked => Settings.data.location.showDateWithClock = checked
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Weather section
ColumnLayout {
spacing: Style.marginM * scaling
@ -111,10 +71,4 @@ ColumnLayout {
onToggled: checked => Settings.data.location.useFahrenheit = checked
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

View file

@ -40,7 +40,7 @@ NBox {
height: 68 * scaling
}
NCircleStat {
value: SystemStatService.memoryUsagePer
value: SystemStatService.memPercent
icon: "memory"
flat: true
contentScale: 0.8
@ -48,7 +48,7 @@ NBox {
height: 68 * scaling
}
NCircleStat {
value: SystemStatService.diskUsage
value: SystemStatService.diskPercent
icon: "hard_drive"
flat: true
contentScale: 0.8

View file

@ -11,71 +11,92 @@ NPanel {
id: root
panelWidth: 460 * scaling
panelHeight: 708 * scaling
panelHeight: contentHeight
// Default height, will be modified via binding when the content is fully loaded
property real contentHeight: 720 * scaling
panelContent: Item {
id: content
property real cardSpacing: Style.marginL * scaling
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: content.cardSpacing
implicitHeight: layout.implicitHeight
width: root.panelWidth
implicitHeight: layout.implicitHeight + (2 * cardSpacing)
height: implicitHeight
// Layout content (not vertically anchored so implicitHeight is valid)
// Update parent's contentHeight whenever our height changes
onHeightChanged: {
root.contentHeight = height
}
onImplicitHeightChanged: {
if (implicitHeight > 0) {
root.contentHeight = implicitHeight
}
}
// Layout content
ColumnLayout {
id: layout
// Use the same spacing value horizontally and vertically
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
x: content.cardSpacing
y: content.cardSpacing
width: parent.width - (2 * content.cardSpacing)
spacing: content.cardSpacing
// Cards (consistent inter-card spacing via ColumnLayout spacing)
ProfileCard {// Layout.topMargin: 0
// Layout.bottomMargin: 0
ProfileCard {
id: profileCard
Layout.fillWidth: true
}
WeatherCard {// Layout.topMargin: 0
// Layout.bottomMargin: 0
WeatherCard {
id: weatherCard
Layout.fillWidth: true
}
// Middle section: media + stats column
RowLayout {
id: middleRow
Layout.fillWidth: true
Layout.topMargin: 0
Layout.bottomMargin: 0
Layout.minimumHeight: 280 * scaling
Layout.preferredHeight: Math.max(280 * scaling, statsCard.implicitHeight)
spacing: content.cardSpacing
// Media card
MediaCard {
id: mediaCard
Layout.fillWidth: true
implicitHeight: statsCard.implicitHeight
Layout.fillHeight: true
}
// System monitors combined in one card
SystemMonitorCard {
id: statsCard
Layout.alignment: Qt.AlignTop
}
}
// Bottom actions (two grouped rows of round buttons)
RowLayout {
id: bottomRow
Layout.fillWidth: true
Layout.topMargin: 0
Layout.bottomMargin: 0
Layout.minimumHeight: 60 * scaling
Layout.preferredHeight: Math.max(60 * scaling, powerProfilesCard.implicitHeight, utilitiesCard.implicitHeight)
spacing: content.cardSpacing
// Power Profiles switcher
PowerProfilesCard {
id: powerProfilesCard
spacing: content.cardSpacing
Layout.fillWidth: true
}
// Utilities buttons
UtilitiesCard {
id: utilitiesCard
spacing: content.cardSpacing
Layout.fillWidth: true
}
}
}

View file

@ -397,6 +397,7 @@ NPanel {
}
outlined: !hovered
fontSize: Style.fontSizeXS * scaling
enabled: !NetworkService.connecting
onClicked: {
if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) {
NetworkService.connect(modelData.ssid)
@ -461,7 +462,7 @@ NPanel {
onVisibleChanged: if (visible)
forceActiveFocus()
onAccepted: {
if (text) {
if (text && !NetworkService.connecting) {
NetworkService.connect(passwordSsid, text)
passwordSsid = ""
passwordInput = ""
@ -481,7 +482,7 @@ NPanel {
NButton {
text: "Connect"
fontSize: Style.fontSizeXXS * scaling
enabled: passwordInput.length > 0
enabled: passwordInput.length > 0 && !NetworkService.connecting
outlined: true
onClicked: {
NetworkService.connect(passwordSsid, passwordInput)

View file

@ -38,6 +38,26 @@ Singleton {
})
property var widgetMetadata: ({
"ActiveWindow": {
"allowUserSettings": true,
"showIcon": true
},
"Battery": {
"allowUserSettings": true,
"alwaysShowPercentage": false,
"warningThreshold": 30
},
"Brightness": {
"allowUserSettings": true,
"alwaysShowPercentage": false
},
"Clock": {
"allowUserSettings": true,
"showDate": false,
"use12HourClock": false,
"showSeconds": false,
"reverseDayMonth": true
},
"CustomButton": {
"allowUserSettings": true,
"icon": "favorite",
@ -45,10 +65,44 @@ Singleton {
"rightClickExec": "",
"middleClickExec": ""
},
"Microphone": {
"allowUserSettings": true,
"alwaysShowPercentage": false
},
"NotificationHistory": {
"allowUserSettings": true,
"showUnreadBadge": true,
"hideWhenZero": true
},
"Spacer": {
"allowUserSettings": true,
"icon": "space_bar",
"width": 20
},
"SystemMonitor": {
"allowUserSettings": true,
"showCpuUsage": true,
"showCpuTemp": true,
"showMemoryUsage": true,
"showMemoryAsPercent": false,
"showNetworkStats": false
},
"Workspace": {
"allowUserSettings": true,
"labelMode": "index"
},
"MediaMini": {
"allowUserSettings": true,
"showAlbumArt": false,
"showVisualizer": false,
"visualizerType": "linear"
},
"SidePanelToggle": {
"allowUserSettings": true,
"useDistroLogo": false
},
"Volume": {
"allowUserSettings": true,
"alwaysShowPercentage": false
}
})

View file

@ -0,0 +1,49 @@
pragma Singleton
import Quickshell
import Quickshell.Services.UPower
Singleton {
id: root
// Choose icon based on charge and charging state
function getIcon(percent, charging, isReady) {
if (!isReady) {
return "battery_error"
}
if (charging) {
if (percent >= 95)
return "battery_full"
if (percent >= 85)
return "battery_charging_90"
if (percent >= 65)
return "battery_charging_80"
if (percent >= 55)
return "battery_charging_60"
if (percent >= 45)
return "battery_charging_50"
if (percent >= 25)
return "battery_charging_30"
if (percent >= 0)
return "battery_charging_20"
} else {
if (percent >= 95)
return "battery_full"
if (percent >= 85)
return "battery_6_bar"
if (percent >= 70)
return "battery_5_bar"
if (percent >= 55)
return "battery_4_bar"
if (percent >= 40)
return "battery_3_bar"
if (percent >= 25)
return "battery_2_bar"
if (percent >= 10)
return "battery_1_bar"
if (percent >= 0)
return "battery_0_bar"
}
}
}

View file

@ -110,9 +110,43 @@ Singleton {
property real lastBrightness: 0
property real queuedBrightness: NaN
// For internal displays - store the backlight device path
property string backlightDevice: ""
property string brightnessPath: ""
property string maxBrightnessPath: ""
property int maxBrightness: 100
property bool ignoreNextChange: false
// Signal for brightness changes
signal brightnessUpdated(real newBrightness)
// FileView to watch for external brightness changes (internal displays only)
readonly property FileView brightnessWatcher: FileView {
id: brightnessWatcher
// Only set path for internal displays with a valid brightness path
path: (!monitor.isDdc && !monitor.isAppleDisplay && monitor.brightnessPath !== "") ? monitor.brightnessPath : ""
watchChanges: path !== ""
onFileChanged: {
reload()
if (monitor.ignoreNextChange) {
monitor.ignoreNextChange = false
return
}
if (text() === "")
return
var current = parseInt(text().trim())
if (!isNaN(current) && monitor.maxBrightness > 0) {
var newBrightness = current / monitor.maxBrightness
// Only update if it's actually different (avoid feedback loops)
if (Math.abs(newBrightness - monitor.brightness) > 0.01) {
monitor.brightness = newBrightness
monitor.brightnessUpdated(monitor.brightness)
//Logger.log("Brightness", "External change detected:", monitor.modelData.name, monitor.brightness)
}
}
}
}
// Initialize brightness
readonly property Process initProc: Process {
stdout: StdioCollector {
@ -121,8 +155,8 @@ Singleton {
if (dataText === "") {
return
}
Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText)
//Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText)
if (monitor.isAppleDisplay) {
var val = parseInt(dataText)
if (!isNaN(val)) {
@ -140,14 +174,20 @@ Singleton {
}
}
} else {
// Internal backlight
var parts = dataText.split(" ")
if (parts.length >= 2) {
var current = parseInt(parts[0])
var max = parseInt(parts[1])
// Internal backlight - parse the response which includes device path
var lines = dataText.split("\n")
if (lines.length >= 3) {
monitor.backlightDevice = lines[0]
monitor.brightnessPath = monitor.backlightDevice + "/brightness"
monitor.maxBrightnessPath = monitor.backlightDevice + "/max_brightness"
var current = parseInt(lines[1])
var max = parseInt(lines[2])
if (!isNaN(current) && !isNaN(max) && max > 0) {
monitor.maxBrightness = max
monitor.brightness = current / max
Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness)
Logger.log("Brightness", "Using backlight device:", monitor.backlightDevice)
}
}
}
@ -171,7 +211,7 @@ Singleton {
function increaseBrightness(): void {
var stepSize = Settings.data.brightness.brightnessStep / 100.0
setBrightnessDebounced(brightness + stepSize)
setBrightnessDebounced(monitor.brightness + stepSize)
}
function decreaseBrightness(): void {
@ -183,22 +223,23 @@ Singleton {
value = Math.max(0, Math.min(1, value))
var rounded = Math.round(value * 100)
if (Math.round(brightness * 100) === rounded)
if (Math.round(monitor.brightness * 100) === rounded)
return
if (isDdc && timer.running) {
queuedBrightness = value
monitor.queuedBrightness = value
return
}
brightness = value
brightnessUpdated(brightness)
monitor.brightness = value
brightnessUpdated(monitor.brightness)
if (isAppleDisplay) {
Quickshell.execDetached(["asdbctl", "set", rounded])
} else if (isDdc) {
Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded])
} else {
monitor.ignoreNextChange = true
Quickshell.execDetached(["brightnessctl", "s", rounded + "%"])
}
@ -208,7 +249,7 @@ Singleton {
}
function setBrightnessDebounced(value: real): void {
queuedBrightness = value
monitor.queuedBrightness = value
timer.restart()
}
@ -218,8 +259,11 @@ Singleton {
} else if (isDdc) {
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]
} else {
// Internal backlight - try to find the first available backlight device
initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$(cat $dev/brightness) $(cat $dev/max_brightness)\"; break; fi; done"]
// Internal backlight - find the first available backlight device and get its info
// This now returns: device_path, current_brightness, max_brightness (on separate lines)
initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do "
+ " if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then " + " echo \"$dev\"; "
+ " cat \"$dev/brightness\"; " + " cat \"$dev/max_brightness\"; " + " break; " + " fi; " + "done"]
}
initProc.running = true
}

View file

@ -37,9 +37,7 @@ Singleton {
Process {
id: process
stdinEnabled: true
running: (Settings.data.audio.visualizerType !== "none")
&& (PanelService.getPanel("sidePanel").active || Settings.data.audio.showMiniplayerCava
|| (PanelService.lockScreen && PanelService.lockScreen.active))
running: true
command: ["cava", "-p", "/dev/stdin"]
onExited: {
stdinEnabled = true

View file

@ -45,7 +45,7 @@ Singleton {
property string version: "Unknown"
property var contributors: []
property double timestamp: 0
property real timestamp: 0
}
}

View file

@ -18,6 +18,9 @@ Singleton {
property string disconnectingFrom: ""
property string forgettingNetwork: ""
property bool ignoreScanResults: false
property bool scanPending: false
// Persistent cache
property string cacheFile: Settings.cacheDir + "network.json"
readonly property string cachedLastConnected: cacheAdapter.lastConnected
@ -54,7 +57,7 @@ Singleton {
Component.onCompleted: {
Logger.log("Network", "Service initialized")
syncWifiState()
refresh()
scan()
}
// Save cache with debounce
@ -75,6 +78,16 @@ Singleton {
onTriggered: scan()
}
// Ethernet check timer
// Always running every 30s
Timer {
id: ethernetCheckTimer
interval: 30000
running: true
repeat: true
onTriggered: ethernetStateProcess.running = true
}
// Core functions
function syncWifiState() {
wifiStateProcess.running = true
@ -82,26 +95,26 @@ Singleton {
function setWifiEnabled(enabled) {
Settings.data.network.wifiEnabled = enabled
wifiToggleProcess.action = enabled ? "on" : "off"
wifiToggleProcess.running = true
}
function refresh() {
ethernetStateProcess.running = true
if (Settings.data.network.wifiEnabled) {
scan()
}
}
function scan() {
if (scanning)
if (!Settings.data.network.wifiEnabled)
return
if (scanning) {
// Mark current scan results to be ignored and schedule a new scan
Logger.log("Network", "Scan already in progress, will ignore results and rescan")
ignoreScanResults = true
scanPending = true
return
}
scanning = true
lastError = ""
scanProcess.running = true
ignoreScanResults = false
// Get existing profiles first, then scan
profileCheckProcess.running = true
Logger.log("Network", "Wi-Fi scan in progress...")
}
@ -174,8 +187,7 @@ Singleton {
"ssid": ssid,
"security": "--",
"signal": 100,
"connected"// Default to good signal until real scan
: true,
"connected": true,
"existing": true,
"cached": true
}
@ -206,7 +218,7 @@ Singleton {
// Processes
Process {
id: ethernetStateProcess
running: false
running: true
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
stdout: StdioCollector {
@ -239,30 +251,34 @@ Singleton {
}
}
// Helper process to get existing profiles
Process {
id: wifiToggleProcess
property string action: "on"
id: profileCheckProcess
running: false
command: ["nmcli", "radio", "wifi", action]
command: ["nmcli", "-t", "-f", "NAME", "connection", "show"]
onRunningChanged: {
if (!running) {
if (action === "on") {
// Clear networks immediately and start delayed scan
root.networks = ({})
delayedScanTimer.interval = 8000
delayedScanTimer.restart()
} else {
root.networks = ({})
}
}
}
stderr: StdioCollector {
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
Logger.warn("Network", "WiFi toggle error: " + text)
if (root.ignoreScanResults) {
Logger.log("Network", "Ignoring profile check results (new scan requested)")
root.scanning = false
// Check if we need to start a new scan
if (root.scanPending) {
root.scanPending = false
delayedScanTimer.interval = 100
delayedScanTimer.restart()
}
return
}
const profiles = {}
const lines = text.split("\n").filter(l => l.trim())
for (const line of lines) {
profiles[line.trim()] = true
}
scanProcess.existingProfiles = profiles
scanProcess.running = true
}
}
}
@ -270,74 +286,103 @@ Singleton {
Process {
id: scanProcess
running: false
command: ["sh", "-c", `
# Get list of saved connection profiles (just the names)
profiles=$(nmcli -t -f NAME connection show | tr '\n' '|')
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list", "--rescan", "yes"]
# Get WiFi networks
nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list --rescan yes | while read line; do
ssid=$(echo "$line" | cut -d: -f1)
security=$(echo "$line" | cut -d: -f2)
signal=$(echo "$line" | cut -d: -f3)
in_use=$(echo "$line" | cut -d: -f4)
# Skip empty SSIDs
if [ -z "$ssid" ]; then
continue
fi
# Check if SSID matches any profile name (simple check)
# This covers most cases where profile name equals or contains the SSID
existing=false
if echo "$profiles" | grep -qF "$ssid|"; then
existing=true
fi
echo "$ssid|$security|$signal|$in_use|$existing"
done
`]
property var existingProfiles: ({})
stdout: StdioCollector {
onStreamFinished: {
const nets = {}
const lines = text.split("\n").filter(l => l.trim())
if (root.ignoreScanResults) {
Logger.log("Network", "Ignoring scan results (new scan requested)")
root.scanning = false
for (const line of lines) {
const parts = line.split("|")
if (parts.length < 5)
// Check if we need to start a new scan
if (root.scanPending) {
root.scanPending = false
delayedScanTimer.interval = 100
delayedScanTimer.restart()
}
return
}
// Process the scan results as before...
const lines = text.split("\n")
const networksMap = {}
for (var i = 0; i < lines.length; ++i) {
const line = lines[i].trim()
if (!line)
continue
const ssid = parts[0]
if (!ssid || ssid.trim() === "")
continue
const network = {
"ssid": ssid,
"security": parts[1] || "--",
"signal": parseInt(parts[2]) || 0,
"connected": parts[3] === "*",
"existing": parts[4] === "true",
"cached": ssid in cacheAdapter.knownNetworks
// Parse from the end to handle SSIDs with colons
// Format is SSID:SECURITY:SIGNAL:IN-USE
// We know the last 3 fields, so everything else is SSID
const lastColonIdx = line.lastIndexOf(":")
if (lastColonIdx === -1) {
Logger.warn("Network", "Malformed nmcli output line:", line)
continue
}
// Track connected network
if (network.connected && cacheAdapter.lastConnected !== ssid) {
cacheAdapter.lastConnected = ssid
saveCache()
const inUse = line.substring(lastColonIdx + 1)
const remainingLine = line.substring(0, lastColonIdx)
const secondLastColonIdx = remainingLine.lastIndexOf(":")
if (secondLastColonIdx === -1) {
Logger.warn("Network", "Malformed nmcli output line:", line)
continue
}
// Keep best signal for duplicate SSIDs
if (!nets[ssid] || network.signal > nets[ssid].signal) {
nets[ssid] = network
const signal = remainingLine.substring(secondLastColonIdx + 1)
const remainingLine2 = remainingLine.substring(0, secondLastColonIdx)
const thirdLastColonIdx = remainingLine2.lastIndexOf(":")
if (thirdLastColonIdx === -1) {
Logger.warn("Network", "Malformed nmcli output line:", line)
continue
}
const security = remainingLine2.substring(thirdLastColonIdx + 1)
const ssid = remainingLine2.substring(0, thirdLastColonIdx)
if (ssid) {
const signalInt = parseInt(signal) || 0
const connected = inUse === "*"
// Track connected network in cache
if (connected && cacheAdapter.lastConnected !== ssid) {
cacheAdapter.lastConnected = ssid
saveCache()
}
if (!networksMap[ssid]) {
networksMap[ssid] = {
"ssid": ssid,
"security": security || "--",
"signal": signalInt,
"connected": connected,
"existing": ssid in scanProcess.existingProfiles,
"cached": ssid in cacheAdapter.knownNetworks
}
} else {
// Keep the best signal for duplicate SSIDs
const existingNet = networksMap[ssid]
if (connected) {
existingNet.connected = true
}
if (signalInt > existingNet.signal) {
existingNet.signal = signalInt
existingNet.security = security || "--"
}
}
}
}
// For logging purpose only
Logger.log("Network", "Wi-Fi scan completed")
// Logging
const oldSSIDs = Object.keys(root.networks)
const newSSIDs = Object.keys(nets)
const newSSIDs = Object.keys(networksMap)
const newNetworks = newSSIDs.filter(ssid => !oldSSIDs.includes(ssid))
const lostNetworks = oldSSIDs.filter(ssid => !newSSIDs.includes(ssid))
if (newNetworks.length > 0 || lostNetworks.length > 0) {
if (newNetworks.length > 0) {
Logger.log("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", "))
@ -345,12 +390,19 @@ Singleton {
if (lostNetworks.length > 0) {
Logger.log("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", "))
}
Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(nets).length)
Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(networksMap).length)
}
// Assign the results
root.networks = nets
Logger.log("Network", "Wi-Fi scan completed")
root.networks = networksMap
root.scanning = false
// Check if we need to start a new scan
if (root.scanPending) {
root.scanPending = false
delayedScanTimer.interval = 100
delayedScanTimer.restart()
}
}
}
@ -359,16 +411,14 @@ Singleton {
root.scanning = false
if (text.trim()) {
Logger.warn("Network", "Scan error: " + text)
// If scan fails, set a short retry
if (Settings.data.network.wifiEnabled) {
delayedScanTimer.interval = 5000
delayedScanTimer.restart()
}
// If scan fails, retry
delayedScanTimer.interval = 5000
delayedScanTimer.restart()
}
}
}
}
Process {
id: connectProcess
property string mode: "new"
@ -390,6 +440,17 @@ Singleton {
stdout: StdioCollector {
onStreamFinished: {
// Check if the output actually indicates success
// nmcli outputs "Device '...' successfully activated" or "Connection successfully activated"
// on success. Empty output or other messages indicate failure.
const output = text.trim()
if (!output || (!output.includes("successfully activated") && !output.includes("Connection successfully"))) {
// No success message - likely an error occurred
// Don't update anything, let stderr handler deal with it
return
}
// Success - update cache
let known = cacheAdapter.knownNetworks
known[connectProcess.ssid] = {
@ -408,7 +469,7 @@ Singleton {
Logger.log("Network", `Connected to network: "${connectProcess.ssid}"`)
// Still do a scan to get accurate signal and security info
delayedScanTimer.interval = 1000
delayedScanTimer.interval = 5000
delayedScanTimer.restart()
}
}
@ -464,7 +525,7 @@ Singleton {
Logger.warn("Network", "Disconnect error: " + text)
}
// Still trigger a scan even on error
delayedScanTimer.interval = 1000
delayedScanTimer.interval = 5000
delayedScanTimer.restart()
}
}
@ -522,8 +583,8 @@ Singleton {
root.forgettingNetwork = ""
// Quick scan to verify the profile is gone
delayedScanTimer.interval = 500
// Scan to verify the profile is gone
delayedScanTimer.interval = 5000
delayedScanTimer.restart()
}
}
@ -535,7 +596,7 @@ Singleton {
Logger.warn("Network", "Forget error: " + text)
}
// Still Trigger a scan even on error
delayedScanTimer.interval = 500
delayedScanTimer.interval = 5000
delayedScanTimer.restart()
}
}

View file

@ -80,7 +80,7 @@ Singleton {
JsonAdapter {
id: historyAdapter
property var history: []
property double timestamp: 0
property real timestamp: 0
}
}
@ -118,12 +118,13 @@ Singleton {
// Function to add notification to model
function addNotification(notification) {
const resolvedImage = resolveNotificationImage(notification)
notificationModel.insert(0, {
"rawNotification": notification,
"summary": notification.summary,
"body": notification.body,
"appName": notification.appName,
"image": notification.image,
"image": resolvedImage,
"appIcon": notification.appIcon,
"urgency": notification.urgency,
"timestamp": new Date()
@ -139,6 +140,40 @@ Singleton {
}
}
// Resolve an image path for a notification, supporting icon names and absolute paths
function resolveNotificationImage(notification) {
try {
// If an explicit image is already provided, prefer it
if (notification && notification.image && notification.image !== "") {
return notification.image
}
// Fallback to appIcon which may be a name or a path (notify-send -i)
const icon = notification ? (notification.appIcon || "") : ""
if (!icon)
return ""
// Accept absolute file paths or file URLs directly
if (icon.startsWith("/")) {
return icon
}
if (icon.startsWith("file://")) {
// Strip the scheme for QML image source compatibility
return icon.substring("file://".length)
}
// Resolve themed icon names to absolute paths
try {
const p = Icons.iconFromName(icon, "")
return p || ""
} catch (e2) {
return ""
}
} catch (e) {
return ""
}
}
// Add a simplified copy into persistent history
function addToHistory(notification) {
historyModel.insert(0, {
@ -166,12 +201,17 @@ Singleton {
const items = historyAdapter.history || []
for (var i = 0; i < items.length; i++) {
const it = items[i]
// Coerce legacy second-based timestamps to milliseconds
var ts = it.timestamp
if (typeof ts === "number" && ts < 1e12) {
ts = ts * 1000
}
historyModel.append({
"summary": it.summary || "",
"body": it.body || "",
"appName": it.appName || "",
"urgency": it.urgency,
"timestamp": it.timestamp ? new Date(it.timestamp) : new Date()
"timestamp": ts ? new Date(ts) : new Date()
})
}
} catch (e) {
@ -190,7 +230,10 @@ Singleton {
"body": n.body,
"appName": n.appName,
"urgency": n.urgency,
"timestamp": (n.timestamp instanceof Date) ? n.timestamp.getTime() : n.timestamp
"timestamp"// Always persist in milliseconds
: (n.timestamp instanceof Date) ? n.timestamp.getTime(
) : (typeof n.timestamp === "number"
&& n.timestamp < 1e12 ? n.timestamp * 1000 : n.timestamp)
})
}
historyAdapter.history = arr

View file

@ -4,6 +4,7 @@ import QtQuick
import Qt.labs.folderlistmodel
import Quickshell
import Quickshell.Io
import qs.Commons
Singleton {
id: root
@ -11,12 +12,313 @@ Singleton {
// Public values
property real cpuUsage: 0
property real cpuTemp: 0
property real memoryUsageGb: 0
property real memoryUsagePer: 0
property real diskUsage: 0
property real memGb: 0
property real memPercent: 0
property real diskPercent: 0
property real rxSpeed: 0
property real txSpeed: 0
// Configuration
property int sleepDuration: 3000
// Internal state for CPU calculation
property var prevCpuStats: null
// Internal state for network speed calculation
// Previous Bytes need to be stored as 'real' as they represent the total of bytes transfered
// since the computer started, so their value will easily overlfow a 32bit int.
property real prevRxBytes: 0
property real prevTxBytes: 0
property real prevTime: 0
// Cpu temperature is the most complex
readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"]
property string cpuTempSensorName: ""
property string cpuTempHwmonPath: ""
// For Intel coretemp averaging of all cores/sensors
property var intelTempValues: []
property int intelTempFilesChecked: 0
property int intelTempMaxFiles: 20 // Will test up to temp20_input
// --------------------------------------------
Component.onCompleted: {
Logger.log("SystemStat", "Service started with interval:", root.sleepDuration, "ms")
// Kickoff the cpu name detection for temperature
cpuTempNameReader.checkNext()
}
// --------------------------------------------
// Timer for periodic updates
Timer {
id: updateTimer
interval: root.sleepDuration
repeat: true
running: true
triggeredOnStart: true
onTriggered: {
// Trigger all direct system files reads
memInfoFile.reload()
cpuStatFile.reload()
netDevFile.reload()
// Run df (disk free) one time
dfProcess.running = true
updateCpuTemperature()
}
}
// --------------------------------------------
// FileView components for reading system files
FileView {
id: memInfoFile
path: "/proc/meminfo"
onLoaded: parseMemoryInfo(text())
}
FileView {
id: cpuStatFile
path: "/proc/stat"
onLoaded: calculateCpuUsage(text())
}
FileView {
id: netDevFile
path: "/proc/net/dev"
onLoaded: calculateNetworkSpeed(text())
}
// --------------------------------------------
// Process to fetch disk usage in percent
// Uses 'df' aka 'disk free'
Process {
id: dfProcess
command: ["df", "--output=pcent", "/"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split('\n')
if (lines.length >= 2) {
const percent = lines[1].replace(/[^0-9]/g, '')
root.diskPercent = parseInt(percent) || 0
}
}
}
}
// --------------------------------------------
// CPU Temperature
// It's more complex.
// ----
// #1 - Find a common cpu sensor name ie: "coretemp", "k10temp", "zenpower"
FileView {
id: cpuTempNameReader
property int currentIndex: 0
function checkNext() {
if (currentIndex >= 10) {
// Check up to hwmon10
Logger.warn("No supported temperature sensor found")
return
}
//Logger.log("SystemStat", "---- Probing: hwmon", currentIndex)
cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`
cpuTempNameReader.reload()
}
onLoaded: {
const name = text().trim()
if (root.supportedTempCpuSensorNames.includes(name)) {
root.cpuTempSensorName = name
root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`
Logger.log("SystemStat", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`)
} else {
currentIndex++
Qt.callLater(() => {
// Qt.callLater is mandatory
checkNext()
})
}
}
onLoadFailed: function (error) {
currentIndex++
Qt.callLater(() => {
// Qt.callLater is mandatory
checkNext()
})
}
}
// ----
// #2 - Read sensor value
FileView {
id: cpuTempReader
printErrors: false
onLoaded: {
const data = text().trim()
if (root.cpuTempSensorName === "coretemp") {
// For Intel, collect all temperature values
const temp = parseInt(data) / 1000.0
//console.log(temp, cpuTempReader.path)
root.intelTempValues.push(temp)
Qt.callLater(() => {
// Qt.callLater is mandatory
checkNextIntelTemp()
})
} else {
// For AMD sensors (k10temp and zenpower), directly set the temperature
root.cpuTemp = Math.round(parseInt(data) / 1000.0)
}
}
onLoadFailed: function (error) {
Qt.callLater(() => {
// Qt.callLater is mandatory
checkNextIntelTemp()
})
}
}
// -------------------------------------------------------
// Parse memory info from /proc/meminfo
function parseMemoryInfo(text) {
if (!text)
return
const lines = text.split('\n')
let memTotal = 0
let memAvailable = 0
for (const line of lines) {
if (line.startsWith('MemTotal:')) {
memTotal = parseInt(line.split(/\s+/)[1]) || 0
} else if (line.startsWith('MemAvailable:')) {
memAvailable = parseInt(line.split(/\s+/)[1]) || 0
}
}
if (memTotal > 0) {
const usageKb = memTotal - memAvailable
root.memGb = (usageKb / 1000000).toFixed(1)
root.memPercent = Math.round((usageKb / memTotal) * 100)
}
}
// -------------------------------------------------------
// Calculate CPU usage from /proc/stat
function calculateCpuUsage(text) {
if (!text)
return
const lines = text.split('\n')
const cpuLine = lines[0]
// First line is total CPU
if (!cpuLine.startsWith('cpu '))
return
const parts = cpuLine.split(/\s+/)
const stats = {
"user": parseInt(parts[1]) || 0,
"nice": parseInt(parts[2]) || 0,
"system": parseInt(parts[3]) || 0,
"idle": parseInt(parts[4]) || 0,
"iowait": parseInt(parts[5]) || 0,
"irq": parseInt(parts[6]) || 0,
"softirq": parseInt(parts[7]) || 0,
"steal": parseInt(parts[8]) || 0,
"guest": parseInt(parts[9]) || 0,
"guestNice": parseInt(parts[10]) || 0
}
const totalIdle = stats.idle + stats.iowait
const total = Object.values(stats).reduce((sum, val) => sum + val, 0)
if (root.prevCpuStats) {
const prevTotalIdle = root.prevCpuStats.idle + root.prevCpuStats.iowait
const prevTotal = Object.values(root.prevCpuStats).reduce((sum, val) => sum + val, 0)
const diffTotal = total - prevTotal
const diffIdle = totalIdle - prevTotalIdle
if (diffTotal > 0) {
root.cpuUsage = (((diffTotal - diffIdle) / diffTotal) * 100).toFixed(1)
}
}
root.prevCpuStats = stats
}
// -------------------------------------------------------
// Calculate RX and TX speed from /proc/net/dev
// Average speed of all interfaces excepted 'lo'
function calculateNetworkSpeed(text) {
if (!text) {
return
}
const currentTime = Date.now() / 1000
const lines = text.split('\n')
let totalRx = 0
let totalTx = 0
for (var i = 2; i < lines.length; i++) {
const line = lines[i].trim()
if (!line) {
continue
}
const colonIndex = line.indexOf(':')
if (colonIndex === -1) {
continue
}
const iface = line.substring(0, colonIndex).trim()
if (iface === 'lo') {
continue
}
const statsLine = line.substring(colonIndex + 1).trim()
const stats = statsLine.split(/\s+/)
const rxBytes = parseInt(stats[0], 10) || 0
const txBytes = parseInt(stats[8], 10) || 0
totalRx += rxBytes
totalTx += txBytes
}
// Compute only if we have a previous run to compare to.
if (root.prevTime > 0) {
const timeDiff = currentTime - root.prevTime
// Avoid division by zero if time hasn't passed.
if (timeDiff > 0) {
let rxDiff = totalRx - root.prevRxBytes
let txDiff = totalTx - root.prevTxBytes
// Handle counter resets (e.g., WiFi reconnect), which would cause a negative value.
if (rxDiff < 0) {
rxDiff = 0
}
if (txDiff < 0) {
txDiff = 0
}
root.rxSpeed = Math.round(rxDiff / timeDiff) // Speed in Bytes/s
root.txSpeed = Math.round(txDiff / timeDiff)
}
}
root.prevRxBytes = totalRx
root.prevTxBytes = totalTx
root.prevTime = currentTime
}
// -------------------------------------------------------
// Helper function to format network speeds
function formatSpeed(bytesPerSecond) {
if (bytesPerSecond < 1024) {
@ -30,27 +332,44 @@ Singleton {
}
}
// Background process emitting one JSON line per sample
Process {
id: reader
running: true
command: ["sh", "-c", Quickshell.shellDir + "/Bin/system-stats.sh"]
stdout: SplitParser {
onRead: function (line) {
try {
const data = JSON.parse(line)
root.cpuUsage = data.cpu
root.cpuTemp = data.cputemp
root.memoryUsageGb = data.memgb
root.memoryUsagePer = data.memper
root.diskUsage = data.diskper
root.rxSpeed = parseFloat(data.rx_speed) || 0
root.txSpeed = parseFloat(data.tx_speed) || 0
} catch (e) {
// ignore malformed lines
}
}
// -------------------------------------------------------
// Function to start fetching and computing the cpu temperature
function updateCpuTemperature() {
// For AMD sensors (k10temp and zenpower), only use Tctl sensor
// temp1_input corresponds to Tctl (Temperature Control) on these sensors
if (root.cpuTempSensorName === "k10temp" || root.cpuTempSensorName === "zenpower") {
cpuTempReader.path = `${root.cpuTempHwmonPath}/temp1_input`
cpuTempReader.reload()
} // For Intel coretemp, start averaging all available sensors/cores
else if (root.cpuTempSensorName === "coretemp") {
root.intelTempValues = []
root.intelTempFilesChecked = 0
checkNextIntelTemp()
}
}
// -------------------------------------------------------
// Function to check next Intel temperature sensor
function checkNextIntelTemp() {
if (root.intelTempFilesChecked >= root.intelTempMaxFiles) {
// Calculate average of all found temperatures
if (root.intelTempValues.length > 0) {
let sum = 0
for (var i = 0; i < root.intelTempValues.length; i++) {
sum += root.intelTempValues[i]
}
root.cpuTemp = Math.round(sum / root.intelTempValues.length)
//Logger.log("SystemStat", `Averaged ${root.intelTempValues.length} CPU thermal sensors: ${root.cpuTemp}°C`)
} else {
Logger.warn("SystemStat", "No temperature sensors found for coretemp")
root.cpuTemp = 0
}
return
}
// Check next temperature file
root.intelTempFilesChecked++
cpuTempReader.path = `${root.cpuTempHwmonPath}/temp${root.intelTempFilesChecked}_input`
cpuTempReader.reload()
}
}

View file

@ -8,7 +8,7 @@ Singleton {
id: root
// Public properties
property string baseVersion: "2.6.0"
property string baseVersion: "2.7.0"
property bool isDevelopment: true
property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}`

View file

@ -216,7 +216,11 @@ Singleton {
// -------------------------------------------------------------------
// Get specific monitor wallpaper - now from cache
function getWallpaper(screenName) {
return currentWallpapers[screenName] || ""
var path = currentWallpapers[screenName] || ""
if (path === "") {
return Settings.data.wallpaper.defaultWallpaper || ""
}
return path
}
// -------------------------------------------------------------------

View file

@ -77,10 +77,12 @@ Rectangle {
RowLayout {
id: contentRow
anchors.centerIn: parent
spacing: Style.marginS * scaling
spacing: Style.marginXS * scaling
// Icon (optional)
NIcon {
Layout.alignment: Qt.AlignVCenter
layoutTopMargin: 1 * scaling
visible: root.icon !== ""
text: root.icon
font.pointSize: root.iconSize
@ -105,6 +107,7 @@ Rectangle {
// Text
NText {
Layout.alignment: Qt.AlignVCenter
visible: root.text !== ""
text: root.text
font.pointSize: root.fontSize

View file

@ -27,6 +27,11 @@ RowLayout {
visible: root.label !== "" || root.description !== ""
}
// Spacer to push the checkbox to the far right
Item {
Layout.fillWidth: true
}
Rectangle {
id: box
@ -39,13 +44,13 @@ RowLayout {
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationNormal
duration: Style.animationFast
}
}

View file

@ -1,34 +0,0 @@
import QtQuick
import qs.Commons
import qs.Services
import qs.Widgets
Rectangle {
id: root
signal entered
signal exited
signal clicked
width: textItem.paintedWidth
height: textItem.paintedHeight
color: Color.transparent
NText {
id: textItem
text: Time.time
anchors.centerIn: parent
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: root.entered()
onExited: root.exited()
onClicked: root.clicked()
}
}

View file

@ -8,8 +8,7 @@ import qs.Widgets
RowLayout {
id: root
readonly property real preferredHeight: Style.baseWidgetSize * 1.1 * scaling
property real preferredWidth: 320 * scaling
property real minimumWidth: 280 * scaling
property real popupHeight: 180 * scaling
property string label: ""
@ -20,9 +19,11 @@ RowLayout {
property string currentKey: ""
property string placeholder: ""
readonly property real preferredHeight: Style.baseWidgetSize * 1.1 * scaling
signal selected(string key)
spacing: Style.marginS * scaling
spacing: Style.marginL * scaling
Layout.fillWidth: true
function findIndexByKey(key) {
@ -39,11 +40,15 @@ RowLayout {
description: root.description
}
Item {
Layout.fillWidth: true
}
ComboBox {
id: combo
Layout.preferredWidth: root.preferredWidth
Layout.preferredHeight: height
Layout.minimumWidth: root.minimumWidth
Layout.preferredHeight: root.preferredHeight
model: model
currentIndex: findIndexByKey(currentKey)
onActivated: {

View file

@ -1,8 +1,11 @@
import QtQuick
import qs.Commons
import qs.Widgets
import QtQuick.Layouts
Text {
// Optional layout nudge for optical alignment when used inside Layouts
property real layoutTopMargin: 0
text: "question_mark"
font.family: "Material Symbols Rounded"
font.pointSize: Style.fontSizeL * scaling
@ -12,4 +15,5 @@ Text {
}
color: Color.mOnSurface
verticalAlignment: Text.AlignVCenter
Layout.topMargin: layoutTopMargin
}

View file

@ -20,6 +20,7 @@ ColumnLayout {
font.capitalization: Font.Capitalize
color: labelColor
visible: label !== ""
Layout.fillWidth: true
}
NText {

View file

@ -14,6 +14,8 @@ Item {
property color iconCircleColor: Color.mPrimary
property color iconTextColor: Color.mSurface
property color collapsedIconColor: Color.mOnSurface
property real iconRotation: 0
property real sizeRatio: 0.8
property bool autoHide: false
property bool forceOpen: false
@ -37,13 +39,13 @@ Item {
property bool shouldAnimateHide: false
// Exposed width logic
readonly property int pillHeight: Style.baseWidgetSize * sizeRatio * scaling
readonly property int iconSize: Style.baseWidgetSize * sizeRatio * scaling
readonly property int pillPaddingHorizontal: Style.marginM * scaling
readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling)
readonly property int pillHeight: iconSize
readonly property int pillPaddingHorizontal: Style.marginS * scaling
readonly property int pillOverlap: iconSize * 0.5
readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
width: iconSize + (effectiveShown ? maxPillWidth - pillOverlap : 0)
width: iconSize + Math.max(0, pill.width - pillOverlap)
height: pillHeight
Rectangle {
@ -65,7 +67,13 @@ Item {
NText {
id: textItem
anchors.centerIn: parent
anchors.verticalCenter: parent.verticalCenter
x: {
// Little tweak to have a better text horizontal centering
var centerX = (parent.width - width) / 2
var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling
return centerX + offset
}
text: root.text
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
@ -97,9 +105,10 @@ Item {
// When forced shown, match pill background; otherwise use accent when hovered
color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant)
anchors.verticalCenter: parent.verticalCenter
border.width: Math.max(1, Style.borderS * scaling)
border.color: forceOpen ? Qt.alpha(Color.mOutline, 0.5) : Color.transparent
anchors.left: rightOpen ? parent.left : undefined
anchors.right: rightOpen ? undefined : parent.right
x: rightOpen ? 0 : (parent.width - width)
Behavior on color {
ColorAnimation {
@ -110,6 +119,7 @@ Item {
NIcon {
text: root.icon
rotation: root.iconRotation
font.pointSize: Style.fontSizeM * scaling
// When forced shown, use pill text color; otherwise accent color when hovered
color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface)

View file

@ -13,4 +13,5 @@ Text {
font.kerning: true
color: Color.mOnSurface
renderType: Text.QtRendering
verticalAlignment: Text.AlignVCenter
}