diff --git a/Assets/Matugen/templates/vesktop.css b/Assets/Matugen/templates/vesktop.css index ac5b166..9876c8a 100644 --- a/Assets/Matugen/templates/vesktop.css +++ b/Assets/Matugen/templates/vesktop.css @@ -510,7 +510,7 @@ } .visual-refresh.theme-dark .slateTextArea_ec4baf > div:first-child .emptyText__1464f::before { - content: "send a message" !important; + content: "Message #general" !important; color: {{colors.on_surface_variant.default.hex}} !important; } diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 097f5e9..0c1dd55 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -278,14 +278,12 @@ Singleton { property string position: "center" property real backgroundOpacity: 1.0 property list pinnedExecs: [] - property bool useApp2Unit: false } // dock property JsonObject dock: JsonObject { property bool autoHide: false property bool exclusive: false - property real backgroundOpacity: 1.0 property list monitors: [] } @@ -297,7 +295,6 @@ Singleton { // notifications property JsonObject notifications: JsonObject { - property bool doNotDisturb: false property list monitors: [] } diff --git a/Commons/Style.qml b/Commons/Style.qml index 902a225..d2af5a8 100644 --- a/Commons/Style.qml +++ b/Commons/Style.qml @@ -29,7 +29,6 @@ Singleton { property int fontWeightBold: 700 // Radii - property int radiusXXS: 4 * Settings.data.general.radiusRatio property int radiusXS: 8 * Settings.data.general.radiusRatio property int radiusS: 12 * Settings.data.general.radiusRatio property int radiusM: 16 * Settings.data.general.radiusRatio diff --git a/Commons/Time.qml b/Commons/Time.qml index d7ec78e..fe1f3fd 100644 --- a/Commons/Time.qml +++ b/Commons/Time.qml @@ -78,34 +78,23 @@ Singleton { } // Format an easy to read approximate duration ex: 4h32m -// Used to display the time remaining on the Battery widget, computer uptime, etc.. +// Used to display the time remaining on the Battery widget function formatVagueHumanReadableDuration(totalSeconds) { - if (typeof totalSeconds !== 'number' || totalSeconds < 0) { - return '0s' + const hours = Math.floor(totalSeconds / 3600) + const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60) + const seconds = totalSeconds - (hours * 3600) - (minutes * 60) + + var str = "" + if (hours) { + str += hours.toString() + "h" + } + if (minutes) { + str += minutes.toString() + "m" } - - // Floor the input to handle decimal seconds - totalSeconds = Math.floor(totalSeconds) - - const days = Math.floor(totalSeconds / 86400) - const hours = Math.floor((totalSeconds % 86400) / 3600) - const minutes = Math.floor((totalSeconds % 3600) / 60) - const seconds = totalSeconds % 60 - - const parts = [] - if (days) - parts.push(`${days}d`) - if (hours) - parts.push(`${hours}h`) - if (minutes) - parts.push(`${minutes}m`) - - // Only show seconds if no hours and no minutes if (!hours && !minutes) { - parts.push(`${seconds}s`) + str += seconds.toString() + "s" } - - return parts.join('') + return str } Timer { diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index a1eca8b..8fed9bf 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -237,7 +237,7 @@ Variants { transitionProgress = 0.0 Qt.callLater(() => { currentWallpaper.asynchronous = true - }) + }, 100) } } diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 65f900e..c7387d1 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -2,45 +2,38 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell -import Quickshell.Wayland import Quickshell.Widgets import qs.Commons import qs.Services import qs.Widgets -RowLayout { +Row { id: root + property ShellScreen screen property real scaling: 1.0 readonly property real minWidth: 160 readonly property real maxWidth: 400 - Layout.alignment: Qt.AlignVCenter + + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling visible: getTitle() !== "" function getTitle() { + // Use the service's focusedWindowTitle property which is updated immediately + // when WindowOpenedOrChanged events are received return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" } function getAppIcon() { - // Try CompositorService first const focusedWindow = CompositorService.getFocusedWindow() - if (focusedWindow && focusedWindow.appId) { - return Icons.iconForAppId(focusedWindow.appId.toLowerCase()) - } + if (!focusedWindow || !focusedWindow.appId) + return "" - // Fallback to ToplevelManager - if (ToplevelManager && ToplevelManager.activeToplevel) { - const activeToplevel = ToplevelManager.activeToplevel - if (activeToplevel.appId) { - return Icons.iconForAppId(activeToplevel.appId.toLowerCase()) - } - } - - return "" + return Icons.iconForAppId(focusedWindow.appId) } - // A hidden text element to safely measure the full title width + // A hidden text element to safely measure the full title width NText { id: fullTitleMetrics visible: false @@ -50,13 +43,15 @@ RowLayout { } Rectangle { - id: windowTitleRect + // Let the Rectangle size itself based on its content (the Row) visible: root.visible - Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + width: row.width + Style.marginM * 2 * scaling + height: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant + anchors.verticalCenter: parent.verticalCenter + Item { id: mainContainer anchors.fill: parent @@ -64,16 +59,16 @@ RowLayout { anchors.rightMargin: Style.marginS * scaling clip: true - RowLayout { - id: contentLayout - anchors.centerIn: parent + Row { + id: row + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling // Window icon Item { - Layout.preferredWidth: Style.fontSizeL * scaling * 1.2 - Layout.preferredHeight: Style.fontSizeL * scaling * 1.2 - Layout.alignment: Qt.AlignVCenter + width: Style.fontSizeL * scaling * 1.2 + height: Style.fontSizeL * scaling * 1.2 + anchors.verticalCenter: parent.verticalCenter visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon IconImage { @@ -88,24 +83,26 @@ RowLayout { NText { id: titleText - Layout.preferredWidth: { + + // For short titles, show full. For long titles, truncate and expand on hover + width: { if (mouseArea.containsMouse) { return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling)) } else { return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling)) } } - Layout.alignment: Qt.AlignVCenter horizontalAlignment: Text.AlignLeft text: getTitle() font.pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightMedium elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight + anchors.verticalCenter: parent.verticalCenter verticalAlignment: Text.AlignVCenter color: Color.mSecondary clip: true - Behavior on Layout.preferredWidth { + Behavior on width { NumberAnimation { duration: Style.animationSlow easing.type: Easing.InOutCubic @@ -123,14 +120,4 @@ RowLayout { } } } - - Connections { - target: CompositorService - function onActiveWindowChanged() { - windowIcon.source = Qt.binding(getAppIcon) - } - function onWindowListChanged() { - windowIcon.source = Qt.binding(getAppIcon) - } - } } diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index ee57b57..9da63f5 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -23,7 +23,7 @@ Rectangle { NTooltip { id: tooltip - text: `${Time.dateString}.` + text: Time.dateString target: clock positionAbove: Settings.data.bar.position === "bottom" } diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index de8f96d..e99537a 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -47,13 +47,13 @@ NIconButton { } else { var lines = [] if (userLeftClickExec !== "") { - lines.push(`Left click: ${userLeftClickExec}.`) + lines.push(`Left click: ${userLeftClickExec}`) } if (userRightClickExec !== "") { - lines.push(`Right click: ${userRightClickExec}.`) + lines.push(`Right click: ${userRightClickExec}`) } if (userMiddleClickExec !== "") { - lines.push(`Middle click: ${userMiddleClickExec}.`) + lines.push(`Middle click: ${userMiddleClickExec}`) } return lines.join("
") } diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml index b9b44a5..24f0c26 100644 --- a/Modules/Bar/Widgets/KeyboardLayout.qml +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Layouts import Quickshell import Quickshell.Wayland import Quickshell.Io @@ -7,7 +6,7 @@ import qs.Commons import qs.Services import qs.Widgets -Item { +Row { id: root property ShellScreen screen @@ -19,13 +18,12 @@ Item { // Use the shared service for keyboard layout property string currentLayout: KeyboardLayoutService.currentLayout - implicitWidth: pill.width - implicitHeight: pill.height + width: pill.width + height: pill.height NPill { id: pill - anchors.verticalCenter: parent.verticalCenter rightOpen: BarWidgetRegistry.getNPillDirection(root) icon: "keyboard_alt" iconCircleColor: Color.mPrimary diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index 2483dbc..7d2ffb7 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 { +Row { id: root property ShellScreen screen @@ -15,10 +15,10 @@ RowLayout { readonly property real minWidth: 160 readonly property real maxWidth: 400 - Layout.alignment: Qt.AlignVCenter + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling visible: MediaService.currentPlayer !== null && MediaService.canPlay - Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 + width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 function getTitle() { return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") @@ -35,13 +35,15 @@ RowLayout { Rectangle { id: mediaMini - Layout.preferredWidth: rowLayout.implicitWidth + Style.marginM * 2 * scaling - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) - Layout.alignment: Qt.AlignVCenter + // Let the Rectangle size itself based on its content (the Row) + width: row.width + Style.marginM * 2 * scaling + height: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant + anchors.verticalCenter: parent.verticalCenter + // Used to anchor the tooltip, so the tooltip does not move when the content expands Item { id: anchor @@ -59,7 +61,7 @@ RowLayout { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear" - && MediaService.isPlaying + && MediaService.isPlaying && MediaService.trackLength > 0 z: 0 sourceComponent: LinearSpectrum { @@ -69,42 +71,42 @@ RowLayout { fillColor: Color.mOnSurfaceVariant opacity: 0.4 } - } - Loader { - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored" - && MediaService.isPlaying - z: 0 + Loader { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored" + && MediaService.isPlaying && MediaService.trackLength > 0 + z: 0 - sourceComponent: MirroredSpectrum { - width: mainContainer.width - Style.marginS * scaling - height: mainContainer.height - Style.marginS * scaling - values: CavaService.values - fillColor: Color.mOnSurfaceVariant - opacity: 0.4 + sourceComponent: MirroredSpectrum { + width: mainContainer.width - Style.marginS * scaling + height: mainContainer.height - Style.marginS * scaling + values: CavaService.values + fillColor: Color.mOnSurfaceVariant + opacity: 0.4 + } + } + + Loader { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave" + && MediaService.isPlaying && MediaService.trackLength > 0 + z: 0 + + sourceComponent: WaveSpectrum { + width: mainContainer.width - Style.marginS * scaling + height: mainContainer.height - Style.marginS * scaling + values: CavaService.values + fillColor: Color.mOnSurfaceVariant + opacity: 0.4 + } } } - Loader { - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave" - && MediaService.isPlaying - z: 0 - - sourceComponent: WaveSpectrum { - width: mainContainer.width - Style.marginS * scaling - height: mainContainer.height - Style.marginS * scaling - values: CavaService.values - fillColor: Color.mOnSurfaceVariant - opacity: 0.4 - } - } - - RowLayout { - id: rowLayout + Row { + id: row anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling z: 1 // Above the visualizer @@ -114,18 +116,17 @@ RowLayout { text: MediaService.isPlaying ? "pause" : "play_arrow" font.pointSize: Style.fontSizeL * scaling verticalAlignment: Text.AlignVCenter - Layout.alignment: Qt.AlignVCenter + anchors.verticalCenter: parent.verticalCenter visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible } - ColumnLayout { - Layout.alignment: Qt.AlignVCenter + Column { + anchors.verticalCenter: parent.verticalCenter visible: Settings.data.audio.showMiniplayerAlbumArt - spacing: 0 Item { - Layout.preferredWidth: Math.round(18 * scaling) - Layout.preferredHeight: Math.round(18 * scaling) + width: Math.round(18 * scaling) + height: Math.round(18 * scaling) NImageCircled { id: trackArt @@ -141,23 +142,23 @@ RowLayout { NText { id: titleText - Layout.preferredWidth: { + // For short titles, show full. For long titles, truncate and expand on hover + width: { if (mouseArea.containsMouse) { return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling)) } else { return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling)) } } - Layout.alignment: Qt.AlignVCenter - text: getTitle() font.pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightMedium elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter verticalAlignment: Text.AlignVCenter color: Color.mTertiary - Behavior on Layout.preferredWidth { + Behavior on width { NumberAnimation { duration: Style.animationSlow easing.type: Easing.InOutCubic @@ -204,10 +205,10 @@ RowLayout { text: { var str = "" if (MediaService.canGoNext) { - str += "Right click for next.\n" + str += "Right click for next\n" } if (MediaService.canGoPrevious) { - str += "Middle click for previous." + str += "Middle click for previous\n" } return str } diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index f4e1c1a..410e041 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -100,7 +100,7 @@ Item { AudioService.setInputMuted(!AudioService.inputMuted) } onMiddleClicked: { - Quickshell.execDetached(["pwvucontrol"]) + Quickshell.execDetached(["pwvucontrol"]); } } } diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 6ea2e20..342a424 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -21,7 +21,7 @@ NIconButton { colorBorderHover: Color.transparent icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off" - tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled." : "disabled."}\nLeft click to toggle.\nRight click to access settings.` + tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled" : "disabled"}\nLeft click to toggle.\nRight click to access settings.` onClicked: Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled onRightClicked: { diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 48a62fd..222a0eb 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -14,14 +14,11 @@ NIconButton { property real scaling: 1.0 sizeRatio: 0.8 - icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications" - tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'." + icon: "notifications" + tooltipText: "Notification history" colorBg: Color.mSurfaceVariant - colorFg: Settings.data.notifications.doNotDisturb ? Color.mError : Color.mOnSurface + colorFg: Color.mOnSurface colorBorder: Color.transparent colorBorderHover: Color.transparent - onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this) - - onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb } diff --git a/Modules/Bar/Widgets/SidePanelToggle.qml b/Modules/Bar/Widgets/SidePanelToggle.qml index b9572fb..1b0b4ce 100644 --- a/Modules/Bar/Widgets/SidePanelToggle.qml +++ b/Modules/Bar/Widgets/SidePanelToggle.qml @@ -12,7 +12,7 @@ NIconButton { property real scaling: 1.0 icon: Settings.data.bar.useDistroLogo ? "" : "widgets" - tooltipText: "Open side panel." + tooltipText: "Open side panel" sizeRatio: 0.8 colorBg: Color.mSurfaceVariant diff --git a/Modules/Bar/Widgets/Spacer.qml b/Modules/Bar/Widgets/Spacer.qml deleted file mode 100644 index 5a62372..0000000 --- a/Modules/Bar/Widgets/Spacer.qml +++ /dev/null @@ -1,56 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import qs.Commons -import qs.Services -import qs.Widgets - -Item { - id: root - - // Widget properties passed from Bar.qml - property var screen - property real scaling: 1.0 - - property string barSection: "" - property int sectionWidgetIndex: -1 - property int sectionWidgetsCount: 0 - - // Get user settings from Settings data - make it reactive - property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() - if (section && sectionWidgetIndex >= 0) { - var widgets = Settings.data.bar.widgets[section] - if (widgets && sectionWidgetIndex < widgets.length) { - return widgets[sectionWidgetIndex] - } - } - return {} - } - - // Use settings or defaults from BarWidgetRegistry - readonly property int userWidth: { - var section = barSection.replace("Section", "").toLowerCase() - if (section && sectionWidgetIndex >= 0) { - var widgets = Settings.data.bar.widgets[section] - if (widgets && sectionWidgetIndex < widgets.length) { - return widgets[sectionWidgetIndex].width || BarWidgetRegistry.widgetMetadata["Spacer"].width - } - } - return BarWidgetRegistry.widgetMetadata["Spacer"].width - } - - // Set the width based on user settings - implicitWidth: userWidth * scaling - implicitHeight: Style.barHeight * scaling - width: implicitWidth - height: implicitHeight - - // Optional: Add a subtle visual indicator in debug mode - Rectangle { - anchors.fill: parent - color: Qt.rgba(1, 0, 0, 0.1) // Very subtle red tint - visible: Settings.data.general.debugMode || false - radius: 2 * scaling - } -} diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index 6c2346c..ce16aa3 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -1,146 +1,145 @@ import QtQuick -import QtQuick.Layouts import Quickshell import qs.Commons import qs.Services import qs.Widgets -RowLayout { +Row { id: root property ShellScreen screen property real scaling: 1.0 - Layout.alignment: Qt.AlignVCenter + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling Rectangle { - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) - Layout.preferredWidth: mainLayout.implicitWidth + Style.marginM * scaling * 2 - Layout.alignment: Qt.AlignVCenter + // Let the Rectangle size itself based on its content (the Row) + width: row.width + Style.marginM * scaling * 2 + height: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant - RowLayout { - id: mainLayout + anchors.verticalCenter: parent.verticalCenter + + Item { + id: mainContainer anchors.fill: parent anchors.leftMargin: Style.marginS * scaling anchors.rightMargin: Style.marginS * scaling - spacing: Style.marginS * scaling - // CPU Usage Component - RowLayout { - id: cpuUsageLayout - spacing: Style.marginXS * scaling - Layout.alignment: Qt.AlignVCenter + Row { + id: row + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS * scaling + Row { + id: cpuUsageLayout + spacing: Style.marginXS * scaling - NIcon { - id: cpuUsageIcon - text: "speed" - Layout.alignment: Qt.AlignVCenter + NIcon { + id: cpuUsageIcon + text: "speed" + anchors.verticalCenter: parent.verticalCenter + } + + NText { + id: cpuUsageText + text: `${SystemStatService.cpuUsage}%` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } } - NText { - id: cpuUsageText - text: `${SystemStatService.cpuUsage}%` - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightMedium - Layout.alignment: Qt.AlignVCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } - } + // CPU Temperature Component + Row { + id: cpuTempLayout + // spacing is thin here to compensate for the vertical thermometer icon + spacing: Style.marginXXS * scaling - // CPU Temperature Component - RowLayout { - id: cpuTempLayout - // spacing is thin here to compensate for the vertical thermometer icon - spacing: Style.marginXXS * scaling - Layout.alignment: Qt.AlignVCenter + NIcon { + text: "thermometer" + anchors.verticalCenter: parent.verticalCenter + } - NIcon { - text: "thermometer" - Layout.alignment: Qt.AlignVCenter + NText { + text: `${SystemStatService.cpuTemp}°C` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } } - NText { - text: `${SystemStatService.cpuTemp}°C` - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightMedium - Layout.alignment: Qt.AlignVCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } - } + // Memory Usage Component + Row { + id: memoryUsageLayout + spacing: Style.marginXS * scaling - // Memory Usage Component - RowLayout { - id: memoryUsageLayout - spacing: Style.marginXS * scaling - Layout.alignment: Qt.AlignVCenter + NIcon { + text: "memory" + anchors.verticalCenter: parent.verticalCenter + } - NIcon { - text: "memory" - Layout.alignment: Qt.AlignVCenter + NText { + text: `${SystemStatService.memoryUsageGb}G` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } } - NText { - text: `${SystemStatService.memoryUsageGb}G` - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightMedium - Layout.alignment: Qt.AlignVCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } - } + // Network Download Speed Component + Row { + id: networkDownloadLayout + spacing: Style.marginXS * scaling + visible: Settings.data.bar.showNetworkStats - // Network Download Speed Component - RowLayout { - id: networkDownloadLayout - spacing: Style.marginXS * scaling - Layout.alignment: Qt.AlignVCenter - visible: Settings.data.bar.showNetworkStats + NIcon { + text: "download" + anchors.verticalCenter: parent.verticalCenter + } - NIcon { - text: "download" - Layout.alignment: Qt.AlignVCenter + NText { + text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } } - NText { - text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightMedium - Layout.alignment: Qt.AlignVCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } - } + // Network Upload Speed Component + Row { + id: networkUploadLayout + spacing: Style.marginXS * scaling + visible: Settings.data.bar.showNetworkStats - // Network Upload Speed Component - RowLayout { - id: networkUploadLayout - spacing: Style.marginXS * scaling - Layout.alignment: Qt.AlignVCenter - visible: Settings.data.bar.showNetworkStats + NIcon { + text: "upload" + anchors.verticalCenter: parent.verticalCenter + } - NIcon { - text: "upload" - Layout.alignment: Qt.AlignVCenter - } - - NText { - text: SystemStatService.formatSpeed(SystemStatService.txSpeed) - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightMedium - Layout.alignment: Qt.AlignVCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary + NText { + text: SystemStatService.formatSpeed(SystemStatService.txSpeed) + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } } } } diff --git a/Modules/Bar/Widgets/Taskbar.qml b/Modules/Bar/Widgets/Taskbar.qml index 103e707..623d7e7 100644 --- a/Modules/Bar/Widgets/Taskbar.qml +++ b/Modules/Bar/Widgets/Taskbar.qml @@ -2,7 +2,6 @@ pragma ComponentBehavior import QtQuick import QtQuick.Controls -import QtQuick.Layouts import Quickshell import Quickshell.Widgets import Quickshell.Wayland @@ -18,14 +17,15 @@ Rectangle { readonly property real itemSize: Style.baseWidgetSize * 0.8 * scaling // Always visible when there are toplevels - implicitWidth: taskbarLayout.implicitWidth + Style.marginM * scaling * 2 + implicitWidth: taskbarRow.width + Style.marginM * scaling * 2 implicitHeight: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant - RowLayout { - id: taskbarLayout - anchors.centerIn: parent + Row { + id: taskbarRow + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter spacing: Style.marginXXS * root.scaling Repeater { @@ -35,10 +35,8 @@ Rectangle { required property Toplevel modelData property Toplevel toplevel: modelData property bool isActive: ToplevelManager.activeToplevel === modelData - - Layout.preferredWidth: root.itemSize - Layout.preferredHeight: root.itemSize - Layout.alignment: Qt.AlignCenter + width: root.itemSize + height: root.itemSize Rectangle { id: iconBackground @@ -91,7 +89,7 @@ Rectangle { NTooltip { id: taskbarTooltip - text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App." + text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App" target: taskbarItem positionAbove: Settings.data.bar.position === "bottom" } diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 06de40f..f29d7b7 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -26,26 +26,26 @@ Rectangle { } visible: SystemTray.items.values.length > 0 - implicitWidth: trayLayout.implicitWidth + Style.marginM * scaling * 2 + implicitWidth: tray.width + Style.marginM * scaling * 2 implicitHeight: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant Layout.alignment: Qt.AlignVCenter - RowLayout { - id: trayLayout - anchors.centerIn: parent + Row { + id: tray + + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter spacing: Style.marginS * scaling Repeater { id: repeater model: SystemTray.items - delegate: Item { - Layout.preferredWidth: itemSize - Layout.preferredHeight: itemSize - Layout.alignment: Qt.AlignCenter + width: itemSize + height: itemSize visible: modelData IconImage { @@ -146,14 +146,13 @@ Rectangle { function open() { visible = true + PanelService.willOpenPanel(trayPanel) } function close() { visible = false - if (trayMenu.item) { - trayMenu.item.hideMenu() - } + trayMenu.item.hideMenu() } // Clicking outside of the rectangle to close diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 84f8b22..5f70998 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -63,8 +63,8 @@ Item { collapsedIconColor: Color.mOnSurface autoHide: false // Important to be false so we can hover as long as we want text: Math.floor(AudioService.volume * 100) + "%" - tooltipText: "Volume: " + Math.round(AudioService.volume * 100) - + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute." + tooltipText: "Volume: " + Math.round( + AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute." onWheel: function (delta) { wheelAccumulator += delta @@ -85,7 +85,7 @@ Item { AudioService.setMuted(!AudioService.muted) } onMiddleClicked: { - Quickshell.execDetached(["pwvucontrol"]) + Quickshell.execDetached(["pwvucontrol"]); } } } diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 77f8664..0917f36 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -13,8 +13,18 @@ NIconButton { property ShellScreen screen property real scaling: 1.0 + visible: Settings.data.network.wifiEnabled + sizeRatio: 0.8 + Component.onCompleted: { + Logger.log("WiFi", "Widget component completed") + Logger.log("WiFi", "NetworkService available:", !!NetworkService) + if (NetworkService) { + Logger.log("WiFi", "NetworkService.networks available:", !!NetworkService.networks) + } + } + colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface colorBorder: Color.transparent @@ -22,7 +32,7 @@ NIconButton { icon: { try { - if (NetworkService.ethernetConnected) { + if (NetworkService.ethernet) { return "lan" } let connected = false @@ -36,10 +46,10 @@ NIconButton { } return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find" } catch (error) { - Logger.error("Wi-Fi", "Error getting icon:", error) + Logger.error("WiFi", "Error getting icon:", error) return "signal_wifi_bad" } } - tooltipText: "Network / Wi-Fi." + tooltipText: "Network / Wi-Fi" onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this) } diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 051bdea..6c53e65 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -11,7 +11,7 @@ import qs.Services Item { id: root - property ShellScreen screen + property ShellScreen screen: null property real scaling: 1.0 property bool isDestroying: false diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 9a71bba..07056ff 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -32,8 +32,6 @@ Variants { screen: modelData - WlrLayershell.namespace: "noctalia-dock" - property bool autoHide: Settings.data.dock.autoHide property bool hidden: autoHide property int hideDelay: 500 @@ -130,9 +128,9 @@ Variants { Rectangle { id: dockContainer - width: dockLayout.implicitWidth + 48 * scaling + width: dock.width + 48 * scaling height: iconSize * 1.4 * scaling - color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) + color: Color.mSurface anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: dockSpacing @@ -178,7 +176,7 @@ Variants { Item { id: dock - width: dockLayout.implicitWidth + width: runningAppsRow.width height: parent.height - (20 * scaling) anchors.centerIn: parent @@ -194,10 +192,10 @@ Variants { return Icons.iconForAppId(toplevel.appId?.toLowerCase()) } - RowLayout { - id: dockLayout + Row { + id: runningAppsRow spacing: Style.marginL * scaling - Layout.preferredHeight: parent.height + height: parent.height anchors.centerIn: parent Repeater { @@ -205,10 +203,8 @@ Variants { delegate: Rectangle { id: appButton - Layout.preferredWidth: iconSize * scaling - Layout.preferredHeight: iconSize * scaling - Layout.alignment: Qt.AlignCenter - + width: iconSize * scaling + height: iconSize * scaling color: Color.transparent radius: Style.radiusM * scaling diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index 8c541a3..7436829 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -38,8 +38,7 @@ Item { function toggleHistory() { notificationHistoryPanel.toggle(getActiveScreen()) } - function toggleDND() { - Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb + function toggleDoNotDisturb() {// TODO } } diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 0ad25da..4726770 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -243,45 +243,52 @@ NPanel { anchors.margins: Style.marginL * scaling spacing: Style.marginM * scaling - NTextInput { - id: searchInput + Item { + id: searchInputWrap Layout.fillWidth: true + Layout.preferredHeight: Math.round(Style.barHeight * scaling) - fontSize: Style.fontSizeL * scaling - fontWeight: Style.fontWeightSemiBold + NTextInput { + id: searchInput + anchors.fill: parent + inputMaxWidth: Number.MAX_SAFE_INTEGER - text: searchText - placeholderText: "Search entries... or use > for commands" + fontSize: Style.fontSizeL * scaling + fontWeight: Style.fontWeightSemiBold - onTextChanged: searchText = text + text: searchText + placeholderText: "Search entries... or use > for commands" - Component.onCompleted: { - if (searchInput.inputItem && searchInput.inputItem.visible) { - searchInput.inputItem.forceActiveFocus() + onTextChanged: searchText = text - // Override the TextField's default Home/End behavior - searchInput.inputItem.Keys.priority = Keys.BeforeItem - searchInput.inputItem.Keys.onPressed.connect(function (event) { - // Intercept Home and End BEFORE the TextField handles them - if (event.key === Qt.Key_Home) { - ui.selectFirst() - event.accepted = true - return - } else if (event.key === Qt.Key_End) { - ui.selectLast() - event.accepted = true - return - } - }) - searchInput.inputItem.Keys.onDownPressed.connect(function (event) { - ui.selectNext() - }) - searchInput.inputItem.Keys.onUpPressed.connect(function (event) { - ui.selectPrevious() - }) - searchInput.inputItem.Keys.onReturnPressed.connect(function (event) { - ui.activate() - }) + Component.onCompleted: { + if (searchInput.inputItem && searchInput.inputItem.visible) { + searchInput.inputItem.forceActiveFocus() + + // Override the TextField's default Home/End behavior + searchInput.inputItem.Keys.priority = Keys.BeforeItem + searchInput.inputItem.Keys.onPressed.connect(function (event) { + // Intercept Home and End BEFORE the TextField handles them + if (event.key === Qt.Key_Home) { + ui.selectFirst() + event.accepted = true + return + } else if (event.key === Qt.Key_End) { + ui.selectLast() + event.accepted = true + return + } + }) + searchInput.inputItem.Keys.onDownPressed.connect(function (event) { + ui.selectNext() + }) + searchInput.inputItem.Keys.onUpPressed.connect(function (event) { + ui.selectPrevious() + }) + searchInput.inputItem.Keys.onReturnPressed.connect(function (event) { + ui.activate() + }) + } } } } diff --git a/Modules/Launcher/Plugins/ApplicationsPlugin.qml b/Modules/Launcher/Plugins/ApplicationsPlugin.qml index 05f086f..4c02e6b 100644 --- a/Modules/Launcher/Plugins/ApplicationsPlugin.qml +++ b/Modules/Launcher/Plugins/ApplicationsPlugin.qml @@ -82,11 +82,7 @@ Item { "isImage": false, "onActivate": function () { Logger.log("ApplicationsPlugin", `Launching: ${app.name}`) - - if (Settings.data.appLauncher.useApp2Unit && app.id) { - Logger.log("ApplicationsPlugin", `Using app2unit for: ${app.id}`) - Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"]) - } else if (app.execute) { + if (app.execute) { app.execute() } else if (app.exec) { // Fallback to manual execution diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 3382b69..d46dcfd 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -155,7 +155,7 @@ Loader { anchors.topMargin: 80 * scaling spacing: 40 * scaling - ColumnLayout { + Column { spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignHCenter @@ -168,7 +168,6 @@ Loader { font.letterSpacing: -2 * scaling color: Color.mOnSurface horizontalAlignment: Text.AlignHCenter - Layout.alignment: Qt.AlignHCenter SequentialAnimation on scale { loops: Animation.Infinite @@ -193,23 +192,22 @@ Loader { font.weight: Font.Light color: Color.mOnSurface horizontalAlignment: Text.AlignHCenter - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: timeText.implicitWidth + width: timeText.width } } - ColumnLayout { + Column { spacing: Style.marginM * scaling Layout.alignment: Qt.AlignHCenter Rectangle { - Layout.preferredWidth: 108 * scaling - Layout.preferredHeight: 108 * scaling - Layout.alignment: Qt.AlignHCenter + width: 108 * scaling + height: 108 * scaling radius: width * 0.5 color: Color.transparent border.color: Color.mPrimary border.width: Math.max(1, Style.borderL * scaling) + anchors.horizontalCenter: parent.horizontalCenter z: 10 Loader { @@ -377,371 +375,377 @@ Loader { anchors.centerIn: parent anchors.verticalCenterOffset: 50 * scaling - Rectangle { - id: terminalBackground - anchors.fill: parent - radius: Style.radiusM * scaling - color: Qt.alpha(Color.mSurface, 0.9) - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderM * scaling) + Item { + width: parent.width + height: 280 * scaling + Layout.fillWidth: true + + Rectangle { + id: terminalBackground + anchors.fill: parent + radius: Style.radiusM * scaling + color: Qt.alpha(Color.mSurface, 0.9) + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderM * scaling) + + Repeater { + model: 20 + Rectangle { + width: parent.width + height: 1 + color: Qt.alpha(Color.mPrimary, 0.1) + y: index * 10 * scaling + opacity: Style.opacityMedium + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 0.6 + duration: 2000 + Math.random() * 1000 + } + NumberAnimation { + to: 0.1 + duration: 2000 + Math.random() * 1000 + } + } + } + } - Repeater { - model: 20 Rectangle { width: parent.width - height: 1 - color: Qt.alpha(Color.mPrimary, 0.1) - y: index * 10 * scaling - opacity: Style.opacityMedium + height: 40 * scaling + color: Qt.alpha(Color.mPrimary, 0.2) + topLeftRadius: Style.radiusS * scaling + topRightRadius: Style.radiusS * scaling + + RowLayout { + anchors.fill: parent + anchors.topMargin: Style.marginM * scaling + anchors.bottomMargin: Style.marginM * scaling + anchors.leftMargin: Style.marginL * scaling + anchors.rightMargin: Style.marginL * scaling + spacing: Style.marginM * scaling + + NText { + text: "SECURE TERMINAL" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + Layout.fillWidth: true + } + + Row { + spacing: Style.marginS * scaling + visible: batteryIndicator.batteryVisible + NIcon { + text: batteryIndicator.getIcon() + font.pointSize: Style.fontSizeM * scaling + color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface + } + NText { + text: Math.round(batteryIndicator.percent) + "%" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + } + + Row { + spacing: Style.marginS * scaling + NText { + text: keyboardLayout.currentLayout + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + NIcon { + text: "keyboard_alt" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurface + } + } + } + } + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Style.marginL * scaling + anchors.topMargin: 70 * scaling + spacing: Style.marginM * scaling + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: Quickshell.env("USER") + "@noctalia:~$" + color: Color.mPrimary + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + } + + NText { + id: welcomeText + text: "" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + property int currentIndex: 0 + property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" + + Timer { + interval: Style.animationFast + running: true + repeat: true + onTriggered: { + if (parent.currentIndex < parent.fullText.length) { + parent.text = parent.fullText.substring(0, parent.currentIndex + 1) + parent.currentIndex++ + } else { + running = false + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: Quickshell.env("USER") + "@noctalia:~$" + color: Color.mPrimary + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + } + + NText { + text: "sudo unlock-session" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + } + + TextInput { + id: passwordInput + width: 0 + height: 0 + visible: false + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + echoMode: TextInput.Password + passwordCharacter: "*" + passwordMaskDelay: 0 + + text: lockContext.currentText + onTextChanged: { + lockContext.currentText = text + } + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + lockContext.tryUnlock() + } + } + + Component.onCompleted: { + forceActiveFocus() + } + } + + NText { + id: asterisksText + text: "*".repeat(passwordInput.text.length) + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + visible: passwordInput.activeFocus + + SequentialAnimation { + id: typingEffect + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.01 + duration: 50 + } + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.0 + duration: 50 + } + } + } + + Rectangle { + width: 8 * scaling + height: 20 * scaling + color: Color.mPrimary + visible: passwordInput.activeFocus + Layout.leftMargin: -Style.marginS * scaling + Layout.alignment: Qt.AlignVCenter + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 500 + } + NumberAnimation { + to: 0.0 + duration: 500 + } + } + } + } + + NText { + text: { + if (lockContext.unlockInProgress) + return "Authenticating..." + if (lockContext.showFailure && lockContext.errorMessage) + return lockContext.errorMessage + if (lockContext.showFailure) + return "Authentication failed." + return "" + } + color: { + if (lockContext.unlockInProgress) + return Color.mPrimary + if (lockContext.showFailure) + return Color.mError + return Color.transparent + } + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeL * scaling + Layout.fillWidth: true + + SequentialAnimation on opacity { + running: lockContext.unlockInProgress + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 800 + } + NumberAnimation { + to: 0.5 + duration: 800 + } + } + } + + Row { + Layout.alignment: Qt.AlignRight + Layout.bottomMargin: -10 * scaling + Rectangle { + width: 120 * scaling + height: 40 * scaling + radius: Style.radiusS * scaling + color: executeButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.2) + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderS * scaling) + enabled: !lockContext.unlockInProgress + + NText { + anchors.centerIn: parent + text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE" + color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + + MouseArea { + id: executeButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + lockContext.tryUnlock() + } + + SequentialAnimation on scale { + running: executeButtonArea.containsMouse + NumberAnimation { + to: 1.05 + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + SequentialAnimation on scale { + running: !executeButtonArea.containsMouse + NumberAnimation { + to: 1.0 + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + SequentialAnimation on scale { + loops: Animation.Infinite + running: lockContext.unlockInProgress + NumberAnimation { + to: 1.02 + duration: 600 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: 600 + easing.type: Easing.InOutQuad + } + } + } + } + } + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: Color.transparent + border.color: Qt.alpha(Color.mPrimary, 0.3) + border.width: Math.max(1, Style.borderS * scaling) + z: -1 + SequentialAnimation on opacity { loops: Animation.Infinite NumberAnimation { to: 0.6 - duration: 2000 + Math.random() * 1000 + duration: 2000 + easing.type: Easing.InOutQuad } NumberAnimation { - to: 0.1 - duration: 2000 + Math.random() * 1000 + to: 0.2 + duration: 2000 + easing.type: Easing.InOutQuad } } } } - - Rectangle { - width: parent.width - height: 40 * scaling - color: Qt.alpha(Color.mPrimary, 0.2) - topLeftRadius: Style.radiusS * scaling - topRightRadius: Style.radiusS * scaling - - RowLayout { - anchors.fill: parent - anchors.topMargin: Style.marginM * scaling - anchors.bottomMargin: Style.marginM * scaling - anchors.leftMargin: Style.marginL * scaling - anchors.rightMargin: Style.marginL * scaling - spacing: Style.marginM * scaling - - NText { - text: "SECURE TERMINAL" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - Layout.fillWidth: true - } - - RowLayout { - spacing: Style.marginS * scaling - visible: batteryIndicator.batteryVisible - NIcon { - text: batteryIndicator.getIcon() - font.pointSize: Style.fontSizeM * scaling - color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface - } - NText { - text: Math.round(batteryIndicator.percent) + "%" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - } - } - - RowLayout { - spacing: Style.marginS * scaling - NText { - text: keyboardLayout.currentLayout - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - } - NIcon { - text: "keyboard_alt" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurface - } - } - } - } - - ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: Style.marginL * scaling - anchors.topMargin: 70 * scaling - spacing: Style.marginM * scaling - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: Quickshell.env("USER") + "@noctalia:~$" - color: Color.mPrimary - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - } - - NText { - id: welcomeText - text: "" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - property int currentIndex: 0 - property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" - - Timer { - interval: Style.animationFast - running: true - repeat: true - onTriggered: { - if (parent.currentIndex < parent.fullText.length) { - parent.text = parent.fullText.substring(0, parent.currentIndex + 1) - parent.currentIndex++ - } else { - running = false - } - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: Quickshell.env("USER") + "@noctalia:~$" - color: Color.mPrimary - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - } - - NText { - text: "sudo unlock-session" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - } - - TextInput { - id: passwordInput - width: 0 - height: 0 - visible: false - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface - echoMode: TextInput.Password - passwordCharacter: "*" - passwordMaskDelay: 0 - - text: lockContext.currentText - onTextChanged: { - lockContext.currentText = text - } - - Keys.onPressed: function (event) { - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - lockContext.tryUnlock() - } - } - - Component.onCompleted: { - forceActiveFocus() - } - } - - NText { - id: asterisksText - text: "*".repeat(passwordInput.text.length) - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - visible: passwordInput.activeFocus - - SequentialAnimation { - id: typingEffect - NumberAnimation { - target: passwordInput - property: "scale" - to: 1.01 - duration: 50 - } - NumberAnimation { - target: passwordInput - property: "scale" - to: 1.0 - duration: 50 - } - } - } - - Rectangle { - width: 8 * scaling - height: 20 * scaling - color: Color.mPrimary - visible: passwordInput.activeFocus - Layout.leftMargin: -Style.marginS * scaling - Layout.alignment: Qt.AlignVCenter - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 500 - } - NumberAnimation { - to: 0.0 - duration: 500 - } - } - } - } - - NText { - text: { - if (lockContext.unlockInProgress) - return "Authenticating..." - if (lockContext.showFailure && lockContext.errorMessage) - return lockContext.errorMessage - if (lockContext.showFailure) - return "Authentication failed." - return "" - } - color: { - if (lockContext.unlockInProgress) - return Color.mPrimary - if (lockContext.showFailure) - return Color.mError - return Color.transparent - } - font.family: "DejaVu Sans Mono" - font.pointSize: Style.fontSizeL * scaling - Layout.fillWidth: true - - SequentialAnimation on opacity { - running: lockContext.unlockInProgress - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 800 - } - NumberAnimation { - to: 0.5 - duration: 800 - } - } - } - - RowLayout { - Layout.alignment: Qt.AlignRight - Layout.bottomMargin: -10 * scaling - Rectangle { - Layout.preferredWidth: 120 * scaling - Layout.preferredHeight: 40 * scaling - radius: Style.radiusS * scaling - color: executeButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.2) - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderS * scaling) - enabled: !lockContext.unlockInProgress - - NText { - anchors.centerIn: parent - text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE" - color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - } - - MouseArea { - id: executeButtonArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - lockContext.tryUnlock() - } - - SequentialAnimation on scale { - running: executeButtonArea.containsMouse - NumberAnimation { - to: 1.05 - duration: Style.animationFast - easing.type: Easing.OutCubic - } - } - - SequentialAnimation on scale { - running: !executeButtonArea.containsMouse - NumberAnimation { - to: 1.0 - duration: Style.animationFast - easing.type: Easing.OutCubic - } - } - } - - SequentialAnimation on scale { - loops: Animation.Infinite - running: lockContext.unlockInProgress - NumberAnimation { - to: 1.02 - duration: 600 - easing.type: Easing.InOutQuad - } - NumberAnimation { - to: 1.0 - duration: 600 - easing.type: Easing.InOutQuad - } - } - } - } - } - - Rectangle { - anchors.fill: parent - radius: parent.radius - color: Color.transparent - border.color: Qt.alpha(Color.mPrimary, 0.3) - border.width: Math.max(1, Style.borderS * scaling) - z: -1 - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { - to: 0.6 - duration: 2000 - easing.type: Easing.InOutQuad - } - NumberAnimation { - to: 0.2 - duration: 2000 - easing.type: Easing.InOutQuad - } - } - } } } - // Power buttons at bottom right - RowLayout { + // Power buttons at bottom + Row { anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: 50 * scaling spacing: 20 * scaling Rectangle { - Layout.preferredWidth: 60 * scaling - Layout.preferredHeight: 60 * scaling + width: 60 * scaling + height: 60 * scaling radius: width * 0.5 color: powerButtonArea.containsMouse ? Color.mError : Qt.alpha(Color.mError, 0.2) border.color: Color.mError @@ -765,8 +769,8 @@ Loader { } Rectangle { - Layout.preferredWidth: 60 * scaling - Layout.preferredHeight: 60 * scaling + width: 60 * scaling + height: 60 * scaling radius: width * 0.5 color: restartButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, Style.opacityLight) border.color: Color.mPrimary @@ -790,8 +794,8 @@ Loader { } Rectangle { - Layout.preferredWidth: 60 * scaling - Layout.preferredHeight: 60 * scaling + width: 60 * scaling + height: 60 * scaling radius: width * 0.5 color: suspendButtonArea.containsMouse ? Color.mSecondary : Qt.alpha(Color.mSecondary, 0.2) border.color: Color.mSecondary diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index fdbe0d2..009d717 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -78,7 +78,7 @@ Variants { } // Main notification container - ColumnLayout { + Column { id: notificationStack // Position based on bar location anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined @@ -92,9 +92,8 @@ Variants { Repeater { model: notificationModel delegate: Rectangle { - Layout.preferredWidth: 360 * scaling - Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling) - Layout.maximumHeight: Layout.preferredHeight + width: 360 * scaling + height: Math.max(80 * scaling, contentRow.implicitHeight + (Style.marginL * 2 * scaling)) clip: true radius: Style.radiusL * scaling border.color: Color.mOutline @@ -106,17 +105,6 @@ Variants { property real opacityValue: 0.0 property bool isRemoving: false - // Right-click to dismiss - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.RightButton - onClicked: { - if (mouse.button === Qt.RightButton) { - animateOut() - } - } - } - // Scale and fade-in animation scale: scaleValue opacity: opacityValue @@ -168,139 +156,104 @@ Variants { } } - ColumnLayout { - id: notificationLayout + RowLayout { + id: contentRow anchors.fill: parent - anchors.margins: Style.marginM * scaling - anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button - spacing: Style.marginM * scaling + anchors.margins: Style.marginL * scaling + spacing: Style.marginL * scaling - // Header section with app name and timestamp - RowLayout { - Layout.fillWidth: true + // Right: header on top, then avatar + texts + ColumnLayout { + id: textColumn spacing: Style.marginS * scaling - - NText { - text: `${(model.appName || model.desktopEntry) - || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}` - color: Color.mSecondary - font.pointSize: Style.fontSizeXS * scaling - } - - Rectangle { - Layout.preferredWidth: 6 * scaling - Layout.preferredHeight: 6 * scaling - radius: Style.radiusXS * scaling - color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary - Layout.alignment: Qt.AlignVCenter - } - - Item { - Layout.fillWidth: true - } - } - - // Main content section - RowLayout { Layout.fillWidth: true - spacing: Style.marginM * scaling - // Avatar - NImageCircled { - id: appAvatar - Layout.preferredWidth: 40 * scaling - Layout.preferredHeight: 40 * scaling - Layout.alignment: Qt.AlignTop - imagePath: model.image && model.image !== "" ? model.image : "" - fallbackIcon: "" - borderColor: Color.transparent - borderWidth: 0 - visible: (model.image && model.image !== "") - } - - // Text content - ColumnLayout { - Layout.fillWidth: true + RowLayout { spacing: Style.marginS * scaling - + id: appHeaderRow NText { - text: model.summary || "No summary" - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightMedium - color: Color.mOnSurface - wrapMode: Text.WrapAtWordBoundaryOrAnywhere + text: `${(model.appName || model.desktopEntry) + || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}` + color: Color.mSecondary + font.pointSize: Style.fontSizeXS * scaling + } + Rectangle { + width: 6 * scaling + height: 6 * scaling + radius: Style.radiusXS * scaling + color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary + Layout.alignment: Qt.AlignVCenter + } + Item { Layout.fillWidth: true - maximumLineCount: 3 - elide: Text.ElideRight + } + } + + RowLayout { + id: bodyRow + spacing: Style.marginM * scaling + + NImageCircled { + id: appAvatar + Layout.preferredWidth: 40 * scaling + Layout.preferredHeight: 40 * scaling + Layout.alignment: Qt.AlignTop + // Start avatar aligned with body (below the summary) + anchors.topMargin: textContent.childrenRect.y + // Prefer notification-provided image (e.g., user avatar) then fall back to app icon + imagePath: (model.image && model.image !== "") ? model.image : Icons.iconFromName( + model.appIcon, "application-x-executable") + fallbackIcon: "apps" + borderColor: Color.transparent + borderWidth: 0 + visible: (imagePath && imagePath !== "") } - NText { - text: model.body || "" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurface - wrapMode: Text.WrapAtWordBoundaryOrAnywhere + Column { + id: textContent + spacing: Style.marginS * scaling Layout.fillWidth: true - maximumLineCount: 5 - elide: Text.ElideRight - visible: text.length > 0 + // Ensure a concrete width so text wraps + width: (textColumn.width - (appAvatar.visible ? (appAvatar.width + Style.marginM * scaling) : 0)) + + NText { + text: model.summary || "No summary" + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + Layout.fillWidth: true + width: parent.width + maximumLineCount: 3 + elide: Text.ElideRight + } + + NText { + text: model.body || "" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurface + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + Layout.fillWidth: true + width: parent.width + maximumLineCount: 5 + elide: Text.ElideRight + } } } } - // Notification actions - RowLayout { - Layout.fillWidth: true - spacing: Style.marginS * scaling - visible: model.rawNotification && model.rawNotification.actions - && model.rawNotification.actions.length > 0 - - property var notificationActions: model.rawNotification ? model.rawNotification.actions : [] - - Repeater { - model: parent.notificationActions - - delegate: NButton { - text: { - var actionText = modelData.text || "Open" - // If text contains comma, take the part after the comma (the display text) - if (actionText.includes(",")) { - return actionText.split(",")[1] || actionText - } - return actionText - } - fontSize: Style.fontSizeS * scaling - backgroundColor: Color.mPrimary - textColor: Color.mOnPrimary - hoverColor: Color.mSecondary - pressColor: Color.mTertiary - outlined: false - customHeight: 32 * scaling - Layout.preferredHeight: 32 * scaling - - onClicked: { - if (modelData && modelData.invoke) { - modelData.invoke() - } - } - } - } - - // Spacer to push buttons to the left if needed - Item { - Layout.fillWidth: true - } - } + // Actions removed } - // Close button positioned absolutely NIconButton { icon: "close" tooltipText: "Close" - sizeRatio: 0.6 + // Compact target (~24dp) and glyph (~16dp) + sizeRatio: 0.75 + fontPointSize: 16 anchors.top: parent.top - anchors.topMargin: Style.marginM * scaling anchors.right: parent.right - anchors.rightMargin: Style.marginM * scaling + anchors.margins: Style.marginS * scaling onClicked: { animateOut() diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 39686df..3b10aec 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -25,7 +25,6 @@ NPanel { anchors.margins: Style.marginL * scaling spacing: Style.marginM * scaling - // Header section RowLayout { Layout.fillWidth: true spacing: Style.marginM * scaling @@ -44,13 +43,6 @@ NPanel { Layout.fillWidth: true } - NIconButton { - icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications_active" - tooltipText: Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' is enabled." : "'Do Not Disturb' is disabled." - sizeRatio: 0.8 - onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb - } - NIconButton { icon: "delete" tooltipText: "Clear history" @@ -73,44 +65,38 @@ NPanel { } // Empty state when no notifications - ColumnLayout { + Item { Layout.fillWidth: true Layout.fillHeight: true - Layout.alignment: Qt.AlignHCenter visible: NotificationService.historyModel.count === 0 - spacing: Style.marginL * scaling - Item { - Layout.fillHeight: true - } + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginM * scaling - NIcon { - text: "notifications_off" - font.pointSize: 64 * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } + NIcon { + text: "notifications_off" + font.pointSize: Style.fontSizeXXXL * scaling + color: Color.mOnSurface + Layout.alignment: Qt.AlignHCenter + } - NText { - text: "No notifications" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } + NText { + text: "No notifications" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + Layout.alignment: Qt.AlignHCenter + } - NText { - text: "Your notifications will show up here as they arrive." - font.pointSize: Style.fontSizeS * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - Item { - Layout.fillHeight: true + NText { + text: "Your notifications will show up here as they arrive." + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } } } - // Notification list ListView { id: notificationList Layout.fillWidth: true @@ -122,21 +108,21 @@ NPanel { visible: NotificationService.historyModel.count > 0 delegate: Rectangle { - width: notificationList.width - height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2) + width: notificationList ? notificationList.width : 380 * scaling + height: Math.max(80, notificationContent.height + 30) radius: Style.radiusM * scaling color: notificationMouseArea.containsMouse ? Color.mSecondary : Color.mSurfaceVariant - border.color: Qt.alpha(Color.mOutline, Style.opacityMedium) - border.width: Math.max(1, Style.borderS * scaling) RowLayout { - id: notificationLayout - anchors.fill: parent - anchors.margins: Style.marginM * scaling + anchors { + fill: parent + margins: Style.marginM * scaling + } spacing: Style.marginM * scaling - // Notification content column - ColumnLayout { + // Notification content + Column { + id: notificationContent Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter spacing: Style.marginXXS * scaling @@ -147,8 +133,7 @@ NPanel { font.weight: Font.Medium color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary wrapMode: Text.Wrap - Layout.fillWidth: true - Layout.maximumWidth: parent.width + width: parent.width - 60 maximumLineCount: 2 elide: Text.ElideRight } @@ -158,27 +143,23 @@ NPanel { font.pointSize: Style.fontSizeXS * scaling color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface wrapMode: Text.Wrap - Layout.fillWidth: true - Layout.maximumWidth: parent.width + width: parent.width - 60 maximumLineCount: 3 elide: Text.ElideRight - visible: text.length > 0 } NText { text: NotificationService.formatTimestamp(timestamp) font.pointSize: Style.fontSizeXS * scaling color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface - Layout.fillWidth: true } } - // Delete button + // Trash icon button NIconButton { icon: "delete" tooltipText: "Delete notification" sizeRatio: 0.7 - Layout.alignment: Qt.AlignTop onClicked: { Logger.log("NotificationHistory", "Removing notification:", summary) @@ -191,7 +172,7 @@ NPanel { MouseArea { id: notificationMouseArea anchors.fill: parent - anchors.rightMargin: Style.marginXL * scaling + anchors.rightMargin: Style.marginL * 3 * scaling hoverEnabled: true } } diff --git a/Modules/PowerPanel/PowerPanel.qml b/Modules/PowerPanel/PowerPanel.qml index efb475e..e6cfb01 100644 --- a/Modules/PowerPanel/PowerPanel.qml +++ b/Modules/PowerPanel/PowerPanel.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Controls import QtQuick.Effects import QtQuick.Layouts import Quickshell @@ -17,7 +16,6 @@ NPanel { panelHeight: 380 * scaling panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true - panelKeyboardFocus: true // Timer properties property int timerDuration: 9000 // 9 seconds @@ -25,44 +23,9 @@ NPanel { property bool timerActive: false property int timeRemaining: 0 - // Navigation properties - property int selectedIndex: 0 - readonly property var powerOptions: [{ - "action": "lock", - "icon": "lock_outline", - "title": "Lock", - "subtitle": "Lock your session" - }, { - "action": "suspend", - "icon": "bedtime", - "title": "Suspend", - "subtitle": "Put the system to sleep" - }, { - "action": "reboot", - "icon": "refresh", - "title": "Reboot", - "subtitle": "Restart the system" - }, { - "action": "logout", - "icon": "exit_to_app", - "title": "Logout", - "subtitle": "End your session" - }, { - "action": "shutdown", - "icon": "power_settings_new", - "title": "Shutdown", - "subtitle": "Turn off the system", - "isShutdown": true - }] - - // Lifecycle handlers - onOpened: { - selectedIndex = 0 - } - + // Cancel timer when panel is closing onClosed: { cancelTimer() - selectedIndex = 0 } // Timer management @@ -116,38 +79,6 @@ NPanel { root.close() } - // Navigation functions - function selectNext() { - if (powerOptions.length > 0) { - selectedIndex = Math.min(selectedIndex + 1, powerOptions.length - 1) - } - } - - function selectPrevious() { - if (powerOptions.length > 0) { - selectedIndex = Math.max(selectedIndex - 1, 0) - } - } - - function selectFirst() { - selectedIndex = 0 - } - - function selectLast() { - if (powerOptions.length > 0) { - selectedIndex = powerOptions.length - 1 - } else { - selectedIndex = 0 - } - } - - function activate() { - if (powerOptions.length > 0 && powerOptions[selectedIndex]) { - const option = powerOptions[selectedIndex] - startTimer(option.action) - } - } - // Countdown timer Timer { id: countdownTimer @@ -162,92 +93,8 @@ NPanel { } panelContent: Rectangle { - id: ui color: Color.transparent - // Keyboard shortcuts - Shortcut { - sequence: "Ctrl+K" - onActivated: ui.selectPrevious() - enabled: root.opened - } - - Shortcut { - sequence: "Ctrl+J" - onActivated: ui.selectNext() - enabled: root.opened - } - - Shortcut { - sequence: "Up" - onActivated: ui.selectPrevious() - enabled: root.opened - } - - Shortcut { - sequence: "Down" - onActivated: ui.selectNext() - enabled: root.opened - } - - Shortcut { - sequence: "Home" - onActivated: ui.selectFirst() - enabled: root.opened - } - - Shortcut { - sequence: "End" - onActivated: ui.selectLast() - enabled: root.opened - } - - Shortcut { - sequence: "Return" - onActivated: ui.activate() - enabled: root.opened - } - - Shortcut { - sequence: "Enter" - onActivated: ui.activate() - enabled: root.opened - } - - Shortcut { - sequence: "Escape" - onActivated: { - if (timerActive) { - cancelTimer() - } else { - cancelTimer() - root.close() - } - } - enabled: root.opened - } - - // Navigation functions - function selectNext() { - root.selectNext() - } - - function selectPrevious() { - root.selectPrevious() - } - - function selectFirst() { - root.selectFirst() - } - - function selectLast() { - root.selectLast() - } - - function activate() { - root.activate() - } - ColumnLayout { anchors.fill: parent anchors.topMargin: Style.marginL * scaling @@ -297,21 +144,55 @@ NPanel { Layout.fillWidth: true spacing: Style.marginM * scaling - Repeater { - model: powerOptions - delegate: PowerButton { - Layout.fillWidth: true - icon: modelData.icon - title: modelData.title - subtitle: modelData.subtitle - isShutdown: modelData.isShutdown || false - isSelected: index === selectedIndex - onClicked: { - selectedIndex = index - startTimer(modelData.action) - } - pending: timerActive && pendingAction === modelData.action - } + // Lock Screen + PowerButton { + Layout.fillWidth: true + icon: "lock_outline" + title: "Lock" + subtitle: "Lock your session" + onClicked: startTimer("lock") + pending: timerActive && pendingAction === "lock" + } + + // Suspend + PowerButton { + Layout.fillWidth: true + icon: "bedtime" + title: "Suspend" + subtitle: "Put the system to sleep" + onClicked: startTimer("suspend") + pending: timerActive && pendingAction === "suspend" + } + + // Reboot + PowerButton { + Layout.fillWidth: true + icon: "refresh" + title: "Reboot" + subtitle: "Restart the system" + onClicked: startTimer("reboot") + pending: timerActive && pendingAction === "reboot" + } + + // Logout + PowerButton { + Layout.fillWidth: true + icon: "exit_to_app" + title: "Logout" + subtitle: "End your session" + onClicked: startTimer("logout") + pending: timerActive && pendingAction === "logout" + } + + // Shutdown + PowerButton { + Layout.fillWidth: true + icon: "power_settings_new" + title: "Shutdown" + subtitle: "Turn off the system" + onClicked: startTimer("shutdown") + pending: timerActive && pendingAction === "shutdown" + isShutdown: true } } } @@ -326,7 +207,6 @@ NPanel { property string subtitle: "" property bool pending: false property bool isShutdown: false - property bool isSelected: false signal clicked @@ -336,7 +216,7 @@ NPanel { if (pending) { return Qt.alpha(Color.mPrimary, 0.08) } - if (isSelected || mouseArea.containsMouse) { + if (mouseArea.containsMouse) { return Color.mSecondary } return Color.transparent @@ -362,12 +242,13 @@ NPanel { anchors.verticalCenter: parent.verticalCenter text: buttonRoot.icon color: { + if (buttonRoot.pending) return Color.mPrimary - if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) + if (buttonRoot.isShutdown && !mouseArea.containsMouse) return Color.mError - if (buttonRoot.isSelected || mouseArea.containsMouse) - return Color.mOnSecondary + if (mouseArea.containsMouse) + return Color.mOnTertiary return Color.mOnSurface } font.pointSize: Style.fontSizeXXXL * scaling @@ -383,7 +264,7 @@ NPanel { } // Text content in the middle - ColumnLayout { + Column { anchors.left: iconElement.right anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right anchors.verticalCenter: parent.verticalCenter @@ -398,10 +279,10 @@ NPanel { color: { if (buttonRoot.pending) return Color.mPrimary - if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) + if (buttonRoot.isShutdown && !mouseArea.containsMouse) return Color.mError - if (buttonRoot.isSelected || mouseArea.containsMouse) - return Color.mOnSecondary + if (mouseArea.containsMouse) + return Color.mOnTertiary return Color.mOnSurface } @@ -423,10 +304,10 @@ NPanel { color: { if (buttonRoot.pending) return Color.mPrimary - if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) + if (buttonRoot.isShutdown && !mouseArea.containsMouse) return Color.mError - if (buttonRoot.isSelected || mouseArea.containsMouse) - return Color.mOnSecondary + if (mouseArea.containsMouse) + return Color.mOnTertiary return Color.mOnSurfaceVariant } opacity: Style.opacityHeavy diff --git a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml index f9aa98d..cccacbb 100644 --- a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml +++ b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml @@ -68,8 +68,6 @@ Popup { sourceComponent: { if (settingsPopup.widgetId === "CustomButton") { return customButtonSettings - } else if (settingsPopup.widgetId === "Spacer") { - return spacerSettings } // Add more widget settings components here as needed return null @@ -159,28 +157,4 @@ Popup { } } } - - // Spacer settings component - Component { - id: spacerSettings - - ColumnLayout { - spacing: Style.marginM * scaling - - function saveSettings() { - var settings = Object.assign({}, settingsPopup.widgetData) - settings.width = parseInt(widthInput.text) || 20 - return settings - } - - NTextInput { - id: widthInput - Layout.fillWidth: true - label: "Width (pixels)" - description: "Width of the spacer in pixels." - text: settingsPopup.widgetData.width || "20" - placeholderText: "Enter width in pixels" - } - } - } } diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 1e6d6cc..2e1e4aa 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -267,269 +267,234 @@ NPanel { } panelContent: Rectangle { + anchors.fill: parent + anchors.margins: Style.marginL * scaling color: Color.transparent - // Main layout container that fills the panel - ColumnLayout { + // Scrolling via keyboard + Shortcut { + sequence: "Down" + onActivated: root.scrollDown() + enabled: root.opened + } + + Shortcut { + sequence: "Up" + onActivated: root.scrollUp() + enabled: root.opened + } + + Shortcut { + sequence: "Ctrl+J" + onActivated: root.scrollDown() + enabled: root.opened + } + + Shortcut { + sequence: "Ctrl+K" + onActivated: root.scrollUp() + enabled: root.opened + } + + Shortcut { + sequence: "PgDown" + onActivated: root.scrollPageDown() + enabled: root.opened + } + + Shortcut { + sequence: "PgUp" + onActivated: root.scrollPageUp() + enabled: root.opened + } + + // Changing tab via keyboard + Shortcut { + sequence: "Tab" + onActivated: root.selectNextTab() + enabled: root.opened + } + + Shortcut { + sequence: "Shift+Tab" + onActivated: root.selectPreviousTab() + enabled: root.opened + } + + RowLayout { anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: 0 + spacing: Style.marginM * scaling - // Keyboard shortcuts container - Item { - Layout.preferredWidth: 0 - Layout.preferredHeight: 0 + Rectangle { + id: sidebar + Layout.preferredWidth: 220 * scaling + Layout.fillHeight: true + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + radius: Style.radiusM * scaling - // Scrolling via keyboard - Shortcut { - sequence: "Down" - onActivated: root.scrollDown() - enabled: root.opened + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton // Don't interfere with clicks + property int wheelAccumulator: 0 + onWheel: wheel => { + wheelAccumulator += wheel.angleDelta.y + if (wheelAccumulator >= 120) { + root.selectPreviousTab() + wheelAccumulator = 0 + } else if (wheelAccumulator <= -120) { + root.selectNextTab() + wheelAccumulator = 0 + } + wheel.accepted = true + } } - Shortcut { - sequence: "Up" - onActivated: root.scrollUp() - enabled: root.opened - } + Column { + anchors.fill: parent + anchors.margins: Style.marginS * scaling + spacing: Style.marginXS * 1.5 * scaling - Shortcut { - sequence: "Ctrl+J" - onActivated: root.scrollDown() - enabled: root.opened - } + Repeater { + id: sections + model: root.tabsModel + delegate: Rectangle { + id: tabItem + width: parent.width + height: 32 * scaling + radius: Style.radiusS * scaling + color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent) + readonly property bool selected: index === currentTabIndex + property bool hovering: false + property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface) - Shortcut { - sequence: "Ctrl+K" - onActivated: root.scrollUp() - enabled: root.opened - } + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } - Shortcut { - sequence: "PgDown" - onActivated: root.scrollPageDown() - enabled: root.opened - } + Behavior on tabTextColor { + ColorAnimation { + duration: Style.animationFast + } + } - Shortcut { - sequence: "PgUp" - onActivated: root.scrollPageUp() - enabled: root.opened - } - - // Changing tab via keyboard - Shortcut { - sequence: "Tab" - onActivated: root.selectNextTab() - enabled: root.opened - } - - Shortcut { - sequence: "Shift+Tab" - onActivated: root.selectPreviousTab() - enabled: root.opened + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginS * scaling + anchors.rightMargin: Style.marginS * scaling + spacing: Style.marginS * scaling + // Tab icon on the left side + NIcon { + text: modelData.icon + color: tabTextColor + font.pointSize: Style.fontSizeL * scaling + } + // Tab label on the left side + NText { + text: modelData.label + color: tabTextColor + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + Layout.fillWidth: true + } + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton + onEntered: tabItem.hovering = true + onExited: tabItem.hovering = false + onCanceled: tabItem.hovering = false + onClicked: currentTabIndex = index + } + } + } } } - // Main content area - RowLayout { + // Content + Rectangle { + id: contentPane Layout.fillWidth: true Layout.fillHeight: true - spacing: Style.marginM * scaling + radius: Style.radiusM * scaling + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + clip: true - // Sidebar - Rectangle { - id: sidebar - Layout.preferredWidth: 220 * scaling - Layout.fillHeight: true - Layout.alignment: Qt.AlignTop - color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - radius: Style.radiusM * scaling + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginS * scaling - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton // Don't interfere with clicks - property int wheelAccumulator: 0 - onWheel: wheel => { - wheelAccumulator += wheel.angleDelta.y - if (wheelAccumulator >= 120) { - root.selectPreviousTab() - wheelAccumulator = 0 - } else if (wheelAccumulator <= -120) { - root.selectNextTab() - wheelAccumulator = 0 - } - wheel.accepted = true - } - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginXS * 1.5 * scaling - - Repeater { - id: sections - model: root.tabsModel - delegate: Rectangle { - id: tabItem - Layout.fillWidth: true - Layout.preferredHeight: tabEntryRow.implicitHeight + Style.marginS * scaling * 2 - radius: Style.radiusS * scaling - color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent) - readonly property bool selected: index === currentTabIndex - property bool hovering: false - property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface) - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - - Behavior on tabTextColor { - ColorAnimation { - duration: Style.animationFast - } - } - - RowLayout { - id: tabEntryRow - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginS * scaling - - // Tab icon - NIcon { - text: modelData.icon - color: tabTextColor - font.pointSize: Style.fontSizeL * scaling - } - - // Tab label - NText { - text: modelData.label - color: tabTextColor - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - Layout.fillWidth: true - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton - onEntered: tabItem.hovering = true - onExited: tabItem.hovering = false - onCanceled: tabItem.hovering = false - onClicked: currentTabIndex = index - } - } - } - - Item { - Layout.fillHeight: true - } - } - } - - // Content pane - Rectangle { - id: contentPane - Layout.fillWidth: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignTop - radius: Style.radiusM * scaling - color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - clip: true - - ColumnLayout { - id: contentLayout - anchors.fill: parent - anchors.margins: Style.marginL * scaling + RowLayout { + id: headerRow + Layout.fillWidth: true spacing: Style.marginS * scaling - // Header row - RowLayout { - id: headerRow - Layout.fillWidth: true - spacing: Style.marginS * scaling - - // Tab title - NText { - text: root.tabsModel[currentTabIndex]?.label || "" - font.pointSize: Style.fontSizeXL * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - } - - // Close button - NIconButton { - icon: "close" - tooltipText: "Close" - Layout.alignment: Qt.AlignVCenter - onClicked: root.close() - } - } - - // Divider - NDivider { + // Tab label on the main right side + NText { + text: root.tabsModel[currentTabIndex].label + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary Layout.fillWidth: true } + NIconButton { + icon: "close" + tooltipText: "Close" + Layout.alignment: Qt.AlignVCenter + onClicked: root.close() + } + } - // Tab content area - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - color: Color.transparent + NDivider { + Layout.fillWidth: true + } - Repeater { - model: root.tabsModel - delegate: Loader { - anchors.fill: parent - active: index === root.currentTabIndex + Item { + Layout.fillWidth: true + Layout.fillHeight: true - onStatusChanged: { - if (status === Loader.Ready && item) { - // Find and store reference to the ScrollView - const scrollView = item.children[0] - if (scrollView && scrollView.toString().includes("ScrollView")) { - root.activeScrollView = scrollView - } + Repeater { + model: root.tabsModel + delegate: Loader { + anchors.fill: parent + active: index === root.currentTabIndex + + onStatusChanged: { + if (status === Loader.Ready && item) { + // Find and store reference to the ScrollView + const scrollView = item.children[0] + if (scrollView && scrollView.toString().includes("ScrollView")) { + root.activeScrollView = scrollView } } + } - sourceComponent: Flickable { - // Using a Flickable here with a pressDelay to fix conflict between - // ScrollView and NTextInput. This fixes the weird text selection issue. - id: flickable - anchors.fill: parent - pressDelay: 200 + sourceComponent: ColumnLayout { + ScrollView { + id: scrollView + Layout.fillWidth: true + Layout.fillHeight: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + padding: Style.marginL * scaling + clip: true - ScrollView { - id: scrollView - anchors.fill: parent - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - padding: Style.marginL * scaling - clip: true + Component.onCompleted: { + root.activeScrollView = scrollView + } - Component.onCompleted: { - root.activeScrollView = scrollView - } - - Loader { - active: true - sourceComponent: root.tabsModel[index]?.source - width: scrollView.availableWidth - } + Loader { + active: true + sourceComponent: root.tabsModel[index].source + width: scrollView.availableWidth } } } diff --git a/Modules/SettingsPanel/Tabs/AboutTab.qml b/Modules/SettingsPanel/Tabs/AboutTab.qml index 1fffadb..a4cb10e 100644 --- a/Modules/SettingsPanel/Tabs/AboutTab.qml +++ b/Modules/SettingsPanel/Tabs/AboutTab.qml @@ -60,7 +60,7 @@ ColumnLayout { Rectangle { Layout.alignment: Qt.AlignCenter Layout.topMargin: Style.marginS * scaling - Layout.preferredWidth: Math.round(updateRow.implicitWidth + (Style.marginL * scaling * 2)) + Layout.preferredWidth: updateText.implicitWidth + 46 * scaling Layout.preferredHeight: Math.round(Style.barHeight * scaling) radius: Style.radiusL * scaling color: updateArea.containsMouse ? Color.mPrimary : Color.transparent @@ -85,12 +85,11 @@ ColumnLayout { } RowLayout { - id: updateRow anchors.centerIn: parent spacing: Style.marginS * scaling NIcon { - text: "download" + text: "system_update" font.pointSize: Style.fontSizeXXL * scaling color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary } diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index 7407112..c0d5f54 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -22,11 +22,9 @@ ColumnLayout { fallbackIcon: "person" borderColor: Color.mPrimary borderWidth: Math.max(1, Style.borderM * scaling) - Layout.alignment: Qt.AlignTop } NTextInput { - Layout.fillWidth: true label: `${Quickshell.env("USER") || "user"}'s profile picture` description: "Your profile picture that appears throughout the interface." text: Settings.data.general.avatarImage @@ -77,45 +75,6 @@ ColumnLayout { onToggled: checked => Settings.data.dock.autoHide = checked } - ColumnLayout { - spacing: Style.marginXXS * scaling - Layout.fillWidth: true - - NText { - text: "Dock Background Opacity" - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSurface - } - - NText { - text: "Adjust the background opacity of the dock." - font.pointSize: Style.fontSizeXS * scaling - color: Color.mOnSurfaceVariant - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - RowLayout { - NSlider { - Layout.fillWidth: true - from: 0 - to: 1 - stepSize: 0.01 - value: Settings.data.dock.backgroundOpacity - onMoved: Settings.data.dock.backgroundOpacity = value - cutoutColor: Color.mSurface - } - - NText { - text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%" - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: Style.marginS * scaling - color: Color.mOnSurface - } - } - } - ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true diff --git a/Modules/SettingsPanel/Tabs/HooksTab.qml b/Modules/SettingsPanel/Tabs/HooksTab.qml index 461a4b8..195844c 100644 --- a/Modules/SettingsPanel/Tabs/HooksTab.qml +++ b/Modules/SettingsPanel/Tabs/HooksTab.qml @@ -5,85 +5,94 @@ import qs.Commons import qs.Services import qs.Widgets -ColumnLayout { - id: contentColumn - spacing: Style.marginL * scaling - width: root.width +ScrollView { + id: root - // Enable/Disable Toggle - NToggle { - label: "Enable Hooks" - description: "Enable or disable all hook commands." - checked: Settings.data.hooks.enabled - onToggled: checked => Settings.data.hooks.enabled = checked - } + property real scaling: 1.0 + + contentWidth: contentColumn.width + contentHeight: contentColumn.height ColumnLayout { - visible: Settings.data.hooks.enabled + id: contentColumn spacing: Style.marginL * scaling - Layout.fillWidth: true + width: root.width - NDivider { - Layout.fillWidth: true + // Enable/Disable Toggle + NToggle { + label: "Enable Hooks" + description: "Enable or disable all hook commands." + checked: Settings.data.hooks.enabled + onToggled: checked => Settings.data.hooks.enabled = checked } - // Wallpaper Hook Section - NInputAction { - id: wallpaperHookInput - label: "Wallpaper Change Hook" - description: "Command to be executed when wallpaper changes." - placeholderText: "e.g., notify-send \"Wallpaper\" \"Changed\"" - text: Settings.data.hooks.wallpaperChange - onEditingFinished: { - Settings.data.hooks.wallpaperChange = wallpaperHookInput.text - } - onActionClicked: { - if (wallpaperHookInput.text) { - HooksService.executeWallpaperHook("test", "test-screen") - } - } - Layout.fillWidth: true - } - - NDivider { - Layout.fillWidth: true - } - - // Dark Mode Hook Section - NInputAction { - id: darkModeHookInput - label: "Theme Toggle Hook" - description: "Command to be executed when theme toggles between dark and light mode." - placeholderText: "e.g., notify-send \"Theme\" \"Toggled\"" - text: Settings.data.hooks.darkModeChange - onEditingFinished: { - Settings.data.hooks.darkModeChange = darkModeHookInput.text - } - onActionClicked: { - if (darkModeHookInput.text) { - HooksService.executeDarkModeHook(Settings.data.colorSchemes.darkMode) - } - } - Layout.fillWidth: true - } - - NDivider { - Layout.fillWidth: true - } - - // Info section ColumnLayout { - spacing: Style.marginM * scaling + visible: Settings.data.hooks.enabled + spacing: Style.marginL * scaling Layout.fillWidth: true - NLabel { - label: "Hook Command Information" - description: "• Commands are executed via shell (sh -c)\n• Commands run in background (detached)\n• Test buttons execute with current values" + NDivider { + Layout.fillWidth: true } - NLabel { - label: "Available Parameters" - description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)" + // Wallpaper Hook Section + NInputAction { + id: wallpaperHookInput + label: "Wallpaper Change Hook" + description: "Command to be executed when wallpaper changes." + placeholderText: "e.g., notify-send \"Wallpaper\" \"Changed\"" + text: Settings.data.hooks.wallpaperChange + onEditingFinished: { + Settings.data.hooks.wallpaperChange = wallpaperHookInput.text + } + onActionClicked: { + if (wallpaperHookInput.text) { + HooksService.executeWallpaperHook("test", "test-screen") + } + } + Layout.fillWidth: true + } + + NDivider { + Layout.fillWidth: true + } + + // Dark Mode Hook Section + NInputAction { + id: darkModeHookInput + label: "Theme Toggle Hook" + description: "Command to be executed when theme toggles between dark and light mode." + placeholderText: "e.g., notify-send \"Theme\" \"Toggled\"" + text: Settings.data.hooks.darkModeChange + onEditingFinished: { + Settings.data.hooks.darkModeChange = darkModeHookInput.text + } + onActionClicked: { + if (darkModeHookInput.text) { + HooksService.executeDarkModeHook(Settings.data.colorSchemes.darkMode) + } + } + Layout.fillWidth: true + } + + NDivider { + Layout.fillWidth: true + } + + // Info section + ColumnLayout { + spacing: Style.marginS * scaling + Layout.fillWidth: true + + NLabel { + label: "Hook Command Information" + description: "• Commands are executed via shell (sh -c)\n• Commands run in background (detached)\n• Test buttons execute with current values" + } + + NLabel { + label: "Available Parameters" + description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)" + } } } } diff --git a/Modules/SettingsPanel/Tabs/LauncherTab.qml b/Modules/SettingsPanel/Tabs/LauncherTab.qml index 6ca4ece..f303273 100644 --- a/Modules/SettingsPanel/Tabs/LauncherTab.qml +++ b/Modules/SettingsPanel/Tabs/LauncherTab.qml @@ -59,13 +59,6 @@ ColumnLayout { onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked } - NToggle { - label: "Use App2Unit for Launching" - description: "Use app2unit -- 'desktop-entry' when launching applications for better systemd integration." - checked: Settings.data.appLauncher.useApp2Unit - onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked - } - ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true diff --git a/Modules/SettingsPanel/Tabs/NetworkTab.qml b/Modules/SettingsPanel/Tabs/NetworkTab.qml index 0e1fd0d..4b56ea7 100644 --- a/Modules/SettingsPanel/Tabs/NetworkTab.qml +++ b/Modules/SettingsPanel/Tabs/NetworkTab.qml @@ -12,14 +12,22 @@ ColumnLayout { spacing: Style.marginL * scaling NToggle { - label: "Enable Wi-Fi" - description: "Enable Wi-Fi connectivity." + label: "WiFi Enabled" + description: "Enable WiFi connectivity." checked: Settings.data.network.wifiEnabled - onToggled: checked => NetworkService.setWifiEnabled(checked) + onToggled: checked => { + Settings.data.network.wifiEnabled = checked + NetworkService.setWifiEnabled(checked) + if (checked) { + ToastService.showNotice("WiFi", "Enabled") + } else { + ToastService.showNotice("WiFi", "Disabled") + } + } } NToggle { - label: "Enable Bluetooth" + label: "Bluetooth Enabled" description: "Enable Bluetooth connectivity." checked: Settings.data.network.bluetoothEnabled onToggled: checked => { diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index 0d40d33..e2832dd 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -115,6 +115,7 @@ ColumnLayout { NColorPicker { selectedColor: Settings.data.wallpaper.fillColor onColorSelected: color => Settings.data.wallpaper.fillColor = color + onColorCancelled: selectedColor = Settings.data.wallpaper.fillColor } } @@ -277,6 +278,7 @@ ColumnLayout { NTextInput { label: "Custom Interval" description: "Enter time as HH:MM (e.g., 01:30)." + inputMaxWidth: 100 * scaling text: { const s = Settings.data.wallpaper.randomIntervalSec const h = Math.floor(s / 3600) diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml index 8eb28e8..2bdd88a 100644 --- a/Modules/SidePanel/Cards/PowerProfilesCard.qml +++ b/Modules/SidePanel/Cards/PowerProfilesCard.qml @@ -29,7 +29,7 @@ NBox { // Performance NIconButton { icon: "speed" - tooltipText: "Set performance power profile." + tooltipText: "Set performance power profile" enabled: hasPP opacity: enabled ? Style.opacityFull : Style.opacityMedium colorBg: (enabled && powerProfiles.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant @@ -43,7 +43,7 @@ NBox { // Balanced NIconButton { icon: "balance" - tooltipText: "Set balanced power profile." + tooltipText: "Set balanced power profile" enabled: hasPP opacity: enabled ? Style.opacityFull : Style.opacityMedium colorBg: (enabled && powerProfiles.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant @@ -57,7 +57,7 @@ NBox { // Eco NIconButton { icon: "eco" - tooltipText: "Set eco power profile." + tooltipText: "Set eco power profile" enabled: hasPP opacity: enabled ? Style.opacityFull : Style.opacityMedium colorBg: (enabled && powerProfiles.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 4c2d1ce..951beba 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -59,7 +59,7 @@ NBox { } NIconButton { icon: "settings" - tooltipText: "Open settings." + tooltipText: "Open settings" onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.General settingsPanel.open(screen) @@ -69,7 +69,7 @@ NBox { NIconButton { id: powerButton icon: "power_settings_new" - tooltipText: "Power menu." + tooltipText: "Power menu" onClicked: { powerPanel.open(screen) sidePanel.close() @@ -79,7 +79,7 @@ NBox { NIconButton { id: closeButton icon: "close" - tooltipText: "Close side panel." + tooltipText: "Close side panel" onClicked: { sidePanel.close() } @@ -104,7 +104,19 @@ NBox { stdout: StdioCollector { onStreamFinished: { var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0]) - uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds) + var minutes = Math.floor(uptimeSeconds / 60) % 60 + var hours = Math.floor(uptimeSeconds / 3600) % 24 + var days = Math.floor(uptimeSeconds / 86400) + + // Format the output + if (days > 0) { + uptimeText = days + "d " + hours + "h" + } else if (hours > 0) { + uptimeText = hours + "h" + minutes + "m" + } else { + uptimeText = minutes + "m" + } + uptimeProcess.running = false } } diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml index 2fc18de..9f05e66 100644 --- a/Modules/SidePanel/Cards/SystemMonitorCard.qml +++ b/Modules/SidePanel/Cards/SystemMonitorCard.qml @@ -11,7 +11,7 @@ NBox { Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling implicitHeight: content.implicitHeight + Style.marginXS * 2 * scaling - ColumnLayout { + Column { id: content anchors.left: parent.left anchors.right: parent.right @@ -22,6 +22,11 @@ NBox { anchors.bottomMargin: Style.marginM * scaling spacing: Style.marginS * scaling + // Slight top padding + Item { + height: Style.marginXS * scaling + } + NCircleStat { value: SystemStatService.cpuUsage icon: "speed" @@ -55,5 +60,10 @@ NBox { width: 72 * scaling height: 68 * scaling } + + // Extra bottom padding to shift the perceived stack slightly upward + Item { + height: Style.marginM * scaling + } } } diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 78fc702..f295224 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -26,7 +26,7 @@ NBox { // Screen Recorder NIconButton { icon: "videocam" - tooltipText: ScreenRecorderService.isRecording ? "Stop screen recording." : "Start screen recording." + tooltipText: ScreenRecorderService.isRecording ? "Stop screen recording" : "Start screen recording" colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary onClicked: { @@ -42,7 +42,7 @@ NBox { // Idle Inhibitor NIconButton { icon: "coffee" - tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake." : "Enable keep awake." + tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake" colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary onClicked: { @@ -54,7 +54,7 @@ NBox { NIconButton { visible: Settings.data.wallpaper.enabled icon: "image" - tooltipText: "Left click: Open wallpaper selector.\nRight click: Set random wallpaper." + tooltipText: "Left click: Open wallpaper selector\nRight click: Set random wallpaper" onClicked: { var settingsPanel = PanelService.getPanel("settingsPanel") settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector diff --git a/Modules/WiFiPanel/WiFiPanel.qml b/Modules/WiFiPanel/WiFiPanel.qml index 1fa5f18..1a77e69 100644 --- a/Modules/WiFiPanel/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -14,11 +14,16 @@ NPanel { panelHeight: 500 * scaling panelKeyboardFocus: true - property string passwordSsid: "" + property string passwordPromptSsid: "" property string passwordInput: "" - property string expandedSsid: "" + property bool showPasswordPrompt: false + property string expandedNetwork: "" // Track which network shows options - onOpened: NetworkService.scan() + onOpened: { + if (Settings.data.network.wifiEnabled) { + NetworkService.refreshNetworks() + } + } panelContent: Rectangle { color: Color.transparent @@ -34,32 +39,35 @@ NPanel { spacing: Style.marginM * scaling NIcon { - text: Settings.data.network.wifiEnabled ? "wifi" : "wifi_off" + text: "wifi" font.pointSize: Style.fontSizeXXL * scaling - color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant + color: Color.mPrimary } NText { - text: "Wi-Fi" + text: "WiFi" font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightBold color: Color.mOnSurface Layout.fillWidth: true + Layout.leftMargin: Style.marginS * scaling } - NToggle { - id: wifiSwitch - checked: Settings.data.network.wifiEnabled - onToggled: checked => NetworkService.setWifiEnabled(checked) - baseSize: Style.baseWidgetSize * 0.65 * scaling + // Connection status indicator + Rectangle { + visible: NetworkService.hasActiveConnection + width: 8 * scaling + height: 8 * scaling + radius: 4 * scaling + color: Color.mPrimary } NIconButton { icon: "refresh" - tooltipText: "Refresh" + tooltipText: "Refresh networks" sizeRatio: 0.8 - enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning - onClicked: NetworkService.scan() + enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading + onClicked: NetworkService.refreshNetworks() } NIconButton { @@ -74,18 +82,17 @@ NPanel { Layout.fillWidth: true } - // Error message + // Error banner Rectangle { - visible: NetworkService.lastError.length > 0 + visible: NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0 Layout.fillWidth: true - Layout.preferredHeight: errorRow.implicitHeight + (Style.marginM * scaling * 2) + Layout.preferredHeight: errorText.implicitHeight + (Style.marginM * scaling * 2) color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.1) radius: Style.radiusS * scaling border.width: Math.max(1, Style.borderS * scaling) border.color: Color.mError RowLayout { - id: errorRow anchors.fill: parent anchors.margins: Style.marginM * scaling spacing: Style.marginS * scaling @@ -97,7 +104,8 @@ NPanel { } NText { - text: NetworkService.lastError + id: errorText + text: NetworkService.connectError color: Color.mError font.pointSize: Style.fontSizeS * scaling wrapMode: Text.Wrap @@ -107,364 +115,301 @@ NPanel { NIconButton { icon: "close" sizeRatio: 0.6 - onClicked: NetworkService.lastError = "" + onClicked: { + NetworkService.connectStatus = "" + NetworkService.connectError = "" + } } } } - // Main content area - Rectangle { + ScrollView { Layout.fillWidth: true Layout.fillHeight: true - color: Color.transparent + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + clip: true + contentWidth: availableWidth - // WiFi disabled state ColumnLayout { - visible: !Settings.data.network.wifiEnabled - anchors.fill: parent + width: parent.width spacing: Style.marginM * scaling - Item { - Layout.fillHeight: true - } - - NIcon { - text: "wifi_off" - font.pointSize: 64 * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Wi-Fi is disabled" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Enable Wi-Fi to see available networks." - font.pointSize: Style.fontSizeS * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - Item { - Layout.fillHeight: true - } - } - - // Scanning state - ColumnLayout { - visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys( - NetworkService.networks).length === 0 - anchors.fill: parent - spacing: Style.marginL * scaling - - Item { - Layout.fillHeight: true - } - - NBusyIndicator { - running: true - color: Color.mPrimary - size: Style.baseWidgetSize * scaling - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Searching for nearby networks..." - font.pointSize: Style.fontSizeNormal * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - Item { - Layout.fillHeight: true - } - } - - // Networks list container - ScrollView { - visible: Settings.data.network.wifiEnabled && (!NetworkService.scanning || Object.keys( - NetworkService.networks).length > 0) - anchors.fill: parent - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - clip: true - + // Loading state ColumnLayout { - width: parent.width + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + visible: Settings.data.network.wifiEnabled && NetworkService.isLoading && Object.keys( + NetworkService.networks).length === 0 spacing: Style.marginM * scaling - // Network list - Repeater { - model: { - if (!Settings.data.network.wifiEnabled) - return [] + NBusyIndicator { + running: true + color: Color.mPrimary + size: Style.baseWidgetSize * scaling + Layout.alignment: Qt.AlignHCenter + } - const nets = Object.values(NetworkService.networks) - return nets.sort((a, b) => { - if (a.connected !== b.connected) - return b.connected - a.connected - return b.signal - a.signal - }) + NText { + text: "Scanning for networks..." + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + + // WiFi disabled state + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + visible: !Settings.data.network.wifiEnabled + spacing: Style.marginM * scaling + + NIcon { + text: "wifi_off" + font.pointSize: Style.fontSizeXXXL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "WiFi is disabled" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NButton { + text: "Enable WiFi" + icon: "wifi" + Layout.alignment: Qt.AlignHCenter + onClicked: { + Settings.data.network.wifiEnabled = true + Settings.save() + NetworkService.setWifiEnabled(true) } + } + } + + // Network list + Repeater { + model: { + if (!Settings.data.network.wifiEnabled || NetworkService.isLoading) + return [] + + // Sort networks: connected first, then by signal strength + const nets = Object.values(NetworkService.networks) + return nets.sort((a, b) => { + if (a.connected && !b.connected) + return -1 + if (!a.connected && b.connected) + return 1 + return b.signal - a.signal + }) + } + + Item { + Layout.fillWidth: true + implicitHeight: networkRect.implicitHeight Rectangle { - Layout.fillWidth: true - implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2) + id: networkRect + width: parent.width + implicitHeight: networkContent.implicitHeight + (Style.marginM * scaling * 2) radius: Style.radiusM * scaling - - // Add opacity for operations in progress - opacity: (NetworkService.disconnectingFrom === modelData.ssid - || NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1.0 - color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.05) : Color.mSurface border.width: Math.max(1, Style.borderS * scaling) border.color: modelData.connected ? Color.mPrimary : Color.mOutline - - // Smooth opacity animation - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - } - } + clip: true ColumnLayout { - id: netColumn + id: networkContent width: parent.width - (Style.marginM * scaling * 2) x: Style.marginM * scaling y: Style.marginM * scaling - spacing: Style.marginS * scaling + spacing: Style.marginM * scaling - // Main row + // Main network row RowLayout { Layout.fillWidth: true spacing: Style.marginS * scaling + // Signal icon NIcon { text: NetworkService.signalIcon(modelData.signal) font.pointSize: Style.fontSizeXXL * scaling color: modelData.connected ? Color.mPrimary : Color.mOnSurface } + // Network info ColumnLayout { Layout.fillWidth: true - spacing: 2 * scaling + Layout.alignment: Qt.AlignVCenter + spacing: 0 NText { - text: modelData.ssid + text: modelData.ssid || "Unknown Network" font.pointSize: Style.fontSizeNormal * scaling font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium - color: Color.mOnSurface elide: Text.ElideRight + color: Color.mOnSurface Layout.fillWidth: true } - RowLayout { - spacing: Style.marginXS * scaling - - NText { - text: `${modelData.signal}%` - font.pointSize: Style.fontSizeXXS * scaling - color: Color.mOnSurfaceVariant - } - - NText { - text: "•" - font.pointSize: Style.fontSizeXXS * scaling - color: Color.mOnSurfaceVariant - } - - NText { - text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open" - font.pointSize: Style.fontSizeXXS * scaling - color: Color.mOnSurfaceVariant - } - - Item { - Layout.preferredWidth: Style.marginXXS * scaling - } - - // Update the status badges area (around line 237) - Rectangle { - visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid - color: Color.mPrimary - radius: height * 0.5 - width: connectedText.implicitWidth + (Style.marginS * scaling * 2) - height: connectedText.implicitHeight + (Style.marginXXS * scaling * 2) - - NText { - id: connectedText - anchors.centerIn: parent - text: "Connected" - font.pointSize: Style.fontSizeXXS * scaling - color: Color.mOnPrimary - } - } - - Rectangle { - visible: NetworkService.disconnectingFrom === modelData.ssid - color: Color.mError - radius: height * 0.5 - width: disconnectingText.implicitWidth + (Style.marginS * scaling * 2) - height: disconnectingText.implicitHeight + (Style.marginXXS * scaling * 2) - - NText { - id: disconnectingText - anchors.centerIn: parent - text: "Disconnecting..." - font.pointSize: Style.fontSizeXXS * scaling - color: Color.mOnPrimary - } - } - - Rectangle { - visible: NetworkService.forgettingNetwork === modelData.ssid - color: Color.mError - radius: height * 0.5 - width: forgettingText.implicitWidth + (Style.marginS * scaling * 2) - height: forgettingText.implicitHeight + (Style.marginXXS * scaling * 2) - - NText { - id: forgettingText - anchors.centerIn: parent - text: "Forgetting..." - font.pointSize: Style.fontSizeXXS * scaling - color: Color.mOnPrimary - } - } - - Rectangle { - visible: modelData.cached && !modelData.connected - && NetworkService.forgettingNetwork !== modelData.ssid - && NetworkService.disconnectingFrom !== modelData.ssid - color: Color.transparent - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - radius: height * 0.5 - width: savedText.implicitWidth + (Style.marginS * scaling * 2) - height: savedText.implicitHeight + (Style.marginXXS * scaling * 2) - - NText { - id: savedText - anchors.centerIn: parent - text: "Saved" - font.pointSize: Style.fontSizeXXS * scaling - color: Color.mOnSurfaceVariant - } + NText { + text: { + const security = modelData.security + && modelData.security !== "--" ? modelData.security : "Open" + const signal = `${modelData.signal}%` + return `${signal} • ${security}` } + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnSurfaceVariant } } - // Action area + // Right-aligned items container RowLayout { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter spacing: Style.marginS * scaling - NBusyIndicator { - visible: NetworkService.connectingTo === modelData.ssid - || NetworkService.disconnectingFrom === modelData.ssid - || NetworkService.forgettingNetwork === modelData.ssid - running: visible + // Connected badge + Rectangle { + visible: modelData.connected color: Color.mPrimary - size: Style.baseWidgetSize * 0.5 * scaling - } + radius: width * 0.5 + width: connectedLabel.implicitWidth + (Style.marginS * scaling * 2) + height: connectedLabel.implicitHeight + (Style.marginXS * scaling * 2) - NIconButton { - visible: (modelData.existing || modelData.cached) && !modelData.connected - && NetworkService.connectingTo !== modelData.ssid - && NetworkService.forgettingNetwork !== modelData.ssid - && NetworkService.disconnectingFrom !== modelData.ssid - icon: "delete" - tooltipText: "Forget network" - sizeRatio: 0.7 - onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid - } - - NButton { - visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid - && passwordSsid !== modelData.ssid - && NetworkService.forgettingNetwork !== modelData.ssid - && NetworkService.disconnectingFrom !== modelData.ssid - text: { - if (modelData.existing || modelData.cached) - return "Connect" - if (!NetworkService.isSecured(modelData.security)) - return "Connect" - return "Password" + NText { + id: connectedLabel + anchors.centerIn: parent + text: "Connected" + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnPrimary } - outlined: !hovered - fontSize: Style.fontSizeXS * scaling - onClicked: { - if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) { - NetworkService.connect(modelData.ssid) - } else { - passwordSsid = modelData.ssid - passwordInput = "" - expandedSsid = "" + } + + // Saved badge - clickable + Rectangle { + visible: modelData.cached && !modelData.connected + color: Color.mSurfaceVariant + radius: width * 0.5 + width: savedLabel.implicitWidth + (Style.marginS * scaling * 2) + height: savedLabel.implicitHeight + (Style.marginXS * scaling * 2) + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.color = Qt.darker(Color.mSurfaceVariant, 1.1) + onExited: parent.color = Color.mSurfaceVariant + onClicked: { + expandedNetwork = expandedNetwork === modelData.ssid ? "" : modelData.ssid + showPasswordPrompt = false } } + + NText { + id: savedLabel + anchors.centerIn: parent + text: "Saved" + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnSurfaceVariant + } } - NButton { - visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid - text: "Disconnect" - outlined: !hovered - fontSize: Style.fontSizeXS * scaling - backgroundColor: Color.mError - onClicked: NetworkService.disconnect(modelData.ssid) + // Loading indicator + NBusyIndicator { + visible: NetworkService.connectingSsid === modelData.ssid + running: NetworkService.connectingSsid === modelData.ssid + color: Color.mPrimary + size: Style.baseWidgetSize * 0.6 * scaling + } + + // Action buttons + RowLayout { + spacing: Style.marginXS * scaling + visible: NetworkService.connectingSsid !== modelData.ssid + + NButton { + visible: !modelData.connected && (expandedNetwork !== modelData.ssid || !showPasswordPrompt) + outlined: !hovered + fontSize: Style.fontSizeXS * scaling + text: modelData.existing ? "Connect" : (NetworkService.isSecured( + modelData.security) ? "Password" : "Connect") + onClicked: { + if (modelData.existing || !NetworkService.isSecured(modelData.security)) { + NetworkService.connectNetwork(modelData.ssid, modelData.security) + } else { + expandedNetwork = modelData.ssid + passwordPromptSsid = modelData.ssid + showPasswordPrompt = true + passwordInput = "" + Qt.callLater(() => passwordInputField.forceActiveFocus()) + } + } + } + + NButton { + visible: modelData.connected + outlined: !hovered + fontSize: Style.fontSizeXS * scaling + backgroundColor: Color.mError + text: "Disconnect" + onClicked: NetworkService.disconnectNetwork(modelData.ssid) + } } } } - // Password input + // Password input section Rectangle { - visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid - && NetworkService.forgettingNetwork !== modelData.ssid + visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt Layout.fillWidth: true - height: passwordRow.implicitHeight + Style.marginS * scaling * 2 + implicitHeight: visible ? 50 * scaling : 0 color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) radius: Style.radiusS * scaling RowLayout { - id: passwordRow anchors.fill: parent anchors.margins: Style.marginS * scaling - spacing: Style.marginM * scaling + spacing: Style.marginS * scaling Rectangle { Layout.fillWidth: true Layout.fillHeight: true - radius: Style.radiusXS * scaling + radius: Style.radiusS * scaling color: Color.mSurface - border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline + border.color: passwordInputField.activeFocus ? Color.mSecondary : Color.mOutline border.width: Math.max(1, Style.borderS * scaling) TextInput { - id: pwdInput + id: passwordInputField anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Style.marginS * scaling + anchors.leftMargin: Style.marginM * scaling + anchors.rightMargin: Style.marginM * scaling + height: parent.height text: passwordInput - font.pointSize: Style.fontSizeS * scaling + font.pointSize: Style.fontSizeM * scaling color: Color.mOnSurface - echoMode: TextInput.Password + verticalAlignment: TextInput.AlignVCenter + clip: true + focus: modelData.ssid === passwordPromptSsid && showPasswordPrompt selectByMouse: true - focus: visible + echoMode: TextInput.Password passwordCharacter: "●" onTextChanged: passwordInput = text - onVisibleChanged: if (visible) - forceActiveFocus() onAccepted: { - if (text) { - NetworkService.connect(passwordSsid, text) - passwordSsid = "" - passwordInput = "" + if (passwordInput) { + NetworkService.submitPassword(passwordPromptSsid, passwordInput) + showPasswordPrompt = false + expandedNetwork = "" } } @@ -480,75 +425,56 @@ NPanel { NButton { text: "Connect" - fontSize: Style.fontSizeXXS * scaling + icon: "check" + fontSize: Style.fontSizeXS * scaling enabled: passwordInput.length > 0 - outlined: true + outlined: !enabled onClicked: { - NetworkService.connect(passwordSsid, passwordInput) - passwordSsid = "" - passwordInput = "" + if (passwordInput) { + NetworkService.submitPassword(passwordPromptSsid, passwordInput) + showPasswordPrompt = false + expandedNetwork = "" + } } } NIconButton { icon: "close" - sizeRatio: 0.8 + tooltipText: "Cancel" + sizeRatio: 0.9 onClicked: { - passwordSsid = "" + showPasswordPrompt = false + expandedNetwork = "" passwordInput = "" } } } } - // Forget network - Rectangle { - visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid - && NetworkService.forgettingNetwork !== modelData.ssid + // Forget network option - appears when saved badge is clicked + RowLayout { + visible: (modelData.existing || modelData.cached) && expandedNetwork === modelData.ssid + && !showPasswordPrompt Layout.fillWidth: true - height: forgetRow.implicitHeight + Style.marginS * 2 * scaling - color: Color.mSurfaceVariant - radius: Style.radiusS * scaling - border.width: Math.max(1, Style.borderS * scaling) - border.color: Color.mOutline + Layout.topMargin: Style.marginXS * scaling + spacing: Style.marginS * scaling - RowLayout { - id: forgetRow - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginM * scaling + Item { + Layout.fillWidth: true + } - RowLayout { - NIcon { - text: "delete_outline" - font.pointSize: Style.fontSizeL * scaling - color: Color.mError - } - - NText { - text: "Forget this network?" - font.pointSize: Style.fontSizeS * scaling - color: Color.mError - Layout.fillWidth: true - } - } - - NButton { - id: forgetButton - text: "Forget" - fontSize: Style.fontSizeXXS * scaling - backgroundColor: Color.mError - outlined: forgetButton.hovered ? false : true - onClicked: { - NetworkService.forget(modelData.ssid) - expandedSsid = "" - } - } - - NIconButton { - icon: "close" - sizeRatio: 0.8 - onClicked: expandedSsid = "" + NButton { + id: forgetButton + text: "Forget Network" + icon: "delete_outline" + fontSize: Style.fontSizeXXS * scaling + backgroundColor: Color.mError + textColor: !forgetButton.hovered ? Color.mError : Color.mOnTertiary + outlined: !forgetButton.hovered + Layout.preferredHeight: 28 * scaling + onClicked: { + NetworkService.forgetNetwork(modelData.ssid) + expandedNetwork = "" } } } @@ -556,42 +482,35 @@ NPanel { } } } - } - // Empty state when no networks - ColumnLayout { - visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys( - NetworkService.networks).length === 0 - anchors.fill: parent - spacing: Style.marginL * scaling + // No networks found + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading && Object.keys( + NetworkService.networks).length === 0 + spacing: Style.marginM * scaling - Item { - Layout.fillHeight: true - } + NIcon { + text: "wifi_find" + font.pointSize: Style.fontSizeXXXL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } - NIcon { - text: "wifi_find" - font.pointSize: 64 * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } + NText { + text: "No networks found" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } - NText { - text: "No networks found" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NButton { - text: "Scan again" - icon: "refresh" - Layout.alignment: Qt.AlignHCenter - onClicked: NetworkService.scan() - } - - Item { - Layout.fillHeight: true + NButton { + text: "Refresh" + icon: "refresh" + Layout.alignment: Qt.AlignHCenter + onClicked: NetworkService.refreshNetworks() + } } } } diff --git a/README.md b/README.md index dcdcd20..223936c 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,6 @@ Alternatively, you can add it to your NixOS configuration or flake: | Toggle Settings Window | `qs -c noctalia-shell ipc call settings toggle` | | Toggle Lock Screen | `qs -c noctalia-shell ipc call lockScreen toggle` | | Toggle Notification History | `qs -c noctalia-shell ipc call notifications toggleHistory` | -| Toggle Notification DND | `qs -c noctalia-shell ipc call notifications toggleDND` | | Change Wallpaper | `qs -c noctalia-shell ipc call wallpaper set $path $monitor` | | Assign a Random Wallpaper | `qs -c noctalia-shell ipc call wallpaper random` | | Toggle Dark Mode | `qs -c noctalia-shell ipc call darkMode toggle` | @@ -266,10 +265,6 @@ The launcher supports special commands for enhanced functionality: For Niri: ``` -debug { - honor-xdg-activation-with-invalid-serial -} - window-rule { geometry-corner-radius 20 clip-to-geometry true @@ -284,8 +279,6 @@ layer-rule { place-within-backdrop true } ``` -`honor-xdg-activation-with-invalid-serial` allows notification actions (like view etc) to work. - --- diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index 39afcd3..65897a6 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -28,7 +28,6 @@ Singleton { "PowerToggle": powerToggleComponent, "ScreenRecorderIndicator": screenRecorderIndicatorComponent, "SidePanelToggle": sidePanelToggleComponent, - "Spacer": spacerComponent, "SystemMonitor": systemMonitorComponent, "Taskbar": taskbarComponent, "Tray": trayComponent, @@ -44,11 +43,6 @@ Singleton { "leftClickExec": "", "rightClickExec": "", "middleClickExec": "" - }, - "Spacer": { - "allowUserSettings": true, - "icon": "space_bar", - "width": 20 } }) @@ -107,9 +101,6 @@ Singleton { property Component sidePanelToggleComponent: Component { SidePanelToggle {} } - property Component spacerComponent: Component { - Spacer {} - } property Component systemMonitorComponent: Component { SystemMonitor {} } diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index c4b5820..fa34523 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -8,239 +8,215 @@ import qs.Commons Singleton { id: root - // Core state + // Core properties property var networks: ({}) - property bool scanning: false - property bool connecting: false - property string connectingTo: "" - property string lastError: "" - property bool ethernetConnected: false - property string disconnectingFrom: "" - property string forgettingNetwork: "" + property string connectingSsid: "" + property string connectStatus: "" + property string connectStatusSsid: "" + property string connectError: "" + property bool isLoading: false + property bool ethernet: false + property int retryCount: 0 + property int maxRetries: 3 - // Persistent cache + // File path for persistent storage property string cacheFile: Settings.cacheDir + "network.json" - readonly property string cachedLastConnected: cacheAdapter.lastConnected - readonly property var cachedNetworks: cacheAdapter.knownNetworks - // Cache file handling + // Stable properties for UI + readonly property alias cache: adapter + readonly property string lastConnectedNetwork: adapter.lastConnected + + // File-based persistent storage FileView { id: cacheFileView path: root.cacheFile - - JsonAdapter { - id: cacheAdapter - property var knownNetworks: ({}) - property string lastConnected: "" - } - - onLoadFailed: { - cacheAdapter.knownNetworks = ({}) - cacheAdapter.lastConnected = "" - } - } - - Connections { - target: Settings.data.network - function onWifiEnabledChanged() { - if (Settings.data.network.wifiEnabled) { - ToastService.showNotice("Wi-Fi", "Enabled") - } else { - ToastService.showNotice("Wi-Fi", "Disabled") + onAdapterUpdated: saveTimer.start() + onLoaded: { + Logger.log("Network", "Loaded network cache from disk") + // Try to auto-connect on startup if WiFi is enabled + if (Settings.data.network.wifiEnabled && adapter.lastConnected) { + autoConnectTimer.start() } } + onLoadFailed: function (error) { + Logger.log("Network", "No existing cache found, creating new one") + // Initialize with empty data + adapter.knownNetworks = ({}) + adapter.lastConnected = "" + } + + JsonAdapter { + id: adapter + property var knownNetworks: ({}) + property string lastConnected: "" + property int lastRefresh: 0 + } } - Component.onCompleted: { - Logger.log("Network", "Service initialized") - syncWifiState() - refresh() - } - - // Save cache with debounce + // Save timer to batch writes Timer { - id: saveDebounce + id: saveTimer + running: false interval: 1000 onTriggered: cacheFileView.writeAdapter() } - function saveCache() { - saveDebounce.restart() - } - - // Delayed scan timer - Timer { - id: delayedScanTimer - interval: 7000 - onTriggered: scan() - } - - // Core functions - function syncWifiState() { - wifiStateProcess.running = true - } - - function setWifiEnabled(enabled) { - Settings.data.network.wifiEnabled = enabled - - wifiToggleProcess.action = enabled ? "on" : "off" - wifiToggleProcess.running = true - } - - function refresh() { - ethernetStateProcess.running = true + Component.onCompleted: { + Logger.log("Network", "Service started") if (Settings.data.network.wifiEnabled) { - scan() + refreshNetworks() } } - function scan() { - if (scanning) - return - - scanning = true - lastError = "" - scanProcess.running = true - Logger.log("Network", "Wi-Fi scan in progress...") - } - - function connect(ssid, password = "") { - if (connecting) - return - - connecting = true - connectingTo = ssid - lastError = "" - - // Check if we have a saved connection - if (networks[ssid]?.existing || cachedNetworks[ssid]) { - connectProcess.mode = "saved" - connectProcess.ssid = ssid - connectProcess.password = "" - } else { - connectProcess.mode = "new" - connectProcess.ssid = ssid - connectProcess.password = password - } - - connectProcess.running = true - } - - function disconnect(ssid) { - disconnectingFrom = ssid - disconnectProcess.ssid = ssid - disconnectProcess.running = true - } - - function forget(ssid) { - forgettingNetwork = ssid - - // Remove from cache - let known = cacheAdapter.knownNetworks - delete known[ssid] - cacheAdapter.knownNetworks = known - - if (cacheAdapter.lastConnected === ssid) { - cacheAdapter.lastConnected = "" - } - - saveCache() - - // Remove from system - forgetProcess.ssid = ssid - forgetProcess.running = true - } - - // Helper function to immediately update network status - function updateNetworkStatus(ssid, connected) { - let nets = networks - - // Update all networks connected status - for (let key in nets) { - if (nets[key].connected && key !== ssid) { - nets[key].connected = false - } - } - - // Update the target network if it exists - if (nets[ssid]) { - nets[ssid].connected = connected - nets[ssid].existing = true - nets[ssid].cached = true - } else if (connected) { - // Create a temporary entry if network doesn't exist yet - nets[ssid] = { - "ssid": ssid, - "security": "--", - "signal": 100, - "connected"// Default to good signal until real scan - : true, - "existing": true, - "cached": true - } - } - - // Trigger property change notification - networks = ({}) - networks = nets - } - - // Helper functions + // Signal strength icon mapping function signalIcon(signal) { - if (signal >= 80) - return "network_wifi" - if (signal >= 60) - return "network_wifi_3_bar" - if (signal >= 40) - return "network_wifi_2_bar" - if (signal >= 20) - return "network_wifi_1_bar" + const levels = [{ + "threshold": 80, + "icon": "network_wifi" + }, { + "threshold": 60, + "icon": "network_wifi_3_bar" + }, { + "threshold": 40, + "icon": "network_wifi_2_bar" + }, { + "threshold": 20, + "icon": "network_wifi_1_bar" + }] + + for (const level of levels) { + if (signal >= level.threshold) + return level.icon + } return "signal_wifi_0_bar" } function isSecured(security) { - return security && security !== "--" && security.trim() !== "" + return security && security.trim() !== "" && security.trim() !== "--" + } + + // Enhanced refresh with retry logic + function refreshNetworks() { + if (isLoading) + return + + isLoading = true + retryCount = 0 + adapter.lastRefresh = Date.now() + performRefresh() + } + + function performRefresh() { + checkEthernet.running = true + existingNetworkProcess.running = true + } + + // Retry mechanism for failed operations + function retryRefresh() { + if (retryCount < maxRetries) { + retryCount++ + Logger.log("Network", `Retrying refresh (${retryCount}/${maxRetries})`) + retryTimer.start() + } else { + isLoading = false + connectError = "Failed to refresh networks after multiple attempts" + } + } + + Timer { + id: retryTimer + interval: 1000 * retryCount // Progressive backoff + repeat: false + onTriggered: performRefresh() + } + + Timer { + id: autoConnectTimer + interval: 3000 + repeat: false + onTriggered: { + if (adapter.lastConnected && networks[adapter.lastConnected]?.existing) { + Logger.log("Network", `Auto-connecting to ${adapter.lastConnected}`) + connectToExisting(adapter.lastConnected) + } + } + } + + // Forget network function + function forgetNetwork(ssid) { + Logger.log("Network", `Forgetting network: ${ssid}`) + + // Remove from cache + let known = adapter.knownNetworks + delete known[ssid] + adapter.knownNetworks = known + + // Clear last connected if it's this network + if (adapter.lastConnected === ssid) { + adapter.lastConnected = "" + } + + // Save changes + saveTimer.restart() + + // Remove NetworkManager profile + forgetProcess.ssid = ssid + forgetProcess.running = true } - // Processes Process { - id: ethernetStateProcess + id: forgetProcess + property string ssid: "" running: false - command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] + command: ["nmcli", "connection", "delete", "id", ssid] stdout: StdioCollector { onStreamFinished: { - const connected = text.split("\n").some(line => { - const parts = line.split(":") - return parts[1] === "ethernet" && parts[2] === "connected" - }) - if (root.ethernetConnected !== connected) { - root.ethernetConnected = connected - Logger.log("Network", "Ethernet connected:", root.ethernetConnected) + Logger.log("Network", `Successfully forgot network: ${forgetProcess.ssid}`) + refreshNetworks() + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + if (text.includes("no such connection profile")) { + Logger.log("Network", `Network profile not found: ${forgetProcess.ssid}`) + } else { + Logger.warn("Network", `Error forgetting network: ${text}`) + } + refreshNetworks() } } } } - Process { - id: wifiStateProcess - running: false - command: ["nmcli", "radio", "wifi"] - - stdout: StdioCollector { - onStreamFinished: { - const enabled = text.trim() === "enabled" - Logger.log("Network", "Wi-Fi enabled:", enabled) - if (Settings.data.network.wifiEnabled !== enabled) { - Settings.data.network.wifiEnabled = enabled + // WiFi enable/disable functions + function setWifiEnabled(enabled) { + if (enabled) { + isLoading = true + wifiRadioProcess.action = "on" + wifiRadioProcess.running = true + } else { + // Save current connection for later + for (const ssid in networks) { + if (networks[ssid].connected) { + adapter.lastConnected = ssid + saveTimer.restart() + disconnectNetwork(ssid) + break } } + + wifiRadioProcess.action = "off" + wifiRadioProcess.running = true } } + // Unified WiFi radio control Process { - id: wifiToggleProcess + id: wifiRadioProcess property string action: "on" running: false command: ["nmcli", "radio", "wifi", action] @@ -248,12 +224,10 @@ Singleton { onRunningChanged: { if (!running) { if (action === "on") { - // Clear networks immediately and start delayed scan - root.networks = ({}) - delayedScanTimer.interval = 8000 - delayedScanTimer.restart() + wifiEnableTimer.start() } else { root.networks = ({}) + root.isLoading = false } } } @@ -261,177 +235,137 @@ Singleton { stderr: StdioCollector { onStreamFinished: { if (text.trim()) { - Logger.warn("Network", "WiFi toggle error: " + text) + Logger.warn("Network", `Error ${action === "on" ? "enabling" : "disabling"} WiFi: ${text}`) } } } } - Process { - id: scanProcess - running: false - command: ["sh", "-c", ` - # Get list of saved connection profiles (just the names) - profiles=$(nmcli -t -f NAME connection show | tr '\n' '|') - - # Get WiFi networks - nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list --rescan yes | while read line; do - ssid=$(echo "$line" | cut -d: -f1) - security=$(echo "$line" | cut -d: -f2) - signal=$(echo "$line" | cut -d: -f3) - in_use=$(echo "$line" | cut -d: -f4) - - # Skip empty SSIDs - if [ -z "$ssid" ]; then - continue - fi - - # Check if SSID matches any profile name (simple check) - # This covers most cases where profile name equals or contains the SSID - existing=false - if echo "$profiles" | grep -qF "$ssid|"; then - existing=true - fi - - echo "$ssid|$security|$signal|$in_use|$existing" - done - `] - - stdout: StdioCollector { - onStreamFinished: { - const nets = {} - const lines = text.split("\n").filter(l => l.trim()) - - for (const line of lines) { - const parts = line.split("|") - if (parts.length < 5) - continue - - const ssid = parts[0] - if (!ssid || ssid.trim() === "") - continue - - const network = { - "ssid": ssid, - "security": parts[1] || "--", - "signal": parseInt(parts[2]) || 0, - "connected": parts[3] === "*", - "existing": parts[4] === "true", - "cached": ssid in cacheAdapter.knownNetworks - } - - // Track connected network - if (network.connected && cacheAdapter.lastConnected !== ssid) { - cacheAdapter.lastConnected = ssid - saveCache() - } - - // Keep best signal for duplicate SSIDs - if (!nets[ssid] || network.signal > nets[ssid].signal) { - nets[ssid] = network - } - } - - // For logging purpose only - Logger.log("Network", "Wi-Fi scan completed") - const oldSSIDs = Object.keys(root.networks) - const newSSIDs = Object.keys(nets) - const newNetworks = newSSIDs.filter(ssid => !oldSSIDs.includes(ssid)) - const lostNetworks = oldSSIDs.filter(ssid => !newSSIDs.includes(ssid)) - if (newNetworks.length > 0 || lostNetworks.length > 0) { - if (newNetworks.length > 0) { - Logger.log("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", ")) - } - if (lostNetworks.length > 0) { - Logger.log("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", ")) - } - Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(nets).length) - } - - // Assign the results - root.networks = nets - root.scanning = false - } - } - - stderr: StdioCollector { - onStreamFinished: { - root.scanning = false - if (text.trim()) { - Logger.warn("Network", "Scan error: " + text) - // If scan fails, set a short retry - if (Settings.data.network.wifiEnabled) { - delayedScanTimer.interval = 5000 - delayedScanTimer.restart() - } - } + Timer { + id: wifiEnableTimer + interval: 2000 + repeat: false + onTriggered: { + refreshNetworks() + if (adapter.lastConnected) { + reconnectTimer.start() } } } + Timer { + id: reconnectTimer + interval: 3000 + repeat: false + onTriggered: { + if (adapter.lastConnected && networks[adapter.lastConnected]?.existing) { + connectToExisting(adapter.lastConnected) + } + } + } + + // Connection management + function connectNetwork(ssid, security) { + connectingSsid = ssid + connectStatus = "" + connectStatusSsid = ssid + connectError = "" + + // Check if profile exists + if (networks[ssid]?.existing) { + connectToExisting(ssid) + return + } + + // Check cache for known network + const known = adapter.knownNetworks[ssid] + if (known?.profileName) { + connectToExisting(known.profileName) + return + } + + // New connection - need password for secured networks + if (isSecured(security)) { + // Password will be provided through submitPassword + return + } + + // Open network - connect directly + createAndConnect(ssid, "", security) + } + + function submitPassword(ssid, password) { + const security = networks[ssid]?.security || "" + createAndConnect(ssid, password, security) + } + + function connectToExisting(ssid) { + connectingSsid = ssid + upConnectionProcess.profileName = ssid + upConnectionProcess.running = true + } + + function createAndConnect(ssid, password, security) { + connectingSsid = ssid + + connectProcess.ssid = ssid + connectProcess.password = password + connectProcess.isSecured = isSecured(security) + connectProcess.running = true + } + + function disconnectNetwork(ssid) { + disconnectProcess.ssid = ssid + disconnectProcess.running = true + } + + // Connection process Process { id: connectProcess - property string mode: "new" property string ssid: "" property string password: "" + property bool isSecured: false running: false command: { - if (mode === "saved") { - return ["nmcli", "connection", "up", "id", ssid] - } else { - const cmd = ["nmcli", "device", "wifi", "connect", ssid] - if (password) { - cmd.push("password", password) - } - return cmd + const cmd = ["nmcli", "device", "wifi", "connect", ssid] + if (isSecured && password) { + cmd.push("password", password) } + return cmd } stdout: StdioCollector { onStreamFinished: { - // Success - update cache - let known = cacheAdapter.knownNetworks - known[connectProcess.ssid] = { - "profileName": connectProcess.ssid, - "lastConnected": Date.now() - } - cacheAdapter.knownNetworks = known - cacheAdapter.lastConnected = connectProcess.ssid - saveCache() - - // Immediately update the UI before scanning - root.updateNetworkStatus(connectProcess.ssid, true) - - root.connecting = false - root.connectingTo = "" - Logger.log("Network", `Connected to network: "${connectProcess.ssid}"`) - - // Still do a scan to get accurate signal and security info - delayedScanTimer.interval = 1000 - delayedScanTimer.restart() + handleConnectionSuccess(connectProcess.ssid) } } stderr: StdioCollector { onStreamFinished: { - root.connecting = false - root.connectingTo = "" - if (text.trim()) { - // Parse common errors - if (text.includes("Secrets were required") || text.includes("no secrets provided")) { - root.lastError = "Incorrect password" - forget(connectProcess.ssid) - } else if (text.includes("No network with SSID")) { - root.lastError = "Network not found" - } else if (text.includes("Timeout")) { - root.lastError = "Connection timeout" - } else { - root.lastError = text.split("\n")[0].trim() - } + handleConnectionError(connectProcess.ssid, text) + } + } + } + } - Logger.warn("Network", "Connect error: " + text) + Process { + id: upConnectionProcess + property string profileName: "" + running: false + command: ["nmcli", "connection", "up", "id", profileName] + + stdout: StdioCollector { + onStreamFinished: { + handleConnectionSuccess(upConnectionProcess.profileName) + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + handleConnectionError(upConnectionProcess.profileName, text) } } } @@ -443,101 +377,221 @@ Singleton { running: false command: ["nmcli", "connection", "down", "id", ssid] - stdout: StdioCollector { - onStreamFinished: { - Logger.log("Network", `Disconnected from network: "${disconnectProcess.ssid}"`) - - // Immediately update UI on successful disconnect - root.updateNetworkStatus(disconnectProcess.ssid, false) - root.disconnectingFrom = "" - - // Do a scan to refresh the list - delayedScanTimer.interval = 1000 - delayedScanTimer.restart() + onRunningChanged: { + if (!running) { + connectingSsid = "" + connectStatus = "" + connectStatusSsid = "" + connectError = "" + refreshNetworks() } } stderr: StdioCollector { onStreamFinished: { - root.disconnectingFrom = "" if (text.trim()) { - Logger.warn("Network", "Disconnect error: " + text) + Logger.warn("Network", `Disconnect warning: ${text}`) + } + } + } + } + + // Connection result handlers + function handleConnectionSuccess(ssid) { + connectingSsid = "" + connectStatus = "success" + connectStatusSsid = ssid + connectError = "" + + // Update cache + let known = adapter.knownNetworks + known[ssid] = { + "profileName": ssid, + "lastConnected": Date.now(), + "autoConnect": true + } + adapter.knownNetworks = known + adapter.lastConnected = ssid + saveTimer.restart() + + Logger.log("Network", `Successfully connected to ${ssid}`) + refreshNetworks() + } + + function handleConnectionError(ssid, error) { + connectingSsid = "" + connectStatus = "error" + connectStatusSsid = ssid + connectError = parseError(error) + + Logger.warn("Network", `Failed to connect to ${ssid}: ${error}`) + } + + function parseError(error) { + // Simplify common error messages + if (error.includes("Secrets were required") || error.includes("no secrets provided")) { + return "Incorrect password" + } + if (error.includes("No network with SSID")) { + return "Network not found" + } + if (error.includes("Connection activation failed")) { + return "Connection failed. Please try again." + } + if (error.includes("Timeout")) { + return "Connection timeout. Network may be out of range." + } + // Return first line only + return error.split("\n")[0].trim() + } + + // Network scanning processes + Process { + id: existingNetworkProcess + running: false + command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] + + stdout: StdioCollector { + onStreamFinished: { + const profiles = {} + const lines = text.split("\n").filter(l => l.trim()) + + for (const line of lines) { + const parts = line.split(":") + const name = parts[0] + const type = parts[1] + if (name && type === "802-11-wireless") { + profiles[name] = { + "ssid": name, + "type": type + } + } + } + + scanProcess.existingProfiles = profiles + scanProcess.running = true + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.warn("Network", "Error listing connections:", text) + retryRefresh() } - // Still trigger a scan even on error - delayedScanTimer.interval = 1000 - delayedScanTimer.restart() } } } Process { - id: forgetProcess - property string ssid: "" + id: scanProcess + property var existingProfiles: ({}) running: false - - // Try multiple common profile name patterns - command: ["sh", "-c", ` - ssid="$1" - deleted=false - - # Try exact SSID match first - if nmcli connection delete id "$ssid" 2>/dev/null; then - echo "Deleted profile: $ssid" - deleted=true - fi - - # Try "Auto " pattern - if nmcli connection delete id "Auto $ssid" 2>/dev/null; then - echo "Deleted profile: Auto $ssid" - deleted=true - fi - - # Try " 1", " 2", etc. patterns - for i in 1 2 3; do - if nmcli connection delete id "$ssid $i" 2>/dev/null; then - echo "Deleted profile: $ssid $i" - deleted=true - fi - done - - if [ "$deleted" = "false" ]; then - echo "No profiles found for SSID: $ssid" - fi - `, "--", ssid] + command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] stdout: StdioCollector { onStreamFinished: { - Logger.log("Network", `Forget network: "${forgetProcess.ssid}"`) - Logger.log("Network", text.trim().replace(/[\r\n]/g, " ")) + const networksMap = {} + const lines = text.split("\n").filter(l => l.trim()) - // Update both cached and existing status immediately - let nets = root.networks - if (nets[forgetProcess.ssid]) { - nets[forgetProcess.ssid].cached = false - nets[forgetProcess.ssid].existing = false - // Trigger property change - root.networks = ({}) - root.networks = nets + for (const line of lines) { + const parts = line.split(":") + if (parts.length < 4) + continue + + const ssid = parts[0] + const security = parts[1] + const signalStr = parts[2] + const inUse = parts[3] + if (!ssid) + continue + + const signal = parseInt(signalStr) || 0 + const connected = inUse === "*" + + // Update last connected if we find the connected network + if (connected && adapter.lastConnected !== ssid) { + adapter.lastConnected = ssid + saveTimer.restart() + } + + // Merge with existing or create new + if (!networksMap[ssid] || signal > networksMap[ssid].signal) { + networksMap[ssid] = { + "ssid": ssid, + "security": security || "--", + "signal": signal, + "connected": connected, + "existing": ssid in scanProcess.existingProfiles, + "cached": ssid in adapter.knownNetworks + } + } } - root.forgettingNetwork = "" + root.networks = networksMap + root.isLoading = false + scanProcess.existingProfiles = {} - // Quick scan to verify the profile is gone - delayedScanTimer.interval = 500 - delayedScanTimer.restart() + //Logger.log("Network", `Found ${Object.keys(networksMap).length} wireless networks`) } } stderr: StdioCollector { onStreamFinished: { - root.forgettingNetwork = "" - if (text.trim() && !text.includes("No profiles found")) { - Logger.warn("Network", "Forget error: " + text) + if (text.trim()) { + Logger.warn("Network", "Error scanning networks:", text) + retryRefresh() } - // Still Trigger a scan even on error - delayedScanTimer.interval = 500 - delayedScanTimer.restart() } } } + + Process { + id: checkEthernet + running: false + command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] + + stdout: StdioCollector { + onStreamFinished: { + root.ethernet = text.split("\n").some(line => { + const parts = line.split(":") + return parts[1] === "ethernet" && parts[2] === "connected" + }) + } + } + } + + // Auto-refresh timer + Timer { + interval: 30000 // 30 seconds + running: Settings.data.network.wifiEnabled && !isLoading + repeat: true + onTriggered: { + // Only refresh if we should + const now = Date.now() + const timeSinceLastRefresh = now - adapter.lastRefresh + + // Refresh if: connected, or it's been more than 30 seconds + if (hasActiveConnection || timeSinceLastRefresh > 30000) { + refreshNetworks() + } + } + } + + property bool hasActiveConnection: { + return Object.values(networks).some(net => net.connected) + } + + // Menu state management + function onMenuOpened() { + if (Settings.data.network.wifiEnabled) { + refreshNetworks() + } + } + + function onMenuClosed() { + // Clean up temporary states + connectStatus = "" + connectError = "" + } } diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 4ce9747..a79b812 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -28,11 +28,11 @@ Singleton { // Signal when notification is received onNotification: function (notification) { - // Always add notification to history - root.addToHistory(notification) - // Check if do-not-disturb is enabled - if (Settings.data.notifications && Settings.data.notifications.doNotDisturb) { + // Check if notifications are suppressed + if (Settings.data.notifications && Settings.data.notifications.suppressed) { + // Still add to history but don't show notification + root.addToHistory(notification) return } @@ -46,6 +46,8 @@ Singleton { // Add to our model root.addNotification(notification) + // Also add to history + root.addToHistory(notification) } } @@ -107,15 +109,6 @@ Singleton { } } - Connections { - target: Settings.data.notifications - function onDoNotDisturbChanged() { - const label = Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' enabled" : "'Do Not Disturb' disabled" - const description = Settings.data.notifications.doNotDisturb ? "You'll find these notifications in your history." : "Showing all notifications." - ToastService.showNotice(label, description) - } - } - // Function to add notification to model function addNotification(notification) { notificationModel.insert(0, { diff --git a/Services/SystemStatService.qml b/Services/SystemStatService.qml index 4f09c1d..57a7346 100644 --- a/Services/SystemStatService.qml +++ b/Services/SystemStatService.qml @@ -22,7 +22,7 @@ Singleton { if (bytesPerSecond < 1024) { return bytesPerSecond.toFixed(0) + "B/s" } else if (bytesPerSecond < 1024 * 1024) { - return (bytesPerSecond / 1024).toFixed(0) + "KB/s" + return (bytesPerSecond / 1024).toFixed(1) + "KB/s" } else if (bytesPerSecond < 1024 * 1024 * 1024) { return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB/s" } else { diff --git a/Services/ToastService.qml b/Services/ToastService.qml index edff04b..6c0be38 100644 --- a/Services/ToastService.qml +++ b/Services/ToastService.qml @@ -165,21 +165,13 @@ Singleton { "timestamp": Date.now() } - // If there's already a toast showing, instantly start hide animation and show new one - if (isShowingToast) { - // Instantly start hide animation of current toast - for (var i = 0; i < allToasts.length; i++) { - allToasts[i].hide() - } - // Clear the queue since we're showing the new toast immediately - messageQueue = [] - } - // Add to queue messageQueue.push(toastData) - // Always process immediately for instant display - processQueue() + // Process queue if not currently showing a toast + if (!isShowingToast) { + processQueue() + } } // Process the message queue @@ -189,6 +181,11 @@ Singleton { return } + if (isShowingToast) { + // Wait for current toast to finish + return + } + var toastData = messageQueue.shift() isShowingToast = true diff --git a/Services/UpdateService.qml b/Services/UpdateService.qml index c4ec7d2..7ec8557 100644 --- a/Services/UpdateService.qml +++ b/Services/UpdateService.qml @@ -8,7 +8,7 @@ Singleton { id: root // Public properties - property string baseVersion: "2.6.0" + property string baseVersion: "2.5.0" property bool isDevelopment: true property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}` diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index e16cb12..1bd9e67 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -1,10 +1,9 @@ import QtQuick -import QtQuick.Layouts import qs.Commons import qs.Services import qs.Widgets -// Compact circular statistic display using Layout management +// Compact circular statistic display used in the SidePanel Rectangle { id: root @@ -29,20 +28,20 @@ Rectangle { // Repaint gauge when the bound value changes onValueChanged: gauge.requestPaint() - ColumnLayout { - id: mainLayout + Row { + id: innerRow anchors.fill: parent anchors.margins: Style.marginS * scaling * contentScale - spacing: 0 + spacing: Style.marginS * scaling * contentScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter - // Main gauge container + // Gauge with percentage label placed inside the open gap (right side) Item { - id: gaugeContainer - Layout.fillWidth: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignCenter - Layout.preferredWidth: 68 * scaling * contentScale - Layout.preferredHeight: 68 * scaling * contentScale + id: gaugeWrap + anchors.verticalCenter: innerRow.verticalCenter + width: 68 * scaling * contentScale + height: 68 * scaling * contentScale Canvas { id: gauge @@ -85,13 +84,15 @@ Rectangle { horizontalAlignment: Text.AlignHCenter } - // Tiny circular badge for the icon, positioned using anchors within the gauge + // Tiny circular badge for the icon, inside the right-side gap Rectangle { id: iconBadge width: 28 * scaling * contentScale height: width radius: width / 2 color: Color.mSurface + // border.color: Color.mPrimary + // border.width: Math.max(1, Style.borderS * scaling) anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: -6 * scaling * contentScale diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml index aa8ce33..f1c0a9b 100644 --- a/Widgets/NClock.qml +++ b/Widgets/NClock.qml @@ -18,7 +18,6 @@ Rectangle { id: textItem text: Time.time anchors.centerIn: parent - font.pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightBold } diff --git a/Widgets/NColorPicker.qml b/Widgets/NColorPicker.qml index 830ba84..8dd348e 100644 --- a/Widgets/NColorPicker.qml +++ b/Widgets/NColorPicker.qml @@ -8,34 +8,39 @@ Rectangle { id: root property color selectedColor: "#000000" + property bool expanded: false signal colorSelected(color color) + signal colorCancelled - implicitWidth: 150 * scaling - implicitHeight: 40 * scaling + implicitWidth: expanded ? 320 * scaling : 150 * scaling + implicitHeight: expanded ? 300 * scaling : 40 * scaling radius: Style.radiusM * scaling color: Color.mSurface border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) - // Minimized Look + property var presetColors: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError, Color.mSurface, Color.mSurfaceVariant, Color.mOutline, "#FFFFFF", "#000000", "#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E"] + + Behavior on implicitWidth { + NumberAnimation { + duration: Style.animationFast + } + } + + Behavior on implicitHeight { + NumberAnimation { + duration: Style.animationFast + } + } + + // Collapsed view - just show current color MouseArea { + visible: !root.expanded anchors.fill: parent cursorShape: Qt.PointingHandCursor - onClicked: { - var dialog = Qt.createComponent("NColorPickerDialog.qml").createObject(root, { - "selectedColor": selectedColor, - "parent": Overlay.overlay - }) - // Connect the dialog's signal to the picker's signal - dialog.colorSelected.connect(function (color) { - root.selectedColor = color - root.colorSelected(color) - }) - - dialog.open() - } + onClicked: root.expanded = true RowLayout { anchors.fill: parent @@ -63,4 +68,119 @@ Rectangle { } } } + + // Expanded view - color selection + ColumnLayout { + visible: root.expanded + anchors.fill: parent + anchors.margins: Style.marginM * scaling + spacing: Style.marginS * scaling + + // Header + RowLayout { + Layout.fillWidth: true + + NText { + text: "Select Color" + font.weight: Style.fontWeightBold + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + onClicked: root.expanded = false + } + } + + // Preset colors grid + Grid { + columns: 9 + spacing: Style.marginXS * scaling + Layout.fillWidth: true + + Repeater { + model: root.presetColors + + Rectangle { + width: Math.round(29 * scaling) + height: width + radius: Style.radiusXS * scaling + color: modelData + border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline + border.width: root.selectedColor === modelData ? 2 : 1 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + root.selectedColor = modelData + // root.colorSelected(modelData) + } + } + } + } + } + + // Custom color input + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS * scaling + + NTextInput { + id: hexInput + label: "Hex Color" + text: root.selectedColor.toString().toUpperCase() + fontFamily: Settings.data.ui.fontFixed + Layout.minimumWidth: 100 * scaling + onEditingFinished: { + if (/^#[0-9A-F]{6}$/i.test(text)) { + root.selectedColor = text + root.colorSelected(text) + } + } + } + + Rectangle { + Layout.preferredWidth: 32 * scaling + Layout.preferredHeight: 32 * scaling + radius: Layout.preferredWidth * 0.5 + color: root.selectedColor + border.color: Color.mOutline + border.width: 1 + Layout.alignment: Qt.AlignBottom + Layout.bottomMargin: 5 * scaling + } + } + + // Action buttons row + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS * scaling + + Item { + Layout.fillWidth: true + } // Spacer + + NButton { + text: "Cancel" + outlined: true + customHeight: Style.baseWidgetSize * scaling + fontSize: Style.fontSizeS * scaling + onClicked: { + root.colorCancelled() + root.expanded = false + } + } + + NButton { + text: "Apply" + customHeight: Style.baseWidgetSize * scaling + fontSize: Style.fontSizeS * scaling + onClicked: { + root.colorSelected(root.selectedColor) + root.expanded = false + } + } + } + } } diff --git a/Widgets/NColorPickerDialog.qml b/Widgets/NColorPickerDialog.qml deleted file mode 100644 index 324e5b6..0000000 --- a/Widgets/NColorPickerDialog.qml +++ /dev/null @@ -1,516 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import qs.Commons -import qs.Widgets - -Popup { - id: root - - property color selectedColor: "#000000" - property real currentHue: 0 - property real currentSaturation: 0 - - signal colorSelected(color color) - - width: 580 * scaling - height: { - const h = scrollView.implicitHeight + padding * 2 - Math.min(h, screen?.height - Style.barHeight * scaling - Style.marginL * 2) - } - padding: Style.marginXL * scaling - - // Center popup in parent - x: (parent.width - width) * 0.5 - y: (parent.height - height) * 0.5 - - modal: true - clip: true - - function rgbToHsv(r, g, b) { - r /= 255 - g /= 255 - b /= 255 - var max = Math.max(r, g, b), min = Math.min(r, g, b) - var h, s, v = max - var d = max - min - s = max === 0 ? 0 : d / max - if (max === min) { - h = 0 - } else { - switch (max) { - case r: - h = (g - b) / d + (g < b ? 6 : 0) - break - case g: - h = (b - r) / d + 2 - break - case b: - h = (r - g) / d + 4 - break - } - h /= 6 - } - return [h * 360, s * 100, v * 100] - } - - function hsvToRgb(h, s, v) { - h /= 360 - s /= 100 - v /= 100 - - var r, g, b - var i = Math.floor(h * 6) - var f = h * 6 - i - var p = v * (1 - s) - var q = v * (1 - f * s) - var t = v * (1 - (1 - f) * s) - - switch (i % 6) { - case 0: - r = v - g = t - b = p - break - case 1: - r = q - g = v - b = p - break - case 2: - r = p - g = v - b = t - break - case 3: - r = p - g = q - b = v - break - case 4: - r = t - g = p - b = v - break - case 5: - r = v - g = p - b = q - break - } - - return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)] - } - - background: Rectangle { - color: Color.mSurface - radius: Style.radiusS * scaling - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderM * scaling) - } - - ScrollView { - id: scrollView - anchors.fill: parent - - ScrollBar.vertical.policy: ScrollBar.AlwaysOff - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - clip: true - - ColumnLayout { - width: scrollView.availableWidth - spacing: Style.marginL * scaling - - // Header - RowLayout { - Layout.fillWidth: true - - RowLayout { - spacing: Style.marginS * scaling - - NIcon { - text: "palette" - font.pointSize: Style.fontSizeXXL * scaling - color: Color.mPrimary - } - - NText { - text: "Color Picker" - font.pointSize: Style.fontSizeXL * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - } - } - - Item { - Layout.fillWidth: true - } - - NIconButton { - icon: "close" - onClicked: root.close() - } - } - - // Color preview section - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 80 * scaling - radius: Style.radiusS * scaling - color: root.selectedColor - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - - ColumnLayout { - spacing: 0 - anchors.fill: parent - - Item { - Layout.fillHeight: true - } - - NText { - text: root.selectedColor.toString().toUpperCase() - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - font.weight: Font.Bold - color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF" - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "RGB(" + Math.round(root.selectedColor.r * 255) + ", " + Math.round( - root.selectedColor.g * 255) + ", " + Math.round(root.selectedColor.b * 255) + ")" - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF" - Layout.alignment: Qt.AlignHCenter - } - - Item { - Layout.fillHeight: true - } - } - } - - // Hex input - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NLabel { - label: "Hex Color" - description: "Enter a hexadecimal color code" - Layout.fillWidth: true - } - - NTextInput { - text: root.selectedColor.toString().toUpperCase() - fontFamily: Settings.data.ui.fontFixed - Layout.fillWidth: true - onEditingFinished: { - if (/^#[0-9A-F]{6}$/i.test(text)) { - root.selectedColor = text - } - } - } - } - - // RGB sliders section - NBox { - Layout.fillWidth: true - Layout.preferredHeight: slidersSection.implicitHeight + Style.marginL * scaling * 2 - - ColumnLayout { - id: slidersSection - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginM * scaling - - NLabel { - label: "RGB Values" - description: "Adjust red, green, blue, and brightness values" - Layout.fillWidth: true - } - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: "R" - font.weight: Font.Bold - Layout.preferredWidth: 20 * scaling - } - - NSlider { - id: redSlider - Layout.fillWidth: true - from: 0 - to: 255 - value: Math.round(root.selectedColor.r * 255) - onMoved: { - root.selectedColor = Qt.rgba(value / 255, root.selectedColor.g, root.selectedColor.b, 1) - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - root.currentHue = hsv[0] - root.currentSaturation = hsv[1] - } - } - - NText { - text: Math.round(redSlider.value) - font.family: Settings.data.ui.fontFixed - Layout.preferredWidth: 30 * scaling - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: "G" - font.weight: Font.Bold - Layout.preferredWidth: 20 * scaling - } - - NSlider { - id: greenSlider - Layout.fillWidth: true - from: 0 - to: 255 - value: Math.round(root.selectedColor.g * 255) - onMoved: { - root.selectedColor = Qt.rgba(root.selectedColor.r, value / 255, root.selectedColor.b, 1) - // Update stored hue and saturation when RGB changes - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - root.currentHue = hsv[0] - root.currentSaturation = hsv[1] - } - } - - NText { - text: Math.round(greenSlider.value) - font.family: Settings.data.ui.fontFixed - Layout.preferredWidth: 30 * scaling - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: "B" - font.weight: Font.Bold - Layout.preferredWidth: 20 * scaling - } - - NSlider { - id: blueSlider - Layout.fillWidth: true - from: 0 - to: 255 - value: Math.round(root.selectedColor.b * 255) - onMoved: { - root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, value / 255, 1) - // Update stored hue and saturation when RGB changes - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - root.currentHue = hsv[0] - root.currentSaturation = hsv[1] - } - } - - NText { - text: Math.round(blueSlider.value) - font.family: Settings.data.ui.fontFixed - Layout.preferredWidth: 30 * scaling - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: "Brightness" - font.weight: Font.Bold - Layout.preferredWidth: 80 * scaling - } - - NSlider { - id: brightnessSlider - Layout.fillWidth: true - from: 0 - to: 100 - value: { - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - return hsv[2] - } - onMoved: { - var hue = root.currentHue - var saturation = root.currentSaturation - - if (hue === 0 && saturation === 0) { - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - hue = hsv[0] - saturation = hsv[1] - root.currentHue = hue - root.currentSaturation = saturation - } - - var rgb = root.hsvToRgb(hue, saturation, value) - root.selectedColor = Qt.rgba(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1) - } - } - - NText { - text: Math.round(brightnessSlider.value) + "%" - font.family: Settings.data.ui.fontFixed - Layout.preferredWidth: 40 * scaling - } - } - } - } - - NBox { - Layout.fillWidth: true - Layout.preferredHeight: themePalette.implicitHeight + Style.marginL * scaling * 2 - - ColumnLayout { - id: themePalette - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginS * scaling - - NLabel { - label: "Theme Colors" - description: "Quick access to your theme's color palette" - Layout.fillWidth: true - } - - Flow { - spacing: 6 * scaling - Layout.fillWidth: true - flow: Flow.LeftToRight - - Repeater { - model: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError, Color.mSurface, Color.mSurfaceVariant, Color.mOutline, "#FFFFFF", "#000000"] - - Rectangle { - width: 24 * scaling - height: 24 * scaling - radius: 4 * scaling - color: modelData - border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline - border.width: root.selectedColor === modelData ? 2 * scaling : 1 * scaling - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - root.selectedColor = modelData - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - root.currentHue = hsv[0] - root.currentSaturation = hsv[1] - } - } - } - } - } - } - } - - NBox { - Layout.fillWidth: true - Layout.preferredHeight: genericPalette.implicitHeight + Style.marginL * scaling * 2 - - ColumnLayout { - id: genericPalette - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginS * scaling - - NLabel { - label: "Colors Palette" - description: "Choose from a wide range of predefined colors" - Layout.fillWidth: true - } - - Flow { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: 6 * scaling - flow: Flow.LeftToRight - - Repeater { - model: ["#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E", "#E74C3C", "#E67E22", "#F1C40F", "#2ECC71", "#1ABC9C", "#3498DB", "#2980B9", "#9B59B6", "#34495E", "#2C3E50", "#95A5A6", "#7F8C8D", "#FFFFFF", "#000000"] - - Rectangle { - width: 24 * scaling - height: 24 * scaling - radius: Style.radiusXXS * scaling - color: modelData - border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline - border.width: Math.max( - 1, root.selectedColor === modelData ? Style.borderM * scaling : Style.borderS * scaling) - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - root.selectedColor = modelData - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - root.currentHue = hsv[0] - root.currentSaturation = hsv[1] - } - } - } - } - } - } - } - - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 20 * scaling - Layout.bottomMargin: 20 * scaling - spacing: 10 * scaling - - Item { - Layout.fillWidth: true - } - - NButton { - id: cancelButton - text: "Cancel" - icon: "close" - outlined: cancelButton.hovered ? false : true - customHeight: 36 * scaling - customWidth: 100 * scaling - onClicked: { - root.close() - } - } - - NButton { - text: "Apply" - icon: "check" - customHeight: 36 * scaling - customWidth: 100 * scaling - onClicked: { - root.colorSelected(root.selectedColor) - root.close() - } - } - } - } - } -} diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index c9755b3..0650839 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -15,6 +15,7 @@ Rectangle { property string tooltipText property bool enabled: true property bool hovering: false + property real fontPointSize: Style.fontSizeM property color colorBg: Color.mSurfaceVariant property color colorFg: Color.mPrimary @@ -40,7 +41,7 @@ Rectangle { NIcon { text: root.icon - font.pointSize: Style.fontSizeM * scaling + font.pointSize: root.fontPointSize * scaling color: root.hovering ? colorFgHover : colorFg // Center horizontally x: (root.width - width) / 2 diff --git a/Widgets/NInputAction.qml b/Widgets/NInputAction.qml index 785b5b0..1f8ce5e 100644 --- a/Widgets/NInputAction.qml +++ b/Widgets/NInputAction.qml @@ -3,8 +3,7 @@ import QtQuick.Layouts import qs.Commons import qs.Widgets -// Input and button row -RowLayout { +ColumnLayout { id: root // Public properties @@ -22,35 +21,57 @@ RowLayout { // Internal properties property real scaling: 1.0 - spacing: Style.marginM * scaling - NTextInput { - id: textInput - label: root.label - description: root.description - placeholderText: root.placeholderText - text: root.text - onEditingFinished: { - root.text = text - root.editingFinished() - } + // Label + NText { + text: root.label + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface Layout.fillWidth: true } - NButton { - Layout.fillWidth: false - Layout.alignment: Qt.AlignBottom + // Description + NText { + text: root.description + font.pointSize: Style.fontSizeS * scaling + color: Color.mOnSurfaceVariant + wrapMode: Text.Wrap + Layout.fillWidth: true + } - text: root.actionButtonText - icon: root.actionButtonIcon - backgroundColor: Color.mSecondary - textColor: Color.mOnSecondary - hoverColor: Color.mTertiary - pressColor: Color.mPrimary - enabled: root.actionButtonEnabled + // Input and button row + RowLayout { + spacing: Style.marginM * scaling + Layout.fillWidth: true - onClicked: { - root.actionClicked() + NTextInput { + id: textInput + placeholderText: root.placeholderText + text: root.text + onEditingFinished: { + root.text = text + root.editingFinished() + } + Layout.fillWidth: true + } + + Item { + Layout.fillWidth: true + } + + NButton { + text: root.actionButtonText + icon: root.actionButtonIcon + backgroundColor: Color.mSecondary + textColor: Color.mOnSecondary + hoverColor: Color.mTertiary + pressColor: Color.mPrimary + enabled: root.actionButtonEnabled + Layout.fillWidth: false + onClicked: { + root.actionClicked() + } } } } diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 2432544..1fcccfc 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -231,7 +231,8 @@ Item { root.clicked() } else if (mouse.button === Qt.RightButton) { root.rightClicked() - } else if (mouse.button === Qt.MiddleButton) { + } + else if (mouse.button === Qt.MiddleButton) { root.middleClicked() } } diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 3db9d9b..1cb7141 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -11,6 +11,7 @@ ColumnLayout { property string description: "" property bool readOnly: false property bool enabled: true + property int inputMaxWidth: Math.round(420 * scaling) property color labelColor: Color.mOnSurface property color descriptionColor: Color.mOnSurfaceVariant property string fontFamily: Settings.data.ui.fontDefault @@ -25,6 +26,7 @@ ColumnLayout { signal editingFinished spacing: Style.marginS * scaling + implicitHeight: frame.height NLabel { label: root.label @@ -32,7 +34,6 @@ ColumnLayout { labelColor: root.labelColor descriptionColor: root.descriptionColor visible: root.label !== "" || root.description !== "" - Layout.fillWidth: true } // Container @@ -41,48 +42,50 @@ ColumnLayout { Layout.fillWidth: true Layout.minimumWidth: 80 * scaling - implicitHeight: Style.baseWidgetSize * 1.1 * scaling + Layout.maximumWidth: root.inputMaxWidth + implicitWidth: parent.width + implicitHeight: Style.baseWidgetSize * 1.1 * scaling radius: Style.radiusM * scaling color: Color.mSurface - border.color: input.activeFocus ? Color.mSecondary : Color.mOutline + border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) - Behavior on border.color { - ColorAnimation { - duration: Style.animationFast + // Focus ring + Rectangle { + anchors.fill: parent + radius: frame.radius + color: Color.transparent + border.color: input.activeFocus ? Color.mSecondary : Color.transparent + border.width: input.activeFocus ? Math.max(1, Style.borderS * scaling) : 0 + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } } } - TextField { - id: input - + RowLayout { anchors.fill: parent anchors.leftMargin: Style.marginM * scaling anchors.rightMargin: Style.marginM * scaling + spacing: Style.marginS * scaling - verticalAlignment: TextInput.AlignVCenter - - echoMode: TextInput.Normal - readOnly: root.readOnly - enabled: root.enabled - color: Color.mOnSurface - placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6) - - selectByMouse: true - - topPadding: 0 - bottomPadding: 0 - leftPadding: 0 - rightPadding: 0 - - background: null - - font.family: root.fontFamily - font.pointSize: root.fontSize - font.weight: root.fontWeight - - onEditingFinished: root.editingFinished() + TextField { + id: input + Layout.fillWidth: true + echoMode: TextInput.Normal + readOnly: root.readOnly + enabled: root.enabled + color: Color.mOnSurface + placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6) + background: null + font.family: fontFamily + font.pointSize: fontSize + font.weight: fontWeight + onEditingFinished: root.editingFinished() + } } } } diff --git a/Widgets/NToast.qml b/Widgets/NToast.qml index 7a60c6c..44a0bb5 100644 --- a/Widgets/NToast.qml +++ b/Widgets/NToast.qml @@ -37,16 +37,7 @@ Item { // NToast updates its scaling when showing. scaling = ScalingService.getScreenScale(screen) - // Stop any running animations and reset state - showAnimation.stop() - hideAnimation.stop() - autoHideTimer.stop() - - // Ensure we start from the hidden position - y = hiddenY visible = true - - // Start the show animation showAnimation.start() if (duration > 0 && !persistent) { autoHideTimer.start() @@ -90,6 +81,7 @@ Item { // Main toast container Rectangle { + id: container anchors.fill: parent radius: Style.radiusL * scaling @@ -145,41 +137,43 @@ Item { } // Label and description - ColumnLayout { + Column { + id: textColumn spacing: Style.marginXXS * scaling Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter NText { - Layout.fillWidth: true + id: labelText text: root.label color: Color.mOnSurface font.pointSize: Style.fontSizeM * scaling font.weight: Style.fontWeightBold wrapMode: Text.WordWrap + width: parent.width visible: text.length > 0 } NText { - Layout.fillWidth: true + id: descriptionText text: root.description color: Color.mOnSurface font.pointSize: Style.fontSizeM * scaling wrapMode: Text.WordWrap + width: parent.width visible: text.length > 0 } } // Close button (only if persistent or manual dismiss needed) NIconButton { + id: closeButton icon: "close" visible: root.persistent || root.duration === 0 - colorBg: Color.mSurfaceVariant - colorFg: Color.mOnSurface - colorBorder: Color.transparent - colorBorderHover: Color.mOutline + color: Color.mOnSurface + fontPointSize: Style.fontSizeM * scaling sizeRatio: 0.8 Layout.alignment: Qt.AlignTop