/*SPDX-FileCopyrightText: 2013 Sebastian KΓΌgler <sebas@kde.org>SPDX-FileCopyrightText: 2014 Martin GrΓ€Γlin <mgraesslin@kde.org>SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>SPDX-FileCopyrightText: 2017 Roman Gilg <subdiff@gmail.com>SPDX-FileCopyrightText: 2020 Nate Graham <nate@kde.org>SPDX-License-Identifier: LGPL-2.0-or-later*/import QtQuick 2.6import QtQuick.Layouts 1.1import QtGraphicalEffects 1.0import org.kde.plasma.core 2.0 as PlasmaCore// for Highlightimport org.kde.plasma.components 2.0 as PlasmaComponentsimport org.kde.plasma.components 3.0 as PlasmaComponents3import org.kde.plasma.extras 2.0 as PlasmaExtrasColumnLayout {property var submodelIndexproperty int flatIndex: isGroup && index != undefined ? index : 0readonly property int appPid: isGroup ? model.AppPid : pidParent// HACK: Avoid blank space in the tooltip after closing a windowListView.onPooled: width = height = 0ListView.onReused: width = height = undefinedreadonly property string mprisSourceName: mpris2Source.sourceNameForLauncherUrl(launcherUrl, appPid)readonly property var playerData: mprisSourceName != "" ? mpris2Source.data[mprisSourceName] : 0readonly property bool hasPlayer: !!mprisSourceName && !!playerDatareadonly property bool playing: hasPlayer && playerData.PlaybackStatus === "Playing"readonly property bool canControl: hasPlayer && playerData.CanControlreadonly property bool canPlay: hasPlayer && playerData.CanPlayreadonly property bool canPause: hasPlayer && playerData.CanPausereadonly property bool canGoBack: hasPlayer && playerData.CanGoPreviousreadonly property bool canGoNext: hasPlayer && playerData.CanGoNextreadonly property bool canRaise: hasPlayer && playerData.CanRaisereadonly property var currentMetadata: hasPlayer ? playerData.Metadata : ({})readonly property string track: {const xesamTitle = currentMetadata["xesam:title"]if (xesamTitle) {return xesamTitle;}// if no track title is given, print out the file nameconst xesamUrl = currentMetadata["xesam:url"] ? currentMetadata["xesam:url"].toString() : ""if (!xesamUrl) {return "";}const lastSlashPos = xesamUrl.lastIndexOf('/')if (lastSlashPos < 0) {return "";}const lastUrlPart = xesamUrl.substring(lastSlashPos + 1)return decodeURIComponent(lastUrlPart) || "";}readonly property string artist: currentMetadata["xesam:artist"] || ""readonly property string albumArt: currentMetadata["mpris:artUrl"] || ""spacing: PlasmaCore.Units.smallSpacing// text labels + close buttonRowLayout {id: header// match spacing of DefaultToolTip.qml in plasma-frameworkspacing: isWin ? PlasmaCore.Units.smallSpacing : PlasmaCore.Units.largeSpacing// This number controls the overall size of the window tooltipsLayout.maximumWidth: PlasmaCore.Units.gridUnit * 16Layout.minimumWidth: isWin ? Layout.maximumWidth : 0Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter// match margins of DefaultToolTip.qml in plasma-frameworkLayout.margins: isWin ? 0 : PlasmaCore.Units.gridUnit / 2// all textlabelsColumnLayout {spacing: 0// app namePlasmaExtras.Heading {id: appNameHeadinglevel: 3maximumLineCount: 1lineHeight: isWin ? 1 : appNameHeading.lineHeightLayout.fillWidth: trueelide: Text.ElideRighttext: appNameopacity: flatIndex == 0visible: text.length !== 0textFormat: Text.PlainText}// window titlePlasmaComponents.Label {readonly property string title: generateTitle()id: winTitlemaximumLineCount: 1Layout.fillWidth: trueelide: Text.ElideRighttext: (!hasPlayer || !title.includes(track)) ? title : ""opacity: 0.75visible: title.length !== 0 && title !== appNameHeading.texttextFormat: Text.PlainText}// subtextPlasmaComponents.Label {id: subtextmaximumLineCount: 1Layout.fillWidth: trueelide: Text.ElideRighttext: isWin ? generateSubText() : ""opacity: 0.6visible: text.length !== 0 && text !== appNameHeading.texttextFormat: Text.PlainText}}// Count badge.// The badge itself is inside an item to better center the text in the bubbleItem {Layout.alignment: Qt.AlignRight | Qt.AlignTopLayout.preferredHeight: closeButton.heightLayout.preferredWidth: closeButton.widthvisible: flatIndex === 0 && smartLauncherCountVisibleBadge {anchors.centerIn: parentheight: PlasmaCore.Units.iconSizes.smallMediumnumber: smartLauncherCount}}// close buttonPlasmaComponents3.ToolButton {id: closeButtonLayout.alignment: Qt.AlignRight | Qt.AlignTopvisible: isWinicon.name: "window-close"onClicked: {backend.cancelHighlightWindows();tasksModel.requestClose(submodelIndex);}}}// thumbnail containerItem {id: thumbnailSourceItemLayout.minimumWidth: header.widthLayout.preferredHeight: header.width / 2clip: truevisible: toolTipDelegate.isWinreadonly property bool isMinimized: isGroup ? IsMinimized == true : isMinimizedParent// TODO: this causes XCB error message when being visible the first timereadonly property var winId: toolTipDelegate.isWin && toolTipDelegate.windows[flatIndex] !== undefined ? toolTipDelegate.windows[flatIndex] : 0// There's no PlasmaComponents3 versionPlasmaComponents.Highlight {anchors.fill: hoverHandlervisible: hoverHandler.item ? hoverHandler.item.containsMouse : falsepressed: hoverHandler.item ? hoverHandler.item.containsPress : false}Loader {id: thumbnailLoaderactive: !albumArtImage.visible && Number.isInteger(thumbnailSourceItem.winId) && flatIndex !== -1 // Avoid loading when the instance is going to be destroyedasynchronous: truevisible: activeanchors.fill: hoverHandler// Indent a little bit so that neither the thumbnail nor the drop// shadow can cover up the highlightanchors.margins: PlasmaCore.Units.smallSpacing * 2sourceComponent: thumbnailSourceItem.isMinimized ? iconItem : x11ThumbnailComponent {id: x11ThumbnailPlasmaCore.WindowThumbnail {winId: thumbnailSourceItem.winId}}// when minimized, we don't have a preview on X11, so show the iconComponent {id: iconItemPlasmaCore.IconItem {source: iconanimated: falseusesPlasmaTheme: falsevisible: valid}}}Loader {id: pipeWireLoaderanchors.fill: hoverHandler// Indent a little bit so that neither the thumbnail nor the drop// shadow can cover up the highlightanchors.margins: thumbnailLoader.anchors.marginsactive: !albumArtImage.visible && !Number.isInteger(thumbnailSourceItem.winId) && flatIndex !== -1//In a loader since we might not have PipeWire available yet (WITH_PIPEWIRE could be undefined in plasma-workspace/libtaskmanager/declarative/taskmanagerplugin.cpp)source: "PipeWireThumbnail.qml"}Loader {active: (pipeWireLoader.active && pipeWireLoader.item.visible) || (thumbnailLoader.status === Loader.Ready && !thumbnailSourceItem.isMinimized)asynchronous: truevisible: activeanchors.fill: pipeWireLoader.active ? pipeWireLoader : thumbnailLoadersourceComponent: DropShadow {horizontalOffset: 0verticalOffset: Math.round(3 * PlasmaCore.Units.devicePixelRatio)radius: Math.round(8.0 * PlasmaCore.Units.devicePixelRatio)samples: Math.round(radius * 1.5)color: "Black"source: pipeWireLoader.active ? pipeWireLoader.item : thumbnailLoader.item // source could be undefined when albumArt is available, so put it in a Loader.}}Loader {active: albumArtImage.visible && albumArtImage.status === Image.Ready && flatIndex !== -1 // Avoid loading when the instance is going to be destroyedasynchronous: truevisible: activeanchors.centerIn: hoverHandlersourceComponent: ShaderEffect {id: albumArtBackgroundreadonly property Image source: albumArtImage// Manual implementation of Image.PreserveAspectCropreadonly property real scaleFactor: Math.max(hoverHandler.width / source.paintedWidth, hoverHandler.height / source.paintedHeight)width: Math.round(source.paintedWidth * scaleFactor)height: Math.round(source.paintedHeight * scaleFactor)layer.enabled: trueopacity: 0.25layer.effect: FastBlur {source: albumArtBackgroundanchors.fill: sourceradius: 30}}}Image {id: albumArtImage// also Image.Loading to prevent loading thumbnails just because the album art takes a split second to load// if this is a group tooltip, we check if window title and track match, to allow distinguishing the different windows// if this app is a browser, we also check the title, so album art is not shown when the user is on some other tab// in all other cases we can safely show the album art without checking the titlereadonly property bool available: (status === Image.Ready || status === Image.Loading)&& (!(isGroup || backend.applicationCategories(launcherUrl).includes("WebBrowser")) || generateTitle().includes(track))anchors.fill: hoverHandler// Indent by one pixel to make sure we never cover up the entire highlightanchors.margins: 1sourceSize: Qt.size(parent.width, parent.height)asynchronous: truesource: albumArtfillMode: Image.PreserveAspectFitvisible: available}// hoverHandler has to be unloaded after the instance is pooled in order to avoid getting the old containsMouse status when the same instance is reused, so put it in a Loader.Loader {id: hoverHandleractive: flatIndex !== -1anchors.fill: parentsourceComponent: ToolTipWindowMouseArea {rootTask: parentTaskmodelIndex: submodelIndexwinId: thumbnailSourceItem.winId}}}// Player controls row, load on demand so group tooltips could be loaded fasterLoader {active: hasPlayer && flatIndex !== -1 // Avoid loading when the instance is going to be destroyedasynchronous: truevisible: activeLayout.fillWidth: trueLayout.maximumWidth: header.Layout.maximumWidthLayout.leftMargin: header.Layout.marginsLayout.rightMargin: header.Layout.marginssourceComponent: RowLayout {enabled: canControlColumnLayout {Layout.fillWidth: trueLayout.topMargin: PlasmaCore.Units.smallSpacingLayout.bottomMargin: PlasmaCore.Units.smallSpacingLayout.rightMargin: isWin ? PlasmaCore.Units.smallSpacing : PlasmaCore.Units.largeSpacingspacing: 0ScrollableTextWrapper {id: songTextWrapperLayout.fillWidth: trueLayout.preferredHeight: songText.heightimplicitWidth: songText.implicitWidthPlasmaComponents3.Label {id: songTextparent: songTextWrapperwidth: parent.widthheight: undefinedlineHeight: 1maximumLineCount: artistText.visible? 1 : 2wrapMode: Text.NoWrapelide: parent.state ? Text.ElideNone : Text.ElideRighttext: tracktextFormat: Text.PlainText}}ScrollableTextWrapper {id: artistTextWrapperLayout.fillWidth: trueLayout.preferredHeight: artistText.heightimplicitWidth: artistText.implicitWidthvisible: artistText.text !== ""PlasmaExtras.DescriptiveLabel {id: artistTextparent: artistTextWrapperwidth: parent.widthheight: undefinedwrapMode: Text.NoWraplineHeight: 1elide: parent.state ? Text.ElideNone : Text.ElideRighttext: artistfont: PlasmaCore.Theme.smallestFonttextFormat: Text.PlainText}}}PlasmaComponents3.ToolButton {enabled: canGoBackicon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward"onClicked: mpris2Source.goPrevious(mprisSourceName)}PlasmaComponents3.ToolButton {enabled: playing ? canPause : canPlayicon.name: playing ? "media-playback-pause" : "media-playback-start"onClicked: {if (!playing) {mpris2Source.play(mprisSourceName);} else {mpris2Source.pause(mprisSourceName);}}}PlasmaComponents3.ToolButton {enabled: canGoNexticon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward"onClicked: mpris2Source.goNext(mprisSourceName)}}}// Volume controlsLoader {active: parentTask&& pulseAudio.item&& parentTask.audioIndicatorsEnabled&& parentTask.hasAudioStream&& flatIndex !== -1 // Avoid loading when the instance is going to be destroyedasynchronous: truevisible: activeLayout.fillWidth: trueLayout.maximumWidth: header.Layout.maximumWidthLayout.leftMargin: header.Layout.marginsLayout.rightMargin: header.Layout.marginssourceComponent: RowLayout {PlasmaComponents3.ToolButton { // Mute buttonicon.width: PlasmaCore.Units.iconSizes.smallicon.height: PlasmaCore.Units.iconSizes.smallicon.name: if (checked) {"audio-volume-muted"} else if (slider.displayValue <= 25) {"audio-volume-low"} else if (slider.displayValue <= 75) {"audio-volume-medium"} else {"audio-volume-high"}onClicked: parentTask.toggleMuted()checked: parentTask.mutedPlasmaComponents3.ToolTip {text: parent.checked ?i18nc("button to unmute app", "Unmute %1", parentTask.appName): i18nc("button to mute app", "Mute %1", parentTask.appName)}}PlasmaComponents3.Slider {id: sliderreadonly property int displayValue: Math.round(value / to * 100)readonly property int loudestVolume: {let v = 0parentTask.audioStreams.forEach((stream) => {v = Math.max(v, stream.volume)})return v}Layout.fillWidth: truefrom: pulseAudio.item.minimalVolumeto: pulseAudio.item.normalVolumevalue: loudestVolumestepSize: to / 100opacity: parentTask.muted ? 0.5 : 1Accessible.name: i18nc("Accessibility data on volume slider", "Adjust volume for %1", parentTask.appName)onMoved: parentTask.audioStreams.forEach((stream) => {let v = Math.max(from, value)if (v > 0 && loudestVolume > 0) { // prevent divide by 0// adjust volume relative to the loudest streamv = Math.min(Math.round(stream.volume / loudestVolume * v), to)}stream.model.Volume = vstream.model.Muted = v === 0})}PlasmaComponents3.Label { // percent labelLayout.alignment: Qt.AlignHCenterLayout.minimumWidth: percentMetrics.advanceWidthhorizontalAlignment: Qt.AlignRighttext: i18nc("volume percentage", "%1%", slider.displayValue)TextMetrics {id: percentMetricstext: i18nc("only used for sizing, should be widest possible string", "100%")}}}}function generateTitle() {if (!isWin) {return genericName != undefined ? genericName : "";}let 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.const counter = text.match(/<\d+>\W*$/);text = text.replace(/\s*<\d+>\W*$/, "");// Remove appName from the end of text.const 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 = "β";}return text.toString();}function generateSubText() {if (activitiesParent === undefined) {return "";}let subTextEntries = [];const onAllDesktops = (isGroup ? IsOnAllVirtualDesktops : isOnAllVirtualDesktopsParent) === true;if (!plasmoid.configuration.showOnlyCurrentDesktop && virtualDesktopInfo.numberOfDesktops > 1) {const virtualDesktops = isGroup ? VirtualDesktops : virtualDesktopParent;if (!onAllDesktops && virtualDesktops !== undefined && virtualDesktops.length > 0) {let virtualDesktopNameList = new Array();for (let 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"));}}const 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) {let activityNames = [];for (let i = 0; i < act.length; i++) {const activity = act[i];const 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");}}