diff --git a/shell/modules/BluetoothMenu.qml b/shell/applets/BluetoothApplet.qml similarity index 56% rename from shell/modules/BluetoothMenu.qml rename to shell/applets/BluetoothApplet.qml index f348e90..64d2584 100644 --- a/shell/modules/BluetoothMenu.qml +++ b/shell/applets/BluetoothApplet.qml @@ -1,46 +1,10 @@ import QtQuick -import Quickshell -import "." as M import "../services" as S -M.HoverPanel { - id: menuWindow +Column { + id: root - contentWidth: 250 - panelNamespace: "nova-bluetooth" - popupMode: true - panelTitle: "Bluetooth" - titleActionsComponent: Component { - Item { - width: 20 - height: 20 - - Text { - anchors.centerIn: parent - text: "\uF011" - color: S.BluetoothService.enabled ? menuWindow.accentColor : S.Theme.base04 - font.pixelSize: S.Theme.fontSize - font.family: S.Theme.iconFontFamily - - Behavior on color { - ColorAnimation { - duration: 100 - } - } - } - - HoverHandler { - cursorShape: Qt.PointingHandCursor - } - - TapHandler { - onTapped: S.BluetoothService.setPower(!S.BluetoothService.enabled) - } - } - } - - onVisibleChanged: if (visible) - S.BluetoothService.refresh() + required property color accentColor Repeater { model: S.BluetoothService.devices @@ -50,7 +14,9 @@ M.HoverPanel { required property var modelData required property int index - width: menuWindow.contentWidth + readonly property bool _pending: S.BluetoothService.pendingMac === entry.modelData.mac + + width: root.width height: 32 Rectangle { @@ -67,7 +33,7 @@ M.HoverPanel { anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: "\uF294" - color: entry.modelData.connected ? menuWindow.accentColor : S.Theme.base04 + color: entry._pending || entry.modelData.connected ? root.accentColor : S.Theme.base04 font.pixelSize: S.Theme.fontSize + 1 font.family: S.Theme.iconFontFamily } @@ -75,27 +41,46 @@ M.HoverPanel { Text { anchors.left: btIcon.right anchors.leftMargin: 8 - anchors.right: batLabel.left + anchors.right: _statusLabel.left anchors.rightMargin: 4 anchors.verticalCenter: parent.verticalCenter text: entry.modelData.name - color: entry.modelData.connected ? menuWindow.accentColor : S.Theme.base05 + color: entry._pending || entry.modelData.connected ? root.accentColor : S.Theme.base05 font.pixelSize: S.Theme.fontSize font.family: S.Theme.fontFamily - font.bold: entry.modelData.connected + font.bold: entry.modelData.connected || entry._pending elide: Text.ElideRight } Text { - id: batLabel + id: _statusLabel anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter - text: entry.modelData.battery >= 0 ? entry.modelData.battery + "%" : "" + text: entry._pending ? (entry.modelData.connected ? "disconnecting..." : "connecting...") : (entry.modelData.battery >= 0 ? entry.modelData.battery + "%" : "") color: S.Theme.base04 font.pixelSize: S.Theme.fontSize - 1 font.family: S.Theme.fontFamily - width: entry.modelData.battery >= 0 ? implicitWidth : 0 + font.italic: entry._pending + width: text ? implicitWidth : 0 + } + + // Pulse animation while pending + SequentialAnimation on opacity { + loops: Animation.Infinite + running: entry._pending + NumberAnimation { + to: 0.5 + duration: 400 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1 + duration: 400 + easing.type: Easing.InOutQuad + } + onRunningChanged: if (!running) + entry.opacity = 1 } HoverHandler { @@ -104,8 +89,8 @@ M.HoverPanel { } TapHandler { onTapped: { - S.BluetoothService.toggleDevice(entry.modelData.mac, !entry.modelData.connected); - menuWindow.keepOpen(500); + if (!entry._pending) + S.BluetoothService.toggleDevice(entry.modelData.mac, !entry.modelData.connected); } } } @@ -113,7 +98,7 @@ M.HoverPanel { Text { visible: S.BluetoothService.devices.length === 0 - width: menuWindow.contentWidth + width: root.width height: 32 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter diff --git a/shell/modules/NetworkMenu.qml b/shell/applets/NetworkApplet.qml similarity index 67% rename from shell/modules/NetworkMenu.qml rename to shell/applets/NetworkApplet.qml index f9c656f..c2aa8e7 100644 --- a/shell/modules/NetworkMenu.qml +++ b/shell/applets/NetworkApplet.qml @@ -1,45 +1,10 @@ import QtQuick -import Quickshell -import "." as M import "../services" as S -M.HoverPanel { - id: menuWindow +Column { + id: root - contentWidth: 250 - panelNamespace: "nova-network" - panelTitle: "Wi-Fi" - titleActionsComponent: Component { - Item { - width: 20 - height: 20 - - Text { - anchors.centerIn: parent - text: "\uF011" - color: S.NetworkService.wifiEnabled ? menuWindow.accentColor : S.Theme.base04 - font.pixelSize: S.Theme.fontSize - font.family: S.Theme.iconFontFamily - - Behavior on color { - ColorAnimation { - duration: 100 - } - } - } - - HoverHandler { - cursorShape: Qt.PointingHandCursor - } - - TapHandler { - onTapped: S.NetworkService.setWifi(!S.NetworkService.wifiEnabled) - } - } - } - - onVisibleChanged: if (visible) - S.NetworkService.refresh() + required property color accentColor Repeater { model: S.NetworkService.networks @@ -49,7 +14,7 @@ M.HoverPanel { required property var modelData required property int index - width: menuWindow.contentWidth + width: root.width height: 32 Rectangle { @@ -66,7 +31,7 @@ M.HoverPanel { anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: entry.modelData.isWifi ? "\uF1EB" : "\uDB80\uDE00" - color: entry.modelData.active ? menuWindow.accentColor : S.Theme.base05 + color: entry.modelData.active ? root.accentColor : S.Theme.base05 font.pixelSize: S.Theme.fontSize + 1 font.family: S.Theme.iconFontFamily } @@ -78,7 +43,7 @@ M.HoverPanel { anchors.rightMargin: 4 anchors.verticalCenter: parent.verticalCenter text: entry.modelData.name - color: entry.modelData.active ? menuWindow.accentColor : S.Theme.base05 + color: entry.modelData.active ? root.accentColor : S.Theme.base05 font.pixelSize: S.Theme.fontSize font.family: S.Theme.fontFamily font.bold: entry.modelData.active @@ -107,7 +72,6 @@ M.HoverPanel { S.NetworkService.disconnectNetwork(entry.modelData.uuid); else S.NetworkService.connectNetwork(entry.modelData.uuid); - menuWindow.keepOpen(500); } } } @@ -115,7 +79,7 @@ M.HoverPanel { Text { visible: S.NetworkService.networks.length === 0 - width: menuWindow.contentWidth + width: root.width height: 32 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter diff --git a/shell/applets/qmldir b/shell/applets/qmldir index dc17e29..1cfc65a 100644 --- a/shell/applets/qmldir +++ b/shell/applets/qmldir @@ -8,6 +8,7 @@ GpuApplet 1.0 GpuApplet.qml HexWaveBackground 1.0 HexWaveBackground.qml MemoryApplet 1.0 MemoryApplet.qml MprisApplet 1.0 MprisApplet.qml +NetworkApplet 1.0 NetworkApplet.qml TemperatureApplet 1.0 TemperatureApplet.qml VolumeApplet 1.0 VolumeApplet.qml # keep-sorted end diff --git a/shell/lock/LockClock.qml b/shell/lock/LockClock.qml index 279bb2b..05d0b40 100644 --- a/shell/lock/LockClock.qml +++ b/shell/lock/LockClock.qml @@ -10,11 +10,12 @@ Item { readonly property real _fontSize: Math.max(48, screenHeight * 0.28) - // Appear as wave reaches the clock (left edge), latch once fully revealed + // Appear as wave reaches the clock (left edge), latch once fully revealed. + // With reducedMotion the wave never runs, so show immediately. property bool _revealed: false on_RawProgressChanged: if (_rawProgress >= 1) _revealed = true - readonly property real _rawProgress: Math.max(0, Math.min(1, wavePhase / 300)) + readonly property real _rawProgress: S.Theme.reducedMotion ? 1 : Math.max(0, Math.min(1, wavePhase / 300)) readonly property real _progress: (_revealed ? 1 : _rawProgress) * unlockFade opacity: _progress property real _slideX: (1 - _progress) * -80 diff --git a/shell/lock/LockWidgets.qml b/shell/lock/LockWidgets.qml index 516a145..90f105b 100644 --- a/shell/lock/LockWidgets.qml +++ b/shell/lock/LockWidgets.qml @@ -12,11 +12,12 @@ Item { property real screenWidth: 0 property real unlockFade: 1 - // Fly in when wave exits the right edge, latch once fully revealed + // Fly in when wave exits the right edge, latch once fully revealed. + // With reducedMotion the wave never runs, so show immediately. property bool _revealed: false on_RawProgressChanged: if (_rawProgress >= 1) _revealed = true - readonly property real _rawProgress: screenWidth > 0 ? Math.max(0, Math.min(1, (wavePhase - screenWidth) / 500)) : 0 + readonly property real _rawProgress: S.Theme.reducedMotion ? 1 : (screenWidth > 0 ? Math.max(0, Math.min(1, (wavePhase - screenWidth) / 500)) : 0) readonly property real _progress: (_revealed ? 1 : _rawProgress) * unlockFade opacity: _progress property real _slideX: (1 - _progress) * 80 diff --git a/shell/modules/Bar.qml b/shell/modules/Bar.qml index 343b682..fd86f92 100644 --- a/shell/modules/Bar.qml +++ b/shell/modules/Bar.qml @@ -181,12 +181,9 @@ PanelWindow { // Connectivity M.BarGroup { M.NetworkModule { - bar: bar visible: S.Modules.network.enable } - M.BluetoothModule { - bar: bar - } + M.BluetoothModule {} } // Controls diff --git a/shell/modules/BluetoothModule.qml b/shell/modules/BluetoothModule.qml index 81e715e..d107f71 100644 --- a/shell/modules/BluetoothModule.qml +++ b/shell/modules/BluetoothModule.qml @@ -2,18 +2,30 @@ import QtQuick import Quickshell import "." as M import "../services" as S +import "../applets" as C M.BarSection { id: root spacing: S.Theme.moduleSpacing opacity: S.Modules.bluetooth.enable && S.BluetoothService.state !== "unavailable" ? 1 : 0 visible: opacity > 0 - tooltip: { - if (S.BluetoothService.state === "off") - return "Bluetooth: off"; - if (S.BluetoothService.state === "connected") - return "Bluetooth: " + S.BluetoothService.device + (S.BluetoothService.batteryPct >= 0 ? "\nBattery: " + S.BluetoothService.batteryPct + "%" : ""); - return "Bluetooth: on"; + tooltip: "" + + property bool _pinned: false + readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered + readonly property bool _showPanel: _anyHover || _pinned + + on_AnyHoverChanged: { + if (_anyHover) + _unpinTimer.stop(); + else if (_pinned) + _unpinTimer.start(); + } + + Timer { + id: _unpinTimer + interval: 500 + onTriggered: root._pinned = false } M.BarIcon { @@ -21,10 +33,7 @@ M.BarSection { color: S.BluetoothService.state === "off" ? S.Theme.base04 : root.accentColor anchors.verticalCenter: parent.verticalCenter TapHandler { - onTapped: { - M.FlyoutState.visible = false; - btLoader.active = true; - } + onTapped: root._pinned = !root._pinned } } M.BarLabel { @@ -32,23 +41,61 @@ M.BarSection { label: S.BluetoothService.device + (S.BluetoothService.batteryPct >= 0 ? " " + S.BluetoothService.batteryPct + "%" : "") anchors.verticalCenter: parent.verticalCenter TapHandler { - onTapped: { - M.FlyoutState.visible = false; - btLoader.active = true; - } + onTapped: root._pinned = !root._pinned } } - required property var bar + M.HoverPanel { + id: hoverPanel + showPanel: root._showPanel + screen: QsWindow.window?.screen ?? null + anchorItem: root + accentColor: root.accentColor + panelNamespace: "nova-bluetooth" + panelTitle: "Bluetooth" + contentWidth: 250 + titleActionsComponent: Component { + Item { + width: 20 + height: 20 - LazyLoader { - id: btLoader - active: false - M.BluetoothMenu { + Text { + anchors.centerIn: parent + text: "\uF011" + color: S.BluetoothService.enabled ? hoverPanel.accentColor : S.Theme.base04 + font.pixelSize: S.Theme.fontSize + font.family: S.Theme.iconFontFamily + + Behavior on color { + ColorAnimation { + duration: 100 + } + } + } + + HoverHandler { + cursorShape: Qt.PointingHandCursor + } + + TapHandler { + onTapped: S.BluetoothService.setPower(!S.BluetoothService.enabled) + } + } + } + + onVisibleChanged: if (visible) + S.BluetoothService.refresh() + + Connections { + target: S.BluetoothService + function onDevicesChanged() { + hoverPanel.keepOpen(500); + } + } + + C.BluetoothApplet { + width: hoverPanel.contentWidth accentColor: root.accentColor - screen: QsWindow.window?.screen ?? null - anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) - onDismissed: btLoader.active = false } } } diff --git a/shell/modules/NetworkModule.qml b/shell/modules/NetworkModule.qml index d37eff8..67194e0 100644 --- a/shell/modules/NetworkModule.qml +++ b/shell/modules/NetworkModule.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import "." as M import "../services" as S +import "../applets" as C M.BarSection { id: root @@ -10,6 +11,23 @@ M.BarSection { readonly property string state: S.NetworkService.state + property bool _pinned: false + readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered + readonly property bool _showPanel: _anyHover || _pinned + + on_AnyHoverChanged: { + if (_anyHover) + _unpinTimer.stop(); + else if (_pinned) + _unpinTimer.start(); + } + + Timer { + id: _unpinTimer + interval: 500 + onTriggered: root._pinned = false + } + M.BarIcon { icon: { if (root.state === "wifi") @@ -22,23 +40,71 @@ M.BarSection { } color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor anchors.verticalCenter: parent.verticalCenter + TapHandler { + onTapped: root._pinned = !root._pinned + } } M.BarLabel { visible: root.state === "wifi" label: S.NetworkService.essid color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor anchors.verticalCenter: parent.verticalCenter + TapHandler { + onTapped: root._pinned = !root._pinned + } } - required property var bar - - readonly property bool _anyHover: root._hovered || networkMenu.panelHovered - - M.NetworkMenu { - id: networkMenu - showPanel: root._anyHover + M.HoverPanel { + id: hoverPanel + showPanel: root._showPanel screen: QsWindow.window?.screen ?? null anchorItem: root accentColor: root.accentColor + panelNamespace: "nova-network" + panelTitle: "Wi-Fi" + contentWidth: 250 + titleActionsComponent: Component { + Item { + width: 20 + height: 20 + + Text { + anchors.centerIn: parent + text: "\uF011" + color: S.NetworkService.wifiEnabled ? hoverPanel.accentColor : S.Theme.base04 + font.pixelSize: S.Theme.fontSize + font.family: S.Theme.iconFontFamily + + Behavior on color { + ColorAnimation { + duration: 100 + } + } + } + + HoverHandler { + cursorShape: Qt.PointingHandCursor + } + + TapHandler { + onTapped: S.NetworkService.setWifi(!S.NetworkService.wifiEnabled) + } + } + } + + onVisibleChanged: if (visible) + S.NetworkService.refresh() + + Connections { + target: S.NetworkService + function onNetworksChanged() { + hoverPanel.keepOpen(500); + } + } + + C.NetworkApplet { + width: hoverPanel.contentWidth + accentColor: root.accentColor + } } } diff --git a/shell/modules/qmldir b/shell/modules/qmldir index c7e2c9f..b4a55ee 100644 --- a/shell/modules/qmldir +++ b/shell/modules/qmldir @@ -8,7 +8,6 @@ BarIcon 1.0 BarIcon.qml BarLabel 1.0 BarLabel.qml BarSection 1.0 BarSection.qml BatteryModule 1.0 BatteryModule.qml -BluetoothMenu 1.0 BluetoothMenu.qml BluetoothModule 1.0 BluetoothModule.qml ClockModule 1.0 ClockModule.qml CpuModule 1.0 CpuModule.qml @@ -19,7 +18,6 @@ HoverPanel 1.0 HoverPanel.qml IdleInhibitorModule 1.0 IdleInhibitorModule.qml MemoryModule 1.0 MemoryModule.qml MprisModule 1.0 MprisModule.qml -NetworkMenu 1.0 NetworkMenu.qml NetworkModule 1.0 NetworkModule.qml NotifCard 1.0 NotifCard.qml NotifCenter 1.0 NotifCenter.qml diff --git a/shell/services/BluetoothService.qml b/shell/services/BluetoothService.qml index e648538..608f1c6 100644 --- a/shell/services/BluetoothService.qml +++ b/shell/services/BluetoothService.qml @@ -26,7 +26,11 @@ QtObject { _powerProc.running = true; } + // MAC address of device currently connecting/disconnecting + property string pendingMac: "" + function toggleDevice(mac, connect) { + pendingMac = mac; _toggleProc._action = connect ? "connect" : "disconnect"; _toggleProc._mac = mac; _toggleProc.running = true; @@ -120,7 +124,9 @@ QtObject { property string _action: "" property string _mac: "" command: ["bluetoothctl", _action, _mac] - onRunningChanged: if (!running) - root.refresh() + onRunningChanged: if (!running) { + root.pendingMac = ""; + root.refresh(); + } } }