Huge quickshell progress dump
Was requested
							
								
								
									
										13
									
								
								modules/user/modules/quickshell/README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
## DO NOT STEAL BLINDLY
 | 
			
		||||
 | 
			
		||||
You think the author's configuration is well formed and a good place to learn from?
 | 
			
		||||
 | 
			
		||||
**Think Again.**
 | 
			
		||||
 | 
			
		||||
This config has evolved over the lifetime of quickshell itself and my own learning of QML with
 | 
			
		||||
very little maintenance. If you are going to steal something please pay close attention to what
 | 
			
		||||
it is doing and why, because it may be a wrong or inefficient way of solving the problem.
 | 
			
		||||
 | 
			
		||||
This will eventually undergo a refactor to be made presentable but it currently is not, and
 | 
			
		||||
should not be treated as a good place to learn from, which is why it isn't linked on any
 | 
			
		||||
quickshell pages.
 | 
			
		||||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
in {
 | 
			
		||||
  home.packages = with pkgs; [
 | 
			
		||||
    qt6.qtimageformats # amog
 | 
			
		||||
    qt6.qt5compat # shader fx
 | 
			
		||||
    quickshell.packages.${system}.default
 | 
			
		||||
    pamtester # lockscreen
 | 
			
		||||
    grim imagemagick # screenshot
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										126
									
								
								modules/user/modules/quickshell/shell/ReloadPopup.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,126 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell
 | 
			
		||||
 | 
			
		||||
Scope {
 | 
			
		||||
	id: root
 | 
			
		||||
	property bool failed;
 | 
			
		||||
	property string errorString;
 | 
			
		||||
 | 
			
		||||
	// Connect to the Quickshell global to listen for the reload signals.
 | 
			
		||||
	Connections {
 | 
			
		||||
		target: Quickshell
 | 
			
		||||
 | 
			
		||||
		function onReloadCompleted() {
 | 
			
		||||
			root.failed = false;
 | 
			
		||||
			popupLoader.loading = true;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function onReloadFailed(error: string) {
 | 
			
		||||
			// Close any existing popup before making a new one.
 | 
			
		||||
			popupLoader.active = false; 
 | 
			
		||||
 | 
			
		||||
			root.failed = true;
 | 
			
		||||
			root.errorString = error;
 | 
			
		||||
			popupLoader.loading = true;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Keep the popup in a loader because it isn't needed most of the timeand will take up
 | 
			
		||||
	// memory that could be used for something else.
 | 
			
		||||
	LazyLoader {
 | 
			
		||||
		id: popupLoader
 | 
			
		||||
 | 
			
		||||
		PanelWindow {
 | 
			
		||||
			id: popup
 | 
			
		||||
 | 
			
		||||
			anchors {
 | 
			
		||||
				top: true
 | 
			
		||||
				left: true
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			margins {
 | 
			
		||||
				top: 25
 | 
			
		||||
				left: 25
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			width: rect.width
 | 
			
		||||
			height: rect.height
 | 
			
		||||
 | 
			
		||||
			// color blending is a bit odd as detailed in the type reference.
 | 
			
		||||
			color: "transparent"
 | 
			
		||||
 | 
			
		||||
			Rectangle {
 | 
			
		||||
				id: rect
 | 
			
		||||
				color: failed ?  "#40802020" : "#40009020"
 | 
			
		||||
 | 
			
		||||
				implicitHeight: layout.implicitHeight + 50
 | 
			
		||||
				implicitWidth: layout.implicitWidth + 30
 | 
			
		||||
 | 
			
		||||
				// Fills the whole area of the rectangle, making any clicks go to it,
 | 
			
		||||
				// which dismiss the popup.
 | 
			
		||||
				MouseArea {
 | 
			
		||||
					id: mouseArea
 | 
			
		||||
					anchors.fill: parent
 | 
			
		||||
					onClicked: popupLoader.active = false
 | 
			
		||||
 | 
			
		||||
					// makes the mouse area track mouse hovering, so the hide animation
 | 
			
		||||
					// can be paused when hovering.
 | 
			
		||||
					hoverEnabled: true
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				ColumnLayout {
 | 
			
		||||
					id: layout
 | 
			
		||||
					anchors {
 | 
			
		||||
						top: parent.top
 | 
			
		||||
						topMargin: 20
 | 
			
		||||
						horizontalCenter: parent.horizontalCenter
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Text {
 | 
			
		||||
						text: root.failed ? "Reload failed." : "Reloaded completed!"
 | 
			
		||||
						color: "white"
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Text {
 | 
			
		||||
						text: root.errorString
 | 
			
		||||
						color: "white"
 | 
			
		||||
						// When visible is false, it also takes up no space.
 | 
			
		||||
						visible: root.errorString != ""
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// A progress bar on the bottom of the screen, showing how long until the
 | 
			
		||||
				// popup is removed.
 | 
			
		||||
				Rectangle {
 | 
			
		||||
					id: bar
 | 
			
		||||
					color: "#20ffffff"
 | 
			
		||||
					anchors.bottom: parent.bottom
 | 
			
		||||
					anchors.left: parent.left
 | 
			
		||||
					height: 20
 | 
			
		||||
 | 
			
		||||
					PropertyAnimation {
 | 
			
		||||
						id: anim
 | 
			
		||||
						target: bar
 | 
			
		||||
						property: "width"
 | 
			
		||||
						from: rect.width
 | 
			
		||||
						to: 0
 | 
			
		||||
						duration: failed ? 10000 : 800
 | 
			
		||||
						onFinished: popupLoader.active = false
 | 
			
		||||
 | 
			
		||||
						// Pause the animation when the mouse is hovering over the popup,
 | 
			
		||||
						// so it stays onscreen while reading. This updates reactively
 | 
			
		||||
						// when the mouse moves on and off the popup.
 | 
			
		||||
						paused: mouseArea.containsMouse
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// We could set `running: true` inside the animation, but the width of the
 | 
			
		||||
				// rectangle might not be calculated yet, due to the layout.
 | 
			
		||||
				// In the `Component.onCompleted` event handler, all of the component's
 | 
			
		||||
				// properties and children have been initialized.
 | 
			
		||||
				Component.onCompleted: anim.start()
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -7,11 +7,13 @@ Singleton {
 | 
			
		|||
	readonly property string rtpath: "/run/user/1000/quickshell"
 | 
			
		||||
 | 
			
		||||
	readonly property var colors: QtObject {
 | 
			
		||||
		readonly property var bar: "#30c0ffff";
 | 
			
		||||
		readonly property var barOutline: "#50ffffff";
 | 
			
		||||
		readonly property var widget: "#40ceffff";
 | 
			
		||||
		readonly property var widgetOutline: "#60ffffff";
 | 
			
		||||
		readonly property var separator: "#60ffffff";
 | 
			
		||||
		readonly property color bar: "#30c0ffff";
 | 
			
		||||
		readonly property color barOutline: "#50ffffff";
 | 
			
		||||
		readonly property color widget: "#25ceffff";
 | 
			
		||||
		readonly property color widgetActive: "#80ceffff";
 | 
			
		||||
		readonly property color widgetOutline: "#40ffffff";
 | 
			
		||||
		readonly property color widgetOutlineSeparate: "#20ffffff";
 | 
			
		||||
		readonly property color separator: "#60ffffff";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	readonly property var popoutXCurve: EasingCurve {
 | 
			
		||||
| 
						 | 
				
			
			@ -31,4 +33,9 @@ Singleton {
 | 
			
		|||
 | 
			
		||||
		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);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										5
									
								
								modules/user/modules/quickshell/shell/Shortcut.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
import Quickshell.Hyprland
 | 
			
		||||
 | 
			
		||||
GlobalShortcut {
 | 
			
		||||
	appid: "shell"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +1,46 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import Quickshell
 | 
			
		||||
import "systray" as SysTray
 | 
			
		||||
import "audio" as Audio
 | 
			
		||||
import "mpris" as Mpris
 | 
			
		||||
import "workspaces" as Workspaces
 | 
			
		||||
 | 
			
		||||
BarContainment {
 | 
			
		||||
	id: root
 | 
			
		||||
 | 
			
		||||
	property bool isSoleBar: Quickshell.screens.length == 1;
 | 
			
		||||
 | 
			
		||||
	ColumnLayout {
 | 
			
		||||
		anchors {
 | 
			
		||||
			left: parent.left
 | 
			
		||||
			right: parent.right
 | 
			
		||||
			top: parent.top
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ColumnLayout {
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
			spacing: 0
 | 
			
		||||
 | 
			
		||||
			Loader {
 | 
			
		||||
				active: isSoleBar
 | 
			
		||||
				Layout.preferredHeight: active ? implicitHeight : 0;
 | 
			
		||||
				Layout.fillWidth: true
 | 
			
		||||
 | 
			
		||||
				sourceComponent: Workspaces.Widget {
 | 
			
		||||
					bar: root
 | 
			
		||||
					wsBaseIndex: 1
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Workspaces.Widget {
 | 
			
		||||
				bar: root
 | 
			
		||||
				Layout.fillWidth: true
 | 
			
		||||
				wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1;
 | 
			
		||||
				hideWhenEmpty: isSoleBar
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ColumnLayout {
 | 
			
		||||
| 
						 | 
				
			
			@ -20,10 +50,19 @@ BarContainment {
 | 
			
		|||
			bottom: parent.bottom
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Mpris.Players {
 | 
			
		||||
			bar: root
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Audio.AudioControls {
 | 
			
		||||
			bar: root
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		SysTray.SysTray {
 | 
			
		||||
			bar: root
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
			//width: 24
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ClockWidget {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,13 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Wayland
 | 
			
		||||
import ".."
 | 
			
		||||
import "root:."
 | 
			
		||||
import "root:lock" as Lock
 | 
			
		||||
 | 
			
		||||
PanelWindow {
 | 
			
		||||
	id: root
 | 
			
		||||
 | 
			
		||||
	default property list<QtObject> widgetSurfaceData;
 | 
			
		||||
	readonly property var widgetSurface: widgetSurface;
 | 
			
		||||
	property list<var> overlays: [];
 | 
			
		||||
	default property alias barItems: containment.data;
 | 
			
		||||
 | 
			
		||||
	anchors {
 | 
			
		||||
		left: true
 | 
			
		||||
| 
						 | 
				
			
			@ -17,9 +16,25 @@ PanelWindow {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	width: 70
 | 
			
		||||
	margins.left: Lock.Controller.locked ? -width : 0
 | 
			
		||||
	exclusiveZone: width - margins.left
 | 
			
		||||
 | 
			
		||||
	color: "transparent"
 | 
			
		||||
	WlrLayershell.namespace: "shell:bar"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	readonly property var tooltip: tooltip;
 | 
			
		||||
	Tooltip {
 | 
			
		||||
		id: tooltip
 | 
			
		||||
		bar: root
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	readonly property real tooltipXOffset: root.width + 2;
 | 
			
		||||
 | 
			
		||||
	function boundedY(targetY: real, height: real): real {
 | 
			
		||||
		return Math.max(barRect.anchors.topMargin + height, Math.min(barRect.height + barRect.anchors.topMargin - height, targetY))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Rectangle {
 | 
			
		||||
		id: barRect
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -43,72 +58,4 @@ PanelWindow {
 | 
			
		|||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// note: must be above the widgetSurface due to reload order
 | 
			
		||||
	PersistentProperties {
 | 
			
		||||
		id: persist
 | 
			
		||||
		reloadableId: "persist"
 | 
			
		||||
 | 
			
		||||
		property bool visible: false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onBackingWindowVisibleChanged: {
 | 
			
		||||
		persist.visible = Qt.binding(() => backingWindowVisible);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	PanelWindow {
 | 
			
		||||
		id: widgetSurface
 | 
			
		||||
		reloadableId: "widgetSurface"
 | 
			
		||||
 | 
			
		||||
		visible: persist.visible
 | 
			
		||||
		anchors: root.anchors
 | 
			
		||||
		screen: root.screen
 | 
			
		||||
		exclusionMode: ExclusionMode.Ignore
 | 
			
		||||
		WlrLayershell.namespace: "shell:bar"
 | 
			
		||||
		WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
 | 
			
		||||
		color: "transparent"
 | 
			
		||||
 | 
			
		||||
		width: {
 | 
			
		||||
			const extents = overlays
 | 
			
		||||
				.filter(overlay => !overlay.fullyCollapsed)
 | 
			
		||||
				.map(overlay => overlayXOffset + overlay.expandedWidth);
 | 
			
		||||
 | 
			
		||||
			return Math.max(root.width, ...extents);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		readonly property real overlayXOffset: root.width + 10;
 | 
			
		||||
		readonly property real tooltipXOffset: root.width + 2;
 | 
			
		||||
 | 
			
		||||
		function overlayRect(targetY: real, size: rect): rect {
 | 
			
		||||
			const y = Math.max(barRect.y, Math.min((barRect.y + barRect.height) - size.height, targetY));
 | 
			
		||||
			return Qt.rect(overlayXOffset, y, size.width, size.height);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function boundedY(targetY: real, height: real): real {
 | 
			
		||||
			return Math.max(0, Math.min(barRect.height - height, targetY))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Item {
 | 
			
		||||
			id: contentArea
 | 
			
		||||
			data: widgetSurfaceData
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		readonly property var tooltip: tooltip;
 | 
			
		||||
		Tooltip {
 | 
			
		||||
			id: tooltip
 | 
			
		||||
			bar: root
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function repositionContentArea() {
 | 
			
		||||
			// abusing the knowledge that both bars are in the same position onscreen
 | 
			
		||||
			const contentRect = containment.mapToItem(root.contentItem, 0, 0, containment.width, containment.height)
 | 
			
		||||
 | 
			
		||||
			contentArea.x = contentRect.x
 | 
			
		||||
			contentArea.y = contentRect.y
 | 
			
		||||
			contentArea.width = contentRect.width
 | 
			
		||||
			contentArea.height = contentRect.height
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onWindowTransformChanged: widgetSurface.repositionContentArea()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,9 +12,21 @@ Scope {
 | 
			
		|||
	readonly property TooltipItem activeItem: activeMenu ?? activeTooltip;
 | 
			
		||||
	property TooltipItem lastActiveItem: null;
 | 
			
		||||
 | 
			
		||||
	property Item tooltipItem: null;
 | 
			
		||||
 | 
			
		||||
	onActiveItemChanged: {
 | 
			
		||||
		if (activeItem != null) activeItem.visible = true;
 | 
			
		||||
		if (lastActiveItem != null) lastActiveItem.visible = false;
 | 
			
		||||
		if (activeItem != null) {
 | 
			
		||||
			activeItem.targetVisible = true;
 | 
			
		||||
 | 
			
		||||
			if (tooltipItem) {
 | 
			
		||||
				activeItem.parent = tooltipItem;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (lastActiveItem != null && lastActiveItem != activeItem) {
 | 
			
		||||
			lastActiveItem.targetVisible = false;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		lastActiveItem = activeItem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,21 +52,22 @@ Scope {
 | 
			
		|||
 | 
			
		||||
		PopupWindow {
 | 
			
		||||
			id: popup
 | 
			
		||||
			parentWindow: bar.widgetSurface
 | 
			
		||||
			relativeX: bar.widgetSurface.tooltipXOffset
 | 
			
		||||
			parentWindow: bar
 | 
			
		||||
			relativeX: bar.tooltipXOffset
 | 
			
		||||
			relativeY: 0
 | 
			
		||||
			height: bar.widgetSurface.height
 | 
			
		||||
			width: tooltipItem.width
 | 
			
		||||
			height: bar.height
 | 
			
		||||
			width: 1000//Math.max(1, widthAnim.running ? Math.max(tooltipItem.targetWidth, tooltipItem.lastTargetWidth) : tooltipItem.targetWidth)
 | 
			
		||||
			visible: true
 | 
			
		||||
			color: "transparent"
 | 
			
		||||
			//color: "#20000000"
 | 
			
		||||
 | 
			
		||||
			mask: Region {
 | 
			
		||||
				item: (activeItem?.isMenu ?? false) ? tooltipItem : null
 | 
			
		||||
				item: (activeItem?.hoverable ?? false) ? tooltipItem : null
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			HyprlandFocusGrab {
 | 
			
		||||
				active: activeItem?.isMenu ?? false
 | 
			
		||||
				windows: [ popup, bar.widgetSurface ]
 | 
			
		||||
				windows: [ popup, bar ]
 | 
			
		||||
				onActiveChanged: {
 | 
			
		||||
					if (!active && activeItem?.isMenu) {
 | 
			
		||||
						activeMenu.close()
 | 
			
		||||
| 
						 | 
				
			
			@ -62,19 +75,34 @@ Scope {
 | 
			
		|||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			BarWidgetInner {
 | 
			
		||||
			Item {
 | 
			
		||||
				id: tooltipItem
 | 
			
		||||
				Component.onCompleted: {
 | 
			
		||||
					root.tooltipItem = this;
 | 
			
		||||
					if (root.activeItem) {
 | 
			
		||||
						root.activeItem.parent = this;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				readonly property var targetWidth: activeItem?.implicitWidth ?? 10;
 | 
			
		||||
				readonly property var targetHeight: (activeItem?.implicitHeight ?? 0) + 10;
 | 
			
		||||
				readonly property var targetWidth: activeItem?.implicitWidth ?? 0;
 | 
			
		||||
				readonly property var targetHeight: activeItem?.implicitHeight ?? 0;
 | 
			
		||||
 | 
			
		||||
				property var lastTargetWidthTracker: 0;
 | 
			
		||||
				property var lastTargetWidth: 0;
 | 
			
		||||
 | 
			
		||||
				onTargetWidthChanged: {
 | 
			
		||||
					lastTargetWidth = lastTargetWidthTracker;
 | 
			
		||||
					lastTargetWidthTracker = targetWidth;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				readonly property real targetY: {
 | 
			
		||||
					if (activeItem == null) return 0;
 | 
			
		||||
					const target = bar.widgetSurface.contentItem.mapFromItem(activeItem.owner, 0, activeItem.targetRelativeY).y;
 | 
			
		||||
					return bar.widgetSurface.boundedY(target, activeItem.implicitHeight / 2);
 | 
			
		||||
					const target = bar.contentItem.mapFromItem(activeItem.owner, 0, activeItem.targetRelativeY).y;
 | 
			
		||||
					return bar.boundedY(target, activeItem.implicitHeight / 2);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				width: targetWidth + 10
 | 
			
		||||
				property var w: -1
 | 
			
		||||
				width: Math.max(1, w)
 | 
			
		||||
 | 
			
		||||
				property var y1: -1
 | 
			
		||||
				property var y2: -1
 | 
			
		||||
| 
						 | 
				
			
			@ -112,13 +140,19 @@ Scope {
 | 
			
		|||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Item {
 | 
			
		||||
					clip: true
 | 
			
		||||
					children: [ activeItem ]
 | 
			
		||||
 | 
			
		||||
					anchors {
 | 
			
		||||
						fill: parent
 | 
			
		||||
						margins: 5
 | 
			
		||||
				SmoothedAnimation {
 | 
			
		||||
					id: widthAnim
 | 
			
		||||
					target: tooltipItem
 | 
			
		||||
					property: "w"
 | 
			
		||||
					to: tooltipItem.targetWidth;
 | 
			
		||||
					onToChanged: {
 | 
			
		||||
						if (tooltipItem.w == -1) {
 | 
			
		||||
							stop();
 | 
			
		||||
							tooltipItem.w = to;
 | 
			
		||||
						} else {
 | 
			
		||||
							velocity = (Math.max(tooltipItem.width, to) - Math.min(tooltipItem.width, to)) * 5;
 | 
			
		||||
							restart();
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,29 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import "root:/"
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property var tooltip;
 | 
			
		||||
	required property Item owner;
 | 
			
		||||
	property bool isMenu: false;
 | 
			
		||||
	property bool hoverable: isMenu;
 | 
			
		||||
	property bool animateSize: true;
 | 
			
		||||
	property bool show: false;
 | 
			
		||||
	property bool preloadBackground: root.visible;
 | 
			
		||||
 | 
			
		||||
	property real targetRelativeY: owner.height / 2;
 | 
			
		||||
	property real hangTime: isMenu ? 0 : 200;
 | 
			
		||||
 | 
			
		||||
	signal close();
 | 
			
		||||
 | 
			
		||||
	default property alias data: contentItem.data;
 | 
			
		||||
 | 
			
		||||
	property Component backgroundComponent: BarWidgetInner {
 | 
			
		||||
		color: ShellGlobals.colors.bar
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onShowChanged: {
 | 
			
		||||
		if (show) {
 | 
			
		||||
			hangTimer.stop();
 | 
			
		||||
| 
						 | 
				
			
			@ -28,4 +38,60 @@ Item {
 | 
			
		|||
		interval: hangTime
 | 
			
		||||
		onTriggered: tooltip.removeItem(root);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property bool targetVisible: false
 | 
			
		||||
	property real targetOpacity: 0
 | 
			
		||||
	opacity: targetOpacity / 1000
 | 
			
		||||
 | 
			
		||||
	Behavior on targetOpacity {
 | 
			
		||||
		id: opacityAnimation
 | 
			
		||||
		SmoothedAnimation { velocity: 5000 }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function snapOpacity(opacity: real) {
 | 
			
		||||
		opacityAnimation.enabled = false;
 | 
			
		||||
		targetOpacity = opacity * 1000
 | 
			
		||||
		opacityAnimation.enabled = true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onTargetVisibleChanged: {
 | 
			
		||||
		if (targetVisible) {
 | 
			
		||||
			visible = true;
 | 
			
		||||
			targetOpacity = 1000;
 | 
			
		||||
		} else {
 | 
			
		||||
			close()
 | 
			
		||||
			targetOpacity = 0;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onTargetOpacityChanged: {
 | 
			
		||||
		if (!targetVisible && targetOpacity == 0) {
 | 
			
		||||
			visible = false;
 | 
			
		||||
			this.parent = null;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	anchors.fill: parent
 | 
			
		||||
	visible: false
 | 
			
		||||
	clip: true
 | 
			
		||||
	implicitHeight: contentItem.implicitHeight + 10
 | 
			
		||||
	implicitWidth: contentItem.implicitWidth + 10
 | 
			
		||||
 | 
			
		||||
	readonly property Item item: contentItem;
 | 
			
		||||
 | 
			
		||||
	Loader {
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		active: root.visible || root.preloadBackground
 | 
			
		||||
		asynchronous: !root.visible && root.preloadBackground
 | 
			
		||||
		sourceComponent: backgroundComponent
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Item {
 | 
			
		||||
		id: contentItem
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		anchors.margins: 5
 | 
			
		||||
 | 
			
		||||
		implicitHeight: childrenRect.height
 | 
			
		||||
		implicitWidth: childrenRect.width
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,78 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell.Services.Pipewire
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
ClickableIcon {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property var bar;
 | 
			
		||||
	required property PwNode node;
 | 
			
		||||
	property bool mixerOpen: false;
 | 
			
		||||
 | 
			
		||||
	PwObjectTracker { objects: [ node ] }
 | 
			
		||||
 | 
			
		||||
	implicitHeight: width;
 | 
			
		||||
	acceptedButtons: Qt.LeftButton | Qt.RightButton;
 | 
			
		||||
	showPressed: mixerOpen
 | 
			
		||||
 | 
			
		||||
	onClicked: event => {
 | 
			
		||||
		event.accepted = true;
 | 
			
		||||
		if (event.button === Qt.LeftButton) {
 | 
			
		||||
			node.audio.muted = !node.audio.muted;
 | 
			
		||||
		} else if (event.button === Qt.RightButton) {
 | 
			
		||||
			mixerOpen = !mixerOpen;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onWheel: event => {
 | 
			
		||||
		event.accepted = true;
 | 
			
		||||
		node.audio.volume += (event.angleDelta.y / 120) * 0.05
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property var tooltip: TooltipItem {
 | 
			
		||||
		tooltip: bar.tooltip
 | 
			
		||||
		owner: root
 | 
			
		||||
 | 
			
		||||
		show: root.containsMouse || mouseArea.containsMouse
 | 
			
		||||
		hoverable: true
 | 
			
		||||
 | 
			
		||||
		MouseArea {
 | 
			
		||||
			id: mouseArea
 | 
			
		||||
			hoverEnabled: true
 | 
			
		||||
			acceptedButtons: Qt.NoButton
 | 
			
		||||
 | 
			
		||||
			implicitWidth: childrenRect.width
 | 
			
		||||
			implicitHeight: childrenRect.height
 | 
			
		||||
 | 
			
		||||
			VolumeSlider {
 | 
			
		||||
				implicitWidth: 200
 | 
			
		||||
				implicitHeight: root.height
 | 
			
		||||
 | 
			
		||||
				//enabled: !node.audio.muted
 | 
			
		||||
				value: node.audio.volume
 | 
			
		||||
				onValueChanged: node.audio.volume = value
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property var rightclickMenu: TooltipItem {
 | 
			
		||||
		tooltip: bar.tooltip
 | 
			
		||||
		owner: root
 | 
			
		||||
 | 
			
		||||
		isMenu: true
 | 
			
		||||
		show: mixerOpen
 | 
			
		||||
 | 
			
		||||
		onClose: mixerOpen = false
 | 
			
		||||
		/*onVisibleChanged: {
 | 
			
		||||
			if (!visible) mixerOpen = false;
 | 
			
		||||
		}*/
 | 
			
		||||
 | 
			
		||||
		Loader {
 | 
			
		||||
			active: rightclickMenu.visible
 | 
			
		||||
			sourceComponent: Mixer {
 | 
			
		||||
				width: 550
 | 
			
		||||
				trackedNode: node
 | 
			
		||||
				nodeImage: root.image
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Services.Pipewire
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
BarWidgetInner {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property var bar;
 | 
			
		||||
	implicitHeight: column.implicitHeight + 10;
 | 
			
		||||
 | 
			
		||||
	ColumnLayout {
 | 
			
		||||
		anchors {
 | 
			
		||||
			fill: parent;
 | 
			
		||||
			margins: 5;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		id: column;
 | 
			
		||||
		implicitHeight: childrenRect.height;
 | 
			
		||||
		spacing: 5;
 | 
			
		||||
 | 
			
		||||
		Loader {
 | 
			
		||||
			Layout.fillWidth: true;
 | 
			
		||||
			active: Pipewire.defaultAudioSink != null;
 | 
			
		||||
 | 
			
		||||
			sourceComponent: AudioControl {
 | 
			
		||||
				bar: root.bar;
 | 
			
		||||
				node: Pipewire.defaultAudioSink;
 | 
			
		||||
				image: `image://icon/${node.audio.muted ? "audio-volume-muted-symbolic" : "audio-volume-high-symbolic"}`
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Loader {
 | 
			
		||||
			Layout.fillWidth: true;
 | 
			
		||||
			active: Pipewire.defaultAudioSource != null;
 | 
			
		||||
 | 
			
		||||
			sourceComponent: AudioControl {
 | 
			
		||||
				bar: root.bar;
 | 
			
		||||
				node: Pipewire.defaultAudioSource;
 | 
			
		||||
				image: `image://icon/${node.audio.muted ? "microphone-sensitivity-muted-symbolic" : "microphone-sensitivity-high-symbolic"}`
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										56
									
								
								modules/user/modules/quickshell/shell/bar/audio/Mixer.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,56 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell.Services.Pipewire
 | 
			
		||||
import ".."
 | 
			
		||||
import "../.."
 | 
			
		||||
 | 
			
		||||
ColumnLayout {
 | 
			
		||||
	required property PwNode trackedNode;
 | 
			
		||||
	required property string nodeImage;
 | 
			
		||||
 | 
			
		||||
	PwNodeLinkTracker {
 | 
			
		||||
		id: linkTracker
 | 
			
		||||
		node: trackedNode
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	PwObjectTracker { objects: [ trackedNode, ...linkTracker.linkGroups ] }
 | 
			
		||||
 | 
			
		||||
	MixerEntry {
 | 
			
		||||
		id: nodeEntry
 | 
			
		||||
		node: trackedNode
 | 
			
		||||
		image: nodeImage
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Rectangle {
 | 
			
		||||
		Layout.fillWidth: true
 | 
			
		||||
		implicitHeight: 1
 | 
			
		||||
		visible: linkTracker.linkGroups.length > 0
 | 
			
		||||
 | 
			
		||||
		color: ShellGlobals.colors.separator
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Repeater {
 | 
			
		||||
		model: linkTracker.linkGroups
 | 
			
		||||
 | 
			
		||||
		MixerEntry {
 | 
			
		||||
			required property PwLinkGroup modelData;
 | 
			
		||||
			node: trackedNode.isSink ? modelData.source : modelData.target;
 | 
			
		||||
			state: modelData.state;
 | 
			
		||||
 | 
			
		||||
			image: {
 | 
			
		||||
				let icon = "";
 | 
			
		||||
				let props = node.properties;
 | 
			
		||||
				if (props["application.icon-name"] != undefined) {
 | 
			
		||||
					icon = props["application.icon-name"];
 | 
			
		||||
				} else if (props["application.process.binary"] != undefined) {
 | 
			
		||||
					icon = props["application.process.binary"];
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// special cases :(
 | 
			
		||||
				if (icon == "firefox") icon = "firefox-devedition";
 | 
			
		||||
 | 
			
		||||
				return `image://icon/${icon}`
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell.Services.Pipewire
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
RowLayout {
 | 
			
		||||
	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
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										109
									
								
								modules/user/modules/quickshell/shell/bar/audio/VolumeSlider.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,109 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Shapes
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	id: root
 | 
			
		||||
	property real from: 0.0
 | 
			
		||||
	property real to: 1.5
 | 
			
		||||
	property real warning: 1.0
 | 
			
		||||
	property real value: 0.0
 | 
			
		||||
 | 
			
		||||
	implicitWidth: groove.implicitWidth
 | 
			
		||||
	implicitHeight: 20
 | 
			
		||||
 | 
			
		||||
	property real __valueOffset: ((value - from) / (to - from)) * groove.width
 | 
			
		||||
	property real __wheelValue: -1
 | 
			
		||||
 | 
			
		||||
	MouseArea {
 | 
			
		||||
		id: mouseArea
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
		Rectangle {
 | 
			
		||||
			id: grooveWarning
 | 
			
		||||
 | 
			
		||||
			anchors {
 | 
			
		||||
				left: groove.left
 | 
			
		||||
				leftMargin: ((warning - from) / (to - from)) * groove.width
 | 
			
		||||
				right: groove.right
 | 
			
		||||
				top: groove.top
 | 
			
		||||
				bottom: groove.bottom
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			color: "#60ffa800"
 | 
			
		||||
			topRightRadius: 5
 | 
			
		||||
			bottomRightRadius: 5
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Rectangle {
 | 
			
		||||
			anchors {
 | 
			
		||||
				top: groove.bottom
 | 
			
		||||
				horizontalCenter: grooveWarning.left
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			color: "#60eeffff"
 | 
			
		||||
			width: 1
 | 
			
		||||
			height: groove.height
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Rectangle {
 | 
			
		||||
			id: grooveFill
 | 
			
		||||
 | 
			
		||||
			anchors {
 | 
			
		||||
				left: groove.left
 | 
			
		||||
				top: groove.top
 | 
			
		||||
				bottom: groove.bottom
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			radius: 5
 | 
			
		||||
			color: "#80ceffff"
 | 
			
		||||
			width: __valueOffset
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Rectangle {
 | 
			
		||||
			id: groove
 | 
			
		||||
 | 
			
		||||
			anchors {
 | 
			
		||||
				left: parent.left
 | 
			
		||||
				right: parent.right
 | 
			
		||||
				verticalCenter: parent.verticalCenter
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			implicitHeight: 7
 | 
			
		||||
			color: "transparent"
 | 
			
		||||
			border.color: "#20050505"
 | 
			
		||||
			border.width: 1
 | 
			
		||||
			radius: 5
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Rectangle {
 | 
			
		||||
			id: handle
 | 
			
		||||
			anchors.verticalCenter: groove.verticalCenter
 | 
			
		||||
			height: 15
 | 
			
		||||
			width: height
 | 
			
		||||
			radius: height * 0.5
 | 
			
		||||
			x: __valueOffset - width * 0.5
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		onWheel: event => {
 | 
			
		||||
			event.accepted = true;
 | 
			
		||||
			__wheelValue = value + (event.angleDelta.y / 120) * 0.05
 | 
			
		||||
			__wheelValue = -1
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Binding {
 | 
			
		||||
		when: mouseArea.pressed
 | 
			
		||||
		target: root
 | 
			
		||||
		property: "value"
 | 
			
		||||
		value: (mouseArea.mouseX / width) * (to - from) + from
 | 
			
		||||
		restoreMode: Binding.RestoreBinding
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Binding {
 | 
			
		||||
		when: __wheelValue != -1
 | 
			
		||||
		target: root
 | 
			
		||||
		property: "value"
 | 
			
		||||
		value: __wheelValue
 | 
			
		||||
		restoreMode: Binding.RestoreBinding
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 650 B  | 
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 474 B  | 
| 
						 | 
				
			
			@ -0,0 +1,161 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Qt5Compat.GraphicalEffects
 | 
			
		||||
import ".."
 | 
			
		||||
import "../.."
 | 
			
		||||
 | 
			
		||||
BarWidgetInner {
 | 
			
		||||
	id: root
 | 
			
		||||
	border.color: "transparent"
 | 
			
		||||
 | 
			
		||||
	property real renderWidth: width
 | 
			
		||||
	property real renderHeight: height
 | 
			
		||||
 | 
			
		||||
	property real blurRadius: 20;
 | 
			
		||||
	property real blurSamples: 41;
 | 
			
		||||
 | 
			
		||||
	property bool reverse: false;
 | 
			
		||||
 | 
			
		||||
	function setArt(art: string, reverse: bool, immediate: bool) {
 | 
			
		||||
		this.reverse = reverse;
 | 
			
		||||
 | 
			
		||||
		if (art.length == 0) {
 | 
			
		||||
			stack.replace(null);
 | 
			
		||||
		} else {
 | 
			
		||||
			stack.replace(component, { uri: art }, immediate)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property var component: Component {
 | 
			
		||||
		Item {
 | 
			
		||||
			id: componentRoot
 | 
			
		||||
			property var uri: null;
 | 
			
		||||
			readonly property bool svReady: image.status === Image.Ready;
 | 
			
		||||
 | 
			
		||||
			Image {
 | 
			
		||||
				id: image
 | 
			
		||||
				anchors.centerIn: parent;
 | 
			
		||||
				source: uri;
 | 
			
		||||
				cache: false;
 | 
			
		||||
				asynchronous: true;
 | 
			
		||||
 | 
			
		||||
				fillMode: Image.PreserveAspectCrop;
 | 
			
		||||
				sourceSize.width: width;
 | 
			
		||||
				sourceSize.height: height;
 | 
			
		||||
				width: stack.width + blurRadius * 2;
 | 
			
		||||
				height: stack.height + blurRadius * 2;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			property Component blurComponent: Item {
 | 
			
		||||
				id: blur
 | 
			
		||||
				//parent: blurContainment
 | 
			
		||||
				// blur into the neighboring elements if applicable
 | 
			
		||||
				x: componentRoot.x - blurRadius * 4
 | 
			
		||||
				y: componentRoot.y + image.y
 | 
			
		||||
				width: componentRoot.width + blurRadius * 8
 | 
			
		||||
				height: image.height
 | 
			
		||||
 | 
			
		||||
				onVisibleChanged: {
 | 
			
		||||
					if (visible) blurSource.scheduleUpdate();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Connections {
 | 
			
		||||
					target: image
 | 
			
		||||
					function onStatusChanged() {
 | 
			
		||||
						if (image.status == Image.Ready) {
 | 
			
		||||
							blurSource.scheduleUpdate();
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				ShaderEffectSource {
 | 
			
		||||
					id: blurSource
 | 
			
		||||
					sourceItem: stack
 | 
			
		||||
					sourceRect: Qt.rect(blur.x, blur.y, blur.width, blur.height);
 | 
			
		||||
					live: false
 | 
			
		||||
					anchors.fill: parent
 | 
			
		||||
					visible: false
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Item {
 | 
			
		||||
					x: blurRadius
 | 
			
		||||
					width: blur.width - blurRadius * 2
 | 
			
		||||
					height: blur.height
 | 
			
		||||
					clip: true
 | 
			
		||||
					GaussianBlur {
 | 
			
		||||
						source: blurSource
 | 
			
		||||
						x: -parent.x
 | 
			
		||||
						width: blur.width
 | 
			
		||||
						height: blur.height
 | 
			
		||||
						radius: root.blurRadius
 | 
			
		||||
						samples: root.blurSamples
 | 
			
		||||
						visible: true
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Weird crash if the blur is not owned by its visual parent,
 | 
			
		||||
			// so it has to be a component.
 | 
			
		||||
			property Item blur: blurComponent.createObject(blurContainment);
 | 
			
		||||
			Component.onDestruction: blur.destroy();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	SlideView {
 | 
			
		||||
		id: stack;
 | 
			
		||||
		height: renderHeight
 | 
			
		||||
		width: renderWidth
 | 
			
		||||
		anchors.centerIn: parent;
 | 
			
		||||
		visible: false;
 | 
			
		||||
		animate: root.visible;
 | 
			
		||||
 | 
			
		||||
		readonly property real fromPos: (stack.width + blurRadius * 2) * (reverse ? -1 : 1);
 | 
			
		||||
 | 
			
		||||
		enterTransition: PropertyAnimation {
 | 
			
		||||
			property: "x"
 | 
			
		||||
			from: stack.fromPos
 | 
			
		||||
			to: 0
 | 
			
		||||
			duration: 400
 | 
			
		||||
			easing.type: Easing.OutExpo;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		exitTransition: PropertyAnimation {
 | 
			
		||||
			property: "x"
 | 
			
		||||
			to: -stack.fromPos
 | 
			
		||||
			duration: 400
 | 
			
		||||
			easing.type: Easing.OutExpo;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Item {
 | 
			
		||||
		id: blurContainment
 | 
			
		||||
		x: stack.x
 | 
			
		||||
		y: stack.y
 | 
			
		||||
		width: stack.width
 | 
			
		||||
		height: stack.height
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	readonly property Rectangle overlay: overlayItem;
 | 
			
		||||
	Rectangle {
 | 
			
		||||
		id: overlayItem
 | 
			
		||||
		visible: false
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		border.color: ShellGlobals.colors.widgetOutlineSeparate
 | 
			
		||||
		border.width: 0//1
 | 
			
		||||
		radius: 0//root.radius
 | 
			
		||||
		color: "transparent"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Templates as T
 | 
			
		||||
 | 
			
		||||
T.Slider {
 | 
			
		||||
	id: control
 | 
			
		||||
 | 
			
		||||
  implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
 | 
			
		||||
    implicitHandleWidth + leftPadding + rightPadding)
 | 
			
		||||
  implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
 | 
			
		||||
    implicitHandleHeight + topPadding + bottomPadding)
 | 
			
		||||
 | 
			
		||||
	background: Rectangle {
 | 
			
		||||
		x: control.leftPadding
 | 
			
		||||
		y: control.topPadding + control.availableHeight / 2 - height / 2
 | 
			
		||||
		implicitWidth: 200
 | 
			
		||||
		implicitHeight: 7
 | 
			
		||||
		width: control.availableWidth
 | 
			
		||||
		height: implicitHeight
 | 
			
		||||
 | 
			
		||||
		radius: 5
 | 
			
		||||
		color: "#30ceffff"
 | 
			
		||||
		border.width: 0
 | 
			
		||||
 | 
			
		||||
		Rectangle {
 | 
			
		||||
			anchors {
 | 
			
		||||
				top: parent.top
 | 
			
		||||
				bottom: parent.bottom
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			width: control.handle.x + (control.handle.width / 2) - parent.x
 | 
			
		||||
			radius: parent.radius
 | 
			
		||||
			color: "#80ceffff"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	handle: Rectangle {
 | 
			
		||||
		x: control.leftPadding + control.visualPosition * (control.availableWidth - width)
 | 
			
		||||
		y: control.topPadding + control.availableHeight / 2 - height / 2
 | 
			
		||||
		implicitWidth: 16
 | 
			
		||||
		implicitHeight: 16
 | 
			
		||||
		radius: 8
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,168 @@
 | 
			
		|||
pragma Singleton
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Services.Mpris
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
import "../.."
 | 
			
		||||
 | 
			
		||||
Singleton {
 | 
			
		||||
	id: root;
 | 
			
		||||
	property MprisPlayer trackedPlayer: null;
 | 
			
		||||
	property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null;
 | 
			
		||||
	signal trackChanged(reverse: bool);
 | 
			
		||||
 | 
			
		||||
	property bool __reverse: false;
 | 
			
		||||
 | 
			
		||||
	property var activeTrack;
 | 
			
		||||
	Component.onCompleted: {
 | 
			
		||||
		for (const player of Mpris.players.values) {
 | 
			
		||||
			if (player.playbackState == MprisPlaybackState.Playing) {
 | 
			
		||||
				if (root.trackedPlayer == null) {
 | 
			
		||||
					root.trackedPlayer = player;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			player.playbackStateChanged.connect(() => {
 | 
			
		||||
				if (root.trackedPlayer !== player) root.trackedPlayer = player;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Connections {
 | 
			
		||||
		target: activePlayer
 | 
			
		||||
 | 
			
		||||
		function onTrackChanged() {
 | 
			
		||||
			root.updateTrack();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Change the tracked player when one changes playback state or is created in a playing state.
 | 
			
		||||
	Connections {
 | 
			
		||||
		target: Mpris.players;
 | 
			
		||||
 | 
			
		||||
		function onObjectInsertedPost(player: MprisPlayer) {
 | 
			
		||||
			if (player.playbackState === MprisPlaybackState.Playing) {
 | 
			
		||||
				if (root.trackedPlayer !== player) root.trackedPlayer = player;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			player.playbackStateChanged.connect(() => {
 | 
			
		||||
				if (root.trackedPlayer !== player) root.trackedPlayer = player;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function onObjectRemovedPre() {
 | 
			
		||||
			console.log(`trackedPlayer: ${root.trackedPlayer}`)
 | 
			
		||||
			if (root.trackedPlayer == null) {
 | 
			
		||||
				for (const player of Mpris.players.values) {
 | 
			
		||||
					if (player.playbackState === MprisPlaybackState.Playing) {
 | 
			
		||||
						root.trackedPlayer = player;
 | 
			
		||||
						break;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onActivePlayerChanged: this.updateTrack();
 | 
			
		||||
 | 
			
		||||
	function updateTrack() {
 | 
			
		||||
		const metadata = this.activePlayer?.metadata ?? {};
 | 
			
		||||
 | 
			
		||||
		this.activeTrack = {
 | 
			
		||||
			artUrl: metadata["mpris:artUrl"] ?? "",
 | 
			
		||||
			title: metadata["xesam:title"] ?? "",
 | 
			
		||||
			artist: metadata["xesam:artist"] ?? "",
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		this.trackChanged(__reverse);
 | 
			
		||||
		this.__reverse = false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property bool isPlaying: this.activePlayer && this.activePlayer.playbackState == MprisPlaybackState.Playing;
 | 
			
		||||
	property bool canPlay: this.activePlayer?.canPlay ?? false;
 | 
			
		||||
	function play() {
 | 
			
		||||
		if (this.canPlay) this.activePlayer.playbackState = MprisPlaybackState.Playing;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property bool canPause: this.activePlayer?.canPause ?? false;
 | 
			
		||||
	function pause() {
 | 
			
		||||
		if (this.canPause) this.activePlayer.playbackState = MprisPlaybackState.Paused;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false;
 | 
			
		||||
	function previous() {
 | 
			
		||||
		if (this.canGoPrevious) {
 | 
			
		||||
			this.__reverse = true;
 | 
			
		||||
			this.activePlayer.previous();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property bool canGoNext: this.activePlayer?.canGoNext ?? false;
 | 
			
		||||
	function next() {
 | 
			
		||||
		if (this.canGoNext) {
 | 
			
		||||
			this.__reverse = false;
 | 
			
		||||
			this.activePlayer.next();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl;
 | 
			
		||||
 | 
			
		||||
	property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl;
 | 
			
		||||
	property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None;
 | 
			
		||||
	function setLoopState(loopState: var) {
 | 
			
		||||
		if (this.loopSupported) {
 | 
			
		||||
			this.activePlayer.loopState = loopState;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl;
 | 
			
		||||
	property bool hasShuffle: this.activePlayer?.shuffle ?? false;
 | 
			
		||||
	function setShuffle(shuffle: bool) {
 | 
			
		||||
		if (this.shuffleSupported) {
 | 
			
		||||
			this.activePlayer.shuffle = shuffle;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function setActivePlayer(player: MprisPlayer) {
 | 
			
		||||
		const targetPlayer = player ?? MprisPlayer.players[0];
 | 
			
		||||
		console.log(`setactive: ${targetPlayer} from ${activePlayer}`)
 | 
			
		||||
 | 
			
		||||
		if (targetPlayer && this.activePlayer) {
 | 
			
		||||
			this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer);
 | 
			
		||||
		} else {
 | 
			
		||||
			// always animate forward if going to null
 | 
			
		||||
			this.__reverse = false;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.trackedPlayer = targetPlayer;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
import Quickshell
 | 
			
		||||
import Quickshell.Services.Mpris
 | 
			
		||||
 | 
			
		||||
Scope {
 | 
			
		||||
	required property MprisPlayer player;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import
 | 
			
		||||
							
								
								
									
										527
									
								
								modules/user/modules/quickshell/shell/bar/mpris/Players.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,527 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Qt5Compat.GraphicalEffects
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Services.Mpris
 | 
			
		||||
import ".."
 | 
			
		||||
import "../.."
 | 
			
		||||
 | 
			
		||||
MouseArea {
 | 
			
		||||
	id: root
 | 
			
		||||
	hoverEnabled: true
 | 
			
		||||
 | 
			
		||||
	required property var bar;
 | 
			
		||||
	implicitHeight: column.implicitHeight + 10
 | 
			
		||||
 | 
			
		||||
	PersistentProperties {
 | 
			
		||||
		id: persist
 | 
			
		||||
		reloadableId: "MusicWidget";
 | 
			
		||||
		property bool widgetOpen: false;
 | 
			
		||||
 | 
			
		||||
		onReloaded: {
 | 
			
		||||
			rightclickMenu.snapOpacity(widgetOpen ? 1.0 : 0.0)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property alias widgetOpen: persist.widgetOpen;
 | 
			
		||||
 | 
			
		||||
	acceptedButtons: Qt.RightButton
 | 
			
		||||
	onClicked: 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));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	readonly property var activePlayer: MprisController.activePlayer
 | 
			
		||||
 | 
			
		||||
	Item {
 | 
			
		||||
		id: widget
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
		property real scaleMul: root.pressed || widgetOpen ? 100 : 1
 | 
			
		||||
		Behavior on scaleMul { SmoothedAnimation { velocity: 600 } }
 | 
			
		||||
		scale: scaleCurve.interpolate(scaleMul / 100, 1, (width - 6) / width)
 | 
			
		||||
 | 
			
		||||
		EasingCurve {
 | 
			
		||||
			id: scaleCurve
 | 
			
		||||
			curve.type: Easing.Linear
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		implicitHeight: column.implicitHeight + 10
 | 
			
		||||
 | 
			
		||||
		BackgroundArt {
 | 
			
		||||
			id: bkg
 | 
			
		||||
			anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
			function updateArt(reverse: bool) {
 | 
			
		||||
				this.setArt(MprisController.activeTrack.artUrl, reverse, false)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Component.onCompleted: this.updateArt(false);
 | 
			
		||||
 | 
			
		||||
			Connections {
 | 
			
		||||
				target: MprisController
 | 
			
		||||
 | 
			
		||||
				function onTrackChanged(reverse: bool) {
 | 
			
		||||
					bkg.updateArt(reverse);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ColumnLayout {
 | 
			
		||||
			id: column
 | 
			
		||||
 | 
			
		||||
			anchors {
 | 
			
		||||
				fill: parent
 | 
			
		||||
				margins: 5;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			ClickableIcon {
 | 
			
		||||
				Layout.fillWidth: true
 | 
			
		||||
				image: "root:icons/rewind.svg"
 | 
			
		||||
				implicitHeight: width
 | 
			
		||||
				scaleIcon: false
 | 
			
		||||
				baseMargin: 3
 | 
			
		||||
				enabled: MprisController.canGoPrevious;
 | 
			
		||||
				onClicked: MprisController.previous();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			ClickableIcon {
 | 
			
		||||
				Layout.fillWidth: true
 | 
			
		||||
				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();
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			ClickableIcon {
 | 
			
		||||
				Layout.fillWidth: true
 | 
			
		||||
				image: "root:icons/fast-forward.svg"
 | 
			
		||||
				implicitHeight: width
 | 
			
		||||
				scaleIcon: false
 | 
			
		||||
				baseMargin: 3
 | 
			
		||||
				enabled: MprisController.canGoNext;
 | 
			
		||||
				onClicked: MprisController.next();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		property var tooltip: TooltipItem {
 | 
			
		||||
			id: tooltip
 | 
			
		||||
			tooltip: bar.tooltip
 | 
			
		||||
			owner: root
 | 
			
		||||
 | 
			
		||||
			show: root.containsMouse && (activePlayer?.metadata["mpris:trackid"] ?? false)
 | 
			
		||||
 | 
			
		||||
			//implicitHeight: root.height - 10
 | 
			
		||||
			//implicitWidth: childrenRect.width
 | 
			
		||||
 | 
			
		||||
			Item {
 | 
			
		||||
				implicitWidth: 200
 | 
			
		||||
				implicitHeight: 100
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			/*Loader {
 | 
			
		||||
				active: tooltip.visible
 | 
			
		||||
 | 
			
		||||
				sourceComponent: ColumnLayout {
 | 
			
		||||
					height: root.height - 10
 | 
			
		||||
					RowLayout {
 | 
			
		||||
						Image {
 | 
			
		||||
							Layout.fillHeight: true
 | 
			
		||||
							source: mainPlayer.metadata["mpris:artUrl"] ?? ""
 | 
			
		||||
 | 
			
		||||
							cache: false
 | 
			
		||||
							fillMode: Image.PreserveAspectCrop
 | 
			
		||||
							sourceSize.width: height
 | 
			
		||||
							sourceSize.height: height
 | 
			
		||||
						}
 | 
			
		||||
						Label {
 | 
			
		||||
							text: mainPlayer.identity
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Slider {
 | 
			
		||||
						Layout.fillWidth: true
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}*/
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		property var rightclickMenu: TooltipItem {
 | 
			
		||||
			id: rightclickMenu
 | 
			
		||||
			tooltip: bar.tooltip
 | 
			
		||||
			owner: root
 | 
			
		||||
 | 
			
		||||
			isMenu: true
 | 
			
		||||
			show: widgetOpen
 | 
			
		||||
			onClose: widgetOpen = false
 | 
			
		||||
 | 
			
		||||
			// some very large covers take a sec to appear in the background,
 | 
			
		||||
			// so we'll try to preload them.
 | 
			
		||||
			preloadBackground: root.containsMouse
 | 
			
		||||
 | 
			
		||||
			backgroundComponent: BackgroundArt {
 | 
			
		||||
				id: popupBkg
 | 
			
		||||
				anchors.fill: parent
 | 
			
		||||
				renderHeight: rightclickMenu.implicitHeight
 | 
			
		||||
				renderWidth: rightclickMenu.implicitWidth
 | 
			
		||||
				blurRadius: 100
 | 
			
		||||
				blurSamples: 201
 | 
			
		||||
 | 
			
		||||
				overlay.color: "#80000000"
 | 
			
		||||
 | 
			
		||||
				Connections {
 | 
			
		||||
					target: MprisController
 | 
			
		||||
 | 
			
		||||
					function onTrackChanged(reverse: bool) {
 | 
			
		||||
						popupBkg.setArt(MprisController.activeTrack.artUrl, reverse, false);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Component.onCompleted: {
 | 
			
		||||
					setArt(MprisController.activeTrack.artUrl, false, true);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Loader {
 | 
			
		||||
				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;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					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 {
 | 
			
		||||
							id: playerSelector
 | 
			
		||||
							anchors.centerIn: parent
 | 
			
		||||
							width: Math.min(implicitWidth, playerSelectorContainment.width)
 | 
			
		||||
 | 
			
		||||
							RowLayout {
 | 
			
		||||
								Repeater {
 | 
			
		||||
									model: Mpris.players
 | 
			
		||||
 | 
			
		||||
									MouseArea {
 | 
			
		||||
										required property MprisPlayer modelData;
 | 
			
		||||
										readonly property bool selected: modelData == player;
 | 
			
		||||
 | 
			
		||||
										implicitWidth: childrenRect.width
 | 
			
		||||
										implicitHeight: childrenRect.height
 | 
			
		||||
 | 
			
		||||
										onClicked: MprisController.setActivePlayer(modelData);
 | 
			
		||||
 | 
			
		||||
										Rectangle {
 | 
			
		||||
											implicitWidth: 50
 | 
			
		||||
											implicitHeight: 50
 | 
			
		||||
											radius: 5
 | 
			
		||||
											color: selected ? "#20ceffff" : "transparent"
 | 
			
		||||
 | 
			
		||||
											Image {
 | 
			
		||||
												anchors.fill: parent
 | 
			
		||||
												anchors.margins: 5
 | 
			
		||||
												// lazy and wont always work, but good enough until a desktop entry impl.
 | 
			
		||||
												source: {
 | 
			
		||||
													const entry = DesktopEntries.byId(modelData.desktopEntry);
 | 
			
		||||
													console.log(`ent ${entry} id ${modelData.desktopEntry}`)
 | 
			
		||||
													if (!entry) return "image://icon/";
 | 
			
		||||
													return `image://icon/${entry.icon}`;
 | 
			
		||||
												}
 | 
			
		||||
												//asynchronous: true
 | 
			
		||||
 | 
			
		||||
												sourceSize.width: 50
 | 
			
		||||
												sourceSize.height: 50
 | 
			
		||||
												cache: false
 | 
			
		||||
											}
 | 
			
		||||
										}
 | 
			
		||||
									}
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Item {
 | 
			
		||||
						Layout.fillWidth: true
 | 
			
		||||
						Layout.bottomMargin: 10
 | 
			
		||||
 | 
			
		||||
						Label {
 | 
			
		||||
							anchors.centerIn: parent
 | 
			
		||||
							text: activePlayer.identity
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					SlideView {
 | 
			
		||||
						id: trackStack
 | 
			
		||||
						Layout.fillWidth: true
 | 
			
		||||
						implicitHeight: 400
 | 
			
		||||
 | 
			
		||||
						property bool reverse: false;
 | 
			
		||||
 | 
			
		||||
						Component.onCompleted: updateTrack(false, true);
 | 
			
		||||
 | 
			
		||||
						function updateTrack(reverse: bool, immediate: bool) {
 | 
			
		||||
							this.reverse = reverse;
 | 
			
		||||
							this.replace(
 | 
			
		||||
								trackComponent,
 | 
			
		||||
								{ track: MprisController.activeTrack },
 | 
			
		||||
								immediate
 | 
			
		||||
							)
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						property var trackComponent: Component {
 | 
			
		||||
							Flickable {
 | 
			
		||||
								id: flickable
 | 
			
		||||
								required property var track;
 | 
			
		||||
								// in most cases this is ready around the same time as the background,
 | 
			
		||||
								// but may take longer if the image is huge.
 | 
			
		||||
								readonly property bool svReady: img.status === Image.Ready;
 | 
			
		||||
								contentWidth: width + 1
 | 
			
		||||
								onDragEnded: {
 | 
			
		||||
									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
 | 
			
		||||
									height: flickable.height
 | 
			
		||||
 | 
			
		||||
									Item {
 | 
			
		||||
										Layout.fillWidth: true
 | 
			
		||||
										implicitHeight: 300//img.implicitHeight
 | 
			
		||||
										implicitWidth: img.implicitWidth
 | 
			
		||||
 | 
			
		||||
										Image {
 | 
			
		||||
											id: img;
 | 
			
		||||
											anchors.centerIn: parent;
 | 
			
		||||
											source: track.artUrl ?? "";
 | 
			
		||||
											//height: 300
 | 
			
		||||
											//fillMode: Image.PreserveAspectFit
 | 
			
		||||
											cache: false
 | 
			
		||||
											asynchronous: true
 | 
			
		||||
 | 
			
		||||
											sourceSize.height: 300
 | 
			
		||||
											sourceSize.width: 300
 | 
			
		||||
										}
 | 
			
		||||
									}
 | 
			
		||||
 | 
			
		||||
									Item {
 | 
			
		||||
										Layout.fillWidth: true
 | 
			
		||||
										Layout.topMargin: 20
 | 
			
		||||
 | 
			
		||||
										Label {
 | 
			
		||||
											anchors.centerIn: parent
 | 
			
		||||
											text: track.title
 | 
			
		||||
										}
 | 
			
		||||
									}
 | 
			
		||||
 | 
			
		||||
									Item {
 | 
			
		||||
										Layout.fillWidth: true
 | 
			
		||||
										Layout.topMargin: 20
 | 
			
		||||
 | 
			
		||||
										Label {
 | 
			
		||||
											anchors.centerIn: parent
 | 
			
		||||
											text: track.artist
 | 
			
		||||
										}
 | 
			
		||||
									}
 | 
			
		||||
 | 
			
		||||
									Item { Layout.fillHeight: true }
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						readonly property real fromPos: trackStack.width * (trackStack.reverse ? -1 : 1);
 | 
			
		||||
 | 
			
		||||
						// intentionally slightly faster than the background
 | 
			
		||||
						enterTransition: PropertyAnimation {
 | 
			
		||||
							property: "x"
 | 
			
		||||
							from: trackStack.fromPos
 | 
			
		||||
							to: 0;
 | 
			
		||||
							duration: 350;
 | 
			
		||||
							easing.type: Easing.OutExpo;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						exitTransition: PropertyAnimation {
 | 
			
		||||
							property: "x"
 | 
			
		||||
							to: target.x - trackStack.fromPos;
 | 
			
		||||
							duration: 350;
 | 
			
		||||
							easing.type: Easing.OutExpo;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					Item { Layout.fillHeight: true }
 | 
			
		||||
 | 
			
		||||
					Item {
 | 
			
		||||
						Layout.fillWidth: true
 | 
			
		||||
						implicitHeight: controlsRow.implicitHeight
 | 
			
		||||
 | 
			
		||||
						RowLayout {
 | 
			
		||||
							id: controlsRow
 | 
			
		||||
							anchors.centerIn: parent
 | 
			
		||||
 | 
			
		||||
							ClickableIcon {
 | 
			
		||||
								image: {
 | 
			
		||||
									switch (MprisController.loopState) {
 | 
			
		||||
									case MprisLoopState.None: return "root:icons/repeat-none.svg";
 | 
			
		||||
									case MprisLoopState.Playlist: return "root:icons/repeat-all.svg";
 | 
			
		||||
									case MprisLoopState.Track: return "root:icons/repeat-once.svg";
 | 
			
		||||
									}
 | 
			
		||||
								}
 | 
			
		||||
 | 
			
		||||
								implicitWidth: 50
 | 
			
		||||
								implicitHeight: width
 | 
			
		||||
								scaleIcon: false
 | 
			
		||||
								baseMargin: 3
 | 
			
		||||
								enabled: MprisController.loopSupported;
 | 
			
		||||
								onClicked: {
 | 
			
		||||
									let target = MprisLoopState.None;
 | 
			
		||||
									switch (MprisController.loopState) {
 | 
			
		||||
									case MprisLoopState.None: target = MprisLoopState.Playlist; break;
 | 
			
		||||
									case MprisLoopState.Playlist: target = MprisLoopState.Track; break;
 | 
			
		||||
									case MprisLoopState.Track: target = MprisLoopState.None; break;
 | 
			
		||||
									}
 | 
			
		||||
 | 
			
		||||
									MprisController.setLoopState(target);
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							ClickableIcon {
 | 
			
		||||
								image: "root:icons/rewind.svg"
 | 
			
		||||
								implicitWidth: 60
 | 
			
		||||
								implicitHeight: width
 | 
			
		||||
								scaleIcon: false
 | 
			
		||||
								baseMargin: 3
 | 
			
		||||
								enabled: MprisController.canGoPrevious;
 | 
			
		||||
								onClicked: MprisController.previous();
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							ClickableIcon {
 | 
			
		||||
								image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`;
 | 
			
		||||
								Layout.leftMargin: -10
 | 
			
		||||
								Layout.rightMargin: -10
 | 
			
		||||
								implicitWidth: 80
 | 
			
		||||
								implicitHeight: width
 | 
			
		||||
								scaleIcon: false
 | 
			
		||||
								enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay
 | 
			
		||||
								onClicked: {
 | 
			
		||||
									if (MprisController.isPlaying) MprisController.pause();
 | 
			
		||||
									else MprisController.play();
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							ClickableIcon {
 | 
			
		||||
								image: "root:icons/fast-forward.svg"
 | 
			
		||||
								implicitWidth: 60
 | 
			
		||||
								implicitHeight: width
 | 
			
		||||
								scaleIcon: false
 | 
			
		||||
								baseMargin: 3
 | 
			
		||||
								enabled: MprisController.canGoNext;
 | 
			
		||||
								onClicked: MprisController.next();
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							ClickableIcon {
 | 
			
		||||
								image: `root:icons/${MprisController.hasShuffle ? "shuffle" : "shuffle-off"}.svg`
 | 
			
		||||
								implicitWidth: 50
 | 
			
		||||
								implicitHeight: width
 | 
			
		||||
								scaleIcon: false
 | 
			
		||||
								enabled: MprisController.shuffleSupported;
 | 
			
		||||
								onClicked: MprisController.setShuffle(!MprisController.hasShuffle);
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					RowLayout {
 | 
			
		||||
						Label {
 | 
			
		||||
							Layout.preferredWidth: lengthLabel.implicitWidth
 | 
			
		||||
							text: timeStr(position)
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						MediaSlider {
 | 
			
		||||
							id: slider
 | 
			
		||||
							Layout.fillWidth: true
 | 
			
		||||
							property var bindSlider: true;
 | 
			
		||||
							enabled: player.canSeek
 | 
			
		||||
							from: 0
 | 
			
		||||
							to: player.length
 | 
			
		||||
 | 
			
		||||
							onPressedChanged: {
 | 
			
		||||
								if (!pressed) player.position = value;
 | 
			
		||||
								bindSlider = !pressed;
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							Binding {
 | 
			
		||||
								when: slider.bindSlider
 | 
			
		||||
								slider.value: player.position
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						Label {
 | 
			
		||||
							id: lengthLabel
 | 
			
		||||
							text: timeStr(length)
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -41,11 +41,8 @@ OverlayWidget {
 | 
			
		|||
 | 
			
		||||
					Layout.fillWidth: true
 | 
			
		||||
					implicitHeight: width
 | 
			
		||||
					Behavior on implicitHeight {
 | 
			
		||||
						SmoothedAnimation { velocity: 50 }
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					MouseArea {
 | 
			
		||||
					ClickableIcon {
 | 
			
		||||
						id: mouseArea
 | 
			
		||||
						anchors {
 | 
			
		||||
							top: parent.top
 | 
			
		||||
| 
						 | 
				
			
			@ -54,31 +51,10 @@ OverlayWidget {
 | 
			
		|||
						}
 | 
			
		||||
						width: height
 | 
			
		||||
 | 
			
		||||
						hoverEnabled: true
 | 
			
		||||
						acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
 | 
			
		||||
 | 
			
		||||
						Image {
 | 
			
		||||
							id: image
 | 
			
		||||
							anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
							anchors.margins: mouseArea.pressed || targetMenuOpen ? 3 : 0
 | 
			
		||||
							Behavior on anchors.margins { SmoothedAnimation { velocity: 30 } }
 | 
			
		||||
 | 
			
		||||
							source: modelData.icon
 | 
			
		||||
							sourceSize.width: width
 | 
			
		||||
							sourceSize.height: height
 | 
			
		||||
							cache: false
 | 
			
		||||
							visible: false
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						MultiEffect {
 | 
			
		||||
							anchors.fill: image
 | 
			
		||||
							source: image
 | 
			
		||||
 | 
			
		||||
							property real targetBrightness: mouseArea.pressed || targetMenuOpen ? -25 : mouseArea.containsMouse ? 75 : 0
 | 
			
		||||
							Behavior on targetBrightness { SmoothedAnimation { velocity: 600 } }
 | 
			
		||||
							brightness: targetBrightness / 1000
 | 
			
		||||
						}
 | 
			
		||||
						image: modelData.icon
 | 
			
		||||
						showPressed: targetMenuOpen
 | 
			
		||||
 | 
			
		||||
						onClicked: event => {
 | 
			
		||||
							event.accepted = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -99,42 +75,30 @@ OverlayWidget {
 | 
			
		|||
						}
 | 
			
		||||
 | 
			
		||||
						property var tooltip: TooltipItem {
 | 
			
		||||
							anchors.fill: parent
 | 
			
		||||
							tooltip: bar.widgetSurface.tooltip
 | 
			
		||||
							owner: image
 | 
			
		||||
							tooltip: bar.tooltip
 | 
			
		||||
							owner: mouseArea
 | 
			
		||||
 | 
			
		||||
							show: mouseArea.containsMouse
 | 
			
		||||
							implicitWidth: tooltipText.implicitWidth
 | 
			
		||||
							implicitHeight: tooltipText.implicitHeight
 | 
			
		||||
 | 
			
		||||
							Text {
 | 
			
		||||
								id: tooltipText
 | 
			
		||||
								anchors.fill: parent
 | 
			
		||||
								text: modelData.tooltipTitle != "" ? modelData.tooltipTitle : modelData.id
 | 
			
		||||
								color: "white"
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						property var rightclickMenu: TooltipItem {
 | 
			
		||||
							anchors.fill: parent
 | 
			
		||||
							tooltip: bar.widgetSurface.tooltip
 | 
			
		||||
							owner: image
 | 
			
		||||
							tooltip: bar.tooltip
 | 
			
		||||
							owner: mouseArea
 | 
			
		||||
 | 
			
		||||
							isMenu: true
 | 
			
		||||
							show: targetMenuOpen && menu.showChildren
 | 
			
		||||
							animateSize: !rightclickItems.animating
 | 
			
		||||
							implicitHeight: rightclickItems.implicitHeight
 | 
			
		||||
							implicitWidth: rightclickItems.implicitWidth
 | 
			
		||||
 | 
			
		||||
							onVisibleChanged: {
 | 
			
		||||
								if (!visible) targetMenuOpen = false;
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							onClose: targetMenuOpen = false;
 | 
			
		||||
 | 
			
		||||
							MenuItemList {
 | 
			
		||||
								id: rightclickItems
 | 
			
		||||
								anchors.fill: parent
 | 
			
		||||
								items: menu == null ? [] : menu.children
 | 
			
		||||
								onClose: targetMenuOpen = false;
 | 
			
		||||
							}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										114
									
								
								modules/user/modules/quickshell/shell/bar/workspaces/Widget.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,114 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
import ".."
 | 
			
		||||
import "root:."
 | 
			
		||||
 | 
			
		||||
MouseArea {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property var bar;
 | 
			
		||||
	required property int wsBaseIndex;
 | 
			
		||||
	property int wsCount: 10;
 | 
			
		||||
	property bool hideWhenEmpty: false;
 | 
			
		||||
	implicitHeight: column.implicitHeight + 10;
 | 
			
		||||
 | 
			
		||||
	acceptedButtons: Qt.NoButton
 | 
			
		||||
 | 
			
		||||
	onWheel: event => {
 | 
			
		||||
		event.accepted = true;
 | 
			
		||||
		const step = -Math.sign(event.angleDelta.y);
 | 
			
		||||
		const targetWs = currentIndex + step;
 | 
			
		||||
 | 
			
		||||
		if (targetWs >= wsBaseIndex && targetWs < wsBaseIndex + wsCount) {
 | 
			
		||||
			Hyprland.dispatch(`workspace ${targetWs}`)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen);
 | 
			
		||||
	property int currentIndex: 0;
 | 
			
		||||
	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);
 | 
			
		||||
 | 
			
		||||
	ColumnLayout {
 | 
			
		||||
		id: column
 | 
			
		||||
		spacing: 0
 | 
			
		||||
		anchors {
 | 
			
		||||
			fill: parent;
 | 
			
		||||
			topMargin: 0;
 | 
			
		||||
			margins: 5;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Repeater {
 | 
			
		||||
			model: 10
 | 
			
		||||
 | 
			
		||||
			MouseArea {
 | 
			
		||||
				id: wsItem
 | 
			
		||||
				onPressed: Hyprland.dispatch(`workspace ${wsIndex}`);
 | 
			
		||||
 | 
			
		||||
				Layout.fillWidth: true
 | 
			
		||||
				implicitHeight: 15
 | 
			
		||||
 | 
			
		||||
				required property int index;
 | 
			
		||||
				property int wsIndex: wsBaseIndex + index;
 | 
			
		||||
				property HyprlandWorkspace workspace: null;
 | 
			
		||||
				property bool exists: workspace != null;
 | 
			
		||||
				property bool active: (monitor?.activeWorkspace ?? false) && monitor.activeWorkspace == workspace;
 | 
			
		||||
 | 
			
		||||
				onActiveChanged: {
 | 
			
		||||
					if (active) root.currentIndex = wsIndex;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				onExistsChanged: {
 | 
			
		||||
					root.existsCount += exists ? 1 : -1;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Connections {
 | 
			
		||||
					target: root
 | 
			
		||||
 | 
			
		||||
					function onWorkspaceAdded(workspace: HyprlandWorkspace) {
 | 
			
		||||
						if (workspace.id == wsItem.wsIndex) {
 | 
			
		||||
							wsItem.workspace = workspace;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				property real animActive: active ? 100 : 0
 | 
			
		||||
				Behavior on animActive { NumberAnimation { duration: 100 } }
 | 
			
		||||
 | 
			
		||||
				property real animExists: exists ? 100 : 0
 | 
			
		||||
				Behavior on animExists { NumberAnimation { duration: 100 } }
 | 
			
		||||
 | 
			
		||||
				Rectangle {
 | 
			
		||||
					anchors.centerIn: parent
 | 
			
		||||
					height: 10
 | 
			
		||||
					width: parent.width
 | 
			
		||||
					scale: 1 + animActive * 0.003
 | 
			
		||||
					radius: height / 2
 | 
			
		||||
					border.color: ShellGlobals.colors.widgetOutline
 | 
			
		||||
					border.width: 1
 | 
			
		||||
					color: ShellGlobals.interpolateColors(animExists * 0.01, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Connections {
 | 
			
		||||
		target: Hyprland.workspaces
 | 
			
		||||
 | 
			
		||||
		function onObjectInsertedPost(workspace) {
 | 
			
		||||
			root.workspaceAdded(workspace);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Component.onCompleted: {
 | 
			
		||||
		Hyprland.workspaces.values.forEach(workspace => {
 | 
			
		||||
			root.workspaceAdded(workspace)
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 445 B  | 
							
								
								
									
										4
									
								
								modules/user/modules/quickshell/shell/icons/icons.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
phosphor icons
 | 
			
		||||
shuffle-off is an edit of shuffle.
 | 
			
		||||
play is edited.
 | 
			
		||||
repeat-none is an edit of repeat.
 | 
			
		||||
							
								
								
									
										1
									
								
								modules/user/modules/quickshell/shell/icons/monitor.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 353 B  | 
							
								
								
									
										1
									
								
								modules/user/modules/quickshell/shell/icons/pause.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 386 B  | 
							
								
								
									
										17
									
								
								modules/user/modules/quickshell/shell/icons/play.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg
 | 
			
		||||
   width="32"
 | 
			
		||||
   height="32"
 | 
			
		||||
   fill="#ffffff"
 | 
			
		||||
   viewBox="0 0 256 256"
 | 
			
		||||
   data-darkreader-inline-fill=""
 | 
			
		||||
   version="1.1"
 | 
			
		||||
   id="svg1"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg">
 | 
			
		||||
  <defs
 | 
			
		||||
     id="defs1" />
 | 
			
		||||
  <path
 | 
			
		||||
     d="m 215.99998,128 a 15.74,15.74 0 0 1 -7.6,13.51 L 64.319977,229.65 a 16,16 0 0 1 -16.2,0.3 15.86,15.86 0 0 1 -8.12,-13.82 V 39.87 a 15.86,15.86 0 0 1 8.12,-13.82 16,16 0 0 1 16.2,0.3 l 144.080003,88.14 a 15.74,15.74 0 0 1 7.6,13.51 z"
 | 
			
		||||
     id="path1" />
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 580 B  | 
| 
						 | 
				
			
			@ -0,0 +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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 521 B  | 
							
								
								
									
										17
									
								
								modules/user/modules/quickshell/shell/icons/repeat-none.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg
 | 
			
		||||
   width="32"
 | 
			
		||||
   height="32"
 | 
			
		||||
   fill="#ffffff"
 | 
			
		||||
   viewBox="0 0 256 256"
 | 
			
		||||
   data-darkreader-inline-fill=""
 | 
			
		||||
   version="1.1"
 | 
			
		||||
   id="svg1"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg">
 | 
			
		||||
  <defs
 | 
			
		||||
     id="defs1" />
 | 
			
		||||
  <path
 | 
			
		||||
     id="path1"
 | 
			
		||||
     d="M 200.25,31.984375 C 196.06443,31.879199 191.99588,35.096466 192,40 V 56 H 96 c -39.746185,0.04409 -71.955911,32.253815 -72,72 0,4.41828 3.581726,8 8,8 4.418274,0 8,-3.58172 8,-8 0.03307,-30.914207 25.085793,-55.96693 56,-56 h 96 v 16 c -0.006,7.132413 8.61568,10.702436 13.65625,5.65625 l 24,-24 c 3.12839,-3.12491 3.12839,-8.18759 0,-11.3125 l -24,-24 C 204.08107,32.766817 202.15253,32.032182 200.25,31.984375 Z M 192.92188,153.6875 A 38.310444,38.310444 0 0 0 154.60938,192 38.310444,38.310444 0 0 0 192.92188,230.3125 38.310444,38.310444 0 0 0 231.21875,192 38.310444,38.310444 0 0 0 192.92188,153.6875 Z" />
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 957 B  | 
| 
						 | 
				
			
			@ -0,0 +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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 627 B  | 
							
								
								
									
										1
									
								
								modules/user/modules/quickshell/shell/icons/rewind.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 415 B  | 
							
								
								
									
										17
									
								
								modules/user/modules/quickshell/shell/icons/shuffle-off.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg
 | 
			
		||||
   width="32"
 | 
			
		||||
   height="32"
 | 
			
		||||
   fill="#ffffff"
 | 
			
		||||
   viewBox="0 0 256 256"
 | 
			
		||||
   data-darkreader-inline-fill=""
 | 
			
		||||
   version="1.1"
 | 
			
		||||
   id="svg1"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg">
 | 
			
		||||
  <defs
 | 
			
		||||
     id="defs1" />
 | 
			
		||||
  <path
 | 
			
		||||
     d="m 237.66,178.34 c 3.12839,3.12491 3.12839,8.19509 0,11.32 l -24,24 C 208.61942,218.70619 199.99439,215.13242 200,208 V 192 H 32 c -4.418281,0 -7.999997,-3.58172 -7.999997,-8 0,-4.41828 3.581716,-8 7.999997,-8 h 168 v -16 c -0.006,-7.13242 8.61942,-10.70619 13.66,-5.66 z M 24,72 c 0,3.6 3.6,8 8,8 h 168 v 16 c -0.006,7.13242 8.61942,10.70619 13.66,5.66 l 24,-24 c 3.12839,-3.124913 3.12839,-8.195087 0,-11.32 l -24,-24 C 208.61942,37.293809 199.99439,40.86758 200,48 V 64 H 32 c -4.4,0 -8,3.6 -8,8 z"
 | 
			
		||||
     id="path1" />
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 847 B  | 
							
								
								
									
										1
									
								
								modules/user/modules/quickshell/shell/icons/shuffle.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 787 B  | 
| 
						 | 
				
			
			@ -0,0 +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="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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 335 B  | 
| 
						 | 
				
			
			@ -0,0 +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="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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 339 B  | 
							
								
								
									
										149
									
								
								modules/user/modules/quickshell/shell/lock/Controller.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,149 @@
 | 
			
		|||
pragma Singleton
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Wayland
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
import ".."
 | 
			
		||||
import "../.."
 | 
			
		||||
 | 
			
		||||
Singleton {
 | 
			
		||||
	id: root
 | 
			
		||||
	function init() {}
 | 
			
		||||
 | 
			
		||||
	property bool locked: false;
 | 
			
		||||
	onLockedChanged: {
 | 
			
		||||
		if (locked) {
 | 
			
		||||
			lockContextLoader.active = true;
 | 
			
		||||
			lock.locked = true;
 | 
			
		||||
		} else {
 | 
			
		||||
			lockClearTimer.start();
 | 
			
		||||
			workspaceUnlockAnimation();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Timer {
 | 
			
		||||
		id: lockClearTimer
 | 
			
		||||
		interval: 600
 | 
			
		||||
		onTriggered: {
 | 
			
		||||
			lock.locked = false;
 | 
			
		||||
			lockContextLoader.active = false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	property var oldWorkspaces: ({});
 | 
			
		||||
 | 
			
		||||
	function workspaceLockAnimation() {
 | 
			
		||||
		const focusedMonitor = Hyprland.focusedMonitor.id;
 | 
			
		||||
 | 
			
		||||
		Hyprland.monitors.values.forEach(monitor => {
 | 
			
		||||
			if (monitor.activeWorkspace) {
 | 
			
		||||
				root.oldWorkspaces[monitor.id] = monitor.activeWorkspace.id;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Hyprland.dispatch(`workspace name:lock_${monitor.name}`);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		Hyprland.dispatch(`focusmonitor ${focusedMonitor}`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function workspaceUnlockAnimation() {
 | 
			
		||||
		const focusedMonitor = Hyprland.focusedMonitor.id;
 | 
			
		||||
 | 
			
		||||
		Hyprland.monitors.values.forEach(monitor => {
 | 
			
		||||
			const workspace = root.oldWorkspaces[monitor.id];
 | 
			
		||||
			if (workspace) Hyprland.dispatch(`workspace ${workspace}`);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		Hyprland.dispatch(`focusmonitor ${focusedMonitor}`);
 | 
			
		||||
 | 
			
		||||
		root.oldWorkspaces = ({});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Shortcut {
 | 
			
		||||
		name: "lock"
 | 
			
		||||
		onPressed: {
 | 
			
		||||
			if (root.locked) root.locked = false;
 | 
			
		||||
			else root.locked = true;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	LazyLoader {
 | 
			
		||||
		id: lockContextLoader
 | 
			
		||||
 | 
			
		||||
		LockContext {
 | 
			
		||||
			onUnlocked: root.locked = false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	WlSessionLock {
 | 
			
		||||
		id: lock
 | 
			
		||||
 | 
			
		||||
		onSecureChanged: {
 | 
			
		||||
			if (secure) {
 | 
			
		||||
				Qt.callLater(() => root.workspaceLockAnimation());
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		WlSessionLockSurface {
 | 
			
		||||
			id: lockSurface
 | 
			
		||||
			color: "transparent"
 | 
			
		||||
 | 
			
		||||
			// Ensure nothing spawns in the workspace behind the transparent lock
 | 
			
		||||
			// by filling in the background after animations complete.
 | 
			
		||||
			Rectangle {
 | 
			
		||||
				anchors.fill: parent
 | 
			
		||||
				color: "gray"
 | 
			
		||||
				visible: backgroundImage.visible
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			BackgroundImage {
 | 
			
		||||
				id: backgroundImage
 | 
			
		||||
				anchors.fill: parent
 | 
			
		||||
				screen: lockSurface.screen
 | 
			
		||||
				visible: !lockAnim.running
 | 
			
		||||
				asynchronous: true
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			LockContent {
 | 
			
		||||
				id: lockContent
 | 
			
		||||
				context: lockContextLoader.item;
 | 
			
		||||
 | 
			
		||||
				visible: false
 | 
			
		||||
				width: lockSurface.width
 | 
			
		||||
				height: lockSurface.height
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			NumberAnimation {
 | 
			
		||||
				id: lockAnim
 | 
			
		||||
				target: lockContent
 | 
			
		||||
				property: "y"
 | 
			
		||||
				to: 0
 | 
			
		||||
				duration: 600
 | 
			
		||||
				easing.type: Easing.BezierSpline
 | 
			
		||||
				easing.bezierCurve: [0.0, 0.75, 0.15, 1.0, 1.0, 1.0]
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			onVisibleChanged: {
 | 
			
		||||
				if (visible) {
 | 
			
		||||
					lockContent.y = -lockSurface.height
 | 
			
		||||
					console.log(`y ${lockContent.y}`)
 | 
			
		||||
					lockContent.visible = true;
 | 
			
		||||
					lockAnim.running = true;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Connections {
 | 
			
		||||
				target: root
 | 
			
		||||
 | 
			
		||||
				function onLockedChanged() {
 | 
			
		||||
					if (!locked) {
 | 
			
		||||
						lockAnim.to = -lockSurface.height
 | 
			
		||||
						lockAnim.running = true;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								modules/user/modules/quickshell/shell/lock/LockButton.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import "root:."
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	id: root
 | 
			
		||||
	implicitHeight: 75
 | 
			
		||||
	implicitWidth: showProgress * 0.1
 | 
			
		||||
 | 
			
		||||
	signal clicked();
 | 
			
		||||
	property string icon;
 | 
			
		||||
	property bool show: true;
 | 
			
		||||
 | 
			
		||||
	property int showProgress: show ? 1000 : 0
 | 
			
		||||
	Behavior on showProgress {
 | 
			
		||||
		NumberAnimation {
 | 
			
		||||
			duration: 100
 | 
			
		||||
			easing.type: Easing.OutQuad
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	MouseArea {
 | 
			
		||||
		id: mouseArea
 | 
			
		||||
		implicitWidth: 75
 | 
			
		||||
		implicitHeight: 75
 | 
			
		||||
		hoverEnabled: true
 | 
			
		||||
 | 
			
		||||
		y: -(height + 30) * (1.0 - showProgress * 0.001)
 | 
			
		||||
		x: 12.5 - 50 * (1.0 - showProgress * 0.001)
 | 
			
		||||
 | 
			
		||||
		Component.onCompleted: clicked.connect(root.clicked);
 | 
			
		||||
 | 
			
		||||
		Rectangle {
 | 
			
		||||
			anchors.fill: parent
 | 
			
		||||
			radius: 5
 | 
			
		||||
			color: ShellGlobals.interpolateColors(hoverColorInterp * 0.001, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
 | 
			
		||||
			border.width: 1
 | 
			
		||||
			border.color: ShellGlobals.colors.widgetOutline
 | 
			
		||||
 | 
			
		||||
			property int hoverColorInterp: mouseArea.containsMouse || mouseArea.pressed ? 1000 : 0;
 | 
			
		||||
			Behavior on hoverColorInterp { SmoothedAnimation { velocity: 10000 } }
 | 
			
		||||
 | 
			
		||||
			Image {
 | 
			
		||||
				anchors.fill: parent
 | 
			
		||||
				anchors.margins: 15
 | 
			
		||||
 | 
			
		||||
				source: root.icon
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										113
									
								
								modules/user/modules/quickshell/shell/lock/LockContent.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,113 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import ".."
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
	id: root
 | 
			
		||||
	required property LockContext context;
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
		implicitHeight: 6
 | 
			
		||||
		implicitWidth: 800
 | 
			
		||||
		radius: height / 2
 | 
			
		||||
		color: ShellGlobals.colors.widget
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ColumnLayout {
 | 
			
		||||
		implicitWidth: sep.implicitWidth
 | 
			
		||||
		anchors.horizontalCenter: parent.horizontalCenter
 | 
			
		||||
		anchors.bottom: sep.top
 | 
			
		||||
		spacing: 0
 | 
			
		||||
 | 
			
		||||
		Text {
 | 
			
		||||
			id: timeText
 | 
			
		||||
			Layout.alignment: Qt.AlignHCenter
 | 
			
		||||
 | 
			
		||||
			font {
 | 
			
		||||
				pointSize: 120
 | 
			
		||||
				hintingPreference: Font.PreferFullHinting
 | 
			
		||||
				family: "Noto Sans"
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			color: "white"
 | 
			
		||||
			renderType: Text.NativeRendering
 | 
			
		||||
 | 
			
		||||
			text: {
 | 
			
		||||
				const hours = ShellGlobals.time.getHours().toString().padStart(2, '0');
 | 
			
		||||
				const minutes = ShellGlobals.time.getMinutes().toString().padStart(2, '0');
 | 
			
		||||
				return `${hours}:${minutes}`;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Item {
 | 
			
		||||
			Layout.alignment: Qt.AlignHCenter
 | 
			
		||||
			implicitHeight: childrenRect.height * focusAnim
 | 
			
		||||
			implicitWidth: sep.implicitWidth
 | 
			
		||||
			clip: true
 | 
			
		||||
 | 
			
		||||
			TextInput {
 | 
			
		||||
				id: textBox
 | 
			
		||||
				focus: true
 | 
			
		||||
				width: parent.width
 | 
			
		||||
 | 
			
		||||
				color: enabled ?
 | 
			
		||||
					root.context.failed ? "#ffa0a0" : "white"
 | 
			
		||||
					: "#80ffffff";
 | 
			
		||||
 | 
			
		||||
				font.pointSize: 24
 | 
			
		||||
				horizontalAlignment: TextInput.AlignHCenter
 | 
			
		||||
				echoMode: TextInput.Password
 | 
			
		||||
				inputMethodHints: Qt.ImhSensitiveData
 | 
			
		||||
 | 
			
		||||
				onTextChanged: root.context.currentText = text;
 | 
			
		||||
 | 
			
		||||
				Window.onActiveChanged: {
 | 
			
		||||
					if (Window.active) {
 | 
			
		||||
						text = root.context.currentText;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				onAccepted: {
 | 
			
		||||
					if (text != "") root.context.tryUnlock();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				enabled: !root.context.isUnlocking;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								modules/user/modules/quickshell/shell/lock/LockContext.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,57 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Io
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
import Quickshell.Services.Mpris
 | 
			
		||||
 | 
			
		||||
Scope {
 | 
			
		||||
	id: root
 | 
			
		||||
	signal unlocked();
 | 
			
		||||
	property string currentText: "";
 | 
			
		||||
	readonly property alias isUnlocking: pamtester.running;
 | 
			
		||||
	property bool failed: false;
 | 
			
		||||
 | 
			
		||||
	onCurrentTextChanged: failed = false;
 | 
			
		||||
 | 
			
		||||
	readonly property bool mediaPlaying: Mpris.players.values.some(player => {
 | 
			
		||||
		return player.playbackState === MprisPlaybackState.Playing && player.canPause;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	function pauseMedia() {
 | 
			
		||||
		Mpris.players.values.forEach(player => {
 | 
			
		||||
			if (player.playbackState === MprisPlaybackState.Playing && player.canPause) {
 | 
			
		||||
				player.playbackState = MprisPlaybackState.Paused;
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function dpms() {
 | 
			
		||||
		Hyprland.dispatch(`dpms`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Process {
 | 
			
		||||
		id: pamtester
 | 
			
		||||
		property bool failed: true
 | 
			
		||||
 | 
			
		||||
		command: ["pamtester", "login", Quickshell.env("USER"), "authenticate"]
 | 
			
		||||
 | 
			
		||||
		onStarted: this.write(`${currentText}\n`)
 | 
			
		||||
 | 
			
		||||
		stdout: SplitParser {
 | 
			
		||||
			// fails go to stderr
 | 
			
		||||
			onRead: pamtester.failed = false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		onExited: {
 | 
			
		||||
			if (failed) {
 | 
			
		||||
				root.failed = true;
 | 
			
		||||
			} else {
 | 
			
		||||
				root.unlocked();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function tryUnlock() {
 | 
			
		||||
		pamtester.running = true;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -99,6 +99,7 @@ Scope {
 | 
			
		|||
				exclusionMode: ExclusionMode.Ignore
 | 
			
		||||
				WlrLayershell.namespace: "shell:screenshot"
 | 
			
		||||
				WlrLayershell.layer: WlrLayer.Overlay
 | 
			
		||||
				WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
 | 
			
		||||
 | 
			
		||||
				anchors {
 | 
			
		||||
					top: true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||