diff --git a/Assets/Matugen/templates/vesktop.css b/Assets/Matugen/templates/vesktop.css index 9876c8a..ac5b166 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: "Message #general" !important; + content: "send a message" !important; color: {{colors.on_surface_variant.default.hex}} !important; } diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 0c1dd55..097f5e9 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -278,12 +278,14 @@ 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: [] } @@ -295,6 +297,7 @@ Singleton { // notifications property JsonObject notifications: JsonObject { + property bool doNotDisturb: false property list monitors: [] } diff --git a/Commons/Style.qml b/Commons/Style.qml index d2af5a8..902a225 100644 --- a/Commons/Style.qml +++ b/Commons/Style.qml @@ -29,6 +29,7 @@ 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 fe1f3fd..d7ec78e 100644 --- a/Commons/Time.qml +++ b/Commons/Time.qml @@ -78,23 +78,34 @@ Singleton { } // Format an easy to read approximate duration ex: 4h32m -// Used to display the time remaining on the Battery widget +// Used to display the time remaining on the Battery widget, computer uptime, etc.. function formatVagueHumanReadableDuration(totalSeconds) { - const hours = Math.floor(totalSeconds / 3600) - const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60) - const seconds = totalSeconds - (hours * 3600) - (minutes * 60) + if (typeof totalSeconds !== 'number' || totalSeconds < 0) { + return '0s' + } - 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) { - str += seconds.toString() + "s" + parts.push(`${seconds}s`) } - return str + + return parts.join('') } Timer { diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 8fed9bf..a1eca8b 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 c7387d1..65f900e 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -2,38 +2,45 @@ 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 -Row { +RowLayout { id: root - property ShellScreen screen property real scaling: 1.0 readonly property real minWidth: 160 readonly property real maxWidth: 400 - - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter 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 "" + if (focusedWindow && focusedWindow.appId) { + return Icons.iconForAppId(focusedWindow.appId.toLowerCase()) + } - return Icons.iconForAppId(focusedWindow.appId) + // Fallback to ToplevelManager + if (ToplevelManager && ToplevelManager.activeToplevel) { + const activeToplevel = ToplevelManager.activeToplevel + if (activeToplevel.appId) { + return Icons.iconForAppId(activeToplevel.appId.toLowerCase()) + } + } + + return "" } - // 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 @@ -43,15 +50,13 @@ Row { } Rectangle { - // Let the Rectangle size itself based on its content (the Row) + id: windowTitleRect visible: root.visible - width: row.width + Style.marginM * 2 * scaling - height: Math.round(Style.capsuleHeight * scaling) + Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant - anchors.verticalCenter: parent.verticalCenter - Item { id: mainContainer anchors.fill: parent @@ -59,16 +64,16 @@ Row { anchors.rightMargin: Style.marginS * scaling clip: true - Row { - id: row - anchors.verticalCenter: parent.verticalCenter + RowLayout { + id: contentLayout + anchors.centerIn: parent spacing: Style.marginS * scaling // Window icon Item { - width: Style.fontSizeL * scaling * 1.2 - height: Style.fontSizeL * scaling * 1.2 - anchors.verticalCenter: parent.verticalCenter + Layout.preferredWidth: Style.fontSizeL * scaling * 1.2 + Layout.preferredHeight: Style.fontSizeL * scaling * 1.2 + Layout.alignment: Qt.AlignVCenter visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon IconImage { @@ -83,26 +88,24 @@ Row { NText { id: titleText - - // For short titles, show full. For long titles, truncate and expand on hover - width: { + Layout.preferredWidth: { 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 width { + Behavior on Layout.preferredWidth { NumberAnimation { duration: Style.animationSlow easing.type: Easing.InOutCubic @@ -120,4 +123,14 @@ Row { } } } + + 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 9da63f5..ee57b57 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 e99537a..de8f96d 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 24f0c26..b9b44a5 100644 --- a/Modules/Bar/Widgets/KeyboardLayout.qml +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Layouts import Quickshell import Quickshell.Wayland import Quickshell.Io @@ -6,7 +7,7 @@ import qs.Commons import qs.Services import qs.Widgets -Row { +Item { id: root property ShellScreen screen @@ -18,12 +19,13 @@ Row { // Use the shared service for keyboard layout property string currentLayout: KeyboardLayoutService.currentLayout - width: pill.width - height: pill.height + implicitWidth: pill.width + implicitHeight: 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 7d2ffb7..2483dbc 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 -Row { +RowLayout { id: root property ShellScreen screen @@ -15,10 +15,10 @@ Row { readonly property real minWidth: 160 readonly property real maxWidth: 400 - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter spacing: Style.marginS * scaling visible: MediaService.currentPlayer !== null && MediaService.canPlay - width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 + Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 function getTitle() { return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") @@ -35,15 +35,13 @@ Row { Rectangle { id: mediaMini - // Let the Rectangle size itself based on its content (the Row) - width: row.width + Style.marginM * 2 * scaling + Layout.preferredWidth: rowLayout.implicitWidth + Style.marginM * 2 * scaling + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.alignment: Qt.AlignVCenter - 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 @@ -61,7 +59,7 @@ Row { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear" - && MediaService.isPlaying && MediaService.trackLength > 0 + && MediaService.isPlaying z: 0 sourceComponent: LinearSpectrum { @@ -71,42 +69,42 @@ Row { 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 && MediaService.trackLength > 0 - z: 0 + Loader { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored" + && MediaService.isPlaying + 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 - } - } - - 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 - } + sourceComponent: MirroredSpectrum { + width: mainContainer.width - Style.marginS * scaling + height: mainContainer.height - Style.marginS * scaling + values: CavaService.values + fillColor: Color.mOnSurfaceVariant + opacity: 0.4 } } - Row { - id: row + 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 anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling z: 1 // Above the visualizer @@ -116,17 +114,18 @@ Row { text: MediaService.isPlaying ? "pause" : "play_arrow" font.pointSize: Style.fontSizeL * scaling verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible } - Column { - anchors.verticalCenter: parent.verticalCenter + ColumnLayout { + Layout.alignment: Qt.AlignVCenter visible: Settings.data.audio.showMiniplayerAlbumArt + spacing: 0 Item { - width: Math.round(18 * scaling) - height: Math.round(18 * scaling) + Layout.preferredWidth: Math.round(18 * scaling) + Layout.preferredHeight: Math.round(18 * scaling) NImageCircled { id: trackArt @@ -142,23 +141,23 @@ Row { NText { id: titleText - // For short titles, show full. For long titles, truncate and expand on hover - width: { + Layout.preferredWidth: { 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 width { + Behavior on Layout.preferredWidth { NumberAnimation { duration: Style.animationSlow easing.type: Easing.InOutCubic @@ -205,10 +204,10 @@ Row { 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\n" + str += "Middle click for previous." } return str } diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index 410e041..f4e1c1a 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 342a424..6ea2e20 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 222a0eb..48a62fd 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -14,11 +14,14 @@ NIconButton { property real scaling: 1.0 sizeRatio: 0.8 - icon: "notifications" - tooltipText: "Notification history" + 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'." colorBg: Color.mSurfaceVariant - colorFg: Color.mOnSurface + colorFg: Settings.data.notifications.doNotDisturb ? Color.mError : 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 1b0b4ce..b9572fb 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 new file mode 100644 index 0000000..5a62372 --- /dev/null +++ b/Modules/Bar/Widgets/Spacer.qml @@ -0,0 +1,56 @@ +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 ce16aa3..6c2346c 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -1,145 +1,146 @@ import QtQuick +import QtQuick.Layouts import Quickshell import qs.Commons import qs.Services import qs.Widgets -Row { +RowLayout { id: root property ShellScreen screen property real scaling: 1.0 - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter spacing: Style.marginS * scaling Rectangle { - // Let the Rectangle size itself based on its content (the Row) - width: row.width + Style.marginM * scaling * 2 + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.preferredWidth: mainLayout.implicitWidth + Style.marginM * scaling * 2 + Layout.alignment: Qt.AlignVCenter - height: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant - anchors.verticalCenter: parent.verticalCenter - - Item { - id: mainContainer + RowLayout { + id: mainLayout anchors.fill: parent anchors.leftMargin: Style.marginS * scaling anchors.rightMargin: Style.marginS * scaling + spacing: Style.marginS * scaling - Row { - id: row - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS * scaling - Row { - id: cpuUsageLayout - spacing: Style.marginXS * scaling + // CPU Usage Component + RowLayout { + id: cpuUsageLayout + spacing: Style.marginXS * scaling + 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 - } + NIcon { + id: cpuUsageIcon + text: "speed" + Layout.alignment: Qt.AlignVCenter } - // CPU Temperature Component - Row { - id: cpuTempLayout - // spacing is thin here to compensate for the vertical thermometer icon - spacing: Style.marginXXS * scaling + 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 + } + } - NIcon { - text: "thermometer" - anchors.verticalCenter: parent.verticalCenter - } + // 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 - 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 - } + NIcon { + text: "thermometer" + Layout.alignment: Qt.AlignVCenter } - // Memory Usage Component - Row { - id: memoryUsageLayout - spacing: Style.marginXS * scaling + 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 + } + } - NIcon { - text: "memory" - anchors.verticalCenter: parent.verticalCenter - } + // Memory Usage Component + RowLayout { + id: memoryUsageLayout + spacing: Style.marginXS * scaling + 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 - } + NIcon { + text: "memory" + Layout.alignment: Qt.AlignVCenter } - // Network Download Speed Component - Row { - id: networkDownloadLayout - spacing: Style.marginXS * scaling - visible: Settings.data.bar.showNetworkStats + 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 + } + } - NIcon { - text: "download" - anchors.verticalCenter: parent.verticalCenter - } + // Network Download Speed Component + RowLayout { + id: networkDownloadLayout + spacing: Style.marginXS * scaling + Layout.alignment: Qt.AlignVCenter + visible: Settings.data.bar.showNetworkStats - 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 - } + NIcon { + text: "download" + Layout.alignment: Qt.AlignVCenter } - // Network Upload Speed Component - Row { - id: networkUploadLayout - spacing: Style.marginXS * scaling - visible: Settings.data.bar.showNetworkStats + 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 + } + } - NIcon { - text: "upload" - anchors.verticalCenter: parent.verticalCenter - } + // Network Upload Speed Component + RowLayout { + id: networkUploadLayout + spacing: Style.marginXS * scaling + Layout.alignment: Qt.AlignVCenter + visible: Settings.data.bar.showNetworkStats - 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 - } + 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 } } } diff --git a/Modules/Bar/Widgets/Taskbar.qml b/Modules/Bar/Widgets/Taskbar.qml index 623d7e7..103e707 100644 --- a/Modules/Bar/Widgets/Taskbar.qml +++ b/Modules/Bar/Widgets/Taskbar.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior import QtQuick import QtQuick.Controls +import QtQuick.Layouts import Quickshell import Quickshell.Widgets import Quickshell.Wayland @@ -17,15 +18,14 @@ Rectangle { readonly property real itemSize: Style.baseWidgetSize * 0.8 * scaling // Always visible when there are toplevels - implicitWidth: taskbarRow.width + Style.marginM * scaling * 2 + implicitWidth: taskbarLayout.implicitWidth + Style.marginM * scaling * 2 implicitHeight: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant - Row { - id: taskbarRow - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter + RowLayout { + id: taskbarLayout + anchors.centerIn: parent spacing: Style.marginXXS * root.scaling Repeater { @@ -35,8 +35,10 @@ Rectangle { required property Toplevel modelData property Toplevel toplevel: modelData property bool isActive: ToplevelManager.activeToplevel === modelData - width: root.itemSize - height: root.itemSize + + Layout.preferredWidth: root.itemSize + Layout.preferredHeight: root.itemSize + Layout.alignment: Qt.AlignCenter Rectangle { id: iconBackground @@ -89,7 +91,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 f29d7b7..06de40f 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: tray.width + Style.marginM * scaling * 2 + implicitWidth: trayLayout.implicitWidth + Style.marginM * scaling * 2 implicitHeight: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant Layout.alignment: Qt.AlignVCenter - Row { - id: tray - - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter + RowLayout { + id: trayLayout + anchors.centerIn: parent spacing: Style.marginS * scaling Repeater { id: repeater model: SystemTray.items + delegate: Item { - width: itemSize - height: itemSize + Layout.preferredWidth: itemSize + Layout.preferredHeight: itemSize + Layout.alignment: Qt.AlignCenter visible: modelData IconImage { @@ -146,13 +146,14 @@ Rectangle { function open() { visible = true - PanelService.willOpenPanel(trayPanel) } function close() { visible = false - trayMenu.item.hideMenu() + if (trayMenu.item) { + 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 5f70998..84f8b22 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 0917f36..77f8664 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -13,18 +13,8 @@ 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 @@ -32,7 +22,7 @@ NIconButton { icon: { try { - if (NetworkService.ethernet) { + if (NetworkService.ethernetConnected) { return "lan" } let connected = false @@ -46,10 +36,10 @@ NIconButton { } return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find" } catch (error) { - Logger.error("WiFi", "Error getting icon:", error) + Logger.error("Wi-Fi", "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 6c53e65..051bdea 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: null + property ShellScreen screen property real scaling: 1.0 property bool isDestroying: false diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 07056ff..9a71bba 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -32,6 +32,8 @@ Variants { screen: modelData + WlrLayershell.namespace: "noctalia-dock" + property bool autoHide: Settings.data.dock.autoHide property bool hidden: autoHide property int hideDelay: 500 @@ -128,9 +130,9 @@ Variants { Rectangle { id: dockContainer - width: dock.width + 48 * scaling + width: dockLayout.implicitWidth + 48 * scaling height: iconSize * 1.4 * scaling - color: Color.mSurface + color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: dockSpacing @@ -176,7 +178,7 @@ Variants { Item { id: dock - width: runningAppsRow.width + width: dockLayout.implicitWidth height: parent.height - (20 * scaling) anchors.centerIn: parent @@ -192,10 +194,10 @@ Variants { return Icons.iconForAppId(toplevel.appId?.toLowerCase()) } - Row { - id: runningAppsRow + RowLayout { + id: dockLayout spacing: Style.marginL * scaling - height: parent.height + Layout.preferredHeight: parent.height anchors.centerIn: parent Repeater { @@ -203,8 +205,10 @@ Variants { delegate: Rectangle { id: appButton - width: iconSize * scaling - height: iconSize * scaling + Layout.preferredWidth: iconSize * scaling + Layout.preferredHeight: iconSize * scaling + Layout.alignment: Qt.AlignCenter + color: Color.transparent radius: Style.radiusM * scaling diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index 7436829..8c541a3 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -38,7 +38,8 @@ Item { function toggleHistory() { notificationHistoryPanel.toggle(getActiveScreen()) } - function toggleDoNotDisturb() {// TODO + function toggleDND() { + Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb } } diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 4726770..0ad25da 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -243,52 +243,45 @@ NPanel { anchors.margins: Style.marginL * scaling spacing: Style.marginM * scaling - Item { - id: searchInputWrap + NTextInput { + id: searchInput Layout.fillWidth: true - Layout.preferredHeight: Math.round(Style.barHeight * scaling) - NTextInput { - id: searchInput - anchors.fill: parent - inputMaxWidth: Number.MAX_SAFE_INTEGER + fontSize: Style.fontSizeL * scaling + fontWeight: Style.fontWeightSemiBold - fontSize: Style.fontSizeL * scaling - fontWeight: Style.fontWeightSemiBold + text: searchText + placeholderText: "Search entries... or use > for commands" - text: searchText - placeholderText: "Search entries... or use > for commands" + onTextChanged: searchText = text - onTextChanged: searchText = text + Component.onCompleted: { + if (searchInput.inputItem && searchInput.inputItem.visible) { + searchInput.inputItem.forceActiveFocus() - 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() - }) - } + // 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 4c02e6b..05f086f 100644 --- a/Modules/Launcher/Plugins/ApplicationsPlugin.qml +++ b/Modules/Launcher/Plugins/ApplicationsPlugin.qml @@ -82,7 +82,11 @@ Item { "isImage": false, "onActivate": function () { Logger.log("ApplicationsPlugin", `Launching: ${app.name}`) - if (app.execute) { + + 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) { app.execute() } else if (app.exec) { // Fallback to manual execution diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index d46dcfd..3382b69 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -155,7 +155,7 @@ Loader { anchors.topMargin: 80 * scaling spacing: 40 * scaling - Column { + ColumnLayout { spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignHCenter @@ -168,6 +168,7 @@ Loader { font.letterSpacing: -2 * scaling color: Color.mOnSurface horizontalAlignment: Text.AlignHCenter + Layout.alignment: Qt.AlignHCenter SequentialAnimation on scale { loops: Animation.Infinite @@ -192,22 +193,23 @@ Loader { font.weight: Font.Light color: Color.mOnSurface horizontalAlignment: Text.AlignHCenter - width: timeText.width + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: timeText.implicitWidth } } - Column { + ColumnLayout { spacing: Style.marginM * scaling Layout.alignment: Qt.AlignHCenter Rectangle { - width: 108 * scaling - height: 108 * scaling + Layout.preferredWidth: 108 * scaling + Layout.preferredHeight: 108 * scaling + Layout.alignment: Qt.AlignHCenter 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 { @@ -375,377 +377,371 @@ Loader { anchors.centerIn: parent anchors.verticalCenterOffset: 50 * 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 - } - } - } - } + 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: 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 - + 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 - easing.type: Easing.InOutQuad + duration: 2000 + Math.random() * 1000 } NumberAnimation { - to: 0.2 - duration: 2000 - easing.type: Easing.InOutQuad + to: 0.1 + duration: 2000 + Math.random() * 1000 } } } } + + 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 - Row { + // Power buttons at bottom right + RowLayout { anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: 50 * scaling spacing: 20 * scaling Rectangle { - width: 60 * scaling - height: 60 * scaling + Layout.preferredWidth: 60 * scaling + Layout.preferredHeight: 60 * scaling radius: width * 0.5 color: powerButtonArea.containsMouse ? Color.mError : Qt.alpha(Color.mError, 0.2) border.color: Color.mError @@ -769,8 +765,8 @@ Loader { } Rectangle { - width: 60 * scaling - height: 60 * scaling + Layout.preferredWidth: 60 * scaling + Layout.preferredHeight: 60 * scaling radius: width * 0.5 color: restartButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, Style.opacityLight) border.color: Color.mPrimary @@ -794,8 +790,8 @@ Loader { } Rectangle { - width: 60 * scaling - height: 60 * scaling + Layout.preferredWidth: 60 * scaling + Layout.preferredHeight: 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 009d717..fdbe0d2 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -78,7 +78,7 @@ Variants { } // Main notification container - Column { + ColumnLayout { id: notificationStack // Position based on bar location anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined @@ -92,8 +92,9 @@ Variants { Repeater { model: notificationModel delegate: Rectangle { - width: 360 * scaling - height: Math.max(80 * scaling, contentRow.implicitHeight + (Style.marginL * 2 * scaling)) + Layout.preferredWidth: 360 * scaling + Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling) + Layout.maximumHeight: Layout.preferredHeight clip: true radius: Style.radiusL * scaling border.color: Color.mOutline @@ -105,6 +106,17 @@ 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 @@ -156,104 +168,139 @@ Variants { } } - RowLayout { - id: contentRow + ColumnLayout { + id: notificationLayout anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginL * scaling + anchors.margins: Style.marginM * scaling + anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button + spacing: Style.marginM * scaling - // Right: header on top, then avatar + texts - ColumnLayout { - id: textColumn - spacing: Style.marginS * scaling + // Header section with app name and timestamp + RowLayout { Layout.fillWidth: true + spacing: Style.marginS * scaling - RowLayout { - spacing: Style.marginS * scaling - id: appHeaderRow - NText { - 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 - } + NText { + text: `${(model.appName || model.desktopEntry) + || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}` + color: Color.mSecondary + font.pointSize: Style.fontSizeXS * scaling } - RowLayout { - id: bodyRow - spacing: Style.marginM * 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 + } - 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 !== "") + 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 + spacing: Style.marginS * scaling + + NText { + text: model.summary || "No summary" + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + Layout.fillWidth: true + maximumLineCount: 3 + elide: Text.ElideRight } - Column { - id: textContent - spacing: Style.marginS * scaling + NText { + text: model.body || "" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurface + wrapMode: Text.WrapAtWordBoundaryOrAnywhere Layout.fillWidth: true - // 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 - } + maximumLineCount: 5 + elide: Text.ElideRight + visible: text.length > 0 } } } - // Actions removed + // 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 + } + } } + // Close button positioned absolutely NIconButton { icon: "close" tooltipText: "Close" - // Compact target (~24dp) and glyph (~16dp) - sizeRatio: 0.75 - fontPointSize: 16 + sizeRatio: 0.6 anchors.top: parent.top + anchors.topMargin: Style.marginM * scaling anchors.right: parent.right - anchors.margins: Style.marginS * scaling + anchors.rightMargin: Style.marginM * scaling onClicked: { animateOut() diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 3b10aec..39686df 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -25,6 +25,7 @@ NPanel { anchors.margins: Style.marginL * scaling spacing: Style.marginM * scaling + // Header section RowLayout { Layout.fillWidth: true spacing: Style.marginM * scaling @@ -43,6 +44,13 @@ 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" @@ -65,38 +73,44 @@ NPanel { } // Empty state when no notifications - Item { + ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter visible: NotificationService.historyModel.count === 0 + spacing: Style.marginL * scaling - ColumnLayout { - anchors.centerIn: parent - spacing: Style.marginM * scaling + Item { + Layout.fillHeight: true + } - NIcon { - text: "notifications_off" - font.pointSize: Style.fontSizeXXXL * scaling - color: Color.mOnSurface - Layout.alignment: Qt.AlignHCenter - } + NIcon { + text: "notifications_off" + font.pointSize: 64 * 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: "No notifications" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } - NText { - text: "Your notifications will show up here as they arrive." - font.pointSize: Style.fontSizeNormal * scaling - color: Color.mOnSurfaceVariant - 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 } } + // Notification list ListView { id: notificationList Layout.fillWidth: true @@ -108,21 +122,21 @@ NPanel { visible: NotificationService.historyModel.count > 0 delegate: Rectangle { - width: notificationList ? notificationList.width : 380 * scaling - height: Math.max(80, notificationContent.height + 30) + width: notificationList.width + height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2) 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 { - anchors { - fill: parent - margins: Style.marginM * scaling - } + id: notificationLayout + anchors.fill: parent + anchors.margins: Style.marginM * scaling spacing: Style.marginM * scaling - // Notification content - Column { - id: notificationContent + // Notification content column + ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter spacing: Style.marginXXS * scaling @@ -133,7 +147,8 @@ NPanel { font.weight: Font.Medium color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary wrapMode: Text.Wrap - width: parent.width - 60 + Layout.fillWidth: true + Layout.maximumWidth: parent.width maximumLineCount: 2 elide: Text.ElideRight } @@ -143,23 +158,27 @@ NPanel { font.pointSize: Style.fontSizeXS * scaling color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface wrapMode: Text.Wrap - width: parent.width - 60 + Layout.fillWidth: true + Layout.maximumWidth: parent.width 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 } } - // Trash icon button + // Delete button NIconButton { icon: "delete" tooltipText: "Delete notification" sizeRatio: 0.7 + Layout.alignment: Qt.AlignTop onClicked: { Logger.log("NotificationHistory", "Removing notification:", summary) @@ -172,7 +191,7 @@ NPanel { MouseArea { id: notificationMouseArea anchors.fill: parent - anchors.rightMargin: Style.marginL * 3 * scaling + anchors.rightMargin: Style.marginXL * scaling hoverEnabled: true } } diff --git a/Modules/PowerPanel/PowerPanel.qml b/Modules/PowerPanel/PowerPanel.qml index e6cfb01..efb475e 100644 --- a/Modules/PowerPanel/PowerPanel.qml +++ b/Modules/PowerPanel/PowerPanel.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Controls import QtQuick.Effects import QtQuick.Layouts import Quickshell @@ -16,6 +17,7 @@ NPanel { panelHeight: 380 * scaling panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true + panelKeyboardFocus: true // Timer properties property int timerDuration: 9000 // 9 seconds @@ -23,9 +25,44 @@ NPanel { property bool timerActive: false property int timeRemaining: 0 - // Cancel timer when panel is closing + // 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 + } + onClosed: { cancelTimer() + selectedIndex = 0 } // Timer management @@ -79,6 +116,38 @@ 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 @@ -93,8 +162,92 @@ 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 @@ -144,55 +297,21 @@ NPanel { Layout.fillWidth: true spacing: Style.marginM * scaling - // 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 + 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 + } } } } @@ -207,6 +326,7 @@ NPanel { property string subtitle: "" property bool pending: false property bool isShutdown: false + property bool isSelected: false signal clicked @@ -216,7 +336,7 @@ NPanel { if (pending) { return Qt.alpha(Color.mPrimary, 0.08) } - if (mouseArea.containsMouse) { + if (isSelected || mouseArea.containsMouse) { return Color.mSecondary } return Color.transparent @@ -242,13 +362,12 @@ NPanel { anchors.verticalCenter: parent.verticalCenter text: buttonRoot.icon color: { - if (buttonRoot.pending) return Color.mPrimary - if (buttonRoot.isShutdown && !mouseArea.containsMouse) + if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) return Color.mError - if (mouseArea.containsMouse) - return Color.mOnTertiary + if (buttonRoot.isSelected || mouseArea.containsMouse) + return Color.mOnSecondary return Color.mOnSurface } font.pointSize: Style.fontSizeXXXL * scaling @@ -264,7 +383,7 @@ NPanel { } // Text content in the middle - Column { + ColumnLayout { anchors.left: iconElement.right anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right anchors.verticalCenter: parent.verticalCenter @@ -279,10 +398,10 @@ NPanel { color: { if (buttonRoot.pending) return Color.mPrimary - if (buttonRoot.isShutdown && !mouseArea.containsMouse) + if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) return Color.mError - if (mouseArea.containsMouse) - return Color.mOnTertiary + if (buttonRoot.isSelected || mouseArea.containsMouse) + return Color.mOnSecondary return Color.mOnSurface } @@ -304,10 +423,10 @@ NPanel { color: { if (buttonRoot.pending) return Color.mPrimary - if (buttonRoot.isShutdown && !mouseArea.containsMouse) + if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) return Color.mError - if (mouseArea.containsMouse) - return Color.mOnTertiary + if (buttonRoot.isSelected || mouseArea.containsMouse) + return Color.mOnSecondary return Color.mOnSurfaceVariant } opacity: Style.opacityHeavy diff --git a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml index cccacbb..f9aa98d 100644 --- a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml +++ b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml @@ -68,6 +68,8 @@ 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 @@ -157,4 +159,28 @@ 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 2e1e4aa..1e6d6cc 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -267,234 +267,269 @@ NPanel { } panelContent: Rectangle { - anchors.fill: parent - anchors.margins: Style.marginL * scaling color: Color.transparent - // 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 { + // Main layout container that fills the panel + ColumnLayout { anchors.fill: parent - spacing: Style.marginM * scaling + anchors.margins: Style.marginL * scaling + spacing: 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 + // Keyboard shortcuts container + Item { + Layout.preferredWidth: 0 + Layout.preferredHeight: 0 - 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 - } + // Scrolling via keyboard + Shortcut { + sequence: "Down" + onActivated: root.scrollDown() + enabled: root.opened } - Column { - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginXS * 1.5 * scaling + Shortcut { + sequence: "Up" + onActivated: root.scrollUp() + 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+J" + onActivated: root.scrollDown() + enabled: root.opened + } - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } + Shortcut { + sequence: "Ctrl+K" + onActivated: root.scrollUp() + enabled: root.opened + } - Behavior on tabTextColor { - ColorAnimation { - duration: Style.animationFast - } - } + Shortcut { + sequence: "PgDown" + onActivated: root.scrollPageDown() + 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 - } - } - } + 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 } } - // Content - Rectangle { - id: contentPane + // Main content area + RowLayout { Layout.fillWidth: true Layout.fillHeight: true - radius: Style.radiusM * scaling - color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - clip: true + spacing: Style.marginM * scaling - ColumnLayout { - id: contentLayout - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginS * scaling + // 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 - RowLayout { - id: headerRow - Layout.fillWidth: true - spacing: Style.marginS * scaling - - // 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() - } + 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 + } } - NDivider { - Layout.fillWidth: true - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginS * scaling + spacing: Style.marginXS * 1.5 * scaling Repeater { + id: sections model: root.tabsModel - delegate: Loader { - anchors.fill: parent - active: index === root.currentTabIndex + 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) - 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 - } + Behavior on color { + ColorAnimation { + duration: Style.animationFast } } - sourceComponent: ColumnLayout { - ScrollView { - id: scrollView + 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 - Layout.fillHeight: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - padding: Style.marginL * scaling - clip: true + } + } - Component.onCompleted: { - root.activeScrollView = scrollView + 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 + 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 { + Layout.fillWidth: true + } + + // Tab content area + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.transparent + + 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 + } } + } - Loader { - active: true - sourceComponent: root.tabsModel[index].source - width: scrollView.availableWidth + 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 + + 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 + } + + 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 a4cb10e..1fffadb 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: updateText.implicitWidth + 46 * scaling + Layout.preferredWidth: Math.round(updateRow.implicitWidth + (Style.marginL * scaling * 2)) Layout.preferredHeight: Math.round(Style.barHeight * scaling) radius: Style.radiusL * scaling color: updateArea.containsMouse ? Color.mPrimary : Color.transparent @@ -85,11 +85,12 @@ ColumnLayout { } RowLayout { + id: updateRow anchors.centerIn: parent spacing: Style.marginS * scaling NIcon { - text: "system_update" + text: "download" 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 c0d5f54..7407112 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -22,9 +22,11 @@ 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 @@ -75,6 +77,45 @@ 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 195844c..461a4b8 100644 --- a/Modules/SettingsPanel/Tabs/HooksTab.qml +++ b/Modules/SettingsPanel/Tabs/HooksTab.qml @@ -5,94 +5,85 @@ import qs.Commons import qs.Services import qs.Widgets -ScrollView { - id: root +ColumnLayout { + id: contentColumn + spacing: Style.marginL * scaling + width: root.width - property real scaling: 1.0 - - contentWidth: contentColumn.width - contentHeight: contentColumn.height + // 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 + } ColumnLayout { - id: contentColumn + visible: Settings.data.hooks.enabled spacing: Style.marginL * scaling - width: root.width + 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 + NDivider { + Layout.fillWidth: true } + // 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 { - visible: Settings.data.hooks.enabled - spacing: Style.marginL * scaling + spacing: Style.marginM * scaling Layout.fillWidth: true - NDivider { - 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" } - // 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)" - } + 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 f303273..6ca4ece 100644 --- a/Modules/SettingsPanel/Tabs/LauncherTab.qml +++ b/Modules/SettingsPanel/Tabs/LauncherTab.qml @@ -59,6 +59,13 @@ 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 4b56ea7..0e1fd0d 100644 --- a/Modules/SettingsPanel/Tabs/NetworkTab.qml +++ b/Modules/SettingsPanel/Tabs/NetworkTab.qml @@ -12,22 +12,14 @@ ColumnLayout { spacing: Style.marginL * scaling NToggle { - label: "WiFi Enabled" - description: "Enable WiFi connectivity." + label: "Enable Wi-Fi" + description: "Enable Wi-Fi connectivity." checked: Settings.data.network.wifiEnabled - onToggled: checked => { - Settings.data.network.wifiEnabled = checked - NetworkService.setWifiEnabled(checked) - if (checked) { - ToastService.showNotice("WiFi", "Enabled") - } else { - ToastService.showNotice("WiFi", "Disabled") - } - } + onToggled: checked => NetworkService.setWifiEnabled(checked) } NToggle { - label: "Bluetooth Enabled" + label: "Enable Bluetooth" 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 e2832dd..0d40d33 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -115,7 +115,6 @@ ColumnLayout { NColorPicker { selectedColor: Settings.data.wallpaper.fillColor onColorSelected: color => Settings.data.wallpaper.fillColor = color - onColorCancelled: selectedColor = Settings.data.wallpaper.fillColor } } @@ -278,7 +277,6 @@ 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 2bdd88a..8eb28e8 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 951beba..4c2d1ce 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,19 +104,7 @@ NBox { stdout: StdioCollector { onStreamFinished: { var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0]) - 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" - } - + uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds) uptimeProcess.running = false } } diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml index 9f05e66..2fc18de 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 - Column { + ColumnLayout { id: content anchors.left: parent.left anchors.right: parent.right @@ -22,11 +22,6 @@ NBox { anchors.bottomMargin: Style.marginM * scaling spacing: Style.marginS * scaling - // Slight top padding - Item { - height: Style.marginXS * scaling - } - NCircleStat { value: SystemStatService.cpuUsage icon: "speed" @@ -60,10 +55,5 @@ 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 f295224..78fc702 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 1a77e69..1fa5f18 100644 --- a/Modules/WiFiPanel/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -14,16 +14,11 @@ NPanel { panelHeight: 500 * scaling panelKeyboardFocus: true - property string passwordPromptSsid: "" + property string passwordSsid: "" property string passwordInput: "" - property bool showPasswordPrompt: false - property string expandedNetwork: "" // Track which network shows options + property string expandedSsid: "" - onOpened: { - if (Settings.data.network.wifiEnabled) { - NetworkService.refreshNetworks() - } - } + onOpened: NetworkService.scan() panelContent: Rectangle { color: Color.transparent @@ -39,35 +34,32 @@ NPanel { spacing: Style.marginM * scaling NIcon { - text: "wifi" + text: Settings.data.network.wifiEnabled ? "wifi" : "wifi_off" font.pointSize: Style.fontSizeXXL * scaling - color: Color.mPrimary + color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant } NText { - text: "WiFi" + text: "Wi-Fi" font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightBold color: Color.mOnSurface Layout.fillWidth: true - Layout.leftMargin: Style.marginS * scaling } - // Connection status indicator - Rectangle { - visible: NetworkService.hasActiveConnection - width: 8 * scaling - height: 8 * scaling - radius: 4 * scaling - color: Color.mPrimary + NToggle { + id: wifiSwitch + checked: Settings.data.network.wifiEnabled + onToggled: checked => NetworkService.setWifiEnabled(checked) + baseSize: Style.baseWidgetSize * 0.65 * scaling } NIconButton { icon: "refresh" - tooltipText: "Refresh networks" + tooltipText: "Refresh" sizeRatio: 0.8 - enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading - onClicked: NetworkService.refreshNetworks() + enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning + onClicked: NetworkService.scan() } NIconButton { @@ -82,17 +74,18 @@ NPanel { Layout.fillWidth: true } - // Error banner + // Error message Rectangle { - visible: NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0 + visible: NetworkService.lastError.length > 0 Layout.fillWidth: true - Layout.preferredHeight: errorText.implicitHeight + (Style.marginM * scaling * 2) + Layout.preferredHeight: errorRow.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 @@ -104,8 +97,7 @@ NPanel { } NText { - id: errorText - text: NetworkService.connectError + text: NetworkService.lastError color: Color.mError font.pointSize: Style.fontSizeS * scaling wrapMode: Text.Wrap @@ -115,301 +107,364 @@ NPanel { NIconButton { icon: "close" sizeRatio: 0.6 - onClicked: { - NetworkService.connectStatus = "" - NetworkService.connectError = "" - } + onClicked: NetworkService.lastError = "" } } } - ScrollView { + // Main content area + Rectangle { Layout.fillWidth: true Layout.fillHeight: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - clip: true - contentWidth: availableWidth + color: Color.transparent + // WiFi disabled state ColumnLayout { - width: parent.width + visible: !Settings.data.network.wifiEnabled + anchors.fill: parent spacing: Style.marginM * scaling - // Loading state - 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 - - NBusyIndicator { - running: true - color: Color.mPrimary - size: Style.baseWidgetSize * scaling - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Scanning for networks..." - font.pointSize: Style.fontSizeNormal * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } + Item { + Layout.fillHeight: true } - // WiFi disabled state + 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 + ColumnLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - visible: !Settings.data.network.wifiEnabled + width: parent.width spacing: Style.marginM * scaling - NIcon { - text: "wifi_off" - font.pointSize: Style.fontSizeXXXL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } + // Network list + Repeater { + model: { + if (!Settings.data.network.wifiEnabled) + return [] - 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) + 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 + }) } - } - } - - // 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 { - id: networkRect - width: parent.width - implicitHeight: networkContent.implicitHeight + (Style.marginM * scaling * 2) + Layout.fillWidth: true + implicitHeight: netColumn.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 - clip: true + + // Smooth opacity animation + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + } + } ColumnLayout { - id: networkContent + id: netColumn width: parent.width - (Style.marginM * scaling * 2) x: Style.marginM * scaling y: Style.marginM * scaling - spacing: Style.marginM * scaling + spacing: Style.marginS * scaling - // Main network row + // Main 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 - Layout.alignment: Qt.AlignVCenter - spacing: 0 + spacing: 2 * scaling NText { - text: modelData.ssid || "Unknown Network" + text: modelData.ssid font.pointSize: Style.fontSizeNormal * scaling font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium - elide: Text.ElideRight color: Color.mOnSurface + elide: Text.ElideRight Layout.fillWidth: true } - 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 - } - } - - // Right-aligned items container - RowLayout { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - spacing: Style.marginS * scaling - - // Connected badge - Rectangle { - visible: modelData.connected - color: Color.mPrimary - radius: width * 0.5 - width: connectedLabel.implicitWidth + (Style.marginS * scaling * 2) - height: connectedLabel.implicitHeight + (Style.marginXS * scaling * 2) + RowLayout { + spacing: Style.marginXS * scaling NText { - id: connectedLabel - anchors.centerIn: parent - text: "Connected" - font.pointSize: Style.fontSizeXXS * scaling - color: Color.mOnPrimary - } - } - - // 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" + text: `${modelData.signal}%` font.pointSize: Style.fontSizeXXS * scaling color: Color.mOnSurfaceVariant } - } - // Loading indicator - NBusyIndicator { - visible: NetworkService.connectingSsid === modelData.ssid - running: NetworkService.connectingSsid === modelData.ssid - color: Color.mPrimary - size: Style.baseWidgetSize * 0.6 * scaling - } + NText { + text: "•" + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnSurfaceVariant + } - // Action buttons - RowLayout { - spacing: Style.marginXS * scaling - visible: NetworkService.connectingSsid !== modelData.ssid + NText { + text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open" + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnSurfaceVariant + } - 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()) - } + 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 } } - NButton { - visible: modelData.connected - outlined: !hovered - fontSize: Style.fontSizeXS * scaling - backgroundColor: Color.mError - text: "Disconnect" - onClicked: NetworkService.disconnectNetwork(modelData.ssid) + 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 + } + } + } + } + + // Action area + RowLayout { + spacing: Style.marginS * scaling + + NBusyIndicator { + visible: NetworkService.connectingTo === modelData.ssid + || NetworkService.disconnectingFrom === modelData.ssid + || NetworkService.forgettingNetwork === modelData.ssid + running: visible + color: Color.mPrimary + size: Style.baseWidgetSize * 0.5 * scaling + } + + 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" + } + 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 = "" + } + } + } + + NButton { + visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid + text: "Disconnect" + outlined: !hovered + fontSize: Style.fontSizeXS * scaling + backgroundColor: Color.mError + onClicked: NetworkService.disconnect(modelData.ssid) } } } - // Password input section + // Password input Rectangle { - visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt + visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid + && NetworkService.forgettingNetwork !== modelData.ssid Layout.fillWidth: true - implicitHeight: visible ? 50 * scaling : 0 + height: passwordRow.implicitHeight + Style.marginS * scaling * 2 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.marginS * scaling + spacing: Style.marginM * scaling Rectangle { Layout.fillWidth: true Layout.fillHeight: true - radius: Style.radiusS * scaling + radius: Style.radiusXS * scaling color: Color.mSurface - border.color: passwordInputField.activeFocus ? Color.mSecondary : Color.mOutline + border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline border.width: Math.max(1, Style.borderS * scaling) TextInput { - id: passwordInputField + id: pwdInput anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.marginM * scaling - anchors.rightMargin: Style.marginM * scaling - height: parent.height + anchors.margins: Style.marginS * scaling text: passwordInput - font.pointSize: Style.fontSizeM * scaling + font.pointSize: Style.fontSizeS * scaling color: Color.mOnSurface - verticalAlignment: TextInput.AlignVCenter - clip: true - focus: modelData.ssid === passwordPromptSsid && showPasswordPrompt - selectByMouse: true echoMode: TextInput.Password + selectByMouse: true + focus: visible passwordCharacter: "●" onTextChanged: passwordInput = text + onVisibleChanged: if (visible) + forceActiveFocus() onAccepted: { - if (passwordInput) { - NetworkService.submitPassword(passwordPromptSsid, passwordInput) - showPasswordPrompt = false - expandedNetwork = "" + if (text) { + NetworkService.connect(passwordSsid, text) + passwordSsid = "" + passwordInput = "" } } @@ -425,56 +480,75 @@ NPanel { NButton { text: "Connect" - icon: "check" - fontSize: Style.fontSizeXS * scaling + fontSize: Style.fontSizeXXS * scaling enabled: passwordInput.length > 0 - outlined: !enabled + outlined: true onClicked: { - if (passwordInput) { - NetworkService.submitPassword(passwordPromptSsid, passwordInput) - showPasswordPrompt = false - expandedNetwork = "" - } + NetworkService.connect(passwordSsid, passwordInput) + passwordSsid = "" + passwordInput = "" } } NIconButton { icon: "close" - tooltipText: "Cancel" - sizeRatio: 0.9 + sizeRatio: 0.8 onClicked: { - showPasswordPrompt = false - expandedNetwork = "" + passwordSsid = "" passwordInput = "" } } } } - // Forget network option - appears when saved badge is clicked - RowLayout { - visible: (modelData.existing || modelData.cached) && expandedNetwork === modelData.ssid - && !showPasswordPrompt + // Forget network + Rectangle { + visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid + && NetworkService.forgettingNetwork !== modelData.ssid Layout.fillWidth: true - Layout.topMargin: Style.marginXS * scaling - spacing: Style.marginS * scaling + 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 - Item { - Layout.fillWidth: true - } + RowLayout { + id: forgetRow + anchors.fill: parent + anchors.margins: Style.marginS * scaling + spacing: Style.marginM * scaling - 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 = "" + 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 = "" } } } @@ -482,35 +556,42 @@ NPanel { } } } + } - // 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 + // 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 - NIcon { - text: "wifi_find" - font.pointSize: Style.fontSizeXXXL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } + Item { + Layout.fillHeight: true + } - NText { - text: "No networks found" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } + NIcon { + text: "wifi_find" + font.pointSize: 64 * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } - NButton { - text: "Refresh" - icon: "refresh" - Layout.alignment: Qt.AlignHCenter - onClicked: NetworkService.refreshNetworks() - } + 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 } } } diff --git a/README.md b/README.md index 223936c..dcdcd20 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,7 @@ 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` | @@ -265,6 +266,10 @@ 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 @@ -279,6 +284,8 @@ 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 65897a6..39afcd3 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -28,6 +28,7 @@ Singleton { "PowerToggle": powerToggleComponent, "ScreenRecorderIndicator": screenRecorderIndicatorComponent, "SidePanelToggle": sidePanelToggleComponent, + "Spacer": spacerComponent, "SystemMonitor": systemMonitorComponent, "Taskbar": taskbarComponent, "Tray": trayComponent, @@ -43,6 +44,11 @@ Singleton { "leftClickExec": "", "rightClickExec": "", "middleClickExec": "" + }, + "Spacer": { + "allowUserSettings": true, + "icon": "space_bar", + "width": 20 } }) @@ -101,6 +107,9 @@ 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 fa34523..c4b5820 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -8,215 +8,239 @@ import qs.Commons Singleton { id: root - // Core properties + // Core state property var networks: ({}) - 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 + property bool scanning: false + property bool connecting: false + property string connectingTo: "" + property string lastError: "" + property bool ethernetConnected: false + property string disconnectingFrom: "" + property string forgettingNetwork: "" - // File path for persistent storage + // Persistent cache property string cacheFile: Settings.cacheDir + "network.json" + readonly property string cachedLastConnected: cacheAdapter.lastConnected + readonly property var cachedNetworks: cacheAdapter.knownNetworks - // Stable properties for UI - readonly property alias cache: adapter - readonly property string lastConnectedNetwork: adapter.lastConnected - - // File-based persistent storage + // Cache file handling FileView { id: cacheFileView path: root.cacheFile - 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 + id: cacheAdapter property var knownNetworks: ({}) property string lastConnected: "" - property int lastRefresh: 0 + } + + onLoadFailed: { + cacheAdapter.knownNetworks = ({}) + cacheAdapter.lastConnected = "" } } - // Save timer to batch writes + Connections { + target: Settings.data.network + function onWifiEnabledChanged() { + if (Settings.data.network.wifiEnabled) { + ToastService.showNotice("Wi-Fi", "Enabled") + } else { + ToastService.showNotice("Wi-Fi", "Disabled") + } + } + } + + Component.onCompleted: { + Logger.log("Network", "Service initialized") + syncWifiState() + refresh() + } + + // Save cache with debounce Timer { - id: saveTimer - running: false + id: saveDebounce interval: 1000 onTriggered: cacheFileView.writeAdapter() } - Component.onCompleted: { - Logger.log("Network", "Service started") + 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 if (Settings.data.network.wifiEnabled) { - refreshNetworks() + scan() } } - // Signal strength icon mapping - function signalIcon(signal) { - 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.trim() !== "" && security.trim() !== "--" - } - - // Enhanced refresh with retry logic - function refreshNetworks() { - if (isLoading) + function scan() { + if (scanning) return - isLoading = true - retryCount = 0 - adapter.lastRefresh = Date.now() - performRefresh() + scanning = true + lastError = "" + scanProcess.running = true + Logger.log("Network", "Wi-Fi scan in progress...") } - function performRefresh() { - checkEthernet.running = true - existingNetworkProcess.running = true - } + function connect(ssid, password = "") { + if (connecting) + return - // Retry mechanism for failed operations - function retryRefresh() { - if (retryCount < maxRetries) { - retryCount++ - Logger.log("Network", `Retrying refresh (${retryCount}/${maxRetries})`) - retryTimer.start() + 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 { - isLoading = false - connectError = "Failed to refresh networks after multiple attempts" + connectProcess.mode = "new" + connectProcess.ssid = ssid + connectProcess.password = password } + + connectProcess.running = true } - Timer { - id: retryTimer - interval: 1000 * retryCount // Progressive backoff - repeat: false - onTriggered: performRefresh() + function disconnect(ssid) { + disconnectingFrom = ssid + disconnectProcess.ssid = ssid + disconnectProcess.running = true } - 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}`) + function forget(ssid) { + forgettingNetwork = ssid // Remove from cache - let known = adapter.knownNetworks + let known = cacheAdapter.knownNetworks delete known[ssid] - adapter.knownNetworks = known + cacheAdapter.knownNetworks = known - // Clear last connected if it's this network - if (adapter.lastConnected === ssid) { - adapter.lastConnected = "" + if (cacheAdapter.lastConnected === ssid) { + cacheAdapter.lastConnected = "" } - // Save changes - saveTimer.restart() + saveCache() - // Remove NetworkManager profile + // 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 + 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" + return "signal_wifi_0_bar" + } + + function isSecured(security) { + return security && security !== "--" && security.trim() !== "" + } + + // Processes Process { - id: forgetProcess - property string ssid: "" + id: ethernetStateProcess running: false - command: ["nmcli", "connection", "delete", "id", ssid] + command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] stdout: StdioCollector { onStreamFinished: { - 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() + 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) } } } } - // 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: wifiRadioProcess + 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 + } + } + } + } + + Process { + id: wifiToggleProcess property string action: "on" running: false command: ["nmcli", "radio", "wifi", action] @@ -224,10 +248,12 @@ Singleton { onRunningChanged: { if (!running) { if (action === "on") { - wifiEnableTimer.start() + // Clear networks immediately and start delayed scan + root.networks = ({}) + delayedScanTimer.interval = 8000 + delayedScanTimer.restart() } else { root.networks = ({}) - root.isLoading = false } } } @@ -235,137 +261,177 @@ Singleton { stderr: StdioCollector { onStreamFinished: { if (text.trim()) { - Logger.warn("Network", `Error ${action === "on" ? "enabling" : "disabling"} WiFi: ${text}`) + Logger.warn("Network", "WiFi toggle error: " + text) } } } } - Timer { - id: wifiEnableTimer - interval: 2000 - repeat: false - onTriggered: { - refreshNetworks() - if (adapter.lastConnected) { - reconnectTimer.start() + 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: 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: { - const cmd = ["nmcli", "device", "wifi", "connect", ssid] - if (isSecured && password) { - cmd.push("password", password) - } - return cmd - } - - stdout: StdioCollector { - onStreamFinished: { - handleConnectionSuccess(connectProcess.ssid) - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - handleConnectionError(connectProcess.ssid, text) + 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 } } - } - - Process { - id: upConnectionProcess - property string profileName: "" - running: false - command: ["nmcli", "connection", "up", "id", profileName] stdout: StdioCollector { onStreamFinished: { - handleConnectionSuccess(upConnectionProcess.profileName) + // 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() } } stderr: StdioCollector { onStreamFinished: { + root.connecting = false + root.connectingTo = "" + if (text.trim()) { - handleConnectionError(upConnectionProcess.profileName, text) + // 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() + } + + Logger.warn("Network", "Connect error: " + text) } } } @@ -377,221 +443,101 @@ Singleton { running: false command: ["nmcli", "connection", "down", "id", ssid] - onRunningChanged: { - if (!running) { - connectingSsid = "" - connectStatus = "" - connectStatusSsid = "" - connectError = "" - refreshNetworks() + 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() } } stderr: StdioCollector { onStreamFinished: { + root.disconnectingFrom = "" if (text.trim()) { - Logger.warn("Network", `Disconnect warning: ${text}`) + Logger.warn("Network", "Disconnect error: " + text) } + // Still trigger a scan even on error + delayedScanTimer.interval = 1000 + delayedScanTimer.restart() } } } - // 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 + id: forgetProcess + property string ssid: "" running: false - command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] + + // 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] stdout: StdioCollector { onStreamFinished: { - const profiles = {} - const lines = text.split("\n").filter(l => l.trim()) + Logger.log("Network", `Forget network: "${forgetProcess.ssid}"`) + Logger.log("Network", text.trim().replace(/[\r\n]/g, " ")) - 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 - } - } + // 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 } - scanProcess.existingProfiles = profiles - scanProcess.running = true + root.forgettingNetwork = "" + + // Quick scan to verify the profile is gone + delayedScanTimer.interval = 500 + delayedScanTimer.restart() } } stderr: StdioCollector { onStreamFinished: { - if (text.trim()) { - Logger.warn("Network", "Error listing connections:", text) - retryRefresh() + root.forgettingNetwork = "" + if (text.trim() && !text.includes("No profiles found")) { + Logger.warn("Network", "Forget error: " + text) } + // Still Trigger a scan even on error + delayedScanTimer.interval = 500 + delayedScanTimer.restart() } } } - - Process { - id: scanProcess - property var existingProfiles: ({}) - running: false - command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] - - stdout: StdioCollector { - onStreamFinished: { - const networksMap = {} - const lines = text.split("\n").filter(l => l.trim()) - - 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.networks = networksMap - root.isLoading = false - scanProcess.existingProfiles = {} - - //Logger.log("Network", `Found ${Object.keys(networksMap).length} wireless networks`) - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - Logger.warn("Network", "Error scanning networks:", text) - retryRefresh() - } - } - } - } - - 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 a79b812..4ce9747 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 notifications are suppressed - if (Settings.data.notifications && Settings.data.notifications.suppressed) { - // Still add to history but don't show notification - root.addToHistory(notification) + // Check if do-not-disturb is enabled + if (Settings.data.notifications && Settings.data.notifications.doNotDisturb) { return } @@ -46,8 +46,6 @@ Singleton { // Add to our model root.addNotification(notification) - // Also add to history - root.addToHistory(notification) } } @@ -109,6 +107,15 @@ 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 57a7346..4f09c1d 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(1) + "KB/s" + return (bytesPerSecond / 1024).toFixed(0) + "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 6c0be38..edff04b 100644 --- a/Services/ToastService.qml +++ b/Services/ToastService.qml @@ -165,13 +165,21 @@ 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) - // Process queue if not currently showing a toast - if (!isShowingToast) { - processQueue() - } + // Always process immediately for instant display + processQueue() } // Process the message queue @@ -181,11 +189,6 @@ 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 7ec8557..c4ec7d2 100644 --- a/Services/UpdateService.qml +++ b/Services/UpdateService.qml @@ -8,7 +8,7 @@ Singleton { id: root // Public properties - property string baseVersion: "2.5.0" + property string baseVersion: "2.6.0" property bool isDevelopment: true property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}` diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index 1bd9e67..e16cb12 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -1,9 +1,10 @@ import QtQuick +import QtQuick.Layouts import qs.Commons import qs.Services import qs.Widgets -// Compact circular statistic display used in the SidePanel +// Compact circular statistic display using Layout management Rectangle { id: root @@ -28,20 +29,20 @@ Rectangle { // Repaint gauge when the bound value changes onValueChanged: gauge.requestPaint() - Row { - id: innerRow + ColumnLayout { + id: mainLayout anchors.fill: parent anchors.margins: Style.marginS * scaling * contentScale - spacing: Style.marginS * scaling * contentScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter + spacing: 0 - // Gauge with percentage label placed inside the open gap (right side) + // Main gauge container Item { - id: gaugeWrap - anchors.verticalCenter: innerRow.verticalCenter - width: 68 * scaling * contentScale - height: 68 * scaling * contentScale + id: gaugeContainer + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: 68 * scaling * contentScale + Layout.preferredHeight: 68 * scaling * contentScale Canvas { id: gauge @@ -84,15 +85,13 @@ Rectangle { horizontalAlignment: Text.AlignHCenter } - // Tiny circular badge for the icon, inside the right-side gap + // Tiny circular badge for the icon, positioned using anchors within the gauge 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 f1c0a9b..aa8ce33 100644 --- a/Widgets/NClock.qml +++ b/Widgets/NClock.qml @@ -18,6 +18,7 @@ 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 8dd348e..830ba84 100644 --- a/Widgets/NColorPicker.qml +++ b/Widgets/NColorPicker.qml @@ -8,39 +8,34 @@ Rectangle { id: root property color selectedColor: "#000000" - property bool expanded: false signal colorSelected(color color) - signal colorCancelled - implicitWidth: expanded ? 320 * scaling : 150 * scaling - implicitHeight: expanded ? 300 * scaling : 40 * scaling + implicitWidth: 150 * scaling + implicitHeight: 40 * scaling radius: Style.radiusM * scaling color: Color.mSurface border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) - 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 + // Minimized Look MouseArea { - visible: !root.expanded anchors.fill: parent cursorShape: Qt.PointingHandCursor - onClicked: root.expanded = true + 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() + } RowLayout { anchors.fill: parent @@ -68,119 +63,4 @@ 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 new file mode 100644 index 0000000..324e5b6 --- /dev/null +++ b/Widgets/NColorPickerDialog.qml @@ -0,0 +1,516 @@ +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 0650839..c9755b3 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -15,7 +15,6 @@ 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 @@ -41,7 +40,7 @@ Rectangle { NIcon { text: root.icon - font.pointSize: root.fontPointSize * scaling + font.pointSize: Style.fontSizeM * scaling color: root.hovering ? colorFgHover : colorFg // Center horizontally x: (root.width - width) / 2 diff --git a/Widgets/NInputAction.qml b/Widgets/NInputAction.qml index 1f8ce5e..785b5b0 100644 --- a/Widgets/NInputAction.qml +++ b/Widgets/NInputAction.qml @@ -3,7 +3,8 @@ import QtQuick.Layouts import qs.Commons import qs.Widgets -ColumnLayout { +// Input and button row +RowLayout { id: root // Public properties @@ -21,57 +22,35 @@ ColumnLayout { // Internal properties property real scaling: 1.0 + spacing: Style.marginM * scaling - // Label - NText { - text: root.label - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSurface + NTextInput { + id: textInput + label: root.label + description: root.description + placeholderText: root.placeholderText + text: root.text + onEditingFinished: { + root.text = text + root.editingFinished() + } Layout.fillWidth: true } - // Description - NText { - text: root.description - font.pointSize: Style.fontSizeS * scaling - color: Color.mOnSurfaceVariant - wrapMode: Text.Wrap - Layout.fillWidth: true - } + NButton { + Layout.fillWidth: false + Layout.alignment: Qt.AlignBottom - // Input and button row - RowLayout { - spacing: Style.marginM * scaling - Layout.fillWidth: true + text: root.actionButtonText + icon: root.actionButtonIcon + backgroundColor: Color.mSecondary + textColor: Color.mOnSecondary + hoverColor: Color.mTertiary + pressColor: Color.mPrimary + enabled: root.actionButtonEnabled - 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() - } + onClicked: { + root.actionClicked() } } } diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 1fcccfc..2432544 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -231,8 +231,7 @@ 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 1cb7141..3db9d9b 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -11,7 +11,6 @@ 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 @@ -26,7 +25,6 @@ ColumnLayout { signal editingFinished spacing: Style.marginS * scaling - implicitHeight: frame.height NLabel { label: root.label @@ -34,6 +32,7 @@ ColumnLayout { labelColor: root.labelColor descriptionColor: root.descriptionColor visible: root.label !== "" || root.description !== "" + Layout.fillWidth: true } // Container @@ -42,50 +41,48 @@ ColumnLayout { Layout.fillWidth: true Layout.minimumWidth: 80 * scaling - Layout.maximumWidth: root.inputMaxWidth - - implicitWidth: parent.width implicitHeight: Style.baseWidgetSize * 1.1 * scaling + radius: Style.radiusM * scaling color: Color.mSurface - border.color: Color.mOutline + border.color: input.activeFocus ? Color.mSecondary : Color.mOutline border.width: Math.max(1, Style.borderS * scaling) - // 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 - } + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast } } - RowLayout { + TextField { + id: input + anchors.fill: parent anchors.leftMargin: Style.marginM * scaling anchors.rightMargin: Style.marginM * scaling - spacing: Style.marginS * scaling - 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() - } + 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() } } } diff --git a/Widgets/NToast.qml b/Widgets/NToast.qml index 44a0bb5..7a60c6c 100644 --- a/Widgets/NToast.qml +++ b/Widgets/NToast.qml @@ -37,7 +37,16 @@ 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() @@ -81,7 +90,6 @@ Item { // Main toast container Rectangle { - id: container anchors.fill: parent radius: Style.radiusL * scaling @@ -137,43 +145,41 @@ Item { } // Label and description - Column { - id: textColumn + ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter NText { - id: labelText + Layout.fillWidth: true 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 { - id: descriptionText + Layout.fillWidth: true 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 - color: Color.mOnSurface + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + colorBorder: Color.transparent + colorBorderHover: Color.mOutline - fontPointSize: Style.fontSizeM * scaling sizeRatio: 0.8 Layout.alignment: Qt.AlignTop