last 7 months of qs changes

This commit is contained in:
outfoxxed 2025-01-06 00:13:19 -08:00
parent 2c64563ade
commit 4b90113a54
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
103 changed files with 3467 additions and 1415 deletions

View file

@ -1,3 +1,4 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
@ -5,7 +6,8 @@ import Quickshell
import "systray" as SysTray
import "audio" as Audio
import "mpris" as Mpris
import "workspaces" as Workspaces
import "power" as Power
import "root:notifications" as Notifs
BarContainment {
id: root
@ -13,6 +15,7 @@ BarContainment {
property bool isSoleBar: Quickshell.screens.length == 1;
ColumnLayout {
anchors {
left: parent.left
right: parent.right
@ -21,24 +24,32 @@ BarContainment {
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Loader {
active: isSoleBar
Layout.preferredHeight: active ? implicitHeight : 0;
Notifs.NotificationWidget {
Layout.fillWidth: true
sourceComponent: Workspaces.Widget {
bar: root
wsBaseIndex: 1
}
bar: root
}
Workspaces.Widget {
bar: root
Layout.fillWidth: true
wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1;
hideWhenEmpty: isSoleBar
ColumnLayout {
spacing: 0
Loader {
active: root.isSoleBar
Layout.preferredHeight: active ? implicitHeight : 0;
Layout.fillWidth: true
sourceComponent: Workspaces {
bar: root
wsBaseIndex: 1
}
}
Workspaces {
bar: root
Layout.fillWidth: true
wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1;
hideWhenEmpty: root.isSoleBar
}
}
}
}
@ -65,8 +76,15 @@ BarContainment {
Layout.fillWidth: true
}
ClockWidget {
Power.Power {
bar: root
Layout.fillWidth: true
}
ClockWidget {
bar: root
Layout.fillWidth: true
}
}
}

View file

@ -0,0 +1,34 @@
import QtQuick
import QtQuick.Effects
FullwidthMouseArea {
id: root
property bool showPressed: mouseArea.pressed;
property real baseMargin: 0;
property bool directScale: false;
readonly property Item contentItem: mContentItem;
default property alias contentItemData: mContentItem.data;
property real targetBrightness: root.showPressed ? -25 : root.mouseArea.containsMouse && root.enabled ? 75 : 0
Behavior on targetBrightness { SmoothedAnimation { velocity: 600 } }
property real targetMargins: root.showPressed ? 3 : 0;
Behavior on targetMargins { SmoothedAnimation { velocity: 25 } }
hoverEnabled: true
Item {
id: mContentItem
anchors.fill: parent;
anchors.margins: root.baseMargin + (root.directScale ? 0 : root.targetMargins);
scale: root.directScale ? (width - root.targetMargins * 2) / width : 1.0;
opacity: root.enabled ? 1.0 : 0.5;
Behavior on opacity { SmoothedAnimation { velocity: 5 } }
layer.enabled: root.targetBrightness != 0
layer.effect: MultiEffect { brightness: root.targetBrightness / 1000 }
}
}

View file

@ -1,8 +1,8 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "root:."
import "root:lock" as Lock
import ".."
import "../lock" as Lock
PanelWindow {
id: root
@ -20,8 +20,8 @@ PanelWindow {
exclusiveZone: width - margins.left
color: "transparent"
WlrLayershell.namespace: "shell:bar"
WlrLayershell.namespace: "shell:bar"
readonly property var tooltip: tooltip;
Tooltip {

View file

@ -1,31 +0,0 @@
import QtQuick
BarWidgetInner {
implicitHeight: 50
SequentialAnimation on implicitHeight {
loops: Animation.Infinite
PropertyAnimation { to: 70; duration: 1000 }
PropertyAnimation { to: 40; duration: 1000 }
}
property int len: 1
Text {
anchors.centerIn: parent
text: `8${'='.repeat(len)}D`
font.pointSize: 16
color: "white"
PropertyAnimation on rotation {
loops: Animation.Infinite
to: 365
duration: 1000
}
}
MouseArea {
anchors.fill: parent
onClicked: len += 1;
}
}

View file

@ -0,0 +1,19 @@
import QtQuick
BarButton {
id: root
required property string image;
property alias cache: imageComponent.cache;
property alias asynchronous: imageComponent.asynchronous;
property bool scaleIcon: !asynchronous
Image {
id: imageComponent
anchors.fill: parent
source: root.image
sourceSize.width: scaleIcon ? width : (root.width - baseMargin)
sourceSize.height: scaleIcon ? height : (root.height - baseMargin)
cache: false
}
}

View file

@ -1,41 +1,67 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import ".."
OverlayWidget {
expandedWidth: 600
expandedHeight: 600
BarWidgetInner {
id: root
required property var bar;
BarWidgetInner {
implicitHeight: layout.implicitHeight
implicitHeight: layout.implicitHeight
ColumnLayout {
id: layout
spacing: 0
SystemClock {
id: clock
precision: tooltip.visible ? SystemClock.Seconds : SystemClock.Minutes;
}
anchors {
right: parent.right
left: parent.left
BarButton {
id: button
anchors.fill: parent
fillWindowWidth: true
acceptedButtons: Qt.NoButton
ColumnLayout {
id: layout
spacing: 0
anchors {
right: parent.right
left: parent.left
}
Text {
Layout.alignment: Qt.AlignHCenter
text: {
const hours = clock.hours.toString().padStart(2, '0')
const minutes = clock.minutes.toString().padStart(2, '0')
return `${hours}\n${minutes}`
}
font.pointSize: 18
color: "white"
}
}
}
property TooltipItem tooltip: TooltipItem {
id: tooltip
tooltip: bar.tooltip
owner: root
show: button.containsMouse
Loader {
active: tooltip.visible
sourceComponent: Label {
text: {
// SystemClock can send an update slightly (<50ms) before the
// time changes. We use its readout so the widget and tooltip match.
const hours = clock.hours.toString().padStart(2, '0');
const minutes = clock.minutes.toString().padStart(2, '0');
const seconds = clock.seconds.toString().padStart(2, '0');
return `${hours}:${minutes}:${seconds}\n` + new Date().toLocaleString(Qt.locale("en_US"), "dddd, MMMM d, yyyy");
}
}
Text {
Layout.alignment: Qt.AlignHCenter
text: ShellGlobals.time.getHours().toString().padStart(2, '0')
font.pointSize: 18
color: "#a0ffffff"
}
Text {
Layout.alignment: Qt.AlignHCenter
text: ShellGlobals.time.getMinutes().toString().padStart(2, '0')
font.pointSize: 18
color: "#a0ffffff"
}
}
MouseArea {
anchors.fill: parent
onClicked: expanded = !expanded
}
}
}

View file

@ -1,97 +0,0 @@
import QtQuick
import Quickshell
import ".."
Item {
required property var bar;
required property real expandedWidth;
required property real expandedHeight;
required default property Item widget;
property bool expanded: false;
onExpandedChanged: {
animateTo(expanded ? 1.0 : 0.0)
if (expanded) popupSurface.activeOverlay = this
}
readonly property bool fullyCollapsed: animationProgress == 0.0;
onFullyCollapsedChanged: {
if (fullyCollapsed && popupSurface.activeOverlay == this) {
popupSurface.activeOverlay = null;
}
/*if (fullyCollapsed) {
widget.x = Qt.binding(() => this.x)
}*/
}
readonly property rect collapsedLayerRect: {
void [barWindow.windowTransform, popupSurface.windowTransform, y, parent.y];
return this.mapToItem(popupSurface.contentItem, 0, 0, width, height);
}
onCollapsedLayerRectChanged: console.log(`clr: ${collapsedLayerRect}`)
onLayerRectChanged: console.log(`lr: ${layerRect}`)
onYChanged: console.log(`y: ${y}`)
readonly property rect expandedLayerRect: bar.widgetSurface.expandedPosition(this)
readonly property rect layerRect: {
const [p, xCurve, yCurve] = [animationProgress, ShellGlobals.popoutXCurve, ShellGlobals.popoutYCurve];
return Qt.rect(
xCurve.interpolate(p, collapsedLayerRect.x, expandedLayerRect.x),
yCurve.interpolate(p, collapsedLayerRect.y, expandedLayerRect.y),
xCurve.interpolate(p, collapsedLayerRect.width, expandedLayerRect.width),
yCurve.interpolate(p, collapsedLayerRect.height, expandedLayerRect.height),
);
}
implicitWidth: widget.implicitWidth
implicitHeight: widget.implicitHeight
Component.onCompleted: {
popupSurface.connectOverlay(this);
widget.x = Qt.binding(() => layerRect.x);
widget.y = Qt.binding(() => layerRect.y);
widget.width = Qt.binding(() => layerRect.width);
widget.height = Qt.binding(() => layerRect.height);
}
Component.onDestruction: {
popupSurface.disconnectOverlay(this)
}
function animateTo(target: real) {
animationProgressInternal = target * 1000
}
property real animationProgress: animationProgressInternal * 0.001
property real animationProgressInternal: 0.0 // animations seem to only have int precision
Behavior on animationProgressInternal {
SmoothedAnimation { velocity: 3000 }
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onPressed: expanded = false
Rectangle {
anchors.fill: parent
border.color: ShellGlobals.colors.widgetOutline
border.width: 1
radius: 5
color: "transparent"
opacity: mouseArea.containsMouse ? 1.0 : 0.0
Behavior on opacity {
SmoothedAnimation { velocity: 4 }
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -1,17 +0,0 @@
import QtQuick
Item {
default property Item item;
property int expandedWidth;
property int expandedHeight;
implicitHeight: item.implicitHeight
implicitWidth: item.implicitWidth
Component.onCompleted: {
item.width = Qt.binding(() => this.width)
item.height = Qt.binding(() => this.height)
}
children: [ item ]
}

View file

@ -1,6 +1,7 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import "root:/"
Scope {
id: root
@ -11,11 +12,14 @@ Scope {
readonly property TooltipItem activeItem: activeMenu ?? activeTooltip;
property TooltipItem lastActiveItem: null;
readonly property TooltipItem shownItem: activeItem ?? lastActiveItem;
property real hangTime: lastActiveItem?.hangTime ?? 0;
property Item tooltipItem: null;
onActiveItemChanged: {
if (activeItem != null) {
hangTimer.stop();
activeItem.targetVisible = true;
if (tooltipItem) {
@ -24,10 +28,12 @@ Scope {
}
if (lastActiveItem != null && lastActiveItem != activeItem) {
lastActiveItem.targetVisible = false;
if (activeItem != null) lastActiveItem.targetVisible = false;
else if (root.hangTime == 0) doLastHide();
else hangTimer.start();
}
lastActiveItem = activeItem;
if (activeItem != null) lastActiveItem = activeItem;
}
function setItem(item: TooltipItem) {
@ -46,28 +52,59 @@ Scope {
}
}
function doLastHide() {
lastActiveItem.targetVisible = false;
}
function onHidden(item: TooltipItem) {
if (item == lastActiveItem) {
lastActiveItem = null;
}
}
Timer {
id: hangTimer
interval: root.hangTime
onTriggered: doLastHide();
}
property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0;
Behavior on scaleMul { SmoothedAnimation { velocity: 5 } }
LazyLoader {
id: popupLoader
activeAsync: activeItem != null
activeAsync: shownItem != null
PopupWindow {
id: popup
parentWindow: bar
relativeX: bar.tooltipXOffset
relativeY: 0
height: bar.height
width: 1000//Math.max(1, widthAnim.running ? Math.max(tooltipItem.targetWidth, tooltipItem.lastTargetWidth) : tooltipItem.targetWidth)
anchor {
window: bar
rect.x: bar.tooltipXOffset
rect.y: tooltipItem.highestAnimY
adjustment: PopupAdjustment.None
}
HyprlandWindow.opacity: root.scaleMul
//height: bar.height
width: Math.max(700, tooltipItem.largestAnimWidth) // max due to qtwayland glitches
height: {
const h = tooltipItem.lowestAnimY - tooltipItem.highestAnimY
//console.log(`seth ${h} ${tooltipItem.highestAnimY} ${tooltipItem.lowestAnimY}; ${tooltipItem.y1} ${tooltipItem.y2}`)
return h
}
visible: true
color: "transparent"
//color: "#20000000"
mask: Region {
item: (activeItem?.hoverable ?? false) ? tooltipItem : null
item: (shownItem?.hoverable ?? false) ? tooltipItem : null
}
HyprlandFocusGrab {
active: activeItem?.isMenu ?? false
windows: [ popup, bar ]
windows: [ popup, bar, ...(activeItem?.grabWindows ?? []) ]
onActiveChanged: {
if (!active && activeItem?.isMenu) {
activeMenu.close()
@ -75,30 +112,76 @@ Scope {
}
}
/*Rectangle {
color: "#10ff0000"
//y: tooltipItem.highestAnimY
height: tooltipItem.lowestAnimY - tooltipItem.highestAnimY
width: parent.width
}
Rectangle {
color: "#1000ff00"
//y: tooltipItem.highestAnimY
height: popup.height
width: parent.width
}*/
Item {
id: tooltipItem
Component.onCompleted: {
root.tooltipItem = this;
if (root.activeItem) {
root.activeItem.parent = this;
if (root.shownItem) {
root.shownItem.parent = this;
}
//highestAnimY = targetY - targetHeight / 2;
//lowestAnimY = targetY + targetHeight / 2;
}
transform: Scale {
origin.x: 0
origin.y: tooltipItem.height / 2
xScale: 0.9 + scaleMul * 0.1
yScale: xScale
}
clip: width != targetWidth || height != targetHeight
// bkg
BarWidgetInner {
anchors.fill: parent
color: ShellGlobals.colors.bar
}
readonly property var targetWidth: shownItem?.implicitWidth ?? 0;
readonly property var targetHeight: shownItem?.implicitHeight ?? 0;
property var largestAnimWidth: 0;
property var highestAnimY: 0; // unused due to reposition timing issues
property var lowestAnimY: bar.height;
onTargetWidthChanged: {
if (targetWidth > largestAnimWidth) {
largestAnimWidth = targetWidth;
}
}
readonly property var targetWidth: activeItem?.implicitWidth ?? 0;
readonly property var targetHeight: activeItem?.implicitHeight ?? 0;
onTargetYChanged: updateYBounds();
onTargetHeightChanged: updateYBounds();
function updateYBounds() {
if (targetY - targetHeight / 2 < highestAnimY) {
//highestAnimY = targetY - targetHeight / 2
}
property var lastTargetWidthTracker: 0;
property var lastTargetWidth: 0;
onTargetWidthChanged: {
lastTargetWidth = lastTargetWidthTracker;
lastTargetWidthTracker = targetWidth;
if (targetY + targetHeight / 2 > lowestAnimY) {
//lowestAnimY = targetY + targetHeight / 2
}
}
readonly property real targetY: {
if (activeItem == null) return 0;
const target = bar.contentItem.mapFromItem(activeItem.owner, 0, activeItem.targetRelativeY).y;
return bar.boundedY(target, activeItem.implicitHeight / 2);
if (shownItem == null) return 0;
const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y;
return bar.boundedY(target, shownItem.implicitHeight / 2);
}
property var w: -1
@ -107,15 +190,24 @@ Scope {
property var y1: -1
property var y2: -1
y: y1
y: y1 - popup.anchor.rect.y
height: y2 - y1
SmoothedAnimation {
target: tooltipItem;
property: "y1";
readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running
onAnyAnimsRunningChanged: {
if (!anyAnimsRunning) {
largestAnimWidth = targetWidth
//highestAnimY = y1;
//lowestAnimY = y2;
}
}
SmoothedAnimation on y1 {
id: y1Anim
to: tooltipItem.targetY - tooltipItem.targetHeight / 2;
onToChanged: {
if (tooltipItem.y1 == -1 || !(activeItem?.animateSize ?? true)) {
if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.y1 = to;
} else {
@ -125,12 +217,11 @@ Scope {
}
}
SmoothedAnimation {
target: tooltipItem
property: "y2"
SmoothedAnimation on y2 {
id: y2Anim
to: tooltipItem.targetY + tooltipItem.targetHeight / 2;
onToChanged: {
if (tooltipItem.y2 == -1 || !(activeItem?.animateSize ?? true)) {
if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.y2 = to;
} else {
@ -140,13 +231,11 @@ Scope {
}
}
SmoothedAnimation {
SmoothedAnimation on w {
id: widthAnim
target: tooltipItem
property: "w"
to: tooltipItem.targetWidth;
onToChanged: {
if (tooltipItem.w == -1) {
if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.w = to;
} else {

View file

@ -1,12 +1,12 @@
import QtQuick
import Quickshell
import "root:/"
Item {
id: root
required property var tooltip;
required property Item owner;
property bool isMenu: false;
property list<QtObject> grabWindows;
property bool hoverable: isMenu;
property bool animateSize: true;
property bool show: false;
@ -17,47 +17,35 @@ Item {
signal close();
readonly property alias contentItem: contentItem;
default property alias data: contentItem.data;
property Component backgroundComponent: BarWidgetInner {
color: ShellGlobals.colors.bar
anchors.fill: parent
}
property Component backgroundComponent: null
onShowChanged: {
if (show) {
hangTimer.stop();
tooltip.setItem(this);
} else if (hangTime == 0) {
tooltip.removeItem(this);
} else hangTimer.start();
}
Timer {
id: hangTimer
interval: hangTime
onTriggered: tooltip.removeItem(root);
if (show) tooltip.setItem(this);
else tooltip.removeItem(this);
}
property bool targetVisible: false
property real targetOpacity: 0
opacity: targetOpacity / 1000
opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul))
Behavior on targetOpacity {
id: opacityAnimation
SmoothedAnimation { velocity: 5000 }
SmoothedAnimation { velocity: 5 }
}
function snapOpacity(opacity: real) {
opacityAnimation.enabled = false;
targetOpacity = opacity * 1000
targetOpacity = opacity;
opacityAnimation.enabled = true;
}
onTargetVisibleChanged: {
if (targetVisible) {
visible = true;
targetOpacity = 1000;
targetOpacity = 1;
} else {
close()
targetOpacity = 0;
@ -68,20 +56,21 @@ Item {
if (!targetVisible && targetOpacity == 0) {
visible = false;
this.parent = null;
if (tooltip) tooltip.onHidden(this);
}
}
anchors.fill: parent
visible: false
clip: true
implicitHeight: contentItem.implicitHeight + 10
implicitWidth: contentItem.implicitWidth + 10
//clip: true
implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
readonly property Item item: contentItem;
Loader {
anchors.fill: parent
active: root.visible || root.preloadBackground
active: root.backgroundComponent && (root.visible || root.preloadBackground)
asynchronous: !root.visible && root.preloadBackground
sourceComponent: backgroundComponent
}
@ -91,7 +80,7 @@ Item {
anchors.fill: parent
anchors.margins: 5
implicitHeight: childrenRect.height
implicitWidth: childrenRect.width
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
}
}

View file

@ -1,17 +1,21 @@
pragma ComponentBehavior: Bound;
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import ".."
import "root:."
MouseArea {
FullwidthMouseArea {
id: root
required property var bar;
required property int wsBaseIndex;
property int wsCount: 10;
property bool hideWhenEmpty: false;
implicitHeight: column.implicitHeight + 10;
fillWindowWidth: true
acceptedButtons: Qt.NoButton
onWheel: event => {
@ -29,9 +33,6 @@ MouseArea {
property int existsCount: 0;
visible: !hideWhenEmpty || existsCount > 0;
property real animPos: 0;
Behavior on animPos { SmoothedAnimation { velocity: 100 } }
// destructor takes care of nulling
signal workspaceAdded(workspace: HyprlandWorkspace);
@ -45,20 +46,22 @@ MouseArea {
}
Repeater {
model: 10
model: root.wsCount
MouseArea {
FullwidthMouseArea {
id: wsItem
onPressed: Hyprland.dispatch(`workspace ${wsIndex}`);
Layout.fillWidth: true
implicitHeight: 15
fillWindowWidth: true
required property int index;
property int wsIndex: wsBaseIndex + index;
property int wsIndex: root.wsBaseIndex + index;
property HyprlandWorkspace workspace: null;
property bool exists: workspace != null;
property bool active: (monitor?.activeWorkspace ?? false) && monitor.activeWorkspace == workspace;
property bool active: (root.monitor?.activeWorkspace ?? false) && root.monitor.activeWorkspace == workspace;
onActiveChanged: {
if (active) root.currentIndex = wsIndex;
@ -78,21 +81,21 @@ MouseArea {
}
}
property real animActive: active ? 100 : 0
Behavior on animActive { NumberAnimation { duration: 100 } }
property real animActive: active ? 1 : 0
Behavior on animActive { NumberAnimation { duration: 150 } }
property real animExists: exists ? 100 : 0
property real animExists: exists ? 1 : 0
Behavior on animExists { NumberAnimation { duration: 100 } }
Rectangle {
anchors.centerIn: parent
height: 10
width: parent.width
scale: 1 + animActive * 0.003
scale: 1 + wsItem.animActive * 0.3
radius: height / 2
border.color: ShellGlobals.colors.widgetOutline
border.width: 1
color: ShellGlobals.interpolateColors(animExists * 0.01, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
color: ShellGlobals.interpolateColors(animExists, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
}
}
}

View file

@ -12,14 +12,20 @@ ClickableIcon {
implicitHeight: width;
acceptedButtons: Qt.LeftButton | Qt.RightButton;
showPressed: mixerOpen
fillWindowWidth: true
showPressed: mixerOpen || (pressedButtons & ~Qt.RightButton)
onPressed: event => {
event.accepted = true;
if (event.button === Qt.RightButton) {
mixerOpen = !mixerOpen;
}
}
onClicked: event => {
event.accepted = true;
if (event.button === Qt.LeftButton) {
event.accepted = true;
node.audio.muted = !node.audio.muted;
} else if (event.button === Qt.RightButton) {
mixerOpen = !mixerOpen;
}
}
@ -71,7 +77,16 @@ ClickableIcon {
sourceComponent: Mixer {
width: 550
trackedNode: node
nodeList: Pipewire.nodes.values.filter(n => n.audio && !n.isStream && n.isSink == node.isSink)
nodeImage: root.image
onSelected: n => {
if (node.isSink) {
Pipewire.preferredDefaultAudioSink = n;
} else {
Pipewire.preferredDefaultAudioSource = n;
}
}
}
}
}

View file

@ -1,12 +1,18 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import ".."
import "../.."
ColumnLayout {
id: root
required property PwNode trackedNode;
required property string nodeImage;
required property list<PwNode> nodeList;
signal selected(node: PwNode);
PwNodeLinkTracker {
id: linkTracker
@ -15,10 +21,13 @@ ColumnLayout {
PwObjectTracker { objects: [ trackedNode, ...linkTracker.linkGroups ] }
MixerEntry {
MixerEntry/*WithSelect*/ {
id: nodeEntry
node: trackedNode
//nodeList: root.nodeList
image: nodeImage
Component.onCompleted: this.selected.connect(root.selected);
}
Rectangle {
@ -49,7 +58,7 @@ ColumnLayout {
// special cases :(
if (icon == "firefox") icon = "firefox-devedition";
return `image://icon/${icon}`
return Quickshell.iconPath(icon)
}
}
}

View file

@ -1,51 +1,11 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import ".."
RowLayout {
MixerEntryBase {
id: root
required property PwNode node;
required property string image;
property int state: PwLinkState.Unlinked;
PwObjectTracker { objects: [ node ] }
ClickableIcon {
image: root.image
asynchronous: true
implicitHeight: 40
implicitWidth: height
}
ColumnLayout {
RowLayout {
Item {
implicitHeight: title.implicitHeight
Layout.fillWidth: true
Text {
id: title
color: "white"
anchors.fill: parent
elide: Text.ElideRight
text: {
const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description);
const mediaName = node.properties["media.name"];
return mediaName != undefined ? `${name} - ${mediaName}` : name;
}
}
}
}
VolumeSlider {
//Layout.fillHeight: true
Layout.fillWidth: true
implicitWidth: 200
value: node.audio.volume
onValueChanged: node.audio.volume = value
}
headerComponent: Text {
color: "white"
elide: Text.ElideRight
text: root.getNodeName(root.node)
}
}

View file

@ -0,0 +1,50 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import ".."
RowLayout {
id: root
required property PwNode node;
required property string image;
required property Item headerComponent;
property int state: PwLinkState.Unlinked;
function getNodeName(node: PwNode): string {
const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description);
const mediaName = node.properties["media.name"];
return mediaName != undefined ? `${name} - ${mediaName}` : name + node.id;
}
PwObjectTracker { objects: [ node ] }
ClickableIcon {
image: root.image
asynchronous: true
implicitHeight: 40
implicitWidth: height
}
ColumnLayout {
Item {
id: container
Layout.fillWidth: true
implicitWidth: headerComponent.implicitWidth
implicitHeight: headerComponent.implicitHeight
children: [ headerComponent ]
Binding { root.headerComponent.anchors.fill: container }
}
VolumeSlider {
Layout.fillWidth: true
value: node.audio.volume
onValueChanged: node.audio.volume = value
}
}
}

View file

@ -0,0 +1,16 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Services.Pipewire
MixerEntryBase {
id: root
required property list<PwNode> nodeList;
signal selected(node: PwNode);
headerComponent: ComboBox {
model: nodeList.map(node => root.getNodeName(node));
currentIndex: nodeList.findIndex(node => node == root.node)
onActivated: index => root.selected(nodeList[index])
}
}

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e8eaed"><path d="M806-56 677.67-184.33q-27 18.66-58 32.16-31 13.5-64.34 21.17v-68.67q20-6.33 38.84-13.66 18.83-7.34 35.5-19l-154.34-155V-160l-200-200h-160v-240H262L51.33-810.67 98.67-858l754.66 754L806-56Zm-26.67-232-48-48q19-33 28.17-69.62 9.17-36.61 9.17-75.38 0-100.22-58.34-179.11Q652-739 555.33-762.33V-831q124 28 202 125.5t78 224.5q0 51.67-14.16 100.67-14.17 49-41.84 92.33Zm-134-134-90-90v-130q47 22 73.5 66t26.5 96q0 15-2.5 29.5t-7.5 28.5Zm-170-170-104-104 104-104v208Zm-66.66 270v-131.33l-80-80H182v106.66h122L408.67-322Zm-40-171.33Z"/></svg>

Before

Width:  |  Height:  |  Size: 650 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e8eaed"><path d="M560-131v-68.67q94.67-27.33 154-105 59.33-77.66 59.33-176.33 0-98.67-59-176.67-59-78-154.33-104.66V-831q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm426.67 45.33v-332Q599-628 629.5-582T660-480q0 55-30.83 100.83-30.84 45.84-82.5 64.5ZM413.33-634l-104 100.67H186.67v106.66h122.66l104 101.34V-634Zm-96 154Z"/></svg>

Before

Width:  |  Height:  |  Size: 474 B

View file

@ -82,7 +82,7 @@ BarWidgetInner {
x: blurRadius
width: blur.width - blurRadius * 2
height: blur.height
clip: true
GaussianBlur {
source: blurSource
x: -parent.x
@ -139,23 +139,27 @@ BarWidgetInner {
readonly property Rectangle overlay: overlayItem;
Rectangle {
id: overlayItem
visible: false
visible: true
anchors.fill: parent
border.color: ShellGlobals.colors.widgetOutlineSeparate
border.width: 0//1
radius: 0//root.radius
radius: root.radius
color: "transparent"
Rectangle {
anchors.fill: parent
radius: root.radius
color: "transparent"
border.color: ShellGlobals.colors.widgetOutlineSeparate;
border.width: 1
}
}
// slightly offset on the corners :/
layer.enabled: true
layer.effect: ShaderEffect {
fragmentShader: "radial_clip.frag.qsb"
// +1 seems to match Rectangle
property real radius: root.radius + 1
property size size: Qt.size(root.width, root.height)
property real borderWidth: 1//.5
property color borderColor: ShellGlobals.colors.widgetOutlineSeparate//"#ffff0000"
property color tint: overlayItem.color
layer.effect: OpacityMask {
maskSource: Rectangle {
width: root.width
height: root.height
radius: root.radius
}
}
}

View file

@ -1,9 +1,11 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQml.Models
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
import Quickshell.Hyprland
import "../.."
Singleton {
@ -15,51 +17,58 @@ Singleton {
property bool __reverse: false;
property var activeTrack;
Component.onCompleted: {
for (const player of Mpris.players.values) {
if (player.playbackState == MprisPlaybackState.Playing) {
if (root.trackedPlayer == null) {
root.trackedPlayer = player;
Instantiator {
model: Mpris.players;
Connections {
required property MprisPlayer modelData;
target: modelData;
Component.onCompleted: {
if (root.trackedPlayer == null || modelData.isPlaying) {
root.trackedPlayer = modelData;
}
}
player.playbackStateChanged.connect(() => {
if (root.trackedPlayer !== player) root.trackedPlayer = player;
});
Component.onDestruction: {
if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
for (const player of Mpris.players.values) {
if (player.playbackState.isPlaying) {
root.trackedPlayer = player;
break;
}
}
if (trackedPlayer == null && Mpris.players.values.length != 0) {
trackedPlayer = Mpris.players.values[0];
}
}
}
function onPlaybackStateChanged() {
if (root.trackedPlayer !== modelData) root.trackedPlayer = modelData;
}
}
}
Connections {
target: activePlayer
function onTrackChanged() {
function onPostTrackChanged() {
root.updateTrack();
}
}
// Change the tracked player when one changes playback state or is created in a playing state.
Connections {
target: Mpris.players;
function onTrackArtUrlChanged() {
console.log("arturl:", activePlayer.trackArtUrl)
//root.updateTrack();
if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) {
// cantata likes to send cover updates *BEFORE* updating the track info.
// as such, art url changes shouldn't be able to break the reverse animation
const r = root.__reverse;
root.updateTrack();
root.__reverse = r;
function onObjectInsertedPost(player: MprisPlayer) {
if (player.playbackState === MprisPlaybackState.Playing) {
if (root.trackedPlayer !== player) root.trackedPlayer = player;
}
player.playbackStateChanged.connect(() => {
if (root.trackedPlayer !== player) root.trackedPlayer = player;
});
}
function onObjectRemovedPre() {
console.log(`trackedPlayer: ${root.trackedPlayer}`)
if (root.trackedPlayer == null) {
for (const player of Mpris.players.values) {
if (player.playbackState === MprisPlaybackState.Playing) {
root.trackedPlayer = player;
break;
}
}
}
}
}
@ -67,27 +76,23 @@ Singleton {
onActivePlayerChanged: this.updateTrack();
function updateTrack() {
const metadata = this.activePlayer?.metadata ?? {};
//console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`)
this.activeTrack = {
artUrl: metadata["mpris:artUrl"] ?? "",
title: metadata["xesam:title"] ?? "",
artist: metadata["xesam:artist"] ?? "",
uniqueId: this.activePlayer?.uniqueId ?? 0,
artUrl: this.activePlayer?.trackArtUrl ?? "",
title: this.activePlayer?.trackTitle || "Unknown Title",
artist: this.activePlayer?.trackArtist || "Unknown Artist",
album: this.activePlayer?.trackAlbum || "Unknown Album",
};
this.trackChanged(__reverse);
this.__reverse = false;
}
property bool isPlaying: this.activePlayer && this.activePlayer.playbackState == MprisPlaybackState.Playing;
property bool canPlay: this.activePlayer?.canPlay ?? false;
function play() {
if (this.canPlay) this.activePlayer.playbackState = MprisPlaybackState.Playing;
}
property bool canPause: this.activePlayer?.canPause ?? false;
function pause() {
if (this.canPause) this.activePlayer.playbackState = MprisPlaybackState.Paused;
property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying;
property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false;
function togglePlaying() {
if (this.canTogglePlaying) this.activePlayer.togglePlaying();
}
property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false;
@ -125,7 +130,7 @@ Singleton {
}
function setActivePlayer(player: MprisPlayer) {
const targetPlayer = player ?? MprisPlayer.players[0];
const targetPlayer = player ?? Mpris.players[0];
console.log(`setactive: ${targetPlayer} from ${activePlayer}`)
if (targetPlayer && this.activePlayer) {
@ -138,31 +143,17 @@ Singleton {
this.trackedPlayer = targetPlayer;
}
Shortcut {
name: "music-pauseall";
onPressed: {
for (let i = 0; i < Mpris.players.length; i++) {
const player = Mpris.players[i];
if (player.canPause) player.playbackState = MprisPlaybackState.Paused;
IpcHandler {
target: "mpris"
function pauseAll(): void {
for (const player of Mpris.players.values) {
if (player.canPause) player.pause();
}
}
}
Shortcut {
name: "music-playpause";
onPressed: {
if (root.isPlaying) root.pause();
else root.play();
}
}
Shortcut {
name: "music-previous";
onPressed: root.previous();
}
Shortcut {
name: "music-next";
onPressed: root.next();
function playPause(): void { root.togglePlaying(); }
function previous(): void { root.previous(); }
function next(): void { root.next(); }
}
}

View file

@ -1,7 +0,0 @@
import Quickshell
import Quickshell.Services.Mpris
Scope {
required property MprisPlayer player;
}

View file

@ -1,2 +0,0 @@
import QtQuick
import

View file

@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@ -7,9 +9,10 @@ import Quickshell.Services.Mpris
import ".."
import "../.."
MouseArea {
FullwidthMouseArea {
id: root
hoverEnabled: true
fillWindowWidth: true
required property var bar;
implicitHeight: column.implicitHeight + 10
@ -27,12 +30,12 @@ MouseArea {
property alias widgetOpen: persist.widgetOpen;
acceptedButtons: Qt.RightButton
onClicked: widgetOpen = !widgetOpen
onPressed: widgetOpen = !widgetOpen
onWheel: event => {
event.accepted = true;
if (MprisController.canChangeVolume) {
this.activePlayer.volume = Math.max(0, Math.min(1, this.activePlayer.volume + (event.angleDelta.y / 120) * 0.05));
root.activePlayer.volume = Math.max(0, Math.min(1, root.activePlayer.volume + (event.angleDelta.y / 120) * 0.05));
}
}
@ -42,7 +45,7 @@ MouseArea {
id: widget
anchors.fill: parent
property real scaleMul: root.pressed || widgetOpen ? 100 : 1
property real scaleMul: widgetOpen ? 100 : 1
Behavior on scaleMul { SmoothedAnimation { velocity: 600 } }
scale: scaleCurve.interpolate(scaleMul / 100, 1, (width - 6) / width)
@ -56,8 +59,10 @@ MouseArea {
BackgroundArt {
id: bkg
anchors.fill: parent
overlay.color: "#30000000"
function updateArt(reverse: bool) {
console.log("update art", MprisController.activeTrack.artUrl)
this.setArt(MprisController.activeTrack.artUrl, reverse, false)
}
@ -86,6 +91,7 @@ MouseArea {
implicitHeight: width
scaleIcon: false
baseMargin: 3
hoverEnabled: false
enabled: MprisController.canGoPrevious;
onClicked: MprisController.previous();
}
@ -95,11 +101,9 @@ MouseArea {
image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`;
implicitHeight: width
scaleIcon: false
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay;
onClicked: {
if (MprisController.isPlaying) MprisController.pause();
else MprisController.play();
}
hoverEnabled: false
enabled: MprisController.canTogglePlaying;
onClicked: MprisController.togglePlaying();
}
ClickableIcon {
@ -108,51 +112,162 @@ MouseArea {
implicitHeight: width
scaleIcon: false
baseMargin: 3
hoverEnabled: false
enabled: MprisController.canGoNext;
onClicked: MprisController.next();
}
}
property var tooltip: TooltipItem {
property Scope positionInfo: Scope {
id: positionInfo
property var player: root.activePlayer;
property int position: Math.floor(player.position);
property int length: Math.floor(player.length);
FrameAnimation {
id: posTracker;
running: positionInfo.player.isPlaying && (tooltip.visible || rightclickMenu.visible);
onTriggered: positionInfo.player.positionChanged();
}
function timeStr(time: int): string {
const seconds = time % 60;
const minutes = Math.floor(time / 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
}
property TooltipItem tooltip: TooltipItem {
id: tooltip
tooltip: bar.tooltip
owner: root
show: root.containsMouse && (activePlayer?.metadata["mpris:trackid"] ?? false)
show: root.containsMouse
//implicitHeight: root.height - 10
//implicitWidth: childrenRect.width
/*ColumnLayout {
ColumnLayout {
visible: MprisController.activePlayer != null
Label { text: MprisController.activeTrack?.title ?? "" }
Label {
text: {
const artist = MprisController.activeTrack?.artist ?? "";
const album = MprisController.activeTrack?.album ?? "";
return artist + (album ? ` - ${album}` : "");
}
}
Label { text: MprisController.activePlayer?.identity ?? "" }
}
Label {
visible: MprisController.activePlayer == null
text: "No media playing"
}
Rectangle { implicitHeight: 10; color: "white"; Layout.fillWidth: true }
}*/
contentItem.anchors.margins: 0
Item {
implicitWidth: 200
implicitHeight: 100
}
id: ttcontent
width: parent.width
height: Math.max(parent.height, implicitHeight)
implicitWidth: cl.implicitWidth + 10
implicitHeight: cl.implicitHeight + 10 + (MprisController.activePlayer ? 8 : 0)
/*Loader {
active: tooltip.visible
ColumnLayout {
id: cl
anchors {
left: parent.left
right: parent.right
top: parent.top
margins: 5
}
sourceComponent: ColumnLayout {
height: root.height - 10
RowLayout {
Image {
Layout.fillHeight: true
source: mainPlayer.metadata["mpris:artUrl"] ?? ""
//visible: MprisController.activePlayer != null
FontMetrics { id: fontmetrics }
component FullheightLabel: Item {
implicitHeight: fontmetrics.height
implicitWidth: label.implicitWidth
property alias text: label.text
cache: false
fillMode: Image.PreserveAspectCrop
sourceSize.width: height
sourceSize.height: height
}
Label {
text: mainPlayer.identity
id: label
anchors.verticalCenter: parent.verticalCenter
}
}
Slider {
Layout.fillWidth: true
FullheightLabel {
visible: MprisController.activePlayer != null
text: MprisController.activeTrack?.title ?? ""
}
FullheightLabel {
visible: MprisController.activePlayer != null
text: MprisController.activeTrack?.artist ?? ""
/*text: {
const artist = MprisController.activeTrack?.artist ?? "";
const album = MprisController.activeTrack?.album ?? "";
return artist + (album ? ` - ${album}` : "");
}*/
}
Label {
text: {
if (!MprisController.activePlayer) return "No media playing";
return MprisController.activePlayer?.identity + " - "
+ positionInfo.timeStr(positionInfo.position) + " / "
+ positionInfo.timeStr(positionInfo.length);
}
}
}
}*/
Rectangle {
id: ttprect
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
color: "#30ceffff"
implicitHeight: 8
visible: MprisController.activePlayer != null
Rectangle {
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
}
color: "#80ceffff"
width: parent.width * (root.activePlayer.position / root.activePlayer.length)
}
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: ttcontent.width
height: ttcontent.height
bottomLeftRadius: 5
bottomRightRadius: 5
}
}
}
}
property var rightclickMenu: TooltipItem {
@ -182,6 +297,7 @@ MouseArea {
target: MprisController
function onTrackChanged(reverse: bool) {
console.log(`track changed: rev: ${reverse}`)
popupBkg.setArt(MprisController.activeTrack.artUrl, reverse, false);
}
}
@ -191,71 +307,41 @@ MouseArea {
}
}
contentItem {
implicitWidth: 500
implicitHeight: 650
}
Loader {
active: rightclickMenu.visible
width: 500
height: 650
active: rightclickMenu.visible
sourceComponent: ColumnLayout {
property var player: activePlayer;
anchors.fill: parent;
property int position: 0;
property int length: 0;
FrameAnimation {
id: posTracker;
running: player.playbackState == MprisPlaybackState.Playing && widgetOpen;
onTriggered: player.positionChanged();
}
Connections {
target: player
function onPositionChanged() {
const newPosition = Math.floor(player.position);
if (newPosition != position) position = newPosition;
}
function onLengthChanged() {
const newLength = Math.floor(player.length);
if (newLength != length) length = newLength;
}
}
property var player: root.activePlayer;
Connections {
target: MprisController
function onTrackChanged(reverse: bool) {
trackStack.updateTrack(reverse, false);
length = Math.floor(player.length);
}
}
Component.onCompleted: {
position = Math.floor(player.position);
length = Math.floor(player.length);
}
function timeStr(time: int): string {
const seconds = time % 60;
const minutes = Math.floor(time / 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
Item {
id: playerSelectorContainment
Layout.fillWidth: true
implicitHeight: playerSelector.implicitHeight + 20
implicitWidth: playerSelector.implicitWidth
ScrollView {
RowLayout { //ScrollView {
id: playerSelector
anchors.centerIn: parent
width: Math.min(implicitWidth, playerSelectorContainment.width)
RowLayout {
//RowLayout {
Repeater {
model: Mpris.players
@ -281,8 +367,7 @@ MouseArea {
source: {
const entry = DesktopEntries.byId(modelData.desktopEntry);
console.log(`ent ${entry} id ${modelData.desktopEntry}`)
if (!entry) return "image://icon/";
return `image://icon/${entry.icon}`;
return Quickshell.iconPath(entry?.icon);
}
//asynchronous: true
@ -292,18 +377,18 @@ MouseArea {
}
}
}
}
//}
}
}
}
Item {
Layout.fillWidth: true
Layout.bottomMargin: 10
Layout.bottomMargin: 20
Label {
anchors.centerIn: parent
text: activePlayer.identity
text: root.activePlayer.identity
}
}
@ -311,7 +396,13 @@ MouseArea {
id: trackStack
Layout.fillWidth: true
implicitHeight: 400
clip: animating || (lastFlicked?.contentX ?? 0) != 0
// inverse of default tooltip margin - 1px for border
Layout.leftMargin: -4
Layout.rightMargin: -4
property Flickable lastFlicked;
property bool reverse: false;
Component.onCompleted: updateTrack(false, true);
@ -333,14 +424,16 @@ MouseArea {
// but may take longer if the image is huge.
readonly property bool svReady: img.status === Image.Ready;
contentWidth: width + 1
onDragStarted: trackStack.lastFlicked = this
onDragEnded: {
return;
//return;
console.log(`dragend ${contentX}`)
if (Math.abs(contentX) > 75) {
if (contentX < 0) MprisController.previous();
else if (contentX > 0) MprisController.next();
}
}
ColumnLayout {
id: trackContent
width: flickable.width
@ -348,7 +441,7 @@ MouseArea {
Item {
Layout.fillWidth: true
implicitHeight: 300//img.implicitHeight
implicitHeight: 302//img.implicitHeight
implicitWidth: img.implicitWidth
Image {
@ -362,27 +455,50 @@ MouseArea {
sourceSize.height: 300
sourceSize.width: 300
layer.enabled: true
layer.effect: OpacityMask {
cached: true
maskSource: Rectangle {
width: img.width
height: img.height
radius: 5
}
}
}
}
Item {
component CenteredText: Item {
Layout.fillWidth: true
Layout.topMargin: 20
property alias text: label.text
property alias font: label.font
Label {
id: label
visible: text != ""
anchors.centerIn: parent
text: track.title
elide: Text.ElideRight
width: Math.min(parent.width - 20, implicitWidth)
}
}
Item {
Layout.fillWidth: true
CenteredText {
Layout.topMargin: 20
text: track.title
font.pointSize: albumLabel.font.pointSize + 1
}
Label {
anchors.centerIn: parent
text: track.artist
}
CenteredText {
id: albumLabel
Layout.topMargin: 18
text: track.album
opacity: 0.8
}
CenteredText {
Layout.topMargin: 25
text: track.artist
}
Item { Layout.fillHeight: true }
@ -462,11 +578,8 @@ MouseArea {
implicitWidth: 80
implicitHeight: width
scaleIcon: false
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay
onClicked: {
if (MprisController.isPlaying) MprisController.pause();
else MprisController.play();
}
enabled: MprisController.canTogglePlaying;
onClicked: MprisController.togglePlaying();
}
ClickableIcon {
@ -491,33 +604,70 @@ MouseArea {
}
RowLayout {
Layout.margins: 5
Label {
Layout.preferredWidth: lengthLabel.implicitWidth
text: timeStr(position)
text: positionInfo.timeStr(positionInfo.position)
}
MediaSlider {
id: slider
property bool bindSlider: true;
property real boundAnimStart: 0;
property real boundAnimFactor: 1;
property real lastPosition: 0;
property real lastLength: 0;
property real boundPosition: {
const ppos = player.position / player.length;
const bpos = boundAnimStart;
return (ppos * boundAnimFactor) + (bpos * (1.0 - boundAnimFactor));
}
NumberAnimation {
id: boundAnim
target: slider
property: "boundAnimFactor"
from: 0
to: 1
duration: 600
easing.type: Easing.OutExpo
}
Connections {
target: player
function onPositionChanged() {
if (false && player.position == 0 && slider.lastPosition != 0 && !boundAnim.running) {
slider.boundAnimStart = slider.lastPosition / slider.lastLength;
boundAnim.start();
}
slider.lastPosition = player.position;
slider.lastLength = player.length;
}
}
Layout.fillWidth: true
property var bindSlider: true;
enabled: player.canSeek
from: 0
to: player.length
to: 1
onPressedChanged: {
if (!pressed) player.position = value;
if (!pressed) player.position = value * player.length;
bindSlider = !pressed;
}
Binding {
when: slider.bindSlider
slider.value: player.position
slider.value: slider.boundPosition
}
}
Label {
id: lengthLabel
text: timeStr(length)
text: positionInfo.timeStr(positionInfo.length)
}
}
}

View file

@ -0,0 +1,45 @@
import QtQuick
import Quickshell.Services.UPower
import "root:."
Item {
id: root
required property UPowerDevice device;
property real scale: 1;
readonly property bool isCharging: root.device.state == UPowerDeviceState.Charging;
readonly property bool isPluggedIn: isCharging || root.device.state == UPowerDeviceState.PendingCharge;
readonly property bool isLow: root.device.percentage <= 0.20;
width: 35 * root.scale
height: 35 * root.scale
Rectangle {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 4 * root.scale
}
width: 13 * root.scale
height: 23 * root.device.percentage * root.scale
radius: 2 * root.scale
color: root.isPluggedIn ? "#359040"
: ShellGlobals.interpolateColors(Math.min(1.0, Math.min(0.5, root.device.percentage) * 2), "red", "white")
}
Image {
id: img
anchors.fill: parent;
source: root.isCharging ? "root:icons/battery-charging.svg"
: root.isPluggedIn ? "root:icons/battery-plus.svg"
: root.isLow ? "root:icons/battery-warning.svg"
: "root:icons/battery-empty.svg"
sourceSize.width: parent.width
sourceSize.height: parent.height
visible: true
}
}

View file

@ -0,0 +1,224 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.UPower
import Quickshell.Widgets
import ".."
import "root:."
import "root:components"
import "power"
BarWidgetInner {
id: root
required property var bar;
readonly property var chargeState: UPower.displayDevice.state
readonly property bool isCharging: chargeState == UPowerDeviceState.Charging;
readonly property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge;
readonly property real percentage: UPower.displayDevice.percentage
readonly property bool isLow: percentage <= 0.20
readonly property UPowerDevice batteryDevice: UPower.devices.values
.find(device => device.isLaptopBattery);
function statusStr() {
return root.isPluggedIn ? `Plugged in, ${root.isCharging ? "Charging" : "Not Charging"}`
: "Discharging";
}
property bool showMenu: false;
implicitHeight: width
color: isLow ? "#45ff6060" : ShellGlobals.colors.widget
BarButton {
id: button
anchors.fill: parent
baseMargin: 5
fillWindowWidth: true
acceptedButtons: Qt.RightButton
directScale: true
showPressed: root.showMenu
onPressed: {
root.showMenu = !root.showMenu
}
BatteryIcon {
device: UPower.displayDevice
}
}
property TooltipItem tooltip: TooltipItem {
id: tooltip
tooltip: bar.tooltip
owner: root
show: button.containsMouse
Loader {
active: tooltip.visible
sourceComponent: Label {
text: {
const status = root.statusStr();
const percentage = Math.round(root.percentage * 100);
let str = `${percentage}% - ${status}`;
return str;
}
}
}
}
property TooltipItem rightclickMenu: TooltipItem {
id: rightclickMenu
tooltip: bar.tooltip
owner: root
isMenu: true
show: root.showMenu
onClose: root.showMenu = false
Loader {
active: rightclickMenu.visible
sourceComponent: ColumnLayout {
spacing: 10
FontMetrics { id: fm }
component SmallLabel: Label {
font.pointSize: fm.font.pointSize * 0.8
color: "#d0eeffff"
}
RowLayout {
IconImage {
source: "root:icons/gauge.svg"
implicitSize: 32
}
ColumnLayout {
spacing: 0
Label { text: "Power Profile" }
OptionSlider {
values: ["Power Save", "Balanced", "Performance"]
index: PowerProfiles.profile
onIndexChanged: PowerProfiles.profile = this.index;
implicitWidth: 350
}
}
}
RowLayout {
IconImage {
Layout.alignment: Qt.AlignTop
source: "root:icons/battery-empty.svg"
implicitSize: 32
}
ColumnLayout {
spacing: 0
RowLayout {
Label { text: "Battery" }
Item { Layout.fillWidth: true }
Label {
text: `${root.statusStr()} -`
color: "#d0eeffff"
}
Label { text: `${Math.round(root.percentage * 100)}%` }
}
ProgressBar {
Layout.topMargin: 5
Layout.bottomMargin: 5
Layout.fillWidth: true
value: UPower.displayDevice.percentage
}
RowLayout {
visible: remainingTimeLbl.text !== ""
SmallLabel { text: "Time remaining" }
Item { Layout.fillWidth: true }
SmallLabel {
id: remainingTimeLbl
text: {
const device = UPower.displayDevice;
const time = device.timeToEmpty || device.timeToFull;
if (time === 0) return "";
const minutes = Math.floor(time / 60).toString().padStart(2, '0');
return `${minutes} minutes`
}
}
}
RowLayout {
visible: root.batteryDevice.healthSupported
SmallLabel { text: "Health" }
Item { Layout.fillWidth: true }
SmallLabel {
text: `${Math.floor((root.batteryDevice?.healthPercentage ?? 0))}%`
}
}
}
}
Repeater {
model: ScriptModel {
// external devices
values: UPower.devices.values.filter(device => !device.powerSupply)
}
RowLayout {
required property UPowerDevice modelData;
IconImage {
Layout.alignment: Qt.AlignTop
source: {
switch (modelData.type) {
case UPowerDeviceType.Headset: return "root:icons/headset.svg";
}
return Quickshell.iconPath(modelData.iconName)
}
implicitSize: 32
}
ColumnLayout {
spacing: 0
RowLayout {
Label { text: modelData.model }
Item { Layout.fillWidth: true }
Label { text: `${Math.round(modelData.percentage * 100)}%` }
}
ProgressBar {
Layout.topMargin: 5
Layout.bottomMargin: 5
Layout.fillWidth: true
value: modelData.percentage
}
RowLayout {
visible: modelData.healthSupported
SmallLabel { text: "Health" }
Item { Layout.fillWidth: true }
SmallLabel {
text: `${Math.floor(modelData.healthPercentage)}%`
}
}
}
}
}
}
}
}
}

View file

@ -6,7 +6,7 @@ Item {
property bool expanded: false;
readonly property bool open: progress != 0;
readonly property bool animating: internalProgress != -1 && internalProgress != 101;
readonly property bool animating: internalProgress != (expanded ? 101 : -1);
implicitHeight: 16
implicitWidth: 16

View file

@ -1,14 +1,15 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.DBusMenu
import "../.."
MouseArea {
id: root
required property var entry;
required property QsMenuEntry entry;
property alias expanded: childrenRevealer.expanded;
property bool animating: childrenRevealer.animating || childrenList.animating;
property bool animating: childrenRevealer.animating || (childMenuLoader?.item?.animating ?? false);
// appears it won't actually create the handler when only used from MenuItemList.
onExpandedChanged: {}
onAnimatingChanged: {}
@ -22,7 +23,7 @@ MouseArea {
onClicked: {
if (entry.hasChildren) childrenRevealer.expanded = !childrenRevealer.expanded
else {
entry.click();
entry.triggered();
if (entry.toggleType == ToggleButtonType.None) close();
}
}
@ -34,19 +35,21 @@ MouseArea {
spacing: 0
RowLayout {
id: innerRow
Item {
implicitWidth: 22
implicitHeight: 22
MenuCheckBox {
anchors.centerIn: parent
visible: entry.toggleType == ToggleButtonType.CheckBox
visible: entry.buttonType == QsMenuButtonType.CheckBox
checkState: entry.checkState
}
MenuRadioButton {
anchors.centerIn: parent
visible: entry.toggleType == ToggleButtonType.RadioButton
visible: entry.buttonType == QsMenuButtonType.RadioButton
checkState: entry.checkState
}
@ -59,7 +62,7 @@ MouseArea {
}
Text {
text: entry.cleanLabel
text: entry.text
color: entry.enabled ? "white" : "#bbbbbb"
}
@ -68,25 +71,32 @@ MouseArea {
implicitWidth: 22
implicitHeight: 22
Image {
IconImage {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: entry.icon != ""
source: entry.icon
sourceSize.height: parent.height
sourceSize.width: parent.height
visible: source != ""
implicitSize: parent.height
}
}
}
Item {
Loader {
id: childMenuLoader
Layout.fillWidth: true
implicitHeight: childrenList.implicitHeight * childrenRevealer.progress
Layout.preferredHeight: active ? item.implicitHeight * childrenRevealer.progress : 0
readonly property real widthDifference: {
Math.max(0, (item?.implicitWidth ?? 0) - innerRow.implicitWidth);
}
Layout.preferredWidth: active ? innerRow.implicitWidth + (widthDifference * childrenRevealer.progress) : 0
active: root.expanded || root.animating
clip: true
MenuItemList {
sourceComponent: MenuView {
id: childrenList
items: entry.children
menu: entry
onClose: root.close()
anchors {

View file

@ -6,17 +6,19 @@ import "../.."
ColumnLayout {
id: root
required property var items;
property alias menu: menuView.menu;
property Item animatingItem: null;
property bool animating: animatingItem != null;
signal close();
signal submenuExpanded(item: var);
QsMenuOpener { id: menuView }
spacing: 0
Repeater {
model: items
model: menuView.children;
Loader {
required property var modelData;

View file

@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
@ -5,102 +7,102 @@ import Quickshell
import Quickshell.Services.SystemTray
import ".."
OverlayWidget {
BarWidgetInner {
id: root
expandedWidth: 600
expandedHeight: 800
required property var bar;
implicitHeight: column.implicitHeight + 10
BarWidgetInner {
implicitHeight: column.implicitHeight + 10
ColumnLayout {
id: column
implicitHeight: childrenRect.height
spacing: 5
ColumnLayout {
id: column
implicitHeight: childrenRect.height
spacing: 5
anchors {
fill: parent
margins: 5
}
anchors {
fill: parent
margins: 5
}
Repeater {
model: SystemTray.items;
Repeater {
model: SystemTray.items;
Item {
id: item
required property SystemTrayItem modelData;
Item {
required property var modelData;
readonly property alias menu: menuWatcher.menu;
property bool targetMenuOpen: false;
SystemTrayMenuWatcher {
id: menuWatcher;
trayItem: modelData;
Layout.fillWidth: true
implicitHeight: width
ClickableIcon {
id: mouseArea
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
width: height
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
image: item.modelData.icon
showPressed: item.targetMenuOpen || (pressedButtons & ~Qt.RightButton)
fillWindowWidth: true
extraVerticalMargin: column.spacing / 2
onClicked: event => {
event.accepted = true;
if (event.button == Qt.LeftButton) {
item.modelData.activate();
} else if (event.button == Qt.MiddleButton) {
item.modelData.secondaryActivate();
}
}
property bool targetMenuOpen: false;
onTargetMenuOpenChanged: menu.showChildren = targetMenuOpen
Layout.fillWidth: true
implicitHeight: width
ClickableIcon {
id: mouseArea
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
onPressed: event => {
if (event.button == Qt.RightButton && item.modelData.hasMenu) {
item.targetMenuOpen = !item.targetMenuOpen;
}
width: height
}
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onWheel: event => {
event.accepted = true;
const points = event.angleDelta.y / 120
item.modelData.scroll(points, false);
}
image: modelData.icon
showPressed: targetMenuOpen
property var tooltip: TooltipItem {
tooltip: root.bar.tooltip
owner: mouseArea
onClicked: event => {
event.accepted = true;
show: mouseArea.containsMouse
if (event.button == Qt.LeftButton) {
modelData.activate();
} else if (event.button == Qt.MiddleButton) {
modelData.secondaryActivate();
} else if (event.button == Qt.RightButton && menu != null) {
targetMenuOpen = !targetMenuOpen;
}
Text {
id: tooltipText
text: item.modelData.tooltipTitle != "" ? item.modelData.tooltipTitle : item.modelData.id
color: "white"
}
}
onWheel: event => {
event.accepted = true;
const points = event.angleDelta.y / 120
modelData.scroll(points, false);
}
property var rightclickMenu: TooltipItem {
id: rightclickMenu
tooltip: root.bar.tooltip
owner: mouseArea
property var tooltip: TooltipItem {
tooltip: bar.tooltip
owner: mouseArea
isMenu: true
show: item.targetMenuOpen
animateSize: !(menuContentLoader?.item?.animating ?? false)
show: mouseArea.containsMouse
onClose: item.targetMenuOpen = false;
Text {
id: tooltipText
text: modelData.tooltipTitle != "" ? modelData.tooltipTitle : modelData.id
color: "white"
}
}
Loader {
id: menuContentLoader
active: item.targetMenuOpen || rightclickMenu.visible || mouseArea.containsMouse
property var rightclickMenu: TooltipItem {
tooltip: bar.tooltip
owner: mouseArea
isMenu: true
show: targetMenuOpen && menu.showChildren
animateSize: !rightclickItems.animating
onClose: targetMenuOpen = false;
MenuItemList {
id: rightclickItems
items: menu == null ? [] : menu.children
onClose: targetMenuOpen = false;
sourceComponent: MenuView {
menu: item.modelData.menu
onClose: item.targetMenuOpen = false;
}
}
}