add noctalia and fuzzel

This commit is contained in:
2026-05-23 21:20:58 +02:00
parent 364801f1a3
commit 9a3eceb3ab
599 changed files with 204318 additions and 0 deletions
@@ -0,0 +1,379 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Compositor
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-position-label")
description: I18n.tr("panels.bar.appearance-position-description")
model: [
{
"key": "top",
"name": I18n.tr("positions.top")
},
{
"key": "bottom",
"name": I18n.tr("positions.bottom")
},
{
"key": "left",
"name": I18n.tr("positions.left")
},
{
"key": "right",
"name": I18n.tr("positions.right")
}
]
currentKey: Settings.data.bar.position
defaultValue: Settings.getDefaultValue("bar.position")
onSelected: key => Settings.data.bar.position = key
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-density-label")
description: I18n.tr("panels.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
defaultValue: Settings.getDefaultValue("bar.density")
onSelected: key => Settings.data.bar.density = key
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-type-label")
description: I18n.tr("panels.bar.appearance-type-description")
model: [
{
"key": "simple",
"name": I18n.tr("options.bar.type-simple")
},
{
"key": "floating",
"name": I18n.tr("options.bar.type-floating")
},
{
"key": "framed",
"name": I18n.tr("options.bar.type-framed")
}
]
currentKey: Settings.data.bar.barType
defaultValue: Settings.getDefaultValue("bar.barType")
onSelected: key => {
Settings.data.bar.barType = key;
}
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("common.display-mode")
description: I18n.tr("panels.bar.appearance-display-mode-description")
model: [
{
"key": "always_visible",
"name": I18n.tr("hide-modes.visible")
},
{
"key": "non_exclusive",
"name": I18n.tr("hide-modes.non-exclusive")
},
{
"key": "auto_hide",
"name": I18n.tr("hide-modes.auto-hide")
}
]
currentKey: Settings.data.bar.displayMode
defaultValue: Settings.getDefaultValue("bar.displayMode")
onSelected: key => Settings.data.bar.displayMode = key
}
NToggle {
label: I18n.tr("panels.bar.appearance-use-separate-opacity-label")
description: I18n.tr("panels.bar.appearance-use-separate-opacity-description")
checked: Settings.data.bar.useSeparateOpacity
defaultValue: Settings.getDefaultValue("bar.useSeparateOpacity")
onToggled: checked => Settings.data.bar.useSeparateOpacity = checked
}
NValueSlider {
Layout.fillWidth: true
visible: Settings.data.bar.useSeparateOpacity
label: I18n.tr("panels.bar.appearance-background-opacity-label")
description: I18n.tr("panels.bar.appearance-background-opacity-description")
from: 0
to: 1
stepSize: 0.01
showReset: true
value: Settings.data.bar.backgroundOpacity
defaultValue: Settings.getDefaultValue("bar.backgroundOpacity")
onMoved: value => Settings.data.bar.backgroundOpacity = value
text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-font-scale-label")
description: I18n.tr("panels.bar.appearance-font-scale-description")
from: 0.5
to: 2.0
stepSize: 0.01
showReset: true
value: Settings.data.bar.fontScale
defaultValue: Settings.getDefaultValue("bar.fontScale")
onMoved: value => Settings.data.bar.fontScale = value
text: Math.floor(Settings.data.bar.fontScale * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-widget-spacing-label")
description: I18n.tr("panels.bar.appearance-widget-spacing-description")
from: 0
to: 30
stepSize: 1
showReset: true
value: Settings.data.bar.widgetSpacing
defaultValue: Settings.getDefaultValue("bar.widgetSpacing")
onMoved: value => Settings.data.bar.widgetSpacing = value
text: Settings.data.bar.widgetSpacing + "px"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-content-padding-label")
description: I18n.tr("panels.bar.appearance-content-padding-description")
from: 0
to: 30
stepSize: 1
showReset: true
value: Settings.data.bar.contentPadding
defaultValue: Settings.getDefaultValue("bar.contentPadding")
onMoved: value => Settings.data.bar.contentPadding = value
text: Settings.data.bar.contentPadding + "px"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-show-outline-label")
description: I18n.tr("panels.bar.appearance-show-outline-description")
checked: Settings.data.bar.showOutline
defaultValue: Settings.getDefaultValue("bar.showOutline")
onToggled: checked => Settings.data.bar.showOutline = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-show-capsule-label")
description: I18n.tr("panels.bar.appearance-show-capsule-description")
checked: Settings.data.bar.showCapsule
defaultValue: Settings.getDefaultValue("bar.showCapsule")
onToggled: checked => Settings.data.bar.showCapsule = checked
}
NColorChoice {
Layout.fillWidth: true
visible: Settings.data.bar.showCapsule
label: I18n.tr("panels.bar.appearance-capsule-color-label")
description: I18n.tr("panels.bar.appearance-capsule-color-description")
noneColor: Color.mSurfaceVariant
noneOnColor: Color.mOnSurfaceVariant
currentKey: Settings.data.bar.capsuleColorKey
onSelected: key => Settings.data.bar.capsuleColorKey = key
}
NValueSlider {
Layout.fillWidth: true
visible: Settings.data.bar.showCapsule
label: I18n.tr("panels.bar.appearance-capsule-opacity-label")
description: I18n.tr("panels.bar.appearance-capsule-opacity-description")
from: 0
to: 1
stepSize: 0.01
showReset: true
value: Settings.data.bar.capsuleOpacity
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("panels.bar.appearance-enable-exclusion-zone-inset-label")
description: I18n.tr("panels.bar.appearance-enable-exclusion-zone-inset-description")
checked: Settings.data.bar.enableExclusionZoneInset
defaultValue: Settings.getDefaultValue("bar.enableExclusionZoneInset")
onToggled: checked => Settings.data.bar.enableExclusionZoneInset = checked
}
NToggle {
Layout.fillWidth: true
visible: CompositorService.isNiri
label: I18n.tr("panels.bar.appearance-hide-on-overview-label")
description: I18n.tr("panels.bar.appearance-hide-on-overview-description")
checked: Settings.data.bar.hideOnOverview
defaultValue: Settings.getDefaultValue("bar.hideOnOverview")
onToggled: checked => Settings.data.bar.hideOnOverview = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-outer-corners-label")
description: I18n.tr("panels.bar.appearance-outer-corners-description")
checked: Settings.data.bar.outerCorners
visible: Settings.data.bar.barType === "simple"
defaultValue: Settings.getDefaultValue("bar.outerCorners")
onToggled: checked => Settings.data.bar.outerCorners = checked
}
ColumnLayout {
visible: Settings.data.bar.barType === "framed"
spacing: Style.marginS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.bar.appearance-frame-settings-label")
description: I18n.tr("panels.bar.appearance-frame-settings-description")
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-frame-thickness")
from: 4
to: 24
stepSize: 1
showReset: true
value: Settings.data.bar.frameThickness
defaultValue: Settings.getDefaultValue("bar.frameThickness")
onMoved: value => Settings.data.bar.frameThickness = value
text: Settings.data.bar.frameThickness + "px"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-frame-radius")
from: 4
to: 24
stepSize: 1
showReset: true
value: Settings.data.bar.frameRadius
defaultValue: Settings.getDefaultValue("bar.frameRadius")
onMoved: value => Settings.data.bar.frameRadius = value
text: Settings.data.bar.frameRadius + "px"
}
}
}
ColumnLayout {
visible: Settings.data.bar.barType === "floating"
spacing: Style.marginL
Layout.fillWidth: true
NDivider {
Layout.fillWidth: true
}
NSpinBox {
label: I18n.tr("panels.bar.appearance-margins-vertical")
description: I18n.tr("panels.bar.appearance-margins-description")
from: 0
to: 500
suffix: "px"
value: Settings.data.bar.marginVertical
defaultValue: Settings.getDefaultValue("bar.marginVertical")
onValueChanged: Settings.data.bar.marginVertical = value
}
NSpinBox {
label: I18n.tr("panels.bar.appearance-margins-horizontal")
description: I18n.tr("panels.bar.appearance-margins-description")
from: 0
to: 500
suffix: "px"
value: Settings.data.bar.marginHorizontal
defaultValue: Settings.getDefaultValue("bar.marginHorizontal")
onValueChanged: Settings.data.bar.marginHorizontal = value
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginS
visible: Settings.data.bar.displayMode === "auto_hide"
}
ColumnLayout {
visible: Settings.data.bar.displayMode === "auto_hide"
spacing: Style.marginS
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-auto-hide-delay-label")
description: I18n.tr("panels.bar.appearance-auto-hide-delay-description")
from: 100
to: 2000
stepSize: 100
showReset: true
value: Settings.data.bar.autoHideDelay
defaultValue: Settings.getDefaultValue("bar.autoHideDelay")
onMoved: value => Settings.data.bar.autoHideDelay = value
text: Settings.data.bar.autoHideDelay + "ms"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-auto-show-delay-label")
description: I18n.tr("panels.bar.appearance-auto-show-delay-description")
from: 0
to: 500
stepSize: 50
showReset: true
value: Settings.data.bar.autoShowDelay
defaultValue: Settings.getDefaultValue("bar.autoShowDelay")
onMoved: value => Settings.data.bar.autoShowDelay = value
text: Settings.data.bar.autoShowDelay + "ms"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-show-on-workspace-switch-label")
description: I18n.tr("panels.bar.appearance-show-on-workspace-switch-description")
checked: Settings.data.bar.showOnWorkspaceSwitch
defaultValue: Settings.getDefaultValue("bar.showOnWorkspaceSwitch")
onToggled: checked => Settings.data.bar.showOnWorkspaceSwitch = checked
}
}
}
@@ -0,0 +1,234 @@
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 (global widgets only).
// These intentionally edit Settings.data.bar.widgets (global defaults),
// not per-screen overrides. Per-screen editing is handled by MonitorWidgetsConfig.qml.
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) {
newWidget[key] = metadata[key];
});
}
}
Settings.data.bar.widgets[section].push(newWidget);
BarService.widgetsRevision++;
}
function _removeWidgetFromSection(section, index) {
var widgets = Settings.data.bar.widgets;
if (index >= 0 && index < widgets[section].length) {
var newArray = widgets[section].slice();
var removedWidgets = newArray.splice(index, 1);
widgets[section] = newArray;
BarService.widgetsRevision++;
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"), 6000);
}
}
}
function _reorderWidgetInSection(section, fromIndex, toIndex) {
var widgets = Settings.data.bar.widgets;
if (fromIndex >= 0 && fromIndex < widgets[section].length && toIndex >= 0 && toIndex < widgets[section].length) {
var newArray = widgets[section].slice();
var item = newArray[fromIndex];
newArray.splice(fromIndex, 1);
newArray.splice(toIndex, 0, item);
widgets[section] = newArray;
BarService.widgetsRevision++;
}
}
// Note: _updateWidgetSettingsInSection does NOT increment revision
// because it only changes settings, not widget structure
function _updateWidgetSettingsInSection(section, index, settings) {
Settings.data.bar.widgets[section][index] = settings;
}
function _moveWidgetBetweenSections(fromSection, index, toSection) {
var widgets = Settings.data.bar.widgets;
if (index >= 0 && index < widgets[fromSection].length) {
var widget = widgets[fromSection][index];
var sourceArray = widgets[fromSection].slice();
sourceArray.splice(index, 1);
widgets[fromSection] = sourceArray;
var targetArray = widgets[toSection].slice();
targetArray.push(widget);
widgets[toSection] = targetArray;
BarService.widgetsRevision++;
Logger.d("BarTab", "_moveWidgetBetweenSections: revision now", BarService.widgetsRevision);
}
}
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: {
Qt.callLater(updateAvailableWidgetsModel);
}
Connections {
target: BarService
function onActiveWidgetsChanged() {
updateAvailableWidgetsModel();
}
}
Connections {
target: BarWidgetRegistry
function onPluginWidgetRegistryUpdated() {
updateAvailableWidgetsModel();
}
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.appearance")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.widgets")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.behavior")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
NTabButton {
text: I18n.tr("common.monitors")
tabIndex: 3
checked: subTabBar.currentIndex === 3
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginS
}
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)
}
BehaviorSubTab {}
MonitorsSubTab {
addMonitor: root.addMonitor
removeMonitor: root.removeMonitor
}
}
NPluginSettingsPopup {
id: pluginSettingsDialog
parent: Overlay.overlay
showToastOnSave: false
}
}
@@ -0,0 +1,172 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Compositor
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
readonly property string effectiveWheelAction: Settings.data.bar.mouseWheelAction || "none"
readonly property string effectiveMiddleClickAction: Settings.data.bar.middleClickAction || "none"
readonly property string effectiveRightClickAction: Settings.data.bar.rightClickAction || "controlCenter"
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-workspace-scroll-label")
description: I18n.tr("panels.bar.behavior-workspace-scroll-description")
model: {
var items = [
{
"key": "none",
"name": I18n.tr("common.none")
},
{
"key": "volume",
"name": I18n.tr("common.volume")
},
{
"key": "workspace",
"name": I18n.tr("panels.bar.behavior-workspace-scroll-option-workspace")
}
];
if (CompositorService.isNiri) {
items.push({
"key": "content",
"name": I18n.tr("panels.bar.behavior-workspace-scroll-option-content")
});
}
return items;
}
currentKey: root.effectiveWheelAction
defaultValue: Settings.getDefaultValue("bar.mouseWheelAction")
onSelected: key => Settings.data.bar.mouseWheelAction = key
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.general.reverse-scrolling-label")
description: I18n.tr("panels.general.reverse-scrolling-description")
checked: Settings.data.bar.reverseScroll
defaultValue: Settings.getDefaultValue("bar.reverseScroll")
onToggled: checked => Settings.data.bar.reverseScroll = checked
visible: Settings.data.bar.mouseWheelAction !== "none"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-wheel-wrap-label")
description: I18n.tr("panels.bar.behavior-wheel-wrap-description")
checked: Settings.data.bar.mouseWheelWrap
defaultValue: Settings.getDefaultValue("bar.mouseWheelWrap")
onToggled: checked => Settings.data.bar.mouseWheelWrap = checked
visible: Settings.data.bar.mouseWheelAction === "workspace"
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-middle-click-label")
description: I18n.tr("panels.bar.behavior-middle-click-description")
model: [
{
"key": "none",
"name": I18n.tr("common.none")
},
{
"key": "controlCenter",
"name": I18n.tr("tooltips.open-control-center")
},
{
"key": "settings",
"name": I18n.tr("tooltips.open-settings")
},
{
"key": "launcherPanel",
"name": I18n.tr("actions.open-launcher")
},
{
"key": "command",
"name": I18n.tr("actions.run-custom-command")
}
]
currentKey: root.effectiveMiddleClickAction
defaultValue: Settings.getDefaultValue("bar.middleClickAction")
onSelected: key => Settings.data.bar.middleClickAction = key
}
NTextInput {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-middle-click-command-label")
description: I18n.tr("panels.bar.behavior-middle-click-command-description")
placeholderText: I18n.tr("panels.bar.behavior-middle-click-command-placeholder")
text: Settings.data.bar.middleClickCommand
fontFamily: Settings.data.ui.fontFixed
onTextChanged: Settings.data.bar.middleClickCommand = text
visible: Settings.data.bar.middleClickAction === "command"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-middle-click-follow-mouse-label")
description: I18n.tr("panels.bar.behavior-middle-click-follow-mouse-description")
checked: Settings.data.bar.middleClickFollowMouse
defaultValue: Settings.getDefaultValue("bar.middleClickFollowMouse")
onToggled: checked => Settings.data.bar.middleClickFollowMouse = checked
visible: Settings.data.bar.middleClickAction !== "none" && Settings.data.bar.middleClickAction !== "command" && !(Settings.data.bar.middleClickAction === "settings" && Settings.data.ui.settingsPanelMode === "window")
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-right-click-label")
description: I18n.tr("panels.bar.behavior-right-click-description")
model: [
{
"key": "none",
"name": I18n.tr("common.none")
},
{
"key": "controlCenter",
"name": I18n.tr("tooltips.open-control-center")
},
{
"key": "settings",
"name": I18n.tr("tooltips.open-settings")
},
{
"key": "launcherPanel",
"name": I18n.tr("actions.open-launcher")
},
{
"key": "command",
"name": I18n.tr("actions.run-custom-command")
}
]
currentKey: root.effectiveRightClickAction
defaultValue: Settings.getDefaultValue("bar.rightClickAction")
onSelected: key => Settings.data.bar.rightClickAction = key
}
NTextInput {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-right-click-command-label")
description: I18n.tr("panels.bar.behavior-right-click-command-description")
placeholderText: I18n.tr("panels.bar.behavior-right-click-command-placeholder")
text: Settings.data.bar.rightClickCommand
fontFamily: Settings.data.ui.fontFixed
onTextChanged: Settings.data.bar.rightClickCommand = text
visible: Settings.data.bar.rightClickAction === "command"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-right-click-follow-mouse-label")
description: I18n.tr("panels.bar.behavior-right-click-follow-mouse-description")
checked: Settings.data.bar.rightClickFollowMouse
defaultValue: Settings.getDefaultValue("bar.rightClickFollowMouse")
onToggled: checked => Settings.data.bar.rightClickFollowMouse = checked
visible: Settings.data.bar.rightClickAction !== "none" && Settings.data.bar.rightClickAction !== "command" && !(Settings.data.bar.rightClickAction === "settings" && Settings.data.ui.settingsPanelMode === "window")
}
}
@@ -0,0 +1,276 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "../../Bar" as BarSettings
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 addMonitor
property var removeMonitor
NText {
text: I18n.tr("panels.bar.monitors-desc-new")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Monitor cards
Repeater {
model: Quickshell.screens || []
delegate: NBox {
id: monitorCard
Layout.fillWidth: true
implicitHeight: cardContent.implicitHeight + Style.margin2L
color: Color.mSurface
required property var modelData
readonly property string screenName: modelData.name || "Unknown"
readonly property real compositorScale: {
const info = CompositorService.displayScales[screenName];
return (info && info.scale) ? info.scale : 1.0;
}
readonly property bool barEnabled: (Settings.data.bar.monitors || []).indexOf(screenName) !== -1
readonly property bool hasOverride: Settings.hasScreenOverride(screenName)
// Track if override is enabled (controls both visibility AND whether overrides are applied)
readonly property bool overrideEnabled: Settings.isScreenOverrideEnabled(screenName)
// Get effective values for this screen
readonly property string effectivePosition: Settings.getBarPositionForScreen(screenName)
readonly property string effectiveDensity: Settings.getBarDensityForScreen(screenName)
ColumnLayout {
id: cardContent
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
RowLayout {
Layout.fillWidth: true
// Header: Monitor name and specs
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS
NText {
Layout.fillWidth: true
text: monitorCard.screenName
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: {
return I18n.tr("system.monitor-description", {
"model": monitorCard.modelData.model || I18n.tr("common.unknown"),
"width": Math.round(monitorCard.modelData.width * monitorCard.compositorScale),
"height": Math.round(monitorCard.modelData.height * monitorCard.compositorScale),
"scale": monitorCard.compositorScale
});
}
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
}
// Enable bar toggle
NToggle {
Layout.fillWidth: true
checked: monitorCard.barEnabled
onToggled: checked => {
if (checked) {
Settings.data.bar.monitors = root.addMonitor(Settings.data.bar.monitors, monitorCard.screenName);
} else {
Settings.data.bar.monitors = root.removeMonitor(Settings.data.bar.monitors, monitorCard.screenName);
}
}
}
}
NDivider {
Layout.fillWidth: true
visible: Settings.data.bar.monitors.includes(monitorCard.screenName)
}
// Override section (only visible when bar is enabled)
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
visible: monitorCard.barEnabled
// Override toggle
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.monitor-override-settings")
description: I18n.tr("panels.bar.monitor-override-settings-description")
checked: monitorCard.overrideEnabled
onToggled: checked => {
Settings.setScreenOverride(monitorCard.screenName, "enabled", checked);
BarService.widgetsRevision++;
}
}
// Override controls (only visible when override toggle is on)
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
visible: monitorCard.overrideEnabled
// Position override
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-position-label")
description: I18n.tr("panels.bar.appearance-position-description")
model: [
{
"key": "top",
"name": I18n.tr("positions.top")
},
{
"key": "bottom",
"name": I18n.tr("positions.bottom")
},
{
"key": "left",
"name": I18n.tr("positions.left")
},
{
"key": "right",
"name": I18n.tr("positions.right")
}
]
currentKey: monitorCard.effectivePosition
onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "position", key)
}
}
// Density override
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-density-label")
description: I18n.tr("panels.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: monitorCard.effectiveDensity
onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "density", key)
}
}
// DisplayMode override
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NComboBox {
Layout.fillWidth: true
label: I18n.tr("common.display-mode")
description: I18n.tr("panels.bar.appearance-display-mode-description")
model: [
{
"key": "always_visible",
"name": I18n.tr("hide-modes.visible")
},
{
"key": "non_exclusive",
"name": I18n.tr("hide-modes.non-exclusive")
},
{
"key": "auto_hide",
"name": I18n.tr("hide-modes.auto-hide")
}
]
currentKey: Settings.getBarDisplayModeForScreen(monitorCard.screenName)
onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "displayMode", key)
}
}
// Widgets configuration button and Reset all
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NButton {
id: widgetConfigButton
property bool expanded: false
Layout.fillWidth: true
fontSize: Style.fontSizeS
text: I18n.tr("panels.bar.monitor-configure-widgets")
icon: expanded ? "chevron-up" : "layout-grid"
onClicked: expanded = !expanded
}
NButton {
visible: Settings.hasScreenOverride(monitorCard.screenName, "widgets")
Layout.fillWidth: true
fontSize: Style.fontSizeS
text: I18n.tr("panels.bar.use-global-widgets")
icon: "refresh"
onClicked: {
Settings.clearScreenOverride(monitorCard.screenName, "widgets");
BarService.widgetsRevision++;
}
}
NButton {
Layout.fillWidth: true
fontSize: Style.fontSizeS
text: I18n.tr("panels.bar.monitor-reset-all")
icon: "restore"
onClicked: {
Settings.clearScreenOverride(monitorCard.screenName);
BarService.widgetsRevision++;
}
}
}
// Inline widget configuration
BarSettings.MonitorWidgetsConfig {
visible: widgetConfigButton.expanded
screen: monitorCard.modelData
Layout.fillWidth: true
Layout.topMargin: Style.marginS
}
}
}
}
}
}
}
@@ -0,0 +1,97 @@
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)
// This sub-tab edits the global default widget configuration (Settings.data.bar.widgets).
// Per-screen widget overrides are edited in MonitorWidgetsConfig.qml (Monitors sub-tab).
// determine bar orientation
readonly property string barPosition: Settings.data.bar.position
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
function getSectionIcons() {
return {
"left": "arrow-bar-to-up",
"center": "layout-distribute-horizontal",
"right": "arrow-bar-to-down"
};
}
NText {
text: I18n.tr("panels.bar.widgets-desc")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Left Section
NSectionEditor {
sectionName: root.barIsVertical ? I18n.tr("positions.top") : I18n.tr("positions.left")
sectionId: "left"
barIsVertical: root.barIsVertical
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.left
sectionIcons: root.getSectionIcons()
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: I18n.tr("positions.center")
sectionId: "center"
barIsVertical: root.barIsVertical
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.center
sectionIcons: root.getSectionIcons()
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: root.barIsVertical ? I18n.tr("positions.bottom") : I18n.tr("positions.right")
sectionId: "right"
barIsVertical: root.barIsVertical
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.right
sectionIcons: root.getSectionIcons()
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)
}
}