diff options
38 files changed, 1344 insertions, 754 deletions
diff --git a/modules/system/quickshell/AnchoredPopup.qml b/modules/system/quickshell/AnchoredPopup.qml new file mode 100644 index 0000000..76d4ccd --- /dev/null +++ b/modules/system/quickshell/AnchoredPopup.qml @@ -0,0 +1,77 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland + +Scope { + id: root + + property string popupName: "" + property var anchorWindow + property var anchorItem + property int popupGap: Theme.popupGap + readonly property bool open: GlobalState.activePopup === popupName + default property alias content: contentRoot.data + + signal opened + signal closed + + PanelWindow { + visible: root.open + anchors { + top: true + bottom: true + left: true + right: true + } + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.margins.top: Theme.barHeight + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + exclusionMode: ExclusionMode.Ignore + color: Theme.transparent + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + hoverEnabled: true + onClicked: GlobalState.close() + onWheel: wheel => wheel.accepted = true + } + } + + PopupWindow { + id: popup + visible: root.open + grabFocus: true + implicitWidth: contentRoot.childrenRect.width + implicitHeight: contentRoot.childrenRect.height + + anchor { + window: root.anchorWindow + item: root.anchorItem + edges: Edges.Bottom + gravity: Edges.Bottom + margins.top: root.popupGap + } + + color: Theme.transparent + + onVisibleChanged: { + if (visible) { + anchor.updateAnchor(); + contentRoot.forceActiveFocus(); + root.opened(); + } else { + root.closed(); + } + } + + Item { + id: contentRoot + focus: true + width: childrenRect.width + height: childrenRect.height + + Keys.onEscapePressed: GlobalState.close() + } + } +} diff --git a/modules/system/quickshell/Background.qml b/modules/system/quickshell/Background.qml index 2bf9ae6..f15022d 100644 --- a/modules/system/quickshell/Background.qml +++ b/modules/system/quickshell/Background.qml @@ -5,7 +5,7 @@ import QtQuick PanelWindow { WlrLayershell.layer: WlrLayer.Background WlrLayershell.exclusiveZone: -1 - + anchors { top: true bottom: true diff --git a/modules/system/quickshell/Bar.qml b/modules/system/quickshell/Bar.qml index 77cfd1e..2356670 100644 --- a/modules/system/quickshell/Bar.qml +++ b/modules/system/quickshell/Bar.qml @@ -61,7 +61,7 @@ PanelWindow { trayMenu.open(trayIcon); } } else { - modelData.activate() + modelData.activate(); } } } @@ -73,36 +73,37 @@ PanelWindow { parentWindow: barWindow } - Bluetooth { anchors.verticalCenter: parent.verticalCenter } - - Wifi { anchors.verticalCenter: parent.verticalCenter } - - Volume { anchors.verticalCenter: parent.verticalCenter } - - Media { anchors.verticalCenter: parent.verticalCenter } - - ControlCenter { anchors.verticalCenter: parent.verticalCenter } + Bluetooth { + anchors.verticalCenter: parent.verticalCenter + } - Text { - id: clock + Wifi { anchors.verticalCenter: parent.verticalCenter - color: Theme.text - font { - family: Theme.mainFont - pixelSize: 13 - weight: Font.Medium - } + } - Timer { - interval: 1000 - running: true - repeat: true - onTriggered: parent.text = Qt.formatDateTime(new Date(), "ddd d MMM HH:mm:ss") - } + Volume { + anchors.verticalCenter: parent.verticalCenter + } - Component.onCompleted: text = Qt.formatDateTime(new Date(), "ddd d MMM HH:mm:ss") + ScreenRecordIndicator { + anchors.verticalCenter: parent.verticalCenter + } + + MicInput { + anchors.verticalCenter: parent.verticalCenter + } + + Media { + anchors.verticalCenter: parent.verticalCenter + } + + ControlCenter { + anchors.verticalCenter: parent.verticalCenter + } + + Clock { + anchors.verticalCenter: parent.verticalCenter } } } } - diff --git a/modules/system/quickshell/Bluetooth.qml b/modules/system/quickshell/Bluetooth.qml index 17091ff..2e9232b 100644 --- a/modules/system/quickshell/Bluetooth.qml +++ b/modules/system/quickshell/Bluetooth.qml @@ -3,7 +3,7 @@ import QtQuick.Layouts import Quickshell import Quickshell.Bluetooth import Quickshell.Widgets -import QtQuick.Effects // Required for icon tinting +import QtQuick.Effects Item { id: root @@ -13,21 +13,23 @@ Item { readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter readonly property var allDevices: adapter ? adapter.devices.values : [] + readonly property bool hasPaired: hasPairedDevice() + readonly property bool hasNewVisible: hasNewVisibleDevice() - property bool hasPaired: false - property bool hasNewVisible: false - - function updateState() { - let paired = false - let newVisible = false - + function hasPairedDevice() { for (const d of internal.allDevices) { - if (d?.paired) paired = true - if (d && !d.paired && d.name) newVisible = true + if (d?.paired) + return true; } + return false; + } - internal.hasPaired = paired - internal.hasNewVisible = newVisible + function hasNewVisibleDevice() { + for (const d of internal.allDevices) { + if (d && !d.paired && d.name) + return true; + } + return false; } } @@ -54,48 +56,25 @@ Item { acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: mouse => { if (mouse.button === Qt.LeftButton) { - GlobalState.toggle("Bluetooth") + GlobalState.toggle("Bluetooth"); } else if (mouse.button === Qt.RightButton && internal.adapter) { - internal.adapter.enabled = !internal.adapter.enabled + internal.adapter.enabled = !internal.adapter.enabled; } } } - PopupWindow { - id: popup - visible: GlobalState.activePopup === "Bluetooth" - grabFocus: true - implicitWidth: card.width - implicitHeight: card.height - - anchor { - window: barWindow - item: root - edges: Edges.Bottom - gravity: Edges.Bottom - margins.top: Theme.popupGap - } - - color: Theme.transparent - - onVisibleChanged: { - if (visible) { - anchor.updateAnchor() - if (internal.adapter) internal.adapter.discovering = true - } else if (internal.adapter?.discovering) { - internal.adapter.discovering = false - } - } - - Connections { - target: internal.adapter ? internal.adapter.devices : null - function onValuesChanged() { internal.updateState() } - } + AnchoredPopup { + popupName: "Bluetooth" + anchorWindow: barWindow + anchorItem: root + onOpened: if (internal.adapter) + internal.adapter.discovering = true + onClosed: if (internal.adapter?.discovering) + internal.adapter.discovering = false PopupCard { id: card - // Main Toggle Header RowLayout { Layout.fillWidth: true spacing: 8 @@ -110,7 +89,9 @@ Item { } } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } Toggle { checked: internal.adapter?.enabled ?? false @@ -125,10 +106,9 @@ Item { color: Theme.border } - // Paired Devices Header Text { visible: internal.hasPaired - text: "My Devices" // macOS typically labels this "My Devices" + text: "My Devices" color: Theme.textMuted font { family: Theme.mainFont @@ -137,7 +117,6 @@ Item { } } - // Paired Devices List ColumnLayout { Layout.fillWidth: true spacing: 2 @@ -156,23 +135,16 @@ Item { fillColor: hovered ? Theme.surface : Theme.transparent cornerRadius: 6 - Connections { - target: pairedItem.modelData - function onPairedChanged() { internal.updateState() } - function onConnectedChanged() { internal.updateState() } - function onNameChanged() { internal.updateState() } - } - Component.onCompleted: internal.updateState() - Component.onDestruction: internal.updateState() - MouseArea { id: pairedArea anchors.fill: parent hoverEnabled: true onClicked: { - const d = pairedItem.modelData - if (d.connected) d.disconnect() - else d.connect() + const d = pairedItem.modelData; + if (d.connected) + d.disconnect(); + else + d.connect(); } } @@ -183,19 +155,18 @@ Item { } spacing: 10 - // macOS uses bare icons in the list, tinted accent color when connected Item { Layout.preferredWidth: 20 Layout.preferredHeight: 20 - + Image { id: deviceIcon anchors.fill: parent source: Quickshell.iconPath("bluetooth-active-symbolic") sourceSize: Qt.size(20, 20) - visible: false + visible: false } - + MultiEffect { anchors.fill: deviceIcon source: deviceIcon @@ -215,7 +186,6 @@ Item { elide: Text.ElideRight } - // macOS displays connection status text instead of a toggle/button Text { text: (pairedItem.modelData?.connected ?? false) ? "Connected" : "Not Connected" color: Theme.textMuted @@ -236,7 +206,6 @@ Item { visible: internal.hasPaired } - // Other Devices Header RowLayout { Layout.fillWidth: true @@ -251,7 +220,6 @@ Item { Layout.fillWidth: true } - // Passive scanning indicator instead of interactive refresh button Image { width: 14 height: 14 @@ -272,7 +240,6 @@ Item { } } - // Unpaired Devices List ColumnLayout { Layout.fillWidth: true spacing: 2 @@ -290,14 +257,6 @@ Item { fillColor: newArea.containsMouse ? Theme.surface : Theme.transparent cornerRadius: 6 - Connections { - target: newItem.modelData - function onPairedChanged() { internal.updateState() } - function onNameChanged() { internal.updateState() } - } - Component.onCompleted: internal.updateState() - Component.onDestruction: internal.updateState() - MouseArea { id: newArea anchors.fill: parent @@ -332,8 +291,7 @@ Item { } elide: Text.ElideRight } - - // Replace + icon with "Connecting..." text when pairing + Text { visible: newItem.modelData?.pairing ?? false text: "Connecting..." diff --git a/modules/system/quickshell/BrightnessService.qml b/modules/system/quickshell/BrightnessService.qml index 991a6ec..6142188 100644 --- a/modules/system/quickshell/BrightnessService.qml +++ b/modules/system/quickshell/BrightnessService.qml @@ -2,31 +2,30 @@ import QtQuick import Quickshell import Quickshell.Io -// Simplified brightness service using brightnessctl. QtObject { id: root property real brightness: 0 property int maxBrightness: 1 function update() { - updateProc.running = true + updateProc.running = true; } function setBrightness(value) { - const raw = Math.round(value * maxBrightness) - Quickshell.execDetached(["brightnessctl", "s", raw.toString()]) - brightness = value + const raw = Math.round(value * maxBrightness); + Quickshell.execDetached(["brightnessctl", "s", raw.toString()]); + brightness = value; } readonly property Process updateProc: Process { - command: ["sh", "-c", "brightnessctl g && brightnessctl m"] + command: ["sh", "-c", "printf '%s %s\\n' \"$(brightnessctl g)\" \"$(brightnessctl m)\""] stdout: SplitParser { onRead: data => { - const lines = data.trim().split("\n") - if (lines.length >= 2) { - const current = parseInt(lines[0]) - root.maxBrightness = parseInt(lines[1]) - root.brightness = current / root.maxBrightness + const parts = data.trim().split(/\s+/); + if (parts.length >= 2) { + const current = parseInt(parts[0]); + root.maxBrightness = parseInt(parts[1]); + root.brightness = current / root.maxBrightness; } } } diff --git a/modules/system/quickshell/Clock.qml b/modules/system/quickshell/Clock.qml new file mode 100644 index 0000000..0ec3933 --- /dev/null +++ b/modules/system/quickshell/Clock.qml @@ -0,0 +1,205 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell + +Item { + id: root + width: clock.implicitWidth + height: parent.height + + Text { + id: clock + anchors.verticalCenter: parent.verticalCenter + color: Theme.text + text: Qt.formatDateTime(new Date(), "ddd d MMM HH:mm:ss") + font { + family: Theme.mainFont + pixelSize: 13 + weight: Font.Medium + } + + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: clock.text = Qt.formatDateTime(new Date(), "ddd d MMM HH:mm:ss") + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: GlobalState.toggle("NotificationHistory") + } + + AnchoredPopup { + popupName: "NotificationHistory" + anchorWindow: barWindow + anchorItem: root + + PopupCard { + width: 360 + maxHeight: 520 + margins: 14 + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 4 + Layout.rightMargin: 4 + spacing: 8 + + Text { + text: "Notifications" + color: Theme.text + font { + family: Theme.mainFont + pixelSize: 14 + weight: Font.DemiBold + } + } + + Item { + Layout.fillWidth: true + } + + Text { + visible: GlobalState.notificationHistory.length > 0 + text: "Clear" + color: clearArea.containsMouse ? Theme.text : Theme.textMuted + font { + family: Theme.mainFont + pixelSize: 12 + weight: Font.Medium + } + + MouseArea { + id: clearArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: GlobalState.clearNotificationHistory() + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Theme.border + } + + Text { + visible: GlobalState.notificationHistory.length === 0 + Layout.fillWidth: true + Layout.topMargin: 20 + Layout.bottomMargin: 20 + horizontalAlignment: Text.AlignHCenter + text: "No notifications" + color: Theme.textMuted + font { + family: Theme.mainFont + pixelSize: 13 + weight: Font.Medium + } + } + + Repeater { + model: GlobalState.notificationHistory + + delegate: Squircle { + id: historyItem + required property var modelData + + Layout.fillWidth: true + Layout.preferredHeight: Math.max(64, historyLayout.implicitHeight + 20) + cornerRadius: 12 + fillColor: historyHover.containsMouse ? Theme.surfaceHover : Theme.surface + strokeColor: Theme.border + strokeWidth: 1 + + MouseArea { + id: historyHover + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + + RowLayout { + id: historyLayout + anchors { + fill: parent + margins: 10 + } + spacing: 10 + + IconCircle { + size: 32 + source: "notifications" + active: false + Layout.alignment: Qt.AlignTop + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + RowLayout { + Layout.fillWidth: true + + Text { + Layout.fillWidth: true + text: historyItem.modelData.nAppName || "Notification" + color: Theme.textMuted + elide: Text.ElideRight + font { + family: Theme.mainFont + pixelSize: 11 + weight: Font.Medium + } + } + + Text { + text: Qt.formatTime(historyItem.modelData.nTimestamp, "HH:mm") + color: Theme.textPlaceholder + font { + family: Theme.mainFont + pixelSize: 10 + } + } + } + + Text { + visible: text !== "" + Layout.fillWidth: true + text: historyItem.modelData.nSummary || "" + color: Theme.text + elide: Text.ElideRight + maximumLineCount: 1 + font { + family: Theme.mainFont + pixelSize: 13 + weight: Font.DemiBold + } + } + + Text { + visible: text !== "" + Layout.fillWidth: true + text: historyItem.modelData.nBody || "" + color: Theme.textMuted + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideRight + textFormat: Text.StyledText + font { + family: Theme.mainFont + pixelSize: 12 + } + } + } + } + } + } + } + } +} diff --git a/modules/system/quickshell/ConnectivityBox.qml b/modules/system/quickshell/ConnectivityBox.qml index e1fb03f..384aff2 100644 --- a/modules/system/quickshell/ConnectivityBox.qml +++ b/modules/system/quickshell/ConnectivityBox.qml @@ -14,19 +14,23 @@ Squircle { ColumnLayout { anchors.fill: parent anchors.margins: 12 - spacing: 0 // Using spacers for better control + spacing: 0 - // WiFi Row Item { Layout.fillWidth: true height: 50 scale: wifiArea.pressed ? 0.96 : 1.0 - Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } + Behavior on scale { + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } RowLayout { anchors.fill: parent spacing: 12 - + IconCircle { source: "network-wireless" active: Networking.wifiEnabled @@ -46,14 +50,16 @@ Squircle { } Text { text: { - const vs = Networking.devices?.values || [] + const vs = Networking.devices?.values || []; for (const device of vs) { if (device.scannerEnabled !== undefined) { - const nets = device.networks?.values || [] - for (const n of nets) if (n.connected) return n.name + const nets = device.networks?.values || []; + for (const n of nets) + if (n.connected) + return n.name; } } - return Networking.wifiEnabled ? "On" : "Off" + return Networking.wifiEnabled ? "On" : "Off"; } color: Theme.textMuted font.pixelSize: 12 @@ -71,19 +77,23 @@ Squircle { } } - Item { Layout.fillHeight: true } // Spacer + //Item { Layout.fillHeight: true } - // Bluetooth Row Item { Layout.fillWidth: true height: 50 scale: btArea.pressed ? 0.96 : 1.0 - Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } + Behavior on scale { + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } RowLayout { anchors.fill: parent spacing: 12 - + IconCircle { source: "bluetooth-active" active: Bluetooth.defaultAdapter?.enabled ?? false diff --git a/modules/system/quickshell/ControlCenter.qml b/modules/system/quickshell/ControlCenter.qml index 25aae5e..af13638 100644 --- a/modules/system/quickshell/ControlCenter.qml +++ b/modules/system/quickshell/ControlCenter.qml @@ -11,19 +11,21 @@ Item { width: childrenRect.width height: parent.height - BrightnessService { id: brightnessService } + BrightnessService { + id: brightnessService + } MouseArea { anchors.fill: parent onClicked: GlobalState.toggle("ControlCenter") } - // Indicator in bar Row { anchors.verticalCenter: parent.verticalCenter spacing: 4 Image { - width: 20; height: 20 + width: 20 + height: 20 source: Quickshell.iconPath("emblem-system-symbolic") sourceSize: Qt.size(width, height) smooth: true @@ -31,33 +33,16 @@ Item { } } - PopupWindow { - id: popup - visible: GlobalState.activePopup === "ControlCenter" - grabFocus: true - implicitWidth: card.width - implicitHeight: card.height - - anchor { - window: barWindow - item: root - edges: Edges.Bottom - gravity: Edges.Bottom - margins.top: Theme.popupGap - } - - color: "transparent" - - onVisibleChanged: { - if (visible) anchor.updateAnchor() - } + AnchoredPopup { + popupName: "ControlCenter" + anchorWindow: barWindow + anchorItem: root PopupCard { id: card width: 320 margins: 16 - // TOP SECTION: Connectivity & Quick Actions RowLayout { Layout.fillWidth: true spacing: 12 @@ -78,7 +63,6 @@ Item { } } - // MIDDLE SECTION: Sliders SliderBox { label: "Display" icon: "display-brightness" @@ -90,16 +74,17 @@ Item { label: "Sound" icon: "audio-volume-high" value: Pipewire.defaultAudioSink?.audio?.volume ?? 0 + clickable: true + onClicked: Qt.callLater(() => GlobalState.open("Volume")) onMoved: val => { - const sink = Pipewire.defaultAudioSink + const sink = Pipewire.defaultAudioSink; if (sink?.audio) { - sink.audio.muted = false - sink.audio.volume = val + sink.audio.muted = false; + sink.audio.volume = val; } } } - // NOW PLAYING BOX Squircle { Layout.fillWidth: true height: 64 @@ -120,22 +105,22 @@ Item { anchors.right: parent.right anchors.leftMargin: 12 anchors.rightMargin: 12 - + isExpanded: false readonly property var activePlayer: { - const ps = Mpris.players?.values || [] - for (const p of ps) if (p.playbackState === MprisPlaybackState.Playing) return p - return ps[0] + const ps = Mpris.players?.values || []; + for (const p of ps) + if (p.playbackState === MprisPlaybackState.Playing) + return p; + return ps[0] ?? null; } player: activePlayer - + onClicked: Qt.callLater(() => GlobalState.open("Media")) } } } } } - - diff --git a/modules/system/quickshell/ControlTile.qml b/modules/system/quickshell/ControlTile.qml index 2c8e0a6..d944e89 100644 --- a/modules/system/quickshell/ControlTile.qml +++ b/modules/system/quickshell/ControlTile.qml @@ -16,12 +16,13 @@ Item { anchors.fill: parent cornerRadius: 16 fillColor: root.active ? Theme.accent : Theme.surface - + MouseArea { anchors.fill: parent hoverEnabled: true onClicked: { - if (root.clickHandler) root.clickHandler() + if (root.clickHandler) + root.clickHandler(); } } @@ -47,4 +48,4 @@ Item { } } } -}
\ No newline at end of file +} diff --git a/modules/system/quickshell/CustomCheckBox.qml b/modules/system/quickshell/CustomCheckBox.qml index 9b7014d..2ed3212 100644 --- a/modules/system/quickshell/CustomCheckBox.qml +++ b/modules/system/quickshell/CustomCheckBox.qml @@ -3,7 +3,7 @@ import QtQuick.Controls CheckBox { id: control - + contentItem: Text { text: control.text color: Theme.text @@ -12,7 +12,7 @@ CheckBox { verticalAlignment: Text.AlignVCenter leftPadding: control.indicator.width + control.spacing } - + indicator: Rectangle { implicitWidth: 14 implicitHeight: 14 diff --git a/modules/system/quickshell/GlobalState.qml b/modules/system/quickshell/GlobalState.qml index c212967..c657b31 100644 --- a/modules/system/quickshell/GlobalState.qml +++ b/modules/system/quickshell/GlobalState.qml @@ -1,23 +1,32 @@ -import QtQuick - pragma Singleton +import QtQuick -// Shared state to coordinate which popup is currently open. -// Ensures only one menu is visible at a time. QtObject { property string activePopup: "" + property var notificationHistory: [] function open(name) { - activePopup = "" - activePopup = name + activePopup = ""; + activePopup = name; } function close() { - activePopup = "" + activePopup = ""; } function toggle(name) { - if (activePopup === name) activePopup = "" - else activePopup = name + if (activePopup === name) + activePopup = ""; + else + activePopup = name; + } + + function addNotification(data) { + const entry = Object.assign({}, data); + notificationHistory = [entry].concat(notificationHistory).slice(0, 50); + } + + function clearNotificationHistory() { + notificationHistory = []; } } diff --git a/modules/system/quickshell/IconCircle.qml b/modules/system/quickshell/IconCircle.qml index 50d5dd6..35a399c 100644 --- a/modules/system/quickshell/IconCircle.qml +++ b/modules/system/quickshell/IconCircle.qml @@ -4,7 +4,7 @@ import Quickshell Rectangle { id: root - + property string source: "" property bool active: false property real size: 24 @@ -23,8 +23,8 @@ Rectangle { sourceSize: Qt.size(width, height) smooth: true mipmap: true - - visible: false + + visible: false } MultiEffect { @@ -32,7 +32,7 @@ Rectangle { source: iconImage colorizationColor: "#FFFFFF" colorization: 1.0 - + opacity: root.active ? 1.0 : 0.6 } } diff --git a/modules/system/quickshell/Launcher.qml b/modules/system/quickshell/Launcher.qml index fc0dc3d..184271f 100644 --- a/modules/system/quickshell/Launcher.qml +++ b/modules/system/quickshell/Launcher.qml @@ -11,25 +11,26 @@ PanelWindow { WlrLayershell.namespace: "launcher" WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive WlrLayershell.exclusiveZone: -1 - + exclusionMode: ExclusionMode.Ignore - - // Centering logic + anchors { top: true + bottom: true + left: true + right: true } - margins.top: 200 implicitWidth: 600 implicitHeight: 400 - + color: "transparent" visible: GlobalState.activePopup === "Launcher" - + onVisibleChanged: { if (visible) { - searchInput.forceActiveFocus() - searchInput.text = "" + searchInput.forceActiveFocus(); + searchInput.text = ""; } } @@ -37,21 +38,35 @@ PanelWindow { id: internal function filterApps(searchText) { - const search = searchText.toLowerCase() - const apps = DesktopEntries.applications.values - + const search = searchText.toLowerCase(); + const apps = DesktopEntries.applications.values; + return apps.filter(app => { - if (app.noDisplay) return false - if (!search) return true - - return app.name.toLowerCase().includes(search) || - (app.comment && app.comment.toLowerCase().includes(search)) - }) + if (app.noDisplay) + return false; + if (!search) + return true; + + return app.name.toLowerCase().includes(search) || (app.comment && app.comment.toLowerCase().includes(search)); + }); } } - FocusScope { + MouseArea { anchors.fill: parent + acceptedButtons: Qt.AllButtons + onClicked: GlobalState.close() + onWheel: wheel => wheel.accepted = true + } + + FocusScope { + width: 600 + height: 400 + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + topMargin: 200 + } focus: true Squircle { @@ -62,7 +77,6 @@ PanelWindow { strokeColor: Theme.border strokeWidth: 1 - // Capture Escape to close Keys.onEscapePressed: GlobalState.close() ColumnLayout { @@ -70,159 +84,160 @@ PanelWindow { anchors.margins: 12 spacing: 8 - // Search Header - RowLayout { - Layout.fillWidth: true - spacing: 12 - - Image { - width: 24; height: 24 - source: Quickshell.iconPath("system-search-symbolic") - sourceSize: Qt.size(24, 24) - smooth: true - mipmap: true - opacity: 0.7 - } - - TextInput { - id: searchInput + RowLayout { Layout.fillWidth: true - font { - family: Theme.mainFont - pixelSize: 20 + spacing: 12 + + Image { + width: 24 + height: 24 + source: Quickshell.iconPath("system-search-symbolic") + sourceSize: Qt.size(24, 24) + smooth: true + mipmap: true + opacity: 0.7 } - color: Theme.text - clip: true - - focus: true - cursorVisible: true - selectByMouse: true - inputMethodHints: Qt.ImhNoPredictiveText - - Text { - visible: searchInput.text === "" - text: "Search Applications..." + + TextInput { + id: searchInput + Layout.fillWidth: true + font { + family: Theme.mainFont + pixelSize: 20 + } color: Theme.text - opacity: 0.3 - font: searchInput.font - } + clip: true + + focus: true + cursorVisible: true + selectByMouse: true + inputMethodHints: Qt.ImhNoPredictiveText + + Text { + visible: searchInput.text === "" + text: "Search Applications..." + color: Theme.text + opacity: 0.3 + font: searchInput.font + } - onTextChanged: resultsList.currentIndex = 0 - - Keys.onPressed: event => { - if (event.key === Qt.Key_Down) { - resultsList.incrementCurrentIndex() - event.accepted = true - } else if (event.key === Qt.Key_Up) { - resultsList.decrementCurrentIndex() - event.accepted = true - } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - if (resultsList.count > 0 && resultsList.currentIndex >= 0) { - if (resultsList.currentItem) { - resultsList.currentItem.launch() + onTextChanged: resultsList.currentIndex = 0 + + Keys.onPressed: event => { + if (event.key === Qt.Key_Down) { + resultsList.incrementCurrentIndex(); + event.accepted = true; + } else if (event.key === Qt.Key_Up) { + resultsList.decrementCurrentIndex(); + event.accepted = true; + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + if (resultsList.count > 0 && resultsList.currentIndex >= 0) { + if (resultsList.currentItem) { + resultsList.currentItem.launch(); + } } + event.accepted = true; } - event.accepted = true } } } - } - - Rectangle { - Layout.fillWidth: true - height: 1 - color: Theme.border - } - ListView { - id: resultsList - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - boundsBehavior: Flickable.StopAtBounds - highlightFollowsCurrentItem: true - highlightMoveDuration: 150 - highlightResizeDuration: 0 - - highlight: Squircle { - width: resultsList.width - height: 44 - cornerRadius: 8 - fillColor: Theme.surface - z: 1 + Rectangle { + Layout.fillWidth: true + height: 1 + color: Theme.border } - - model: internal.filterApps(searchInput.text) - - delegate: Squircle { - id: delegateRoot - required property var modelData - required property int index - - height: 44 - width: resultsList.width - - cornerRadius: 8 - fillColor: "transparent" - z: 5 - function launch() { - if (modelData && modelData.execute) { - modelData.execute() - GlobalState.close() - } + ListView { + id: resultsList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + boundsBehavior: Flickable.StopAtBounds + highlightFollowsCurrentItem: true + highlightMoveDuration: 150 + highlightResizeDuration: 0 + + highlight: Squircle { + width: resultsList.width + height: 44 + cornerRadius: 8 + fillColor: Theme.surface + z: 1 } - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 12 + model: internal.filterApps(searchInput.text) + + delegate: Squircle { + id: delegateRoot + required property var modelData + required property int index + + height: 44 + width: resultsList.width - Image { - width: 24; height: 24 - source: Quickshell.iconPath(modelData.icon) - sourceSize: Qt.size(32, 32) - smooth: true - mipmap: true + cornerRadius: 8 + fillColor: "transparent" + z: 5 + + function launch() { + if (modelData && modelData.execute) { + modelData.execute(); + GlobalState.close(); + } } - Column { - Layout.fillWidth: true - Text { - text: delegateRoot.modelData.name - color: Theme.text - font { - family: Theme.mainFont - pixelSize: 14 - weight: Font.Medium - } + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 12 + + Image { + width: 24 + height: 24 + source: Quickshell.iconPath(modelData.icon) + sourceSize: Qt.size(32, 32) + smooth: true + mipmap: true } - Text { - visible: delegateRoot.modelData.comment !== "" - text: delegateRoot.modelData.comment - color: Theme.textMuted - font { - family: Theme.mainFont - pixelSize: 11 + + Column { + Layout.fillWidth: true + Text { + text: delegateRoot.modelData.name + color: Theme.text + font { + family: Theme.mainFont + pixelSize: 14 + weight: Font.Medium + } + } + Text { + visible: delegateRoot.modelData.comment !== "" + text: delegateRoot.modelData.comment + color: Theme.textMuted + font { + family: Theme.mainFont + pixelSize: 11 + } + elide: Text.ElideRight + width: parent.width } - elide: Text.ElideRight - width: parent.width } } - } - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: resultsList.currentIndex = index - onClicked: delegateRoot.launch() + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: resultsList.currentIndex = index + onClicked: delegateRoot.launch() + } } + + ScrollBar.vertical: ScrollBar {} } - - ScrollBar.vertical: ScrollBar {} } } } } -} diff --git a/modules/system/quickshell/LockContext.qml b/modules/system/quickshell/LockContext.qml index 05fd1de..ea32a57 100644 --- a/modules/system/quickshell/LockContext.qml +++ b/modules/system/quickshell/LockContext.qml @@ -4,8 +4,8 @@ import Quickshell.Services.Pam Scope { id: root - signal unlocked() - signal failed() + signal unlocked + signal failed property string currentText: "" property bool unlockInProgress: false @@ -18,7 +18,8 @@ Scope { } function tryUnlock() { - if (currentText === "" || unlockInProgress) return; + if (currentText === "" || unlockInProgress) + return; unlockInProgress = true; pam.start(); } diff --git a/modules/system/quickshell/LockSurface.qml b/modules/system/quickshell/LockSurface.qml index eacd98f..1a4667a 100644 --- a/modules/system/quickshell/LockSurface.qml +++ b/modules/system/quickshell/LockSurface.qml @@ -19,19 +19,18 @@ Item { running: true stdout: SplitParser { onRead: line => { - root.realName = line.trim() + root.realName = line.trim(); } } } - // Capture keyboard input to reveal the text field - Keys.onPressed: (event) => { + Keys.onPressed: event => { if (!showInput && event.key !== Qt.Key_Escape) { - showInput = true - passwordInput.forceActiveFocus() + showInput = true; + passwordInput.forceActiveFocus(); if (event.text !== "") { - passwordInput.text = event.text - root.context.currentText = event.text + passwordInput.text = event.text; + root.context.currentText = event.text; } } } @@ -39,8 +38,8 @@ Item { MouseArea { anchors.fill: parent onClicked: { - root.showInput = true - passwordInput.forceActiveFocus() + root.showInput = true; + passwordInput.forceActiveFocus(); } } @@ -50,7 +49,6 @@ Item { fillMode: Image.PreserveAspectCrop } - // Main Content (Clock) Column { anchors { top: parent.top @@ -79,9 +77,9 @@ Item { weight: Font.DemiBold letterSpacing: -8 } - } } + } + } - // Profile and Password Area (Bottom Center) ColumnLayout { id: passwordContainer anchors { @@ -93,13 +91,55 @@ Item { SequentialAnimation { id: shakeAnimation - NumberAnimation { target: passwordContainer; property: "anchors.horizontalCenterOffset"; to: -6; duration: 30; easing.type: Easing.OutQuad } - NumberAnimation { target: passwordContainer; property: "anchors.horizontalCenterOffset"; to: 6; duration: 60; easing.type: Easing.InOutQuad } - NumberAnimation { target: passwordContainer; property: "anchors.horizontalCenterOffset"; to: -6; duration: 60; easing.type: Easing.InOutQuad } - NumberAnimation { target: passwordContainer; property: "anchors.horizontalCenterOffset"; to: 6; duration: 60; easing.type: Easing.InOutQuad } - NumberAnimation { target: passwordContainer; property: "anchors.horizontalCenterOffset"; to: -6; duration: 60; easing.type: Easing.InOutQuad } - NumberAnimation { target: passwordContainer; property: "anchors.horizontalCenterOffset"; to: 6; duration: 60; easing.type: Easing.InOutQuad } - NumberAnimation { target: passwordContainer; property: "anchors.horizontalCenterOffset"; to: 0; duration: 30; easing.type: Easing.InQuad } + NumberAnimation { + target: passwordContainer + property: "anchors.horizontalCenterOffset" + to: -6 + duration: 30 + easing.type: Easing.OutQuad + } + NumberAnimation { + target: passwordContainer + property: "anchors.horizontalCenterOffset" + to: 6 + duration: 60 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: passwordContainer + property: "anchors.horizontalCenterOffset" + to: -6 + duration: 60 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: passwordContainer + property: "anchors.horizontalCenterOffset" + to: 6 + duration: 60 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: passwordContainer + property: "anchors.horizontalCenterOffset" + to: -6 + duration: 60 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: passwordContainer + property: "anchors.horizontalCenterOffset" + to: 6 + duration: 60 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: passwordContainer + property: "anchors.horizontalCenterOffset" + to: 0 + duration: 30 + easing.type: Easing.InQuad + } } Connections { @@ -109,7 +149,6 @@ Item { } } - // Avatar Placeholder Rectangle { Layout.alignment: Qt.AlignHCenter width: 60 @@ -118,7 +157,6 @@ Item { color: "#b0b0b0" } - // User Name Text { Layout.alignment: Qt.AlignHCenter text: root.realName || "User" @@ -131,7 +169,6 @@ Item { visible: !root.showInput } - // Prompt Text Text { Layout.alignment: Qt.AlignHCenter text: "Enter Password" @@ -144,24 +181,20 @@ Item { visible: !root.showInput } - // Password Input Field TextField { id: passwordInput Layout.preferredWidth: 220 Layout.preferredHeight: 32 Layout.alignment: Qt.AlignHCenter visible: root.showInput - + placeholderText: root.context.pamMessage || "Enter Password" placeholderTextColor: Theme.textPlaceholder echoMode: TextInput.Password inputMethodHints: Qt.ImhSensitiveData enabled: !root.context.unlockInProgress - - // Update context when text is changed directly in this field onTextChanged: root.context.currentText = this.text - - // Sync text from context to support multi-monitor mirroring securely + Connections { target: root.context function onCurrentTextChanged() { @@ -170,7 +203,7 @@ Item { } } } - + background: Rectangle { radius: height / 2 color: Theme.surface @@ -184,13 +217,13 @@ Item { verticalAlignment: TextInput.AlignVCenter onAccepted: { - root.context.tryUnlock() + root.context.tryUnlock(); } Keys.onEscapePressed: { - root.showInput = false - root.context.currentText = "" - root.forceActiveFocus() + root.showInput = false; + root.context.currentText = ""; + root.forceActiveFocus(); } } } diff --git a/modules/system/quickshell/Media.qml b/modules/system/quickshell/Media.qml index ee5bf42..f16207c 100644 --- a/modules/system/quickshell/Media.qml +++ b/modules/system/quickshell/Media.qml @@ -42,22 +42,10 @@ Item { onClicked: GlobalState.toggle("Media") } - PopupWindow { - id: popup - visible: GlobalState.activePopup === "Media" - grabFocus: true - implicitWidth: card.width - implicitHeight: card.height - - anchor { - window: barWindow - item: root - edges: Edges.Bottom - gravity: Edges.Bottom - margins.top: Theme.popupGap - } - - color: Theme.transparent + AnchoredPopup { + popupName: "Media" + anchorWindow: barWindow + anchorItem: root PopupCard { id: card diff --git a/modules/system/quickshell/MediaCard.qml b/modules/system/quickshell/MediaCard.qml index 5aba34c..3156734 100644 --- a/modules/system/quickshell/MediaCard.qml +++ b/modules/system/quickshell/MediaCard.qml @@ -8,13 +8,16 @@ Item { property QtObject player: null property bool isExpanded: false - signal clicked() + signal clicked Layout.fillWidth: true implicitHeight: layout.implicitHeight Behavior on implicitHeight { - NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } } MouseArea { @@ -24,9 +27,9 @@ Item { } function formatTime(s) { - const mins = Math.floor(s / 60) - const secs = Math.floor(s % 60) - return `${mins}:${secs.toString().padStart(2, '0')}` + const mins = Math.floor(s / 60); + const secs = Math.floor(s % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; } ColumnLayout { @@ -35,7 +38,6 @@ Item { anchors.right: parent.right spacing: 16 - // Top Row: Minimal view and header for expanded view RowLayout { Layout.fillWidth: true spacing: 12 @@ -47,9 +49,21 @@ Item { fillColor: root.player ? Theme.transparent : Theme.surface clip: true - Behavior on width { NumberAnimation { duration: 200 } } - Behavior on height { NumberAnimation { duration: 200 } } - Behavior on cornerRadius { NumberAnimation { duration: 200 } } + Behavior on width { + NumberAnimation { + duration: 200 + } + } + Behavior on height { + NumberAnimation { + duration: 200 + } + } + Behavior on cornerRadius { + NumberAnimation { + duration: 200 + } + } Image { anchors.fill: parent @@ -101,14 +115,14 @@ Item { } } - // Minimal Controls: Only visible when not expanded Row { visible: !root.isExpanded spacing: 12 Layout.alignment: Qt.AlignVCenter Item { - width: 24; height: 24 + width: 24 + height: 24 Image { anchors.centerIn: parent source: Quickshell.iconPath(root.player?.playbackState === MprisPlaybackState.Playing ? "media-playback-pause-symbolic" : "media-playback-start-symbolic") @@ -119,12 +133,14 @@ Item { id: minPlayMouse anchors.fill: parent enabled: root.player?.canTogglePlaying ?? false - onClicked: if (root.player) root.player.togglePlaying() + onClicked: if (root.player) + root.player.togglePlaying() } } Item { - width: 24; height: 24 + width: 24 + height: 24 Image { anchors.centerIn: parent source: Quickshell.iconPath("media-skip-forward-symbolic") @@ -135,13 +151,13 @@ Item { id: minNextMouse anchors.fill: parent enabled: root.player?.canGoNext ?? false - onClicked: if (root.player) root.player.next() + onClicked: if (root.player) + root.player.next() } } } } - // Expanded Controls: Only visible when expanded and a player exists ColumnLayout { visible: root.isExpanded && root.player !== null Layout.fillWidth: true @@ -159,7 +175,8 @@ Item { value: root.player?.position || 0 enabled: root.player?.canSeek ?? false - onMoved: if (root.player) root.player.position = value + onMoved: if (root.player) + root.player.position = value Timer { running: root.player?.playbackState === MprisPlaybackState.Playing && root.isExpanded @@ -177,7 +194,9 @@ Item { font.pixelSize: 9 font.weight: Font.Medium } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } Text { text: root.formatTime(root.player?.length || 0) color: Theme.textPlaceholder @@ -192,7 +211,8 @@ Item { spacing: 32 Item { - width: 24; height: 24 + width: 24 + height: 24 Image { anchors.centerIn: parent source: Quickshell.iconPath("media-skip-backward-symbolic") @@ -203,12 +223,14 @@ Item { id: prevMouse anchors.fill: parent enabled: root.player?.canGoPrevious ?? false - onClicked: if (root.player) root.player.previous() + onClicked: if (root.player) + root.player.previous() } } Item { - width: 32; height: 32 + width: 32 + height: 32 Image { anchors.centerIn: parent source: Quickshell.iconPath(root.player?.playbackState === MprisPlaybackState.Playing ? "media-playback-pause-symbolic" : "media-playback-start-symbolic") @@ -219,12 +241,14 @@ Item { id: maxPlayMouse anchors.fill: parent enabled: root.player?.canTogglePlaying ?? false - onClicked: if (root.player) root.player.togglePlaying() + onClicked: if (root.player) + root.player.togglePlaying() } } Item { - width: 24; height: 24 + width: 24 + height: 24 Image { anchors.centerIn: parent source: Quickshell.iconPath("media-skip-forward-symbolic") @@ -235,7 +259,8 @@ Item { id: maxNextMouse anchors.fill: parent enabled: root.player?.canGoNext ?? false - onClicked: if (root.player) root.player.next() + onClicked: if (root.player) + root.player.next() } } } diff --git a/modules/system/quickshell/MicInput.qml b/modules/system/quickshell/MicInput.qml new file mode 100644 index 0000000..2a1f653 --- /dev/null +++ b/modules/system/quickshell/MicInput.qml @@ -0,0 +1,204 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire + +Item { + id: root + width: indicator.visible ? indicator.width : 0 + height: parent.height + visible: root.micInUse + + readonly property PwNode source: Pipewire.defaultAudioSource + readonly property bool muted: source && source.audio ? source.audio.muted : true + readonly property real volume: source && source.audio ? source.audio.volume : 0 + readonly property bool micInUse: { + const nodes = Pipewire.nodes?.values || []; + for (const node of nodes) { + if (node && (node.type & PwNodeType.AudioInStream)) + return true; + } + return false; + } + + onMicInUseChanged: { + if (!micInUse && GlobalState.activePopup === "MicInput") + GlobalState.close(); + } + + PwObjectTracker { + objects: root.source ? [root.source] : [] + } + + function setVolume(value) { + if (source?.ready && source?.audio) { + source.audio.muted = false; + source.audio.volume = value; + } + } + + function toggleMute() { + if (source?.ready && source?.audio) { + source.audio.muted = !source.audio.muted; + } + } + + function getDeviceIcon(node) { + return node?.properties?.["device.icon-name"] ?? "audio-input-microphone"; + } + + Squircle { + id: indicator + anchors.verticalCenter: parent.verticalCenter + width: 28 + height: 22 + cornerRadius: 8 + fillColor: "#FFFF9500" + + Image { + anchors.centerIn: parent + width: 16 + height: 16 + source: Quickshell.iconPath(root.muted ? "microphone-sensitivity-muted-symbolic" : "audio-input-microphone-symbolic") + sourceSize: Qt.size(width, height) + smooth: true + mipmap: true + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + onClicked: mouse => { + if (mouse.button === Qt.RightButton) + root.toggleMute(); + else + GlobalState.toggle("MicInput"); + } + onWheel: wheel => { + const step = 0.05; + const next = wheel.angleDelta.y > 0 ? root.volume + step : root.volume - step; + root.setVolume(Math.max(0.0, Math.min(1.0, next))); + } + } + + AnchoredPopup { + popupName: "MicInput" + anchorWindow: barWindow + anchorItem: root + + PopupCard { + width: 280 + margins: 14 + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Text { + text: "Microphone" + color: Theme.text + font { + family: Theme.mainFont + pixelSize: 14 + weight: Font.DemiBold + } + } + + Item { + Layout.fillWidth: true + } + + Toggle { + checked: !root.muted + enabled: root.source !== null + onToggled: root.toggleMute() + } + } + + PillSlider { + id: micSlider + Layout.fillWidth: true + icon: "audio-input-microphone-symbolic" + value: root.volume + enabled: root.source !== null + colorProgress: "#FFFF9500" + onMoved: root.setVolume(value) + + Binding { + target: micSlider + property: "value" + value: root.volume + when: !micSlider.pressed + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Theme.border + } + + Text { + text: "Input Devices" + color: Theme.textMuted + font { + family: Theme.mainFont + pixelSize: 12 + weight: Font.DemiBold + } + Layout.leftMargin: 4 + } + + Repeater { + model: Pipewire.nodes + + delegate: Squircle { + id: inputItem + required property var modelData + readonly property bool isInput: modelData && (modelData.type & PwNodeType.AudioSource) && !modelData.isStream && modelData.name !== "Dummy-Driver" + + visible: isInput + Layout.fillWidth: true + Layout.preferredHeight: visible ? 40 : 0 + cornerRadius: 6 + fillColor: inputArea.containsMouse ? Theme.surfaceLighter : Theme.transparent + + RowLayout { + anchors { + fill: parent + margins: 8 + } + spacing: 12 + + IconCircle { + size: 24 + source: root.getDeviceIcon(inputItem.modelData) + active: root.source && root.source.id === inputItem.modelData.id + } + + Text { + Layout.fillWidth: true + text: inputItem.modelData.description || inputItem.modelData.nickname || inputItem.modelData.name + color: (root.source && root.source.id === inputItem.modelData.id) ? Theme.text : Theme.textDim + elide: Text.ElideRight + font { + family: Theme.mainFont + pixelSize: 13 + } + } + } + + MouseArea { + id: inputArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Pipewire.preferredDefaultAudioSource = inputItem.modelData + } + } + } + } + } +} diff --git a/modules/system/quickshell/MusicVisualizer.qml b/modules/system/quickshell/MusicVisualizer.qml index e136304..f0c3227 100644 --- a/modules/system/quickshell/MusicVisualizer.qml +++ b/modules/system/quickshell/MusicVisualizer.qml @@ -1,7 +1,5 @@ import QtQuick -// iOS-style animated music visualizer bars. -// Simulates audio reactivity by randomly changing bar heights when active. Row { id: root property bool active: false @@ -23,12 +21,15 @@ Row { interval: 100 + Math.random() * 200 repeat: true onTriggered: { - bar.height = 4 + Math.random() * (root.height - 4) + bar.height = 4 + Math.random() * (root.height - 4); } } Behavior on height { - NumberAnimation { duration: 150; easing.type: Easing.OutCubic } + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } } } } diff --git a/modules/system/quickshell/NotificationCard.qml b/modules/system/quickshell/NotificationCard.qml index 8b6478a..04810cb 100644 --- a/modules/system/quickshell/NotificationCard.qml +++ b/modules/system/quickshell/NotificationCard.qml @@ -22,7 +22,7 @@ Item { property bool hovered: cardHover.containsMouse || closeHover.containsMouse || (replyInput && replyInput.activeFocus) - readonly property bool hasBottom: (nActions ? nActions.count > 0 : false) || nHasInlineReply + readonly property bool hasBottom: (nActions ? nActions.length > 0 : false) || nHasInlineReply signal actionInvoked(int id, string identifier) signal replySent(int id, string text) @@ -36,15 +36,23 @@ Item { const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); - if (diffMins < 1) return "Just now"; - if (diffMins < 60) return diffMins + "m ago"; - if (diffHours < 24) return diffHours + "h ago"; + if (diffMins < 1) + return "Just now"; + if (diffMins < 60) + return diffMins + "m ago"; + if (diffHours < 24) + return diffHours + "h ago"; return Qt.formatTime(timestamp, "h:mm p"); } width: ListView.view ? ListView.view.width : 400 height: contentColumn.implicitHeight + 32 - Behavior on height { NumberAnimation { duration: 200; easing.type: Easing.OutSine } } + Behavior on height { + NumberAnimation { + duration: 200 + easing.type: Easing.OutSine + } + } Squircle { anchors.fill: parent @@ -60,7 +68,8 @@ Item { hoverEnabled: true cursorShape: nClickable ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: { - if (nClickable) card.activated(nId) + if (nClickable) + card.activated(nId); } } @@ -79,7 +88,6 @@ Item { spacing: 12 Layout.alignment: Qt.AlignTop - // LEFT: Icon Item { width: 44 height: 44 @@ -94,14 +102,14 @@ Item { Image { visible: nAppIcon !== "" anchors.centerIn: parent - width: 30; height: 30 + width: 30 + height: 30 source: nAppIcon fillMode: Image.PreserveAspectFit smooth: true } } - // MIDDLE: Text ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignTop @@ -171,7 +179,6 @@ Item { } } - // RIGHT: Image Item { visible: nImage !== "" width: visible ? 44 : 0 @@ -209,10 +216,10 @@ Item { Layout.fillWidth: true visible: card.hasBottom spacing: 8 - Layout.leftMargin: 56 // Align with text + Layout.leftMargin: 56 RowLayout { - visible: nActions && nActions.count > 0 + visible: nActions && nActions.length > 0 Layout.fillWidth: true spacing: 8 @@ -232,7 +239,12 @@ Item { cornerRadius: 6 fillColor: actionHover.containsMouse ? Theme.surfaceHover : Theme.surfaceLighter scale: actionHover.pressed ? 0.95 : 1.0 - Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } + Behavior on scale { + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } } Text { @@ -271,9 +283,12 @@ Item { TextInput { id: replyInput anchors { - left: parent.left; leftMargin: 10 - right: sendBtn.left; rightMargin: 6 - top: parent.top; bottom: parent.bottom + left: parent.left + leftMargin: 10 + right: sendBtn.left + rightMargin: 6 + top: parent.top + bottom: parent.bottom } verticalAlignment: TextInput.AlignVCenter color: Theme.text @@ -294,17 +309,25 @@ Item { font: replyInput.font } - Keys.onReturnPressed: if (text.length > 0) { card.replySent(card.nId, text); text = "" } - Keys.onEnterPressed: if (text.length > 0) { card.replySent(card.nId, text); text = "" } + Keys.onReturnPressed: if (text.length > 0) { + card.replySent(card.nId, text); + text = ""; + } + Keys.onEnterPressed: if (text.length > 0) { + card.replySent(card.nId, text); + text = ""; + } } Item { id: sendBtn anchors { - right: parent.right; rightMargin: 4 + right: parent.right + rightMargin: 4 verticalCenter: parent.verticalCenter } - width: 22; height: 22 + width: 22 + height: 22 opacity: replyInput.text.length > 0 ? 1.0 : 0.4 Rectangle { @@ -326,8 +349,8 @@ Item { enabled: replyInput.text.length > 0 cursorShape: Qt.PointingHandCursor onClicked: { - card.replySent(card.nId, replyInput.text) - replyInput.text = "" + card.replySent(card.nId, replyInput.text); + replyInput.text = ""; } } } @@ -335,7 +358,6 @@ Item { } } - // Close button — top-left, visible on hover Rectangle { width: 20 height: 20 @@ -350,7 +372,11 @@ Item { border.color: Theme.border border.width: 1 opacity: card.hovered ? 1.0 : 0.0 - Behavior on opacity { NumberAnimation { duration: 150 } } + Behavior on opacity { + NumberAnimation { + duration: 150 + } + } Item { anchors.centerIn: parent @@ -397,9 +423,11 @@ Item { target: card function onHoveredChanged() { if (!nResident) { - if (card.hovered) hideTimer.stop() - else hideTimer.restart() + if (card.hovered) + hideTimer.stop(); + else + hideTimer.restart(); } } } -}
\ No newline at end of file +} diff --git a/modules/system/quickshell/NotificationPopupList.qml b/modules/system/quickshell/NotificationPopupList.qml index 6b9280b..2ea7871 100644 --- a/modules/system/quickshell/NotificationPopupList.qml +++ b/modules/system/quickshell/NotificationPopupList.qml @@ -14,11 +14,14 @@ PanelWindow { visible: popupList.count > 0 - anchors { top: true; right: true } + anchors { + top: true + right: true + } WlrLayershell.margins.top: Theme.barHeight exclusionMode: ExclusionMode.Ignore color: "transparent" - + readonly property int edgeMargin: 12 readonly property int animationSafeMargin: 80 readonly property int popupWidth: 400 @@ -43,7 +46,7 @@ PanelWindow { model: root.popupModel interactive: false clip: false - + delegate: NotificationCard { onActionInvoked: (id, identifier) => root.actionInvoked(id, identifier) onReplySent: (id, text) => root.replySent(id, text) @@ -54,22 +57,46 @@ PanelWindow { add: Transition { ParallelAnimation { - NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 250; easing.type: Easing.OutSine } - NumberAnimation { property: "x"; from: popupWidth + edgeMargin; duration: 350; easing.type: Easing.OutBack } + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: 250 + easing.type: Easing.OutSine + } + NumberAnimation { + property: "x" + from: popupWidth + edgeMargin + duration: 350 + easing.type: Easing.OutBack + } } } remove: Transition { ParallelAnimation { - NumberAnimation { property: "opacity"; from: 1; to: 0; duration: 200; easing.type: Easing.InSine } - NumberAnimation { property: "x"; to: popupWidth + edgeMargin; duration: 200; easing.type: Easing.InSine } + NumberAnimation { + property: "opacity" + from: 1 + to: 0 + duration: 200 + easing.type: Easing.InSine + } + NumberAnimation { + property: "x" + to: popupWidth + edgeMargin + duration: 200 + easing.type: Easing.InSine + } } } displaced: Transition { - NumberAnimation { properties: "x,y"; duration: 250; easing.type: Easing.OutSine } + NumberAnimation { + properties: "x,y" + duration: 250 + easing.type: Easing.OutSine + } } } } - - diff --git a/modules/system/quickshell/Notifications.qml b/modules/system/quickshell/Notifications.qml index d17e903..79223eb 100644 --- a/modules/system/quickshell/Notifications.qml +++ b/modules/system/quickshell/Notifications.qml @@ -11,68 +11,72 @@ Scope { property int nextId: 0 property var liveNotifs: ({}) - ListModel { id: popupModel } + ListModel { + id: popupModel + } function removeNotificationData(id) { for (let i = 0; i < popupModel.count; i++) { if (popupModel.get(i).nId === id) { - popupModel.remove(i) - break + popupModel.remove(i); + break; } } - delete liveNotifs[id] + delete liveNotifs[id]; } function dismissExplicitly(id) { - const n = liveNotifs[id] - if (n) n.dismiss() - removeNotificationData(id) + const n = liveNotifs[id]; + if (n) + n.dismiss(); + removeNotificationData(id); } function hidePopup(id) { for (let i = 0; i < popupModel.count; i++) { if (popupModel.get(i).nId === id) { - popupModel.remove(i) - break + popupModel.remove(i); + break; } } } function activateById(id) { - const n = liveNotifs[id] - if (!n) return - - let invoked = false + const n = liveNotifs[id]; + if (!n) + return; + let invoked = false; for (const action of n.actions) { if (action.identifier === "default") { - action.invoke() - invoked = true - break + action.invoke(); + invoked = true; + break; } } if (!invoked && n.desktopEntry) { - Quickshell.execDetached(["gtk-launch", n.desktopEntry]) + Quickshell.execDetached(["gtk-launch", n.desktopEntry]); } - dismissExplicitly(id) + dismissExplicitly(id); } function invokeAction(id, identifier) { - const n = liveNotifs[id] + const n = liveNotifs[id]; if (n) { for (const action of n.actions) { if (action.identifier === identifier) { - action.invoke() - break + action.invoke(); + break; } } } - dismissExplicitly(id) + dismissExplicitly(id); } function sendReply(id, text) { - const n = liveNotifs[id] - if (n && n.hasInlineReply) n.sendInlineReply(text) - dismissExplicitly(id) + const n = liveNotifs[id]; + if (n && n.hasInlineReply) + n.sendInlineReply(text); + dismissExplicitly(id); } NotificationServer { @@ -87,34 +91,38 @@ Scope { persistenceSupported: true onNotification: notif => { - notif.tracked = true + notif.tracked = true; - const id = nextId - nextId = nextId + 1 - liveNotifs[id] = notif + const id = nextId; + nextId = nextId + 1; + liveNotifs[id] = notif; - const acts = [] - let hasDefault = false + const acts = []; + let hasDefault = false; for (const a of notif.actions) { - if (a.identifier === "default") hasDefault = true - else acts.push({ identifier: a.identifier, text: a.text }) + if (a.identifier === "default") + hasDefault = true; + else + acts.push({ + identifier: a.identifier, + text: a.text + }); } - notif.closed.connect(() => removeNotificationData(id)) + notif.closed.connect(() => removeNotificationData(id)); - let formattedAppIcon = notif.appIcon || "" + let formattedAppIcon = notif.appIcon || ""; if (formattedAppIcon !== "") { - if (formattedAppIcon.startsWith("file://")) { - } else if (formattedAppIcon.startsWith("/")) { - formattedAppIcon = "file://" + formattedAppIcon + if (formattedAppIcon.startsWith("file://")) {} else if (formattedAppIcon.startsWith("/")) { + formattedAppIcon = "file://" + formattedAppIcon; } else { - formattedAppIcon = `image://icon/${formattedAppIcon}` + formattedAppIcon = `image://icon/${formattedAppIcon}`; } } - let formattedImage = notif.image || "" + let formattedImage = notif.image || ""; if (formattedImage !== "" && formattedImage.startsWith("/")) { - formattedImage = "file://" + formattedImage + formattedImage = "file://" + formattedImage; } const data = { @@ -131,9 +139,10 @@ Scope { nHasInlineReply: notif.hasInlineReply || false, nReplyPlaceholder: notif.inlineReplyPlaceholder || "Reply", nResident: notif.resident || false - } + }; - popupModel.insert(0, data) + GlobalState.addNotification(data); + popupModel.insert(0, data); } } @@ -146,4 +155,3 @@ Scope { onHideRequested: id => scope.hidePopup(id) } } - diff --git a/modules/system/quickshell/PillSlider.qml b/modules/system/quickshell/PillSlider.qml index 5330ef8..e24663d 100644 --- a/modules/system/quickshell/PillSlider.qml +++ b/modules/system/quickshell/PillSlider.qml @@ -12,15 +12,15 @@ Slider { property color colorHandle: "#FFFFFF" property color iconColor: Theme.iconDefault - implicitHeight: 24 - padding: 0 + implicitHeight: 24 + padding: 0 background: Rectangle { width: root.width height: root.height radius: height / 2 color: root.colorTrack - + clip: true Rectangle { @@ -35,7 +35,7 @@ Slider { anchors.left: parent.left anchors.leftMargin: 6 anchors.verticalCenter: parent.verticalCenter - source: Quickshell.iconPath(root.icon) + source: root.icon !== "" ? Quickshell.iconPath(root.icon) : "" sourceSize: Qt.size(14, 14) width: 14 height: 14 @@ -52,7 +52,7 @@ Slider { handle: Item { x: root.visualPosition * (root.availableWidth - width) y: root.topPadding + root.availableHeight / 2 - height / 2 - + width: root.height height: root.height @@ -62,7 +62,7 @@ Slider { radius: width / 2 color: root.colorHandle border.width: 1 - border.color: "#1A000000" + border.color: "#1A000000" } MultiEffect { diff --git a/modules/system/quickshell/Polkit.qml b/modules/system/quickshell/Polkit.qml index 4ac8bd1..8c46084 100644 --- a/modules/system/quickshell/Polkit.qml +++ b/modules/system/quickshell/Polkit.qml @@ -10,12 +10,12 @@ PanelWindow { PolkitAgent { id: agent - + Component.onCompleted: console.log("PolkitAgent status: " + (isRegistered ? "Registered" : "Not Registered")) onIsRegisteredChanged: console.log("PolkitAgent registration changed: " + isRegistered) - + onIsActiveChanged: { - console.log("PolkitAgent active state: " + isActive) + console.log("PolkitAgent active state: " + isActive); if (isActive) { root.visible = true; passwordInput.text = ""; @@ -35,14 +35,14 @@ PanelWindow { right: true } exclusiveZone: 0 - + WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None Rectangle { anchors.fill: parent color: Theme.scrim - MouseArea { + MouseArea { anchors.fill: parent onClicked: { if (agent.flow) { @@ -64,7 +64,7 @@ PanelWindow { MouseArea { anchors.fill: parent - onClicked: (mouse) => mouse.accepted = true + onClicked: mouse => mouse.accepted = true } RowLayout { @@ -77,7 +77,7 @@ PanelWindow { Layout.alignment: Qt.AlignTop Layout.preferredWidth: 64 Layout.preferredHeight: 64 - + Image { anchors.fill: parent source: agent.flow ? Quickshell.iconPath(agent.flow.iconName || "dialog-password-symbolic") : "" @@ -136,7 +136,7 @@ PanelWindow { font.pixelSize: 12 color: Theme.text echoMode: (agent.flow && agent.flow.responseVisible) ? TextInput.Normal : TextInput.Password - + background: Rectangle { color: Theme.surface radius: 4 @@ -153,17 +153,21 @@ PanelWindow { } } - Item { Layout.preferredHeight: 4 } + Item { + Layout.preferredHeight: 4 + } RowLayout { Layout.fillWidth: true spacing: 8 - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } Button { text: "Cancel" - + contentItem: Text { text: parent.text color: Theme.text @@ -172,7 +176,7 @@ PanelWindow { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } - + background: Rectangle { implicitWidth: 80 implicitHeight: 28 @@ -181,7 +185,7 @@ PanelWindow { border.color: Theme.border border.width: 1 } - + onClicked: { if (agent.flow) { agent.flow.cancelAuthenticationRequest(); @@ -192,7 +196,7 @@ PanelWindow { Button { enabled: agent.flow && (!agent.flow.isResponseRequired || passwordInput.text !== "") text: "Authenticate" - + contentItem: Text { text: parent.text color: enabled ? Theme.text : Theme.textDisabled @@ -200,14 +204,14 @@ PanelWindow { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } - + background: Rectangle { implicitWidth: 100 implicitHeight: 28 color: parent.enabled ? (parent.hovered ? Theme.accentHover : Theme.accent) : Theme.surface radius: 6 } - + onClicked: { if (agent.flow) { agent.flow.submit(passwordInput.text); diff --git a/modules/system/quickshell/PopupCard.qml b/modules/system/quickshell/PopupCard.qml index 6563877..d4d609b 100644 --- a/modules/system/quickshell/PopupCard.qml +++ b/modules/system/quickshell/PopupCard.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts Squircle { id: root - readonly property int maxHeight: 440 + property int maxHeight: 440 property int margins: 16 default property alias content: contentCol.data @@ -13,7 +13,10 @@ Squircle { height: Math.min(Math.max(contentCol.implicitHeight + 2 * margins, 80), maxHeight) Behavior on height { - SpringAnimation { spring: 3; damping: 0.25 } + SpringAnimation { + spring: 3 + damping: 0.25 + } } fillColor: Theme.bg @@ -29,9 +32,7 @@ Squircle { } clip: true contentWidth: availableWidth - ScrollBar.vertical.policy: contentCol.implicitHeight > height - ? ScrollBar.AsNeeded - : ScrollBar.AlwaysOff + ScrollBar.vertical.policy: contentCol.implicitHeight > height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff ColumnLayout { id: contentCol diff --git a/modules/system/quickshell/ScreenRecordIndicator.qml b/modules/system/quickshell/ScreenRecordIndicator.qml new file mode 100644 index 0000000..6630b81 --- /dev/null +++ b/modules/system/quickshell/ScreenRecordIndicator.qml @@ -0,0 +1,38 @@ +import QtQuick +import Quickshell +import Quickshell.Services.Pipewire + +Item { + id: root + width: indicator.width + height: parent.height + visible: root.recording + + readonly property bool recording: { + const nodes = Pipewire.nodes?.values || []; + for (const node of nodes) { + if (node && node.isStream && (node.type & PwNodeType.VideoSource)) + return true; + } + return false; + } + + Squircle { + id: indicator + anchors.verticalCenter: parent.verticalCenter + width: 28 + height: 22 + cornerRadius: 8 + fillColor: Theme.destructive + + Image { + anchors.centerIn: parent + width: 15 + height: 15 + source: Quickshell.iconPath("media-record-symbolic") + sourceSize: Qt.size(width, height) + smooth: true + mipmap: true + } + } +} diff --git a/modules/system/quickshell/SliderBox.qml b/modules/system/quickshell/SliderBox.qml index 59b994d..1d84e72 100644 --- a/modules/system/quickshell/SliderBox.qml +++ b/modules/system/quickshell/SliderBox.qml @@ -7,7 +7,9 @@ Squircle { property string label: "" property string icon: "" property real value: 0 + property bool clickable: false signal moved(real val) + signal clicked Layout.fillWidth: true height: 64 @@ -18,7 +20,9 @@ Squircle { id: hoverArea anchors.fill: parent hoverEnabled: true - acceptedButtons: Qt.NoButton + acceptedButtons: root.clickable ? Qt.LeftButton : Qt.NoButton + cursorShape: root.clickable ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: root.clicked() } ColumnLayout { @@ -38,7 +42,9 @@ Squircle { } Layout.leftMargin: 2 } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } } PillSlider { diff --git a/modules/system/quickshell/Theme.qml b/modules/system/quickshell/Theme.qml index f409f98..249a808 100644 --- a/modules/system/quickshell/Theme.qml +++ b/modules/system/quickshell/Theme.qml @@ -1,22 +1,21 @@ -import QtQuick - pragma Singleton +import QtQuick QtObject { // Basics - readonly property color bg: "#33000000" // More translucent for macOS 18 style + readonly property color bg: "#33000000" readonly property color barBg: "#66000000" - readonly property color surface: "#4DFFFFFF" // Semi-transparent white/gray + readonly property color surface: "#4DFFFFFF" readonly property color surfaceHover: "#66FFFFFF" readonly property color surfaceLighter: "#80FFFFFF" readonly property color border: "#1AFFFFFF" - + // Accents - readonly property color accent: "#007AFF" // macOS Blue + readonly property color accent: "#007AFF" readonly property color accentHover: "#0066CC" readonly property color destructive: "#FF3B30" readonly property color focus: "#007AFF" - + // Text readonly property color text: "#FFFFFF" readonly property color textDim: "#EBEBEB" @@ -24,22 +23,22 @@ QtObject { readonly property color textDisabled: "#999999" readonly property color textPlaceholder: "#808080" readonly property color textLight: "#FFFFFF" - + // Icons readonly property color iconDefault: "#FFFFFF" readonly property color iconActive: "#007AFF" - + // Components readonly property color sliderTrack: "#33FFFFFF" readonly property color sliderHandle: "#FFFFFF" readonly property color sliderOutline: "#1A000000" - + // Transparency Helpers readonly property color scrim: "#80000000" readonly property color transparent: "transparent" // Layout - readonly property int barHeight: 32 // Slightly taller for macOS look + readonly property int barHeight: 32 readonly property int popupGap: 8 // Fonts diff --git a/modules/system/quickshell/Toggle.qml b/modules/system/quickshell/Toggle.qml index 76eb36c..d9e83e2 100644 --- a/modules/system/quickshell/Toggle.qml +++ b/modules/system/quickshell/Toggle.qml @@ -1,12 +1,9 @@ import QtQuick -// iOS-style on/off switch. `checked` is the visual state; the parent -// owns truth and handles `toggled` by flipping it. Uses Item.enabled -// for the disabled visual + input gating. Rectangle { id: root property bool checked: false - signal toggled() + signal toggled width: 40 height: 22 @@ -19,13 +16,21 @@ Rectangle { height: 18 radius: 9 color: Theme.text - anchors { verticalCenter: parent.verticalCenter } + anchors { + verticalCenter: parent.verticalCenter + } x: root.checked ? parent.width - width - 2 : 2 - Behavior on x { NumberAnimation { duration: 150 } } + Behavior on x { + NumberAnimation { + duration: 150 + } + } } MouseArea { - anchors { fill: parent } + anchors { + fill: parent + } onClicked: root.toggled() } } diff --git a/modules/system/quickshell/TrayMenu.qml b/modules/system/quickshell/TrayMenu.qml index 9e2e9f2..e8dd837 100644 --- a/modules/system/quickshell/TrayMenu.qml +++ b/modules/system/quickshell/TrayMenu.qml @@ -6,33 +6,33 @@ Scope { id: root property var menuItem: null property var parentWindow - property var anchorItem + property var anchorItem: null property bool active: false function open(item) { - anchorItem = item - active = true - menuAnchor.open() + anchorItem = item; + active = true; + menuAnchor.open(); } function close() { - active = false - menuAnchor.close() + active = false; + menuAnchor.close(); } QsMenuAnchor { id: menuAnchor menu: root.menuItem - + anchor.window: root.parentWindow anchor.item: root.anchorItem anchor.edges: Edges.Bottom anchor.gravity: Edges.Bottom anchor.margins.top: Theme.popupGap - + onVisibleChanged: { if (!visible && root.active) { - root.active = false + root.active = false; } } } diff --git a/modules/system/quickshell/Volume.qml b/modules/system/quickshell/Volume.qml index c4d1625..ea151dc 100644 --- a/modules/system/quickshell/Volume.qml +++ b/modules/system/quickshell/Volume.qml @@ -9,12 +9,10 @@ Item { width: childrenRect.width height: parent.height - // Properties bound directly to the Pipewire service readonly property PwNode sink: Pipewire.defaultAudioSink readonly property bool muted: sink && sink.audio ? sink.audio.muted : true readonly property real volume: sink && sink.audio ? sink.audio.volume : 0 - // Pipewire nodes only emit property updates while tracked. PwObjectTracker { objects: root.sink ? [root.sink] : [] } @@ -24,7 +22,6 @@ Item { sink.audio.muted = false; sink.audio.volume = value; } else { - // Fallback for unbound nodes Quickshell.execDetached(["wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", value.toFixed(2)]); } } @@ -38,7 +35,7 @@ Item { } function getDeviceIcon(node) { - return node?.properties?.["device.icon-name"] ?? "audio-card" + return node?.properties?.["device.icon-name"] ?? "audio-card"; } Row { @@ -64,39 +61,23 @@ Item { acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: mouse => { if (mouse.button === Qt.LeftButton) { - GlobalState.toggle("Volume") + GlobalState.toggle("Volume"); } else if (mouse.button === Qt.RightButton) { - root.toggleMute() + root.toggleMute(); } } onWheel: wheel => { - const step = 0.05 - const next = wheel.angleDelta.y > 0 ? root.volume + step : root.volume - step - root.setVolume(Math.max(0.0, Math.min(1.0, next))) + const step = 0.05; + const next = wheel.angleDelta.y > 0 ? root.volume + step : root.volume - step; + root.setVolume(Math.max(0.0, Math.min(1.0, next))); } } - PopupWindow { - id: popup - visible: GlobalState.activePopup === "Volume" - grabFocus: true - implicitWidth: bgRect.width - implicitHeight: bgRect.height - - anchor { - window: barWindow - item: root - edges: Edges.Bottom - gravity: Edges.Bottom - margins.top: Theme.popupGap - } - - color: "transparent" - - onVisibleChanged: { - if (visible) anchor.updateAnchor() - } - + AnchoredPopup { + popupName: "Volume" + anchorWindow: barWindow + anchorItem: root + Squircle { id: bgRect width: 260 @@ -140,7 +121,11 @@ Item { } } - Rectangle { width: parent.width; height: 1; color: Theme.border } + Rectangle { + width: parent.width + height: 1 + color: Theme.border + } Text { text: "Output Devices" @@ -160,23 +145,23 @@ Item { property bool isOutput: modelData && modelData.isSink && !modelData.isStream && modelData.name !== "Dummy-Driver" visible: isOutput height: isOutput ? 40 : 0 - + Squircle { anchors.fill: parent fillColor: mouseArea.containsMouse ? Theme.surfaceLighter : Theme.transparent cornerRadius: 6 - + RowLayout { anchors.fill: parent anchors.margins: 8 spacing: 12 - + IconCircle { size: 24 source: root.getDeviceIcon(modelData) active: root.sink && root.sink.id === modelData.id } - + Text { Layout.fillWidth: true text: modelData.description || modelData.nickname || modelData.name @@ -188,13 +173,13 @@ Item { elide: Text.ElideRight } } - + MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true onClicked: { - Quickshell.execDetached(["wpctl", "set-default", modelData.id.toString()]) + Quickshell.execDetached(["wpctl", "set-default", modelData.id.toString()]); } } } diff --git a/modules/system/quickshell/VolumeOSD.qml b/modules/system/quickshell/VolumeOSD.qml index 8efe305..67a1d62 100644 --- a/modules/system/quickshell/VolumeOSD.qml +++ b/modules/system/quickshell/VolumeOSD.qml @@ -4,7 +4,6 @@ import Quickshell import Quickshell.Services.Pipewire import Quickshell.Wayland -// Volume OSD that appears on volume change, styled to match the system menus. Scope { id: root @@ -18,14 +17,14 @@ Scope { Connections { target: root.sink && root.sink.audio ? root.sink.audio : null ignoreUnknownSignals: true - + function onVolumeChanged() { - root.visible = true - hideTimer.restart() + root.visible = true; + hideTimer.restart(); } function onMutedChanged() { - root.visible = true - hideTimer.restart() + root.visible = true; + hideTimer.restart(); } } @@ -37,19 +36,18 @@ Scope { PanelWindow { visible: root.visible - - // Compositor centers horizontally if only bottom anchor is set + anchors.bottom: true margins.bottom: 100 exclusiveZone: 0 implicitWidth: 240 implicitHeight: 64 - + WlrLayershell.layer: WlrLayer.Overlay exclusionMode: ExclusionMode.Ignore color: Theme.transparent - mask: Region {} // Pass-through clicks + mask: Region {} Squircle { id: card @@ -69,12 +67,16 @@ Scope { IconCircle { size: 32 source: { - if (root.sink?.audio?.muted) return "audio-volume-muted" - const vol = root.sink?.audio?.volume ?? 0 - if (vol <= 0) return "audio-volume-low" - if (vol <= 0.33) return "audio-volume-low" - if (vol <= 0.66) return "audio-volume-medium" - return "audio-volume-high" + if (root.sink?.audio?.muted) + return "audio-volume-muted"; + const vol = root.sink?.audio?.volume ?? 0; + if (vol <= 0) + return "audio-volume-low"; + if (vol <= 0.33) + return "audio-volume-low"; + if (vol <= 0.66) + return "audio-volume-medium"; + return "audio-volume-high"; } active: !(root.sink?.audio?.muted ?? true) } @@ -82,7 +84,7 @@ Scope { PillSlider { Layout.fillWidth: true value: root.sink?.audio?.volume ?? 0 - enabled: false // OSD is for display only + enabled: false } } } diff --git a/modules/system/quickshell/Wifi.qml b/modules/system/quickshell/Wifi.qml index 0ce57a0..9960575 100644 --- a/modules/system/quickshell/Wifi.qml +++ b/modules/system/quickshell/Wifi.qml @@ -11,72 +11,85 @@ Item { id: internal readonly property var device: { - if (!Networking.devices) return null + if (!Networking.devices) + return null; for (const d of Networking.devices.values || []) { - if (d && d.scannerEnabled !== undefined) return d + if (d && d.scannerEnabled !== undefined) + return d; } - return null + return null; } readonly property var allNetworks: device?.networks ? device.networks.values : [] + readonly property bool hasKnown: hasKnownNetwork() + readonly property bool hasOther: hasOtherNetwork() + readonly property var activeNetwork: findActiveNetwork() - property bool hasKnown: false - property bool hasOther: false - property var activeNetwork: null + function hasKnownNetwork() { + for (const n of internal.allNetworks) { + if (n?.known) + return true; + } + return false; + } - function updateState() { - let known = false - let other = false - let active = null - + function hasOtherNetwork() { for (const n of internal.allNetworks) { - if (n?.connected) active = n - if (n?.known) known = true - if (n && !n.known && n.name) other = true + if (n && !n.known && n.name) + return true; } - - internal.hasKnown = known - internal.hasOther = other - internal.activeNetwork = active + return false; } - Connections { - target: internal.device ? internal.device.networks : null - function onValuesChanged() { internal.updateState() } + function findActiveNetwork() { + for (const n of internal.allNetworks) { + if (n?.connected) + return n; + } + return null; } } function _getWifiIcon(strength) { - if (!Networking.wifiEnabled) return "network-wireless-offline-symbolic" - if (!internal.activeNetwork && !internal.device?.enabled) return "network-wireless-offline-symbolic" - - const s = strength ?? 0 - if (s >= 0.75) return "network-wireless-signal-excellent-symbolic" - if (s >= 0.50) return "network-wireless-signal-good-symbolic" - if (s >= 0.25) return "network-wireless-signal-ok-symbolic" - if (s > 0) return "network-wireless-signal-weak-symbolic" - return "network-wireless-signal-none-symbolic" + if (!Networking.wifiEnabled) + return "network-wireless-offline-symbolic"; + if (!internal.activeNetwork && !internal.device?.enabled) + return "network-wireless-offline-symbolic"; + + const s = strength ?? 0; + if (s >= 0.75) + return "network-wireless-signal-excellent-symbolic"; + if (s >= 0.50) + return "network-wireless-signal-good-symbolic"; + if (s >= 0.25) + return "network-wireless-signal-ok-symbolic"; + if (s > 0) + return "network-wireless-signal-weak-symbolic"; + return "network-wireless-signal-none-symbolic"; } function _onNetworkClick(net) { - if (!net) return - if (net.connected) { net.disconnect(); return } - if (net.stateChanging) return + if (!net) + return; + if (net.connected) { + net.disconnect(); + return; + } + if (net.stateChanging) + return; if (net.known) { - net.connect() + net.connect(); } else if ((net.security ?? 0) === 0) { - net.connect() + net.connect(); } else { - pskPrompt.network = net - pskPrompt.open(net.name ?? "") + pskPrompt.network = net; + pskPrompt.open(net.name ?? ""); } } width: childrenRect.width height: parent.height - Component.onCompleted: internal.updateState() - Row { anchors { verticalCenter: parent.verticalCenter @@ -98,44 +111,26 @@ Item { acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: mouse => { if (mouse.button === Qt.LeftButton) { - GlobalState.toggle("Wifi") + GlobalState.toggle("Wifi"); } else if (mouse.button === Qt.RightButton) { - Networking.wifiEnabled = !Networking.wifiEnabled + Networking.wifiEnabled = !Networking.wifiEnabled; } } } - PopupWindow { - id: popup - visible: GlobalState.activePopup === "Wifi" - grabFocus: true - implicitWidth: card.width - implicitHeight: card.height - - anchor { - window: barWindow - item: root - edges: Edges.Bottom - gravity: Edges.Bottom - margins.top: Theme.popupGap - } - - color: Theme.transparent - - onVisibleChanged: { - if (visible) { - anchor.updateAnchor() - if (internal.device) internal.device.scannerEnabled = true - } else if (internal.device?.scannerEnabled) { - internal.device.scannerEnabled = false - } - } + AnchoredPopup { + popupName: "Wifi" + anchorWindow: barWindow + anchorItem: root + onOpened: if (internal.device) + internal.device.scannerEnabled = true + onClosed: if (internal.device?.scannerEnabled) + internal.device.scannerEnabled = false PopupCard { id: card margins: 16 - // Wi-Fi { Toggle } RowLayout { Layout.fillWidth: true Layout.topMargin: 4 @@ -154,7 +149,9 @@ Item { } } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } Toggle { checked: Networking.wifiEnabled @@ -171,7 +168,6 @@ Item { Layout.bottomMargin: 4 } - // Known Network Header Text { visible: internal.hasKnown text: "Known Network" @@ -186,7 +182,6 @@ Item { Layout.bottomMargin: 2 } - // Known networks ColumnLayout { Layout.fillWidth: true spacing: 2 @@ -205,14 +200,6 @@ Item { fillColor: knownArea.containsMouse ? Theme.surface : Theme.transparent cornerRadius: 6 - Connections { - target: knownItem.modelData - function onKnownChanged() { internal.updateState() } - function onConnectedChanged() { internal.updateState() } - function onNameChanged() { internal.updateState() } - function onSignalStrengthChanged() { internal.updateState() } - } - MouseArea { id: knownArea anchors.fill: parent @@ -265,7 +252,6 @@ Item { Layout.bottomMargin: 4 } - // Other Networks Header RowLayout { Layout.fillWidth: true Layout.leftMargin: 8 @@ -307,14 +293,13 @@ Item { cursorShape: Qt.PointingHandCursor onClicked: { if (internal.device) { - internal.device.scannerEnabled = !internal.device.scannerEnabled + internal.device.scannerEnabled = !internal.device.scannerEnabled; } } } } } - // Other networks list ColumnLayout { Layout.fillWidth: true spacing: 2 @@ -332,13 +317,6 @@ Item { fillColor: otherArea.containsMouse ? Theme.surface : Theme.transparent cornerRadius: 6 - Connections { - target: otherItem.modelData - function onKnownChanged() { internal.updateState() } - function onNameChanged() { internal.updateState() } - function onSignalStrengthChanged() { internal.updateState() } - } - MouseArea { id: otherArea anchors.fill: parent @@ -390,8 +368,9 @@ Item { WifiPasswordPrompt { id: pskPrompt property var network: null - onSubmitted: (text, remember) => { - if (network) network.connectWithPsk(text) + onSubmitted: text => { + if (network) + network.connectWithPsk(text); } } } diff --git a/modules/system/quickshell/WifiPasswordPrompt.qml b/modules/system/quickshell/WifiPasswordPrompt.qml index c77c351..7a5eafc 100644 --- a/modules/system/quickshell/WifiPasswordPrompt.qml +++ b/modules/system/quickshell/WifiPasswordPrompt.qml @@ -3,42 +3,38 @@ import QtQuick.Layouts import QtQuick.Controls import Quickshell import Quickshell.Wayland -import Quickshell.Hyprland +import Quickshell.Hyprland PanelWindow { id: root - // Properties property string networkName: "" property string title: "The Wi-Fi network \"" + networkName + "\" requires a password." property string submitLabel: "Join" property string iconSource: "network-wireless-symbolic" - - signal submitted(string text, bool remember) - signal cancelled() - // Methods + signal submitted(string text) + signal cancelled + function open(name) { - networkName = name - passwordInput.text = "" - showPasswordCheck.checked = false - rememberNetworkCheck.checked = true - visible = true - Qt.callLater(() => passwordInput.forceActiveFocus()) + networkName = name; + passwordInput.text = ""; + showPasswordCheck.checked = false; + visible = true; + Qt.callLater(() => passwordInput.forceActiveFocus()); } - // Window Configuration visible: false color: Theme.transparent exclusiveZone: 0 - + anchors { top: true bottom: true left: true right: true } - + WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive @@ -48,12 +44,11 @@ PanelWindow { active: root.visible } - // UI Layout Rectangle { anchors.fill: parent color: Theme.scrim - MouseArea { - anchors.fill: parent + MouseArea { + anchors.fill: parent acceptedButtons: Qt.AllButtons hoverEnabled: true onWheel: wheel => wheel.accepted = true @@ -71,8 +66,8 @@ PanelWindow { cornerRadius: 12 Keys.onEscapePressed: { - root.visible = false - root.cancelled() + root.visible = false; + root.cancelled(); } RowLayout { @@ -85,7 +80,7 @@ PanelWindow { Layout.alignment: Qt.AlignTop Layout.preferredWidth: 64 Layout.preferredHeight: 64 - + Image { anchors.fill: parent source: Quickshell.iconPath(root.iconSource) @@ -133,7 +128,7 @@ PanelWindow { font.pixelSize: 12 color: Theme.text echoMode: showPasswordCheck.checked ? TextInput.Normal : TextInput.Password - + background: Rectangle { color: Theme.surface radius: 4 @@ -142,8 +137,8 @@ PanelWindow { } onAccepted: { - root.submitted(passwordInput.text, rememberNetworkCheck.checked) - root.visible = false + root.submitted(passwordInput.text); + root.visible = false; } } @@ -152,27 +147,24 @@ PanelWindow { text: "Show password" Layout.fillWidth: true } - - CustomCheckBox { - id: rememberNetworkCheck - text: "Remember this network" - checked: true - Layout.fillWidth: true - } } } - Item { Layout.preferredHeight: 4 } + Item { + Layout.preferredHeight: 4 + } RowLayout { Layout.fillWidth: true spacing: 8 - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } Button { text: "Cancel" - + contentItem: Text { text: parent.text color: Theme.text @@ -180,7 +172,7 @@ PanelWindow { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } - + background: Rectangle { implicitWidth: 80 implicitHeight: 28 @@ -189,16 +181,16 @@ PanelWindow { border.color: Theme.border border.width: 1 } - + onClicked: { - root.visible = false - root.cancelled() + root.visible = false; + root.cancelled(); } } Button { text: root.submitLabel - + contentItem: Text { text: parent.text color: Theme.text @@ -206,17 +198,17 @@ PanelWindow { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } - + background: Rectangle { implicitWidth: 80 implicitHeight: 28 color: parent.hovered ? Theme.accentHover : Theme.accent radius: 6 } - + onClicked: { - root.submitted(passwordInput.text, rememberNetworkCheck.checked) - root.visible = false + root.submitted(passwordInput.text); + root.visible = false; } } } diff --git a/modules/system/quickshell/Workspaces.qml b/modules/system/quickshell/Workspaces.qml index b63826d..44c0743 100644 --- a/modules/system/quickshell/Workspaces.qml +++ b/modules/system/quickshell/Workspaces.qml @@ -8,17 +8,19 @@ Item { implicitWidth: row.implicitWidth implicitHeight: 32 - ListModel { id: workspaceModel } + ListModel { + id: workspaceModel + } function updateWorkspaces(json) { try { - const ws = JSON.parse(json) - workspaceModel.clear() + const ws = JSON.parse(json); + workspaceModel.clear(); ws.forEach(w => workspaceModel.append({ - wsNum: w.num, - wsName: w.name, - wsFocused: w.focused - })) + wsNum: w.num, + wsName: w.name, + wsFocused: w.focused + })); } catch (_) {} } @@ -29,7 +31,8 @@ Item { stdout: SplitParser { onRead: _ => { - if (!refresher.running) refresher.running = true + if (!refresher.running) + refresher.running = true; } } } @@ -46,8 +49,8 @@ Item { onRunningChanged: { if (!running && buf !== "") { - root.updateWorkspaces(buf) - buf = "" + root.updateWorkspaces(buf); + buf = ""; } } @@ -89,8 +92,8 @@ Item { MouseArea { anchors.fill: parent onClicked: { - switcher.running = false - switcher.running = true + switcher.running = false; + switcher.running = true; } } } diff --git a/modules/system/quickshell/qmldir b/modules/system/quickshell/qmldir index 1c0341b..7f82d41 100644 --- a/modules/system/quickshell/qmldir +++ b/modules/system/quickshell/qmldir @@ -1,7 +1,9 @@ Bar Bar.qml +AnchoredPopup AnchoredPopup.qml Background Background.qml Bluetooth Bluetooth.qml BrightnessService BrightnessService.qml +Clock Clock.qml ControlCenter ControlCenter.qml singleton GlobalState GlobalState.qml IconCircle IconCircle.qml @@ -25,6 +27,8 @@ WifiPasswordPrompt WifiPasswordPrompt.qml Workspaces Workspaces.qml CustomCheckBox CustomCheckBox.qml MediaCard MediaCard.qml +MicInput MicInput.qml +ScreenRecordIndicator ScreenRecordIndicator.qml ConnectivityBox ConnectivityBox.qml ControlTile ControlTile.qml SliderBox SliderBox.qml diff --git a/modules/system/quickshell/shell.qml b/modules/system/quickshell/shell.qml index a61edad..bee997c 100644 --- a/modules/system/quickshell/shell.qml +++ b/modules/system/quickshell/shell.qml @@ -6,9 +6,9 @@ import Quickshell.Wayland ShellRoot { Component.onCompleted: { - Qt.application.font.family = "Inter" - Qt.application.font.hintingPreference = Font.PreferNoHinting - Qt.application.font.styleStrategy = Font.NoSubpixelAntialias + Qt.application.font.family = "Inter"; + Qt.application.font.hintingPreference = Font.PreferNoHinting; + Qt.application.font.styleStrategy = Font.NoSubpixelAntialias; } Variants { @@ -44,7 +44,7 @@ ShellRoot { WlSessionLock { id: sessionLock - + WlSessionLockSurface { LockSurface { anchors.fill: parent @@ -56,13 +56,12 @@ ShellRoot { IpcHandler { target: "bar" function toggleLauncher() { - GlobalState.toggle("Launcher") + GlobalState.toggle("Launcher"); } function lock() { lockContext.reset(); - sessionLock.locked = true + sessionLock.locked = true; } } } - diff --git a/modules/system/quickshell/squircle.frag b/modules/system/quickshell/squircle.frag index df2477f..058468e 100644 --- a/modules/system/quickshell/squircle.frag +++ b/modules/system/quickshell/squircle.frag @@ -27,13 +27,11 @@ void main() { vec2 halfSize = vec2(ubuf.width, ubuf.height) * 0.5; vec2 p = (qt_TexCoord0 * vec2(ubuf.width, ubuf.height)) - halfSize; - // Applied the scaling factor mentioned in the original comment float r = ubuf.cornerRadius * 1.5286; float dist = squircleSDF(p, halfSize, r); float fwidth_dist = fwidth(dist); - // Corrected edge parameter order to avoid undefined behavior float alpha = 1.0 - smoothstep(-fwidth_dist, fwidth_dist, dist); if (ubuf.strokeWidth > 0.0) { |
