OsdTab: move all OSD related settings into their own tab

OSD: add Left/Right Center options (will display vertically)
TablerIcons: add OSD Tab icon
i18n: added translation to all files for OSDTab (generated)
This commit is contained in:
Ly-sec
2025-09-26 15:05:53 +02:00
parent 9d927bd7fc
commit 695d002d6a
13 changed files with 1940 additions and 78 deletions
+183 -63
View File
@@ -7,7 +7,7 @@ import qs.Commons
import qs.Services
import qs.Widgets
// Unified OSD component - handles both volume and brightness with a single instance
// Unified OSD component
// Loader activates only when showing OSD, deactivates when hidden to save resources
Variants {
model: Quickshell.screens
@@ -22,7 +22,7 @@ Variants {
property ListModel notificationModel: NotificationService.activeList
// If no notification display activated in settings, then show them all
property bool canShowOnThisScreen: Settings.isLoaded && modelData && (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0))
property bool canShowOnThisScreen: Settings.isLoaded && modelData && (Settings.data.osd.monitors.includes(modelData.name) || (Settings.data.osd.monitors.length === 0))
// Loader is only active when actually showing something
active: false
@@ -71,9 +71,13 @@ Variants {
// Get display percentage
function getDisplayPercentage() {
if (currentOSDType === "volume") {
return isMuted ? "0%" : Math.round(currentVolume * 100) + "%"
if (isMuted)
return "0%"
const pct = Math.round(Math.min(1.0, currentVolume) * 100)
return pct + "%"
} else if (currentOSDType === "brightness") {
return Math.round(currentBrightness * 100) + "%"
const pct = Math.round(Math.min(1.0, currentBrightness) * 100)
return pct + "%"
}
return ""
}
@@ -83,8 +87,6 @@ Variants {
if (currentOSDType === "volume") {
if (isMuted)
return Color.mError
if (currentVolume > 1.0)
return Color.mError
return Color.mPrimary
}
return Color.mPrimary
@@ -99,14 +101,23 @@ Variants {
}
sourceComponent: PanelWindow {
id: panel
screen: modelData
readonly property string location: (Settings.isLoaded && Settings.data && Settings.data.notifications && Settings.data.notifications.location) ? Settings.data.notifications.location : "top_right"
readonly property string location: (Settings.isLoaded && Settings.data && Settings.data.osd && Settings.data.osd.location) ? Settings.data.osd.location : "top_right"
readonly property bool isTop: (location === "top") || (location.length >= 3 && location.substring(0, 3) === "top")
readonly property bool isBottom: (location === "bottom") || (location.length >= 6 && location.substring(0, 6) === "bottom")
readonly property bool isLeft: location.indexOf("_left") >= 0
readonly property bool isRight: location.indexOf("_right") >= 0
readonly property bool isLeft: (location.indexOf("_left") >= 0) || (location === "left")
readonly property bool isRight: (location.indexOf("_right") >= 0) || (location === "right")
readonly property bool isCentered: (location === "top" || location === "bottom")
readonly property bool verticalMode: (location === "left" || location === "right")
readonly property int hWidth: Math.round(320 * root.scaling)
readonly property int hHeight: Math.round(64 * root.scaling)
// Ensure an even width to keep the vertical bar perfectly centered
readonly property int barThickness: (function () {
const base = Math.max(6, Math.round(6 * root.scaling))
return (base % 2 === 0) ? base : base + 1
})()
// Anchor selection based on location (window edges)
anchors.top: isTop
@@ -159,7 +170,7 @@ Variants {
return base
}
implicitWidth: 320 * root.scaling
implicitWidth: verticalMode ? hHeight : hWidth
implicitHeight: osdItem.height
color: Color.transparent
@@ -171,16 +182,20 @@ Variants {
id: osdItem
width: parent.width
height: Math.round(contentLayout.implicitHeight + Style.marginL * 2 * root.scaling)
height: panel.verticalMode ? panel.hWidth : Math.round(64 * root.scaling)
radius: Style.radiusL * root.scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(2, Style.borderM * root.scaling)
border.width: (function () {
const bw = Math.max(2, Math.round(Style.borderM * root.scaling))
return (bw % 2 === 0) ? bw : bw + 1
})()
visible: false
opacity: 0
scale: 0.85
anchors.horizontalCenter: parent.horizontalCenter
anchors.horizontalCenter: verticalMode ? undefined : parent.horizontalCenter
anchors.verticalCenter: verticalMode ? parent.verticalCenter : undefined
Behavior on opacity {
NumberAnimation {
@@ -200,7 +215,7 @@ Variants {
Timer {
id: hideTimer
interval: 2000
interval: Settings.data.osd.autoHideMs
onTriggered: osdItem.hide()
}
@@ -216,52 +231,30 @@ Variants {
}
}
RowLayout {
id: contentLayout
Loader {
id: contentLoader
anchors.fill: parent
anchors.margins: Style.marginM * root.scaling
spacing: Style.marginM * root.scaling
active: true
sourceComponent: verticalMode ? verticalContent : horizontalContent
}
NIcon {
icon: root.getIcon()
color: root.getIconColor()
font.pointSize: Style.fontSizeXL * root.scaling
Layout.alignment: Qt.AlignVCenter
Component {
id: horizontalContent
Item {
anchors.fill: parent
// Smooth icon transitions
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
}
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Style.marginM * root.scaling
spacing: Style.marginM * root.scaling
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginXS * root.scaling
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.round(6 * root.scaling)
radius: Math.round(3 * root.scaling)
color: Color.mSurfaceVariant
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * Math.min(1.0, root.getCurrentValue())
radius: parent.radius
color: root.getProgressColor()
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
NIcon {
icon: root.getIcon()
color: root.getIconColor()
font.pointSize: Style.fontSizeXL * root.scaling
anchors.verticalCenter: parent.verticalCenter
Behavior on color {
ColorAnimation {
@@ -270,14 +263,141 @@ Variants {
}
}
}
// Progress bar with calculated width
Rectangle {
width: Math.round(220 * root.scaling)
height: panel.barThickness
radius: Math.round(panel.barThickness / 2)
color: Color.mSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * Math.min(1.0, root.getCurrentValue())
radius: parent.radius
color: root.getProgressColor()
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
}
}
// Percentage text
NText {
text: root.getDisplayPercentage()
color: Color.mOnSurface
font.pointSize: Style.fontSizeS * root.scaling
font.family: Settings.data.ui.fontFixed
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
width: Math.round(50 * root.scaling)
}
}
}
}
Component {
id: verticalContent
ColumnLayout {
// Ensure inner padding respects the rounded corners; avoid clipping the icon/text
property int vMargin: (function () {
const styleMargin = Math.round(Style.marginL * root.scaling)
const cornerGuard = Math.round(osdItem.radius)
return Math.max(styleMargin, cornerGuard)
})()
property int vMarginTop: Math.max(Math.round(osdItem.radius), Math.round(Style.marginS * root.scaling))
property int balanceDelta: Math.round(Style.marginS * root.scaling)
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: vMarginTop
anchors.bottomMargin: vMargin
width: (function () {
const w = parent.width - (vMargin * 2)
return (w % 2 === 0) ? w : w - 1
})()
spacing: Math.round(Style.marginS * root.scaling)
// Percentage text at top
Item {
Layout.fillWidth: true
Layout.preferredHeight: percentText.implicitHeight
NText {
id: percentText
text: root.getDisplayPercentage()
color: Color.mOnSurface
font.pointSize: Style.fontSizeS * root.scaling
font.family: Settings.data.ui.fontFixed
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
NText {
text: root.getDisplayPercentage()
color: Color.mOnSurface
font.pointSize: Style.fontSizeS * root.scaling
Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: Math.round(32 * root.scaling)
// Progress bar
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.bottom: parent.bottom
width: panel.barThickness
radius: Math.round(panel.barThickness / 2)
color: Color.mSurfaceVariant
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: parent.height * Math.min(1.0, root.getCurrentValue())
radius: parent.radius
color: root.getProgressColor()
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
}
}
}
// Icon at bottom
NIcon {
icon: root.getIcon()
color: root.getIconColor()
font.pointSize: Style.fontSizeXL * root.scaling
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.bottomMargin: vMargin + Math.round(Style.marginM * root.scaling) + balanceDelta
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
}
}
}
@@ -387,7 +507,7 @@ Variants {
function showOSD(type) {
// Check if OSD is enabled in settings and can show on this screen
if (!Settings.data.notifications.enableOSD || !canShowOnThisScreen) {
if (!Settings.data.osd.enabled || !canShowOnThisScreen) {
return
}
+10
View File
@@ -29,6 +29,7 @@ NPanel {
Audio,
Bar,
ColorScheme,
OSD,
Display,
Dock,
General,
@@ -70,6 +71,10 @@ NPanel {
id: displayTab
DisplayTab {}
}
Component {
id: osdTab
OsdTab {}
}
Component {
id: networkTab
NetworkTab {}
@@ -139,6 +144,11 @@ NPanel {
"label": "settings.display.title",
"icon": "settings-display",
"source": displayTab
}, {
"id": SettingsPanel.Tab.OSD,
"label": "settings.osd.title",
"icon": "settings-osd",
"source": osdTab
}, {
"id": SettingsPanel.Tab.Notifications,
"label": "settings.notifications.title",
+1 -6
View File
@@ -72,12 +72,7 @@ ColumnLayout {
onToggled: checked => Settings.data.notifications.alwaysOnTop = checked
}
NToggle {
label: I18n.tr("settings.notifications.settings.enable-osd.label")
description: I18n.tr("settings.notifications.settings.enable-osd.description")
checked: Settings.data.notifications.enableOSD
onToggled: checked => Settings.data.notifications.enableOSD = checked
}
// OSD settings moved to the dedicated OSD tab
}
NDivider {
+139
View File
@@ -0,0 +1,139 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
// Helper functions to update arrays immutably
function addMonitor(list, name) {
const arr = (list || []).slice()
if (!arr.includes(name))
arr.push(name)
return arr
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name
})
}
// Display
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NComboBox {
label: I18n.tr("settings.osd.location.label")
description: I18n.tr("settings.osd.location.description")
model: [{
"key": "top",
"name": I18n.tr("options.osd.position.top_center")
}, {
"key": "top_left",
"name": I18n.tr("options.osd.position.top_left")
}, {
"key": "top_right",
"name": I18n.tr("options.osd.position.top_right")
}, {
"key": "bottom",
"name": I18n.tr("options.osd.position.bottom_center")
}, {
"key": "bottom_left",
"name": I18n.tr("options.osd.position.bottom_left")
}, {
"key": "bottom_right",
"name": I18n.tr("options.osd.position.bottom_right")
}, {
"key": "left",
"name": I18n.tr("options.osd.position.center_left")
}, {
"key": "right",
"name": I18n.tr("options.osd.position.center_right")
}]
currentKey: Settings.data.osd.location || "top_right"
onSelected: key => Settings.data.osd.location = key
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// General
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.osd.section.general.label")
description: I18n.tr("settings.osd.section.general.description")
}
NToggle {
label: I18n.tr("settings.osd.enabled.label")
description: I18n.tr("settings.osd.enabled.description")
checked: Settings.data.osd.enabled
onToggled: checked => Settings.data.osd.enabled = checked
}
NLabel {
label: I18n.tr("settings.osd.duration.auto-hide.label")
description: I18n.tr("settings.osd.duration.auto-hide.description")
}
NValueSlider {
Layout.fillWidth: true
from: 500
to: 5000
stepSize: 100
value: Settings.data.osd.autoHideMs
onMoved: value => Settings.data.osd.autoHideMs = value
text: Math.round(Settings.data.osd.autoHideMs / 1000 * 10) / 10 + "s"
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Monitor Configuration
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.osd.monitors.section.label")
description: I18n.tr("settings.osd.monitors.section.description")
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || I18n.tr("system.unknown")
description: I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width,
"height": modelData.height
})
checked: (Settings.data.osd.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.osd.monitors = addMonitor(Settings.data.osd.monitors, modelData.name)
} else {
Settings.data.osd.monitors = removeMonitor(Settings.data.osd.monitors, modelData.name)
}
}
}
}
}
}