Settings: Subtabs with horizontal scrolling

This commit is contained in:
Lemmy
2026-01-04 23:40:32 -05:00
parent 68425ef8c8
commit 5e690ed766
43 changed files with 4671 additions and 4202 deletions
+54 -1
View File
@@ -1059,6 +1059,11 @@
"volumes": "Volumes"
}
},
"tabs": {
"devices": "Devices",
"media": "Media",
"volumes": "Volumes"
},
"title": "Audio",
"volumes": {
"input-volume": {
@@ -1146,6 +1151,11 @@
"label": "Monitors display"
}
},
"tabs": {
"appearance": "Appearance",
"monitors": "Monitors",
"widgets": "Widgets"
},
"title": "Bar",
"tray": {
"back": "Back",
@@ -1343,6 +1353,10 @@
}
}
},
"tabs": {
"colors": "Colors",
"templates": "Templates"
},
"title": "Color Scheme"
},
"control-center": {
@@ -1622,6 +1636,10 @@
"night-description": "Controls the temperature during nighttime."
}
},
"tabs": {
"brightness": "Brightness",
"night-light": "Night Light"
},
"title": "Display"
},
"dock": {
@@ -1688,6 +1706,10 @@
"label": "Monitor display"
}
},
"tabs": {
"appearance": "Appearance",
"monitors": "Monitors"
},
"title": "Dock"
},
"general": {
@@ -2137,6 +2159,12 @@
"label": "Sound volume"
}
},
"tabs": {
"general": "General",
"history": "History",
"sounds": "Sounds",
"toast": "Toast"
},
"title": "Notifications",
"toast": {
"keyboard": {
@@ -2485,9 +2513,18 @@
"warning": "Warning threshold"
},
"thresholds-section": {
"description": "Adjust warning/critical thresholds and polling intervals for each system metric.",
"description": "Adjust warning/critical thresholds for each system metric.",
"label": "Thresholds"
},
"polling-section": {
"description": "Configure how often each system metric is updated.",
"label": "Polling intervals"
},
"tabs": {
"general": "General",
"polling": "Polling",
"thresholds": "Thresholds"
},
"title": "System Monitor",
"use-custom-highlight-colors": {
"description": "When disabled, theme default highlight colors are used.",
@@ -2567,10 +2604,21 @@
},
"label": "Drop shadows"
},
"tabs": {
"appearance": "Appearance",
"panels": "Panels",
"screen-corners": "Screen Corners"
},
"title": "User Interface",
"tooltips": {
"description": "Enable or disable tooltips throughout the interface.",
"label": "Show tooltips"
},
"appearance": {
"section": {
"description": "Customize visual elements like tooltips, borders, and shadows.",
"label": "Appearance"
}
}
},
"wallpaper": {
@@ -2669,6 +2717,11 @@
"label": "Position"
}
},
"tabs": {
"automation": "Automation",
"look-feel": "Look & feel",
"settings": "Settings"
},
"title": "Wallpaper"
}
},
@@ -4,8 +4,16 @@ import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Panels.Settings.Tabs
import qs.Modules.Panels.Settings.Tabs.Audio
import qs.Modules.Panels.Settings.Tabs.Bar
import qs.Modules.Panels.Settings.Tabs.ColorScheme
import qs.Modules.Panels.Settings.Tabs.Display
import qs.Modules.Panels.Settings.Tabs.Dock
import qs.Modules.Panels.Settings.Tabs.Notifications
import qs.Modules.Panels.Settings.Tabs.SessionMenu
import qs.Modules.Panels.Settings.Tabs.SystemMonitor
import qs.Modules.Panels.Settings.Tabs.UserInterface
import qs.Modules.Panels.Settings.Tabs.Wallpaper
import qs.Services.System
import qs.Services.UI
import qs.Widgets
@@ -0,0 +1,47 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("settings.audio.tabs.volumes")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.audio.tabs.devices")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("settings.audio.tabs.media")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
VolumesSubTab {}
DevicesSubTab {}
MediaSubTab {}
}
}
@@ -0,0 +1,75 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Services.Media
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.audio.devices.section.label")
description: I18n.tr("settings.audio.devices.section.description")
}
// Output Devices
ButtonGroup {
id: sinks
}
ColumnLayout {
spacing: Style.marginXS
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL
NLabel {
label: I18n.tr("settings.audio.devices.output-device.label")
description: I18n.tr("settings.audio.devices.output-device.description")
}
Repeater {
model: AudioService.sinks
NRadioButton {
ButtonGroup.group: sinks
required property PwNode modelData
text: modelData.description
checked: AudioService.sink?.id === modelData.id
onClicked: {
AudioService.setAudioSink(modelData);
}
Layout.fillWidth: true
}
}
}
// Input Devices
ButtonGroup {
id: sources
}
ColumnLayout {
spacing: Style.marginXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.audio.devices.input-device.label")
description: I18n.tr("settings.audio.devices.input-device.description")
}
Repeater {
model: AudioService.sources
NRadioButton {
ButtonGroup.group: sources
required property PwNode modelData
text: modelData.description
checked: AudioService.source?.id === modelData.id
onClicked: AudioService.setAudioSource(modelData)
Layout.fillWidth: true
}
}
}
}
@@ -0,0 +1,191 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Media
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.audio.media.section.label")
description: I18n.tr("settings.audio.media.section.description")
}
// Preferred player
NTextInput {
label: I18n.tr("settings.audio.media.primary-player.label")
description: I18n.tr("settings.audio.media.primary-player.description")
placeholderText: I18n.tr("settings.audio.media.primary-player.placeholder")
text: Settings.data.audio.preferredPlayer
isSettings: true
defaultValue: Settings.getDefaultValue("audio.preferredPlayer")
onTextChanged: {
Settings.data.audio.preferredPlayer = text;
MediaService.updateCurrentPlayer();
}
}
// Blacklist editor
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NTextInputButton {
id: blacklistInput
label: I18n.tr("settings.audio.media.excluded-player.label")
description: I18n.tr("settings.audio.media.excluded-player.description")
placeholderText: I18n.tr("settings.audio.media.excluded-player.placeholder")
buttonIcon: "add"
Layout.fillWidth: true
onButtonClicked: {
const val = (blacklistInput.text || "").trim();
if (val !== "") {
const arr = (Settings.data.audio.mprisBlacklist || []);
if (!arr.find(x => String(x).toLowerCase() === val.toLowerCase())) {
Settings.data.audio.mprisBlacklist = [...arr, val];
blacklistInput.text = "";
MediaService.updateCurrentPlayer();
}
}
}
}
// Current blacklist entries
Flow {
Layout.fillWidth: true
Layout.leftMargin: Style.marginS
spacing: Style.marginS
Repeater {
model: Settings.data.audio.mprisBlacklist
delegate: Rectangle {
required property string modelData
property real pad: Style.marginS
color: Qt.alpha(Color.mOnSurface, 0.125)
border.color: Qt.alpha(Color.mOnSurface, Style.opacityLight)
border.width: Style.borderS
RowLayout {
id: chipRow
spacing: Style.marginXS
anchors.fill: parent
anchors.margins: pad
NText {
text: modelData
color: Color.mOnSurface
pointSize: Style.fontSizeS
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: Style.marginXS
onClicked: {
const arr = (Settings.data.audio.mprisBlacklist || []);
const idx = arr.findIndex(x => String(x) === modelData);
if (idx >= 0) {
arr.splice(idx, 1);
Settings.data.audio.mprisBlacklist = arr;
MediaService.updateCurrentPlayer();
}
}
}
}
implicitWidth: chipRow.implicitWidth + pad * 2
implicitHeight: Math.max(chipRow.implicitHeight + pad * 2, Style.baseWidgetSize * 0.8)
radius: Style.radiusM
}
}
}
}
// Audio Visualizer section
NComboBox {
label: I18n.tr("settings.audio.media.visualizer-type.label")
description: I18n.tr("settings.audio.media.visualizer-type.description")
model: [
{
"key": "none",
"name": I18n.tr("options.visualizer-types.none")
},
{
"key": "linear",
"name": I18n.tr("options.visualizer-types.linear")
},
{
"key": "mirrored",
"name": I18n.tr("options.visualizer-types.mirrored")
},
{
"key": "wave",
"name": I18n.tr("options.visualizer-types.wave")
}
]
currentKey: Settings.data.audio.visualizerType
isSettings: true
defaultValue: Settings.getDefaultValue("audio.visualizerType")
onSelected: key => Settings.data.audio.visualizerType = key
}
NComboBox {
label: I18n.tr("settings.audio.media.frame-rate.label")
description: I18n.tr("settings.audio.media.frame-rate.description")
model: [
{
"key": "30",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "30"
})
},
{
"key": "60",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "60"
})
},
{
"key": "100",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "100"
})
},
{
"key": "120",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "120"
})
},
{
"key": "144",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "144"
})
},
{
"key": "165",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "165"
})
},
{
"key": "240",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "240"
})
}
]
currentKey: Settings.data.audio.cavaFrameRate
isSettings: true
defaultValue: Settings.getDefaultValue("audio.cavaFrameRate")
onSelected: key => Settings.data.audio.cavaFrameRate = key
}
}
@@ -0,0 +1,162 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Media
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property real localVolume: AudioService.volume
Connections {
target: AudioService
function onSinkChanged() {
localVolume = AudioService.volume;
}
function onVolumeChanged() {
localVolume = AudioService.volume;
}
}
Connections {
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
function onVolumeChanged() {
localVolume = AudioService.volume;
}
}
NHeader {
label: I18n.tr("settings.audio.volumes.section.label")
description: I18n.tr("settings.audio.volumes.section.description")
}
// Master Volume
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.audio.volumes.output-volume.label")
description: I18n.tr("settings.audio.volumes.output-volume.description")
}
Timer {
interval: 100
running: true
repeat: true
onTriggered: {
if (!AudioService.isSwitchingSink && Math.abs(localVolume - AudioService.volume) >= 0.01) {
AudioService.setVolume(localVolume);
}
}
}
NValueSlider {
Layout.fillWidth: true
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: localVolume
stepSize: 0.01
text: Math.round(AudioService.volume * 100) + "%"
onMoved: value => localVolume = value
}
}
// Mute Toggle
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("settings.audio.volumes.mute-output.label")
description: I18n.tr("settings.audio.volumes.mute-output.description")
checked: AudioService.muted
onToggled: checked => {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = checked;
}
}
}
}
// Input Volume
ColumnLayout {
spacing: Style.marginXS
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.audio.volumes.input-volume.label")
description: I18n.tr("settings.audio.volumes.input-volume.description")
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: AudioService.inputVolume
stepSize: 0.01
text: Math.round(AudioService.inputVolume * 100) + "%"
onMoved: value => AudioService.setInputVolume(value)
}
}
// Input Mute Toggle
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("settings.audio.volumes.mute-input.label")
description: I18n.tr("settings.audio.volumes.mute-input.description")
checked: AudioService.inputMuted
onToggled: checked => AudioService.setInputMuted(checked)
}
}
// Volume Step Size
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NSpinBox {
Layout.fillWidth: true
label: I18n.tr("settings.audio.volumes.step-size.label")
description: I18n.tr("settings.audio.volumes.step-size.description")
minimum: 1
maximum: 25
value: Settings.data.audio.volumeStep
stepSize: 1
suffix: "%"
isSettings: true
defaultValue: Settings.getDefaultValue("audio.volumeStep")
onValueChanged: Settings.data.audio.volumeStep = value
}
}
// Raise maximum volume above 100%
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("settings.audio.volumes.volume-overdrive.label")
description: I18n.tr("settings.audio.volumes.volume-overdrive.description")
checked: Settings.data.audio.volumeOverdrive
isSettings: true
defaultValue: Settings.getDefaultValue("audio.volumeOverdrive")
onToggled: checked => Settings.data.audio.volumeOverdrive = checked
}
}
// External mixer command
NTextInput {
label: I18n.tr("settings.audio.external-mixer.label")
description: I18n.tr("settings.audio.external-mixer.description")
placeholderText: I18n.tr("settings.audio.external-mixer.placeholder")
text: Settings.data.audio.externalMixer
isSettings: true
defaultValue: Settings.getDefaultValue("audio.externalMixer")
onTextChanged: Settings.data.audio.externalMixer = text
}
}
-445
View File
@@ -1,445 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Services.Media
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
property real localVolume: AudioService.volume
Connections {
target: AudioService
function onSinkChanged() {
// Immediately update local volume when device changes to prevent old value from being applied
localVolume = AudioService.volume;
}
function onVolumeChanged() {
localVolume = AudioService.volume;
}
}
Connections {
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
function onVolumeChanged() {
localVolume = AudioService.volume;
}
}
NHeader {
label: I18n.tr("settings.audio.volumes.section.label")
description: I18n.tr("settings.audio.volumes.section.description")
}
// Master Volume
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.audio.volumes.output-volume.label")
description: I18n.tr("settings.audio.volumes.output-volume.description")
}
// Pipewire seems a bit finicky, if we spam too many volume changes it breaks easily
// Probably because they have some quick fades in and out to avoid clipping
// We use a timer to space out the updates, to avoid lock up
Timer {
interval: 100
running: true
repeat: true
onTriggered: {
// Don't set volume if device is switching - wait for new device's volume to be read
if (!AudioService.isSwitchingSink && Math.abs(localVolume - AudioService.volume) >= 0.01) {
AudioService.setVolume(localVolume);
}
}
}
NValueSlider {
Layout.fillWidth: true
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: localVolume
stepSize: 0.01
text: Math.round(AudioService.volume * 100) + "%"
onMoved: value => localVolume = value
}
}
// Mute Toggle
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("settings.audio.volumes.mute-output.label")
description: I18n.tr("settings.audio.volumes.mute-output.description")
checked: AudioService.muted
onToggled: checked => {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = checked;
}
}
}
}
// Input Volume
ColumnLayout {
spacing: Style.marginXS
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.audio.volumes.input-volume.label")
description: I18n.tr("settings.audio.volumes.input-volume.description")
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: AudioService.inputVolume
stepSize: 0.01
text: Math.round(AudioService.inputVolume * 100) + "%"
onMoved: value => AudioService.setInputVolume(value)
}
}
// Input Mute Toggle
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("settings.audio.volumes.mute-input.label")
description: I18n.tr("settings.audio.volumes.mute-input.description")
checked: AudioService.inputMuted
onToggled: checked => AudioService.setInputMuted(checked)
}
}
// Volume Step Size
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NSpinBox {
Layout.fillWidth: true
label: I18n.tr("settings.audio.volumes.step-size.label")
description: I18n.tr("settings.audio.volumes.step-size.description")
minimum: 1
maximum: 25
value: Settings.data.audio.volumeStep
stepSize: 1
suffix: "%"
isSettings: true
defaultValue: Settings.getDefaultValue("audio.volumeStep")
onValueChanged: Settings.data.audio.volumeStep = value
}
}
// Raise maximum volume above 100%
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("settings.audio.volumes.volume-overdrive.label")
description: I18n.tr("settings.audio.volumes.volume-overdrive.description")
checked: Settings.data.audio.volumeOverdrive
isSettings: true
defaultValue: Settings.getDefaultValue("audio.volumeOverdrive")
onToggled: checked => Settings.data.audio.volumeOverdrive = checked
}
}
// External mixer command
NTextInput {
label: I18n.tr("settings.audio.external-mixer.label")
description: I18n.tr("settings.audio.external-mixer.description")
placeholderText: I18n.tr("settings.audio.external-mixer.placeholder")
text: Settings.data.audio.externalMixer
isSettings: true
defaultValue: Settings.getDefaultValue("audio.externalMixer")
onTextChanged: Settings.data.audio.externalMixer = text
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// AudioService Devices
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.audio.devices.section.label")
description: I18n.tr("settings.audio.devices.section.description")
}
// -------------------------------
// Output Devices
ButtonGroup {
id: sinks
}
ColumnLayout {
spacing: Style.marginXS
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL
NLabel {
label: I18n.tr("settings.audio.devices.output-device.label")
description: I18n.tr("settings.audio.devices.output-device.description")
}
Repeater {
model: AudioService.sinks
NRadioButton {
ButtonGroup.group: sinks
required property PwNode modelData
text: modelData.description
checked: AudioService.sink?.id === modelData.id
onClicked: {
AudioService.setAudioSink(modelData);
localVolume = AudioService.volume;
}
Layout.fillWidth: true
}
}
}
// -------------------------------
// Input Devices
ButtonGroup {
id: sources
}
ColumnLayout {
spacing: Style.marginXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.audio.devices.input-device.label")
description: I18n.tr("settings.audio.devices.input-device.description")
}
Repeater {
model: AudioService.sources
//Layout.fillWidth: true
NRadioButton {
ButtonGroup.group: sources
required property PwNode modelData
text: modelData.description
checked: AudioService.source?.id === modelData.id
onClicked: AudioService.setAudioSource(modelData)
Layout.fillWidth: true
}
}
}
}
// Divider
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Media Player Preferences
ColumnLayout {
spacing: Style.marginL
NHeader {
label: I18n.tr("settings.audio.media.section.label")
description: I18n.tr("settings.audio.media.section.description")
}
// Preferred player
NTextInput {
label: I18n.tr("settings.audio.media.primary-player.label")
description: I18n.tr("settings.audio.media.primary-player.description")
placeholderText: I18n.tr("settings.audio.media.primary-player.placeholder")
text: Settings.data.audio.preferredPlayer
isSettings: true
defaultValue: Settings.getDefaultValue("audio.preferredPlayer")
onTextChanged: {
Settings.data.audio.preferredPlayer = text;
MediaService.updateCurrentPlayer();
}
}
// Blacklist editor
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NTextInputButton {
id: blacklistInput
label: I18n.tr("settings.audio.media.excluded-player.label")
description: I18n.tr("settings.audio.media.excluded-player.description")
placeholderText: I18n.tr("settings.audio.media.excluded-player.placeholder")
buttonIcon: "add"
Layout.fillWidth: true
onButtonClicked: {
const val = (blacklistInput.text || "").trim();
if (val !== "") {
const arr = (Settings.data.audio.mprisBlacklist || []);
if (!arr.find(x => String(x).toLowerCase() === val.toLowerCase())) {
Settings.data.audio.mprisBlacklist = [...arr, val];
blacklistInput.text = "";
MediaService.updateCurrentPlayer();
}
}
}
}
// Current blacklist entries
Flow {
Layout.fillWidth: true
Layout.leftMargin: Style.marginS
spacing: Style.marginS
Repeater {
model: Settings.data.audio.mprisBlacklist
delegate: Rectangle {
required property string modelData
// Padding around the inner row
property real pad: Style.marginS
// Visuals
color: Qt.alpha(Color.mOnSurface, 0.125)
border.color: Qt.alpha(Color.mOnSurface, Style.opacityLight)
border.width: Style.borderS
// Content
RowLayout {
id: chipRow
spacing: Style.marginXS
anchors.fill: parent
anchors.margins: pad
NText {
text: modelData
color: Color.mOnSurface
pointSize: Style.fontSizeS
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: Style.marginXS
onClicked: {
const arr = (Settings.data.audio.mprisBlacklist || []);
const idx = arr.findIndex(x => String(x) === modelData);
if (idx >= 0) {
arr.splice(idx, 1);
Settings.data.audio.mprisBlacklist = arr;
MediaService.updateCurrentPlayer();
}
}
}
}
// Intrinsic size derived from inner row + padding
implicitWidth: chipRow.implicitWidth + pad * 2
implicitHeight: Math.max(chipRow.implicitHeight + pad * 2, Style.baseWidgetSize * 0.8)
radius: Style.radiusM
}
}
}
}
// AudioService Visualizer section
NComboBox {
label: I18n.tr("settings.audio.media.visualizer-type.label")
description: I18n.tr("settings.audio.media.visualizer-type.description")
model: [
{
"key": "none",
"name": I18n.tr("options.visualizer-types.none")
},
{
"key": "linear",
"name": I18n.tr("options.visualizer-types.linear")
},
{
"key": "mirrored",
"name": I18n.tr("options.visualizer-types.mirrored")
},
{
"key": "wave",
"name": I18n.tr("options.visualizer-types.wave")
}
]
currentKey: Settings.data.audio.visualizerType
isSettings: true
defaultValue: Settings.getDefaultValue("audio.visualizerType")
onSelected: key => Settings.data.audio.visualizerType = key
}
NComboBox {
label: I18n.tr("settings.audio.media.frame-rate.label")
description: I18n.tr("settings.audio.media.frame-rate.description")
model: [
{
"key": "30",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "30"
})
},
{
"key": "60",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "60"
})
},
{
"key": "100",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "100"
})
},
{
"key": "120",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "120"
})
},
{
"key": "144",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "144"
})
},
{
"key": "165",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "165"
})
},
{
"key": "240",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "240"
})
}
]
currentKey: Settings.data.audio.cavaFrameRate
isSettings: true
defaultValue: Settings.getDefaultValue("audio.cavaFrameRate")
onSelected: key => Settings.data.audio.cavaFrameRate = key
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
}
@@ -0,0 +1,213 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.bar.appearance.section.label")
description: I18n.tr("settings.bar.appearance.section.description")
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.position.label")
description: I18n.tr("settings.bar.appearance.position.description")
model: [
{
"key": "top",
"name": I18n.tr("options.bar.position.top")
},
{
"key": "bottom",
"name": I18n.tr("options.bar.position.bottom")
},
{
"key": "left",
"name": I18n.tr("options.bar.position.left")
},
{
"key": "right",
"name": I18n.tr("options.bar.position.right")
}
]
currentKey: Settings.data.bar.position
isSettings: true
defaultValue: Settings.getDefaultValue("bar.position")
onSelected: key => Settings.data.bar.position = key
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.density.label")
description: I18n.tr("settings.bar.appearance.density.description")
model: [
{
"key": "mini",
"name": I18n.tr("options.bar.density.mini")
},
{
"key": "compact",
"name": I18n.tr("options.bar.density.compact")
},
{
"key": "default",
"name": I18n.tr("options.bar.density.default")
},
{
"key": "comfortable",
"name": I18n.tr("options.bar.density.comfortable")
},
{
"key": "spacious",
"name": I18n.tr("options.bar.density.spacious")
}
]
currentKey: Settings.data.bar.density
isSettings: true
defaultValue: Settings.getDefaultValue("bar.density")
onSelected: key => Settings.data.bar.density = key
}
NToggle {
label: I18n.tr("settings.bar.appearance.use-separate-opacity.label")
description: I18n.tr("settings.bar.appearance.use-separate-opacity.description")
checked: Settings.data.bar.useSeparateOpacity
isSettings: true
defaultValue: Settings.getDefaultValue("bar.useSeparateOpacity")
onToggled: checked => Settings.data.bar.useSeparateOpacity = checked
}
NValueSlider {
Layout.fillWidth: true
visible: Settings.data.bar.useSeparateOpacity
label: I18n.tr("settings.bar.appearance.background-opacity.label")
description: I18n.tr("settings.bar.appearance.background-opacity.description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.backgroundOpacity
isSettings: true
defaultValue: Settings.getDefaultValue("bar.backgroundOpacity")
onMoved: value => Settings.data.bar.backgroundOpacity = value
text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.show-outline.label")
description: I18n.tr("settings.bar.appearance.show-outline.description")
checked: Settings.data.bar.showOutline
isSettings: true
defaultValue: Settings.getDefaultValue("bar.showOutline")
onToggled: checked => Settings.data.bar.showOutline = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.show-capsule.label")
description: I18n.tr("settings.bar.appearance.show-capsule.description")
checked: Settings.data.bar.showCapsule
isSettings: true
defaultValue: Settings.getDefaultValue("bar.showCapsule")
onToggled: checked => Settings.data.bar.showCapsule = checked
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS
visible: Settings.data.bar.showCapsule
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.capsule-opacity.label")
description: I18n.tr("settings.bar.appearance.capsule-opacity.description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.capsuleOpacity
isSettings: true
defaultValue: Settings.getDefaultValue("bar.capsuleOpacity")
onMoved: value => Settings.data.bar.capsuleOpacity = value
text: Math.floor(Settings.data.bar.capsuleOpacity * 100) + "%"
}
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.floating.label")
description: I18n.tr("settings.bar.appearance.floating.description")
checked: Settings.data.bar.floating
isSettings: true
defaultValue: Settings.getDefaultValue("bar.floating")
onToggled: checked => {
Settings.data.bar.floating = checked;
}
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.outer-corners.label")
description: I18n.tr("settings.bar.appearance.outer-corners.description")
checked: Settings.data.bar.outerCorners
visible: !Settings.data.bar.floating
isSettings: true
defaultValue: Settings.getDefaultValue("bar.outerCorners")
onToggled: checked => Settings.data.bar.outerCorners = checked
}
ColumnLayout {
visible: Settings.data.bar.floating
spacing: Style.marginS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.bar.appearance.margins.label")
description: I18n.tr("settings.bar.appearance.margins.description")
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL
ColumnLayout {
spacing: Style.marginXXS
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.margins.vertical")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.marginVertical
isSettings: true
defaultValue: Settings.getDefaultValue("bar.marginVertical")
onMoved: value => Settings.data.bar.marginVertical = value
text: Math.round(Settings.data.bar.marginVertical * 100) + "%"
}
}
ColumnLayout {
spacing: Style.marginXXS
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.margins.horizontal")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.marginHorizontal
isSettings: true
defaultValue: Settings.getDefaultValue("bar.marginHorizontal")
onMoved: value => Settings.data.bar.marginHorizontal = value
text: Math.ceil(Settings.data.bar.marginHorizontal * 100) + "%"
}
}
}
}
}
+217
View File
@@ -0,0 +1,217 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
// 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;
});
}
// Signal functions for widgets sub-tab
function _addWidgetToSection(widgetId, section) {
var newWidget = {
"id": widgetId
};
if (BarWidgetRegistry.widgetHasUserSettings(widgetId)) {
var metadata = BarWidgetRegistry.widgetMetadata[widgetId];
if (metadata) {
Object.keys(metadata).forEach(function (key) {
if (key !== "allowUserSettings") {
newWidget[key] = metadata[key];
}
});
}
}
Settings.data.bar.widgets[section].push(newWidget);
}
function _removeWidgetFromSection(section, index) {
if (index >= 0 && index < Settings.data.bar.widgets[section].length) {
var newArray = Settings.data.bar.widgets[section].slice();
var removedWidgets = newArray.splice(index, 1);
Settings.data.bar.widgets[section] = newArray;
if (removedWidgets[0].id === "ControlCenter" && BarService.lookupWidget("ControlCenter") === undefined) {
ToastService.showWarning(I18n.tr("toast.missing-control-center.label"), I18n.tr("toast.missing-control-center.description"), 12000);
}
}
}
function _reorderWidgetInSection(section, fromIndex, toIndex) {
if (fromIndex >= 0 && fromIndex < Settings.data.bar.widgets[section].length && toIndex >= 0 && toIndex < Settings.data.bar.widgets[section].length) {
var newArray = Settings.data.bar.widgets[section].slice();
var item = newArray[fromIndex];
newArray.splice(fromIndex, 1);
newArray.splice(toIndex, 0, item);
Settings.data.bar.widgets[section] = newArray;
}
}
function _updateWidgetSettingsInSection(section, index, settings) {
Settings.data.bar.widgets[section][index] = settings;
}
function _moveWidgetBetweenSections(fromSection, index, toSection) {
if (index >= 0 && index < Settings.data.bar.widgets[fromSection].length) {
var widget = Settings.data.bar.widgets[fromSection][index];
var sourceArray = Settings.data.bar.widgets[fromSection].slice();
sourceArray.splice(index, 1);
Settings.data.bar.widgets[fromSection] = sourceArray;
var targetArray = Settings.data.bar.widgets[toSection].slice();
targetArray.push(widget);
Settings.data.bar.widgets[toSection] = targetArray;
}
}
function getWidgetLocations(widgetId) {
if (!BarService)
return [];
const instances = BarService.getAllRegisteredWidgets();
const locations = {};
for (var i = 0; i < instances.length; i++) {
if (instances[i].widgetId === widgetId) {
const section = instances[i].section;
if (section === "left")
locations["arrow-bar-to-left"] = true;
else if (section === "center")
locations["layout-columns"] = true;
else if (section === "right")
locations["arrow-bar-to-right"] = true;
}
}
return Object.keys(locations);
}
function createBadges(isPlugin, locations) {
const badges = [];
if (isPlugin) {
badges.push({
"icon": "plugin",
"color": Color.mSecondary
});
}
locations.forEach(function (location) {
badges.push({
"icon": location,
"color": Color.mOnSurfaceVariant
});
});
return badges;
}
function updateAvailableWidgetsModel() {
availableWidgets.clear();
const widgets = BarWidgetRegistry.getAvailableWidgets();
widgets.forEach(entry => {
const isPlugin = BarWidgetRegistry.isPluginWidget(entry);
let displayName = entry;
if (isPlugin) {
const pluginId = entry.replace("plugin:", "");
const manifest = PluginRegistry.getPluginManifest(pluginId);
if (manifest && manifest.name) {
displayName = manifest.name;
} else {
displayName = pluginId;
}
}
availableWidgets.append({
"key": entry,
"name": displayName,
"badges": createBadges(isPlugin, getWidgetLocations(entry))
});
});
}
ListModel {
id: availableWidgets
}
Component.onCompleted: {
updateAvailableWidgetsModel();
}
Connections {
target: BarService
function onActiveWidgetsChanged() {
updateAvailableWidgetsModel();
}
}
Connections {
target: BarWidgetRegistry
function onPluginWidgetRegistryUpdated() {
updateAvailableWidgetsModel();
}
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("settings.bar.tabs.appearance")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.bar.tabs.widgets")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("settings.bar.tabs.monitors")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
AppearanceSubTab {}
WidgetsSubTab {
availableWidgets: availableWidgets
addWidgetToSection: root._addWidgetToSection
removeWidgetFromSection: root._removeWidgetFromSection
reorderWidgetInSection: root._reorderWidgetInSection
updateWidgetSettingsInSection: root._updateWidgetSettingsInSection
moveWidgetBetweenSections: root._moveWidgetBetweenSections
onOpenPluginSettings: manifest => pluginSettingsDialog.openPluginSettings(manifest)
}
MonitorsSubTab {
addMonitor: root.addMonitor
removeMonitor: root.removeMonitor
}
}
NPluginSettingsPopup {
id: pluginSettingsDialog
parent: Overlay.overlay
showToastOnSave: false
}
}
@@ -0,0 +1,46 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var addMonitor
property var removeMonitor
NHeader {
label: I18n.tr("settings.bar.monitors.section.label")
description: I18n.tr("settings.bar.monitors.section.description")
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || "Unknown"
description: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.bar.monitors = root.addMonitor(Settings.data.bar.monitors, modelData.name);
} else {
Settings.data.bar.monitors = root.removeMonitor(Settings.data.bar.monitors, modelData.name);
}
}
}
}
}
@@ -0,0 +1,85 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var availableWidgets
property var addWidgetToSection
property var removeWidgetFromSection
property var reorderWidgetInSection
property var updateWidgetSettingsInSection
property var moveWidgetBetweenSections
signal openPluginSettings(var manifest)
NHeader {
label: I18n.tr("settings.bar.widgets.section.label")
}
NLabel {
description: I18n.tr("settings.bar.widgets.section.description")
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: Style.marginM
spacing: Style.marginM
// Left Section
NSectionEditor {
sectionName: "Left"
sectionId: "left"
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.left
availableWidgets: root.availableWidgets
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest)
}
// Center Section
NSectionEditor {
sectionName: "Center"
sectionId: "center"
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.center
availableWidgets: root.availableWidgets
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest)
}
// Right Section
NSectionEditor {
sectionName: "Right"
sectionId: "right"
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.right
availableWidgets: root.availableWidgets
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest)
}
}
}
-526
View File
@@ -1,526 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
// 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;
});
}
NHeader {
label: I18n.tr("settings.bar.appearance.section.label")
description: I18n.tr("settings.bar.appearance.section.description")
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.position.label")
description: I18n.tr("settings.bar.appearance.position.description")
model: [
{
"key": "top",
"name": I18n.tr("options.bar.position.top")
},
{
"key": "bottom",
"name": I18n.tr("options.bar.position.bottom")
},
{
"key": "left",
"name": I18n.tr("options.bar.position.left")
},
{
"key": "right",
"name": I18n.tr("options.bar.position.right")
}
]
currentKey: Settings.data.bar.position
isSettings: true
defaultValue: Settings.getDefaultValue("bar.position")
onSelected: key => Settings.data.bar.position = key
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.density.label")
description: I18n.tr("settings.bar.appearance.density.description")
model: [
{
"key": "mini",
"name": I18n.tr("options.bar.density.mini")
},
{
"key": "compact",
"name": I18n.tr("options.bar.density.compact")
},
{
"key": "default",
"name": I18n.tr("options.bar.density.default")
},
{
"key": "comfortable",
"name": I18n.tr("options.bar.density.comfortable")
},
{
"key": "spacious",
"name": I18n.tr("options.bar.density.spacious")
}
]
currentKey: Settings.data.bar.density
isSettings: true
defaultValue: Settings.getDefaultValue("bar.density")
onSelected: key => Settings.data.bar.density = key
}
// Use Separate Bar Opacity Toggle
NToggle {
label: I18n.tr("settings.bar.appearance.use-separate-opacity.label")
description: I18n.tr("settings.bar.appearance.use-separate-opacity.description")
checked: Settings.data.bar.useSeparateOpacity
isSettings: true
defaultValue: Settings.getDefaultValue("bar.useSeparateOpacity")
onToggled: checked => Settings.data.bar.useSeparateOpacity = checked
}
// Bar Background Opacity (only visible when separate opacity is enabled)
NValueSlider {
Layout.fillWidth: true
visible: Settings.data.bar.useSeparateOpacity
label: I18n.tr("settings.bar.appearance.background-opacity.label")
description: I18n.tr("settings.bar.appearance.background-opacity.description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.backgroundOpacity
isSettings: true
defaultValue: Settings.getDefaultValue("bar.backgroundOpacity")
onMoved: value => Settings.data.bar.backgroundOpacity = value
text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.show-outline.label")
description: I18n.tr("settings.bar.appearance.show-outline.description")
checked: Settings.data.bar.showOutline
isSettings: true
defaultValue: Settings.getDefaultValue("bar.showOutline")
onToggled: checked => Settings.data.bar.showOutline = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.show-capsule.label")
description: I18n.tr("settings.bar.appearance.show-capsule.description")
checked: Settings.data.bar.showCapsule
isSettings: true
defaultValue: Settings.getDefaultValue("bar.showCapsule")
onToggled: checked => Settings.data.bar.showCapsule = checked
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS
visible: Settings.data.bar.showCapsule
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.capsule-opacity.label")
description: I18n.tr("settings.bar.appearance.capsule-opacity.description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.capsuleOpacity
isSettings: true
defaultValue: Settings.getDefaultValue("bar.capsuleOpacity")
onMoved: value => Settings.data.bar.capsuleOpacity = value
text: Math.floor(Settings.data.bar.capsuleOpacity * 100) + "%"
}
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.floating.label")
description: I18n.tr("settings.bar.appearance.floating.description")
checked: Settings.data.bar.floating
isSettings: true
defaultValue: Settings.getDefaultValue("bar.floating")
onToggled: checked => {
Settings.data.bar.floating = checked;
}
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.outer-corners.label")
description: I18n.tr("settings.bar.appearance.outer-corners.description")
checked: Settings.data.bar.outerCorners
visible: !Settings.data.bar.floating
isSettings: true
defaultValue: Settings.getDefaultValue("bar.outerCorners")
onToggled: checked => Settings.data.bar.outerCorners = checked
}
// Floating bar options - only show when floating is enabled
ColumnLayout {
visible: Settings.data.bar.floating
spacing: Style.marginS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.bar.appearance.margins.label")
description: I18n.tr("settings.bar.appearance.margins.description")
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL
ColumnLayout {
spacing: Style.marginXXS
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.margins.vertical")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.marginVertical
isSettings: true
defaultValue: Settings.getDefaultValue("bar.marginVertical")
onMoved: value => Settings.data.bar.marginVertical = value
text: Math.round(Settings.data.bar.marginVertical * 100) + "%"
}
}
ColumnLayout {
spacing: Style.marginXXS
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.margins.horizontal")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.marginHorizontal
isSettings: true
defaultValue: Settings.getDefaultValue("bar.marginHorizontal")
onMoved: value => Settings.data.bar.marginHorizontal = value
text: Math.ceil(Settings.data.bar.marginHorizontal * 100) + "%"
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Widgets Management Section
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.bar.widgets.section.label")
}
NLabel {
description: I18n.tr("settings.bar.widgets.section.description")
}
// Bar Sections
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: Style.marginM
spacing: Style.marginM
// Left Section
NSectionEditor {
sectionName: "Left"
sectionId: "left"
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.left
availableWidgets: availableWidgets
onAddWidget: (widgetId, section) => _addWidgetToSection(widgetId, section)
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
onOpenPluginSettingsRequested: manifest => pluginSettingsDialog.openPluginSettings(manifest)
}
// Center Section
NSectionEditor {
sectionName: "Center"
sectionId: "center"
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.center
availableWidgets: availableWidgets
onAddWidget: (widgetId, section) => _addWidgetToSection(widgetId, section)
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
onOpenPluginSettingsRequested: manifest => pluginSettingsDialog.openPluginSettings(manifest)
}
// Right Section
NSectionEditor {
sectionName: "Right"
sectionId: "right"
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.right
availableWidgets: availableWidgets
onAddWidget: (widgetId, section) => _addWidgetToSection(widgetId, section)
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
onOpenPluginSettingsRequested: manifest => pluginSettingsDialog.openPluginSettings(manifest)
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Monitor Configuration
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.bar.monitors.section.label")
description: I18n.tr("settings.bar.monitors.section.description")
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || "Unknown"
description: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name);
} else {
Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name);
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Signal functions
function _addWidgetToSection(widgetId, section) {
var newWidget = {
"id": widgetId
};
if (BarWidgetRegistry.widgetHasUserSettings(widgetId)) {
var metadata = BarWidgetRegistry.widgetMetadata[widgetId];
if (metadata) {
Object.keys(metadata).forEach(function (key) {
if (key !== "allowUserSettings") {
newWidget[key] = metadata[key];
}
});
}
}
Settings.data.bar.widgets[section].push(newWidget);
}
function _removeWidgetFromSection(section, index) {
if (index >= 0 && index < Settings.data.bar.widgets[section].length) {
var newArray = Settings.data.bar.widgets[section].slice();
var removedWidgets = newArray.splice(index, 1);
Settings.data.bar.widgets[section] = newArray;
// Check that we still have a control center
if (removedWidgets[0].id === "ControlCenter" && BarService.lookupWidget("ControlCenter") === undefined) {
ToastService.showWarning(I18n.tr("toast.missing-control-center.label"), I18n.tr("toast.missing-control-center.description"), 12000);
}
}
}
function _reorderWidgetInSection(section, fromIndex, toIndex) {
if (fromIndex >= 0 && fromIndex < Settings.data.bar.widgets[section].length && toIndex >= 0 && toIndex < Settings.data.bar.widgets[section].length) {
// Create a new array to avoid modifying the original
var newArray = Settings.data.bar.widgets[section].slice();
var item = newArray[fromIndex];
newArray.splice(fromIndex, 1);
newArray.splice(toIndex, 0, item);
Settings.data.bar.widgets[section] = newArray;
//Logger.i("BarTab", "Widget reordered. New array:", JSON.stringify(newArray))
}
}
function _updateWidgetSettingsInSection(section, index, settings) {
// Update the widget settings in the Settings data
Settings.data.bar.widgets[section][index] = settings;
//Logger.i("BarTab", `Updated widget settings for ${settings.id} in ${section} section`)
}
function _moveWidgetBetweenSections(fromSection, index, toSection) {
// Get the widget from the source section
if (index >= 0 && index < Settings.data.bar.widgets[fromSection].length) {
var widget = Settings.data.bar.widgets[fromSection][index];
// Remove from source section
var sourceArray = Settings.data.bar.widgets[fromSection].slice();
sourceArray.splice(index, 1);
Settings.data.bar.widgets[fromSection] = sourceArray;
// Add to target section
var targetArray = Settings.data.bar.widgets[toSection].slice();
targetArray.push(widget);
Settings.data.bar.widgets[toSection] = targetArray;
//Logger.i("BarTab", `Moved widget ${widget.id} from ${fromSection} to ${toSection}`)
}
}
// Data model functions
function getWidgetLocations(widgetId) {
if (!BarService)
return [];
const instances = BarService.getAllRegisteredWidgets();
const locations = {};
for (var i = 0; i < instances.length; i++) {
if (instances[i].widgetId === widgetId) {
const section = instances[i].section;
if (section === "left")
locations["arrow-bar-to-left"] = true;
else if (section === "center")
locations["layout-columns"] = true;
else if (section === "right")
locations["arrow-bar-to-right"] = true;
}
}
return Object.keys(locations);
}
function createBadges(isPlugin, locations) {
const badges = [];
// Add plugin badge first (with custom color)
if (isPlugin) {
badges.push({
"icon": "plugin",
"color": Color.mSecondary
});
}
// Add location badges (with default styling)
locations.forEach(function (location) {
badges.push({
"icon": location,
"color": Color.mOnSurfaceVariant
});
});
return badges;
}
function updateAvailableWidgetsModel() {
availableWidgets.clear();
const widgets = BarWidgetRegistry.getAvailableWidgets();
widgets.forEach(entry => {
const isPlugin = BarWidgetRegistry.isPluginWidget(entry);
let displayName = entry;
// For plugin widgets, strip the "plugin:" prefix and try to get the actual plugin name
if (isPlugin) {
const pluginId = entry.replace("plugin:", "");
const manifest = PluginRegistry.getPluginManifest(pluginId);
if (manifest && manifest.name) {
displayName = manifest.name;
} else {
// Fallback: just strip the prefix
displayName = pluginId;
}
}
availableWidgets.append({
"key": entry,
"name": displayName,
"badges": createBadges(isPlugin, getWidgetLocations(entry))
});
});
}
// Base list model for all combo boxes
ListModel {
id: availableWidgets
}
Component.onCompleted: {
updateAvailableWidgetsModel();
}
Connections {
target: BarService
function onActiveWidgetsChanged() {
updateAvailableWidgetsModel();
}
}
// Update available widgets when plugin widgets are registered/unregistered
Connections {
target: BarWidgetRegistry
function onPluginWidgetRegistryUpdated() {
updateAvailableWidgetsModel();
}
}
// Shared Plugin Settings Popup
NPluginSettingsPopup {
id: pluginSettingsDialog
parent: Overlay.overlay
showToastOnSave: false
}
}
@@ -12,15 +12,13 @@ import qs.Widgets
ColumnLayout {
id: root
// Cache for scheme JSON (can be flat or {dark, light})
property var schemeColorsCache: ({})
property int cacheVersion: 0 // Increment to trigger UI updates
spacing: 0
// Time dropdown options (00:00 .. 23:30)
ListModel {
id: timeOptions
}
Component.onCompleted: {
for (var h = 0; h < 24; h++) {
for (var m = 0; m < 60; m += 30) {
@@ -35,82 +33,6 @@ ColumnLayout {
}
}
spacing: Style.marginL
// Helper function to extract scheme name from path
function extractSchemeName(schemePath) {
var pathParts = schemePath.split("/");
var filename = pathParts[pathParts.length - 1];
var schemeName = filename.replace(".json", "");
if (schemeName === "Noctalia-default") {
schemeName = "Noctalia (default)";
} else if (schemeName === "Noctalia-legacy") {
schemeName = "Noctalia (legacy)";
} else if (schemeName === "Tokyo-Night") {
schemeName = "Tokyo Night";
} else if (schemeName === "Rosepine") {
schemeName = "Rose Pine";
}
return schemeName;
}
// Helper function to get color from scheme file (supports dark/light variants)
function getSchemeColor(schemeName, colorKey) {
// Access cache version to create dependency
var _ = cacheVersion;
if (schemeColorsCache[schemeName]) {
var entry = schemeColorsCache[schemeName];
var variant = entry;
// Check if scheme has dark/light variants
if (entry.dark || entry.light) {
variant = Settings.data.colorSchemes.darkMode ? (entry.dark || entry.light) : (entry.light || entry.dark);
}
if (variant && variant[colorKey]) {
return variant[colorKey];
}
}
// Return visible defaults while loading
if (colorKey === "mSurface")
return Color.mSurfaceVariant;
if (colorKey === "mPrimary")
return Color.mPrimary;
if (colorKey === "mSecondary")
return Color.mSecondary;
if (colorKey === "mTertiary")
return Color.mTertiary;
if (colorKey === "mError")
return Color.mError;
return Color.mOnSurfaceVariant;
}
// This function is called by the FileView Repeater when a scheme file is loaded
function schemeLoaded(schemeName, jsonData) {
var value = jsonData || {};
schemeColorsCache[schemeName] = value;
// Force UI update by incrementing cache version
cacheVersion++;
}
// Function to open download popup
function openDownloadPopup() {
downloadPopupLoader.open();
}
// When the list of available schemes changes, clear the cache
Connections {
target: ColorSchemeService
function onSchemesChanged() {
schemeColorsCache = {};
cacheVersion++;
}
}
// Simple process to check if matugen exists
Process {
id: matugenCheck
@@ -131,799 +53,65 @@ ColumnLayout {
stderr: StdioCollector {}
}
// A non-visual Item to host the Repeater that loads the color scheme files
// Download popup
Loader {
id: downloadPopupLoader
active: false
sourceComponent: SchemeDownloader {
parent: Overlay.overlay
}
property bool pendingOpen: false
function open() {
pendingOpen = true;
active = true;
if (item) {
item.open();
pendingOpen = false;
}
}
onItemChanged: {
if (item && pendingOpen) {
item.open();
pendingOpen = false;
}
}
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("settings.color-scheme.tabs.colors")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.color-scheme.tabs.templates")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
id: fileLoaders
visible: false
Repeater {
model: ColorSchemeService.schemes
delegate: Item {
FileView {
path: modelData
blockLoading: false
onLoaded: {
var schemeName = root.extractSchemeName(path);
try {
var jsonData = JSON.parse(text());
root.schemeLoaded(schemeName, jsonData);
} catch (e) {
Logger.w("ColorSchemeTab", "Failed to parse JSON for scheme:", schemeName, e);
root.schemeLoaded(schemeName, null);
}
}
}
}
}
}
// Main Toggles - Dark Mode / Matugen
NHeader {
label: I18n.tr("settings.color-scheme.color-source.section.label")
description: I18n.tr("settings.color-scheme.color-source.section.description")
}
// Dark Mode Toggle
NToggle {
label: I18n.tr("settings.color-scheme.dark-mode.switch.label")
description: I18n.tr("settings.color-scheme.dark-mode.switch.description")
checked: Settings.data.colorSchemes.darkMode
onToggled: checked => {
Settings.data.colorSchemes.darkMode = checked;
root.cacheVersion++; // Force UI update for dark/light variants
}
}
NComboBox {
label: I18n.tr("settings.color-scheme.dark-mode.mode.label")
description: I18n.tr("settings.color-scheme.dark-mode.mode.description")
model: [
{
"name": I18n.tr("settings.color-scheme.dark-mode.mode.off"),
"key": "off"
},
{
"name": I18n.tr("settings.color-scheme.dark-mode.mode.manual"),
"key": "manual"
},
{
"name": I18n.tr("settings.color-scheme.dark-mode.mode.location"),
"key": "location"
}
]
currentKey: Settings.data.colorSchemes.schedulingMode
onSelected: key => {
Settings.data.colorSchemes.schedulingMode = key;
AppThemeService.generate();
}
}
// Manual scheduling
ColumnLayout {
spacing: Style.marginS
visible: Settings.data.colorSchemes.schedulingMode === "manual"
NLabel {
label: I18n.tr("settings.display.night-light.manual-schedule.label")
description: I18n.tr("settings.display.night-light.manual-schedule.description")
}
RowLayout {
Layout.fillWidth: false
spacing: Style.marginS
NText {
text: I18n.tr("settings.display.night-light.manual-schedule.sunrise")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
NComboBox {
model: timeOptions
currentKey: Settings.data.colorSchemes.manualSunrise
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-start")
onSelected: key => Settings.data.colorSchemes.manualSunrise = key
minimumWidth: 120
}
Item {
Layout.preferredWidth: 20
}
NText {
text: I18n.tr("settings.display.night-light.manual-schedule.sunset")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
NComboBox {
model: timeOptions
currentKey: Settings.data.colorSchemes.manualSunset
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-stop")
onSelected: key => Settings.data.colorSchemes.manualSunset = key
minimumWidth: 120
}
}
}
// Use Wallpaper Colors
NToggle {
label: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label")
description: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.description")
enabled: ProgramCheckerService.matugenAvailable
checked: Settings.data.colorSchemes.useWallpaperColors
onToggled: checked => {
if (checked) {
matugenCheck.running = true;
} else {
Settings.data.colorSchemes.useWallpaperColors = false;
ToastService.showNotice(I18n.tr("toast.wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.disabled"), "settings-color-scheme");
if (Settings.data.colorSchemes.predefinedScheme) {
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
}
}
}
}
// Matugen Scheme Type Selection [Descriptions sourced from DankMaterialShell]
NComboBox {
label: I18n.tr("settings.color-scheme.color-source.matugen-scheme-type.label")
description: I18n.tr("settings.color-scheme.color-source.matugen-scheme-type.description")
enabled: Settings.data.colorSchemes.useWallpaperColors
visible: Settings.data.colorSchemes.useWallpaperColors
model: [
{
"key": "scheme-content",
"name": "Content"
},
{
"key": "scheme-expressive",
"name": "Expressive"
},
{
"key": "scheme-fidelity",
"name": "Fidelity"
},
{
"key": "scheme-fruit-salad",
"name": "Fruit Salad"
},
{
"key": "scheme-monochrome",
"name": "Monochrome"
},
{
"key": "scheme-neutral",
"name": "Neutral"
},
{
"key": "scheme-rainbow",
"name": "Rainbow"
},
{
"key": "scheme-tonal-spot",
"name": "Tonal Spot"
}
]
currentKey: Settings.data.colorSchemes.matugenSchemeType
onSelected: key => {
Settings.data.colorSchemes.matugenSchemeType = key;
AppThemeService.generate();
}
isSettings: true
defaultValue: Settings.getDefaultValue("colorSchemes.matugenSchemeType")
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
visible: !Settings.data.colorSchemes.useWallpaperColors
Layout.preferredHeight: Style.marginL
}
// Predefined Color Schemes
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
visible: !Settings.data.colorSchemes.useWallpaperColors
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
NHeader {
label: I18n.tr("settings.color-scheme.predefined.section.label")
description: I18n.tr("settings.color-scheme.predefined.section.description")
Layout.fillWidth: true
ColorsSubTab {
timeOptions: timeOptions
onCheckMatugen: matugenCheck.running = true
onOpenDownloadPopup: downloadPopupLoader.open()
}
NButton {
text: I18n.tr("settings.color-scheme.download.button")
icon: "download"
onClicked: root.openDownloadPopup()
Layout.alignment: Qt.AlignRight
}
// Download popup
Loader {
id: downloadPopupLoader
active: false
sourceComponent: SchemeDownloader {
parent: Overlay.overlay
}
property bool pendingOpen: false
function open() {
pendingOpen = true;
active = true;
if (item) {
item.open();
pendingOpen = false;
}
}
onItemChanged: {
if (item && pendingOpen) {
item.open();
pendingOpen = false;
}
}
}
// Color Schemes Grid
GridLayout {
columns: 2
rowSpacing: Style.marginM
columnSpacing: Style.marginM
Layout.fillWidth: true
Repeater {
model: ColorSchemeService.schemes
Rectangle {
id: schemeItem
property string schemePath: modelData
property string schemeName: root.extractSchemeName(modelData)
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
height: 50 * Style.uiScaleRatio
radius: Style.radiusS
color: root.getSchemeColor(schemeName, "mSurface")
border.width: Style.borderL
border.color: {
if (Settings.data.colorSchemes.predefinedScheme === schemeName) {
return Color.mSecondary;
}
if (itemMouseArea.containsMouse) {
return Color.mHover;
}
return Color.mOutline;
}
RowLayout {
id: scheme
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginS
NText {
text: schemeItem.schemeName
pointSize: Style.fontSizeS
color: Color.mOnSurface
Layout.fillWidth: true
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
maximumLineCount: 1
}
property int diameter: 16 * Style.uiScaleRatio
Rectangle {
width: scheme.diameter
height: scheme.diameter
radius: scheme.diameter * 0.5
color: root.getSchemeColor(schemeItem.schemeName, "mPrimary")
}
Rectangle {
width: scheme.diameter
height: scheme.diameter
radius: scheme.diameter * 0.5
color: root.getSchemeColor(schemeItem.schemeName, "mSecondary")
}
Rectangle {
width: scheme.diameter
height: scheme.diameter
radius: scheme.diameter * 0.5
color: root.getSchemeColor(schemeItem.schemeName, "mTertiary")
}
Rectangle {
width: scheme.diameter
height: scheme.diameter
radius: scheme.diameter * 0.5
color: root.getSchemeColor(schemeItem.schemeName, "mError")
}
}
MouseArea {
id: itemMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.data.colorSchemes.useWallpaperColors = false;
Logger.i("ColorSchemeTab", "Disabled wallpaper colors");
Settings.data.colorSchemes.predefinedScheme = schemeItem.schemeName;
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
}
}
// Selection indicator
Rectangle {
visible: (Settings.data.colorSchemes.predefinedScheme === schemeItem.schemeName)
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 0
anchors.topMargin: -3
width: 20
height: 20
radius: Math.min(Style.radiusL, width / 2)
color: Color.mSecondary
border.width: Style.borderS
border.color: Color.mOnSecondary
NIcon {
icon: "check"
pointSize: Style.fontSizeXS
color: Color.mOnSecondary
anchors.centerIn: parent
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationNormal
}
}
}
}
}
// Generate templates for predefined schemes
NCheckbox {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.predefined.generate-templates.label")
description: I18n.tr("settings.color-scheme.predefined.generate-templates.description")
checked: Settings.data.colorSchemes.generateTemplatesForPredefined
onToggled: checked => {
Settings.data.colorSchemes.generateTemplatesForPredefined = checked;
if (!Settings.data.colorSchemes.useWallpaperColors && Settings.data.colorSchemes.predefinedScheme) {
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
}
}
Layout.topMargin: Style.marginL
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Template toggles organized by category
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginL
visible: Settings.data.colorSchemes.useWallpaperColors || Settings.data.colorSchemes.generateTemplatesForPredefined
NHeader {
label: I18n.tr("settings.color-scheme.templates.section.label")
description: I18n.tr("settings.color-scheme.templates.section.description")
}
// UI Components
NCollapsible {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.ui.label")
description: I18n.tr("settings.color-scheme.templates.ui.description")
defaultExpanded: false
NCheckbox {
label: "GTK"
description: I18n.tr("settings.color-scheme.templates.ui.gtk.description", {
"filepath": "~/.config/gtk-3.0/gtk.css & ~/.config/gtk-4.0/gtk.css"
})
checked: Settings.data.templates.gtk
onToggled: checked => {
Settings.data.templates.gtk = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Qt"
description: I18n.tr("settings.color-scheme.templates.ui.qt.description", {
"filepath": "~/.config/qt5ct/colors/noctalia.conf & ~/.config/qt6ct/colors/noctalia.conf"
})
checked: Settings.data.templates.qt
onToggled: checked => {
Settings.data.templates.qt = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "KColorScheme"
description: I18n.tr("settings.color-scheme.templates.ui.kcolorscheme.description", {
"filepath": "~/.local/share/color-schemes/noctalia.colors"
})
checked: Settings.data.templates.kcolorscheme
onToggled: checked => {
Settings.data.templates.kcolorscheme = checked;
AppThemeService.generate();
}
}
}
// Compositors
NCollapsible {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.compositors.label")
description: I18n.tr("settings.color-scheme.templates.compositors.description")
defaultExpanded: false
NCheckbox {
label: "Niri"
description: I18n.tr("settings.color-scheme.templates.compositors.niri.description", {
"filepath": "~/.config/niri/noctalia.kdl"
})
checked: Settings.data.templates.niri
onToggled: checked => {
Settings.data.templates.niri = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Hyprland"
description: I18n.tr("settings.color-scheme.templates.compositors.hyprland.description", {
"filepath": "~/.config/hypr/noctalia/noctalia-colors.conf"
})
checked: Settings.data.templates.hyprland
onToggled: checked => {
Settings.data.templates.hyprland = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Mango"
description: I18n.tr("settings.color-scheme.templates.compositors.mango.description", {
"filepath": "~/.config/mango/noctalia.conf"
})
checked: Settings.data.templates.mango
onToggled: checked => {
Settings.data.templates.mango = checked;
AppThemeService.generate();
}
}
}
// Terminal Emulators
NCollapsible {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.terminal.label")
description: I18n.tr("settings.color-scheme.templates.terminal.description")
defaultExpanded: false
NCheckbox {
label: "Alacritty"
description: I18n.tr("settings.color-scheme.templates.terminal.alacritty.description", {
"filepath": "~/.config/alacritty/themes/noctalia"
})
checked: Settings.data.templates.alacritty
onToggled: checked => {
Settings.data.templates.alacritty = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Kitty"
description: I18n.tr("settings.color-scheme.templates.terminal.kitty.description", {
"filepath": "~/.config/kitty/themes/noctalia.conf"
})
checked: Settings.data.templates.kitty
onToggled: checked => {
Settings.data.templates.kitty = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Ghostty"
description: I18n.tr("settings.color-scheme.templates.terminal.ghostty.description", {
"filepath": "~/.config/ghostty/themes/noctalia"
})
checked: Settings.data.templates.ghostty
onToggled: checked => {
Settings.data.templates.ghostty = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Foot"
description: I18n.tr("settings.color-scheme.templates.terminal.foot.description", {
"filepath": "~/.config/foot/themes/noctalia"
})
checked: Settings.data.templates.foot
onToggled: checked => {
Settings.data.templates.foot = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Wezterm"
description: I18n.tr("settings.color-scheme.templates.terminal.wezterm.description", {
"filepath": "~/.config/wezterm/colors/Noctalia.toml"
})
checked: Settings.data.templates.wezterm
onToggled: checked => {
Settings.data.templates.wezterm = checked;
AppThemeService.generate();
}
}
}
// Applications
NCollapsible {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.programs.label")
description: I18n.tr("settings.color-scheme.templates.programs.description")
defaultExpanded: false
NCheckbox {
label: "Fuzzel"
description: I18n.tr("settings.color-scheme.templates.programs.fuzzel.description", {
"filepath": "~/.config/fuzzel/themes/noctalia"
})
checked: Settings.data.templates.fuzzel
onToggled: checked => {
Settings.data.templates.fuzzel = checked;
AppThemeService.generate();
}
}
// Discord clients - single toggle with dynamic description
NCheckbox {
id: discordToggle
label: "Discord"
description: {
if (ProgramCheckerService.availableDiscordClients.length === 0) {
return I18n.tr("settings.color-scheme.templates.programs.discord.description-missing");
} else {
// Show detected clients
var clientInfo = [];
for (var i = 0; i < ProgramCheckerService.availableDiscordClients.length; i++) {
var client = ProgramCheckerService.availableDiscordClients[i];
clientInfo.push(client.name.charAt(0).toUpperCase() + client.name.slice(1));
}
return I18n.tr("settings.color-scheme.templates.programs.discord.description-detected", {
"clients": clientInfo.join(", ")
});
}
}
Layout.fillWidth: true
Layout.preferredWidth: -1
checked: Settings.data.templates.discord
enabled: ProgramCheckerService.availableDiscordClients.length > 0
onToggled: checked => {
// Set unified discord property
Settings.data.templates.discord = checked;
if (ProgramCheckerService.availableDiscordClients.length > 0) {
AppThemeService.generate();
}
}
}
NCheckbox {
label: "Pywalfox"
description: I18n.tr("settings.color-scheme.templates.programs.pywalfox.description", {
"filepath": "~/.cache/wal/colors.json"
})
checked: Settings.data.templates.pywalfox
onToggled: checked => {
Settings.data.templates.pywalfox = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Vicinae"
description: I18n.tr("settings.color-scheme.templates.programs.vicinae.description", {
"filepath": "~/.local/share/vicinae/themes/matugen.toml"
})
checked: Settings.data.templates.vicinae
onToggled: checked => {
Settings.data.templates.vicinae = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Walker"
description: I18n.tr("settings.color-scheme.templates.programs.walker.description", {
"filepath": "~/.config/walker/style.css"
})
checked: Settings.data.templates.walker
onToggled: checked => {
Settings.data.templates.walker = checked;
AppThemeService.generate();
}
}
// Code clients - single toggle with dynamic description
NCheckbox {
id: codeToggle
label: "Code"
description: {
if (ProgramCheckerService.availableCodeClients.length === 0) {
return I18n.tr("settings.color-scheme.templates.programs.code.description-missing");
} else {
// Show detected clients
var clientInfo = [];
for (var i = 0; i < ProgramCheckerService.availableCodeClients.length; i++) {
var client = ProgramCheckerService.availableCodeClients[i];
// Capitalize first letter and format nicely
var clientName = client.name === "code" ? "VSCode" : "VSCodium";
clientInfo.push(clientName);
}
return I18n.tr("settings.color-scheme.templates.programs.code.description-detected", {
"clients": clientInfo.join(", ")
});
}
}
Layout.fillWidth: true
Layout.preferredWidth: -1
checked: Settings.data.templates.code
enabled: ProgramCheckerService.availableCodeClients.length > 0
onToggled: checked => {
// Set unified code property
Settings.data.templates.code = checked;
if (ProgramCheckerService.availableCodeClients.length > 0) {
if (!checked) {
const homeDir = Quickshell.env("HOME");
for (var i = 0; i < ProgramCheckerService.availableCodeClients.length; i++) {
var client = ProgramCheckerService.availableCodeClients[i];
}
}
AppThemeService.generate();
}
}
}
NCheckbox {
label: "Spicetify"
description: I18n.tr("settings.color-scheme.templates.programs.spicetify.description", {
"filepath": "~/.config/spicetify/Themes/Comfy/color.ini"
})
checked: Settings.data.templates.spicetify
onToggled: checked => {
Settings.data.templates.spicetify = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Telegram"
description: I18n.tr("settings.color-scheme.templates.programs.telegram.description", {
"filepath": "~/.config/telegram-desktop/themes/noctalia.tdesktop-theme"
})
checked: Settings.data.templates.telegram
onToggled: checked => {
Settings.data.templates.telegram = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Cava"
description: I18n.tr("settings.color-scheme.templates.programs.cava.description", {
"filepath": "~/.config/cava/themes/noctalia"
})
checked: Settings.data.templates.cava
onToggled: checked => {
Settings.data.templates.cava = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Yazi"
description: I18n.tr("settings.color-scheme.templates.programs.yazi.description", {
"filepath": "~/.config/yazi/flavors/noctalia.yazi/flavor.toml"
})
checked: Settings.data.templates.yazi
onToggled: checked => {
Settings.data.templates.yazi = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Zed"
description: I18n.tr("settings.color-scheme.templates.programs.zed.description", {
"filepath": "~/.config/zed/themes/noctalia.json"
})
checked: Settings.data.templates.zed
onToggled: checked => {
Settings.data.templates.zed = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Zen Browser"
description: I18n.tr("settings.color-scheme.templates.programs.zen-browser.description", {
"filepath": "~/.cache/noctalia/zen-browser/zen-userChrome.css"
})
checked: Settings.data.templates.zenBrowser
onToggled: checked => {
Settings.data.templates.zenBrowser = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Emacs"
description: I18n.tr("settings.color-scheme.templates.programs.emacs.description")
checked: Settings.data.templates.emacs
onToggled: checked => {
Settings.data.templates.emacs = checked;
AppThemeService.generate();
}
}
}
// Miscellaneous
NCollapsible {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.misc.label")
description: I18n.tr("settings.color-scheme.templates.misc.description")
defaultExpanded: false
NCheckbox {
label: I18n.tr("settings.color-scheme.templates.misc.user-templates.label")
description: I18n.tr("settings.color-scheme.templates.misc.user-templates.description")
checked: Settings.data.templates.enableUserTemplates
onToggled: checked => {
Settings.data.templates.enableUserTemplates = checked;
if (checked) {
TemplateRegistry.writeUserTemplatesToml();
}
AppThemeService.generate();
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
TemplatesSubTab {}
}
}
@@ -0,0 +1,433 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Io
import "."
import qs.Commons
import qs.Services.System
import qs.Services.Theming
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var timeOptions
property var schemeColorsCache: ({})
property int cacheVersion: 0
signal checkMatugen
signal openDownloadPopup
function extractSchemeName(schemePath) {
var pathParts = schemePath.split("/");
var filename = pathParts[pathParts.length - 1];
var schemeName = filename.replace(".json", "");
if (schemeName === "Noctalia-default") {
schemeName = "Noctalia (default)";
} else if (schemeName === "Noctalia-legacy") {
schemeName = "Noctalia (legacy)";
} else if (schemeName === "Tokyo-Night") {
schemeName = "Tokyo Night";
} else if (schemeName === "Rosepine") {
schemeName = "Rose Pine";
}
return schemeName;
}
function getSchemeColor(schemeName, colorKey) {
var _ = cacheVersion;
if (schemeColorsCache[schemeName]) {
var entry = schemeColorsCache[schemeName];
var variant = entry;
if (entry.dark || entry.light) {
variant = Settings.data.colorSchemes.darkMode ? (entry.dark || entry.light) : (entry.light || entry.dark);
}
if (variant && variant[colorKey]) {
return variant[colorKey];
}
}
if (colorKey === "mSurface")
return Color.mSurfaceVariant;
if (colorKey === "mPrimary")
return Color.mPrimary;
if (colorKey === "mSecondary")
return Color.mSecondary;
if (colorKey === "mTertiary")
return Color.mTertiary;
if (colorKey === "mError")
return Color.mError;
return Color.mOnSurfaceVariant;
}
function schemeLoaded(schemeName, jsonData) {
var value = jsonData || {};
schemeColorsCache[schemeName] = value;
cacheVersion++;
}
Connections {
target: ColorSchemeService
function onSchemesChanged() {
root.schemeColorsCache = {};
root.cacheVersion++;
}
}
Item {
id: fileLoaders
visible: false
Repeater {
model: ColorSchemeService.schemes
delegate: Item {
FileView {
path: modelData
blockLoading: false
onLoaded: {
var schemeName = root.extractSchemeName(path);
try {
var jsonData = JSON.parse(text());
root.schemeLoaded(schemeName, jsonData);
} catch (e) {
Logger.w("ColorSchemeTab", "Failed to parse JSON for scheme:", schemeName, e);
root.schemeLoaded(schemeName, null);
}
}
}
}
}
}
NHeader {
label: I18n.tr("settings.color-scheme.color-source.section.label")
description: I18n.tr("settings.color-scheme.color-source.section.description")
}
NToggle {
label: I18n.tr("settings.color-scheme.dark-mode.switch.label")
description: I18n.tr("settings.color-scheme.dark-mode.switch.description")
checked: Settings.data.colorSchemes.darkMode
onToggled: checked => {
Settings.data.colorSchemes.darkMode = checked;
root.cacheVersion++;
}
}
NComboBox {
label: I18n.tr("settings.color-scheme.dark-mode.mode.label")
description: I18n.tr("settings.color-scheme.dark-mode.mode.description")
model: [
{
"name": I18n.tr("settings.color-scheme.dark-mode.mode.off"),
"key": "off"
},
{
"name": I18n.tr("settings.color-scheme.dark-mode.mode.manual"),
"key": "manual"
},
{
"name": I18n.tr("settings.color-scheme.dark-mode.mode.location"),
"key": "location"
}
]
currentKey: Settings.data.colorSchemes.schedulingMode
onSelected: key => {
Settings.data.colorSchemes.schedulingMode = key;
AppThemeService.generate();
}
}
ColumnLayout {
spacing: Style.marginS
visible: Settings.data.colorSchemes.schedulingMode === "manual"
NLabel {
label: I18n.tr("settings.display.night-light.manual-schedule.label")
description: I18n.tr("settings.display.night-light.manual-schedule.description")
}
RowLayout {
Layout.fillWidth: false
spacing: Style.marginS
NText {
text: I18n.tr("settings.display.night-light.manual-schedule.sunrise")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
NComboBox {
model: root.timeOptions
currentKey: Settings.data.colorSchemes.manualSunrise
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-start")
onSelected: key => Settings.data.colorSchemes.manualSunrise = key
minimumWidth: 120
}
Item {
Layout.preferredWidth: 20
}
NText {
text: I18n.tr("settings.display.night-light.manual-schedule.sunset")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
NComboBox {
model: root.timeOptions
currentKey: Settings.data.colorSchemes.manualSunset
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-stop")
onSelected: key => Settings.data.colorSchemes.manualSunset = key
minimumWidth: 120
}
}
}
NToggle {
label: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label")
description: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.description")
enabled: ProgramCheckerService.matugenAvailable
checked: Settings.data.colorSchemes.useWallpaperColors
onToggled: checked => {
if (checked) {
root.checkMatugen();
} else {
Settings.data.colorSchemes.useWallpaperColors = false;
ToastService.showNotice(I18n.tr("toast.wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.disabled"), "settings-color-scheme");
if (Settings.data.colorSchemes.predefinedScheme) {
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
}
}
}
}
NComboBox {
label: I18n.tr("settings.color-scheme.color-source.matugen-scheme-type.label")
description: I18n.tr("settings.color-scheme.color-source.matugen-scheme-type.description")
enabled: Settings.data.colorSchemes.useWallpaperColors
visible: Settings.data.colorSchemes.useWallpaperColors
model: [
{
"key": "scheme-content",
"name": "Content"
},
{
"key": "scheme-expressive",
"name": "Expressive"
},
{
"key": "scheme-fidelity",
"name": "Fidelity"
},
{
"key": "scheme-fruit-salad",
"name": "Fruit Salad"
},
{
"key": "scheme-monochrome",
"name": "Monochrome"
},
{
"key": "scheme-neutral",
"name": "Neutral"
},
{
"key": "scheme-rainbow",
"name": "Rainbow"
},
{
"key": "scheme-tonal-spot",
"name": "Tonal Spot"
}
]
currentKey: Settings.data.colorSchemes.matugenSchemeType
onSelected: key => {
Settings.data.colorSchemes.matugenSchemeType = key;
AppThemeService.generate();
}
isSettings: true
defaultValue: Settings.getDefaultValue("colorSchemes.matugenSchemeType")
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
visible: !Settings.data.colorSchemes.useWallpaperColors
}
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
visible: !Settings.data.colorSchemes.useWallpaperColors
NHeader {
label: I18n.tr("settings.color-scheme.predefined.section.label")
description: I18n.tr("settings.color-scheme.predefined.section.description")
Layout.fillWidth: true
}
NButton {
text: I18n.tr("settings.color-scheme.download.button")
icon: "download"
onClicked: root.openDownloadPopup()
Layout.alignment: Qt.AlignRight
}
GridLayout {
columns: 2
rowSpacing: Style.marginM
columnSpacing: Style.marginM
Layout.fillWidth: true
Repeater {
model: ColorSchemeService.schemes
Rectangle {
id: schemeItem
property string schemePath: modelData
property string schemeName: root.extractSchemeName(modelData)
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
height: 50 * Style.uiScaleRatio
radius: Style.radiusS
color: root.getSchemeColor(schemeName, "mSurface")
border.width: Style.borderL
border.color: {
if (Settings.data.colorSchemes.predefinedScheme === schemeName) {
return Color.mSecondary;
}
if (itemMouseArea.containsMouse) {
return Color.mHover;
}
return Color.mOutline;
}
RowLayout {
id: scheme
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginS
NText {
text: schemeItem.schemeName
pointSize: Style.fontSizeS
color: Color.mOnSurface
Layout.fillWidth: true
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
maximumLineCount: 1
}
property int diameter: 16 * Style.uiScaleRatio
Rectangle {
width: scheme.diameter
height: scheme.diameter
radius: scheme.diameter * 0.5
color: root.getSchemeColor(schemeItem.schemeName, "mPrimary")
}
Rectangle {
width: scheme.diameter
height: scheme.diameter
radius: scheme.diameter * 0.5
color: root.getSchemeColor(schemeItem.schemeName, "mSecondary")
}
Rectangle {
width: scheme.diameter
height: scheme.diameter
radius: scheme.diameter * 0.5
color: root.getSchemeColor(schemeItem.schemeName, "mTertiary")
}
Rectangle {
width: scheme.diameter
height: scheme.diameter
radius: scheme.diameter * 0.5
color: root.getSchemeColor(schemeItem.schemeName, "mError")
}
}
MouseArea {
id: itemMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.data.colorSchemes.useWallpaperColors = false;
Logger.i("ColorSchemeTab", "Disabled wallpaper colors");
Settings.data.colorSchemes.predefinedScheme = schemeItem.schemeName;
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
}
}
Rectangle {
visible: (Settings.data.colorSchemes.predefinedScheme === schemeItem.schemeName)
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 0
anchors.topMargin: -3
width: 20
height: 20
radius: Math.min(Style.radiusL, width / 2)
color: Color.mSecondary
border.width: Style.borderS
border.color: Color.mOnSecondary
NIcon {
icon: "check"
pointSize: Style.fontSizeXS
color: Color.mOnSecondary
anchors.centerIn: parent
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationNormal
}
}
}
}
}
NCheckbox {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.predefined.generate-templates.label")
description: I18n.tr("settings.color-scheme.predefined.generate-templates.description")
checked: Settings.data.colorSchemes.generateTemplatesForPredefined
onToggled: checked => {
Settings.data.colorSchemes.generateTemplatesForPredefined = checked;
if (!Settings.data.colorSchemes.useWallpaperColors && Settings.data.colorSchemes.predefinedScheme) {
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
}
}
Layout.topMargin: Style.marginL
}
}
}
@@ -0,0 +1,388 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.System
import qs.Services.Theming
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.color-scheme.templates.section.label")
description: I18n.tr("settings.color-scheme.templates.section.description")
}
NCollapsible {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.ui.label")
description: I18n.tr("settings.color-scheme.templates.ui.description")
defaultExpanded: false
NCheckbox {
label: "GTK"
description: I18n.tr("settings.color-scheme.templates.ui.gtk.description", {
"filepath": "~/.config/gtk-3.0/gtk.css & ~/.config/gtk-4.0/gtk.css"
})
checked: Settings.data.templates.gtk
onToggled: checked => {
Settings.data.templates.gtk = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Qt"
description: I18n.tr("settings.color-scheme.templates.ui.qt.description", {
"filepath": "~/.config/qt5ct/colors/noctalia.conf & ~/.config/qt6ct/colors/noctalia.conf"
})
checked: Settings.data.templates.qt
onToggled: checked => {
Settings.data.templates.qt = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "KColorScheme"
description: I18n.tr("settings.color-scheme.templates.ui.kcolorscheme.description", {
"filepath": "~/.local/share/color-schemes/noctalia.colors"
})
checked: Settings.data.templates.kcolorscheme
onToggled: checked => {
Settings.data.templates.kcolorscheme = checked;
AppThemeService.generate();
}
}
}
NCollapsible {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.compositors.label")
description: I18n.tr("settings.color-scheme.templates.compositors.description")
defaultExpanded: false
NCheckbox {
label: "Niri"
description: I18n.tr("settings.color-scheme.templates.compositors.niri.description", {
"filepath": "~/.config/niri/noctalia.kdl"
})
checked: Settings.data.templates.niri
onToggled: checked => {
Settings.data.templates.niri = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Hyprland"
description: I18n.tr("settings.color-scheme.templates.compositors.hyprland.description", {
"filepath": "~/.config/hypr/noctalia/noctalia-colors.conf"
})
checked: Settings.data.templates.hyprland
onToggled: checked => {
Settings.data.templates.hyprland = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Mango"
description: I18n.tr("settings.color-scheme.templates.compositors.mango.description", {
"filepath": "~/.config/mango/noctalia.conf"
})
checked: Settings.data.templates.mango
onToggled: checked => {
Settings.data.templates.mango = checked;
AppThemeService.generate();
}
}
}
NCollapsible {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.terminal.label")
description: I18n.tr("settings.color-scheme.templates.terminal.description")
defaultExpanded: false
NCheckbox {
label: "Alacritty"
description: I18n.tr("settings.color-scheme.templates.terminal.alacritty.description", {
"filepath": "~/.config/alacritty/themes/noctalia"
})
checked: Settings.data.templates.alacritty
onToggled: checked => {
Settings.data.templates.alacritty = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Kitty"
description: I18n.tr("settings.color-scheme.templates.terminal.kitty.description", {
"filepath": "~/.config/kitty/themes/noctalia.conf"
})
checked: Settings.data.templates.kitty
onToggled: checked => {
Settings.data.templates.kitty = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Ghostty"
description: I18n.tr("settings.color-scheme.templates.terminal.ghostty.description", {
"filepath": "~/.config/ghostty/themes/noctalia"
})
checked: Settings.data.templates.ghostty
onToggled: checked => {
Settings.data.templates.ghostty = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Foot"
description: I18n.tr("settings.color-scheme.templates.terminal.foot.description", {
"filepath": "~/.config/foot/themes/noctalia"
})
checked: Settings.data.templates.foot
onToggled: checked => {
Settings.data.templates.foot = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Wezterm"
description: I18n.tr("settings.color-scheme.templates.terminal.wezterm.description", {
"filepath": "~/.config/wezterm/colors/Noctalia.toml"
})
checked: Settings.data.templates.wezterm
onToggled: checked => {
Settings.data.templates.wezterm = checked;
AppThemeService.generate();
}
}
}
NCollapsible {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.programs.label")
description: I18n.tr("settings.color-scheme.templates.programs.description")
defaultExpanded: false
NCheckbox {
label: "Fuzzel"
description: I18n.tr("settings.color-scheme.templates.programs.fuzzel.description", {
"filepath": "~/.config/fuzzel/themes/noctalia"
})
checked: Settings.data.templates.fuzzel
onToggled: checked => {
Settings.data.templates.fuzzel = checked;
AppThemeService.generate();
}
}
NCheckbox {
id: discordToggle
label: "Discord"
description: {
if (ProgramCheckerService.availableDiscordClients.length === 0) {
return I18n.tr("settings.color-scheme.templates.programs.discord.description-missing");
} else {
var clientInfo = [];
for (var i = 0; i < ProgramCheckerService.availableDiscordClients.length; i++) {
var client = ProgramCheckerService.availableDiscordClients[i];
clientInfo.push(client.name.charAt(0).toUpperCase() + client.name.slice(1));
}
return I18n.tr("settings.color-scheme.templates.programs.discord.description-detected", {
"clients": clientInfo.join(", ")
});
}
}
Layout.fillWidth: true
Layout.preferredWidth: -1
checked: Settings.data.templates.discord
enabled: ProgramCheckerService.availableDiscordClients.length > 0
onToggled: checked => {
Settings.data.templates.discord = checked;
if (ProgramCheckerService.availableDiscordClients.length > 0) {
AppThemeService.generate();
}
}
}
NCheckbox {
label: "Pywalfox"
description: I18n.tr("settings.color-scheme.templates.programs.pywalfox.description", {
"filepath": "~/.cache/wal/colors.json"
})
checked: Settings.data.templates.pywalfox
onToggled: checked => {
Settings.data.templates.pywalfox = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Vicinae"
description: I18n.tr("settings.color-scheme.templates.programs.vicinae.description", {
"filepath": "~/.local/share/vicinae/themes/matugen.toml"
})
checked: Settings.data.templates.vicinae
onToggled: checked => {
Settings.data.templates.vicinae = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Walker"
description: I18n.tr("settings.color-scheme.templates.programs.walker.description", {
"filepath": "~/.config/walker/style.css"
})
checked: Settings.data.templates.walker
onToggled: checked => {
Settings.data.templates.walker = checked;
AppThemeService.generate();
}
}
NCheckbox {
id: codeToggle
label: "Code"
description: {
if (ProgramCheckerService.availableCodeClients.length === 0) {
return I18n.tr("settings.color-scheme.templates.programs.code.description-missing");
} else {
var clientInfo = [];
for (var i = 0; i < ProgramCheckerService.availableCodeClients.length; i++) {
var client = ProgramCheckerService.availableCodeClients[i];
var clientName = client.name === "code" ? "VSCode" : "VSCodium";
clientInfo.push(clientName);
}
return I18n.tr("settings.color-scheme.templates.programs.code.description-detected", {
"clients": clientInfo.join(", ")
});
}
}
Layout.fillWidth: true
Layout.preferredWidth: -1
checked: Settings.data.templates.code
enabled: ProgramCheckerService.availableCodeClients.length > 0
onToggled: checked => {
Settings.data.templates.code = checked;
if (ProgramCheckerService.availableCodeClients.length > 0) {
AppThemeService.generate();
}
}
}
NCheckbox {
label: "Spicetify"
description: I18n.tr("settings.color-scheme.templates.programs.spicetify.description", {
"filepath": "~/.config/spicetify/Themes/Comfy/color.ini"
})
checked: Settings.data.templates.spicetify
onToggled: checked => {
Settings.data.templates.spicetify = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Telegram"
description: I18n.tr("settings.color-scheme.templates.programs.telegram.description", {
"filepath": "~/.config/telegram-desktop/themes/noctalia.tdesktop-theme"
})
checked: Settings.data.templates.telegram
onToggled: checked => {
Settings.data.templates.telegram = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Cava"
description: I18n.tr("settings.color-scheme.templates.programs.cava.description", {
"filepath": "~/.config/cava/themes/noctalia"
})
checked: Settings.data.templates.cava
onToggled: checked => {
Settings.data.templates.cava = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Yazi"
description: I18n.tr("settings.color-scheme.templates.programs.yazi.description", {
"filepath": "~/.config/yazi/flavors/noctalia.yazi/flavor.toml"
})
checked: Settings.data.templates.yazi
onToggled: checked => {
Settings.data.templates.yazi = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Zed"
description: I18n.tr("settings.color-scheme.templates.programs.zed.description", {
"filepath": "~/.config/zed/themes/noctalia.json"
})
checked: Settings.data.templates.zed
onToggled: checked => {
Settings.data.templates.zed = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Zen Browser"
description: I18n.tr("settings.color-scheme.templates.programs.zen-browser.description", {
"filepath": "~/.cache/noctalia/zen-browser/zen-userChrome.css"
})
checked: Settings.data.templates.zenBrowser
onToggled: checked => {
Settings.data.templates.zenBrowser = checked;
AppThemeService.generate();
}
}
NCheckbox {
label: "Emacs"
description: I18n.tr("settings.color-scheme.templates.programs.emacs.description")
checked: Settings.data.templates.emacs
onToggled: checked => {
Settings.data.templates.emacs = checked;
AppThemeService.generate();
}
}
}
NCollapsible {
Layout.fillWidth: true
label: I18n.tr("settings.color-scheme.templates.misc.label")
description: I18n.tr("settings.color-scheme.templates.misc.description")
defaultExpanded: false
NCheckbox {
label: I18n.tr("settings.color-scheme.templates.misc.user-templates.label")
description: I18n.tr("settings.color-scheme.templates.misc.user-templates.description")
checked: Settings.data.templates.enableUserTemplates
onToggled: checked => {
Settings.data.templates.enableUserTemplates = checked;
if (checked) {
TemplateRegistry.writeUserTemplatesToml();
}
AppThemeService.generate();
}
}
}
}
@@ -0,0 +1,158 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Services.Hardware
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.display.monitors.section.label")
description: I18n.tr("settings.display.monitors.section.description")
}
ColumnLayout {
spacing: Style.marginL
Repeater {
model: Quickshell.screens || []
delegate: Rectangle {
Layout.fillWidth: true
implicitHeight: contentCol.implicitHeight + Style.marginL * 2
radius: Style.radiusM
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Style.borderS
property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData)
ColumnLayout {
id: contentCol
width: parent.width - 2 * Style.marginL
x: Style.marginL
y: Style.marginL
spacing: Style.marginXXS
NLabel {
label: modelData.name || "Unknown"
description: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
}
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
visible: brightnessMonitor !== undefined && brightnessMonitor !== null
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL
NText {
text: I18n.tr("settings.display.monitors.brightness")
Layout.preferredWidth: 90
Layout.alignment: Qt.AlignVCenter
}
NValueSlider {
id: brightnessSlider
from: 0
to: 1
value: brightnessMonitor ? brightnessMonitor.brightness : 0.5
stepSize: 0.01
enabled: brightnessMonitor ? brightnessMonitor.brightnessControlAvailable : false
onMoved: value => {
if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) {
brightnessMonitor.setBrightness(value);
}
}
onPressedChanged: (pressed, value) => {
if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) {
brightnessMonitor.setBrightness(value);
}
}
Layout.fillWidth: true
}
NText {
text: brightnessMonitor ? Math.round(brightnessSlider.value * 100) + "%" : "N/A"
Layout.preferredWidth: 55
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignVCenter
opacity: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable ? 0.5 : 1.0
}
Item {
Layout.preferredWidth: 30
Layout.fillHeight: true
NIcon {
icon: brightnessMonitor && brightnessMonitor.method == "internal" ? "device-laptop" : "device-desktop"
anchors.centerIn: parent
opacity: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable ? 0.5 : 1.0
}
}
}
NText {
visible: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable
text: !Settings.data.brightness.enableDdcSupport ? I18n.tr("settings.display.monitors.brightness-unavailable.ddc-disabled") : I18n.tr("settings.display.monitors.brightness-unavailable.generic")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
}
}
}
NSpinBox {
Layout.fillWidth: true
label: I18n.tr("settings.display.monitors.brightness-step.label")
description: I18n.tr("settings.display.monitors.brightness-step.description")
minimum: 1
maximum: 50
value: Settings.data.brightness.brightnessStep
stepSize: 1
suffix: "%"
onValueChanged: Settings.data.brightness.brightnessStep = value
isSettings: true
defaultValue: Settings.getDefaultValue("brightness.brightnessStep")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.display.monitors.enforce-minimum.label")
description: I18n.tr("settings.display.monitors.enforce-minimum.description")
checked: Settings.data.brightness.enforceMinimum
onToggled: checked => Settings.data.brightness.enforceMinimum = checked
isSettings: true
defaultValue: Settings.getDefaultValue("brightness.enforceMinimum")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.display.monitors.external-brightness.label")
description: I18n.tr("settings.display.monitors.external-brightness.description")
checked: Settings.data.brightness.enableDdcSupport
onToggled: checked => {
Settings.data.brightness.enableDdcSupport = checked;
}
isSettings: true
defaultValue: Settings.getDefaultValue("brightness.enableDdcSupport")
}
}
}
@@ -0,0 +1,87 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Io
import qs.Commons
import qs.Services.Location
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
// Time dropdown options (00:00 .. 23:30)
ListModel {
id: timeOptions
}
Component.onCompleted: {
for (var h = 0; h < 24; h++) {
for (var m = 0; m < 60; m += 30) {
var hh = ("0" + h).slice(-2);
var mm = ("0" + m).slice(-2);
var key = hh + ":" + mm;
timeOptions.append({
"key": key,
"name": key
});
}
}
}
// Check for wlsunset availability when enabling Night Light
Process {
id: wlsunsetCheck
command: ["sh", "-c", "command -v wlsunset"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
Settings.data.nightLight.enabled = true;
NightLightService.apply();
ToastService.showNotice(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.enabled"), "nightlight-on");
} else {
Settings.data.nightLight.enabled = false;
ToastService.showWarning(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.not-installed"));
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("settings.display.tabs.brightness")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.display.tabs.night-light")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
BrightnessSubTab {}
NightLightSubTab {
timeOptions: timeOptions
onCheckWlsunset: wlsunsetCheck.running = true
}
}
}
@@ -0,0 +1,226 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Location
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var timeOptions
signal checkWlsunset
NHeader {
label: I18n.tr("settings.display.night-light.section.label")
description: I18n.tr("settings.display.night-light.section.description")
}
NToggle {
label: I18n.tr("settings.display.night-light.enable.label")
description: I18n.tr("settings.display.night-light.enable.description")
checked: Settings.data.nightLight.enabled
onToggled: checked => {
if (checked) {
root.checkWlsunset();
} else {
Settings.data.nightLight.enabled = false;
Settings.data.nightLight.forced = false;
NightLightService.apply();
ToastService.showNotice(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.disabled"), "nightlight-off");
}
}
}
ColumnLayout {
visible: Settings.data.nightLight.enabled
spacing: Style.marginM
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.display.night-light.temperature.night")
description: I18n.tr("settings.display.night-light.temperature.night-description")
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NSlider {
id: nightSlider
Layout.fillWidth: true
from: 1000
to: 6500
value: Settings.data.nightLight.nightTemp
onValueChanged: {
var dayTemp = parseInt(Settings.data.nightLight.dayTemp);
var v = Math.round(value);
if (!isNaN(dayTemp)) {
var maxNight = dayTemp - 500;
v = Math.min(maxNight, Math.max(1000, v));
} else {
v = Math.max(1000, v);
}
if (v !== value)
value = v;
}
onPressedChanged: {
if (!pressed) {
var dayTemp = parseInt(Settings.data.nightLight.dayTemp);
var v = Math.round(value);
if (!isNaN(dayTemp)) {
var maxNight = dayTemp - 500;
v = Math.min(maxNight, Math.max(1000, v));
} else {
v = Math.max(1000, v);
}
Settings.data.nightLight.nightTemp = v;
}
}
}
NText {
text: nightSlider.value + "K"
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
}
NLabel {
label: I18n.tr("settings.display.night-light.temperature.day")
description: I18n.tr("settings.display.night-light.temperature.day-description")
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NSlider {
id: daySlider
Layout.fillWidth: true
from: 1000
to: 6500
value: Settings.data.nightLight.dayTemp
onValueChanged: {
var nightTemp = parseInt(Settings.data.nightLight.nightTemp);
var v = Math.round(value);
if (!isNaN(nightTemp)) {
var minDay = nightTemp + 500;
v = Math.max(minDay, Math.min(6500, v));
} else {
v = Math.min(6500, v);
}
if (v !== value)
value = v;
}
onPressedChanged: {
if (!pressed) {
var nightTemp = parseInt(Settings.data.nightLight.nightTemp);
var v = Math.round(value);
if (!isNaN(nightTemp)) {
var minDay = nightTemp + 500;
v = Math.max(minDay, Math.min(6500, v));
} else {
v = Math.min(6500, v);
}
Settings.data.nightLight.dayTemp = v;
}
}
}
NText {
text: daySlider.value + "K"
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
}
}
NToggle {
label: I18n.tr("settings.display.night-light.auto-schedule.label")
description: I18n.tr("settings.display.night-light.auto-schedule.description", {
"location": LocationService.stableName
})
checked: Settings.data.nightLight.autoSchedule
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
visible: Settings.data.nightLight.enabled
}
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
NLabel {
label: I18n.tr("settings.display.night-light.manual-schedule.label")
description: I18n.tr("settings.display.night-light.manual-schedule.description")
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: I18n.tr("settings.display.night-light.manual-schedule.sunrise")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NComboBox {
model: root.timeOptions
currentKey: Settings.data.nightLight.manualSunrise
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-start")
onSelected: key => Settings.data.nightLight.manualSunrise = key
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: I18n.tr("settings.display.night-light.manual-schedule.sunset")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NComboBox {
model: root.timeOptions
currentKey: Settings.data.nightLight.manualSunset
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-stop")
onSelected: key => Settings.data.nightLight.manualSunset = key
Layout.fillWidth: true
}
}
}
NToggle {
label: I18n.tr("settings.display.night-light.force-activation.label")
description: I18n.tr("settings.display.night-light.force-activation.description")
checked: Settings.data.nightLight.forced
onToggled: checked => {
Settings.data.nightLight.forced = checked;
if (checked && !Settings.data.nightLight.enabled) {
root.checkWlsunset();
} else {
NightLightService.apply();
}
}
visible: Settings.data.nightLight.enabled
}
}
-455
View File
@@ -1,455 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.Compositor
import qs.Services.Hardware
import qs.Services.Location
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
// Time dropdown options (00:00 .. 23:30)
ListModel {
id: timeOptions
}
Component.onCompleted: {
for (var h = 0; h < 24; h++) {
for (var m = 0; m < 60; m += 30) {
var hh = ("0" + h).slice(-2);
var mm = ("0" + m).slice(-2);
var key = hh + ":" + mm;
timeOptions.append({
"key": key,
"name": key
});
}
}
}
// Check for wlsunset availability when enabling Night Light
Process {
id: wlsunsetCheck
command: ["sh", "-c", "command -v wlsunset"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
Settings.data.nightLight.enabled = true;
NightLightService.apply();
ToastService.showNotice(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.enabled"), "nightlight-on");
} else {
Settings.data.nightLight.enabled = false;
ToastService.showWarning(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.not-installed"));
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
spacing: Style.marginL
NHeader {
label: I18n.tr("settings.display.monitors.section.label")
description: I18n.tr("settings.display.monitors.section.description")
}
ColumnLayout {
spacing: Style.marginL
Repeater {
model: Quickshell.screens || []
delegate: Rectangle {
Layout.fillWidth: true
implicitHeight: contentCol.implicitHeight + Style.marginL * 2
radius: Style.radiusM
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Style.borderS
property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData)
ColumnLayout {
id: contentCol
width: parent.width - 2 * Style.marginL
x: Style.marginL
y: Style.marginL
spacing: Style.marginXXS
NLabel {
label: modelData.name || "Unknown"
description: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
}
// Brightness
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
visible: brightnessMonitor !== undefined && brightnessMonitor !== null
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL
NText {
text: I18n.tr("settings.display.monitors.brightness")
Layout.preferredWidth: 90
Layout.alignment: Qt.AlignVCenter
}
NValueSlider {
id: brightnessSlider
from: 0
to: 1
value: brightnessMonitor ? brightnessMonitor.brightness : 0.5
stepSize: 0.01
enabled: brightnessMonitor ? brightnessMonitor.brightnessControlAvailable : false
onMoved: value => {
if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) {
brightnessMonitor.setBrightness(value);
}
}
onPressedChanged: (pressed, value) => {
if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) {
brightnessMonitor.setBrightness(value);
}
}
Layout.fillWidth: true
}
NText {
text: brightnessMonitor ? Math.round(brightnessSlider.value * 100) + "%" : "N/A"
Layout.preferredWidth: 55
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignVCenter
opacity: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable ? 0.5 : 1.0
}
Item {
Layout.preferredWidth: 30
Layout.fillHeight: true
NIcon {
icon: brightnessMonitor && brightnessMonitor.method == "internal" ? "device-laptop" : "device-desktop"
anchors.centerIn: parent
opacity: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable ? 0.5 : 1.0
}
}
}
// Show message when brightness control is not available
NText {
visible: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable
text: !Settings.data.brightness.enableDdcSupport ? I18n.tr("settings.display.monitors.brightness-unavailable.ddc-disabled") : I18n.tr("settings.display.monitors.brightness-unavailable.generic")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
}
}
}
// Brightness Step
NSpinBox {
Layout.fillWidth: true
label: I18n.tr("settings.display.monitors.brightness-step.label")
description: I18n.tr("settings.display.monitors.brightness-step.description")
minimum: 1
maximum: 50
value: Settings.data.brightness.brightnessStep
stepSize: 1
suffix: "%"
onValueChanged: Settings.data.brightness.brightnessStep = value
isSettings: true
defaultValue: Settings.getDefaultValue("brightness.brightnessStep")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.display.monitors.enforce-minimum.label")
description: I18n.tr("settings.display.monitors.enforce-minimum.description")
checked: Settings.data.brightness.enforceMinimum
onToggled: checked => Settings.data.brightness.enforceMinimum = checked
isSettings: true
defaultValue: Settings.getDefaultValue("brightness.enforceMinimum")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.display.monitors.external-brightness.label")
description: I18n.tr("settings.display.monitors.external-brightness.description")
checked: Settings.data.brightness.enableDdcSupport
onToggled: checked => {
Settings.data.brightness.enableDdcSupport = checked;
// DDC detection will run on next monitor change when enabled
// Monitors will stop using DDC immediately when disabled
}
isSettings: true
defaultValue: Settings.getDefaultValue("brightness.enableDdcSupport")
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Night Light Section
ColumnLayout {
spacing: Style.marginXS
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.display.night-light.section.label")
description: I18n.tr("settings.display.night-light.section.description")
}
}
NToggle {
label: I18n.tr("settings.display.night-light.enable.label")
description: I18n.tr("settings.display.night-light.enable.description")
checked: Settings.data.nightLight.enabled
onToggled: checked => {
if (checked) {
// Verify wlsunset exists before enabling
wlsunsetCheck.running = true;
} else {
Settings.data.nightLight.enabled = false;
Settings.data.nightLight.forced = false;
NightLightService.apply();
ToastService.showNotice(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.disabled"), "nightlight-off");
}
}
}
// Temperature
ColumnLayout {
visible: Settings.data.nightLight.enabled
spacing: Style.marginM
Layout.fillWidth: true
// Night temperature
NLabel {
label: I18n.tr("settings.display.night-light.temperature.night")
description: I18n.tr("settings.display.night-light.temperature.night-description")
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NSlider {
id: nightSlider
Layout.fillWidth: true
from: 1000
to: 6500
value: Settings.data.nightLight.nightTemp
// Clamp as the thumb moves, but do NOT change Settings here
onValueChanged: {
var dayTemp = parseInt(Settings.data.nightLight.dayTemp);
var v = Math.round(value);
if (!isNaN(dayTemp)) {
var maxNight = dayTemp - 500;
v = Math.min(maxNight, Math.max(1000, v));
} else {
v = Math.max(1000, v);
}
if (v !== value)
value = v;
}
// Only write back to Settings when the user releases the slider
onPressedChanged: {
if (!pressed) {
var dayTemp = parseInt(Settings.data.nightLight.dayTemp);
var v = Math.round(value);
if (!isNaN(dayTemp)) {
var maxNight = dayTemp - 500;
v = Math.min(maxNight, Math.max(1000, v));
} else {
v = Math.max(1000, v);
}
Settings.data.nightLight.nightTemp = v;
}
}
}
NText {
text: nightSlider.value + "K"
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
}
// Day temperature
NLabel {
label: I18n.tr("settings.display.night-light.temperature.day")
description: I18n.tr("settings.display.night-light.temperature.day-description")
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NSlider {
id: daySlider
Layout.fillWidth: true
from: 1000
to: 6500
value: Settings.data.nightLight.dayTemp
// Clamp as the thumb moves, but do NOT change Settings here
onValueChanged: {
var nightTemp = parseInt(Settings.data.nightLight.nightTemp);
var v = Math.round(value);
if (!isNaN(nightTemp)) {
var minDay = nightTemp + 500;
v = Math.max(minDay, Math.min(6500, v));
} else {
v = Math.min(6500, v);
}
if (v !== value)
value = v;
}
// Only write back to Settings when the user releases the slider
onPressedChanged: {
if (!pressed) {
var nightTemp = parseInt(Settings.data.nightLight.nightTemp);
var v = Math.round(value);
if (!isNaN(nightTemp)) {
var minDay = nightTemp + 500;
v = Math.max(minDay, Math.min(6500, v));
} else {
v = Math.min(6500, v);
}
Settings.data.nightLight.dayTemp = v;
}
}
}
NText {
text: daySlider.value + "K"
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
}
}
NToggle {
label: I18n.tr("settings.display.night-light.auto-schedule.label")
description: I18n.tr("settings.display.night-light.auto-schedule.description", {
"location": LocationService.stableName
})
checked: Settings.data.nightLight.autoSchedule
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
visible: Settings.data.nightLight.enabled
}
// Manual scheduling
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
NLabel {
label: I18n.tr("settings.display.night-light.manual-schedule.label")
description: I18n.tr("settings.display.night-light.manual-schedule.description")
}
// Sunrise time
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: I18n.tr("settings.display.night-light.manual-schedule.sunrise")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunrise
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-start")
onSelected: key => Settings.data.nightLight.manualSunrise = key
Layout.fillWidth: true
}
}
// Sunset time
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: I18n.tr("settings.display.night-light.manual-schedule.sunset")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunset
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-stop")
onSelected: key => Settings.data.nightLight.manualSunset = key
Layout.fillWidth: true
}
}
}
// Force activation toggle
NToggle {
label: I18n.tr("settings.display.night-light.force-activation.label")
description: I18n.tr("settings.display.night-light.force-activation.description")
checked: Settings.data.nightLight.forced
onToggled: checked => {
Settings.data.nightLight.forced = checked;
if (checked && !Settings.data.nightLight.enabled) {
// Ensure enabled when forcing
wlsunsetCheck.running = true;
} else {
NightLightService.apply();
}
}
visible: Settings.data.nightLight.enabled
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
}
@@ -1,28 +1,13 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
// 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;
});
}
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.dock.appearance.section.label")
@@ -181,54 +166,4 @@ ColumnLayout {
defaultValue: Settings.getDefaultValue("dock.colorizeIcons")
onToggled: checked => Settings.data.dock.colorizeIcons = checked
}
NDivider {
visible: Settings.data.dock.enabled
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Monitor Configuration
ColumnLayout {
visible: Settings.data.dock.enabled
spacing: Style.marginM
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.dock.monitors.section.label")
description: I18n.tr("settings.dock.monitors.section.description")
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || "Unknown"
description: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name);
} else {
Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name);
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
}
@@ -0,0 +1,57 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
// 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;
});
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("settings.dock.tabs.appearance")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.dock.tabs.monitors")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
AppearanceSubTab {}
MonitorsSubTab {
addMonitor: root.addMonitor
removeMonitor: root.removeMonitor
}
}
}
@@ -0,0 +1,46 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var addMonitor
property var removeMonitor
NHeader {
label: I18n.tr("settings.dock.monitors.section.label")
description: I18n.tr("settings.dock.monitors.section.description")
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || "Unknown"
description: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name);
} else {
Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name);
}
}
}
}
}
@@ -0,0 +1,132 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var addMonitor
property var removeMonitor
NHeader {
label: I18n.tr("settings.notifications.settings.section.label")
description: I18n.tr("settings.notifications.settings.section.description")
}
NToggle {
label: I18n.tr("settings.notifications.settings.enabled.label")
description: I18n.tr("settings.notifications.settings.enabled.description")
checked: Settings.data.notifications.enabled !== false
onToggled: checked => Settings.data.notifications.enabled = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.enabled")
}
NToggle {
label: I18n.tr("settings.notifications.settings.do-not-disturb.label")
description: I18n.tr("settings.notifications.settings.do-not-disturb.description")
checked: NotificationService.doNotDisturb
onToggled: checked => NotificationService.doNotDisturb = checked
}
NComboBox {
label: I18n.tr("settings.notifications.settings.location.label")
description: I18n.tr("settings.notifications.settings.location.description")
model: [
{
"key": "top",
"name": I18n.tr("options.launcher.position.top_center")
},
{
"key": "top_left",
"name": I18n.tr("options.launcher.position.top_left")
},
{
"key": "top_right",
"name": I18n.tr("options.launcher.position.top_right")
},
{
"key": "bottom",
"name": I18n.tr("options.launcher.position.bottom_center")
},
{
"key": "bottom_left",
"name": I18n.tr("options.launcher.position.bottom_left")
},
{
"key": "bottom_right",
"name": I18n.tr("options.launcher.position.bottom_right")
}
]
currentKey: Settings.data.notifications.location || "top_right"
onSelected: key => Settings.data.notifications.location = key
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.location")
}
NToggle {
label: I18n.tr("settings.notifications.settings.always-on-top.label")
description: I18n.tr("settings.notifications.settings.always-on-top.description")
checked: Settings.data.notifications.overlayLayer
onToggled: checked => Settings.data.notifications.overlayLayer = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.overlayLayer")
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.settings.background-opacity.label")
description: I18n.tr("settings.notifications.settings.background-opacity.description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.notifications.backgroundOpacity
onMoved: value => Settings.data.notifications.backgroundOpacity = value
text: Math.round(Settings.data.notifications.backgroundOpacity * 100) + "%"
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.backgroundOpacity")
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
NHeader {
label: I18n.tr("settings.notifications.monitors.section.label")
description: I18n.tr("settings.notifications.monitors.section.description")
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || I18n.tr("system.unknown")
description: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.notifications.monitors = root.addMonitor(Settings.data.notifications.monitors, modelData.name);
} else {
Settings.data.notifications.monitors = root.removeMonitor(Settings.data.notifications.monitors, modelData.name);
}
}
}
}
}
@@ -0,0 +1,162 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.notifications.duration.section.label")
description: I18n.tr("settings.notifications.duration.section.description")
}
NToggle {
label: I18n.tr("settings.notifications.duration.respect-expire.label")
description: I18n.tr("settings.notifications.duration.respect-expire.description")
checked: Settings.data.notifications.respectExpireTimeout
onToggled: checked => Settings.data.notifications.respectExpireTimeout = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.respectExpireTimeout")
}
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.duration.low-urgency.label")
description: I18n.tr("settings.notifications.duration.low-urgency.description")
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.lowUrgencyDuration
onMoved: value => Settings.data.notifications.lowUrgencyDuration = value
text: Settings.data.notifications.lowUrgencyDuration + "s"
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.lowUrgencyDuration")
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.notifications.duration.reset")
onClicked: Settings.data.notifications.lowUrgencyDuration = 3
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.duration.normal-urgency.label")
description: I18n.tr("settings.notifications.duration.normal-urgency.description")
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.normalUrgencyDuration
onMoved: value => Settings.data.notifications.normalUrgencyDuration = value
text: Settings.data.notifications.normalUrgencyDuration + "s"
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.normalUrgencyDuration")
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.notifications.duration.reset")
onClicked: Settings.data.notifications.normalUrgencyDuration = 8
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.duration.critical-urgency.label")
description: I18n.tr("settings.notifications.duration.critical-urgency.description")
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.criticalUrgencyDuration
onMoved: value => Settings.data.notifications.criticalUrgencyDuration = value
text: Settings.data.notifications.criticalUrgencyDuration + "s"
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.criticalUrgencyDuration")
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.notifications.duration.reset")
onClicked: Settings.data.notifications.criticalUrgencyDuration = 15
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
NHeader {
label: I18n.tr("settings.notifications.history.section.label")
description: I18n.tr("settings.notifications.history.section.description")
}
NToggle {
label: I18n.tr("settings.notifications.history.low-urgency.label")
description: I18n.tr("settings.notifications.history.low-urgency.description")
checked: Settings.data.notifications?.saveToHistory?.low !== false
onToggled: checked => Settings.data.notifications.saveToHistory.low = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.saveToHistory.low")
}
NToggle {
label: I18n.tr("settings.notifications.history.normal-urgency.label")
description: I18n.tr("settings.notifications.history.normal-urgency.description")
checked: Settings.data.notifications?.saveToHistory?.normal !== false
onToggled: checked => Settings.data.notifications.saveToHistory.normal = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.saveToHistory.normal")
}
NToggle {
label: I18n.tr("settings.notifications.history.critical-urgency.label")
description: I18n.tr("settings.notifications.history.critical-urgency.description")
checked: Settings.data.notifications?.saveToHistory?.critical !== false
onToggled: checked => Settings.data.notifications.saveToHistory.critical = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.saveToHistory.critical")
}
}
@@ -0,0 +1,145 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
// 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;
});
}
// File pickers for sound sub-tab
function openUnifiedSoundPicker() {
unifiedSoundFilePicker.open();
}
function openLowSoundPicker() {
lowSoundFilePicker.open();
}
function openNormalSoundPicker() {
normalSoundFilePicker.open();
}
function openCriticalSoundPicker() {
criticalSoundFilePicker.open();
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("settings.notifications.tabs.general")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.notifications.tabs.sounds")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("settings.notifications.tabs.history")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
NTabButton {
text: I18n.tr("settings.notifications.tabs.toast")
tabIndex: 3
checked: subTabBar.currentIndex === 3
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
GeneralSubTab {
addMonitor: root.addMonitor
removeMonitor: root.removeMonitor
}
SoundsSubTab {
onOpenUnifiedPicker: root.openUnifiedSoundPicker()
onOpenLowPicker: root.openLowSoundPicker()
onOpenNormalPicker: root.openNormalSoundPicker()
onOpenCriticalPicker: root.openCriticalSoundPicker()
}
HistorySubTab {}
ToastSubTab {}
}
// File Pickers for Sound Files
NFilePicker {
id: unifiedSoundFilePicker
title: I18n.tr("settings.notifications.sounds.files.unified.select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
const soundPath = paths[0];
Settings.data.notifications.sounds.normalSoundFile = soundPath;
Settings.data.notifications.sounds.lowSoundFile = soundPath;
Settings.data.notifications.sounds.criticalSoundFile = soundPath;
}
}
}
NFilePicker {
id: lowSoundFilePicker
title: I18n.tr("settings.notifications.sounds.files.low.select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.notifications.sounds.lowSoundFile = paths[0];
}
}
}
NFilePicker {
id: normalSoundFilePicker
title: I18n.tr("settings.notifications.sounds.files.normal.select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.notifications.sounds.normalSoundFile = paths[0];
}
}
}
NFilePicker {
id: criticalSoundFilePicker
title: I18n.tr("settings.notifications.sounds.files.critical.select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.notifications.sounds.criticalSoundFile = paths[0];
}
}
}
}
@@ -0,0 +1,209 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
signal openUnifiedPicker
signal openLowPicker
signal openNormalPicker
signal openCriticalPicker
NHeader {
label: I18n.tr("settings.notifications.sounds.section.label")
description: I18n.tr("settings.notifications.sounds.section.description")
}
// QtMultimedia unavailable message
NBox {
Layout.fillWidth: true
visible: !SoundService.multimediaAvailable
implicitHeight: unavailableContent.implicitHeight + Style.marginL * 2
RowLayout {
id: unavailableContent
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
NIcon {
icon: "warning"
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeXL
Layout.alignment: Qt.AlignVCenter
}
NLabel {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.sounds.unavailable.label")
description: I18n.tr("settings.notifications.sounds.unavailable.description")
}
}
}
NToggle {
label: I18n.tr("settings.notifications.sounds.enabled.label")
description: I18n.tr("settings.notifications.sounds.enabled.description")
checked: Settings.data.notifications?.sounds?.enabled ?? false
visible: SoundService.multimediaAvailable
onToggled: checked => Settings.data.notifications.sounds.enabled = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.sounds.enabled")
}
// Sound Volume
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
visible: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.sounds.volume.label")
description: I18n.tr("settings.notifications.sounds.volume.description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.notifications?.sounds?.volume ?? 0.5
onMoved: value => Settings.data.notifications.sounds.volume = value
text: Math.round((Settings.data.notifications?.sounds?.volume ?? 0.5) * 100) + "%"
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.sounds.volume")
}
}
// Separate Sounds Toggle
NToggle {
Layout.fillWidth: true
visible: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
label: I18n.tr("settings.notifications.sounds.separate.label")
description: I18n.tr("settings.notifications.sounds.separate.description")
checked: Settings.data.notifications?.sounds?.separateSounds ?? false
onToggled: checked => Settings.data.notifications.sounds.separateSounds = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.sounds.separateSounds")
}
// Unified Sound File (shown when separateSounds is false)
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
visible: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && !(Settings.data.notifications?.sounds?.separateSounds ?? false)
NLabel {
label: I18n.tr("settings.notifications.sounds.files.unified.label")
description: I18n.tr("settings.notifications.sounds.files.unified.description")
}
NTextInputButton {
Layout.fillWidth: true
placeholderText: I18n.tr("settings.notifications.sounds.files.placeholder")
text: Settings.data.notifications?.sounds?.normalSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("settings.notifications.sounds.files.select-file")
onInputEditingFinished: {
const soundPath = text;
Settings.data.notifications.sounds.normalSoundFile = soundPath;
Settings.data.notifications.sounds.lowSoundFile = soundPath;
Settings.data.notifications.sounds.criticalSoundFile = soundPath;
}
onButtonClicked: root.openUnifiedPicker()
}
}
// Separate Sound Files (shown when separateSounds is true)
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
visible: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false)
// Low Urgency Sound File
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.notifications.sounds.files.low.label")
description: I18n.tr("settings.notifications.sounds.files.low.description")
}
NTextInputButton {
Layout.fillWidth: true
placeholderText: I18n.tr("settings.notifications.sounds.files.placeholder")
text: Settings.data.notifications?.sounds?.lowSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("settings.notifications.sounds.files.select-file")
onInputEditingFinished: Settings.data.notifications.sounds.lowSoundFile = text
onButtonClicked: root.openLowPicker()
}
}
// Normal Urgency Sound File
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.notifications.sounds.files.normal.label")
description: I18n.tr("settings.notifications.sounds.files.normal.description")
}
NTextInputButton {
Layout.fillWidth: true
placeholderText: I18n.tr("settings.notifications.sounds.files.placeholder")
text: Settings.data.notifications?.sounds?.normalSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("settings.notifications.sounds.files.select-file")
onInputEditingFinished: Settings.data.notifications.sounds.normalSoundFile = text
onButtonClicked: root.openNormalPicker()
}
}
// Critical Urgency Sound File
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.notifications.sounds.files.critical.label")
description: I18n.tr("settings.notifications.sounds.files.critical.description")
}
NTextInputButton {
Layout.fillWidth: true
placeholderText: I18n.tr("settings.notifications.sounds.files.placeholder")
text: Settings.data.notifications?.sounds?.criticalSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("settings.notifications.sounds.files.select-file")
onInputEditingFinished: Settings.data.notifications.sounds.criticalSoundFile = text
onButtonClicked: root.openCriticalPicker()
}
}
}
// Excluded Apps List
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
visible: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
NLabel {
label: I18n.tr("settings.notifications.sounds.excluded-apps.label")
description: I18n.tr("settings.notifications.sounds.excluded-apps.description")
}
NTextInput {
Layout.fillWidth: true
placeholderText: I18n.tr("settings.notifications.sounds.excluded-apps.placeholder")
text: Settings.data.notifications?.sounds?.excludedApps ?? ""
onEditingFinished: Settings.data.notifications.sounds.excludedApps = text
}
}
}
@@ -0,0 +1,25 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.notifications.toast.section.label")
description: I18n.tr("settings.notifications.toast.section.description")
}
NToggle {
label: I18n.tr("settings.notifications.toast.keyboard.label")
description: I18n.tr("settings.notifications.toast.keyboard.description")
checked: Settings.data.notifications.enableKeyboardLayoutToast
onToggled: checked => Settings.data.notifications.enableKeyboardLayoutToast = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.enableKeyboardLayoutToast")
}
}
@@ -1,605 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Services.System
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;
});
}
// General Notification Settings
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.notifications.settings.section.label")
description: I18n.tr("settings.notifications.settings.section.description")
}
NToggle {
label: I18n.tr("settings.notifications.settings.enabled.label")
description: I18n.tr("settings.notifications.settings.enabled.description")
checked: Settings.data.notifications.enabled !== false
onToggled: checked => Settings.data.notifications.enabled = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.enabled")
}
NToggle {
label: I18n.tr("settings.notifications.settings.do-not-disturb.label")
description: I18n.tr("settings.notifications.settings.do-not-disturb.description")
checked: NotificationService.doNotDisturb
onToggled: checked => NotificationService.doNotDisturb = checked
}
NComboBox {
label: I18n.tr("settings.notifications.settings.location.label")
description: I18n.tr("settings.notifications.settings.location.description")
model: [
{
"key": "top",
"name": I18n.tr("options.launcher.position.top_center")
},
{
"key": "top_left",
"name": I18n.tr("options.launcher.position.top_left")
},
{
"key": "top_right",
"name": I18n.tr("options.launcher.position.top_right")
},
{
"key": "bottom",
"name": I18n.tr("options.launcher.position.bottom_center")
},
{
"key": "bottom_left",
"name": I18n.tr("options.launcher.position.bottom_left")
},
{
"key": "bottom_right",
"name": I18n.tr("options.launcher.position.bottom_right")
}
]
currentKey: Settings.data.notifications.location || "top_right"
onSelected: key => Settings.data.notifications.location = key
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.location")
}
NToggle {
label: I18n.tr("settings.notifications.settings.always-on-top.label")
description: I18n.tr("settings.notifications.settings.always-on-top.description")
checked: Settings.data.notifications.overlayLayer
onToggled: checked => Settings.data.notifications.overlayLayer = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.overlayLayer")
}
// Background Opacity
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.settings.background-opacity.label")
description: I18n.tr("settings.notifications.settings.background-opacity.description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.notifications.backgroundOpacity
onMoved: value => Settings.data.notifications.backgroundOpacity = value
text: Math.round(Settings.data.notifications.backgroundOpacity * 100) + "%"
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.backgroundOpacity")
}
// OSD settings moved to the dedicated OSD tab
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Sound Settings
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.notifications.sounds.section.label")
description: I18n.tr("settings.notifications.sounds.section.description")
}
// QtMultimedia unavailable message
NBox {
Layout.fillWidth: true
visible: !SoundService.multimediaAvailable
implicitHeight: unavailableContent.implicitHeight + Style.marginL * 2
RowLayout {
id: unavailableContent
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
NIcon {
icon: "warning"
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeXL
Layout.alignment: Qt.AlignVCenter
}
NLabel {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.sounds.unavailable.label")
description: I18n.tr("settings.notifications.sounds.unavailable.description")
}
}
}
NToggle {
label: I18n.tr("settings.notifications.sounds.enabled.label")
description: I18n.tr("settings.notifications.sounds.enabled.description")
checked: Settings.data.notifications?.sounds?.enabled ?? false
visible: SoundService.multimediaAvailable
onToggled: checked => Settings.data.notifications.sounds.enabled = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.sounds.enabled")
}
// Sound Volume
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
visible: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.sounds.volume.label")
description: I18n.tr("settings.notifications.sounds.volume.description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.notifications?.sounds?.volume ?? 0.5
onMoved: value => Settings.data.notifications.sounds.volume = value
text: Math.round((Settings.data.notifications?.sounds?.volume ?? 0.5) * 100) + "%"
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.sounds.volume")
}
}
// Separate Sounds Toggle
NToggle {
Layout.fillWidth: true
visible: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
label: I18n.tr("settings.notifications.sounds.separate.label")
description: I18n.tr("settings.notifications.sounds.separate.description")
checked: Settings.data.notifications?.sounds?.separateSounds ?? false
onToggled: checked => Settings.data.notifications.sounds.separateSounds = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.sounds.separateSounds")
}
// Unified Sound File (shown when separateSounds is false)
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
visible: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && !(Settings.data.notifications?.sounds?.separateSounds ?? false)
NLabel {
label: I18n.tr("settings.notifications.sounds.files.unified.label")
description: I18n.tr("settings.notifications.sounds.files.unified.description")
}
NTextInputButton {
Layout.fillWidth: true
placeholderText: I18n.tr("settings.notifications.sounds.files.placeholder")
text: Settings.data.notifications?.sounds?.normalSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("settings.notifications.sounds.files.select-file")
onInputEditingFinished: {
const soundPath = text;
Settings.data.notifications.sounds.normalSoundFile = soundPath;
Settings.data.notifications.sounds.lowSoundFile = soundPath;
Settings.data.notifications.sounds.criticalSoundFile = soundPath;
}
onButtonClicked: unifiedSoundFilePicker.open()
}
}
// Separate Sound Files (shown when separateSounds is true)
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
visible: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false)
// Low Urgency Sound File
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.notifications.sounds.files.low.label")
description: I18n.tr("settings.notifications.sounds.files.low.description")
}
NTextInputButton {
Layout.fillWidth: true
placeholderText: I18n.tr("settings.notifications.sounds.files.placeholder")
text: Settings.data.notifications?.sounds?.lowSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("settings.notifications.sounds.files.select-file")
onInputEditingFinished: Settings.data.notifications.sounds.lowSoundFile = text
onButtonClicked: lowSoundFilePicker.open()
}
}
// Normal Urgency Sound File
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.notifications.sounds.files.normal.label")
description: I18n.tr("settings.notifications.sounds.files.normal.description")
}
NTextInputButton {
Layout.fillWidth: true
placeholderText: I18n.tr("settings.notifications.sounds.files.placeholder")
text: Settings.data.notifications?.sounds?.normalSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("settings.notifications.sounds.files.select-file")
onInputEditingFinished: Settings.data.notifications.sounds.normalSoundFile = text
onButtonClicked: normalSoundFilePicker.open()
}
}
// Critical Urgency Sound File
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.notifications.sounds.files.critical.label")
description: I18n.tr("settings.notifications.sounds.files.critical.description")
}
NTextInputButton {
Layout.fillWidth: true
placeholderText: I18n.tr("settings.notifications.sounds.files.placeholder")
text: Settings.data.notifications?.sounds?.criticalSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("settings.notifications.sounds.files.select-file")
onInputEditingFinished: Settings.data.notifications.sounds.criticalSoundFile = text
onButtonClicked: criticalSoundFilePicker.open()
}
}
}
}
// Excluded Apps List
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
visible: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
NLabel {
label: I18n.tr("settings.notifications.sounds.excluded-apps.label")
description: I18n.tr("settings.notifications.sounds.excluded-apps.description")
}
NTextInput {
Layout.fillWidth: true
placeholderText: I18n.tr("settings.notifications.sounds.excluded-apps.placeholder")
text: Settings.data.notifications?.sounds?.excludedApps ?? ""
onEditingFinished: Settings.data.notifications.sounds.excludedApps = text
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Duration
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.notifications.duration.section.label")
description: I18n.tr("settings.notifications.duration.section.description")
}
// Respect Expire Timeout (eg. --expire-time flag in notify-send)
NToggle {
label: I18n.tr("settings.notifications.duration.respect-expire.label")
description: I18n.tr("settings.notifications.duration.respect-expire.description")
checked: Settings.data.notifications.respectExpireTimeout
onToggled: checked => Settings.data.notifications.respectExpireTimeout = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.respectExpireTimeout")
}
// Low Urgency Duration
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.duration.low-urgency.label")
description: I18n.tr("settings.notifications.duration.low-urgency.description")
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.lowUrgencyDuration
onMoved: value => Settings.data.notifications.lowUrgencyDuration = value
text: Settings.data.notifications.lowUrgencyDuration + "s"
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.lowUrgencyDuration")
}
// Reset button container
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.notifications.duration.reset")
onClicked: Settings.data.notifications.lowUrgencyDuration = 3
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Normal Urgency Duration
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.duration.normal-urgency.label")
description: I18n.tr("settings.notifications.duration.normal-urgency.description")
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.normalUrgencyDuration
onMoved: value => Settings.data.notifications.normalUrgencyDuration = value
text: Settings.data.notifications.normalUrgencyDuration + "s"
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.normalUrgencyDuration")
}
// Reset button container
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.notifications.duration.reset")
onClicked: Settings.data.notifications.normalUrgencyDuration = 8
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Critical Urgency Duration
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.notifications.duration.critical-urgency.label")
description: I18n.tr("settings.notifications.duration.critical-urgency.description")
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.criticalUrgencyDuration
onMoved: value => Settings.data.notifications.criticalUrgencyDuration = value
text: Settings.data.notifications.criticalUrgencyDuration + "s"
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.criticalUrgencyDuration")
}
// Reset button container
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.notifications.duration.reset")
onClicked: Settings.data.notifications.criticalUrgencyDuration = 15
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// History Configuration
NHeader {
label: I18n.tr("settings.notifications.history.section.label")
description: I18n.tr("settings.notifications.history.section.description")
}
NToggle {
label: I18n.tr("settings.notifications.history.low-urgency.label")
description: I18n.tr("settings.notifications.history.low-urgency.description")
checked: Settings.data.notifications?.saveToHistory?.low !== false
onToggled: checked => Settings.data.notifications.saveToHistory.low = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.saveToHistory.low")
}
NToggle {
label: I18n.tr("settings.notifications.history.normal-urgency.label")
description: I18n.tr("settings.notifications.history.normal-urgency.description")
checked: Settings.data.notifications?.saveToHistory?.normal !== false
onToggled: checked => Settings.data.notifications.saveToHistory.normal = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.saveToHistory.normal")
}
NToggle {
label: I18n.tr("settings.notifications.history.critical-urgency.label")
description: I18n.tr("settings.notifications.history.critical-urgency.description")
checked: Settings.data.notifications?.saveToHistory?.critical !== false
onToggled: checked => Settings.data.notifications.saveToHistory.critical = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.saveToHistory.critical")
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Monitor Configuration
NHeader {
label: I18n.tr("settings.notifications.monitors.section.label")
description: I18n.tr("settings.notifications.monitors.section.description")
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || I18n.tr("system.unknown")
description: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, modelData.name);
} else {
Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, modelData.name);
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Toast Configuration
NHeader {
label: I18n.tr("settings.notifications.toast.section.label")
description: I18n.tr("settings.notifications.toast.section.description")
}
NToggle {
label: I18n.tr("settings.notifications.toast.keyboard.label")
description: I18n.tr("settings.notifications.toast.keyboard.description")
checked: Settings.data.notifications.enableKeyboardLayoutToast
onToggled: checked => Settings.data.notifications.enableKeyboardLayoutToast = checked
isSettings: true
defaultValue: Settings.getDefaultValue("notifications.enableKeyboardLayoutToast")
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// File Pickers for Sound Files
NFilePicker {
id: unifiedSoundFilePicker
title: I18n.tr("settings.notifications.sounds.files.unified.select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
const soundPath = paths[0];
Settings.data.notifications.sounds.normalSoundFile = soundPath;
Settings.data.notifications.sounds.lowSoundFile = soundPath;
Settings.data.notifications.sounds.criticalSoundFile = soundPath;
}
}
}
NFilePicker {
id: lowSoundFilePicker
title: I18n.tr("settings.notifications.sounds.files.low.select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.notifications.sounds.lowSoundFile = paths[0];
}
}
}
NFilePicker {
id: normalSoundFilePicker
title: I18n.tr("settings.notifications.sounds.files.normal.select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.notifications.sounds.normalSoundFile = paths[0];
}
}
}
NFilePicker {
id: criticalSoundFilePicker
title: I18n.tr("settings.notifications.sounds.files.critical.select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.notifications.sounds.criticalSoundFile = paths[0];
}
}
}
}
@@ -0,0 +1,100 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var screen
NHeader {
Layout.fillWidth: true
label: I18n.tr("settings.system-monitor.general.section.label")
description: I18n.tr("settings.system-monitor.general.section.description")
}
NToggle {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
label: I18n.tr("settings.system-monitor.enable-dgpu-monitoring.label")
description: I18n.tr("settings.system-monitor.enable-dgpu-monitoring.description")
checked: Settings.data.systemMonitor.enableDgpuMonitoring
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.enableDgpuMonitoring")
onToggled: checked => Settings.data.systemMonitor.enableDgpuMonitoring = checked
}
// Colors Section
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NToggle {
label: I18n.tr("settings.system-monitor.use-custom-highlight-colors.label")
description: I18n.tr("settings.system-monitor.use-custom-highlight-colors.description")
checked: Settings.data.systemMonitor.useCustomColors
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.useCustomColors")
onToggled: checked => {
// If enabling custom colors and no custom color is saved, persist current theme colors
if (checked) {
if (!Settings.data.systemMonitor.warningColor || Settings.data.systemMonitor.warningColor === "") {
Settings.data.systemMonitor.warningColor = Color.mTertiary.toString();
}
if (!Settings.data.systemMonitor.criticalColor || Settings.data.systemMonitor.criticalColor === "") {
Settings.data.systemMonitor.criticalColor = Color.mError.toString();
}
}
Settings.data.systemMonitor.useCustomColors = checked;
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
visible: Settings.data.systemMonitor.useCustomColors
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
text: I18n.tr("settings.system-monitor.warning-color.label")
pointSize: Style.fontSizeS
}
NColorPicker {
screen: root.screen
Layout.preferredWidth: Style.sliderWidth
Layout.preferredHeight: Style.baseWidgetSize
enabled: Settings.data.systemMonitor.useCustomColors
selectedColor: Settings.data.systemMonitor.warningColor || Color.mTertiary
onColorSelected: color => Settings.data.systemMonitor.warningColor = color
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
text: I18n.tr("settings.system-monitor.critical-color.label")
pointSize: Style.fontSizeS
}
NColorPicker {
screen: root.screen
Layout.preferredWidth: Style.sliderWidth
Layout.preferredHeight: Style.baseWidgetSize
enabled: Settings.data.systemMonitor.useCustomColors
selectedColor: Settings.data.systemMonitor.criticalColor || Color.mError
onColorSelected: color => Settings.data.systemMonitor.criticalColor = color
}
}
}
}
@@ -0,0 +1,195 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
Layout.fillWidth: true
label: I18n.tr("settings.system-monitor.polling-section.label")
description: I18n.tr("settings.system-monitor.polling-section.description")
}
// CPU Polling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.fillWidth: true
text: I18n.tr("settings.system-monitor.cpu-section.label")
pointSize: Style.fontSizeM
}
NSpinBox {
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.cpuPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.cpuPollingInterval")
onValueChanged: Settings.data.systemMonitor.cpuPollingInterval = value
suffix: " ms"
}
}
// Temperature Polling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.fillWidth: true
text: I18n.tr("settings.system-monitor.temperature-section.label")
pointSize: Style.fontSizeM
}
NSpinBox {
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.tempPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.tempPollingInterval")
onValueChanged: Settings.data.systemMonitor.tempPollingInterval = value
suffix: " ms"
}
}
// GPU Polling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
visible: SystemStatService.gpuAvailable
NText {
Layout.fillWidth: true
text: I18n.tr("settings.system-monitor.gpu-section.label")
pointSize: Style.fontSizeM
}
NSpinBox {
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.gpuPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.gpuPollingInterval")
onValueChanged: Settings.data.systemMonitor.gpuPollingInterval = value
suffix: " ms"
}
}
// Load Average Polling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.fillWidth: true
text: I18n.tr("settings.system-monitor.load-average-section.label")
pointSize: Style.fontSizeM
}
NSpinBox {
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.loadAvgPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.loadAvgPollingInterval")
onValueChanged: Settings.data.systemMonitor.loadAvgPollingInterval = value
suffix: " ms"
}
}
// Memory Polling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.fillWidth: true
text: I18n.tr("settings.system-monitor.memory-section.label")
pointSize: Style.fontSizeM
}
NSpinBox {
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.memPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.memPollingInterval")
onValueChanged: Settings.data.systemMonitor.memPollingInterval = value
suffix: " ms"
}
}
// Disk Polling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.fillWidth: true
text: I18n.tr("settings.system-monitor.disk-section.label")
pointSize: Style.fontSizeM
}
NSpinBox {
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.diskPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.diskPollingInterval")
onValueChanged: Settings.data.systemMonitor.diskPollingInterval = value
suffix: " ms"
}
}
// Network Polling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.fillWidth: true
text: I18n.tr("settings.system-monitor.network-section.label")
pointSize: Style.fontSizeM
}
NSpinBox {
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.networkPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.networkPollingInterval")
onValueChanged: Settings.data.systemMonitor.networkPollingInterval = value
suffix: " ms"
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
}
NTextInput {
label: I18n.tr("settings.system-monitor.external-monitor.label")
description: I18n.tr("settings.system-monitor.external-monitor.description")
placeholderText: I18n.tr("settings.system-monitor.external-monitor.placeholder")
text: Settings.data.systemMonitor.externalMonitor
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.externalMonitor")
onTextChanged: Settings.data.systemMonitor.externalMonitor = text
}
}
@@ -0,0 +1,51 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
property var screen
NTabBar {
id: subTabBar
Layout.fillWidth: true
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("settings.system-monitor.tabs.general")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.system-monitor.tabs.thresholds")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("settings.system-monitor.tabs.polling")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
GeneralSubTab {
screen: root.screen
}
ThresholdsSubTab {}
PollingSubTab {}
}
}
@@ -1,109 +1,14 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.System
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
property var screen
spacing: Style.marginL
NHeader {
Layout.fillWidth: true
label: I18n.tr("settings.system-monitor.general.section.label")
description: I18n.tr("settings.system-monitor.general.section.description")
}
NToggle {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
label: I18n.tr("settings.system-monitor.enable-dgpu-monitoring.label")
description: I18n.tr("settings.system-monitor.enable-dgpu-monitoring.description")
checked: Settings.data.systemMonitor.enableDgpuMonitoring
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.enableDgpuMonitoring")
onToggled: checked => Settings.data.systemMonitor.enableDgpuMonitoring = checked
}
// Colors Section
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NToggle {
label: I18n.tr("settings.system-monitor.use-custom-highlight-colors.label")
description: I18n.tr("settings.system-monitor.use-custom-highlight-colors.description")
checked: Settings.data.systemMonitor.useCustomColors
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.useCustomColors")
onToggled: checked => {
// If enabling custom colors and no custom color is saved, persist current theme colors
if (checked) {
if (!Settings.data.systemMonitor.warningColor || Settings.data.systemMonitor.warningColor === "") {
Settings.data.systemMonitor.warningColor = Color.mTertiary.toString();
}
if (!Settings.data.systemMonitor.criticalColor || Settings.data.systemMonitor.criticalColor === "") {
Settings.data.systemMonitor.criticalColor = Color.mError.toString();
}
}
Settings.data.systemMonitor.useCustomColors = checked;
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
visible: Settings.data.systemMonitor.useCustomColors
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
text: I18n.tr("settings.system-monitor.warning-color.label")
pointSize: Style.fontSizeS
}
NColorPicker {
screen: root.screen
Layout.preferredWidth: Style.sliderWidth
Layout.preferredHeight: Style.baseWidgetSize
enabled: Settings.data.systemMonitor.useCustomColors
selectedColor: Settings.data.systemMonitor.warningColor || Color.mTertiary
onColorSelected: color => Settings.data.systemMonitor.warningColor = color
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
text: I18n.tr("settings.system-monitor.critical-color.label")
pointSize: Style.fontSizeS
}
NColorPicker {
screen: root.screen
Layout.preferredWidth: Style.sliderWidth
Layout.preferredHeight: Style.baseWidgetSize
enabled: Settings.data.systemMonitor.useCustomColors
selectedColor: Settings.data.systemMonitor.criticalColor || Color.mError
onColorSelected: color => Settings.data.systemMonitor.criticalColor = color
}
}
}
NDivider {
Layout.fillWidth: true
}
Layout.fillWidth: true
NHeader {
Layout.fillWidth: true
@@ -144,7 +49,6 @@ ColumnLayout {
defaultValue: Settings.getDefaultValue("systemMonitor.cpuWarningThreshold")
onValueChanged: {
Settings.data.systemMonitor.cpuWarningThreshold = value;
// Ensure critical >= warning
if (Settings.data.systemMonitor.cpuCriticalThreshold < value) {
Settings.data.systemMonitor.cpuCriticalThreshold = value;
}
@@ -176,30 +80,6 @@ ColumnLayout {
suffix: "%"
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.polling-interval.label")
pointSize: Style.fontSizeS
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.cpuPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.cpuPollingInterval")
onValueChanged: Settings.data.systemMonitor.cpuPollingInterval = value
suffix: " ms"
}
}
}
// Temperature
@@ -266,30 +146,6 @@ ColumnLayout {
suffix: "°C"
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.polling-interval.label")
pointSize: Style.fontSizeS
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.tempPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.tempPollingInterval")
onValueChanged: Settings.data.systemMonitor.tempPollingInterval = value
suffix: " ms"
}
}
}
// GPU Temperature
@@ -358,67 +214,6 @@ ColumnLayout {
suffix: "°C"
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.polling-interval.label")
pointSize: Style.fontSizeS
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.gpuPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.gpuPollingInterval")
onValueChanged: Settings.data.systemMonitor.gpuPollingInterval = value
suffix: " ms"
}
}
}
// Load Average
NText {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
text: I18n.tr("settings.system-monitor.load-average-section.label")
pointSize: Style.fontSizeM
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.polling-interval.label")
pointSize: Style.fontSizeS
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.loadAvgPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.loadAvgPollingInterval")
onValueChanged: Settings.data.systemMonitor.loadAvgPollingInterval = value
suffix: " ms"
}
}
}
// Memory Usage
@@ -485,30 +280,6 @@ ColumnLayout {
suffix: "%"
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.polling-interval.label")
pointSize: Style.fontSizeS
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.memPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.memPollingInterval")
onValueChanged: Settings.data.systemMonitor.memPollingInterval = value
suffix: " ms"
}
}
}
// Disk Usage
@@ -575,82 +346,5 @@ ColumnLayout {
suffix: "%"
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.polling-interval.label")
pointSize: Style.fontSizeS
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.diskPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.diskPollingInterval")
onValueChanged: Settings.data.systemMonitor.diskPollingInterval = value
suffix: " ms"
}
}
}
// Network
NText {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
text: I18n.tr("settings.system-monitor.network-section.label")
pointSize: Style.fontSizeM
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("settings.system-monitor.polling-interval.label")
pointSize: Style.fontSizeS
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.networkPollingInterval
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.networkPollingInterval")
onValueChanged: Settings.data.systemMonitor.networkPollingInterval = value
suffix: " ms"
}
}
}
NTextInput {
label: I18n.tr("settings.system-monitor.external-monitor.label")
description: I18n.tr("settings.system-monitor.external-monitor.description")
placeholderText: I18n.tr("settings.system-monitor.external-monitor.placeholder")
text: Settings.data.systemMonitor.externalMonitor
isSettings: true
defaultValue: Settings.getDefaultValue("systemMonitor.externalMonitor")
onTextChanged: Settings.data.systemMonitor.externalMonitor = text
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
}
@@ -0,0 +1,272 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.user-interface.appearance.section.label")
description: I18n.tr("settings.user-interface.appearance.section.description")
}
NToggle {
label: I18n.tr("settings.user-interface.tooltips.label")
description: I18n.tr("settings.user-interface.tooltips.description")
checked: Settings.data.ui.tooltipsEnabled
isSettings: true
defaultValue: Settings.getDefaultValue("ui.tooltipsEnabled")
onToggled: checked => Settings.data.ui.tooltipsEnabled = checked
}
NToggle {
label: I18n.tr("settings.user-interface.box-border.label")
description: I18n.tr("settings.user-interface.box-border.description")
checked: Settings.data.ui.boxBorderEnabled
isSettings: true
defaultValue: Settings.getDefaultValue("ui.boxBorderEnabled")
onToggled: checked => Settings.data.ui.boxBorderEnabled = checked
}
NToggle {
label: I18n.tr("settings.user-interface.shadows.label")
description: I18n.tr("settings.user-interface.shadows.description")
checked: Settings.data.general.enableShadows
isSettings: true
defaultValue: Settings.getDefaultValue("general.enableShadows")
onToggled: checked => Settings.data.general.enableShadows = checked
}
NComboBox {
visible: Settings.data.general.enableShadows
label: I18n.tr("settings.user-interface.shadows.direction.label")
description: I18n.tr("settings.user-interface.shadows.direction.description")
Layout.fillWidth: true
readonly property var shadowOptionsMap: ({
"top_left": {
"name": I18n.tr("options.shadow-direction.top_left"),
"p": Qt.point(-2, -2)
},
"top": {
"name": I18n.tr("options.shadow-direction.top"),
"p": Qt.point(0, -3)
},
"top_right": {
"name": I18n.tr("options.shadow-direction.top_right"),
"p": Qt.point(2, -2)
},
"left": {
"name": I18n.tr("options.shadow-direction.left"),
"p": Qt.point(-3, 0)
},
"center": {
"name": I18n.tr("options.shadow-direction.center"),
"p": Qt.point(0, 0)
},
"right": {
"name": I18n.tr("options.shadow-direction.right"),
"p": Qt.point(3, 0)
},
"bottom_left": {
"name": I18n.tr("options.shadow-direction.bottom_left"),
"p": Qt.point(-2, 2)
},
"bottom": {
"name": I18n.tr("options.shadow-direction.bottom"),
"p": Qt.point(0, 3)
},
"bottom_right": {
"name": I18n.tr("options.shadow-direction.bottom_right"),
"p": Qt.point(2, 3)
}
})
model: Object.keys(shadowOptionsMap).map(function (k) {
return {
"key": k,
"name": shadowOptionsMap[k].name
};
})
currentKey: Settings.data.general.shadowDirection
isSettings: true
defaultValue: Settings.getDefaultValue("general.shadowDirection")
onSelected: function (key) {
var opt = shadowOptionsMap[key];
if (opt) {
Settings.data.general.shadowDirection = key;
Settings.data.general.shadowOffsetX = opt.p.x;
Settings.data.general.shadowOffsetY = opt.p.y;
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.user-interface.scaling.label")
description: I18n.tr("settings.user-interface.scaling.description")
from: 0.8
to: 1.2
stepSize: 0.05
value: Settings.data.general.scaleRatio
isSettings: true
defaultValue: Settings.getDefaultValue("general.scaleRatio")
onMoved: value => Settings.data.general.scaleRatio = value
text: Math.floor(Settings.data.general.scaleRatio * 100) + "%"
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.user-interface.scaling.reset-scaling")
onClicked: Settings.data.general.scaleRatio = 1.0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.user-interface.box-border-radius.label")
description: I18n.tr("settings.user-interface.box-border-radius.description")
from: 0
to: 2
stepSize: 0.01
value: Settings.data.general.radiusRatio
isSettings: true
defaultValue: Settings.getDefaultValue("general.radiusRatio")
onMoved: value => Settings.data.general.radiusRatio = value
text: Math.floor(Settings.data.general.radiusRatio * 100) + "%"
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.user-interface.box-border-radius.reset")
onClicked: Settings.data.general.radiusRatio = 1.0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.user-interface.control-border-radius.label")
description: I18n.tr("settings.user-interface.control-border-radius.description")
from: 0
to: 2
stepSize: 0.01
value: Settings.data.general.iRadiusRatio
isSettings: true
defaultValue: Settings.getDefaultValue("general.iRadiusRatio")
onMoved: value => Settings.data.general.iRadiusRatio = value
text: Math.floor(Settings.data.general.iRadiusRatio * 100) + "%"
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.user-interface.control-border-radius.reset")
onClicked: Settings.data.general.iRadiusRatio = 1.0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
visible: !Settings.data.general.animationDisabled
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.user-interface.animation-speed.label")
description: I18n.tr("settings.user-interface.animation-speed.description")
from: 0
to: 2.0
stepSize: 0.01
value: Settings.data.general.animationSpeed
isSettings: true
defaultValue: Settings.getDefaultValue("general.animationSpeed")
onMoved: value => Settings.data.general.animationSpeed = Math.max(value, 0.05)
text: Math.round(Settings.data.general.animationSpeed * 100) + "%"
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.user-interface.animation-speed.reset")
onClicked: Settings.data.general.animationSpeed = 1.0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
NToggle {
label: I18n.tr("settings.user-interface.animation-disable.label")
description: I18n.tr("settings.user-interface.animation-disable.description")
checked: Settings.data.general.animationDisabled
isSettings: true
defaultValue: Settings.getDefaultValue("general.animationDisabled")
onToggled: checked => Settings.data.general.animationDisabled = checked
}
}
}
@@ -0,0 +1,88 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.user-interface.section.label")
description: I18n.tr("settings.user-interface.section.description")
}
NToggle {
label: I18n.tr("settings.user-interface.panels-attached-to-bar.label")
description: I18n.tr("settings.user-interface.panels-attached-to-bar.description")
checked: Settings.data.ui.panelsAttachedToBar
isSettings: true
defaultValue: Settings.getDefaultValue("ui.panelsAttachedToBar")
onToggled: checked => Settings.data.ui.panelsAttachedToBar = checked
}
NToggle {
visible: (Quickshell.screens.length > 1)
label: I18n.tr("settings.user-interface.allow-panels-without-bar.label")
description: I18n.tr("settings.user-interface.allow-panels-without-bar.description")
checked: Settings.data.general.allowPanelsOnScreenWithoutBar
isSettings: true
defaultValue: Settings.getDefaultValue("general.allowPanelsOnScreenWithoutBar")
onToggled: checked => Settings.data.general.allowPanelsOnScreenWithoutBar = checked
}
NComboBox {
label: I18n.tr("settings.user-interface.settings-panel-mode.label")
description: I18n.tr("settings.user-interface.settings-panel-mode.description")
Layout.fillWidth: true
model: [
{
"key": "attached",
"name": I18n.tr("options.settings-panel-mode.attached")
},
{
"key": "centered",
"name": I18n.tr("options.settings-panel-mode.centered")
},
{
"key": "window",
"name": I18n.tr("options.settings-panel-mode.window")
}
]
currentKey: Settings.data.ui.settingsPanelMode
isSettings: true
defaultValue: Settings.getDefaultValue("ui.settingsPanelMode")
onSelected: key => Settings.data.ui.settingsPanelMode = key
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.user-interface.panel-background-opacity.label")
description: I18n.tr("settings.user-interface.panel-background-opacity.description")
from: 0.4
to: 1
stepSize: 0.01
value: Settings.data.ui.panelBackgroundOpacity
isSettings: true
defaultValue: Settings.getDefaultValue("ui.panelBackgroundOpacity")
onMoved: value => Settings.data.ui.panelBackgroundOpacity = value
text: Math.floor(Settings.data.ui.panelBackgroundOpacity * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.user-interface.dimmer-opacity.label")
description: I18n.tr("settings.user-interface.dimmer-opacity.description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.general.dimmerOpacity
isSettings: true
defaultValue: Settings.getDefaultValue("general.dimmerOpacity")
onMoved: value => Settings.data.general.dimmerOpacity = value
text: Math.floor(Settings.data.general.dimmerOpacity * 100) + "%"
}
}
@@ -0,0 +1,72 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.general.screen-corners.section.label")
description: I18n.tr("settings.general.screen-corners.section.description")
}
NToggle {
label: I18n.tr("settings.general.screen-corners.show-corners.label")
description: I18n.tr("settings.general.screen-corners.show-corners.description")
checked: Settings.data.general.showScreenCorners
isSettings: true
defaultValue: Settings.getDefaultValue("general.showScreenCorners")
onToggled: checked => Settings.data.general.showScreenCorners = checked
}
NToggle {
label: I18n.tr("settings.general.screen-corners.solid-black.label")
description: I18n.tr("settings.general.screen-corners.solid-black.description")
checked: Settings.data.general.forceBlackScreenCorners
isSettings: true
defaultValue: Settings.getDefaultValue("general.forceBlackScreenCorners")
onToggled: checked => Settings.data.general.forceBlackScreenCorners = checked
}
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.general.screen-corners.radius.label")
description: I18n.tr("settings.general.screen-corners.radius.description")
from: 0
to: 2
stepSize: 0.01
value: Settings.data.general.screenRadiusRatio
isSettings: true
defaultValue: Settings.getDefaultValue("general.screenRadiusRatio")
onMoved: value => Settings.data.general.screenRadiusRatio = value
text: Math.floor(Settings.data.general.screenRadiusRatio * 100) + "%"
}
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.general.screen-corners.radius.reset")
onClicked: Settings.data.general.screenRadiusRatio = 1.0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
@@ -0,0 +1,47 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("settings.user-interface.tabs.panels")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.user-interface.tabs.appearance")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("settings.user-interface.tabs.screen-corners")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
PanelsSubTab {}
AppearanceSubTab {}
ScreenCornersSubTab {}
}
}
@@ -1,447 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
// User Interface
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.user-interface.section.label")
description: I18n.tr("settings.user-interface.section.description")
}
// Panels attached to bar and screen edges
NToggle {
label: I18n.tr("settings.user-interface.panels-attached-to-bar.label")
description: I18n.tr("settings.user-interface.panels-attached-to-bar.description")
checked: Settings.data.ui.panelsAttachedToBar
isSettings: true
defaultValue: Settings.getDefaultValue("ui.panelsAttachedToBar")
onToggled: checked => Settings.data.ui.panelsAttachedToBar = checked
}
NToggle {
visible: (Quickshell.screens.length > 1)
label: I18n.tr("settings.user-interface.allow-panels-without-bar.label")
description: I18n.tr("settings.user-interface.allow-panels-without-bar.description")
checked: Settings.data.general.allowPanelsOnScreenWithoutBar
isSettings: true
defaultValue: Settings.getDefaultValue("general.allowPanelsOnScreenWithoutBar")
onToggled: checked => Settings.data.general.allowPanelsOnScreenWithoutBar = checked
}
// Settings panel display mode
NComboBox {
label: I18n.tr("settings.user-interface.settings-panel-mode.label")
description: I18n.tr("settings.user-interface.settings-panel-mode.description")
Layout.fillWidth: true
model: [
{
"key": "attached",
"name": I18n.tr("options.settings-panel-mode.attached")
},
{
"key": "centered",
"name": I18n.tr("options.settings-panel-mode.centered")
},
{
"key": "window",
"name": I18n.tr("options.settings-panel-mode.window")
}
]
currentKey: Settings.data.ui.settingsPanelMode
isSettings: true
defaultValue: Settings.getDefaultValue("ui.settingsPanelMode")
onSelected: key => Settings.data.ui.settingsPanelMode = key
}
// Panel Background Opacity
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.user-interface.panel-background-opacity.label")
description: I18n.tr("settings.user-interface.panel-background-opacity.description")
from: 0.4
to: 1
stepSize: 0.01
value: Settings.data.ui.panelBackgroundOpacity
isSettings: true
defaultValue: Settings.getDefaultValue("ui.panelBackgroundOpacity")
onMoved: value => Settings.data.ui.panelBackgroundOpacity = value
text: Math.floor(Settings.data.ui.panelBackgroundOpacity * 100) + "%"
}
// Dim desktop opacity
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.user-interface.dimmer-opacity.label")
description: I18n.tr("settings.user-interface.dimmer-opacity.description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.general.dimmerOpacity
isSettings: true
defaultValue: Settings.getDefaultValue("general.dimmerOpacity")
onMoved: value => Settings.data.general.dimmerOpacity = value
text: Math.floor(Settings.data.general.dimmerOpacity * 100) + "%"
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
NToggle {
label: I18n.tr("settings.user-interface.tooltips.label")
description: I18n.tr("settings.user-interface.tooltips.description")
checked: Settings.data.ui.tooltipsEnabled
isSettings: true
defaultValue: Settings.getDefaultValue("ui.tooltipsEnabled")
onToggled: checked => Settings.data.ui.tooltipsEnabled = checked
}
NToggle {
label: I18n.tr("settings.user-interface.box-border.label")
description: I18n.tr("settings.user-interface.box-border.description")
checked: Settings.data.ui.boxBorderEnabled
isSettings: true
defaultValue: Settings.getDefaultValue("ui.boxBorderEnabled")
onToggled: checked => Settings.data.ui.boxBorderEnabled = checked
}
NToggle {
label: I18n.tr("settings.user-interface.shadows.label")
description: I18n.tr("settings.user-interface.shadows.description")
checked: Settings.data.general.enableShadows
isSettings: true
defaultValue: Settings.getDefaultValue("general.enableShadows")
onToggled: checked => Settings.data.general.enableShadows = checked
}
// Shadow direction
NComboBox {
visible: Settings.data.general.enableShadows
label: I18n.tr("settings.user-interface.shadows.direction.label")
description: I18n.tr("settings.user-interface.shadows.direction.description")
Layout.fillWidth: true
readonly property var shadowOptionsMap: ({
"top_left": {
"name": I18n.tr("options.shadow-direction.top_left"),
"p": Qt.point(-2, -2)
},
"top": {
"name": I18n.tr("options.shadow-direction.top"),
"p": Qt.point(0, -3)
},
"top_right": {
"name": I18n.tr("options.shadow-direction.top_right"),
"p": Qt.point(2, -2)
},
"left": {
"name": I18n.tr("options.shadow-direction.left"),
"p": Qt.point(-3, 0)
},
"center": {
"name": I18n.tr("options.shadow-direction.center"),
"p": Qt.point(0, 0)
},
"right": {
"name": I18n.tr("options.shadow-direction.right"),
"p": Qt.point(3, 0)
},
"bottom_left": {
"name": I18n.tr("options.shadow-direction.bottom_left"),
"p": Qt.point(-2, 2)
},
"bottom": {
"name": I18n.tr("options.shadow-direction.bottom"),
"p": Qt.point(0, 3)
},
"bottom_right": {
"name": I18n.tr("options.shadow-direction.bottom_right"),
"p": Qt.point(2, 3)
}
})
model: Object.keys(shadowOptionsMap).map(function (k) {
return {
"key": k,
"name": shadowOptionsMap[k].name
};
})
currentKey: Settings.data.general.shadowDirection
isSettings: true
defaultValue: Settings.getDefaultValue("general.shadowDirection")
onSelected: function (key) {
var opt = shadowOptionsMap[key];
if (opt) {
Settings.data.general.shadowDirection = key;
Settings.data.general.shadowOffsetX = opt.p.x;
Settings.data.general.shadowOffsetY = opt.p.y;
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// User Interface Scaling
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.user-interface.scaling.label")
description: I18n.tr("settings.user-interface.scaling.description")
from: 0.8
to: 1.2
stepSize: 0.05
value: Settings.data.general.scaleRatio
isSettings: true
defaultValue: Settings.getDefaultValue("general.scaleRatio")
onMoved: value => Settings.data.general.scaleRatio = value
text: Math.floor(Settings.data.general.scaleRatio * 100) + "%"
}
// Reset button container
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.user-interface.scaling.reset-scaling")
onClicked: Settings.data.general.scaleRatio = 1.0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
// Container Border Radius
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.user-interface.box-border-radius.label")
description: I18n.tr("settings.user-interface.box-border-radius.description")
from: 0
to: 2
stepSize: 0.01
value: Settings.data.general.radiusRatio
isSettings: true
defaultValue: Settings.getDefaultValue("general.radiusRatio")
onMoved: value => Settings.data.general.radiusRatio = value
text: Math.floor(Settings.data.general.radiusRatio * 100) + "%"
}
// Reset button container
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.user-interface.box-border-radius.reset")
onClicked: Settings.data.general.radiusRatio = 1.0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Control Border Radius (for UI components)
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.user-interface.control-border-radius.label")
description: I18n.tr("settings.user-interface.control-border-radius.description")
from: 0
to: 2
stepSize: 0.01
value: Settings.data.general.iRadiusRatio
isSettings: true
defaultValue: Settings.getDefaultValue("general.iRadiusRatio")
onMoved: value => Settings.data.general.iRadiusRatio = value
text: Math.floor(Settings.data.general.iRadiusRatio * 100) + "%"
}
// Reset button container
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.user-interface.control-border-radius.reset")
onClicked: Settings.data.general.iRadiusRatio = 1.0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Animation Speed
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
visible: !Settings.data.general.animationDisabled
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.user-interface.animation-speed.label")
description: I18n.tr("settings.user-interface.animation-speed.description")
from: 0
to: 2.0
stepSize: 0.01
value: Settings.data.general.animationSpeed
isSettings: true
defaultValue: Settings.getDefaultValue("general.animationSpeed")
onMoved: value => Settings.data.general.animationSpeed = Math.max(value, 0.05)
text: Math.round(Settings.data.general.animationSpeed * 100) + "%"
}
// Reset button container
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.user-interface.animation-speed.reset")
onClicked: Settings.data.general.animationSpeed = 1.0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
NToggle {
label: I18n.tr("settings.user-interface.animation-disable.label")
description: I18n.tr("settings.user-interface.animation-disable.description")
checked: Settings.data.general.animationDisabled
isSettings: true
defaultValue: Settings.getDefaultValue("general.animationDisabled")
onToggled: checked => Settings.data.general.animationDisabled = checked
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
// Dock
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.general.screen-corners.section.label")
description: I18n.tr("settings.general.screen-corners.section.description")
}
NToggle {
label: I18n.tr("settings.general.screen-corners.show-corners.label")
description: I18n.tr("settings.general.screen-corners.show-corners.description")
checked: Settings.data.general.showScreenCorners
isSettings: true
defaultValue: Settings.getDefaultValue("general.showScreenCorners")
onToggled: checked => Settings.data.general.showScreenCorners = checked
}
NToggle {
label: I18n.tr("settings.general.screen-corners.solid-black.label")
description: I18n.tr("settings.general.screen-corners.solid-black.description")
checked: Settings.data.general.forceBlackScreenCorners
isSettings: true
defaultValue: Settings.getDefaultValue("general.forceBlackScreenCorners")
onToggled: checked => Settings.data.general.forceBlackScreenCorners = checked
}
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.general.screen-corners.radius.label")
description: I18n.tr("settings.general.screen-corners.radius.description")
from: 0
to: 2
stepSize: 0.01
value: Settings.data.general.screenRadiusRatio
isSettings: true
defaultValue: Settings.getDefaultValue("general.screenRadiusRatio")
onMoved: value => Settings.data.general.screenRadiusRatio = value
text: Math.floor(Settings.data.general.screenRadiusRatio * 100) + "%"
}
// Reset button container
Item {
Layout.preferredWidth: 30 * Style.uiScaleRatio
Layout.preferredHeight: 30 * Style.uiScaleRatio
NIconButton {
icon: "restore"
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("settings.general.screen-corners.radius.reset")
onClicked: Settings.data.general.screenRadiusRatio = 1.0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
}
@@ -0,0 +1,157 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.wallpaper.automation.section.label")
}
NToggle {
label: I18n.tr("settings.wallpaper.automation.scheduled-change.label")
description: I18n.tr("settings.wallpaper.automation.scheduled-change.description")
checked: Settings.data.wallpaper.randomEnabled
onToggled: checked => Settings.data.wallpaper.randomEnabled = checked
}
NComboBox {
visible: Settings.data.wallpaper.randomEnabled
label: I18n.tr("settings.wallpaper.automation.change-mode.label")
description: I18n.tr("settings.wallpaper.automation.change-mode.description")
Layout.fillWidth: true
model: [
{
"key": "random",
"name": I18n.tr("settings.wallpaper.automation.change-mode.random")
},
{
"key": "alphabetical",
"name": I18n.tr("settings.wallpaper.automation.change-mode.alphabetical")
}
]
currentKey: Settings.data.wallpaper.wallpaperChangeMode || "random"
onSelected: key => Settings.data.wallpaper.wallpaperChangeMode = key
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.transitionType")
}
ColumnLayout {
visible: Settings.data.wallpaper.randomEnabled
RowLayout {
NLabel {
label: I18n.tr("settings.wallpaper.automation.interval.label")
description: I18n.tr("settings.wallpaper.automation.interval.description")
Layout.fillWidth: true
}
NText {
text: Time.formatVagueHumanReadableDuration(Settings.data.wallpaper.randomIntervalSec)
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
}
}
RowLayout {
id: presetRow
spacing: Style.marginS
property var intervalPresets: [5 * 60, 10 * 60, 15 * 60, 30 * 60, 45 * 60, 60 * 60, 90 * 60, 120 * 60]
property bool isCurrentPreset: {
return intervalPresets.some(seconds => seconds === Settings.data.wallpaper.randomIntervalSec);
}
property bool customForcedVisible: false
function setIntervalSeconds(sec) {
Settings.data.wallpaper.randomIntervalSec = sec;
WallpaperService.restartRandomWallpaperTimer();
customForcedVisible = false;
}
function isSelected(sec) {
return Settings.data.wallpaper.randomIntervalSec === sec;
}
Repeater {
model: presetRow.intervalPresets
delegate: IntervalPresetChip {
seconds: modelData
label: Time.formatVagueHumanReadableDuration(modelData)
selected: presetRow.isSelected(modelData)
onClicked: presetRow.setIntervalSeconds(modelData)
}
}
IntervalPresetChip {
label: customRow.visible ? "Custom" : "Custom…"
selected: customRow.visible
onClicked: presetRow.customForcedVisible = !presetRow.customForcedVisible
}
}
RowLayout {
id: customRow
visible: presetRow.customForcedVisible || !presetRow.isCurrentPreset
spacing: Style.marginS
Layout.topMargin: Style.marginS
NTextInput {
label: I18n.tr("settings.wallpaper.automation.custom-interval.label")
description: I18n.tr("settings.wallpaper.automation.custom-interval.description")
text: {
const s = Settings.data.wallpaper.randomIntervalSec;
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
return h + ":" + (m < 10 ? ("0" + m) : m);
}
onEditingFinished: {
const m = text.trim().match(/^(\d{1,2}):(\d{2})$/);
if (m) {
let h = parseInt(m[1]);
let min = parseInt(m[2]);
if (isNaN(h) || isNaN(min))
return;
h = Math.max(0, Math.min(24, h));
min = Math.max(0, Math.min(59, min));
Settings.data.wallpaper.randomIntervalSec = (h * 3600) + (min * 60);
WallpaperService.restartRandomWallpaperTimer();
presetRow.customForcedVisible = true;
}
}
}
}
}
component IntervalPresetChip: Rectangle {
property int seconds: 0
property string label: ""
property bool selected: false
signal clicked
radius: height * 0.5
color: selected ? Color.mPrimary : Color.mSurfaceVariant
implicitHeight: Math.max(Style.baseWidgetSize * 0.55, 24)
implicitWidth: chipLabel.implicitWidth + Style.marginM * 1.5
border.width: Style.borderS
border.color: selected ? "transparent" : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: parent.clicked()
}
NText {
id: chipLabel
anchors.centerIn: parent
text: parent.label
pointSize: Style.fontSizeS
color: parent.selected ? Color.mOnPrimary : Color.mOnSurface
}
}
}
@@ -0,0 +1,79 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var screen
NHeader {
label: I18n.tr("settings.wallpaper.look-feel.section.label")
}
NComboBox {
label: I18n.tr("settings.wallpaper.look-feel.fill-mode.label")
description: I18n.tr("settings.wallpaper.look-feel.fill-mode.description")
model: WallpaperService.fillModeModel
currentKey: Settings.data.wallpaper.fillMode
onSelected: key => Settings.data.wallpaper.fillMode = key
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.fillMode")
}
RowLayout {
NLabel {
label: I18n.tr("settings.wallpaper.look-feel.fill-color.label")
description: I18n.tr("settings.wallpaper.look-feel.fill-color.description")
Layout.alignment: Qt.AlignTop
}
NColorPicker {
screen: root.screen
selectedColor: Settings.data.wallpaper.fillColor
onColorSelected: color => Settings.data.wallpaper.fillColor = color
}
}
NComboBox {
label: I18n.tr("settings.wallpaper.look-feel.transition-type.label")
description: I18n.tr("settings.wallpaper.look-feel.transition-type.description")
model: WallpaperService.transitionsModel
currentKey: Settings.data.wallpaper.transitionType
onSelected: key => Settings.data.wallpaper.transitionType = key
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.transitionType")
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.wallpaper.look-feel.transition-duration.label")
description: I18n.tr("settings.wallpaper.look-feel.transition-duration.description")
from: 500
to: 10000
stepSize: 100
value: Settings.data.wallpaper.transitionDuration
onMoved: value => Settings.data.wallpaper.transitionDuration = value
text: (Settings.data.wallpaper.transitionDuration / 1000).toFixed(1) + "s"
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.transitionDuration")
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.wallpaper.look-feel.edge-smoothness.label")
description: I18n.tr("settings.wallpaper.look-feel.edge-smoothness.description")
from: 0.0
to: 1.0
value: Settings.data.wallpaper.transitionEdgeSmoothness
onMoved: value => Settings.data.wallpaper.transitionEdgeSmoothness = value
text: Math.round(Settings.data.wallpaper.transitionEdgeSmoothness * 100) + "%"
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.transitionEdgeSmoothness")
}
}
@@ -0,0 +1,180 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var screen
signal openMainFolderPicker
signal openMonitorFolderPicker(string monitorName)
NHeader {
label: I18n.tr("settings.wallpaper.settings.section.label")
description: I18n.tr("settings.wallpaper.settings.section.description")
}
NToggle {
label: I18n.tr("settings.wallpaper.settings.enable-management.label")
description: I18n.tr("settings.wallpaper.settings.enable-management.description")
checked: Settings.data.wallpaper.enabled
onToggled: checked => Settings.data.wallpaper.enabled = checked
Layout.bottomMargin: Style.marginL
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.enabled")
}
NToggle {
visible: Settings.data.wallpaper.enabled && CompositorService.isNiri
label: I18n.tr("settings.wallpaper.settings.enable-overview.label")
description: I18n.tr("settings.wallpaper.settings.enable-overview.description")
checked: Settings.data.wallpaper.overviewEnabled
onToggled: checked => Settings.data.wallpaper.overviewEnabled = checked
Layout.bottomMargin: Style.marginL
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.overviewEnabled")
}
ColumnLayout {
visible: Settings.data.wallpaper.enabled
spacing: Style.marginL
Layout.fillWidth: true
NTextInputButton {
id: wallpaperPathInput
label: I18n.tr("settings.wallpaper.settings.folder.label")
description: I18n.tr("settings.wallpaper.settings.folder.description")
text: Settings.data.wallpaper.directory
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("settings.wallpaper.settings.folder.tooltip")
Layout.fillWidth: true
onInputEditingFinished: Settings.data.wallpaper.directory = text
onButtonClicked: root.openMainFolderPicker()
}
RowLayout {
NLabel {
label: I18n.tr("settings.wallpaper.settings.selector.label")
description: I18n.tr("settings.wallpaper.settings.selector.description")
Layout.alignment: Qt.AlignTop
}
NIconButton {
icon: "wallpaper-selector"
tooltipText: I18n.tr("settings.wallpaper.settings.selector.tooltip")
onClicked: PanelService.getPanel("wallpaperPanel", root.screen)?.toggle()
}
}
NToggle {
label: I18n.tr("settings.wallpaper.settings.recursive-search.label")
description: I18n.tr("settings.wallpaper.settings.recursive-search.description")
checked: Settings.data.wallpaper.recursiveSearch
onToggled: checked => Settings.data.wallpaper.recursiveSearch = checked
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.recursiveSearch")
}
NToggle {
label: I18n.tr("settings.wallpaper.settings.monitor-specific.label")
description: I18n.tr("settings.wallpaper.settings.monitor-specific.description")
checked: Settings.data.wallpaper.enableMultiMonitorDirectories
onToggled: checked => Settings.data.wallpaper.enableMultiMonitorDirectories = checked
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.enableMultiMonitorDirectories")
}
NBox {
visible: Settings.data.wallpaper.enableMultiMonitorDirectories
Layout.fillWidth: true
radius: Style.radiusM
color: Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS
implicitHeight: contentCol.implicitHeight + Style.marginL * 2
clip: true
ColumnLayout {
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
Repeater {
model: Quickshell.screens || []
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: (modelData.name || "Unknown")
color: Color.mPrimary
font.weight: Style.fontWeightBold
pointSize: Style.fontSizeM
}
NTextInputButton {
text: WallpaperService.getMonitorDirectory(modelData.name)
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("settings.wallpaper.settings.monitor-specific.tooltip")
Layout.fillWidth: true
onInputEditingFinished: WallpaperService.setMonitorDirectory(modelData.name, text)
onButtonClicked: root.openMonitorFolderPicker(modelData.name)
}
}
}
}
}
NComboBox {
label: I18n.tr("settings.wallpaper.settings.selector-position.label")
description: I18n.tr("settings.wallpaper.settings.selector-position.description")
Layout.fillWidth: true
model: [
{
"key": "follow_bar",
"name": I18n.tr("options.launcher.position.follow_bar")
},
{
"key": "center",
"name": I18n.tr("options.launcher.position.center")
},
{
"key": "top_center",
"name": I18n.tr("options.launcher.position.top_center")
},
{
"key": "top_left",
"name": I18n.tr("options.launcher.position.top_left")
},
{
"key": "top_right",
"name": I18n.tr("options.launcher.position.top_right")
},
{
"key": "bottom_left",
"name": I18n.tr("options.launcher.position.bottom_left")
},
{
"key": "bottom_right",
"name": I18n.tr("options.launcher.position.bottom_right")
},
{
"key": "bottom_center",
"name": I18n.tr("options.launcher.position.bottom_center")
}
]
currentKey: Settings.data.wallpaper.panelPosition
onSelected: key => Settings.data.wallpaper.panelPosition = key
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.panelPosition")
}
}
}
@@ -0,0 +1,92 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
property var screen
function openMainFolderPicker() {
mainFolderPicker.open();
}
function openMonitorFolderPicker(monitorName) {
specificFolderMonitorName = monitorName;
monitorFolderPicker.open();
}
property string specificFolderMonitorName: ""
NTabBar {
id: subTabBar
Layout.fillWidth: true
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("settings.wallpaper.tabs.settings")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("settings.wallpaper.tabs.look-feel")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("settings.wallpaper.tabs.automation")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
SettingsSubTab {
screen: root.screen
onOpenMainFolderPicker: root.openMainFolderPicker()
onOpenMonitorFolderPicker: monitorName => root.openMonitorFolderPicker(monitorName)
}
LookAndFeelSubTab {
screen: root.screen
}
AutomationSubTab {}
}
NFilePicker {
id: mainFolderPicker
selectionMode: "folders"
title: I18n.tr("settings.wallpaper.settings.select-folder")
initialPath: Settings.data.wallpaper.directory || Quickshell.env("HOME") + "/Pictures"
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.wallpaper.directory = paths[0];
}
}
}
NFilePicker {
id: monitorFolderPicker
selectionMode: "folders"
title: I18n.tr("settings.wallpaper.settings.select-monitor-folder")
initialPath: WallpaperService.getMonitorDirectory(specificFolderMonitorName) || Quickshell.env("HOME") + "/Pictures"
onAccepted: paths => {
if (paths.length > 0) {
WallpaperService.setMonitorDirectory(specificFolderMonitorName, paths[0]);
}
}
}
}
@@ -1,482 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.Compositor
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
property var screen
property string specificFolderMonitorName: ""
spacing: Style.marginL
NHeader {
label: I18n.tr("settings.wallpaper.settings.section.label")
description: I18n.tr("settings.wallpaper.settings.section.description")
}
NToggle {
label: I18n.tr("settings.wallpaper.settings.enable-management.label")
description: I18n.tr("settings.wallpaper.settings.enable-management.description")
checked: Settings.data.wallpaper.enabled
onToggled: checked => Settings.data.wallpaper.enabled = checked
Layout.bottomMargin: Style.marginL
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.enabled")
}
NToggle {
visible: Settings.data.wallpaper.enabled && CompositorService.isNiri
label: I18n.tr("settings.wallpaper.settings.enable-overview.label")
description: I18n.tr("settings.wallpaper.settings.enable-overview.description")
checked: Settings.data.wallpaper.overviewEnabled
onToggled: checked => Settings.data.wallpaper.overviewEnabled = checked
Layout.bottomMargin: Style.marginL
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.overviewEnabled")
}
NDivider {
visible: Settings.data.wallpaper.enabled
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
ColumnLayout {
visible: Settings.data.wallpaper.enabled
spacing: Style.marginL
Layout.fillWidth: true
NTextInputButton {
id: wallpaperPathInput
label: I18n.tr("settings.wallpaper.settings.folder.label")
description: I18n.tr("settings.wallpaper.settings.folder.description")
text: Settings.data.wallpaper.directory
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("settings.wallpaper.settings.folder.tooltip")
Layout.fillWidth: true
onInputEditingFinished: Settings.data.wallpaper.directory = text
onButtonClicked: mainFolderPicker.open()
}
RowLayout {
NLabel {
label: I18n.tr("settings.wallpaper.settings.selector.label")
description: I18n.tr("settings.wallpaper.settings.selector.description")
Layout.alignment: Qt.AlignTop
}
NIconButton {
icon: "wallpaper-selector"
tooltipText: I18n.tr("settings.wallpaper.settings.selector.tooltip")
onClicked: PanelService.getPanel("wallpaperPanel", screen)?.toggle()
}
}
// Recursive search
NToggle {
label: I18n.tr("settings.wallpaper.settings.recursive-search.label")
description: I18n.tr("settings.wallpaper.settings.recursive-search.description")
checked: Settings.data.wallpaper.recursiveSearch
onToggled: checked => Settings.data.wallpaper.recursiveSearch = checked
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.recursiveSearch")
}
// Monitor-specific directories
NToggle {
label: I18n.tr("settings.wallpaper.settings.monitor-specific.label")
description: I18n.tr("settings.wallpaper.settings.monitor-specific.description")
checked: Settings.data.wallpaper.enableMultiMonitorDirectories
onToggled: checked => Settings.data.wallpaper.enableMultiMonitorDirectories = checked
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.enableMultiMonitorDirectories")
}
NBox {
visible: Settings.data.wallpaper.enableMultiMonitorDirectories
Layout.fillWidth: true
radius: Style.radiusM
color: Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS
implicitHeight: contentCol.implicitHeight + Style.marginL * 2
clip: true
ColumnLayout {
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
Repeater {
model: Quickshell.screens || []
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: (modelData.name || "Unknown")
color: Color.mPrimary
font.weight: Style.fontWeightBold
pointSize: Style.fontSizeM
}
NTextInputButton {
text: WallpaperService.getMonitorDirectory(modelData.name)
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("settings.wallpaper.settings.monitor-specific.tooltip")
Layout.fillWidth: true
onInputEditingFinished: WallpaperService.setMonitorDirectory(modelData.name, text)
onButtonClicked: {
specificFolderMonitorName = modelData.name;
monitorFolderPicker.open();
}
}
}
}
}
}
NComboBox {
label: I18n.tr("settings.wallpaper.settings.selector-position.label")
description: I18n.tr("settings.wallpaper.settings.selector-position.description")
Layout.fillWidth: true
model: [
{
"key": "follow_bar",
"name": I18n.tr("options.launcher.position.follow_bar")
},
{
"key": "center",
"name": I18n.tr("options.launcher.position.center")
},
{
"key": "top_center",
"name": I18n.tr("options.launcher.position.top_center")
},
{
"key": "top_left",
"name": I18n.tr("options.launcher.position.top_left")
},
{
"key": "top_right",
"name": I18n.tr("options.launcher.position.top_right")
},
{
"key": "bottom_left",
"name": I18n.tr("options.launcher.position.bottom_left")
},
{
"key": "bottom_right",
"name": I18n.tr("options.launcher.position.bottom_right")
},
{
"key": "bottom_center",
"name": I18n.tr("options.launcher.position.bottom_center")
}
]
currentKey: Settings.data.wallpaper.panelPosition
onSelected: key => Settings.data.wallpaper.panelPosition = key
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.panelPosition")
}
}
NDivider {
visible: Settings.data.wallpaper.enabled
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
ColumnLayout {
visible: Settings.data.wallpaper.enabled
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.wallpaper.look-feel.section.label")
}
// Fill Mode
NComboBox {
label: I18n.tr("settings.wallpaper.look-feel.fill-mode.label")
description: I18n.tr("settings.wallpaper.look-feel.fill-mode.description")
model: WallpaperService.fillModeModel
currentKey: Settings.data.wallpaper.fillMode
onSelected: key => Settings.data.wallpaper.fillMode = key
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.fillMode")
}
RowLayout {
NLabel {
label: I18n.tr("settings.wallpaper.look-feel.fill-color.label")
description: I18n.tr("settings.wallpaper.look-feel.fill-color.description")
Layout.alignment: Qt.AlignTop
}
NColorPicker {
screen: root.screen
selectedColor: Settings.data.wallpaper.fillColor
onColorSelected: color => Settings.data.wallpaper.fillColor = color
}
}
// Transition Type
NComboBox {
label: I18n.tr("settings.wallpaper.look-feel.transition-type.label")
description: I18n.tr("settings.wallpaper.look-feel.transition-type.description")
model: WallpaperService.transitionsModel
currentKey: Settings.data.wallpaper.transitionType
onSelected: key => Settings.data.wallpaper.transitionType = key
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.transitionType")
}
// Transition Duration
ColumnLayout {
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.wallpaper.look-feel.transition-duration.label")
description: I18n.tr("settings.wallpaper.look-feel.transition-duration.description")
from: 500
to: 10000
stepSize: 100
value: Settings.data.wallpaper.transitionDuration
onMoved: value => Settings.data.wallpaper.transitionDuration = value
text: (Settings.data.wallpaper.transitionDuration / 1000).toFixed(1) + "s"
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.transitionDuration")
}
}
// Edge Smoothness
ColumnLayout {
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("settings.wallpaper.look-feel.edge-smoothness.label")
description: I18n.tr("settings.wallpaper.look-feel.edge-smoothness.description")
from: 0.0
to: 1.0
value: Settings.data.wallpaper.transitionEdgeSmoothness
onMoved: value => Settings.data.wallpaper.transitionEdgeSmoothness = value
text: Math.round(Settings.data.wallpaper.transitionEdgeSmoothness * 100) + "%"
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.transitionEdgeSmoothness")
}
}
}
NDivider {
visible: Settings.data.wallpaper.enabled
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
ColumnLayout {
visible: Settings.data.wallpaper.enabled
spacing: Style.marginL
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.wallpaper.automation.section.label")
}
// Scheduled change toggle
NToggle {
label: I18n.tr("settings.wallpaper.automation.scheduled-change.label")
description: I18n.tr("settings.wallpaper.automation.scheduled-change.description")
checked: Settings.data.wallpaper.randomEnabled
onToggled: checked => Settings.data.wallpaper.randomEnabled = checked
}
// Change mode combo box
NComboBox {
visible: Settings.data.wallpaper.randomEnabled
label: I18n.tr("settings.wallpaper.automation.change-mode.label")
description: I18n.tr("settings.wallpaper.automation.change-mode.description")
Layout.fillWidth: true
model: [
{
"key": "random",
"name": I18n.tr("settings.wallpaper.automation.change-mode.random")
},
{
"key": "alphabetical",
"name": I18n.tr("settings.wallpaper.automation.change-mode.alphabetical")
}
]
currentKey: Settings.data.wallpaper.wallpaperChangeMode || "random"
onSelected: key => Settings.data.wallpaper.wallpaperChangeMode = key
isSettings: true
defaultValue: Settings.getDefaultValue("wallpaper.transitionType")
}
// Interval
ColumnLayout {
visible: Settings.data.wallpaper.randomEnabled
RowLayout {
NLabel {
label: I18n.tr("settings.wallpaper.automation.interval.label")
description: I18n.tr("settings.wallpaper.automation.interval.description")
Layout.fillWidth: true
}
NText {
// Show friendly H:MM format from current settings
text: Time.formatVagueHumanReadableDuration(Settings.data.wallpaper.randomIntervalSec)
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
}
}
// Preset chips using Repeater
RowLayout {
id: presetRow
spacing: Style.marginS
// Factorized presets data
property var intervalPresets: [5 * 60, 10 * 60, 15 * 60, 30 * 60, 45 * 60, 60 * 60, 90 * 60, 120 * 60]
// Whether current interval equals one of the presets
property bool isCurrentPreset: {
return intervalPresets.some(seconds => seconds === Settings.data.wallpaper.randomIntervalSec);
}
// Allow user to force open the custom input; otherwise it's auto-open when not a preset
property bool customForcedVisible: false
function setIntervalSeconds(sec) {
Settings.data.wallpaper.randomIntervalSec = sec;
WallpaperService.restartRandomWallpaperTimer();
// Hide custom when selecting a preset
customForcedVisible = false;
}
// Helper to color selected chip
function isSelected(sec) {
return Settings.data.wallpaper.randomIntervalSec === sec;
}
// Repeater for preset chips
Repeater {
model: presetRow.intervalPresets
delegate: IntervalPresetChip {
seconds: modelData
label: Time.formatVagueHumanReadableDuration(modelData)
selected: presetRow.isSelected(modelData)
onClicked: presetRow.setIntervalSeconds(modelData)
}
}
// Custom… opens inline input
IntervalPresetChip {
label: customRow.visible ? "Custom" : "Custom…"
selected: customRow.visible
onClicked: presetRow.customForcedVisible = !presetRow.customForcedVisible
}
}
// Custom HH:MM inline input
RowLayout {
id: customRow
visible: presetRow.customForcedVisible || !presetRow.isCurrentPreset
spacing: Style.marginS
Layout.topMargin: Style.marginS
NTextInput {
label: I18n.tr("settings.wallpaper.automation.custom-interval.label")
description: I18n.tr("settings.wallpaper.automation.custom-interval.description")
text: {
const s = Settings.data.wallpaper.randomIntervalSec;
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
return h + ":" + (m < 10 ? ("0" + m) : m);
}
onEditingFinished: {
const m = text.trim().match(/^(\d{1,2}):(\d{2})$/);
if (m) {
let h = parseInt(m[1]);
let min = parseInt(m[2]);
if (isNaN(h) || isNaN(min))
return;
h = Math.max(0, Math.min(24, h));
min = Math.max(0, Math.min(59, min));
Settings.data.wallpaper.randomIntervalSec = (h * 3600) + (min * 60);
WallpaperService.restartRandomWallpaperTimer();
// Keep custom visible after manual entry
presetRow.customForcedVisible = true;
}
}
}
}
}
}
// Reusable component for interval preset chips
component IntervalPresetChip: Rectangle {
property int seconds: 0
property string label: ""
property bool selected: false
signal clicked
radius: height * 0.5
color: selected ? Color.mPrimary : Color.mSurfaceVariant
implicitHeight: Math.max(Style.baseWidgetSize * 0.55, 24)
implicitWidth: chipLabel.implicitWidth + Style.marginM * 1.5
border.width: Style.borderS
border.color: selected ? "transparent" : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: parent.clicked()
}
NText {
id: chipLabel
anchors.centerIn: parent
text: parent.label
pointSize: Style.fontSizeS
color: parent.selected ? Color.mOnPrimary : Color.mOnSurface
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
Layout.bottomMargin: Style.marginL
}
NFilePicker {
id: mainFolderPicker
selectionMode: "folders"
title: I18n.tr("settings.wallpaper.settings.select-folder")
initialPath: Settings.data.wallpaper.directory || Quickshell.env("HOME") + "/Pictures"
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.wallpaper.directory = paths[0];
}
}
}
NFilePicker {
id: monitorFolderPicker
selectionMode: "folders"
title: I18n.tr("settings.wallpaper.settings.select-monitor-folder")
initialPath: WallpaperService.getMonitorDirectory(specificFolderMonitorName) || Quickshell.env("HOME") + "/Pictures"
onAccepted: paths => {
if (paths.length > 0) {
WallpaperService.setMonitorDirectory(specificFolderMonitorName, paths[0]);
}
}
}
}
+114
View File
@@ -0,0 +1,114 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
Item {
id: root
property int currentIndex: 0
// Private
property int previousIndex: 0
property bool initialized: false
property bool animating: false
property real animatingHeight: 0
property real transitionGap: Style.marginXL
property real transitionTime: Style.animationNormal
property list<Item> contentItems: []
default property alias content: container.data
clip: true
Layout.fillWidth: true
// During animation, use max height to prevent clipping. Otherwise use current item height.
implicitHeight: animating ? animatingHeight : (contentItems[currentIndex] ? contentItems[currentIndex].implicitHeight : 0)
Item {
id: container
anchors.fill: parent
}
Component.onCompleted: {
_initializeItems();
}
function _initializeItems() {
contentItems = [];
for (let i = 0; i < container.children.length; i++) {
const child = container.children[i];
contentItems.push(child);
child.width = Qt.binding(() => root.width);
if (i === currentIndex) {
child.x = 0;
child.visible = true;
} else {
child.x = root.width;
child.visible = false;
}
}
initialized = true;
}
onCurrentIndexChanged: {
if (!initialized || contentItems.length === 0)
return;
if (previousIndex === currentIndex)
return;
_animateTransition(previousIndex, currentIndex);
previousIndex = currentIndex;
}
function _animateTransition(fromIdx, toIdx) {
const fromItem = contentItems[fromIdx];
const toItem = contentItems[toIdx];
const slideLeft = toIdx > fromIdx;
// Set height to max of both items during animation
const fromHeight = fromItem ? fromItem.implicitHeight : 0;
const toHeight = toItem ? toItem.implicitHeight : 0;
animatingHeight = Math.max(fromHeight, toHeight);
animating = true;
// Position incoming item off-screen (with gap)
if (toItem) {
toItem.visible = true;
toItem.x = slideLeft ? root.width + transitionGap : -root.width - transitionGap;
}
// Animate both items together (with gap)
if (fromItem) {
fromAnim.target = fromItem;
fromAnim.to = slideLeft ? -root.width - transitionGap : root.width + transitionGap;
fromAnim.start();
}
if (toItem) {
toAnim.target = toItem;
toAnim.start();
}
}
NumberAnimation {
id: fromAnim
property: "x"
duration: root.transitionTime
easing.type: Easing.OutCubic
onFinished: {
if (target && target !== contentItems[currentIndex]) {
target.visible = false;
}
animating = false;
}
}
NumberAnimation {
id: toAnim
property: "x"
to: 0
duration: root.transitionTime
easing.type: Easing.OutCubic
}
}