last 7 months of qs changes
							
								
								
									
										55
									
								
								modules/user/modules/greetd/default.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
{ inputs, system, config, lib, pkgs, ... }: let
 | 
			
		||||
  hyprlandPackage = config.home-manager.users.${config.main-user}.wayland.windowManager.hyprland.package;
 | 
			
		||||
  hyprlandConfig = pkgs.writeText "greetd-hyprland-config" ''
 | 
			
		||||
    # for some reason pkill is way faster than dispatching exit, to the point greetd thinks the greeter died.
 | 
			
		||||
    exec-once = quickshell -c greeter >& qslog.txt && pkill Hyprland
 | 
			
		||||
 | 
			
		||||
    input {
 | 
			
		||||
      kb_layout = us
 | 
			
		||||
      sensitivity = 0
 | 
			
		||||
      follow_mouse = 1
 | 
			
		||||
      # mouse_refocus = false - #6393
 | 
			
		||||
      accel_profile = flat
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    decoration {
 | 
			
		||||
      blur {
 | 
			
		||||
        enabled = no
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    animations {
 | 
			
		||||
      enabled = no
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    misc {
 | 
			
		||||
      disable_hyprland_logo = true
 | 
			
		||||
      disable_splash_rendering = true
 | 
			
		||||
      background_color = 0x000000
 | 
			
		||||
      key_press_enables_dpms = true
 | 
			
		||||
      mouse_move_enables_dpms = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ${config.hyprland-session.extraConfigStatic}
 | 
			
		||||
  '';
 | 
			
		||||
in {
 | 
			
		||||
  services.greetd = {
 | 
			
		||||
    enable = true;
 | 
			
		||||
    restart = false;
 | 
			
		||||
    settings.default_session.command = "${lib.getExe hyprlandPackage} -c ${hyprlandConfig}";
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  # needed for hyprland cache dir
 | 
			
		||||
  users.users.greeter = {
 | 
			
		||||
    home = "/home/greeter";
 | 
			
		||||
    createHome = true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  home-manager.users.greeter = {
 | 
			
		||||
    home.stateVersion = config.system.stateVersion;
 | 
			
		||||
    imports = [
 | 
			
		||||
      ../../../theme/home.nix # also fixes cursor
 | 
			
		||||
      ../quickshell # set up quickshell manifest and such
 | 
			
		||||
    ];
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +1,29 @@
 | 
			
		|||
{ inputs, pkgs, lib, system, impurity, ... }: let
 | 
			
		||||
{ config, inputs, pkgs, lib, system, impurity, ... }: let
 | 
			
		||||
  inherit (inputs) quickshell;
 | 
			
		||||
  # hack because the greeter user cant access /home/admin
 | 
			
		||||
  maybeLink = path: if config.home.username == "admin" then impurity.link path else path;
 | 
			
		||||
in {
 | 
			
		||||
  home.packages = with pkgs; [
 | 
			
		||||
    qt6.qtimageformats # amog
 | 
			
		||||
    qt6.qt5compat # shader fx
 | 
			
		||||
    quickshell.packages.${system}.default
 | 
			
		||||
    pamtester # lockscreen
 | 
			
		||||
    (quickshell.packages.${system}.default.override (prevqs: {
 | 
			
		||||
      debug = true;
 | 
			
		||||
      qt6 = prevqs.qt6.overrideScope (_: prevqt: {
 | 
			
		||||
        qtdeclarative = prevqt.qtdeclarative.overrideAttrs (prev: {
 | 
			
		||||
          cmakeBuildType = "Debug";
 | 
			
		||||
          dontStrip = true;
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      breakpad = prevqs.breakpad.override rec {
 | 
			
		||||
        stdenv = pkgs.gcc13Stdenv;
 | 
			
		||||
      };
 | 
			
		||||
    }))
 | 
			
		||||
    grim imagemagick # screenshot
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  xdg.configFile."quickshell/manifest.conf".text = lib.generators.toKeyValue {} {
 | 
			
		||||
    shell = "${impurity.link ./shell}";
 | 
			
		||||
    lockscreen = "${impurity.link ./lockscreen}";
 | 
			
		||||
    shell = "${maybeLink ./.}/shell";
 | 
			
		||||
    greeter = "${maybeLink ./.}/shell/greeter.qml";
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,43 +0,0 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Io
 | 
			
		||||
 | 
			
		||||
QtObject {
 | 
			
		||||
	property int status: AuthContext.Status.FirstLogin
 | 
			
		||||
	signal unlocked();
 | 
			
		||||
 | 
			
		||||
	enum Status {
 | 
			
		||||
		FirstLogin,
 | 
			
		||||
		Authenticating,
 | 
			
		||||
		LoginFailed
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property string password
 | 
			
		||||
 | 
			
		||||
	property var pamtester: Process {
 | 
			
		||||
		property bool failed: true
 | 
			
		||||
 | 
			
		||||
		command: ["pamtester", "login", Quickshell.env("USER"), "authenticate"]
 | 
			
		||||
 | 
			
		||||
		onStarted: this.write(`${password}\n`)
 | 
			
		||||
 | 
			
		||||
		stdout: SplitParser {
 | 
			
		||||
			// fails go to stderr
 | 
			
		||||
			onRead: pamtester.failed = false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		onExited: {
 | 
			
		||||
			if (failed) {
 | 
			
		||||
				status = AuthContext.Status.LoginFailed;
 | 
			
		||||
			} else {
 | 
			
		||||
				unlocked();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function tryLogin(password: string) {
 | 
			
		||||
		this.password = password
 | 
			
		||||
		status = AuthContext.Status.Authenticating;
 | 
			
		||||
		pamtester.running = true;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,17 +0,0 @@
 | 
			
		|||
pragma Singleton
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
 | 
			
		||||
Singleton {
 | 
			
		||||
	property var time: new Date();
 | 
			
		||||
	property string text;
 | 
			
		||||
 | 
			
		||||
	Timer {
 | 
			
		||||
		interval: 10000
 | 
			
		||||
		running: true
 | 
			
		||||
		repeat: true
 | 
			
		||||
 | 
			
		||||
		onTriggered: time = new Date()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,131 +0,0 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Controls.Basic
 | 
			
		||||
import Quickshell.Io
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	required property AuthContext context
 | 
			
		||||
 | 
			
		||||
	Item {
 | 
			
		||||
		anchors.centerIn: parent
 | 
			
		||||
 | 
			
		||||
		Text {
 | 
			
		||||
			id: timeText
 | 
			
		||||
			anchors {
 | 
			
		||||
				bottom: entryBox.top
 | 
			
		||||
				bottomMargin: 100
 | 
			
		||||
				horizontalCenter: parent.horizontalCenter
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			font {
 | 
			
		||||
				pointSize: 120
 | 
			
		||||
				hintingPreference: Font.PreferFullHinting
 | 
			
		||||
				family: "Noto Sans"
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			color: "white"
 | 
			
		||||
 | 
			
		||||
			text: {
 | 
			
		||||
				const hours = LockGlobals.time.getHours().toString().padStart(2, '0');
 | 
			
		||||
				const minutes = LockGlobals.time.getMinutes().toString().padStart(2, '0');
 | 
			
		||||
				return `${hours}:${minutes}`;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Text {
 | 
			
		||||
			anchors {
 | 
			
		||||
				top: timeText.bottom
 | 
			
		||||
				topMargin: -20
 | 
			
		||||
				horizontalCenter: parent.horizontalCenter
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			font.pointSize: 40
 | 
			
		||||
 | 
			
		||||
			color: "#50ffffff"
 | 
			
		||||
			text: "Locked"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		TextField {
 | 
			
		||||
			id: entryBox
 | 
			
		||||
			anchors.centerIn: parent
 | 
			
		||||
			width: 600
 | 
			
		||||
			font.pointSize: 24
 | 
			
		||||
 | 
			
		||||
			enabled: context.status != AuthContext.Status.Authenticating
 | 
			
		||||
			focus: true
 | 
			
		||||
			horizontalAlignment: TextInput.AlignHCenter
 | 
			
		||||
			echoMode: TextInput.Password
 | 
			
		||||
			inputMethodHints: Qt.ImhSensitiveData
 | 
			
		||||
			onCursorVisibleChanged: {
 | 
			
		||||
				if (cursorVisible) cursorVisible = false;
 | 
			
		||||
			}
 | 
			
		||||
			cursorVisible: false
 | 
			
		||||
 | 
			
		||||
			color: "white"
 | 
			
		||||
 | 
			
		||||
			background: Rectangle {
 | 
			
		||||
				color: "#20ffffff"
 | 
			
		||||
				border.color: "#30ffffff"
 | 
			
		||||
				radius: height / 2
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			text: LockGlobals.text
 | 
			
		||||
			onTextChanged: {
 | 
			
		||||
				LockGlobals.text = text
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			onAccepted: {
 | 
			
		||||
				if (text != "") context.tryLogin(text)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			onEnabledChanged: {
 | 
			
		||||
				if (enabled) text = ""
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Text {
 | 
			
		||||
			id: status
 | 
			
		||||
			color: "white"
 | 
			
		||||
			font.pointSize: 24
 | 
			
		||||
 | 
			
		||||
			anchors {
 | 
			
		||||
				horizontalCenter: entryBox.horizontalCenter
 | 
			
		||||
				top: entryBox.bottom
 | 
			
		||||
				topMargin: 40
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			text: {
 | 
			
		||||
				switch (context.status) {
 | 
			
		||||
				case AuthContext.Status.FirstLogin: return ""
 | 
			
		||||
				case AuthContext.Status.Authenticating: return "Authenticating"
 | 
			
		||||
				case AuthContext.Status.LoginFailed: return "Login Failed"
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Button {
 | 
			
		||||
		anchors {
 | 
			
		||||
			horizontalCenter: parent.horizontalCenter
 | 
			
		||||
			bottom: parent.bottom
 | 
			
		||||
			bottomMargin: 20
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		contentItem: Text {
 | 
			
		||||
			text: "Turn off Monitors"
 | 
			
		||||
			color: "#aaeeffff"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		onClicked: dpms.running = true
 | 
			
		||||
 | 
			
		||||
		background: Rectangle {
 | 
			
		||||
			color: "#20ffffff"
 | 
			
		||||
			border.color: "#30ffffff"
 | 
			
		||||
			radius: height / 2
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Process {
 | 
			
		||||
			id: dpms
 | 
			
		||||
			command: [ "hyprctl", "dispatch", "dpms" ]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,36 +0,0 @@
 | 
			
		|||
//@ pragma NativeTextRendering
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Wayland
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
ShellRoot {
 | 
			
		||||
	AuthContext {
 | 
			
		||||
		id: authContext
 | 
			
		||||
		onUnlocked: lock.locked = false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	WlSessionLock {
 | 
			
		||||
		id: lock
 | 
			
		||||
		locked: true
 | 
			
		||||
 | 
			
		||||
		onLockedChanged: {
 | 
			
		||||
			if (!locked) Qt.quit();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		WlSessionLockSurface {
 | 
			
		||||
			id: surface
 | 
			
		||||
 | 
			
		||||
			BackgroundImage {
 | 
			
		||||
				anchors.fill: parent
 | 
			
		||||
				screen: surface.screen
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Lockscreen {
 | 
			
		||||
				anchors.fill: parent
 | 
			
		||||
				context: authContext
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,39 +0,0 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
ShellRoot {
 | 
			
		||||
	AuthContext {
 | 
			
		||||
		id: authContext
 | 
			
		||||
		onUnlocked: Qt.quit()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	FloatingWindow {
 | 
			
		||||
		BackgroundImage {
 | 
			
		||||
			anchors.fill: parent
 | 
			
		||||
			screen: Quickshell.screens.filter(s => s.name == "eDP-1")[0]
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Lockscreen {
 | 
			
		||||
			anchors {
 | 
			
		||||
				left: parent.left
 | 
			
		||||
				top: parent.top
 | 
			
		||||
				bottom: parent.bottom
 | 
			
		||||
				right: parent.horizontalCenter
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			context: authContext
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Lockscreen {
 | 
			
		||||
			anchors {
 | 
			
		||||
				left: parent.horizontalCenter
 | 
			
		||||
				top: parent.top
 | 
			
		||||
				bottom: parent.bottom
 | 
			
		||||
				right: parent.right
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			context: authContext
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,26 +0,0 @@
 | 
			
		|||
pragma Singleton
 | 
			
		||||
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Io
 | 
			
		||||
 | 
			
		||||
Singleton {
 | 
			
		||||
	signal windowOpened(address: string, workspace: string, klass: string, title: string);
 | 
			
		||||
 | 
			
		||||
	Socket {
 | 
			
		||||
		connected: true
 | 
			
		||||
		path: `/tmp/hypr/${Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")}/.socket2.sock`
 | 
			
		||||
 | 
			
		||||
		parser: SplitParser {
 | 
			
		||||
			onRead: message => {
 | 
			
		||||
				const [type, body] = message.split(">>");
 | 
			
		||||
				const args = body.split(",");
 | 
			
		||||
 | 
			
		||||
				switch (type) {
 | 
			
		||||
				case "openwindow":
 | 
			
		||||
					windowOpened(args[0], args[1], args[2], args[3])
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,81 +0,0 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Wayland
 | 
			
		||||
 | 
			
		||||
WlrLayershell {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property var bar;
 | 
			
		||||
 | 
			
		||||
	property var popup: null;
 | 
			
		||||
	property list<variant> overlays: [];
 | 
			
		||||
	property variant activeOverlay: null;
 | 
			
		||||
	property variant lastActiveOverlay: null;
 | 
			
		||||
 | 
			
		||||
	onActiveOverlayChanged: {
 | 
			
		||||
		if (lastActiveOverlay != null && lastActiveOverlay != activeOverlay) {
 | 
			
		||||
			lastActiveOverlay.expanded = false;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		lastActiveOverlay = activeOverlay;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	readonly property rect barRect: {
 | 
			
		||||
		void [width, height];
 | 
			
		||||
		this.contentItem.mapFromItem(bar, 0, 0, bar.width, bar.height)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	readonly property real overlayXOffset: barRect.x + barRect.width + 10
 | 
			
		||||
 | 
			
		||||
	exclusionMode: ExclusionMode.Ignore
 | 
			
		||||
	color: "transparent"
 | 
			
		||||
 | 
			
		||||
	namespace: "shell:bar"
 | 
			
		||||
 | 
			
		||||
	Variants {
 | 
			
		||||
		id: masks
 | 
			
		||||
		model: overlays
 | 
			
		||||
 | 
			
		||||
		Region {
 | 
			
		||||
			required property var modelData;
 | 
			
		||||
			item: modelData.widget
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mask: Region {
 | 
			
		||||
		regions: masks.instances
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	anchors {
 | 
			
		||||
		left: true
 | 
			
		||||
		top: true
 | 
			
		||||
		bottom: true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	width: {
 | 
			
		||||
		const extents = overlays
 | 
			
		||||
			.filter(overlay => !overlay.fullyCollapsed)
 | 
			
		||||
			.map(overlay => overlayXOffset + overlay.expandedWidth);
 | 
			
		||||
 | 
			
		||||
		return Math.max(barRect.x + barRect.width, ...extents);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function connectOverlay(overlay: variant) {
 | 
			
		||||
		overlay.widget.parent = this.contentItem
 | 
			
		||||
		overlays.push(overlay);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function disconnectOverlay(overlay: variant) {
 | 
			
		||||
		// Splice seems to make it undefined as an intermediary step
 | 
			
		||||
		// which breaks bindings.
 | 
			
		||||
		overlays = overlays.filter(o => o != overlay);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function expandedPosition(overlay: variant): rect {
 | 
			
		||||
		const rect = overlay.collapsedLayerRect;
 | 
			
		||||
 | 
			
		||||
		const idealY = rect.y + (rect.height / 2) - (overlay.expandedHeight / 2)
 | 
			
		||||
		const y = Math.max(barRect.y, Math.min((barRect.y + barRect.height) - overlay.expandedHeight, idealY));
 | 
			
		||||
 | 
			
		||||
		return Qt.rect(overlayXOffset, y, overlay.expandedWidth, overlay.expandedHeight);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,27 +0,0 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property var screen;
 | 
			
		||||
	required property var selectionArea;
 | 
			
		||||
	signal selectionComplete(x: real, y: real, width: real, height: real)
 | 
			
		||||
 | 
			
		||||
	MouseArea {
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
		onPressed: {
 | 
			
		||||
			selectionArea.startX = mouseX;
 | 
			
		||||
			selectionArea.startY = mouseY;
 | 
			
		||||
			selectionArea.endX = mouseX;
 | 
			
		||||
			selectionArea.endY = mouseY;
 | 
			
		||||
			selectionArea.startSelection(false);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		onPositionChanged: {
 | 
			
		||||
			selectionArea.endX = mouseX;
 | 
			
		||||
			selectionArea.endY = mouseY;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		onReleased: selectionArea.endSelection();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,99 +0,0 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Wayland
 | 
			
		||||
 | 
			
		||||
WlrLayershell {
 | 
			
		||||
	signal selectionComplete(x: real, y: real, width: real, height: real)
 | 
			
		||||
 | 
			
		||||
	color: "transparent"
 | 
			
		||||
	visible: selectionArea.selecting || selectionArea.initializing
 | 
			
		||||
	exclusionMode: ExclusionMode.Ignore
 | 
			
		||||
	layer: WlrLayer.Overlay
 | 
			
		||||
	namespace: "termspawner"
 | 
			
		||||
 | 
			
		||||
	anchors {
 | 
			
		||||
		left: true
 | 
			
		||||
		right: true
 | 
			
		||||
		top: true
 | 
			
		||||
		bottom: true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property var selectionArea: area
 | 
			
		||||
 | 
			
		||||
	Rectangle {
 | 
			
		||||
		id: area
 | 
			
		||||
		property bool selecting: false
 | 
			
		||||
		property bool initializing: false
 | 
			
		||||
		property bool locked: false
 | 
			
		||||
		property real startX: 0
 | 
			
		||||
		property real startY: 0
 | 
			
		||||
		property real endX: 0
 | 
			
		||||
		property real endY: 0
 | 
			
		||||
 | 
			
		||||
		readonly property bool bigEnough: width > 300 && height > 150
 | 
			
		||||
 | 
			
		||||
		border.color: bigEnough ? "#ee33ccff" : "#aa595959"
 | 
			
		||||
		border.width: 1
 | 
			
		||||
		radius: 5
 | 
			
		||||
		color: "#66001017"
 | 
			
		||||
		visible: selecting
 | 
			
		||||
 | 
			
		||||
		x: Math.min(startX, endX) - border.width
 | 
			
		||||
		y: Math.min(startY, endY) - border.width
 | 
			
		||||
		width: Math.max(startX, endX) - x + border.width * 2
 | 
			
		||||
		height: Math.max(startY, endY) - y + border.width * 2
 | 
			
		||||
 | 
			
		||||
		function startSelection(initialize: bool) {
 | 
			
		||||
			locked = false;
 | 
			
		||||
			if (!initialize) {
 | 
			
		||||
				selecting = true;
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			initializing = true
 | 
			
		||||
			if (selecting) {
 | 
			
		||||
				area.startX = mouseArea.mouseX;
 | 
			
		||||
				area.startY = mouseArea.mouseY;
 | 
			
		||||
				area.endX = mouseArea.mouseX;
 | 
			
		||||
				area.endY = mouseArea.mouseY;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function endSelection() {
 | 
			
		||||
			const wasSelecting = selecting;
 | 
			
		||||
			initializing = false;
 | 
			
		||||
 | 
			
		||||
			if (wasSelecting && bigEnough) {
 | 
			
		||||
				locked = true;
 | 
			
		||||
				selectionComplete(x + 1, y + 1, width - 2, height - 2);
 | 
			
		||||
			} else {
 | 
			
		||||
				selecting = false;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	MouseArea {
 | 
			
		||||
		id: mouseArea
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
		hoverEnabled: true
 | 
			
		||||
		onPositionChanged: {
 | 
			
		||||
			if (area.initializing) {
 | 
			
		||||
				if (!containsMouse) {
 | 
			
		||||
					area.initializing = false;
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				area.startX = mouseX;
 | 
			
		||||
				area.startY = mouseY;
 | 
			
		||||
				area.initializing = false;
 | 
			
		||||
				area.selecting = true;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (!selectionArea.locked) {
 | 
			
		||||
				area.endX = mouseX;
 | 
			
		||||
				area.endY = mouseY;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -24,16 +24,6 @@ Singleton {
 | 
			
		|||
		curve.type: Easing.InQuart
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property var time: new Date();
 | 
			
		||||
 | 
			
		||||
	Timer {
 | 
			
		||||
		interval: 1000
 | 
			
		||||
		running: true
 | 
			
		||||
		repeat: true
 | 
			
		||||
 | 
			
		||||
		onTriggered: time = new Date()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function interpolateColors(x: real, a: color, b: color): color {
 | 
			
		||||
		const xa = 1.0 - x;
 | 
			
		||||
		return Qt.rgba(a.r * xa + b.r * x, a.g * xa + b.g * x, a.b * xa + b.b * x, a.a * xa + b.a * x);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,14 @@
 | 
			
		|||
pragma Singleton
 | 
			
		||||
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Io
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
 | 
			
		||||
Singleton {
 | 
			
		||||
	readonly property alias termSelect: termSelectBind.pressed;
 | 
			
		||||
	signal screenshot();
 | 
			
		||||
 | 
			
		||||
	Shortcut {
 | 
			
		||||
		name: "screenshot"
 | 
			
		||||
		onPressed: screenshot()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Shortcut {
 | 
			
		||||
		id: termSelectBind
 | 
			
		||||
		name: "termselect"
 | 
			
		||||
	IpcHandler {
 | 
			
		||||
		target: "screenshot"
 | 
			
		||||
		function takeScreenshot() { screenshot(); }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
import Quickshell.Hyprland
 | 
			
		||||
 | 
			
		||||
GlobalShortcut {
 | 
			
		||||
	appid: "shell"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										112
									
								
								modules/user/modules/quickshell/shell/SlideView.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -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<SlideViewItem> 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();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								modules/user/modules/quickshell/shell/SlideViewItem.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
import Quickshell
 | 
			
		||||
import QtQuick
 | 
			
		||||
 | 
			
		||||
QtObject {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property Item item;
 | 
			
		||||
	property Animation activeAnimation: null;
 | 
			
		||||
	signal animationCompleted(self: SlideViewItem);
 | 
			
		||||
 | 
			
		||||
	property Connections __animConnection: Connections {
 | 
			
		||||
		target: activeAnimation
 | 
			
		||||
 | 
			
		||||
		function onStopped() {
 | 
			
		||||
			root.activeAnimation.destroy();
 | 
			
		||||
			root.animationCompleted(root);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function createAnimation(component: Component) {
 | 
			
		||||
		this.stopIfRunning();
 | 
			
		||||
		this.activeAnimation = component.createObject(this, { target: this.item });
 | 
			
		||||
		this.activeAnimation.running = true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function stopIfRunning() {
 | 
			
		||||
		if (this.activeAnimation) {
 | 
			
		||||
			this.activeAnimation.stop();
 | 
			
		||||
			this.activeAnimation = null;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function finishIfRunning() {
 | 
			
		||||
		if (this.activeAnimation) {
 | 
			
		||||
			// animator types dont handle complete correctly.
 | 
			
		||||
			this.activeAnimation.complete();
 | 
			
		||||
			this.activeAnimation.stop();
 | 
			
		||||
			this.item.x = 0;
 | 
			
		||||
			this.item.y = 0;
 | 
			
		||||
			this.activeAnimation = null;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function destroyAll() {
 | 
			
		||||
		this.item.destroy();
 | 
			
		||||
		this.destroy();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
pragma ComponentBehavior: Bound
 | 
			
		||||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
| 
						 | 
				
			
			@ -5,7 +6,8 @@ import Quickshell
 | 
			
		|||
import "systray" as SysTray
 | 
			
		||||
import "audio" as Audio
 | 
			
		||||
import "mpris" as Mpris
 | 
			
		||||
import "workspaces" as Workspaces
 | 
			
		||||
import "power" as Power
 | 
			
		||||
import "root:notifications" as Notifs
 | 
			
		||||
 | 
			
		||||
BarContainment {
 | 
			
		||||
	id: root
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +15,7 @@ BarContainment {
 | 
			
		|||
	property bool isSoleBar: Quickshell.screens.length == 1;
 | 
			
		||||
 | 
			
		||||
	ColumnLayout {
 | 
			
		||||
 | 
			
		||||
		anchors {
 | 
			
		||||
			left: parent.left
 | 
			
		||||
			right: parent.right
 | 
			
		||||
| 
						 | 
				
			
			@ -21,24 +24,32 @@ BarContainment {
 | 
			
		|||
 | 
			
		||||
		ColumnLayout {
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
			spacing: 0
 | 
			
		||||
 | 
			
		||||
			Loader {
 | 
			
		||||
				active: isSoleBar
 | 
			
		||||
				Layout.preferredHeight: active ? implicitHeight : 0;
 | 
			
		||||
			Notifs.NotificationWidget {
 | 
			
		||||
				Layout.fillWidth: true
 | 
			
		||||
 | 
			
		||||
				sourceComponent: Workspaces.Widget {
 | 
			
		||||
					bar: root
 | 
			
		||||
					wsBaseIndex: 1
 | 
			
		||||
				}
 | 
			
		||||
				bar: root
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Workspaces.Widget {
 | 
			
		||||
				bar: root
 | 
			
		||||
				Layout.fillWidth: true
 | 
			
		||||
				wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1;
 | 
			
		||||
				hideWhenEmpty: isSoleBar
 | 
			
		||||
			ColumnLayout {
 | 
			
		||||
				spacing: 0
 | 
			
		||||
 | 
			
		||||
				Loader {
 | 
			
		||||
					active: root.isSoleBar
 | 
			
		||||
					Layout.preferredHeight: active ? implicitHeight : 0;
 | 
			
		||||
					Layout.fillWidth: true
 | 
			
		||||
 | 
			
		||||
					sourceComponent: Workspaces {
 | 
			
		||||
						bar: root
 | 
			
		||||
						wsBaseIndex: 1
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Workspaces {
 | 
			
		||||
					bar: root
 | 
			
		||||
					Layout.fillWidth: true
 | 
			
		||||
					wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1;
 | 
			
		||||
					hideWhenEmpty: root.isSoleBar
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -65,8 +76,15 @@ BarContainment {
 | 
			
		|||
			Layout.fillWidth: true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ClockWidget {
 | 
			
		||||
		Power.Power {
 | 
			
		||||
			bar: root
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ClockWidget {
 | 
			
		||||
			bar: root
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										34
									
								
								modules/user/modules/quickshell/shell/bar/BarButton.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Effects
 | 
			
		||||
 | 
			
		||||
FullwidthMouseArea {
 | 
			
		||||
	id: root
 | 
			
		||||
	property bool showPressed: mouseArea.pressed;
 | 
			
		||||
	property real baseMargin: 0;
 | 
			
		||||
	property bool directScale: false;
 | 
			
		||||
 | 
			
		||||
	readonly property Item contentItem: mContentItem;
 | 
			
		||||
	default property alias contentItemData: mContentItem.data;
 | 
			
		||||
 | 
			
		||||
	property real targetBrightness: root.showPressed ? -25 : root.mouseArea.containsMouse && root.enabled ? 75 : 0
 | 
			
		||||
	Behavior on targetBrightness { SmoothedAnimation { velocity: 600 } }
 | 
			
		||||
 | 
			
		||||
	property real targetMargins: root.showPressed ? 3 : 0;
 | 
			
		||||
	Behavior on targetMargins { SmoothedAnimation { velocity: 25 } }
 | 
			
		||||
 | 
			
		||||
	hoverEnabled: true
 | 
			
		||||
 | 
			
		||||
	Item {
 | 
			
		||||
		id: mContentItem
 | 
			
		||||
		anchors.fill: parent;
 | 
			
		||||
 | 
			
		||||
		anchors.margins: root.baseMargin + (root.directScale ? 0 : root.targetMargins);
 | 
			
		||||
		scale: root.directScale ? (width - root.targetMargins * 2) / width : 1.0;
 | 
			
		||||
 | 
			
		||||
		opacity: root.enabled ? 1.0 : 0.5;
 | 
			
		||||
		Behavior on opacity { SmoothedAnimation { velocity: 5 } }
 | 
			
		||||
 | 
			
		||||
		layer.enabled: root.targetBrightness != 0
 | 
			
		||||
		layer.effect: MultiEffect { brightness: root.targetBrightness / 1000 }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Wayland
 | 
			
		||||
import "root:."
 | 
			
		||||
import "root:lock" as Lock
 | 
			
		||||
import ".."
 | 
			
		||||
import "../lock" as Lock
 | 
			
		||||
 | 
			
		||||
PanelWindow {
 | 
			
		||||
	id: root
 | 
			
		||||
| 
						 | 
				
			
			@ -20,8 +20,8 @@ PanelWindow {
 | 
			
		|||
	exclusiveZone: width - margins.left
 | 
			
		||||
 | 
			
		||||
	color: "transparent"
 | 
			
		||||
	WlrLayershell.namespace: "shell:bar"
 | 
			
		||||
 | 
			
		||||
	WlrLayershell.namespace: "shell:bar"
 | 
			
		||||
 | 
			
		||||
	readonly property var tooltip: tooltip;
 | 
			
		||||
	Tooltip {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,31 +0,0 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
 | 
			
		||||
BarWidgetInner {
 | 
			
		||||
	implicitHeight: 50
 | 
			
		||||
 | 
			
		||||
	SequentialAnimation on implicitHeight {
 | 
			
		||||
		loops: Animation.Infinite
 | 
			
		||||
		PropertyAnimation { to: 70; duration: 1000 }
 | 
			
		||||
		PropertyAnimation { to: 40; duration: 1000 }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property int len: 1
 | 
			
		||||
 | 
			
		||||
	Text {
 | 
			
		||||
		anchors.centerIn: parent
 | 
			
		||||
		text: `8${'='.repeat(len)}D`
 | 
			
		||||
		font.pointSize: 16
 | 
			
		||||
		color: "white"
 | 
			
		||||
 | 
			
		||||
		PropertyAnimation on rotation {
 | 
			
		||||
			loops: Animation.Infinite
 | 
			
		||||
			to: 365
 | 
			
		||||
			duration: 1000
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	MouseArea {
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		onClicked: len += 1;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								modules/user/modules/quickshell/shell/bar/ClickableIcon.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
 | 
			
		||||
BarButton {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property string image;
 | 
			
		||||
	property alias cache: imageComponent.cache;
 | 
			
		||||
	property alias asynchronous: imageComponent.asynchronous;
 | 
			
		||||
	property bool scaleIcon: !asynchronous
 | 
			
		||||
 | 
			
		||||
	Image {
 | 
			
		||||
		id: imageComponent
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
		source: root.image
 | 
			
		||||
		sourceSize.width: scaleIcon ? width : (root.width - baseMargin)
 | 
			
		||||
		sourceSize.height: scaleIcon ? height : (root.height - baseMargin)
 | 
			
		||||
		cache: false
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,41 +1,67 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import Quickshell
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
OverlayWidget {
 | 
			
		||||
	expandedWidth: 600
 | 
			
		||||
	expandedHeight: 600
 | 
			
		||||
BarWidgetInner {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property var bar;
 | 
			
		||||
 | 
			
		||||
	BarWidgetInner {
 | 
			
		||||
		implicitHeight: layout.implicitHeight
 | 
			
		||||
	implicitHeight: layout.implicitHeight
 | 
			
		||||
 | 
			
		||||
		ColumnLayout {
 | 
			
		||||
			id: layout
 | 
			
		||||
			spacing: 0
 | 
			
		||||
	SystemClock {
 | 
			
		||||
		id: clock
 | 
			
		||||
		precision: tooltip.visible ? SystemClock.Seconds : SystemClock.Minutes;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
			anchors {
 | 
			
		||||
				right: parent.right
 | 
			
		||||
				left: parent.left
 | 
			
		||||
	BarButton {
 | 
			
		||||
		id: button
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		fillWindowWidth: true
 | 
			
		||||
		acceptedButtons: Qt.NoButton
 | 
			
		||||
 | 
			
		||||
   	ColumnLayout {
 | 
			
		||||
   		id: layout
 | 
			
		||||
   		spacing: 0
 | 
			
		||||
 | 
			
		||||
   		anchors {
 | 
			
		||||
   			right: parent.right
 | 
			
		||||
   			left: parent.left
 | 
			
		||||
   		}
 | 
			
		||||
 | 
			
		||||
   		Text {
 | 
			
		||||
   			Layout.alignment: Qt.AlignHCenter
 | 
			
		||||
   			text: {
 | 
			
		||||
   				const hours = clock.hours.toString().padStart(2, '0')
 | 
			
		||||
   				const minutes = clock.minutes.toString().padStart(2, '0')
 | 
			
		||||
   				return `${hours}\n${minutes}`
 | 
			
		||||
   			}
 | 
			
		||||
   			font.pointSize: 18
 | 
			
		||||
   			color: "white"
 | 
			
		||||
   		}
 | 
			
		||||
   	}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property TooltipItem tooltip: TooltipItem {
 | 
			
		||||
		id: tooltip
 | 
			
		||||
		tooltip: bar.tooltip
 | 
			
		||||
		owner: root
 | 
			
		||||
		show: button.containsMouse
 | 
			
		||||
 | 
			
		||||
		Loader {
 | 
			
		||||
			active: tooltip.visible
 | 
			
		||||
			sourceComponent: Label {
 | 
			
		||||
				text: {
 | 
			
		||||
					// SystemClock can send an update slightly (<50ms) before the
 | 
			
		||||
					// time changes. We use its readout so the widget and tooltip match.
 | 
			
		||||
					const hours = clock.hours.toString().padStart(2, '0');
 | 
			
		||||
					const minutes = clock.minutes.toString().padStart(2, '0');
 | 
			
		||||
					const seconds = clock.seconds.toString().padStart(2, '0');
 | 
			
		||||
 | 
			
		||||
					return `${hours}:${minutes}:${seconds}\n` + new Date().toLocaleString(Qt.locale("en_US"), "dddd, MMMM d, yyyy");
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Text {
 | 
			
		||||
				Layout.alignment: Qt.AlignHCenter
 | 
			
		||||
				text: ShellGlobals.time.getHours().toString().padStart(2, '0')
 | 
			
		||||
				font.pointSize: 18
 | 
			
		||||
				color: "#a0ffffff"
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Text {
 | 
			
		||||
				Layout.alignment: Qt.AlignHCenter
 | 
			
		||||
				text: ShellGlobals.time.getMinutes().toString().padStart(2, '0')
 | 
			
		||||
				font.pointSize: 18
 | 
			
		||||
				color: "#a0ffffff"
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		MouseArea {
 | 
			
		||||
			anchors.fill: parent
 | 
			
		||||
			onClicked: expanded = !expanded
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,97 +0,0 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	required property var bar;
 | 
			
		||||
	required property real expandedWidth;
 | 
			
		||||
	required property real expandedHeight;
 | 
			
		||||
	required default property Item widget;
 | 
			
		||||
 | 
			
		||||
	property bool expanded: false;
 | 
			
		||||
 | 
			
		||||
	onExpandedChanged: {
 | 
			
		||||
		animateTo(expanded ? 1.0 : 0.0)
 | 
			
		||||
		if (expanded) popupSurface.activeOverlay = this
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	readonly property bool fullyCollapsed: animationProgress == 0.0;
 | 
			
		||||
 | 
			
		||||
	onFullyCollapsedChanged: {
 | 
			
		||||
		if (fullyCollapsed && popupSurface.activeOverlay == this) {
 | 
			
		||||
			popupSurface.activeOverlay = null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/*if (fullyCollapsed) {
 | 
			
		||||
			widget.x = Qt.binding(() => this.x)
 | 
			
		||||
		}*/
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	readonly property rect collapsedLayerRect: {
 | 
			
		||||
		void [barWindow.windowTransform, popupSurface.windowTransform, y, parent.y];
 | 
			
		||||
		return this.mapToItem(popupSurface.contentItem, 0, 0, width, height);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onCollapsedLayerRectChanged: console.log(`clr: ${collapsedLayerRect}`)
 | 
			
		||||
	onLayerRectChanged: console.log(`lr: ${layerRect}`)
 | 
			
		||||
	onYChanged: console.log(`y: ${y}`)
 | 
			
		||||
 | 
			
		||||
	readonly property rect expandedLayerRect: bar.widgetSurface.expandedPosition(this)
 | 
			
		||||
 | 
			
		||||
	readonly property rect layerRect: {
 | 
			
		||||
		const [p, xCurve, yCurve] = [animationProgress, ShellGlobals.popoutXCurve, ShellGlobals.popoutYCurve];
 | 
			
		||||
 | 
			
		||||
		return Qt.rect(
 | 
			
		||||
			xCurve.interpolate(p, collapsedLayerRect.x, expandedLayerRect.x),
 | 
			
		||||
			yCurve.interpolate(p, collapsedLayerRect.y, expandedLayerRect.y),
 | 
			
		||||
			xCurve.interpolate(p, collapsedLayerRect.width, expandedLayerRect.width),
 | 
			
		||||
			yCurve.interpolate(p, collapsedLayerRect.height, expandedLayerRect.height),
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	implicitWidth: widget.implicitWidth
 | 
			
		||||
	implicitHeight: widget.implicitHeight
 | 
			
		||||
 | 
			
		||||
	Component.onCompleted: {
 | 
			
		||||
		popupSurface.connectOverlay(this);
 | 
			
		||||
		widget.x = Qt.binding(() => layerRect.x);
 | 
			
		||||
		widget.y = Qt.binding(() => layerRect.y);
 | 
			
		||||
		widget.width = Qt.binding(() => layerRect.width);
 | 
			
		||||
		widget.height = Qt.binding(() => layerRect.height);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Component.onDestruction: {
 | 
			
		||||
		popupSurface.disconnectOverlay(this)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function animateTo(target: real) {
 | 
			
		||||
		animationProgressInternal = target * 1000
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property real animationProgress: animationProgressInternal * 0.001
 | 
			
		||||
	property real animationProgressInternal: 0.0 // animations seem to only have int precision
 | 
			
		||||
 | 
			
		||||
	Behavior on animationProgressInternal {
 | 
			
		||||
		SmoothedAnimation { velocity: 3000 }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	MouseArea {
 | 
			
		||||
		id: mouseArea
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		hoverEnabled: true
 | 
			
		||||
		onPressed: expanded = false
 | 
			
		||||
 | 
			
		||||
		Rectangle {
 | 
			
		||||
			anchors.fill: parent
 | 
			
		||||
			border.color: ShellGlobals.colors.widgetOutline
 | 
			
		||||
			border.width: 1
 | 
			
		||||
			radius: 5
 | 
			
		||||
			color: "transparent"
 | 
			
		||||
			opacity: mouseArea.containsMouse ? 1.0 : 0.0
 | 
			
		||||
 | 
			
		||||
			Behavior on opacity {
 | 
			
		||||
				SmoothedAnimation { velocity: 4 }
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,17 +0,0 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	default property Item item;
 | 
			
		||||
	property int expandedWidth;
 | 
			
		||||
	property int expandedHeight;
 | 
			
		||||
 | 
			
		||||
	implicitHeight: item.implicitHeight
 | 
			
		||||
	implicitWidth: item.implicitWidth
 | 
			
		||||
 | 
			
		||||
	Component.onCompleted: {
 | 
			
		||||
		item.width = Qt.binding(() => this.width)
 | 
			
		||||
		item.height = Qt.binding(() => this.height)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	children: [ item ]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
import "root:/"
 | 
			
		||||
 | 
			
		||||
Scope {
 | 
			
		||||
	id: root
 | 
			
		||||
| 
						 | 
				
			
			@ -11,11 +12,14 @@ Scope {
 | 
			
		|||
 | 
			
		||||
	readonly property TooltipItem activeItem: activeMenu ?? activeTooltip;
 | 
			
		||||
	property TooltipItem lastActiveItem: null;
 | 
			
		||||
	readonly property TooltipItem shownItem: activeItem ?? lastActiveItem;
 | 
			
		||||
	property real hangTime: lastActiveItem?.hangTime ?? 0;
 | 
			
		||||
 | 
			
		||||
	property Item tooltipItem: null;
 | 
			
		||||
 | 
			
		||||
	onActiveItemChanged: {
 | 
			
		||||
		if (activeItem != null) {
 | 
			
		||||
			hangTimer.stop();
 | 
			
		||||
			activeItem.targetVisible = true;
 | 
			
		||||
 | 
			
		||||
			if (tooltipItem) {
 | 
			
		||||
| 
						 | 
				
			
			@ -24,10 +28,12 @@ Scope {
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		if (lastActiveItem != null && lastActiveItem != activeItem) {
 | 
			
		||||
			lastActiveItem.targetVisible = false;
 | 
			
		||||
			if (activeItem != null) lastActiveItem.targetVisible = false;
 | 
			
		||||
			else if (root.hangTime == 0) doLastHide();
 | 
			
		||||
			else hangTimer.start();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		lastActiveItem = activeItem;
 | 
			
		||||
		if (activeItem != null) lastActiveItem = activeItem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function setItem(item: TooltipItem) {
 | 
			
		||||
| 
						 | 
				
			
			@ -46,28 +52,59 @@ Scope {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function doLastHide() {
 | 
			
		||||
		lastActiveItem.targetVisible = false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function onHidden(item: TooltipItem) {
 | 
			
		||||
		if (item == lastActiveItem) {
 | 
			
		||||
			lastActiveItem = null;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Timer {
 | 
			
		||||
		id: hangTimer
 | 
			
		||||
		interval: root.hangTime
 | 
			
		||||
		onTriggered: doLastHide();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0;
 | 
			
		||||
	Behavior on scaleMul { SmoothedAnimation { velocity: 5 } }
 | 
			
		||||
 | 
			
		||||
	LazyLoader {
 | 
			
		||||
		id: popupLoader
 | 
			
		||||
		activeAsync: activeItem != null
 | 
			
		||||
		activeAsync: shownItem != null
 | 
			
		||||
 | 
			
		||||
		PopupWindow {
 | 
			
		||||
			id: popup
 | 
			
		||||
			parentWindow: bar
 | 
			
		||||
			relativeX: bar.tooltipXOffset
 | 
			
		||||
			relativeY: 0
 | 
			
		||||
			height: bar.height
 | 
			
		||||
			width: 1000//Math.max(1, widthAnim.running ? Math.max(tooltipItem.targetWidth, tooltipItem.lastTargetWidth) : tooltipItem.targetWidth)
 | 
			
		||||
 | 
			
		||||
			anchor {
 | 
			
		||||
				window: bar
 | 
			
		||||
				rect.x: bar.tooltipXOffset
 | 
			
		||||
				rect.y: tooltipItem.highestAnimY
 | 
			
		||||
				adjustment: PopupAdjustment.None
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			HyprlandWindow.opacity: root.scaleMul
 | 
			
		||||
 | 
			
		||||
			//height: bar.height
 | 
			
		||||
			width: Math.max(700, tooltipItem.largestAnimWidth) // max due to qtwayland glitches
 | 
			
		||||
			height: {
 | 
			
		||||
				const h = tooltipItem.lowestAnimY - tooltipItem.highestAnimY
 | 
			
		||||
				//console.log(`seth ${h} ${tooltipItem.highestAnimY} ${tooltipItem.lowestAnimY}; ${tooltipItem.y1} ${tooltipItem.y2}`)
 | 
			
		||||
				return h
 | 
			
		||||
			}
 | 
			
		||||
			visible: true
 | 
			
		||||
			color: "transparent"
 | 
			
		||||
			//color: "#20000000"
 | 
			
		||||
 | 
			
		||||
			mask: Region {
 | 
			
		||||
				item: (activeItem?.hoverable ?? false) ? tooltipItem : null
 | 
			
		||||
				item: (shownItem?.hoverable ?? false) ? tooltipItem : null
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			HyprlandFocusGrab {
 | 
			
		||||
				active: activeItem?.isMenu ?? false
 | 
			
		||||
				windows: [ popup, bar ]
 | 
			
		||||
				windows: [ popup, bar, ...(activeItem?.grabWindows ?? []) ]
 | 
			
		||||
				onActiveChanged: {
 | 
			
		||||
					if (!active && activeItem?.isMenu) {
 | 
			
		||||
						activeMenu.close()
 | 
			
		||||
| 
						 | 
				
			
			@ -75,30 +112,76 @@ Scope {
 | 
			
		|||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			/*Rectangle {
 | 
			
		||||
				color: "#10ff0000"
 | 
			
		||||
				//y: tooltipItem.highestAnimY
 | 
			
		||||
				height: tooltipItem.lowestAnimY - tooltipItem.highestAnimY
 | 
			
		||||
				width: parent.width
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Rectangle {
 | 
			
		||||
				color: "#1000ff00"
 | 
			
		||||
				//y: tooltipItem.highestAnimY
 | 
			
		||||
				height: popup.height
 | 
			
		||||
				width: parent.width
 | 
			
		||||
			}*/
 | 
			
		||||
 | 
			
		||||
			Item {
 | 
			
		||||
				id: tooltipItem
 | 
			
		||||
				Component.onCompleted: {
 | 
			
		||||
					root.tooltipItem = this;
 | 
			
		||||
					if (root.activeItem) {
 | 
			
		||||
						root.activeItem.parent = this;
 | 
			
		||||
					if (root.shownItem) {
 | 
			
		||||
						root.shownItem.parent = this;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					//highestAnimY = targetY - targetHeight / 2;
 | 
			
		||||
					//lowestAnimY = targetY + targetHeight / 2;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				transform: Scale {
 | 
			
		||||
					origin.x: 0
 | 
			
		||||
					origin.y: tooltipItem.height / 2
 | 
			
		||||
					xScale: 0.9 + scaleMul * 0.1
 | 
			
		||||
					yScale: xScale
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				clip: width != targetWidth || height != targetHeight
 | 
			
		||||
 | 
			
		||||
				// bkg
 | 
			
		||||
				BarWidgetInner {
 | 
			
		||||
					anchors.fill: parent
 | 
			
		||||
					color: ShellGlobals.colors.bar
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				readonly property var targetWidth: shownItem?.implicitWidth ?? 0;
 | 
			
		||||
				readonly property var targetHeight: shownItem?.implicitHeight ?? 0;
 | 
			
		||||
 | 
			
		||||
				property var largestAnimWidth: 0;
 | 
			
		||||
				property var highestAnimY: 0; // unused due to reposition timing issues
 | 
			
		||||
				property var lowestAnimY: bar.height;
 | 
			
		||||
 | 
			
		||||
				onTargetWidthChanged: {
 | 
			
		||||
					if (targetWidth > largestAnimWidth) {
 | 
			
		||||
						largestAnimWidth = targetWidth;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				readonly property var targetWidth: activeItem?.implicitWidth ?? 0;
 | 
			
		||||
				readonly property var targetHeight: activeItem?.implicitHeight ?? 0;
 | 
			
		||||
				onTargetYChanged: updateYBounds();
 | 
			
		||||
				onTargetHeightChanged: updateYBounds();
 | 
			
		||||
				function updateYBounds() {
 | 
			
		||||
					if (targetY - targetHeight / 2 < highestAnimY) {
 | 
			
		||||
						//highestAnimY = targetY - targetHeight / 2
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
				property var lastTargetWidthTracker: 0;
 | 
			
		||||
				property var lastTargetWidth: 0;
 | 
			
		||||
 | 
			
		||||
				onTargetWidthChanged: {
 | 
			
		||||
					lastTargetWidth = lastTargetWidthTracker;
 | 
			
		||||
					lastTargetWidthTracker = targetWidth;
 | 
			
		||||
					if (targetY + targetHeight / 2 > lowestAnimY) {
 | 
			
		||||
						//lowestAnimY = targetY + targetHeight / 2
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				readonly property real targetY: {
 | 
			
		||||
					if (activeItem == null) return 0;
 | 
			
		||||
					const target = bar.contentItem.mapFromItem(activeItem.owner, 0, activeItem.targetRelativeY).y;
 | 
			
		||||
					return bar.boundedY(target, activeItem.implicitHeight / 2);
 | 
			
		||||
					if (shownItem == null) return 0;
 | 
			
		||||
					const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y;
 | 
			
		||||
					return bar.boundedY(target, shownItem.implicitHeight / 2);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				property var w: -1
 | 
			
		||||
| 
						 | 
				
			
			@ -107,15 +190,24 @@ Scope {
 | 
			
		|||
				property var y1: -1
 | 
			
		||||
				property var y2: -1
 | 
			
		||||
 | 
			
		||||
				y: y1
 | 
			
		||||
				y: y1 - popup.anchor.rect.y
 | 
			
		||||
				height: y2 - y1
 | 
			
		||||
 | 
			
		||||
				SmoothedAnimation {
 | 
			
		||||
					target: tooltipItem;
 | 
			
		||||
					property: "y1";
 | 
			
		||||
				readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running
 | 
			
		||||
 | 
			
		||||
				onAnyAnimsRunningChanged: {
 | 
			
		||||
					if (!anyAnimsRunning) {
 | 
			
		||||
						largestAnimWidth = targetWidth
 | 
			
		||||
						//highestAnimY = y1;
 | 
			
		||||
						//lowestAnimY = y2;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				SmoothedAnimation on y1 {
 | 
			
		||||
					id: y1Anim
 | 
			
		||||
					to: tooltipItem.targetY - tooltipItem.targetHeight / 2;
 | 
			
		||||
					onToChanged: {
 | 
			
		||||
						if (tooltipItem.y1 == -1 || !(activeItem?.animateSize ?? true)) {
 | 
			
		||||
						if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) {
 | 
			
		||||
							stop();
 | 
			
		||||
							tooltipItem.y1 = to;
 | 
			
		||||
						} else {
 | 
			
		||||
| 
						 | 
				
			
			@ -125,12 +217,11 @@ Scope {
 | 
			
		|||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				SmoothedAnimation {
 | 
			
		||||
					target: tooltipItem
 | 
			
		||||
					property: "y2"
 | 
			
		||||
				SmoothedAnimation on y2 {
 | 
			
		||||
					id: y2Anim
 | 
			
		||||
					to: tooltipItem.targetY + tooltipItem.targetHeight / 2;
 | 
			
		||||
					onToChanged: {
 | 
			
		||||
						if (tooltipItem.y2 == -1 || !(activeItem?.animateSize ?? true)) {
 | 
			
		||||
						if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) {
 | 
			
		||||
							stop();
 | 
			
		||||
							tooltipItem.y2 = to;
 | 
			
		||||
						} else {
 | 
			
		||||
| 
						 | 
				
			
			@ -140,13 +231,11 @@ Scope {
 | 
			
		|||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				SmoothedAnimation {
 | 
			
		||||
				SmoothedAnimation on w {
 | 
			
		||||
					id: widthAnim
 | 
			
		||||
					target: tooltipItem
 | 
			
		||||
					property: "w"
 | 
			
		||||
					to: tooltipItem.targetWidth;
 | 
			
		||||
					onToChanged: {
 | 
			
		||||
						if (tooltipItem.w == -1) {
 | 
			
		||||
						if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) {
 | 
			
		||||
							stop();
 | 
			
		||||
							tooltipItem.w = to;
 | 
			
		||||
						} else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,12 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import "root:/"
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property var tooltip;
 | 
			
		||||
	required property Item owner;
 | 
			
		||||
	property bool isMenu: false;
 | 
			
		||||
	property list<QtObject> grabWindows;
 | 
			
		||||
	property bool hoverable: isMenu;
 | 
			
		||||
	property bool animateSize: true;
 | 
			
		||||
	property bool show: false;
 | 
			
		||||
| 
						 | 
				
			
			@ -17,47 +17,35 @@ Item {
 | 
			
		|||
 | 
			
		||||
	signal close();
 | 
			
		||||
 | 
			
		||||
	readonly property alias contentItem: contentItem;
 | 
			
		||||
	default property alias data: contentItem.data;
 | 
			
		||||
 | 
			
		||||
	property Component backgroundComponent: BarWidgetInner {
 | 
			
		||||
		color: ShellGlobals.colors.bar
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
	}
 | 
			
		||||
	property Component backgroundComponent: null
 | 
			
		||||
 | 
			
		||||
	onShowChanged: {
 | 
			
		||||
		if (show) {
 | 
			
		||||
			hangTimer.stop();
 | 
			
		||||
			tooltip.setItem(this);
 | 
			
		||||
		} else if (hangTime == 0) {
 | 
			
		||||
			tooltip.removeItem(this);
 | 
			
		||||
		} else hangTimer.start();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Timer {
 | 
			
		||||
		id: hangTimer
 | 
			
		||||
		interval: hangTime
 | 
			
		||||
		onTriggered: tooltip.removeItem(root);
 | 
			
		||||
		if (show) tooltip.setItem(this);
 | 
			
		||||
		else tooltip.removeItem(this);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property bool targetVisible: false
 | 
			
		||||
	property real targetOpacity: 0
 | 
			
		||||
	opacity: targetOpacity / 1000
 | 
			
		||||
	opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul))
 | 
			
		||||
 | 
			
		||||
	Behavior on targetOpacity {
 | 
			
		||||
		id: opacityAnimation
 | 
			
		||||
		SmoothedAnimation { velocity: 5000 }
 | 
			
		||||
		SmoothedAnimation { velocity: 5 }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function snapOpacity(opacity: real) {
 | 
			
		||||
		opacityAnimation.enabled = false;
 | 
			
		||||
		targetOpacity = opacity * 1000
 | 
			
		||||
		targetOpacity = opacity;
 | 
			
		||||
		opacityAnimation.enabled = true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onTargetVisibleChanged: {
 | 
			
		||||
		if (targetVisible) {
 | 
			
		||||
			visible = true;
 | 
			
		||||
			targetOpacity = 1000;
 | 
			
		||||
			targetOpacity = 1;
 | 
			
		||||
		} else {
 | 
			
		||||
			close()
 | 
			
		||||
			targetOpacity = 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -68,20 +56,21 @@ Item {
 | 
			
		|||
		if (!targetVisible && targetOpacity == 0) {
 | 
			
		||||
			visible = false;
 | 
			
		||||
			this.parent = null;
 | 
			
		||||
			if (tooltip) tooltip.onHidden(this);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	anchors.fill: parent
 | 
			
		||||
	visible: false
 | 
			
		||||
	clip: true
 | 
			
		||||
	implicitHeight: contentItem.implicitHeight + 10
 | 
			
		||||
	implicitWidth: contentItem.implicitWidth + 10
 | 
			
		||||
	//clip: true
 | 
			
		||||
	implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
 | 
			
		||||
	implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
 | 
			
		||||
 | 
			
		||||
	readonly property Item item: contentItem;
 | 
			
		||||
 | 
			
		||||
	Loader {
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		active: root.visible || root.preloadBackground
 | 
			
		||||
		active: root.backgroundComponent && (root.visible || root.preloadBackground)
 | 
			
		||||
		asynchronous: !root.visible && root.preloadBackground
 | 
			
		||||
		sourceComponent: backgroundComponent
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -91,7 +80,7 @@ Item {
 | 
			
		|||
		anchors.fill: parent
 | 
			
		||||
		anchors.margins: 5
 | 
			
		||||
 | 
			
		||||
		implicitHeight: childrenRect.height
 | 
			
		||||
		implicitWidth: childrenRect.width
 | 
			
		||||
		implicitHeight: children[0].implicitHeight
 | 
			
		||||
		implicitWidth: children[0].implicitWidth
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,21 @@
 | 
			
		|||
pragma ComponentBehavior: Bound;
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
import ".."
 | 
			
		||||
import "root:."
 | 
			
		||||
 | 
			
		||||
MouseArea {
 | 
			
		||||
FullwidthMouseArea {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property var bar;
 | 
			
		||||
	required property int wsBaseIndex;
 | 
			
		||||
	property int wsCount: 10;
 | 
			
		||||
	property bool hideWhenEmpty: false;
 | 
			
		||||
 | 
			
		||||
	implicitHeight: column.implicitHeight + 10;
 | 
			
		||||
 | 
			
		||||
	fillWindowWidth: true
 | 
			
		||||
	acceptedButtons: Qt.NoButton
 | 
			
		||||
 | 
			
		||||
	onWheel: event => {
 | 
			
		||||
| 
						 | 
				
			
			@ -29,9 +33,6 @@ MouseArea {
 | 
			
		|||
	property int existsCount: 0;
 | 
			
		||||
	visible: !hideWhenEmpty || existsCount > 0;
 | 
			
		||||
 | 
			
		||||
	property real animPos: 0;
 | 
			
		||||
	Behavior on animPos { SmoothedAnimation { velocity: 100 } }
 | 
			
		||||
 | 
			
		||||
	// destructor takes care of nulling
 | 
			
		||||
	signal workspaceAdded(workspace: HyprlandWorkspace);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -45,20 +46,22 @@ MouseArea {
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		Repeater {
 | 
			
		||||
			model: 10
 | 
			
		||||
			model: root.wsCount
 | 
			
		||||
 | 
			
		||||
			MouseArea {
 | 
			
		||||
			FullwidthMouseArea {
 | 
			
		||||
				id: wsItem
 | 
			
		||||
				onPressed: Hyprland.dispatch(`workspace ${wsIndex}`);
 | 
			
		||||
 | 
			
		||||
				Layout.fillWidth: true
 | 
			
		||||
				implicitHeight: 15
 | 
			
		||||
 | 
			
		||||
				fillWindowWidth: true
 | 
			
		||||
 | 
			
		||||
				required property int index;
 | 
			
		||||
				property int wsIndex: wsBaseIndex + index;
 | 
			
		||||
				property int wsIndex: root.wsBaseIndex + index;
 | 
			
		||||
				property HyprlandWorkspace workspace: null;
 | 
			
		||||
				property bool exists: workspace != null;
 | 
			
		||||
				property bool active: (monitor?.activeWorkspace ?? false) && monitor.activeWorkspace == workspace;
 | 
			
		||||
				property bool active: (root.monitor?.activeWorkspace ?? false) && root.monitor.activeWorkspace == workspace;
 | 
			
		||||
 | 
			
		||||
				onActiveChanged: {
 | 
			
		||||
					if (active) root.currentIndex = wsIndex;
 | 
			
		||||
| 
						 | 
				
			
			@ -78,21 +81,21 @@ MouseArea {
 | 
			
		|||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				property real animActive: active ? 100 : 0
 | 
			
		||||
				Behavior on animActive { NumberAnimation { duration: 100 } }
 | 
			
		||||
				property real animActive: active ? 1 : 0
 | 
			
		||||
				Behavior on animActive { NumberAnimation { duration: 150 } }
 | 
			
		||||
 | 
			
		||||
				property real animExists: exists ? 100 : 0
 | 
			
		||||
				property real animExists: exists ? 1 : 0
 | 
			
		||||
				Behavior on animExists { NumberAnimation { duration: 100 } }
 | 
			
		||||
 | 
			
		||||
				Rectangle {
 | 
			
		||||
					anchors.centerIn: parent
 | 
			
		||||
					height: 10
 | 
			
		||||
					width: parent.width
 | 
			
		||||
					scale: 1 + animActive * 0.003
 | 
			
		||||
					scale: 1 + wsItem.animActive * 0.3
 | 
			
		||||
					radius: height / 2
 | 
			
		||||
					border.color: ShellGlobals.colors.widgetOutline
 | 
			
		||||
					border.width: 1
 | 
			
		||||
					color: ShellGlobals.interpolateColors(animExists * 0.01, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
 | 
			
		||||
					color: ShellGlobals.interpolateColors(animExists, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -12,14 +12,20 @@ ClickableIcon {
 | 
			
		|||
 | 
			
		||||
	implicitHeight: width;
 | 
			
		||||
	acceptedButtons: Qt.LeftButton | Qt.RightButton;
 | 
			
		||||
	showPressed: mixerOpen
 | 
			
		||||
	fillWindowWidth: true
 | 
			
		||||
	showPressed: mixerOpen || (pressedButtons & ~Qt.RightButton)
 | 
			
		||||
 | 
			
		||||
	onPressed: event => {
 | 
			
		||||
		event.accepted = true;
 | 
			
		||||
		if (event.button === Qt.RightButton) {
 | 
			
		||||
			mixerOpen = !mixerOpen;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onClicked: event => {
 | 
			
		||||
		event.accepted = true;
 | 
			
		||||
		if (event.button === Qt.LeftButton) {
 | 
			
		||||
			event.accepted = true;
 | 
			
		||||
			node.audio.muted = !node.audio.muted;
 | 
			
		||||
		} else if (event.button === Qt.RightButton) {
 | 
			
		||||
			mixerOpen = !mixerOpen;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +77,16 @@ ClickableIcon {
 | 
			
		|||
			sourceComponent: Mixer {
 | 
			
		||||
				width: 550
 | 
			
		||||
				trackedNode: node
 | 
			
		||||
				nodeList: Pipewire.nodes.values.filter(n => n.audio && !n.isStream && n.isSink == node.isSink)
 | 
			
		||||
				nodeImage: root.image
 | 
			
		||||
 | 
			
		||||
				onSelected: n => {
 | 
			
		||||
					if (node.isSink) {
 | 
			
		||||
						Pipewire.preferredDefaultAudioSink = n;
 | 
			
		||||
					} else {
 | 
			
		||||
						Pipewire.preferredDefaultAudioSource = n;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,18 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Services.Pipewire
 | 
			
		||||
import ".."
 | 
			
		||||
import "../.."
 | 
			
		||||
 | 
			
		||||
ColumnLayout {
 | 
			
		||||
	id: root
 | 
			
		||||
 | 
			
		||||
	required property PwNode trackedNode;
 | 
			
		||||
	required property string nodeImage;
 | 
			
		||||
	required property list<PwNode> nodeList;
 | 
			
		||||
 | 
			
		||||
	signal selected(node: PwNode);
 | 
			
		||||
 | 
			
		||||
	PwNodeLinkTracker {
 | 
			
		||||
		id: linkTracker
 | 
			
		||||
| 
						 | 
				
			
			@ -15,10 +21,13 @@ ColumnLayout {
 | 
			
		|||
 | 
			
		||||
	PwObjectTracker { objects: [ trackedNode, ...linkTracker.linkGroups ] }
 | 
			
		||||
 | 
			
		||||
	MixerEntry {
 | 
			
		||||
	MixerEntry/*WithSelect*/ {
 | 
			
		||||
		id: nodeEntry
 | 
			
		||||
		node: trackedNode
 | 
			
		||||
		//nodeList: root.nodeList
 | 
			
		||||
		image: nodeImage
 | 
			
		||||
 | 
			
		||||
		Component.onCompleted: this.selected.connect(root.selected);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Rectangle {
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +58,7 @@ ColumnLayout {
 | 
			
		|||
				// special cases :(
 | 
			
		||||
				if (icon == "firefox") icon = "firefox-devedition";
 | 
			
		||||
 | 
			
		||||
				return `image://icon/${icon}`
 | 
			
		||||
				return Quickshell.iconPath(icon)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,51 +1,11 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell.Services.Pipewire
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
RowLayout {
 | 
			
		||||
MixerEntryBase {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property PwNode node;
 | 
			
		||||
	required property string image;
 | 
			
		||||
	property int state: PwLinkState.Unlinked;
 | 
			
		||||
 | 
			
		||||
	PwObjectTracker { objects: [ node ] }
 | 
			
		||||
 | 
			
		||||
	ClickableIcon {
 | 
			
		||||
		image: root.image
 | 
			
		||||
		asynchronous: true
 | 
			
		||||
		implicitHeight: 40
 | 
			
		||||
		implicitWidth: height
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ColumnLayout {
 | 
			
		||||
		RowLayout {
 | 
			
		||||
			Item {
 | 
			
		||||
				implicitHeight: title.implicitHeight
 | 
			
		||||
				Layout.fillWidth: true
 | 
			
		||||
 | 
			
		||||
				Text {
 | 
			
		||||
					id: title
 | 
			
		||||
					color: "white"
 | 
			
		||||
					anchors.fill: parent
 | 
			
		||||
					elide: Text.ElideRight
 | 
			
		||||
					text: {
 | 
			
		||||
						const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description);
 | 
			
		||||
						const mediaName = node.properties["media.name"];
 | 
			
		||||
 | 
			
		||||
						return mediaName != undefined ? `${name} - ${mediaName}` : name;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		VolumeSlider {
 | 
			
		||||
			//Layout.fillHeight: true
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
			implicitWidth: 200
 | 
			
		||||
 | 
			
		||||
			value: node.audio.volume
 | 
			
		||||
			onValueChanged: node.audio.volume = value
 | 
			
		||||
		}
 | 
			
		||||
	headerComponent: Text {
 | 
			
		||||
		color: "white"
 | 
			
		||||
		elide: Text.ElideRight
 | 
			
		||||
		text: root.getNodeName(root.node)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell.Services.Pipewire
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
RowLayout {
 | 
			
		||||
	id: root
 | 
			
		||||
 | 
			
		||||
	required property PwNode node;
 | 
			
		||||
	required property string image;
 | 
			
		||||
	required property Item headerComponent;
 | 
			
		||||
 | 
			
		||||
	property int state: PwLinkState.Unlinked;
 | 
			
		||||
 | 
			
		||||
	function getNodeName(node: PwNode): string {
 | 
			
		||||
		const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description);
 | 
			
		||||
		const mediaName = node.properties["media.name"];
 | 
			
		||||
 | 
			
		||||
		return mediaName != undefined ? `${name} - ${mediaName}` : name + node.id;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	PwObjectTracker { objects: [ node ] }
 | 
			
		||||
 | 
			
		||||
	ClickableIcon {
 | 
			
		||||
		image: root.image
 | 
			
		||||
		asynchronous: true
 | 
			
		||||
		implicitHeight: 40
 | 
			
		||||
		implicitWidth: height
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ColumnLayout {
 | 
			
		||||
		Item {
 | 
			
		||||
			id: container
 | 
			
		||||
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
			implicitWidth: headerComponent.implicitWidth
 | 
			
		||||
			implicitHeight: headerComponent.implicitHeight
 | 
			
		||||
 | 
			
		||||
			children: [ headerComponent ]
 | 
			
		||||
			Binding { root.headerComponent.anchors.fill: container }
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		VolumeSlider {
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
 | 
			
		||||
			value: node.audio.volume
 | 
			
		||||
			onValueChanged: node.audio.volume = value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import Quickshell.Services.Pipewire
 | 
			
		||||
 | 
			
		||||
MixerEntryBase {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property list<PwNode> nodeList;
 | 
			
		||||
 | 
			
		||||
	signal selected(node: PwNode);
 | 
			
		||||
 | 
			
		||||
	headerComponent: ComboBox {
 | 
			
		||||
		model: nodeList.map(node => root.getNodeName(node));
 | 
			
		||||
		currentIndex: nodeList.findIndex(node => node == root.node)
 | 
			
		||||
		onActivated: index => root.selected(nodeList[index])
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1 +0,0 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e8eaed"><path d="M806-56 677.67-184.33q-27 18.66-58 32.16-31 13.5-64.34 21.17v-68.67q20-6.33 38.84-13.66 18.83-7.34 35.5-19l-154.34-155V-160l-200-200h-160v-240H262L51.33-810.67 98.67-858l754.66 754L806-56Zm-26.67-232-48-48q19-33 28.17-69.62 9.17-36.61 9.17-75.38 0-100.22-58.34-179.11Q652-739 555.33-762.33V-831q124 28 202 125.5t78 224.5q0 51.67-14.16 100.67-14.17 49-41.84 92.33Zm-134-134-90-90v-130q47 22 73.5 66t26.5 96q0 15-2.5 29.5t-7.5 28.5Zm-170-170-104-104 104-104v208Zm-66.66 270v-131.33l-80-80H182v106.66h122L408.67-322Zm-40-171.33Z"/></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 650 B  | 
| 
						 | 
				
			
			@ -1 +0,0 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e8eaed"><path d="M560-131v-68.67q94.67-27.33 154-105 59.33-77.66 59.33-176.33 0-98.67-59-176.67-59-78-154.33-104.66V-831q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm426.67 45.33v-332Q599-628 629.5-582T660-480q0 55-30.83 100.83-30.84 45.84-82.5 64.5ZM413.33-634l-104 100.67H186.67v106.66h122.66l104 101.34V-634Zm-96 154Z"/></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 474 B  | 
| 
						 | 
				
			
			@ -82,7 +82,7 @@ BarWidgetInner {
 | 
			
		|||
					x: blurRadius
 | 
			
		||||
					width: blur.width - blurRadius * 2
 | 
			
		||||
					height: blur.height
 | 
			
		||||
					clip: true
 | 
			
		||||
 | 
			
		||||
					GaussianBlur {
 | 
			
		||||
						source: blurSource
 | 
			
		||||
						x: -parent.x
 | 
			
		||||
| 
						 | 
				
			
			@ -139,23 +139,27 @@ BarWidgetInner {
 | 
			
		|||
	readonly property Rectangle overlay: overlayItem;
 | 
			
		||||
	Rectangle {
 | 
			
		||||
		id: overlayItem
 | 
			
		||||
		visible: false
 | 
			
		||||
		visible: true
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		border.color: ShellGlobals.colors.widgetOutlineSeparate
 | 
			
		||||
		border.width: 0//1
 | 
			
		||||
		radius: 0//root.radius
 | 
			
		||||
		radius: root.radius
 | 
			
		||||
		color: "transparent"
 | 
			
		||||
 | 
			
		||||
		Rectangle {
 | 
			
		||||
			anchors.fill: parent
 | 
			
		||||
			radius: root.radius
 | 
			
		||||
			color: "transparent"
 | 
			
		||||
			border.color: ShellGlobals.colors.widgetOutlineSeparate;
 | 
			
		||||
			border.width: 1
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// slightly offset on the corners :/
 | 
			
		||||
	layer.enabled: true
 | 
			
		||||
	layer.effect: ShaderEffect {
 | 
			
		||||
		fragmentShader: "radial_clip.frag.qsb"
 | 
			
		||||
		// +1 seems to match Rectangle
 | 
			
		||||
		property real radius: root.radius + 1
 | 
			
		||||
		property size size: Qt.size(root.width, root.height)
 | 
			
		||||
		property real borderWidth: 1//.5
 | 
			
		||||
		property color borderColor: ShellGlobals.colors.widgetOutlineSeparate//"#ffff0000"
 | 
			
		||||
		property color tint: overlayItem.color
 | 
			
		||||
	layer.effect: OpacityMask {
 | 
			
		||||
		maskSource: Rectangle {
 | 
			
		||||
			width: root.width
 | 
			
		||||
			height: root.height
 | 
			
		||||
			radius: root.radius
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,11 @@
 | 
			
		|||
pragma Singleton
 | 
			
		||||
pragma ComponentBehavior: Bound
 | 
			
		||||
 | 
			
		||||
import QtQml.Models
 | 
			
		||||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Io
 | 
			
		||||
import Quickshell.Services.Mpris
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
import "../.."
 | 
			
		||||
 | 
			
		||||
Singleton {
 | 
			
		||||
| 
						 | 
				
			
			@ -15,51 +17,58 @@ Singleton {
 | 
			
		|||
	property bool __reverse: false;
 | 
			
		||||
 | 
			
		||||
	property var activeTrack;
 | 
			
		||||
	Component.onCompleted: {
 | 
			
		||||
		for (const player of Mpris.players.values) {
 | 
			
		||||
			if (player.playbackState == MprisPlaybackState.Playing) {
 | 
			
		||||
				if (root.trackedPlayer == null) {
 | 
			
		||||
					root.trackedPlayer = player;
 | 
			
		||||
 | 
			
		||||
	Instantiator {
 | 
			
		||||
		model: Mpris.players;
 | 
			
		||||
 | 
			
		||||
		Connections {
 | 
			
		||||
			required property MprisPlayer modelData;
 | 
			
		||||
			target: modelData;
 | 
			
		||||
 | 
			
		||||
			Component.onCompleted: {
 | 
			
		||||
				if (root.trackedPlayer == null || modelData.isPlaying) {
 | 
			
		||||
					root.trackedPlayer = modelData;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			player.playbackStateChanged.connect(() => {
 | 
			
		||||
				if (root.trackedPlayer !== player) root.trackedPlayer = player;
 | 
			
		||||
			});
 | 
			
		||||
			Component.onDestruction: {
 | 
			
		||||
				if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
 | 
			
		||||
					for (const player of Mpris.players.values) {
 | 
			
		||||
						if (player.playbackState.isPlaying) {
 | 
			
		||||
							root.trackedPlayer = player;
 | 
			
		||||
							break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if (trackedPlayer == null && Mpris.players.values.length != 0) {
 | 
			
		||||
						trackedPlayer = Mpris.players.values[0];
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			function onPlaybackStateChanged() {
 | 
			
		||||
				if (root.trackedPlayer !== modelData) root.trackedPlayer = modelData;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Connections {
 | 
			
		||||
		target: activePlayer
 | 
			
		||||
 | 
			
		||||
		function onTrackChanged() {
 | 
			
		||||
		function onPostTrackChanged() {
 | 
			
		||||
			root.updateTrack();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Change the tracked player when one changes playback state or is created in a playing state.
 | 
			
		||||
	Connections {
 | 
			
		||||
		target: Mpris.players;
 | 
			
		||||
		function onTrackArtUrlChanged() {
 | 
			
		||||
			console.log("arturl:", activePlayer.trackArtUrl)
 | 
			
		||||
				//root.updateTrack();
 | 
			
		||||
			if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) {
 | 
			
		||||
				// cantata likes to send cover updates *BEFORE* updating the track info.
 | 
			
		||||
				// as such, art url changes shouldn't be able to break the reverse animation
 | 
			
		||||
				const r = root.__reverse;
 | 
			
		||||
				root.updateTrack();
 | 
			
		||||
				root.__reverse = r;
 | 
			
		||||
 | 
			
		||||
		function onObjectInsertedPost(player: MprisPlayer) {
 | 
			
		||||
			if (player.playbackState === MprisPlaybackState.Playing) {
 | 
			
		||||
				if (root.trackedPlayer !== player) root.trackedPlayer = player;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			player.playbackStateChanged.connect(() => {
 | 
			
		||||
				if (root.trackedPlayer !== player) root.trackedPlayer = player;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function onObjectRemovedPre() {
 | 
			
		||||
			console.log(`trackedPlayer: ${root.trackedPlayer}`)
 | 
			
		||||
			if (root.trackedPlayer == null) {
 | 
			
		||||
				for (const player of Mpris.players.values) {
 | 
			
		||||
					if (player.playbackState === MprisPlaybackState.Playing) {
 | 
			
		||||
						root.trackedPlayer = player;
 | 
			
		||||
						break;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -67,27 +76,23 @@ Singleton {
 | 
			
		|||
	onActivePlayerChanged: this.updateTrack();
 | 
			
		||||
 | 
			
		||||
	function updateTrack() {
 | 
			
		||||
		const metadata = this.activePlayer?.metadata ?? {};
 | 
			
		||||
 | 
			
		||||
		//console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`)
 | 
			
		||||
		this.activeTrack = {
 | 
			
		||||
			artUrl: metadata["mpris:artUrl"] ?? "",
 | 
			
		||||
			title: metadata["xesam:title"] ?? "",
 | 
			
		||||
			artist: metadata["xesam:artist"] ?? "",
 | 
			
		||||
			uniqueId: this.activePlayer?.uniqueId ?? 0,
 | 
			
		||||
			artUrl: this.activePlayer?.trackArtUrl ?? "",
 | 
			
		||||
			title: this.activePlayer?.trackTitle || "Unknown Title",
 | 
			
		||||
			artist: this.activePlayer?.trackArtist || "Unknown Artist",
 | 
			
		||||
			album: this.activePlayer?.trackAlbum || "Unknown Album",
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		this.trackChanged(__reverse);
 | 
			
		||||
		this.__reverse = false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property bool isPlaying: this.activePlayer && this.activePlayer.playbackState == MprisPlaybackState.Playing;
 | 
			
		||||
	property bool canPlay: this.activePlayer?.canPlay ?? false;
 | 
			
		||||
	function play() {
 | 
			
		||||
		if (this.canPlay) this.activePlayer.playbackState = MprisPlaybackState.Playing;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property bool canPause: this.activePlayer?.canPause ?? false;
 | 
			
		||||
	function pause() {
 | 
			
		||||
		if (this.canPause) this.activePlayer.playbackState = MprisPlaybackState.Paused;
 | 
			
		||||
	property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying;
 | 
			
		||||
	property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false;
 | 
			
		||||
	function togglePlaying() {
 | 
			
		||||
		if (this.canTogglePlaying) this.activePlayer.togglePlaying();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false;
 | 
			
		||||
| 
						 | 
				
			
			@ -125,7 +130,7 @@ Singleton {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	function setActivePlayer(player: MprisPlayer) {
 | 
			
		||||
		const targetPlayer = player ?? MprisPlayer.players[0];
 | 
			
		||||
		const targetPlayer = player ?? Mpris.players[0];
 | 
			
		||||
		console.log(`setactive: ${targetPlayer} from ${activePlayer}`)
 | 
			
		||||
 | 
			
		||||
		if (targetPlayer && this.activePlayer) {
 | 
			
		||||
| 
						 | 
				
			
			@ -138,31 +143,17 @@ Singleton {
 | 
			
		|||
		this.trackedPlayer = targetPlayer;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Shortcut {
 | 
			
		||||
		name: "music-pauseall";
 | 
			
		||||
		onPressed: {
 | 
			
		||||
			for (let i = 0; i < Mpris.players.length; i++) {
 | 
			
		||||
				const player = Mpris.players[i];
 | 
			
		||||
				if (player.canPause) player.playbackState = MprisPlaybackState.Paused;
 | 
			
		||||
	IpcHandler {
 | 
			
		||||
		target: "mpris"
 | 
			
		||||
 | 
			
		||||
		function pauseAll(): void {
 | 
			
		||||
			for (const player of Mpris.players.values) {
 | 
			
		||||
				if (player.canPause) player.pause();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Shortcut {
 | 
			
		||||
		name: "music-playpause";
 | 
			
		||||
		onPressed: {
 | 
			
		||||
			if (root.isPlaying) root.pause();
 | 
			
		||||
			else root.play();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Shortcut {
 | 
			
		||||
		name: "music-previous";
 | 
			
		||||
		onPressed: root.previous();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Shortcut {
 | 
			
		||||
		name: "music-next";
 | 
			
		||||
		onPressed: root.next();
 | 
			
		||||
		function playPause(): void { root.togglePlaying(); }
 | 
			
		||||
		function previous(): void { root.previous(); }
 | 
			
		||||
		function next(): void { root.next(); }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +0,0 @@
 | 
			
		|||
import Quickshell
 | 
			
		||||
import Quickshell.Services.Mpris
 | 
			
		||||
 | 
			
		||||
Scope {
 | 
			
		||||
	required property MprisPlayer player;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,2 +0,0 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,5 @@
 | 
			
		|||
pragma ComponentBehavior: Bound
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
| 
						 | 
				
			
			@ -7,9 +9,10 @@ import Quickshell.Services.Mpris
 | 
			
		|||
import ".."
 | 
			
		||||
import "../.."
 | 
			
		||||
 | 
			
		||||
MouseArea {
 | 
			
		||||
FullwidthMouseArea {
 | 
			
		||||
	id: root
 | 
			
		||||
	hoverEnabled: true
 | 
			
		||||
	fillWindowWidth: true
 | 
			
		||||
 | 
			
		||||
	required property var bar;
 | 
			
		||||
	implicitHeight: column.implicitHeight + 10
 | 
			
		||||
| 
						 | 
				
			
			@ -27,12 +30,12 @@ MouseArea {
 | 
			
		|||
	property alias widgetOpen: persist.widgetOpen;
 | 
			
		||||
 | 
			
		||||
	acceptedButtons: Qt.RightButton
 | 
			
		||||
	onClicked: widgetOpen = !widgetOpen
 | 
			
		||||
	onPressed: widgetOpen = !widgetOpen
 | 
			
		||||
 | 
			
		||||
	onWheel: event => {
 | 
			
		||||
		event.accepted = true;
 | 
			
		||||
		if (MprisController.canChangeVolume) {
 | 
			
		||||
			this.activePlayer.volume = Math.max(0, Math.min(1, this.activePlayer.volume + (event.angleDelta.y / 120) * 0.05));
 | 
			
		||||
			root.activePlayer.volume = Math.max(0, Math.min(1, root.activePlayer.volume + (event.angleDelta.y / 120) * 0.05));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +45,7 @@ MouseArea {
 | 
			
		|||
		id: widget
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
		property real scaleMul: root.pressed || widgetOpen ? 100 : 1
 | 
			
		||||
		property real scaleMul: widgetOpen ? 100 : 1
 | 
			
		||||
		Behavior on scaleMul { SmoothedAnimation { velocity: 600 } }
 | 
			
		||||
		scale: scaleCurve.interpolate(scaleMul / 100, 1, (width - 6) / width)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -56,8 +59,10 @@ MouseArea {
 | 
			
		|||
		BackgroundArt {
 | 
			
		||||
			id: bkg
 | 
			
		||||
			anchors.fill: parent
 | 
			
		||||
			overlay.color: "#30000000"
 | 
			
		||||
 | 
			
		||||
			function updateArt(reverse: bool) {
 | 
			
		||||
				console.log("update art", MprisController.activeTrack.artUrl)
 | 
			
		||||
				this.setArt(MprisController.activeTrack.artUrl, reverse, false)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -86,6 +91,7 @@ MouseArea {
 | 
			
		|||
				implicitHeight: width
 | 
			
		||||
				scaleIcon: false
 | 
			
		||||
				baseMargin: 3
 | 
			
		||||
				hoverEnabled: false
 | 
			
		||||
				enabled: MprisController.canGoPrevious;
 | 
			
		||||
				onClicked: MprisController.previous();
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -95,11 +101,9 @@ MouseArea {
 | 
			
		|||
				image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`;
 | 
			
		||||
				implicitHeight: width
 | 
			
		||||
				scaleIcon: false
 | 
			
		||||
				enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay;
 | 
			
		||||
				onClicked: {
 | 
			
		||||
					if (MprisController.isPlaying) MprisController.pause();
 | 
			
		||||
					else MprisController.play();
 | 
			
		||||
				}
 | 
			
		||||
				hoverEnabled: false
 | 
			
		||||
				enabled: MprisController.canTogglePlaying;
 | 
			
		||||
				onClicked: MprisController.togglePlaying();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			ClickableIcon {
 | 
			
		||||
| 
						 | 
				
			
			@ -108,51 +112,162 @@ MouseArea {
 | 
			
		|||
				implicitHeight: width
 | 
			
		||||
				scaleIcon: false
 | 
			
		||||
				baseMargin: 3
 | 
			
		||||
				hoverEnabled: false
 | 
			
		||||
				enabled: MprisController.canGoNext;
 | 
			
		||||
				onClicked: MprisController.next();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		property var tooltip: TooltipItem {
 | 
			
		||||
		property Scope positionInfo: Scope {
 | 
			
		||||
			id: positionInfo
 | 
			
		||||
 | 
			
		||||
			property var player: root.activePlayer;
 | 
			
		||||
			property int position: Math.floor(player.position);
 | 
			
		||||
			property int length: Math.floor(player.length);
 | 
			
		||||
 | 
			
		||||
			FrameAnimation {
 | 
			
		||||
				id: posTracker;
 | 
			
		||||
				running: positionInfo.player.isPlaying && (tooltip.visible || rightclickMenu.visible);
 | 
			
		||||
				onTriggered: positionInfo.player.positionChanged();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			function timeStr(time: int): string {
 | 
			
		||||
				const seconds = time % 60;
 | 
			
		||||
				const minutes = Math.floor(time / 60);
 | 
			
		||||
 | 
			
		||||
				return `${minutes}:${seconds.toString().padStart(2, '0')}`
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		property TooltipItem tooltip: TooltipItem {
 | 
			
		||||
			id: tooltip
 | 
			
		||||
			tooltip: bar.tooltip
 | 
			
		||||
			owner: root
 | 
			
		||||
 | 
			
		||||
			show: root.containsMouse && (activePlayer?.metadata["mpris:trackid"] ?? false)
 | 
			
		||||
			show: root.containsMouse
 | 
			
		||||
 | 
			
		||||
			//implicitHeight: root.height - 10
 | 
			
		||||
			//implicitWidth: childrenRect.width
 | 
			
		||||
			/*ColumnLayout {
 | 
			
		||||
				ColumnLayout {
 | 
			
		||||
					visible: MprisController.activePlayer != null
 | 
			
		||||
 | 
			
		||||
					Label { text: MprisController.activeTrack?.title ?? "" }
 | 
			
		||||
 | 
			
		||||
					Label {
 | 
			
		||||
						text: {
 | 
			
		||||
							const artist = MprisController.activeTrack?.artist ?? "";
 | 
			
		||||
							const album = MprisController.activeTrack?.album ?? "";
 | 
			
		||||
 | 
			
		||||
							return artist + (album ? ` - ${album}` : "");
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Label { text: MprisController.activePlayer?.identity ?? "" }
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Label {
 | 
			
		||||
					visible: MprisController.activePlayer == null
 | 
			
		||||
					text: "No media playing"
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Rectangle { implicitHeight: 10; color: "white"; Layout.fillWidth: true }
 | 
			
		||||
				}*/
 | 
			
		||||
 | 
			
		||||
			contentItem.anchors.margins: 0
 | 
			
		||||
 | 
			
		||||
			Item {
 | 
			
		||||
				implicitWidth: 200
 | 
			
		||||
				implicitHeight: 100
 | 
			
		||||
			}
 | 
			
		||||
				id: ttcontent
 | 
			
		||||
				width: parent.width
 | 
			
		||||
				height: Math.max(parent.height, implicitHeight)
 | 
			
		||||
				implicitWidth: cl.implicitWidth + 10
 | 
			
		||||
				implicitHeight: cl.implicitHeight + 10 + (MprisController.activePlayer ? 8 : 0)
 | 
			
		||||
 | 
			
		||||
			/*Loader {
 | 
			
		||||
				active: tooltip.visible
 | 
			
		||||
				ColumnLayout {
 | 
			
		||||
					id: cl
 | 
			
		||||
					anchors {
 | 
			
		||||
						left: parent.left
 | 
			
		||||
						right: parent.right
 | 
			
		||||
						top: parent.top
 | 
			
		||||
						margins: 5
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
				sourceComponent: ColumnLayout {
 | 
			
		||||
					height: root.height - 10
 | 
			
		||||
					RowLayout {
 | 
			
		||||
						Image {
 | 
			
		||||
							Layout.fillHeight: true
 | 
			
		||||
							source: mainPlayer.metadata["mpris:artUrl"] ?? ""
 | 
			
		||||
					//visible: MprisController.activePlayer != null
 | 
			
		||||
 | 
			
		||||
					FontMetrics { id: fontmetrics }
 | 
			
		||||
 | 
			
		||||
					component FullheightLabel: Item {
 | 
			
		||||
						implicitHeight: fontmetrics.height
 | 
			
		||||
						implicitWidth: label.implicitWidth
 | 
			
		||||
 | 
			
		||||
						property alias text: label.text
 | 
			
		||||
 | 
			
		||||
							cache: false
 | 
			
		||||
							fillMode: Image.PreserveAspectCrop
 | 
			
		||||
							sourceSize.width: height
 | 
			
		||||
							sourceSize.height: height
 | 
			
		||||
						}
 | 
			
		||||
						Label {
 | 
			
		||||
							text: mainPlayer.identity
 | 
			
		||||
							id: label
 | 
			
		||||
							anchors.verticalCenter: parent.verticalCenter
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Slider {
 | 
			
		||||
						Layout.fillWidth: true
 | 
			
		||||
					FullheightLabel {
 | 
			
		||||
						visible: MprisController.activePlayer != null
 | 
			
		||||
						text: MprisController.activeTrack?.title ?? ""
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					FullheightLabel {
 | 
			
		||||
						visible: MprisController.activePlayer != null
 | 
			
		||||
						text: MprisController.activeTrack?.artist ?? ""
 | 
			
		||||
						/*text: {
 | 
			
		||||
							const artist = MprisController.activeTrack?.artist ?? "";
 | 
			
		||||
							const album = MprisController.activeTrack?.album ?? "";
 | 
			
		||||
 | 
			
		||||
							return artist + (album ? ` - ${album}` : "");
 | 
			
		||||
						}*/
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Label {
 | 
			
		||||
						text: {
 | 
			
		||||
							if (!MprisController.activePlayer) return "No media playing";
 | 
			
		||||
 | 
			
		||||
							return MprisController.activePlayer?.identity + " - "
 | 
			
		||||
								+ positionInfo.timeStr(positionInfo.position) + " / "
 | 
			
		||||
								+ positionInfo.timeStr(positionInfo.length);
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}*/
 | 
			
		||||
 | 
			
		||||
				Rectangle {
 | 
			
		||||
					id: ttprect
 | 
			
		||||
					anchors {
 | 
			
		||||
						left: parent.left
 | 
			
		||||
						right: parent.right
 | 
			
		||||
						bottom: parent.bottom
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					color: "#30ceffff"
 | 
			
		||||
					implicitHeight: 8
 | 
			
		||||
					visible: MprisController.activePlayer != null
 | 
			
		||||
 | 
			
		||||
					Rectangle {
 | 
			
		||||
						anchors {
 | 
			
		||||
							left: parent.left
 | 
			
		||||
							top: parent.top
 | 
			
		||||
							bottom: parent.bottom
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						color: "#80ceffff"
 | 
			
		||||
						width: parent.width * (root.activePlayer.position / root.activePlayer.length)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
				layer.enabled: true
 | 
			
		||||
				layer.effect: OpacityMask {
 | 
			
		||||
					maskSource: Rectangle {
 | 
			
		||||
						width: ttcontent.width
 | 
			
		||||
						height: ttcontent.height
 | 
			
		||||
						bottomLeftRadius: 5
 | 
			
		||||
						bottomRightRadius: 5
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		property var rightclickMenu: TooltipItem {
 | 
			
		||||
| 
						 | 
				
			
			@ -182,6 +297,7 @@ MouseArea {
 | 
			
		|||
					target: MprisController
 | 
			
		||||
 | 
			
		||||
					function onTrackChanged(reverse: bool) {
 | 
			
		||||
						console.log(`track changed: rev: ${reverse}`)
 | 
			
		||||
						popupBkg.setArt(MprisController.activeTrack.artUrl, reverse, false);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			@ -191,71 +307,41 @@ MouseArea {
 | 
			
		|||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			contentItem {
 | 
			
		||||
				implicitWidth: 500
 | 
			
		||||
				implicitHeight: 650
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Loader {
 | 
			
		||||
				active: rightclickMenu.visible
 | 
			
		||||
				width: 500
 | 
			
		||||
				height: 650
 | 
			
		||||
				active: rightclickMenu.visible
 | 
			
		||||
 | 
			
		||||
				sourceComponent: ColumnLayout {
 | 
			
		||||
					property var player: activePlayer;
 | 
			
		||||
					anchors.fill: parent;
 | 
			
		||||
 | 
			
		||||
					property int position: 0;
 | 
			
		||||
					property int length: 0;
 | 
			
		||||
 | 
			
		||||
					FrameAnimation {
 | 
			
		||||
						id: posTracker;
 | 
			
		||||
						running: player.playbackState == MprisPlaybackState.Playing && widgetOpen;
 | 
			
		||||
						onTriggered: player.positionChanged();
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Connections {
 | 
			
		||||
						target: player
 | 
			
		||||
 | 
			
		||||
						function onPositionChanged() {
 | 
			
		||||
							const newPosition = Math.floor(player.position);
 | 
			
		||||
							if (newPosition != position) position = newPosition;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						function onLengthChanged() {
 | 
			
		||||
							const newLength = Math.floor(player.length);
 | 
			
		||||
							if (newLength != length) length = newLength;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					property var player: root.activePlayer;
 | 
			
		||||
 | 
			
		||||
					Connections {
 | 
			
		||||
						target: MprisController
 | 
			
		||||
 | 
			
		||||
						function onTrackChanged(reverse: bool) {
 | 
			
		||||
							trackStack.updateTrack(reverse, false);
 | 
			
		||||
							length = Math.floor(player.length);
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Component.onCompleted: {
 | 
			
		||||
						position = Math.floor(player.position);
 | 
			
		||||
						length = Math.floor(player.length);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					function timeStr(time: int): string {
 | 
			
		||||
						const seconds = time % 60;
 | 
			
		||||
						const minutes = Math.floor(time / 60);
 | 
			
		||||
 | 
			
		||||
						return `${minutes}:${seconds.toString().padStart(2, '0')}`
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Item {
 | 
			
		||||
						id: playerSelectorContainment
 | 
			
		||||
						Layout.fillWidth: true
 | 
			
		||||
						implicitHeight: playerSelector.implicitHeight + 20
 | 
			
		||||
						implicitWidth: playerSelector.implicitWidth
 | 
			
		||||
 | 
			
		||||
						ScrollView {
 | 
			
		||||
						RowLayout { //ScrollView {
 | 
			
		||||
							id: playerSelector
 | 
			
		||||
							anchors.centerIn: parent
 | 
			
		||||
							width: Math.min(implicitWidth, playerSelectorContainment.width)
 | 
			
		||||
 | 
			
		||||
							RowLayout {
 | 
			
		||||
							//RowLayout {
 | 
			
		||||
								Repeater {
 | 
			
		||||
									model: Mpris.players
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -281,8 +367,7 @@ MouseArea {
 | 
			
		|||
												source: {
 | 
			
		||||
													const entry = DesktopEntries.byId(modelData.desktopEntry);
 | 
			
		||||
													console.log(`ent ${entry} id ${modelData.desktopEntry}`)
 | 
			
		||||
													if (!entry) return "image://icon/";
 | 
			
		||||
													return `image://icon/${entry.icon}`;
 | 
			
		||||
													return Quickshell.iconPath(entry?.icon);
 | 
			
		||||
												}
 | 
			
		||||
												//asynchronous: true
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -292,18 +377,18 @@ MouseArea {
 | 
			
		|||
											}
 | 
			
		||||
										}
 | 
			
		||||
									}
 | 
			
		||||
								}
 | 
			
		||||
								//}
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Item {
 | 
			
		||||
						Layout.fillWidth: true
 | 
			
		||||
						Layout.bottomMargin: 10
 | 
			
		||||
						Layout.bottomMargin: 20
 | 
			
		||||
 | 
			
		||||
						Label {
 | 
			
		||||
							anchors.centerIn: parent
 | 
			
		||||
							text: activePlayer.identity
 | 
			
		||||
							text: root.activePlayer.identity
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -311,7 +396,13 @@ MouseArea {
 | 
			
		|||
						id: trackStack
 | 
			
		||||
						Layout.fillWidth: true
 | 
			
		||||
						implicitHeight: 400
 | 
			
		||||
						clip: animating || (lastFlicked?.contentX ?? 0) != 0
 | 
			
		||||
 | 
			
		||||
						// inverse of default tooltip margin - 1px for border
 | 
			
		||||
						Layout.leftMargin: -4
 | 
			
		||||
						Layout.rightMargin: -4
 | 
			
		||||
 | 
			
		||||
						property Flickable lastFlicked;
 | 
			
		||||
						property bool reverse: false;
 | 
			
		||||
 | 
			
		||||
						Component.onCompleted: updateTrack(false, true);
 | 
			
		||||
| 
						 | 
				
			
			@ -333,14 +424,16 @@ MouseArea {
 | 
			
		|||
								// but may take longer if the image is huge.
 | 
			
		||||
								readonly property bool svReady: img.status === Image.Ready;
 | 
			
		||||
								contentWidth: width + 1
 | 
			
		||||
								onDragStarted: trackStack.lastFlicked = this
 | 
			
		||||
								onDragEnded: {
 | 
			
		||||
									return;
 | 
			
		||||
									//return;
 | 
			
		||||
									console.log(`dragend ${contentX}`)
 | 
			
		||||
									if (Math.abs(contentX) > 75) {
 | 
			
		||||
										if (contentX < 0) MprisController.previous();
 | 
			
		||||
										else if (contentX > 0) MprisController.next();
 | 
			
		||||
									}
 | 
			
		||||
								}
 | 
			
		||||
 | 
			
		||||
								ColumnLayout {
 | 
			
		||||
									id: trackContent
 | 
			
		||||
									width: flickable.width
 | 
			
		||||
| 
						 | 
				
			
			@ -348,7 +441,7 @@ MouseArea {
 | 
			
		|||
 | 
			
		||||
									Item {
 | 
			
		||||
										Layout.fillWidth: true
 | 
			
		||||
										implicitHeight: 300//img.implicitHeight
 | 
			
		||||
										implicitHeight: 302//img.implicitHeight
 | 
			
		||||
										implicitWidth: img.implicitWidth
 | 
			
		||||
 | 
			
		||||
										Image {
 | 
			
		||||
| 
						 | 
				
			
			@ -362,27 +455,50 @@ MouseArea {
 | 
			
		|||
 | 
			
		||||
											sourceSize.height: 300
 | 
			
		||||
											sourceSize.width: 300
 | 
			
		||||
 | 
			
		||||
											layer.enabled: true
 | 
			
		||||
											layer.effect: OpacityMask {
 | 
			
		||||
												cached: true
 | 
			
		||||
												maskSource: Rectangle {
 | 
			
		||||
													width: img.width
 | 
			
		||||
													height: img.height
 | 
			
		||||
													radius: 5
 | 
			
		||||
												}
 | 
			
		||||
											}
 | 
			
		||||
										}
 | 
			
		||||
									}
 | 
			
		||||
 | 
			
		||||
									Item {
 | 
			
		||||
									component CenteredText: Item {
 | 
			
		||||
										Layout.fillWidth: true
 | 
			
		||||
										Layout.topMargin: 20
 | 
			
		||||
 | 
			
		||||
										property alias text: label.text
 | 
			
		||||
										property alias font: label.font
 | 
			
		||||
 | 
			
		||||
										Label {
 | 
			
		||||
											id: label
 | 
			
		||||
											visible: text != ""
 | 
			
		||||
											anchors.centerIn: parent
 | 
			
		||||
											text: track.title
 | 
			
		||||
											elide: Text.ElideRight
 | 
			
		||||
											width: Math.min(parent.width - 20, implicitWidth)
 | 
			
		||||
										}
 | 
			
		||||
									}
 | 
			
		||||
 | 
			
		||||
									Item {
 | 
			
		||||
										Layout.fillWidth: true
 | 
			
		||||
									CenteredText {
 | 
			
		||||
										Layout.topMargin: 20
 | 
			
		||||
										text: track.title
 | 
			
		||||
										font.pointSize: albumLabel.font.pointSize + 1
 | 
			
		||||
									}
 | 
			
		||||
 | 
			
		||||
										Label {
 | 
			
		||||
											anchors.centerIn: parent
 | 
			
		||||
											text: track.artist
 | 
			
		||||
										}
 | 
			
		||||
									CenteredText {
 | 
			
		||||
										id: albumLabel
 | 
			
		||||
										Layout.topMargin: 18
 | 
			
		||||
										text: track.album
 | 
			
		||||
										opacity: 0.8
 | 
			
		||||
									}
 | 
			
		||||
 | 
			
		||||
									CenteredText {
 | 
			
		||||
										Layout.topMargin: 25
 | 
			
		||||
										text: track.artist
 | 
			
		||||
									}
 | 
			
		||||
 | 
			
		||||
									Item { Layout.fillHeight: true }
 | 
			
		||||
| 
						 | 
				
			
			@ -462,11 +578,8 @@ MouseArea {
 | 
			
		|||
								implicitWidth: 80
 | 
			
		||||
								implicitHeight: width
 | 
			
		||||
								scaleIcon: false
 | 
			
		||||
								enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay
 | 
			
		||||
								onClicked: {
 | 
			
		||||
									if (MprisController.isPlaying) MprisController.pause();
 | 
			
		||||
									else MprisController.play();
 | 
			
		||||
								}
 | 
			
		||||
								enabled: MprisController.canTogglePlaying;
 | 
			
		||||
								onClicked: MprisController.togglePlaying();
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							ClickableIcon {
 | 
			
		||||
| 
						 | 
				
			
			@ -491,33 +604,70 @@ MouseArea {
 | 
			
		|||
					}
 | 
			
		||||
 | 
			
		||||
					RowLayout {
 | 
			
		||||
						Layout.margins: 5
 | 
			
		||||
 | 
			
		||||
						Label {
 | 
			
		||||
							Layout.preferredWidth: lengthLabel.implicitWidth
 | 
			
		||||
							text: timeStr(position)
 | 
			
		||||
							text: positionInfo.timeStr(positionInfo.position)
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						MediaSlider {
 | 
			
		||||
							id: slider
 | 
			
		||||
							property bool bindSlider: true;
 | 
			
		||||
 | 
			
		||||
							property real boundAnimStart: 0;
 | 
			
		||||
							property real boundAnimFactor: 1;
 | 
			
		||||
							property real lastPosition: 0;
 | 
			
		||||
							property real lastLength: 0;
 | 
			
		||||
							property real boundPosition: {
 | 
			
		||||
								const ppos = player.position / player.length;
 | 
			
		||||
								const bpos = boundAnimStart;
 | 
			
		||||
								return (ppos * boundAnimFactor) + (bpos * (1.0 - boundAnimFactor));
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							NumberAnimation {
 | 
			
		||||
								id: boundAnim
 | 
			
		||||
								target: slider
 | 
			
		||||
								property: "boundAnimFactor"
 | 
			
		||||
								from: 0
 | 
			
		||||
								to: 1
 | 
			
		||||
								duration: 600
 | 
			
		||||
								easing.type: Easing.OutExpo
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							Connections {
 | 
			
		||||
								target: player
 | 
			
		||||
 | 
			
		||||
								function onPositionChanged() {
 | 
			
		||||
									if (false && player.position == 0 && slider.lastPosition != 0 && !boundAnim.running) {
 | 
			
		||||
										slider.boundAnimStart = slider.lastPosition / slider.lastLength;
 | 
			
		||||
										boundAnim.start();
 | 
			
		||||
									}
 | 
			
		||||
 | 
			
		||||
									slider.lastPosition = player.position;
 | 
			
		||||
									slider.lastLength = player.length;
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							Layout.fillWidth: true
 | 
			
		||||
							property var bindSlider: true;
 | 
			
		||||
							enabled: player.canSeek
 | 
			
		||||
							from: 0
 | 
			
		||||
							to: player.length
 | 
			
		||||
							to: 1
 | 
			
		||||
 | 
			
		||||
							onPressedChanged: {
 | 
			
		||||
								if (!pressed) player.position = value;
 | 
			
		||||
								if (!pressed) player.position = value * player.length;
 | 
			
		||||
								bindSlider = !pressed;
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							Binding {
 | 
			
		||||
								when: slider.bindSlider
 | 
			
		||||
								slider.value: player.position
 | 
			
		||||
								slider.value: slider.boundPosition
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						Label {
 | 
			
		||||
							id: lengthLabel
 | 
			
		||||
							text: timeStr(length)
 | 
			
		||||
							text: positionInfo.timeStr(positionInfo.length)
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,45 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell.Services.UPower
 | 
			
		||||
import "root:."
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property UPowerDevice device;
 | 
			
		||||
	property real scale: 1;
 | 
			
		||||
 | 
			
		||||
	readonly property bool isCharging: root.device.state == UPowerDeviceState.Charging;
 | 
			
		||||
	readonly property bool isPluggedIn: isCharging || root.device.state == UPowerDeviceState.PendingCharge;
 | 
			
		||||
	readonly property bool isLow: root.device.percentage <= 0.20;
 | 
			
		||||
 | 
			
		||||
	width: 35 * root.scale
 | 
			
		||||
	height: 35 * root.scale
 | 
			
		||||
 | 
			
		||||
	Rectangle {
 | 
			
		||||
		anchors {
 | 
			
		||||
			horizontalCenter: parent.horizontalCenter
 | 
			
		||||
			bottom: parent.bottom
 | 
			
		||||
			bottomMargin: 4 * root.scale
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		width: 13 * root.scale
 | 
			
		||||
		height: 23 * root.device.percentage * root.scale
 | 
			
		||||
		radius: 2 * root.scale
 | 
			
		||||
 | 
			
		||||
		color: root.isPluggedIn ? "#359040"
 | 
			
		||||
		     : ShellGlobals.interpolateColors(Math.min(1.0, Math.min(0.5, root.device.percentage) * 2), "red", "white")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Image {
 | 
			
		||||
		id: img
 | 
			
		||||
		anchors.fill: parent;
 | 
			
		||||
 | 
			
		||||
		source: root.isCharging ? "root:icons/battery-charging.svg"
 | 
			
		||||
		      : root.isPluggedIn ? "root:icons/battery-plus.svg"
 | 
			
		||||
					: root.isLow ? "root:icons/battery-warning.svg"
 | 
			
		||||
		      : "root:icons/battery-empty.svg"
 | 
			
		||||
 | 
			
		||||
		sourceSize.width: parent.width
 | 
			
		||||
		sourceSize.height: parent.height
 | 
			
		||||
		visible: true
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										224
									
								
								modules/user/modules/quickshell/shell/bar/power/Power.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,224 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Services.UPower
 | 
			
		||||
import Quickshell.Widgets
 | 
			
		||||
import ".."
 | 
			
		||||
import "root:."
 | 
			
		||||
import "root:components"
 | 
			
		||||
import "power"
 | 
			
		||||
 | 
			
		||||
BarWidgetInner {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property var bar;
 | 
			
		||||
 | 
			
		||||
	readonly property var chargeState: UPower.displayDevice.state
 | 
			
		||||
	readonly property bool isCharging: chargeState == UPowerDeviceState.Charging;
 | 
			
		||||
	readonly property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge;
 | 
			
		||||
	readonly property real percentage: UPower.displayDevice.percentage
 | 
			
		||||
	readonly property bool isLow: percentage <= 0.20
 | 
			
		||||
 | 
			
		||||
	readonly property UPowerDevice batteryDevice: UPower.devices.values
 | 
			
		||||
		.find(device => device.isLaptopBattery);
 | 
			
		||||
 | 
			
		||||
	function statusStr() {
 | 
			
		||||
		return root.isPluggedIn ? `Plugged in, ${root.isCharging ? "Charging" : "Not Charging"}`
 | 
			
		||||
		                        : "Discharging";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property bool showMenu: false;
 | 
			
		||||
 | 
			
		||||
	implicitHeight: width
 | 
			
		||||
	color: isLow ? "#45ff6060" : ShellGlobals.colors.widget
 | 
			
		||||
 | 
			
		||||
	BarButton {
 | 
			
		||||
		id: button
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		baseMargin: 5
 | 
			
		||||
		fillWindowWidth: true
 | 
			
		||||
		acceptedButtons: Qt.RightButton
 | 
			
		||||
		directScale: true
 | 
			
		||||
		showPressed: root.showMenu
 | 
			
		||||
 | 
			
		||||
		onPressed: {
 | 
			
		||||
			root.showMenu = !root.showMenu
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		BatteryIcon {
 | 
			
		||||
			device: UPower.displayDevice
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property TooltipItem tooltip: TooltipItem {
 | 
			
		||||
		id: tooltip
 | 
			
		||||
		tooltip: bar.tooltip
 | 
			
		||||
		owner: root
 | 
			
		||||
		show: button.containsMouse
 | 
			
		||||
 | 
			
		||||
		Loader {
 | 
			
		||||
			active: tooltip.visible
 | 
			
		||||
 | 
			
		||||
			sourceComponent: Label {
 | 
			
		||||
				text: {
 | 
			
		||||
					const status = root.statusStr();
 | 
			
		||||
 | 
			
		||||
					const percentage = Math.round(root.percentage * 100);
 | 
			
		||||
 | 
			
		||||
					let str = `${percentage}% - ${status}`;
 | 
			
		||||
					return str;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property TooltipItem rightclickMenu: TooltipItem {
 | 
			
		||||
		id: rightclickMenu
 | 
			
		||||
		tooltip: bar.tooltip
 | 
			
		||||
		owner: root
 | 
			
		||||
 | 
			
		||||
		isMenu: true
 | 
			
		||||
		show: root.showMenu
 | 
			
		||||
		onClose: root.showMenu = false
 | 
			
		||||
 | 
			
		||||
		Loader {
 | 
			
		||||
			active: rightclickMenu.visible
 | 
			
		||||
			sourceComponent: ColumnLayout {
 | 
			
		||||
				spacing: 10
 | 
			
		||||
 | 
			
		||||
				FontMetrics { id: fm }
 | 
			
		||||
 | 
			
		||||
				component SmallLabel: Label {
 | 
			
		||||
					font.pointSize: fm.font.pointSize * 0.8
 | 
			
		||||
					color: "#d0eeffff"
 | 
			
		||||
				}
 | 
			
		||||
			
 | 
			
		||||
				RowLayout {
 | 
			
		||||
					IconImage {
 | 
			
		||||
						source: "root:icons/gauge.svg"
 | 
			
		||||
						implicitSize: 32
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					ColumnLayout {
 | 
			
		||||
						spacing: 0
 | 
			
		||||
						Label { text: "Power Profile" }
 | 
			
		||||
 | 
			
		||||
						OptionSlider {
 | 
			
		||||
							values: ["Power Save", "Balanced", "Performance"]
 | 
			
		||||
							index: PowerProfiles.profile
 | 
			
		||||
							onIndexChanged: PowerProfiles.profile = this.index;
 | 
			
		||||
							implicitWidth: 350
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				RowLayout {
 | 
			
		||||
					IconImage {
 | 
			
		||||
						Layout.alignment: Qt.AlignTop
 | 
			
		||||
						source: "root:icons/battery-empty.svg"
 | 
			
		||||
						implicitSize: 32
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					ColumnLayout {
 | 
			
		||||
						spacing: 0
 | 
			
		||||
 | 
			
		||||
						RowLayout {
 | 
			
		||||
							Label { text: "Battery" }
 | 
			
		||||
							Item { Layout.fillWidth: true }
 | 
			
		||||
							Label {
 | 
			
		||||
								text: `${root.statusStr()} -`
 | 
			
		||||
								color: "#d0eeffff"
 | 
			
		||||
							}
 | 
			
		||||
							Label { text: `${Math.round(root.percentage * 100)}%` }
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						ProgressBar {
 | 
			
		||||
							Layout.topMargin: 5
 | 
			
		||||
							Layout.bottomMargin: 5
 | 
			
		||||
							Layout.fillWidth: true
 | 
			
		||||
							value: UPower.displayDevice.percentage
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						RowLayout {
 | 
			
		||||
							visible: remainingTimeLbl.text !== ""
 | 
			
		||||
 | 
			
		||||
							SmallLabel { text: "Time remaining" }
 | 
			
		||||
							Item { Layout.fillWidth: true }
 | 
			
		||||
 | 
			
		||||
				     	SmallLabel {
 | 
			
		||||
								id: remainingTimeLbl
 | 
			
		||||
				     		text: {
 | 
			
		||||
				     			const device = UPower.displayDevice;
 | 
			
		||||
				     			const time = device.timeToEmpty || device.timeToFull;
 | 
			
		||||
 | 
			
		||||
									if (time === 0) return "";
 | 
			
		||||
									const minutes = Math.floor(time / 60).toString().padStart(2, '0');
 | 
			
		||||
									return `${minutes} minutes`
 | 
			
		||||
				     		}
 | 
			
		||||
				     	}
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						RowLayout {
 | 
			
		||||
							visible: root.batteryDevice.healthSupported
 | 
			
		||||
							SmallLabel { text: "Health" }
 | 
			
		||||
							Item { Layout.fillWidth: true }
 | 
			
		||||
 | 
			
		||||
				     	SmallLabel {
 | 
			
		||||
				     		text: `${Math.floor((root.batteryDevice?.healthPercentage ?? 0))}%`
 | 
			
		||||
				     	}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Repeater {
 | 
			
		||||
					model: ScriptModel {
 | 
			
		||||
						// external devices
 | 
			
		||||
						values: UPower.devices.values.filter(device => !device.powerSupply)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
			   	RowLayout {
 | 
			
		||||
						required property UPowerDevice modelData;
 | 
			
		||||
 | 
			
		||||
			   		IconImage {
 | 
			
		||||
			   			Layout.alignment: Qt.AlignTop
 | 
			
		||||
			   			source: {
 | 
			
		||||
								switch (modelData.type) {
 | 
			
		||||
								case UPowerDeviceType.Headset: return "root:icons/headset.svg";
 | 
			
		||||
								}
 | 
			
		||||
								return Quickshell.iconPath(modelData.iconName)
 | 
			
		||||
							}
 | 
			
		||||
			   			implicitSize: 32
 | 
			
		||||
			   		}
 | 
			
		||||
 | 
			
		||||
			   		ColumnLayout {
 | 
			
		||||
			   			spacing: 0
 | 
			
		||||
 | 
			
		||||
			   			RowLayout {
 | 
			
		||||
			   				Label { text: modelData.model }
 | 
			
		||||
			   				Item { Layout.fillWidth: true }
 | 
			
		||||
			   				Label { text: `${Math.round(modelData.percentage * 100)}%` }
 | 
			
		||||
			   			}
 | 
			
		||||
 | 
			
		||||
			   			ProgressBar {
 | 
			
		||||
			   				Layout.topMargin: 5
 | 
			
		||||
			   				Layout.bottomMargin: 5
 | 
			
		||||
			   				Layout.fillWidth: true
 | 
			
		||||
			   				value: modelData.percentage
 | 
			
		||||
			   			}
 | 
			
		||||
 | 
			
		||||
			   			RowLayout {
 | 
			
		||||
			   				visible: modelData.healthSupported
 | 
			
		||||
			   				SmallLabel { text: "Health" }
 | 
			
		||||
			   				Item { Layout.fillWidth: true }
 | 
			
		||||
 | 
			
		||||
			   	     	SmallLabel {
 | 
			
		||||
			   	     		text: `${Math.floor(modelData.healthPercentage)}%`
 | 
			
		||||
			   	     	}
 | 
			
		||||
			   			}
 | 
			
		||||
			   		}
 | 
			
		||||
			   	}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ Item {
 | 
			
		|||
	property bool expanded: false;
 | 
			
		||||
 | 
			
		||||
	readonly property bool open: progress != 0;
 | 
			
		||||
	readonly property bool animating: internalProgress != -1 && internalProgress != 101;
 | 
			
		||||
	readonly property bool animating: internalProgress != (expanded ? 101 : -1);
 | 
			
		||||
 | 
			
		||||
	implicitHeight: 16
 | 
			
		||||
	implicitWidth: 16
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,15 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Widgets
 | 
			
		||||
import Quickshell.DBusMenu
 | 
			
		||||
import "../.."
 | 
			
		||||
 | 
			
		||||
MouseArea {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property var entry;
 | 
			
		||||
	required property QsMenuEntry entry;
 | 
			
		||||
	property alias expanded: childrenRevealer.expanded;
 | 
			
		||||
	property bool animating: childrenRevealer.animating || childrenList.animating;
 | 
			
		||||
	property bool animating: childrenRevealer.animating || (childMenuLoader?.item?.animating ?? false);
 | 
			
		||||
	// appears it won't actually create the handler when only used from MenuItemList.
 | 
			
		||||
	onExpandedChanged: {}
 | 
			
		||||
	onAnimatingChanged: {}
 | 
			
		||||
| 
						 | 
				
			
			@ -22,7 +23,7 @@ MouseArea {
 | 
			
		|||
	onClicked: {
 | 
			
		||||
		if (entry.hasChildren) childrenRevealer.expanded = !childrenRevealer.expanded
 | 
			
		||||
		else {
 | 
			
		||||
			entry.click();
 | 
			
		||||
			entry.triggered();
 | 
			
		||||
			if (entry.toggleType == ToggleButtonType.None) close();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -34,19 +35,21 @@ MouseArea {
 | 
			
		|||
		spacing: 0
 | 
			
		||||
 | 
			
		||||
		RowLayout {
 | 
			
		||||
			id: innerRow
 | 
			
		||||
 | 
			
		||||
			Item {
 | 
			
		||||
				implicitWidth: 22
 | 
			
		||||
				implicitHeight: 22
 | 
			
		||||
 | 
			
		||||
				MenuCheckBox {
 | 
			
		||||
					anchors.centerIn: parent
 | 
			
		||||
					visible: entry.toggleType == ToggleButtonType.CheckBox
 | 
			
		||||
					visible: entry.buttonType == QsMenuButtonType.CheckBox
 | 
			
		||||
					checkState: entry.checkState
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				MenuRadioButton {
 | 
			
		||||
					anchors.centerIn: parent
 | 
			
		||||
					visible: entry.toggleType == ToggleButtonType.RadioButton
 | 
			
		||||
					visible: entry.buttonType == QsMenuButtonType.RadioButton
 | 
			
		||||
					checkState: entry.checkState
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +62,7 @@ MouseArea {
 | 
			
		|||
			}
 | 
			
		||||
 | 
			
		||||
			Text {
 | 
			
		||||
				text: entry.cleanLabel
 | 
			
		||||
				text: entry.text
 | 
			
		||||
				color: entry.enabled ? "white" : "#bbbbbb"
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -68,25 +71,32 @@ MouseArea {
 | 
			
		|||
				implicitWidth: 22
 | 
			
		||||
				implicitHeight: 22
 | 
			
		||||
 | 
			
		||||
				Image {
 | 
			
		||||
				IconImage {
 | 
			
		||||
					anchors.right: parent.right
 | 
			
		||||
					anchors.verticalCenter: parent.verticalCenter
 | 
			
		||||
					visible: entry.icon != ""
 | 
			
		||||
					source: entry.icon
 | 
			
		||||
					sourceSize.height: parent.height
 | 
			
		||||
					sourceSize.width: parent.height
 | 
			
		||||
					visible: source != ""
 | 
			
		||||
					implicitSize: parent.height
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Item {
 | 
			
		||||
		Loader {
 | 
			
		||||
			id: childMenuLoader
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
			implicitHeight: childrenList.implicitHeight * childrenRevealer.progress
 | 
			
		||||
			Layout.preferredHeight: active ? item.implicitHeight * childrenRevealer.progress : 0
 | 
			
		||||
 | 
			
		||||
			readonly property real widthDifference: {
 | 
			
		||||
				Math.max(0, (item?.implicitWidth ?? 0) - innerRow.implicitWidth);
 | 
			
		||||
			}
 | 
			
		||||
			Layout.preferredWidth: active ? innerRow.implicitWidth + (widthDifference * childrenRevealer.progress) : 0
 | 
			
		||||
 | 
			
		||||
			active: root.expanded || root.animating
 | 
			
		||||
			clip: true
 | 
			
		||||
 | 
			
		||||
			MenuItemList {
 | 
			
		||||
			sourceComponent: MenuView {
 | 
			
		||||
				id: childrenList
 | 
			
		||||
				items: entry.children
 | 
			
		||||
				menu: entry
 | 
			
		||||
				onClose: root.close()
 | 
			
		||||
 | 
			
		||||
				anchors {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,17 +6,19 @@ import "../.."
 | 
			
		|||
 | 
			
		||||
ColumnLayout {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property var items;
 | 
			
		||||
	property alias menu: menuView.menu;
 | 
			
		||||
	property Item animatingItem: null;
 | 
			
		||||
	property bool animating: animatingItem != null;
 | 
			
		||||
 | 
			
		||||
	signal close();
 | 
			
		||||
	signal submenuExpanded(item: var);
 | 
			
		||||
 | 
			
		||||
	QsMenuOpener { id: menuView }
 | 
			
		||||
 | 
			
		||||
	spacing: 0
 | 
			
		||||
 | 
			
		||||
	Repeater {
 | 
			
		||||
		model: items
 | 
			
		||||
		model: menuView.children;
 | 
			
		||||
 | 
			
		||||
		Loader {
 | 
			
		||||
			required property var modelData;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,5 @@
 | 
			
		|||
pragma ComponentBehavior: Bound
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import QtQuick.Effects
 | 
			
		||||
| 
						 | 
				
			
			@ -5,102 +7,102 @@ import Quickshell
 | 
			
		|||
import Quickshell.Services.SystemTray
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
OverlayWidget {
 | 
			
		||||
BarWidgetInner {
 | 
			
		||||
	id: root
 | 
			
		||||
	expandedWidth: 600
 | 
			
		||||
	expandedHeight: 800
 | 
			
		||||
	required property var bar;
 | 
			
		||||
	implicitHeight: column.implicitHeight + 10
 | 
			
		||||
 | 
			
		||||
	BarWidgetInner {
 | 
			
		||||
		implicitHeight: column.implicitHeight + 10
 | 
			
		||||
	ColumnLayout {
 | 
			
		||||
		id: column
 | 
			
		||||
		implicitHeight: childrenRect.height
 | 
			
		||||
		spacing: 5
 | 
			
		||||
 | 
			
		||||
		ColumnLayout {
 | 
			
		||||
			id: column
 | 
			
		||||
			implicitHeight: childrenRect.height
 | 
			
		||||
			spacing: 5
 | 
			
		||||
		anchors {
 | 
			
		||||
			fill: parent
 | 
			
		||||
			margins: 5
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
			anchors {
 | 
			
		||||
				fill: parent
 | 
			
		||||
				margins: 5
 | 
			
		||||
			}
 | 
			
		||||
		Repeater {
 | 
			
		||||
			model: SystemTray.items;
 | 
			
		||||
 | 
			
		||||
			Repeater {
 | 
			
		||||
				model: SystemTray.items;
 | 
			
		||||
			Item {
 | 
			
		||||
				id: item
 | 
			
		||||
				required property SystemTrayItem modelData;
 | 
			
		||||
 | 
			
		||||
				Item {
 | 
			
		||||
					required property var modelData;
 | 
			
		||||
					readonly property alias menu: menuWatcher.menu;
 | 
			
		||||
				property bool targetMenuOpen: false;
 | 
			
		||||
 | 
			
		||||
					SystemTrayMenuWatcher {
 | 
			
		||||
						id: menuWatcher;
 | 
			
		||||
						trayItem: modelData;
 | 
			
		||||
				Layout.fillWidth: true
 | 
			
		||||
				implicitHeight: width
 | 
			
		||||
 | 
			
		||||
				ClickableIcon {
 | 
			
		||||
					id: mouseArea
 | 
			
		||||
					anchors {
 | 
			
		||||
						top: parent.top
 | 
			
		||||
						bottom: parent.bottom
 | 
			
		||||
						horizontalCenter: parent.horizontalCenter
 | 
			
		||||
					}
 | 
			
		||||
					width: height
 | 
			
		||||
 | 
			
		||||
					acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
 | 
			
		||||
 | 
			
		||||
					image: item.modelData.icon
 | 
			
		||||
					showPressed: item.targetMenuOpen || (pressedButtons & ~Qt.RightButton)
 | 
			
		||||
					fillWindowWidth: true
 | 
			
		||||
					extraVerticalMargin: column.spacing / 2
 | 
			
		||||
 | 
			
		||||
					onClicked: event => {
 | 
			
		||||
						event.accepted = true;
 | 
			
		||||
 | 
			
		||||
						if (event.button == Qt.LeftButton) {
 | 
			
		||||
							item.modelData.activate();
 | 
			
		||||
						} else if (event.button == Qt.MiddleButton) {
 | 
			
		||||
							item.modelData.secondaryActivate();
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					property bool targetMenuOpen: false;
 | 
			
		||||
					onTargetMenuOpenChanged: menu.showChildren = targetMenuOpen
 | 
			
		||||
 | 
			
		||||
					Layout.fillWidth: true
 | 
			
		||||
					implicitHeight: width
 | 
			
		||||
 | 
			
		||||
					ClickableIcon {
 | 
			
		||||
						id: mouseArea
 | 
			
		||||
						anchors {
 | 
			
		||||
							top: parent.top
 | 
			
		||||
							bottom: parent.bottom
 | 
			
		||||
							horizontalCenter: parent.horizontalCenter
 | 
			
		||||
					onPressed: event => {
 | 
			
		||||
						if (event.button == Qt.RightButton && item.modelData.hasMenu) {
 | 
			
		||||
							item.targetMenuOpen = !item.targetMenuOpen;
 | 
			
		||||
						}
 | 
			
		||||
						width: height
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
						acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
 | 
			
		||||
					onWheel: event => {
 | 
			
		||||
						event.accepted = true;
 | 
			
		||||
						const points = event.angleDelta.y / 120
 | 
			
		||||
						item.modelData.scroll(points, false);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
						image: modelData.icon
 | 
			
		||||
						showPressed: targetMenuOpen
 | 
			
		||||
					property var tooltip: TooltipItem {
 | 
			
		||||
						tooltip: root.bar.tooltip
 | 
			
		||||
						owner: mouseArea
 | 
			
		||||
 | 
			
		||||
						onClicked: event => {
 | 
			
		||||
							event.accepted = true;
 | 
			
		||||
						show: mouseArea.containsMouse
 | 
			
		||||
 | 
			
		||||
							if (event.button == Qt.LeftButton) {
 | 
			
		||||
								modelData.activate();
 | 
			
		||||
							} else if (event.button == Qt.MiddleButton) {
 | 
			
		||||
								modelData.secondaryActivate();
 | 
			
		||||
							} else if (event.button == Qt.RightButton && menu != null) {
 | 
			
		||||
								targetMenuOpen = !targetMenuOpen;
 | 
			
		||||
							}
 | 
			
		||||
						Text {
 | 
			
		||||
							id: tooltipText
 | 
			
		||||
							text: item.modelData.tooltipTitle != "" ? item.modelData.tooltipTitle : item.modelData.id
 | 
			
		||||
							color: "white"
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
						onWheel: event => {
 | 
			
		||||
							event.accepted = true;
 | 
			
		||||
							const points = event.angleDelta.y / 120
 | 
			
		||||
							modelData.scroll(points, false);
 | 
			
		||||
						}
 | 
			
		||||
					property var rightclickMenu: TooltipItem {
 | 
			
		||||
						id: rightclickMenu
 | 
			
		||||
						tooltip: root.bar.tooltip
 | 
			
		||||
						owner: mouseArea
 | 
			
		||||
 | 
			
		||||
						property var tooltip: TooltipItem {
 | 
			
		||||
							tooltip: bar.tooltip
 | 
			
		||||
							owner: mouseArea
 | 
			
		||||
						isMenu: true
 | 
			
		||||
						show: item.targetMenuOpen
 | 
			
		||||
						animateSize: !(menuContentLoader?.item?.animating ?? false)
 | 
			
		||||
 | 
			
		||||
							show: mouseArea.containsMouse
 | 
			
		||||
						onClose: item.targetMenuOpen = false;
 | 
			
		||||
 | 
			
		||||
							Text {
 | 
			
		||||
								id: tooltipText
 | 
			
		||||
								text: modelData.tooltipTitle != "" ? modelData.tooltipTitle : modelData.id
 | 
			
		||||
								color: "white"
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
						Loader {
 | 
			
		||||
							id: menuContentLoader
 | 
			
		||||
							active: item.targetMenuOpen || rightclickMenu.visible || mouseArea.containsMouse
 | 
			
		||||
 | 
			
		||||
						property var rightclickMenu: TooltipItem {
 | 
			
		||||
							tooltip: bar.tooltip
 | 
			
		||||
							owner: mouseArea
 | 
			
		||||
 | 
			
		||||
							isMenu: true
 | 
			
		||||
							show: targetMenuOpen && menu.showChildren
 | 
			
		||||
							animateSize: !rightclickItems.animating
 | 
			
		||||
 | 
			
		||||
							onClose: targetMenuOpen = false;
 | 
			
		||||
 | 
			
		||||
							MenuItemList {
 | 
			
		||||
								id: rightclickItems
 | 
			
		||||
								items: menu == null ? [] : menu.children
 | 
			
		||||
								onClose: targetMenuOpen = false;
 | 
			
		||||
							sourceComponent: MenuView {
 | 
			
		||||
								menu: item.modelData.menu
 | 
			
		||||
								onClose: item.targetMenuOpen = false;
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,90 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
 | 
			
		||||
Scope {
 | 
			
		||||
	id: root
 | 
			
		||||
 | 
			
		||||
	required property MouseArea target;
 | 
			
		||||
 | 
			
		||||
	property real velocityX: 0;
 | 
			
		||||
	property real velocityY: 0;
 | 
			
		||||
 | 
			
		||||
	signal flickStarted();
 | 
			
		||||
	signal flickCompleted();
 | 
			
		||||
 | 
			
		||||
	property real dragStartX: 0;
 | 
			
		||||
	property real dragStartY: 0;
 | 
			
		||||
	property real dragDeltaX: 0;
 | 
			
		||||
	property real dragDeltaY: 0;
 | 
			
		||||
	property real dragEndX: 0;
 | 
			
		||||
	property real dragEndY: 0;
 | 
			
		||||
 | 
			
		||||
	property var sampleIdx: -1
 | 
			
		||||
	property var tSamples: []
 | 
			
		||||
	property var xSamples: []
 | 
			
		||||
	property var ySamples: []
 | 
			
		||||
 | 
			
		||||
	ElapsedTimer { id: velocityTimer }
 | 
			
		||||
 | 
			
		||||
	function resetSamples() {
 | 
			
		||||
		velocityTimer.restart();
 | 
			
		||||
		sampleIdx = -1;
 | 
			
		||||
		tSamples = [];
 | 
			
		||||
		xSamples = [];
 | 
			
		||||
		ySamples = [];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function sample() {
 | 
			
		||||
		const deltaT = velocityTimer.elapsed();
 | 
			
		||||
 | 
			
		||||
		sampleIdx++;
 | 
			
		||||
		if (sampleIdx > 5) {
 | 
			
		||||
			sampleIdx = 0;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tSamples[sampleIdx] = deltaT;
 | 
			
		||||
		xSamples[sampleIdx] = dragDeltaX;
 | 
			
		||||
		ySamples[sampleIdx] = dragDeltaY;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function updateVelocity() {
 | 
			
		||||
		let firstIdx = sampleIdx + 1;
 | 
			
		||||
		if (firstIdx > tSamples.length - 1) firstIdx = 0;
 | 
			
		||||
 | 
			
		||||
		const deltaT = tSamples[sampleIdx] - tSamples[firstIdx];
 | 
			
		||||
		const deltaX = xSamples[sampleIdx] - xSamples[firstIdx];
 | 
			
		||||
		const deltaY = ySamples[sampleIdx] - ySamples[firstIdx];
 | 
			
		||||
 | 
			
		||||
		root.velocityX = deltaX / deltaT;
 | 
			
		||||
		root.velocityY = deltaY / deltaT;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Connections {
 | 
			
		||||
		target: root.target;
 | 
			
		||||
 | 
			
		||||
		function onPressed(event: MouseEvent) {
 | 
			
		||||
			root.resetSamples();
 | 
			
		||||
			root.dragDeltaX = 0;
 | 
			
		||||
			root.dragDeltaY = 0;
 | 
			
		||||
			root.dragStartX = event.x;
 | 
			
		||||
			root.dragStartY = event.y;
 | 
			
		||||
			root.flickStarted();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function onReleased(event: MouseEvent) {
 | 
			
		||||
			root.dragDeltaX = event.x - root.dragStartX;
 | 
			
		||||
			root.dragDeltaY = event.y - root.dragStartY;
 | 
			
		||||
			root.dragEndX = event.x;
 | 
			
		||||
			root.dragEndY = event.y;
 | 
			
		||||
			root.sample();
 | 
			
		||||
			root.updateVelocity();
 | 
			
		||||
			root.flickCompleted();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function onPositionChanged(event: MouseEvent) {
 | 
			
		||||
			root.dragDeltaX = event.x - root.dragStartX;
 | 
			
		||||
			root.dragDeltaY = event.y - root.dragStartY;
 | 
			
		||||
			root.sample();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,99 @@
 | 
			
		|||
pragma ComponentBehavior: Bound;
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	id: root
 | 
			
		||||
 | 
			
		||||
	property list<string> values;
 | 
			
		||||
	property int index: 0;
 | 
			
		||||
 | 
			
		||||
	implicitWidth: 300
 | 
			
		||||
	implicitHeight: 40
 | 
			
		||||
 | 
			
		||||
	MouseArea {
 | 
			
		||||
		id: mouseArea
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
		property real halfHandle: handle.width / 2;
 | 
			
		||||
		property real activeWidth: groove.width - handle.width;
 | 
			
		||||
		property real valueOffset: mouseArea.halfHandle + (root.index / (root.values.length - 1)) * mouseArea.activeWidth;
 | 
			
		||||
 | 
			
		||||
		Repeater {
 | 
			
		||||
			model: root.values
 | 
			
		||||
 | 
			
		||||
			Item {
 | 
			
		||||
				id: delegate
 | 
			
		||||
				required property int index;
 | 
			
		||||
				required property string modelData;
 | 
			
		||||
 | 
			
		||||
				anchors.top: groove.bottom
 | 
			
		||||
				anchors.topMargin: 2
 | 
			
		||||
				x: mouseArea.halfHandle + (delegate.index / (root.values.length - 1)) * mouseArea.activeWidth
 | 
			
		||||
 | 
			
		||||
				Rectangle {
 | 
			
		||||
					id: mark
 | 
			
		||||
					color: "#60eeffff"
 | 
			
		||||
					width: 1
 | 
			
		||||
					height: groove.height
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Text {
 | 
			
		||||
					anchors.top: mark.bottom
 | 
			
		||||
 | 
			
		||||
					x: delegate.index === 0 ? -4
 | 
			
		||||
					 : delegate.index === root.values.length - 1 ? -this.width + 4
 | 
			
		||||
					 : -(this.width / 2);
 | 
			
		||||
 | 
			
		||||
					text: delegate.modelData
 | 
			
		||||
					color: "#a0eeffff"
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Rectangle {
 | 
			
		||||
			id: grooveFill
 | 
			
		||||
 | 
			
		||||
			anchors {
 | 
			
		||||
				left: groove.left
 | 
			
		||||
				top: groove.top
 | 
			
		||||
				bottom: groove.bottom
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			radius: 5
 | 
			
		||||
			color: "#80ceffff"
 | 
			
		||||
			width: mouseArea.valueOffset
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Rectangle {
 | 
			
		||||
			id: groove
 | 
			
		||||
 | 
			
		||||
			anchors {
 | 
			
		||||
				left: parent.left
 | 
			
		||||
				right: parent.right
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			y: 5
 | 
			
		||||
			implicitHeight: 7
 | 
			
		||||
			color: "transparent"
 | 
			
		||||
			border.color: "#20eeffff"
 | 
			
		||||
			border.width: 1
 | 
			
		||||
			radius: 5
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Rectangle {
 | 
			
		||||
			id: handle
 | 
			
		||||
			anchors.verticalCenter: groove.verticalCenter
 | 
			
		||||
			height: 15
 | 
			
		||||
			width: height
 | 
			
		||||
			radius: height * 0.5
 | 
			
		||||
			x: mouseArea.valueOffset - width * 0.5
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Binding {
 | 
			
		||||
		when: mouseArea.pressed
 | 
			
		||||
		root.index: Math.max(0, Math.min(root.values.length - 1, Math.round((mouseArea.mouseX / root.width) * (root.values.length - 1))));
 | 
			
		||||
		restoreMode: Binding.RestoreBinding
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	id: root
 | 
			
		||||
 | 
			
		||||
	property real from: 0.0
 | 
			
		||||
	property real to: 1.0
 | 
			
		||||
	property real value: 0.0
 | 
			
		||||
 | 
			
		||||
	implicitHeight: 7
 | 
			
		||||
	implicitWidth: 200
 | 
			
		||||
 | 
			
		||||
	Rectangle {
 | 
			
		||||
		id: grooveFill
 | 
			
		||||
 | 
			
		||||
		anchors {
 | 
			
		||||
			left: groove.left
 | 
			
		||||
			top: groove.top
 | 
			
		||||
			bottom: groove.bottom
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		radius: 5
 | 
			
		||||
		color: "#80ceffff"
 | 
			
		||||
		width: root.width * ((root.value - root.from) / (root.to - root.from))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Rectangle {
 | 
			
		||||
		id: groove
 | 
			
		||||
 | 
			
		||||
		anchors {
 | 
			
		||||
			left: parent.left
 | 
			
		||||
			right: parent.right
 | 
			
		||||
			verticalCenter: parent.verticalCenter
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		height: 7
 | 
			
		||||
		color: "transparent"
 | 
			
		||||
		border.color: "#20eeffff"
 | 
			
		||||
		border.width: 1
 | 
			
		||||
		radius: 5
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	id: root
 | 
			
		||||
	onChildrenChanged: recalc();
 | 
			
		||||
 | 
			
		||||
	Instantiator {
 | 
			
		||||
		model: root.children
 | 
			
		||||
 | 
			
		||||
		Connections {
 | 
			
		||||
			required property Item modelData;
 | 
			
		||||
			target: modelData;
 | 
			
		||||
 | 
			
		||||
			function onImplicitHeightChanged() {
 | 
			
		||||
				root.recalc();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			function onImplicitWidthChanged() {
 | 
			
		||||
				root.recalc();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function recalc() {
 | 
			
		||||
		let y = 0
 | 
			
		||||
		let w = 0
 | 
			
		||||
		for (const child of this.children) {
 | 
			
		||||
			child.y = y;
 | 
			
		||||
			y += child.implicitHeight
 | 
			
		||||
			if (child.implicitWidth > w) w = child.implicitWidth;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.implicitHeight = y;
 | 
			
		||||
		this.implicitWidth = w;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								modules/user/modules/quickshell/shell/greeter.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Wayland
 | 
			
		||||
import Quickshell.Services.Greetd
 | 
			
		||||
import ".."
 | 
			
		||||
import "lock"
 | 
			
		||||
 | 
			
		||||
ShellRoot {
 | 
			
		||||
	GreeterContext {
 | 
			
		||||
		id: context
 | 
			
		||||
 | 
			
		||||
		onLaunch: {
 | 
			
		||||
			lock.locked = false;
 | 
			
		||||
			Greetd.launch(["hyprland"]);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	WlSessionLock {
 | 
			
		||||
		id: lock
 | 
			
		||||
		locked: true
 | 
			
		||||
 | 
			
		||||
		WlSessionLockSurface {
 | 
			
		||||
			id: lockSurface
 | 
			
		||||
 | 
			
		||||
			BackgroundImage {
 | 
			
		||||
				id: backgroundImage
 | 
			
		||||
				anchors.fill: parent
 | 
			
		||||
				screen: lockSurface.screen
 | 
			
		||||
				asynchronous: true
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			LockContent {
 | 
			
		||||
				anchors.fill: parent
 | 
			
		||||
				state: context.state
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M150.81,131.79a8,8,0,0,1,.35,7.79l-16,32a8,8,0,0,1-14.32-7.16L131.06,144H112a8,8,0,0,1-7.16-11.58l16-32a8,8,0,1,1,14.32,7.16L124.94,128H144A8,8,0,0,1,150.81,131.79ZM96,16h64a8,8,0,0,0,0-16H96a8,8,0,0,0,0,16ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 499 B  | 
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M88,8a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,8ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 348 B  | 
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M88,8a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,8ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Zm-28,76H136V112a8,8,0,0,0-16,0v20H100a8,8,0,0,0,0,16h20v20a8,8,0,0,0,16,0V148h20a8,8,0,0,0,0-16Z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 443 B  | 
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M120,136V96a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm8,24a12,12,0,1,0,12,12A12,12,0,0,0,128,160ZM96,16h64a8,8,0,0,0,0-16H96a8,8,0,0,0,0,16ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 424 B  | 
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M221.8,175.94C216.25,166.38,208,139.33,208,104a80,80,0,1,0-160,0c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.81a40,40,0,0,0,78.38,0H208a16,16,0,0,0,13.8-24.06ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216Z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 348 B  | 
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M53.92,34.62A8,8,0,1,0,42.08,45.38L58.82,63.8A79.59,79.59,0,0,0,48,104c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.8a40,40,0,0,0,78.4,0h15.44l19.44,21.38a8,8,0,1,0,11.84-10.76ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216ZM48,184c7.7-13.24,16-43.92,16-80a63.65,63.65,0,0,1,6.26-27.62L168.09,184Zm166-4.73a8.13,8.13,0,0,1-2.93.55,8,8,0,0,1-7.44-5.08C196.35,156.19,192,129.75,192,104A64,64,0,0,0,96.43,48.31a8,8,0,0,1-7.9-13.91A80,80,0,0,1,208,104c0,35.35,8.05,58.59,10.52,64.88A8,8,0,0,1,214,179.25Z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 641 B  | 
							
								
								
									
										1
									
								
								modules/user/modules/quickshell/shell/icons/bell.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M221.8,175.94C216.25,166.38,208,139.33,208,104a80,80,0,1,0-160,0c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.81a40,40,0,0,0,78.38,0H208a16,16,0,0,0,13.8-24.06ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216ZM48,184c7.7-13.24,16-43.92,16-80a64,64,0,1,1,128,0c0,36.05,8.28,66.73,16,80Z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 424 B  | 
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M256,128a15.76,15.76,0,0,1-7.33,13.34L160.48,197.5A15.91,15.91,0,0,1,136,184.16v-37.3L56.48,197.5A15.91,15.91,0,0,1,32,184.16V71.84A15.91,15.91,0,0,1,56.48,58.5L136,109.14V71.84A15.91,15.91,0,0,1,160.48,58.5l88.19,56.16A15.76,15.76,0,0,1,256,128Z"></path></svg>
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M256,128a15.76,15.76,0,0,1-7.33,13.34L160.48,197.5A15.91,15.91,0,0,1,136,184.16v-37.3L56.48,197.5A15.91,15.91,0,0,1,32,184.16V71.84A15.91,15.91,0,0,1,56.48,58.5L136,109.14V71.84A15.91,15.91,0,0,1,160.48,58.5l88.19,56.16A15.76,15.76,0,0,1,256,128Z"></path></svg>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
		 Before Width: | Height: | Size: 445 B After Width: | Height: | Size: 445 B  | 
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M72,128a134.63,134.63,0,0,1-14.16,60.47,8,8,0,1,1-14.32-7.12A118.8,118.8,0,0,0,56,128,71.73,71.73,0,0,1,83,71.8,8,8,0,1,1,93,84.29,55.76,55.76,0,0,0,72,128Zm56-8a8,8,0,0,0-8,8,184.12,184.12,0,0,1-23,89.1,8,8,0,0,0,14,7.76A200.19,200.19,0,0,0,136,128,8,8,0,0,0,128,120Zm0-32a40,40,0,0,0-40,40,8,8,0,0,0,16,0,24,24,0,0,1,48,0,214.09,214.09,0,0,1-20.51,92A8,8,0,1,0,146,226.83,230,230,0,0,0,168,128,40,40,0,0,0,128,88Zm0-64A104.11,104.11,0,0,0,24,128a87.76,87.76,0,0,1-5,29.33,8,8,0,0,0,15.09,5.33A103.9,103.9,0,0,0,40,128a88,88,0,0,1,176,0,282.24,282.24,0,0,1-5.29,54.45,8,8,0,0,0,6.3,9.4,8.22,8.22,0,0,0,1.55.15,8,8,0,0,0,7.84-6.45A298.37,298.37,0,0,0,232,128,104.12,104.12,0,0,0,128,24ZM94.4,152.17A8,8,0,0,0,85,158.42a151,151,0,0,1-17.21,45.44,8,8,0,0,0,13.86,8,166.67,166.67,0,0,0,19-50.25A8,8,0,0,0,94.4,152.17ZM128,56a72.85,72.85,0,0,0-9,.56,8,8,0,0,0,2,15.87A56.08,56.08,0,0,1,184,128a252.12,252.12,0,0,1-1.92,31A8,8,0,0,0,189,168a8.39,8.39,0,0,0,1,.06,8,8,0,0,0,7.92-7,266.48,266.48,0,0,0,2-33A72.08,72.08,0,0,0,128,56Zm57.93,128.25a8,8,0,0,0-9.75,5.75c-1.46,5.69-3.15,11.4-5,17a8,8,0,0,0,5,10.13,7.88,7.88,0,0,0,2.55.42,8,8,0,0,0,7.58-5.46c2-5.92,3.79-12,5.35-18.05A8,8,0,0,0,185.94,184.26Z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
							
								
								
									
										1
									
								
								modules/user/modules/quickshell/shell/icons/gauge.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M207.06,72.67A111.24,111.24,0,0,0,128,40h-.4C66.07,40.21,16,91,16,153.13V176a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V152A111.25,111.25,0,0,0,207.06,72.67ZM224,176H119.71l54.76-75.3a8,8,0,0,0-12.94-9.42L99.92,176H32V153.13c0-3.08.15-6.12.43-9.13H56a8,8,0,0,0,0-16H35.27c10.32-38.86,44-68.24,84.73-71.66V80a8,8,0,0,0,16,0V56.33A96.14,96.14,0,0,1,221,128H200a8,8,0,0,0,0,16h23.67c.21,2.65.33,5.31.33,8Z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 523 B  | 
							
								
								
									
										1
									
								
								modules/user/modules/quickshell/shell/icons/headset.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M201.89,54.66A103.43,103.43,0,0,0,128.79,24H128A104,104,0,0,0,24,128v56a24,24,0,0,0,24,24H64a24,24,0,0,0,24-24V144a24,24,0,0,0-24-24H40.36A88.12,88.12,0,0,1,190.54,65.93,87.39,87.39,0,0,1,215.65,120H192a24,24,0,0,0-24,24v40a24,24,0,0,0,24,24h24a24,24,0,0,1-24,24H136a8,8,0,0,0,0,16h56a40,40,0,0,0,40-40V128A103.41,103.41,0,0,0,201.89,54.66ZM64,136a8,8,0,0,1,8,8v40a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V136Zm128,56a8,8,0,0,1-8-8V144a8,8,0,0,1,8-8h24v56Z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 570 B  | 
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M232.49,215.51,185,168a92.12,92.12,0,1,0-17,17l47.53,47.54a12,12,0,0,0,17-17ZM44,112a68,68,0,1,1,68,68A68.07,68.07,0,0,1,44,112Z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 252 B  | 
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256"><path d="M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8Zm-48,48a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,224Z"></path></svg>
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8Zm-48,48a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,224Z"></path></svg>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
		 Before Width: | Height: | Size: 353 B After Width: | Height: | Size: 354 B  | 
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M216,48V208a16,16,0,0,1-16,16H160a16,16,0,0,1-16-16V48a16,16,0,0,1,16-16h40A16,16,0,0,1,216,48ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Z"></path></svg>
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M216,48V208a16,16,0,0,1-16,16H160a16,16,0,0,1-16-16V48a16,16,0,0,1,16-16h40A16,16,0,0,1,216,48ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Z"></path></svg>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
		 Before Width: | Height: | Size: 386 B After Width: | Height: | Size: 386 B  | 
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
<svg
 | 
			
		||||
   width="32"
 | 
			
		||||
   height="32"
 | 
			
		||||
   fill="#ffffff"
 | 
			
		||||
   fill="#f0f0f0"
 | 
			
		||||
   viewBox="0 0 256 256"
 | 
			
		||||
   data-darkreader-inline-fill=""
 | 
			
		||||
   version="1.1"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
		 Before Width: | Height: | Size: 580 B After Width: | Height: | Size: 580 B  | 
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Z"></path></svg>
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Z"></path></svg>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
		 Before Width: | Height: | Size: 521 B After Width: | Height: | Size: 521 B  | 
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
<svg
 | 
			
		||||
   width="32"
 | 
			
		||||
   height="32"
 | 
			
		||||
   fill="#ffffff"
 | 
			
		||||
   fill="#f0f0f0"
 | 
			
		||||
   viewBox="0 0 256 256"
 | 
			
		||||
   data-darkreader-inline-fill=""
 | 
			
		||||
   version="1.1"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
		 Before Width: | Height: | Size: 957 B After Width: | Height: | Size: 957 B  | 
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Zm-88,40a8,8,0,0,0,8-8V104a8,8,0,0,0-11.58-7.16l-16,8a8,8,0,1,0,7.16,14.31l4.42-2.21V152A8,8,0,0,0,136,160Z"></path></svg>
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Zm-88,40a8,8,0,0,0,8-8V104a8,8,0,0,0-11.58-7.16l-16,8a8,8,0,1,0,7.16,14.31l4.42-2.21V152A8,8,0,0,0,136,160Z"></path></svg>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
		 Before Width: | Height: | Size: 627 B After Width: | Height: | Size: 627 B  | 
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M232,71.84V184.16a15.92,15.92,0,0,1-24.48,13.34L128,146.86v37.3a15.92,15.92,0,0,1-24.48,13.34L15.33,141.34a15.8,15.8,0,0,1,0-26.68L103.52,58.5A15.91,15.91,0,0,1,128,71.84v37.3L207.52,58.5A15.91,15.91,0,0,1,232,71.84Z"></path></svg>
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M232,71.84V184.16a15.92,15.92,0,0,1-24.48,13.34L128,146.86v37.3a15.92,15.92,0,0,1-24.48,13.34L15.33,141.34a15.8,15.8,0,0,1,0-26.68L103.52,58.5A15.91,15.91,0,0,1,128,71.84v37.3L207.52,58.5A15.91,15.91,0,0,1,232,71.84Z"></path></svg>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
		 Before Width: | Height: | Size: 415 B After Width: | Height: | Size: 415 B  | 
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
<svg
 | 
			
		||||
   width="32"
 | 
			
		||||
   height="32"
 | 
			
		||||
   fill="#ffffff"
 | 
			
		||||
   fill="#f0f0f0"
 | 
			
		||||
   viewBox="0 0 256 256"
 | 
			
		||||
   data-darkreader-inline-fill=""
 | 
			
		||||
   version="1.1"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
		 Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 847 B  | 
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M237.66,178.34a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,200,208V192a72.15,72.15,0,0,1-57.65-30.14l-41.72-58.4A56.1,56.1,0,0,0,55.06,80H32a8,8,0,0,1,0-16H55.06a72.12,72.12,0,0,1,58.59,30.15l41.72,58.4A56.08,56.08,0,0,0,200,176V160a8,8,0,0,1,13.66-5.66ZM143,107a8,8,0,0,0,11.16-1.86l1.2-1.67A56.08,56.08,0,0,1,200,80V96a8,8,0,0,0,13.66,5.66l24-24a8,8,0,0,0,0-11.32l-24-24A8,8,0,0,0,200,48V64a72.15,72.15,0,0,0-57.65,30.14l-1.2,1.67A8,8,0,0,0,143,107Zm-30,42a8,8,0,0,0-11.16,1.86l-1.2,1.67A56.1,56.1,0,0,1,55.06,176H32a8,8,0,0,0,0,16H55.06a72.12,72.12,0,0,0,58.59-30.15l1.2-1.67A8,8,0,0,0,113,149Z"></path></svg>
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M237.66,178.34a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,200,208V192a72.15,72.15,0,0,1-57.65-30.14l-41.72-58.4A56.1,56.1,0,0,0,55.06,80H32a8,8,0,0,1,0-16H55.06a72.12,72.12,0,0,1,58.59,30.15l41.72,58.4A56.08,56.08,0,0,0,200,176V160a8,8,0,0,1,13.66-5.66ZM143,107a8,8,0,0,0,11.16-1.86l1.2-1.67A56.08,56.08,0,0,1,200,80V96a8,8,0,0,0,13.66,5.66l24-24a8,8,0,0,0,0-11.32l-24-24A8,8,0,0,0,200,48V64a72.15,72.15,0,0,0-57.65,30.14l-1.2,1.67A8,8,0,0,0,143,107Zm-30,42a8,8,0,0,0-11.16,1.86l-1.2,1.67A56.1,56.1,0,0,1,55.06,176H32a8,8,0,0,0,0,16H55.06a72.12,72.12,0,0,0,58.59-30.15l1.2-1.67A8,8,0,0,0,113,149Z"></path></svg>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
		 Before Width: | Height: | Size: 787 B After Width: | Height: | Size: 787 B  | 
| 
						 | 
				
			
			@ -1 +0,0 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M208,47.88V208.12a16,16,0,0,1-24.43,13.43L64,146.77V216a8,8,0,0,1-16,0V40a8,8,0,0,1,16,0v69.23L183.57,34.45A15.95,15.95,0,0,1,208,47.88Z"></path></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 335 B  | 
| 
						 | 
				
			
			@ -1 +0,0 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M208,40V216a8,8,0,0,1-16,0V146.77L72.43,221.55A15.95,15.95,0,0,1,48,208.12V47.88A15.95,15.95,0,0,1,72.43,34.45L192,109.23V40a8,8,0,0,1,16,0Z"></path></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 339 B  | 
							
								
								
									
										281
									
								
								modules/user/modules/quickshell/shell/launcher/Controller.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,281 @@
 | 
			
		|||
pragma Singleton
 | 
			
		||||
pragma ComponentBehavior: Bound
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Io
 | 
			
		||||
import Quickshell.Wayland
 | 
			
		||||
import Quickshell.Widgets
 | 
			
		||||
import Quickshell.Services.SystemTray
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
Singleton {
 | 
			
		||||
	PersistentProperties {
 | 
			
		||||
		id: persist
 | 
			
		||||
		property bool launcherOpen: false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	IpcHandler {
 | 
			
		||||
		target: "launcher"
 | 
			
		||||
 | 
			
		||||
		function open(): void {
 | 
			
		||||
			persist.launcherOpen = true;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function close(): void {
 | 
			
		||||
			persist.launcherOpen = false;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function toggle(): void {
 | 
			
		||||
			persist.launcherOpen = !persist.launcherOpen
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	LazyLoader {
 | 
			
		||||
		id: loader
 | 
			
		||||
		activeAsync: persist.launcherOpen
 | 
			
		||||
 | 
			
		||||
		PanelWindow {
 | 
			
		||||
			width: 450
 | 
			
		||||
			height: 7 + searchContainer.implicitHeight + list.topMargin * 2 + list.delegateHeight * 10
 | 
			
		||||
			color: "transparent"
 | 
			
		||||
			WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
 | 
			
		||||
			WlrLayershell.namespace: "shell:launcher"
 | 
			
		||||
 | 
			
		||||
			Rectangle {
 | 
			
		||||
				//anchors.fill: parent
 | 
			
		||||
				height: 7 + searchContainer.implicitHeight + list.topMargin + list.bottomMargin + Math.min(list.contentHeight, list.delegateHeight * 10)
 | 
			
		||||
				Behavior on height { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
 | 
			
		||||
				width: 450
 | 
			
		||||
				color: ShellGlobals.colors.bar
 | 
			
		||||
				radius: 5
 | 
			
		||||
				border.color: ShellGlobals.colors.barOutline
 | 
			
		||||
				border.width: 1
 | 
			
		||||
 | 
			
		||||
				ColumnLayout {
 | 
			
		||||
					anchors.fill: parent
 | 
			
		||||
					anchors.margins: 7
 | 
			
		||||
					anchors.bottomMargin: 0
 | 
			
		||||
					spacing: 0
 | 
			
		||||
 | 
			
		||||
					Rectangle {
 | 
			
		||||
						id: searchContainer
 | 
			
		||||
						Layout.fillWidth: true
 | 
			
		||||
						implicitHeight: searchbox.implicitHeight + 10
 | 
			
		||||
						color: "#30c0ffff"
 | 
			
		||||
						radius: 3
 | 
			
		||||
						border.color: "#50ffffff"
 | 
			
		||||
 | 
			
		||||
						RowLayout {
 | 
			
		||||
							id: searchbox
 | 
			
		||||
							anchors.fill: parent
 | 
			
		||||
							anchors.margins: 5
 | 
			
		||||
 | 
			
		||||
							IconImage {
 | 
			
		||||
								implicitSize: parent.height
 | 
			
		||||
								source: "root:icons/magnifying-glass.svg"
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							TextInput {
 | 
			
		||||
								id: search
 | 
			
		||||
								Layout.fillWidth: true
 | 
			
		||||
								color: "white"
 | 
			
		||||
 | 
			
		||||
								focus: true
 | 
			
		||||
								Keys.forwardTo: [list]
 | 
			
		||||
								Keys.onEscapePressed: persist.launcherOpen = false
 | 
			
		||||
 | 
			
		||||
								Keys.onPressed: event => {
 | 
			
		||||
									if (event.modifiers & Qt.ControlModifier) {
 | 
			
		||||
										if (event.key == Qt.Key_J) {
 | 
			
		||||
											list.currentIndex = list.currentIndex == list.count - 1 ? 0 : list.currentIndex + 1;
 | 
			
		||||
											event.accepted = true;
 | 
			
		||||
										} else if (event.key == Qt.Key_K) {
 | 
			
		||||
											list.currentIndex = list.currentIndex == 0 ? list.count - 1 : list.currentIndex - 1;
 | 
			
		||||
											event.accepted = true;
 | 
			
		||||
										}
 | 
			
		||||
									}
 | 
			
		||||
								}
 | 
			
		||||
 | 
			
		||||
								onAccepted: {
 | 
			
		||||
									if (list.currentItem) {
 | 
			
		||||
										list.currentItem.clicked(null);
 | 
			
		||||
									}
 | 
			
		||||
								}
 | 
			
		||||
 | 
			
		||||
								onTextChanged: {
 | 
			
		||||
									list.currentIndex = 0;
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					ListView {
 | 
			
		||||
						id: list
 | 
			
		||||
						Layout.fillWidth: true
 | 
			
		||||
						Layout.fillHeight: true
 | 
			
		||||
						clip: true
 | 
			
		||||
						cacheBuffer: 0 // works around QTBUG-131106
 | 
			
		||||
						//reuseItems: true
 | 
			
		||||
						model: ScriptModel {
 | 
			
		||||
							values: DesktopEntries.applications.values
 | 
			
		||||
								.map(object => {
 | 
			
		||||
									const stxt = search.text.toLowerCase();
 | 
			
		||||
									const ntxt = object.name.toLowerCase();
 | 
			
		||||
									let si = 0;
 | 
			
		||||
									let ni = 0;
 | 
			
		||||
 | 
			
		||||
									let matches = [];
 | 
			
		||||
									let startMatch = -1;
 | 
			
		||||
 | 
			
		||||
									for (let si = 0; si != stxt.length; ++si) {
 | 
			
		||||
										const sc = stxt[si];
 | 
			
		||||
 | 
			
		||||
										while (true) {
 | 
			
		||||
											// Drop any entries with letters that don't exist in order
 | 
			
		||||
											if (ni == ntxt.length) return null;
 | 
			
		||||
 | 
			
		||||
											const nc = ntxt[ni++];
 | 
			
		||||
 | 
			
		||||
											if (nc == sc) {
 | 
			
		||||
												if (startMatch == -1) startMatch = ni;
 | 
			
		||||
												break;
 | 
			
		||||
											} else {
 | 
			
		||||
												if (startMatch != -1) {
 | 
			
		||||
													matches.push({
 | 
			
		||||
														index: startMatch,
 | 
			
		||||
														length: ni - startMatch,
 | 
			
		||||
													});
 | 
			
		||||
 | 
			
		||||
													startMatch = -1;
 | 
			
		||||
												}
 | 
			
		||||
											}
 | 
			
		||||
										}
 | 
			
		||||
									}
 | 
			
		||||
 | 
			
		||||
									if (startMatch != -1) {
 | 
			
		||||
										matches.push({
 | 
			
		||||
											index: startMatch,
 | 
			
		||||
											length: ni - startMatch + 1,
 | 
			
		||||
										});
 | 
			
		||||
									}
 | 
			
		||||
 | 
			
		||||
									return {
 | 
			
		||||
										object: object,
 | 
			
		||||
										matches: matches,
 | 
			
		||||
									};
 | 
			
		||||
								})
 | 
			
		||||
								.filter(entry => entry !== null)
 | 
			
		||||
								.sort((a, b) => {
 | 
			
		||||
									let ai = 0;
 | 
			
		||||
									let bi = 0;
 | 
			
		||||
									let s = 0;
 | 
			
		||||
 | 
			
		||||
									while (ai != a.matches.length && bi != b.matches.length) {
 | 
			
		||||
										const am = a.matches[ai];
 | 
			
		||||
										const bm = b.matches[bi];
 | 
			
		||||
 | 
			
		||||
										s = bm.length - am.length;
 | 
			
		||||
										if (s != 0) return s;
 | 
			
		||||
 | 
			
		||||
										s = am.index - bm.index;
 | 
			
		||||
										if (s != 0) return s;
 | 
			
		||||
 | 
			
		||||
										++ai;
 | 
			
		||||
										++bi;
 | 
			
		||||
									}
 | 
			
		||||
 | 
			
		||||
									s = a.matches.length - b.matches.length;
 | 
			
		||||
									if (s != 0) return s;
 | 
			
		||||
 | 
			
		||||
									s = a.object.name.length - b.object.name.length;
 | 
			
		||||
									if (s != 0) return s;
 | 
			
		||||
 | 
			
		||||
									return a.object.name.localeCompare(b.object.name);
 | 
			
		||||
								})
 | 
			
		||||
								.map(entry => entry.object);
 | 
			
		||||
 | 
			
		||||
							onValuesChanged: list.currentIndex = 0
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						topMargin: 7
 | 
			
		||||
						bottomMargin: list.count == 0 ? 0 : 7
 | 
			
		||||
 | 
			
		||||
						add: Transition {
 | 
			
		||||
							NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 100 }
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						displaced: Transition {
 | 
			
		||||
							NumberAnimation { property: "y"; duration: 200; easing.type: Easing.OutCubic }
 | 
			
		||||
							NumberAnimation { property: "opacity"; to: 1; duration: 100 }
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						move: Transition {
 | 
			
		||||
							NumberAnimation { property: "y"; duration: 200; easing.type: Easing.OutCubic }
 | 
			
		||||
							NumberAnimation { property: "opacity"; to: 1; duration: 100 }
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						remove: Transition {
 | 
			
		||||
							NumberAnimation { property: "y"; duration: 200; easing.type: Easing.OutCubic }
 | 
			
		||||
							NumberAnimation { property: "opacity"; to: 0; duration: 100 }
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						highlight: Rectangle {
 | 
			
		||||
							radius: 5
 | 
			
		||||
							color: "#20e0ffff"
 | 
			
		||||
							border.color: "#30ffffff"
 | 
			
		||||
							border.width: 1
 | 
			
		||||
						}
 | 
			
		||||
						keyNavigationEnabled: true
 | 
			
		||||
						keyNavigationWraps: true
 | 
			
		||||
						highlightMoveVelocity: -1
 | 
			
		||||
						highlightMoveDuration: 100
 | 
			
		||||
						preferredHighlightBegin: list.topMargin
 | 
			
		||||
						preferredHighlightEnd: list.height - list.bottomMargin
 | 
			
		||||
						highlightRangeMode: ListView.ApplyRange
 | 
			
		||||
						snapMode: ListView.SnapToItem
 | 
			
		||||
 | 
			
		||||
						readonly property real delegateHeight: 44
 | 
			
		||||
 | 
			
		||||
						delegate: MouseArea {
 | 
			
		||||
							required property DesktopEntry modelData;
 | 
			
		||||
 | 
			
		||||
							implicitHeight: list.delegateHeight
 | 
			
		||||
							implicitWidth: ListView.view.width
 | 
			
		||||
 | 
			
		||||
							onClicked: {
 | 
			
		||||
								modelData.execute();
 | 
			
		||||
								persist.launcherOpen = false;
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							RowLayout {
 | 
			
		||||
								id: delegateLayout
 | 
			
		||||
								anchors {
 | 
			
		||||
									verticalCenter: parent.verticalCenter
 | 
			
		||||
									left: parent.left
 | 
			
		||||
									leftMargin: 5
 | 
			
		||||
								}
 | 
			
		||||
 | 
			
		||||
								IconImage {
 | 
			
		||||
									Layout.alignment: Qt.AlignVCenter
 | 
			
		||||
									asynchronous: true
 | 
			
		||||
									implicitSize: 30
 | 
			
		||||
									source: Quickshell.iconPath(modelData.icon)
 | 
			
		||||
								}
 | 
			
		||||
								Text {
 | 
			
		||||
									text: modelData.name
 | 
			
		||||
									color: "#f0f0f0"
 | 
			
		||||
									Layout.alignment: Qt.AlignVCenter
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function init() {}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,8 +3,10 @@ pragma Singleton
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Io
 | 
			
		||||
import Quickshell.Wayland
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
import Quickshell.Services.Pam
 | 
			
		||||
import ".."
 | 
			
		||||
import "../.."
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -61,18 +63,15 @@ Singleton {
 | 
			
		|||
		root.oldWorkspaces = ({});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Shortcut {
 | 
			
		||||
		name: "lock"
 | 
			
		||||
		onPressed: {
 | 
			
		||||
			if (root.locked) root.locked = false;
 | 
			
		||||
			else root.locked = true;
 | 
			
		||||
		}
 | 
			
		||||
	IpcHandler {
 | 
			
		||||
		target: "lockscreen"
 | 
			
		||||
		function lock(): void { root.locked = true; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	LazyLoader {
 | 
			
		||||
		id: lockContextLoader
 | 
			
		||||
 | 
			
		||||
		LockContext {
 | 
			
		||||
		SessionLockContext {
 | 
			
		||||
			onUnlocked: root.locked = false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +81,7 @@ Singleton {
 | 
			
		|||
 | 
			
		||||
		onSecureChanged: {
 | 
			
		||||
			if (secure) {
 | 
			
		||||
				Qt.callLater(() => root.workspaceLockAnimation());
 | 
			
		||||
				root.workspaceLockAnimation();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -108,7 +107,7 @@ Singleton {
 | 
			
		|||
 | 
			
		||||
			LockContent {
 | 
			
		||||
				id: lockContent
 | 
			
		||||
				context: lockContextLoader.item;
 | 
			
		||||
				state: lockContextLoader.item.state;
 | 
			
		||||
 | 
			
		||||
				visible: false
 | 
			
		||||
				width: lockSurface.width
 | 
			
		||||
| 
						 | 
				
			
			@ -128,7 +127,6 @@ Singleton {
 | 
			
		|||
			onVisibleChanged: {
 | 
			
		||||
				if (visible) {
 | 
			
		||||
					lockContent.y = -lockSurface.height
 | 
			
		||||
					console.log(`y ${lockContent.y}`)
 | 
			
		||||
					lockContent.visible = true;
 | 
			
		||||
					lockAnim.running = true;
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Services.Greetd
 | 
			
		||||
 | 
			
		||||
Scope {
 | 
			
		||||
	id: root
 | 
			
		||||
	signal launch();
 | 
			
		||||
 | 
			
		||||
	property LockState state: LockState {
 | 
			
		||||
		onTryPasswordUnlock: {
 | 
			
		||||
			this.isUnlocking = true;
 | 
			
		||||
			Greetd.createSession("admin");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Connections {
 | 
			
		||||
		target: Greetd
 | 
			
		||||
 | 
			
		||||
		function onAuthMessage(message: string, error: bool, responseRequired: bool, echoResponse: bool) {
 | 
			
		||||
			if (responseRequired) {
 | 
			
		||||
				Greetd.respond(root.state.currentText);
 | 
			
		||||
			} // else ignore - only supporting passwords
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function onAuthFailure() {
 | 
			
		||||
			root.state.currentText = "";
 | 
			
		||||
			root.state.error = "Invalid password";
 | 
			
		||||
			root.state.failed = true;
 | 
			
		||||
			root.state.isUnlocking = false;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function onReadyToLaunch() {
 | 
			
		||||
			root.state.isUnlocking = false;
 | 
			
		||||
			root.launch();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -44,6 +44,8 @@ Item {
 | 
			
		|||
				anchors.margins: 15
 | 
			
		||||
 | 
			
		||||
				source: root.icon
 | 
			
		||||
				sourceSize.width: width
 | 
			
		||||
				sourceSize.height: height
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,113 +1,202 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import Quickshell
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property LockContext context;
 | 
			
		||||
	required property LockState state;
 | 
			
		||||
 | 
			
		||||
	property real focusAnim: focusAnimInternal * 0.001
 | 
			
		||||
	property int focusAnimInternal: Window.active ? 1000 : 0
 | 
			
		||||
	Behavior on focusAnimInternal { SmoothedAnimation { velocity: 5000 } }
 | 
			
		||||
 | 
			
		||||
	Rectangle {
 | 
			
		||||
		anchors.horizontalCenter: parent.horizontalCenter
 | 
			
		||||
		y: parent.height / 2 + textBox.height
 | 
			
		||||
		id: sep
 | 
			
		||||
	MouseArea {
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		hoverEnabled: true
 | 
			
		||||
 | 
			
		||||
		implicitHeight: 6
 | 
			
		||||
		implicitWidth: 800
 | 
			
		||||
		radius: height / 2
 | 
			
		||||
		color: ShellGlobals.colors.widget
 | 
			
		||||
	}
 | 
			
		||||
		property real startMoveX: 0
 | 
			
		||||
		property real startMoveY: 0
 | 
			
		||||
 | 
			
		||||
	ColumnLayout {
 | 
			
		||||
		implicitWidth: sep.implicitWidth
 | 
			
		||||
		anchors.horizontalCenter: parent.horizontalCenter
 | 
			
		||||
		anchors.bottom: sep.top
 | 
			
		||||
		spacing: 0
 | 
			
		||||
 | 
			
		||||
		Text {
 | 
			
		||||
			id: timeText
 | 
			
		||||
			Layout.alignment: Qt.AlignHCenter
 | 
			
		||||
 | 
			
		||||
			font {
 | 
			
		||||
				pointSize: 120
 | 
			
		||||
				hintingPreference: Font.PreferFullHinting
 | 
			
		||||
				family: "Noto Sans"
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			color: "white"
 | 
			
		||||
			renderType: Text.NativeRendering
 | 
			
		||||
 | 
			
		||||
			text: {
 | 
			
		||||
				const hours = ShellGlobals.time.getHours().toString().padStart(2, '0');
 | 
			
		||||
				const minutes = ShellGlobals.time.getMinutes().toString().padStart(2, '0');
 | 
			
		||||
				return `${hours}:${minutes}`;
 | 
			
		||||
		// prevents wakeups from bumping the mouse
 | 
			
		||||
		onPositionChanged: event => {
 | 
			
		||||
			if (root.state.fadedOut) {
 | 
			
		||||
				if (root.state.mouseMoved()) {
 | 
			
		||||
					const xOffset = Math.abs(event.x - startMoveX);
 | 
			
		||||
					const yOffset = Math.abs(event.y - startMoveY);
 | 
			
		||||
					const distanceSq = (xOffset * xOffset) + (yOffset * yOffset);
 | 
			
		||||
					if (distanceSq > (100 * 100)) root.state.fadeIn();
 | 
			
		||||
				} else {
 | 
			
		||||
					startMoveX = event.x;
 | 
			
		||||
					startMoveY = event.y;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Item {
 | 
			
		||||
			Layout.alignment: Qt.AlignHCenter
 | 
			
		||||
			implicitHeight: childrenRect.height * focusAnim
 | 
			
		||||
			implicitWidth: sep.implicitWidth
 | 
			
		||||
			clip: true
 | 
			
		||||
			id: content
 | 
			
		||||
			width: parent.width
 | 
			
		||||
			height: parent.height
 | 
			
		||||
			y: root.state.fadeOutMul * (height / 2 + childrenRect.height)
 | 
			
		||||
 | 
			
		||||
			TextInput {
 | 
			
		||||
				id: textBox
 | 
			
		||||
				focus: true
 | 
			
		||||
				width: parent.width
 | 
			
		||||
			Rectangle {
 | 
			
		||||
				anchors.horizontalCenter: parent.horizontalCenter
 | 
			
		||||
				y: parent.height / 2 + textBox.height
 | 
			
		||||
				id: sep
 | 
			
		||||
 | 
			
		||||
				color: enabled ?
 | 
			
		||||
					root.context.failed ? "#ffa0a0" : "white"
 | 
			
		||||
					: "#80ffffff";
 | 
			
		||||
				implicitHeight: 6
 | 
			
		||||
				implicitWidth: 800
 | 
			
		||||
				radius: height / 2
 | 
			
		||||
				color: ShellGlobals.colors.widget
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
				font.pointSize: 24
 | 
			
		||||
				horizontalAlignment: TextInput.AlignHCenter
 | 
			
		||||
				echoMode: TextInput.Password
 | 
			
		||||
				inputMethodHints: Qt.ImhSensitiveData
 | 
			
		||||
			ColumnLayout {
 | 
			
		||||
				implicitWidth: sep.implicitWidth
 | 
			
		||||
				anchors.horizontalCenter: parent.horizontalCenter
 | 
			
		||||
				anchors.bottom: sep.top
 | 
			
		||||
				spacing: 0
 | 
			
		||||
 | 
			
		||||
				onTextChanged: root.context.currentText = text;
 | 
			
		||||
				SystemClock {
 | 
			
		||||
					id: clock
 | 
			
		||||
					precision: SystemClock.Minutes
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Window.onActiveChanged: {
 | 
			
		||||
					if (Window.active) {
 | 
			
		||||
						text = root.context.currentText;
 | 
			
		||||
				Text {
 | 
			
		||||
					id: timeText
 | 
			
		||||
					Layout.alignment: Qt.AlignHCenter
 | 
			
		||||
 | 
			
		||||
					font {
 | 
			
		||||
						pointSize: 120
 | 
			
		||||
						hintingPreference: Font.PreferFullHinting
 | 
			
		||||
						family: "Noto Sans"
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					color: "white"
 | 
			
		||||
					renderType: Text.NativeRendering
 | 
			
		||||
 | 
			
		||||
					text: {
 | 
			
		||||
						const hours = clock.hours.toString().padStart(2, '0');
 | 
			
		||||
						const minutes = clock.minutes.toString().padStart(2, '0');
 | 
			
		||||
						return `${hours}:${minutes}`;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				onAccepted: {
 | 
			
		||||
					if (text != "") root.context.tryUnlock();
 | 
			
		||||
				}
 | 
			
		||||
				Item {
 | 
			
		||||
					Layout.alignment: Qt.AlignHCenter
 | 
			
		||||
					implicitHeight: textBox.height * focusAnim
 | 
			
		||||
					implicitWidth: sep.implicitWidth
 | 
			
		||||
					clip: true
 | 
			
		||||
 | 
			
		||||
				enabled: !root.context.isUnlocking;
 | 
			
		||||
					TextInput {
 | 
			
		||||
						id: textBox
 | 
			
		||||
						focus: true
 | 
			
		||||
						width: parent.width
 | 
			
		||||
 | 
			
		||||
						color: enabled ?
 | 
			
		||||
							root.state.failed ? "#ffa0a0" : "white"
 | 
			
		||||
							: "#80ffffff";
 | 
			
		||||
 | 
			
		||||
						font.pointSize: 24
 | 
			
		||||
						horizontalAlignment: TextInput.AlignHCenter
 | 
			
		||||
						echoMode: TextInput.Password
 | 
			
		||||
						inputMethodHints: Qt.ImhSensitiveData
 | 
			
		||||
 | 
			
		||||
						cursorVisible: text != ""
 | 
			
		||||
						onCursorVisibleChanged: cursorVisible = text != ""
 | 
			
		||||
 | 
			
		||||
						onTextChanged: {
 | 
			
		||||
							root.state.currentText = text;
 | 
			
		||||
							cursorVisible = text != ""
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						Window.onActiveChanged: {
 | 
			
		||||
							if (Window.active) {
 | 
			
		||||
								text = root.state.currentText;
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						Connections {
 | 
			
		||||
							target: root.state
 | 
			
		||||
 | 
			
		||||
							function onCurrentTextChanged() {
 | 
			
		||||
								textBox.text = root.state.currentText;
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						onAccepted: {
 | 
			
		||||
							if (text != "") root.state.tryPasswordUnlock();
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						enabled: !root.state.isUnlocking;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Text {
 | 
			
		||||
						anchors.fill: textBox
 | 
			
		||||
						font: textBox.font
 | 
			
		||||
						color: root.state.failed ? "#ffa0a0" : "#80ffffff";
 | 
			
		||||
						horizontalAlignment: TextInput.AlignHCenter
 | 
			
		||||
						visible: !textBox.cursorVisible
 | 
			
		||||
						text: root.state.failed ? root.state.error
 | 
			
		||||
							: root.state.fprintAvailable ? "Touch sensor or enter password" : "Enter password";
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Rectangle {
 | 
			
		||||
						Layout.fillHeight: true
 | 
			
		||||
						implicitWidth: height
 | 
			
		||||
						color: "transparent"
 | 
			
		||||
						visible: root.state.fprintAvailable
 | 
			
		||||
 | 
			
		||||
						anchors {
 | 
			
		||||
							right: textBox.right
 | 
			
		||||
							top: textBox.top
 | 
			
		||||
							bottom: textBox.bottom
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						Image {
 | 
			
		||||
							anchors.fill: parent
 | 
			
		||||
							anchors.margins: 5
 | 
			
		||||
							source: "root:icons/fingerprint.svg"
 | 
			
		||||
							sourceSize.width: width
 | 
			
		||||
							sourceSize.height: height
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Item {
 | 
			
		||||
				anchors.horizontalCenter: parent.horizontalCenter
 | 
			
		||||
				anchors.top: sep.bottom
 | 
			
		||||
				implicitHeight: (75 + 30) * focusAnim
 | 
			
		||||
				implicitWidth: sep.implicitWidth
 | 
			
		||||
				clip: true
 | 
			
		||||
 | 
			
		||||
				RowLayout {
 | 
			
		||||
					anchors.horizontalCenter: parent.horizontalCenter
 | 
			
		||||
					anchors.bottom: parent.bottom
 | 
			
		||||
					anchors.topMargin: 50
 | 
			
		||||
					spacing: 0
 | 
			
		||||
 | 
			
		||||
					LockButton {
 | 
			
		||||
						icon: "root:icons/monitor.svg"
 | 
			
		||||
						onClicked: root.state.fadeOut();
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					LockButton {
 | 
			
		||||
						icon: "root:icons/pause.svg"
 | 
			
		||||
						show: root.state.mediaPlaying;
 | 
			
		||||
						onClicked: root.state.pauseMedia();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Item {
 | 
			
		||||
		anchors.horizontalCenter: parent.horizontalCenter
 | 
			
		||||
		anchors.top: sep.bottom
 | 
			
		||||
		implicitHeight: (childrenRect.height + 30) * focusAnim
 | 
			
		||||
		implicitWidth: sep.implicitWidth
 | 
			
		||||
		clip: true
 | 
			
		||||
 | 
			
		||||
		RowLayout {
 | 
			
		||||
			anchors.horizontalCenter: parent.horizontalCenter
 | 
			
		||||
			anchors.bottom: parent.bottom
 | 
			
		||||
			anchors.topMargin: 50
 | 
			
		||||
			spacing: 0
 | 
			
		||||
 | 
			
		||||
			LockButton {
 | 
			
		||||
				icon: "root:icons/monitor.svg"
 | 
			
		||||
				onClicked: root.context.dpms();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			LockButton {
 | 
			
		||||
				icon: "root:icons/pause.svg"
 | 
			
		||||
				show: context.mediaPlaying;
 | 
			
		||||
				onClicked: root.context.pauseMedia();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	Rectangle {
 | 
			
		||||
		id: darkenOverlay
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		color: "black"
 | 
			
		||||
		opacity: root.state.fadeOutMul
 | 
			
		||||
		visible: opacity != 0
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,57 +0,0 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Io
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
import Quickshell.Services.Mpris
 | 
			
		||||
 | 
			
		||||
Scope {
 | 
			
		||||
	id: root
 | 
			
		||||
	signal unlocked();
 | 
			
		||||
	property string currentText: "";
 | 
			
		||||
	readonly property alias isUnlocking: pamtester.running;
 | 
			
		||||
	property bool failed: false;
 | 
			
		||||
 | 
			
		||||
	onCurrentTextChanged: failed = false;
 | 
			
		||||
 | 
			
		||||
	readonly property bool mediaPlaying: Mpris.players.values.some(player => {
 | 
			
		||||
		return player.playbackState === MprisPlaybackState.Playing && player.canPause;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	function pauseMedia() {
 | 
			
		||||
		Mpris.players.values.forEach(player => {
 | 
			
		||||
			if (player.playbackState === MprisPlaybackState.Playing && player.canPause) {
 | 
			
		||||
				player.playbackState = MprisPlaybackState.Paused;
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function dpms() {
 | 
			
		||||
		Hyprland.dispatch(`dpms`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Process {
 | 
			
		||||
		id: pamtester
 | 
			
		||||
		property bool failed: true
 | 
			
		||||
 | 
			
		||||
		command: ["pamtester", "login", Quickshell.env("USER"), "authenticate"]
 | 
			
		||||
 | 
			
		||||
		onStarted: this.write(`${currentText}\n`)
 | 
			
		||||
 | 
			
		||||
		stdout: SplitParser {
 | 
			
		||||
			// fails go to stderr
 | 
			
		||||
			onRead: pamtester.failed = false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		onExited: {
 | 
			
		||||
			if (failed) {
 | 
			
		||||
				root.failed = true;
 | 
			
		||||
			} else {
 | 
			
		||||
				root.unlocked();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function tryUnlock() {
 | 
			
		||||
		pamtester.running = true;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								modules/user/modules/quickshell/shell/lock/LockState.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,70 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
import Quickshell.Services.Mpris
 | 
			
		||||
 | 
			
		||||
Scope {
 | 
			
		||||
	signal tryPasswordUnlock();
 | 
			
		||||
	property string currentText: "";
 | 
			
		||||
	property string error: "";
 | 
			
		||||
	property bool isUnlocking: false;
 | 
			
		||||
	property bool failed: false;
 | 
			
		||||
	property bool fprintAvailable: false;
 | 
			
		||||
 | 
			
		||||
	property bool fadedOut: false
 | 
			
		||||
	property real fadeOutMul: 0
 | 
			
		||||
 | 
			
		||||
	NumberAnimation on fadeOutMul {
 | 
			
		||||
		id: fadeAnim
 | 
			
		||||
		duration: 600
 | 
			
		||||
		easing.type: Easing.BezierSpline
 | 
			
		||||
		easing.bezierCurve: [0.0, 0.75, 0.15, 1.0, 1.0, 1.0]
 | 
			
		||||
 | 
			
		||||
		onStopped: {
 | 
			
		||||
			if (fadedOut) Hyprland.dispatch("dpms off");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onCurrentTextChanged: {
 | 
			
		||||
		failed = false;
 | 
			
		||||
		error = "";
 | 
			
		||||
 | 
			
		||||
		if (fadedOut) {
 | 
			
		||||
			fadeIn();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function fadeOut() {
 | 
			
		||||
		if (fadedOut) return;
 | 
			
		||||
		fadedOut = true;
 | 
			
		||||
		fadeAnim.to = 1;
 | 
			
		||||
		fadeAnim.restart();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function fadeIn() {
 | 
			
		||||
		if (!fadedOut) return;
 | 
			
		||||
		Hyprland.dispatch("dpms on");
 | 
			
		||||
		fadedOut = false;
 | 
			
		||||
		fadeAnim.to = 0;
 | 
			
		||||
		fadeAnim.restart();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ElapsedTimer { id: mouseTimer }
 | 
			
		||||
 | 
			
		||||
	// returns if mouse move should be continued, false should restart
 | 
			
		||||
	function mouseMoved(): bool {
 | 
			
		||||
		return mouseTimer.restart() < 0.2;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	readonly property bool mediaPlaying: Mpris.players.values.some(player => {
 | 
			
		||||
		return player.playbackState === MprisPlaybackState.Playing && player.canPause;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	function pauseMedia() {
 | 
			
		||||
		Mpris.players.values.forEach(player => {
 | 
			
		||||
			if (player.playbackState === MprisPlaybackState.Playing && player.canPause) {
 | 
			
		||||
				player.playbackState = MprisPlaybackState.Paused;
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
import Quickshell
 | 
			
		||||
import Quickshell.Services.Pam
 | 
			
		||||
 | 
			
		||||
Scope {
 | 
			
		||||
	id: root
 | 
			
		||||
	signal unlocked();
 | 
			
		||||
 | 
			
		||||
	property LockState state: LockState {
 | 
			
		||||
		onTryPasswordUnlock: {
 | 
			
		||||
			root.state.isUnlocking = true;
 | 
			
		||||
			pam.start();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	PamContext {
 | 
			
		||||
		id: pam
 | 
			
		||||
		configDirectory: "pam"
 | 
			
		||||
		config: "password.conf"
 | 
			
		||||
 | 
			
		||||
		onPamMessage: {
 | 
			
		||||
			if (this.responseRequired) {
 | 
			
		||||
				this.respond(root.state.currentText);
 | 
			
		||||
			} else if (this.messageIsError) {
 | 
			
		||||
				root.state.currentText = "";
 | 
			
		||||
				root.state.failed = true;
 | 
			
		||||
				root.state.error = this.message;
 | 
			
		||||
			} // else ignore
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		onCompleted: status => {
 | 
			
		||||
			const success = status == PamResult.Success;
 | 
			
		||||
 | 
			
		||||
			if (!success) {
 | 
			
		||||
				root.state.currentText = "";
 | 
			
		||||
				root.state.error = "Invalid password";
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			root.state.failed = !success;
 | 
			
		||||
			root.state.isUnlocking = false;
 | 
			
		||||
 | 
			
		||||
			if (success) root.unlocked();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
auth required /run/current-system/sw/lib/security/pam_fprintd.so
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
auth required pam_unix.so
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
 | 
			
		||||
Canvas {
 | 
			
		||||
	id: root
 | 
			
		||||
	property real ringFill: 1.0
 | 
			
		||||
 | 
			
		||||
	onRingFillChanged: requestPaint();
 | 
			
		||||
 | 
			
		||||
	renderStrategy: Canvas.Cooperative
 | 
			
		||||
 | 
			
		||||
	onPaint: {
 | 
			
		||||
		const ctx = getContext("2d");
 | 
			
		||||
		ctx.reset();
 | 
			
		||||
 | 
			
		||||
		ctx.lineWidth = 2;
 | 
			
		||||
		ctx.strokeStyle = "#70ffffff";
 | 
			
		||||
 | 
			
		||||
		ctx.beginPath();
 | 
			
		||||
		const half = Math.round(root.width / 2);
 | 
			
		||||
		const start = -Math.PI * 0.5;
 | 
			
		||||
		const endM = ringFill == 0.0 || ringFill == 1.0 ? ringFill : 1.0 - ringFill
 | 
			
		||||
		ctx.arc(half, half, half - ctx.lineWidth, start, start + 2 * Math.PI * endM, true);
 | 
			
		||||
		ctx.stroke();
 | 
			
		||||
 | 
			
		||||
		const xMin = Math.min(root.width * 0.3);
 | 
			
		||||
		const xMax = Math.max(root.width * 0.7);
 | 
			
		||||
		ctx.strokeStyle = "white";
 | 
			
		||||
 | 
			
		||||
		ctx.beginPath();
 | 
			
		||||
		ctx.moveTo(xMin, xMin);
 | 
			
		||||
		ctx.lineTo(xMax, xMax);
 | 
			
		||||
		ctx.stroke();
 | 
			
		||||
 | 
			
		||||
		ctx.beginPath();
 | 
			
		||||
		ctx.moveTo(xMax, xMin);
 | 
			
		||||
		ctx.lineTo(xMin, xMax);
 | 
			
		||||
		ctx.stroke();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
pragma ComponentBehavior: Bound
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Services.Notifications
 | 
			
		||||
 | 
			
		||||
TrackedNotification {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property Notification notif;
 | 
			
		||||
 | 
			
		||||
	renderComponent: StandardNotificationRenderer {
 | 
			
		||||
		notif: root.notif
 | 
			
		||||
		backer: root
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function handleDiscard() {
 | 
			
		||||
		if (!lock.retained) notif.dismiss();
 | 
			
		||||
		root.discarded();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function handleDismiss() {
 | 
			
		||||
		//handleDiscard();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	RetainableLock {
 | 
			
		||||
		id: lock
 | 
			
		||||
		object: root.notif
 | 
			
		||||
		locked: true
 | 
			
		||||
		onRetainedChanged: {
 | 
			
		||||
			if (retained) root.discard();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expireTimeout: notif.expireTimeout
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,243 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import "../components"
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	id: root
 | 
			
		||||
 | 
			
		||||
	enum FlingState {
 | 
			
		||||
		Inert,
 | 
			
		||||
		Returning,
 | 
			
		||||
		Flinging,
 | 
			
		||||
		Dismissing
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	implicitWidth: display.implicitWidth
 | 
			
		||||
 | 
			
		||||
	// note: can be 0, use ZHVStack
 | 
			
		||||
	implicitHeight: (display.implicitHeight + padding * 2) * display.meshFactor
 | 
			
		||||
 | 
			
		||||
	z: 1.0 - display.meshFactor
 | 
			
		||||
 | 
			
		||||
	property var view;
 | 
			
		||||
	property Item contentItem;
 | 
			
		||||
	property real padding: 5;
 | 
			
		||||
	property real edgeXOffset;
 | 
			
		||||
	property bool canOverlap: display.rotation > 2 || Math.abs(display.displayY) > 10 || display.displayX < -60
 | 
			
		||||
	property bool canDismiss: display.state != FlickableNotification.Dismissing && display.state != FlickableNotification.Flinging;
 | 
			
		||||
 | 
			
		||||
	property alias displayContainer: displayContainer;
 | 
			
		||||
 | 
			
		||||
	signal leftViewBounds();
 | 
			
		||||
	signal dismissed();
 | 
			
		||||
	signal discarded();
 | 
			
		||||
	signal startedFlick();
 | 
			
		||||
 | 
			
		||||
	function playEntry(delay: real) {
 | 
			
		||||
		if (display.state != FlickableNotification.Flinging) {
 | 
			
		||||
			display.displayX = -display.width + edgeXOffset
 | 
			
		||||
			root.playReturn(delay);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function playDismiss(delay: real) {
 | 
			
		||||
		if (display.state != FlickableNotification.Flinging && display.state != FlickableNotification.Dismissing) {
 | 
			
		||||
			display.state = FlickableNotification.Dismissing;
 | 
			
		||||
			display.animationDelay = delay;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function playDiscard(delay: real) {
 | 
			
		||||
		if (display.state != FlickableNotification.Flinging && display.state != FlickableNotification.Dismissing) {
 | 
			
		||||
			display.velocityX = 500;
 | 
			
		||||
			display.velocityY = 1500;
 | 
			
		||||
			display.state = FlickableNotification.Flinging;
 | 
			
		||||
			display.animationDelay = delay;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function playReturn(delay: real) {
 | 
			
		||||
		if (display.state != FlickableNotification.Flinging) {
 | 
			
		||||
			display.state = FlickableNotification.Returning;
 | 
			
		||||
			display.animationDelay = delay;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	MouseArea {
 | 
			
		||||
		id: mouseArea
 | 
			
		||||
		width: display.width
 | 
			
		||||
		height: display.height
 | 
			
		||||
		enabled: display.state == FlickableNotification.Inert || display.state == FlickableNotification.Returning
 | 
			
		||||
 | 
			
		||||
		FlickMonitor {
 | 
			
		||||
			id: flickMonitor
 | 
			
		||||
			target: mouseArea
 | 
			
		||||
 | 
			
		||||
			onDragDeltaXChanged: {
 | 
			
		||||
				const delta = dragDeltaX;
 | 
			
		||||
				display.displayX = delta < 0 ? delta : Math.pow(delta, 0.8);
 | 
			
		||||
				display.updateMeshFactor(true);
 | 
			
		||||
				updateDragY();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			onDragDeltaYChanged: {
 | 
			
		||||
				updateDragY();
 | 
			
		||||
				display.state = FlickableNotification.Inert;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			function updateDragY() {
 | 
			
		||||
				//const xMul = 1//dragDeltaX < 0 ? 0 : Math.min(1, Math.pow(dragDeltaX / 200, 0.8));
 | 
			
		||||
				const d = Math.max(0, Math.min(5000, display.displayX)) / 2000;
 | 
			
		||||
				const xMul = d
 | 
			
		||||
				const targetY = dragDeltaY;
 | 
			
		||||
				display.displayY = root.padding + targetY * xMul;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			onFlickStarted: {
 | 
			
		||||
				display.initialAnimComplete = true;
 | 
			
		||||
				root.startedFlick();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			onFlickCompleted: {
 | 
			
		||||
				display.releaseY = dragEndY;
 | 
			
		||||
 | 
			
		||||
				if (velocityX > 1000 || (velocityX > -100 && display.displayX > display.width * 0.4)) {
 | 
			
		||||
					display.velocityX = Math.max(velocityX * 0.8, 1000);
 | 
			
		||||
					display.velocityY = velocityY * 0.6;
 | 
			
		||||
					display.state = FlickableNotification.Flinging;
 | 
			
		||||
					root.discarded();
 | 
			
		||||
				} else if (velocityX < -1500 || (velocityX < 100 && display.displayX < -(display.width * 0.4))) {
 | 
			
		||||
					display.velocityX = Math.min(velocityX * 0.8, -700)
 | 
			
		||||
					display.velocityY = 0
 | 
			
		||||
					display.state = FlickableNotification.Dismissing;
 | 
			
		||||
					root.dismissed();
 | 
			
		||||
				} else {
 | 
			
		||||
					display.velocityX = 0;
 | 
			
		||||
					display.velocityY = 0;
 | 
			
		||||
					display.state = FlickableNotification.Returning;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Item {
 | 
			
		||||
			id: displayContainer
 | 
			
		||||
			layer.enabled: view && view.topNotification == root
 | 
			
		||||
			opacity: layer.enabled ? 0 : 1 // shader ignores it
 | 
			
		||||
			width: Math.ceil(display.width + display.xPadding * 2)
 | 
			
		||||
			height: Math.ceil(display.height + display.yPadding * 2)
 | 
			
		||||
 | 
			
		||||
			x: Math.floor(display.targetContainmentX)
 | 
			
		||||
			y: Math.floor(display.targetContainmentY)
 | 
			
		||||
 | 
			
		||||
			Item {
 | 
			
		||||
				id: display
 | 
			
		||||
				//anchors.centerIn: parent
 | 
			
		||||
				x: xPadding + (targetContainmentX - displayContainer.x)
 | 
			
		||||
				y: yPadding + (targetContainmentY - displayContainer.y)
 | 
			
		||||
				//visible: meshFactor > 0.95
 | 
			
		||||
 | 
			
		||||
				children: [root.contentItem]
 | 
			
		||||
				implicitWidth: root.contentItem?.width ?? 0
 | 
			
		||||
				implicitHeight: root.contentItem?.height ?? 0
 | 
			
		||||
 | 
			
		||||
				property var state: FlickableNotification.Inert;
 | 
			
		||||
				property real meshFactor: 1;
 | 
			
		||||
				property real velocityX;
 | 
			
		||||
				property real velocityY;
 | 
			
		||||
				property real releaseY;
 | 
			
		||||
				property real animationDelay;
 | 
			
		||||
				property bool initialAnimComplete;
 | 
			
		||||
 | 
			
		||||
				property real displayX;
 | 
			
		||||
				property real displayY;
 | 
			
		||||
 | 
			
		||||
				property real tiltSize: Math.max(width, height) * 1.2;
 | 
			
		||||
				property real xPadding: (tiltSize - width) / 2;
 | 
			
		||||
				property real yPadding: (tiltSize - height) / 2;
 | 
			
		||||
 | 
			
		||||
				property real targetContainmentX: display.displayX - display.xPadding
 | 
			
		||||
				property real targetContainmentY: root.padding + display.displayY - display.yPadding
 | 
			
		||||
 | 
			
		||||
				function updateMeshFactor(canRemesh: bool) {
 | 
			
		||||
					let meshFactor = (display.implicitWidth - Math.abs(display.displayX)) / display.implicitWidth;
 | 
			
		||||
					meshFactor = 0.8 + (meshFactor * 0.2);
 | 
			
		||||
					meshFactor = Math.max(0, meshFactor);
 | 
			
		||||
 | 
			
		||||
					if (canRemesh) this.meshFactor = meshFactor;
 | 
			
		||||
					else this.meshFactor = Math.min(this.meshFactor, meshFactor);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				function unmesh(delta: real) {
 | 
			
		||||
					if (meshFactor > 0) {
 | 
			
		||||
						this.meshFactor = Math.max(0, this.meshFactor - delta * 5);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				rotation: display.displayX < 0 ? 0 : display.displayX * (initialAnimComplete ? 0.1 : 0.02)
 | 
			
		||||
 | 
			
		||||
				property real lastX;
 | 
			
		||||
 | 
			
		||||
				FrameAnimation {
 | 
			
		||||
					function dampingVelocity(currentVelocity, delta) {
 | 
			
		||||
						const spring = 1.0;
 | 
			
		||||
						const damping = 0.1;
 | 
			
		||||
						const springForce = spring * delta;
 | 
			
		||||
						const dampingForce = -damping * currentVelocity;
 | 
			
		||||
						return currentVelocity + (springForce + dampingForce);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					running: display.state != FlickableNotification.Inert
 | 
			
		||||
					onTriggered: {
 | 
			
		||||
						let frameTime = this.frameTime;
 | 
			
		||||
						if (display.animationDelay != 0) {
 | 
			
		||||
							const usedDelay = Math.min(display.animationDelay, frameTime);
 | 
			
		||||
							frameTime -= usedDelay;
 | 
			
		||||
							display.animationDelay -= usedDelay;
 | 
			
		||||
							if (frameTime == 0) return;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						if (display.state == FlickableNotification.Flinging) {
 | 
			
		||||
							display.velocityY += frameTime * 100000 * (1 / display.velocityX * 100);
 | 
			
		||||
							//display.velocityX -= display.velocityX * 0.98 * frameTime
 | 
			
		||||
							display.unmesh(frameTime);
 | 
			
		||||
						} else if (display.state == FlickableNotification.Dismissing) {
 | 
			
		||||
							const d = Math.max(0, Math.min(5000, display.displayX)) / 2000;
 | 
			
		||||
							display.displayY = root.padding + display.releaseY * d;
 | 
			
		||||
							display.velocityY = 0;
 | 
			
		||||
 | 
			
		||||
							display.velocityX += frameTime * -20000;
 | 
			
		||||
 | 
			
		||||
							if (display.displayX + display.width > 0) display.updateMeshFactor(false);
 | 
			
		||||
							else display.unmesh(frameTime);
 | 
			
		||||
						} else {
 | 
			
		||||
							const deltaX = 0 - display.displayX;
 | 
			
		||||
							const deltaY = root.padding - display.displayY;
 | 
			
		||||
 | 
			
		||||
							display.velocityX = dampingVelocity(display.velocityX, deltaX);
 | 
			
		||||
							display.velocityY = dampingVelocity(display.velocityY, deltaY);
 | 
			
		||||
 | 
			
		||||
							if (Math.abs(display.velocityX) < 0.01 && Math.abs(deltaX) < 1
 | 
			
		||||
								&& Math.abs(display.velocityY) < 0.01 && Math.abs(deltaY) < 1) {
 | 
			
		||||
									display.state = FlickableNotification.Inert;
 | 
			
		||||
									display.displayX = 0;
 | 
			
		||||
									display.displayY = root.padding;
 | 
			
		||||
									display.velocityX = 0;
 | 
			
		||||
									display.velocityY = 0;
 | 
			
		||||
									display.initialAnimComplete = true;
 | 
			
		||||
								}
 | 
			
		||||
 | 
			
		||||
							display.updateMeshFactor(true);
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						display.displayX += display.velocityX * frameTime;
 | 
			
		||||
						display.displayY += display.velocityY * frameTime;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
						// todo: actually base this on the viewport
 | 
			
		||||
						if (display.displayX > 10000 || display.displayY > 10000 || (display.displayX + display.width < root.edgeXOffset && display.meshFactor == 0) || display.displayY < -10000) root.leftViewBounds();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,126 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Effects
 | 
			
		||||
import Qt5Compat.GraphicalEffects
 | 
			
		||||
import "../components"
 | 
			
		||||
import "../shaders" as Shaders
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	id: root
 | 
			
		||||
	property list<Item> notifications: [];
 | 
			
		||||
	property list<Item> heightStack: [];
 | 
			
		||||
 | 
			
		||||
	property alias stack: stack;
 | 
			
		||||
	property alias topNotification: stack.topNotification;
 | 
			
		||||
 | 
			
		||||
	function addNotificationInert(notification: TrackedNotification): Item {
 | 
			
		||||
		const harness = stack._harnessComponent.createObject(stack, {
 | 
			
		||||
			backer: notification,
 | 
			
		||||
			view: root,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		harness.contentItem = notification.renderComponent.createObject(harness);
 | 
			
		||||
 | 
			
		||||
		notifications = [...notifications, harness];
 | 
			
		||||
		heightStack = [harness, ...heightStack];
 | 
			
		||||
 | 
			
		||||
		return harness;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function addNotification(notification: TrackedNotification) {
 | 
			
		||||
		const harness = root.addNotificationInert(notification);
 | 
			
		||||
		harness.playEntry(0);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function dismissAll() {
 | 
			
		||||
		let delay = 0;
 | 
			
		||||
 | 
			
		||||
		for (const notification of root.notifications) {
 | 
			
		||||
			if (!notification.canDismiss) continue;
 | 
			
		||||
			notification.playDismiss(delay);
 | 
			
		||||
			notification.dismissed();
 | 
			
		||||
			delay += 0.025;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function discardAll() {
 | 
			
		||||
		let delay = 0;
 | 
			
		||||
 | 
			
		||||
		for (const notification of root.notifications) {
 | 
			
		||||
			if (!notification.canDismiss) continue;
 | 
			
		||||
			notification.playDismiss(delay);
 | 
			
		||||
			notification.discarded();
 | 
			
		||||
			delay += 0.025;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function addSet(notifications: list<TrackedNotification>) {
 | 
			
		||||
		let delay = 0;
 | 
			
		||||
 | 
			
		||||
		for (const notification of notifications) {
 | 
			
		||||
			if (notification.visualizer) {
 | 
			
		||||
				notification.visualizer.playReturn(delay);
 | 
			
		||||
			} else {
 | 
			
		||||
				const harness = root.addNotificationInert(notification);
 | 
			
		||||
				harness.playEntry(delay);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			delay += 0.025;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Item {
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
		layer.enabled: stack.topNotification != null
 | 
			
		||||
		layer.effect: Shaders.MaskedOverlay {
 | 
			
		||||
			overlayItem: stack.topNotification?.displayContainer ?? null
 | 
			
		||||
			overlayPos: Qt.point(stack.x + stack.topNotification.x + overlayItem.x, stack.y + stack.topNotification.y + overlayItem.y)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ZHVStack {
 | 
			
		||||
			id: stack
 | 
			
		||||
 | 
			
		||||
			property Item topNotification: {
 | 
			
		||||
				if (root.heightStack.length < 2) return null;
 | 
			
		||||
				const top = root.heightStack[0] ?? null;
 | 
			
		||||
				return top && top.canOverlap ? top : null;
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			property Component _harnessComponent: FlickableNotification {
 | 
			
		||||
				id: notification
 | 
			
		||||
				required property TrackedNotification backer;
 | 
			
		||||
 | 
			
		||||
				edgeXOffset: -stack.x
 | 
			
		||||
 | 
			
		||||
				onDismissed: backer.handleDismiss();
 | 
			
		||||
				onDiscarded: backer.handleDiscard();
 | 
			
		||||
 | 
			
		||||
				onLeftViewBounds: {
 | 
			
		||||
					root.notifications = root.notifications.filter(n => n != this);
 | 
			
		||||
					root.heightStack = root.heightStack.filter(n => n != this);
 | 
			
		||||
					this.destroy();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				onStartedFlick: {
 | 
			
		||||
					root.heightStack = [this, ...root.heightStack.filter(n => n != this)];
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Component.onCompleted: backer.visualizer = this;
 | 
			
		||||
 | 
			
		||||
				Connections {
 | 
			
		||||
					target: backer
 | 
			
		||||
 | 
			
		||||
					function onDismiss() {
 | 
			
		||||
						notification.playDismiss(0);
 | 
			
		||||
						notification.dismissed();
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					function onDiscard() {
 | 
			
		||||
						notification.playDismiss(0);
 | 
			
		||||
						notification.discarded();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,74 @@
 | 
			
		|||
pragma Singleton
 | 
			
		||||
pragma ComponentBehavior: Bound
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Services.Notifications
 | 
			
		||||
 | 
			
		||||
Singleton {
 | 
			
		||||
	id: root
 | 
			
		||||
 | 
			
		||||
	property list<TrackedNotification> notifications;
 | 
			
		||||
	property Component notifComponent: DaemonNotification {}
 | 
			
		||||
 | 
			
		||||
	property bool showTrayNotifs: false;
 | 
			
		||||
	property bool dnd: false;
 | 
			
		||||
	property bool hasNotifs: root.notifications.length != 0
 | 
			
		||||
	property var lastHoveredNotif;
 | 
			
		||||
 | 
			
		||||
	property var overlay;
 | 
			
		||||
 | 
			
		||||
	signal notif(notif: TrackedNotification);
 | 
			
		||||
	signal showAll(notifications: list<TrackedNotification>);
 | 
			
		||||
	signal dismissAll(notifications: list<TrackedNotification>);
 | 
			
		||||
	signal discardAll(notifications: list<TrackedNotification>);
 | 
			
		||||
 | 
			
		||||
	NotificationServer {
 | 
			
		||||
		imageSupported: true
 | 
			
		||||
		actionsSupported: true
 | 
			
		||||
		actionIconsSupported: true
 | 
			
		||||
 | 
			
		||||
		onNotification: notification => {
 | 
			
		||||
			notification.tracked = true;
 | 
			
		||||
 | 
			
		||||
			const notif = root.notifComponent.createObject(null, { notif: notification });
 | 
			
		||||
			root.notifications = [...root.notifications, notif];
 | 
			
		||||
 | 
			
		||||
			root.notif(notif);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Instantiator {
 | 
			
		||||
		model: root.notifications
 | 
			
		||||
 | 
			
		||||
		Connections {
 | 
			
		||||
			required property TrackedNotification modelData;
 | 
			
		||||
			target: modelData;
 | 
			
		||||
 | 
			
		||||
			function onDiscarded() {
 | 
			
		||||
				root.notifications = root.notifications.filter(n => n != target);
 | 
			
		||||
				modelData.untrack();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			function onDiscard() {
 | 
			
		||||
				if (!modelData.visualizer) modelData.discarded();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onShowTrayNotifsChanged: {
 | 
			
		||||
		if (showTrayNotifs) {
 | 
			
		||||
			for (const notif of root.notifications) {
 | 
			
		||||
				notif.inTray = true;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			root.showAll(root.notifications);
 | 
			
		||||
		} else {
 | 
			
		||||
			root.dismissAll(root.notifications);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function sendDiscardAll() {
 | 
			
		||||
		root.discardAll(root.notifications);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Wayland
 | 
			
		||||
 | 
			
		||||
PanelWindow {
 | 
			
		||||
	WlrLayershell.namespace: "shell:notifications"
 | 
			
		||||
	exclusionMode: ExclusionMode.Ignore
 | 
			
		||||
	color: "transparent"
 | 
			
		||||
 | 
			
		||||
	anchors {
 | 
			
		||||
		left: true
 | 
			
		||||
		top: true
 | 
			
		||||
		bottom: true
 | 
			
		||||
		right: true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property Component notifComponent: DaemonNotification {}
 | 
			
		||||
 | 
			
		||||
	NotificationDisplay {
 | 
			
		||||
		id: display
 | 
			
		||||
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
		stack.y: 5 + 55//(NotificationManager.showTrayNotifs ? 55 : 0)
 | 
			
		||||
		stack.x: 72
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	visible: display.stack.children.length != 0
 | 
			
		||||
 | 
			
		||||
	mask: Region { item: display.stack }
 | 
			
		||||
 | 
			
		||||
	Component.onCompleted: {
 | 
			
		||||
		NotificationManager.overlay = this;
 | 
			
		||||
		NotificationManager.notif.connect(display.addNotification);
 | 
			
		||||
		NotificationManager.showAll.connect(display.addSet);
 | 
			
		||||
		NotificationManager.dismissAll.connect(display.dismissAll);
 | 
			
		||||
		NotificationManager.discardAll.connect(display.discardAll);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,114 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import "root:bar"
 | 
			
		||||
 | 
			
		||||
BarWidgetInner {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property var bar;
 | 
			
		||||
 | 
			
		||||
	property bool controlsOpen: false;
 | 
			
		||||
	onControlsOpenChanged: NotificationManager.showTrayNotifs = controlsOpen;
 | 
			
		||||
 | 
			
		||||
	Connections {
 | 
			
		||||
		target: NotificationManager
 | 
			
		||||
 | 
			
		||||
		function onHasNotifsChanged() {
 | 
			
		||||
			if (!NotificationManager.hasNotifs) {
 | 
			
		||||
				root.controlsOpen = false;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	implicitHeight: width
 | 
			
		||||
 | 
			
		||||
	BarButton {
 | 
			
		||||
		id: button
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		baseMargin: 8
 | 
			
		||||
		fillWindowWidth: true
 | 
			
		||||
		acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
 | 
			
		||||
		showPressed: root.controlsOpen || (pressedButtons & ~Qt.RightButton)
 | 
			
		||||
 | 
			
		||||
		Image {
 | 
			
		||||
			anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
			source: NotificationManager.hasNotifs
 | 
			
		||||
				? "root:icons/bell-fill.svg"
 | 
			
		||||
				: "root:icons/bell.svg"
 | 
			
		||||
 | 
			
		||||
			fillMode: Image.PreserveAspectFit
 | 
			
		||||
 | 
			
		||||
			sourceSize.width: width
 | 
			
		||||
			sourceSize.height: height
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		onPressed: event => {
 | 
			
		||||
			if (event.button == Qt.RightButton && NotificationManager.hasNotifs) {
 | 
			
		||||
				root.controlsOpen = !root.controlsOpen;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property var tooltip: TooltipItem {
 | 
			
		||||
		tooltip: bar.tooltip
 | 
			
		||||
		owner: root
 | 
			
		||||
		show: button.containsMouse
 | 
			
		||||
 | 
			
		||||
		Label {
 | 
			
		||||
			anchors.verticalCenter: parent.verticalCenter
 | 
			
		||||
			text: {
 | 
			
		||||
				const count = NotificationManager.notifications.length;
 | 
			
		||||
				return count == 0 ? "No notifications"
 | 
			
		||||
					: count == 1 ? "1 notification"
 | 
			
		||||
					: `${count} notifications`;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property var rightclickMenu: TooltipItem {
 | 
			
		||||
		tooltip: bar.tooltip
 | 
			
		||||
		owner: root
 | 
			
		||||
		isMenu: true
 | 
			
		||||
		grabWindows: [NotificationManager.overlay]
 | 
			
		||||
		show: root.controlsOpen
 | 
			
		||||
		onClose: root.controlsOpen = false
 | 
			
		||||
 | 
			
		||||
		Item {
 | 
			
		||||
			implicitWidth: 440
 | 
			
		||||
			implicitHeight: root.implicitHeight - 10
 | 
			
		||||
 | 
			
		||||
			MouseArea {
 | 
			
		||||
				id: closeArea
 | 
			
		||||
 | 
			
		||||
				anchors {
 | 
			
		||||
					right: parent.right
 | 
			
		||||
					rightMargin: 5
 | 
			
		||||
					verticalCenter: parent.verticalCenter
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				implicitWidth: 30
 | 
			
		||||
				implicitHeight: 30
 | 
			
		||||
 | 
			
		||||
				hoverEnabled: true
 | 
			
		||||
				onPressed: {
 | 
			
		||||
					NotificationManager.sendDiscardAll()
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Rectangle {
 | 
			
		||||
					anchors.fill: parent
 | 
			
		||||
					anchors.margins: 5
 | 
			
		||||
					radius: width * 0.5
 | 
			
		||||
					antialiasing: true
 | 
			
		||||
					color: "#60ffffff"
 | 
			
		||||
					opacity: closeArea.containsMouse ? 1 : 0
 | 
			
		||||
					Behavior on opacity { SmoothedAnimation { velocity: 8 } }
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				CloseButton {
 | 
			
		||||
					anchors.fill: parent
 | 
			
		||||
					ringFill: root.backer.timePercentage
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,203 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Services.Notifications
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
Rectangle {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property Notification notif;
 | 
			
		||||
	required property var backer;
 | 
			
		||||
 | 
			
		||||
	color: notif.urgency == NotificationUrgency.Critical ? "#30ff2030" : "#30c0ffff"
 | 
			
		||||
	radius: 5
 | 
			
		||||
	implicitWidth: 450
 | 
			
		||||
	implicitHeight: c.implicitHeight
 | 
			
		||||
 | 
			
		||||
	HoverHandler {
 | 
			
		||||
		onHoveredChanged: {
 | 
			
		||||
			backer.pauseCounter += hovered ? 1 : -1;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Rectangle {
 | 
			
		||||
		id: border
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		color: "transparent"
 | 
			
		||||
		border.width: 2
 | 
			
		||||
		border.color: ShellGlobals.colors.widgetOutline
 | 
			
		||||
		radius: root.radius
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ColumnLayout {
 | 
			
		||||
		id: c
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		spacing: 0
 | 
			
		||||
 | 
			
		||||
		ColumnLayout {
 | 
			
		||||
			Layout.margins: 10
 | 
			
		||||
 | 
			
		||||
			RowLayout {
 | 
			
		||||
				Image {
 | 
			
		||||
					visible: source != ""
 | 
			
		||||
					source: notif.appIcon ? Quickshell.iconPath(notif.appIcon) : ""
 | 
			
		||||
					fillMode: Image.PreserveAspectFit
 | 
			
		||||
					antialiasing: true
 | 
			
		||||
					sourceSize.width: 30
 | 
			
		||||
					sourceSize.height: 30
 | 
			
		||||
					Layout.preferredWidth: 30
 | 
			
		||||
					Layout.preferredHeight: 30
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Label {
 | 
			
		||||
					visible: text != ""
 | 
			
		||||
					text: notif.summary
 | 
			
		||||
					font.pointSize: 20
 | 
			
		||||
					elide: Text.ElideRight
 | 
			
		||||
					Layout.maximumWidth: root.implicitWidth - 100 // QTBUG-127649
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Item { Layout.fillWidth: true }
 | 
			
		||||
 | 
			
		||||
				MouseArea {
 | 
			
		||||
					id: closeArea
 | 
			
		||||
					Layout.preferredWidth: 30
 | 
			
		||||
					Layout.preferredHeight: 30
 | 
			
		||||
 | 
			
		||||
					hoverEnabled: true
 | 
			
		||||
					onPressed: root.backer.discard();
 | 
			
		||||
 | 
			
		||||
					Rectangle {
 | 
			
		||||
						anchors.fill: parent
 | 
			
		||||
						anchors.margins: 5
 | 
			
		||||
						radius: width * 0.5
 | 
			
		||||
						antialiasing: true
 | 
			
		||||
						color: "#60ffffff"
 | 
			
		||||
						opacity: closeArea.containsMouse ? 1 : 0
 | 
			
		||||
						Behavior on opacity { SmoothedAnimation { velocity: 8 } }
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					CloseButton {
 | 
			
		||||
						anchors.fill: parent
 | 
			
		||||
						ringFill: root.backer.timePercentage
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Item {
 | 
			
		||||
				Layout.topMargin: 3
 | 
			
		||||
				visible: bodyLabel.text != "" || notifImage.visible
 | 
			
		||||
				implicitWidth: bodyLabel.width
 | 
			
		||||
				implicitHeight: Math.max(notifImage.size, bodyLabel.implicitHeight)
 | 
			
		||||
 | 
			
		||||
				Image {
 | 
			
		||||
					id: notifImage
 | 
			
		||||
					readonly property int size: visible ? 14 * 8 : 0
 | 
			
		||||
					y: bodyLabel.y + bodyLabel.topPadding
 | 
			
		||||
 | 
			
		||||
					visible: source != ""
 | 
			
		||||
					source: notif.image
 | 
			
		||||
					fillMode: Image.PreserveAspectFit
 | 
			
		||||
					cache: false
 | 
			
		||||
					antialiasing: true
 | 
			
		||||
 | 
			
		||||
					width: size
 | 
			
		||||
					height: size
 | 
			
		||||
					sourceSize.width: size
 | 
			
		||||
					sourceSize.height: size
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Label {
 | 
			
		||||
					id: bodyLabel
 | 
			
		||||
					width: root.implicitWidth - 20
 | 
			
		||||
					text: notif.body
 | 
			
		||||
					wrapMode: Text.Wrap
 | 
			
		||||
 | 
			
		||||
					onLineLaidOut: line => {
 | 
			
		||||
						if (!notifImage.visible) return;
 | 
			
		||||
 | 
			
		||||
						const isize = notifImage.size + 6;
 | 
			
		||||
						if (line.y + line.height <= notifImage.y + isize) {
 | 
			
		||||
							line.x += isize;
 | 
			
		||||
							line.width -= isize;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ColumnLayout {
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
			Layout.margins: root.border.width
 | 
			
		||||
			spacing: 0
 | 
			
		||||
			visible: notif.actions.length != 0
 | 
			
		||||
 | 
			
		||||
			Rectangle {
 | 
			
		||||
				height: border.border.width
 | 
			
		||||
				Layout.fillWidth: true
 | 
			
		||||
				color: border.border.color
 | 
			
		||||
				antialiasing: true
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			RowLayout {
 | 
			
		||||
				spacing: 0
 | 
			
		||||
 | 
			
		||||
				Repeater {
 | 
			
		||||
					model: notif.actions
 | 
			
		||||
 | 
			
		||||
					Item {
 | 
			
		||||
						required property NotificationAction modelData;
 | 
			
		||||
						required property int index;
 | 
			
		||||
 | 
			
		||||
						Layout.fillWidth: true
 | 
			
		||||
						implicitHeight: 35
 | 
			
		||||
 | 
			
		||||
						Rectangle {
 | 
			
		||||
							anchors {
 | 
			
		||||
								top: parent.top
 | 
			
		||||
								bottom: parent.bottom
 | 
			
		||||
								left: parent.left
 | 
			
		||||
								leftMargin: -implicitWidth * 0.5
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							visible: index != 0
 | 
			
		||||
							implicitWidth: root.border.width
 | 
			
		||||
							color: ShellGlobals.colors.widgetOutline
 | 
			
		||||
							antialiasing: true
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						MouseArea {
 | 
			
		||||
							id: actionArea
 | 
			
		||||
							anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
							onClicked: {
 | 
			
		||||
								modelData.invoke();
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							Rectangle {
 | 
			
		||||
								anchors.fill: parent
 | 
			
		||||
								color: actionArea.pressed && actionArea.containsMouse ? "#20000000" : "transparent"
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							RowLayout {
 | 
			
		||||
								anchors.centerIn: parent
 | 
			
		||||
 | 
			
		||||
								Image {
 | 
			
		||||
									visible: notif.hasActionIcons
 | 
			
		||||
									source: Quickshell.iconPath(modelData.identifier)
 | 
			
		||||
									fillMode: Image.PreserveAspectFit
 | 
			
		||||
									antialiasing: true
 | 
			
		||||
									sourceSize.height: 25
 | 
			
		||||
									sourceSize.width: 25
 | 
			
		||||
								}
 | 
			
		||||
 | 
			
		||||
								Label { text: modelData.text }
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
 | 
			
		||||
TrackedNotification {
 | 
			
		||||
	renderComponent: StandardNotificationRenderer {}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,65 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
 | 
			
		||||
Scope {
 | 
			
		||||
	id: root
 | 
			
		||||
 | 
			
		||||
	required property Component renderComponent;
 | 
			
		||||
 | 
			
		||||
	property bool inTray: false;
 | 
			
		||||
	property bool destroyOnInvisible: false;
 | 
			
		||||
	property int visualizerCount: 0;
 | 
			
		||||
	property FlickableNotification visualizer;
 | 
			
		||||
 | 
			
		||||
	signal dismiss();
 | 
			
		||||
	signal discard();
 | 
			
		||||
	signal discarded();
 | 
			
		||||
 | 
			
		||||
	function handleDismiss() {}
 | 
			
		||||
	function handleDiscard() {}
 | 
			
		||||
 | 
			
		||||
	onVisualizerChanged: {
 | 
			
		||||
		if (!visualizer) {
 | 
			
		||||
			expireAnim.stop();
 | 
			
		||||
			timePercentage = 1;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!visualizer && destroyOnInvisible) this.destroy();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function untrack() {
 | 
			
		||||
		destroyOnInvisible = true;
 | 
			
		||||
		if (!visualizer) this.destroy();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property int expireTimeout: -1
 | 
			
		||||
	property real timePercentage: 1
 | 
			
		||||
	property int pauseCounter: 0
 | 
			
		||||
	readonly property bool shouldPause: root.pauseCounter != 0 || (NotificationManager.lastHoveredNotif?.pauseCounter ?? 0) != 0
 | 
			
		||||
 | 
			
		||||
	onPauseCounterChanged: {
 | 
			
		||||
		if (pauseCounter > 0) {
 | 
			
		||||
			NotificationManager.lastHoveredNotif = this;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	NumberAnimation on timePercentage {
 | 
			
		||||
		id: expireAnim
 | 
			
		||||
		running: expireTimeout != 0
 | 
			
		||||
		paused: running && root.shouldPause && to == 0
 | 
			
		||||
		duration: expireTimeout == -1 ? 10000 : expireTimeout
 | 
			
		||||
		to: 0
 | 
			
		||||
		onFinished: {
 | 
			
		||||
			if (!inTray) root.dismiss();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onInTrayChanged: {
 | 
			
		||||
		if (inTray) {
 | 
			
		||||
			expireAnim.stop();
 | 
			
		||||
			expireAnim.duration = 300 * (1 - timePercentage);
 | 
			
		||||
			expireAnim.to = 1;
 | 
			
		||||
			expireAnim.start();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										95
									
								
								modules/user/modules/quickshell/shell/notifications/test.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,95 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell
 | 
			
		||||
import "../components"
 | 
			
		||||
 | 
			
		||||
ShellRoot {
 | 
			
		||||
	Component {
 | 
			
		||||
		id: demoNotif
 | 
			
		||||
 | 
			
		||||
		FlickableNotification {
 | 
			
		||||
			contentItem: Rectangle {
 | 
			
		||||
				color: "white"
 | 
			
		||||
				border.color: "blue"
 | 
			
		||||
				border.width: 2
 | 
			
		||||
				radius: 10
 | 
			
		||||
				width: 400
 | 
			
		||||
				height: 150
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			onLeftViewBounds: this.destroy()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property Component testComponent: TrackedNotification {
 | 
			
		||||
		id: notification
 | 
			
		||||
 | 
			
		||||
		renderComponent: Rectangle {
 | 
			
		||||
			color: "white"
 | 
			
		||||
			border.color: "blue"
 | 
			
		||||
			border.width: 2
 | 
			
		||||
			radius: 10
 | 
			
		||||
			width: 400
 | 
			
		||||
			height: 150
 | 
			
		||||
 | 
			
		||||
			ColumnLayout {
 | 
			
		||||
				Button {
 | 
			
		||||
					text: "dismiss"
 | 
			
		||||
					onClicked: notification.dismiss();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Button {
 | 
			
		||||
					text: "discard"
 | 
			
		||||
					onClicked: notification.discard();
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function handleDismiss() {
 | 
			
		||||
			console.log(`dismiss (sub)`)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function handleDiscard() {
 | 
			
		||||
			console.log(`discard (sub)`)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Component.onDestruction: console.log(`destroy (sub)`)
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	property Component realComponent: DaemonNotification {
 | 
			
		||||
		id: dn
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Daemon {
 | 
			
		||||
		onNotification: notification => {
 | 
			
		||||
			notification.tracked = true;
 | 
			
		||||
 | 
			
		||||
			const o = realComponent.createObject(null, { notif: notification });
 | 
			
		||||
			display.addNotification(o);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	FloatingWindow {
 | 
			
		||||
		color: "transparent"
 | 
			
		||||
 | 
			
		||||
		ColumnLayout {
 | 
			
		||||
			x: 5
 | 
			
		||||
 | 
			
		||||
			Button {
 | 
			
		||||
				visible: false
 | 
			
		||||
				text: "add notif"
 | 
			
		||||
 | 
			
		||||
				onClicked: {
 | 
			
		||||
					//const notif = demoNotif.createObject(stack);
 | 
			
		||||
					//stack.children = [...stack.children, notif];
 | 
			
		||||
					const notif = testComponent.createObject(null);
 | 
			
		||||
					display.addNotification(notif);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			//ZHVStack { id: stack }
 | 
			
		||||
			NotificationDisplay { id: display }
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
// very bad code DO NOT COPY
 | 
			
		||||
pragma ComponentBehavior: Bound
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
| 
						 | 
				
			
			@ -25,7 +26,7 @@ Scope {
 | 
			
		|||
 | 
			
		||||
	Process {
 | 
			
		||||
		id: grimProc
 | 
			
		||||
		command: ["grim", "-l", "0", path]
 | 
			
		||||
		command: ["grim", "-l", "0", root.path]
 | 
			
		||||
		onExited: code => {
 | 
			
		||||
			if (code == 0) {
 | 
			
		||||
				root.visible = true
 | 
			
		||||
| 
						 | 
				
			
			@ -40,12 +41,11 @@ Scope {
 | 
			
		|||
		id: magickProc
 | 
			
		||||
		command: [
 | 
			
		||||
			"magick",
 | 
			
		||||
			path,
 | 
			
		||||
			"-crop",
 | 
			
		||||
			`${selection.w}x${selection.h}+${selection.x}+${selection.y}`,
 | 
			
		||||
			"-quality",
 | 
			
		||||
			"70",
 | 
			
		||||
			path,
 | 
			
		||||
			root.path,
 | 
			
		||||
			"-crop", `${selection.w}x${selection.h}+${selection.x}+${selection.y}`,
 | 
			
		||||
			"-quality", "70",
 | 
			
		||||
			"-page", "0x0+0+0", // removes page size and shot position
 | 
			
		||||
			root.path,
 | 
			
		||||
		]
 | 
			
		||||
 | 
			
		||||
		onExited: wlCopy.running = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -53,14 +53,14 @@ Scope {
 | 
			
		|||
 | 
			
		||||
	Process {
 | 
			
		||||
		id: wlCopy
 | 
			
		||||
		command: ["sh", "-c", `wl-copy < '${path}'`]
 | 
			
		||||
		command: ["sh", "-c", `wl-copy < '${root.path}'`]
 | 
			
		||||
 | 
			
		||||
		onExited: shootingComplete = true;
 | 
			
		||||
		onExited: root.shootingComplete = true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Process {
 | 
			
		||||
		id: cleanupProc
 | 
			
		||||
		command: ["rm", path]
 | 
			
		||||
		command: ["rm", root.path]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	QtObject {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +0,0 @@
 | 
			
		|||
import Quickshell
 | 
			
		||||
import Quickshell.Wayland
 | 
			
		||||
 | 
			
		||||
PanelWindow {
 | 
			
		||||
	visible: false
 | 
			
		||||
 | 
			
		||||
	anchors {
 | 
			
		||||
		left: true
 | 
			
		||||
		right: true
 | 
			
		||||
		top: true
 | 
			
		||||
		bottom: true
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
 | 
			
		||||
ShaderEffect {
 | 
			
		||||
	property Item overlayItem;
 | 
			
		||||
	property point overlayPos: Qt.point(overlayItem.x, overlayItem.y);
 | 
			
		||||
 | 
			
		||||
	fragmentShader: Qt.resolvedUrl("masked_overlay.frag.qsb")
 | 
			
		||||
 | 
			
		||||
	property point pOverlayPos: Qt.point(
 | 
			
		||||
		overlayPos.x / width,
 | 
			
		||||
		overlayPos.y / height
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	property point pOverlaySize: Qt.point(
 | 
			
		||||
		overlayItem.width / width,
 | 
			
		||||
		overlayItem.height / height
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	property point pMergeInset: Qt.point(
 | 
			
		||||
		3 / width,
 | 
			
		||||
		3 / height
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	property real pMergeCutoff: 0.15
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
#version 440
 | 
			
		||||
layout(location = 0) in vec2 qt_TexCoord0;
 | 
			
		||||
layout(location = 1) out vec4 fragColor;
 | 
			
		||||
layout(binding = 1) uniform sampler2D source;
 | 
			
		||||
layout(binding = 2) uniform sampler2D overlayItem;
 | 
			
		||||
 | 
			
		||||
layout(std140, binding = 0) uniform buf {
 | 
			
		||||
	mat4 qt_Matrix;
 | 
			
		||||
	float qt_Opacity;
 | 
			
		||||
	vec2 pOverlayPos;
 | 
			
		||||
	vec2 pOverlaySize;
 | 
			
		||||
	vec2 pMergeInset;
 | 
			
		||||
	float pMergeCutoff;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void main() {
 | 
			
		||||
	vec2 overlayCoord = (qt_TexCoord0 - pOverlayPos) / pOverlaySize;
 | 
			
		||||
 | 
			
		||||
	if (overlayCoord.x >= 0 && overlayCoord.y >= 0 && overlayCoord.x < 1 && overlayCoord.y < 1) {
 | 
			
		||||
		fragColor = texture(overlayItem, overlayCoord);
 | 
			
		||||
 | 
			
		||||
		if (fragColor.a != 0) {
 | 
			
		||||
			vec4 baseColor = texture(source, qt_TexCoord0);
 | 
			
		||||
			// imperfect but visually good enough for now. if more is needed we'll probably need a mask tex
 | 
			
		||||
			if (baseColor.a != 0
 | 
			
		||||
					&& fragColor.a < pMergeCutoff
 | 
			
		||||
					&& (texture(overlayItem, overlayCoord + vec2(0, pMergeInset.y)).a == 0
 | 
			
		||||
							|| texture(overlayItem, overlayCoord + vec2(pMergeInset.x, 0)).a == 0
 | 
			
		||||
							|| texture(overlayItem, overlayCoord + vec2(0, -pMergeInset.y)).a == 0
 | 
			
		||||
							|| texture(overlayItem, overlayCoord + vec2(-pMergeInset.x, 0)).a == 0)) {
 | 
			
		||||
				fragColor += baseColor * (1 - fragColor.a);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			fragColor *= qt_Opacity;
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fragColor = texture(source, qt_TexCoord0) * qt_Opacity;
 | 
			
		||||
}
 | 
			
		||||