1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

media: fix player button control popup things

This commit is contained in:
bbedward
2025-11-24 20:51:05 -05:00
parent fa98a27c90
commit 5288d042ca
4 changed files with 761 additions and 737 deletions

View File

@@ -1,11 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.DankDash
@@ -27,42 +23,130 @@ DankPopout {
property bool __focusArmed: false
property bool __contentReady: false
property var __mediaTabRef: null
property int __dropdownType: 0
property point __dropdownAnchor: Qt.point(0, 0)
property bool __dropdownRightEdge: false
property var __dropdownPlayer: null
property var __dropdownPlayers: []
function __showVolumeDropdown(pos, rightEdge, player, players) {
__dropdownAnchor = pos;
__dropdownRightEdge = rightEdge;
__dropdownPlayer = player;
__dropdownPlayers = players;
__dropdownType = 1;
}
function __showAudioDevicesDropdown(pos, rightEdge) {
__dropdownAnchor = pos;
__dropdownRightEdge = rightEdge;
__dropdownType = 2;
}
function __showPlayersDropdown(pos, rightEdge, player, players) {
__dropdownAnchor = pos;
__dropdownRightEdge = rightEdge;
__dropdownPlayer = player;
__dropdownPlayers = players;
__dropdownType = 3;
}
function __hideDropdowns() {
__volumeCloseTimer.stop();
__dropdownType = 0;
__mediaTabRef?.resetDropdownStates();
}
function __startCloseTimer() {
__volumeCloseTimer.restart();
}
function __stopCloseTimer() {
__volumeCloseTimer.stop();
}
Timer {
id: __volumeCloseTimer
interval: 400
onTriggered: {
if (__dropdownType === 1) {
__hideDropdowns();
}
}
}
overlayContent: Component {
MediaDropdownOverlay {
dropdownType: root.__dropdownType
anchorPos: root.__dropdownAnchor
isRightEdge: root.__dropdownRightEdge
activePlayer: root.__dropdownPlayer
allPlayers: root.__dropdownPlayers
onCloseRequested: root.__hideDropdowns()
onPanelEntered: root.__stopCloseTimer()
onPanelExited: root.__startCloseTimer()
onVolumeChanged: volume => {
const player = root.__dropdownPlayer;
const isChrome = player?.identity?.toLowerCase().includes("chrome") || player?.identity?.toLowerCase().includes("chromium");
const usePlayerVolume = player && player.volumeSupported && !isChrome;
if (usePlayerVolume) {
player.volume = volume;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = volume;
}
}
onPlayerSelected: player => {
const currentPlayer = MprisController.activePlayer;
if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) {
currentPlayer.pause();
}
MprisController.activePlayer = player;
root.__hideDropdowns();
}
onDeviceSelected: device => {
root.__hideDropdowns();
}
}
}
function __tryFocusOnce() {
if (!__focusArmed)
return
const win = root.window
return;
const win = root.window;
if (!win || !win.visible)
return
return;
if (!contentLoader.item)
return
return;
if (win.requestActivate)
win.requestActivate()
contentLoader.item.forceActiveFocus(Qt.TabFocusReason)
win.requestActivate();
contentLoader.item.forceActiveFocus(Qt.TabFocusReason);
if (contentLoader.item.activeFocus)
__focusArmed = false
__focusArmed = false;
}
onDashVisibleChanged: {
if (dashVisible) {
__focusArmed = true
__contentReady = !!contentLoader.item
open()
__tryFocusOnce()
__focusArmed = true;
__contentReady = !!contentLoader.item;
open();
__tryFocusOnce();
} else {
__focusArmed = false
__contentReady = false
close()
__focusArmed = false;
__contentReady = false;
__hideDropdowns();
close();
}
}
Connections {
target: contentLoader
function onLoaded() {
__contentReady = true
__contentReady = true;
if (__focusArmed)
__tryFocusOnce()
__tryFocusOnce();
}
}
@@ -71,12 +155,12 @@ DankPopout {
enabled: !!root.window
function onVisibleChanged() {
if (__focusArmed)
__tryFocusOnce()
__tryFocusOnce();
}
}
onBackgroundClicked: {
dashVisible = false
dashVisible = false;
}
content: Component {
@@ -90,7 +174,7 @@ DankPopout {
Component.onCompleted: {
if (root.shouldBeVisible) {
mainContainer.forceActiveFocus()
mainContainer.forceActiveFocus();
}
}
@@ -99,54 +183,54 @@ DankPopout {
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(function () {
mainContainer.forceActiveFocus()
})
mainContainer.forceActiveFocus();
});
}
}
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
root.dashVisible = false
event.accepted = true
return
root.dashVisible = false;
event.accepted = true;
return;
}
if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) {
let nextIndex = root.currentTabIndex + 1
let nextIndex = root.currentTabIndex + 1;
while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) {
nextIndex++
nextIndex++;
}
if (nextIndex >= tabBar.model.length) {
nextIndex = 0
nextIndex = 0;
}
root.currentTabIndex = nextIndex
event.accepted = true
return
root.currentTabIndex = nextIndex;
event.accepted = true;
return;
}
if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {
let prevIndex = root.currentTabIndex - 1
let prevIndex = root.currentTabIndex - 1;
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex--
prevIndex--;
}
if (prevIndex < 0) {
prevIndex = tabBar.model.length - 1
prevIndex = tabBar.model.length - 1;
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex--
prevIndex--;
}
}
if (prevIndex >= 0) {
root.currentTabIndex = prevIndex
root.currentTabIndex = prevIndex;
}
event.accepted = true
return
event.accepted = true;
return;
}
if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) {
if (wallpaperTab.handleKeyEvent(event)) {
event.accepted = true
return
event.accepted = true;
return;
}
}
}
@@ -171,50 +255,54 @@ DankPopout {
focus: false
activeFocusOnTab: false
nextFocusTarget: {
const item = pages.currentItem
const item = pages.currentItem;
if (!item)
return null
return null;
if (item.focusTarget)
return item.focusTarget
return item
return item.focusTarget;
return item;
}
model: {
let tabs = [{
"icon": "dashboard",
"text": I18n.tr("Overview")
}, {
"icon": "music_note",
"text": I18n.tr("Media")
}, {
"icon": "wallpaper",
"text": I18n.tr("Wallpapers")
}]
let tabs = [
{
"icon": "dashboard",
"text": I18n.tr("Overview")
},
{
"icon": "music_note",
"text": I18n.tr("Media")
},
{
"icon": "wallpaper",
"text": I18n.tr("Wallpapers")
}
];
if (SettingsData.weatherEnabled) {
tabs.push({
"icon": "wb_sunny",
"text": I18n.tr("Weather")
})
"icon": "wb_sunny",
"text": I18n.tr("Weather")
});
}
tabs.push({
"icon": "settings",
"text": I18n.tr("Settings"),
"isAction": true
})
return tabs
"icon": "settings",
"text": I18n.tr("Settings"),
"isAction": true
});
return tabs;
}
onTabClicked: function (index) {
root.currentTabIndex = index
root.currentTabIndex = index;
}
onActionTriggered: function (index) {
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3;
if (index === settingsIndex) {
dashVisible = false
settingsModal.show()
dashVisible = false;
settingsModal.show();
}
}
}
@@ -229,14 +317,14 @@ DankPopout {
width: parent.width
implicitHeight: {
if (currentIndex === 0)
return overviewTab.implicitHeight
return overviewTab.implicitHeight;
if (currentIndex === 1)
return mediaTab.implicitHeight
return mediaTab.implicitHeight;
if (currentIndex === 2)
return wallpaperTab.implicitHeight
return wallpaperTab.implicitHeight;
if (SettingsData.weatherEnabled && currentIndex === 3)
return weatherTab.implicitHeight
return overviewTab.implicitHeight
return weatherTab.implicitHeight;
return overviewTab.implicitHeight;
}
currentIndex: root.currentTabIndex
@@ -244,24 +332,42 @@ DankPopout {
id: overviewTab
onCloseDash: {
root.dashVisible = false
root.dashVisible = false;
}
onSwitchToWeatherTab: {
if (SettingsData.weatherEnabled) {
tabBar.currentIndex = 3
tabBar.tabClicked(3)
tabBar.currentIndex = 3;
tabBar.tabClicked(3);
}
}
onSwitchToMediaTab: {
tabBar.currentIndex = 1
tabBar.tabClicked(1)
tabBar.currentIndex = 1;
tabBar.tabClicked(1);
}
}
MediaPlayerTab {
id: mediaTab
targetScreen: root.screen
popoutX: root.alignedX
popoutY: root.alignedY
popoutWidth: root.alignedWidth
popoutHeight: root.alignedHeight
contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS
Component.onCompleted: root.__mediaTabRef = this
onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => {
root.__showVolumeDropdown(pos, rightEdge, player, players);
}
onShowAudioDevicesDropdown: (pos, screen, rightEdge) => {
root.__showAudioDevicesDropdown(pos, rightEdge);
}
onShowPlayersDropdown: (pos, screen, rightEdge, player, players) => {
root.__showPlayersDropdown(pos, rightEdge, player, players);
}
onHideDropdowns: root.__hideDropdowns()
onVolumeButtonExited: root.__startCloseTimer()
}
WallpaperTab {

View File

@@ -0,0 +1,491 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property int dropdownType: 0
property var activePlayer: null
property var allPlayers: []
property point anchorPos: Qt.point(0, 0)
property bool isRightEdge: false
property bool __isChromeBrowser: {
if (!activePlayer?.identity)
return false;
const id = activePlayer.identity.toLowerCase();
return id.includes("chrome") || id.includes("chromium");
}
property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)
property var availableDevices: Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream)
signal closeRequested
signal deviceSelected(var device)
signal playerSelected(var player)
signal volumeChanged(real volume)
signal panelEntered
signal panelExited
property int __volumeHoverCount: 0
function volumeAreaEntered() {
__volumeHoverCount++;
panelEntered();
}
function volumeAreaExited() {
__volumeHoverCount--;
Qt.callLater(() => {
if (__volumeHoverCount <= 0)
panelExited();
});
}
Rectangle {
id: volumePanel
visible: dropdownType === 1 && volumeAvailable
width: 60
height: 180
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1
opacity: dropdownType === 1 ? 1 : 0
scale: dropdownType === 1 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.4)
shadowOpacity: 0.7
}
MouseArea {
anchors.fill: parent
anchors.margins: -12
hoverEnabled: true
onEntered: volumeAreaEntered()
onExited: volumeAreaExited()
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
Item {
id: volumeSlider
width: parent.width * 0.5
height: parent.height - Theme.spacingXL * 2
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
width: parent.width
height: parent.height
anchors.centerIn: parent
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
radius: Theme.cornerRadius
}
Rectangle {
width: parent.width
height: volumeAvailable ? (Math.min(1.0, currentVolume) * parent.height) : 0
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.primary
bottomLeftRadius: Theme.cornerRadius
bottomRightRadius: Theme.cornerRadius
}
Rectangle {
width: parent.width + 8
height: 8
radius: Theme.cornerRadius
y: {
const ratio = volumeAvailable ? Math.min(1.0, currentVolume) : 0;
const travel = parent.height - height;
return Math.max(0, Math.min(travel, travel * (1 - ratio)));
}
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.primary
border.width: 3
border.color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1.0)
}
MouseArea {
anchors.fill: parent
anchors.margins: -12
enabled: volumeAvailable
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: true
onEntered: volumeAreaEntered()
onExited: volumeAreaExited()
onPressed: mouse => updateVolume(mouse)
onPositionChanged: mouse => {
if (pressed)
updateVolume(mouse);
}
onClicked: mouse => updateVolume(mouse)
function updateVolume(mouse) {
if (!volumeAvailable)
return;
const ratio = 1.0 - (mouse.y / height);
const volume = Math.max(0, Math.min(1, ratio));
root.volumeChanged(volume);
}
}
}
StyledText {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: Theme.spacingL
text: volumeAvailable ? Math.round(currentVolume * 100) + "%" : "0%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
}
}
Rectangle {
id: audioDevicesPanel
visible: dropdownType === 2
width: 280
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2
opacity: dropdownType === 2 ? 1 : 0
scale: dropdownType === 2 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.4)
shadowOpacity: 0.7
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
StyledText {
text: I18n.tr("Audio Output Devices (") + availableDevices.length + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
bottomPadding: Theme.spacingM
}
DankFlickable {
width: parent.width
height: parent.height - 40
contentHeight: deviceColumn.height
clip: true
Column {
id: deviceColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: availableDevices
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 48
radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: modelData === AudioService.sink ? 2 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
width: parent.width - Theme.spacingM * 2
DankIcon {
name: getAudioDeviceIcon(modelData)
size: 20
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
function getAudioDeviceIcon(device) {
if (!device?.name)
return "speaker";
const name = device.name.toLowerCase();
if (name.includes("bluez") || name.includes("bluetooth"))
return "headset";
if (name.includes("hdmi"))
return "tv";
if (name.includes("usb"))
return "headset";
return "speaker";
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - Theme.spacingM * 2
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
StyledText {
text: modelData === AudioService.sink ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSink = modelData;
root.deviceSelected(modelData);
}
}
}
}
}
}
}
}
}
Rectangle {
id: playersPanel
visible: dropdownType === 3
width: 240
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2
opacity: dropdownType === 3 ? 1 : 0
scale: dropdownType === 3 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.4)
shadowOpacity: 0.7
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
StyledText {
text: I18n.tr("Media Players (") + (allPlayers?.length || 0) + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
bottomPadding: Theme.spacingM
}
DankFlickable {
width: parent.width
height: parent.height - 40
contentHeight: playerColumn.height
clip: true
Column {
id: playerColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: allPlayers || []
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 48
radius: Theme.cornerRadius
color: playerMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: modelData === activePlayer ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: modelData === activePlayer ? 2 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
width: parent.width - Theme.spacingM * 2
DankIcon {
name: "music_note"
size: 20
color: modelData === activePlayer ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - Theme.spacingM * 2
StyledText {
text: {
if (!modelData)
return "Unknown Player";
const identity = modelData.identity || "Unknown Player";
const trackTitle = modelData.trackTitle || "";
return trackTitle.length > 0 ? identity + " - " + trackTitle : identity;
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === activePlayer ? Font.Medium : Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
StyledText {
text: {
if (!modelData)
return "";
const artist = modelData.trackArtist || "";
const isActive = modelData === activePlayer;
if (artist.length > 0)
return artist + (isActive ? " (Active)" : "");
return isActive ? "Active" : "Available";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
}
}
MouseArea {
id: playerMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData?.identity) {
root.playerSelected(modelData);
}
}
}
}
}
}
}
}
}
MouseArea {
anchors.fill: parent
z: -1
enabled: dropdownType !== 0
onClicked: closeRequested()
}
}

View File

@@ -1,9 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell.Services.Mpris
import Quickshell.Services.Pipewire
import Quickshell.Io
import Quickshell
import qs.Common
@@ -15,14 +13,42 @@ Item {
property MprisPlayer activePlayer: MprisController.activePlayer
property var allPlayers: MprisController.availablePlayers
property var targetScreen: null
property real popoutX: 0
property real popoutY: 0
property real popoutWidth: 0
property real popoutHeight: 0
property real contentOffsetY: 0
signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
signal hideDropdowns
signal volumeButtonExited
property bool volumeExpanded: false
property bool devicesExpanded: false
property bool playersExpanded: false
function resetDropdownStates() {
volumeExpanded = false;
devicesExpanded = false;
playersExpanded = false;
}
DankTooltipV2 {
id: sharedTooltip
}
readonly property bool isRightEdge: (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Right
readonly property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported) || (AudioService.sink && AudioService.sink.audio)
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported
readonly property bool __isChromeBrowser: {
if (!activePlayer?.identity)
return false;
const id = activePlayer.identity.toLowerCase();
return id.includes("chrome") || id.includes("chromium");
}
readonly property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
// Palette that stays stable across track switches until new colors are ready
@@ -334,383 +360,6 @@ Item {
anchors.fill: parent
clip: false
visible: !_noneAvailable && (!showNoPlayerNow)
MouseArea {
anchors.fill: parent
enabled: audioDevicesButton.devicesExpanded || volumeButton.volumeExpanded || playerSelectorButton.playersExpanded
onClicked: function (mouse) {
const clickOutside = item => {
return mouse.x < item.x || mouse.x > item.x + item.width || mouse.y < item.y || mouse.y > item.y + item.height;
};
if (playerSelectorButton.playersExpanded && clickOutside(playerSelectorDropdown)) {
playerSelectorButton.playersExpanded = false;
}
if (audioDevicesButton.devicesExpanded && clickOutside(audioDevicesDropdown)) {
audioDevicesButton.devicesExpanded = false;
}
if (volumeButton.volumeExpanded && clickOutside(volumeSliderPanel) && clickOutside(volumeButton)) {
volumeButton.volumeExpanded = false;
}
}
}
Popup {
id: audioDevicesDropdown
width: 280
height: audioDevicesButton.devicesExpanded ? Math.max(200, Math.min(280, audioDevicesDropdown.availableDevices.length * 50 + 100)) : 0
x: isRightEdge ? audioDevicesButton.x + audioDevicesButton.width + Theme.spacingS : audioDevicesButton.x - width - Theme.spacingS
y: audioDevicesButton.y - 50
visible: audioDevicesButton.devicesExpanded
closePolicy: Popup.NoAutoClose
modal: false
dim: false
padding: 0
property var availableDevices: Pipewire.nodes.values.filter(node => {
return node.audio && node.isSink && !node.isStream;
})
background: Rectangle {
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2
radius: Theme.cornerRadius * 2
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.4)
shadowOpacity: 0.7
}
}
Behavior on height {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
StyledText {
text: I18n.tr("Audio Output Devices (") + audioDevicesDropdown.availableDevices.length + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
bottomPadding: Theme.spacingM
}
DankFlickable {
width: parent.width
height: parent.height - 40
contentHeight: deviceColumn.height
clip: true
Column {
id: deviceColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: audioDevicesDropdown.availableDevices
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 48
radius: Theme.cornerRadius
color: deviceMouseAreaLeft.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: modelData === AudioService.sink ? 2 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
width: parent.width - Theme.spacingM * 2
DankIcon {
name: getAudioDeviceIcon(modelData)
size: 20
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - Theme.spacingM * 2
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
StyledText {
text: modelData === AudioService.sink ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: deviceMouseAreaLeft
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSink = modelData;
}
audioDevicesButton.devicesExpanded = false;
}
}
Behavior on border.color {
ColorAnimation {
duration: Anims.durShort
}
}
}
}
}
}
}
}
Popup {
id: playerSelectorDropdown
width: 240
height: playerSelectorButton.playersExpanded ? Math.max(180, Math.min(240, (root.allPlayers?.length || 0) * 50 + 80)) : 0
x: isRightEdge ? playerSelectorButton.x + playerSelectorButton.width + Theme.spacingS : playerSelectorButton.x - width - Theme.spacingS
y: playerSelectorButton.y - 50
visible: playerSelectorButton.playersExpanded
closePolicy: Popup.NoAutoClose
modal: false
dim: false
padding: 0
background: Rectangle {
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2
radius: Theme.cornerRadius * 2
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.4)
shadowOpacity: 0.7
}
}
Behavior on height {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
StyledText {
text: I18n.tr("Media Players (") + (allPlayers?.length || 0) + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
bottomPadding: Theme.spacingM
}
DankFlickable {
width: parent.width
height: parent.height - 40
contentHeight: playerColumn.height
clip: true
Column {
id: playerColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: allPlayers || []
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 48
radius: Theme.cornerRadius
color: playerMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: modelData === activePlayer ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: modelData === activePlayer ? 2 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
width: parent.width - Theme.spacingM * 2
DankIcon {
name: "music_note"
size: 20
color: modelData === activePlayer ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - Theme.spacingM * 2
StyledText {
text: {
if (!modelData)
return "Unknown Player";
const identity = modelData.identity || "Unknown Player";
const trackTitle = modelData.trackTitle || "";
if (trackTitle.length > 0) {
return identity + " - " + trackTitle;
}
return identity;
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === activePlayer ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
StyledText {
text: {
if (!modelData)
return "";
const artist = modelData.trackArtist || "";
const isActive = modelData === activePlayer;
if (artist.length > 0) {
return artist + (isActive ? " (Active)" : "");
}
return isActive ? "Active" : "Available";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: playerMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && modelData.identity) {
if (activePlayer && activePlayer !== modelData && activePlayer.canPause) {
activePlayer.pause();
}
MprisController.activePlayer = modelData;
}
playerSelectorButton.playersExpanded = false;
}
}
Behavior on border.color {
ColorAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
}
}
}
}
}
}
// Center Column: Main Media Content
ColumnLayout {
x: 72
y: 20
@@ -1051,14 +700,12 @@ Item {
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 185
color: playerSelectorArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
color: playerSelectorArea.containsMouse || playersExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1
z: 100
visible: (allPlayers?.length || 0) >= 1
property bool playersExpanded: false
DankIcon {
anchors.centerIn: parent
name: "assistant_device"
@@ -1072,14 +719,20 @@ Item {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
parent.playersExpanded = !parent.playersExpanded;
}
onEntered: {
sharedTooltip.show("Media Players", playerSelectorButton, 0, 0, isRightEdge ? "right" : "left");
}
onExited: {
sharedTooltip.hide();
if (playersExpanded) {
hideDropdowns();
return;
}
hideDropdowns();
playersExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = playerSelectorButton.y + playerSelectorButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
}
onEntered: sharedTooltip.show("Media Players", playerSelectorButton, 0, 0, isRightEdge ? "right" : "left")
onExited: sharedTooltip.hide()
}
}
@@ -1090,21 +743,14 @@ Item {
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 130
color: volumeButtonArea.containsMouse && volumeAvailable ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, volumeAvailable ? 0.3 : 0.15)
border.width: 1
z: 101
enabled: volumeAvailable
property bool volumeExpanded: false
property real previousVolume: 0.0
Timer {
id: volumeHideTimer
interval: 500
onTriggered: volumeButton.volumeExpanded = false
}
DankIcon {
anchors.centerIn: parent
name: getVolumeIcon()
@@ -1118,11 +764,19 @@ Item {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
volumeButton.volumeExpanded = true;
volumeHideTimer.stop();
if (volumeExpanded)
return;
hideDropdowns();
volumeExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = volumeButton.y + volumeButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showVolumeDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
}
onExited: {
volumeHideTimer.restart();
if (volumeExpanded)
volumeButtonExited();
}
onClicked: {
if (currentVolume > 0) {
@@ -1142,22 +796,15 @@ Item {
}
}
onWheel: wheelEvent => {
let delta = wheelEvent.angleDelta.y;
let current = (currentVolume * 100) || 0;
let newVolume;
if (delta > 0) {
newVolume = Math.min(100, current + 5);
} else {
newVolume = Math.max(0, current - 5);
}
const delta = wheelEvent.angleDelta.y;
const current = (currentVolume * 100) || 0;
const newVolume = delta > 0 ? Math.min(100, current + 5) : Math.max(0, current - 5);
if (usePlayerVolume) {
activePlayer.volume = newVolume / 100;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = newVolume / 100;
}
volumeButton.volumeExpanded = true;
wheelEvent.accepted = true;
}
}
@@ -1170,16 +817,14 @@ Item {
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 240
color: audioDevicesArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
color: audioDevicesArea.containsMouse || devicesExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1
z: 100
property bool devicesExpanded: false
DankIcon {
anchors.centerIn: parent
name: parent.devicesExpanded ? "expand_less" : "speaker"
name: devicesExpanded ? "expand_less" : "speaker"
size: 18
color: Theme.surfaceText
}
@@ -1190,247 +835,20 @@ Item {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
parent.devicesExpanded = !parent.devicesExpanded;
if (devicesExpanded) {
hideDropdowns();
return;
}
hideDropdowns();
devicesExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = audioDevicesButton.y + audioDevicesButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight);
}
onEntered: {
sharedTooltip.show("Output Device", audioDevicesButton, 0, 0, isRightEdge ? "right" : "left");
}
onExited: {
sharedTooltip.hide();
}
}
}
}
Popup {
id: volumeSliderPanel
width: 60
height: 180
x: isRightEdge ? volumeButton.x + volumeButton.width + Theme.spacingS : volumeButton.x - width - Theme.spacingS
y: volumeButton.y - 50
visible: volumeButton.volumeExpanded && volumeAvailable
closePolicy: Popup.NoAutoClose
modal: false
dim: false
padding: 0
background: Rectangle {
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.4)
shadowOpacity: 0.7
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
Item {
id: volumeSlider
width: parent.width * 0.5
height: parent.height - Theme.spacingXL * 2
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
property bool dragging: false
property bool containsMouse: volumeSliderArea.containsMouse
property bool active: volumeSliderArea.containsMouse || volumeSliderArea.pressed || dragging
Rectangle {
id: sliderTrack
width: parent.width
height: parent.height
anchors.centerIn: parent
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
radius: Theme.cornerRadius
}
Rectangle {
id: sliderFill
width: parent.width
height: volumeAvailable ? (Math.min(1.0, currentVolume) * parent.height) : 0
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.primary
bottomLeftRadius: Theme.cornerRadius
bottomRightRadius: Theme.cornerRadius
topLeftRadius: 0
topRightRadius: 0
}
Rectangle {
id: sliderHandle
width: parent.width + 8
height: 8
radius: Theme.cornerRadius
y: {
const ratio = volumeAvailable ? Math.min(1.0, currentVolume) : 0;
const travel = parent.height - height;
return Math.max(0, Math.min(travel, travel * (1 - ratio)));
}
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.primary
border.width: 3
border.color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1.0)
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.onPrimary
opacity: volumeSliderArea.pressed ? 0.16 : (volumeSliderArea.containsMouse ? 0.08 : 0)
visible: opacity > 0
}
Rectangle {
id: ripple
anchors.centerIn: parent
width: 0
height: 0
radius: width / 2
color: Theme.onPrimary
opacity: 0
function start() {
opacity = 0.16;
width = 0;
height = 0;
rippleAnimation.start();
}
SequentialAnimation {
id: rippleAnimation
NumberAnimation {
target: ripple
properties: "width,height"
to: 28
duration: 180
}
NumberAnimation {
target: ripple
property: "opacity"
to: 0
duration: 150
}
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onPressedChanged: {
if (pressed) {
ripple.start();
}
}
}
scale: volumeSlider.active ? 1.05 : 1.0
Behavior on scale {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
}
MouseArea {
id: volumeSliderArea
anchors.fill: parent
anchors.margins: -12
enabled: volumeAvailable
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: true
onEntered: {
volumeHideTimer.stop();
}
onExited: {
volumeHideTimer.restart();
}
onPressed: function (mouse) {
parent.dragging = true;
updateVolume(mouse);
}
onReleased: {
parent.dragging = false;
}
onPositionChanged: function (mouse) {
if (pressed) {
updateVolume(mouse);
}
}
onClicked: function (mouse) {
updateVolume(mouse);
}
onWheel: wheelEvent => {
const step = 1;
adjustVolume(wheelEvent.angleDelta.y > 0 ? step : -step);
wheelEvent.accepted = true;
}
function updateVolume(mouse) {
if (volumeAvailable) {
const ratio = 1.0 - (mouse.y / height);
const volume = Math.max(0, Math.min(1, ratio));
if (usePlayerVolume) {
activePlayer.volume = volume;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = volume;
}
}
}
}
}
StyledText {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: Theme.spacingL
text: volumeAvailable ? Math.round(currentVolume * 100) + "%" : "0%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
onEntered: sharedTooltip.show("Output Device", audioDevicesButton, 0, 0, isRightEdge ? "right" : "left")
onExited: sharedTooltip.hide()
}
}
}