diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 3998311..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.envrc -.direnv/ \ No newline at end of file diff --git a/modules/hyprland/hyprland.conf b/modules/hyprland/hyprland.conf index 44bd308..578181f 100644 --- a/modules/hyprland/hyprland.conf +++ b/modules/hyprland/hyprland.conf @@ -114,6 +114,9 @@ layerrule = noanim, shell:bar layerrule = noanim, shell:screenshot +layerrule = blur, walker +layerrule = ignorezero, walker +layerrule = animation popin 90%, walker windowrulev2 = float, class:^(opensnitch_ui)$ windowrulev2 = dimaround, class:^(opensnitch_ui)$ diff --git a/modules/user/modules/greetd/default.nix b/modules/user/modules/greetd/default.nix deleted file mode 100644 index 1828133..0000000 --- a/modules/user/modules/greetd/default.nix +++ /dev/null @@ -1,55 +0,0 @@ -{ inputs, system, config, lib, pkgs, ... }: let - hyprlandPackage = config.home-manager.users.${config.main-user}.wayland.windowManager.hyprland.package; - hyprlandConfig = pkgs.writeText "greetd-hyprland-config" '' - # for some reason pkill is way faster than dispatching exit, to the point greetd thinks the greeter died. - exec-once = quickshell -c greeter >& qslog.txt && pkill Hyprland - - input { - kb_layout = us - sensitivity = 0 - follow_mouse = 1 - # mouse_refocus = false - #6393 - accel_profile = flat - } - - decoration { - blur { - enabled = no - } - } - - animations { - enabled = no - } - - misc { - disable_hyprland_logo = true - disable_splash_rendering = true - background_color = 0x000000 - key_press_enables_dpms = true - mouse_move_enables_dpms = true - } - - ${config.hyprland-session.extraConfigStatic} - ''; -in { - services.greetd = { - enable = true; - restart = false; - settings.default_session.command = "${lib.getExe hyprlandPackage} -c ${hyprlandConfig}"; - }; - - # needed for hyprland cache dir - users.users.greeter = { - home = "/home/greeter"; - createHome = true; - }; - - home-manager.users.greeter = { - home.stateVersion = config.system.stateVersion; - imports = [ - ../../../theme/home.nix # also fixes cursor - ../quickshell # set up quickshell manifest and such - ]; - }; -} diff --git a/modules/user/modules/quickshell/default.nix b/modules/user/modules/quickshell/default.nix index 2cbae77..567db05 100644 --- a/modules/user/modules/quickshell/default.nix +++ b/modules/user/modules/quickshell/default.nix @@ -1,29 +1,16 @@ -{ config, inputs, pkgs, lib, system, impurity, ... }: let +{ inputs, pkgs, lib, system, impurity, ... }: let inherit (inputs) quickshell; - # hack because the greeter user cant access /home/admin - maybeLink = path: if config.home.username == "admin" then impurity.link path else path; in { home.packages = with pkgs; [ qt6.qtimageformats # amog qt6.qt5compat # shader fx - (quickshell.packages.${system}.default.override (prevqs: { - debug = true; - qt6 = prevqs.qt6.overrideScope (_: prevqt: { - qtdeclarative = prevqt.qtdeclarative.overrideAttrs (prev: { - cmakeBuildType = "Debug"; - dontStrip = true; - }); - }); - - breakpad = prevqs.breakpad.override rec { - stdenv = pkgs.gcc13Stdenv; - }; - })) + quickshell.packages.${system}.default + pamtester # lockscreen grim imagemagick # screenshot ]; xdg.configFile."quickshell/manifest.conf".text = lib.generators.toKeyValue {} { - shell = "${maybeLink ./.}/shell"; - greeter = "${maybeLink ./.}/shell/greeter.qml"; + shell = "${impurity.link ./shell}"; + lockscreen = "${impurity.link ./lockscreen}"; }; } diff --git a/modules/user/modules/quickshell/lockscreen/AuthContext.qml b/modules/user/modules/quickshell/lockscreen/AuthContext.qml new file mode 100644 index 0000000..f5c5745 --- /dev/null +++ b/modules/user/modules/quickshell/lockscreen/AuthContext.qml @@ -0,0 +1,43 @@ +import QtQuick +import Quickshell +import Quickshell.Io + +QtObject { + property int status: AuthContext.Status.FirstLogin + signal unlocked(); + + enum Status { + FirstLogin, + Authenticating, + LoginFailed + } + + property string password + + property var pamtester: Process { + property bool failed: true + + command: ["pamtester", "login", Quickshell.env("USER"), "authenticate"] + + onStarted: this.write(`${password}\n`) + + stdout: SplitParser { + // fails go to stderr + onRead: pamtester.failed = false + } + + onExited: { + if (failed) { + status = AuthContext.Status.LoginFailed; + } else { + unlocked(); + } + } + } + + function tryLogin(password: string) { + this.password = password + status = AuthContext.Status.Authenticating; + pamtester.running = true; + } +} diff --git a/modules/user/modules/quickshell/lockscreen/LockGlobals.qml b/modules/user/modules/quickshell/lockscreen/LockGlobals.qml new file mode 100644 index 0000000..70088e4 --- /dev/null +++ b/modules/user/modules/quickshell/lockscreen/LockGlobals.qml @@ -0,0 +1,17 @@ +pragma Singleton + +import QtQuick +import Quickshell + +Singleton { + property var time: new Date(); + property string text; + + Timer { + interval: 10000 + running: true + repeat: true + + onTriggered: time = new Date() + } +} diff --git a/modules/user/modules/quickshell/lockscreen/Lockscreen.qml b/modules/user/modules/quickshell/lockscreen/Lockscreen.qml new file mode 100644 index 0000000..4b258d0 --- /dev/null +++ b/modules/user/modules/quickshell/lockscreen/Lockscreen.qml @@ -0,0 +1,131 @@ +import QtQuick +import QtQuick.Controls.Basic +import Quickshell.Io + +Item { + required property AuthContext context + + Item { + anchors.centerIn: parent + + Text { + id: timeText + anchors { + bottom: entryBox.top + bottomMargin: 100 + horizontalCenter: parent.horizontalCenter + } + + font { + pointSize: 120 + hintingPreference: Font.PreferFullHinting + family: "Noto Sans" + } + + color: "white" + + text: { + const hours = LockGlobals.time.getHours().toString().padStart(2, '0'); + const minutes = LockGlobals.time.getMinutes().toString().padStart(2, '0'); + return `${hours}:${minutes}`; + } + } + + Text { + anchors { + top: timeText.bottom + topMargin: -20 + horizontalCenter: parent.horizontalCenter + } + + font.pointSize: 40 + + color: "#50ffffff" + text: "Locked" + } + + TextField { + id: entryBox + anchors.centerIn: parent + width: 600 + font.pointSize: 24 + + enabled: context.status != AuthContext.Status.Authenticating + focus: true + horizontalAlignment: TextInput.AlignHCenter + echoMode: TextInput.Password + inputMethodHints: Qt.ImhSensitiveData + onCursorVisibleChanged: { + if (cursorVisible) cursorVisible = false; + } + cursorVisible: false + + color: "white" + + background: Rectangle { + color: "#20ffffff" + border.color: "#30ffffff" + radius: height / 2 + } + + text: LockGlobals.text + onTextChanged: { + LockGlobals.text = text + } + + onAccepted: { + if (text != "") context.tryLogin(text) + } + + onEnabledChanged: { + if (enabled) text = "" + } + } + + Text { + id: status + color: "white" + font.pointSize: 24 + + anchors { + horizontalCenter: entryBox.horizontalCenter + top: entryBox.bottom + topMargin: 40 + } + + text: { + switch (context.status) { + case AuthContext.Status.FirstLogin: return "" + case AuthContext.Status.Authenticating: return "Authenticating" + case AuthContext.Status.LoginFailed: return "Login Failed" + } + } + } + } + + Button { + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: 20 + } + + contentItem: Text { + text: "Turn off Monitors" + color: "#aaeeffff" + } + + onClicked: dpms.running = true + + background: Rectangle { + color: "#20ffffff" + border.color: "#30ffffff" + radius: height / 2 + } + + Process { + id: dpms + command: [ "hyprctl", "dispatch", "dpms" ] + } + } +} diff --git a/modules/user/modules/quickshell/lockscreen/shell.qml b/modules/user/modules/quickshell/lockscreen/shell.qml new file mode 100644 index 0000000..e44d7ac --- /dev/null +++ b/modules/user/modules/quickshell/lockscreen/shell.qml @@ -0,0 +1,36 @@ +//@ pragma NativeTextRendering + +import QtQuick +import Quickshell +import Quickshell.Wayland +import ".." + +ShellRoot { + AuthContext { + id: authContext + onUnlocked: lock.locked = false + } + + WlSessionLock { + id: lock + locked: true + + onLockedChanged: { + if (!locked) Qt.quit(); + } + + WlSessionLockSurface { + id: surface + + BackgroundImage { + anchors.fill: parent + screen: surface.screen + } + + Lockscreen { + anchors.fill: parent + context: authContext + } + } + } +} diff --git a/modules/user/modules/quickshell/lockscreen/test.qml b/modules/user/modules/quickshell/lockscreen/test.qml new file mode 100644 index 0000000..9792c89 --- /dev/null +++ b/modules/user/modules/quickshell/lockscreen/test.qml @@ -0,0 +1,39 @@ +import QtQuick +import Quickshell +import ".." + +ShellRoot { + AuthContext { + id: authContext + onUnlocked: Qt.quit() + } + + FloatingWindow { + BackgroundImage { + anchors.fill: parent + screen: Quickshell.screens.filter(s => s.name == "eDP-1")[0] + } + + Lockscreen { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + right: parent.horizontalCenter + } + + context: authContext + } + + Lockscreen { + anchors { + left: parent.horizontalCenter + top: parent.top + bottom: parent.bottom + right: parent.right + } + + context: authContext + } + } +} diff --git a/modules/user/modules/quickshell/shell/HyprlandIpc.qml b/modules/user/modules/quickshell/shell/HyprlandIpc.qml new file mode 100644 index 0000000..55aa941 --- /dev/null +++ b/modules/user/modules/quickshell/shell/HyprlandIpc.qml @@ -0,0 +1,26 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io + +Singleton { + signal windowOpened(address: string, workspace: string, klass: string, title: string); + + Socket { + connected: true + path: `/tmp/hypr/${Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")}/.socket2.sock` + + parser: SplitParser { + onRead: message => { + const [type, body] = message.split(">>"); + const args = body.split(","); + + switch (type) { + case "openwindow": + windowOpened(args[0], args[1], args[2], args[3]) + break; + } + } + } + } +} diff --git a/modules/user/modules/quickshell/shell/PopupSurface.qml b/modules/user/modules/quickshell/shell/PopupSurface.qml new file mode 100644 index 0000000..d92134d --- /dev/null +++ b/modules/user/modules/quickshell/shell/PopupSurface.qml @@ -0,0 +1,81 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland + +WlrLayershell { + id: root + required property var bar; + + property var popup: null; + property list overlays: []; + property variant activeOverlay: null; + property variant lastActiveOverlay: null; + + onActiveOverlayChanged: { + if (lastActiveOverlay != null && lastActiveOverlay != activeOverlay) { + lastActiveOverlay.expanded = false; + } + + lastActiveOverlay = activeOverlay; + } + + readonly property rect barRect: { + void [width, height]; + this.contentItem.mapFromItem(bar, 0, 0, bar.width, bar.height) + } + + readonly property real overlayXOffset: barRect.x + barRect.width + 10 + + exclusionMode: ExclusionMode.Ignore + color: "transparent" + + namespace: "shell:bar" + + Variants { + id: masks + model: overlays + + Region { + required property var modelData; + item: modelData.widget + } + } + + mask: Region { + regions: masks.instances + } + + anchors { + left: true + top: true + bottom: true + } + + width: { + const extents = overlays + .filter(overlay => !overlay.fullyCollapsed) + .map(overlay => overlayXOffset + overlay.expandedWidth); + + return Math.max(barRect.x + barRect.width, ...extents); + } + + function connectOverlay(overlay: variant) { + overlay.widget.parent = this.contentItem + overlays.push(overlay); + } + + function disconnectOverlay(overlay: variant) { + // Splice seems to make it undefined as an intermediary step + // which breaks bindings. + overlays = overlays.filter(o => o != overlay); + } + + function expandedPosition(overlay: variant): rect { + const rect = overlay.collapsedLayerRect; + + const idealY = rect.y + (rect.height / 2) - (overlay.expandedHeight / 2) + const y = Math.max(barRect.y, Math.min((barRect.y + barRect.height) - overlay.expandedHeight, idealY)); + + return Qt.rect(overlayXOffset, y, overlay.expandedWidth, overlay.expandedHeight); + } +} diff --git a/modules/user/modules/quickshell/shell/SelectionArea.qml b/modules/user/modules/quickshell/shell/SelectionArea.qml new file mode 100644 index 0000000..b96ed83 --- /dev/null +++ b/modules/user/modules/quickshell/shell/SelectionArea.qml @@ -0,0 +1,27 @@ +import QtQuick + +Item { + id: root + required property var screen; + required property var selectionArea; + signal selectionComplete(x: real, y: real, width: real, height: real) + + MouseArea { + anchors.fill: parent + + onPressed: { + selectionArea.startX = mouseX; + selectionArea.startY = mouseY; + selectionArea.endX = mouseX; + selectionArea.endY = mouseY; + selectionArea.startSelection(false); + } + + onPositionChanged: { + selectionArea.endX = mouseX; + selectionArea.endY = mouseY; + } + + onReleased: selectionArea.endSelection(); + } +} diff --git a/modules/user/modules/quickshell/shell/SelectionLayer.qml b/modules/user/modules/quickshell/shell/SelectionLayer.qml new file mode 100644 index 0000000..a877f51 --- /dev/null +++ b/modules/user/modules/quickshell/shell/SelectionLayer.qml @@ -0,0 +1,99 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland + +WlrLayershell { + signal selectionComplete(x: real, y: real, width: real, height: real) + + color: "transparent" + visible: selectionArea.selecting || selectionArea.initializing + exclusionMode: ExclusionMode.Ignore + layer: WlrLayer.Overlay + namespace: "termspawner" + + anchors { + left: true + right: true + top: true + bottom: true + } + + property var selectionArea: area + + Rectangle { + id: area + property bool selecting: false + property bool initializing: false + property bool locked: false + property real startX: 0 + property real startY: 0 + property real endX: 0 + property real endY: 0 + + readonly property bool bigEnough: width > 300 && height > 150 + + border.color: bigEnough ? "#ee33ccff" : "#aa595959" + border.width: 1 + radius: 5 + color: "#66001017" + visible: selecting + + x: Math.min(startX, endX) - border.width + y: Math.min(startY, endY) - border.width + width: Math.max(startX, endX) - x + border.width * 2 + height: Math.max(startY, endY) - y + border.width * 2 + + function startSelection(initialize: bool) { + locked = false; + if (!initialize) { + selecting = true; + return; + } + + initializing = true + if (selecting) { + area.startX = mouseArea.mouseX; + area.startY = mouseArea.mouseY; + area.endX = mouseArea.mouseX; + area.endY = mouseArea.mouseY; + } + } + + function endSelection() { + const wasSelecting = selecting; + initializing = false; + + if (wasSelecting && bigEnough) { + locked = true; + selectionComplete(x + 1, y + 1, width - 2, height - 2); + } else { + selecting = false; + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + + hoverEnabled: true + onPositionChanged: { + if (area.initializing) { + if (!containsMouse) { + area.initializing = false; + return; + } + + area.startX = mouseX; + area.startY = mouseY; + area.initializing = false; + area.selecting = true; + } + + if (!selectionArea.locked) { + area.endX = mouseX; + area.endY = mouseY; + } + } + } +} diff --git a/modules/user/modules/quickshell/shell/ShellGlobals.qml b/modules/user/modules/quickshell/shell/ShellGlobals.qml index 69541eb..030322b 100644 --- a/modules/user/modules/quickshell/shell/ShellGlobals.qml +++ b/modules/user/modules/quickshell/shell/ShellGlobals.qml @@ -24,6 +24,16 @@ Singleton { curve.type: Easing.InQuart } + property var time: new Date(); + + Timer { + interval: 1000 + running: true + repeat: true + + onTriggered: time = new Date() + } + function interpolateColors(x: real, a: color, b: color): color { const xa = 1.0 - x; return Qt.rgba(a.r * xa + b.r * x, a.g * xa + b.g * x, a.b * xa + b.b * x, a.a * xa + b.a * x); diff --git a/modules/user/modules/quickshell/shell/ShellIpc.qml b/modules/user/modules/quickshell/shell/ShellIpc.qml index b6d6adc..3431a6a 100644 --- a/modules/user/modules/quickshell/shell/ShellIpc.qml +++ b/modules/user/modules/quickshell/shell/ShellIpc.qml @@ -1,14 +1,19 @@ pragma Singleton import Quickshell -import Quickshell.Io import Quickshell.Hyprland Singleton { + readonly property alias termSelect: termSelectBind.pressed; signal screenshot(); - IpcHandler { - target: "screenshot" - function takeScreenshot() { screenshot(); } + Shortcut { + name: "screenshot" + onPressed: screenshot() + } + + Shortcut { + id: termSelectBind + name: "termselect" } } diff --git a/modules/user/modules/quickshell/shell/Shortcut.qml b/modules/user/modules/quickshell/shell/Shortcut.qml new file mode 100644 index 0000000..aea3c7a --- /dev/null +++ b/modules/user/modules/quickshell/shell/Shortcut.qml @@ -0,0 +1,5 @@ +import Quickshell.Hyprland + +GlobalShortcut { + appid: "shell" +} diff --git a/modules/user/modules/quickshell/shell/SlideView.qml b/modules/user/modules/quickshell/shell/SlideView.qml deleted file mode 100644 index f6cb951..0000000 --- a/modules/user/modules/quickshell/shell/SlideView.qml +++ /dev/null @@ -1,112 +0,0 @@ -import QtQuick - -// kind of like a lighter StackView which handles replacement better. -Item { - id: root - - property Component enterTransition: XAnimator { - from: root.width - duration: 3000 - } - - property Component exitTransition: XAnimator { - to: target.x - target.width - duration: 3000 - } - - property bool animate: this.visible; - - onAnimateChanged: { - if (!this.animate) this.finishAnimations(); - } - - property Component itemComponent: SlideViewItem {} - property SlideViewItem activeItem: null; - property Item pendingItem: null; - property bool pendingNoAnim: false; - property list removingItems; - - readonly property bool animating: activeItem?.activeAnimation != null - - function replace(component: Component, defaults: var, noanim: bool) { - this.pendingNoAnim = noanim; - - if (component) { - const props = defaults ?? {}; - props.parent = null; - props.width = Qt.binding(() => this.width); - props.height = Qt.binding(() => this.height); - - const item = component.createObject(this, props); - if (pendingItem) pendingItem.destroy(); - pendingItem = item; - const ready = item?.svReady ?? true; - if (ready) finishPending(); - } else { - finishPending(); // remove - } - } - - Connections { - target: pendingItem - - function onSvReadyChanged() { - if (pendingItem.svReady) { - root.finishPending(); - } - } - } - - function finishPending() { - const noanim = this.pendingNoAnim || !this.animate; - if (this.activeItem) { - if (noanim) { - this.activeItem.destroyAll(); - this.activeItem = null; - } else { - removingItems.push(this.activeItem); - this.activeItem.animationCompleted.connect(item => root.removeItem(item)); - this.activeItem.stopIfRunning(); - this.activeItem.createAnimation(exitTransition); - this.activeItem = null; - } - } - - if (!this.animate) finishAnimations(); - - if (this.pendingItem) { - pendingItem.parent = this; - this.activeItem = itemComponent.createObject(this, { item: this.pendingItem }); - this.pendingItem = null; - if (!noanim) { - this.activeItem.createAnimation(enterTransition); - } - } - } - - function removeItem(item: SlideViewItem) { - item.destroyAll(); - - for (const i = 0; i !== this.removingItems.length; i++) { - if (this.removingItems[i] === item) { - removingItems.splice(i, 1); - break; - } - } - } - - function finishAnimations() { - this.removingItems.forEach(item => item.destroyAll()) - this.removingItems = []; - - if (this.activeItem) { - this.activeItem.finishIfRunning(); - } - } - - Component.onDestruction: { - this.removingItems.forEach(item => item.destroyAll()); - this.activeItem?.destroyAll(); - this.pendingItem?.destroy(); - } -} diff --git a/modules/user/modules/quickshell/shell/SlideViewItem.qml b/modules/user/modules/quickshell/shell/SlideViewItem.qml deleted file mode 100644 index 855e26c..0000000 --- a/modules/user/modules/quickshell/shell/SlideViewItem.qml +++ /dev/null @@ -1,47 +0,0 @@ -import Quickshell -import QtQuick - -QtObject { - id: root - required property Item item; - property Animation activeAnimation: null; - signal animationCompleted(self: SlideViewItem); - - property Connections __animConnection: Connections { - target: activeAnimation - - function onStopped() { - root.activeAnimation.destroy(); - root.animationCompleted(root); - } - } - - function createAnimation(component: Component) { - this.stopIfRunning(); - this.activeAnimation = component.createObject(this, { target: this.item }); - this.activeAnimation.running = true; - } - - function stopIfRunning() { - if (this.activeAnimation) { - this.activeAnimation.stop(); - this.activeAnimation = null; - } - } - - function finishIfRunning() { - if (this.activeAnimation) { - // animator types dont handle complete correctly. - this.activeAnimation.complete(); - this.activeAnimation.stop(); - this.item.x = 0; - this.item.y = 0; - this.activeAnimation = null; - } - } - - function destroyAll() { - this.item.destroy(); - this.destroy(); - } -} diff --git a/modules/user/modules/quickshell/shell/bar/Bar.qml b/modules/user/modules/quickshell/shell/bar/Bar.qml index 0d484a7..99acc26 100644 --- a/modules/user/modules/quickshell/shell/bar/Bar.qml +++ b/modules/user/modules/quickshell/shell/bar/Bar.qml @@ -1,4 +1,3 @@ -pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import QtQuick.Controls @@ -6,8 +5,7 @@ import Quickshell import "systray" as SysTray import "audio" as Audio import "mpris" as Mpris -import "power" as Power -import "root:notifications" as Notifs +import "workspaces" as Workspaces BarContainment { id: root @@ -15,7 +13,6 @@ BarContainment { property bool isSoleBar: Quickshell.screens.length == 1; ColumnLayout { - anchors { left: parent.left right: parent.right @@ -24,32 +21,24 @@ BarContainment { ColumnLayout { Layout.fillWidth: true + spacing: 0 - Notifs.NotificationWidget { + Loader { + active: isSoleBar + Layout.preferredHeight: active ? implicitHeight : 0; Layout.fillWidth: true - bar: root + + sourceComponent: Workspaces.Widget { + bar: root + wsBaseIndex: 1 + } } - ColumnLayout { - spacing: 0 - - Loader { - active: root.isSoleBar - Layout.preferredHeight: active ? implicitHeight : 0; - Layout.fillWidth: true - - sourceComponent: Workspaces { - bar: root - wsBaseIndex: 1 - } - } - - Workspaces { - bar: root - Layout.fillWidth: true - wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1; - hideWhenEmpty: root.isSoleBar - } + Workspaces.Widget { + bar: root + Layout.fillWidth: true + wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1; + hideWhenEmpty: isSoleBar } } } @@ -76,15 +65,8 @@ BarContainment { Layout.fillWidth: true } - Power.Power { - bar: root - Layout.fillWidth: true - } - ClockWidget { - bar: root Layout.fillWidth: true } - } } diff --git a/modules/user/modules/quickshell/shell/bar/BarButton.qml b/modules/user/modules/quickshell/shell/bar/BarButton.qml deleted file mode 100644 index 4d6a3cd..0000000 --- a/modules/user/modules/quickshell/shell/bar/BarButton.qml +++ /dev/null @@ -1,34 +0,0 @@ -import QtQuick -import QtQuick.Effects - -FullwidthMouseArea { - id: root - property bool showPressed: mouseArea.pressed; - property real baseMargin: 0; - property bool directScale: false; - - readonly property Item contentItem: mContentItem; - default property alias contentItemData: mContentItem.data; - - property real targetBrightness: root.showPressed ? -25 : root.mouseArea.containsMouse && root.enabled ? 75 : 0 - Behavior on targetBrightness { SmoothedAnimation { velocity: 600 } } - - property real targetMargins: root.showPressed ? 3 : 0; - Behavior on targetMargins { SmoothedAnimation { velocity: 25 } } - - hoverEnabled: true - - Item { - id: mContentItem - anchors.fill: parent; - - anchors.margins: root.baseMargin + (root.directScale ? 0 : root.targetMargins); - scale: root.directScale ? (width - root.targetMargins * 2) / width : 1.0; - - opacity: root.enabled ? 1.0 : 0.5; - Behavior on opacity { SmoothedAnimation { velocity: 5 } } - - layer.enabled: root.targetBrightness != 0 - layer.effect: MultiEffect { brightness: root.targetBrightness / 1000 } - } -} diff --git a/modules/user/modules/quickshell/shell/bar/BarContainment.qml b/modules/user/modules/quickshell/shell/bar/BarContainment.qml index 87ad94d..cb4343f 100644 --- a/modules/user/modules/quickshell/shell/bar/BarContainment.qml +++ b/modules/user/modules/quickshell/shell/bar/BarContainment.qml @@ -1,8 +1,8 @@ import QtQuick import Quickshell import Quickshell.Wayland -import ".." -import "../lock" as Lock +import "root:." +import "root:lock" as Lock PanelWindow { id: root @@ -20,9 +20,9 @@ PanelWindow { exclusiveZone: width - margins.left color: "transparent" - WlrLayershell.namespace: "shell:bar" + readonly property var tooltip: tooltip; Tooltip { id: tooltip diff --git a/modules/user/modules/quickshell/shell/bar/BugTester.qml b/modules/user/modules/quickshell/shell/bar/BugTester.qml new file mode 100644 index 0000000..6becec6 --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/BugTester.qml @@ -0,0 +1,31 @@ +import QtQuick + +BarWidgetInner { + implicitHeight: 50 + + SequentialAnimation on implicitHeight { + loops: Animation.Infinite + PropertyAnimation { to: 70; duration: 1000 } + PropertyAnimation { to: 40; duration: 1000 } + } + + property int len: 1 + + Text { + anchors.centerIn: parent + text: `8${'='.repeat(len)}D` + font.pointSize: 16 + color: "white" + + PropertyAnimation on rotation { + loops: Animation.Infinite + to: 365 + duration: 1000 + } + } + + MouseArea { + anchors.fill: parent + onClicked: len += 1; + } +} diff --git a/modules/user/modules/quickshell/shell/bar/ClickableIcon.qml b/modules/user/modules/quickshell/shell/bar/ClickableIcon.qml deleted file mode 100644 index 0df230e..0000000 --- a/modules/user/modules/quickshell/shell/bar/ClickableIcon.qml +++ /dev/null @@ -1,19 +0,0 @@ -import QtQuick - -BarButton { - id: root - required property string image; - property alias cache: imageComponent.cache; - property alias asynchronous: imageComponent.asynchronous; - property bool scaleIcon: !asynchronous - - Image { - id: imageComponent - anchors.fill: parent - - source: root.image - sourceSize.width: scaleIcon ? width : (root.width - baseMargin) - sourceSize.height: scaleIcon ? height : (root.height - baseMargin) - cache: false - } -} diff --git a/modules/user/modules/quickshell/shell/bar/ClockWidget.qml b/modules/user/modules/quickshell/shell/bar/ClockWidget.qml index 9a764f7..a843a43 100644 --- a/modules/user/modules/quickshell/shell/bar/ClockWidget.qml +++ b/modules/user/modules/quickshell/shell/bar/ClockWidget.qml @@ -1,67 +1,41 @@ import QtQuick import QtQuick.Layouts -import QtQuick.Controls -import Quickshell import ".." -BarWidgetInner { - id: root - required property var bar; +OverlayWidget { + expandedWidth: 600 + expandedHeight: 600 - implicitHeight: layout.implicitHeight + BarWidgetInner { + implicitHeight: layout.implicitHeight - SystemClock { - id: clock - precision: tooltip.visible ? SystemClock.Seconds : SystemClock.Minutes; - } + ColumnLayout { + id: layout + spacing: 0 - BarButton { - id: button - anchors.fill: parent - fillWindowWidth: true - acceptedButtons: Qt.NoButton - - ColumnLayout { - id: layout - spacing: 0 - - anchors { - right: parent.right - left: parent.left - } - - Text { - Layout.alignment: Qt.AlignHCenter - text: { - const hours = clock.hours.toString().padStart(2, '0') - const minutes = clock.minutes.toString().padStart(2, '0') - return `${hours}\n${minutes}` - } - font.pointSize: 18 - color: "white" - } - } - } - - property TooltipItem tooltip: TooltipItem { - id: tooltip - tooltip: bar.tooltip - owner: root - show: button.containsMouse - - Loader { - active: tooltip.visible - sourceComponent: Label { - text: { - // SystemClock can send an update slightly (<50ms) before the - // time changes. We use its readout so the widget and tooltip match. - const hours = clock.hours.toString().padStart(2, '0'); - const minutes = clock.minutes.toString().padStart(2, '0'); - const seconds = clock.seconds.toString().padStart(2, '0'); - - return `${hours}:${minutes}:${seconds}\n` + new Date().toLocaleString(Qt.locale("en_US"), "dddd, MMMM d, yyyy"); - } + anchors { + right: parent.right + left: parent.left } + + Text { + Layout.alignment: Qt.AlignHCenter + text: ShellGlobals.time.getHours().toString().padStart(2, '0') + font.pointSize: 18 + color: "#a0ffffff" + } + + Text { + Layout.alignment: Qt.AlignHCenter + text: ShellGlobals.time.getMinutes().toString().padStart(2, '0') + font.pointSize: 18 + color: "#a0ffffff" + } + } + + MouseArea { + anchors.fill: parent + onClicked: expanded = !expanded } } } diff --git a/modules/user/modules/quickshell/shell/bar/ExpandingWidget.qml b/modules/user/modules/quickshell/shell/bar/ExpandingWidget.qml new file mode 100644 index 0000000..909bd6a --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/ExpandingWidget.qml @@ -0,0 +1,97 @@ +import QtQuick +import Quickshell +import ".." + +Item { + required property var bar; + required property real expandedWidth; + required property real expandedHeight; + required default property Item widget; + + property bool expanded: false; + + onExpandedChanged: { + animateTo(expanded ? 1.0 : 0.0) + if (expanded) popupSurface.activeOverlay = this + } + + readonly property bool fullyCollapsed: animationProgress == 0.0; + + onFullyCollapsedChanged: { + if (fullyCollapsed && popupSurface.activeOverlay == this) { + popupSurface.activeOverlay = null; + } + + /*if (fullyCollapsed) { + widget.x = Qt.binding(() => this.x) + }*/ + } + + readonly property rect collapsedLayerRect: { + void [barWindow.windowTransform, popupSurface.windowTransform, y, parent.y]; + return this.mapToItem(popupSurface.contentItem, 0, 0, width, height); + } + + onCollapsedLayerRectChanged: console.log(`clr: ${collapsedLayerRect}`) + onLayerRectChanged: console.log(`lr: ${layerRect}`) + onYChanged: console.log(`y: ${y}`) + + readonly property rect expandedLayerRect: bar.widgetSurface.expandedPosition(this) + + readonly property rect layerRect: { + const [p, xCurve, yCurve] = [animationProgress, ShellGlobals.popoutXCurve, ShellGlobals.popoutYCurve]; + + return Qt.rect( + xCurve.interpolate(p, collapsedLayerRect.x, expandedLayerRect.x), + yCurve.interpolate(p, collapsedLayerRect.y, expandedLayerRect.y), + xCurve.interpolate(p, collapsedLayerRect.width, expandedLayerRect.width), + yCurve.interpolate(p, collapsedLayerRect.height, expandedLayerRect.height), + ); + } + + implicitWidth: widget.implicitWidth + implicitHeight: widget.implicitHeight + + Component.onCompleted: { + popupSurface.connectOverlay(this); + widget.x = Qt.binding(() => layerRect.x); + widget.y = Qt.binding(() => layerRect.y); + widget.width = Qt.binding(() => layerRect.width); + widget.height = Qt.binding(() => layerRect.height); + } + + Component.onDestruction: { + popupSurface.disconnectOverlay(this) + } + + function animateTo(target: real) { + animationProgressInternal = target * 1000 + } + + property real animationProgress: animationProgressInternal * 0.001 + property real animationProgressInternal: 0.0 // animations seem to only have int precision + + Behavior on animationProgressInternal { + SmoothedAnimation { velocity: 3000 } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onPressed: expanded = false + + Rectangle { + anchors.fill: parent + border.color: ShellGlobals.colors.widgetOutline + border.width: 1 + radius: 5 + color: "transparent" + opacity: mouseArea.containsMouse ? 1.0 : 0.0 + + Behavior on opacity { + SmoothedAnimation { velocity: 4 } + } + } + } +} diff --git a/modules/user/modules/quickshell/shell/bar/FullwidthMouseArea.qml b/modules/user/modules/quickshell/shell/bar/FullwidthMouseArea.qml deleted file mode 100644 index bb41790..0000000 --- a/modules/user/modules/quickshell/shell/bar/FullwidthMouseArea.qml +++ /dev/null @@ -1,53 +0,0 @@ -import QtQuick - -Item { - id: root - - property bool fillWindowWidth: false; - property real extraVerticalMargin: 0; - - property alias mouseArea: mouseArea; - property alias hoverEnabled: mouseArea.hoverEnabled; - property alias acceptedButtons: mouseArea.acceptedButtons; - property alias pressedButtons: mouseArea.pressedButtons; - property alias containsMouse: mouseArea.containsMouse; - property alias isPressed: mouseArea.pressed; - - signal clicked(event: MouseEvent); - signal entered(); - signal exited(); - signal pressed(event: MouseEvent); - signal released(event: MouseEvent); - signal wheel(event: WheelEvent); - - MouseArea { - id: mouseArea - - anchors { - fill: parent - // not much point in finding exact values - leftMargin: root.fillWindowWidth ? -50 : 0 - rightMargin: root.fillWindowWidth ? -50 : 0 - topMargin: -root.extraVerticalMargin - bottomMargin: -root.extraVerticalMargin - } - - Component.onCompleted: { - this.clicked.connect(root.clicked); - //this.entered.connect(root.entered); - //this.exited.connect(root.exited); - //this.pressed.connect(root.pressed); - this.released.connect(root.released); - //this.wheel.connect(root.wheel); - } - - // for some reason MouseArea.pressed is both a prop and signal so connect doesn't work - onPressed: event => root.pressed(event); - - // connecting to onwheel seems to implicitly accept it. undo that. - onWheel: event => { - event.accepted = false; - root.wheel(event); - } - } -} diff --git a/modules/user/modules/quickshell/shell/bar/OverlayWidget.qml b/modules/user/modules/quickshell/shell/bar/OverlayWidget.qml new file mode 100644 index 0000000..7d31693 --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/OverlayWidget.qml @@ -0,0 +1,17 @@ +import QtQuick + +Item { + default property Item item; + property int expandedWidth; + property int expandedHeight; + + implicitHeight: item.implicitHeight + implicitWidth: item.implicitWidth + + Component.onCompleted: { + item.width = Qt.binding(() => this.width) + item.height = Qt.binding(() => this.height) + } + + children: [ item ] +} diff --git a/modules/user/modules/quickshell/shell/bar/Tooltip.qml b/modules/user/modules/quickshell/shell/bar/Tooltip.qml index 463036e..31c7171 100644 --- a/modules/user/modules/quickshell/shell/bar/Tooltip.qml +++ b/modules/user/modules/quickshell/shell/bar/Tooltip.qml @@ -1,7 +1,6 @@ import QtQuick import Quickshell import Quickshell.Hyprland -import "root:/" Scope { id: root @@ -12,14 +11,11 @@ Scope { readonly property TooltipItem activeItem: activeMenu ?? activeTooltip; property TooltipItem lastActiveItem: null; - readonly property TooltipItem shownItem: activeItem ?? lastActiveItem; - property real hangTime: lastActiveItem?.hangTime ?? 0; property Item tooltipItem: null; onActiveItemChanged: { if (activeItem != null) { - hangTimer.stop(); activeItem.targetVisible = true; if (tooltipItem) { @@ -28,12 +24,10 @@ Scope { } if (lastActiveItem != null && lastActiveItem != activeItem) { - if (activeItem != null) lastActiveItem.targetVisible = false; - else if (root.hangTime == 0) doLastHide(); - else hangTimer.start(); + lastActiveItem.targetVisible = false; } - if (activeItem != null) lastActiveItem = activeItem; + lastActiveItem = activeItem; } function setItem(item: TooltipItem) { @@ -52,59 +46,28 @@ Scope { } } - function doLastHide() { - lastActiveItem.targetVisible = false; - } - - function onHidden(item: TooltipItem) { - if (item == lastActiveItem) { - lastActiveItem = null; - } - } - - Timer { - id: hangTimer - interval: root.hangTime - onTriggered: doLastHide(); - } - - property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0; - Behavior on scaleMul { SmoothedAnimation { velocity: 5 } } - LazyLoader { id: popupLoader - activeAsync: shownItem != null + activeAsync: activeItem != null PopupWindow { id: popup - - anchor { - window: bar - rect.x: bar.tooltipXOffset - rect.y: tooltipItem.highestAnimY - adjustment: PopupAdjustment.None - } - - HyprlandWindow.opacity: root.scaleMul - - //height: bar.height - width: Math.max(700, tooltipItem.largestAnimWidth) // max due to qtwayland glitches - height: { - const h = tooltipItem.lowestAnimY - tooltipItem.highestAnimY - //console.log(`seth ${h} ${tooltipItem.highestAnimY} ${tooltipItem.lowestAnimY}; ${tooltipItem.y1} ${tooltipItem.y2}`) - return h - } + parentWindow: bar + relativeX: bar.tooltipXOffset + relativeY: 0 + height: bar.height + width: 1000//Math.max(1, widthAnim.running ? Math.max(tooltipItem.targetWidth, tooltipItem.lastTargetWidth) : tooltipItem.targetWidth) visible: true color: "transparent" //color: "#20000000" mask: Region { - item: (shownItem?.hoverable ?? false) ? tooltipItem : null + item: (activeItem?.hoverable ?? false) ? tooltipItem : null } HyprlandFocusGrab { active: activeItem?.isMenu ?? false - windows: [ popup, bar, ...(activeItem?.grabWindows ?? []) ] + windows: [ popup, bar ] onActiveChanged: { if (!active && activeItem?.isMenu) { activeMenu.close() @@ -112,76 +75,30 @@ Scope { } } - /*Rectangle { - color: "#10ff0000" - //y: tooltipItem.highestAnimY - height: tooltipItem.lowestAnimY - tooltipItem.highestAnimY - width: parent.width - } - - Rectangle { - color: "#1000ff00" - //y: tooltipItem.highestAnimY - height: popup.height - width: parent.width - }*/ - Item { id: tooltipItem Component.onCompleted: { root.tooltipItem = this; - if (root.shownItem) { - root.shownItem.parent = this; + if (root.activeItem) { + root.activeItem.parent = this; } - - //highestAnimY = targetY - targetHeight / 2; - //lowestAnimY = targetY + targetHeight / 2; } - transform: Scale { - origin.x: 0 - origin.y: tooltipItem.height / 2 - xScale: 0.9 + scaleMul * 0.1 - yScale: xScale - } + readonly property var targetWidth: activeItem?.implicitWidth ?? 0; + readonly property var targetHeight: activeItem?.implicitHeight ?? 0; - clip: width != targetWidth || height != targetHeight - - // bkg - BarWidgetInner { - anchors.fill: parent - color: ShellGlobals.colors.bar - } - - readonly property var targetWidth: shownItem?.implicitWidth ?? 0; - readonly property var targetHeight: shownItem?.implicitHeight ?? 0; - - property var largestAnimWidth: 0; - property var highestAnimY: 0; // unused due to reposition timing issues - property var lowestAnimY: bar.height; + property var lastTargetWidthTracker: 0; + property var lastTargetWidth: 0; onTargetWidthChanged: { - if (targetWidth > largestAnimWidth) { - largestAnimWidth = targetWidth; - } - } - - onTargetYChanged: updateYBounds(); - onTargetHeightChanged: updateYBounds(); - function updateYBounds() { - if (targetY - targetHeight / 2 < highestAnimY) { - //highestAnimY = targetY - targetHeight / 2 - } - - if (targetY + targetHeight / 2 > lowestAnimY) { - //lowestAnimY = targetY + targetHeight / 2 - } + lastTargetWidth = lastTargetWidthTracker; + lastTargetWidthTracker = targetWidth; } readonly property real targetY: { - if (shownItem == null) return 0; - const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y; - return bar.boundedY(target, shownItem.implicitHeight / 2); + if (activeItem == null) return 0; + const target = bar.contentItem.mapFromItem(activeItem.owner, 0, activeItem.targetRelativeY).y; + return bar.boundedY(target, activeItem.implicitHeight / 2); } property var w: -1 @@ -190,24 +107,15 @@ Scope { property var y1: -1 property var y2: -1 - y: y1 - popup.anchor.rect.y + y: y1 height: y2 - y1 - readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running - - onAnyAnimsRunningChanged: { - if (!anyAnimsRunning) { - largestAnimWidth = targetWidth - //highestAnimY = y1; - //lowestAnimY = y2; - } - } - - SmoothedAnimation on y1 { - id: y1Anim + SmoothedAnimation { + target: tooltipItem; + property: "y1"; to: tooltipItem.targetY - tooltipItem.targetHeight / 2; onToChanged: { - if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) { + if (tooltipItem.y1 == -1 || !(activeItem?.animateSize ?? true)) { stop(); tooltipItem.y1 = to; } else { @@ -217,11 +125,12 @@ Scope { } } - SmoothedAnimation on y2 { - id: y2Anim + SmoothedAnimation { + target: tooltipItem + property: "y2" to: tooltipItem.targetY + tooltipItem.targetHeight / 2; onToChanged: { - if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) { + if (tooltipItem.y2 == -1 || !(activeItem?.animateSize ?? true)) { stop(); tooltipItem.y2 = to; } else { @@ -231,11 +140,13 @@ Scope { } } - SmoothedAnimation on w { + SmoothedAnimation { id: widthAnim + target: tooltipItem + property: "w" to: tooltipItem.targetWidth; onToChanged: { - if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) { + if (tooltipItem.w == -1) { stop(); tooltipItem.w = to; } else { diff --git a/modules/user/modules/quickshell/shell/bar/TooltipItem.qml b/modules/user/modules/quickshell/shell/bar/TooltipItem.qml index b5d035c..aadb179 100644 --- a/modules/user/modules/quickshell/shell/bar/TooltipItem.qml +++ b/modules/user/modules/quickshell/shell/bar/TooltipItem.qml @@ -1,12 +1,12 @@ import QtQuick import Quickshell +import "root:/" Item { id: root required property var tooltip; required property Item owner; property bool isMenu: false; - property list grabWindows; property bool hoverable: isMenu; property bool animateSize: true; property bool show: false; @@ -17,35 +17,47 @@ Item { signal close(); - readonly property alias contentItem: contentItem; default property alias data: contentItem.data; - property Component backgroundComponent: null + property Component backgroundComponent: BarWidgetInner { + color: ShellGlobals.colors.bar + anchors.fill: parent + } onShowChanged: { - if (show) tooltip.setItem(this); - else tooltip.removeItem(this); + if (show) { + hangTimer.stop(); + tooltip.setItem(this); + } else if (hangTime == 0) { + tooltip.removeItem(this); + } else hangTimer.start(); + } + + Timer { + id: hangTimer + interval: hangTime + onTriggered: tooltip.removeItem(root); } property bool targetVisible: false property real targetOpacity: 0 - opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul)) + opacity: targetOpacity / 1000 Behavior on targetOpacity { id: opacityAnimation - SmoothedAnimation { velocity: 5 } + SmoothedAnimation { velocity: 5000 } } function snapOpacity(opacity: real) { opacityAnimation.enabled = false; - targetOpacity = opacity; + targetOpacity = opacity * 1000 opacityAnimation.enabled = true; } onTargetVisibleChanged: { if (targetVisible) { visible = true; - targetOpacity = 1; + targetOpacity = 1000; } else { close() targetOpacity = 0; @@ -56,21 +68,20 @@ Item { if (!targetVisible && targetOpacity == 0) { visible = false; this.parent = null; - if (tooltip) tooltip.onHidden(this); } } anchors.fill: parent visible: false - //clip: true - implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin - implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin + clip: true + implicitHeight: contentItem.implicitHeight + 10 + implicitWidth: contentItem.implicitWidth + 10 readonly property Item item: contentItem; Loader { anchors.fill: parent - active: root.backgroundComponent && (root.visible || root.preloadBackground) + active: root.visible || root.preloadBackground asynchronous: !root.visible && root.preloadBackground sourceComponent: backgroundComponent } @@ -80,7 +91,7 @@ Item { anchors.fill: parent anchors.margins: 5 - implicitHeight: children[0].implicitHeight - implicitWidth: children[0].implicitWidth + implicitHeight: childrenRect.height + implicitWidth: childrenRect.width } } diff --git a/modules/user/modules/quickshell/shell/bar/audio/AudioControl.qml b/modules/user/modules/quickshell/shell/bar/audio/AudioControl.qml index ab19af8..525b895 100644 --- a/modules/user/modules/quickshell/shell/bar/audio/AudioControl.qml +++ b/modules/user/modules/quickshell/shell/bar/audio/AudioControl.qml @@ -12,20 +12,14 @@ ClickableIcon { implicitHeight: width; acceptedButtons: Qt.LeftButton | Qt.RightButton; - fillWindowWidth: true - showPressed: mixerOpen || (pressedButtons & ~Qt.RightButton) - - onPressed: event => { - event.accepted = true; - if (event.button === Qt.RightButton) { - mixerOpen = !mixerOpen; - } - } + showPressed: mixerOpen onClicked: event => { + event.accepted = true; if (event.button === Qt.LeftButton) { - event.accepted = true; node.audio.muted = !node.audio.muted; + } else if (event.button === Qt.RightButton) { + mixerOpen = !mixerOpen; } } @@ -77,16 +71,7 @@ ClickableIcon { sourceComponent: Mixer { width: 550 trackedNode: node - nodeList: Pipewire.nodes.values.filter(n => n.audio && !n.isStream && n.isSink == node.isSink) nodeImage: root.image - - onSelected: n => { - if (node.isSink) { - Pipewire.preferredDefaultAudioSink = n; - } else { - Pipewire.preferredDefaultAudioSource = n; - } - } } } } diff --git a/modules/user/modules/quickshell/shell/bar/audio/Mixer.qml b/modules/user/modules/quickshell/shell/bar/audio/Mixer.qml index 98d8d6d..ab11b22 100644 --- a/modules/user/modules/quickshell/shell/bar/audio/Mixer.qml +++ b/modules/user/modules/quickshell/shell/bar/audio/Mixer.qml @@ -1,18 +1,12 @@ import QtQuick import QtQuick.Layouts -import Quickshell import Quickshell.Services.Pipewire import ".." import "../.." ColumnLayout { - id: root - required property PwNode trackedNode; required property string nodeImage; - required property list nodeList; - - signal selected(node: PwNode); PwNodeLinkTracker { id: linkTracker @@ -21,13 +15,10 @@ ColumnLayout { PwObjectTracker { objects: [ trackedNode, ...linkTracker.linkGroups ] } - MixerEntry/*WithSelect*/ { + MixerEntry { id: nodeEntry node: trackedNode - //nodeList: root.nodeList image: nodeImage - - Component.onCompleted: this.selected.connect(root.selected); } Rectangle { @@ -58,7 +49,7 @@ ColumnLayout { // special cases :( if (icon == "firefox") icon = "firefox-devedition"; - return Quickshell.iconPath(icon) + return `image://icon/${icon}` } } } diff --git a/modules/user/modules/quickshell/shell/bar/audio/MixerEntry.qml b/modules/user/modules/quickshell/shell/bar/audio/MixerEntry.qml index 08bee93..ec78ec0 100644 --- a/modules/user/modules/quickshell/shell/bar/audio/MixerEntry.qml +++ b/modules/user/modules/quickshell/shell/bar/audio/MixerEntry.qml @@ -1,11 +1,51 @@ import QtQuick +import QtQuick.Layouts +import Quickshell.Services.Pipewire +import ".." -MixerEntryBase { +RowLayout { id: root + required property PwNode node; + required property string image; + property int state: PwLinkState.Unlinked; - headerComponent: Text { - color: "white" - elide: Text.ElideRight - text: root.getNodeName(root.node) + PwObjectTracker { objects: [ node ] } + + ClickableIcon { + image: root.image + asynchronous: true + implicitHeight: 40 + implicitWidth: height + } + + ColumnLayout { + RowLayout { + Item { + implicitHeight: title.implicitHeight + Layout.fillWidth: true + + Text { + id: title + color: "white" + anchors.fill: parent + elide: Text.ElideRight + text: { + const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description); + const mediaName = node.properties["media.name"]; + + return mediaName != undefined ? `${name} - ${mediaName}` : name; + } + } + } + } + + VolumeSlider { + //Layout.fillHeight: true + Layout.fillWidth: true + implicitWidth: 200 + + value: node.audio.volume + onValueChanged: node.audio.volume = value + } } } diff --git a/modules/user/modules/quickshell/shell/bar/audio/MixerEntryBase.qml b/modules/user/modules/quickshell/shell/bar/audio/MixerEntryBase.qml deleted file mode 100644 index 36491b6..0000000 --- a/modules/user/modules/quickshell/shell/bar/audio/MixerEntryBase.qml +++ /dev/null @@ -1,50 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell.Services.Pipewire -import ".." - -RowLayout { - id: root - - required property PwNode node; - required property string image; - required property Item headerComponent; - - property int state: PwLinkState.Unlinked; - - function getNodeName(node: PwNode): string { - const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description); - const mediaName = node.properties["media.name"]; - - return mediaName != undefined ? `${name} - ${mediaName}` : name + node.id; - } - - PwObjectTracker { objects: [ node ] } - - ClickableIcon { - image: root.image - asynchronous: true - implicitHeight: 40 - implicitWidth: height - } - - ColumnLayout { - Item { - id: container - - Layout.fillWidth: true - implicitWidth: headerComponent.implicitWidth - implicitHeight: headerComponent.implicitHeight - - children: [ headerComponent ] - Binding { root.headerComponent.anchors.fill: container } - } - - VolumeSlider { - Layout.fillWidth: true - - value: node.audio.volume - onValueChanged: node.audio.volume = value - } - } -} diff --git a/modules/user/modules/quickshell/shell/bar/audio/MixerEntryWithSelect.qml b/modules/user/modules/quickshell/shell/bar/audio/MixerEntryWithSelect.qml deleted file mode 100644 index f75ac87..0000000 --- a/modules/user/modules/quickshell/shell/bar/audio/MixerEntryWithSelect.qml +++ /dev/null @@ -1,16 +0,0 @@ -import QtQuick -import QtQuick.Controls -import Quickshell.Services.Pipewire - -MixerEntryBase { - id: root - required property list nodeList; - - signal selected(node: PwNode); - - headerComponent: ComboBox { - model: nodeList.map(node => root.getNodeName(node)); - currentIndex: nodeList.findIndex(node => node == root.node) - onActivated: index => root.selected(nodeList[index]) - } -} diff --git a/modules/user/modules/quickshell/shell/bar/audio/volume_off.svg b/modules/user/modules/quickshell/shell/bar/audio/volume_off.svg new file mode 100644 index 0000000..aeff292 --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/audio/volume_off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modules/user/modules/quickshell/shell/bar/audio/volume_up.svg b/modules/user/modules/quickshell/shell/bar/audio/volume_up.svg new file mode 100644 index 0000000..0d394c0 --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/audio/volume_up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modules/user/modules/quickshell/shell/bar/mpris/BackgroundArt.qml b/modules/user/modules/quickshell/shell/bar/mpris/BackgroundArt.qml index bfdacc8..b80994e 100644 --- a/modules/user/modules/quickshell/shell/bar/mpris/BackgroundArt.qml +++ b/modules/user/modules/quickshell/shell/bar/mpris/BackgroundArt.qml @@ -82,7 +82,7 @@ BarWidgetInner { x: blurRadius width: blur.width - blurRadius * 2 height: blur.height - + clip: true GaussianBlur { source: blurSource x: -parent.x @@ -139,27 +139,23 @@ BarWidgetInner { readonly property Rectangle overlay: overlayItem; Rectangle { id: overlayItem - visible: true + visible: false anchors.fill: parent - radius: root.radius + border.color: ShellGlobals.colors.widgetOutlineSeparate + border.width: 0//1 + radius: 0//root.radius color: "transparent" - - Rectangle { - anchors.fill: parent - radius: root.radius - color: "transparent" - border.color: ShellGlobals.colors.widgetOutlineSeparate; - border.width: 1 - } } // slightly offset on the corners :/ layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: root.width - height: root.height - radius: root.radius - } + layer.effect: ShaderEffect { + fragmentShader: "radial_clip.frag.qsb" + // +1 seems to match Rectangle + property real radius: root.radius + 1 + property size size: Qt.size(root.width, root.height) + property real borderWidth: 1//.5 + property color borderColor: ShellGlobals.colors.widgetOutlineSeparate//"#ffff0000" + property color tint: overlayItem.color } } diff --git a/modules/user/modules/quickshell/shell/bar/mpris/MprisController.qml b/modules/user/modules/quickshell/shell/bar/mpris/MprisController.qml index 24977f7..4a54ac3 100644 --- a/modules/user/modules/quickshell/shell/bar/mpris/MprisController.qml +++ b/modules/user/modules/quickshell/shell/bar/mpris/MprisController.qml @@ -1,11 +1,9 @@ pragma Singleton -pragma ComponentBehavior: Bound -import QtQml.Models import QtQuick import Quickshell -import Quickshell.Io import Quickshell.Services.Mpris +import Quickshell.Hyprland import "../.." Singleton { @@ -17,58 +15,51 @@ Singleton { property bool __reverse: false; property var activeTrack; - - Instantiator { - model: Mpris.players; - - Connections { - required property MprisPlayer modelData; - target: modelData; - - Component.onCompleted: { - if (root.trackedPlayer == null || modelData.isPlaying) { - root.trackedPlayer = modelData; + Component.onCompleted: { + for (const player of Mpris.players.values) { + if (player.playbackState == MprisPlaybackState.Playing) { + if (root.trackedPlayer == null) { + root.trackedPlayer = player; } } - Component.onDestruction: { - if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) { - for (const player of Mpris.players.values) { - if (player.playbackState.isPlaying) { - root.trackedPlayer = player; - break; - } - } - - if (trackedPlayer == null && Mpris.players.values.length != 0) { - trackedPlayer = Mpris.players.values[0]; - } - } - } - - function onPlaybackStateChanged() { - if (root.trackedPlayer !== modelData) root.trackedPlayer = modelData; - } + player.playbackStateChanged.connect(() => { + if (root.trackedPlayer !== player) root.trackedPlayer = player; + }); } } Connections { target: activePlayer - function onPostTrackChanged() { + function onTrackChanged() { root.updateTrack(); } + } - function onTrackArtUrlChanged() { - console.log("arturl:", activePlayer.trackArtUrl) - //root.updateTrack(); - if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) { - // cantata likes to send cover updates *BEFORE* updating the track info. - // as such, art url changes shouldn't be able to break the reverse animation - const r = root.__reverse; - root.updateTrack(); - root.__reverse = r; + // Change the tracked player when one changes playback state or is created in a playing state. + Connections { + target: Mpris.players; + function onObjectInsertedPost(player: MprisPlayer) { + if (player.playbackState === MprisPlaybackState.Playing) { + if (root.trackedPlayer !== player) root.trackedPlayer = player; + } + + player.playbackStateChanged.connect(() => { + if (root.trackedPlayer !== player) root.trackedPlayer = player; + }); + } + + function onObjectRemovedPre() { + console.log(`trackedPlayer: ${root.trackedPlayer}`) + if (root.trackedPlayer == null) { + for (const player of Mpris.players.values) { + if (player.playbackState === MprisPlaybackState.Playing) { + root.trackedPlayer = player; + break; + } + } } } } @@ -76,23 +67,27 @@ Singleton { onActivePlayerChanged: this.updateTrack(); function updateTrack() { - //console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`) + const metadata = this.activePlayer?.metadata ?? {}; + this.activeTrack = { - uniqueId: this.activePlayer?.uniqueId ?? 0, - artUrl: this.activePlayer?.trackArtUrl ?? "", - title: this.activePlayer?.trackTitle || "Unknown Title", - artist: this.activePlayer?.trackArtist || "Unknown Artist", - album: this.activePlayer?.trackAlbum || "Unknown Album", + artUrl: metadata["mpris:artUrl"] ?? "", + title: metadata["xesam:title"] ?? "", + artist: metadata["xesam:artist"] ?? "", }; this.trackChanged(__reverse); this.__reverse = false; } - property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying; - property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false; - function togglePlaying() { - if (this.canTogglePlaying) this.activePlayer.togglePlaying(); + property bool isPlaying: this.activePlayer && this.activePlayer.playbackState == MprisPlaybackState.Playing; + property bool canPlay: this.activePlayer?.canPlay ?? false; + function play() { + if (this.canPlay) this.activePlayer.playbackState = MprisPlaybackState.Playing; + } + + property bool canPause: this.activePlayer?.canPause ?? false; + function pause() { + if (this.canPause) this.activePlayer.playbackState = MprisPlaybackState.Paused; } property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false; @@ -130,7 +125,7 @@ Singleton { } function setActivePlayer(player: MprisPlayer) { - const targetPlayer = player ?? Mpris.players[0]; + const targetPlayer = player ?? MprisPlayer.players[0]; console.log(`setactive: ${targetPlayer} from ${activePlayer}`) if (targetPlayer && this.activePlayer) { @@ -143,17 +138,31 @@ Singleton { this.trackedPlayer = targetPlayer; } - IpcHandler { - target: "mpris" - - function pauseAll(): void { - for (const player of Mpris.players.values) { - if (player.canPause) player.pause(); + Shortcut { + name: "music-pauseall"; + onPressed: { + for (let i = 0; i < Mpris.players.length; i++) { + const player = Mpris.players[i]; + if (player.canPause) player.playbackState = MprisPlaybackState.Paused; } } + } - function playPause(): void { root.togglePlaying(); } - function previous(): void { root.previous(); } - function next(): void { root.next(); } + Shortcut { + name: "music-playpause"; + onPressed: { + if (root.isPlaying) root.pause(); + else root.play(); + } + } + + Shortcut { + name: "music-previous"; + onPressed: root.previous(); + } + + Shortcut { + name: "music-next"; + onPressed: root.next(); } } diff --git a/modules/user/modules/quickshell/shell/bar/mpris/Player.qml b/modules/user/modules/quickshell/shell/bar/mpris/Player.qml new file mode 100644 index 0000000..4f771ea --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/mpris/Player.qml @@ -0,0 +1,7 @@ +import Quickshell +import Quickshell.Services.Mpris + +Scope { + required property MprisPlayer player; + +} diff --git a/modules/user/modules/quickshell/shell/bar/mpris/PlayerPopup.qml b/modules/user/modules/quickshell/shell/bar/mpris/PlayerPopup.qml new file mode 100644 index 0000000..de82804 --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/mpris/PlayerPopup.qml @@ -0,0 +1,2 @@ +import QtQuick +import diff --git a/modules/user/modules/quickshell/shell/bar/mpris/Players.qml b/modules/user/modules/quickshell/shell/bar/mpris/Players.qml index d4e54e6..fc914e6 100644 --- a/modules/user/modules/quickshell/shell/bar/mpris/Players.qml +++ b/modules/user/modules/quickshell/shell/bar/mpris/Players.qml @@ -1,5 +1,3 @@ -pragma ComponentBehavior: Bound - import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -9,10 +7,9 @@ import Quickshell.Services.Mpris import ".." import "../.." -FullwidthMouseArea { +MouseArea { id: root hoverEnabled: true - fillWindowWidth: true required property var bar; implicitHeight: column.implicitHeight + 10 @@ -30,12 +27,12 @@ FullwidthMouseArea { property alias widgetOpen: persist.widgetOpen; acceptedButtons: Qt.RightButton - onPressed: widgetOpen = !widgetOpen + onClicked: widgetOpen = !widgetOpen onWheel: event => { event.accepted = true; if (MprisController.canChangeVolume) { - root.activePlayer.volume = Math.max(0, Math.min(1, root.activePlayer.volume + (event.angleDelta.y / 120) * 0.05)); + this.activePlayer.volume = Math.max(0, Math.min(1, this.activePlayer.volume + (event.angleDelta.y / 120) * 0.05)); } } @@ -45,7 +42,7 @@ FullwidthMouseArea { id: widget anchors.fill: parent - property real scaleMul: widgetOpen ? 100 : 1 + property real scaleMul: root.pressed || widgetOpen ? 100 : 1 Behavior on scaleMul { SmoothedAnimation { velocity: 600 } } scale: scaleCurve.interpolate(scaleMul / 100, 1, (width - 6) / width) @@ -59,10 +56,8 @@ FullwidthMouseArea { BackgroundArt { id: bkg anchors.fill: parent - overlay.color: "#30000000" function updateArt(reverse: bool) { - console.log("update art", MprisController.activeTrack.artUrl) this.setArt(MprisController.activeTrack.artUrl, reverse, false) } @@ -91,7 +86,6 @@ FullwidthMouseArea { implicitHeight: width scaleIcon: false baseMargin: 3 - hoverEnabled: false enabled: MprisController.canGoPrevious; onClicked: MprisController.previous(); } @@ -101,9 +95,11 @@ FullwidthMouseArea { image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`; implicitHeight: width scaleIcon: false - hoverEnabled: false - enabled: MprisController.canTogglePlaying; - onClicked: MprisController.togglePlaying(); + enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay; + onClicked: { + if (MprisController.isPlaying) MprisController.pause(); + else MprisController.play(); + } } ClickableIcon { @@ -112,162 +108,51 @@ FullwidthMouseArea { implicitHeight: width scaleIcon: false baseMargin: 3 - hoverEnabled: false enabled: MprisController.canGoNext; onClicked: MprisController.next(); } } - property Scope positionInfo: Scope { - id: positionInfo - - property var player: root.activePlayer; - property int position: Math.floor(player.position); - property int length: Math.floor(player.length); - - FrameAnimation { - id: posTracker; - running: positionInfo.player.isPlaying && (tooltip.visible || rightclickMenu.visible); - onTriggered: positionInfo.player.positionChanged(); - } - - function timeStr(time: int): string { - const seconds = time % 60; - const minutes = Math.floor(time / 60); - - return `${minutes}:${seconds.toString().padStart(2, '0')}` - } - } - - property TooltipItem tooltip: TooltipItem { + property var tooltip: TooltipItem { id: tooltip tooltip: bar.tooltip owner: root - show: root.containsMouse + show: root.containsMouse && (activePlayer?.metadata["mpris:trackid"] ?? false) - /*ColumnLayout { - ColumnLayout { - visible: MprisController.activePlayer != null - - Label { text: MprisController.activeTrack?.title ?? "" } - - Label { - text: { - const artist = MprisController.activeTrack?.artist ?? ""; - const album = MprisController.activeTrack?.album ?? ""; - - return artist + (album ? ` - ${album}` : ""); - } - } - - Label { text: MprisController.activePlayer?.identity ?? "" } - } - - Label { - visible: MprisController.activePlayer == null - text: "No media playing" - } - - Rectangle { implicitHeight: 10; color: "white"; Layout.fillWidth: true } - }*/ - - contentItem.anchors.margins: 0 + //implicitHeight: root.height - 10 + //implicitWidth: childrenRect.width Item { - id: ttcontent - width: parent.width - height: Math.max(parent.height, implicitHeight) - implicitWidth: cl.implicitWidth + 10 - implicitHeight: cl.implicitHeight + 10 + (MprisController.activePlayer ? 8 : 0) - - ColumnLayout { - id: cl - anchors { - left: parent.left - right: parent.right - top: parent.top - margins: 5 - } - - //visible: MprisController.activePlayer != null - - FontMetrics { id: fontmetrics } - - component FullheightLabel: Item { - implicitHeight: fontmetrics.height - implicitWidth: label.implicitWidth - - property alias text: label.text - - Label { - id: label - anchors.verticalCenter: parent.verticalCenter - } - } - - FullheightLabel { - visible: MprisController.activePlayer != null - text: MprisController.activeTrack?.title ?? "" - } - - FullheightLabel { - visible: MprisController.activePlayer != null - text: MprisController.activeTrack?.artist ?? "" - /*text: { - const artist = MprisController.activeTrack?.artist ?? ""; - const album = MprisController.activeTrack?.album ?? ""; - - return artist + (album ? ` - ${album}` : ""); - }*/ - } - - Label { - text: { - if (!MprisController.activePlayer) return "No media playing"; - - return MprisController.activePlayer?.identity + " - " - + positionInfo.timeStr(positionInfo.position) + " / " - + positionInfo.timeStr(positionInfo.length); - } - } - } - - Rectangle { - id: ttprect - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } - - color: "#30ceffff" - implicitHeight: 8 - visible: MprisController.activePlayer != null - - Rectangle { - anchors { - left: parent.left - top: parent.top - bottom: parent.bottom - } - - color: "#80ceffff" - width: parent.width * (root.activePlayer.position / root.activePlayer.length) - } - } - - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: ttcontent.width - height: ttcontent.height - bottomLeftRadius: 5 - bottomRightRadius: 5 - } - } + implicitWidth: 200 + implicitHeight: 100 } + + /*Loader { + active: tooltip.visible + + sourceComponent: ColumnLayout { + height: root.height - 10 + RowLayout { + Image { + Layout.fillHeight: true + source: mainPlayer.metadata["mpris:artUrl"] ?? "" + + cache: false + fillMode: Image.PreserveAspectCrop + sourceSize.width: height + sourceSize.height: height + } + Label { + text: mainPlayer.identity + } + } + + Slider { + Layout.fillWidth: true + } + } + }*/ } property var rightclickMenu: TooltipItem { @@ -297,7 +182,6 @@ FullwidthMouseArea { target: MprisController function onTrackChanged(reverse: bool) { - console.log(`track changed: rev: ${reverse}`) popupBkg.setArt(MprisController.activeTrack.artUrl, reverse, false); } } @@ -307,41 +191,71 @@ FullwidthMouseArea { } } - contentItem { - implicitWidth: 500 - implicitHeight: 650 - } - Loader { - active: rightclickMenu.visible width: 500 height: 650 + active: rightclickMenu.visible sourceComponent: ColumnLayout { + property var player: activePlayer; anchors.fill: parent; - property var player: root.activePlayer; + property int position: 0; + property int length: 0; + + FrameAnimation { + id: posTracker; + running: player.playbackState == MprisPlaybackState.Playing && widgetOpen; + onTriggered: player.positionChanged(); + } + + Connections { + target: player + + function onPositionChanged() { + const newPosition = Math.floor(player.position); + if (newPosition != position) position = newPosition; + } + + function onLengthChanged() { + const newLength = Math.floor(player.length); + if (newLength != length) length = newLength; + } + } Connections { target: MprisController function onTrackChanged(reverse: bool) { trackStack.updateTrack(reverse, false); + length = Math.floor(player.length); } } + Component.onCompleted: { + position = Math.floor(player.position); + length = Math.floor(player.length); + } + + function timeStr(time: int): string { + const seconds = time % 60; + const minutes = Math.floor(time / 60); + + return `${minutes}:${seconds.toString().padStart(2, '0')}` + } + Item { id: playerSelectorContainment Layout.fillWidth: true implicitHeight: playerSelector.implicitHeight + 20 implicitWidth: playerSelector.implicitWidth - RowLayout { //ScrollView { + ScrollView { id: playerSelector anchors.centerIn: parent width: Math.min(implicitWidth, playerSelectorContainment.width) - //RowLayout { + RowLayout { Repeater { model: Mpris.players @@ -367,7 +281,8 @@ FullwidthMouseArea { source: { const entry = DesktopEntries.byId(modelData.desktopEntry); console.log(`ent ${entry} id ${modelData.desktopEntry}`) - return Quickshell.iconPath(entry?.icon); + if (!entry) return "image://icon/"; + return `image://icon/${entry.icon}`; } //asynchronous: true @@ -377,18 +292,18 @@ FullwidthMouseArea { } } } - //} + } } } } Item { Layout.fillWidth: true - Layout.bottomMargin: 20 + Layout.bottomMargin: 10 Label { anchors.centerIn: parent - text: root.activePlayer.identity + text: activePlayer.identity } } @@ -396,13 +311,7 @@ FullwidthMouseArea { id: trackStack Layout.fillWidth: true implicitHeight: 400 - clip: animating || (lastFlicked?.contentX ?? 0) != 0 - // inverse of default tooltip margin - 1px for border - Layout.leftMargin: -4 - Layout.rightMargin: -4 - - property Flickable lastFlicked; property bool reverse: false; Component.onCompleted: updateTrack(false, true); @@ -424,16 +333,14 @@ FullwidthMouseArea { // but may take longer if the image is huge. readonly property bool svReady: img.status === Image.Ready; contentWidth: width + 1 - onDragStarted: trackStack.lastFlicked = this onDragEnded: { - //return; + return; console.log(`dragend ${contentX}`) if (Math.abs(contentX) > 75) { if (contentX < 0) MprisController.previous(); else if (contentX > 0) MprisController.next(); } } - ColumnLayout { id: trackContent width: flickable.width @@ -441,7 +348,7 @@ FullwidthMouseArea { Item { Layout.fillWidth: true - implicitHeight: 302//img.implicitHeight + implicitHeight: 300//img.implicitHeight implicitWidth: img.implicitWidth Image { @@ -455,50 +362,27 @@ FullwidthMouseArea { sourceSize.height: 300 sourceSize.width: 300 - - layer.enabled: true - layer.effect: OpacityMask { - cached: true - maskSource: Rectangle { - width: img.width - height: img.height - radius: 5 - } - } } } - component CenteredText: Item { + Item { Layout.fillWidth: true - - property alias text: label.text - property alias font: label.font + Layout.topMargin: 20 Label { - id: label - visible: text != "" anchors.centerIn: parent - elide: Text.ElideRight - width: Math.min(parent.width - 20, implicitWidth) + text: track.title } } - CenteredText { + Item { + Layout.fillWidth: true Layout.topMargin: 20 - text: track.title - font.pointSize: albumLabel.font.pointSize + 1 - } - CenteredText { - id: albumLabel - Layout.topMargin: 18 - text: track.album - opacity: 0.8 - } - - CenteredText { - Layout.topMargin: 25 - text: track.artist + Label { + anchors.centerIn: parent + text: track.artist + } } Item { Layout.fillHeight: true } @@ -578,8 +462,11 @@ FullwidthMouseArea { implicitWidth: 80 implicitHeight: width scaleIcon: false - enabled: MprisController.canTogglePlaying; - onClicked: MprisController.togglePlaying(); + enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay + onClicked: { + if (MprisController.isPlaying) MprisController.pause(); + else MprisController.play(); + } } ClickableIcon { @@ -604,70 +491,33 @@ FullwidthMouseArea { } RowLayout { - Layout.margins: 5 - Label { Layout.preferredWidth: lengthLabel.implicitWidth - text: positionInfo.timeStr(positionInfo.position) + text: timeStr(position) } MediaSlider { id: slider - property bool bindSlider: true; - - property real boundAnimStart: 0; - property real boundAnimFactor: 1; - property real lastPosition: 0; - property real lastLength: 0; - property real boundPosition: { - const ppos = player.position / player.length; - const bpos = boundAnimStart; - return (ppos * boundAnimFactor) + (bpos * (1.0 - boundAnimFactor)); - } - - NumberAnimation { - id: boundAnim - target: slider - property: "boundAnimFactor" - from: 0 - to: 1 - duration: 600 - easing.type: Easing.OutExpo - } - - Connections { - target: player - - function onPositionChanged() { - if (false && player.position == 0 && slider.lastPosition != 0 && !boundAnim.running) { - slider.boundAnimStart = slider.lastPosition / slider.lastLength; - boundAnim.start(); - } - - slider.lastPosition = player.position; - slider.lastLength = player.length; - } - } - Layout.fillWidth: true + property var bindSlider: true; enabled: player.canSeek from: 0 - to: 1 + to: player.length onPressedChanged: { - if (!pressed) player.position = value * player.length; + if (!pressed) player.position = value; bindSlider = !pressed; } Binding { when: slider.bindSlider - slider.value: slider.boundPosition + slider.value: player.position } } Label { id: lengthLabel - text: positionInfo.timeStr(positionInfo.length) + text: timeStr(length) } } } diff --git a/modules/user/modules/quickshell/shell/bar/power/BatteryIcon.qml b/modules/user/modules/quickshell/shell/bar/power/BatteryIcon.qml deleted file mode 100644 index 335e8d9..0000000 --- a/modules/user/modules/quickshell/shell/bar/power/BatteryIcon.qml +++ /dev/null @@ -1,45 +0,0 @@ -import QtQuick -import Quickshell.Services.UPower -import "root:." - -Item { - id: root - required property UPowerDevice device; - property real scale: 1; - - readonly property bool isCharging: root.device.state == UPowerDeviceState.Charging; - readonly property bool isPluggedIn: isCharging || root.device.state == UPowerDeviceState.PendingCharge; - readonly property bool isLow: root.device.percentage <= 0.20; - - width: 35 * root.scale - height: 35 * root.scale - - Rectangle { - anchors { - horizontalCenter: parent.horizontalCenter - bottom: parent.bottom - bottomMargin: 4 * root.scale - } - - width: 13 * root.scale - height: 23 * root.device.percentage * root.scale - radius: 2 * root.scale - - color: root.isPluggedIn ? "#359040" - : ShellGlobals.interpolateColors(Math.min(1.0, Math.min(0.5, root.device.percentage) * 2), "red", "white") - } - - Image { - id: img - anchors.fill: parent; - - source: root.isCharging ? "root:icons/battery-charging.svg" - : root.isPluggedIn ? "root:icons/battery-plus.svg" - : root.isLow ? "root:icons/battery-warning.svg" - : "root:icons/battery-empty.svg" - - sourceSize.width: parent.width - sourceSize.height: parent.height - visible: true - } -} diff --git a/modules/user/modules/quickshell/shell/bar/power/Power.qml b/modules/user/modules/quickshell/shell/bar/power/Power.qml deleted file mode 100644 index 93a8ac6..0000000 --- a/modules/user/modules/quickshell/shell/bar/power/Power.qml +++ /dev/null @@ -1,224 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Services.UPower -import Quickshell.Widgets -import ".." -import "root:." -import "root:components" -import "power" - -BarWidgetInner { - id: root - required property var bar; - - readonly property var chargeState: UPower.displayDevice.state - readonly property bool isCharging: chargeState == UPowerDeviceState.Charging; - readonly property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge; - readonly property real percentage: UPower.displayDevice.percentage - readonly property bool isLow: percentage <= 0.20 - - readonly property UPowerDevice batteryDevice: UPower.devices.values - .find(device => device.isLaptopBattery); - - function statusStr() { - return root.isPluggedIn ? `Plugged in, ${root.isCharging ? "Charging" : "Not Charging"}` - : "Discharging"; - } - - property bool showMenu: false; - - implicitHeight: width - color: isLow ? "#45ff6060" : ShellGlobals.colors.widget - - BarButton { - id: button - anchors.fill: parent - baseMargin: 5 - fillWindowWidth: true - acceptedButtons: Qt.RightButton - directScale: true - showPressed: root.showMenu - - onPressed: { - root.showMenu = !root.showMenu - } - - BatteryIcon { - device: UPower.displayDevice - } - } - - property TooltipItem tooltip: TooltipItem { - id: tooltip - tooltip: bar.tooltip - owner: root - show: button.containsMouse - - Loader { - active: tooltip.visible - - sourceComponent: Label { - text: { - const status = root.statusStr(); - - const percentage = Math.round(root.percentage * 100); - - let str = `${percentage}% - ${status}`; - return str; - } - } - } - } - - property TooltipItem rightclickMenu: TooltipItem { - id: rightclickMenu - tooltip: bar.tooltip - owner: root - - isMenu: true - show: root.showMenu - onClose: root.showMenu = false - - Loader { - active: rightclickMenu.visible - sourceComponent: ColumnLayout { - spacing: 10 - - FontMetrics { id: fm } - - component SmallLabel: Label { - font.pointSize: fm.font.pointSize * 0.8 - color: "#d0eeffff" - } - - RowLayout { - IconImage { - source: "root:icons/gauge.svg" - implicitSize: 32 - } - - ColumnLayout { - spacing: 0 - Label { text: "Power Profile" } - - OptionSlider { - values: ["Power Save", "Balanced", "Performance"] - index: PowerProfiles.profile - onIndexChanged: PowerProfiles.profile = this.index; - implicitWidth: 350 - } - } - } - - RowLayout { - IconImage { - Layout.alignment: Qt.AlignTop - source: "root:icons/battery-empty.svg" - implicitSize: 32 - } - - ColumnLayout { - spacing: 0 - - RowLayout { - Label { text: "Battery" } - Item { Layout.fillWidth: true } - Label { - text: `${root.statusStr()} -` - color: "#d0eeffff" - } - Label { text: `${Math.round(root.percentage * 100)}%` } - } - - ProgressBar { - Layout.topMargin: 5 - Layout.bottomMargin: 5 - Layout.fillWidth: true - value: UPower.displayDevice.percentage - } - - RowLayout { - visible: remainingTimeLbl.text !== "" - - SmallLabel { text: "Time remaining" } - Item { Layout.fillWidth: true } - - SmallLabel { - id: remainingTimeLbl - text: { - const device = UPower.displayDevice; - const time = device.timeToEmpty || device.timeToFull; - - if (time === 0) return ""; - const minutes = Math.floor(time / 60).toString().padStart(2, '0'); - return `${minutes} minutes` - } - } - } - - RowLayout { - visible: root.batteryDevice.healthSupported - SmallLabel { text: "Health" } - Item { Layout.fillWidth: true } - - SmallLabel { - text: `${Math.floor((root.batteryDevice?.healthPercentage ?? 0))}%` - } - } - } - } - - Repeater { - model: ScriptModel { - // external devices - values: UPower.devices.values.filter(device => !device.powerSupply) - } - - RowLayout { - required property UPowerDevice modelData; - - IconImage { - Layout.alignment: Qt.AlignTop - source: { - switch (modelData.type) { - case UPowerDeviceType.Headset: return "root:icons/headset.svg"; - } - return Quickshell.iconPath(modelData.iconName) - } - implicitSize: 32 - } - - ColumnLayout { - spacing: 0 - - RowLayout { - Label { text: modelData.model } - Item { Layout.fillWidth: true } - Label { text: `${Math.round(modelData.percentage * 100)}%` } - } - - ProgressBar { - Layout.topMargin: 5 - Layout.bottomMargin: 5 - Layout.fillWidth: true - value: modelData.percentage - } - - RowLayout { - visible: modelData.healthSupported - SmallLabel { text: "Health" } - Item { Layout.fillWidth: true } - - SmallLabel { - text: `${Math.floor(modelData.healthPercentage)}%` - } - } - } - } - } - } - } - } -} diff --git a/modules/user/modules/quickshell/shell/bar/systray/MenuChildrenRevealer.qml b/modules/user/modules/quickshell/shell/bar/systray/MenuChildrenRevealer.qml index ad7c0f8..c937a4b 100644 --- a/modules/user/modules/quickshell/shell/bar/systray/MenuChildrenRevealer.qml +++ b/modules/user/modules/quickshell/shell/bar/systray/MenuChildrenRevealer.qml @@ -6,7 +6,7 @@ Item { property bool expanded: false; readonly property bool open: progress != 0; - readonly property bool animating: internalProgress != (expanded ? 101 : -1); + readonly property bool animating: internalProgress != -1 && internalProgress != 101; implicitHeight: 16 implicitWidth: 16 diff --git a/modules/user/modules/quickshell/shell/bar/systray/MenuItem.qml b/modules/user/modules/quickshell/shell/bar/systray/MenuItem.qml index e7592e2..9da1e95 100644 --- a/modules/user/modules/quickshell/shell/bar/systray/MenuItem.qml +++ b/modules/user/modules/quickshell/shell/bar/systray/MenuItem.qml @@ -1,15 +1,14 @@ import QtQuick import QtQuick.Layouts import Quickshell -import Quickshell.Widgets import Quickshell.DBusMenu import "../.." MouseArea { id: root - required property QsMenuEntry entry; + required property var entry; property alias expanded: childrenRevealer.expanded; - property bool animating: childrenRevealer.animating || (childMenuLoader?.item?.animating ?? false); + property bool animating: childrenRevealer.animating || childrenList.animating; // appears it won't actually create the handler when only used from MenuItemList. onExpandedChanged: {} onAnimatingChanged: {} @@ -23,7 +22,7 @@ MouseArea { onClicked: { if (entry.hasChildren) childrenRevealer.expanded = !childrenRevealer.expanded else { - entry.triggered(); + entry.click(); if (entry.toggleType == ToggleButtonType.None) close(); } } @@ -35,21 +34,19 @@ MouseArea { spacing: 0 RowLayout { - id: innerRow - Item { implicitWidth: 22 implicitHeight: 22 MenuCheckBox { anchors.centerIn: parent - visible: entry.buttonType == QsMenuButtonType.CheckBox + visible: entry.toggleType == ToggleButtonType.CheckBox checkState: entry.checkState } MenuRadioButton { anchors.centerIn: parent - visible: entry.buttonType == QsMenuButtonType.RadioButton + visible: entry.toggleType == ToggleButtonType.RadioButton checkState: entry.checkState } @@ -62,7 +59,7 @@ MouseArea { } Text { - text: entry.text + text: entry.cleanLabel color: entry.enabled ? "white" : "#bbbbbb" } @@ -71,32 +68,25 @@ MouseArea { implicitWidth: 22 implicitHeight: 22 - IconImage { + Image { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + visible: entry.icon != "" source: entry.icon - visible: source != "" - implicitSize: parent.height + sourceSize.height: parent.height + sourceSize.width: parent.height } } } - Loader { - id: childMenuLoader + Item { Layout.fillWidth: true - Layout.preferredHeight: active ? item.implicitHeight * childrenRevealer.progress : 0 - - readonly property real widthDifference: { - Math.max(0, (item?.implicitWidth ?? 0) - innerRow.implicitWidth); - } - Layout.preferredWidth: active ? innerRow.implicitWidth + (widthDifference * childrenRevealer.progress) : 0 - - active: root.expanded || root.animating + implicitHeight: childrenList.implicitHeight * childrenRevealer.progress clip: true - sourceComponent: MenuView { + MenuItemList { id: childrenList - menu: entry + items: entry.children onClose: root.close() anchors { diff --git a/modules/user/modules/quickshell/shell/bar/systray/MenuView.qml b/modules/user/modules/quickshell/shell/bar/systray/MenuItemList.qml similarity index 93% rename from modules/user/modules/quickshell/shell/bar/systray/MenuView.qml rename to modules/user/modules/quickshell/shell/bar/systray/MenuItemList.qml index da02e5c..8a809e1 100644 --- a/modules/user/modules/quickshell/shell/bar/systray/MenuView.qml +++ b/modules/user/modules/quickshell/shell/bar/systray/MenuItemList.qml @@ -6,19 +6,17 @@ import "../.." ColumnLayout { id: root - property alias menu: menuView.menu; + required property var items; property Item animatingItem: null; property bool animating: animatingItem != null; signal close(); signal submenuExpanded(item: var); - QsMenuOpener { id: menuView } - spacing: 0 Repeater { - model: menuView.children; + model: items Loader { required property var modelData; diff --git a/modules/user/modules/quickshell/shell/bar/systray/SysTray.qml b/modules/user/modules/quickshell/shell/bar/systray/SysTray.qml index b393268..b243080 100644 --- a/modules/user/modules/quickshell/shell/bar/systray/SysTray.qml +++ b/modules/user/modules/quickshell/shell/bar/systray/SysTray.qml @@ -1,5 +1,3 @@ -pragma ComponentBehavior: Bound - import QtQuick import QtQuick.Layouts import QtQuick.Effects @@ -7,102 +5,102 @@ import Quickshell import Quickshell.Services.SystemTray import ".." -BarWidgetInner { +OverlayWidget { id: root + expandedWidth: 600 + expandedHeight: 800 required property var bar; - implicitHeight: column.implicitHeight + 10 - ColumnLayout { - id: column - implicitHeight: childrenRect.height - spacing: 5 + BarWidgetInner { + implicitHeight: column.implicitHeight + 10 - anchors { - fill: parent - margins: 5 - } + ColumnLayout { + id: column + implicitHeight: childrenRect.height + spacing: 5 - Repeater { - model: SystemTray.items; + anchors { + fill: parent + margins: 5 + } - Item { - id: item - required property SystemTrayItem modelData; + Repeater { + model: SystemTray.items; - property bool targetMenuOpen: false; + Item { + required property var modelData; + readonly property alias menu: menuWatcher.menu; - Layout.fillWidth: true - implicitHeight: width - - ClickableIcon { - id: mouseArea - anchors { - top: parent.top - bottom: parent.bottom - horizontalCenter: parent.horizontalCenter + SystemTrayMenuWatcher { + id: menuWatcher; + trayItem: modelData; } - width: height - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + property bool targetMenuOpen: false; + onTargetMenuOpenChanged: menu.showChildren = targetMenuOpen - image: item.modelData.icon - showPressed: item.targetMenuOpen || (pressedButtons & ~Qt.RightButton) - fillWindowWidth: true - extraVerticalMargin: column.spacing / 2 + Layout.fillWidth: true + implicitHeight: width - onClicked: event => { - event.accepted = true; - - if (event.button == Qt.LeftButton) { - item.modelData.activate(); - } else if (event.button == Qt.MiddleButton) { - item.modelData.secondaryActivate(); + ClickableIcon { + id: mouseArea + anchors { + top: parent.top + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter } - } + width: height - onPressed: event => { - if (event.button == Qt.RightButton && item.modelData.hasMenu) { - item.targetMenuOpen = !item.targetMenuOpen; + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + + image: modelData.icon + showPressed: targetMenuOpen + + onClicked: event => { + event.accepted = true; + + if (event.button == Qt.LeftButton) { + modelData.activate(); + } else if (event.button == Qt.MiddleButton) { + modelData.secondaryActivate(); + } else if (event.button == Qt.RightButton && menu != null) { + targetMenuOpen = !targetMenuOpen; + } } - } - onWheel: event => { - event.accepted = true; - const points = event.angleDelta.y / 120 - item.modelData.scroll(points, false); - } - - property var tooltip: TooltipItem { - tooltip: root.bar.tooltip - owner: mouseArea - - show: mouseArea.containsMouse - - Text { - id: tooltipText - text: item.modelData.tooltipTitle != "" ? item.modelData.tooltipTitle : item.modelData.id - color: "white" + onWheel: event => { + event.accepted = true; + const points = event.angleDelta.y / 120 + modelData.scroll(points, false); } - } - property var rightclickMenu: TooltipItem { - id: rightclickMenu - tooltip: root.bar.tooltip - owner: mouseArea + property var tooltip: TooltipItem { + tooltip: bar.tooltip + owner: mouseArea - isMenu: true - show: item.targetMenuOpen - animateSize: !(menuContentLoader?.item?.animating ?? false) + show: mouseArea.containsMouse - onClose: item.targetMenuOpen = false; + Text { + id: tooltipText + text: modelData.tooltipTitle != "" ? modelData.tooltipTitle : modelData.id + color: "white" + } + } - Loader { - id: menuContentLoader - active: item.targetMenuOpen || rightclickMenu.visible || mouseArea.containsMouse + property var rightclickMenu: TooltipItem { + tooltip: bar.tooltip + owner: mouseArea - sourceComponent: MenuView { - menu: item.modelData.menu - onClose: item.targetMenuOpen = false; + isMenu: true + show: targetMenuOpen && menu.showChildren + animateSize: !rightclickItems.animating + + onClose: targetMenuOpen = false; + + MenuItemList { + id: rightclickItems + items: menu == null ? [] : menu.children + onClose: targetMenuOpen = false; } } } diff --git a/modules/user/modules/quickshell/shell/bar/Workspaces.qml b/modules/user/modules/quickshell/shell/bar/workspaces/Widget.qml similarity index 76% rename from modules/user/modules/quickshell/shell/bar/Workspaces.qml rename to modules/user/modules/quickshell/shell/bar/workspaces/Widget.qml index 5c2a1f6..c52d21b 100644 --- a/modules/user/modules/quickshell/shell/bar/Workspaces.qml +++ b/modules/user/modules/quickshell/shell/bar/workspaces/Widget.qml @@ -1,21 +1,17 @@ -pragma ComponentBehavior: Bound; - import QtQuick import QtQuick.Layouts import Quickshell.Hyprland import ".." import "root:." -FullwidthMouseArea { +MouseArea { id: root required property var bar; required property int wsBaseIndex; property int wsCount: 10; property bool hideWhenEmpty: false; - implicitHeight: column.implicitHeight + 10; - fillWindowWidth: true acceptedButtons: Qt.NoButton onWheel: event => { @@ -33,6 +29,9 @@ FullwidthMouseArea { property int existsCount: 0; visible: !hideWhenEmpty || existsCount > 0; + property real animPos: 0; + Behavior on animPos { SmoothedAnimation { velocity: 100 } } + // destructor takes care of nulling signal workspaceAdded(workspace: HyprlandWorkspace); @@ -46,22 +45,20 @@ FullwidthMouseArea { } Repeater { - model: root.wsCount + model: 10 - FullwidthMouseArea { + MouseArea { id: wsItem onPressed: Hyprland.dispatch(`workspace ${wsIndex}`); Layout.fillWidth: true implicitHeight: 15 - fillWindowWidth: true - required property int index; - property int wsIndex: root.wsBaseIndex + index; + property int wsIndex: wsBaseIndex + index; property HyprlandWorkspace workspace: null; property bool exists: workspace != null; - property bool active: (root.monitor?.activeWorkspace ?? false) && root.monitor.activeWorkspace == workspace; + property bool active: (monitor?.activeWorkspace ?? false) && monitor.activeWorkspace == workspace; onActiveChanged: { if (active) root.currentIndex = wsIndex; @@ -81,21 +78,21 @@ FullwidthMouseArea { } } - property real animActive: active ? 1 : 0 - Behavior on animActive { NumberAnimation { duration: 150 } } + property real animActive: active ? 100 : 0 + Behavior on animActive { NumberAnimation { duration: 100 } } - property real animExists: exists ? 1 : 0 + property real animExists: exists ? 100 : 0 Behavior on animExists { NumberAnimation { duration: 100 } } Rectangle { anchors.centerIn: parent height: 10 width: parent.width - scale: 1 + wsItem.animActive * 0.3 + scale: 1 + animActive * 0.003 radius: height / 2 border.color: ShellGlobals.colors.widgetOutline border.width: 1 - color: ShellGlobals.interpolateColors(animExists, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive); + color: ShellGlobals.interpolateColors(animExists * 0.01, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive); } } } diff --git a/modules/user/modules/quickshell/shell/components/FlickMonitor.qml b/modules/user/modules/quickshell/shell/components/FlickMonitor.qml deleted file mode 100644 index 56da066..0000000 --- a/modules/user/modules/quickshell/shell/components/FlickMonitor.qml +++ /dev/null @@ -1,90 +0,0 @@ -import QtQuick -import Quickshell - -Scope { - id: root - - required property MouseArea target; - - property real velocityX: 0; - property real velocityY: 0; - - signal flickStarted(); - signal flickCompleted(); - - property real dragStartX: 0; - property real dragStartY: 0; - property real dragDeltaX: 0; - property real dragDeltaY: 0; - property real dragEndX: 0; - property real dragEndY: 0; - - property var sampleIdx: -1 - property var tSamples: [] - property var xSamples: [] - property var ySamples: [] - - ElapsedTimer { id: velocityTimer } - - function resetSamples() { - velocityTimer.restart(); - sampleIdx = -1; - tSamples = []; - xSamples = []; - ySamples = []; - } - - function sample() { - const deltaT = velocityTimer.elapsed(); - - sampleIdx++; - if (sampleIdx > 5) { - sampleIdx = 0; - } - - tSamples[sampleIdx] = deltaT; - xSamples[sampleIdx] = dragDeltaX; - ySamples[sampleIdx] = dragDeltaY; - } - - function updateVelocity() { - let firstIdx = sampleIdx + 1; - if (firstIdx > tSamples.length - 1) firstIdx = 0; - - const deltaT = tSamples[sampleIdx] - tSamples[firstIdx]; - const deltaX = xSamples[sampleIdx] - xSamples[firstIdx]; - const deltaY = ySamples[sampleIdx] - ySamples[firstIdx]; - - root.velocityX = deltaX / deltaT; - root.velocityY = deltaY / deltaT; - } - - Connections { - target: root.target; - - function onPressed(event: MouseEvent) { - root.resetSamples(); - root.dragDeltaX = 0; - root.dragDeltaY = 0; - root.dragStartX = event.x; - root.dragStartY = event.y; - root.flickStarted(); - } - - function onReleased(event: MouseEvent) { - root.dragDeltaX = event.x - root.dragStartX; - root.dragDeltaY = event.y - root.dragStartY; - root.dragEndX = event.x; - root.dragEndY = event.y; - root.sample(); - root.updateVelocity(); - root.flickCompleted(); - } - - function onPositionChanged(event: MouseEvent) { - root.dragDeltaX = event.x - root.dragStartX; - root.dragDeltaY = event.y - root.dragStartY; - root.sample(); - } - } -} diff --git a/modules/user/modules/quickshell/shell/components/OptionSlider.qml b/modules/user/modules/quickshell/shell/components/OptionSlider.qml deleted file mode 100644 index 99f8aad..0000000 --- a/modules/user/modules/quickshell/shell/components/OptionSlider.qml +++ /dev/null @@ -1,99 +0,0 @@ -pragma ComponentBehavior: Bound; - -import QtQuick - -Item { - id: root - - property list values; - property int index: 0; - - implicitWidth: 300 - implicitHeight: 40 - - MouseArea { - id: mouseArea - anchors.fill: parent - - property real halfHandle: handle.width / 2; - property real activeWidth: groove.width - handle.width; - property real valueOffset: mouseArea.halfHandle + (root.index / (root.values.length - 1)) * mouseArea.activeWidth; - - Repeater { - model: root.values - - Item { - id: delegate - required property int index; - required property string modelData; - - anchors.top: groove.bottom - anchors.topMargin: 2 - x: mouseArea.halfHandle + (delegate.index / (root.values.length - 1)) * mouseArea.activeWidth - - Rectangle { - id: mark - color: "#60eeffff" - width: 1 - height: groove.height - } - - Text { - anchors.top: mark.bottom - - x: delegate.index === 0 ? -4 - : delegate.index === root.values.length - 1 ? -this.width + 4 - : -(this.width / 2); - - text: delegate.modelData - color: "#a0eeffff" - } - } - } - - Rectangle { - id: grooveFill - - anchors { - left: groove.left - top: groove.top - bottom: groove.bottom - } - - radius: 5 - color: "#80ceffff" - width: mouseArea.valueOffset - } - - Rectangle { - id: groove - - anchors { - left: parent.left - right: parent.right - } - - y: 5 - implicitHeight: 7 - color: "transparent" - border.color: "#20eeffff" - border.width: 1 - radius: 5 - } - - Rectangle { - id: handle - anchors.verticalCenter: groove.verticalCenter - height: 15 - width: height - radius: height * 0.5 - x: mouseArea.valueOffset - width * 0.5 - } - } - - Binding { - when: mouseArea.pressed - root.index: Math.max(0, Math.min(root.values.length - 1, Math.round((mouseArea.mouseX / root.width) * (root.values.length - 1)))); - restoreMode: Binding.RestoreBinding - } -} diff --git a/modules/user/modules/quickshell/shell/components/ProgressBar.qml b/modules/user/modules/quickshell/shell/components/ProgressBar.qml deleted file mode 100644 index 087d5e7..0000000 --- a/modules/user/modules/quickshell/shell/components/ProgressBar.qml +++ /dev/null @@ -1,42 +0,0 @@ -import QtQuick - -Item { - id: root - - property real from: 0.0 - property real to: 1.0 - property real value: 0.0 - - implicitHeight: 7 - implicitWidth: 200 - - Rectangle { - id: grooveFill - - anchors { - left: groove.left - top: groove.top - bottom: groove.bottom - } - - radius: 5 - color: "#80ceffff" - width: root.width * ((root.value - root.from) / (root.to - root.from)) - } - - Rectangle { - id: groove - - anchors { - left: parent.left - right: parent.right - verticalCenter: parent.verticalCenter - } - - height: 7 - color: "transparent" - border.color: "#20eeffff" - border.width: 1 - radius: 5 - } -} diff --git a/modules/user/modules/quickshell/shell/components/ZHVStack.qml b/modules/user/modules/quickshell/shell/components/ZHVStack.qml deleted file mode 100644 index 8321014..0000000 --- a/modules/user/modules/quickshell/shell/components/ZHVStack.qml +++ /dev/null @@ -1,36 +0,0 @@ -import QtQuick - -Item { - id: root - onChildrenChanged: recalc(); - - Instantiator { - model: root.children - - Connections { - required property Item modelData; - target: modelData; - - function onImplicitHeightChanged() { - root.recalc(); - } - - function onImplicitWidthChanged() { - root.recalc(); - } - } - } - - function recalc() { - let y = 0 - let w = 0 - for (const child of this.children) { - child.y = y; - y += child.implicitHeight - if (child.implicitWidth > w) w = child.implicitWidth; - } - - this.implicitHeight = y; - this.implicitWidth = w; - } -} diff --git a/modules/user/modules/quickshell/shell/greeter.qml b/modules/user/modules/quickshell/shell/greeter.qml deleted file mode 100644 index 5dc98b4..0000000 --- a/modules/user/modules/quickshell/shell/greeter.qml +++ /dev/null @@ -1,38 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Wayland -import Quickshell.Services.Greetd -import ".." -import "lock" - -ShellRoot { - GreeterContext { - id: context - - onLaunch: { - lock.locked = false; - Greetd.launch(["hyprland"]); - } - } - - WlSessionLock { - id: lock - locked: true - - WlSessionLockSurface { - id: lockSurface - - BackgroundImage { - id: backgroundImage - anchors.fill: parent - screen: lockSurface.screen - asynchronous: true - } - - LockContent { - anchors.fill: parent - state: context.state - } - } - } -} diff --git a/modules/user/modules/quickshell/shell/icons/battery-charging.svg b/modules/user/modules/quickshell/shell/icons/battery-charging.svg deleted file mode 100644 index 324212f..0000000 --- a/modules/user/modules/quickshell/shell/icons/battery-charging.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/modules/user/modules/quickshell/shell/icons/battery-empty.svg b/modules/user/modules/quickshell/shell/icons/battery-empty.svg deleted file mode 100644 index 92c1a0f..0000000 --- a/modules/user/modules/quickshell/shell/icons/battery-empty.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/modules/user/modules/quickshell/shell/icons/battery-plus.svg b/modules/user/modules/quickshell/shell/icons/battery-plus.svg deleted file mode 100644 index 1323cac..0000000 --- a/modules/user/modules/quickshell/shell/icons/battery-plus.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/modules/user/modules/quickshell/shell/icons/battery-warning.svg b/modules/user/modules/quickshell/shell/icons/battery-warning.svg deleted file mode 100644 index 54e3322..0000000 --- a/modules/user/modules/quickshell/shell/icons/battery-warning.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/modules/user/modules/quickshell/shell/icons/bell-fill.svg b/modules/user/modules/quickshell/shell/icons/bell-fill.svg deleted file mode 100644 index 2f7f0b6..0000000 --- a/modules/user/modules/quickshell/shell/icons/bell-fill.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/modules/user/modules/quickshell/shell/icons/bell-slash.svg b/modules/user/modules/quickshell/shell/icons/bell-slash.svg deleted file mode 100644 index daf40a0..0000000 --- a/modules/user/modules/quickshell/shell/icons/bell-slash.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/modules/user/modules/quickshell/shell/icons/bell.svg b/modules/user/modules/quickshell/shell/icons/bell.svg deleted file mode 100644 index 44abf73..0000000 --- a/modules/user/modules/quickshell/shell/icons/bell.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/modules/user/modules/quickshell/shell/icons/fast-forward.svg b/modules/user/modules/quickshell/shell/icons/fast-forward.svg index bc4e36e..1745658 100644 --- a/modules/user/modules/quickshell/shell/icons/fast-forward.svg +++ b/modules/user/modules/quickshell/shell/icons/fast-forward.svg @@ -1 +1 @@ - + diff --git a/modules/user/modules/quickshell/shell/icons/fingerprint.svg b/modules/user/modules/quickshell/shell/icons/fingerprint.svg deleted file mode 100644 index a8d0149..0000000 --- a/modules/user/modules/quickshell/shell/icons/fingerprint.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/modules/user/modules/quickshell/shell/icons/gauge.svg b/modules/user/modules/quickshell/shell/icons/gauge.svg deleted file mode 100644 index d784f93..0000000 --- a/modules/user/modules/quickshell/shell/icons/gauge.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/modules/user/modules/quickshell/shell/icons/headset.svg b/modules/user/modules/quickshell/shell/icons/headset.svg deleted file mode 100644 index f86e298..0000000 --- a/modules/user/modules/quickshell/shell/icons/headset.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/modules/user/modules/quickshell/shell/icons/magnifying-glass.svg b/modules/user/modules/quickshell/shell/icons/magnifying-glass.svg deleted file mode 100644 index 9effa71..0000000 --- a/modules/user/modules/quickshell/shell/icons/magnifying-glass.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/modules/user/modules/quickshell/shell/icons/monitor.svg b/modules/user/modules/quickshell/shell/icons/monitor.svg index 2763ca3..2f51e19 100644 --- a/modules/user/modules/quickshell/shell/icons/monitor.svg +++ b/modules/user/modules/quickshell/shell/icons/monitor.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/modules/user/modules/quickshell/shell/icons/pause.svg b/modules/user/modules/quickshell/shell/icons/pause.svg index f48c41a..d83a876 100644 --- a/modules/user/modules/quickshell/shell/icons/pause.svg +++ b/modules/user/modules/quickshell/shell/icons/pause.svg @@ -1 +1 @@ - + diff --git a/modules/user/modules/quickshell/shell/icons/play.svg b/modules/user/modules/quickshell/shell/icons/play.svg index 8dfd279..3927ce5 100644 --- a/modules/user/modules/quickshell/shell/icons/play.svg +++ b/modules/user/modules/quickshell/shell/icons/play.svg @@ -2,7 +2,7 @@ + diff --git a/modules/user/modules/quickshell/shell/icons/repeat-none.svg b/modules/user/modules/quickshell/shell/icons/repeat-none.svg index cda7ae2..17b0b9a 100644 --- a/modules/user/modules/quickshell/shell/icons/repeat-none.svg +++ b/modules/user/modules/quickshell/shell/icons/repeat-none.svg @@ -2,7 +2,7 @@ + diff --git a/modules/user/modules/quickshell/shell/icons/rewind.svg b/modules/user/modules/quickshell/shell/icons/rewind.svg index f26adec..7810ae5 100644 --- a/modules/user/modules/quickshell/shell/icons/rewind.svg +++ b/modules/user/modules/quickshell/shell/icons/rewind.svg @@ -1 +1 @@ - + diff --git a/modules/user/modules/quickshell/shell/icons/shuffle-off.svg b/modules/user/modules/quickshell/shell/icons/shuffle-off.svg index c1df764..f0b25da 100644 --- a/modules/user/modules/quickshell/shell/icons/shuffle-off.svg +++ b/modules/user/modules/quickshell/shell/icons/shuffle-off.svg @@ -2,7 +2,7 @@ + diff --git a/modules/user/modules/quickshell/shell/icons/skip-back.svg b/modules/user/modules/quickshell/shell/icons/skip-back.svg new file mode 100644 index 0000000..ae5bb3a --- /dev/null +++ b/modules/user/modules/quickshell/shell/icons/skip-back.svg @@ -0,0 +1 @@ + diff --git a/modules/user/modules/quickshell/shell/icons/skip-forward.svg b/modules/user/modules/quickshell/shell/icons/skip-forward.svg new file mode 100644 index 0000000..70d3c49 --- /dev/null +++ b/modules/user/modules/quickshell/shell/icons/skip-forward.svg @@ -0,0 +1 @@ + diff --git a/modules/user/modules/quickshell/shell/launcher/Controller.qml b/modules/user/modules/quickshell/shell/launcher/Controller.qml deleted file mode 100644 index e803bbc..0000000 --- a/modules/user/modules/quickshell/shell/launcher/Controller.qml +++ /dev/null @@ -1,281 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import Quickshell.Widgets -import Quickshell.Services.SystemTray -import ".." - -Singleton { - PersistentProperties { - id: persist - property bool launcherOpen: false; - } - - IpcHandler { - target: "launcher" - - function open(): void { - persist.launcherOpen = true; - } - - function close(): void { - persist.launcherOpen = false; - } - - function toggle(): void { - persist.launcherOpen = !persist.launcherOpen - } - } - - LazyLoader { - id: loader - activeAsync: persist.launcherOpen - - PanelWindow { - width: 450 - height: 7 + searchContainer.implicitHeight + list.topMargin * 2 + list.delegateHeight * 10 - color: "transparent" - WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive - WlrLayershell.namespace: "shell:launcher" - - Rectangle { - //anchors.fill: parent - height: 7 + searchContainer.implicitHeight + list.topMargin + list.bottomMargin + Math.min(list.contentHeight, list.delegateHeight * 10) - Behavior on height { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } } - width: 450 - color: ShellGlobals.colors.bar - radius: 5 - border.color: ShellGlobals.colors.barOutline - border.width: 1 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 7 - anchors.bottomMargin: 0 - spacing: 0 - - Rectangle { - id: searchContainer - Layout.fillWidth: true - implicitHeight: searchbox.implicitHeight + 10 - color: "#30c0ffff" - radius: 3 - border.color: "#50ffffff" - - RowLayout { - id: searchbox - anchors.fill: parent - anchors.margins: 5 - - IconImage { - implicitSize: parent.height - source: "root:icons/magnifying-glass.svg" - } - - TextInput { - id: search - Layout.fillWidth: true - color: "white" - - focus: true - Keys.forwardTo: [list] - Keys.onEscapePressed: persist.launcherOpen = false - - Keys.onPressed: event => { - if (event.modifiers & Qt.ControlModifier) { - if (event.key == Qt.Key_J) { - list.currentIndex = list.currentIndex == list.count - 1 ? 0 : list.currentIndex + 1; - event.accepted = true; - } else if (event.key == Qt.Key_K) { - list.currentIndex = list.currentIndex == 0 ? list.count - 1 : list.currentIndex - 1; - event.accepted = true; - } - } - } - - onAccepted: { - if (list.currentItem) { - list.currentItem.clicked(null); - } - } - - onTextChanged: { - list.currentIndex = 0; - } - } - } - } - - ListView { - id: list - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - cacheBuffer: 0 // works around QTBUG-131106 - //reuseItems: true - model: ScriptModel { - values: DesktopEntries.applications.values - .map(object => { - const stxt = search.text.toLowerCase(); - const ntxt = object.name.toLowerCase(); - let si = 0; - let ni = 0; - - let matches = []; - let startMatch = -1; - - for (let si = 0; si != stxt.length; ++si) { - const sc = stxt[si]; - - while (true) { - // Drop any entries with letters that don't exist in order - if (ni == ntxt.length) return null; - - const nc = ntxt[ni++]; - - if (nc == sc) { - if (startMatch == -1) startMatch = ni; - break; - } else { - if (startMatch != -1) { - matches.push({ - index: startMatch, - length: ni - startMatch, - }); - - startMatch = -1; - } - } - } - } - - if (startMatch != -1) { - matches.push({ - index: startMatch, - length: ni - startMatch + 1, - }); - } - - return { - object: object, - matches: matches, - }; - }) - .filter(entry => entry !== null) - .sort((a, b) => { - let ai = 0; - let bi = 0; - let s = 0; - - while (ai != a.matches.length && bi != b.matches.length) { - const am = a.matches[ai]; - const bm = b.matches[bi]; - - s = bm.length - am.length; - if (s != 0) return s; - - s = am.index - bm.index; - if (s != 0) return s; - - ++ai; - ++bi; - } - - s = a.matches.length - b.matches.length; - if (s != 0) return s; - - s = a.object.name.length - b.object.name.length; - if (s != 0) return s; - - return a.object.name.localeCompare(b.object.name); - }) - .map(entry => entry.object); - - onValuesChanged: list.currentIndex = 0 - } - - topMargin: 7 - bottomMargin: list.count == 0 ? 0 : 7 - - add: Transition { - NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 100 } - } - - displaced: Transition { - NumberAnimation { property: "y"; duration: 200; easing.type: Easing.OutCubic } - NumberAnimation { property: "opacity"; to: 1; duration: 100 } - } - - move: Transition { - NumberAnimation { property: "y"; duration: 200; easing.type: Easing.OutCubic } - NumberAnimation { property: "opacity"; to: 1; duration: 100 } - } - - remove: Transition { - NumberAnimation { property: "y"; duration: 200; easing.type: Easing.OutCubic } - NumberAnimation { property: "opacity"; to: 0; duration: 100 } - } - - highlight: Rectangle { - radius: 5 - color: "#20e0ffff" - border.color: "#30ffffff" - border.width: 1 - } - keyNavigationEnabled: true - keyNavigationWraps: true - highlightMoveVelocity: -1 - highlightMoveDuration: 100 - preferredHighlightBegin: list.topMargin - preferredHighlightEnd: list.height - list.bottomMargin - highlightRangeMode: ListView.ApplyRange - snapMode: ListView.SnapToItem - - readonly property real delegateHeight: 44 - - delegate: MouseArea { - required property DesktopEntry modelData; - - implicitHeight: list.delegateHeight - implicitWidth: ListView.view.width - - onClicked: { - modelData.execute(); - persist.launcherOpen = false; - } - - RowLayout { - id: delegateLayout - anchors { - verticalCenter: parent.verticalCenter - left: parent.left - leftMargin: 5 - } - - IconImage { - Layout.alignment: Qt.AlignVCenter - asynchronous: true - implicitSize: 30 - source: Quickshell.iconPath(modelData.icon) - } - Text { - text: modelData.name - color: "#f0f0f0" - Layout.alignment: Qt.AlignVCenter - } - } - } - } - } - } - } - } - - function init() {} -} diff --git a/modules/user/modules/quickshell/shell/lock/Controller.qml b/modules/user/modules/quickshell/shell/lock/Controller.qml index 36384f1..715772b 100644 --- a/modules/user/modules/quickshell/shell/lock/Controller.qml +++ b/modules/user/modules/quickshell/shell/lock/Controller.qml @@ -3,10 +3,8 @@ pragma Singleton import QtQuick import QtQuick.Controls import Quickshell -import Quickshell.Io import Quickshell.Wayland import Quickshell.Hyprland -import Quickshell.Services.Pam import ".." import "../.." @@ -63,15 +61,18 @@ Singleton { root.oldWorkspaces = ({}); } - IpcHandler { - target: "lockscreen" - function lock(): void { root.locked = true; } + Shortcut { + name: "lock" + onPressed: { + if (root.locked) root.locked = false; + else root.locked = true; + } } LazyLoader { id: lockContextLoader - SessionLockContext { + LockContext { onUnlocked: root.locked = false; } } @@ -81,7 +82,7 @@ Singleton { onSecureChanged: { if (secure) { - root.workspaceLockAnimation(); + Qt.callLater(() => root.workspaceLockAnimation()); } } @@ -107,7 +108,7 @@ Singleton { LockContent { id: lockContent - state: lockContextLoader.item.state; + context: lockContextLoader.item; visible: false width: lockSurface.width @@ -127,6 +128,7 @@ Singleton { onVisibleChanged: { if (visible) { lockContent.y = -lockSurface.height + console.log(`y ${lockContent.y}`) lockContent.visible = true; lockAnim.running = true; } diff --git a/modules/user/modules/quickshell/shell/lock/GreeterContext.qml b/modules/user/modules/quickshell/shell/lock/GreeterContext.qml deleted file mode 100644 index c9f0da8..0000000 --- a/modules/user/modules/quickshell/shell/lock/GreeterContext.qml +++ /dev/null @@ -1,37 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Services.Greetd - -Scope { - id: root - signal launch(); - - property LockState state: LockState { - onTryPasswordUnlock: { - this.isUnlocking = true; - Greetd.createSession("admin"); - } - } - - Connections { - target: Greetd - - function onAuthMessage(message: string, error: bool, responseRequired: bool, echoResponse: bool) { - if (responseRequired) { - Greetd.respond(root.state.currentText); - } // else ignore - only supporting passwords - } - - function onAuthFailure() { - root.state.currentText = ""; - root.state.error = "Invalid password"; - root.state.failed = true; - root.state.isUnlocking = false; - } - - function onReadyToLaunch() { - root.state.isUnlocking = false; - root.launch(); - } - } -} diff --git a/modules/user/modules/quickshell/shell/lock/LockButton.qml b/modules/user/modules/quickshell/shell/lock/LockButton.qml index 74beede..af07509 100644 --- a/modules/user/modules/quickshell/shell/lock/LockButton.qml +++ b/modules/user/modules/quickshell/shell/lock/LockButton.qml @@ -44,8 +44,6 @@ Item { anchors.margins: 15 source: root.icon - sourceSize.width: width - sourceSize.height: height } } } diff --git a/modules/user/modules/quickshell/shell/lock/LockContent.qml b/modules/user/modules/quickshell/shell/lock/LockContent.qml index c8b6eb2..bfce2cc 100644 --- a/modules/user/modules/quickshell/shell/lock/LockContent.qml +++ b/modules/user/modules/quickshell/shell/lock/LockContent.qml @@ -1,202 +1,113 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls -import Quickshell import ".." Item { id: root - required property LockState state; + required property LockContext context; property real focusAnim: focusAnimInternal * 0.001 property int focusAnimInternal: Window.active ? 1000 : 0 Behavior on focusAnimInternal { SmoothedAnimation { velocity: 5000 } } - MouseArea { - anchors.fill: parent - hoverEnabled: true + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + y: parent.height / 2 + textBox.height + id: sep - property real startMoveX: 0 - property real startMoveY: 0 + implicitHeight: 6 + implicitWidth: 800 + radius: height / 2 + color: ShellGlobals.colors.widget + } - // prevents wakeups from bumping the mouse - onPositionChanged: event => { - if (root.state.fadedOut) { - if (root.state.mouseMoved()) { - const xOffset = Math.abs(event.x - startMoveX); - const yOffset = Math.abs(event.y - startMoveY); - const distanceSq = (xOffset * xOffset) + (yOffset * yOffset); - if (distanceSq > (100 * 100)) root.state.fadeIn(); - } else { - startMoveX = event.x; - startMoveY = event.y; - } + ColumnLayout { + implicitWidth: sep.implicitWidth + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: sep.top + spacing: 0 + + Text { + id: timeText + Layout.alignment: Qt.AlignHCenter + + font { + pointSize: 120 + hintingPreference: Font.PreferFullHinting + family: "Noto Sans" + } + + color: "white" + renderType: Text.NativeRendering + + text: { + const hours = ShellGlobals.time.getHours().toString().padStart(2, '0'); + const minutes = ShellGlobals.time.getMinutes().toString().padStart(2, '0'); + return `${hours}:${minutes}`; } } Item { - id: content - width: parent.width - height: parent.height - y: root.state.fadeOutMul * (height / 2 + childrenRect.height) + Layout.alignment: Qt.AlignHCenter + implicitHeight: childrenRect.height * focusAnim + implicitWidth: sep.implicitWidth + clip: true - Rectangle { - anchors.horizontalCenter: parent.horizontalCenter - y: parent.height / 2 + textBox.height - id: sep + TextInput { + id: textBox + focus: true + width: parent.width - implicitHeight: 6 - implicitWidth: 800 - radius: height / 2 - color: ShellGlobals.colors.widget - } + color: enabled ? + root.context.failed ? "#ffa0a0" : "white" + : "#80ffffff"; - ColumnLayout { - implicitWidth: sep.implicitWidth - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: sep.top - spacing: 0 + font.pointSize: 24 + horizontalAlignment: TextInput.AlignHCenter + echoMode: TextInput.Password + inputMethodHints: Qt.ImhSensitiveData - SystemClock { - id: clock - precision: SystemClock.Minutes - } + onTextChanged: root.context.currentText = text; - Text { - id: timeText - Layout.alignment: Qt.AlignHCenter - - font { - pointSize: 120 - hintingPreference: Font.PreferFullHinting - family: "Noto Sans" - } - - color: "white" - renderType: Text.NativeRendering - - text: { - const hours = clock.hours.toString().padStart(2, '0'); - const minutes = clock.minutes.toString().padStart(2, '0'); - return `${hours}:${minutes}`; + Window.onActiveChanged: { + if (Window.active) { + text = root.context.currentText; } } - Item { - Layout.alignment: Qt.AlignHCenter - implicitHeight: textBox.height * focusAnim - implicitWidth: sep.implicitWidth - clip: true - - TextInput { - id: textBox - focus: true - width: parent.width - - color: enabled ? - root.state.failed ? "#ffa0a0" : "white" - : "#80ffffff"; - - font.pointSize: 24 - horizontalAlignment: TextInput.AlignHCenter - echoMode: TextInput.Password - inputMethodHints: Qt.ImhSensitiveData - - cursorVisible: text != "" - onCursorVisibleChanged: cursorVisible = text != "" - - onTextChanged: { - root.state.currentText = text; - cursorVisible = text != "" - } - - Window.onActiveChanged: { - if (Window.active) { - text = root.state.currentText; - } - } - - Connections { - target: root.state - - function onCurrentTextChanged() { - textBox.text = root.state.currentText; - } - } - - onAccepted: { - if (text != "") root.state.tryPasswordUnlock(); - } - - enabled: !root.state.isUnlocking; - } - - Text { - anchors.fill: textBox - font: textBox.font - color: root.state.failed ? "#ffa0a0" : "#80ffffff"; - horizontalAlignment: TextInput.AlignHCenter - visible: !textBox.cursorVisible - text: root.state.failed ? root.state.error - : root.state.fprintAvailable ? "Touch sensor or enter password" : "Enter password"; - } - - Rectangle { - Layout.fillHeight: true - implicitWidth: height - color: "transparent" - visible: root.state.fprintAvailable - - anchors { - right: textBox.right - top: textBox.top - bottom: textBox.bottom - } - - Image { - anchors.fill: parent - anchors.margins: 5 - source: "root:icons/fingerprint.svg" - sourceSize.width: width - sourceSize.height: height - } - } + onAccepted: { + if (text != "") root.context.tryUnlock(); } - } - Item { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: sep.bottom - implicitHeight: (75 + 30) * focusAnim - implicitWidth: sep.implicitWidth - clip: true - - RowLayout { - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.topMargin: 50 - spacing: 0 - - LockButton { - icon: "root:icons/monitor.svg" - onClicked: root.state.fadeOut(); - } - - LockButton { - icon: "root:icons/pause.svg" - show: root.state.mediaPlaying; - onClicked: root.state.pauseMedia(); - } - } + enabled: !root.context.isUnlocking; } } } - Rectangle { - id: darkenOverlay - anchors.fill: parent - color: "black" - opacity: root.state.fadeOutMul - visible: opacity != 0 + Item { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: sep.bottom + implicitHeight: (childrenRect.height + 30) * focusAnim + implicitWidth: sep.implicitWidth + clip: true + + RowLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.topMargin: 50 + spacing: 0 + + LockButton { + icon: "root:icons/monitor.svg" + onClicked: root.context.dpms(); + } + + LockButton { + icon: "root:icons/pause.svg" + show: context.mediaPlaying; + onClicked: root.context.pauseMedia(); + } + } } } diff --git a/modules/user/modules/quickshell/shell/lock/LockContext.qml b/modules/user/modules/quickshell/shell/lock/LockContext.qml new file mode 100644 index 0000000..d8c1a87 --- /dev/null +++ b/modules/user/modules/quickshell/shell/lock/LockContext.qml @@ -0,0 +1,57 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland +import Quickshell.Services.Mpris + +Scope { + id: root + signal unlocked(); + property string currentText: ""; + readonly property alias isUnlocking: pamtester.running; + property bool failed: false; + + onCurrentTextChanged: failed = false; + + readonly property bool mediaPlaying: Mpris.players.values.some(player => { + return player.playbackState === MprisPlaybackState.Playing && player.canPause; + }); + + function pauseMedia() { + Mpris.players.values.forEach(player => { + if (player.playbackState === MprisPlaybackState.Playing && player.canPause) { + player.playbackState = MprisPlaybackState.Paused; + } + }); + } + + function dpms() { + Hyprland.dispatch(`dpms`); + } + + Process { + id: pamtester + property bool failed: true + + command: ["pamtester", "login", Quickshell.env("USER"), "authenticate"] + + onStarted: this.write(`${currentText}\n`) + + stdout: SplitParser { + // fails go to stderr + onRead: pamtester.failed = false + } + + onExited: { + if (failed) { + root.failed = true; + } else { + root.unlocked(); + } + } + } + + function tryUnlock() { + pamtester.running = true; + } +} diff --git a/modules/user/modules/quickshell/shell/lock/LockState.qml b/modules/user/modules/quickshell/shell/lock/LockState.qml deleted file mode 100644 index c54db0e..0000000 --- a/modules/user/modules/quickshell/shell/lock/LockState.qml +++ /dev/null @@ -1,70 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Hyprland -import Quickshell.Services.Mpris - -Scope { - signal tryPasswordUnlock(); - property string currentText: ""; - property string error: ""; - property bool isUnlocking: false; - property bool failed: false; - property bool fprintAvailable: false; - - property bool fadedOut: false - property real fadeOutMul: 0 - - NumberAnimation on fadeOutMul { - id: fadeAnim - duration: 600 - easing.type: Easing.BezierSpline - easing.bezierCurve: [0.0, 0.75, 0.15, 1.0, 1.0, 1.0] - - onStopped: { - if (fadedOut) Hyprland.dispatch("dpms off"); - } - } - - onCurrentTextChanged: { - failed = false; - error = ""; - - if (fadedOut) { - fadeIn(); - } - } - - function fadeOut() { - if (fadedOut) return; - fadedOut = true; - fadeAnim.to = 1; - fadeAnim.restart(); - } - - function fadeIn() { - if (!fadedOut) return; - Hyprland.dispatch("dpms on"); - fadedOut = false; - fadeAnim.to = 0; - fadeAnim.restart(); - } - - ElapsedTimer { id: mouseTimer } - - // returns if mouse move should be continued, false should restart - function mouseMoved(): bool { - return mouseTimer.restart() < 0.2; - } - - readonly property bool mediaPlaying: Mpris.players.values.some(player => { - return player.playbackState === MprisPlaybackState.Playing && player.canPause; - }); - - function pauseMedia() { - Mpris.players.values.forEach(player => { - if (player.playbackState === MprisPlaybackState.Playing && player.canPause) { - player.playbackState = MprisPlaybackState.Paused; - } - }); - } -} diff --git a/modules/user/modules/quickshell/shell/lock/SessionLockContext.qml b/modules/user/modules/quickshell/shell/lock/SessionLockContext.qml deleted file mode 100644 index 08ffc6b..0000000 --- a/modules/user/modules/quickshell/shell/lock/SessionLockContext.qml +++ /dev/null @@ -1,44 +0,0 @@ -import Quickshell -import Quickshell.Services.Pam - -Scope { - id: root - signal unlocked(); - - property LockState state: LockState { - onTryPasswordUnlock: { - root.state.isUnlocking = true; - pam.start(); - } - } - - PamContext { - id: pam - configDirectory: "pam" - config: "password.conf" - - onPamMessage: { - if (this.responseRequired) { - this.respond(root.state.currentText); - } else if (this.messageIsError) { - root.state.currentText = ""; - root.state.failed = true; - root.state.error = this.message; - } // else ignore - } - - onCompleted: status => { - const success = status == PamResult.Success; - - if (!success) { - root.state.currentText = ""; - root.state.error = "Invalid password"; - } - - root.state.failed = !success; - root.state.isUnlocking = false; - - if (success) root.unlocked(); - } - } -} diff --git a/modules/user/modules/quickshell/shell/lock/pam/fprint.conf b/modules/user/modules/quickshell/shell/lock/pam/fprint.conf deleted file mode 100644 index 32955a9..0000000 --- a/modules/user/modules/quickshell/shell/lock/pam/fprint.conf +++ /dev/null @@ -1 +0,0 @@ -auth required /run/current-system/sw/lib/security/pam_fprintd.so \ No newline at end of file diff --git a/modules/user/modules/quickshell/shell/lock/pam/password.conf b/modules/user/modules/quickshell/shell/lock/pam/password.conf deleted file mode 100644 index 7b313c8..0000000 --- a/modules/user/modules/quickshell/shell/lock/pam/password.conf +++ /dev/null @@ -1 +0,0 @@ -auth required pam_unix.so \ No newline at end of file diff --git a/modules/user/modules/quickshell/shell/notifications/CloseButton.qml b/modules/user/modules/quickshell/shell/notifications/CloseButton.qml deleted file mode 100644 index 6a0df0a..0000000 --- a/modules/user/modules/quickshell/shell/notifications/CloseButton.qml +++ /dev/null @@ -1,39 +0,0 @@ -import QtQuick - -Canvas { - id: root - property real ringFill: 1.0 - - onRingFillChanged: requestPaint(); - - renderStrategy: Canvas.Cooperative - - onPaint: { - const ctx = getContext("2d"); - ctx.reset(); - - ctx.lineWidth = 2; - ctx.strokeStyle = "#70ffffff"; - - ctx.beginPath(); - const half = Math.round(root.width / 2); - const start = -Math.PI * 0.5; - const endM = ringFill == 0.0 || ringFill == 1.0 ? ringFill : 1.0 - ringFill - ctx.arc(half, half, half - ctx.lineWidth, start, start + 2 * Math.PI * endM, true); - ctx.stroke(); - - const xMin = Math.min(root.width * 0.3); - const xMax = Math.max(root.width * 0.7); - ctx.strokeStyle = "white"; - - ctx.beginPath(); - ctx.moveTo(xMin, xMin); - ctx.lineTo(xMax, xMax); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(xMax, xMin); - ctx.lineTo(xMin, xMax); - ctx.stroke(); - } -} diff --git a/modules/user/modules/quickshell/shell/notifications/DaemonNotification.qml b/modules/user/modules/quickshell/shell/notifications/DaemonNotification.qml deleted file mode 100644 index 2443fa2..0000000 --- a/modules/user/modules/quickshell/shell/notifications/DaemonNotification.qml +++ /dev/null @@ -1,35 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Services.Notifications - -TrackedNotification { - id: root - required property Notification notif; - - renderComponent: StandardNotificationRenderer { - notif: root.notif - backer: root - } - - function handleDiscard() { - if (!lock.retained) notif.dismiss(); - root.discarded(); - } - - function handleDismiss() { - //handleDiscard(); - } - - RetainableLock { - id: lock - object: root.notif - locked: true - onRetainedChanged: { - if (retained) root.discard(); - } - } - - expireTimeout: notif.expireTimeout -} diff --git a/modules/user/modules/quickshell/shell/notifications/FlickableNotification.qml b/modules/user/modules/quickshell/shell/notifications/FlickableNotification.qml deleted file mode 100644 index a7013fd..0000000 --- a/modules/user/modules/quickshell/shell/notifications/FlickableNotification.qml +++ /dev/null @@ -1,243 +0,0 @@ -import QtQuick -import Quickshell -import "../components" - -Item { - id: root - - enum FlingState { - Inert, - Returning, - Flinging, - Dismissing - } - - implicitWidth: display.implicitWidth - - // note: can be 0, use ZHVStack - implicitHeight: (display.implicitHeight + padding * 2) * display.meshFactor - - z: 1.0 - display.meshFactor - - property var view; - property Item contentItem; - property real padding: 5; - property real edgeXOffset; - property bool canOverlap: display.rotation > 2 || Math.abs(display.displayY) > 10 || display.displayX < -60 - property bool canDismiss: display.state != FlickableNotification.Dismissing && display.state != FlickableNotification.Flinging; - - property alias displayContainer: displayContainer; - - signal leftViewBounds(); - signal dismissed(); - signal discarded(); - signal startedFlick(); - - function playEntry(delay: real) { - if (display.state != FlickableNotification.Flinging) { - display.displayX = -display.width + edgeXOffset - root.playReturn(delay); - } - } - - function playDismiss(delay: real) { - if (display.state != FlickableNotification.Flinging && display.state != FlickableNotification.Dismissing) { - display.state = FlickableNotification.Dismissing; - display.animationDelay = delay; - } - } - - function playDiscard(delay: real) { - if (display.state != FlickableNotification.Flinging && display.state != FlickableNotification.Dismissing) { - display.velocityX = 500; - display.velocityY = 1500; - display.state = FlickableNotification.Flinging; - display.animationDelay = delay; - } - } - - function playReturn(delay: real) { - if (display.state != FlickableNotification.Flinging) { - display.state = FlickableNotification.Returning; - display.animationDelay = delay; - } - } - - MouseArea { - id: mouseArea - width: display.width - height: display.height - enabled: display.state == FlickableNotification.Inert || display.state == FlickableNotification.Returning - - FlickMonitor { - id: flickMonitor - target: mouseArea - - onDragDeltaXChanged: { - const delta = dragDeltaX; - display.displayX = delta < 0 ? delta : Math.pow(delta, 0.8); - display.updateMeshFactor(true); - updateDragY(); - } - - onDragDeltaYChanged: { - updateDragY(); - display.state = FlickableNotification.Inert; - } - - function updateDragY() { - //const xMul = 1//dragDeltaX < 0 ? 0 : Math.min(1, Math.pow(dragDeltaX / 200, 0.8)); - const d = Math.max(0, Math.min(5000, display.displayX)) / 2000; - const xMul = d - const targetY = dragDeltaY; - display.displayY = root.padding + targetY * xMul; - } - - onFlickStarted: { - display.initialAnimComplete = true; - root.startedFlick(); - } - - onFlickCompleted: { - display.releaseY = dragEndY; - - if (velocityX > 1000 || (velocityX > -100 && display.displayX > display.width * 0.4)) { - display.velocityX = Math.max(velocityX * 0.8, 1000); - display.velocityY = velocityY * 0.6; - display.state = FlickableNotification.Flinging; - root.discarded(); - } else if (velocityX < -1500 || (velocityX < 100 && display.displayX < -(display.width * 0.4))) { - display.velocityX = Math.min(velocityX * 0.8, -700) - display.velocityY = 0 - display.state = FlickableNotification.Dismissing; - root.dismissed(); - } else { - display.velocityX = 0; - display.velocityY = 0; - display.state = FlickableNotification.Returning; - } - } - } - - Item { - id: displayContainer - layer.enabled: view && view.topNotification == root - opacity: layer.enabled ? 0 : 1 // shader ignores it - width: Math.ceil(display.width + display.xPadding * 2) - height: Math.ceil(display.height + display.yPadding * 2) - - x: Math.floor(display.targetContainmentX) - y: Math.floor(display.targetContainmentY) - - Item { - id: display - //anchors.centerIn: parent - x: xPadding + (targetContainmentX - displayContainer.x) - y: yPadding + (targetContainmentY - displayContainer.y) - //visible: meshFactor > 0.95 - - children: [root.contentItem] - implicitWidth: root.contentItem?.width ?? 0 - implicitHeight: root.contentItem?.height ?? 0 - - property var state: FlickableNotification.Inert; - property real meshFactor: 1; - property real velocityX; - property real velocityY; - property real releaseY; - property real animationDelay; - property bool initialAnimComplete; - - property real displayX; - property real displayY; - - property real tiltSize: Math.max(width, height) * 1.2; - property real xPadding: (tiltSize - width) / 2; - property real yPadding: (tiltSize - height) / 2; - - property real targetContainmentX: display.displayX - display.xPadding - property real targetContainmentY: root.padding + display.displayY - display.yPadding - - function updateMeshFactor(canRemesh: bool) { - let meshFactor = (display.implicitWidth - Math.abs(display.displayX)) / display.implicitWidth; - meshFactor = 0.8 + (meshFactor * 0.2); - meshFactor = Math.max(0, meshFactor); - - if (canRemesh) this.meshFactor = meshFactor; - else this.meshFactor = Math.min(this.meshFactor, meshFactor); - } - - function unmesh(delta: real) { - if (meshFactor > 0) { - this.meshFactor = Math.max(0, this.meshFactor - delta * 5); - } - } - - rotation: display.displayX < 0 ? 0 : display.displayX * (initialAnimComplete ? 0.1 : 0.02) - - property real lastX; - - FrameAnimation { - function dampingVelocity(currentVelocity, delta) { - const spring = 1.0; - const damping = 0.1; - const springForce = spring * delta; - const dampingForce = -damping * currentVelocity; - return currentVelocity + (springForce + dampingForce); - } - - running: display.state != FlickableNotification.Inert - onTriggered: { - let frameTime = this.frameTime; - if (display.animationDelay != 0) { - const usedDelay = Math.min(display.animationDelay, frameTime); - frameTime -= usedDelay; - display.animationDelay -= usedDelay; - if (frameTime == 0) return; - } - - if (display.state == FlickableNotification.Flinging) { - display.velocityY += frameTime * 100000 * (1 / display.velocityX * 100); - //display.velocityX -= display.velocityX * 0.98 * frameTime - display.unmesh(frameTime); - } else if (display.state == FlickableNotification.Dismissing) { - const d = Math.max(0, Math.min(5000, display.displayX)) / 2000; - display.displayY = root.padding + display.releaseY * d; - display.velocityY = 0; - - display.velocityX += frameTime * -20000; - - if (display.displayX + display.width > 0) display.updateMeshFactor(false); - else display.unmesh(frameTime); - } else { - const deltaX = 0 - display.displayX; - const deltaY = root.padding - display.displayY; - - display.velocityX = dampingVelocity(display.velocityX, deltaX); - display.velocityY = dampingVelocity(display.velocityY, deltaY); - - if (Math.abs(display.velocityX) < 0.01 && Math.abs(deltaX) < 1 - && Math.abs(display.velocityY) < 0.01 && Math.abs(deltaY) < 1) { - display.state = FlickableNotification.Inert; - display.displayX = 0; - display.displayY = root.padding; - display.velocityX = 0; - display.velocityY = 0; - display.initialAnimComplete = true; - } - - display.updateMeshFactor(true); - } - - display.displayX += display.velocityX * frameTime; - display.displayY += display.velocityY * frameTime; - - - // todo: actually base this on the viewport - if (display.displayX > 10000 || display.displayY > 10000 || (display.displayX + display.width < root.edgeXOffset && display.meshFactor == 0) || display.displayY < -10000) root.leftViewBounds(); - } - } - } - } - } -} diff --git a/modules/user/modules/quickshell/shell/notifications/NotificationDisplay.qml b/modules/user/modules/quickshell/shell/notifications/NotificationDisplay.qml deleted file mode 100644 index c71a2c8..0000000 --- a/modules/user/modules/quickshell/shell/notifications/NotificationDisplay.qml +++ /dev/null @@ -1,126 +0,0 @@ -import QtQuick -import QtQuick.Effects -import Qt5Compat.GraphicalEffects -import "../components" -import "../shaders" as Shaders - -Item { - id: root - property list notifications: []; - property list heightStack: []; - - property alias stack: stack; - property alias topNotification: stack.topNotification; - - function addNotificationInert(notification: TrackedNotification): Item { - const harness = stack._harnessComponent.createObject(stack, { - backer: notification, - view: root, - }); - - harness.contentItem = notification.renderComponent.createObject(harness); - - notifications = [...notifications, harness]; - heightStack = [harness, ...heightStack]; - - return harness; - } - - function addNotification(notification: TrackedNotification) { - const harness = root.addNotificationInert(notification); - harness.playEntry(0); - } - - function dismissAll() { - let delay = 0; - - for (const notification of root.notifications) { - if (!notification.canDismiss) continue; - notification.playDismiss(delay); - notification.dismissed(); - delay += 0.025; - } - } - - function discardAll() { - let delay = 0; - - for (const notification of root.notifications) { - if (!notification.canDismiss) continue; - notification.playDismiss(delay); - notification.discarded(); - delay += 0.025; - } - } - - function addSet(notifications: list) { - let delay = 0; - - for (const notification of notifications) { - if (notification.visualizer) { - notification.visualizer.playReturn(delay); - } else { - const harness = root.addNotificationInert(notification); - harness.playEntry(delay); - } - - delay += 0.025; - } - } - - Item { - anchors.fill: parent - - layer.enabled: stack.topNotification != null - layer.effect: Shaders.MaskedOverlay { - overlayItem: stack.topNotification?.displayContainer ?? null - overlayPos: Qt.point(stack.x + stack.topNotification.x + overlayItem.x, stack.y + stack.topNotification.y + overlayItem.y) - } - - ZHVStack { - id: stack - - property Item topNotification: { - if (root.heightStack.length < 2) return null; - const top = root.heightStack[0] ?? null; - return top && top.canOverlap ? top : null; - }; - - property Component _harnessComponent: FlickableNotification { - id: notification - required property TrackedNotification backer; - - edgeXOffset: -stack.x - - onDismissed: backer.handleDismiss(); - onDiscarded: backer.handleDiscard(); - - onLeftViewBounds: { - root.notifications = root.notifications.filter(n => n != this); - root.heightStack = root.heightStack.filter(n => n != this); - this.destroy(); - } - - onStartedFlick: { - root.heightStack = [this, ...root.heightStack.filter(n => n != this)]; - } - - Component.onCompleted: backer.visualizer = this; - - Connections { - target: backer - - function onDismiss() { - notification.playDismiss(0); - notification.dismissed(); - } - - function onDiscard() { - notification.playDismiss(0); - notification.discarded(); - } - } - } - } - } -} diff --git a/modules/user/modules/quickshell/shell/notifications/NotificationManager.qml b/modules/user/modules/quickshell/shell/notifications/NotificationManager.qml deleted file mode 100644 index 97144e5..0000000 --- a/modules/user/modules/quickshell/shell/notifications/NotificationManager.qml +++ /dev/null @@ -1,74 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Services.Notifications - -Singleton { - id: root - - property list notifications; - property Component notifComponent: DaemonNotification {} - - property bool showTrayNotifs: false; - property bool dnd: false; - property bool hasNotifs: root.notifications.length != 0 - property var lastHoveredNotif; - - property var overlay; - - signal notif(notif: TrackedNotification); - signal showAll(notifications: list); - signal dismissAll(notifications: list); - signal discardAll(notifications: list); - - NotificationServer { - imageSupported: true - actionsSupported: true - actionIconsSupported: true - - onNotification: notification => { - notification.tracked = true; - - const notif = root.notifComponent.createObject(null, { notif: notification }); - root.notifications = [...root.notifications, notif]; - - root.notif(notif); - } - } - - Instantiator { - model: root.notifications - - Connections { - required property TrackedNotification modelData; - target: modelData; - - function onDiscarded() { - root.notifications = root.notifications.filter(n => n != target); - modelData.untrack(); - } - - function onDiscard() { - if (!modelData.visualizer) modelData.discarded(); - } - } - } - - onShowTrayNotifsChanged: { - if (showTrayNotifs) { - for (const notif of root.notifications) { - notif.inTray = true; - } - - root.showAll(root.notifications); - } else { - root.dismissAll(root.notifications); - } - } - - function sendDiscardAll() { - root.discardAll(root.notifications); - } -} diff --git a/modules/user/modules/quickshell/shell/notifications/NotificationOverlay.qml b/modules/user/modules/quickshell/shell/notifications/NotificationOverlay.qml deleted file mode 100644 index 498ef16..0000000 --- a/modules/user/modules/quickshell/shell/notifications/NotificationOverlay.qml +++ /dev/null @@ -1,39 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Wayland - -PanelWindow { - WlrLayershell.namespace: "shell:notifications" - exclusionMode: ExclusionMode.Ignore - color: "transparent" - - anchors { - left: true - top: true - bottom: true - right: true - } - - property Component notifComponent: DaemonNotification {} - - NotificationDisplay { - id: display - - anchors.fill: parent - - stack.y: 5 + 55//(NotificationManager.showTrayNotifs ? 55 : 0) - stack.x: 72 - } - - visible: display.stack.children.length != 0 - - mask: Region { item: display.stack } - - Component.onCompleted: { - NotificationManager.overlay = this; - NotificationManager.notif.connect(display.addNotification); - NotificationManager.showAll.connect(display.addSet); - NotificationManager.dismissAll.connect(display.dismissAll); - NotificationManager.discardAll.connect(display.discardAll); - } -} diff --git a/modules/user/modules/quickshell/shell/notifications/NotificationWidget.qml b/modules/user/modules/quickshell/shell/notifications/NotificationWidget.qml deleted file mode 100644 index a63ff89..0000000 --- a/modules/user/modules/quickshell/shell/notifications/NotificationWidget.qml +++ /dev/null @@ -1,114 +0,0 @@ -import QtQuick -import QtQuick.Controls -import "root:bar" - -BarWidgetInner { - id: root - required property var bar; - - property bool controlsOpen: false; - onControlsOpenChanged: NotificationManager.showTrayNotifs = controlsOpen; - - Connections { - target: NotificationManager - - function onHasNotifsChanged() { - if (!NotificationManager.hasNotifs) { - root.controlsOpen = false; - } - } - } - - implicitHeight: width - - BarButton { - id: button - anchors.fill: parent - baseMargin: 8 - fillWindowWidth: true - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - showPressed: root.controlsOpen || (pressedButtons & ~Qt.RightButton) - - Image { - anchors.fill: parent - - source: NotificationManager.hasNotifs - ? "root:icons/bell-fill.svg" - : "root:icons/bell.svg" - - fillMode: Image.PreserveAspectFit - - sourceSize.width: width - sourceSize.height: height - } - - onPressed: event => { - if (event.button == Qt.RightButton && NotificationManager.hasNotifs) { - root.controlsOpen = !root.controlsOpen; - } - } - } - - property var tooltip: TooltipItem { - tooltip: bar.tooltip - owner: root - show: button.containsMouse - - Label { - anchors.verticalCenter: parent.verticalCenter - text: { - const count = NotificationManager.notifications.length; - return count == 0 ? "No notifications" - : count == 1 ? "1 notification" - : `${count} notifications`; - } - } - } - - property var rightclickMenu: TooltipItem { - tooltip: bar.tooltip - owner: root - isMenu: true - grabWindows: [NotificationManager.overlay] - show: root.controlsOpen - onClose: root.controlsOpen = false - - Item { - implicitWidth: 440 - implicitHeight: root.implicitHeight - 10 - - MouseArea { - id: closeArea - - anchors { - right: parent.right - rightMargin: 5 - verticalCenter: parent.verticalCenter - } - - implicitWidth: 30 - implicitHeight: 30 - - hoverEnabled: true - onPressed: { - NotificationManager.sendDiscardAll() - } - - Rectangle { - anchors.fill: parent - anchors.margins: 5 - radius: width * 0.5 - antialiasing: true - color: "#60ffffff" - opacity: closeArea.containsMouse ? 1 : 0 - Behavior on opacity { SmoothedAnimation { velocity: 8 } } - } - - CloseButton { - anchors.fill: parent - ringFill: root.backer.timePercentage - } - } - } - } -} diff --git a/modules/user/modules/quickshell/shell/notifications/StandardNotificationRenderer.qml b/modules/user/modules/quickshell/shell/notifications/StandardNotificationRenderer.qml deleted file mode 100644 index 26cad48..0000000 --- a/modules/user/modules/quickshell/shell/notifications/StandardNotificationRenderer.qml +++ /dev/null @@ -1,203 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Services.Notifications -import ".." - -Rectangle { - id: root - required property Notification notif; - required property var backer; - - color: notif.urgency == NotificationUrgency.Critical ? "#30ff2030" : "#30c0ffff" - radius: 5 - implicitWidth: 450 - implicitHeight: c.implicitHeight - - HoverHandler { - onHoveredChanged: { - backer.pauseCounter += hovered ? 1 : -1; - } - } - - Rectangle { - id: border - anchors.fill: parent - color: "transparent" - border.width: 2 - border.color: ShellGlobals.colors.widgetOutline - radius: root.radius - } - - ColumnLayout { - id: c - anchors.fill: parent - spacing: 0 - - ColumnLayout { - Layout.margins: 10 - - RowLayout { - Image { - visible: source != "" - source: notif.appIcon ? Quickshell.iconPath(notif.appIcon) : "" - fillMode: Image.PreserveAspectFit - antialiasing: true - sourceSize.width: 30 - sourceSize.height: 30 - Layout.preferredWidth: 30 - Layout.preferredHeight: 30 - } - - Label { - visible: text != "" - text: notif.summary - font.pointSize: 20 - elide: Text.ElideRight - Layout.maximumWidth: root.implicitWidth - 100 // QTBUG-127649 - } - - Item { Layout.fillWidth: true } - - MouseArea { - id: closeArea - Layout.preferredWidth: 30 - Layout.preferredHeight: 30 - - hoverEnabled: true - onPressed: root.backer.discard(); - - Rectangle { - anchors.fill: parent - anchors.margins: 5 - radius: width * 0.5 - antialiasing: true - color: "#60ffffff" - opacity: closeArea.containsMouse ? 1 : 0 - Behavior on opacity { SmoothedAnimation { velocity: 8 } } - } - - CloseButton { - anchors.fill: parent - ringFill: root.backer.timePercentage - } - } - } - - Item { - Layout.topMargin: 3 - visible: bodyLabel.text != "" || notifImage.visible - implicitWidth: bodyLabel.width - implicitHeight: Math.max(notifImage.size, bodyLabel.implicitHeight) - - Image { - id: notifImage - readonly property int size: visible ? 14 * 8 : 0 - y: bodyLabel.y + bodyLabel.topPadding - - visible: source != "" - source: notif.image - fillMode: Image.PreserveAspectFit - cache: false - antialiasing: true - - width: size - height: size - sourceSize.width: size - sourceSize.height: size - } - - Label { - id: bodyLabel - width: root.implicitWidth - 20 - text: notif.body - wrapMode: Text.Wrap - - onLineLaidOut: line => { - if (!notifImage.visible) return; - - const isize = notifImage.size + 6; - if (line.y + line.height <= notifImage.y + isize) { - line.x += isize; - line.width -= isize; - } - } - } - } - } - - ColumnLayout { - Layout.fillWidth: true - Layout.margins: root.border.width - spacing: 0 - visible: notif.actions.length != 0 - - Rectangle { - height: border.border.width - Layout.fillWidth: true - color: border.border.color - antialiasing: true - } - - RowLayout { - spacing: 0 - - Repeater { - model: notif.actions - - Item { - required property NotificationAction modelData; - required property int index; - - Layout.fillWidth: true - implicitHeight: 35 - - Rectangle { - anchors { - top: parent.top - bottom: parent.bottom - left: parent.left - leftMargin: -implicitWidth * 0.5 - } - - visible: index != 0 - implicitWidth: root.border.width - color: ShellGlobals.colors.widgetOutline - antialiasing: true - } - - MouseArea { - id: actionArea - anchors.fill: parent - - onClicked: { - modelData.invoke(); - } - - Rectangle { - anchors.fill: parent - color: actionArea.pressed && actionArea.containsMouse ? "#20000000" : "transparent" - } - - RowLayout { - anchors.centerIn: parent - - Image { - visible: notif.hasActionIcons - source: Quickshell.iconPath(modelData.identifier) - fillMode: Image.PreserveAspectFit - antialiasing: true - sourceSize.height: 25 - sourceSize.width: 25 - } - - Label { text: modelData.text } - } - } - } - } - } - } - } -} diff --git a/modules/user/modules/quickshell/shell/notifications/TestNotification.qml b/modules/user/modules/quickshell/shell/notifications/TestNotification.qml deleted file mode 100644 index 713f4bc..0000000 --- a/modules/user/modules/quickshell/shell/notifications/TestNotification.qml +++ /dev/null @@ -1,5 +0,0 @@ -import QtQuick - -TrackedNotification { - renderComponent: StandardNotificationRenderer {} -} diff --git a/modules/user/modules/quickshell/shell/notifications/TrackedNotification.qml b/modules/user/modules/quickshell/shell/notifications/TrackedNotification.qml deleted file mode 100644 index 3c47ed3..0000000 --- a/modules/user/modules/quickshell/shell/notifications/TrackedNotification.qml +++ /dev/null @@ -1,65 +0,0 @@ -import QtQuick -import Quickshell - -Scope { - id: root - - required property Component renderComponent; - - property bool inTray: false; - property bool destroyOnInvisible: false; - property int visualizerCount: 0; - property FlickableNotification visualizer; - - signal dismiss(); - signal discard(); - signal discarded(); - - function handleDismiss() {} - function handleDiscard() {} - - onVisualizerChanged: { - if (!visualizer) { - expireAnim.stop(); - timePercentage = 1; - } - - if (!visualizer && destroyOnInvisible) this.destroy(); - } - - function untrack() { - destroyOnInvisible = true; - if (!visualizer) this.destroy(); - } - - property int expireTimeout: -1 - property real timePercentage: 1 - property int pauseCounter: 0 - readonly property bool shouldPause: root.pauseCounter != 0 || (NotificationManager.lastHoveredNotif?.pauseCounter ?? 0) != 0 - - onPauseCounterChanged: { - if (pauseCounter > 0) { - NotificationManager.lastHoveredNotif = this; - } - } - - NumberAnimation on timePercentage { - id: expireAnim - running: expireTimeout != 0 - paused: running && root.shouldPause && to == 0 - duration: expireTimeout == -1 ? 10000 : expireTimeout - to: 0 - onFinished: { - if (!inTray) root.dismiss(); - } - } - - onInTrayChanged: { - if (inTray) { - expireAnim.stop(); - expireAnim.duration = 300 * (1 - timePercentage); - expireAnim.to = 1; - expireAnim.start(); - } - } -} diff --git a/modules/user/modules/quickshell/shell/notifications/test.qml b/modules/user/modules/quickshell/shell/notifications/test.qml deleted file mode 100644 index c823ae6..0000000 --- a/modules/user/modules/quickshell/shell/notifications/test.qml +++ /dev/null @@ -1,95 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import "../components" - -ShellRoot { - Component { - id: demoNotif - - FlickableNotification { - contentItem: Rectangle { - color: "white" - border.color: "blue" - border.width: 2 - radius: 10 - width: 400 - height: 150 - } - - onLeftViewBounds: this.destroy() - } - } - - property Component testComponent: TrackedNotification { - id: notification - - renderComponent: Rectangle { - color: "white" - border.color: "blue" - border.width: 2 - radius: 10 - width: 400 - height: 150 - - ColumnLayout { - Button { - text: "dismiss" - onClicked: notification.dismiss(); - } - - Button { - text: "discard" - onClicked: notification.discard(); - } - } - } - - function handleDismiss() { - console.log(`dismiss (sub)`) - } - - function handleDiscard() { - console.log(`discard (sub)`) - } - - Component.onDestruction: console.log(`destroy (sub)`) - }; - - property Component realComponent: DaemonNotification { - id: dn - } - - Daemon { - onNotification: notification => { - notification.tracked = true; - - const o = realComponent.createObject(null, { notif: notification }); - display.addNotification(o); - } - } - - FloatingWindow { - color: "transparent" - - ColumnLayout { - x: 5 - - Button { - visible: false - text: "add notif" - - onClicked: { - //const notif = demoNotif.createObject(stack); - //stack.children = [...stack.children, notif]; - const notif = testComponent.createObject(null); - display.addNotification(notif); - } - } - - //ZHVStack { id: stack } - NotificationDisplay { id: display } - } - } -} diff --git a/modules/user/modules/quickshell/shell/screenshot/Controller.qml b/modules/user/modules/quickshell/shell/screenshot/Controller.qml index ffb92a2..2f54761 100644 --- a/modules/user/modules/quickshell/shell/screenshot/Controller.qml +++ b/modules/user/modules/quickshell/shell/screenshot/Controller.qml @@ -1,5 +1,4 @@ // very bad code DO NOT COPY -pragma ComponentBehavior: Bound import QtQuick import Quickshell @@ -26,7 +25,7 @@ Scope { Process { id: grimProc - command: ["grim", "-l", "0", root.path] + command: ["grim", "-l", "0", path] onExited: code => { if (code == 0) { root.visible = true @@ -41,11 +40,12 @@ Scope { id: magickProc command: [ "magick", - root.path, - "-crop", `${selection.w}x${selection.h}+${selection.x}+${selection.y}`, - "-quality", "70", - "-page", "0x0+0+0", // removes page size and shot position - root.path, + path, + "-crop", + `${selection.w}x${selection.h}+${selection.x}+${selection.y}`, + "-quality", + "70", + path, ] onExited: wlCopy.running = true; @@ -53,14 +53,14 @@ Scope { Process { id: wlCopy - command: ["sh", "-c", `wl-copy < '${root.path}'`] + command: ["sh", "-c", `wl-copy < '${path}'`] - onExited: root.shootingComplete = true; + onExited: shootingComplete = true; } Process { id: cleanupProc - command: ["rm", root.path] + command: ["rm", path] } QtObject { diff --git a/modules/user/modules/quickshell/shell/selection/SelectionLayer.qml b/modules/user/modules/quickshell/shell/selection/SelectionLayer.qml new file mode 100644 index 0000000..367d866 --- /dev/null +++ b/modules/user/modules/quickshell/shell/selection/SelectionLayer.qml @@ -0,0 +1,13 @@ +import Quickshell +import Quickshell.Wayland + +PanelWindow { + visible: false + + anchors { + left: true + right: true + top: true + bottom: true + } +} diff --git a/modules/user/modules/quickshell/shell/shaders/MaskedOverlay.qml b/modules/user/modules/quickshell/shell/shaders/MaskedOverlay.qml deleted file mode 100644 index d5b221c..0000000 --- a/modules/user/modules/quickshell/shell/shaders/MaskedOverlay.qml +++ /dev/null @@ -1,25 +0,0 @@ -import QtQuick - -ShaderEffect { - property Item overlayItem; - property point overlayPos: Qt.point(overlayItem.x, overlayItem.y); - - fragmentShader: Qt.resolvedUrl("masked_overlay.frag.qsb") - - property point pOverlayPos: Qt.point( - overlayPos.x / width, - overlayPos.y / height - ); - - property point pOverlaySize: Qt.point( - overlayItem.width / width, - overlayItem.height / height - ); - - property point pMergeInset: Qt.point( - 3 / width, - 3 / height - ); - - property real pMergeCutoff: 0.15 -} diff --git a/modules/user/modules/quickshell/shell/shaders/masked_overlay.frag b/modules/user/modules/quickshell/shell/shaders/masked_overlay.frag deleted file mode 100644 index a8f98df..0000000 --- a/modules/user/modules/quickshell/shell/shaders/masked_overlay.frag +++ /dev/null @@ -1,40 +0,0 @@ -#version 440 -layout(location = 0) in vec2 qt_TexCoord0; -layout(location = 1) out vec4 fragColor; -layout(binding = 1) uniform sampler2D source; -layout(binding = 2) uniform sampler2D overlayItem; - -layout(std140, binding = 0) uniform buf { - mat4 qt_Matrix; - float qt_Opacity; - vec2 pOverlayPos; - vec2 pOverlaySize; - vec2 pMergeInset; - float pMergeCutoff; -}; - -void main() { - vec2 overlayCoord = (qt_TexCoord0 - pOverlayPos) / pOverlaySize; - - if (overlayCoord.x >= 0 && overlayCoord.y >= 0 && overlayCoord.x < 1 && overlayCoord.y < 1) { - fragColor = texture(overlayItem, overlayCoord); - - if (fragColor.a != 0) { - vec4 baseColor = texture(source, qt_TexCoord0); - // imperfect but visually good enough for now. if more is needed we'll probably need a mask tex - if (baseColor.a != 0 - && fragColor.a < pMergeCutoff - && (texture(overlayItem, overlayCoord + vec2(0, pMergeInset.y)).a == 0 - || texture(overlayItem, overlayCoord + vec2(pMergeInset.x, 0)).a == 0 - || texture(overlayItem, overlayCoord + vec2(0, -pMergeInset.y)).a == 0 - || texture(overlayItem, overlayCoord + vec2(-pMergeInset.x, 0)).a == 0)) { - fragColor += baseColor * (1 - fragColor.a); - } - - fragColor *= qt_Opacity; - return; - } - } - - fragColor = texture(source, qt_TexCoord0) * qt_Opacity; -} diff --git a/modules/user/modules/quickshell/shell/shaders/masked_overlay.frag.qsb b/modules/user/modules/quickshell/shell/shaders/masked_overlay.frag.qsb deleted file mode 100644 index f419913..0000000 Binary files a/modules/user/modules/quickshell/shell/shaders/masked_overlay.frag.qsb and /dev/null differ diff --git a/modules/user/modules/quickshell/shell/shell.nix b/modules/user/modules/quickshell/shell/shell.nix deleted file mode 100644 index 197ef0c..0000000 --- a/modules/user/modules/quickshell/shell/shell.nix +++ /dev/null @@ -1,3 +0,0 @@ -{ pkgs ? import {} }: pkgs.mkShell { - packages = [ pkgs.qt6.qtdeclarative ]; -} diff --git a/modules/user/modules/quickshell/shell/shell.qml b/modules/user/modules/quickshell/shell/shell.qml index da818f1..e0e9b7b 100644 --- a/modules/user/modules/quickshell/shell/shell.qml +++ b/modules/user/modules/quickshell/shell/shell.qml @@ -1,4 +1,3 @@ -//@ pragma ShellId shell import Quickshell import Quickshell.Io import Quickshell.Wayland @@ -7,15 +6,8 @@ import QtQuick.Layouts import ".." import "screenshot" as Screenshot import "bar" as Bar -import "lock" as Lock -import "notifications" as Notifs -import "launcher" as Launcher ShellRoot { - Component.onCompleted: [Lock.Controller, Launcher.Controller.init()] - - ReloadPopup {} - Process { command: ["mkdir", "-p", ShellGlobals.rtpath] running: true @@ -37,16 +29,15 @@ ShellRoot { } } - Notifs.NotificationOverlay { - screen: Quickshell.screens.find(s => s.name == "DP-1") - } - Variants { model: Quickshell.screens Scope { property var modelData + /*Bar { + screen: modelData + }*/ Bar.Bar { screen: modelData } @@ -55,8 +46,6 @@ ShellRoot { id: window screen: modelData - - exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: WlrLayer.Background WlrLayershell.namespace: "shell:background" @@ -71,6 +60,101 @@ ShellRoot { anchors.fill: parent screen: window.screen } + + SelectionLayer { + id: selectionLayer + + onSelectionComplete: (x, y, width, height) => { + console.log(`selection complete: ${x} ${y} ${width} ${height}`) + termSpawner.x = x + termSpawner.y = y + termSpawner.width = width + termSpawner.height = height + termSpawner.running = true + } + + Process { + id: termSpawner + property real x; + property real y; + property real width; + property real height; + + command: [ + "hyprctl", + "dispatch", + "exec", + `[float;; noanim; move ${x} ${y}; size ${width} ${height}] alacritty --class AlacrittyTermselect` + ] + } + + Connections { + target: ShellIpc + + function onTermSelectChanged() { + if (ShellIpc.termSelect) { + selectionLayer.selectionArea.startSelection(true); + } else { + selectionLayer.selectionArea.endSelection(); + } + } + } + + Connections { + target: HyprlandIpc + + function onWindowOpened(_, _, klass, _) { + if (klass == "AlacrittyTermselect") { + selectionLayer.selectionArea.selecting = false + } + } + } + } + + SelectionArea { + anchors.fill: parent + screen: window.screen + selectionArea: selectionLayer.selectionArea + + } + } + + PanelWindow { + visible: false + screen: modelData + WlrLayershell.layer: WlrLayer.Overlay + + anchors { + right: true + bottom: true + } + + margins { + right: 50 + bottom: 50 + } + + width: content.width + height: content.height + + color: "transparent" + mask: Region {} + + ColumnLayout { + id: content + + Text { + text: "Activate Linux" + color: "#50ffffff" + font.pointSize: 22 + } + + Text { + text: "Go to Settings to activate Linux" + color: "#50ffffff" + font.pointSize: 14 + } + } } } } diff --git a/modules/user/modules/walker/default.nix b/modules/user/modules/walker/default.nix new file mode 100644 index 0000000..32ad833 --- /dev/null +++ b/modules/user/modules/walker/default.nix @@ -0,0 +1,57 @@ +{ inputs, impurity, ... }: { + imports = [ inputs.walker.homeManagerModules.walker ]; + + programs.walker = { + enable = true; + runAsService = true; + + config = { + show_initial_entries = true; + fullscreen = true; + scrollbar_policy = "external"; + activation_mode.use_alt = true; + + search = { + hide_icons = false; + hide_spinner = true; + }; + + align = { + width = 500; + horizontal = "center"; + vertical = "center"; + margins.top = 0; + }; + + list = { + height = 500; + fixed_height = true; + always_show = true; + }; + + icons.hide = false; + + modules = [ + { + name = "applications"; + prefix = ""; + } + { + name = "runner"; + prefix = ">"; + } + { + name = "commands"; + prefix = ""; + switcher_exclusive = true; + } + { + name = "switcher"; + prefix = "/"; + } + ]; + }; + }; + + xdg.configFile."walker/style.css".source = impurity.link ./style.css; +} diff --git a/modules/user/modules/walker/style.css b/modules/user/modules/walker/style.css new file mode 100644 index 0000000..5268da9 --- /dev/null +++ b/modules/user/modules/walker/style.css @@ -0,0 +1,91 @@ +* { + color: white; +} + +#window { + background: none; +} + +#box { + background: #c0ffff30; + padding: 7px; + + border: 1px solid #ffffff50; + border-radius: 5px; +} + +#search, +#typeahead { + border-radius: 0; + outline: none; + outline-width: 0px; + box-shadow: none; + border-bottom: none; + border: none; + background: #e0ffff30; + padding-left: 10px; + padding-right: 10px; + padding-top: 0px; + padding-bottom: 0px; + border-radius: 5px; +} + +#typeahead { + background: none; + opacity: 0.5; +} + +#search placeholder { + opacity: 0.5; +} + +#search text { + padding-left: 7px; +} + +#list { + background: none; +} + +#list *:selected { + border-radius: 5px; + border: 1px solid #ffffff30; + background: #e0ffff20; +} + +.item { + padding: 5px; + border-radius: 2px; +} + +.icon { + padding-right: 5px; +} + +.textwrapper { +} + +.label { +} + +.sub { + opacity: 0.5; +} + +.activationlabel { + opacity: 0.25; +} + +.activation .activationlabel { + opacity: 1; + color: #76946a; +} + +.activation .textwrapper, +.activation .icon, +.activation .search { + opacity: 0.5; +} + +#spinner { opacity: 0 } +#spinner.visible { opacity: 1 }