/* SPDX-FileCopyrightText: 2013 Sebastian Kügler SPDX-FileCopyrightText: 2014 Martin Gräßlin SPDX-FileCopyrightText: 2016 Kai Uwe Broulik SPDX-FileCopyrightText: 2017 Roman Gilg SPDX-FileCopyrightText: 2020 Nate Graham SPDX-License-Identifier: LGPL-2.0-or-later */ import QtQuick 2.6 import QtQuick.Layouts 1.1 import QtGraphicalEffects 1.0 import QtQml.Models 2.2 import org.kde.plasma.core 2.0 as PlasmaCore // for Highlight import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents3 import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kquickcontrolsaddons 2.0 as KQuickControlsAddons ColumnLayout { id: tooltipInstance property var submodelIndex property int flatIndex: isGroup && index != undefined ? index : 0 readonly property string mprisSourceName: mpris2Source.sourceNameForLauncherUrl(toolTipDelegate.launcherUrl, isGroup ? AppPid : pidParent) readonly property var playerData: mprisSourceName != "" ? mpris2Source.data[mprisSourceName] : 0 readonly property bool hasPlayer: !!mprisSourceName && !!playerData readonly property bool playing: hasPlayer && playerData.PlaybackStatus === "Playing" readonly property bool canControl: hasPlayer && playerData.CanControl readonly property bool canPlay: hasPlayer && playerData.CanPlay readonly property bool canPause: hasPlayer && playerData.CanPause readonly property bool canGoBack: hasPlayer && playerData.CanGoPrevious readonly property bool canGoNext: hasPlayer && playerData.CanGoNext readonly property bool canRaise: hasPlayer && playerData.CanRaise readonly property var currentMetadata: hasPlayer ? playerData.Metadata : ({}) // Used to highlight the tooltip that points to the window that's currently in focus. property bool activeWindow: plasmoid.nativeInterface.isActiveWindow(thumbnailSourceItem.winId); readonly property string track: { var xesamTitle = currentMetadata["xesam:title"] if (xesamTitle) { return xesamTitle; } // if no track title is given, print out the file name var xesamUrl = currentMetadata["xesam:url"] ? currentMetadata["xesam:url"].toString() : "" if (!xesamUrl) { return ""; } var lastSlashPos = xesamUrl.lastIndexOf('/') if (lastSlashPos < 0) { return ""; } var lastUrlPart = xesamUrl.substring(lastSlashPos + 1) return decodeURIComponent(lastUrlPart); } readonly property string artist: currentMetadata["xesam:artist"] || "" readonly property string albumArt: currentMetadata["mpris:artUrl"] || "" spacing: isWin ? PlasmaCore.Units.smallSpacing * 2 : 0 // text labels + close button RowLayout { id: header // match spacing of DefaultToolTip.qml in plasma-framework //spacing: isWin ? PlasmaCore.Units.smallSpacing : 0 //isWin ? PlasmaCore.Units.smallSpacing : PlasmaCore.Units.largeSpacing spacing: 0 // This number controls the overall size of the window tooltips Layout.maximumWidth: PlasmaCore.Units.gridUnit * 12 Layout.minimumWidth: isWin ? Layout.maximumWidth : 0 Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter // match margins of DefaultToolTip.qml in plasma-framework Layout.margins: isWin ? 0 : PlasmaCore.Units.smallSpacing / 2 // There's no PlasmaComponents3 version // This component tracks the mouse and highlights the tooltip when it's hovered over or clicked on. ToolTipWindowMouseArea { id: hoverHandler Layout.alignment: Qt.AlignTop height: { if(playerControls.visible) return tooltipInstance.height - parent.height; else return tooltipInstance.height; } width: header.width rootTask: parentTask modelIndex: submodelIndex winId: thumbnailSourceItem.winId z: 0 enabled: visible closeBtn: closeButtonMA PlasmaCore.FrameSvgItem { imagePath: Qt.resolvedUrl("svgs/taskbarhover.svg") z: -1 anchors { top: parent.top; left: parent.left; right: parent.right; bottom: parent.bottom; bottomMargin: PlasmaCore.Units.smallSpacing + ((playerControls.visible) ? -header.height : 0); leftMargin: PlasmaCore.Units.smallSpacing rightMargin: PlasmaCore.Units.smallSpacing topMargin: PlasmaCore.Units.smallSpacing } // The currently active window has a blue tinted version of the same texture. prefix: { if(activeWindow) return "active"; return "normal"; } Behavior on opacity { NumberAnimation { duration: 150 } } opacity: isWin ? (hoverHandler.containsPress ? 1.0 : ( (hoverHandler.opacityHover || closeButton.hovered) ? ((activeWindow) ? 1.0 : 0.7) : (activeWindow ? 0.7 : 0) )) : 0; } } // The icon in the corner of the tooltip. PlasmaCore.IconItem { id: iconSmall implicitWidth: PlasmaCore.Units.iconSizes.small; implicitHeight: PlasmaCore.Units.iconSizes.small; Layout.topMargin: PlasmaCore.Units.smallSpacing; Layout.leftMargin: PlasmaCore.Units.smallSpacing*3; Layout.alignment: Qt.AlignVCenter source: icon animated: false usesPlasmaTheme: false roundToIconSize: false visible: valid && isWin } ColumnLayout { spacing: 0 // Program name. PlasmaExtras.Heading { id: appNameHeading level: 5 maximumLineCount: 1 lineHeight: isWin ? iconSmall.implicitHeight * 2 : 1.0 lineHeightMode: isWin ? Text.FixedHeight : Text.ProportionalHeight Layout.fillWidth: true Layout.topMargin: isWin ? 1 : 0; verticalAlignment: Text.AlignVCenter Layout.leftMargin: isWin ? PlasmaCore.Units.smallSpacing : 0; Layout.rightMargin: (!isWin ? 0 : ((!closeButton.isvisible) ? (PlasmaCore.Units.smallSpacing*2) : PlasmaCore.Units.smallSpacing)); elide: Text.ElideRight text: isWin ? generateTitle() : appName; opacity: 1 textFormat: Text.PlainText } // Window title. PlasmaComponents.Label { id: winTitle maximumLineCount: 1 lineHeight: 1.0 Layout.fillWidth: true Layout.leftMargin: isWin ? PlasmaCore.Units.smallSpacing : 0; Layout.rightMargin: (!isWin ? 0 : ((!closeButton.isvisible) ? (PlasmaCore.Units.smallSpacing*3) : 0)); elide: Text.ElideRight text: ""; opacity: 0.75 visible: false textFormat: Text.PlainText } // Subtext. PlasmaComponents.Label { readonly property string title: generateTitle() id: subtext maximumLineCount: 1 Layout.fillWidth: true Layout.leftMargin: isWin ? PlasmaCore.Units.smallSpacing : 0; Layout.rightMargin: (!isWin ? 0 : ((!closeButton.isvisible) ? (PlasmaCore.Units.smallSpacing*3) : 0)); elide: Text.ElideRight text: title; //text: isWin ? generateSubText() : "" opacity: 0.7 visible: !isWin && (title.length !== 0) textFormat: Text.PlainText } } // Count badge, no longer used. // The badge itself is inside an item to better center the text in the bubble /*Item { Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredHeight: closeButton.height Layout.preferredWidth: closeButton.width visible: flatIndex === 0 && smartLauncherCountVisible Badge { anchors.centerIn: parent height: PlasmaCore.Units.iconSizes.smallMedium number: smartLauncherCount } }*/ // Close button, which is no longer an icon but rather a SVG texture. PlasmaCore.FrameSvgItem { id: closeButton property bool isvisible: closeButton.visible Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredWidth: PlasmaCore.Units.smallSpacing*3+2; Layout.preferredHeight: PlasmaCore.Units.smallSpacing*3+2; Layout.rightMargin: PlasmaCore.Units.smallSpacing*3-1; Layout.topMargin: PlasmaCore.Units.smallSpacing*3-1; visible: isWin && hoverHandler.opacityHover imagePath: Qt.resolvedUrl("svgs/button-close.svg") prefix: closeButtonMA.containsPress ? "pressed" : (closeButtonMA.containsMouse ? "hover" : "normal"); MouseArea { id: closeButtonMA anchors.fill: parent onClicked: { backend.cancelHighlightWindows(); tasksModel.requestClose(submodelIndex); } onEntered: { hoverHandler.opacityHover = 1; tasks.windowsHovered([thumbnailSourceItem.winId], 1); } hoverEnabled: true z: 7 } z: 6 } } // Thumbnail container. Item { id: thumbnailSourceItem Layout.minimumWidth: header.width - PlasmaCore.Units.smallSpacing*4; Layout.preferredHeight: header.width / 2 Layout.leftMargin: PlasmaCore.Units.smallSpacing*2; Layout.bottomMargin: albumArtImage.visible || hasPlayer ? 0 : PlasmaCore.Units.smallSpacing*2; visible: toolTipDelegate.isWin readonly property bool isMinimized: isGroup ? IsMinimized == true : isMinimizedParent // TODO: this causes XCB error message when being visible the first time readonly property var winId: toolTipDelegate.isWin && toolTipDelegate.windows[flatIndex] !== undefined ? toolTipDelegate.windows[flatIndex] : 0 PlasmaCore.WindowThumbnail { id: x11Thumbnail anchors { fill: parent bottomMargin: PlasmaCore.Units.smallSpacing } visible: !albumArtImage.visible && !thumbnailSourceItem.isMinimized && Number.isInteger(thumbnailSourceItem.winId) winId: Number.isInteger(thumbnailSourceItem.winId) ? thumbnailSourceItem.winId : 0 } Loader { id: pipeWireLoader Layout.alignment: Qt.AlignTop //anchors.fill: hoverHandler // Indent a little bit so that neither the thumbnail nor the drop // shadow can cover up the highlight anchors.margins: PlasmaCore.Units.smallSpacing * 2 active: !albumArtImage.visible && !Number.isInteger(thumbnailSourceItem.winId) //In a loader since we might not have PipeWire available yet source: "PipeWireThumbnail.qml" } DropShadow { anchors.fill: pipeWireLoader.active ? pipeWireLoader : x11Thumbnail visible: pipeWireLoader.active ? pipeWireLoader.item.visible : x11Thumbnail.visible horizontalOffset: 0 verticalOffset: Math.round(3 * PlasmaCore.Units.devicePixelRatio) radius: Math.round(2 * PlasmaCore.Units.devicePixelRatio) samples: Math.round(radius * 1.5) color: "Black" source: pipeWireLoader.active ? pipeWireLoader.item : x11Thumbnail } Image { id: albumArtBackground source: albumArt sourceSize: Qt.size(x11Thumbnail.width, x11Thumbnail.height) anchors { top: x11Thumbnail.top bottom: x11Thumbnail.bottom left: parent.left right: parent.right } fillMode: Image.PreserveAspectCrop visible: albumArtImage.available asynchronous: true layer.enabled: true opacity: 0.5 layer.effect: FastBlur { source: albumArtBackground anchors.fill: parent radius: 30 } } // Borders around the blurred album art for added visuals. PlasmaCore.FrameSvgItem { id: albumArtBorder visible: albumArtImage.available anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right anchors.margins: -PlasmaCore.Units.smallSpacing/4 imagePath: "widgets/pager" prefix: "hover" opacity: 0.4 } Image { id: albumArtImage // also Image.Loading to prevent loading thumbnails just because the album art takes a split second to load // don't show album art if window title doesn't include media title (eg we're in a different browser tab) readonly property bool available: (status === Image.Ready || status === Image.Loading) && generateTitle().includes(track) anchors.fill: x11Thumbnail // Indent by one pixel to make sure we never cover up the entire highlight anchors.margins: 1 anchors.topMargin: PlasmaCore.Units.smallSpacing*2; sourceSize: Qt.size(parent.width, parent.height) asynchronous: true source: albumArt fillMode: Image.PreserveAspectFit visible: available } // when minimized, we don't have a preview on X11, so show the icon PlasmaCore.IconItem { width: parent.width height: thumbnailSourceItem.height anchors.horizontalCenter: parent.horizontalCenter source: thumbnailSourceItem.isMinimized && !albumArtImage.visible && Number.isInteger(thumbnailSourceItem.winId) ? icon : "" animated: false usesPlasmaTheme: false visible: valid && !pipeWireLoader.active } } // Player controls row RowLayout { id: playerControls Layout.maximumWidth: header.Layout.maximumWidth // Match margins of header Layout.leftMargin: isWin ? 0 : PlasmaCore.Units.gridUnit / 2 Layout.rightMargin: isWin ? 0 : PlasmaCore.Units.gridUnit / 2 //Layout.bottomMargin: isWin ? 0 : PlasmaCore.Units.gridUnit visible: hasPlayer enabled: canControl ColumnLayout { Layout.fillWidth: true Layout.topMargin: PlasmaCore.Units.smallSpacing Layout.bottomMargin: PlasmaCore.Units.smallSpacing*2 Layout.rightMargin: isWin ? PlasmaCore.Units.smallSpacing : PlasmaCore.Units.largeSpacing Layout.columnSpan: 2 Layout.column: 0 spacing: 0 z: -1 ScrollableTextWrapper { id: songTextWrapper Layout.fillWidth: true Layout.preferredHeight: songText.height implicitWidth: songText.implicitWidth Layout.leftMargin: PlasmaCore.Units.smallSpacing*2; PlasmaComponents3.Label { id: songText parent: songTextWrapper width: parent.width height: undefined lineHeight: 1 maximumLineCount: artistText.visible? 1 : 2 wrapMode: Text.NoWrap elide: parent.state ? Text.ElideNone : Text.ElideRight text: track || "" textFormat: Text.PlainText } } ScrollableTextWrapper { id: artistTextWrapper z: -1 Layout.fillWidth: true Layout.preferredHeight: artistText.height Layout.leftMargin: PlasmaCore.Units.smallSpacing*2; implicitWidth: artistText.implicitWidth visible: artistText.text !== "" PlasmaComponents3.Label { id: artistText parent: artistTextWrapper width: parent.width height: undefined wrapMode: Text.NoWrap lineHeight: 1 elide: parent.state ? Text.ElideNone : Text.ElideRight text: artist || "" font: PlasmaCore.Theme.smallestFont textFormat: Text.PlainText //color: "white"; } } } RowLayout { Layout.fillWidth: true //Layout.topMargin: PlasmaCore.Units.smallSpacing Layout.bottomMargin: PlasmaCore.Units.smallSpacing Layout.rightMargin: isWin ? PlasmaCore.Units.smallSpacing*2 : PlasmaCore.Units.largeSpacing spacing: -1//PlasmaCore.Units.smallSpacing / 4 MediaButton { id: canGoBackMedia orientation: "left" mediaIcon: "previous" enableButton: canGoBack onClicked: { mpris2Source.goPrevious(mprisSourceName) } } MediaButton { id: playPauseMedia orientation: "center" mediaIcon: "pause" fallbackMediaIcon: "play" togglePlayPause: playing enableButton: playing ? canPause : canPlay onClicked: { if (!playing) { mpris2Source.play(mprisSourceName); } else { mpris2Source.pause(mprisSourceName); } } } MediaButton { id: canGoForwardMedia enableButton: canGoNext orientation: "right" mediaIcon: "skip" onClicked: mpris2Source.goNext(mprisSourceName) } } } function generateTitle() { if (!isWin) { return genericName != undefined ? genericName : ""; } var text; if (isGroup) { if (model.display === undefined) { return ""; } text = model.display.toString(); } else { text = displayParent; } // KWin appends increasing integers in between pointy brackets to otherwise equal window titles. // In this case save <#number> as counter and delete it at the end of text. var counter = text.match(/<\d+>\W*$/); text = text.replace(/\s*<\d+>\W*$/, ""); // Remove appName from the end of text. var appNameRegex = new RegExp(appName + "$", "i"); text = text.replace(appNameRegex, ""); text = text.replace(/\s*(?:-|—)*\s*$/, ""); // Add counter back at the end. if (counter !== null) { if (text === "") { text = counter; } else { text = text + " " + counter; } } // In case the window title had only redundant information (i.e. appName), text is now empty. // Add a hyphen to indicate that and avoid empty space. if (text === "") { text = appName; } return text.toString(); } function generateSubText() { if (activitiesParent === undefined) { return ""; } var subTextEntries = []; var onAllDesktops = (isGroup ? IsOnAllVirtualDesktops : isOnAllVirtualDesktopsParent) === true; if (!plasmoid.configuration.showOnlyCurrentDesktop && virtualDesktopInfo.numberOfDesktops > 1) { var virtualDesktops = isGroup ? VirtualDesktops : virtualDesktopParent; if (!onAllDesktops && virtualDesktops !== undefined && virtualDesktops.length > 0) { var virtualDesktopNameList = new Array(); for (var i = 0; i < virtualDesktops.length; ++i) { virtualDesktopNameList.push(virtualDesktopInfo.desktopNames[virtualDesktopInfo.desktopIds.indexOf(virtualDesktops[i])]); } subTextEntries.push(i18nc("Comma-separated list of desktops", "On %1", virtualDesktopNameList.join(", "))); } else if (onAllDesktops) { subTextEntries.push(i18nc("Comma-separated list of desktops", "Pinned to all desktops")); } } var act = isGroup ? Activities : activitiesParent; if (act === undefined) { return subTextEntries.join("\n"); } if (act.length === 0 && activityInfo.numberOfRunningActivities > 1) { subTextEntries.push(i18nc("Which virtual desktop a window is currently on", "Available on all activities")); } else if (act.length > 0) { var activityNames = []; for (var i = 0; i < act.length; i++) { var activity = act[i]; var activityName = activityInfo.activityName(act[i]); if (activityName === "") { continue; } if (plasmoid.configuration.showOnlyCurrentActivity) { if (activity !== activityInfo.currentActivity) { activityNames.push(activityName); } } else if (activity !== activityInfo.currentActivity) { activityNames.push(activityName); } } if (plasmoid.configuration.showOnlyCurrentActivity) { if (activityNames.length > 0) { subTextEntries.push(i18nc("Activities a window is currently on (apart from the current one)", "Also available on %1", activityNames.join(", "))); } } else if (activityNames.length > 0) { subTextEntries.push(i18nc("Which activities a window is currently on", "Available on %1", activityNames.join(", "))); } } return subTextEntries.join("\n"); } }