diff --git a/hosts/laptop/configuration.nix b/hosts/laptop/configuration.nix index d54daf7..378888c 100755 --- a/hosts/laptop/configuration.nix +++ b/hosts/laptop/configuration.nix @@ -22,6 +22,8 @@ documentation.man.generateCaches = true; # used for the man script +qt.enable = true; + # grub theme boot.loader.grub = { theme = inputs.nixos-grub-themes.packages.${pkgs.system}.nixos; # if you want to use nixos grub theme @@ -207,10 +209,14 @@ krita # image edition yt-dlp# some youtube downloader vlc -#some nix tools + #some nix tools manix deadnix alejandra + # libs or apps for quickshell + libsForQt5.qt5.qtgraphicaleffects + mprisence + kdePackages.qt5compat ]; # fonts diff --git a/hosts/laptop/home.nix b/hosts/laptop/home.nix index c1f014f..c4097bb 100755 --- a/hosts/laptop/home.nix +++ b/hosts/laptop/home.nix @@ -22,7 +22,8 @@ #../../modules/home-manager/neovim2.nix # ./neovim2.nix uses vim plug instead of the nix repository as with ./neovim.nix see the begining of neovim2 for an explanation ../../modules/home-manager/rclone.nix ../../modules/home-manager/waybar.nix - #../../modules/home-manager/oh-my-posh.nix # zsh customizer + ../../modules/home-manager/quickshell.nix # used for widgets. Maybe it will replace waybar +#../../modules/home-manager/oh-my-posh.nix # zsh customizer ../../modules/home-manager/oh-my-zsh.nix # another zsh customizer ../../modules/home-manager/hypridle.nix ../../modules/home-manager/swaylock.nix diff --git a/modules/home-manager/hyprland.nix b/modules/home-manager/hyprland.nix index 074a421..793d442 100644 --- a/modules/home-manager/hyprland.nix +++ b/modules/home-manager/hyprland.nix @@ -228,6 +228,9 @@ in { pseudotile = true; preserve_split = true; }; + scrolling = { + column_width = 0.45; + }; # █▀▄▀█ █ █▀ █▀▀ # █░▀░█ █ ▄█ █▄▄ diff --git a/modules/home-manager/quickshell.nix b/modules/home-manager/quickshell.nix new file mode 100644 index 0000000..dc6789d --- /dev/null +++ b/modules/home-manager/quickshell.nix @@ -0,0 +1,17 @@ +# pulled from https://codeberg.org/zacoons/rivendell-hyprdots + +{ + config, + pkgs, + ... +}: { + qt.enable =true; + programs.quickshell = { + systemd.enable = true; + enable = true; + }; +home.file.".config/quickshell" = { + source = ../../other/quickshell; + recursive = true; +}; +} diff --git a/other/framework_laptop_16_rgb_macropad.layout.json b/other/kblayouts/framework_laptop_16_rgb_macropad.layout.json similarity index 100% rename from other/framework_laptop_16_rgb_macropad.layout.json rename to other/kblayouts/framework_laptop_16_rgb_macropad.layout.json diff --git a/other/quickshell/Bg.qml b/other/quickshell/Bg.qml new file mode 100644 index 0000000..b6d4dbf --- /dev/null +++ b/other/quickshell/Bg.qml @@ -0,0 +1,59 @@ +import Quickshell +import Quickshell.Io +import QtQuick +import QtQuick.Effects +import "./" + +PanelWindow { + property var modelData + screen: modelData + + exclusionMode: ExclusionMode.Ignore + aboveWindows: false + color: "transparent" + anchors { + top: true + left: true + bottom: true + right: true + } + Image { + id: bg + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: `./assets/${Globals.isDarkTheme ? "darkbg" : "bg"}.png` + smooth: false + } + MultiEffect { + source: bg + anchors.fill: bg + blurEnabled: false + blurMax: 24 + blur: 1.0 + } + + Text { + id: timetxt + x: 900 + y: 150 + font.family: "BigBlueTermPlusNerdFont" + font.pointSize: 100 + font.bold: true + color: Globals.isDarkTheme ? "#9292B6" : "#3B2640" + + Process { + id: dateProc + command: ["date", "+%H %M"] + running: true + stdout: SplitParser { + onRead: data => timetxt.text = data + } + } + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: dateProc.running = true + } + } +} diff --git a/other/quickshell/Globals.qml b/other/quickshell/Globals.qml new file mode 100644 index 0000000..75efc69 --- /dev/null +++ b/other/quickshell/Globals.qml @@ -0,0 +1,35 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + readonly property var getThemeProc: Process { + running: true + command: ["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"] + stdout: SplitParser { + onRead: data => root.isDarkTheme = data == "'prefer-dark'" + } + } + property var isDarkTheme: false + readonly property var kittenThemeProc: Process { + command: ["kitten", "theme", "--reload-in=all", `"Gruvbox ${root.isDarkTheme ? "Dark" : "Light"} Soft"`] + } + readonly property var gtk4ThemeProc: Process { + command: ["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", `"prefer-${root.isDarkTheme ? "dark" : "light"}"`] + } + readonly property var gtk3ThemeProc: Process { + command: ["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", `"Adwaita${root.isDarkTheme ? "-dark" : ""}"`] + } + onIsDarkThemeChanged: () => { + kittenThemeProc.command = ["kitten", "theme", "--reload-in=all", `Gruvbox ${root.isDarkTheme ? "Dark" : "Light"} Soft`]; + kittenThemeProc.running = true; + gtk4ThemeProc.command = ["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", `"prefer-${root.isDarkTheme ? "dark" : "light"}"`]; + gtk4ThemeProc.running = true; + gtk3ThemeProc.command = ["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", `"Adwaita${root.isDarkTheme ? "-dark" : ""}"`]; + gtk3ThemeProc.running = true; + } +} diff --git a/other/quickshell/assets/audio.png b/other/quickshell/assets/audio.png new file mode 100644 index 0000000..5d40697 Binary files /dev/null and b/other/quickshell/assets/audio.png differ diff --git a/other/quickshell/assets/bar-decor.png b/other/quickshell/assets/bar-decor.png new file mode 100644 index 0000000..4d35906 Binary files /dev/null and b/other/quickshell/assets/bar-decor.png differ diff --git a/other/quickshell/assets/battery-mask.png b/other/quickshell/assets/battery-mask.png new file mode 100644 index 0000000..9f91305 Binary files /dev/null and b/other/quickshell/assets/battery-mask.png differ diff --git a/other/quickshell/assets/bg.png b/other/quickshell/assets/bg.png new file mode 100644 index 0000000..49c496b Binary files /dev/null and b/other/quickshell/assets/bg.png differ diff --git a/other/quickshell/assets/border.png b/other/quickshell/assets/border.png new file mode 100644 index 0000000..f304ee5 Binary files /dev/null and b/other/quickshell/assets/border.png differ diff --git a/other/quickshell/assets/chest.png b/other/quickshell/assets/chest.png new file mode 100644 index 0000000..6f46727 Binary files /dev/null and b/other/quickshell/assets/chest.png differ diff --git a/other/quickshell/assets/chi-ro.png b/other/quickshell/assets/chi-ro.png new file mode 100644 index 0000000..f3dc483 Binary files /dev/null and b/other/quickshell/assets/chi-ro.png differ diff --git a/other/quickshell/assets/darkbg.png b/other/quickshell/assets/darkbg.png new file mode 100644 index 0000000..7bd1866 Binary files /dev/null and b/other/quickshell/assets/darkbg.png differ diff --git a/other/quickshell/assets/darkborder.png b/other/quickshell/assets/darkborder.png new file mode 100644 index 0000000..261e808 Binary files /dev/null and b/other/quickshell/assets/darkborder.png differ diff --git a/other/quickshell/assets/fastforward.png b/other/quickshell/assets/fastforward.png new file mode 100644 index 0000000..1d44c08 Binary files /dev/null and b/other/quickshell/assets/fastforward.png differ diff --git a/other/quickshell/assets/knight.png b/other/quickshell/assets/knight.png new file mode 100644 index 0000000..f58bdd5 Binary files /dev/null and b/other/quickshell/assets/knight.png differ diff --git a/other/quickshell/assets/launcherbg.png b/other/quickshell/assets/launcherbg.png new file mode 100644 index 0000000..176c43d Binary files /dev/null and b/other/quickshell/assets/launcherbg.png differ diff --git a/other/quickshell/assets/lock-bg.png b/other/quickshell/assets/lock-bg.png new file mode 100644 index 0000000..4a1b1c3 Binary files /dev/null and b/other/quickshell/assets/lock-bg.png differ diff --git a/other/quickshell/assets/moon.png b/other/quickshell/assets/moon.png new file mode 100644 index 0000000..fbb3071 Binary files /dev/null and b/other/quickshell/assets/moon.png differ diff --git a/other/quickshell/assets/mpd.png b/other/quickshell/assets/mpd.png new file mode 100644 index 0000000..cd1819b Binary files /dev/null and b/other/quickshell/assets/mpd.png differ diff --git a/other/quickshell/assets/noaudio.png b/other/quickshell/assets/noaudio.png new file mode 100644 index 0000000..2ff0a53 Binary files /dev/null and b/other/quickshell/assets/noaudio.png differ diff --git a/other/quickshell/assets/noshuffle.png b/other/quickshell/assets/noshuffle.png new file mode 100644 index 0000000..badb6ac Binary files /dev/null and b/other/quickshell/assets/noshuffle.png differ diff --git a/other/quickshell/assets/paper-close.wav b/other/quickshell/assets/paper-close.wav new file mode 100644 index 0000000..df56a88 Binary files /dev/null and b/other/quickshell/assets/paper-close.wav differ diff --git a/other/quickshell/assets/paper-open.wav b/other/quickshell/assets/paper-open.wav new file mode 100644 index 0000000..dd58306 Binary files /dev/null and b/other/quickshell/assets/paper-open.wav differ diff --git a/other/quickshell/assets/pause.png b/other/quickshell/assets/pause.png new file mode 100644 index 0000000..41dd57d Binary files /dev/null and b/other/quickshell/assets/pause.png differ diff --git a/other/quickshell/assets/play.png b/other/quickshell/assets/play.png new file mode 100644 index 0000000..ff318c7 Binary files /dev/null and b/other/quickshell/assets/play.png differ diff --git a/other/quickshell/assets/repeat-off.png b/other/quickshell/assets/repeat-off.png new file mode 100644 index 0000000..3708945 Binary files /dev/null and b/other/quickshell/assets/repeat-off.png differ diff --git a/other/quickshell/assets/repeat-once.png b/other/quickshell/assets/repeat-once.png new file mode 100644 index 0000000..ae0b0d9 Binary files /dev/null and b/other/quickshell/assets/repeat-once.png differ diff --git a/other/quickshell/assets/repeat.png b/other/quickshell/assets/repeat.png new file mode 100644 index 0000000..6ae9eff Binary files /dev/null and b/other/quickshell/assets/repeat.png differ diff --git a/other/quickshell/assets/rope-tie.png b/other/quickshell/assets/rope-tie.png new file mode 100644 index 0000000..f2b9b25 Binary files /dev/null and b/other/quickshell/assets/rope-tie.png differ diff --git a/other/quickshell/assets/shuffle.png b/other/quickshell/assets/shuffle.png new file mode 100644 index 0000000..09f001b Binary files /dev/null and b/other/quickshell/assets/shuffle.png differ diff --git a/other/quickshell/assets/sun.png b/other/quickshell/assets/sun.png new file mode 100644 index 0000000..acc3cb5 Binary files /dev/null and b/other/quickshell/assets/sun.png differ diff --git a/other/quickshell/assets/trumpet-top.png b/other/quickshell/assets/trumpet-top.png new file mode 100644 index 0000000..0b848b1 Binary files /dev/null and b/other/quickshell/assets/trumpet-top.png differ diff --git a/other/quickshell/assets/trumpet.wav b/other/quickshell/assets/trumpet.wav new file mode 100644 index 0000000..e8a86ce Binary files /dev/null and b/other/quickshell/assets/trumpet.wav differ diff --git a/other/quickshell/assets/wilhelm-scream.ogg b/other/quickshell/assets/wilhelm-scream.ogg new file mode 100644 index 0000000..31aff8b Binary files /dev/null and b/other/quickshell/assets/wilhelm-scream.ogg differ diff --git a/other/quickshell/assets/wood.png b/other/quickshell/assets/wood.png new file mode 100644 index 0000000..791f128 Binary files /dev/null and b/other/quickshell/assets/wood.png differ diff --git a/other/quickshell/assets/workspace.png b/other/quickshell/assets/workspace.png new file mode 100644 index 0000000..e59ade7 Binary files /dev/null and b/other/quickshell/assets/workspace.png differ diff --git a/other/quickshell/bar/Bar.qml b/other/quickshell/bar/Bar.qml new file mode 100644 index 0000000..6b73802 --- /dev/null +++ b/other/quickshell/bar/Bar.qml @@ -0,0 +1,166 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import QtQuick.Layouts +import Quickshell.Wayland +import "mpris" as Mpris +import "systray" as SysTray +import ".." + +PanelWindow { + id: root + + property var modelData + readonly property var tooltip: tooltip + readonly property real tooltipYOffset: root.height + 2 + Tooltip { + id: tooltip + bar: root + } + function boundedY(targetY: real, height: real): real { + return Math.max(bar.anchors.topMargin + height, Math.min(bar.height + bar.anchors.topMargin - height, targetY)); + } + function boundedX(targetX: real, width: real): real { + return Math.max(bar.anchors.leftMargin + width, Math.min(bar.width + bar.anchors.leftMargin - width, targetX)); + } + + WlrLayershell.namespace: "shell:bar" + anchors { + top: true + left: true + right: true + } + exclusiveZone: 48 + implicitHeight: 72 + color: "transparent" + mask: barRegion + + Region { + id: barRegion + width: bar.width + height: bar.height + } + + Item { + id: bar + + anchors { + top: parent.top + left: parent.left + right: parent.right + } + + implicitHeight: 48 + + Rectangle { + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + } + + implicitWidth: left.width + left.anchors.leftMargin + left.anchors.rightMargin + implicitHeight: left.height + color: "#3B253F" + + Rectangle { + z: -1 + anchors.fill: parent + color: "#BE850E" + anchors.margins: -4 + } + Image { + width: 16 * 6 + height: 4 * 6 + anchors.top: parent.bottom + anchors.left: parent.left + source: "../assets/bar-decor.png" + smooth: false + } + + RowLayout { + id: left + + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + leftMargin: 7 + rightMargin: 7 + } + + spacing: 12 + + Launcher {} + + Workspaces { + bar: root + } + + SysTray.SysTray { + bar: root + } + } + } + + Rectangle { + anchors { + top: parent.top + bottom: parent.bottom + right: parent.right + } + + implicitWidth: right.width + right.anchors.leftMargin + right.anchors.rightMargin + implicitHeight: right.height + color: "#3B253F" + + Rectangle { + z: -1 + anchors.fill: parent + color: "#BE850E" + anchors.margins: -4 + } + Image { + width: 16 * 6 + height: 4 * 6 + anchors.top: parent.bottom + anchors.right: parent.right + source: "../assets/bar-decor.png" + smooth: false + mirror: true + } + + RowLayout { + id: right + + anchors { + top: parent.top + bottom: parent.bottom + right: parent.right + leftMargin: 7 + rightMargin: 7 + } + + spacing: 12 + + Time {} + + Volume { + bar: root + } + + Mpris.Players { + window: root + bar: root + } + + Theme {} + + Power { + id: power + bar: root + } + } + } + } +} diff --git a/other/quickshell/bar/BarButton.qml b/other/quickshell/bar/BarButton.qml new file mode 100644 index 0000000..c012fb5 --- /dev/null +++ b/other/quickshell/bar/BarButton.qml @@ -0,0 +1,48 @@ +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/other/quickshell/bar/ClickableIcon.qml b/other/quickshell/bar/ClickableIcon.qml new file mode 100644 index 0000000..a87ab98 --- /dev/null +++ b/other/quickshell/bar/ClickableIcon.qml @@ -0,0 +1,20 @@ +import QtQuick + +BarButton { + id: root + property alias image: img.source + property alias mirror: img.mirror + implicitWidth: 24 + implicitHeight: 24 + Image { + id: img + anchors.fill: parent + smooth: false + scale: root.containsMouse ? 0.9 : 1 + Behavior on scale { + NumberAnimation { + duration: 70 + } + } + } +} diff --git a/other/quickshell/bar/FullwidthMouseArea.qml b/other/quickshell/bar/FullwidthMouseArea.qml new file mode 100644 index 0000000..0889a5b --- /dev/null +++ b/other/quickshell/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/other/quickshell/bar/Launcher.qml b/other/quickshell/bar/Launcher.qml new file mode 100644 index 0000000..4f4e908 --- /dev/null +++ b/other/quickshell/bar/Launcher.qml @@ -0,0 +1,39 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Io +import QtQuick.Layouts +import "../launcher" as Launcher + +MouseArea { + id: root + + implicitWidth: 26 + implicitHeight: 26 + + acceptedButtons: Qt.LeftButton + + Image { + id: img + anchors.fill: parent + source: "../assets/chi-ro.png" + smooth: false + scale: Launcher.Controller.isOpen ? 0.8 : 1 + Behavior on scale { + NumberAnimation { + duration: 70 + } + } + } + MultiEffect { + source: img + anchors.fill: img + blur: 1.0 + brightness: 0.3 + blurEnabled: true + } + + onPressed: () => { + Launcher.Controller.isOpen = true; + } +} diff --git a/other/quickshell/bar/MouseAreaItem.qml b/other/quickshell/bar/MouseAreaItem.qml new file mode 100644 index 0000000..bc38391 --- /dev/null +++ b/other/quickshell/bar/MouseAreaItem.qml @@ -0,0 +1,43 @@ +import QtQuick + +Item { + id: root + + 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 + + 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/other/quickshell/bar/Power.qml b/other/quickshell/bar/Power.qml new file mode 100644 index 0000000..95ea880 --- /dev/null +++ b/other/quickshell/bar/Power.qml @@ -0,0 +1,54 @@ +import QtQuick +import QtQuick.Effects +import Quickshell.Services.UPower +import Qt5Compat.GraphicalEffects + +Item { + 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 bool isLaptop: UPowerDevice.isLaptopBattery + width: 32 + height: 32 + + Image { + id: mask + source: "../assets/battery-mask.png" + smooth: false + anchors.fill: parent + visible: false + } + + Rectangle { + id: progressBar + anchors.fill: parent + color: "black" + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: root.isLaptop ? root.percentage * parent.height : parent.height + color: root.isLaptop ? (root.isLow ? "red" : "#EEA939") : "#EEA939" + } + visible: false + } + OpacityMask { + id: pBarMask + anchors.fill: progressBar + source: progressBar + maskSource: mask + } + MultiEffect { + source: pBarMask + anchors.fill: pBarMask + brightness: root.isCharging ? 0.3 : 0 + blur: 1.0 + blurEnabled: root.isCharging + } +} diff --git a/other/quickshell/bar/SlideView.qml b/other/quickshell/bar/SlideView.qml new file mode 100644 index 0000000..f6cb951 --- /dev/null +++ b/other/quickshell/bar/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/other/quickshell/bar/SlideViewItem.qml b/other/quickshell/bar/SlideViewItem.qml new file mode 100644 index 0000000..04764bb --- /dev/null +++ b/other/quickshell/bar/SlideViewItem.qml @@ -0,0 +1,49 @@ +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/other/quickshell/bar/Theme.qml b/other/quickshell/bar/Theme.qml new file mode 100644 index 0000000..891bf0f --- /dev/null +++ b/other/quickshell/bar/Theme.qml @@ -0,0 +1,31 @@ +import QtQuick +import Quickshell +import QtQuick.Layouts +import "../" + +MouseArea { + id: root + + implicitWidth: 26 + implicitHeight: 26 + + hoverEnabled: true + acceptedButtons: Qt.LeftButton + + Image { + anchors.fill: parent + source: `../assets/${Globals.isDarkTheme ? "sun" : "moon"}.png` + smooth: false + + scale: root.containsMouse ? 0.9 : 1 + Behavior on scale { + NumberAnimation { + duration: 70 + } + } + } + + onPressed: () => { + Globals.isDarkTheme = !Globals.isDarkTheme; + } +} diff --git a/other/quickshell/bar/Time.qml b/other/quickshell/bar/Time.qml new file mode 100644 index 0000000..dd14c58 --- /dev/null +++ b/other/quickshell/bar/Time.qml @@ -0,0 +1,28 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Io +import ".." + +Text { + id: timetxt + font.family: "BigBlueTermPlusNerdFont" + font.pointSize: 16 + font.bold: true + color: "white" + opacity: 0.5 + + Process { + id: dateProc + command: ["date", "+%H⯁%M"] + running: true + stdout: SplitParser { + onRead: data => timetxt.text = data + } + } + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: dateProc.running = true + } +} diff --git a/other/quickshell/bar/Tooltip.qml b/other/quickshell/bar/Tooltip.qml new file mode 100644 index 0000000..23a9b23 --- /dev/null +++ b/other/quickshell/bar/Tooltip.qml @@ -0,0 +1,239 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Hyprland +import "root:/" + +Scope { + id: root + required property var bar + + property TooltipItem activeTooltip: null + property TooltipItem activeMenu: null + + 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) { + activeItem.parent = tooltipItem; + } + } + + if (lastActiveItem != null && lastActiveItem != activeItem) { + if (activeItem != null) + lastActiveItem.targetVisible = false; + else if (root.hangTime == 0) + doLastHide(); + else + hangTimer.start(); + } + + if (activeItem != null) + lastActiveItem = activeItem; + } + + function setItem(item: TooltipItem) { + if (item.isMenu) { + activeMenu = item; + } else { + activeTooltip = item; + } + } + + function removeItem(item: TooltipItem) { + if (item.isMenu && activeMenu == item) { + activeMenu = null; + } else if (!item.isMenu && activeTooltip == item) { + activeTooltip = null; + } + } + + function doLastHide() { + lastActiveItem.targetVisible = false; + } + + function onHidden(item: TooltipItem) { + if (item == lastActiveItem) { + lastActiveItem = null; + } + } + + Timer { + id: hangTimer + interval: root.hangTime + onTriggered: root.doLastHide() + } + + property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0 + Behavior on scaleMul { + SmoothedAnimation { + velocity: 5 + } + } + + LazyLoader { + id: popupLoader + activeAsync: root.shownItem != null + + PopupWindow { + id: popup + + anchor { + window: root.bar + rect.y: root.bar.tooltipYOffset + adjustment: PopupAdjustment.None + } + + HyprlandWindow.opacity: root.scaleMul + + HyprlandWindow.visibleMask: Region { + id: visibleMask + item: tooltipItem + } + + Connections { + target: root + + function onScaleMulChanged() { + visibleMask.changed(); + } + } + + //height: bar.height + //implicitWidth: Math.max(700, tooltipItem.largestAnimWidth) // max due to qtwayland glitches + //implicitHeight: Math.max(700, tooltipItem.largestAnimHeight) // max due to qtwayland glitches + visible: true + color: "transparent" + //color: "#20ff0000" + + mask: Region { + item: (shownItem?.hoverable ?? false) ? tooltipItem : null + } + + HyprlandFocusGrab { + active: activeItem?.isMenu ?? false + windows: [popup, bar, ...(activeItem?.grabWindows ?? [])] + onActiveChanged: { + if (!active && activeItem?.isMenu) { + activeMenu.close(); + } + } + } + + Item { + id: tooltipItem + Component.onCompleted: { + root.tooltipItem = 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 + Rectangle { + anchors.fill: parent + color: "#3B253F" + } + + readonly property var targetWidth: shownItem?.implicitWidth ?? 0 + readonly property var targetHeight: shownItem?.implicitHeight ?? 0 + + readonly property real targetY: { + if (shownItem == null) + return 0; + const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y; + return bar.boundedY(target, shownItem.implicitHeight / 2); + } + readonly property real targetX: { + if (root.shownItem == null) + return 0; + const target = root.bar.contentItem.mapFromItem(root.shownItem.owner, 0, root.shownItem.targetRelativeX).x; + return root.bar.boundedX(target, root.shownItem.implicitWidth / 2); + } + + property var w: -1 + implicitWidth: Math.max(1, w) + + property var y1: -1 + property var y2: -1 + + y: y1 - popup.anchor.rect.y + height: y2 - y1 + + readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running + + onAnyAnimsRunningChanged: { + if (!anyAnimsRunning) { + largestAnimWidth = targetWidth; + //highestAnimY = y1; + //lowestAnimY = y2; + } + } + + SmoothedAnimation on y1 { + id: y1Anim + to: tooltipItem.targetY - tooltipItem.targetHeight / 2 + onToChanged: { + if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) { + stop(); + tooltipItem.y1 = to; + } else { + velocity = (Math.max(tooltipItem.y1, to) - Math.min(tooltipItem.y1, to)) * 5; + restart(); + } + } + } + + SmoothedAnimation on y2 { + id: y2Anim + to: tooltipItem.targetY + tooltipItem.targetHeight / 2 + onToChanged: { + if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) { + stop(); + tooltipItem.y2 = to; + } else { + velocity = (Math.max(tooltipItem.y2, to) - Math.min(tooltipItem.y2, to)) * 5; + restart(); + } + } + } + + SmoothedAnimation on w { + id: widthAnim + to: tooltipItem.targetWidth + onToChanged: { + if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) { + stop(); + tooltipItem.w = to; + } else { + velocity = (Math.max(tooltipItem.width, to) - Math.min(tooltipItem.width, to)) * 5; + restart(); + } + } + } + } + } + } +} diff --git a/other/quickshell/bar/TooltipBackup.qml b/other/quickshell/bar/TooltipBackup.qml new file mode 100644 index 0000000..4a5d3cd --- /dev/null +++ b/other/quickshell/bar/TooltipBackup.qml @@ -0,0 +1,274 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Hyprland +import "root:/" + +Scope { + id: root + required property var bar + + property TooltipItem activeTooltip: null + property TooltipItem activeMenu: null + + 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) { + activeItem.parent = tooltipItem; + } + } + + if (lastActiveItem != null && lastActiveItem != activeItem) { + if (activeItem != null) + lastActiveItem.targetVisible = false; + else if (root.hangTime == 0) + doLastHide(); + else + hangTimer.start(); + } + + if (activeItem != null) + lastActiveItem = activeItem; + } + + function setItem(item: TooltipItem) { + if (item.isMenu) { + activeMenu = item; + } else { + activeTooltip = item; + } + } + + function removeItem(item: TooltipItem) { + if (item.isMenu && activeMenu == item) { + activeMenu = null; + } else if (!item.isMenu && activeTooltip == item) { + activeTooltip = null; + } + } + + function doLastHide() { + lastActiveItem.targetVisible = false; + } + + function onHidden(item: TooltipItem) { + if (item == lastActiveItem) { + lastActiveItem = null; + } + } + + Timer { + id: hangTimer + interval: root.hangTime + onTriggered: doLastHide() + } + + property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0 + Behavior on scaleMul { + SmoothedAnimation { + velocity: 5 + } + } + + LazyLoader { + id: popupLoader + activeAsync: shownItem != null + + PopupWindow { + id: popup + + anchor { + window: bar + rect.x: tooltipItem.highestAnimX + rect.y: bar.tooltipYOffset + adjustment: PopupAdjustment.None + } + + HyprlandWindow.opacity: root.scaleMul + + HyprlandWindow.visibleMask: Region { + id: visibleMask + item: tooltipItem + } + + Connections { + target: root + + function onScaleMulChanged() { + visibleMask.changed(); + } + } + + //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: "#20ff0000" + + mask: Region { + item: (shownItem?.hoverable ?? false) ? tooltipItem : null + } + + HyprlandFocusGrab { + active: root.activeItem?.isMenu ?? false + windows: [popup, root.bar, ...(root.activeItem?.grabWindows ?? [])] + onActiveChanged: { + if (!active && root.activeItem?.isMenu) { + activeMenu.close(); + } + } + } + + /*Rectangle { + color: "#10ff0000" + //y: tooltipItem.highestAnimY + height: tooltipItem.lowestAnimY - tooltipItem.highestAnimY + width: parent.width + } + + Rectangle { + color: "#1000ff00" + //y: tooltipItem.highestAnimY + height: popup.height + width: parent.width + }*/ + + Item { + id: tooltipItem + Component.onCompleted: { + root.tooltipItem = this; + if (root.shownItem) { + root.shownItem.parent = this; + } + + //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 + Rectangle { + anchors.fill: parent + color: "red" + } + + 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; + } + } + + onTargetYChanged: updateYBounds() + onTargetHeightChanged: updateYBounds() + function updateYBounds() { + if (targetY - targetHeight / 2 < highestAnimY) + //highestAnimY = targetY - targetHeight / 2 + {} + + if (targetY + targetHeight / 2 > lowestAnimY) + //lowestAnimY = targetY + targetHeight / 2 + {} + } + + readonly property real targetY: { + if (shownItem == null) + return 0; + const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y; + return bar.boundedY(target, shownItem.implicitHeight / 2); + } + + property var w: -1 + width: Math.max(1, w) + + property var y1: -1 + property var y2: -1 + + y: y1 - popup.anchor.rect.y + height: y2 - y1 + + readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running + + onAnyAnimsRunningChanged: { + if (!anyAnimsRunning) { + largestAnimWidth = targetWidth; + //highestAnimY = y1; + //lowestAnimY = y2; + } + } + + SmoothedAnimation on y1 { + id: y1Anim + to: tooltipItem.targetY - tooltipItem.targetHeight / 2 + onToChanged: { + if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) { + stop(); + tooltipItem.y1 = to; + } else { + velocity = (Math.max(tooltipItem.y1, to) - Math.min(tooltipItem.y1, to)) * 5; + restart(); + } + } + } + + SmoothedAnimation on y2 { + id: y2Anim + to: tooltipItem.targetY + tooltipItem.targetHeight / 2 + onToChanged: { + if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) { + stop(); + tooltipItem.y2 = to; + } else { + velocity = (Math.max(tooltipItem.y2, to) - Math.min(tooltipItem.y2, to)) * 5; + restart(); + } + } + } + + SmoothedAnimation on w { + id: widthAnim + to: tooltipItem.targetWidth + onToChanged: { + if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) { + stop(); + tooltipItem.w = to; + } else { + velocity = (Math.max(tooltipItem.width, to) - Math.min(tooltipItem.width, to)) * 5; + restart(); + } + } + } + } + } + } +} diff --git a/other/quickshell/bar/TooltipItem.qml b/other/quickshell/bar/TooltipItem.qml new file mode 100644 index 0000000..10a19f4 --- /dev/null +++ b/other/quickshell/bar/TooltipItem.qml @@ -0,0 +1,91 @@ +import QtQuick +import Quickshell + +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 + property bool preloadBackground: root.visible + + property real targetRelativeX: owner.width / 2 + property real hangTime: isMenu ? 0 : 200 + + signal close + + readonly property alias contentItem: contentItem + default property alias data: contentItem.data + + property Component backgroundComponent: null + + onShowChanged: { + if (show) + tooltip.setItem(this); + else + tooltip.removeItem(this); + } + + property bool targetVisible: false + property real targetOpacity: 0 + opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul)) + + Behavior on targetOpacity { + id: opacityAnimation + SmoothedAnimation { + velocity: 5 + } + } + + function snapOpacity(opacity: real) { + opacityAnimation.enabled = false; + targetOpacity = opacity; + opacityAnimation.enabled = true; + } + + onTargetVisibleChanged: { + if (targetVisible) { + visible = true; + targetOpacity = 1; + } else { + close(); + targetOpacity = 0; + } + } + + onTargetOpacityChanged: { + if (!targetVisible && targetOpacity == 0) { + visible = false; + this.parent = null; + if (tooltip) + tooltip.onHidden(this); + } + } + + anchors.fill: parent + visible: false + //clip: true + implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin + implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin + + readonly property Item item: contentItem + + Loader { + anchors.fill: parent + active: root.backgroundComponent && (root.visible || root.preloadBackground) + asynchronous: !root.visible && root.preloadBackground + sourceComponent: backgroundComponent + } + + Item { + id: contentItem + anchors.fill: parent + anchors.margins: 5 + + implicitHeight: children[0].implicitHeight + implicitWidth: children[0].implicitWidth + } +} diff --git a/other/quickshell/bar/TooltipItemBackup.qml b/other/quickshell/bar/TooltipItemBackup.qml new file mode 100644 index 0000000..8e6af19 --- /dev/null +++ b/other/quickshell/bar/TooltipItemBackup.qml @@ -0,0 +1,92 @@ +import QtQuick +import Quickshell + +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 + property bool preloadBackground: root.visible + + property real targetRelativeY: owner.height / 2 + property real hangTime: isMenu ? 0 : 200 + + signal close + + readonly property alias contentItem: contentItem + default property alias data: contentItem.data + + property Component backgroundComponent: null + + onShowChanged: { + console.log(this); + if (show) + tooltip.setItem(this); + else + tooltip.removeItem(this); + } + + property bool targetVisible: false + property real targetOpacity: 0 + opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul)) + + Behavior on targetOpacity { + id: opacityAnimation + SmoothedAnimation { + velocity: 5 + } + } + + function snapOpacity(opacity: real) { + opacityAnimation.enabled = false; + targetOpacity = opacity; + opacityAnimation.enabled = true; + } + + onTargetVisibleChanged: { + if (targetVisible) { + visible = true; + targetOpacity = 1; + } else { + close(); + targetOpacity = 0; + } + } + + onTargetOpacityChanged: { + if (!targetVisible && targetOpacity == 0) { + visible = false; + this.parent = null; + if (tooltip) + tooltip.onHidden(this); + } + } + + anchors.fill: parent + visible: false + //clip: true + implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin + implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin + + readonly property Item item: contentItem + + Loader { + anchors.fill: parent + active: root.backgroundComponent && (root.visible || root.preloadBackground) + asynchronous: !root.visible && root.preloadBackground + sourceComponent: root.backgroundComponent + } + + Item { + id: contentItem + anchors.fill: parent + anchors.margins: 5 + + implicitHeight: children[0].implicitHeight + implicitWidth: children[0].implicitWidth + } +} diff --git a/other/quickshell/bar/Volume.qml b/other/quickshell/bar/Volume.qml new file mode 100644 index 0000000..aa85f1a --- /dev/null +++ b/other/quickshell/bar/Volume.qml @@ -0,0 +1,133 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Services.Pipewire +import Quickshell.Widgets +import "mpris" + +MouseArea { + id: root + + required property var bar + + property var node: Pipewire.defaultAudioSink + PwObjectTracker { + objects: [root.node] + } + + implicitWidth: 26 + implicitHeight: 26 + + hoverEnabled: true + acceptedButtons: Qt.LeftButton + + //enabled: MprisController.canChangeVolume + + opacity: enabled ? 1 : 0.5 + + Image { + anchors.fill: parent + source: `../assets/${root.node.audio.muted ? "noaudio" : "audio"}.png` + smooth: false + + scale: popupLoader.active ? 0.9 : 1 + Behavior on scale { + NumberAnimation { + duration: 70 + } + } + } + + onClicked: node.audio.muted = !node.audio.muted + + LazyLoader { + id: popupLoader + + property bool popupContainsMouse + active: root.enabled && (root.containsMouse || popupContainsMouse) + Behavior on active { + NumberAnimation { + duration: 100 + } + } + + PopupWindow { + id: popup + + visible: true + anchor.window: root.bar + width: 200 + height: 32 + color: "#9B5B36" + Connections { + target: root + function onXChanged() { + popup.updatePos(); + } + } + Component.onCompleted: updatePos() + function updatePos() { + const pos = root.mapToGlobal(root.width / 2 - popup.width / 2, root.height); + popup.anchor.rect.x = pos.x; + popup.anchor.rect.y = pos.y + 5; + } + + Slider { + id: slider + + anchors { + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + } + anchors.margins: 5 + from: 0 + to: 1 + + value: root.node.audio.volume + onValueChanged: root.node.audio.volume = value + + background: Rectangle { + x: slider.leftPadding + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + implicitHeight: 16 + width: slider.availableWidth + height: implicitHeight + color: "#824524" + opacity: node.audio.muted ? 0.5 : 1 + + Rectangle { + anchors.margins: 5 + x: anchors.leftMargin + y: anchors.topMargin + width: slider.visualPosition * (parent.width - anchors.leftMargin - anchors.rightMargin) + height: parent.height - anchors.topMargin - anchors.bottomMargin + color: "#8D804B" + } + } + + handle: Rectangle { + x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + implicitWidth: 12 + implicitHeight: 12 + rotation: 45 + color: "#8D804B" + } + } + + MouseArea { + id: popupMouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + onEntered: popupLoader.popupContainsMouse = true + onExited: popupLoader.popupContainsMouse = false + } + } + } +} diff --git a/other/quickshell/bar/Workspaces.qml b/other/quickshell/bar/Workspaces.qml new file mode 100644 index 0000000..c14f0a8 --- /dev/null +++ b/other/quickshell/bar/Workspaces.qml @@ -0,0 +1,122 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell.Hyprland + +MouseArea { + id: root + + required property var bar + property int wsBaseIndex: 1 + property int wsCount: 7 + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen) + property int currentIndex: 0 + property int existsCount: 0 + + signal workspaceAdded(workspace: HyprlandWorkspace) + + implicitWidth: row.implicitWidth + implicitHeight: row.implicitHeight + + acceptedButtons: Qt.NoButton + + onWheel: e => { + e.accepted = true; + const step = -Math.sign(e.angleDelta.y); + const targetWs = currentIndex + step; + + if (targetWs >= wsBaseIndex && targetWs < wsBaseIndex + wsCount) { + Hyprland.dispatch(`workspace ${targetWs}`); + } + } + + RowLayout { + id: row + + anchors.fill: parent + spacing: 8 + + Repeater { + model: root.wsCount + + MouseArea { + id: wsItem + + required property int index + property int wsIndex: root.wsBaseIndex + index + property HyprlandWorkspace workspace: null + property bool exists: workspace != null + property bool active: (root.monitor?.activeWorkspace ?? false) && root.monitor.activeWorkspace == workspace + + implicitWidth: 26 + implicitHeight: 26 + + acceptedButtons: Qt.LeftButton + + onPressed: Hyprland.dispatch(`workspace ${wsIndex}`) + + onExistsChanged: { + root.existsCount += exists ? 1 : -1; + } + + onActiveChanged: { + if (active) + root.currentIndex = wsIndex; + } + + Connections { + target: root + + function onWorkspaceAdded(workspace: HyprlandWorkspace) { + if (workspace.id == wsItem.wsIndex) { + wsItem.workspace = workspace; + } + } + } + + Rectangle { + id: fill + anchors.fill: parent + anchors.margins: 2 + radius: width / 2 + color: wsItem.exists ? "#C0DB4C" : "#59622D" + } + Image { + anchors.fill: parent + smooth: false + source: "../assets/workspace.png" + } + MultiEffect { + source: fill + anchors.fill: fill + blur: 1.0 + brightness: 0.3 + blurEnabled: true + visible: wsItem.active + opacity: wsItem.active ? 1.0 : 0 + Behavior on opacity { + NumberAnimation { + duration: 100 + } + } + } + } + } + } + + Connections { + target: Hyprland.workspaces + + function onObjectInsertedPost(workspace) { + root.workspaceAdded(workspace); + } + } + + Component.onCompleted: { + Hyprland.workspaces.values.forEach(workspace => { + root.workspaceAdded(workspace); + }); + } +} diff --git a/other/quickshell/bar/mpris/MprisController.qml b/other/quickshell/bar/mpris/MprisController.qml new file mode 100644 index 0000000..04f19cb --- /dev/null +++ b/other/quickshell/bar/mpris/MprisController.qml @@ -0,0 +1,165 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQml.Models +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Mpris + +Singleton { + id: root + property MprisPlayer trackedPlayer: null + property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null + signal trackChanged(reverse: bool) + + property bool __reverse: false + + property var activeTrack + + Instantiator { + model: Mpris.players + + Connections { + required property MprisPlayer modelData + target: modelData + + Component.onCompleted: { + if (root.trackedPlayer == null || modelData.isPlaying) { + root.trackedPlayer = modelData; + } + } + + Component.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 onPostTrackChanged() { + root.updateTrack(); + } + + function onTrackArtUrlChanged() { + //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; + } + } + } + + onActivePlayerChanged: this.updateTrack() + + function updateTrack() { + //console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`) + this.activeTrack = { + uniqueId: this.activePlayer?.uniqueId ?? 0, + artUrl: this.activePlayer?.trackArtUrl ?? "", + title: this.activePlayer?.trackTitle || "Unknown Title", + artist: this.activePlayer?.trackArtist || "Unknown Artist", + album: this.activePlayer?.trackAlbum || "Unknown Album" + }; + + this.trackChanged(__reverse); + this.__reverse = false; + } + + property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying + property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false + function togglePlaying() { + if (this.canTogglePlaying) + this.activePlayer.togglePlaying(); + } + + property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false + function previous() { + if (this.canGoPrevious) { + this.__reverse = true; + this.activePlayer.previous(); + } + } + + property bool canGoNext: this.activePlayer?.canGoNext ?? false + function next() { + if (this.canGoNext) { + this.__reverse = false; + this.activePlayer.next(); + } + } + + property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl + + property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl + property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None + function setLoopState(loopState: var) { + if (this.loopSupported) { + this.activePlayer.loopState = loopState; + } + } + + property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl + property bool hasShuffle: this.activePlayer?.shuffle ?? false + function setShuffle(shuffle: bool) { + if (this.shuffleSupported) { + this.activePlayer.shuffle = shuffle; + } + } + + function setActivePlayer(player: MprisPlayer) { + const targetPlayer = player ?? Mpris.players[0]; + console.log(`setactive: ${targetPlayer} from ${activePlayer}`); + + if (targetPlayer && this.activePlayer) { + this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer); + } else { + // always animate forward if going to null + this.__reverse = false; + } + + this.trackedPlayer = targetPlayer; + } + + IpcHandler { + target: "mpris" + + function pauseAll(): void { + for (const player of Mpris.players.values) { + if (player.canPause) + player.pause(); + } + } + + function playPause(): void { + root.togglePlaying(); + } + function previous(): void { + root.previous(); + } + function next(): void { + root.next(); + } + } +} diff --git a/other/quickshell/bar/mpris/Players.qml b/other/quickshell/bar/mpris/Players.qml new file mode 100644 index 0000000..adf9e72 --- /dev/null +++ b/other/quickshell/bar/mpris/Players.qml @@ -0,0 +1,151 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Hyprland +import Quickshell.Services.Mpris +import "../.." +import ".." + +MouseArea { + id: root + + required property var window + required property var bar + + readonly property var activePlayer: MprisController.activePlayer + + property var isPopupOpen: false + + implicitWidth: column.implicitWidth + 10 + Layout.fillHeight: true + + acceptedButtons: Qt.RightButton + onClicked: isPopupOpen = !isPopupOpen + + Item { + id: widget + anchors.fill: parent + + scale: root.isPopupOpen ? 0.92 : 1 + Behavior on scale { + NumberAnimation { + duration: 70 + } + } + + implicitHeight: column.implicitHeight + 10 + + RowLayout { + id: column + + anchors { + fill: parent + } + + ClickableIcon { + acceptedButtons: Qt.LeftButton + implicitWidth: 20 + implicitHeight: 20 + image: "../../assets/fastforward.png" + mirror: true + hoverEnabled: true + enabled: MprisController.canGoPrevious + onClicked: MprisController.previous() + scale: containsMouse ? 0.9 : 1 + Behavior on scale { + NumberAnimation { + duration: 70 + } + } + } + + ClickableIcon { + acceptedButtons: Qt.LeftButton + implicitWidth: 26 + implicitHeight: 26 + Layout.leftMargin: -4 + Layout.rightMargin: -4 + image: `../../assets/${MprisController.isPlaying ? "pause" : "play"}.png` + hoverEnabled: true + enabled: MprisController.canTogglePlaying + onClicked: MprisController.togglePlaying() + } + + ClickableIcon { + acceptedButtons: Qt.LeftButton + implicitWidth: 20 + implicitHeight: 20 + image: "../../assets/fastforward.png" + hoverEnabled: true + enabled: MprisController.canGoNext + onClicked: MprisController.next() + scale: containsMouse ? 0.9 : 1 + Behavior on scale { + NumberAnimation { + duration: 70 + } + } + } + } + + Connections { + target: root + function onIsPopupOpenChanged() { + root.window.focusable = root.isPopupOpen; + } + } + LazyLoader { + id: popupLoader + active: root.isPopupOpen + // Leave time for exit anim + Behavior on active { + enabled: popupLoader.active + NumberAnimation { + duration: 1000 + } + } + + PlayersPopup { + id: popup + visible: true + isOpen: root.isPopupOpen + + anchor.window: root.window + anchor.rect.x: root.window.width + anchor.rect.y: 0 + + HyprlandFocusGrab { + active: root.isPopupOpen + windows: [popup] + } + contentItem { + focus: true + Keys.onEscapePressed: root.isPopupOpen = false + } + } + } + } + + component CenteredText: Item { + Layout.fillWidth: true + + property alias text: label.text + property alias font: label.font + + Label { + id: label + visible: text != "" + anchors.centerIn: parent + elide: Text.ElideRight + width: Math.min(parent.width - 20, implicitWidth) + font.family: "BigBlueTermPlusNerdFont" + color: "white" + } + } +} diff --git a/other/quickshell/bar/mpris/PlayersPopup.qml b/other/quickshell/bar/mpris/PlayersPopup.qml new file mode 100644 index 0000000..33fc82e --- /dev/null +++ b/other/quickshell/bar/mpris/PlayersPopup.qml @@ -0,0 +1,529 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Hyprland +import Quickshell.Services.Mpris +import "../.." +import "../../components" +import ".." + +PopupWindow { + id: root + + property bool isOpen: false + + implicitWidth: 700 + //implicitHeight: popupContent.implicitHeight + popupContent.anchors.margins * 2 + implicitHeight: 800 + + mask: Region { + item: wrapper + } + + color: "transparent" + + property Scope positionInfo: Scope { + id: positionInfo + + property int position: Math.floor(MprisController.activePlayer.position) + property int length: Math.floor(MprisController.activePlayer.length) + + FrameAnimation { + id: posTracker + running: MprisController.activePlayer.isPlaying + onTriggered: MprisController.activePlayer.positionChanged() + } + + function timeStr(time: int): string { + const seconds = time % 60; + const minutes = Math.floor(time / 60); + + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + } + } + + Rope { + segmentCount: 4 + segmentLen: 22 + start: Qt.vector2d(250, -5) + end: Qt.vector2d(wrapper.x + 49, wrapper.y) + } + Rope { + segmentCount: 4 + segmentLen: 22 + start: Qt.vector2d(wrapper.width + 200 - 50, -5) + end: Qt.vector2d(wrapper.width + wrapper.x - 49, wrapper.y) + } + + Rectangle { + id: wrapper + implicitWidth: 500 + implicitHeight: popupContent.implicitHeight + popupContent.anchors.margins * 2 + color: "#824524" + + x: 200 + y: -wrapper.height + readonly property var targetX: 200 + readonly property var targetY: root.isOpen ? 70 : -wrapper.height - 50 + property var velocityX: 0 + property var velocityY: 0 + + FrameAnimation { + running: true + function dampingVelocity(currentVelocity, delta) { + const spring = 8.0; + const damping = 0.3; + const springForce = spring * delta; + const dampingForce = -damping * currentVelocity; + return currentVelocity + (springForce + dampingForce); + } + onTriggered: { + const deltaX = wrapper.targetX - wrapper.x; + const deltaY = wrapper.targetY - wrapper.y; + if (Math.abs(deltaX) > 0.1 || Math.abs(deltaY) > 0.1) { + wrapper.velocityX = dampingVelocity(wrapper.velocityX, deltaX); + wrapper.velocityY = dampingVelocity(wrapper.velocityY, deltaY); + wrapper.x += wrapper.velocityX * frameTime; + wrapper.y += wrapper.velocityY * frameTime; + } + } + } + MouseArea { + property var prevMouseX: 0 + property var prevMouseY: 0 + anchors.fill: parent + onPressed: e => { + prevMouseX = e.x; + prevMouseY = e.y; + } + onPositionChanged: e => { + wrapper.x += (e.x - prevMouseX) * 0.3; + wrapper.y += (e.y - prevMouseY) * 0.3; + prevMouseX = e.x; + prevMouseY = e.y; + } + } + + Item { + anchors.fill: parent + anchors.margins: 8 + clip: true + + Image { + scale: 10 + anchors.fill: parent + source: "../../assets/wood.png" + smooth: false + fillMode: Image.Tile + opacity: 0.4 + } + } + + ColumnLayout { + id: popupContent + + anchors.fill: parent + anchors.margins: 8 + + Connections { + target: MprisController + + function onTrackChanged(reverse: bool) { + trackStack.updateTrack(reverse, false); + } + } + + Item { + id: playerSelectorContainment + Layout.fillWidth: true + implicitHeight: playerSelector.implicitHeight + implicitWidth: playerSelector.implicitWidth + + Rectangle { + anchors.centerIn: parent + implicitWidth: 50 + implicitHeight: 50 + color: "#824524" + } + + RowLayout { + id: playerSelector + property Item selectedPlayerDisplay: Mpris.players[0] + x: parent.width / 2 - (selectedPlayerDisplay ? selectedPlayerDisplay.x + selectedPlayerDisplay.width / 2 : 0) + anchors.verticalCenter: parent.verticalCenter + + Behavior on x { + NumberAnimation { + duration: 400 + easing.type: Easing.OutExpo + } + } + + Repeater { + model: Mpris.players + + MouseArea { + required property MprisPlayer modelData + readonly property bool selected: modelData == MprisController.activePlayer + onSelectedChanged: () => { + if (selected) { + playerSelector.selectedPlayerDisplay = this; + } + } + + implicitWidth: childrenRect.width + implicitHeight: childrenRect.height + + onClicked: MprisController.setActivePlayer(modelData) + + Item { + width: 50 + height: 50 + + Image { + anchors.fill: parent + anchors.margins: 0 + source: { + // So janky xD + const identity = modelData.identity.toLowerCase(); + + if (identity.includes("chrome") || identity.includes("chromium")) { + return Quickshell.iconPath("google-chrome") || Quickshell.iconPath("chromium"); + } + if (identity.includes("firefox")) { + return Quickshell.iconPath("firefox"); + } + if (identity.includes("spotify")) { + return Quickshell.iconPath("spotify"); + } + if (modelData.identity == "Brave") { + return "/opt/brave-bin/product_logo_64.png"; + } + const entry = DesktopEntries.byId(modelData.desktopEntry); + if (entry && entry.icon) { + return Quickshell.iconPath(entry.icon); + } + + return "../../assets/mpd.png"; + } + smooth: false + sourceSize.width: 50 + sourceSize.height: 50 + cache: false + } + } + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.bottomMargin: 16 + Label { + anchors.centerIn: parent + text: MprisController.activePlayer.identity + font.family: "BigBlueTermPlusNerdFont" + color: "white" + } + } + + SlideView { + id: trackStack + Layout.fillWidth: true + implicitHeight: 100 + clip: animating || (lastFlicked?.contentX ?? 0) != 0 + + property Flickable lastFlicked + property bool reverse: false + + Component.onCompleted: updateTrack(false, true) + + function updateTrack(reverse: bool, immediate: bool) { + this.reverse = reverse; + this.replace(trackComponent, { + track: MprisController.activeTrack + }, immediate); + } + + property var trackComponent: Component { + Flickable { + id: flickable + required property var track + readonly property bool svReady: img.status === Image.Ready + contentWidth: width + 1 + onDragStarted: trackStack.lastFlicked = this + onDragEnded: { + if (Math.abs(contentX) > 75) { + if (contentX < 0) + MprisController.previous(); + else if (contentX > 0) + MprisController.next(); + } + } + + RowLayout { + anchors.fill: parent + spacing: 8 + + Rectangle { + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + implicitHeight: 128 + implicitWidth: 128 + color: "transparent" + + Image { + id: img + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + visible: flickable.track.artUrl != "" + source: flickable.track.artUrl + cache: false + asynchronous: true + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + + Label { + text: flickable.track.title + font.pointSize: albumLabel.font.pointSize + 1 + font.family: "BigBlueTermPlusNerdFont" + color: "white" + elide: Text.ElideRight + Layout.maximumWidth: Math.min(300, implicitWidth) + } + + Label { + id: albumLabel + text: flickable.track.album + opacity: 0.8 + font.family: "BigBlueTermPlusNerdFont" + color: "white" + Layout.maximumWidth: Math.min(300, implicitWidth) + } + + Label { + text: flickable.track.artist + opacity: 0.8 + font.family: "BigBlueTermPlusNerdFont" + color: "white" + Layout.maximumWidth: Math.min(300, implicitWidth) + } + } + } + } + } + + readonly property real fromPos: trackStack.width * (trackStack.reverse ? -1 : 1) + + enterTransition: PropertyAnimation { + property: "x" + from: trackStack.fromPos + to: 0 + duration: 350 + easing.type: Easing.OutExpo + } + exitTransition: PropertyAnimation { + property: "x" + to: target.x - trackStack.fromPos + duration: 350 + easing.type: Easing.OutExpo + } + } + + Item { + Layout.fillWidth: true + implicitHeight: controlsRow.implicitHeight + + RowLayout { + id: controlsRow + anchors.centerIn: parent + + ClickableIcon { + Layout.leftMargin: 2 + Layout.rightMargin: 2 + implicitWidth: 24 + implicitHeight: 24 + enabled: MprisController.loopSupported + image: { + switch (MprisController.loopState) { + case MprisLoopState.None: + return "../../assets/repeat-off.png"; + case MprisLoopState.Playlist: + return "../../assets/repeat.png"; + case MprisLoopState.Track: + return "../../assets/repeat-once.png"; + } + } + onClicked: { + let target = MprisLoopState.None; + switch (MprisController.loopState) { + case MprisLoopState.None: + target = MprisLoopState.Playlist; + break; + case MprisLoopState.Playlist: + target = MprisLoopState.Track; + break; + case MprisLoopState.Track: + target = MprisLoopState.None; + break; + } + MprisController.setLoopState(target); + } + } + + ClickableIcon { + Layout.leftMargin: 2 + Layout.rightMargin: 2 + implicitWidth: 32 + implicitHeight: 32 + enabled: MprisController.canGoPrevious + image: "../../assets/fastforward.png" + mirror: true + onClicked: MprisController.previous() + } + + ClickableIcon { + implicitWidth: 42 + implicitHeight: 42 + enabled: MprisController.canTogglePlaying + image: `../../assets/${MprisController.isPlaying ? "pause" : "play"}.png` + onClicked: MprisController.togglePlaying() + } + + ClickableIcon { + Layout.leftMargin: 2 + Layout.rightMargin: 2 + implicitWidth: 32 + implicitHeight: 32 + enabled: MprisController.canGoNext + image: "../../assets/fastforward.png" + onClicked: MprisController.next() + } + + ClickableIcon { + Layout.leftMargin: 2 + Layout.rightMargin: 2 + implicitWidth: 24 + implicitHeight: 24 + enabled: MprisController.shuffleSupported + image: `../../assets/${MprisController.hasShuffle ? "shuffle" : "noshuffle"}.png` + onClicked: MprisController.setShuffle(!MprisController.hasShuffle) + } + } + } + + RowLayout { + Layout.margins: 5 + + Label { + Layout.preferredWidth: lengthLabel.implicitWidth + text: positionInfo.timeStr(positionInfo.position) + font.family: "BigBlueTermPlusNerdFont" + color: "white" + } + + Slider { + 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 = MprisController.activePlayer.position / MprisController.activePlayer.length; + const bpos = boundAnimStart; + return (ppos * boundAnimFactor) + (bpos * (1.0 - boundAnimFactor)); + } + + Layout.fillWidth: true + enabled: MprisController.activePlayer.canSeek + from: 0 + to: 1 + + background: Rectangle { + x: slider.leftPadding + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + implicitHeight: 24 + width: slider.availableWidth + height: implicitHeight + color: "#824524" + + Rectangle { + anchors.margins: 8 + x: anchors.leftMargin + y: anchors.topMargin + width: slider.visualPosition * (parent.width - anchors.leftMargin - anchors.rightMargin) + height: parent.height - anchors.topMargin - anchors.bottomMargin + color: "#8D804B" + } + } + + handle: Rectangle { + x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + implicitWidth: 16 + implicitHeight: 16 + rotation: 45 + color: "#8D804B" + } + + Connections { + target: MprisController.activePlayer + + function onPositionChanged() { + slider.lastPosition = MprisController.activePlayer.position; + slider.lastLength = MprisController.activePlayer.length; + } + } + + onPressedChanged: () => { + if (!pressed) + MprisController.activePlayer.position = value * MprisController.activePlayer.length; + bindSlider = !pressed; + } + + Binding { + when: slider.bindSlider + slider.value: slider.boundPosition + } + } + + Label { + id: lengthLabel + text: positionInfo.timeStr(positionInfo.length) + font.family: "BigBlueTermPlusNerdFont" + color: "white" + } + } + } + } + Image { + source: "../../assets/rope-tie.png" + smooth: false + width: 32 + height: 32 + x: wrapper.x + 49 - width / 2 + y: wrapper.y - height / 2 + } + Image { + source: "../../assets/rope-tie.png" + smooth: false + width: 32 + height: 32 + x: wrapper.width + wrapper.x - 49 - width / 2 + y: wrapper.y - height / 2 + } +} diff --git a/other/quickshell/bar/systray/MenuCheckBox.qml b/other/quickshell/bar/systray/MenuCheckBox.qml new file mode 100644 index 0000000..2f24423 --- /dev/null +++ b/other/quickshell/bar/systray/MenuCheckBox.qml @@ -0,0 +1,44 @@ +import QtQuick +import QtQuick.Shapes +import "../.." + +Rectangle { + property var checkState: Qt.Unchecked + implicitHeight: 18 + implicitWidth: 18 + radius: 3 + color: checkState == Qt.Checked ? "#C5E3EE" : "#90ACAD" + + Shape { + visible: checkState == Qt.Checked + anchors.fill: parent + layer.enabled: true + layer.samples: 10 + + ShapePath { + strokeWidth: 2 + capStyle: ShapePath.RoundCap + joinStyle: ShapePath.RoundJoin + fillColor: "transparent" + + startX: start.x + startY: start.y + + PathLine { + id: start + x: width * 0.8 + y: height * 0.2 + } + + PathLine { + x: width * 0.35 + y: height * 0.8 + } + + PathLine { + x: width * 0.2 + y: height * 0.6 + } + } + } +} diff --git a/other/quickshell/bar/systray/MenuChildrenRevealer.qml b/other/quickshell/bar/systray/MenuChildrenRevealer.qml new file mode 100644 index 0000000..ad7c0f8 --- /dev/null +++ b/other/quickshell/bar/systray/MenuChildrenRevealer.qml @@ -0,0 +1,56 @@ +import QtQuick +import QtQuick.Shapes +import Quickshell + +Item { + property bool expanded: false; + + readonly property bool open: progress != 0; + readonly property bool animating: internalProgress != (expanded ? 101 : -1); + + implicitHeight: 16 + implicitWidth: 16 + property var xStart: Math.round(width * 0.3) + property var yStart: Math.round(height * 0.1) + property var xEnd: Math.round(width * 0.7) + property var yEnd: Math.round(height * 0.9) + + property real internalProgress: expanded ? 101 : -1; + Behavior on internalProgress { SmoothedAnimation { velocity: 300 } } + + EasingCurve { + id: curve + curve.type: Easing.InOutQuad + } + + readonly property real progress: curve.valueAt(Math.min(100, Math.max(internalProgress, 0)) * 0.01) + + rotation: progress * 90; + + Shape { + anchors.fill: parent + + layer.enabled: true + layer.samples: 3 + + ShapePath { + strokeWidth: 2 + capStyle: ShapePath.RoundCap + joinStyle: ShapePath.MiterJoin + fillColor: "transparent" + + startX: xStart + startY: yStart + + PathLine { + x: xEnd + y: height / 2 + } + + PathLine { + y: yEnd + x: xStart + } + } + } +} diff --git a/other/quickshell/bar/systray/MenuItem.qml b/other/quickshell/bar/systray/MenuItem.qml new file mode 100644 index 0000000..ab3c819 --- /dev/null +++ b/other/quickshell/bar/systray/MenuItem.qml @@ -0,0 +1,118 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.DBusMenu +import "../.." + +MouseArea { + id: root + required property QsMenuEntry entry + property alias expanded: childrenRevealer.expanded + property bool animating: childrenRevealer.animating || (childMenuLoader?.item?.animating ?? false) + // appears it won't actually create the handler when only used from MenuItemList. + onExpandedChanged: {} + onAnimatingChanged: {} + + signal close + + implicitWidth: row.implicitWidth + 4 + implicitHeight: row.implicitHeight + 4 + + hoverEnabled: true + onClicked: { + if (entry.hasChildren) + childrenRevealer.expanded = !childrenRevealer.expanded; + else { + entry.triggered(); + if (entry.toggleType == ToggleButtonType.None) + close(); + } + } + + Rectangle { + anchors.fill: parent + visible: root.containsMouse || childrenRevealer.expanded + + color: "#824524" + } + ColumnLayout { + id: row + anchors.fill: parent + anchors.margins: 2 + spacing: 0 + + RowLayout { + id: innerRow + + Item { + implicitWidth: 22 + implicitHeight: 22 + + MenuCheckBox { + anchors.centerIn: parent + visible: entry.buttonType == QsMenuButtonType.CheckBox + checkState: entry.checkState + } + + MenuRadioButton { + anchors.centerIn: parent + visible: entry.buttonType == QsMenuButtonType.RadioButton + checkState: entry.checkState + } + + MenuChildrenRevealer { + id: childrenRevealer + anchors.centerIn: parent + visible: entry.hasChildren + onOpenChanged: entry.showChildren = open + } + } + + Text { + text: entry.text + color: entry.enabled ? "white" : "#bbbbbb" + } + + Item { + Layout.fillWidth: true + implicitWidth: 22 + implicitHeight: 22 + + IconImage { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + source: entry.icon + visible: source != "" + implicitSize: parent.height + } + } + } + + Loader { + id: childMenuLoader + Layout.fillWidth: true + Layout.preferredHeight: active ? item.implicitHeight * childrenRevealer.progress : 0 + + readonly property real widthDifference: { + Math.max(0, (item?.implicitWidth ?? 0) - innerRow.implicitWidth); + } + Layout.preferredWidth: active ? innerRow.implicitWidth + (widthDifference * childrenRevealer.progress) : 0 + + active: root.expanded || root.animating + clip: true + + sourceComponent: MenuView { + id: childrenList + menu: entry + onClose: root.close() + + anchors { + top: parent.top + left: parent.left + right: parent.right + } + } + } + } +} diff --git a/other/quickshell/bar/systray/MenuRadioButton.qml b/other/quickshell/bar/systray/MenuRadioButton.qml new file mode 100644 index 0000000..2abd692 --- /dev/null +++ b/other/quickshell/bar/systray/MenuRadioButton.qml @@ -0,0 +1,19 @@ +import QtQuick +import "../.." + +Rectangle { + property var checkState: Qt.Unchecked + implicitHeight: 18 + implicitWidth: 18 + radius: width / 2 + color: checkState == Qt.Checked ? "#C5E3EE" : "#90ACAD" + + Rectangle { + x: parent.width * 0.25 + y: parent.height * 0.25 + visible: checkState == Qt.Checked + width: parent.width * 0.5 + height: width + radius: width / 2 + } +} diff --git a/other/quickshell/bar/systray/MenuView.qml b/other/quickshell/bar/systray/MenuView.qml new file mode 100644 index 0000000..2b2682b --- /dev/null +++ b/other/quickshell/bar/systray/MenuView.qml @@ -0,0 +1,73 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell + +ColumnLayout { + id: root + 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: menuView.children + + Loader { + required property var modelData + + property var item: Component { + BoundComponent { + id: itemComponent + source: "MenuItem.qml" + + property var entry: modelData + + function onClose() { + root.close(); + } + + function onExpandedChanged() { + if (item.expanded) + root.submenuExpanded(item); + } + + function onAnimatingChanged() { + if (item.animating) { + root.animatingItem = this; + } else if (root.animatingItem == this) { + root.animatingItem = null; + } + } + + Connections { + target: root + + function onSubmenuExpanded(expandedItem) { + if (item != expandedItem) + item.expanded = false; + } + } + } + } + + property var separator: Component { + Rectangle { + color: "#824524" + implicitHeight: 4 + } + } + + sourceComponent: modelData.isSeparator ? separator : item + Layout.fillHeight: true + Layout.fillWidth: true + } + } +} diff --git a/other/quickshell/bar/systray/SysTray.qml b/other/quickshell/bar/systray/SysTray.qml new file mode 100644 index 0000000..19c01a7 --- /dev/null +++ b/other/quickshell/bar/systray/SysTray.qml @@ -0,0 +1,143 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import Quickshell.Services.SystemTray +import ".." +import "../.." +import "../../components" + +Rectangle { + id: root + required property var bar + implicitWidth: row.implicitWidth + 10 + Layout.fillHeight: true + + color: "transparent" + + RowLayout { + id: row + implicitWidth: childrenRect.width + spacing: 5 + + anchors { + fill: parent + margins: 5 + } + + Repeater { + model: SystemTray.items + + Item { + id: item + required property SystemTrayItem modelData + + property bool targetMenuOpen: false + + Layout.fillHeight: true + implicitWidth: height + + Image { + anchors { + top: parent.top + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + width: height + source: item.modelData.icon + scale: item.targetMenuOpen ? 0.8 : 1 + MouseArea { + id: mouseArea + + anchors.fill: parent + + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + + // showPressed: item.targetMenuOpen || (pressedButtons & ~Qt.RightButton) + // fillWindowWidth: true + // extraVerticalMargin: row.spacing / 2 + + onClicked: event => { + event.accepted = true; + + if (event.button == Qt.LeftButton) { + item.modelData.activate(); + } else if (event.button == Qt.MiddleButton) { + item.modelData.secondaryActivate(); + } + } + + onPressed: event => { + if (event.button == Qt.RightButton && item.modelData.hasMenu) { + item.targetMenuOpen = !item.targetMenuOpen; + } + } + + onWheel: event => { + event.accepted = true; + const points = event.angleDelta.y / 120; + item.modelData.scroll(points, false); + } + + // property var tooltip: TooltipItem { + // tooltip: root.bar.tooltip + // owner: mouseArea + // + // show: mouseArea.containsMouse + // + // Text { + // id: tooltipText + // text: item.modelData.tooltipTitle != "" ? item.modelData.tooltipTitle : item.modelData.id + // color: "white" + // } + // } + + LazyLoader { + active: item.targetMenuOpen + + PopupWindow { + id: rightclickMenu + anchor.window: root.bar + visible: true + + Component.onCompleted: () => { + root.bar.focusable = true; + const pos = mouseArea.mapToGlobal(mouseArea.x, mouseArea.y); + anchor.rect.x = pos.x; + anchor.rect.y = pos.y + mouseArea.height; + } + Connections { + target: item + function onTargetMenuOpenChanged() { + root.bar.focusable = false; + } + } + contentItem { + focus: true + Keys.onEscapePressed: item.targetMenuOpen = false + } + + implicitWidth: Math.max(1, menuView.implicitWidth) + implicitHeight: Math.max(1, menuView.implicitHeight) + color: "transparent" + + Rectangle { + anchors.fill: parent + color: "#9B5B36" + MenuView { + id: menuView + anchors.fill: parent + menu: item.modelData.menu + onClose: item.targetMenuOpen = false + } + } + } + } + } + } + } + } + } +} diff --git a/other/quickshell/components/Rope.qml b/other/quickshell/components/Rope.qml new file mode 100644 index 0000000..9d1c7db --- /dev/null +++ b/other/quickshell/components/Rope.qml @@ -0,0 +1,98 @@ +import QtQuick +import QtQuick.Shapes + +Item { + id: root + + required property vector2d start + required property vector2d end + + property double segmentCount: 8 + property double segmentLen: 50 + + property alias color: path.strokeColor + + readonly property Component p: PathLine { + property vector2d pos + property vector2d prevPos: pos + property vector2d acc + + x: pos.x + y: pos.y + } + readonly property double gravity: 5000 + readonly property int constraintRunCount: 40 + + Shape { + id: shape + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + ShapePath { + id: path + capStyle: ShapePath.RoundCap + strokeColor: "#DAC99B" + strokeWidth: 10 + fillColor: "transparent" + } + } + + Component.onCompleted: () => { + // Create the points + const xInc = (end.x - start.x) / segmentCount; + const yInc = (end.y - start.y) / segmentCount; + let i = 0; + while (i < segmentCount) { + path.pathElements.push(p.createObject(root, { + pos: start.plus(Qt.vector2d(xInc * i, yInc * i)), + acc: Qt.vector2d(0, root.gravity) + })); + i++; + } + + // Set up the first and last points + path.pathElements[0].acc.y = 0; + path.pathElements[segmentCount - 1].acc.y = 0; + + path.startX = path.pathElements[0].x; + path.startY = path.pathElements[0].y; + } + + FrameAnimation { + running: true + onTriggered: () => { + const dt = frameTime; + + for (const p of path.pathElements.slice(1, root.segmentCount - 2)) { + // https://en.wikipedia.org/wiki/Verlet_integration + const newPos = p.pos.times(2.0).minus(p.prevPos).plus(p.acc.times(dt * dt)); + p.prevPos = p.pos; + p.pos = newPos; + } + + for (let i = 0; i < root.segmentCount - 1; i++) { + const cur = path.pathElements[i]; + const next = path.pathElements[i + 1]; + + // Keep fixed distance between segments + for (let j = 0; j < root.constraintRunCount; j++) { + const toNext = next.pos.minus(cur.pos); + const distToNext = toNext.length(); + const error = root.segmentLen - distToNext; + const pull = toNext.times(1.0 / distToNext).times(error).times(0.1); + if (i != 0) { + cur.pos = cur.pos.minus(pull.times(0.5)); + next.pos = next.pos.plus(pull.times(0.5)); + } else { + next.pos = next.pos.plus(pull); + } + } + } + + path.pathElements[0].pos = root.start; + path.pathElements[root.segmentCount - 1].pos = root.end; + + path.startX = path.pathElements[0].x; + path.startY = path.pathElements[0].y; + } + } +} diff --git a/other/quickshell/helloworld.qml b/other/quickshell/helloworld.qml new file mode 100644 index 0000000..0c30bbe --- /dev/null +++ b/other/quickshell/helloworld.qml @@ -0,0 +1,16 @@ +import Quickshell +import QtQuick + +FloatingWindow { + visible: true + width: 200 + height: 100 + + Text { + anchors.centerIn: parent + // centers it within parent + text: "Hello world!" + color: "#0db9d7" + font.pixelSize: 18 + } +} diff --git a/other/quickshell/launcher/Controller.qml b/other/quickshell/launcher/Controller.qml new file mode 100644 index 0000000..ee68419 --- /dev/null +++ b/other/quickshell/launcher/Controller.qml @@ -0,0 +1,40 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import "." as Launcher + +Singleton { + id: root + property bool isOpen: false + property var lastUsedTimes: ({}) + + IpcHandler { + target: "launcher" + + function open() { + root.isOpen = true; + } + + function close() { + root.isOpen = false; + } + + function toggle() { + root.isOpen = !root.isOpen; + } + } + + LazyLoader { + active: root.isOpen + Launcher.Overlay { + controller: root + } + } + + // Empty function to define first reference to singleton + function init() { + } +} diff --git a/other/quickshell/launcher/Overlay.qml b/other/quickshell/launcher/Overlay.qml new file mode 100644 index 0000000..ad750c2 --- /dev/null +++ b/other/quickshell/launcher/Overlay.qml @@ -0,0 +1,302 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import ".." + +PanelWindow { + id: root + + required property var controller + + exclusionMode: ExclusionMode.Ignore + anchors { + left: true + right: true + top: true + bottom: true + } + color: "transparent" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + WlrLayershell.namespace: "shell:launcher" + + Rectangle { + id: main + + anchors.centerIn: 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: Globals.isDarkTheme ? "#393536" : "#f3eac7" + + ColumnLayout { + anchors.fill: parent + anchors.margins: 7 + anchors.bottomMargin: 0 + spacing: 0 + + Rectangle { + id: searchContainer + Layout.fillWidth: true + implicitHeight: search.implicitHeight + 10 + color: Globals.isDarkTheme ? "#44000000" : "#11000000" + + TextInput { + id: search + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 4 + color: Globals.isDarkTheme ? "#D6C8A6" : "#403D3B" + + font.family: "BigBlueTermPlusNerdFont" + + focus: true + Keys.forwardTo: [list] + Keys.onEscapePressed: root.controller.isOpen = 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) => { + if (!search.text) { + if (controller.lastUsedTimes[a.object.id]) + print(controller.lastUsedTimes[a.object.id]); + let aDate = controller.lastUsedTimes[a.object.id] ?? new Date(0); + let bDate = controller.lastUsedTimes[b.object.id] ?? new Date(0); + return bDate - aDate; + } + + 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 { + color: Globals.isDarkTheme ? "#20e0ffff" : "#11000000" + } + 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: { + controller.lastUsedTimes[modelData.id] = new Date(); + modelData.execute(); + root.controller.isOpen = 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 + font.family: "BigBlueTermPlusNerdFont" + color: Globals.isDarkTheme ? "#D6C8A6" : "#403D3B" + Layout.alignment: Qt.AlignVCenter + } + } + } + } + } + } + MultiEffect { + source: main + anchors.fill: main + shadowBlur: 0 + shadowEnabled: true + shadowColor: "#33000000" + shadowVerticalOffset: 16 + shadowHorizontalOffset: 12 + } +} diff --git a/other/quickshell/lock/Context.qml b/other/quickshell/lock/Context.qml new file mode 100644 index 0000000..a29895e --- /dev/null +++ b/other/quickshell/lock/Context.qml @@ -0,0 +1,56 @@ +import QtQuick +import Quickshell +import Quickshell.Services.Pam + +Scope { + id: root + signal pamSuccess + signal unlocked + signal failed + + // These properties are in the context and not individual lock surfaces + // so all surfaces can share the same state. + property string currentText: "" + property bool unlockInProgress: false + property bool showFailure: false + + // Clear the failure text once the user starts typing. + onCurrentTextChanged: showFailure = false + + function tryUnlock() { + if (currentText === "") + return; + + root.unlockInProgress = true; + pam.start(); + } + + PamContext { + id: pam + + // Its best to have a custom pam config for quickshell, as the system one + // might not be what your interface expects, and break in some way. + // This particular example only supports passwords. + configDirectory: "pam" + config: "password.conf" + + // pam_unix will ask for a response for the password prompt + onPamMessage: { + if (this.responseRequired) { + this.respond(root.currentText); + } + } + + // pam_unix won't send any important messages so all we need is the completion status. + onCompleted: result => { + if (result == PamResult.Success) { + root.pamSuccess(); + } else { + root.currentText = ""; + root.showFailure = true; + } + + root.unlockInProgress = false; + } + } +} diff --git a/other/quickshell/lock/Controller.qml b/other/quickshell/lock/Controller.qml new file mode 100644 index 0000000..f2b01d8 --- /dev/null +++ b/other/quickshell/lock/Controller.qml @@ -0,0 +1,47 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import QtQuick +import "." as Lock + +Singleton { + Process { + id: screencopyCmd + running: false + command: ["grim", "-s", "0.5", "-t", "jpeg", "/tmp/lock_screencopy.jpeg"] + onExited: lock.locked = true + } + IpcHandler { + target: "lockscreen" + + function lock(): void { + screencopyCmd.running = true; + } + } + + WlSessionLock { + id: lock + locked: false + + WlSessionLockSurface { + id: lockSurface + color: "transparent" + Lock.Surface { + anchors.fill: parent + context: lockContext + } + } + } + + Lock.Context { + id: lockContext + onUnlocked: lock.locked = false + } + + // Empty function to define first reference to singleton + function init() { + } +} diff --git a/other/quickshell/lock/Surface.qml b/other/quickshell/lock/Surface.qml new file mode 100644 index 0000000..4426b74 --- /dev/null +++ b/other/quickshell/lock/Surface.qml @@ -0,0 +1,281 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects +import QtQuick.Controls.Fusion +import Quickshell.Wayland +import Quickshell +import "." as Lock + +Rectangle { + id: root + required property Lock.Context context + + color: "black" + + Image { + id: chest + anchors.centerIn: parent + source: "../assets/chest.png" + sourceClipRect: Qt.rect(32, 0, 32, 32) + smooth: false + height: width + + // Rectangles + RowLayout { + visible: passwordBox.enabled + anchors.centerIn: parent + anchors.verticalCenterOffset: 31 + spacing: 31 + Repeater { + model: 6 + delegate: Rectangle { + id: r + required property var modelData + color: root.context.showFailure ? "#D6568F" : "#6BCBF3" + implicitWidth: 31 + implicitHeight: 31 + MultiEffect { + id: glow + source: parent + anchors.fill: parent + brightness: passwordBox.length > r.modelData ? 0.7 : 0 + blur: passwordBox.length > r.modelData ? 2.0 : 0 + blurEnabled: true + } + } + } + } + + Image { + id: screencopy + anchors.centerIn: parent + source: "/tmp/lock_screencopy.jpeg" + height: width * (9 / 16) // Width is animated below + } + + Image { + id: chestInFront + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.width / 2 + source: "../assets/chest.png" + sourceClipRect: Qt.rect(32, 16, 32, 16) + smooth: false + visible: false + z: 9999 + } + } + + // Enter anim + // ------------ + + // Screencopy anim + NumberAnimation { + target: screencopy + property: "width" + from: 1920 + to: 250 + duration: 700 + easing.type: Easing.OutBack + easing.overshoot: 0.9 + running: true + } + SequentialAnimation { + id: screencopyAnim + running: true + NumberAnimation { + target: screencopy + property: "anchors.verticalCenterOffset" + to: -150 + duration: 500 + easing.type: Easing.OutBack + } + PauseAnimation { + duration: 200 + } + PropertyAction { + target: chestInFront + property: "visible" + value: true + } + NumberAnimation { + target: screencopy + property: "anchors.verticalCenterOffset" + to: 100 + duration: 300 + easing.type: Easing.Linear + } + } + + // Chest anim + SequentialAnimation { + id: chestAnim + running: true + NumberAnimation { + target: chest + property: "width" + from: 2000 + to: 500 + duration: 500 + easing.type: Easing.OutBack + running: true + } + PauseAnimation { + duration: 500 + } + PropertyAction { + target: chest + property: "sourceClipRect.x" + value: 0 + } + PropertyAction { + target: chestInFront + property: "visible" + value: false + } + PropertyAction { + target: screencopy + property: "visible" + value: false + } + NumberAnimation { + onStarted: screencopy.visible = false + target: chest + property: "anchors.verticalCenterOffset" + to: 50 + duration: 100 + easing.type: Easing.OutBack + } + NumberAnimation { + target: chest + property: "anchors.verticalCenterOffset" + to: 0 + duration: 100 + easing.type: Easing.OutBack + } + } + + // Exit anim + // ------------ + + // Screencopy anim + SequentialAnimation { + id: screencopyAnimExit + running: false + PropertyAction { + target: chestInFront + property: "visible" + value: true + } + NumberAnimation { + target: screencopy + property: "anchors.verticalCenterOffset" + to: -150 + duration: 200 + easing.type: Easing.OutBack + } + PropertyAction { + target: chestInFront + property: "visible" + value: false + } + NumberAnimation { + target: screencopy + property: "anchors.verticalCenterOffset" + to: 0 + duration: 400 + easing.type: Easing.OutBack + } + NumberAnimation { + target: screencopy + property: "width" + to: 1920 + duration: 400 + easing.type: Easing.OutBack + running: true + } + onFinished: () => root.context.unlocked() + } + + // Chest anim + SequentialAnimation { + id: chestAnimExit + running: false + PropertyAction { + target: chest + property: "sourceClipRect.x" + value: 32 + } + PropertyAction { + target: screencopy + property: "visible" + value: true + } + NumberAnimation { + target: chest + property: "anchors.verticalCenterOffset" + to: -50 + duration: 100 + easing.type: Easing.OutBack + } + NumberAnimation { + target: chest + property: "anchors.verticalCenterOffset" + to: 0 + duration: 200 + easing.type: Easing.OutBack + } + } + + TextField { + id: passwordBox + + implicitWidth: 400 + font.pointSize: 50 + color: "transparent" + cursorVisible: false + background: Rectangle { + color: "transparent" + } + + focus: true + enabled: !screencopyAnim.running && !chestAnim.running && !screencopyAnimExit.running && !chestAnimExit.running && !root.context.unlockInProgress + opacity: 0 + echoMode: TextInput.Password + inputMethodHints: Qt.ImhSensitiveData + + onTextChanged: () => { + if (text != "") { + root.context.showFailure = false; + } + } + + onAccepted: () => { + root.context.currentText = text; + root.context.tryUnlock(); + } + + Connections { + target: root.context + + function onUnlockInProgressChanged() { + if (root.context.unlockInProgress == false) { + passwordBox.text = ""; + } + } + + function onPamSuccess() { + screencopyAnimExit.running = true; + chestAnimExit.running = true; + } + } + } + + // Button { + // text: "Its not working, let me out" + // onClicked: root.context.unlocked() + // } +} diff --git a/other/quickshell/lock/pam/password.conf b/other/quickshell/lock/pam/password.conf new file mode 100644 index 0000000..50babc8 --- /dev/null +++ b/other/quickshell/lock/pam/password.conf @@ -0,0 +1,2 @@ +auth required pam_unix.so + diff --git a/other/quickshell/notifs/Display.qml b/other/quickshell/notifs/Display.qml new file mode 100644 index 0000000..d4bdf7d --- /dev/null +++ b/other/quickshell/notifs/Display.qml @@ -0,0 +1,70 @@ +// This is how the notification looks + +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.Notifications + +Item { + id: root + + required property Notification notif + + implicitWidth: 240 + implicitHeight: layout.height + + ColumnLayout { + id: layout + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + spacing: 0 + + Image { + id: trumpetTop + Layout.fillWidth: true + Layout.preferredHeight: width * (sourceSize.height / sourceSize.width) + source: "../assets/trumpet-top.png" + smooth: false + } + + Rectangle { + id: bannerRect + + color: "#3B253F" + Layout.fillWidth: true + implicitHeight: textColumn.height + Layout.leftMargin: (width / trumpetTop.sourceSize.width) * 2 + 3 + Layout.rightMargin: (width / trumpetTop.sourceSize.width) * 2 + 3 + + ColumnLayout { + id: textColumn + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + Text { + Layout.maximumWidth: bannerRect.width + text: root.notif.summary + (root.notif.body ? "\n=======" : "") + font.family: "BigBlueTermPlusNerdFont" + wrapMode: Text.Wrap + font.pointSize: 18 + font.bold: true + color: "#9292B6" + } + + Text { + Layout.maximumWidth: bannerRect.width + text: root.notif.body + font.family: "BigBlueTermPlusNerdFont" + wrapMode: Text.Wrap + font.pointSize: 14 + font.bold: false + color: "#9292B6" + } + } + } + } +} diff --git a/other/quickshell/notifs/Notif.qml b/other/quickshell/notifs/Notif.qml new file mode 100644 index 0000000..59a5b79 --- /dev/null +++ b/other/quickshell/notifs/Notif.qml @@ -0,0 +1,183 @@ +// This is all the behaviour and animations for the notification + +import QtQuick +import Quickshell.Io +import "." as Notifs + +Item { + id: root + + signal dismissed + + required property var notif + property var screen: Qt.size(1920, 1080) + + property var lifetime: 5000 + + enum AnimState { + Returning, + Inert, + Flinging, + Dismissing + } + property var state: Notif.Returning + property var isDragging: false + + property var initialX: 200 + property var initialY: 100 + property var initialR: 45 + property var targetX: 0 + property var targetY: 0 + property var targetR: 0 + property var velocityX: 0 + property var velocityY: 0 + property var velocityR: 0 + + 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: root.state != Notif.Inert + onTriggered: { + if (root.state == Notif.Returning) { + const deltaX = root.targetX - display.x; + const deltaY = root.targetY - display.y; + const deltaR = root.targetR - display.rotation; + + root.velocityX = dampingVelocity(root.velocityX, deltaX); + root.velocityY = dampingVelocity(root.velocityY, deltaY); + root.velocityR = dampingVelocity(root.velocityR, deltaR); + + if (Math.abs(root.velocityX) < 0.1 && Math.abs(root.velocityY) < 0.1) { + console.log("inter"); + root.state = Notif.Inert; + root.velocityX = 0; + root.velocityY = 0; + root.velocityR = 0; + display.x = root.targetX; + display.y = root.targetY; + display.rotation = root.targetR; + } + + if (root.isDragging) { + if (Math.abs(root.velocityX) > 1200 || Math.abs(root.velocityY) > 1200) { + root.state = Notif.Flinging; + } + } + } else if (root.state == Notif.Flinging) { + root.velocityY += 3000 * frameTime; + display.rotation = -root.velocityY * frameTime; + + knight.visible = true; + knight.x += root.velocityX * frameTime; + knight.y += root.velocityY * frameTime; + knight.rotation += root.velocityX * 0.2 * frameTime; + + if (display.x > display.width || display.y > root.screen.height) { + root.dismissed(); + } + } else if (root.state == Notif.Dismissing) { + root.velocityX += frameTime * 20000; + + if (display.x > display.width) { + root.dismissed(); + } + } + + display.x += root.velocityX * frameTime; + display.y += root.velocityY * frameTime; + display.rotation += root.velocityR * frameTime; + } + } + + implicitWidth: display.width + implicitHeight: display.height + anchors.fill: display + + Notifs.Display { + id: display + notif: root.notif + x: root.initialX + y: root.initialY + rotation: root.initialR + transformOrigin: Item.Right + } + Image { + id: knight + source: "../assets/knight.png" + x: width + width: sourceSize.width * 10 + height: sourceSize.height * 10 + smooth: false + visible: false + Process { + id: screamCmd + command: ["play", "--no-show-progress", "assets/wilhelm-scream.ogg"] + } + onVisibleChanged: () => { + if (visible) { + screamCmd.startDetached(); + } + } + } + + MouseArea { + id: mouseArea + + anchors.fill: display + acceptedButtons: Qt.LeftButton | Qt.RightButton + enabled: root.state != Notif.Flinging + + property var prevMouseX: 0 + property var prevMouseY: 0 + + onPressed: e => { + if (enabled && e.buttons & Qt.LeftButton) { + prevMouseX = e.x; + prevMouseY = e.y; + root.isDragging = true; + root.state = Notif.Returning; + } + } + onReleased: e => { + if (!(e.buttons & Qt.LeftButton)) { + root.isDragging = false; + } + } + onPositionChanged: e => { + if (enabled && root.isDragging) { + root.velocityX = (e.x - prevMouseX) * 200; + root.velocityY = (e.y - prevMouseY) * 200; + prevMouseX = e.x; + prevMouseY = e.y; + } + } + + onClicked: e => { + if (enabled && e.button & Qt.RightButton) { + root.state = Notif.Dismissing; + } + } + } + + Timer { + id: timer + interval: root.lifetime + repeat: false + running: !mouseArea.containsMouse && root.state == Notif.Inert + onTriggered: () => { + root.state = Notif.Dismissing; + } + } + + Process { + id: playSoundCmd + command: ["play", "--no-show-progress", "assets/trumpet.wav"] + } + Component.onCompleted: playSoundCmd.startDetached() +} diff --git a/other/quickshell/notifs/Overlay.qml b/other/quickshell/notifs/Overlay.qml new file mode 100644 index 0000000..dc2bbe6 --- /dev/null +++ b/other/quickshell/notifs/Overlay.qml @@ -0,0 +1,90 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Notifications +import "." + +PanelWindow { + id: root + + required property var bar + property list notifs + + WlrLayershell.namespace: "shell:notifications" + exclusionMode: ExclusionMode.Ignore + color: "transparent" + + anchors { + left: true + top: true + bottom: true + right: true + } + + NotificationServer { + actionsSupported: true + + onNotification: notif => { + notif.tracked = true; + root.notifs = [...root.notifs, notif]; + } + } + + visible: stack.children.length != 0 + mask: Region { + item: stack + } + + ListView { + id: stack + + model: ScriptModel { + values: [...root.notifs] + } + anchors.right: parent.right + y: root.bar.height + implicitWidth: 240 + implicitHeight: children.reduce((h, c) => h + c.height, 0) + interactive: false + spacing: 20 + + displaced: Transition { + NumberAnimation { + property: "y" + duration: 200 + easing.type: Easing.OutCubic + } + } + + move: Transition { + NumberAnimation { + property: "y" + duration: 200 + easing.type: Easing.OutCubic + } + } + + remove: Transition { + NumberAnimation { + property: "y" + duration: 200 + easing.type: Easing.OutCubic + } + } + + delegate: Notif { + required property Notification modelData + notif: modelData + + onDismissed: () => { + modelData.dismiss(); + // Remove from list + const index = root.notifs.indexOf(notif); + if (index > -1) + root.notifs.splice(index, 1); + } + } + } +} diff --git a/other/quickshell/screenshot/Controller.qml b/other/quickshell/screenshot/Controller.qml new file mode 100644 index 0000000..0797cf5 --- /dev/null +++ b/other/quickshell/screenshot/Controller.qml @@ -0,0 +1,39 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import "." as Screenshot + +Singleton { + id: root + property bool isOpen: false + + IpcHandler { + target: "screenshot" + + function open() { + root.isOpen = true; + } + + function close() { + root.isOpen = false; + } + + function toggle() { + root.isOpen = !root.isOpen; + } + } + + LazyLoader { + active: root.isOpen + Screenshot.Overlay { + controller: root + } + } + + // Empty function to define first reference to singleton + function init() { + } +} diff --git a/other/quickshell/screenshot/Overlay.qml b/other/quickshell/screenshot/Overlay.qml new file mode 100644 index 0000000..0625cfb --- /dev/null +++ b/other/quickshell/screenshot/Overlay.qml @@ -0,0 +1,198 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import "../components" + +// TODO: make configurable: cursor visibility, screen freeze +PanelWindow { + id: root + + required property var controller + + exclusionMode: ExclusionMode.Ignore + anchors { + left: true + right: true + top: true + bottom: true + } + color: "transparent" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + WlrLayershell.namespace: "shell:screenshot" + + visible: screencopy.hasContent + ScreencopyView { + id: screencopy + captureSource: root.screen + anchors.fill: parent + paintCursor: false + } + + Process { + id: cmd + running: false + onExited: notifyCmd.running = true + } + Process { + id: notifyCmd + command: ["notify-send", "\"Screenshot copied\"", "\"The screenshot was copied to your clipboard\""] + running: false + onExited: root.controller.isOpen = false + } + + contentItem { + focus: true + Keys.onEscapePressed: root.controller.isOpen = false + + Keys.onReturnPressed: () => { + const x = Math.ceil(root.left); + const y = Math.ceil(root.top); + const w = Math.floor(root.right - root.left); + const h = Math.floor(root.bottom - root.top); + cmd.command = ["sh", "-c", `grim -g "${x},${y} ${w}x${h}" - | wl-copy`]; + cmd.running = true; + } + } + + property var left: 0 + property var top: 0 + property var right: 0 + property var bottom: 0 + + Canvas { + id: canvas + anchors.fill: parent + + readonly property var borderExtendX: 56 + readonly property var borderExtendY: 48 + readonly property var borderWidthH: 12 + readonly property var borderWidthV: 18 + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + + // Background + ctx.fillStyle = "#44000000"; + ctx.fillRect(0, 0, root.width, root.height); + + // Border + ctx.fillStyle = "#824524"; + ctx.fillRect(root.left - borderExtendX, root.top - borderWidthH, root.right - root.left + borderExtendX * 2, borderWidthH); + ctx.fillRect(root.left - borderExtendX, root.bottom, root.right - root.left + borderExtendX * 2, borderWidthH); + ctx.fillRect(root.left - borderWidthV, root.top - borderExtendY, borderWidthV, root.bottom - root.top + borderExtendY * 2); + ctx.fillRect(root.right, root.top - borderExtendY, borderWidthV, root.bottom - root.top + borderExtendY * 2); + + // Rect + ctx.clearRect(root.left, root.top, root.right - root.left, root.bottom - root.top); + } + } + + Rope { + id: topLeftRope + anchors.fill: parent + start: Qt.vector2d(0, 0) + end: Qt.vector2d(0, 0) + } + Rope { + id: topRightRope + anchors.fill: parent + start: Qt.vector2d(0, 0) + end: Qt.vector2d(0, 0) + } + Rope { + id: bottomRightRope + anchors.fill: parent + start: Qt.vector2d(0, 0) + end: Qt.vector2d(0, 0) + } + Rope { + id: bottomLeftRope + anchors.fill: parent + start: Qt.vector2d(0, 0) + end: Qt.vector2d(0, 0) + } + + RopeTie { + x: root.left - width / 2 + y: root.top - height / 2 + } + RopeTie { + x: root.right - width / 2 + y: root.top - height / 2 + } + RopeTie { + x: root.right - width / 2 + y: root.bottom - height / 2 + } + RopeTie { + x: root.left - width / 2 + y: root.bottom - height / 2 + } + + MouseArea { + anchors.fill: parent + + onPressed: e => { + root.left = e.x; + root.top = e.y; + root.right = e.x; + root.bottom = e.y; + } + onPositionChanged: e => { + if (e.x > root.left) { + root.right = e.x; + } + + if (e.y > root.top) { + root.bottom = e.y; + } + } + } + + onLeftChanged: () => { + canvas.requestPaint(); + topLeftRope.end = Qt.vector2d(left, top); + bottomLeftRope.end = Qt.vector2d(left, bottom); + } + onTopChanged: () => { + canvas.requestPaint(); + topLeftRope.end = Qt.vector2d(left, top); + topRightRope.end = Qt.vector2d(right, top); + } + onRightChanged: () => { + canvas.requestPaint(); + topRightRope.end = Qt.vector2d(right, top); + bottomRightRope.end = Qt.vector2d(right, bottom); + } + onBottomChanged: () => { + canvas.requestPaint(); + bottomRightRope.end = Qt.vector2d(right, bottom); + bottomLeftRope.end = Qt.vector2d(left, bottom); + } + + onWidthChanged: () => { + left = width / 2; + right = width / 2; + + topRightRope.start = Qt.vector2d(width, 0); + bottomRightRope.start = Qt.vector2d(width, height); + } + onHeightChanged: () => { + top = height / 2; + bottom = height / 2; + + bottomRightRope.start = Qt.vector2d(width, height); + bottomLeftRope.start = Qt.vector2d(0, height); + } + + component RopeTie: Image { + width: 48 + height: 48 + source: "../assets/rope-tie.png" + smooth: false + } +} diff --git a/other/quickshell/shell.qml b/other/quickshell/shell.qml new file mode 100644 index 0000000..1924892 --- /dev/null +++ b/other/quickshell/shell.qml @@ -0,0 +1,25 @@ +import Quickshell +import QtQuick +import "bar" as Bar +import "notifs" as Notifs +import "launcher" as Launcher +import "screenshot" as Screenshot +import "lock" as Lock + +ShellRoot { + Bg {} + + Bar.Bar { + id: bar + } + + Notifs.Overlay { + bar: bar + } + + Component.onCompleted: () => { + Launcher.Controller.init(); + Screenshot.Controller.init(); + Lock.Controller.init(); + } +} diff --git a/todo b/todo index f89dd2a..0409d7e 100644 --- a/todo +++ b/todo @@ -1,13 +1,6 @@ -use hyprland -transfer everything -list to things to transfere: - -some cursor-themed audacity.x86_64 biber.noarch dolphin.x86_64 may be customized or nautilus cust -gimp.x86_64 -hyprcursor.x86_64 ollama.x86_64 plymouth.x86_64 python3.x86_64