diff --git a/Commons/IconsSets/TablerIcons.qml b/Commons/IconsSets/TablerIcons.qml index b4f241a..823fe87 100644 --- a/Commons/IconsSets/TablerIcons.qml +++ b/Commons/IconsSets/TablerIcons.qml @@ -106,6 +106,7 @@ Singleton { "settings-wallpaper-selector": "library-photo", "settings-screen-recorder": "video", "settings-hooks": "link", + "settings-notification": "bell", "settings-about": "info-square-rounded", "bluetooth": "bluetooth", "bt-device-generic": "bluetooth", diff --git a/Commons/KeyboardLayout.qml b/Commons/KeyboardLayout.qml index 61f5a74..38d08aa 100644 --- a/Commons/KeyboardLayout.qml +++ b/Commons/KeyboardLayout.qml @@ -13,8 +13,9 @@ QtObject { "united states": "us", "us english": "us", "british": "gb", - "uk": "gb", - "united kingdom": "gb", + "uk": "ua", + "united kingdom"// FIXED: Ukrainian language code should map to Ukraine + : "gb", "english (uk)": "gb", "canadian": "ca", "canada": "ca", @@ -91,7 +92,9 @@ QtObject { "slovak": "sk", "slovenčina": "sk", "slovakia": "sk", - "ukrainian": "ua", + "uk": "ua", + "ukrainian"// Ukrainian language code + : "ua", "українська": "ua", "ukraine": "ua", "bulgarian": "bg", diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 92b2f7a..622b92a 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -263,7 +263,7 @@ Singleton { // bar property JsonObject bar: JsonObject { - property string position: "top" // "top" or "bottom" + property string position: "top" // "top", "bottom", "left", or "right" property real backgroundOpacity: 1.0 property list monitors: [] @@ -399,6 +399,10 @@ Singleton { property list monitors: [] // Last time the user opened the notification history (ms since epoch) property real lastSeenTs: 0 + // Duration settings for different urgency levels (in seconds) + property int lowUrgencyDuration: 3 + property int normalUrgencyDuration: 8 + property int criticalUrgencyDuration: 15 } // audio diff --git a/Commons/Style.qml b/Commons/Style.qml index 1a232ba..15497b6 100644 --- a/Commons/Style.qml +++ b/Commons/Style.qml @@ -66,9 +66,9 @@ Singleton { property int animationSlowest: Math.round(750 / Settings.data.general.animationSpeed) // Dimensions - property int barHeight: 36 + property int barHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 40 : 36 property int capsuleHeight: (barHeight * 0.73) - property int baseWidgetSize: 32 + property int baseWidgetSize: (barHeight * 0.9) property int sliderWidth: 200 // Delays diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index de720c4..8573bcf 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -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 {} diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 579e854..09bf572 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -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 + } + } + } } } } diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index d3fb762..e9bb66d 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -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) } diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index 56e786d..3e754f3 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -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') diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index 79f8393..5cdf228 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -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 } } diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index e902e75..c71b448 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -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 + } + } + } + } } } diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 5c1b090..7ce5659 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -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 { diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 2dfa42f..fe795a0 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -61,15 +61,6 @@ Item { } } - Timer { - id: externalHideTimer - running: false - interval: 1500 - onTriggered: { - pill.hide() - } - } - NPill { id: pill diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 562994c..8d668a9 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -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 + } + } + } + } } diff --git a/Modules/BluetoothPanel/BluetoothPanel.qml b/Modules/BluetoothPanel/BluetoothPanel.qml index 8b53a1d..12a5ce0 100644 --- a/Modules/BluetoothPanel/BluetoothPanel.qml +++ b/Modules/BluetoothPanel/BluetoothPanel.qml @@ -63,7 +63,7 @@ NPanel { NIconButton { icon: "close" - tooltipText: "Close" + tooltipText: "Close." sizeRatio: 0.8 onClicked: { root.close() diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 4c4f99b..80c6fd3 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -12,7 +12,7 @@ NPanel { preferredWidth: 340 preferredHeight: 320 - panelAnchorRight: true + panelAnchorRight: Settings.data.bar.position === "right" // Main Column panelContent: ColumnLayout { diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 4020a0b..cec00ca 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -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 diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index c51c043..940c6d6 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -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 diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index c25c25f..0a9e852 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -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() diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index ecefb1c..37d54a5 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -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() } diff --git a/Modules/SettingsPanel/Tabs/AboutTab.qml b/Modules/SettingsPanel/Tabs/AboutTab.qml index 24890a1..df2231b 100644 --- a/Modules/SettingsPanel/Tabs/AboutTab.qml +++ b/Modules/SettingsPanel/Tabs/AboutTab.qml @@ -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} awesome contributors!` - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface - Layout.alignment: Qt.AlignCenter + NHeader { + label: "Contributors" + description: `Shout-out to our ${root.contributors.length} awesome contributors!` } GridView { id: contributorsGrid - - Layout.topMargin: Style.marginL * scaling Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: cellWidth * 3 // Fixed 3 columns Layout.preferredHeight: { diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index d60ae38..c7755d0 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -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 diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 966bf26..c9d24d5 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -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 diff --git a/Modules/SettingsPanel/Tabs/BrightnessTab.qml b/Modules/SettingsPanel/Tabs/BrightnessTab.qml deleted file mode 100644 index 0f6f167..0000000 --- a/Modules/SettingsPanel/Tabs/BrightnessTab.qml +++ /dev/null @@ -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 ${LocationService.stableName} - 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 - } -} diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index f358500..f20b711 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -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 diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index 97d7c67..f360e1c 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -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 ${LocationService.stableName} - 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 + } } diff --git a/Modules/SettingsPanel/Tabs/DockTab.qml b/Modules/SettingsPanel/Tabs/DockTab.qml index 3ce3dd9..7324838 100644 --- a/Modules/SettingsPanel/Tabs/DockTab.qml +++ b/Modules/SettingsPanel/Tabs/DockTab.qml @@ -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 + } } diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index aed5758..a6141f3 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -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) { diff --git a/Modules/SettingsPanel/Tabs/HooksTab.qml b/Modules/SettingsPanel/Tabs/HooksTab.qml index 461a4b8..b3c789d 100644 --- a/Modules/SettingsPanel/Tabs/HooksTab.qml +++ b/Modules/SettingsPanel/Tabs/HooksTab.qml @@ -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" diff --git a/Modules/SettingsPanel/Tabs/LauncherTab.qml b/Modules/SettingsPanel/Tabs/LauncherTab.qml index 28bb9f0..6427918 100644 --- a/Modules/SettingsPanel/Tabs/LauncherTab.qml +++ b/Modules/SettingsPanel/Tabs/LauncherTab.qml @@ -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 diff --git a/Modules/SettingsPanel/Tabs/NetworkTab.qml b/Modules/SettingsPanel/Tabs/NetworkTab.qml index c4ac87a..5feb6fa 100644 --- a/Modules/SettingsPanel/Tabs/NetworkTab.qml +++ b/Modules/SettingsPanel/Tabs/NetworkTab.qml @@ -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." diff --git a/Modules/SettingsPanel/Tabs/NotificationTab.qml b/Modules/SettingsPanel/Tabs/NotificationTab.qml new file mode 100644 index 0000000..8adddae --- /dev/null +++ b/Modules/SettingsPanel/Tabs/NotificationTab.qml @@ -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 + } + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml index 58f1e8d..f1b99a3 100644 --- a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml +++ b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml @@ -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 diff --git a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml index f82c4c9..a6d340a 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml @@ -9,7 +9,6 @@ import qs.Widgets ColumnLayout { id: root width: parent.width - spacing: Style.marginL * scaling property list 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." } } diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index 0d40d33..f36c462 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -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 diff --git a/Modules/SettingsPanel/Tabs/WeatherTab.qml b/Modules/SettingsPanel/Tabs/WeatherTab.qml index 667aca5..dbf9f5f 100644 --- a/Modules/SettingsPanel/Tabs/WeatherTab.qml +++ b/Modules/SettingsPanel/Tabs/WeatherTab.qml @@ -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 { diff --git a/Modules/Toast/ToastOverlay.qml b/Modules/Toast/ToastOverlay.qml index b14f5b4..abbd33d 100644 --- a/Modules/Toast/ToastOverlay.qml +++ b/Modules/Toast/ToastOverlay.qml @@ -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 { } } } -} +} \ No newline at end of file diff --git a/Modules/WiFiPanel/WiFiPanel.qml b/Modules/WiFiPanel/WiFiPanel.qml index 02cbc46..bfda627 100644 --- a/Modules/WiFiPanel/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -64,7 +64,7 @@ NPanel { NIconButton { icon: "close" - tooltipText: "Close" + tooltipText: "Close." sizeRatio: 0.8 onClicked: root.close() } diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index e31ad14..98ad57b 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -87,9 +87,26 @@ Singleton { // Maximum visible notifications property int maxVisible: 5 + // Function to get duration based on urgency + function getDurationForUrgency(urgency) { + switch (urgency) { + case 0: + // Low urgency + return (Settings.data.notifications.lowUrgencyDuration || 3) * 1000 + case 1: + // Normal urgency + return (Settings.data.notifications.normalUrgencyDuration || 8) * 1000 + case 2: + // Critical urgency + return (Settings.data.notifications.criticalUrgencyDuration || 15) * 1000 + default: + return (Settings.data.notifications.normalUrgencyDuration || 8) * 1000 + } + } + // Auto-hide timer property Timer hideTimer: Timer { - interval: 8000 // 8 seconds - longer display time + interval: 1000 // Check every second repeat: true running: notificationModel.count > 0 @@ -98,11 +115,26 @@ Singleton { return } - // Remove the oldest notification (last in the list) - let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification - if (oldestNotification) { - // Trigger animation signal instead of direct dismiss - animateAndRemove(oldestNotification, notificationModel.count - 1) + // Check each notification for expiration + for (var i = notificationModel.count - 1; i >= 0; i--) { + let notificationData = notificationModel.get(i) + if (notificationData && notificationData.rawNotification) { + let notification = notificationData.rawNotification + let urgency = notificationData.urgency + let timestamp = notificationData.timestamp + + // Calculate if this notification should be removed + let duration = getDurationForUrgency(urgency) + let now = new Date() + let elapsed = now.getTime() - timestamp.getTime() + + if (elapsed >= duration) { + // Trigger animation signal instead of direct dismiss + animateAndRemove(notification, i) + break + // Only remove one notification per check to avoid conflicts + } + } } } } diff --git a/Widgets/NCheckbox.qml b/Widgets/NCheckbox.qml index a48db95..d9c23a3 100644 --- a/Widgets/NCheckbox.qml +++ b/Widgets/NCheckbox.qml @@ -13,7 +13,7 @@ RowLayout { property bool hovering: false property color activeColor: Color.mPrimary property color activeOnColor: Color.mOnPrimary - property int baseSize: Math.max(Style.baseWidgetSize * 0.8, 14) + property int baseSize: Math.max(Style.baseWidgetSize * 0.7, 14) signal toggled(bool checked) signal entered @@ -39,7 +39,7 @@ RowLayout { implicitHeight: root.baseSize * scaling radius: Style.radiusXS * scaling color: root.checked ? root.activeColor : Color.mSurface - border.color: root.checked ? root.activeColor : Color.mOutline + border.color: Color.mOutline border.width: Math.max(1, Style.borderM * scaling) Behavior on color { @@ -57,9 +57,10 @@ RowLayout { NIcon { visible: root.checked anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 * scaling icon: "check" color: root.activeOnColor - font.pointSize: Math.max(Style.fontSizeS, root.baseSize * 0.7) * scaling + font.pointSize: Math.max(Style.fontSizeXS, root.baseSize * 0.6) * scaling } MouseArea { diff --git a/Widgets/NHeader.qml b/Widgets/NHeader.qml new file mode 100644 index 0000000..16180dc --- /dev/null +++ b/Widgets/NHeader.qml @@ -0,0 +1,32 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons + +ColumnLayout { + id: root + + property string label: "" + property string description: "" + + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM * scaling + + NText { + text: root.label + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + font.capitalization: Font.Capitalize + color: Color.mSecondary + visible: root.title !== "" + } + + NText { + text: root.description + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + visible: root.description !== "" + } +} diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index bf50e5f..ef286f1 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -40,6 +40,7 @@ Loader { property real opacityValue: originalOpacity property alias isClosing: hideTimer.running + readonly property string barPosition: Settings.data.bar.position signal opened signal closed @@ -141,6 +142,7 @@ Loader { // PanelWindow has its own screen property inherited of QsWindow property real scaling: ScalingService.getScreenScale(screen) readonly property real barHeight: Math.round(Style.barHeight * scaling) + readonly property real barWidth: Math.round(Style.barHeight * scaling) readonly property bool barAtBottom: Settings.data.bar.position === "bottom" readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) || (Settings.data.bar.monitors.length === 0)) @@ -148,7 +150,7 @@ Loader { target: ScalingService function onScaleChanged(screenName, scale) { if ((screen !== null) && (screenName === screen.name)) { - root.scaling = scale + root.scaling = scaling = scale } } } @@ -157,7 +159,7 @@ Loader { target: panelWindow function onScreenChanged() { root.screen = screen - root.scaling = ScalingService.getScreenScale(screen) + root.scaling = scaling = ScalingService.getScreenScale(screen) // It's mandatory to force refresh the subloader to ensure the scaling is properly dispatched panelContentLoader.active = false @@ -184,8 +186,29 @@ Loader { anchors.left: true anchors.right: true anchors.bottom: true - margins.top: (barIsVisible && !barAtBottom) ? (barHeight + ((Settings.data.bar.floating && !panelAnchorVerticalCenter) ? Settings.data.bar.marginVertical : 0)) : 0 - margins.bottom: (barIsVisible && barAtBottom) ? (barHeight + ((Settings.data.bar.floating && !panelAnchorVerticalCenter) ? Settings.data.bar.marginVertical : 0)) : 0 + margins.top: { + if (!barIsVisible || barAtBottom) { + return 0 + } + switch (Settings.data.bar.position) { + case "top": + return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating && !panelAnchorVerticalCenter ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0) + default: + return Style.marginM * scaling + } + } + + margins.bottom: { + if (!barIsVisible || !barAtBottom) { + return 0 + } + switch (Settings.data.bar.position) { + case "bottom": + return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating && !panelAnchorVerticalCenter ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0) + default: + return 0 + } + } // Close any panel with Esc without requiring focus Shortcut { @@ -237,31 +260,119 @@ Loader { y: calculatedY property int calculatedX: { - if (root.useButtonPosition) { - // Position panel relative to button - var targetX = root.buttonPosition.x + (root.buttonWidth / 2) - (preferredWidth / 2) + var barPosition = Settings.data.bar.position - // Keep panel within screen bounds - var maxX = panelWindow.width - panelBackground.width - (Style.marginS * scaling) - var minX = Style.marginS * scaling - - return Math.round(Math.max(minX, Math.min(targetX, maxX))) - } else if (!panelAnchorHorizontalCenter && panelAnchorLeft) { + // Check anchor properties first, even when using button positioning + if (!panelAnchorHorizontalCenter && panelAnchorLeft) { return Math.round(Style.marginS * scaling) } else if (!panelAnchorHorizontalCenter && panelAnchorRight) { - return Math.round(panelWindow.width - panelBackground.width - (Style.marginS * scaling)) + // For right anchor, consider bar position + if (barPosition === "right") { + // If bar is on right, position panel to the left of the bar + var maxX = panelWindow.width - barWidth - panelBackground.width - (Style.marginS * scaling) + + // If we have button position, position close to the button like working panels + if (root.useButtonPosition) { + // Use the same logic as working panels - position at edge of bar with spacing + var maxXWithSpacing = panelWindow.width - barWidth - panelBackground.width + // Add spacing - more if screen corners are disabled, less if enabled + if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) { + maxXWithSpacing -= Style.marginL * scaling + } else { + maxXWithSpacing -= Style.marginM * scaling + } + return Math.round(maxXWithSpacing) + } else { + return Math.round(maxX) + } + } else { + // Default right positioning + var rightX = panelWindow.width - panelBackground.width - (Style.marginS * scaling) + return Math.round(rightX) + } + } else if (root.useButtonPosition) { + // Position panel relative to button (only if no explicit anchoring) + var targetX + + // For vertical bars, position panel close to the button + if (barPosition === "left") { + // Position panel to the right of the left bar, close to the button + var minX = barWidth + // Add spacing - more if screen corners are disabled, less if enabled + if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) { + minX += Style.marginL * scaling + } else { + minX += Style.marginM * scaling + } + targetX = minX + } else if (barPosition === "right") { + // Position panel to the left of the right bar, close to the button + var maxX = panelWindow.width - barWidth - panelBackground.width + // Add spacing - more if screen corners are disabled, less if enabled + if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) { + maxX -= Style.marginL * scaling + } else { + maxX -= Style.marginM * scaling + } + targetX = maxX + } else { + // For horizontal bars, center panel on button + targetX = root.buttonPosition.x + (root.buttonWidth / 2) - (panelBackground.width / 2) + } + + // Keep panel within screen bounds + var maxScreenX = panelWindow.width - panelBackground.width - (Style.marginS * scaling) + var minScreenX = Style.marginS * scaling + + return Math.round(Math.max(minScreenX, Math.min(targetX, maxScreenX))) } else { - return Math.round((panelWindow.width - panelBackground.width) / 2) + // For vertical bars, center but avoid bar overlap + var centerX = (panelWindow.width - panelBackground.width) / 2 + if (barPosition === "left") { + var minX = barWidth + // Add spacing - more if screen corners are disabled, less if enabled + if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) { + minX += Style.marginL * scaling + } else { + minX += Style.marginM * scaling + } + centerX = Math.max(centerX, minX) + } else if (barPosition === "right") { + // For right bar, center but ensure it doesn't overlap with the bar + var maxX = panelWindow.width - barWidth - panelBackground.width + // Add spacing - more if screen corners are disabled, less if enabled + if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) { + maxX -= Style.marginL * scaling + } else { + maxX -= Style.marginM * scaling + } + centerX = Math.min(centerX, maxX) + } + return Math.round(centerX) } } property int calculatedY: { - if (panelAnchorVerticalCenter) { + var barPosition = Settings.data.bar.position + + if (root.useButtonPosition) { + // Position panel relative to button + var targetY = root.buttonPosition.y + (root.buttonHeight / 2) - (panelBackground.height / 2) + + // Keep panel within screen bounds + var maxY = panelWindow.height - panelBackground.height - (Style.marginS * scaling) + var minY = Style.marginS * scaling + + return Math.round(Math.max(minY, Math.min(targetY, maxY))) + } else if (panelAnchorVerticalCenter) { return Math.round((panelWindow.height - panelBackground.height) / 2) } else if (panelAnchorBottom) { return Math.round(panelWindow.height - panelBackground.height - (Style.marginS * scaling)) } else if (panelAnchorTop) { return Math.round(Style.marginS * scaling) + } else if (barPosition === "left" || barPosition === "right") { + // For vertical bars, center vertically + return Math.round((panelWindow.height - panelBackground.height) / 2) } else if (!barAtBottom) { // Below the top bar return Math.round(Style.marginS * scaling) diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index e3faeaf..d417203 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -17,8 +17,8 @@ Item { property bool hovered: false property real fontSize: Style.fontSizeXS - // Effective shown state (true if hovered/animated open or forced) - readonly property bool revealed: forceOpen || showPill + readonly property string barPosition: Settings.data.bar.position + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" signal shown signal hidden @@ -29,258 +29,81 @@ Item { signal middleClicked signal wheel(int delta) - // Internal state - property bool showPill: false - property bool shouldAnimateHide: false + // Dynamic sizing based on loaded component + width: pillLoader.item ? pillLoader.item.width : 0 + height: pillLoader.item ? pillLoader.item.height : 0 - // Exposed width logic - readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling) - readonly property int pillHeight: iconSize - readonly property int pillPaddingHorizontal: Style.marginS * scaling - readonly property int pillOverlap: iconSize * 0.5 - readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) + // Loader to switch between vertical and horizontal pill implementations + Loader { + id: pillLoader + sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent - width: iconSize + Math.max(0, pill.width - pillOverlap) - height: pillHeight + Component { + id: verticalPillComponent + NPillVertical { + icon: root.icon + text: root.text + tooltipText: root.tooltipText + sizeRatio: root.sizeRatio + autoHide: root.autoHide + forceOpen: root.forceOpen + disableOpen: root.disableOpen + rightOpen: root.rightOpen + hovered: root.hovered + fontSize: root.fontSize - Rectangle { - id: pill - width: revealed ? maxPillWidth : 1 - height: pillHeight - - x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right - (iconCircle.x + iconCircle.width / 2) - width // Opens left - - opacity: revealed ? Style.opacityFull : Style.opacityNone - color: Color.mSurfaceVariant - - topLeftRadius: rightOpen ? 0 : pillHeight * 0.5 - bottomLeftRadius: rightOpen ? 0 : pillHeight * 0.5 - topRightRadius: rightOpen ? pillHeight * 0.5 : 0 - bottomRightRadius: rightOpen ? pillHeight * 0.5 : 0 - anchors.verticalCenter: parent.verticalCenter - - NText { - id: textItem - anchors.verticalCenter: parent.verticalCenter - x: { - // Little tweak to have a better text horizontal centering - var centerX = (parent.width - width) / 2 - var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling - return centerX + offset - } - text: root.text - font.pointSize: root.fontSize * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - visible: revealed - } - - Behavior on width { - enabled: showAnim.running || hideAnim.running - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - } - Behavior on opacity { - enabled: showAnim.running || hideAnim.running - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - } - } - - Rectangle { - id: iconCircle - width: iconSize - height: iconSize - radius: width * 0.5 - color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant - anchors.verticalCenter: parent.verticalCenter - - x: rightOpen ? 0 : (parent.width - width) - - Behavior on color { - ColorAnimation { - duration: Style.animationNormal - easing.type: Easing.InOutQuad + onShown: root.shown() + onHidden: root.hidden() + onEntered: root.entered() + onExited: root.exited() + onClicked: root.clicked() + onRightClicked: root.rightClicked() + onMiddleClicked: root.middleClicked() + onWheel: root.wheel } } - NIcon { - icon: root.icon - font.pointSize: Style.fontSizeM * scaling - color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurface - // Center horizontally - x: (iconCircle.width - width) / 2 - // Center vertically accounting for font metrics - y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2 - } - } + Component { + id: horizontalPillComponent + NPillHorizontal { + icon: root.icon + text: root.text + tooltipText: root.tooltipText + sizeRatio: root.sizeRatio + autoHide: root.autoHide + forceOpen: root.forceOpen + disableOpen: root.disableOpen + rightOpen: root.rightOpen + hovered: root.hovered + fontSize: root.fontSize - ParallelAnimation { - id: showAnim - running: false - NumberAnimation { - target: pill - property: "width" - from: 1 - to: maxPillWidth - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - NumberAnimation { - target: pill - property: "opacity" - from: 0 - to: 1 - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - onStarted: { - showPill = true - } - onStopped: { - delayedHideAnim.start() - root.shown() - } - } - - SequentialAnimation { - id: delayedHideAnim - running: false - PauseAnimation { - duration: 2500 - } - ScriptAction { - script: if (shouldAnimateHide) { - hideAnim.start() - } - } - } - - ParallelAnimation { - id: hideAnim - running: false - NumberAnimation { - target: pill - property: "width" - from: maxPillWidth - to: 1 - duration: Style.animationNormal - easing.type: Easing.InCubic - } - NumberAnimation { - target: pill - property: "opacity" - from: 1 - to: 0 - duration: Style.animationNormal - easing.type: Easing.InCubic - } - onStopped: { - showPill = false - shouldAnimateHide = false - root.hidden() - } - } - - NTooltip { - id: tooltip - positionAbove: Settings.data.bar.position === "bottom" - target: pill - delay: Style.tooltipDelayLong - text: root.tooltipText - } - - Timer { - id: showTimer - interval: Style.pillDelay - onTriggered: { - if (!showPill) { - showAnim.start() + onShown: root.shown() + onHidden: root.hidden() + onEntered: root.entered() + onExited: root.exited() + onClicked: root.clicked() + onRightClicked: root.rightClicked() + onMiddleClicked: root.middleClicked() + onWheel: root.wheel } } } - MouseArea { - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onEntered: { - hovered = true - root.entered() - tooltip.show() - if (disableOpen) { - return - } - if (!forceOpen) { - showDelayed() - } - } - onExited: { - hovered = false - root.exited() - if (!forceOpen) { - hide() - } - tooltip.hide() - } - onClicked: function (mouse) { - if (mouse.button === Qt.LeftButton) { - root.clicked() - } else if (mouse.button === Qt.RightButton) { - root.rightClicked() - } else if (mouse.button === Qt.MiddleButton) { - root.middleClicked() - } - } - onWheel: wheel => { - root.wheel(wheel.angleDelta.y) - } - } - function show() { - if (!showPill) { - shouldAnimateHide = autoHide - showAnim.start() - } else { - hideAnim.stop() - delayedHideAnim.restart() + if (pillLoader.item && pillLoader.item.show) { + pillLoader.item.show() } } function hide() { - if (forceOpen) { - return + if (pillLoader.item && pillLoader.item.hide) { + pillLoader.item.hide() } - if (showPill) { - hideAnim.start() - } - showTimer.stop() } function showDelayed() { - if (!showPill) { - shouldAnimateHide = autoHide - showTimer.start() - } else { - hideAnim.stop() - delayedHideAnim.restart() - } - } - - onForceOpenChanged: { - if (forceOpen) { - // Immediately lock open without animations - showAnim.stop() - hideAnim.stop() - delayedHideAnim.stop() - showPill = true - } else { - hide() + if (pillLoader.item && pillLoader.item.showDelayed) { + pillLoader.item.showDelayed() } } } diff --git a/Widgets/NPillHorizontal.qml b/Widgets/NPillHorizontal.qml new file mode 100644 index 0000000..58e7692 --- /dev/null +++ b/Widgets/NPillHorizontal.qml @@ -0,0 +1,326 @@ +import QtQuick +import QtQuick.Controls +import qs.Commons +import qs.Services + +Item { + id: root + + property string icon: "" + property string text: "" + property string tooltipText: "" + property real sizeRatio: 0.8 + property bool autoHide: false + property bool forceOpen: false + property bool disableOpen: false + property bool rightOpen: false + property bool hovered: false + property real fontSize: Style.fontSizeXS + + // Bar position detection for pill direction + readonly property string barPosition: Settings.data.bar.position + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" + + // Determine pill direction based on section position + readonly property bool openRightward: rightOpen + readonly property bool openLeftward: !rightOpen + + // Effective shown state (true if animated open or forced) + readonly property bool revealed: forceOpen || showPill + + signal shown + signal hidden + signal entered + signal exited + signal clicked + signal rightClicked + signal middleClicked + signal wheel(int delta) + + // Internal state + property bool showPill: false + property bool shouldAnimateHide: false + + // Sizing logic for horizontal bars + readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling) + readonly property int pillWidth: iconSize + readonly property int pillPaddingHorizontal: Style.marginS * scaling + readonly property int pillPaddingVertical: Style.marginS * scaling + readonly property int pillOverlap: iconSize * 0.5 + readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 4) + readonly property int maxPillHeight: iconSize + + // For horizontal bars: height is just icon size, width includes pill space + width: revealed ? (openRightward ? (iconSize + maxPillWidth - pillOverlap) : (iconSize + maxPillWidth - pillOverlap)) : iconSize + height: iconSize + + Rectangle { + id: pill + width: revealed ? maxPillWidth : 1 + height: revealed ? maxPillHeight : 1 + + // Position based on direction - center the pill relative to the icon + x: openLeftward ? (iconCircle.x + iconCircle.width / 2 - width) : (iconCircle.x + iconCircle.width / 2 - pillOverlap) + y: 0 + + opacity: revealed ? Style.opacityFull : Style.opacityNone + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + // Radius logic for horizontal expansion - rounded on the side that connects to icon + topLeftRadius: openLeftward ? iconSize * 0.5 : 0 + bottomLeftRadius: openLeftward ? iconSize * 0.5 : 0 + topRightRadius: openRightward ? iconSize * 0.5 : 0 + bottomRightRadius: openRightward ? iconSize * 0.5 : 0 + + anchors.verticalCenter: parent.verticalCenter + + NText { + id: textItem + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenterOffset: openLeftward ? -6 * scaling : 6 * scaling // Adjust based on opening direction + text: root.text + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXXS * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + visible: revealed + } + + Behavior on width { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on height { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on opacity { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + id: iconCircle + width: iconSize + height: iconSize + radius: width * 0.5 + color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant + + // Icon positioning based on direction + x: openLeftward ? (parent.width - width) : 0 + y: 0 + anchors.verticalCenter: parent.verticalCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + + NIcon { + icon: root.icon + font.pointSize: Style.fontSizeM * scaling + color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurfaceVariant + // Center horizontally + x: (iconCircle.width - width) / 2 + // Center vertically accounting for font metrics + y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2 + } + } + + ParallelAnimation { + id: showAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: 1 + to: maxPillWidth + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "height" + from: 1 + to: maxPillHeight + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 0 + to: 1 + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + onStarted: { + showPill = true + } + onStopped: { + delayedHideAnim.start() + root.shown() + } + } + + SequentialAnimation { + id: delayedHideAnim + running: false + PauseAnimation { + duration: 2500 + } + ScriptAction { + script: if (shouldAnimateHide) { + hideAnim.start() + } + } + } + + ParallelAnimation { + id: hideAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: maxPillWidth + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "height" + from: maxPillHeight + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 1 + to: 0 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + onStopped: { + showPill = false + shouldAnimateHide = false + root.hidden() + } + } + + NTooltip { + id: tooltip + target: pill + text: root.tooltipText + positionLeft: barPosition === "right" + positionRight: barPosition === "left" + positionAbove: Settings.data.bar.position === "bottom" + delay: Style.tooltipDelayLong + } + + Timer { + id: showTimer + interval: Style.pillDelay + onTriggered: { + if (!showPill) { + showAnim.start() + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onEntered: { + hovered = true + root.entered() + tooltip.show() + if (disableOpen) { + return + } + if (!forceOpen) { + showDelayed() + } + } + onExited: { + hovered = false + root.exited() + if (!forceOpen) { + hide() + } + tooltip.hide() + } + onClicked: function (mouse) { + if (mouse.button === Qt.LeftButton) { + root.clicked() + } else if (mouse.button === Qt.RightButton) { + root.rightClicked() + } else if (mouse.button === Qt.MiddleButton) { + root.middleClicked() + } + } + onWheel: wheel => { + root.wheel(wheel.angleDelta.y) + } + } + + function show() { + if (!showPill) { + shouldAnimateHide = autoHide + showAnim.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } + + function hide() { + if (forceOpen) { + return + } + if (showPill) { + hideAnim.start() + } + showTimer.stop() + } + + function showDelayed() { + if (!showPill) { + shouldAnimateHide = autoHide + showTimer.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } + + onForceOpenChanged: { + if (forceOpen) { + // Immediately lock open without animations + showAnim.stop() + hideAnim.stop() + delayedHideAnim.stop() + showPill = true + } else { + hide() + } + } +} diff --git a/Widgets/NPillVertical.qml b/Widgets/NPillVertical.qml new file mode 100644 index 0000000..160ffc8 --- /dev/null +++ b/Widgets/NPillVertical.qml @@ -0,0 +1,325 @@ +import QtQuick +import QtQuick.Controls +import qs.Commons +import qs.Services + +Item { + id: root + + property string icon: "" + property string text: "" + property string tooltipText: "" + property real sizeRatio: 0.8 + property bool autoHide: false + property bool forceOpen: false + property bool disableOpen: false + property bool rightOpen: false + property bool hovered: false + property real fontSize: Style.fontSizeXS + + // Bar position detection for pill direction + readonly property string barPosition: Settings.data.bar.position + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" + + // Determine pill direction based on section position + readonly property bool openDownward: rightOpen + readonly property bool openUpward: !rightOpen + + // Effective shown state (true if animated open or forced) + readonly property bool revealed: forceOpen || showPill + + signal shown + signal hidden + signal entered + signal exited + signal clicked + signal rightClicked + signal middleClicked + signal wheel(int delta) + + // Internal state + property bool showPill: false + property bool shouldAnimateHide: false + + // Sizing logic for vertical bars + readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling) + readonly property int pillHeight: iconSize + readonly property int pillPaddingHorizontal: Style.marginS * scaling + readonly property int pillPaddingVertical: Style.marginS * scaling + readonly property int pillOverlap: iconSize * 0.5 + readonly property int maxPillWidth: iconSize + readonly property int maxPillHeight: Math.max(1, textItem.implicitHeight + pillPaddingVertical * 3) + + // For vertical bars: width is just icon size, height includes pill space + width: iconSize + height: revealed ? (iconSize + maxPillHeight - pillOverlap) : iconSize + + Rectangle { + id: pill + width: revealed ? maxPillWidth : 1 + height: revealed ? maxPillHeight : 1 + + // Position based on direction - center the pill relative to the icon + x: 0 + y: openUpward ? (iconCircle.y + iconCircle.height / 2 - height) : (iconCircle.y + iconCircle.height / 2) + + opacity: revealed ? Style.opacityFull : Style.opacityNone + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + // Radius logic for vertical expansion - rounded on the side that connects to icon + topLeftRadius: openUpward ? iconSize * 0.5 : 0 + bottomLeftRadius: openDownward ? iconSize * 0.5 : 0 + topRightRadius: openUpward ? iconSize * 0.5 : 0 + bottomRightRadius: openDownward ? iconSize * 0.5 : 0 + + anchors.horizontalCenter: parent.horizontalCenter + + NTextVertical { + id: textItem + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: openUpward ? -6 * scaling : 6 * scaling // Adjust based on opening direction + text: root.text + fontSize: Style.fontSizeXXS * scaling + fontWeight: Style.fontWeightBold + color: Color.mOnSurface + visible: revealed + } + + Behavior on width { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on height { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on opacity { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + id: iconCircle + width: iconSize + height: iconSize + radius: width * 0.5 + color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant + + // Icon positioning based on direction + x: 0 + y: openUpward ? (parent.height - height) : 0 + anchors.horizontalCenter: parent.horizontalCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + + NIcon { + icon: root.icon + font.pointSize: Style.fontSizeM * scaling + color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurfaceVariant + // Center horizontally + x: (iconCircle.width - width) / 2 + // Center vertically accounting for font metrics + y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2 + } + } + + ParallelAnimation { + id: showAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: 1 + to: maxPillWidth + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "height" + from: 1 + to: maxPillHeight + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 0 + to: 1 + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + onStarted: { + showPill = true + } + onStopped: { + delayedHideAnim.start() + root.shown() + } + } + + SequentialAnimation { + id: delayedHideAnim + running: false + PauseAnimation { + duration: 2500 + } + ScriptAction { + script: if (shouldAnimateHide) { + hideAnim.start() + } + } + } + + ParallelAnimation { + id: hideAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: maxPillWidth + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "height" + from: maxPillHeight + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 1 + to: 0 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + onStopped: { + showPill = false + shouldAnimateHide = false + root.hidden() + } + } + + NTooltip { + id: tooltip + target: pill + text: root.tooltipText + positionLeft: barPosition === "right" + positionRight: barPosition === "left" + positionAbove: Settings.data.bar.position === "bottom" + delay: Style.tooltipDelayLong + } + + Timer { + id: showTimer + interval: Style.pillDelay + onTriggered: { + if (!showPill) { + showAnim.start() + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onEntered: { + hovered = true + root.entered() + tooltip.show() + if (disableOpen) { + return + } + if (!forceOpen) { + showDelayed() + } + } + onExited: { + hovered = false + root.exited() + if (!forceOpen) { + hide() + } + tooltip.hide() + } + onClicked: function (mouse) { + if (mouse.button === Qt.LeftButton) { + root.clicked() + } else if (mouse.button === Qt.RightButton) { + root.rightClicked() + } else if (mouse.button === Qt.MiddleButton) { + root.middleClicked() + } + } + onWheel: wheel => { + root.wheel(wheel.angleDelta.y) + } + } + + function show() { + if (!showPill) { + shouldAnimateHide = autoHide + showAnim.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } + + function hide() { + if (forceOpen) { + return + } + if (showPill) { + hideAnim.start() + } + showTimer.stop() + } + + function showDelayed() { + if (!showPill) { + shouldAnimateHide = autoHide + showTimer.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } + + onForceOpenChanged: { + if (forceOpen) { + // Immediately lock open without animations + showAnim.stop() + hideAnim.stop() + delayedHideAnim.stop() + showPill = true + } else { + hide() + } + } +} diff --git a/Widgets/NSearchableComboBox.qml b/Widgets/NSearchableComboBox.qml new file mode 100644 index 0000000..3f91321 --- /dev/null +++ b/Widgets/NSearchableComboBox.qml @@ -0,0 +1,253 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets +import "../Helpers/FuzzySort.js" as Fuzzysort + +RowLayout { + id: root + + property real minimumWidth: 280 * scaling + property real popupHeight: 180 * scaling + + property string label: "" + property string description: "" + property ListModel model: { + + } + property string currentKey: "" + property string placeholder: "" + property string searchPlaceholder: "Search..." + + readonly property real preferredHeight: Style.baseWidgetSize * 1.1 * scaling + + signal selected(string key) + + spacing: Style.marginL * scaling + Layout.fillWidth: true + + // Filtered model for search results + property ListModel filteredModel: ListModel {} + property string searchText: "" + + function findIndexByKey(key) { + for (var i = 0; i < root.model.count; i++) { + if (root.model.get(i).key === key) { + return i + } + } + return -1 + } + + function findIndexByKeyInFiltered(key) { + for (var i = 0; i < root.filteredModel.count; i++) { + if (root.filteredModel.get(i).key === key) { + return i + } + } + return -1 + } + + function filterModel() { + filteredModel.clear() + + if (searchText.trim() === "") { + // If no search text, show all items + for (var i = 0; i < root.model.count; i++) { + filteredModel.append(root.model.get(i)) + } + } else { + // Convert ListModel to array for fuzzy search + var items = [] + for (var i = 0; i < root.model.count; i++) { + items.push(root.model.get(i)) + } + + // Use fuzzy search if available, fallback to simple search + if (typeof Fuzzysort !== 'undefined') { + var fuzzyResults = Fuzzysort.go(searchText, items, { + "key": "name", + "threshold": -1000, + "limit": 50 + }) + + // Add results in order of relevance + for (var j = 0; j < fuzzyResults.length; j++) { + filteredModel.append(fuzzyResults[j].obj) + } + } else { + // Fallback to simple search + var searchLower = searchText.toLowerCase() + for (var i = 0; i < items.length; i++) { + var item = items[i] + if (item.name.toLowerCase().includes(searchLower)) { + filteredModel.append(item) + } + } + } + } + } + + onSearchTextChanged: filterModel() + onModelChanged: filterModel() + + NLabel { + label: root.label + description: root.description + } + + Item { + Layout.fillWidth: true + } + + ComboBox { + id: combo + + Layout.minimumWidth: root.minimumWidth + Layout.preferredHeight: root.preferredHeight + model: filteredModel + currentIndex: findIndexByKeyInFiltered(currentKey) + onActivated: { + if (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) { + root.selected(filteredModel.get(combo.currentIndex).key) + } + } + + background: Rectangle { + implicitWidth: Style.baseWidgetSize * 3.75 * scaling + implicitHeight: preferredHeight + color: Color.mSurface + border.color: combo.activeFocus ? Color.mSecondary : Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + radius: Style.radiusM * scaling + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + contentItem: NText { + leftPadding: Style.marginL * scaling + rightPadding: combo.indicator.width + Style.marginL * scaling + font.pointSize: Style.fontSizeM * scaling + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + color: (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) ? Color.mOnSurface : Color.mOnSurfaceVariant + text: (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) ? filteredModel.get(combo.currentIndex).name : root.placeholder + } + + indicator: NIcon { + x: combo.width - width - Style.marginM * scaling + y: combo.topPadding + (combo.availableHeight - height) / 2 + icon: "caret-down" + font.pointSize: Style.fontSizeL * scaling + } + + popup: Popup { + y: combo.height + width: combo.width + height: root.popupHeight + 60 * scaling + padding: Style.marginM * scaling + + contentItem: ColumnLayout { + spacing: Style.marginS * scaling + + // Search input + NTextInput { + id: searchInput + Layout.fillWidth: true + placeholderText: root.searchPlaceholder + text: root.searchText + onTextChanged: root.searchText = text + fontSize: Style.fontSizeS * scaling + } + + // Font list + ListView { + id: listView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: combo.popup.visible ? filteredModel : null + ScrollIndicator.vertical: ScrollIndicator {} + + delegate: ItemDelegate { + width: listView.width + hoverEnabled: true + highlighted: ListView.view.currentIndex === index + + onHoveredChanged: { + if (hovered) { + ListView.view.currentIndex = index + } + } + + onClicked: { + root.selected(filteredModel.get(index).key) + combo.currentIndex = root.findIndexByKeyInFiltered(filteredModel.get(index).key) + combo.popup.close() + } + + contentItem: NText { + text: name + font.pointSize: Style.fontSizeM * scaling + color: highlighted ? Color.mSurface : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + background: Rectangle { + width: listView.width * scaling + color: highlighted ? Color.mTertiary : Color.transparent + radius: Style.radiusS * scaling + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } + } + + background: Rectangle { + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + radius: Style.radiusM * scaling + } + } + + // Update the currentIndex if the currentKey is changed externally + Connections { + target: root + function onCurrentKeyChanged() { + combo.currentIndex = root.findIndexByKeyInFiltered(currentKey) + } + } + + // Focus search input when popup opens + Connections { + target: combo.popup + function onVisibleChanged() { + if (combo.popup.visible) { + // Small delay to ensure the popup is fully rendered + Qt.callLater(function () { + if (searchInput && searchInput.inputItem) { + searchInput.inputItem.forceActiveFocus() + } + }) + } + } + } + } +} diff --git a/Widgets/NTextVertical.qml b/Widgets/NTextVertical.qml new file mode 100644 index 0000000..956fe1d --- /dev/null +++ b/Widgets/NTextVertical.qml @@ -0,0 +1,26 @@ +import QtQuick +import qs.Commons +import qs.Services + +Column { + id: root + + property string text: "" + property real fontSize: Style.fontSizeXS + property color color: Color.mOnSurface + property int fontWeight: Style.fontWeightBold + + spacing: -2 * scaling + + Repeater { + model: root.text.split("") + NText { + text: modelData + font.family: Settings.data.ui.fontFixed + font.pointSize: root.fontSize + font.weight: root.fontWeight + color: root.color + horizontalAlignment: Text.AlignHCenter + } + } +} diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index f04d117..35413cd 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -11,7 +11,7 @@ RowLayout { property string description: "" property bool checked: false property bool hovering: false - property int baseSize: Style.baseWidgetSize + property int baseSize: Style.baseWidgetSize * 0.8 signal toggled(bool checked) signal entered @@ -31,7 +31,7 @@ RowLayout { implicitHeight: root.baseSize * scaling radius: height * 0.5 color: root.checked ? Color.mPrimary : Color.mSurface - border.color: root.checked ? Color.mPrimary : Color.mOutline + border.color: Color.mOutline border.width: Math.max(1, Style.borderM * scaling) Behavior on color { @@ -53,7 +53,7 @@ RowLayout { color: root.checked ? Color.mOnPrimary : Color.mPrimary border.color: root.checked ? Color.mSurface : Color.mSurface border.width: Math.max(1, Style.borderM * scaling) - y: 2 * scaling + anchors.verticalCenter: parent.verticalCenter x: root.checked ? switcher.width - width - 2 * scaling : 2 * scaling Behavior on x { diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 168d7ee..eb8afd1 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -13,6 +13,8 @@ Window { property bool positionLeft: false property bool positionRight: false + readonly property string barPosition: Settings.data.bar.position + flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint color: Color.transparent visible: false @@ -46,17 +48,34 @@ Window { return } - if (positionLeft) { + // Auto-detect positioning based on bar position if not explicitly set + var shouldPositionLeft = positionLeft + var shouldPositionRight = positionRight + var shouldPositionAbove = positionAbove + + // If no explicit positioning is set, auto-detect based on bar position + if (!positionLeft && !positionRight && !positionAbove) { + if (barPosition === "left") { + shouldPositionRight = true + } else if (barPosition === "right") { + shouldPositionLeft = true + } else if (barPosition === "bottom") { + shouldPositionAbove = true + } + // For "top" bar, default to below (no change needed) + } + + if (shouldPositionLeft) { // Position tooltip to the left of the target var pos = target.mapToGlobal(0, 0) x = pos.x - width - 12 // 12 px margin to the left y = pos.y - height / 2 + target.height / 2 - } else if (positionRight) { + } else if (shouldPositionRight) { // Position tooltip to the right of the target var pos = target.mapToGlobal(target.width, 0) x = pos.x + 12 // 12 px margin to the right y = pos.y - height / 2 + target.height / 2 - } else if (positionAbove) { + } else if (shouldPositionAbove) { // Position tooltip above the target var pos = target.mapToGlobal(0, 0) x = pos.x - width / 2 + target.width / 2 diff --git a/flake.nix b/flake.nix index b2daca6..cdb0de6 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,5 @@ { - description = - "Noctalia shell - a Wayland desktop shell built with Quickshell"; + description = "Noctalia shell - a Wayland desktop shell built with Quickshell"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; @@ -12,13 +11,22 @@ }; }; - outputs = { self, nixpkgs, systems, quickshell, ... }: - let eachSystem = nixpkgs.lib.genAttrs (import systems); - in { - formatter = - eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra); + outputs = + { + self, + nixpkgs, + systems, + quickshell, + ... + }: + let + eachSystem = nixpkgs.lib.genAttrs (import systems); + in + { + formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra); - packages = eachSystem (system: + packages = eachSystem ( + system: let pkgs = nixpkgs.legacyPackages.${system}; qs = quickshell.packages.${system}.default.override { @@ -26,7 +34,8 @@ withI3 = false; }; - runtimeDeps = with pkgs; + runtimeDeps = + with pkgs; [ bash bluez @@ -41,21 +50,34 @@ matugen networkmanager wl-clipboard - ] ++ lib.optionals (pkgs.stdenv.hostPlatform.isx86_64) - [ gpu-screen-recorder ]; + ] + ++ lib.optionals (pkgs.stdenv.hostPlatform.isx86_64) [ + gpu-screen-recorder + ]; fontconfig = pkgs.makeFontsConf { - fontDirectories = [ pkgs.roboto pkgs.inter-nerdfont ]; + fontDirectories = [ + pkgs.roboto + pkgs.inter-nerdfont + ]; }; - in { + in + { default = pkgs.stdenv.mkDerivation { pname = "noctalia-shell"; version = self.rev or self.dirtyRev or "dirty"; src = ./.; - nativeBuildInputs = - [ pkgs.gcc pkgs.makeWrapper pkgs.qt6.wrapQtAppsHook ]; - buildInputs = [ qs pkgs.xkeyboard_config pkgs.qt6.qtbase ]; + nativeBuildInputs = [ + pkgs.gcc + pkgs.makeWrapper + pkgs.qt6.wrapQtAppsHook + ]; + buildInputs = [ + qs + pkgs.xkeyboard-config + pkgs.qt6.qtbase + ]; propagatedBuildInputs = runtimeDeps; installPhase = '' @@ -69,14 +91,14 @@ ''; meta = { - description = - "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell."; + description = "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell."; homepage = "https://github.com/noctalia-dev/noctalia-shell"; license = pkgs.lib.licenses.mit; mainProgram = "noctalia-shell"; }; }; - }); + } + ); defaultPackage = eachSystem (system: self.packages.${system}.default); };