Vertical Bar!

This commit is contained in:
LemmyCook 2025-09-14 11:26:36 -04:00
commit 0c6aea7154
49 changed files with 3140 additions and 1154 deletions

View file

@ -48,6 +48,8 @@ Loader {
margins {
top: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
bottom: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
left: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "left" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
right: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "right" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
}
mask: Region {}

View file

@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.UPower
@ -34,29 +35,30 @@ Variants {
WlrLayershell.namespace: "noctalia-bar"
implicitHeight: Math.round(Style.barHeight * scaling)
implicitHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? screen.height : Math.round(Style.barHeight * scaling)
implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Math.round(Style.barHeight * scaling) : screen.width
color: Color.transparent
anchors {
top: Settings.data.bar.position === "top"
bottom: Settings.data.bar.position === "bottom"
left: true
right: true
top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
bottom: Settings.data.bar.position === "bottom" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
left: Settings.data.bar.position === "left" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
}
// Floating bar margins - only apply when floating is enabled
margins {
top: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0
bottom: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0
left: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
right: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
top: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
bottom: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
left: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
right: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
}
Item {
anchors.fill: parent
clip: true
// Background fill
// Background fill with shadow
Rectangle {
id: bar
@ -67,88 +69,181 @@ Variants {
radius: Settings.data.bar.floating ? Style.radiusL : 0
}
// ------------------------------
// Left Section - Dynamic Widgets
Row {
id: leftSection
objectName: "leftSection"
// For vertical bars, use a single column layout
Loader {
id: verticalBarLayout
anchors.fill: parent
visible: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
sourceComponent: verticalBarComponent
}
height: parent.height
anchors.left: parent.left
anchors.leftMargin: Style.marginS * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
// For horizontal bars, use the original three-section layout
Loader {
id: horizontalBarLayout
anchors.fill: parent
visible: Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
sourceComponent: horizontalBarComponent
}
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": parent.objectName.replace("Section", "").toLowerCase(),
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
// Main layout components
Component {
id: verticalBarComponent
Item {
anchors.fill: parent
// Top section (left widgets)
Column {
spacing: Style.marginS * root.scaling
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Style.marginM * root.scaling
width: parent.width
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "left",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
}
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
// Center section (center widgets)
Column {
spacing: Style.marginS * root.scaling
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: parent.width
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "center",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
// Bottom section (right widgets)
Column {
spacing: Style.marginS * root.scaling
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.marginM * root.scaling
width: parent.width
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "right",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
}
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
// ------------------------------
// Center Section - Dynamic Widgets
Row {
id: centerSection
objectName: "centerSection"
Component {
id: horizontalBarComponent
Item {
anchors.fill: parent
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": parent.objectName.replace("Section", "").toLowerCase(),
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
// Left Section
RowLayout {
id: leftSection
objectName: "leftSection"
anchors.left: parent.left
anchors.leftMargin: Style.marginS * root.scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "left",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
}
}
}
}
}
}
// ------------------------------
// Right Section - Dynamic Widgets
Row {
id: rightSection
objectName: "rightSection"
height: parent.height
anchors.right: bar.right
anchors.rightMargin: Style.marginS * scaling
anchors.verticalCenter: bar.verticalCenter
spacing: Style.marginS * scaling
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": parent.objectName.replace("Section", "").toLowerCase(),
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
}
// Center Section
RowLayout {
id: centerSection
objectName: "centerSection"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "center",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
}
}
}
// Right Section
RowLayout {
id: rightSection
objectName: "rightSection"
anchors.right: parent.right
anchors.rightMargin: Style.marginS * root.scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "right",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
}
}
}
}
}
}

View file

@ -8,7 +8,7 @@ import qs.Commons
import qs.Services
import qs.Widgets
RowLayout {
Item {
id: root
property ShellScreen screen
property real scaling: 1.0
@ -36,6 +36,10 @@ RowLayout {
readonly property real minWidth: Math.max(1, screen.width * 0.06)
readonly property real maxWidth: minWidth * 2
readonly property string barPosition: Settings.data.bar.position
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
function getTitle() {
try {
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
@ -45,10 +49,33 @@ RowLayout {
}
}
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
visible: getTitle() !== ""
function calculatedVerticalHeight() {
// Use standard widget height like other widgets
return Math.round(Style.capsuleHeight * scaling)
}
function calculatedHorizontalWidth() {
let total = Style.marginM * 2 * scaling // internal padding
if (showIcon) {
total += Style.baseWidgetSize * 0.5 * scaling + 2 * scaling // icon + spacing
}
// Calculate actual text width more accurately
const title = getTitle()
if (title !== "") {
// Estimate text width: average character width * number of characters
const avgCharWidth = Style.fontSizeS * scaling * 0.6 // rough estimate
const titleWidth = Math.min(title.length * avgCharWidth, 80 * scaling)
total += titleWidth
}
// Row layout handles spacing between widgets
return Math.max(total, Style.capsuleHeight * scaling) // Minimum width
}
function getAppIcon() {
try {
// Try CompositorService first
@ -102,27 +129,31 @@ RowLayout {
Rectangle {
id: windowTitleRect
visible: root.visible
Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
Item {
id: mainContainer
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
clip: true
// Horizontal layout for top/bottom bars
RowLayout {
id: contentLayout
id: horizontalLayout
anchors.centerIn: parent
spacing: Style.marginS * scaling
spacing: 2 * scaling
visible: barPosition === "top" || barPosition === "bottom"
// Window icon
Item {
Layout.preferredWidth: Style.fontSizeL * scaling * 1.2
Layout.preferredHeight: Style.fontSizeL * scaling * 1.2
Layout.preferredWidth: Style.baseWidgetSize * 0.5 * scaling
Layout.preferredHeight: Style.baseWidgetSize * 0.5 * scaling
Layout.alignment: Qt.AlignVCenter
visible: getTitle() !== "" && showIcon
@ -150,11 +181,11 @@ RowLayout {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
return Math.round(Math.min(fullTitleMetrics.contentWidth, 80 * scaling)) // Limited width for horizontal bars
}
} catch (e) {
Logger.warn("ActiveWindow", "Error calculating width:", e)
return root.minWidth * scaling
return 80 * scaling
}
}
Layout.alignment: Qt.AlignVCenter
@ -176,12 +207,65 @@ RowLayout {
}
}
// Vertical layout for left/right bars - icon only
Item {
id: verticalLayout
anchors.centerIn: parent
width: parent.width - Style.marginXS * scaling * 2
height: parent.height - Style.marginXS * scaling * 2
visible: barPosition === "left" || barPosition === "right"
// Window icon
Item {
width: Style.baseWidgetSize * 0.5 * scaling
height: Style.baseWidgetSize * 0.5 * scaling
anchors.centerIn: parent
visible: getTitle() !== "" && showIcon
IconImage {
id: windowIconVertical
anchors.fill: parent
source: getAppIcon()
asynchronous: true
smooth: true
visible: source !== ""
// Handle loading errors gracefully
onStatusChanged: {
if (status === Image.Error) {
Logger.warn("ActiveWindow", "Failed to load icon:", source)
}
}
}
}
}
// Mouse area for hover detection
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
if (barPosition === "left" || barPosition === "right") {
tooltip.show()
}
}
onExited: {
if (barPosition === "left" || barPosition === "right") {
tooltip.hide()
}
}
}
// Hover tooltip with full title (only for vertical bars)
NTooltip {
id: tooltip
target: verticalLayout
text: getTitle()
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
delay: 500
}
}
}
@ -191,6 +275,7 @@ RowLayout {
function onActiveWindowChanged() {
try {
windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
} catch (e) {
Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e)
}
@ -198,6 +283,7 @@ RowLayout {
function onWindowListChanged() {
try {
windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
} catch (e) {
Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e)
}

View file

@ -28,13 +28,19 @@ Rectangle {
return {}
}
readonly property string barPosition: Settings.data.bar.position
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property bool use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock
readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth !== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth
readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat
implicitWidth: Math.round(layout.implicitWidth + Style.marginM * 2 * scaling)
implicitHeight: Math.round(Style.capsuleHeight * scaling)
// Use compact mode for vertical bars
readonly property bool useCompactMode: barPosition === "left" || barPosition === "right"
implicitWidth: useCompactMode ? Math.round(Style.capsuleHeight * scaling) : Math.round(layout.implicitWidth + Style.marginM * 2 * scaling)
implicitHeight: useCompactMode ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusS * scaling)
color: Color.mSurfaceVariant
@ -46,66 +52,133 @@ Rectangle {
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: -3 * scaling
spacing: useCompactMode ? -2 * scaling : -3 * scaling
// First line
NText {
readonly property bool showSeconds: (displayFormat === "time-seconds")
readonly property bool inlineDate: (displayFormat === "time-date")
// Compact mode for vertical bars - Time section (HH, MM)
Repeater {
model: useCompactMode ? 2 : 1
NText {
readonly property bool showSeconds: (displayFormat === "time-seconds")
readonly property bool inlineDate: (displayFormat === "time-date")
readonly property var now: Time.date
text: {
const now = Time.date
let timeStr = ""
if (use12h) {
// 12-hour format with proper padding and consistent spacing
const hours = now.getHours()
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
const paddedHours = displayHours.toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
const ampm = hours < 12 ? 'AM' : 'PM'
if (showSeconds) {
const seconds = now.getSeconds().toString().padStart(2, '0')
timeStr = `${paddedHours}:${minutes}:${seconds} ${ampm}`
text: {
if (useCompactMode) {
// Compact mode: time section (first 2 lines)
switch (index) {
case 0:
// Hours
if (use12h) {
const hours = now.getHours()
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
return displayHours.toString().padStart(2, '0')
} else {
return now.getHours().toString().padStart(2, '0')
}
case 1:
// Minutes
return now.getMinutes().toString().padStart(2, '0')
default:
return ""
}
} else {
timeStr = `${paddedHours}:${minutes} ${ampm}`
}
} else {
// 24-hour format with padding
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
// Normal mode: single line with time
let timeStr = ""
if (showSeconds) {
const seconds = now.getSeconds().toString().padStart(2, '0')
timeStr = `${hours}:${minutes}:${seconds}`
} else {
timeStr = `${hours}:${minutes}`
if (use12h) {
// 12-hour format with proper padding and consistent spacing
const hours = now.getHours()
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
const paddedHours = displayHours.toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
const ampm = hours < 12 ? 'AM' : 'PM'
if (showSeconds) {
const seconds = now.getSeconds().toString().padStart(2, '0')
timeStr = `${paddedHours}:${minutes}:${seconds} ${ampm}`
} else {
timeStr = `${paddedHours}:${minutes} ${ampm}`
}
} else {
// 24-hour format with padding
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
if (showSeconds) {
const seconds = now.getSeconds().toString().padStart(2, '0')
timeStr = `${hours}:${minutes}:${seconds}`
} else {
timeStr = `${hours}:${minutes}`
}
}
// Add inline date if needed
if (inlineDate) {
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
const day = now.getDate().toString().padStart(2, '0')
let month = now.toLocaleDateString(Qt.locale(), "MMM")
timeStr += " - " + (reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
}
return timeStr
}
}
// Add inline date if needed
if (inlineDate) {
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
const day = now.getDate().toString().padStart(2, '0')
let month = now.toLocaleDateString(Qt.locale(), "MMM")
timeStr += " - " + (reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
}
return timeStr
//font.family: Settings.data.ui.fontFixed
font.pointSize: useCompactMode ? Style.fontSizeXXS * scaling : Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
//font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
// Second line
// Separator line for compact mode (between time and date)
Rectangle {
visible: useCompactMode
Layout.preferredWidth: 20 * scaling
Layout.preferredHeight: 2 * scaling
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 3 * scaling
Layout.bottomMargin: 3 * scaling
color: Color.mPrimary
opacity: 0.3
radius: 1 * scaling
}
// Compact mode for vertical bars - Date section (DD, MM)
Repeater {
model: useCompactMode ? 2 : 0
NText {
readonly property var now: Time.date
text: {
if (useCompactMode) {
// Compact mode: date section (last 2 lines)
switch (index) {
case 0:
// Day
return now.getDate().toString().padStart(2, '0')
case 1:
// Month
return (now.getMonth() + 1).toString().padStart(2, '0')
default:
return ""
}
}
return ""
}
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
// Second line for normal mode (date)
NText {
visible: (displayFormat === "time-date-short")
visible: !useCompactMode && (displayFormat === "time-date-short")
text: {
const now = Time.date
const day = now.getDate().toString().padStart(2, '0')

View file

@ -7,7 +7,7 @@ import qs.Commons
import qs.Services
import qs.Widgets
RowLayout {
Item {
id: root
property ShellScreen screen
@ -30,6 +30,8 @@ RowLayout {
return {}
}
readonly property string barPosition: Settings.data.bar.position
readonly property bool showAlbumArt: (widgetSettings.showAlbumArt !== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt
readonly property bool showVisualizer: (widgetSettings.showVisualizer !== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType !== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType
@ -42,10 +44,26 @@ RowLayout {
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
}
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
function calculatedVerticalHeight() {
return Math.round(Style.baseWidgetSize * 0.8 * scaling)
}
function calculatedHorizontalWidth() {
let total = Style.marginM * 2 * scaling // internal padding
if (showAlbumArt) {
total += 18 * scaling + 2 * scaling // album art + spacing
} else {
total += Style.fontSizeL * scaling + 2 * scaling // icon + spacing
}
total += Math.min(fullTitleMetrics.contentWidth, maxWidth * scaling) // title text
// Row layout handles spacing between widgets
return total
}
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)) : 0
visible: MediaService.currentPlayer !== null && MediaService.canPlay
Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
// A hidden text element to safely measure the full title width
NText {
@ -57,12 +75,12 @@ RowLayout {
Rectangle {
id: mediaMini
Layout.preferredWidth: rowLayout.implicitWidth + Style.marginM * 2 * scaling
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
radius: Math.round(Style.radiusM * scaling)
visible: root.visible
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
// Used to anchor the tooltip, so the tooltip does not move when the content expands
@ -75,8 +93,8 @@ RowLayout {
Item {
id: mainContainer
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
Loader {
anchors.verticalCenter: parent.verticalCenter
@ -123,10 +141,12 @@ RowLayout {
}
}
// Horizontal layout for top/bottom bars
RowLayout {
id: rowLayout
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: barPosition === "top" || barPosition === "bottom"
z: 1 // Above the visualizer
NIcon {
@ -187,6 +207,33 @@ RowLayout {
}
}
// Vertical layout for left/right bars - icon only
Item {
id: verticalLayout
anchors.centerIn: parent
width: parent.width - Style.marginM * scaling * 2
height: parent.height - Style.marginM * scaling * 2
visible: barPosition === "left" || barPosition === "right"
z: 1 // Above the visualizer
// Media icon
Item {
width: Style.baseWidgetSize * 0.5 * scaling
height: Style.baseWidgetSize * 0.5 * scaling
anchors.centerIn: parent
visible: getTitle() !== ""
NIcon {
id: mediaIconVertical
anchors.fill: parent
icon: MediaService.isPlaying ? "media-pause" : "media-play"
font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
}
// Mouse area for hover detection
MouseArea {
id: mouseArea
@ -209,12 +256,18 @@ RowLayout {
}
onEntered: {
if (tooltip.text !== "") {
if (barPosition === "left" || barPosition === "right") {
tooltip.show()
} else if (tooltip.text !== "") {
tooltip.show()
}
}
onExited: {
tooltip.hide()
if (barPosition === "left" || barPosition === "right") {
tooltip.hide()
} else {
tooltip.hide()
}
}
}
}
@ -223,16 +276,23 @@ RowLayout {
NTooltip {
id: tooltip
text: {
var str = ""
if (MediaService.canGoNext) {
str += "Right click for next.\n"
if (barPosition === "left" || barPosition === "right") {
return getTitle()
} else {
var str = ""
if (MediaService.canGoNext) {
str += "Right click for next.\n"
}
if (MediaService.canGoPrevious) {
str += "Middle click for previous."
}
return str
}
if (MediaService.canGoPrevious) {
str += "Middle click for previous."
}
return str
}
target: anchor
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : anchor
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
positionAbove: Settings.data.bar.position === "bottom"
delay: 500
}
}

View file

@ -5,7 +5,7 @@ import qs.Commons
import qs.Services
import qs.Widgets
RowLayout {
Item {
id: root
property ShellScreen screen
@ -28,6 +28,8 @@ RowLayout {
return {}
}
readonly property string barPosition: Settings.data.bar.position
readonly property bool showCpuUsage: (widgetSettings.showCpuUsage !== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage
readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp
readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage
@ -35,22 +37,109 @@ RowLayout {
readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats
readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginL * 2 * scaling)
function calculatedVerticalHeight() {
let total = 0
let visibleCount = 0
if (showCpuUsage)
visibleCount++
if (showCpuTemp)
visibleCount++
if (showMemoryUsage)
visibleCount++
if (showNetworkStats)
visibleCount += 2 // download + upload
if (showDiskUsage)
visibleCount++
total = visibleCount * Math.round(Style.capsuleHeight * scaling)
total += Math.max(visibleCount - 1, 0) * Style.marginXS * scaling
total += Style.marginXS * scaling * 2 // minimal padding to match other widgets
return total
}
function calculatedHorizontalWidth() {
let total = Style.marginL * scaling * 2.5 // base padding
if (showCpuUsage) {
// Icon + "99%" text
total += Style.fontSizeM * scaling * 1.2 + // icon
Style.fontSizeXS * scaling * 2.5 + // text (~3 chars)
2 * scaling // spacing
}
if (showCpuTemp) {
// Icon + "85°C" text
total += Style.fontSizeS * scaling * 1.2 + // smaller fire icon
Style.fontSizeXS * scaling * 3.5 + // text (~4 chars)
2 * scaling // spacing
}
if (showMemoryUsage) {
// Icon + "16G" or "85%" text
total += Style.fontSizeM * scaling * 1.2 + // icon
Style.fontSizeXS * scaling * 3 + // text (~3-4 chars)
2 * scaling // spacing
}
if (showNetworkStats) {
// Download: icon + "1.2M" text
total += Style.fontSizeM * scaling * 1.2 + // icon
Style.fontSizeXS * scaling * 3.5 + // text
Style.marginXS * scaling + 2 * scaling // spacing
// Upload: icon + "256K" text
total += Style.fontSizeM * scaling * 1.2 + // icon
Style.fontSizeXS * scaling * 3.5 + // text
Style.marginXS * scaling + 2 * scaling // spacing
}
if (showDiskUsage) {
// Icon + "75%" text
total += Style.fontSizeM * scaling * 1.2 + // icon
Style.fontSizeXS * scaling * 3 + // text (~3 chars)
Style.marginXS * scaling + 2 * scaling // spacing
}
// Add spacing between visible components
let visibleCount = 0
if (showCpuUsage)
visibleCount++
if (showCpuTemp)
visibleCount++
if (showMemoryUsage)
visibleCount++
if (showNetworkStats)
visibleCount += 2
if (showDiskUsage)
visibleCount++
if (visibleCount > 1) {
total += (visibleCount - 1) * Style.marginXS * scaling
}
// Row layout handles spacing between widgets
return Math.max(total, Style.capsuleHeight * scaling)
}
Rectangle {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: mainLayout.implicitWidth + Style.marginM * scaling * 2
Layout.alignment: Qt.AlignVCenter
id: backgroundContainer
anchors.centerIn: parent
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginL * 2 * scaling)
height: (barPosition === "left" || barPosition === "right") ? parent.height : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
// Horizontal layout for top/bottom bars
RowLayout {
id: mainLayout
anchors.centerIn: parent // Better centering than margins
width: parent.width - Style.marginM * scaling * 2
spacing: Style.marginS * scaling
id: horizontalLayout
anchors.centerIn: parent
spacing: Style.marginXS * scaling
visible: barPosition === "top" || barPosition === "bottom"
// CPU Usage Component
Item {
@ -62,7 +151,7 @@ RowLayout {
RowLayout {
id: cpuUsageRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
spacing: 2 * scaling
NIcon {
icon: "cpu-usage"
@ -92,7 +181,7 @@ RowLayout {
RowLayout {
id: cpuTempRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
spacing: 2 * scaling
NIcon {
icon: "cpu-temperature"
@ -123,7 +212,7 @@ RowLayout {
RowLayout {
id: memoryUsageRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
spacing: 2 * scaling
NIcon {
icon: "memory"
@ -233,5 +322,196 @@ RowLayout {
}
}
}
// Vertical layout for left/right bars
ColumnLayout {
id: verticalLayout
anchors.centerIn: parent
width: Math.round(28 * scaling)
height: parent.height
spacing: Style.marginXXS * scaling
visible: barPosition === "left" || barPosition === "right"
// CPU Usage Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(28 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showCpuUsage
Column {
id: cpuUsageRowVertical
anchors.centerIn: parent
spacing: 1 * scaling
NIcon {
icon: "cpu-usage"
font.pointSize: Style.fontSizeS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
NText {
text: `${Math.round(SystemStatService.cpuUsage)}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling * 0.8
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
// CPU Temperature Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(28 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showCpuTemp
Column {
id: cpuTempRowVertical
anchors.centerIn: parent
spacing: 1 * scaling
NIcon {
icon: "cpu-temperature"
// Fire is so tall, we need to make it smaller
font.pointSize: Style.fontSizeXS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
NText {
text: `${SystemStatService.cpuTemp}°`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling * 0.8
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
// Memory Usage Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(28 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showMemoryUsage
Column {
id: memoryUsageRowVertical
anchors.centerIn: parent
spacing: 1 * scaling
NIcon {
icon: "memory"
font.pointSize: Style.fontSizeS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
NText {
text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${Math.round(SystemStatService.memGb)}G`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling * 0.8
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
// Network Download Speed Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(28 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showNetworkStats
Column {
id: networkDownloadRowVertical
anchors.centerIn: parent
spacing: 1 * scaling
NIcon {
icon: "download-speed"
font.pointSize: Style.fontSizeS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
NText {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling * 0.8
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
// Network Upload Speed Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(28 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showNetworkStats
Column {
id: networkUploadRowVertical
anchors.centerIn: parent
spacing: 1 * scaling
NIcon {
icon: "upload-speed"
font.pointSize: Style.fontSizeS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
NText {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling * 0.8
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
// Disk Usage Component (primary drive)
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(28 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showDiskUsage
ColumnLayout {
id: diskUsageRowVertical
anchors.centerIn: parent
spacing: 1 * scaling
NIcon {
icon: "storage"
font.pointSize: Style.fontSizeS * scaling
Layout.alignment: Qt.AlignHCenter
}
NText {
text: `${SystemStatService.diskPercent}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling * 0.8
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
}
}
}

View file

@ -17,6 +17,7 @@ Rectangle {
property real scaling: 1.0
readonly property real itemSize: 24 * scaling
readonly property string barPosition: Settings.data.bar.position
function onLoaded() {
// When the widget is fully initialized with its props
@ -27,8 +28,8 @@ Rectangle {
}
visible: SystemTray.items.values.length > 0
implicitWidth: trayLayout.implicitWidth + Style.marginM * scaling * 2
implicitHeight: Math.round(Style.capsuleHeight * scaling)
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (trayLayout.implicitWidth + Style.marginM * scaling * 2)
implicitHeight: (barPosition === "left" || barPosition === "right") ? Math.round(trayLayout.implicitHeight + Style.marginM * scaling * 2) : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
@ -111,9 +112,21 @@ Rectangle {
if (modelData.hasMenu && modelData.menu && trayMenu.item) {
trayPanel.open()
// Anchor the menu to the tray icon item (parent) and position it below the icon
const menuX = (width / 2) - (trayMenu.item.width / 2)
const menuY = Math.round(Style.barHeight * scaling)
// Position menu based on bar position
let menuX, menuY
if (barPosition === "left") {
// For left bar: position menu to the right of the bar
menuX = width + Style.marginM * scaling
menuY = 0
} else if (barPosition === "right") {
// For right bar: position menu to the left of the bar
menuX = -trayMenu.item.width - Style.marginM * scaling
menuY = 0
} else {
// For horizontal bars: center horizontally and position below
menuX = (width / 2) - (trayMenu.item.width / 2)
menuY = Math.round(Style.barHeight * scaling)
}
trayMenu.item.menu = modelData.menu
trayMenu.item.showAt(parent, menuX, menuY)
} else {

View file

@ -61,15 +61,6 @@ Item {
}
}
Timer {
id: externalHideTimer
running: false
interval: 1500
onTriggered: {
pill.hide()
}
}
NPill {
id: pill

View file

@ -31,6 +31,8 @@ Item {
return {}
}
readonly property string barPosition: Settings.data.bar.position
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied
@ -47,17 +49,8 @@ Item {
signal workspaceChanged(int workspaceId, color accentColor)
implicitHeight: Math.round(Style.barHeight * scaling)
implicitWidth: {
let total = 0
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
total += calculatedWsWidth(ws)
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return total
}
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.barHeight * scaling) : calculatedHorizontalWidth()
function calculatedWsWidth(ws) {
if (ws.isFocused)
@ -68,6 +61,37 @@ Item {
return Math.round(20 * scaling)
}
function calculatedWsHeight(ws) {
if (ws.isFocused)
return Math.round(44 * scaling)
else if (ws.isActive)
return Math.round(28 * scaling)
else
return Math.round(20 * scaling)
}
function calculatedVerticalHeight() {
let total = 0
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
total += calculatedWsHeight(ws)
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return total
}
function calculatedHorizontalWidth() {
let total = 0
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
total += calculatedWsWidth(ws)
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return total
}
Component.onCompleted: {
refreshWorkspaces()
}
@ -99,7 +123,8 @@ Item {
}
}
}
workspaceRepeater.model = localWorkspaces
workspaceRepeaterHorizontal.model = localWorkspaces
workspaceRepeaterVertical.model = localWorkspaces
updateWorkspaceFocus()
}
@ -148,9 +173,8 @@ Item {
Rectangle {
id: workspaceBackground
width: parent.width
height: Math.round(Style.capsuleHeight * scaling)
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : parent.width
height: (barPosition === "left" || barPosition === "right") ? parent.height : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
@ -158,14 +182,17 @@ Item {
anchors.verticalCenter: parent.verticalCenter
}
// Horizontal layout for top/bottom bars
Row {
id: pillRow
spacing: spacingBetweenPills
anchors.verticalCenter: workspaceBackground.verticalCenter
width: root.width - horizontalPadding * 2
x: horizontalPadding
visible: barPosition === "top" || barPosition === "bottom"
Repeater {
id: workspaceRepeater
id: workspaceRepeaterHorizontal
model: localWorkspaces
Item {
id: workspacePillContainer
@ -299,4 +326,149 @@ Item {
}
}
}
// Vertical layout for left/right bars
Column {
id: pillColumn
spacing: spacingBetweenPills
anchors.horizontalCenter: workspaceBackground.horizontalCenter
height: root.height - horizontalPadding * 2
y: horizontalPadding
visible: barPosition === "left" || barPosition === "right"
Repeater {
id: workspaceRepeaterVertical
model: localWorkspaces
Item {
id: workspacePillContainerVertical
width: (labelMode !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling)
height: root.calculatedWsHeight(model)
Rectangle {
id: pillVertical
anchors.fill: parent
Loader {
active: (labelMode !== "none")
sourceComponent: Component {
Text {
x: (pillVertical.width - width) / 2
y: (pillVertical.height - height) / 2 + (height - contentHeight) / 2
text: {
if (labelMode === "name" && model.name && model.name.length > 0) {
return model.name.substring(0, 2)
} else {
return model.idx.toString()
}
}
font.pointSize: model.isFocused ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
font.capitalization: Font.AllUppercase
font.family: Settings.data.ui.fontFixed
font.weight: Style.fontWeightBold
wrapMode: Text.Wrap
color: {
if (model.isFocused)
return Color.mOnPrimary
if (model.isUrgent)
return Color.mOnError
if (model.isActive || model.isOccupied)
return Color.mOnSecondary
return Color.mOnSurface
}
}
}
}
radius: width * 0.5
color: {
if (model.isFocused)
return Color.mPrimary
if (model.isUrgent)
return Color.mError
if (model.isActive || model.isOccupied)
return Color.mSecondary
return Color.mOutline
}
scale: model.isFocused ? 1.0 : 0.9
z: 0
MouseArea {
id: pillMouseAreaVertical
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
WorkspaceService.switchToWorkspace(model.idx)
}
hoverEnabled: true
}
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
Behavior on radius {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
}
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
// Burst effect overlay for focused pill (smaller outline)
Rectangle {
id: pillBurstVertical
anchors.centerIn: workspacePillContainerVertical
width: workspacePillContainerVertical.width + 18 * root.masterProgress * scale
height: workspacePillContainerVertical.height + 18 * root.masterProgress * scale
radius: width / 2
color: Color.transparent
border.color: root.effectColor
border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * scaling))
opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0
visible: root.effectsActive && model.isFocused
z: 1
}
}
}
}
}

View file

@ -63,7 +63,7 @@ NPanel {
NIconButton {
icon: "close"
tooltipText: "Close"
tooltipText: "Close."
sizeRatio: 0.8
onClicked: {
root.close()

View file

@ -12,7 +12,7 @@ NPanel {
preferredWidth: 340
preferredHeight: 320
panelAnchorRight: true
panelAnchorRight: Settings.data.bar.position === "right"
// Main Column
panelContent: ColumnLayout {

View file

@ -166,7 +166,15 @@ Variants {
// Position above the bar if it's at bottom
anchors.bottom: true
margins.bottom: barAtBottom ? barHeight + floatingMargin : floatingMargin
margins.bottom: {
switch (Settings.data.bar.position) {
case "bottom":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling + floatingMargin : floatingMargin)
default:
return floatingMargin
}
}
// Rectangle {
// anchors.fill: parent

View file

@ -33,13 +33,50 @@ Variants {
screen: modelData
color: Color.transparent
// Position based on bar location
anchors.top: Settings.data.bar.position === "top"
anchors.bottom: Settings.data.bar.position === "bottom"
anchors.right: true
margins.top: Settings.data.bar.position === "top" ? (Style.barHeight + Style.marginM + (Settings.data.bar.floating ? Settings.data.bar.marginVertical : 0)) * scaling : 0
margins.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginM + (Settings.data.bar.floating ? Settings.data.bar.marginVertical : 0)) * scaling : 0
margins.right: Style.marginM * scaling
// Position based on bar location - always at top
anchors.top: true
anchors.right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
anchors.left: Settings.data.bar.position === "left"
margins.top: {
switch (Settings.data.bar.position) {
case "top":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
return Style.marginM * scaling
}
}
margins.bottom: {
switch (Settings.data.bar.position) {
case "bottom":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
return 0
}
}
margins.left: {
switch (Settings.data.bar.position) {
case "left":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
default:
return 0
}
}
margins.right: {
switch (Settings.data.bar.position) {
case "right":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
case "top":
case "bottom":
return Style.marginM * scaling
default:
return 0
}
}
implicitWidth: 360 * scaling
implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling)
//WlrLayershell.layer: WlrLayer.Overlay
@ -77,10 +114,10 @@ Variants {
// Main notification container
ColumnLayout {
id: notificationStack
// Position based on bar location
anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined
anchors.bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined
anchors.right: parent.right
// Position based on bar location - always at top
anchors.top: parent.top
anchors.right: (Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom") ? parent.right : undefined
anchors.left: Settings.data.bar.position === "left" ? parent.left : undefined
spacing: Style.marginS * scaling
width: 360 * scaling
visible: true
@ -288,7 +325,7 @@ Variants {
// Close button positioned absolutely
NIconButton {
icon: "close"
tooltipText: "Close"
tooltipText: "Close."
sizeRatio: 0.6
anchors.top: parent.top
anchors.topMargin: Style.marginM * scaling

View file

@ -14,7 +14,7 @@ NPanel {
preferredWidth: 380
preferredHeight: 500
panelAnchorRight: true
panelAnchorRight: Settings.data.bar.position === "right"
panelKeyboardFocus: true
panelContent: Rectangle {
@ -65,7 +65,7 @@ NPanel {
NIconButton {
icon: "close"
tooltipText: "Close"
tooltipText: "Close."
sizeRatio: 0.8
onClicked: {
root.close()

View file

@ -29,11 +29,11 @@ NPanel {
Dock,
Hooks,
Launcher,
Brightness,
ColorScheme,
Display,
General,
Network,
Notification,
ScreenRecorder,
Weather,
Wallpaper,
@ -72,10 +72,6 @@ NPanel {
id: audioTab
Tabs.AudioTab {}
}
Component {
id: brightnessTab
Tabs.BrightnessTab {}
}
Component {
id: displayTab
Tabs.DisplayTab {}
@ -116,6 +112,10 @@ NPanel {
id: dockTab
Tabs.DockTab {}
}
Component {
id: notificationTab
Tabs.NotificationTab {}
}
// Order *DOES* matter
function updateTabsModel() {
@ -149,16 +149,16 @@ NPanel {
"label": "Display",
"icon": "settings-display",
"source": displayTab
}, {
"id": SettingsPanel.Tab.Notification,
"label": "Notification",
"icon": "settings-notification",
"source": notificationTab
}, {
"id": SettingsPanel.Tab.Network,
"label": "Network",
"icon": "settings-network",
"source": networkTab
}, {
"id": SettingsPanel.Tab.Brightness,
"label": "Brightness",
"icon": "settings-brightness",
"source": brightnessTab
}, {
"id": SettingsPanel.Tab.Weather,
"label": "Weather",
@ -468,7 +468,7 @@ NPanel {
NIcon {
icon: root.tabsModel[currentTabIndex]?.icon
color: Color.mPrimary
font.pointSize: Style.fontSizeXL * scaling
font.pointSize: Style.fontSizeXXL * scaling
}
// Main title
@ -484,7 +484,7 @@ NPanel {
// Close button
NIconButton {
icon: "close"
tooltipText: "Close"
tooltipText: "Close."
Layout.alignment: Qt.AlignVCenter
onClicked: root.close()
}

View file

@ -10,23 +10,23 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL * scaling
property string latestVersion: GitHubService.latestVersion
property string currentVersion: UpdateService.currentVersion
property var contributors: GitHubService.contributors
NText {
text: "Noctalia Shell"
font.pointSize: Style.fontSizeXXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Noctalia Shell"
description: "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell."
}
RowLayout {
spacing: Style.marginL * scaling
// Versions
GridLayout {
Layout.alignment: Qt.AlignCenter
columns: 2
rowSpacing: Style.marginXS * scaling
columnSpacing: Style.marginS * scaling
@ -34,7 +34,6 @@ ColumnLayout {
NText {
text: "Latest Version:"
color: Color.mOnSurface
Layout.alignment: Qt.AlignRight
}
NText {
@ -46,7 +45,6 @@ ColumnLayout {
NText {
text: "Installed Version:"
color: Color.mOnSurface
Layout.alignment: Qt.AlignRight
}
NText {
@ -56,10 +54,13 @@ ColumnLayout {
}
}
// Updater
Item {
Layout.fillWidth: true
}
// Update button
Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.topMargin: Style.marginS * scaling
Layout.alignment: Qt.alignmentRight
Layout.preferredWidth: Math.round(updateRow.implicitWidth + (Style.marginL * scaling * 2))
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
radius: Style.radiusL * scaling
@ -115,23 +116,21 @@ ColumnLayout {
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
NText {
text: `Shout-out to our ${root.contributors.length} <b>awesome</b> contributors!`
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignCenter
NHeader {
label: "Contributors"
description: `Shout-out to our ${root.contributors.length} <b>awesome</b> contributors!`
}
GridView {
id: contributorsGrid
Layout.topMargin: Style.marginL * scaling
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: cellWidth * 3 // Fixed 3 columns
Layout.preferredHeight: {

View file

@ -8,6 +8,12 @@ import qs.Services
ColumnLayout {
id: root
spacing: Style.marginL * scaling
NHeader {
label: "Volumes"
description: "Configure volume controls and audio levels."
}
property real localVolume: AudioService.volume
@ -20,7 +26,7 @@ ColumnLayout {
// Master Volume
ColumnLayout {
spacing: Style.marginS * scaling
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
@ -67,7 +73,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NToggle {
label: "Mute Audio Output"
@ -83,9 +88,8 @@ ColumnLayout {
// Input Volume
ColumnLayout {
spacing: Style.marginS * scaling
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NLabel {
label: "Input Volume"
@ -117,7 +121,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NToggle {
label: "Mute Audio Input"
@ -131,7 +134,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NSpinBox {
Layout.fillWidth: true
@ -158,12 +160,9 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginS * scaling
NText {
text: "Audio Devices"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Audio Devices"
description: "Configure audio input and output devices."
}
// -------------------------------
@ -203,7 +202,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL * scaling
NLabel {
label: "Input Device"
@ -234,12 +232,9 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginL * scaling
NText {
text: "Media Player"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Media Player"
description: "Configure your favorite media players."
}
// Preferred player
@ -360,12 +355,9 @@ ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NText {
text: "Audio Visualizer"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Audio Visualizer"
description: "Customize visual effects that respond to audio playback."
}
// AudioService Visualizer section

View file

@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
@ -8,6 +9,21 @@ import qs.Modules.SettingsPanel.Bar
ColumnLayout {
id: root
spacing: Style.marginL * scaling
// 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
})
}
// Handler for drag start - disables panel background clicks
function handleDragStart() {
@ -24,9 +40,12 @@ ColumnLayout {
panel.enableBackgroundClick()
}
}
NHeader {
label: "Appearance"
description: "Configure bar appearance and positioning."
}
ColumnLayout {
spacing: Style.marginL * scaling
RowLayout {
NComboBox {
@ -42,6 +61,14 @@ ColumnLayout {
key: "bottom"
name: "Bottom"
}
ListElement {
key: "left"
name: "Left"
}
ListElement {
key: "right"
name: "Right"
}
}
currentKey: Settings.data.bar.position
onSelected: key => Settings.data.bar.position = key
@ -52,19 +79,9 @@ ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Background Opacity"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Adjust the background opacity of the bar."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
NLabel {
label: "Background Opacity"
description: "Adjust the background opacity of the bar."
}
RowLayout {
@ -86,7 +103,6 @@ ColumnLayout {
}
}
}
NToggle {
Layout.fillWidth: true
label: "Floating Bar"
@ -173,6 +189,40 @@ ColumnLayout {
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Monitor Configuration
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NHeader {
label: "Monitors Configuration"
description: "Choose which monitors should display the bar."
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: `${modelData.name || "Unknown"}${modelData.model ? `: ${modelData.model}` : ""}`
description: `${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})`
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 {
@ -186,20 +236,9 @@ ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Widgets Positioning"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
}
NText {
text: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
NHeader {
label: "Widgets Positioning"
description: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
}
// Bar Sections

View file

@ -1,340 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
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: ["which", "wlsunset"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
Settings.data.nightLight.enabled = true
NightLightService.apply()
ToastService.showNotice("Night Light", "Enabled")
} else {
Settings.data.nightLight.enabled = false
ToastService.showWarning("Night Light", "wlsunset not installed")
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
spacing: Style.marginL * scaling
// Brightness Step Section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NSpinBox {
Layout.fillWidth: true
label: "Brightness Step Size"
description: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)."
minimum: 1
maximum: 50
value: Settings.data.brightness.brightnessStep
stepSize: 1
suffix: "%"
onValueChanged: {
Settings.data.brightness.brightnessStep = value
}
}
}
// Monitor Overview Section
ColumnLayout {
spacing: Style.marginL * scaling
NLabel {
label: "Monitors Brightness Control"
description: "Current brightness levels for all detected monitors."
}
// Single monitor display using the same data source as the bar icon
Repeater {
model: BrightnessService.monitors
Rectangle {
Layout.fillWidth: true
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
ColumnLayout {
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: `${model.modelData.name} [${model.modelData.model}]`
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
Item {
Layout.fillWidth: true
}
NText {
text: model.method
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignRight
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: "Brightness:"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
}
NSlider {
Layout.fillWidth: true
from: 0
to: 1
value: model.brightness
stepSize: 0.05
onPressedChanged: {
if (!pressed) {
var monitor = BrightnessService.getMonitorForScreen(model.modelData)
monitor.setBrightness(value)
}
}
}
NText {
text: Math.round(model.brightness * 100) + "%"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignRight
}
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Night Light Section
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
NText {
text: "Night Light"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: "Reduce blue light emission to help you sleep better and reduce eye strain."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
NToggle {
label: "Enable Night Light"
description: "Apply a warm color filter to reduce blue light emission."
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("Night Light", "Disabled")
}
}
}
// Temperature
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
NLabel {
label: "Color temperature"
description: "Choose two temperatures in Kelvin."
}
RowLayout {
visible: Settings.data.nightLight.enabled
spacing: Style.marginM * scaling
Layout.fillWidth: false
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
NText {
text: "Night"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.nightTemp
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var nightTemp = parseInt(text)
var dayTemp = parseInt(Settings.data.nightLight.dayTemp)
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
// Clamp value between [1000 .. (dayTemp-500)]
var clampedValue = Math.min(dayTemp - 500, Math.max(1000, nightTemp))
text = Settings.data.nightLight.nightTemp = clampedValue.toString()
}
}
}
Item {}
NText {
text: "Day"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.dayTemp
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var dayTemp = parseInt(text)
var nightTemp = parseInt(Settings.data.nightLight.nightTemp)
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
// Clamp value between [(nightTemp+500) .. 6500]
var clampedValue = Math.max(nightTemp + 500, Math.min(6500, dayTemp))
text = Settings.data.nightLight.dayTemp = clampedValue.toString()
}
}
}
}
}
NToggle {
label: "Automatic Scheduling"
description: `Based on the sunset and sunrise time in <i>${LocationService.stableName}</i> - recommended.`
checked: Settings.data.nightLight.autoSchedule
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
visible: Settings.data.nightLight.enabled
}
// Schedule settings
ColumnLayout {
spacing: Style.marginXS * scaling
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
RowLayout {
Layout.fillWidth: false
spacing: Style.marginM * scaling
NLabel {
label: "Manual Scheduling"
}
Item {// add a little more spacing
}
NText {
text: "Sunrise Time"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
}
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunrise
placeholder: "Select start time"
onSelected: key => Settings.data.nightLight.manualSunrise = key
minimumWidth: 120 * scaling
}
Item {// add a little more spacing
}
NText {
text: "Sunset Time"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
}
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunset
placeholder: "Select stop time"
onSelected: key => Settings.data.nightLight.manualSunset = key
minimumWidth: 120 * scaling
}
}
}
// Force activation toggle
NToggle {
label: "Force activation"
description: "Immediately apply night temperature without scheduling or fade."
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
}
}

View file

@ -8,7 +8,9 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: 0
spacing: Style.marginL * scaling
// Cache for scheme JSON (can be flat or {dark, light})
property var schemeColorsCache: ({})
@ -104,10 +106,12 @@ ColumnLayout {
}
}
// Main Toggles - Dark Mode / Matugen
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NHeader {
label: "Behavior"
description: "Main settings for Noctalia's colors."
}
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
NToggle {
@ -138,7 +142,7 @@ ColumnLayout {
}
}
}
}
NDivider {
Layout.fillWidth: true
@ -151,19 +155,9 @@ ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NText {
text: "Predefined Color Schemes"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
wrapMode: Text.WordWrap
NHeader {
label: "Predefined Color Schemes"
description: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper."
}
// Color Schemes Grid

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
@ -9,36 +10,54 @@ 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
// Time dropdown options (00:00 .. 23:30)
ListModel {
id: timeOptions
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name
})
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
})
}
}
}
NText {
text: "Monitor-specific configuration"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
// Check for wlsunset availability when enabling Night Light
Process {
id: wlsunsetCheck
command: ["which", "wlsunset"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
Settings.data.nightLight.enabled = true
NightLightService.apply()
ToastService.showNotice("Night Light", "Enabled")
} else {
Settings.data.nightLight.enabled = false
ToastService.showWarning("Night Light", "wlsunset not installed")
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
NText {
text: "Bars and notifications appear on all displays by default. Choose specific displays below to limit where they're shown."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
spacing: Style.marginL * scaling
NHeader {
label: "Monitor-specific configuration"
description: "Configure scaling and brightness settings individually for each connected display."
}
ColumnLayout {
spacing: Style.marginL * scaling
Layout.topMargin: Style.marginL * scaling
Repeater {
model: Quickshell.screens || []
@ -46,11 +65,13 @@ ColumnLayout {
Layout.fillWidth: true
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
property real localScaling: ScalingService.getScreenScale(modelData)
property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData)
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
@ -67,119 +88,116 @@ ColumnLayout {
spacing: Style.marginXXS * scaling
NText {
text: (modelData.name || "Unknown")
font.pointSize: Style.fontSizeXL * scaling
text: (`${modelData.name}: ${modelData.model}` || "Unknown")
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
color: Color.mPrimary
}
NText {
text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})`
text: `Resolution: ${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})`
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Scale
ColumnLayout {
spacing: Style.marginL * scaling
spacing: Style.marginS * scaling
Layout.fillWidth: true
NToggle {
Layout.fillWidth: true
label: "Bar"
description: "Enable the bar on this monitor."
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)
}
}
}
NToggle {
Layout.fillWidth: true
label: "Notifications"
description: "Enable notifications on this monitor."
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)
}
}
}
NToggle {
Layout.fillWidth: true
label: "Dock"
description: "Enable the dock on this monitor."
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)
}
}
}
ColumnLayout {
spacing: Style.marginS * scaling
RowLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL * scaling
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Scale"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Scale the user interface on this monitor."
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
NText {
text: `${Math.round(localScaling * 100)}%`
Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: 50 * scaling
horizontalAlignment: Text.AlignRight
}
NText {
text: "Scale"
Layout.preferredWidth: 80 * scaling
}
RowLayout {
spacing: Style.marginS * scaling
NSlider {
id: scaleSlider
from: 0.7
to: 1.8
stepSize: 0.01
value: localScaling
onPressedChanged: ScalingService.setScreenScale(modelData, value)
Layout.fillWidth: true
Layout.minimumWidth: 200 * scaling
}
NSlider {
id: scaleSlider
from: 0.7
to: 1.8
stepSize: 0.01
value: localScaling
onPressedChanged: ScalingService.setScreenScale(modelData, value)
Layout.fillWidth: true
}
NText {
text: `${Math.round(localScaling * 100)}%`
Layout.preferredWidth: 50 * scaling
horizontalAlignment: Text.AlignRight
}
// Reset button container
Item {
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 30 * scaling
NIconButton {
icon: "refresh"
sizeRatio: 0.8
tooltipText: "Reset scaling"
onClicked: ScalingService.setScreenScale(modelData, 1.0)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
// Brightness
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
visible: brightnessMonitor !== undefined && brightnessMonitor !== null
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: "Brightness"
Layout.preferredWidth: 80 * scaling
}
NSlider {
Layout.fillWidth: true
Layout.minimumWidth: 200 * scaling
from: 0
to: 1
value: brightnessMonitor ? brightnessMonitor.brightness : 0.5
stepSize: 0.05
onPressedChanged: {
if (!pressed && brightnessMonitor) {
brightnessMonitor.setBrightness(value)
}
}
}
NText {
text: brightnessMonitor ? Math.round(brightnessMonitor.brightness * 100) + "%" : "N/A"
Layout.preferredWidth: 50 * scaling
horizontalAlignment: Text.AlignRight
}
// Empty container to match scale row layout
Item {
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 30 * scaling
// Method text positioned in the button area
NText {
text: brightnessMonitor ? brightnessMonitor.method : ""
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignRight
}
}
}
@ -188,4 +206,212 @@ ColumnLayout {
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Brightness Section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NHeader {
label: "Brightness"
description: "Adjust brightness related settings."
}
// Brightness Step Section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NSpinBox {
Layout.fillWidth: true
label: "Brightness Step Size"
description: "Adjust the step size for brightness changes (scroll wheel and keyboard shortcuts)."
minimum: 1
maximum: 50
value: Settings.data.brightness.brightnessStep
stepSize: 1
suffix: "%"
onValueChanged: {
Settings.data.brightness.brightnessStep = value
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Night Light Section
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
NHeader {
label: "Night Light"
description: "Reduce blue light emission to help you sleep better and reduce eye strain."
}
}
NToggle {
label: "Enable Night Light"
description: "Apply a warm color filter to reduce blue light emission."
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("Night Light", "Disabled")
}
}
}
// Temperature
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
NLabel {
label: "Color temperature"
description: "Choose two temperatures in Kelvin."
}
RowLayout {
visible: Settings.data.nightLight.enabled
spacing: Style.marginM * scaling
Layout.fillWidth: false
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
NText {
text: "Night"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.nightTemp
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var nightTemp = parseInt(text)
var dayTemp = parseInt(Settings.data.nightLight.dayTemp)
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
// Clamp value between [1000 .. (dayTemp-500)]
var clampedValue = Math.min(dayTemp - 500, Math.max(1000, nightTemp))
text = Settings.data.nightLight.nightTemp = clampedValue.toString()
}
}
}
Item {}
NText {
text: "Day"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.dayTemp
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var dayTemp = parseInt(text)
var nightTemp = parseInt(Settings.data.nightLight.nightTemp)
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
// Clamp value between [(nightTemp+500) .. 6500]
var clampedValue = Math.max(nightTemp + 500, Math.min(6500, dayTemp))
text = Settings.data.nightLight.dayTemp = clampedValue.toString()
}
}
}
}
}
NToggle {
label: "Automatic Scheduling"
description: `Based on the sunset and sunrise time in <i>${LocationService.stableName}</i> - recommended.`
checked: Settings.data.nightLight.autoSchedule
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
visible: Settings.data.nightLight.enabled
}
// Schedule settings
ColumnLayout {
spacing: Style.marginXS * scaling
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
RowLayout {
Layout.fillWidth: false
spacing: Style.marginM * scaling
NLabel {
label: "Manual Scheduling"
}
Item {// add a little more spacing
}
NText {
text: "Sunrise Time"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
}
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunrise
placeholder: "Select start time"
onSelected: key => Settings.data.nightLight.manualSunrise = key
minimumWidth: 120 * scaling
}
Item {// add a little more spacing
}
NText {
text: "Sunset Time"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
}
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunset
placeholder: "Select stop time"
onSelected: key => Settings.data.nightLight.manualSunset = key
minimumWidth: 120 * scaling
}
}
}
// Force activation toggle
NToggle {
label: "Force activation"
description: "Immediately apply night temperature without scheduling or fade."
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
}
}

View file

@ -1,14 +1,34 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: contentColumn
id: root
spacing: Style.marginL * scaling
width: root.width
// 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: "Appearance"
description: "Configure dock behavior and appearance."
}
NToggle {
label: "Auto-hide"
@ -27,7 +47,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
label: "Background Opacity"
description: "Adjust the background opacity."
@ -81,4 +100,44 @@ ColumnLayout {
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Monitor Configuration
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NHeader {
label: "Monitors Configuration"
description: "Choose which monitors should display the dock."
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: `${modelData.name || "Unknown"}${modelData.model ? `: ${modelData.model}` : ""}`
description: `${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})`
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.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

View file

@ -9,6 +9,11 @@ import qs.Widgets
ColumnLayout {
id: root
NHeader {
label: "Profile"
description: "Configure your user profile and avatar settings."
}
// Profile section
RowLayout {
Layout.fillWidth: true
@ -48,12 +53,9 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "User Interface"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "User Interface"
description: "Main settings for the user interface."
}
NToggle {
@ -133,12 +135,9 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Screen Corners"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Screen Corners"
description: "Customize screen corner rounding and visual effects."
}
NToggle {
@ -187,12 +186,10 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Fonts"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Fonts"
description: "Configure interface typography."
}
// Font configuration section
@ -200,12 +197,13 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NComboBox {
NSearchableComboBox {
label: "Default Font"
description: "Main font used throughout the interface."
model: FontService.availableFonts
currentKey: Settings.data.ui.fontDefault
placeholder: "Select default font..."
searchPlaceholder: "Search fonts..."
popupHeight: 420 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) {
@ -213,12 +211,13 @@ ColumnLayout {
}
}
NComboBox {
NSearchableComboBox {
label: "Fixed Width Font"
description: "Monospace font used for terminal and code display."
model: FontService.monospaceFonts
currentKey: Settings.data.ui.fontFixed
placeholder: "Select monospace font..."
searchPlaceholder: "Search monospace fonts..."
popupHeight: 320 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) {
@ -226,12 +225,13 @@ ColumnLayout {
}
}
NComboBox {
NSearchableComboBox {
label: "Billboard Font"
description: "Large font used for clocks and prominent displays."
model: FontService.displayFonts
currentKey: Settings.data.ui.fontBillboard
placeholder: "Select display font..."
searchPlaceholder: "Search display fonts..."
popupHeight: 320 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) {

View file

@ -10,6 +10,11 @@ ColumnLayout {
spacing: Style.marginL * scaling
width: root.width
NHeader {
label: "System Hooks"
description: "Configure commands to be executed when system events occur."
}
// Enable/Disable Toggle
NToggle {
label: "Enable Hooks"

View file

@ -7,9 +7,12 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL * scaling
ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Appearance"
description: "Configure the launcher behavior and appearance."
}
NComboBox {
id: launcherPosition
@ -105,7 +108,7 @@ ColumnLayout {
checked: Settings.data.appLauncher.useApp2Unit
onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
}
}
NDivider {
Layout.fillWidth: true

View file

@ -11,6 +11,11 @@ ColumnLayout {
id: root
spacing: Style.marginL * scaling
NHeader {
label: "Network Settings"
description: "Configure Wi-Fi and Bluetooth connectivity options."
}
NToggle {
label: "Enable Wi-Fi"
description: "Enable Wi-Fi connectivity."

View file

@ -0,0 +1,183 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
// Helper functions to update arrays immutably
function addMonitor(list, name) {
const arr = (list || []).slice()
if (!arr.includes(name))
arr.push(name)
return arr
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name
})
}
// General Notification Settings
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NHeader {
label: "Appearance"
description: "Configure notifications appearance and behavior."
}
NToggle {
label: "Do Not Disturb"
description: "Disable all notification popups when enabled."
checked: Settings.data.notifications.doNotDisturb
onToggled: checked => Settings.data.notifications.doNotDisturb = checked
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Monitor Configuration
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NHeader {
label: "Monitors Configuration"
description: "Choose which monitors should display notifications."
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: `${modelData.name || "Unknown"}${modelData.model ? `: ${modelData.model}` : ""}`
description: `${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})`
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.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Notification Duration Settings
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NHeader {
label: "Notification Duration"
description: "Configure how long notifications stay visible based on their urgency level."
}
// Low Urgency Duration
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
label: "Low Urgency Duration"
description: "How long low priority notifications stay visible."
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.lowUrgencyDuration
onMoved: Settings.data.notifications.lowUrgencyDuration = value
cutoutColor: Color.mSurface
}
NText {
text: Settings.data.notifications.lowUrgencyDuration + "s"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
// Normal Urgency Duration
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
label: "Normal Urgency Duration"
description: "How long normal priority notifications stay visible."
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.normalUrgencyDuration
onMoved: Settings.data.notifications.normalUrgencyDuration = value
cutoutColor: Color.mSurface
}
NText {
text: Settings.data.notifications.normalUrgencyDuration + "s"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
// Critical Urgency Duration
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
label: "Critical Urgency Duration"
description: "How long critical priority notifications stay visible."
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.criticalUrgencyDuration
onMoved: Settings.data.notifications.criticalUrgencyDuration = value
cutoutColor: Color.mSurface
}
NText {
text: Settings.data.notifications.criticalUrgencyDuration + "s"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
}
}

View file

@ -10,6 +10,11 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "General Settings"
description: "Configure screen recording output and content."
}
// Output Directory
ColumnLayout {
spacing: Style.marginS * scaling
@ -53,12 +58,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Video Settings"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Video Settings"
}
// Source
@ -203,12 +204,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Audio Settings"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Audio Settings"
}
// Audio Source

View file

@ -9,7 +9,6 @@ import qs.Widgets
ColumnLayout {
id: root
width: parent.width
spacing: Style.marginL * scaling
property list<string> wallpapersList: []
@ -42,11 +41,9 @@ ColumnLayout {
}
// Current wallpaper display
NText {
text: "Current Wallpaper"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
NHeader {
label: "Current Wallpaper"
description: "Preview and manage your desktop background."
}
Rectangle {
@ -80,18 +77,9 @@ ColumnLayout {
Layout.fillWidth: true
// Wallpaper grid
NText {
text: "Wallpaper Selector"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: "Click on a wallpaper to set it as your current wallpaper."
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
NHeader {
label: "Wallpaper Selector"
description: "Click on a wallpaper to set it as your current wallpaper."
}
}

View file

@ -9,6 +9,12 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL * scaling
NHeader {
label: "Wallpaper Settings"
description: "Control how wallpapers are managed and displayed."
}
NToggle {
label: "Enable Wallpaper Management"
@ -22,6 +28,7 @@ ColumnLayout {
visible: Settings.data.wallpaper.enabled
spacing: Style.marginL * scaling
Layout.fillWidth: true
NTextInput {
label: "Wallpaper Directory"
description: "Path to your common wallpaper directory."
@ -61,7 +68,7 @@ ColumnLayout {
delegate: RowLayout {
NText {
text: (modelData.name || "Unknown")
color: Color.mSecondary
color: Color.mPrimary
font.weight: Style.fontWeightBold
Layout.preferredWidth: 90 * scaling
}
@ -89,11 +96,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Look & Feel"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
NHeader {
label: "Look & Feel"
}
// Fill Mode
@ -189,11 +193,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Automation"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
NHeader {
label: "Automation"
}
// Random Wallpaper

View file

@ -7,6 +7,12 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL * scaling
NHeader {
label: "Your Location"
description: "Set your location for weather, time zones, and scheduling."
}
// Location section
RowLayout {
@ -57,11 +63,9 @@ ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NText {
text: "Weather"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
NHeader {
label: "Weather"
description: "Configure weather display preferences and temperature units."
}
NToggle {

View file

@ -30,10 +30,11 @@ Variants {
screen: modelData
// Position based on bar location, like Notification popup does
// Position at top of screen, always allow horizontal centering
anchors {
top: Settings.data.bar.position === "top"
bottom: Settings.data.bar.position === "bottom"
top: true
left: true
right: true
}
// Set a width instead of anchoring left/right so we can click on the side of the toast
@ -43,8 +44,43 @@ Variants {
implicitHeight: Math.round(toast.visible ? toast.height + Style.marginM * scaling : 1)
// Set margins based on bar position
margins.top: Settings.data.bar.position === "top" ? (Style.barHeight + Style.marginS + (Settings.data.bar.floating ? Settings.data.bar.marginVertical : 0)) * scaling : 0
margins.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginS + (Settings.data.bar.floating ? Settings.data.bar.marginVertical : 0)) * scaling : 0
margins.top: {
switch (Settings.data.bar.position) {
case "top":
return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
return 0
}
}
margins.bottom: {
switch (Settings.data.bar.position) {
case "bottom":
return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
return 0
}
}
margins.right: {
switch (Settings.data.bar.position) {
case "left":
case "top":
case "bottom":
return Style.marginM * scaling
default:
return 0
}
}
margins.left: {
switch (Settings.data.bar.position) {
case "right":
return Style.marginM * scaling
default:
return 0
}
}
// Transparent background
color: Color.transparent
@ -61,8 +97,8 @@ Variants {
// Simple positioning - margins already account for bar
targetY: Style.marginS * scaling
// Hidden position based on bar location
hiddenY: Settings.data.bar.position === "top" ? -toast.height - 20 : toast.height + 20
// Hidden position - always start from above the screen
hiddenY: -toast.height - 20
Component.onCompleted: {
// Register this toast with the service
@ -74,4 +110,4 @@ Variants {
}
}
}
}
}

View file

@ -64,7 +64,7 @@ NPanel {
NIconButton {
icon: "close"
tooltipText: "Close"
tooltipText: "Close."
sizeRatio: 0.8
onClicked: root.close()
}