ported some quickshell stuff from rivendell-hyprdots

This commit is contained in:
Tomas Rivera
2026-03-10 16:22:35 +01:00
parent 4b442dd121
commit b9e3f35abe
80 changed files with 4462 additions and 9 deletions
+166
View File
@@ -0,0 +1,166 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import QtQuick.Layouts
import Quickshell.Wayland
import "mpris" as Mpris
import "systray" as SysTray
import ".."
PanelWindow {
id: root
property var modelData
readonly property var tooltip: tooltip
readonly property real tooltipYOffset: root.height + 2
Tooltip {
id: tooltip
bar: root
}
function boundedY(targetY: real, height: real): real {
return Math.max(bar.anchors.topMargin + height, Math.min(bar.height + bar.anchors.topMargin - height, targetY));
}
function boundedX(targetX: real, width: real): real {
return Math.max(bar.anchors.leftMargin + width, Math.min(bar.width + bar.anchors.leftMargin - width, targetX));
}
WlrLayershell.namespace: "shell:bar"
anchors {
top: true
left: true
right: true
}
exclusiveZone: 48
implicitHeight: 72
color: "transparent"
mask: barRegion
Region {
id: barRegion
width: bar.width
height: bar.height
}
Item {
id: bar
anchors {
top: parent.top
left: parent.left
right: parent.right
}
implicitHeight: 48
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
implicitWidth: left.width + left.anchors.leftMargin + left.anchors.rightMargin
implicitHeight: left.height
color: "#3B253F"
Rectangle {
z: -1
anchors.fill: parent
color: "#BE850E"
anchors.margins: -4
}
Image {
width: 16 * 6
height: 4 * 6
anchors.top: parent.bottom
anchors.left: parent.left
source: "../assets/bar-decor.png"
smooth: false
}
RowLayout {
id: left
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
leftMargin: 7
rightMargin: 7
}
spacing: 12
Launcher {}
Workspaces {
bar: root
}
SysTray.SysTray {
bar: root
}
}
}
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
}
implicitWidth: right.width + right.anchors.leftMargin + right.anchors.rightMargin
implicitHeight: right.height
color: "#3B253F"
Rectangle {
z: -1
anchors.fill: parent
color: "#BE850E"
anchors.margins: -4
}
Image {
width: 16 * 6
height: 4 * 6
anchors.top: parent.bottom
anchors.right: parent.right
source: "../assets/bar-decor.png"
smooth: false
mirror: true
}
RowLayout {
id: right
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
leftMargin: 7
rightMargin: 7
}
spacing: 12
Time {}
Volume {
bar: root
}
Mpris.Players {
window: root
bar: root
}
Theme {}
Power {
id: power
bar: root
}
}
}
}
}
+48
View File
@@ -0,0 +1,48 @@
import QtQuick
import QtQuick.Effects
FullwidthMouseArea {
id: root
property bool showPressed: mouseArea.pressed
property real baseMargin: 0
property bool directScale: false
readonly property Item contentItem: mContentItem
default property alias contentItemData: mContentItem.data
property real targetBrightness: root.showPressed ? -25 : root.mouseArea.containsMouse && root.enabled ? 75 : 0
Behavior on targetBrightness {
SmoothedAnimation {
velocity: 600
}
}
property real targetMargins: root.showPressed ? 3 : 0
Behavior on targetMargins {
SmoothedAnimation {
velocity: 25
}
}
hoverEnabled: true
Item {
id: mContentItem
anchors.fill: parent
anchors.margins: root.baseMargin + (root.directScale ? 0 : root.targetMargins)
scale: root.directScale ? (width - root.targetMargins * 2) / width : 1.0
opacity: root.enabled ? 1.0 : 0.5
Behavior on opacity {
SmoothedAnimation {
velocity: 5
}
}
layer.enabled: root.targetBrightness != 0
layer.effect: MultiEffect {
brightness: root.targetBrightness / 1000
}
}
}
+20
View File
@@ -0,0 +1,20 @@
import QtQuick
BarButton {
id: root
property alias image: img.source
property alias mirror: img.mirror
implicitWidth: 24
implicitHeight: 24
Image {
id: img
anchors.fill: parent
smooth: false
scale: root.containsMouse ? 0.9 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
}
}
@@ -0,0 +1,53 @@
import QtQuick
Item {
id: root
property bool fillWindowWidth: false
property real extraVerticalMargin: 0
property alias mouseArea: mouseArea
property alias hoverEnabled: mouseArea.hoverEnabled
property alias acceptedButtons: mouseArea.acceptedButtons
property alias pressedButtons: mouseArea.pressedButtons
property alias containsMouse: mouseArea.containsMouse
property alias isPressed: mouseArea.pressed
signal clicked(event: MouseEvent)
signal entered
signal exited
signal pressed(event: MouseEvent)
signal released(event: MouseEvent)
signal wheel(event: WheelEvent)
MouseArea {
id: mouseArea
anchors {
fill: parent
// not much point in finding exact values
leftMargin: root.fillWindowWidth ? -50 : 0
rightMargin: root.fillWindowWidth ? -50 : 0
topMargin: -root.extraVerticalMargin
bottomMargin: -root.extraVerticalMargin
}
Component.onCompleted: {
this.clicked.connect(root.clicked);
//this.entered.connect(root.entered);
//this.exited.connect(root.exited);
//this.pressed.connect(root.pressed);
this.released.connect(root.released);
//this.wheel.connect(root.wheel);
}
// for some reason MouseArea.pressed is both a prop and signal so connect doesn't work
onPressed: event => root.pressed(event)
// connecting to onwheel seems to implicitly accept it. undo that.
onWheel: event => {
event.accepted = false;
root.wheel(event);
}
}
}
+39
View File
@@ -0,0 +1,39 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import QtQuick.Layouts
import "../launcher" as Launcher
MouseArea {
id: root
implicitWidth: 26
implicitHeight: 26
acceptedButtons: Qt.LeftButton
Image {
id: img
anchors.fill: parent
source: "../assets/chi-ro.png"
smooth: false
scale: Launcher.Controller.isOpen ? 0.8 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
}
MultiEffect {
source: img
anchors.fill: img
blur: 1.0
brightness: 0.3
blurEnabled: true
}
onPressed: () => {
Launcher.Controller.isOpen = true;
}
}
+43
View File
@@ -0,0 +1,43 @@
import QtQuick
Item {
id: root
property alias mouseArea: mouseArea
property alias hoverEnabled: mouseArea.hoverEnabled
property alias acceptedButtons: mouseArea.acceptedButtons
property alias pressedButtons: mouseArea.pressedButtons
property alias containsMouse: mouseArea.containsMouse
property alias isPressed: mouseArea.pressed
signal clicked(event: MouseEvent)
signal entered
signal exited
signal pressed(event: MouseEvent)
signal released(event: MouseEvent)
signal wheel(event: WheelEvent)
MouseArea {
id: mouseArea
anchors.fill: parent
Component.onCompleted: {
this.clicked.connect(root.clicked);
//this.entered.connect(root.entered);
//this.exited.connect(root.exited);
//this.pressed.connect(root.pressed);
this.released.connect(root.released);
//this.wheel.connect(root.wheel);
}
// For some reason MouseArea.pressed is both a prop and signal so connect doesn't work
onPressed: event => root.pressed(event)
// Connecting to onwheel seems to implicitly accept it. undo that.
onWheel: event => {
event.accepted = false;
root.wheel(event);
}
}
}
+54
View File
@@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Services.UPower
import Qt5Compat.GraphicalEffects
Item {
id: root
required property var bar
readonly property var chargeState: UPower.displayDevice.state
readonly property bool isCharging: chargeState == UPowerDeviceState.Charging
readonly property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge
readonly property real percentage: UPower.displayDevice.percentage
readonly property bool isLow: percentage <= 0.20
readonly property bool isLaptop: UPowerDevice.isLaptopBattery
width: 32
height: 32
Image {
id: mask
source: "../assets/battery-mask.png"
smooth: false
anchors.fill: parent
visible: false
}
Rectangle {
id: progressBar
anchors.fill: parent
color: "black"
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: root.isLaptop ? root.percentage * parent.height : parent.height
color: root.isLaptop ? (root.isLow ? "red" : "#EEA939") : "#EEA939"
}
visible: false
}
OpacityMask {
id: pBarMask
anchors.fill: progressBar
source: progressBar
maskSource: mask
}
MultiEffect {
source: pBarMask
anchors.fill: pBarMask
brightness: root.isCharging ? 0.3 : 0
blur: 1.0
blurEnabled: root.isCharging
}
}
+112
View File
@@ -0,0 +1,112 @@
import QtQuick
// kind of like a lighter StackView which handles replacement better.
Item {
id: root
property Component enterTransition: XAnimator {
from: root.width
duration: 3000
}
property Component exitTransition: XAnimator {
to: target.x - target.width
duration: 3000
}
property bool animate: this.visible;
onAnimateChanged: {
if (!this.animate) this.finishAnimations();
}
property Component itemComponent: SlideViewItem {}
property SlideViewItem activeItem: null;
property Item pendingItem: null;
property bool pendingNoAnim: false;
property list<SlideViewItem> removingItems;
readonly property bool animating: activeItem?.activeAnimation != null
function replace(component: Component, defaults: var, noanim: bool) {
this.pendingNoAnim = noanim;
if (component) {
const props = defaults ?? {};
props.parent = null;
props.width = Qt.binding(() => this.width);
props.height = Qt.binding(() => this.height);
const item = component.createObject(this, props);
if (pendingItem) pendingItem.destroy();
pendingItem = item;
const ready = item?.svReady ?? true;
if (ready) finishPending();
} else {
finishPending(); // remove
}
}
Connections {
target: pendingItem
function onSvReadyChanged() {
if (pendingItem.svReady) {
root.finishPending();
}
}
}
function finishPending() {
const noanim = this.pendingNoAnim || !this.animate;
if (this.activeItem) {
if (noanim) {
this.activeItem.destroyAll();
this.activeItem = null;
} else {
removingItems.push(this.activeItem);
this.activeItem.animationCompleted.connect(item => root.removeItem(item));
this.activeItem.stopIfRunning();
this.activeItem.createAnimation(exitTransition);
this.activeItem = null;
}
}
if (!this.animate) finishAnimations();
if (this.pendingItem) {
pendingItem.parent = this;
this.activeItem = itemComponent.createObject(this, { item: this.pendingItem });
this.pendingItem = null;
if (!noanim) {
this.activeItem.createAnimation(enterTransition);
}
}
}
function removeItem(item: SlideViewItem) {
item.destroyAll();
for (const i = 0; i !== this.removingItems.length; i++) {
if (this.removingItems[i] === item) {
removingItems.splice(i, 1);
break;
}
}
}
function finishAnimations() {
this.removingItems.forEach(item => item.destroyAll())
this.removingItems = [];
if (this.activeItem) {
this.activeItem.finishIfRunning();
}
}
Component.onDestruction: {
this.removingItems.forEach(item => item.destroyAll());
this.activeItem?.destroyAll();
this.pendingItem?.destroy();
}
}
+49
View File
@@ -0,0 +1,49 @@
import Quickshell
import QtQuick
QtObject {
id: root
required property Item item
property Animation activeAnimation: null
signal animationCompleted(self: SlideViewItem)
property Connections __animConnection: Connections {
target: activeAnimation
function onStopped() {
root.activeAnimation.destroy();
root.animationCompleted(root);
}
}
function createAnimation(component: Component) {
this.stopIfRunning();
this.activeAnimation = component.createObject(this, {
target: this.item
});
this.activeAnimation.running = true;
}
function stopIfRunning() {
if (this.activeAnimation) {
this.activeAnimation.stop();
this.activeAnimation = null;
}
}
function finishIfRunning() {
if (this.activeAnimation) {
// animator types dont handle complete correctly.
this.activeAnimation.complete();
this.activeAnimation.stop();
this.item.x = 0;
this.item.y = 0;
this.activeAnimation = null;
}
}
function destroyAll() {
this.item.destroy();
this.destroy();
}
}
+31
View File
@@ -0,0 +1,31 @@
import QtQuick
import Quickshell
import QtQuick.Layouts
import "../"
MouseArea {
id: root
implicitWidth: 26
implicitHeight: 26
hoverEnabled: true
acceptedButtons: Qt.LeftButton
Image {
anchors.fill: parent
source: `../assets/${Globals.isDarkTheme ? "sun" : "moon"}.png`
smooth: false
scale: root.containsMouse ? 0.9 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
}
onPressed: () => {
Globals.isDarkTheme = !Globals.isDarkTheme;
}
}
+28
View File
@@ -0,0 +1,28 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import ".."
Text {
id: timetxt
font.family: "BigBlueTermPlusNerdFont"
font.pointSize: 16
font.bold: true
color: "white"
opacity: 0.5
Process {
id: dateProc
command: ["date", "+%H⯁%M"]
running: true
stdout: SplitParser {
onRead: data => timetxt.text = data
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: dateProc.running = true
}
}
+239
View File
@@ -0,0 +1,239 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Hyprland
import "root:/"
Scope {
id: root
required property var bar
property TooltipItem activeTooltip: null
property TooltipItem activeMenu: null
readonly property TooltipItem activeItem: activeMenu ?? activeTooltip
property TooltipItem lastActiveItem: null
readonly property TooltipItem shownItem: activeItem ?? lastActiveItem
property real hangTime: lastActiveItem?.hangTime ?? 0
property Item tooltipItem: null
onActiveItemChanged: {
if (activeItem != null) {
hangTimer.stop();
activeItem.targetVisible = true;
if (tooltipItem) {
activeItem.parent = tooltipItem;
}
}
if (lastActiveItem != null && lastActiveItem != activeItem) {
if (activeItem != null)
lastActiveItem.targetVisible = false;
else if (root.hangTime == 0)
doLastHide();
else
hangTimer.start();
}
if (activeItem != null)
lastActiveItem = activeItem;
}
function setItem(item: TooltipItem) {
if (item.isMenu) {
activeMenu = item;
} else {
activeTooltip = item;
}
}
function removeItem(item: TooltipItem) {
if (item.isMenu && activeMenu == item) {
activeMenu = null;
} else if (!item.isMenu && activeTooltip == item) {
activeTooltip = null;
}
}
function doLastHide() {
lastActiveItem.targetVisible = false;
}
function onHidden(item: TooltipItem) {
if (item == lastActiveItem) {
lastActiveItem = null;
}
}
Timer {
id: hangTimer
interval: root.hangTime
onTriggered: root.doLastHide()
}
property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0
Behavior on scaleMul {
SmoothedAnimation {
velocity: 5
}
}
LazyLoader {
id: popupLoader
activeAsync: root.shownItem != null
PopupWindow {
id: popup
anchor {
window: root.bar
rect.y: root.bar.tooltipYOffset
adjustment: PopupAdjustment.None
}
HyprlandWindow.opacity: root.scaleMul
HyprlandWindow.visibleMask: Region {
id: visibleMask
item: tooltipItem
}
Connections {
target: root
function onScaleMulChanged() {
visibleMask.changed();
}
}
//height: bar.height
//implicitWidth: Math.max(700, tooltipItem.largestAnimWidth) // max due to qtwayland glitches
//implicitHeight: Math.max(700, tooltipItem.largestAnimHeight) // max due to qtwayland glitches
visible: true
color: "transparent"
//color: "#20ff0000"
mask: Region {
item: (shownItem?.hoverable ?? false) ? tooltipItem : null
}
HyprlandFocusGrab {
active: activeItem?.isMenu ?? false
windows: [popup, bar, ...(activeItem?.grabWindows ?? [])]
onActiveChanged: {
if (!active && activeItem?.isMenu) {
activeMenu.close();
}
}
}
Item {
id: tooltipItem
Component.onCompleted: {
root.tooltipItem = this;
if (root.shownItem) {
root.shownItem.parent = this;
}
//highestAnimY = targetY - targetHeight / 2;
//lowestAnimY = targetY + targetHeight / 2;
}
transform: Scale {
origin.x: 0
origin.y: tooltipItem.height / 2
xScale: 0.9 + scaleMul * 0.1
yScale: xScale
}
clip: width != targetWidth || height != targetHeight
// bkg
Rectangle {
anchors.fill: parent
color: "#3B253F"
}
readonly property var targetWidth: shownItem?.implicitWidth ?? 0
readonly property var targetHeight: shownItem?.implicitHeight ?? 0
readonly property real targetY: {
if (shownItem == null)
return 0;
const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y;
return bar.boundedY(target, shownItem.implicitHeight / 2);
}
readonly property real targetX: {
if (root.shownItem == null)
return 0;
const target = root.bar.contentItem.mapFromItem(root.shownItem.owner, 0, root.shownItem.targetRelativeX).x;
return root.bar.boundedX(target, root.shownItem.implicitWidth / 2);
}
property var w: -1
implicitWidth: Math.max(1, w)
property var y1: -1
property var y2: -1
y: y1 - popup.anchor.rect.y
height: y2 - y1
readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running
onAnyAnimsRunningChanged: {
if (!anyAnimsRunning) {
largestAnimWidth = targetWidth;
//highestAnimY = y1;
//lowestAnimY = y2;
}
}
SmoothedAnimation on y1 {
id: y1Anim
to: tooltipItem.targetY - tooltipItem.targetHeight / 2
onToChanged: {
if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.y1 = to;
} else {
velocity = (Math.max(tooltipItem.y1, to) - Math.min(tooltipItem.y1, to)) * 5;
restart();
}
}
}
SmoothedAnimation on y2 {
id: y2Anim
to: tooltipItem.targetY + tooltipItem.targetHeight / 2
onToChanged: {
if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.y2 = to;
} else {
velocity = (Math.max(tooltipItem.y2, to) - Math.min(tooltipItem.y2, to)) * 5;
restart();
}
}
}
SmoothedAnimation on w {
id: widthAnim
to: tooltipItem.targetWidth
onToChanged: {
if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.w = to;
} else {
velocity = (Math.max(tooltipItem.width, to) - Math.min(tooltipItem.width, to)) * 5;
restart();
}
}
}
}
}
}
}
+274
View File
@@ -0,0 +1,274 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Hyprland
import "root:/"
Scope {
id: root
required property var bar
property TooltipItem activeTooltip: null
property TooltipItem activeMenu: null
readonly property TooltipItem activeItem: activeMenu ?? activeTooltip
property TooltipItem lastActiveItem: null
readonly property TooltipItem shownItem: activeItem ?? lastActiveItem
property real hangTime: lastActiveItem?.hangTime ?? 0
property Item tooltipItem: null
onActiveItemChanged: {
if (activeItem != null) {
hangTimer.stop();
activeItem.targetVisible = true;
if (tooltipItem) {
activeItem.parent = tooltipItem;
}
}
if (lastActiveItem != null && lastActiveItem != activeItem) {
if (activeItem != null)
lastActiveItem.targetVisible = false;
else if (root.hangTime == 0)
doLastHide();
else
hangTimer.start();
}
if (activeItem != null)
lastActiveItem = activeItem;
}
function setItem(item: TooltipItem) {
if (item.isMenu) {
activeMenu = item;
} else {
activeTooltip = item;
}
}
function removeItem(item: TooltipItem) {
if (item.isMenu && activeMenu == item) {
activeMenu = null;
} else if (!item.isMenu && activeTooltip == item) {
activeTooltip = null;
}
}
function doLastHide() {
lastActiveItem.targetVisible = false;
}
function onHidden(item: TooltipItem) {
if (item == lastActiveItem) {
lastActiveItem = null;
}
}
Timer {
id: hangTimer
interval: root.hangTime
onTriggered: doLastHide()
}
property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0
Behavior on scaleMul {
SmoothedAnimation {
velocity: 5
}
}
LazyLoader {
id: popupLoader
activeAsync: shownItem != null
PopupWindow {
id: popup
anchor {
window: bar
rect.x: tooltipItem.highestAnimX
rect.y: bar.tooltipYOffset
adjustment: PopupAdjustment.None
}
HyprlandWindow.opacity: root.scaleMul
HyprlandWindow.visibleMask: Region {
id: visibleMask
item: tooltipItem
}
Connections {
target: root
function onScaleMulChanged() {
visibleMask.changed();
}
}
//height: bar.height
width: Math.max(700, tooltipItem.largestAnimWidth) // max due to qtwayland glitches
height: {
const h = tooltipItem.lowestAnimY - tooltipItem.highestAnimY;
//console.log(`seth ${h} ${tooltipItem.highestAnimY} ${tooltipItem.lowestAnimY}; ${tooltipItem.y1} ${tooltipItem.y2}`)
return h;
}
visible: true
color: "transparent"
//color: "#20ff0000"
mask: Region {
item: (shownItem?.hoverable ?? false) ? tooltipItem : null
}
HyprlandFocusGrab {
active: root.activeItem?.isMenu ?? false
windows: [popup, root.bar, ...(root.activeItem?.grabWindows ?? [])]
onActiveChanged: {
if (!active && root.activeItem?.isMenu) {
activeMenu.close();
}
}
}
/*Rectangle {
color: "#10ff0000"
//y: tooltipItem.highestAnimY
height: tooltipItem.lowestAnimY - tooltipItem.highestAnimY
width: parent.width
}
Rectangle {
color: "#1000ff00"
//y: tooltipItem.highestAnimY
height: popup.height
width: parent.width
}*/
Item {
id: tooltipItem
Component.onCompleted: {
root.tooltipItem = this;
if (root.shownItem) {
root.shownItem.parent = this;
}
//highestAnimY = targetY - targetHeight / 2;
//lowestAnimY = targetY + targetHeight / 2;
}
transform: Scale {
origin.x: 0
origin.y: tooltipItem.height / 2
xScale: 0.9 + scaleMul * 0.1
yScale: xScale
}
clip: width != targetWidth || height != targetHeight
// bkg
Rectangle {
anchors.fill: parent
color: "red"
}
readonly property var targetWidth: shownItem?.implicitWidth ?? 0
readonly property var targetHeight: shownItem?.implicitHeight ?? 0
property var largestAnimWidth: 0
property var highestAnimY: 0 // unused due to reposition timing issues
property var lowestAnimY: bar.height
onTargetWidthChanged: {
if (targetWidth > largestAnimWidth) {
largestAnimWidth = targetWidth;
}
}
onTargetYChanged: updateYBounds()
onTargetHeightChanged: updateYBounds()
function updateYBounds() {
if (targetY - targetHeight / 2 < highestAnimY)
//highestAnimY = targetY - targetHeight / 2
{}
if (targetY + targetHeight / 2 > lowestAnimY)
//lowestAnimY = targetY + targetHeight / 2
{}
}
readonly property real targetY: {
if (shownItem == null)
return 0;
const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y;
return bar.boundedY(target, shownItem.implicitHeight / 2);
}
property var w: -1
width: Math.max(1, w)
property var y1: -1
property var y2: -1
y: y1 - popup.anchor.rect.y
height: y2 - y1
readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running
onAnyAnimsRunningChanged: {
if (!anyAnimsRunning) {
largestAnimWidth = targetWidth;
//highestAnimY = y1;
//lowestAnimY = y2;
}
}
SmoothedAnimation on y1 {
id: y1Anim
to: tooltipItem.targetY - tooltipItem.targetHeight / 2
onToChanged: {
if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.y1 = to;
} else {
velocity = (Math.max(tooltipItem.y1, to) - Math.min(tooltipItem.y1, to)) * 5;
restart();
}
}
}
SmoothedAnimation on y2 {
id: y2Anim
to: tooltipItem.targetY + tooltipItem.targetHeight / 2
onToChanged: {
if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.y2 = to;
} else {
velocity = (Math.max(tooltipItem.y2, to) - Math.min(tooltipItem.y2, to)) * 5;
restart();
}
}
}
SmoothedAnimation on w {
id: widthAnim
to: tooltipItem.targetWidth
onToChanged: {
if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.w = to;
} else {
velocity = (Math.max(tooltipItem.width, to) - Math.min(tooltipItem.width, to)) * 5;
restart();
}
}
}
}
}
}
}
+91
View File
@@ -0,0 +1,91 @@
import QtQuick
import Quickshell
Item {
id: root
required property var tooltip
required property Item owner
property bool isMenu: false
property list<QtObject> grabWindows
property bool hoverable: isMenu
property bool animateSize: true
property bool show: false
property bool preloadBackground: root.visible
property real targetRelativeX: owner.width / 2
property real hangTime: isMenu ? 0 : 200
signal close
readonly property alias contentItem: contentItem
default property alias data: contentItem.data
property Component backgroundComponent: null
onShowChanged: {
if (show)
tooltip.setItem(this);
else
tooltip.removeItem(this);
}
property bool targetVisible: false
property real targetOpacity: 0
opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul))
Behavior on targetOpacity {
id: opacityAnimation
SmoothedAnimation {
velocity: 5
}
}
function snapOpacity(opacity: real) {
opacityAnimation.enabled = false;
targetOpacity = opacity;
opacityAnimation.enabled = true;
}
onTargetVisibleChanged: {
if (targetVisible) {
visible = true;
targetOpacity = 1;
} else {
close();
targetOpacity = 0;
}
}
onTargetOpacityChanged: {
if (!targetVisible && targetOpacity == 0) {
visible = false;
this.parent = null;
if (tooltip)
tooltip.onHidden(this);
}
}
anchors.fill: parent
visible: false
//clip: true
implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
readonly property Item item: contentItem
Loader {
anchors.fill: parent
active: root.backgroundComponent && (root.visible || root.preloadBackground)
asynchronous: !root.visible && root.preloadBackground
sourceComponent: backgroundComponent
}
Item {
id: contentItem
anchors.fill: parent
anchors.margins: 5
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
}
}
@@ -0,0 +1,92 @@
import QtQuick
import Quickshell
Item {
id: root
required property var tooltip
required property Item owner
property bool isMenu: false
property list<QtObject> grabWindows
property bool hoverable: isMenu
property bool animateSize: true
property bool show: false
property bool preloadBackground: root.visible
property real targetRelativeY: owner.height / 2
property real hangTime: isMenu ? 0 : 200
signal close
readonly property alias contentItem: contentItem
default property alias data: contentItem.data
property Component backgroundComponent: null
onShowChanged: {
console.log(this);
if (show)
tooltip.setItem(this);
else
tooltip.removeItem(this);
}
property bool targetVisible: false
property real targetOpacity: 0
opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul))
Behavior on targetOpacity {
id: opacityAnimation
SmoothedAnimation {
velocity: 5
}
}
function snapOpacity(opacity: real) {
opacityAnimation.enabled = false;
targetOpacity = opacity;
opacityAnimation.enabled = true;
}
onTargetVisibleChanged: {
if (targetVisible) {
visible = true;
targetOpacity = 1;
} else {
close();
targetOpacity = 0;
}
}
onTargetOpacityChanged: {
if (!targetVisible && targetOpacity == 0) {
visible = false;
this.parent = null;
if (tooltip)
tooltip.onHidden(this);
}
}
anchors.fill: parent
visible: false
//clip: true
implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
readonly property Item item: contentItem
Loader {
anchors.fill: parent
active: root.backgroundComponent && (root.visible || root.preloadBackground)
asynchronous: !root.visible && root.preloadBackground
sourceComponent: root.backgroundComponent
}
Item {
id: contentItem
anchors.fill: parent
anchors.margins: 5
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
}
}
+133
View File
@@ -0,0 +1,133 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import "mpris"
MouseArea {
id: root
required property var bar
property var node: Pipewire.defaultAudioSink
PwObjectTracker {
objects: [root.node]
}
implicitWidth: 26
implicitHeight: 26
hoverEnabled: true
acceptedButtons: Qt.LeftButton
//enabled: MprisController.canChangeVolume
opacity: enabled ? 1 : 0.5
Image {
anchors.fill: parent
source: `../assets/${root.node.audio.muted ? "noaudio" : "audio"}.png`
smooth: false
scale: popupLoader.active ? 0.9 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
}
onClicked: node.audio.muted = !node.audio.muted
LazyLoader {
id: popupLoader
property bool popupContainsMouse
active: root.enabled && (root.containsMouse || popupContainsMouse)
Behavior on active {
NumberAnimation {
duration: 100
}
}
PopupWindow {
id: popup
visible: true
anchor.window: root.bar
width: 200
height: 32
color: "#9B5B36"
Connections {
target: root
function onXChanged() {
popup.updatePos();
}
}
Component.onCompleted: updatePos()
function updatePos() {
const pos = root.mapToGlobal(root.width / 2 - popup.width / 2, root.height);
popup.anchor.rect.x = pos.x;
popup.anchor.rect.y = pos.y + 5;
}
Slider {
id: slider
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
anchors.margins: 5
from: 0
to: 1
value: root.node.audio.volume
onValueChanged: root.node.audio.volume = value
background: Rectangle {
x: slider.leftPadding
y: slider.topPadding + slider.availableHeight / 2 - height / 2
implicitHeight: 16
width: slider.availableWidth
height: implicitHeight
color: "#824524"
opacity: node.audio.muted ? 0.5 : 1
Rectangle {
anchors.margins: 5
x: anchors.leftMargin
y: anchors.topMargin
width: slider.visualPosition * (parent.width - anchors.leftMargin - anchors.rightMargin)
height: parent.height - anchors.topMargin - anchors.bottomMargin
color: "#8D804B"
}
}
handle: Rectangle {
x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
y: slider.topPadding + slider.availableHeight / 2 - height / 2
implicitWidth: 12
implicitHeight: 12
rotation: 45
color: "#8D804B"
}
}
MouseArea {
id: popupMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
onEntered: popupLoader.popupContainsMouse = true
onExited: popupLoader.popupContainsMouse = false
}
}
}
}
+122
View File
@@ -0,0 +1,122 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell.Hyprland
MouseArea {
id: root
required property var bar
property int wsBaseIndex: 1
property int wsCount: 7
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen)
property int currentIndex: 0
property int existsCount: 0
signal workspaceAdded(workspace: HyprlandWorkspace)
implicitWidth: row.implicitWidth
implicitHeight: row.implicitHeight
acceptedButtons: Qt.NoButton
onWheel: e => {
e.accepted = true;
const step = -Math.sign(e.angleDelta.y);
const targetWs = currentIndex + step;
if (targetWs >= wsBaseIndex && targetWs < wsBaseIndex + wsCount) {
Hyprland.dispatch(`workspace ${targetWs}`);
}
}
RowLayout {
id: row
anchors.fill: parent
spacing: 8
Repeater {
model: root.wsCount
MouseArea {
id: wsItem
required property int index
property int wsIndex: root.wsBaseIndex + index
property HyprlandWorkspace workspace: null
property bool exists: workspace != null
property bool active: (root.monitor?.activeWorkspace ?? false) && root.monitor.activeWorkspace == workspace
implicitWidth: 26
implicitHeight: 26
acceptedButtons: Qt.LeftButton
onPressed: Hyprland.dispatch(`workspace ${wsIndex}`)
onExistsChanged: {
root.existsCount += exists ? 1 : -1;
}
onActiveChanged: {
if (active)
root.currentIndex = wsIndex;
}
Connections {
target: root
function onWorkspaceAdded(workspace: HyprlandWorkspace) {
if (workspace.id == wsItem.wsIndex) {
wsItem.workspace = workspace;
}
}
}
Rectangle {
id: fill
anchors.fill: parent
anchors.margins: 2
radius: width / 2
color: wsItem.exists ? "#C0DB4C" : "#59622D"
}
Image {
anchors.fill: parent
smooth: false
source: "../assets/workspace.png"
}
MultiEffect {
source: fill
anchors.fill: fill
blur: 1.0
brightness: 0.3
blurEnabled: true
visible: wsItem.active
opacity: wsItem.active ? 1.0 : 0
Behavior on opacity {
NumberAnimation {
duration: 100
}
}
}
}
}
}
Connections {
target: Hyprland.workspaces
function onObjectInsertedPost(workspace) {
root.workspaceAdded(workspace);
}
}
Component.onCompleted: {
Hyprland.workspaces.values.forEach(workspace => {
root.workspaceAdded(workspace);
});
}
}
@@ -0,0 +1,165 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQml.Models
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
Singleton {
id: root
property MprisPlayer trackedPlayer: null
property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null
signal trackChanged(reverse: bool)
property bool __reverse: false
property var activeTrack
Instantiator {
model: Mpris.players
Connections {
required property MprisPlayer modelData
target: modelData
Component.onCompleted: {
if (root.trackedPlayer == null || modelData.isPlaying) {
root.trackedPlayer = modelData;
}
}
Component.onDestruction: {
if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
for (const player of Mpris.players.values) {
if (player.playbackState.isPlaying) {
root.trackedPlayer = player;
break;
}
}
if (trackedPlayer == null && Mpris.players.values.length != 0) {
trackedPlayer = Mpris.players.values[0];
}
}
}
function onPlaybackStateChanged() {
if (root.trackedPlayer !== modelData)
root.trackedPlayer = modelData;
}
}
}
Connections {
target: activePlayer
function onPostTrackChanged() {
root.updateTrack();
}
function onTrackArtUrlChanged() {
//root.updateTrack();
if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) {
// cantata likes to send cover updates *BEFORE* updating the track info.
// as such, art url changes shouldn't be able to break the reverse animation
const r = root.__reverse;
root.updateTrack();
root.__reverse = r;
}
}
}
onActivePlayerChanged: this.updateTrack()
function updateTrack() {
//console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`)
this.activeTrack = {
uniqueId: this.activePlayer?.uniqueId ?? 0,
artUrl: this.activePlayer?.trackArtUrl ?? "",
title: this.activePlayer?.trackTitle || "Unknown Title",
artist: this.activePlayer?.trackArtist || "Unknown Artist",
album: this.activePlayer?.trackAlbum || "Unknown Album"
};
this.trackChanged(__reverse);
this.__reverse = false;
}
property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying
property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false
function togglePlaying() {
if (this.canTogglePlaying)
this.activePlayer.togglePlaying();
}
property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false
function previous() {
if (this.canGoPrevious) {
this.__reverse = true;
this.activePlayer.previous();
}
}
property bool canGoNext: this.activePlayer?.canGoNext ?? false
function next() {
if (this.canGoNext) {
this.__reverse = false;
this.activePlayer.next();
}
}
property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl
property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl
property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None
function setLoopState(loopState: var) {
if (this.loopSupported) {
this.activePlayer.loopState = loopState;
}
}
property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl
property bool hasShuffle: this.activePlayer?.shuffle ?? false
function setShuffle(shuffle: bool) {
if (this.shuffleSupported) {
this.activePlayer.shuffle = shuffle;
}
}
function setActivePlayer(player: MprisPlayer) {
const targetPlayer = player ?? Mpris.players[0];
console.log(`setactive: ${targetPlayer} from ${activePlayer}`);
if (targetPlayer && this.activePlayer) {
this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer);
} else {
// always animate forward if going to null
this.__reverse = false;
}
this.trackedPlayer = targetPlayer;
}
IpcHandler {
target: "mpris"
function pauseAll(): void {
for (const player of Mpris.players.values) {
if (player.canPause)
player.pause();
}
}
function playPause(): void {
root.togglePlaying();
}
function previous(): void {
root.previous();
}
function next(): void {
root.next();
}
}
}
+151
View File
@@ -0,0 +1,151 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Services.Mpris
import "../.."
import ".."
MouseArea {
id: root
required property var window
required property var bar
readonly property var activePlayer: MprisController.activePlayer
property var isPopupOpen: false
implicitWidth: column.implicitWidth + 10
Layout.fillHeight: true
acceptedButtons: Qt.RightButton
onClicked: isPopupOpen = !isPopupOpen
Item {
id: widget
anchors.fill: parent
scale: root.isPopupOpen ? 0.92 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
implicitHeight: column.implicitHeight + 10
RowLayout {
id: column
anchors {
fill: parent
}
ClickableIcon {
acceptedButtons: Qt.LeftButton
implicitWidth: 20
implicitHeight: 20
image: "../../assets/fastforward.png"
mirror: true
hoverEnabled: true
enabled: MprisController.canGoPrevious
onClicked: MprisController.previous()
scale: containsMouse ? 0.9 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
}
ClickableIcon {
acceptedButtons: Qt.LeftButton
implicitWidth: 26
implicitHeight: 26
Layout.leftMargin: -4
Layout.rightMargin: -4
image: `../../assets/${MprisController.isPlaying ? "pause" : "play"}.png`
hoverEnabled: true
enabled: MprisController.canTogglePlaying
onClicked: MprisController.togglePlaying()
}
ClickableIcon {
acceptedButtons: Qt.LeftButton
implicitWidth: 20
implicitHeight: 20
image: "../../assets/fastforward.png"
hoverEnabled: true
enabled: MprisController.canGoNext
onClicked: MprisController.next()
scale: containsMouse ? 0.9 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
}
}
Connections {
target: root
function onIsPopupOpenChanged() {
root.window.focusable = root.isPopupOpen;
}
}
LazyLoader {
id: popupLoader
active: root.isPopupOpen
// Leave time for exit anim
Behavior on active {
enabled: popupLoader.active
NumberAnimation {
duration: 1000
}
}
PlayersPopup {
id: popup
visible: true
isOpen: root.isPopupOpen
anchor.window: root.window
anchor.rect.x: root.window.width
anchor.rect.y: 0
HyprlandFocusGrab {
active: root.isPopupOpen
windows: [popup]
}
contentItem {
focus: true
Keys.onEscapePressed: root.isPopupOpen = false
}
}
}
}
component CenteredText: Item {
Layout.fillWidth: true
property alias text: label.text
property alias font: label.font
Label {
id: label
visible: text != ""
anchors.centerIn: parent
elide: Text.ElideRight
width: Math.min(parent.width - 20, implicitWidth)
font.family: "BigBlueTermPlusNerdFont"
color: "white"
}
}
}
+529
View File
@@ -0,0 +1,529 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Services.Mpris
import "../.."
import "../../components"
import ".."
PopupWindow {
id: root
property bool isOpen: false
implicitWidth: 700
//implicitHeight: popupContent.implicitHeight + popupContent.anchors.margins * 2
implicitHeight: 800
mask: Region {
item: wrapper
}
color: "transparent"
property Scope positionInfo: Scope {
id: positionInfo
property int position: Math.floor(MprisController.activePlayer.position)
property int length: Math.floor(MprisController.activePlayer.length)
FrameAnimation {
id: posTracker
running: MprisController.activePlayer.isPlaying
onTriggered: MprisController.activePlayer.positionChanged()
}
function timeStr(time: int): string {
const seconds = time % 60;
const minutes = Math.floor(time / 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
}
Rope {
segmentCount: 4
segmentLen: 22
start: Qt.vector2d(250, -5)
end: Qt.vector2d(wrapper.x + 49, wrapper.y)
}
Rope {
segmentCount: 4
segmentLen: 22
start: Qt.vector2d(wrapper.width + 200 - 50, -5)
end: Qt.vector2d(wrapper.width + wrapper.x - 49, wrapper.y)
}
Rectangle {
id: wrapper
implicitWidth: 500
implicitHeight: popupContent.implicitHeight + popupContent.anchors.margins * 2
color: "#824524"
x: 200
y: -wrapper.height
readonly property var targetX: 200
readonly property var targetY: root.isOpen ? 70 : -wrapper.height - 50
property var velocityX: 0
property var velocityY: 0
FrameAnimation {
running: true
function dampingVelocity(currentVelocity, delta) {
const spring = 8.0;
const damping = 0.3;
const springForce = spring * delta;
const dampingForce = -damping * currentVelocity;
return currentVelocity + (springForce + dampingForce);
}
onTriggered: {
const deltaX = wrapper.targetX - wrapper.x;
const deltaY = wrapper.targetY - wrapper.y;
if (Math.abs(deltaX) > 0.1 || Math.abs(deltaY) > 0.1) {
wrapper.velocityX = dampingVelocity(wrapper.velocityX, deltaX);
wrapper.velocityY = dampingVelocity(wrapper.velocityY, deltaY);
wrapper.x += wrapper.velocityX * frameTime;
wrapper.y += wrapper.velocityY * frameTime;
}
}
}
MouseArea {
property var prevMouseX: 0
property var prevMouseY: 0
anchors.fill: parent
onPressed: e => {
prevMouseX = e.x;
prevMouseY = e.y;
}
onPositionChanged: e => {
wrapper.x += (e.x - prevMouseX) * 0.3;
wrapper.y += (e.y - prevMouseY) * 0.3;
prevMouseX = e.x;
prevMouseY = e.y;
}
}
Item {
anchors.fill: parent
anchors.margins: 8
clip: true
Image {
scale: 10
anchors.fill: parent
source: "../../assets/wood.png"
smooth: false
fillMode: Image.Tile
opacity: 0.4
}
}
ColumnLayout {
id: popupContent
anchors.fill: parent
anchors.margins: 8
Connections {
target: MprisController
function onTrackChanged(reverse: bool) {
trackStack.updateTrack(reverse, false);
}
}
Item {
id: playerSelectorContainment
Layout.fillWidth: true
implicitHeight: playerSelector.implicitHeight
implicitWidth: playerSelector.implicitWidth
Rectangle {
anchors.centerIn: parent
implicitWidth: 50
implicitHeight: 50
color: "#824524"
}
RowLayout {
id: playerSelector
property Item selectedPlayerDisplay: Mpris.players[0]
x: parent.width / 2 - (selectedPlayerDisplay ? selectedPlayerDisplay.x + selectedPlayerDisplay.width / 2 : 0)
anchors.verticalCenter: parent.verticalCenter
Behavior on x {
NumberAnimation {
duration: 400
easing.type: Easing.OutExpo
}
}
Repeater {
model: Mpris.players
MouseArea {
required property MprisPlayer modelData
readonly property bool selected: modelData == MprisController.activePlayer
onSelectedChanged: () => {
if (selected) {
playerSelector.selectedPlayerDisplay = this;
}
}
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
onClicked: MprisController.setActivePlayer(modelData)
Item {
width: 50
height: 50
Image {
anchors.fill: parent
anchors.margins: 0
source: {
// So janky xD
const identity = modelData.identity.toLowerCase();
if (identity.includes("chrome") || identity.includes("chromium")) {
return Quickshell.iconPath("google-chrome") || Quickshell.iconPath("chromium");
}
if (identity.includes("firefox")) {
return Quickshell.iconPath("firefox");
}
if (identity.includes("spotify")) {
return Quickshell.iconPath("spotify");
}
if (modelData.identity == "Brave") {
return "/opt/brave-bin/product_logo_64.png";
}
const entry = DesktopEntries.byId(modelData.desktopEntry);
if (entry && entry.icon) {
return Quickshell.iconPath(entry.icon);
}
return "../../assets/mpd.png";
}
smooth: false
sourceSize.width: 50
sourceSize.height: 50
cache: false
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.bottomMargin: 16
Label {
anchors.centerIn: parent
text: MprisController.activePlayer.identity
font.family: "BigBlueTermPlusNerdFont"
color: "white"
}
}
SlideView {
id: trackStack
Layout.fillWidth: true
implicitHeight: 100
clip: animating || (lastFlicked?.contentX ?? 0) != 0
property Flickable lastFlicked
property bool reverse: false
Component.onCompleted: updateTrack(false, true)
function updateTrack(reverse: bool, immediate: bool) {
this.reverse = reverse;
this.replace(trackComponent, {
track: MprisController.activeTrack
}, immediate);
}
property var trackComponent: Component {
Flickable {
id: flickable
required property var track
readonly property bool svReady: img.status === Image.Ready
contentWidth: width + 1
onDragStarted: trackStack.lastFlicked = this
onDragEnded: {
if (Math.abs(contentX) > 75) {
if (contentX < 0)
MprisController.previous();
else if (contentX > 0)
MprisController.next();
}
}
RowLayout {
anchors.fill: parent
spacing: 8
Rectangle {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
implicitHeight: 128
implicitWidth: 128
color: "transparent"
Image {
id: img
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
visible: flickable.track.artUrl != ""
source: flickable.track.artUrl
cache: false
asynchronous: true
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Label {
text: flickable.track.title
font.pointSize: albumLabel.font.pointSize + 1
font.family: "BigBlueTermPlusNerdFont"
color: "white"
elide: Text.ElideRight
Layout.maximumWidth: Math.min(300, implicitWidth)
}
Label {
id: albumLabel
text: flickable.track.album
opacity: 0.8
font.family: "BigBlueTermPlusNerdFont"
color: "white"
Layout.maximumWidth: Math.min(300, implicitWidth)
}
Label {
text: flickable.track.artist
opacity: 0.8
font.family: "BigBlueTermPlusNerdFont"
color: "white"
Layout.maximumWidth: Math.min(300, implicitWidth)
}
}
}
}
}
readonly property real fromPos: trackStack.width * (trackStack.reverse ? -1 : 1)
enterTransition: PropertyAnimation {
property: "x"
from: trackStack.fromPos
to: 0
duration: 350
easing.type: Easing.OutExpo
}
exitTransition: PropertyAnimation {
property: "x"
to: target.x - trackStack.fromPos
duration: 350
easing.type: Easing.OutExpo
}
}
Item {
Layout.fillWidth: true
implicitHeight: controlsRow.implicitHeight
RowLayout {
id: controlsRow
anchors.centerIn: parent
ClickableIcon {
Layout.leftMargin: 2
Layout.rightMargin: 2
implicitWidth: 24
implicitHeight: 24
enabled: MprisController.loopSupported
image: {
switch (MprisController.loopState) {
case MprisLoopState.None:
return "../../assets/repeat-off.png";
case MprisLoopState.Playlist:
return "../../assets/repeat.png";
case MprisLoopState.Track:
return "../../assets/repeat-once.png";
}
}
onClicked: {
let target = MprisLoopState.None;
switch (MprisController.loopState) {
case MprisLoopState.None:
target = MprisLoopState.Playlist;
break;
case MprisLoopState.Playlist:
target = MprisLoopState.Track;
break;
case MprisLoopState.Track:
target = MprisLoopState.None;
break;
}
MprisController.setLoopState(target);
}
}
ClickableIcon {
Layout.leftMargin: 2
Layout.rightMargin: 2
implicitWidth: 32
implicitHeight: 32
enabled: MprisController.canGoPrevious
image: "../../assets/fastforward.png"
mirror: true
onClicked: MprisController.previous()
}
ClickableIcon {
implicitWidth: 42
implicitHeight: 42
enabled: MprisController.canTogglePlaying
image: `../../assets/${MprisController.isPlaying ? "pause" : "play"}.png`
onClicked: MprisController.togglePlaying()
}
ClickableIcon {
Layout.leftMargin: 2
Layout.rightMargin: 2
implicitWidth: 32
implicitHeight: 32
enabled: MprisController.canGoNext
image: "../../assets/fastforward.png"
onClicked: MprisController.next()
}
ClickableIcon {
Layout.leftMargin: 2
Layout.rightMargin: 2
implicitWidth: 24
implicitHeight: 24
enabled: MprisController.shuffleSupported
image: `../../assets/${MprisController.hasShuffle ? "shuffle" : "noshuffle"}.png`
onClicked: MprisController.setShuffle(!MprisController.hasShuffle)
}
}
}
RowLayout {
Layout.margins: 5
Label {
Layout.preferredWidth: lengthLabel.implicitWidth
text: positionInfo.timeStr(positionInfo.position)
font.family: "BigBlueTermPlusNerdFont"
color: "white"
}
Slider {
id: slider
property bool bindSlider: true
property real boundAnimStart: 0
property real boundAnimFactor: 1
property real lastPosition: 0
property real lastLength: 0
property real boundPosition: {
const ppos = MprisController.activePlayer.position / MprisController.activePlayer.length;
const bpos = boundAnimStart;
return (ppos * boundAnimFactor) + (bpos * (1.0 - boundAnimFactor));
}
Layout.fillWidth: true
enabled: MprisController.activePlayer.canSeek
from: 0
to: 1
background: Rectangle {
x: slider.leftPadding
y: slider.topPadding + slider.availableHeight / 2 - height / 2
implicitHeight: 24
width: slider.availableWidth
height: implicitHeight
color: "#824524"
Rectangle {
anchors.margins: 8
x: anchors.leftMargin
y: anchors.topMargin
width: slider.visualPosition * (parent.width - anchors.leftMargin - anchors.rightMargin)
height: parent.height - anchors.topMargin - anchors.bottomMargin
color: "#8D804B"
}
}
handle: Rectangle {
x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
y: slider.topPadding + slider.availableHeight / 2 - height / 2
implicitWidth: 16
implicitHeight: 16
rotation: 45
color: "#8D804B"
}
Connections {
target: MprisController.activePlayer
function onPositionChanged() {
slider.lastPosition = MprisController.activePlayer.position;
slider.lastLength = MprisController.activePlayer.length;
}
}
onPressedChanged: () => {
if (!pressed)
MprisController.activePlayer.position = value * MprisController.activePlayer.length;
bindSlider = !pressed;
}
Binding {
when: slider.bindSlider
slider.value: slider.boundPosition
}
}
Label {
id: lengthLabel
text: positionInfo.timeStr(positionInfo.length)
font.family: "BigBlueTermPlusNerdFont"
color: "white"
}
}
}
}
Image {
source: "../../assets/rope-tie.png"
smooth: false
width: 32
height: 32
x: wrapper.x + 49 - width / 2
y: wrapper.y - height / 2
}
Image {
source: "../../assets/rope-tie.png"
smooth: false
width: 32
height: 32
x: wrapper.width + wrapper.x - 49 - width / 2
y: wrapper.y - height / 2
}
}
@@ -0,0 +1,44 @@
import QtQuick
import QtQuick.Shapes
import "../.."
Rectangle {
property var checkState: Qt.Unchecked
implicitHeight: 18
implicitWidth: 18
radius: 3
color: checkState == Qt.Checked ? "#C5E3EE" : "#90ACAD"
Shape {
visible: checkState == Qt.Checked
anchors.fill: parent
layer.enabled: true
layer.samples: 10
ShapePath {
strokeWidth: 2
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
startX: start.x
startY: start.y
PathLine {
id: start
x: width * 0.8
y: height * 0.2
}
PathLine {
x: width * 0.35
y: height * 0.8
}
PathLine {
x: width * 0.2
y: height * 0.6
}
}
}
}
@@ -0,0 +1,56 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
Item {
property bool expanded: false;
readonly property bool open: progress != 0;
readonly property bool animating: internalProgress != (expanded ? 101 : -1);
implicitHeight: 16
implicitWidth: 16
property var xStart: Math.round(width * 0.3)
property var yStart: Math.round(height * 0.1)
property var xEnd: Math.round(width * 0.7)
property var yEnd: Math.round(height * 0.9)
property real internalProgress: expanded ? 101 : -1;
Behavior on internalProgress { SmoothedAnimation { velocity: 300 } }
EasingCurve {
id: curve
curve.type: Easing.InOutQuad
}
readonly property real progress: curve.valueAt(Math.min(100, Math.max(internalProgress, 0)) * 0.01)
rotation: progress * 90;
Shape {
anchors.fill: parent
layer.enabled: true
layer.samples: 3
ShapePath {
strokeWidth: 2
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.MiterJoin
fillColor: "transparent"
startX: xStart
startY: yStart
PathLine {
x: xEnd
y: height / 2
}
PathLine {
y: yEnd
x: xStart
}
}
}
}
+118
View File
@@ -0,0 +1,118 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.DBusMenu
import "../.."
MouseArea {
id: root
required property QsMenuEntry entry
property alias expanded: childrenRevealer.expanded
property bool animating: childrenRevealer.animating || (childMenuLoader?.item?.animating ?? false)
// appears it won't actually create the handler when only used from MenuItemList.
onExpandedChanged: {}
onAnimatingChanged: {}
signal close
implicitWidth: row.implicitWidth + 4
implicitHeight: row.implicitHeight + 4
hoverEnabled: true
onClicked: {
if (entry.hasChildren)
childrenRevealer.expanded = !childrenRevealer.expanded;
else {
entry.triggered();
if (entry.toggleType == ToggleButtonType.None)
close();
}
}
Rectangle {
anchors.fill: parent
visible: root.containsMouse || childrenRevealer.expanded
color: "#824524"
}
ColumnLayout {
id: row
anchors.fill: parent
anchors.margins: 2
spacing: 0
RowLayout {
id: innerRow
Item {
implicitWidth: 22
implicitHeight: 22
MenuCheckBox {
anchors.centerIn: parent
visible: entry.buttonType == QsMenuButtonType.CheckBox
checkState: entry.checkState
}
MenuRadioButton {
anchors.centerIn: parent
visible: entry.buttonType == QsMenuButtonType.RadioButton
checkState: entry.checkState
}
MenuChildrenRevealer {
id: childrenRevealer
anchors.centerIn: parent
visible: entry.hasChildren
onOpenChanged: entry.showChildren = open
}
}
Text {
text: entry.text
color: entry.enabled ? "white" : "#bbbbbb"
}
Item {
Layout.fillWidth: true
implicitWidth: 22
implicitHeight: 22
IconImage {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
source: entry.icon
visible: source != ""
implicitSize: parent.height
}
}
}
Loader {
id: childMenuLoader
Layout.fillWidth: true
Layout.preferredHeight: active ? item.implicitHeight * childrenRevealer.progress : 0
readonly property real widthDifference: {
Math.max(0, (item?.implicitWidth ?? 0) - innerRow.implicitWidth);
}
Layout.preferredWidth: active ? innerRow.implicitWidth + (widthDifference * childrenRevealer.progress) : 0
active: root.expanded || root.animating
clip: true
sourceComponent: MenuView {
id: childrenList
menu: entry
onClose: root.close()
anchors {
top: parent.top
left: parent.left
right: parent.right
}
}
}
}
}
@@ -0,0 +1,19 @@
import QtQuick
import "../.."
Rectangle {
property var checkState: Qt.Unchecked
implicitHeight: 18
implicitWidth: 18
radius: width / 2
color: checkState == Qt.Checked ? "#C5E3EE" : "#90ACAD"
Rectangle {
x: parent.width * 0.25
y: parent.height * 0.25
visible: checkState == Qt.Checked
width: parent.width * 0.5
height: width
radius: width / 2
}
}
+73
View File
@@ -0,0 +1,73 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
ColumnLayout {
id: root
property alias menu: menuView.menu
property Item animatingItem: null
property bool animating: animatingItem != null
signal close
signal submenuExpanded(item: var)
QsMenuOpener {
id: menuView
}
spacing: 0
Repeater {
model: menuView.children
Loader {
required property var modelData
property var item: Component {
BoundComponent {
id: itemComponent
source: "MenuItem.qml"
property var entry: modelData
function onClose() {
root.close();
}
function onExpandedChanged() {
if (item.expanded)
root.submenuExpanded(item);
}
function onAnimatingChanged() {
if (item.animating) {
root.animatingItem = this;
} else if (root.animatingItem == this) {
root.animatingItem = null;
}
}
Connections {
target: root
function onSubmenuExpanded(expandedItem) {
if (item != expandedItem)
item.expanded = false;
}
}
}
}
property var separator: Component {
Rectangle {
color: "#824524"
implicitHeight: 4
}
}
sourceComponent: modelData.isSeparator ? separator : item
Layout.fillHeight: true
Layout.fillWidth: true
}
}
}
+143
View File
@@ -0,0 +1,143 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import Quickshell.Services.SystemTray
import ".."
import "../.."
import "../../components"
Rectangle {
id: root
required property var bar
implicitWidth: row.implicitWidth + 10
Layout.fillHeight: true
color: "transparent"
RowLayout {
id: row
implicitWidth: childrenRect.width
spacing: 5
anchors {
fill: parent
margins: 5
}
Repeater {
model: SystemTray.items
Item {
id: item
required property SystemTrayItem modelData
property bool targetMenuOpen: false
Layout.fillHeight: true
implicitWidth: height
Image {
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
width: height
source: item.modelData.icon
scale: item.targetMenuOpen ? 0.8 : 1
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
// showPressed: item.targetMenuOpen || (pressedButtons & ~Qt.RightButton)
// fillWindowWidth: true
// extraVerticalMargin: row.spacing / 2
onClicked: event => {
event.accepted = true;
if (event.button == Qt.LeftButton) {
item.modelData.activate();
} else if (event.button == Qt.MiddleButton) {
item.modelData.secondaryActivate();
}
}
onPressed: event => {
if (event.button == Qt.RightButton && item.modelData.hasMenu) {
item.targetMenuOpen = !item.targetMenuOpen;
}
}
onWheel: event => {
event.accepted = true;
const points = event.angleDelta.y / 120;
item.modelData.scroll(points, false);
}
// property var tooltip: TooltipItem {
// tooltip: root.bar.tooltip
// owner: mouseArea
//
// show: mouseArea.containsMouse
//
// Text {
// id: tooltipText
// text: item.modelData.tooltipTitle != "" ? item.modelData.tooltipTitle : item.modelData.id
// color: "white"
// }
// }
LazyLoader {
active: item.targetMenuOpen
PopupWindow {
id: rightclickMenu
anchor.window: root.bar
visible: true
Component.onCompleted: () => {
root.bar.focusable = true;
const pos = mouseArea.mapToGlobal(mouseArea.x, mouseArea.y);
anchor.rect.x = pos.x;
anchor.rect.y = pos.y + mouseArea.height;
}
Connections {
target: item
function onTargetMenuOpenChanged() {
root.bar.focusable = false;
}
}
contentItem {
focus: true
Keys.onEscapePressed: item.targetMenuOpen = false
}
implicitWidth: Math.max(1, menuView.implicitWidth)
implicitHeight: Math.max(1, menuView.implicitHeight)
color: "transparent"
Rectangle {
anchors.fill: parent
color: "#9B5B36"
MenuView {
id: menuView
anchors.fill: parent
menu: item.modelData.menu
onClose: item.targetMenuOpen = false
}
}
}
}
}
}
}
}
}
}