last 7 months of qs changes
This commit is contained in:
parent
2c64563ade
commit
4b90113a54
103 changed files with 3467 additions and 1415 deletions
|
|
@ -1,3 +1,4 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
|
@ -5,7 +6,8 @@ import Quickshell
|
|||
import "systray" as SysTray
|
||||
import "audio" as Audio
|
||||
import "mpris" as Mpris
|
||||
import "workspaces" as Workspaces
|
||||
import "power" as Power
|
||||
import "root:notifications" as Notifs
|
||||
|
||||
BarContainment {
|
||||
id: root
|
||||
|
|
@ -13,6 +15,7 @@ BarContainment {
|
|||
property bool isSoleBar: Quickshell.screens.length == 1;
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
|
|
@ -21,24 +24,32 @@ BarContainment {
|
|||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Loader {
|
||||
active: isSoleBar
|
||||
Layout.preferredHeight: active ? implicitHeight : 0;
|
||||
Notifs.NotificationWidget {
|
||||
Layout.fillWidth: true
|
||||
|
||||
sourceComponent: Workspaces.Widget {
|
||||
bar: root
|
||||
wsBaseIndex: 1
|
||||
}
|
||||
bar: root
|
||||
}
|
||||
|
||||
Workspaces.Widget {
|
||||
bar: root
|
||||
Layout.fillWidth: true
|
||||
wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1;
|
||||
hideWhenEmpty: isSoleBar
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Loader {
|
||||
active: root.isSoleBar
|
||||
Layout.preferredHeight: active ? implicitHeight : 0;
|
||||
Layout.fillWidth: true
|
||||
|
||||
sourceComponent: Workspaces {
|
||||
bar: root
|
||||
wsBaseIndex: 1
|
||||
}
|
||||
}
|
||||
|
||||
Workspaces {
|
||||
bar: root
|
||||
Layout.fillWidth: true
|
||||
wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1;
|
||||
hideWhenEmpty: root.isSoleBar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -65,8 +76,15 @@ BarContainment {
|
|||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ClockWidget {
|
||||
Power.Power {
|
||||
bar: root
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ClockWidget {
|
||||
bar: root
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
modules/user/modules/quickshell/shell/bar/BarButton.qml
Normal file
34
modules/user/modules/quickshell/shell/bar/BarButton.qml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
FullwidthMouseArea {
|
||||
id: root
|
||||
property bool showPressed: mouseArea.pressed;
|
||||
property real baseMargin: 0;
|
||||
property bool directScale: false;
|
||||
|
||||
readonly property Item contentItem: mContentItem;
|
||||
default property alias contentItemData: mContentItem.data;
|
||||
|
||||
property real targetBrightness: root.showPressed ? -25 : root.mouseArea.containsMouse && root.enabled ? 75 : 0
|
||||
Behavior on targetBrightness { SmoothedAnimation { velocity: 600 } }
|
||||
|
||||
property real targetMargins: root.showPressed ? 3 : 0;
|
||||
Behavior on targetMargins { SmoothedAnimation { velocity: 25 } }
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
Item {
|
||||
id: mContentItem
|
||||
anchors.fill: parent;
|
||||
|
||||
anchors.margins: root.baseMargin + (root.directScale ? 0 : root.targetMargins);
|
||||
scale: root.directScale ? (width - root.targetMargins * 2) / width : 1.0;
|
||||
|
||||
opacity: root.enabled ? 1.0 : 0.5;
|
||||
Behavior on opacity { SmoothedAnimation { velocity: 5 } }
|
||||
|
||||
layer.enabled: root.targetBrightness != 0
|
||||
layer.effect: MultiEffect { brightness: root.targetBrightness / 1000 }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "root:."
|
||||
import "root:lock" as Lock
|
||||
import ".."
|
||||
import "../lock" as Lock
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
|
@ -20,8 +20,8 @@ PanelWindow {
|
|||
exclusiveZone: width - margins.left
|
||||
|
||||
color: "transparent"
|
||||
WlrLayershell.namespace: "shell:bar"
|
||||
|
||||
WlrLayershell.namespace: "shell:bar"
|
||||
|
||||
readonly property var tooltip: tooltip;
|
||||
Tooltip {
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
import QtQuick
|
||||
|
||||
BarWidgetInner {
|
||||
implicitHeight: 50
|
||||
|
||||
SequentialAnimation on implicitHeight {
|
||||
loops: Animation.Infinite
|
||||
PropertyAnimation { to: 70; duration: 1000 }
|
||||
PropertyAnimation { to: 40; duration: 1000 }
|
||||
}
|
||||
|
||||
property int len: 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: `8${'='.repeat(len)}D`
|
||||
font.pointSize: 16
|
||||
color: "white"
|
||||
|
||||
PropertyAnimation on rotation {
|
||||
loops: Animation.Infinite
|
||||
to: 365
|
||||
duration: 1000
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: len += 1;
|
||||
}
|
||||
}
|
||||
19
modules/user/modules/quickshell/shell/bar/ClickableIcon.qml
Normal file
19
modules/user/modules/quickshell/shell/bar/ClickableIcon.qml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import QtQuick
|
||||
|
||||
BarButton {
|
||||
id: root
|
||||
required property string image;
|
||||
property alias cache: imageComponent.cache;
|
||||
property alias asynchronous: imageComponent.asynchronous;
|
||||
property bool scaleIcon: !asynchronous
|
||||
|
||||
Image {
|
||||
id: imageComponent
|
||||
anchors.fill: parent
|
||||
|
||||
source: root.image
|
||||
sourceSize.width: scaleIcon ? width : (root.width - baseMargin)
|
||||
sourceSize.height: scaleIcon ? height : (root.height - baseMargin)
|
||||
cache: false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,67 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import ".."
|
||||
|
||||
OverlayWidget {
|
||||
expandedWidth: 600
|
||||
expandedHeight: 600
|
||||
BarWidgetInner {
|
||||
id: root
|
||||
required property var bar;
|
||||
|
||||
BarWidgetInner {
|
||||
implicitHeight: layout.implicitHeight
|
||||
implicitHeight: layout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
spacing: 0
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: tooltip.visible ? SystemClock.Seconds : SystemClock.Minutes;
|
||||
}
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
left: parent.left
|
||||
BarButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
fillWindowWidth: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
spacing: 0
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
left: parent.left
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: {
|
||||
const hours = clock.hours.toString().padStart(2, '0')
|
||||
const minutes = clock.minutes.toString().padStart(2, '0')
|
||||
return `${hours}\n${minutes}`
|
||||
}
|
||||
font.pointSize: 18
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property TooltipItem tooltip: TooltipItem {
|
||||
id: tooltip
|
||||
tooltip: bar.tooltip
|
||||
owner: root
|
||||
show: button.containsMouse
|
||||
|
||||
Loader {
|
||||
active: tooltip.visible
|
||||
sourceComponent: Label {
|
||||
text: {
|
||||
// SystemClock can send an update slightly (<50ms) before the
|
||||
// time changes. We use its readout so the widget and tooltip match.
|
||||
const hours = clock.hours.toString().padStart(2, '0');
|
||||
const minutes = clock.minutes.toString().padStart(2, '0');
|
||||
const seconds = clock.seconds.toString().padStart(2, '0');
|
||||
|
||||
return `${hours}:${minutes}:${seconds}\n` + new Date().toLocaleString(Qt.locale("en_US"), "dddd, MMMM d, yyyy");
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: ShellGlobals.time.getHours().toString().padStart(2, '0')
|
||||
font.pointSize: 18
|
||||
color: "#a0ffffff"
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: ShellGlobals.time.getMinutes().toString().padStart(2, '0')
|
||||
font.pointSize: 18
|
||||
color: "#a0ffffff"
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: expanded = !expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import ".."
|
||||
|
||||
Item {
|
||||
required property var bar;
|
||||
required property real expandedWidth;
|
||||
required property real expandedHeight;
|
||||
required default property Item widget;
|
||||
|
||||
property bool expanded: false;
|
||||
|
||||
onExpandedChanged: {
|
||||
animateTo(expanded ? 1.0 : 0.0)
|
||||
if (expanded) popupSurface.activeOverlay = this
|
||||
}
|
||||
|
||||
readonly property bool fullyCollapsed: animationProgress == 0.0;
|
||||
|
||||
onFullyCollapsedChanged: {
|
||||
if (fullyCollapsed && popupSurface.activeOverlay == this) {
|
||||
popupSurface.activeOverlay = null;
|
||||
}
|
||||
|
||||
/*if (fullyCollapsed) {
|
||||
widget.x = Qt.binding(() => this.x)
|
||||
}*/
|
||||
}
|
||||
|
||||
readonly property rect collapsedLayerRect: {
|
||||
void [barWindow.windowTransform, popupSurface.windowTransform, y, parent.y];
|
||||
return this.mapToItem(popupSurface.contentItem, 0, 0, width, height);
|
||||
}
|
||||
|
||||
onCollapsedLayerRectChanged: console.log(`clr: ${collapsedLayerRect}`)
|
||||
onLayerRectChanged: console.log(`lr: ${layerRect}`)
|
||||
onYChanged: console.log(`y: ${y}`)
|
||||
|
||||
readonly property rect expandedLayerRect: bar.widgetSurface.expandedPosition(this)
|
||||
|
||||
readonly property rect layerRect: {
|
||||
const [p, xCurve, yCurve] = [animationProgress, ShellGlobals.popoutXCurve, ShellGlobals.popoutYCurve];
|
||||
|
||||
return Qt.rect(
|
||||
xCurve.interpolate(p, collapsedLayerRect.x, expandedLayerRect.x),
|
||||
yCurve.interpolate(p, collapsedLayerRect.y, expandedLayerRect.y),
|
||||
xCurve.interpolate(p, collapsedLayerRect.width, expandedLayerRect.width),
|
||||
yCurve.interpolate(p, collapsedLayerRect.height, expandedLayerRect.height),
|
||||
);
|
||||
}
|
||||
|
||||
implicitWidth: widget.implicitWidth
|
||||
implicitHeight: widget.implicitHeight
|
||||
|
||||
Component.onCompleted: {
|
||||
popupSurface.connectOverlay(this);
|
||||
widget.x = Qt.binding(() => layerRect.x);
|
||||
widget.y = Qt.binding(() => layerRect.y);
|
||||
widget.width = Qt.binding(() => layerRect.width);
|
||||
widget.height = Qt.binding(() => layerRect.height);
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
popupSurface.disconnectOverlay(this)
|
||||
}
|
||||
|
||||
function animateTo(target: real) {
|
||||
animationProgressInternal = target * 1000
|
||||
}
|
||||
|
||||
property real animationProgress: animationProgressInternal * 0.001
|
||||
property real animationProgressInternal: 0.0 // animations seem to only have int precision
|
||||
|
||||
Behavior on animationProgressInternal {
|
||||
SmoothedAnimation { velocity: 3000 }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onPressed: expanded = false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
border.color: ShellGlobals.colors.widgetOutline
|
||||
border.width: 1
|
||||
radius: 5
|
||||
color: "transparent"
|
||||
opacity: mouseArea.containsMouse ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
SmoothedAnimation { velocity: 4 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool fillWindowWidth: false;
|
||||
property real extraVerticalMargin: 0;
|
||||
|
||||
property alias mouseArea: mouseArea;
|
||||
property alias hoverEnabled: mouseArea.hoverEnabled;
|
||||
property alias acceptedButtons: mouseArea.acceptedButtons;
|
||||
property alias pressedButtons: mouseArea.pressedButtons;
|
||||
property alias containsMouse: mouseArea.containsMouse;
|
||||
property alias isPressed: mouseArea.pressed;
|
||||
|
||||
signal clicked(event: MouseEvent);
|
||||
signal entered();
|
||||
signal exited();
|
||||
signal pressed(event: MouseEvent);
|
||||
signal released(event: MouseEvent);
|
||||
signal wheel(event: WheelEvent);
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors {
|
||||
fill: parent
|
||||
// not much point in finding exact values
|
||||
leftMargin: root.fillWindowWidth ? -50 : 0
|
||||
rightMargin: root.fillWindowWidth ? -50 : 0
|
||||
topMargin: -root.extraVerticalMargin
|
||||
bottomMargin: -root.extraVerticalMargin
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
this.clicked.connect(root.clicked);
|
||||
//this.entered.connect(root.entered);
|
||||
//this.exited.connect(root.exited);
|
||||
//this.pressed.connect(root.pressed);
|
||||
this.released.connect(root.released);
|
||||
//this.wheel.connect(root.wheel);
|
||||
}
|
||||
|
||||
// for some reason MouseArea.pressed is both a prop and signal so connect doesn't work
|
||||
onPressed: event => root.pressed(event);
|
||||
|
||||
// connecting to onwheel seems to implicitly accept it. undo that.
|
||||
onWheel: event => {
|
||||
event.accepted = false;
|
||||
root.wheel(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import QtQuick
|
||||
|
||||
Item {
|
||||
default property Item item;
|
||||
property int expandedWidth;
|
||||
property int expandedHeight;
|
||||
|
||||
implicitHeight: item.implicitHeight
|
||||
implicitWidth: item.implicitWidth
|
||||
|
||||
Component.onCompleted: {
|
||||
item.width = Qt.binding(() => this.width)
|
||||
item.height = Qt.binding(() => this.height)
|
||||
}
|
||||
|
||||
children: [ item ]
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import "root:/"
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
|
@ -11,11 +12,14 @@ Scope {
|
|||
|
||||
readonly property TooltipItem activeItem: activeMenu ?? activeTooltip;
|
||||
property TooltipItem lastActiveItem: null;
|
||||
readonly property TooltipItem shownItem: activeItem ?? lastActiveItem;
|
||||
property real hangTime: lastActiveItem?.hangTime ?? 0;
|
||||
|
||||
property Item tooltipItem: null;
|
||||
|
||||
onActiveItemChanged: {
|
||||
if (activeItem != null) {
|
||||
hangTimer.stop();
|
||||
activeItem.targetVisible = true;
|
||||
|
||||
if (tooltipItem) {
|
||||
|
|
@ -24,10 +28,12 @@ Scope {
|
|||
}
|
||||
|
||||
if (lastActiveItem != null && lastActiveItem != activeItem) {
|
||||
lastActiveItem.targetVisible = false;
|
||||
if (activeItem != null) lastActiveItem.targetVisible = false;
|
||||
else if (root.hangTime == 0) doLastHide();
|
||||
else hangTimer.start();
|
||||
}
|
||||
|
||||
lastActiveItem = activeItem;
|
||||
if (activeItem != null) lastActiveItem = activeItem;
|
||||
}
|
||||
|
||||
function setItem(item: TooltipItem) {
|
||||
|
|
@ -46,28 +52,59 @@ Scope {
|
|||
}
|
||||
}
|
||||
|
||||
function doLastHide() {
|
||||
lastActiveItem.targetVisible = false;
|
||||
}
|
||||
|
||||
function onHidden(item: TooltipItem) {
|
||||
if (item == lastActiveItem) {
|
||||
lastActiveItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hangTimer
|
||||
interval: root.hangTime
|
||||
onTriggered: doLastHide();
|
||||
}
|
||||
|
||||
property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0;
|
||||
Behavior on scaleMul { SmoothedAnimation { velocity: 5 } }
|
||||
|
||||
LazyLoader {
|
||||
id: popupLoader
|
||||
activeAsync: activeItem != null
|
||||
activeAsync: shownItem != null
|
||||
|
||||
PopupWindow {
|
||||
id: popup
|
||||
parentWindow: bar
|
||||
relativeX: bar.tooltipXOffset
|
||||
relativeY: 0
|
||||
height: bar.height
|
||||
width: 1000//Math.max(1, widthAnim.running ? Math.max(tooltipItem.targetWidth, tooltipItem.lastTargetWidth) : tooltipItem.targetWidth)
|
||||
|
||||
anchor {
|
||||
window: bar
|
||||
rect.x: bar.tooltipXOffset
|
||||
rect.y: tooltipItem.highestAnimY
|
||||
adjustment: PopupAdjustment.None
|
||||
}
|
||||
|
||||
HyprlandWindow.opacity: root.scaleMul
|
||||
|
||||
//height: bar.height
|
||||
width: Math.max(700, tooltipItem.largestAnimWidth) // max due to qtwayland glitches
|
||||
height: {
|
||||
const h = tooltipItem.lowestAnimY - tooltipItem.highestAnimY
|
||||
//console.log(`seth ${h} ${tooltipItem.highestAnimY} ${tooltipItem.lowestAnimY}; ${tooltipItem.y1} ${tooltipItem.y2}`)
|
||||
return h
|
||||
}
|
||||
visible: true
|
||||
color: "transparent"
|
||||
//color: "#20000000"
|
||||
|
||||
mask: Region {
|
||||
item: (activeItem?.hoverable ?? false) ? tooltipItem : null
|
||||
item: (shownItem?.hoverable ?? false) ? tooltipItem : null
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
active: activeItem?.isMenu ?? false
|
||||
windows: [ popup, bar ]
|
||||
windows: [ popup, bar, ...(activeItem?.grabWindows ?? []) ]
|
||||
onActiveChanged: {
|
||||
if (!active && activeItem?.isMenu) {
|
||||
activeMenu.close()
|
||||
|
|
@ -75,30 +112,76 @@ Scope {
|
|||
}
|
||||
}
|
||||
|
||||
/*Rectangle {
|
||||
color: "#10ff0000"
|
||||
//y: tooltipItem.highestAnimY
|
||||
height: tooltipItem.lowestAnimY - tooltipItem.highestAnimY
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: "#1000ff00"
|
||||
//y: tooltipItem.highestAnimY
|
||||
height: popup.height
|
||||
width: parent.width
|
||||
}*/
|
||||
|
||||
Item {
|
||||
id: tooltipItem
|
||||
Component.onCompleted: {
|
||||
root.tooltipItem = this;
|
||||
if (root.activeItem) {
|
||||
root.activeItem.parent = this;
|
||||
if (root.shownItem) {
|
||||
root.shownItem.parent = this;
|
||||
}
|
||||
|
||||
//highestAnimY = targetY - targetHeight / 2;
|
||||
//lowestAnimY = targetY + targetHeight / 2;
|
||||
}
|
||||
|
||||
transform: Scale {
|
||||
origin.x: 0
|
||||
origin.y: tooltipItem.height / 2
|
||||
xScale: 0.9 + scaleMul * 0.1
|
||||
yScale: xScale
|
||||
}
|
||||
|
||||
clip: width != targetWidth || height != targetHeight
|
||||
|
||||
// bkg
|
||||
BarWidgetInner {
|
||||
anchors.fill: parent
|
||||
color: ShellGlobals.colors.bar
|
||||
}
|
||||
|
||||
readonly property var targetWidth: shownItem?.implicitWidth ?? 0;
|
||||
readonly property var targetHeight: shownItem?.implicitHeight ?? 0;
|
||||
|
||||
property var largestAnimWidth: 0;
|
||||
property var highestAnimY: 0; // unused due to reposition timing issues
|
||||
property var lowestAnimY: bar.height;
|
||||
|
||||
onTargetWidthChanged: {
|
||||
if (targetWidth > largestAnimWidth) {
|
||||
largestAnimWidth = targetWidth;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var targetWidth: activeItem?.implicitWidth ?? 0;
|
||||
readonly property var targetHeight: activeItem?.implicitHeight ?? 0;
|
||||
onTargetYChanged: updateYBounds();
|
||||
onTargetHeightChanged: updateYBounds();
|
||||
function updateYBounds() {
|
||||
if (targetY - targetHeight / 2 < highestAnimY) {
|
||||
//highestAnimY = targetY - targetHeight / 2
|
||||
}
|
||||
|
||||
property var lastTargetWidthTracker: 0;
|
||||
property var lastTargetWidth: 0;
|
||||
|
||||
onTargetWidthChanged: {
|
||||
lastTargetWidth = lastTargetWidthTracker;
|
||||
lastTargetWidthTracker = targetWidth;
|
||||
if (targetY + targetHeight / 2 > lowestAnimY) {
|
||||
//lowestAnimY = targetY + targetHeight / 2
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real targetY: {
|
||||
if (activeItem == null) return 0;
|
||||
const target = bar.contentItem.mapFromItem(activeItem.owner, 0, activeItem.targetRelativeY).y;
|
||||
return bar.boundedY(target, activeItem.implicitHeight / 2);
|
||||
if (shownItem == null) return 0;
|
||||
const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y;
|
||||
return bar.boundedY(target, shownItem.implicitHeight / 2);
|
||||
}
|
||||
|
||||
property var w: -1
|
||||
|
|
@ -107,15 +190,24 @@ Scope {
|
|||
property var y1: -1
|
||||
property var y2: -1
|
||||
|
||||
y: y1
|
||||
y: y1 - popup.anchor.rect.y
|
||||
height: y2 - y1
|
||||
|
||||
SmoothedAnimation {
|
||||
target: tooltipItem;
|
||||
property: "y1";
|
||||
readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running
|
||||
|
||||
onAnyAnimsRunningChanged: {
|
||||
if (!anyAnimsRunning) {
|
||||
largestAnimWidth = targetWidth
|
||||
//highestAnimY = y1;
|
||||
//lowestAnimY = y2;
|
||||
}
|
||||
}
|
||||
|
||||
SmoothedAnimation on y1 {
|
||||
id: y1Anim
|
||||
to: tooltipItem.targetY - tooltipItem.targetHeight / 2;
|
||||
onToChanged: {
|
||||
if (tooltipItem.y1 == -1 || !(activeItem?.animateSize ?? true)) {
|
||||
if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) {
|
||||
stop();
|
||||
tooltipItem.y1 = to;
|
||||
} else {
|
||||
|
|
@ -125,12 +217,11 @@ Scope {
|
|||
}
|
||||
}
|
||||
|
||||
SmoothedAnimation {
|
||||
target: tooltipItem
|
||||
property: "y2"
|
||||
SmoothedAnimation on y2 {
|
||||
id: y2Anim
|
||||
to: tooltipItem.targetY + tooltipItem.targetHeight / 2;
|
||||
onToChanged: {
|
||||
if (tooltipItem.y2 == -1 || !(activeItem?.animateSize ?? true)) {
|
||||
if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) {
|
||||
stop();
|
||||
tooltipItem.y2 = to;
|
||||
} else {
|
||||
|
|
@ -140,13 +231,11 @@ Scope {
|
|||
}
|
||||
}
|
||||
|
||||
SmoothedAnimation {
|
||||
SmoothedAnimation on w {
|
||||
id: widthAnim
|
||||
target: tooltipItem
|
||||
property: "w"
|
||||
to: tooltipItem.targetWidth;
|
||||
onToChanged: {
|
||||
if (tooltipItem.w == -1) {
|
||||
if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) {
|
||||
stop();
|
||||
tooltipItem.w = to;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "root:/"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property var tooltip;
|
||||
required property Item owner;
|
||||
property bool isMenu: false;
|
||||
property list<QtObject> grabWindows;
|
||||
property bool hoverable: isMenu;
|
||||
property bool animateSize: true;
|
||||
property bool show: false;
|
||||
|
|
@ -17,47 +17,35 @@ Item {
|
|||
|
||||
signal close();
|
||||
|
||||
readonly property alias contentItem: contentItem;
|
||||
default property alias data: contentItem.data;
|
||||
|
||||
property Component backgroundComponent: BarWidgetInner {
|
||||
color: ShellGlobals.colors.bar
|
||||
anchors.fill: parent
|
||||
}
|
||||
property Component backgroundComponent: null
|
||||
|
||||
onShowChanged: {
|
||||
if (show) {
|
||||
hangTimer.stop();
|
||||
tooltip.setItem(this);
|
||||
} else if (hangTime == 0) {
|
||||
tooltip.removeItem(this);
|
||||
} else hangTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hangTimer
|
||||
interval: hangTime
|
||||
onTriggered: tooltip.removeItem(root);
|
||||
if (show) tooltip.setItem(this);
|
||||
else tooltip.removeItem(this);
|
||||
}
|
||||
|
||||
property bool targetVisible: false
|
||||
property real targetOpacity: 0
|
||||
opacity: targetOpacity / 1000
|
||||
opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul))
|
||||
|
||||
Behavior on targetOpacity {
|
||||
id: opacityAnimation
|
||||
SmoothedAnimation { velocity: 5000 }
|
||||
SmoothedAnimation { velocity: 5 }
|
||||
}
|
||||
|
||||
function snapOpacity(opacity: real) {
|
||||
opacityAnimation.enabled = false;
|
||||
targetOpacity = opacity * 1000
|
||||
targetOpacity = opacity;
|
||||
opacityAnimation.enabled = true;
|
||||
}
|
||||
|
||||
onTargetVisibleChanged: {
|
||||
if (targetVisible) {
|
||||
visible = true;
|
||||
targetOpacity = 1000;
|
||||
targetOpacity = 1;
|
||||
} else {
|
||||
close()
|
||||
targetOpacity = 0;
|
||||
|
|
@ -68,20 +56,21 @@ Item {
|
|||
if (!targetVisible && targetOpacity == 0) {
|
||||
visible = false;
|
||||
this.parent = null;
|
||||
if (tooltip) tooltip.onHidden(this);
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
clip: true
|
||||
implicitHeight: contentItem.implicitHeight + 10
|
||||
implicitWidth: contentItem.implicitWidth + 10
|
||||
//clip: true
|
||||
implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
|
||||
implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
|
||||
|
||||
readonly property Item item: contentItem;
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: root.visible || root.preloadBackground
|
||||
active: root.backgroundComponent && (root.visible || root.preloadBackground)
|
||||
asynchronous: !root.visible && root.preloadBackground
|
||||
sourceComponent: backgroundComponent
|
||||
}
|
||||
|
|
@ -91,7 +80,7 @@ Item {
|
|||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
|
||||
implicitHeight: childrenRect.height
|
||||
implicitWidth: childrenRect.width
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
pragma ComponentBehavior: Bound;
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Hyprland
|
||||
import ".."
|
||||
import "root:."
|
||||
|
||||
MouseArea {
|
||||
FullwidthMouseArea {
|
||||
id: root
|
||||
required property var bar;
|
||||
required property int wsBaseIndex;
|
||||
property int wsCount: 10;
|
||||
property bool hideWhenEmpty: false;
|
||||
|
||||
implicitHeight: column.implicitHeight + 10;
|
||||
|
||||
fillWindowWidth: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
|
||||
onWheel: event => {
|
||||
|
|
@ -29,9 +33,6 @@ MouseArea {
|
|||
property int existsCount: 0;
|
||||
visible: !hideWhenEmpty || existsCount > 0;
|
||||
|
||||
property real animPos: 0;
|
||||
Behavior on animPos { SmoothedAnimation { velocity: 100 } }
|
||||
|
||||
// destructor takes care of nulling
|
||||
signal workspaceAdded(workspace: HyprlandWorkspace);
|
||||
|
||||
|
|
@ -45,20 +46,22 @@ MouseArea {
|
|||
}
|
||||
|
||||
Repeater {
|
||||
model: 10
|
||||
model: root.wsCount
|
||||
|
||||
MouseArea {
|
||||
FullwidthMouseArea {
|
||||
id: wsItem
|
||||
onPressed: Hyprland.dispatch(`workspace ${wsIndex}`);
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 15
|
||||
|
||||
fillWindowWidth: true
|
||||
|
||||
required property int index;
|
||||
property int wsIndex: wsBaseIndex + index;
|
||||
property int wsIndex: root.wsBaseIndex + index;
|
||||
property HyprlandWorkspace workspace: null;
|
||||
property bool exists: workspace != null;
|
||||
property bool active: (monitor?.activeWorkspace ?? false) && monitor.activeWorkspace == workspace;
|
||||
property bool active: (root.monitor?.activeWorkspace ?? false) && root.monitor.activeWorkspace == workspace;
|
||||
|
||||
onActiveChanged: {
|
||||
if (active) root.currentIndex = wsIndex;
|
||||
|
|
@ -78,21 +81,21 @@ MouseArea {
|
|||
}
|
||||
}
|
||||
|
||||
property real animActive: active ? 100 : 0
|
||||
Behavior on animActive { NumberAnimation { duration: 100 } }
|
||||
property real animActive: active ? 1 : 0
|
||||
Behavior on animActive { NumberAnimation { duration: 150 } }
|
||||
|
||||
property real animExists: exists ? 100 : 0
|
||||
property real animExists: exists ? 1 : 0
|
||||
Behavior on animExists { NumberAnimation { duration: 100 } }
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
height: 10
|
||||
width: parent.width
|
||||
scale: 1 + animActive * 0.003
|
||||
scale: 1 + wsItem.animActive * 0.3
|
||||
radius: height / 2
|
||||
border.color: ShellGlobals.colors.widgetOutline
|
||||
border.width: 1
|
||||
color: ShellGlobals.interpolateColors(animExists * 0.01, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
|
||||
color: ShellGlobals.interpolateColors(animExists, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,14 +12,20 @@ ClickableIcon {
|
|||
|
||||
implicitHeight: width;
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton;
|
||||
showPressed: mixerOpen
|
||||
fillWindowWidth: true
|
||||
showPressed: mixerOpen || (pressedButtons & ~Qt.RightButton)
|
||||
|
||||
onPressed: event => {
|
||||
event.accepted = true;
|
||||
if (event.button === Qt.RightButton) {
|
||||
mixerOpen = !mixerOpen;
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: event => {
|
||||
event.accepted = true;
|
||||
if (event.button === Qt.LeftButton) {
|
||||
event.accepted = true;
|
||||
node.audio.muted = !node.audio.muted;
|
||||
} else if (event.button === Qt.RightButton) {
|
||||
mixerOpen = !mixerOpen;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +77,16 @@ ClickableIcon {
|
|||
sourceComponent: Mixer {
|
||||
width: 550
|
||||
trackedNode: node
|
||||
nodeList: Pipewire.nodes.values.filter(n => n.audio && !n.isStream && n.isSink == node.isSink)
|
||||
nodeImage: root.image
|
||||
|
||||
onSelected: n => {
|
||||
if (node.isSink) {
|
||||
Pipewire.preferredDefaultAudioSink = n;
|
||||
} else {
|
||||
Pipewire.preferredDefaultAudioSource = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import ".."
|
||||
import "../.."
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property PwNode trackedNode;
|
||||
required property string nodeImage;
|
||||
required property list<PwNode> nodeList;
|
||||
|
||||
signal selected(node: PwNode);
|
||||
|
||||
PwNodeLinkTracker {
|
||||
id: linkTracker
|
||||
|
|
@ -15,10 +21,13 @@ ColumnLayout {
|
|||
|
||||
PwObjectTracker { objects: [ trackedNode, ...linkTracker.linkGroups ] }
|
||||
|
||||
MixerEntry {
|
||||
MixerEntry/*WithSelect*/ {
|
||||
id: nodeEntry
|
||||
node: trackedNode
|
||||
//nodeList: root.nodeList
|
||||
image: nodeImage
|
||||
|
||||
Component.onCompleted: this.selected.connect(root.selected);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
|
@ -49,7 +58,7 @@ ColumnLayout {
|
|||
// special cases :(
|
||||
if (icon == "firefox") icon = "firefox-devedition";
|
||||
|
||||
return `image://icon/${icon}`
|
||||
return Quickshell.iconPath(icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,11 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Pipewire
|
||||
import ".."
|
||||
|
||||
RowLayout {
|
||||
MixerEntryBase {
|
||||
id: root
|
||||
required property PwNode node;
|
||||
required property string image;
|
||||
property int state: PwLinkState.Unlinked;
|
||||
|
||||
PwObjectTracker { objects: [ node ] }
|
||||
|
||||
ClickableIcon {
|
||||
image: root.image
|
||||
asynchronous: true
|
||||
implicitHeight: 40
|
||||
implicitWidth: height
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
RowLayout {
|
||||
Item {
|
||||
implicitHeight: title.implicitHeight
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
id: title
|
||||
color: "white"
|
||||
anchors.fill: parent
|
||||
elide: Text.ElideRight
|
||||
text: {
|
||||
const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description);
|
||||
const mediaName = node.properties["media.name"];
|
||||
|
||||
return mediaName != undefined ? `${name} - ${mediaName}` : name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VolumeSlider {
|
||||
//Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
implicitWidth: 200
|
||||
|
||||
value: node.audio.volume
|
||||
onValueChanged: node.audio.volume = value
|
||||
}
|
||||
headerComponent: Text {
|
||||
color: "white"
|
||||
elide: Text.ElideRight
|
||||
text: root.getNodeName(root.node)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Pipewire
|
||||
import ".."
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
required property PwNode node;
|
||||
required property string image;
|
||||
required property Item headerComponent;
|
||||
|
||||
property int state: PwLinkState.Unlinked;
|
||||
|
||||
function getNodeName(node: PwNode): string {
|
||||
const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description);
|
||||
const mediaName = node.properties["media.name"];
|
||||
|
||||
return mediaName != undefined ? `${name} - ${mediaName}` : name + node.id;
|
||||
}
|
||||
|
||||
PwObjectTracker { objects: [ node ] }
|
||||
|
||||
ClickableIcon {
|
||||
image: root.image
|
||||
asynchronous: true
|
||||
implicitHeight: 40
|
||||
implicitWidth: height
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Item {
|
||||
id: container
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitWidth: headerComponent.implicitWidth
|
||||
implicitHeight: headerComponent.implicitHeight
|
||||
|
||||
children: [ headerComponent ]
|
||||
Binding { root.headerComponent.anchors.fill: container }
|
||||
}
|
||||
|
||||
VolumeSlider {
|
||||
Layout.fillWidth: true
|
||||
|
||||
value: node.audio.volume
|
||||
onValueChanged: node.audio.volume = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
MixerEntryBase {
|
||||
id: root
|
||||
required property list<PwNode> nodeList;
|
||||
|
||||
signal selected(node: PwNode);
|
||||
|
||||
headerComponent: ComboBox {
|
||||
model: nodeList.map(node => root.getNodeName(node));
|
||||
currentIndex: nodeList.findIndex(node => node == root.node)
|
||||
onActivated: index => root.selected(nodeList[index])
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e8eaed"><path d="M806-56 677.67-184.33q-27 18.66-58 32.16-31 13.5-64.34 21.17v-68.67q20-6.33 38.84-13.66 18.83-7.34 35.5-19l-154.34-155V-160l-200-200h-160v-240H262L51.33-810.67 98.67-858l754.66 754L806-56Zm-26.67-232-48-48q19-33 28.17-69.62 9.17-36.61 9.17-75.38 0-100.22-58.34-179.11Q652-739 555.33-762.33V-831q124 28 202 125.5t78 224.5q0 51.67-14.16 100.67-14.17 49-41.84 92.33Zm-134-134-90-90v-130q47 22 73.5 66t26.5 96q0 15-2.5 29.5t-7.5 28.5Zm-170-170-104-104 104-104v208Zm-66.66 270v-131.33l-80-80H182v106.66h122L408.67-322Zm-40-171.33Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 650 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e8eaed"><path d="M560-131v-68.67q94.67-27.33 154-105 59.33-77.66 59.33-176.33 0-98.67-59-176.67-59-78-154.33-104.66V-831q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm426.67 45.33v-332Q599-628 629.5-582T660-480q0 55-30.83 100.83-30.84 45.84-82.5 64.5ZM413.33-634l-104 100.67H186.67v106.66h122.66l104 101.34V-634Zm-96 154Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 474 B |
|
|
@ -82,7 +82,7 @@ BarWidgetInner {
|
|||
x: blurRadius
|
||||
width: blur.width - blurRadius * 2
|
||||
height: blur.height
|
||||
clip: true
|
||||
|
||||
GaussianBlur {
|
||||
source: blurSource
|
||||
x: -parent.x
|
||||
|
|
@ -139,23 +139,27 @@ BarWidgetInner {
|
|||
readonly property Rectangle overlay: overlayItem;
|
||||
Rectangle {
|
||||
id: overlayItem
|
||||
visible: false
|
||||
visible: true
|
||||
anchors.fill: parent
|
||||
border.color: ShellGlobals.colors.widgetOutlineSeparate
|
||||
border.width: 0//1
|
||||
radius: 0//root.radius
|
||||
radius: root.radius
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.radius
|
||||
color: "transparent"
|
||||
border.color: ShellGlobals.colors.widgetOutlineSeparate;
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
|
||||
// slightly offset on the corners :/
|
||||
layer.enabled: true
|
||||
layer.effect: ShaderEffect {
|
||||
fragmentShader: "radial_clip.frag.qsb"
|
||||
// +1 seems to match Rectangle
|
||||
property real radius: root.radius + 1
|
||||
property size size: Qt.size(root.width, root.height)
|
||||
property real borderWidth: 1//.5
|
||||
property color borderColor: ShellGlobals.colors.widgetOutlineSeparate//"#ffff0000"
|
||||
property color tint: overlayItem.color
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: root.width
|
||||
height: root.height
|
||||
radius: root.radius
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQml.Models
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Hyprland
|
||||
import "../.."
|
||||
|
||||
Singleton {
|
||||
|
|
@ -15,51 +17,58 @@ Singleton {
|
|||
property bool __reverse: false;
|
||||
|
||||
property var activeTrack;
|
||||
Component.onCompleted: {
|
||||
for (const player of Mpris.players.values) {
|
||||
if (player.playbackState == MprisPlaybackState.Playing) {
|
||||
if (root.trackedPlayer == null) {
|
||||
root.trackedPlayer = player;
|
||||
|
||||
Instantiator {
|
||||
model: Mpris.players;
|
||||
|
||||
Connections {
|
||||
required property MprisPlayer modelData;
|
||||
target: modelData;
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.trackedPlayer == null || modelData.isPlaying) {
|
||||
root.trackedPlayer = modelData;
|
||||
}
|
||||
}
|
||||
|
||||
player.playbackStateChanged.connect(() => {
|
||||
if (root.trackedPlayer !== player) root.trackedPlayer = player;
|
||||
});
|
||||
Component.onDestruction: {
|
||||
if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
|
||||
for (const player of Mpris.players.values) {
|
||||
if (player.playbackState.isPlaying) {
|
||||
root.trackedPlayer = player;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (trackedPlayer == null && Mpris.players.values.length != 0) {
|
||||
trackedPlayer = Mpris.players.values[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onPlaybackStateChanged() {
|
||||
if (root.trackedPlayer !== modelData) root.trackedPlayer = modelData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: activePlayer
|
||||
|
||||
function onTrackChanged() {
|
||||
function onPostTrackChanged() {
|
||||
root.updateTrack();
|
||||
}
|
||||
}
|
||||
|
||||
// Change the tracked player when one changes playback state or is created in a playing state.
|
||||
Connections {
|
||||
target: Mpris.players;
|
||||
function onTrackArtUrlChanged() {
|
||||
console.log("arturl:", activePlayer.trackArtUrl)
|
||||
//root.updateTrack();
|
||||
if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) {
|
||||
// cantata likes to send cover updates *BEFORE* updating the track info.
|
||||
// as such, art url changes shouldn't be able to break the reverse animation
|
||||
const r = root.__reverse;
|
||||
root.updateTrack();
|
||||
root.__reverse = r;
|
||||
|
||||
function onObjectInsertedPost(player: MprisPlayer) {
|
||||
if (player.playbackState === MprisPlaybackState.Playing) {
|
||||
if (root.trackedPlayer !== player) root.trackedPlayer = player;
|
||||
}
|
||||
|
||||
player.playbackStateChanged.connect(() => {
|
||||
if (root.trackedPlayer !== player) root.trackedPlayer = player;
|
||||
});
|
||||
}
|
||||
|
||||
function onObjectRemovedPre() {
|
||||
console.log(`trackedPlayer: ${root.trackedPlayer}`)
|
||||
if (root.trackedPlayer == null) {
|
||||
for (const player of Mpris.players.values) {
|
||||
if (player.playbackState === MprisPlaybackState.Playing) {
|
||||
root.trackedPlayer = player;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,27 +76,23 @@ Singleton {
|
|||
onActivePlayerChanged: this.updateTrack();
|
||||
|
||||
function updateTrack() {
|
||||
const metadata = this.activePlayer?.metadata ?? {};
|
||||
|
||||
//console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`)
|
||||
this.activeTrack = {
|
||||
artUrl: metadata["mpris:artUrl"] ?? "",
|
||||
title: metadata["xesam:title"] ?? "",
|
||||
artist: metadata["xesam:artist"] ?? "",
|
||||
uniqueId: this.activePlayer?.uniqueId ?? 0,
|
||||
artUrl: this.activePlayer?.trackArtUrl ?? "",
|
||||
title: this.activePlayer?.trackTitle || "Unknown Title",
|
||||
artist: this.activePlayer?.trackArtist || "Unknown Artist",
|
||||
album: this.activePlayer?.trackAlbum || "Unknown Album",
|
||||
};
|
||||
|
||||
this.trackChanged(__reverse);
|
||||
this.__reverse = false;
|
||||
}
|
||||
|
||||
property bool isPlaying: this.activePlayer && this.activePlayer.playbackState == MprisPlaybackState.Playing;
|
||||
property bool canPlay: this.activePlayer?.canPlay ?? false;
|
||||
function play() {
|
||||
if (this.canPlay) this.activePlayer.playbackState = MprisPlaybackState.Playing;
|
||||
}
|
||||
|
||||
property bool canPause: this.activePlayer?.canPause ?? false;
|
||||
function pause() {
|
||||
if (this.canPause) this.activePlayer.playbackState = MprisPlaybackState.Paused;
|
||||
property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying;
|
||||
property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false;
|
||||
function togglePlaying() {
|
||||
if (this.canTogglePlaying) this.activePlayer.togglePlaying();
|
||||
}
|
||||
|
||||
property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false;
|
||||
|
|
@ -125,7 +130,7 @@ Singleton {
|
|||
}
|
||||
|
||||
function setActivePlayer(player: MprisPlayer) {
|
||||
const targetPlayer = player ?? MprisPlayer.players[0];
|
||||
const targetPlayer = player ?? Mpris.players[0];
|
||||
console.log(`setactive: ${targetPlayer} from ${activePlayer}`)
|
||||
|
||||
if (targetPlayer && this.activePlayer) {
|
||||
|
|
@ -138,31 +143,17 @@ Singleton {
|
|||
this.trackedPlayer = targetPlayer;
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
name: "music-pauseall";
|
||||
onPressed: {
|
||||
for (let i = 0; i < Mpris.players.length; i++) {
|
||||
const player = Mpris.players[i];
|
||||
if (player.canPause) player.playbackState = MprisPlaybackState.Paused;
|
||||
IpcHandler {
|
||||
target: "mpris"
|
||||
|
||||
function pauseAll(): void {
|
||||
for (const player of Mpris.players.values) {
|
||||
if (player.canPause) player.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
name: "music-playpause";
|
||||
onPressed: {
|
||||
if (root.isPlaying) root.pause();
|
||||
else root.play();
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
name: "music-previous";
|
||||
onPressed: root.previous();
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
name: "music-next";
|
||||
onPressed: root.next();
|
||||
function playPause(): void { root.togglePlaying(); }
|
||||
function previous(): void { root.previous(); }
|
||||
function next(): void { root.next(); }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
|
||||
Scope {
|
||||
required property MprisPlayer player;
|
||||
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
import QtQuick
|
||||
import
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
|
@ -7,9 +9,10 @@ import Quickshell.Services.Mpris
|
|||
import ".."
|
||||
import "../.."
|
||||
|
||||
MouseArea {
|
||||
FullwidthMouseArea {
|
||||
id: root
|
||||
hoverEnabled: true
|
||||
fillWindowWidth: true
|
||||
|
||||
required property var bar;
|
||||
implicitHeight: column.implicitHeight + 10
|
||||
|
|
@ -27,12 +30,12 @@ MouseArea {
|
|||
property alias widgetOpen: persist.widgetOpen;
|
||||
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: widgetOpen = !widgetOpen
|
||||
onPressed: widgetOpen = !widgetOpen
|
||||
|
||||
onWheel: event => {
|
||||
event.accepted = true;
|
||||
if (MprisController.canChangeVolume) {
|
||||
this.activePlayer.volume = Math.max(0, Math.min(1, this.activePlayer.volume + (event.angleDelta.y / 120) * 0.05));
|
||||
root.activePlayer.volume = Math.max(0, Math.min(1, root.activePlayer.volume + (event.angleDelta.y / 120) * 0.05));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +45,7 @@ MouseArea {
|
|||
id: widget
|
||||
anchors.fill: parent
|
||||
|
||||
property real scaleMul: root.pressed || widgetOpen ? 100 : 1
|
||||
property real scaleMul: widgetOpen ? 100 : 1
|
||||
Behavior on scaleMul { SmoothedAnimation { velocity: 600 } }
|
||||
scale: scaleCurve.interpolate(scaleMul / 100, 1, (width - 6) / width)
|
||||
|
||||
|
|
@ -56,8 +59,10 @@ MouseArea {
|
|||
BackgroundArt {
|
||||
id: bkg
|
||||
anchors.fill: parent
|
||||
overlay.color: "#30000000"
|
||||
|
||||
function updateArt(reverse: bool) {
|
||||
console.log("update art", MprisController.activeTrack.artUrl)
|
||||
this.setArt(MprisController.activeTrack.artUrl, reverse, false)
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +91,7 @@ MouseArea {
|
|||
implicitHeight: width
|
||||
scaleIcon: false
|
||||
baseMargin: 3
|
||||
hoverEnabled: false
|
||||
enabled: MprisController.canGoPrevious;
|
||||
onClicked: MprisController.previous();
|
||||
}
|
||||
|
|
@ -95,11 +101,9 @@ MouseArea {
|
|||
image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`;
|
||||
implicitHeight: width
|
||||
scaleIcon: false
|
||||
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay;
|
||||
onClicked: {
|
||||
if (MprisController.isPlaying) MprisController.pause();
|
||||
else MprisController.play();
|
||||
}
|
||||
hoverEnabled: false
|
||||
enabled: MprisController.canTogglePlaying;
|
||||
onClicked: MprisController.togglePlaying();
|
||||
}
|
||||
|
||||
ClickableIcon {
|
||||
|
|
@ -108,51 +112,162 @@ MouseArea {
|
|||
implicitHeight: width
|
||||
scaleIcon: false
|
||||
baseMargin: 3
|
||||
hoverEnabled: false
|
||||
enabled: MprisController.canGoNext;
|
||||
onClicked: MprisController.next();
|
||||
}
|
||||
}
|
||||
|
||||
property var tooltip: TooltipItem {
|
||||
property Scope positionInfo: Scope {
|
||||
id: positionInfo
|
||||
|
||||
property var player: root.activePlayer;
|
||||
property int position: Math.floor(player.position);
|
||||
property int length: Math.floor(player.length);
|
||||
|
||||
FrameAnimation {
|
||||
id: posTracker;
|
||||
running: positionInfo.player.isPlaying && (tooltip.visible || rightclickMenu.visible);
|
||||
onTriggered: positionInfo.player.positionChanged();
|
||||
}
|
||||
|
||||
function timeStr(time: int): string {
|
||||
const seconds = time % 60;
|
||||
const minutes = Math.floor(time / 60);
|
||||
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
}
|
||||
|
||||
property TooltipItem tooltip: TooltipItem {
|
||||
id: tooltip
|
||||
tooltip: bar.tooltip
|
||||
owner: root
|
||||
|
||||
show: root.containsMouse && (activePlayer?.metadata["mpris:trackid"] ?? false)
|
||||
show: root.containsMouse
|
||||
|
||||
//implicitHeight: root.height - 10
|
||||
//implicitWidth: childrenRect.width
|
||||
/*ColumnLayout {
|
||||
ColumnLayout {
|
||||
visible: MprisController.activePlayer != null
|
||||
|
||||
Label { text: MprisController.activeTrack?.title ?? "" }
|
||||
|
||||
Label {
|
||||
text: {
|
||||
const artist = MprisController.activeTrack?.artist ?? "";
|
||||
const album = MprisController.activeTrack?.album ?? "";
|
||||
|
||||
return artist + (album ? ` - ${album}` : "");
|
||||
}
|
||||
}
|
||||
|
||||
Label { text: MprisController.activePlayer?.identity ?? "" }
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: MprisController.activePlayer == null
|
||||
text: "No media playing"
|
||||
}
|
||||
|
||||
Rectangle { implicitHeight: 10; color: "white"; Layout.fillWidth: true }
|
||||
}*/
|
||||
|
||||
contentItem.anchors.margins: 0
|
||||
|
||||
Item {
|
||||
implicitWidth: 200
|
||||
implicitHeight: 100
|
||||
}
|
||||
id: ttcontent
|
||||
width: parent.width
|
||||
height: Math.max(parent.height, implicitHeight)
|
||||
implicitWidth: cl.implicitWidth + 10
|
||||
implicitHeight: cl.implicitHeight + 10 + (MprisController.activePlayer ? 8 : 0)
|
||||
|
||||
/*Loader {
|
||||
active: tooltip.visible
|
||||
ColumnLayout {
|
||||
id: cl
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
margins: 5
|
||||
}
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
height: root.height - 10
|
||||
RowLayout {
|
||||
Image {
|
||||
Layout.fillHeight: true
|
||||
source: mainPlayer.metadata["mpris:artUrl"] ?? ""
|
||||
//visible: MprisController.activePlayer != null
|
||||
|
||||
FontMetrics { id: fontmetrics }
|
||||
|
||||
component FullheightLabel: Item {
|
||||
implicitHeight: fontmetrics.height
|
||||
implicitWidth: label.implicitWidth
|
||||
|
||||
property alias text: label.text
|
||||
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: height
|
||||
sourceSize.height: height
|
||||
}
|
||||
Label {
|
||||
text: mainPlayer.identity
|
||||
id: label
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Slider {
|
||||
Layout.fillWidth: true
|
||||
FullheightLabel {
|
||||
visible: MprisController.activePlayer != null
|
||||
text: MprisController.activeTrack?.title ?? ""
|
||||
}
|
||||
|
||||
FullheightLabel {
|
||||
visible: MprisController.activePlayer != null
|
||||
text: MprisController.activeTrack?.artist ?? ""
|
||||
/*text: {
|
||||
const artist = MprisController.activeTrack?.artist ?? "";
|
||||
const album = MprisController.activeTrack?.album ?? "";
|
||||
|
||||
return artist + (album ? ` - ${album}` : "");
|
||||
}*/
|
||||
}
|
||||
|
||||
Label {
|
||||
text: {
|
||||
if (!MprisController.activePlayer) return "No media playing";
|
||||
|
||||
return MprisController.activePlayer?.identity + " - "
|
||||
+ positionInfo.timeStr(positionInfo.position) + " / "
|
||||
+ positionInfo.timeStr(positionInfo.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
Rectangle {
|
||||
id: ttprect
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
color: "#30ceffff"
|
||||
implicitHeight: 8
|
||||
visible: MprisController.activePlayer != null
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
color: "#80ceffff"
|
||||
width: parent.width * (root.activePlayer.position / root.activePlayer.length)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: ttcontent.width
|
||||
height: ttcontent.height
|
||||
bottomLeftRadius: 5
|
||||
bottomRightRadius: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var rightclickMenu: TooltipItem {
|
||||
|
|
@ -182,6 +297,7 @@ MouseArea {
|
|||
target: MprisController
|
||||
|
||||
function onTrackChanged(reverse: bool) {
|
||||
console.log(`track changed: rev: ${reverse}`)
|
||||
popupBkg.setArt(MprisController.activeTrack.artUrl, reverse, false);
|
||||
}
|
||||
}
|
||||
|
|
@ -191,71 +307,41 @@ MouseArea {
|
|||
}
|
||||
}
|
||||
|
||||
contentItem {
|
||||
implicitWidth: 500
|
||||
implicitHeight: 650
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: rightclickMenu.visible
|
||||
width: 500
|
||||
height: 650
|
||||
active: rightclickMenu.visible
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
property var player: activePlayer;
|
||||
anchors.fill: parent;
|
||||
|
||||
property int position: 0;
|
||||
property int length: 0;
|
||||
|
||||
FrameAnimation {
|
||||
id: posTracker;
|
||||
running: player.playbackState == MprisPlaybackState.Playing && widgetOpen;
|
||||
onTriggered: player.positionChanged();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: player
|
||||
|
||||
function onPositionChanged() {
|
||||
const newPosition = Math.floor(player.position);
|
||||
if (newPosition != position) position = newPosition;
|
||||
}
|
||||
|
||||
function onLengthChanged() {
|
||||
const newLength = Math.floor(player.length);
|
||||
if (newLength != length) length = newLength;
|
||||
}
|
||||
}
|
||||
property var player: root.activePlayer;
|
||||
|
||||
Connections {
|
||||
target: MprisController
|
||||
|
||||
function onTrackChanged(reverse: bool) {
|
||||
trackStack.updateTrack(reverse, false);
|
||||
length = Math.floor(player.length);
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
position = Math.floor(player.position);
|
||||
length = Math.floor(player.length);
|
||||
}
|
||||
|
||||
function timeStr(time: int): string {
|
||||
const seconds = time % 60;
|
||||
const minutes = Math.floor(time / 60);
|
||||
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
Item {
|
||||
id: playerSelectorContainment
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: playerSelector.implicitHeight + 20
|
||||
implicitWidth: playerSelector.implicitWidth
|
||||
|
||||
ScrollView {
|
||||
RowLayout { //ScrollView {
|
||||
id: playerSelector
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(implicitWidth, playerSelectorContainment.width)
|
||||
|
||||
RowLayout {
|
||||
//RowLayout {
|
||||
Repeater {
|
||||
model: Mpris.players
|
||||
|
||||
|
|
@ -281,8 +367,7 @@ MouseArea {
|
|||
source: {
|
||||
const entry = DesktopEntries.byId(modelData.desktopEntry);
|
||||
console.log(`ent ${entry} id ${modelData.desktopEntry}`)
|
||||
if (!entry) return "image://icon/";
|
||||
return `image://icon/${entry.icon}`;
|
||||
return Quickshell.iconPath(entry?.icon);
|
||||
}
|
||||
//asynchronous: true
|
||||
|
||||
|
|
@ -292,18 +377,18 @@ MouseArea {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: 10
|
||||
Layout.bottomMargin: 20
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: activePlayer.identity
|
||||
text: root.activePlayer.identity
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -311,7 +396,13 @@ MouseArea {
|
|||
id: trackStack
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 400
|
||||
clip: animating || (lastFlicked?.contentX ?? 0) != 0
|
||||
|
||||
// inverse of default tooltip margin - 1px for border
|
||||
Layout.leftMargin: -4
|
||||
Layout.rightMargin: -4
|
||||
|
||||
property Flickable lastFlicked;
|
||||
property bool reverse: false;
|
||||
|
||||
Component.onCompleted: updateTrack(false, true);
|
||||
|
|
@ -333,14 +424,16 @@ MouseArea {
|
|||
// but may take longer if the image is huge.
|
||||
readonly property bool svReady: img.status === Image.Ready;
|
||||
contentWidth: width + 1
|
||||
onDragStarted: trackStack.lastFlicked = this
|
||||
onDragEnded: {
|
||||
return;
|
||||
//return;
|
||||
console.log(`dragend ${contentX}`)
|
||||
if (Math.abs(contentX) > 75) {
|
||||
if (contentX < 0) MprisController.previous();
|
||||
else if (contentX > 0) MprisController.next();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: trackContent
|
||||
width: flickable.width
|
||||
|
|
@ -348,7 +441,7 @@ MouseArea {
|
|||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 300//img.implicitHeight
|
||||
implicitHeight: 302//img.implicitHeight
|
||||
implicitWidth: img.implicitWidth
|
||||
|
||||
Image {
|
||||
|
|
@ -362,27 +455,50 @@ MouseArea {
|
|||
|
||||
sourceSize.height: 300
|
||||
sourceSize.width: 300
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
cached: true
|
||||
maskSource: Rectangle {
|
||||
width: img.width
|
||||
height: img.height
|
||||
radius: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
component CenteredText: Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 20
|
||||
|
||||
property alias text: label.text
|
||||
property alias font: label.font
|
||||
|
||||
Label {
|
||||
id: label
|
||||
visible: text != ""
|
||||
anchors.centerIn: parent
|
||||
text: track.title
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(parent.width - 20, implicitWidth)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
CenteredText {
|
||||
Layout.topMargin: 20
|
||||
text: track.title
|
||||
font.pointSize: albumLabel.font.pointSize + 1
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: track.artist
|
||||
}
|
||||
CenteredText {
|
||||
id: albumLabel
|
||||
Layout.topMargin: 18
|
||||
text: track.album
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
CenteredText {
|
||||
Layout.topMargin: 25
|
||||
text: track.artist
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
|
@ -462,11 +578,8 @@ MouseArea {
|
|||
implicitWidth: 80
|
||||
implicitHeight: width
|
||||
scaleIcon: false
|
||||
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay
|
||||
onClicked: {
|
||||
if (MprisController.isPlaying) MprisController.pause();
|
||||
else MprisController.play();
|
||||
}
|
||||
enabled: MprisController.canTogglePlaying;
|
||||
onClicked: MprisController.togglePlaying();
|
||||
}
|
||||
|
||||
ClickableIcon {
|
||||
|
|
@ -491,33 +604,70 @@ MouseArea {
|
|||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.margins: 5
|
||||
|
||||
Label {
|
||||
Layout.preferredWidth: lengthLabel.implicitWidth
|
||||
text: timeStr(position)
|
||||
text: positionInfo.timeStr(positionInfo.position)
|
||||
}
|
||||
|
||||
MediaSlider {
|
||||
id: slider
|
||||
property bool bindSlider: true;
|
||||
|
||||
property real boundAnimStart: 0;
|
||||
property real boundAnimFactor: 1;
|
||||
property real lastPosition: 0;
|
||||
property real lastLength: 0;
|
||||
property real boundPosition: {
|
||||
const ppos = player.position / player.length;
|
||||
const bpos = boundAnimStart;
|
||||
return (ppos * boundAnimFactor) + (bpos * (1.0 - boundAnimFactor));
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: boundAnim
|
||||
target: slider
|
||||
property: "boundAnimFactor"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: 600
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: player
|
||||
|
||||
function onPositionChanged() {
|
||||
if (false && player.position == 0 && slider.lastPosition != 0 && !boundAnim.running) {
|
||||
slider.boundAnimStart = slider.lastPosition / slider.lastLength;
|
||||
boundAnim.start();
|
||||
}
|
||||
|
||||
slider.lastPosition = player.position;
|
||||
slider.lastLength = player.length;
|
||||
}
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
property var bindSlider: true;
|
||||
enabled: player.canSeek
|
||||
from: 0
|
||||
to: player.length
|
||||
to: 1
|
||||
|
||||
onPressedChanged: {
|
||||
if (!pressed) player.position = value;
|
||||
if (!pressed) player.position = value * player.length;
|
||||
bindSlider = !pressed;
|
||||
}
|
||||
|
||||
Binding {
|
||||
when: slider.bindSlider
|
||||
slider.value: player.position
|
||||
slider.value: slider.boundPosition
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: lengthLabel
|
||||
text: timeStr(length)
|
||||
text: positionInfo.timeStr(positionInfo.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import QtQuick
|
||||
import Quickshell.Services.UPower
|
||||
import "root:."
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property UPowerDevice device;
|
||||
property real scale: 1;
|
||||
|
||||
readonly property bool isCharging: root.device.state == UPowerDeviceState.Charging;
|
||||
readonly property bool isPluggedIn: isCharging || root.device.state == UPowerDeviceState.PendingCharge;
|
||||
readonly property bool isLow: root.device.percentage <= 0.20;
|
||||
|
||||
width: 35 * root.scale
|
||||
height: 35 * root.scale
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 4 * root.scale
|
||||
}
|
||||
|
||||
width: 13 * root.scale
|
||||
height: 23 * root.device.percentage * root.scale
|
||||
radius: 2 * root.scale
|
||||
|
||||
color: root.isPluggedIn ? "#359040"
|
||||
: ShellGlobals.interpolateColors(Math.min(1.0, Math.min(0.5, root.device.percentage) * 2), "red", "white")
|
||||
}
|
||||
|
||||
Image {
|
||||
id: img
|
||||
anchors.fill: parent;
|
||||
|
||||
source: root.isCharging ? "root:icons/battery-charging.svg"
|
||||
: root.isPluggedIn ? "root:icons/battery-plus.svg"
|
||||
: root.isLow ? "root:icons/battery-warning.svg"
|
||||
: "root:icons/battery-empty.svg"
|
||||
|
||||
sourceSize.width: parent.width
|
||||
sourceSize.height: parent.height
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
224
modules/user/modules/quickshell/shell/bar/power/Power.qml
Normal file
224
modules/user/modules/quickshell/shell/bar/power/Power.qml
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import Quickshell.Widgets
|
||||
import ".."
|
||||
import "root:."
|
||||
import "root:components"
|
||||
import "power"
|
||||
|
||||
BarWidgetInner {
|
||||
id: root
|
||||
required property var bar;
|
||||
|
||||
readonly property var chargeState: UPower.displayDevice.state
|
||||
readonly property bool isCharging: chargeState == UPowerDeviceState.Charging;
|
||||
readonly property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge;
|
||||
readonly property real percentage: UPower.displayDevice.percentage
|
||||
readonly property bool isLow: percentage <= 0.20
|
||||
|
||||
readonly property UPowerDevice batteryDevice: UPower.devices.values
|
||||
.find(device => device.isLaptopBattery);
|
||||
|
||||
function statusStr() {
|
||||
return root.isPluggedIn ? `Plugged in, ${root.isCharging ? "Charging" : "Not Charging"}`
|
||||
: "Discharging";
|
||||
}
|
||||
|
||||
property bool showMenu: false;
|
||||
|
||||
implicitHeight: width
|
||||
color: isLow ? "#45ff6060" : ShellGlobals.colors.widget
|
||||
|
||||
BarButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
baseMargin: 5
|
||||
fillWindowWidth: true
|
||||
acceptedButtons: Qt.RightButton
|
||||
directScale: true
|
||||
showPressed: root.showMenu
|
||||
|
||||
onPressed: {
|
||||
root.showMenu = !root.showMenu
|
||||
}
|
||||
|
||||
BatteryIcon {
|
||||
device: UPower.displayDevice
|
||||
}
|
||||
}
|
||||
|
||||
property TooltipItem tooltip: TooltipItem {
|
||||
id: tooltip
|
||||
tooltip: bar.tooltip
|
||||
owner: root
|
||||
show: button.containsMouse
|
||||
|
||||
Loader {
|
||||
active: tooltip.visible
|
||||
|
||||
sourceComponent: Label {
|
||||
text: {
|
||||
const status = root.statusStr();
|
||||
|
||||
const percentage = Math.round(root.percentage * 100);
|
||||
|
||||
let str = `${percentage}% - ${status}`;
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property TooltipItem rightclickMenu: TooltipItem {
|
||||
id: rightclickMenu
|
||||
tooltip: bar.tooltip
|
||||
owner: root
|
||||
|
||||
isMenu: true
|
||||
show: root.showMenu
|
||||
onClose: root.showMenu = false
|
||||
|
||||
Loader {
|
||||
active: rightclickMenu.visible
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: 10
|
||||
|
||||
FontMetrics { id: fm }
|
||||
|
||||
component SmallLabel: Label {
|
||||
font.pointSize: fm.font.pointSize * 0.8
|
||||
color: "#d0eeffff"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
IconImage {
|
||||
source: "root:icons/gauge.svg"
|
||||
implicitSize: 32
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
Label { text: "Power Profile" }
|
||||
|
||||
OptionSlider {
|
||||
values: ["Power Save", "Balanced", "Performance"]
|
||||
index: PowerProfiles.profile
|
||||
onIndexChanged: PowerProfiles.profile = this.index;
|
||||
implicitWidth: 350
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
IconImage {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
source: "root:icons/battery-empty.svg"
|
||||
implicitSize: 32
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Label { text: "Battery" }
|
||||
Item { Layout.fillWidth: true }
|
||||
Label {
|
||||
text: `${root.statusStr()} -`
|
||||
color: "#d0eeffff"
|
||||
}
|
||||
Label { text: `${Math.round(root.percentage * 100)}%` }
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 5
|
||||
Layout.fillWidth: true
|
||||
value: UPower.displayDevice.percentage
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: remainingTimeLbl.text !== ""
|
||||
|
||||
SmallLabel { text: "Time remaining" }
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
SmallLabel {
|
||||
id: remainingTimeLbl
|
||||
text: {
|
||||
const device = UPower.displayDevice;
|
||||
const time = device.timeToEmpty || device.timeToFull;
|
||||
|
||||
if (time === 0) return "";
|
||||
const minutes = Math.floor(time / 60).toString().padStart(2, '0');
|
||||
return `${minutes} minutes`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: root.batteryDevice.healthSupported
|
||||
SmallLabel { text: "Health" }
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
SmallLabel {
|
||||
text: `${Math.floor((root.batteryDevice?.healthPercentage ?? 0))}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
// external devices
|
||||
values: UPower.devices.values.filter(device => !device.powerSupply)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
required property UPowerDevice modelData;
|
||||
|
||||
IconImage {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
source: {
|
||||
switch (modelData.type) {
|
||||
case UPowerDeviceType.Headset: return "root:icons/headset.svg";
|
||||
}
|
||||
return Quickshell.iconPath(modelData.iconName)
|
||||
}
|
||||
implicitSize: 32
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Label { text: modelData.model }
|
||||
Item { Layout.fillWidth: true }
|
||||
Label { text: `${Math.round(modelData.percentage * 100)}%` }
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 5
|
||||
Layout.fillWidth: true
|
||||
value: modelData.percentage
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: modelData.healthSupported
|
||||
SmallLabel { text: "Health" }
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
SmallLabel {
|
||||
text: `${Math.floor(modelData.healthPercentage)}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ Item {
|
|||
property bool expanded: false;
|
||||
|
||||
readonly property bool open: progress != 0;
|
||||
readonly property bool animating: internalProgress != -1 && internalProgress != 101;
|
||||
readonly property bool animating: internalProgress != (expanded ? 101 : -1);
|
||||
|
||||
implicitHeight: 16
|
||||
implicitWidth: 16
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.DBusMenu
|
||||
import "../.."
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
required property var entry;
|
||||
required property QsMenuEntry entry;
|
||||
property alias expanded: childrenRevealer.expanded;
|
||||
property bool animating: childrenRevealer.animating || childrenList.animating;
|
||||
property bool animating: childrenRevealer.animating || (childMenuLoader?.item?.animating ?? false);
|
||||
// appears it won't actually create the handler when only used from MenuItemList.
|
||||
onExpandedChanged: {}
|
||||
onAnimatingChanged: {}
|
||||
|
|
@ -22,7 +23,7 @@ MouseArea {
|
|||
onClicked: {
|
||||
if (entry.hasChildren) childrenRevealer.expanded = !childrenRevealer.expanded
|
||||
else {
|
||||
entry.click();
|
||||
entry.triggered();
|
||||
if (entry.toggleType == ToggleButtonType.None) close();
|
||||
}
|
||||
}
|
||||
|
|
@ -34,19 +35,21 @@ MouseArea {
|
|||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
id: innerRow
|
||||
|
||||
Item {
|
||||
implicitWidth: 22
|
||||
implicitHeight: 22
|
||||
|
||||
MenuCheckBox {
|
||||
anchors.centerIn: parent
|
||||
visible: entry.toggleType == ToggleButtonType.CheckBox
|
||||
visible: entry.buttonType == QsMenuButtonType.CheckBox
|
||||
checkState: entry.checkState
|
||||
}
|
||||
|
||||
MenuRadioButton {
|
||||
anchors.centerIn: parent
|
||||
visible: entry.toggleType == ToggleButtonType.RadioButton
|
||||
visible: entry.buttonType == QsMenuButtonType.RadioButton
|
||||
checkState: entry.checkState
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +62,7 @@ MouseArea {
|
|||
}
|
||||
|
||||
Text {
|
||||
text: entry.cleanLabel
|
||||
text: entry.text
|
||||
color: entry.enabled ? "white" : "#bbbbbb"
|
||||
}
|
||||
|
||||
|
|
@ -68,25 +71,32 @@ MouseArea {
|
|||
implicitWidth: 22
|
||||
implicitHeight: 22
|
||||
|
||||
Image {
|
||||
IconImage {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: entry.icon != ""
|
||||
source: entry.icon
|
||||
sourceSize.height: parent.height
|
||||
sourceSize.width: parent.height
|
||||
visible: source != ""
|
||||
implicitSize: parent.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Loader {
|
||||
id: childMenuLoader
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: childrenList.implicitHeight * childrenRevealer.progress
|
||||
Layout.preferredHeight: active ? item.implicitHeight * childrenRevealer.progress : 0
|
||||
|
||||
readonly property real widthDifference: {
|
||||
Math.max(0, (item?.implicitWidth ?? 0) - innerRow.implicitWidth);
|
||||
}
|
||||
Layout.preferredWidth: active ? innerRow.implicitWidth + (widthDifference * childrenRevealer.progress) : 0
|
||||
|
||||
active: root.expanded || root.animating
|
||||
clip: true
|
||||
|
||||
MenuItemList {
|
||||
sourceComponent: MenuView {
|
||||
id: childrenList
|
||||
items: entry.children
|
||||
menu: entry
|
||||
onClose: root.close()
|
||||
|
||||
anchors {
|
||||
|
|
|
|||
|
|
@ -6,17 +6,19 @@ import "../.."
|
|||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
required property var items;
|
||||
property alias menu: menuView.menu;
|
||||
property Item animatingItem: null;
|
||||
property bool animating: animatingItem != null;
|
||||
|
||||
signal close();
|
||||
signal submenuExpanded(item: var);
|
||||
|
||||
QsMenuOpener { id: menuView }
|
||||
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: items
|
||||
model: menuView.children;
|
||||
|
||||
Loader {
|
||||
required property var modelData;
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
|
|
@ -5,102 +7,102 @@ import Quickshell
|
|||
import Quickshell.Services.SystemTray
|
||||
import ".."
|
||||
|
||||
OverlayWidget {
|
||||
BarWidgetInner {
|
||||
id: root
|
||||
expandedWidth: 600
|
||||
expandedHeight: 800
|
||||
required property var bar;
|
||||
implicitHeight: column.implicitHeight + 10
|
||||
|
||||
BarWidgetInner {
|
||||
implicitHeight: column.implicitHeight + 10
|
||||
ColumnLayout {
|
||||
id: column
|
||||
implicitHeight: childrenRect.height
|
||||
spacing: 5
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
implicitHeight: childrenRect.height
|
||||
spacing: 5
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: 5
|
||||
}
|
||||
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: 5
|
||||
}
|
||||
Repeater {
|
||||
model: SystemTray.items;
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items;
|
||||
Item {
|
||||
id: item
|
||||
required property SystemTrayItem modelData;
|
||||
|
||||
Item {
|
||||
required property var modelData;
|
||||
readonly property alias menu: menuWatcher.menu;
|
||||
property bool targetMenuOpen: false;
|
||||
|
||||
SystemTrayMenuWatcher {
|
||||
id: menuWatcher;
|
||||
trayItem: modelData;
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: width
|
||||
|
||||
ClickableIcon {
|
||||
id: mouseArea
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
width: height
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
|
||||
image: item.modelData.icon
|
||||
showPressed: item.targetMenuOpen || (pressedButtons & ~Qt.RightButton)
|
||||
fillWindowWidth: true
|
||||
extraVerticalMargin: column.spacing / 2
|
||||
|
||||
onClicked: event => {
|
||||
event.accepted = true;
|
||||
|
||||
if (event.button == Qt.LeftButton) {
|
||||
item.modelData.activate();
|
||||
} else if (event.button == Qt.MiddleButton) {
|
||||
item.modelData.secondaryActivate();
|
||||
}
|
||||
}
|
||||
|
||||
property bool targetMenuOpen: false;
|
||||
onTargetMenuOpenChanged: menu.showChildren = targetMenuOpen
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: width
|
||||
|
||||
ClickableIcon {
|
||||
id: mouseArea
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
onPressed: event => {
|
||||
if (event.button == Qt.RightButton && item.modelData.hasMenu) {
|
||||
item.targetMenuOpen = !item.targetMenuOpen;
|
||||
}
|
||||
width: height
|
||||
}
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onWheel: event => {
|
||||
event.accepted = true;
|
||||
const points = event.angleDelta.y / 120
|
||||
item.modelData.scroll(points, false);
|
||||
}
|
||||
|
||||
image: modelData.icon
|
||||
showPressed: targetMenuOpen
|
||||
property var tooltip: TooltipItem {
|
||||
tooltip: root.bar.tooltip
|
||||
owner: mouseArea
|
||||
|
||||
onClicked: event => {
|
||||
event.accepted = true;
|
||||
show: mouseArea.containsMouse
|
||||
|
||||
if (event.button == Qt.LeftButton) {
|
||||
modelData.activate();
|
||||
} else if (event.button == Qt.MiddleButton) {
|
||||
modelData.secondaryActivate();
|
||||
} else if (event.button == Qt.RightButton && menu != null) {
|
||||
targetMenuOpen = !targetMenuOpen;
|
||||
}
|
||||
Text {
|
||||
id: tooltipText
|
||||
text: item.modelData.tooltipTitle != "" ? item.modelData.tooltipTitle : item.modelData.id
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: event => {
|
||||
event.accepted = true;
|
||||
const points = event.angleDelta.y / 120
|
||||
modelData.scroll(points, false);
|
||||
}
|
||||
property var rightclickMenu: TooltipItem {
|
||||
id: rightclickMenu
|
||||
tooltip: root.bar.tooltip
|
||||
owner: mouseArea
|
||||
|
||||
property var tooltip: TooltipItem {
|
||||
tooltip: bar.tooltip
|
||||
owner: mouseArea
|
||||
isMenu: true
|
||||
show: item.targetMenuOpen
|
||||
animateSize: !(menuContentLoader?.item?.animating ?? false)
|
||||
|
||||
show: mouseArea.containsMouse
|
||||
onClose: item.targetMenuOpen = false;
|
||||
|
||||
Text {
|
||||
id: tooltipText
|
||||
text: modelData.tooltipTitle != "" ? modelData.tooltipTitle : modelData.id
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: menuContentLoader
|
||||
active: item.targetMenuOpen || rightclickMenu.visible || mouseArea.containsMouse
|
||||
|
||||
property var rightclickMenu: TooltipItem {
|
||||
tooltip: bar.tooltip
|
||||
owner: mouseArea
|
||||
|
||||
isMenu: true
|
||||
show: targetMenuOpen && menu.showChildren
|
||||
animateSize: !rightclickItems.animating
|
||||
|
||||
onClose: targetMenuOpen = false;
|
||||
|
||||
MenuItemList {
|
||||
id: rightclickItems
|
||||
items: menu == null ? [] : menu.children
|
||||
onClose: targetMenuOpen = false;
|
||||
sourceComponent: MenuView {
|
||||
menu: item.modelData.menu
|
||||
onClose: item.targetMenuOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue