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