From 2c64563ade97d539d485a66dc816054db7fd2644 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 5 Jan 2025 23:27:49 -0800 Subject: [PATCH 1/2] remove walker --- modules/hyprland/hyprland.conf | 3 - modules/user/modules/walker/default.nix | 57 ---------------- modules/user/modules/walker/style.css | 91 ------------------------- 3 files changed, 151 deletions(-) delete mode 100644 modules/user/modules/walker/default.nix delete mode 100644 modules/user/modules/walker/style.css diff --git a/modules/hyprland/hyprland.conf b/modules/hyprland/hyprland.conf index 578181f..44bd308 100644 --- a/modules/hyprland/hyprland.conf +++ b/modules/hyprland/hyprland.conf @@ -114,9 +114,6 @@ 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/walker/default.nix b/modules/user/modules/walker/default.nix deleted file mode 100644 index 32ad833..0000000 --- a/modules/user/modules/walker/default.nix +++ /dev/null @@ -1,57 +0,0 @@ -{ 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 deleted file mode 100644 index 5268da9..0000000 --- a/modules/user/modules/walker/style.css +++ /dev/null @@ -1,91 +0,0 @@ -* { - 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 } From 4b90113a545f97dbfc80d47810b6730c6d729921 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 6 Jan 2025 00:13:19 -0800 Subject: [PATCH 2/2] last 7 months of qs changes --- .gitignore | 2 + modules/user/modules/greetd/default.nix | 55 +++ modules/user/modules/quickshell/default.nix | 23 +- .../quickshell/lockscreen/AuthContext.qml | 43 --- .../quickshell/lockscreen/LockGlobals.qml | 17 - .../quickshell/lockscreen/Lockscreen.qml | 131 ------- .../modules/quickshell/lockscreen/shell.qml | 36 -- .../modules/quickshell/lockscreen/test.qml | 39 -- .../modules/quickshell/shell/HyprlandIpc.qml | 26 -- .../modules/quickshell/shell/PopupSurface.qml | 81 ---- .../quickshell/shell/SelectionArea.qml | 27 -- .../quickshell/shell/SelectionLayer.qml | 99 ----- .../modules/quickshell/shell/ShellGlobals.qml | 10 - .../modules/quickshell/shell/ShellIpc.qml | 13 +- .../modules/quickshell/shell/Shortcut.qml | 5 - .../modules/quickshell/shell/SlideView.qml | 112 ++++++ .../quickshell/shell/SlideViewItem.qml | 47 +++ .../user/modules/quickshell/shell/bar/Bar.qml | 50 ++- .../quickshell/shell/bar/BarButton.qml | 34 ++ .../quickshell/shell/bar/BarContainment.qml | 6 +- .../quickshell/shell/bar/BugTester.qml | 31 -- .../quickshell/shell/bar/ClickableIcon.qml | 19 + .../quickshell/shell/bar/ClockWidget.qml | 86 +++-- .../quickshell/shell/bar/ExpandingWidget.qml | 97 ----- .../shell/bar/FullwidthMouseArea.qml | 53 +++ .../quickshell/shell/bar/OverlayWidget.qml | 17 - .../modules/quickshell/shell/bar/Tooltip.qml | 161 ++++++-- .../quickshell/shell/bar/TooltipItem.qml | 43 +-- .../{workspaces/Widget.qml => Workspaces.qml} | 29 +- .../shell/bar/audio/AudioControl.qml | 23 +- .../quickshell/shell/bar/audio/Mixer.qml | 13 +- .../quickshell/shell/bar/audio/MixerEntry.qml | 50 +-- .../shell/bar/audio/MixerEntryBase.qml | 50 +++ .../shell/bar/audio/MixerEntryWithSelect.qml | 16 + .../quickshell/shell/bar/audio/volume_off.svg | 1 - .../quickshell/shell/bar/audio/volume_up.svg | 1 - .../shell/bar/mpris/BackgroundArt.qml | 30 +- .../shell/bar/mpris/MprisController.qml | 133 ++++--- .../quickshell/shell/bar/mpris/Player.qml | 7 - .../shell/bar/mpris/PlayerPopup.qml | 2 - .../quickshell/shell/bar/mpris/Players.qml | 348 +++++++++++++----- .../shell/bar/power/BatteryIcon.qml | 45 +++ .../quickshell/shell/bar/power/Power.qml | 224 +++++++++++ .../bar/systray/MenuChildrenRevealer.qml | 2 +- .../quickshell/shell/bar/systray/MenuItem.qml | 38 +- .../{MenuItemList.qml => MenuView.qml} | 6 +- .../quickshell/shell/bar/systray/SysTray.qml | 152 ++++---- .../shell/components/FlickMonitor.qml | 90 +++++ .../shell/components/OptionSlider.qml | 99 +++++ .../shell/components/ProgressBar.qml | 42 +++ .../quickshell/shell/components/ZHVStack.qml | 36 ++ .../user/modules/quickshell/shell/greeter.qml | 38 ++ .../shell/icons/battery-charging.svg | 1 + .../quickshell/shell/icons/battery-empty.svg | 1 + .../quickshell/shell/icons/battery-plus.svg | 1 + .../shell/icons/battery-warning.svg | 1 + .../quickshell/shell/icons/bell-fill.svg | 1 + .../quickshell/shell/icons/bell-slash.svg | 1 + .../modules/quickshell/shell/icons/bell.svg | 1 + .../quickshell/shell/icons/fast-forward.svg | 2 +- .../quickshell/shell/icons/fingerprint.svg | 1 + .../modules/quickshell/shell/icons/gauge.svg | 1 + .../quickshell/shell/icons/headset.svg | 1 + .../shell/icons/magnifying-glass.svg | 1 + .../quickshell/shell/icons/monitor.svg | 2 +- .../modules/quickshell/shell/icons/pause.svg | 2 +- .../modules/quickshell/shell/icons/play.svg | 2 +- .../quickshell/shell/icons/repeat-all.svg | 2 +- .../quickshell/shell/icons/repeat-none.svg | 2 +- .../quickshell/shell/icons/repeat-once.svg | 2 +- .../modules/quickshell/shell/icons/rewind.svg | 2 +- .../quickshell/shell/icons/shuffle-off.svg | 2 +- .../quickshell/shell/icons/shuffle.svg | 2 +- .../quickshell/shell/icons/skip-back.svg | 1 - .../quickshell/shell/icons/skip-forward.svg | 1 - .../quickshell/shell/launcher/Controller.qml | 281 ++++++++++++++ .../quickshell/shell/lock/Controller.qml | 18 +- .../quickshell/shell/lock/GreeterContext.qml | 37 ++ .../quickshell/shell/lock/LockButton.qml | 2 + .../quickshell/shell/lock/LockContent.qml | 249 +++++++++---- .../quickshell/shell/lock/LockContext.qml | 57 --- .../quickshell/shell/lock/LockState.qml | 70 ++++ .../shell/lock/SessionLockContext.qml | 44 +++ .../quickshell/shell/lock/pam/fprint.conf | 1 + .../quickshell/shell/lock/pam/password.conf | 1 + .../shell/notifications/CloseButton.qml | 39 ++ .../notifications/DaemonNotification.qml | 35 ++ .../notifications/FlickableNotification.qml | 243 ++++++++++++ .../notifications/NotificationDisplay.qml | 126 +++++++ .../notifications/NotificationManager.qml | 74 ++++ .../notifications/NotificationOverlay.qml | 39 ++ .../notifications/NotificationWidget.qml | 114 ++++++ .../StandardNotificationRenderer.qml | 203 ++++++++++ .../shell/notifications/TestNotification.qml | 5 + .../notifications/TrackedNotification.qml | 65 ++++ .../quickshell/shell/notifications/test.qml | 95 +++++ .../shell/screenshot/Controller.qml | 20 +- .../shell/selection/SelectionLayer.qml | 13 - .../shell/shaders/MaskedOverlay.qml | 25 ++ .../shell/shaders/masked_overlay.frag | 40 ++ .../shell/shaders/masked_overlay.frag.qsb | Bin 0 -> 3175 bytes .../user/modules/quickshell/shell/shell.nix | 3 + .../user/modules/quickshell/shell/shell.qml | 112 +----- 103 files changed, 3467 insertions(+), 1415 deletions(-) create mode 100644 .gitignore create mode 100644 modules/user/modules/greetd/default.nix delete mode 100644 modules/user/modules/quickshell/lockscreen/AuthContext.qml delete mode 100644 modules/user/modules/quickshell/lockscreen/LockGlobals.qml delete mode 100644 modules/user/modules/quickshell/lockscreen/Lockscreen.qml delete mode 100644 modules/user/modules/quickshell/lockscreen/shell.qml delete mode 100644 modules/user/modules/quickshell/lockscreen/test.qml delete mode 100644 modules/user/modules/quickshell/shell/HyprlandIpc.qml delete mode 100644 modules/user/modules/quickshell/shell/PopupSurface.qml delete mode 100644 modules/user/modules/quickshell/shell/SelectionArea.qml delete mode 100644 modules/user/modules/quickshell/shell/SelectionLayer.qml delete mode 100644 modules/user/modules/quickshell/shell/Shortcut.qml create mode 100644 modules/user/modules/quickshell/shell/SlideView.qml create mode 100644 modules/user/modules/quickshell/shell/SlideViewItem.qml create mode 100644 modules/user/modules/quickshell/shell/bar/BarButton.qml delete mode 100644 modules/user/modules/quickshell/shell/bar/BugTester.qml create mode 100644 modules/user/modules/quickshell/shell/bar/ClickableIcon.qml delete mode 100644 modules/user/modules/quickshell/shell/bar/ExpandingWidget.qml create mode 100644 modules/user/modules/quickshell/shell/bar/FullwidthMouseArea.qml delete mode 100644 modules/user/modules/quickshell/shell/bar/OverlayWidget.qml rename modules/user/modules/quickshell/shell/bar/{workspaces/Widget.qml => Workspaces.qml} (76%) create mode 100644 modules/user/modules/quickshell/shell/bar/audio/MixerEntryBase.qml create mode 100644 modules/user/modules/quickshell/shell/bar/audio/MixerEntryWithSelect.qml delete mode 100644 modules/user/modules/quickshell/shell/bar/audio/volume_off.svg delete mode 100644 modules/user/modules/quickshell/shell/bar/audio/volume_up.svg delete mode 100644 modules/user/modules/quickshell/shell/bar/mpris/Player.qml delete mode 100644 modules/user/modules/quickshell/shell/bar/mpris/PlayerPopup.qml create mode 100644 modules/user/modules/quickshell/shell/bar/power/BatteryIcon.qml create mode 100644 modules/user/modules/quickshell/shell/bar/power/Power.qml rename modules/user/modules/quickshell/shell/bar/systray/{MenuItemList.qml => MenuView.qml} (93%) create mode 100644 modules/user/modules/quickshell/shell/components/FlickMonitor.qml create mode 100644 modules/user/modules/quickshell/shell/components/OptionSlider.qml create mode 100644 modules/user/modules/quickshell/shell/components/ProgressBar.qml create mode 100644 modules/user/modules/quickshell/shell/components/ZHVStack.qml create mode 100644 modules/user/modules/quickshell/shell/greeter.qml create mode 100644 modules/user/modules/quickshell/shell/icons/battery-charging.svg create mode 100644 modules/user/modules/quickshell/shell/icons/battery-empty.svg create mode 100644 modules/user/modules/quickshell/shell/icons/battery-plus.svg create mode 100644 modules/user/modules/quickshell/shell/icons/battery-warning.svg create mode 100644 modules/user/modules/quickshell/shell/icons/bell-fill.svg create mode 100644 modules/user/modules/quickshell/shell/icons/bell-slash.svg create mode 100644 modules/user/modules/quickshell/shell/icons/bell.svg create mode 100644 modules/user/modules/quickshell/shell/icons/fingerprint.svg create mode 100644 modules/user/modules/quickshell/shell/icons/gauge.svg create mode 100644 modules/user/modules/quickshell/shell/icons/headset.svg create mode 100644 modules/user/modules/quickshell/shell/icons/magnifying-glass.svg delete mode 100644 modules/user/modules/quickshell/shell/icons/skip-back.svg delete mode 100644 modules/user/modules/quickshell/shell/icons/skip-forward.svg create mode 100644 modules/user/modules/quickshell/shell/launcher/Controller.qml create mode 100644 modules/user/modules/quickshell/shell/lock/GreeterContext.qml delete mode 100644 modules/user/modules/quickshell/shell/lock/LockContext.qml create mode 100644 modules/user/modules/quickshell/shell/lock/LockState.qml create mode 100644 modules/user/modules/quickshell/shell/lock/SessionLockContext.qml create mode 100644 modules/user/modules/quickshell/shell/lock/pam/fprint.conf create mode 100644 modules/user/modules/quickshell/shell/lock/pam/password.conf create mode 100644 modules/user/modules/quickshell/shell/notifications/CloseButton.qml create mode 100644 modules/user/modules/quickshell/shell/notifications/DaemonNotification.qml create mode 100644 modules/user/modules/quickshell/shell/notifications/FlickableNotification.qml create mode 100644 modules/user/modules/quickshell/shell/notifications/NotificationDisplay.qml create mode 100644 modules/user/modules/quickshell/shell/notifications/NotificationManager.qml create mode 100644 modules/user/modules/quickshell/shell/notifications/NotificationOverlay.qml create mode 100644 modules/user/modules/quickshell/shell/notifications/NotificationWidget.qml create mode 100644 modules/user/modules/quickshell/shell/notifications/StandardNotificationRenderer.qml create mode 100644 modules/user/modules/quickshell/shell/notifications/TestNotification.qml create mode 100644 modules/user/modules/quickshell/shell/notifications/TrackedNotification.qml create mode 100644 modules/user/modules/quickshell/shell/notifications/test.qml delete mode 100644 modules/user/modules/quickshell/shell/selection/SelectionLayer.qml create mode 100644 modules/user/modules/quickshell/shell/shaders/MaskedOverlay.qml create mode 100644 modules/user/modules/quickshell/shell/shaders/masked_overlay.frag create mode 100644 modules/user/modules/quickshell/shell/shaders/masked_overlay.frag.qsb create mode 100644 modules/user/modules/quickshell/shell/shell.nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3998311 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.envrc +.direnv/ \ No newline at end of file diff --git a/modules/user/modules/greetd/default.nix b/modules/user/modules/greetd/default.nix new file mode 100644 index 0000000..1828133 --- /dev/null +++ b/modules/user/modules/greetd/default.nix @@ -0,0 +1,55 @@ +{ 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 567db05..2cbae77 100644 --- a/modules/user/modules/quickshell/default.nix +++ b/modules/user/modules/quickshell/default.nix @@ -1,16 +1,29 @@ -{ inputs, pkgs, lib, system, impurity, ... }: let +{ config, 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 - pamtester # lockscreen + (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; + }; + })) grim imagemagick # screenshot ]; xdg.configFile."quickshell/manifest.conf".text = lib.generators.toKeyValue {} { - shell = "${impurity.link ./shell}"; - lockscreen = "${impurity.link ./lockscreen}"; + shell = "${maybeLink ./.}/shell"; + greeter = "${maybeLink ./.}/shell/greeter.qml"; }; } diff --git a/modules/user/modules/quickshell/lockscreen/AuthContext.qml b/modules/user/modules/quickshell/lockscreen/AuthContext.qml deleted file mode 100644 index f5c5745..0000000 --- a/modules/user/modules/quickshell/lockscreen/AuthContext.qml +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index 70088e4..0000000 --- a/modules/user/modules/quickshell/lockscreen/LockGlobals.qml +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 4b258d0..0000000 --- a/modules/user/modules/quickshell/lockscreen/Lockscreen.qml +++ /dev/null @@ -1,131 +0,0 @@ -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 deleted file mode 100644 index e44d7ac..0000000 --- a/modules/user/modules/quickshell/lockscreen/shell.qml +++ /dev/null @@ -1,36 +0,0 @@ -//@ 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 deleted file mode 100644 index 9792c89..0000000 --- a/modules/user/modules/quickshell/lockscreen/test.qml +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 55aa941..0000000 --- a/modules/user/modules/quickshell/shell/HyprlandIpc.qml +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index d92134d..0000000 --- a/modules/user/modules/quickshell/shell/PopupSurface.qml +++ /dev/null @@ -1,81 +0,0 @@ -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 deleted file mode 100644 index b96ed83..0000000 --- a/modules/user/modules/quickshell/shell/SelectionArea.qml +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index a877f51..0000000 --- a/modules/user/modules/quickshell/shell/SelectionLayer.qml +++ /dev/null @@ -1,99 +0,0 @@ -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 030322b..69541eb 100644 --- a/modules/user/modules/quickshell/shell/ShellGlobals.qml +++ b/modules/user/modules/quickshell/shell/ShellGlobals.qml @@ -24,16 +24,6 @@ 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 3431a6a..b6d6adc 100644 --- a/modules/user/modules/quickshell/shell/ShellIpc.qml +++ b/modules/user/modules/quickshell/shell/ShellIpc.qml @@ -1,19 +1,14 @@ pragma Singleton import Quickshell +import Quickshell.Io import Quickshell.Hyprland Singleton { - readonly property alias termSelect: termSelectBind.pressed; signal screenshot(); - Shortcut { - name: "screenshot" - onPressed: screenshot() - } - - Shortcut { - id: termSelectBind - name: "termselect" + IpcHandler { + target: "screenshot" + function takeScreenshot() { screenshot(); } } } diff --git a/modules/user/modules/quickshell/shell/Shortcut.qml b/modules/user/modules/quickshell/shell/Shortcut.qml deleted file mode 100644 index aea3c7a..0000000 --- a/modules/user/modules/quickshell/shell/Shortcut.qml +++ /dev/null @@ -1,5 +0,0 @@ -import Quickshell.Hyprland - -GlobalShortcut { - appid: "shell" -} diff --git a/modules/user/modules/quickshell/shell/SlideView.qml b/modules/user/modules/quickshell/shell/SlideView.qml new file mode 100644 index 0000000..f6cb951 --- /dev/null +++ b/modules/user/modules/quickshell/shell/SlideView.qml @@ -0,0 +1,112 @@ +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 new file mode 100644 index 0000000..855e26c --- /dev/null +++ b/modules/user/modules/quickshell/shell/SlideViewItem.qml @@ -0,0 +1,47 @@ +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 99acc26..0d484a7 100644 --- a/modules/user/modules/quickshell/shell/bar/Bar.qml +++ b/modules/user/modules/quickshell/shell/bar/Bar.qml @@ -1,3 +1,4 @@ +pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import QtQuick.Controls @@ -5,7 +6,8 @@ import Quickshell import "systray" as SysTray import "audio" as Audio import "mpris" as Mpris -import "workspaces" as Workspaces +import "power" as Power +import "root:notifications" as Notifs BarContainment { id: root @@ -13,6 +15,7 @@ BarContainment { property bool isSoleBar: Quickshell.screens.length == 1; ColumnLayout { + anchors { left: parent.left right: parent.right @@ -21,24 +24,32 @@ BarContainment { ColumnLayout { Layout.fillWidth: true - spacing: 0 - Loader { - active: isSoleBar - Layout.preferredHeight: active ? implicitHeight : 0; + Notifs.NotificationWidget { Layout.fillWidth: true - - sourceComponent: Workspaces.Widget { - bar: root - wsBaseIndex: 1 - } + bar: root } - Workspaces.Widget { - bar: root - Layout.fillWidth: true - wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1; - hideWhenEmpty: isSoleBar + 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 + } } } } @@ -65,8 +76,15 @@ BarContainment { Layout.fillWidth: true } - ClockWidget { + 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 new file mode 100644 index 0000000..4d6a3cd --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/BarButton.qml @@ -0,0 +1,34 @@ +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 cb4343f..87ad94d 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 "root:." -import "root:lock" as Lock +import ".." +import "../lock" as Lock PanelWindow { id: root @@ -20,8 +20,8 @@ PanelWindow { exclusiveZone: width - margins.left color: "transparent" - WlrLayershell.namespace: "shell:bar" + WlrLayershell.namespace: "shell:bar" readonly property var tooltip: tooltip; Tooltip { diff --git a/modules/user/modules/quickshell/shell/bar/BugTester.qml b/modules/user/modules/quickshell/shell/bar/BugTester.qml deleted file mode 100644 index 6becec6..0000000 --- a/modules/user/modules/quickshell/shell/bar/BugTester.qml +++ /dev/null @@ -1,31 +0,0 @@ -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 new file mode 100644 index 0000000..0df230e --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/ClickableIcon.qml @@ -0,0 +1,19 @@ +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 a843a43..9a764f7 100644 --- a/modules/user/modules/quickshell/shell/bar/ClockWidget.qml +++ b/modules/user/modules/quickshell/shell/bar/ClockWidget.qml @@ -1,41 +1,67 @@ import QtQuick import QtQuick.Layouts +import QtQuick.Controls +import Quickshell import ".." -OverlayWidget { - expandedWidth: 600 - expandedHeight: 600 +BarWidgetInner { + id: root + required property var bar; - BarWidgetInner { - implicitHeight: layout.implicitHeight + implicitHeight: layout.implicitHeight - ColumnLayout { - id: layout - spacing: 0 + SystemClock { + id: clock + precision: tooltip.visible ? SystemClock.Seconds : SystemClock.Minutes; + } - anchors { - right: parent.right - left: parent.left + 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"); + } } - - 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 deleted file mode 100644 index 909bd6a..0000000 --- a/modules/user/modules/quickshell/shell/bar/ExpandingWidget.qml +++ /dev/null @@ -1,97 +0,0 @@ -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 new file mode 100644 index 0000000..bb41790 --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/FullwidthMouseArea.qml @@ -0,0 +1,53 @@ +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 deleted file mode 100644 index 7d31693..0000000 --- a/modules/user/modules/quickshell/shell/bar/OverlayWidget.qml +++ /dev/null @@ -1,17 +0,0 @@ -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 31c7171..463036e 100644 --- a/modules/user/modules/quickshell/shell/bar/Tooltip.qml +++ b/modules/user/modules/quickshell/shell/bar/Tooltip.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Hyprland +import "root:/" Scope { id: root @@ -11,11 +12,14 @@ 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) { @@ -24,10 +28,12 @@ Scope { } if (lastActiveItem != null && lastActiveItem != activeItem) { - lastActiveItem.targetVisible = false; + if (activeItem != null) lastActiveItem.targetVisible = false; + else if (root.hangTime == 0) doLastHide(); + else hangTimer.start(); } - lastActiveItem = activeItem; + if (activeItem != null) lastActiveItem = activeItem; } function setItem(item: TooltipItem) { @@ -46,28 +52,59 @@ 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: activeItem != null + activeAsync: shownItem != null PopupWindow { id: popup - 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) + + 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 + } visible: true color: "transparent" //color: "#20000000" mask: Region { - item: (activeItem?.hoverable ?? false) ? tooltipItem : null + item: (shownItem?.hoverable ?? false) ? tooltipItem : null } HyprlandFocusGrab { active: activeItem?.isMenu ?? false - windows: [ popup, bar ] + windows: [ popup, bar, ...(activeItem?.grabWindows ?? []) ] onActiveChanged: { if (!active && activeItem?.isMenu) { activeMenu.close() @@ -75,30 +112,76 @@ 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.activeItem) { - root.activeItem.parent = this; + if (root.shownItem) { + root.shownItem.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 + } + + 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; + + onTargetWidthChanged: { + if (targetWidth > largestAnimWidth) { + largestAnimWidth = targetWidth; } } - readonly property var targetWidth: activeItem?.implicitWidth ?? 0; - readonly property var targetHeight: activeItem?.implicitHeight ?? 0; + onTargetYChanged: updateYBounds(); + onTargetHeightChanged: updateYBounds(); + function updateYBounds() { + if (targetY - targetHeight / 2 < highestAnimY) { + //highestAnimY = targetY - targetHeight / 2 + } - property var lastTargetWidthTracker: 0; - property var lastTargetWidth: 0; - - onTargetWidthChanged: { - lastTargetWidth = lastTargetWidthTracker; - lastTargetWidthTracker = targetWidth; + if (targetY + targetHeight / 2 > lowestAnimY) { + //lowestAnimY = targetY + targetHeight / 2 + } } readonly property real targetY: { - if (activeItem == null) return 0; - const target = bar.contentItem.mapFromItem(activeItem.owner, 0, activeItem.targetRelativeY).y; - return bar.boundedY(target, activeItem.implicitHeight / 2); + if (shownItem == null) return 0; + const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y; + return bar.boundedY(target, shownItem.implicitHeight / 2); } property var w: -1 @@ -107,15 +190,24 @@ Scope { property var y1: -1 property var y2: -1 - y: y1 + y: y1 - popup.anchor.rect.y height: y2 - y1 - SmoothedAnimation { - target: tooltipItem; - property: "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 to: tooltipItem.targetY - tooltipItem.targetHeight / 2; onToChanged: { - if (tooltipItem.y1 == -1 || !(activeItem?.animateSize ?? true)) { + if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) { stop(); tooltipItem.y1 = to; } else { @@ -125,12 +217,11 @@ Scope { } } - SmoothedAnimation { - target: tooltipItem - property: "y2" + SmoothedAnimation on y2 { + id: y2Anim to: tooltipItem.targetY + tooltipItem.targetHeight / 2; onToChanged: { - if (tooltipItem.y2 == -1 || !(activeItem?.animateSize ?? true)) { + if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) { stop(); tooltipItem.y2 = to; } else { @@ -140,13 +231,11 @@ Scope { } } - SmoothedAnimation { + SmoothedAnimation on w { id: widthAnim - target: tooltipItem - property: "w" to: tooltipItem.targetWidth; onToChanged: { - if (tooltipItem.w == -1) { + if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) { 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 aadb179..b5d035c 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,47 +17,35 @@ Item { signal close(); + readonly property alias contentItem: contentItem; default property alias data: contentItem.data; - property Component backgroundComponent: BarWidgetInner { - color: ShellGlobals.colors.bar - anchors.fill: parent - } + property Component backgroundComponent: null onShowChanged: { - 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); + if (show) tooltip.setItem(this); + else tooltip.removeItem(this); } property bool targetVisible: false property real targetOpacity: 0 - opacity: targetOpacity / 1000 + opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul)) Behavior on targetOpacity { id: opacityAnimation - SmoothedAnimation { velocity: 5000 } + SmoothedAnimation { velocity: 5 } } function snapOpacity(opacity: real) { opacityAnimation.enabled = false; - targetOpacity = opacity * 1000 + targetOpacity = opacity; opacityAnimation.enabled = true; } onTargetVisibleChanged: { if (targetVisible) { visible = true; - targetOpacity = 1000; + targetOpacity = 1; } else { close() targetOpacity = 0; @@ -68,20 +56,21 @@ 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 + 10 - implicitWidth: contentItem.implicitWidth + 10 + //clip: true + implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin + implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin readonly property Item item: contentItem; Loader { anchors.fill: parent - active: root.visible || root.preloadBackground + active: root.backgroundComponent && (root.visible || root.preloadBackground) asynchronous: !root.visible && root.preloadBackground sourceComponent: backgroundComponent } @@ -91,7 +80,7 @@ Item { anchors.fill: parent anchors.margins: 5 - implicitHeight: childrenRect.height - implicitWidth: childrenRect.width + implicitHeight: children[0].implicitHeight + implicitWidth: children[0].implicitWidth } } diff --git a/modules/user/modules/quickshell/shell/bar/workspaces/Widget.qml b/modules/user/modules/quickshell/shell/bar/Workspaces.qml similarity index 76% rename from modules/user/modules/quickshell/shell/bar/workspaces/Widget.qml rename to modules/user/modules/quickshell/shell/bar/Workspaces.qml index c52d21b..5c2a1f6 100644 --- a/modules/user/modules/quickshell/shell/bar/workspaces/Widget.qml +++ b/modules/user/modules/quickshell/shell/bar/Workspaces.qml @@ -1,17 +1,21 @@ +pragma ComponentBehavior: Bound; + import QtQuick import QtQuick.Layouts import Quickshell.Hyprland import ".." import "root:." -MouseArea { +FullwidthMouseArea { 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 => { @@ -29,9 +33,6 @@ MouseArea { 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); @@ -45,20 +46,22 @@ MouseArea { } Repeater { - model: 10 + model: root.wsCount - MouseArea { + FullwidthMouseArea { id: wsItem onPressed: Hyprland.dispatch(`workspace ${wsIndex}`); Layout.fillWidth: true implicitHeight: 15 + fillWindowWidth: true + required property int index; - property int wsIndex: wsBaseIndex + index; + property int wsIndex: root.wsBaseIndex + index; property HyprlandWorkspace workspace: null; property bool exists: workspace != null; - property bool active: (monitor?.activeWorkspace ?? false) && monitor.activeWorkspace == workspace; + property bool active: (root.monitor?.activeWorkspace ?? false) && root.monitor.activeWorkspace == workspace; onActiveChanged: { if (active) root.currentIndex = wsIndex; @@ -78,21 +81,21 @@ MouseArea { } } - property real animActive: active ? 100 : 0 - Behavior on animActive { NumberAnimation { duration: 100 } } + property real animActive: active ? 1 : 0 + Behavior on animActive { NumberAnimation { duration: 150 } } - property real animExists: exists ? 100 : 0 + property real animExists: exists ? 1 : 0 Behavior on animExists { NumberAnimation { duration: 100 } } Rectangle { anchors.centerIn: parent height: 10 width: parent.width - scale: 1 + animActive * 0.003 + scale: 1 + wsItem.animActive * 0.3 radius: height / 2 border.color: ShellGlobals.colors.widgetOutline border.width: 1 - color: ShellGlobals.interpolateColors(animExists * 0.01, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive); + color: ShellGlobals.interpolateColors(animExists, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive); } } } diff --git a/modules/user/modules/quickshell/shell/bar/audio/AudioControl.qml b/modules/user/modules/quickshell/shell/bar/audio/AudioControl.qml index 525b895..ab19af8 100644 --- a/modules/user/modules/quickshell/shell/bar/audio/AudioControl.qml +++ b/modules/user/modules/quickshell/shell/bar/audio/AudioControl.qml @@ -12,14 +12,20 @@ ClickableIcon { implicitHeight: width; acceptedButtons: Qt.LeftButton | Qt.RightButton; - showPressed: mixerOpen + fillWindowWidth: true + showPressed: mixerOpen || (pressedButtons & ~Qt.RightButton) + + onPressed: event => { + event.accepted = true; + if (event.button === Qt.RightButton) { + mixerOpen = !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; } } @@ -71,7 +77,16 @@ 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 ab11b22..98d8d6d 100644 --- a/modules/user/modules/quickshell/shell/bar/audio/Mixer.qml +++ b/modules/user/modules/quickshell/shell/bar/audio/Mixer.qml @@ -1,12 +1,18 @@ 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 @@ -15,10 +21,13 @@ ColumnLayout { PwObjectTracker { objects: [ trackedNode, ...linkTracker.linkGroups ] } - MixerEntry { + MixerEntry/*WithSelect*/ { id: nodeEntry node: trackedNode + //nodeList: root.nodeList image: nodeImage + + Component.onCompleted: this.selected.connect(root.selected); } Rectangle { @@ -49,7 +58,7 @@ ColumnLayout { // special cases :( if (icon == "firefox") icon = "firefox-devedition"; - return `image://icon/${icon}` + return Quickshell.iconPath(icon) } } } diff --git a/modules/user/modules/quickshell/shell/bar/audio/MixerEntry.qml b/modules/user/modules/quickshell/shell/bar/audio/MixerEntry.qml index ec78ec0..08bee93 100644 --- a/modules/user/modules/quickshell/shell/bar/audio/MixerEntry.qml +++ b/modules/user/modules/quickshell/shell/bar/audio/MixerEntry.qml @@ -1,51 +1,11 @@ import QtQuick -import QtQuick.Layouts -import Quickshell.Services.Pipewire -import ".." -RowLayout { +MixerEntryBase { id: root - required property PwNode node; - required property string image; - property int state: PwLinkState.Unlinked; - 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 - } + headerComponent: Text { + color: "white" + elide: Text.ElideRight + text: root.getNodeName(root.node) } } diff --git a/modules/user/modules/quickshell/shell/bar/audio/MixerEntryBase.qml b/modules/user/modules/quickshell/shell/bar/audio/MixerEntryBase.qml new file mode 100644 index 0000000..36491b6 --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/audio/MixerEntryBase.qml @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000..f75ac87 --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/audio/MixerEntryWithSelect.qml @@ -0,0 +1,16 @@ +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 deleted file mode 100644 index aeff292..0000000 --- a/modules/user/modules/quickshell/shell/bar/audio/volume_off.svg +++ /dev/null @@ -1 +0,0 @@ - \ 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 deleted file mode 100644 index 0d394c0..0000000 --- a/modules/user/modules/quickshell/shell/bar/audio/volume_up.svg +++ /dev/null @@ -1 +0,0 @@ - \ 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 b80994e..bfdacc8 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,23 +139,27 @@ BarWidgetInner { readonly property Rectangle overlay: overlayItem; Rectangle { id: overlayItem - visible: false + visible: true anchors.fill: parent - border.color: ShellGlobals.colors.widgetOutlineSeparate - border.width: 0//1 - radius: 0//root.radius + radius: 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: 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 + layer.effect: OpacityMask { + maskSource: Rectangle { + width: root.width + height: root.height + radius: root.radius + } } } diff --git a/modules/user/modules/quickshell/shell/bar/mpris/MprisController.qml b/modules/user/modules/quickshell/shell/bar/mpris/MprisController.qml index 4a54ac3..24977f7 100644 --- a/modules/user/modules/quickshell/shell/bar/mpris/MprisController.qml +++ b/modules/user/modules/quickshell/shell/bar/mpris/MprisController.qml @@ -1,9 +1,11 @@ pragma Singleton +pragma ComponentBehavior: Bound +import QtQml.Models import QtQuick import Quickshell +import Quickshell.Io import Quickshell.Services.Mpris -import Quickshell.Hyprland import "../.." Singleton { @@ -15,51 +17,58 @@ Singleton { property bool __reverse: false; property var activeTrack; - Component.onCompleted: { - for (const player of Mpris.players.values) { - if (player.playbackState == MprisPlaybackState.Playing) { - if (root.trackedPlayer == null) { - root.trackedPlayer = player; + + Instantiator { + model: Mpris.players; + + Connections { + required property MprisPlayer modelData; + target: modelData; + + Component.onCompleted: { + if (root.trackedPlayer == null || modelData.isPlaying) { + root.trackedPlayer = modelData; } } - player.playbackStateChanged.connect(() => { - if (root.trackedPlayer !== player) 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; + } } } Connections { target: activePlayer - function onTrackChanged() { + function onPostTrackChanged() { root.updateTrack(); } - } - // Change the tracked player when one changes playback state or is created in a playing state. - Connections { - target: Mpris.players; + 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; - 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; - } - } } } } @@ -67,27 +76,23 @@ Singleton { onActivePlayerChanged: this.updateTrack(); function updateTrack() { - const metadata = this.activePlayer?.metadata ?? {}; - + //console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`) this.activeTrack = { - artUrl: metadata["mpris:artUrl"] ?? "", - title: metadata["xesam:title"] ?? "", - artist: metadata["xesam:artist"] ?? "", + 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", }; this.trackChanged(__reverse); this.__reverse = false; } - 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 isPlaying: this.activePlayer && this.activePlayer.isPlaying; + property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false; + function togglePlaying() { + if (this.canTogglePlaying) this.activePlayer.togglePlaying(); } property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false; @@ -125,7 +130,7 @@ Singleton { } function setActivePlayer(player: MprisPlayer) { - const targetPlayer = player ?? MprisPlayer.players[0]; + const targetPlayer = player ?? Mpris.players[0]; console.log(`setactive: ${targetPlayer} from ${activePlayer}`) if (targetPlayer && this.activePlayer) { @@ -138,31 +143,17 @@ Singleton { this.trackedPlayer = targetPlayer; } - 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; + IpcHandler { + target: "mpris" + + function pauseAll(): void { + for (const player of Mpris.players.values) { + if (player.canPause) player.pause(); } } - } - 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(); + function playPause(): void { root.togglePlaying(); } + function previous(): void { root.previous(); } + function next(): void { root.next(); } } } diff --git a/modules/user/modules/quickshell/shell/bar/mpris/Player.qml b/modules/user/modules/quickshell/shell/bar/mpris/Player.qml deleted file mode 100644 index 4f771ea..0000000 --- a/modules/user/modules/quickshell/shell/bar/mpris/Player.qml +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index de82804..0000000 --- a/modules/user/modules/quickshell/shell/bar/mpris/PlayerPopup.qml +++ /dev/null @@ -1,2 +0,0 @@ -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 fc914e6..d4e54e6 100644 --- a/modules/user/modules/quickshell/shell/bar/mpris/Players.qml +++ b/modules/user/modules/quickshell/shell/bar/mpris/Players.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -7,9 +9,10 @@ import Quickshell.Services.Mpris import ".." import "../.." -MouseArea { +FullwidthMouseArea { id: root hoverEnabled: true + fillWindowWidth: true required property var bar; implicitHeight: column.implicitHeight + 10 @@ -27,12 +30,12 @@ MouseArea { property alias widgetOpen: persist.widgetOpen; acceptedButtons: Qt.RightButton - onClicked: widgetOpen = !widgetOpen + onPressed: widgetOpen = !widgetOpen onWheel: event => { event.accepted = true; if (MprisController.canChangeVolume) { - this.activePlayer.volume = Math.max(0, Math.min(1, this.activePlayer.volume + (event.angleDelta.y / 120) * 0.05)); + root.activePlayer.volume = Math.max(0, Math.min(1, root.activePlayer.volume + (event.angleDelta.y / 120) * 0.05)); } } @@ -42,7 +45,7 @@ MouseArea { id: widget anchors.fill: parent - property real scaleMul: root.pressed || widgetOpen ? 100 : 1 + property real scaleMul: widgetOpen ? 100 : 1 Behavior on scaleMul { SmoothedAnimation { velocity: 600 } } scale: scaleCurve.interpolate(scaleMul / 100, 1, (width - 6) / width) @@ -56,8 +59,10 @@ MouseArea { 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) } @@ -86,6 +91,7 @@ MouseArea { implicitHeight: width scaleIcon: false baseMargin: 3 + hoverEnabled: false enabled: MprisController.canGoPrevious; onClicked: MprisController.previous(); } @@ -95,11 +101,9 @@ MouseArea { image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`; implicitHeight: width scaleIcon: false - enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay; - onClicked: { - if (MprisController.isPlaying) MprisController.pause(); - else MprisController.play(); - } + hoverEnabled: false + enabled: MprisController.canTogglePlaying; + onClicked: MprisController.togglePlaying(); } ClickableIcon { @@ -108,51 +112,162 @@ MouseArea { implicitHeight: width scaleIcon: false baseMargin: 3 + hoverEnabled: false enabled: MprisController.canGoNext; onClicked: MprisController.next(); } } - property var tooltip: TooltipItem { + 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 { id: tooltip tooltip: bar.tooltip owner: root - show: root.containsMouse && (activePlayer?.metadata["mpris:trackid"] ?? false) + show: root.containsMouse - //implicitHeight: root.height - 10 - //implicitWidth: childrenRect.width + /*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 Item { - implicitWidth: 200 - implicitHeight: 100 - } + id: ttcontent + width: parent.width + height: Math.max(parent.height, implicitHeight) + implicitWidth: cl.implicitWidth + 10 + implicitHeight: cl.implicitHeight + 10 + (MprisController.activePlayer ? 8 : 0) - /*Loader { - active: tooltip.visible + ColumnLayout { + id: cl + anchors { + left: parent.left + right: parent.right + top: parent.top + margins: 5 + } - sourceComponent: ColumnLayout { - height: root.height - 10 - RowLayout { - Image { - Layout.fillHeight: true - source: mainPlayer.metadata["mpris:artUrl"] ?? "" + //visible: MprisController.activePlayer != null + + FontMetrics { id: fontmetrics } + + component FullheightLabel: Item { + implicitHeight: fontmetrics.height + implicitWidth: label.implicitWidth + + property alias text: label.text - cache: false - fillMode: Image.PreserveAspectCrop - sourceSize.width: height - sourceSize.height: height - } Label { - text: mainPlayer.identity + id: label + anchors.verticalCenter: parent.verticalCenter } } - Slider { - Layout.fillWidth: true + 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 + } + } + } } property var rightclickMenu: TooltipItem { @@ -182,6 +297,7 @@ MouseArea { target: MprisController function onTrackChanged(reverse: bool) { + console.log(`track changed: rev: ${reverse}`) popupBkg.setArt(MprisController.activeTrack.artUrl, reverse, false); } } @@ -191,71 +307,41 @@ MouseArea { } } + 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 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; - } - } + property var player: root.activePlayer; 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 - ScrollView { + RowLayout { //ScrollView { id: playerSelector anchors.centerIn: parent width: Math.min(implicitWidth, playerSelectorContainment.width) - RowLayout { + //RowLayout { Repeater { model: Mpris.players @@ -281,8 +367,7 @@ MouseArea { source: { const entry = DesktopEntries.byId(modelData.desktopEntry); console.log(`ent ${entry} id ${modelData.desktopEntry}`) - if (!entry) return "image://icon/"; - return `image://icon/${entry.icon}`; + return Quickshell.iconPath(entry?.icon); } //asynchronous: true @@ -292,18 +377,18 @@ MouseArea { } } } - } + //} } } } Item { Layout.fillWidth: true - Layout.bottomMargin: 10 + Layout.bottomMargin: 20 Label { anchors.centerIn: parent - text: activePlayer.identity + text: root.activePlayer.identity } } @@ -311,7 +396,13 @@ MouseArea { 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); @@ -333,14 +424,16 @@ MouseArea { // 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 @@ -348,7 +441,7 @@ MouseArea { Item { Layout.fillWidth: true - implicitHeight: 300//img.implicitHeight + implicitHeight: 302//img.implicitHeight implicitWidth: img.implicitWidth Image { @@ -362,27 +455,50 @@ MouseArea { sourceSize.height: 300 sourceSize.width: 300 + + layer.enabled: true + layer.effect: OpacityMask { + cached: true + maskSource: Rectangle { + width: img.width + height: img.height + radius: 5 + } + } } } - Item { + component CenteredText: Item { Layout.fillWidth: true - Layout.topMargin: 20 + + property alias text: label.text + property alias font: label.font Label { + id: label + visible: text != "" anchors.centerIn: parent - text: track.title + elide: Text.ElideRight + width: Math.min(parent.width - 20, implicitWidth) } } - Item { - Layout.fillWidth: true + CenteredText { Layout.topMargin: 20 + text: track.title + font.pointSize: albumLabel.font.pointSize + 1 + } - Label { - anchors.centerIn: parent - text: track.artist - } + CenteredText { + id: albumLabel + Layout.topMargin: 18 + text: track.album + opacity: 0.8 + } + + CenteredText { + Layout.topMargin: 25 + text: track.artist } Item { Layout.fillHeight: true } @@ -462,11 +578,8 @@ MouseArea { implicitWidth: 80 implicitHeight: width scaleIcon: false - enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay - onClicked: { - if (MprisController.isPlaying) MprisController.pause(); - else MprisController.play(); - } + enabled: MprisController.canTogglePlaying; + onClicked: MprisController.togglePlaying(); } ClickableIcon { @@ -491,33 +604,70 @@ MouseArea { } RowLayout { + Layout.margins: 5 + Label { Layout.preferredWidth: lengthLabel.implicitWidth - text: timeStr(position) + text: positionInfo.timeStr(positionInfo.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: player.length + to: 1 onPressedChanged: { - if (!pressed) player.position = value; + if (!pressed) player.position = value * player.length; bindSlider = !pressed; } Binding { when: slider.bindSlider - slider.value: player.position + slider.value: slider.boundPosition } } Label { id: lengthLabel - text: timeStr(length) + text: positionInfo.timeStr(positionInfo.length) } } } diff --git a/modules/user/modules/quickshell/shell/bar/power/BatteryIcon.qml b/modules/user/modules/quickshell/shell/bar/power/BatteryIcon.qml new file mode 100644 index 0000000..335e8d9 --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/power/BatteryIcon.qml @@ -0,0 +1,45 @@ +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 new file mode 100644 index 0000000..93a8ac6 --- /dev/null +++ b/modules/user/modules/quickshell/shell/bar/power/Power.qml @@ -0,0 +1,224 @@ +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 c937a4b..ad7c0f8 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 != -1 && internalProgress != 101; + readonly property bool animating: internalProgress != (expanded ? 101 : -1); 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 9da1e95..e7592e2 100644 --- a/modules/user/modules/quickshell/shell/bar/systray/MenuItem.qml +++ b/modules/user/modules/quickshell/shell/bar/systray/MenuItem.qml @@ -1,14 +1,15 @@ import QtQuick import QtQuick.Layouts import Quickshell +import Quickshell.Widgets import Quickshell.DBusMenu import "../.." MouseArea { id: root - required property var entry; + required property QsMenuEntry entry; property alias expanded: childrenRevealer.expanded; - property bool animating: childrenRevealer.animating || childrenList.animating; + property bool animating: childrenRevealer.animating || (childMenuLoader?.item?.animating ?? false); // appears it won't actually create the handler when only used from MenuItemList. onExpandedChanged: {} onAnimatingChanged: {} @@ -22,7 +23,7 @@ MouseArea { onClicked: { if (entry.hasChildren) childrenRevealer.expanded = !childrenRevealer.expanded else { - entry.click(); + entry.triggered(); if (entry.toggleType == ToggleButtonType.None) close(); } } @@ -34,19 +35,21 @@ MouseArea { spacing: 0 RowLayout { + id: innerRow + Item { implicitWidth: 22 implicitHeight: 22 MenuCheckBox { anchors.centerIn: parent - visible: entry.toggleType == ToggleButtonType.CheckBox + visible: entry.buttonType == QsMenuButtonType.CheckBox checkState: entry.checkState } MenuRadioButton { anchors.centerIn: parent - visible: entry.toggleType == ToggleButtonType.RadioButton + visible: entry.buttonType == QsMenuButtonType.RadioButton checkState: entry.checkState } @@ -59,7 +62,7 @@ MouseArea { } Text { - text: entry.cleanLabel + text: entry.text color: entry.enabled ? "white" : "#bbbbbb" } @@ -68,25 +71,32 @@ MouseArea { implicitWidth: 22 implicitHeight: 22 - Image { + IconImage { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - visible: entry.icon != "" source: entry.icon - sourceSize.height: parent.height - sourceSize.width: parent.height + visible: source != "" + implicitSize: parent.height } } } - Item { + Loader { + id: childMenuLoader Layout.fillWidth: true - implicitHeight: childrenList.implicitHeight * childrenRevealer.progress + 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 clip: true - MenuItemList { + sourceComponent: MenuView { id: childrenList - items: entry.children + menu: entry onClose: root.close() anchors { diff --git a/modules/user/modules/quickshell/shell/bar/systray/MenuItemList.qml b/modules/user/modules/quickshell/shell/bar/systray/MenuView.qml similarity index 93% rename from modules/user/modules/quickshell/shell/bar/systray/MenuItemList.qml rename to modules/user/modules/quickshell/shell/bar/systray/MenuView.qml index 8a809e1..da02e5c 100644 --- a/modules/user/modules/quickshell/shell/bar/systray/MenuItemList.qml +++ b/modules/user/modules/quickshell/shell/bar/systray/MenuView.qml @@ -6,17 +6,19 @@ import "../.." ColumnLayout { id: root - required property var items; + property alias menu: menuView.menu; property Item animatingItem: null; property bool animating: animatingItem != null; signal close(); signal submenuExpanded(item: var); + QsMenuOpener { id: menuView } + spacing: 0 Repeater { - model: items + model: menuView.children; 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 b243080..b393268 100644 --- a/modules/user/modules/quickshell/shell/bar/systray/SysTray.qml +++ b/modules/user/modules/quickshell/shell/bar/systray/SysTray.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Layouts import QtQuick.Effects @@ -5,102 +7,102 @@ import Quickshell import Quickshell.Services.SystemTray import ".." -OverlayWidget { +BarWidgetInner { id: root - expandedWidth: 600 - expandedHeight: 800 required property var bar; + implicitHeight: column.implicitHeight + 10 - BarWidgetInner { - implicitHeight: column.implicitHeight + 10 + ColumnLayout { + id: column + implicitHeight: childrenRect.height + spacing: 5 - ColumnLayout { - id: column - implicitHeight: childrenRect.height - spacing: 5 + anchors { + fill: parent + margins: 5 + } - anchors { - fill: parent - margins: 5 - } + Repeater { + model: SystemTray.items; - Repeater { - model: SystemTray.items; + Item { + id: item + required property SystemTrayItem modelData; - Item { - required property var modelData; - readonly property alias menu: menuWatcher.menu; + property bool targetMenuOpen: false; - SystemTrayMenuWatcher { - id: menuWatcher; - trayItem: modelData; + Layout.fillWidth: true + implicitHeight: width + + ClickableIcon { + id: mouseArea + anchors { + top: parent.top + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + width: height + + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + + image: item.modelData.icon + showPressed: item.targetMenuOpen || (pressedButtons & ~Qt.RightButton) + fillWindowWidth: true + extraVerticalMargin: column.spacing / 2 + + onClicked: event => { + event.accepted = true; + + if (event.button == Qt.LeftButton) { + item.modelData.activate(); + } else if (event.button == Qt.MiddleButton) { + item.modelData.secondaryActivate(); + } } - property bool targetMenuOpen: false; - onTargetMenuOpenChanged: menu.showChildren = targetMenuOpen - - Layout.fillWidth: true - implicitHeight: width - - ClickableIcon { - id: mouseArea - anchors { - top: parent.top - bottom: parent.bottom - horizontalCenter: parent.horizontalCenter + onPressed: event => { + if (event.button == Qt.RightButton && item.modelData.hasMenu) { + item.targetMenuOpen = !item.targetMenuOpen; } - width: height + } - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onWheel: event => { + event.accepted = true; + const points = event.angleDelta.y / 120 + item.modelData.scroll(points, false); + } - image: modelData.icon - showPressed: targetMenuOpen + property var tooltip: TooltipItem { + tooltip: root.bar.tooltip + owner: mouseArea - onClicked: event => { - event.accepted = true; + show: mouseArea.containsMouse - 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; - } + 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 - - isMenu: true - show: targetMenuOpen && menu.showChildren - animateSize: !rightclickItems.animating - - onClose: targetMenuOpen = false; - - MenuItemList { - id: rightclickItems - items: menu == null ? [] : menu.children - onClose: targetMenuOpen = false; + sourceComponent: MenuView { + menu: item.modelData.menu + onClose: item.targetMenuOpen = false; } } } diff --git a/modules/user/modules/quickshell/shell/components/FlickMonitor.qml b/modules/user/modules/quickshell/shell/components/FlickMonitor.qml new file mode 100644 index 0000000..56da066 --- /dev/null +++ b/modules/user/modules/quickshell/shell/components/FlickMonitor.qml @@ -0,0 +1,90 @@ +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 new file mode 100644 index 0000000..99f8aad --- /dev/null +++ b/modules/user/modules/quickshell/shell/components/OptionSlider.qml @@ -0,0 +1,99 @@ +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 new file mode 100644 index 0000000..087d5e7 --- /dev/null +++ b/modules/user/modules/quickshell/shell/components/ProgressBar.qml @@ -0,0 +1,42 @@ +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 new file mode 100644 index 0000000..8321014 --- /dev/null +++ b/modules/user/modules/quickshell/shell/components/ZHVStack.qml @@ -0,0 +1,36 @@ +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 new file mode 100644 index 0000000..5dc98b4 --- /dev/null +++ b/modules/user/modules/quickshell/shell/greeter.qml @@ -0,0 +1,38 @@ +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 new file mode 100644 index 0000000..324212f --- /dev/null +++ b/modules/user/modules/quickshell/shell/icons/battery-charging.svg @@ -0,0 +1 @@ + diff --git a/modules/user/modules/quickshell/shell/icons/battery-empty.svg b/modules/user/modules/quickshell/shell/icons/battery-empty.svg new file mode 100644 index 0000000..92c1a0f --- /dev/null +++ b/modules/user/modules/quickshell/shell/icons/battery-empty.svg @@ -0,0 +1 @@ + diff --git a/modules/user/modules/quickshell/shell/icons/battery-plus.svg b/modules/user/modules/quickshell/shell/icons/battery-plus.svg new file mode 100644 index 0000000..1323cac --- /dev/null +++ b/modules/user/modules/quickshell/shell/icons/battery-plus.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 0000000..54e3322 --- /dev/null +++ b/modules/user/modules/quickshell/shell/icons/battery-warning.svg @@ -0,0 +1 @@ + diff --git a/modules/user/modules/quickshell/shell/icons/bell-fill.svg b/modules/user/modules/quickshell/shell/icons/bell-fill.svg new file mode 100644 index 0000000..2f7f0b6 --- /dev/null +++ b/modules/user/modules/quickshell/shell/icons/bell-fill.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 0000000..daf40a0 --- /dev/null +++ b/modules/user/modules/quickshell/shell/icons/bell-slash.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 0000000..44abf73 --- /dev/null +++ b/modules/user/modules/quickshell/shell/icons/bell.svg @@ -0,0 +1 @@ + \ 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 1745658..bc4e36e 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 new file mode 100644 index 0000000..a8d0149 --- /dev/null +++ b/modules/user/modules/quickshell/shell/icons/fingerprint.svg @@ -0,0 +1 @@ + diff --git a/modules/user/modules/quickshell/shell/icons/gauge.svg b/modules/user/modules/quickshell/shell/icons/gauge.svg new file mode 100644 index 0000000..d784f93 --- /dev/null +++ b/modules/user/modules/quickshell/shell/icons/gauge.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 0000000..f86e298 --- /dev/null +++ b/modules/user/modules/quickshell/shell/icons/headset.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 0000000..9effa71 --- /dev/null +++ b/modules/user/modules/quickshell/shell/icons/magnifying-glass.svg @@ -0,0 +1 @@ + \ 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 2f51e19..2763ca3 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 d83a876..f48c41a 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 3927ce5..8dfd279 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 17b0b9a..cda7ae2 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 7810ae5..f26adec 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 f0b25da..c1df764 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 deleted file mode 100644 index ae5bb3a..0000000 --- a/modules/user/modules/quickshell/shell/icons/skip-back.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/modules/user/modules/quickshell/shell/icons/skip-forward.svg b/modules/user/modules/quickshell/shell/icons/skip-forward.svg deleted file mode 100644 index 70d3c49..0000000 --- a/modules/user/modules/quickshell/shell/icons/skip-forward.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/modules/user/modules/quickshell/shell/launcher/Controller.qml b/modules/user/modules/quickshell/shell/launcher/Controller.qml new file mode 100644 index 0000000..e803bbc --- /dev/null +++ b/modules/user/modules/quickshell/shell/launcher/Controller.qml @@ -0,0 +1,281 @@ +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 715772b..36384f1 100644 --- a/modules/user/modules/quickshell/shell/lock/Controller.qml +++ b/modules/user/modules/quickshell/shell/lock/Controller.qml @@ -3,8 +3,10 @@ pragma Singleton import QtQuick import QtQuick.Controls import Quickshell +import Quickshell.Io import Quickshell.Wayland import Quickshell.Hyprland +import Quickshell.Services.Pam import ".." import "../.." @@ -61,18 +63,15 @@ Singleton { root.oldWorkspaces = ({}); } - Shortcut { - name: "lock" - onPressed: { - if (root.locked) root.locked = false; - else root.locked = true; - } + IpcHandler { + target: "lockscreen" + function lock(): void { root.locked = true; } } LazyLoader { id: lockContextLoader - LockContext { + SessionLockContext { onUnlocked: root.locked = false; } } @@ -82,7 +81,7 @@ Singleton { onSecureChanged: { if (secure) { - Qt.callLater(() => root.workspaceLockAnimation()); + root.workspaceLockAnimation(); } } @@ -108,7 +107,7 @@ Singleton { LockContent { id: lockContent - context: lockContextLoader.item; + state: lockContextLoader.item.state; visible: false width: lockSurface.width @@ -128,7 +127,6 @@ 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 new file mode 100644 index 0000000..c9f0da8 --- /dev/null +++ b/modules/user/modules/quickshell/shell/lock/GreeterContext.qml @@ -0,0 +1,37 @@ +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 af07509..74beede 100644 --- a/modules/user/modules/quickshell/shell/lock/LockButton.qml +++ b/modules/user/modules/quickshell/shell/lock/LockButton.qml @@ -44,6 +44,8 @@ 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 bfce2cc..c8b6eb2 100644 --- a/modules/user/modules/quickshell/shell/lock/LockContent.qml +++ b/modules/user/modules/quickshell/shell/lock/LockContent.qml @@ -1,113 +1,202 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls +import Quickshell import ".." Item { id: root - required property LockContext context; + required property LockState state; property real focusAnim: focusAnimInternal * 0.001 property int focusAnimInternal: Window.active ? 1000 : 0 Behavior on focusAnimInternal { SmoothedAnimation { velocity: 5000 } } - Rectangle { - anchors.horizontalCenter: parent.horizontalCenter - y: parent.height / 2 + textBox.height - id: sep + MouseArea { + anchors.fill: parent + hoverEnabled: true - implicitHeight: 6 - implicitWidth: 800 - radius: height / 2 - color: ShellGlobals.colors.widget - } + property real startMoveX: 0 + property real startMoveY: 0 - 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}`; + // 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; + } } } Item { - Layout.alignment: Qt.AlignHCenter - implicitHeight: childrenRect.height * focusAnim - implicitWidth: sep.implicitWidth - clip: true + id: content + width: parent.width + height: parent.height + y: root.state.fadeOutMul * (height / 2 + childrenRect.height) - TextInput { - id: textBox - focus: true - width: parent.width + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + y: parent.height / 2 + textBox.height + id: sep - color: enabled ? - root.context.failed ? "#ffa0a0" : "white" - : "#80ffffff"; + implicitHeight: 6 + implicitWidth: 800 + radius: height / 2 + color: ShellGlobals.colors.widget + } - font.pointSize: 24 - horizontalAlignment: TextInput.AlignHCenter - echoMode: TextInput.Password - inputMethodHints: Qt.ImhSensitiveData + ColumnLayout { + implicitWidth: sep.implicitWidth + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: sep.top + spacing: 0 - onTextChanged: root.context.currentText = text; + SystemClock { + id: clock + precision: SystemClock.Minutes + } - Window.onActiveChanged: { - if (Window.active) { - text = root.context.currentText; + 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}`; } } - onAccepted: { - if (text != "") root.context.tryUnlock(); - } + Item { + Layout.alignment: Qt.AlignHCenter + implicitHeight: textBox.height * focusAnim + implicitWidth: sep.implicitWidth + clip: true - enabled: !root.context.isUnlocking; + 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 + } + } + } + } + + 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(); + } + } } } } - 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(); - } - } + Rectangle { + id: darkenOverlay + anchors.fill: parent + color: "black" + opacity: root.state.fadeOutMul + visible: opacity != 0 } } diff --git a/modules/user/modules/quickshell/shell/lock/LockContext.qml b/modules/user/modules/quickshell/shell/lock/LockContext.qml deleted file mode 100644 index d8c1a87..0000000 --- a/modules/user/modules/quickshell/shell/lock/LockContext.qml +++ /dev/null @@ -1,57 +0,0 @@ -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 new file mode 100644 index 0000000..c54db0e --- /dev/null +++ b/modules/user/modules/quickshell/shell/lock/LockState.qml @@ -0,0 +1,70 @@ +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 new file mode 100644 index 0000000..08ffc6b --- /dev/null +++ b/modules/user/modules/quickshell/shell/lock/SessionLockContext.qml @@ -0,0 +1,44 @@ +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 new file mode 100644 index 0000000..32955a9 --- /dev/null +++ b/modules/user/modules/quickshell/shell/lock/pam/fprint.conf @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..7b313c8 --- /dev/null +++ b/modules/user/modules/quickshell/shell/lock/pam/password.conf @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..6a0df0a --- /dev/null +++ b/modules/user/modules/quickshell/shell/notifications/CloseButton.qml @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000..2443fa2 --- /dev/null +++ b/modules/user/modules/quickshell/shell/notifications/DaemonNotification.qml @@ -0,0 +1,35 @@ +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 new file mode 100644 index 0000000..a7013fd --- /dev/null +++ b/modules/user/modules/quickshell/shell/notifications/FlickableNotification.qml @@ -0,0 +1,243 @@ +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 new file mode 100644 index 0000000..c71a2c8 --- /dev/null +++ b/modules/user/modules/quickshell/shell/notifications/NotificationDisplay.qml @@ -0,0 +1,126 @@ +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 new file mode 100644 index 0000000..97144e5 --- /dev/null +++ b/modules/user/modules/quickshell/shell/notifications/NotificationManager.qml @@ -0,0 +1,74 @@ +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 new file mode 100644 index 0000000..498ef16 --- /dev/null +++ b/modules/user/modules/quickshell/shell/notifications/NotificationOverlay.qml @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000..a63ff89 --- /dev/null +++ b/modules/user/modules/quickshell/shell/notifications/NotificationWidget.qml @@ -0,0 +1,114 @@ +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 new file mode 100644 index 0000000..26cad48 --- /dev/null +++ b/modules/user/modules/quickshell/shell/notifications/StandardNotificationRenderer.qml @@ -0,0 +1,203 @@ +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 new file mode 100644 index 0000000..713f4bc --- /dev/null +++ b/modules/user/modules/quickshell/shell/notifications/TestNotification.qml @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..3c47ed3 --- /dev/null +++ b/modules/user/modules/quickshell/shell/notifications/TrackedNotification.qml @@ -0,0 +1,65 @@ +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 new file mode 100644 index 0000000..c823ae6 --- /dev/null +++ b/modules/user/modules/quickshell/shell/notifications/test.qml @@ -0,0 +1,95 @@ +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 2f54761..ffb92a2 100644 --- a/modules/user/modules/quickshell/shell/screenshot/Controller.qml +++ b/modules/user/modules/quickshell/shell/screenshot/Controller.qml @@ -1,4 +1,5 @@ // very bad code DO NOT COPY +pragma ComponentBehavior: Bound import QtQuick import Quickshell @@ -25,7 +26,7 @@ Scope { Process { id: grimProc - command: ["grim", "-l", "0", path] + command: ["grim", "-l", "0", root.path] onExited: code => { if (code == 0) { root.visible = true @@ -40,12 +41,11 @@ Scope { id: magickProc command: [ "magick", - path, - "-crop", - `${selection.w}x${selection.h}+${selection.x}+${selection.y}`, - "-quality", - "70", - path, + 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, ] onExited: wlCopy.running = true; @@ -53,14 +53,14 @@ Scope { Process { id: wlCopy - command: ["sh", "-c", `wl-copy < '${path}'`] + command: ["sh", "-c", `wl-copy < '${root.path}'`] - onExited: shootingComplete = true; + onExited: root.shootingComplete = true; } Process { id: cleanupProc - command: ["rm", path] + command: ["rm", root.path] } QtObject { diff --git a/modules/user/modules/quickshell/shell/selection/SelectionLayer.qml b/modules/user/modules/quickshell/shell/selection/SelectionLayer.qml deleted file mode 100644 index 367d866..0000000 --- a/modules/user/modules/quickshell/shell/selection/SelectionLayer.qml +++ /dev/null @@ -1,13 +0,0 @@ -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 new file mode 100644 index 0000000..d5b221c --- /dev/null +++ b/modules/user/modules/quickshell/shell/shaders/MaskedOverlay.qml @@ -0,0 +1,25 @@ +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 new file mode 100644 index 0000000..a8f98df --- /dev/null +++ b/modules/user/modules/quickshell/shell/shaders/masked_overlay.frag @@ -0,0 +1,40 @@ +#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 new file mode 100644 index 0000000000000000000000000000000000000000..f419913183246deb62a1653b7fc9d9a0f4a2bbb8 GIT binary patch literal 3175 zcmZQz@S0vR=WWdI^5~<2_ihx%Oj+`9qLT(|p2vcqM;eoI)D5?NUcr1s^`K^%;!#ua z$X!KKZcI{+RV`alIc3r%v0qJ0x>LSQ5&OYh<@Y=I|LS>m=U!%}m+hbWEH3t^;B2QR zk#$Sw&OL6vdGqegQOQfM=z6U@`z2>l`t)5(YM!p*e7cg=bkV6V>lb~}a;=s9vf}@v zrAl#Y9+ZVDG_IL-@zCZdnOEULcNZ-S?LG2y^6E>k3~Ent&(n?Cd$7FC=f{#=9vV|` zarHfJ`W^N=QQhU(6&uc{9!J8v4Ju`B%!(@)`aENoPvD9P!jVE2-s?gHTZ6yUXnN&@ zPrhMsbGKKna>cf(3b(BnA5DAle1+CT-Tnrtjm4*LW_R=k{HSS)yx^Gfe_r;}`L(4P zx1TaR2@HC)((+Yj@(!y-F53l<7)7oL@UcC(C)2Web#3m!-4g%x{Y%Q;yiEHWttm0T zp-Q?jxjWjk<-WYNZJkx**Uu;S{hKl^Xzr~3-(@TEWO?hJZ8|zBNUfAjbh-EPW#yas zejWMpJK?d&*766%)AP!&-mZRS!yC2T=w*85>;vU77pxqtRWs%;m@B&F?1H(%Tl6l> zyBuNt`j^D_+%Nwa3T}MMyC~NZKl_56*du$ky4CMXR{S^l75R_v(W?t@{d(`J1TUAf zX`i>dW50Cs&4rn>OQp&mKP=N4Sn}f!a@ zUa`&L|Jn_EnSbr9mClG?`0ee1Q{TQVu5CZCTX_C%*Z+DwSN&f7l=^aq+m@^5`zp78 z8|M9H)q8he=~32Cc_E7({};TgeJOF;o@e&$hsO_}dZK&z-7?i0j$Ma0pWpUg@myk+ zJkz|z3;3PoxA(Vx=y9X!j}&&lIG-ew`@8mm ztxJ&Xs*Ob(@74<~<=$q|o)_3VCsC(=v2D+s+sl09<|?e5Th()B$9=_$cPxKJA3gjQ zJ7IV5iMzZzW1ZfuUa+5SXSKrVFN<5giCu^|{`b@q*30jj`T7p#UHI>2Gsn5w;qjaw z>@O<>_WhszV`sj;Ex!_9b?dwJ&flG#%4JGcTQf?%{wIDi+V;=C@=pzw-x!_6e|%?h z??157zkli_lWF($3uHPz7;^rKZ&YTz<@jJjRTWF0$p#yae}4~L)GjdaZ<`l)BAumg zUq{84=Db1~rn8SWR8_I|9X@c0`5#XK4~NW|;+A=gC*&J_=LH|1I@8}Q-H+k=<(9_h zXUq>2Gg%sQNc}!o{8lja+4qhQj{`6KXR?=LKKty&K!Py zqI}BC$^P8)(jB%Z+i>noH#m0fT8qTv_e{?Vmeb!mDRS`sOvm0_PG6%+&7iHz(8=EB^O7WN-QY^oz~^&NhE*`0!rn%USOKS{o`o ze}uEf{(9b=cip)A?;?rYQh(+${bv>IKKqlZu(_YK zW`FxW@r!XxXOBH&{jpIc>9f+8{R&B+g$(v+Bz-n|VPtln{qC`2zcwi(edhYJpW~ml zl9|1O{qbkBI(RR~FMTfcWv^D!=feRxaok6rx$AyXDgZQZ}`vspk8!O**|;jiu#pjq>LDMW_R3L7p0UchzHfWz=UdTjrF*tPYe^^FY{b^N#353ybT=Nu~gTE5kGcRXK* z%&%sycb@XA9?bjV{-H+0;GZVbWA_EoKJw1rlp9|rCs-w|dVJP`yGVKcE7jsRk8P*# z_1YStXZbQjYuoYY4|^x&*_GX@eR8K|_WG3dJ1%c^YTPlWeQsCz%CI#R8-I39*UsAa z`RL5gpZyZI&H2hc`@~Tp!Q{~WL8pK6Wp>)i7r!u_{`L19?|s#3ufw~zn`bREH7Lxn zo2~t;yRGWx>C`zt4G&q&jJQswtiHLf z%*fU?$u8YoH+0eG;Rq?XTq4`T}qSvwL z%(8jQnX=5_TE`thxnsNfcAeC?*61@~)#Cz{$s7XB9Hl($H(VxXJFD$x77pvuWsUl} zG4doIW0YV2N0qzf8i%GfipE}KEWf<+%qzF+H`lAz&3SZFdC%3=VMT_5Qv8hiSC7w| zAlr4h;7A2WSlp#Uv2V`iU7M8gD(jnPd*78cXP0g;=eD&96h6%mm$k@*_f;3i&6QU( z+zP5L>o!le+w<$emu2gd_wc*${8%n7%WG&Q9p$!s{r=RFs>`eU{o?=MT69@thQEEe z$&nW&8Q0FNZcgJrp8i%_$fQ;L+ybS&-vpj}b^B-3-}D!CNq&3(`H@WX#ac>x&b5Wl zT<~_nx!S{3Gr6Xxe+&JX(c{f~Ytjvo={Nr>eOGdlG21HA-Rc!lz38dXw~KC^vpw%t zTz$q>nzs8;PWTMoePyjT9qz0%xGf41c+0gpHZiZJ_2;$~2H(xJ&ivDNX^qrz6`lTc zR{pkyOs>)WwYwkuX%d_Bl_%rOw$Sidr8i&e8QIsXF8lIQZKYoRu3MX8BiVdTpV|}~ z&gOf1*QVG|v+P?lR&CnYxkW?c=&D^cx;HOu=`%9Q^54Dh*Tnku#+O&fZ|8q{hIiMx zvps4cwn!U{HXP5Q#y$;(-E3a=qu=-=-_PF3lA~Sb+RC7KJw@Ej*4(4tW zPH*2PJ{eygIeKO%gw2}UeBfB@-gp?;eovy`xrZH`IxSoeh3kn zncC!i%*nsPKjQJv1s5ijEAw-_E-`ttNb^6}qJP~5kJ;=kay;^6Iz0OP4b$d-ZA_YL znB8^lzw-Hg4D7v!s{EwhRryP0-@JY1;9*`_n_1kppKEi11z!f-wS2tO^vvE}{8q`qVo>eWxES2-`=do(Nb-`>ukvU9707;N3WUH`Tf{T11^ zD*tPsxOwe*`9z* z?exx|ReI-EJ>N6QAlL5bTBr7Zv*&EdG&a5Z_{k~NtZ5}C@{ekk))oKh-F$D}ueUz; zWOg(?-+9ur`b*xyp2LyN`fGo_vGjQ%rOwsP5*v^q**njGU)%die(9Rie|W6V>$9@y z*Klb~oAlPZZ}zK6i)EtLKlzs{q$cOLJ(b^)u~s{-Zi^lJ!EGlV$f-}Mo&Pe`8r)fSJEG5|9F*h*HW-_c9C%W@-^yly7>{e!gWq( zbLS=Mu8VYf{q+<3f(6GyW4CYc`QoUt=kU73=I$LWoY5xJjbeB89bW76B8{!y+ {} }: 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 e0e9b7b..da818f1 100644 --- a/modules/user/modules/quickshell/shell/shell.qml +++ b/modules/user/modules/quickshell/shell/shell.qml @@ -1,3 +1,4 @@ +//@ pragma ShellId shell import Quickshell import Quickshell.Io import Quickshell.Wayland @@ -6,8 +7,15 @@ 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 @@ -29,15 +37,16 @@ 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 } @@ -46,6 +55,8 @@ ShellRoot { id: window screen: modelData + + exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: WlrLayer.Background WlrLayershell.namespace: "shell:background" @@ -60,101 +71,6 @@ 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 - } - } } } }