/*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-2024 Nate Graham <nate@kde.org>SPDX-License-Identifier: LGPL-2.0-or-later*/pragma ComponentBehavior: Boundimport QtQuickimport QtQuick.Layoutsimport Qt5Compat.GraphicalEffects as GEimport org.kde.plasma.plasmoidimport org.kde.plasma.core as PlasmaCoreimport org.kde.plasma.components as PlasmaComponents3import org.kde.plasma.extras as PlasmaExtrasimport org.kde.kirigami as Kirigamiimport org.kde.kwindowsystemColumnLayout {id: rootrequired property var modelrequired property int indexrequired property /*QModelIndex*/ var submodelIndexrequired property int appPidrequired property string displayrequired property bool isMinimizedrequired property bool isOnAllVirtualDesktopsrequired property /*list<var>*/ var virtualDesktops // Can't use list<var> because of QTBUG-127600required property list<string> activitiesrequired property bool isReadyForPaintingrequired property bool isExcludedFromCaptureproperty bool hasTrackInATitle: falseproperty int orientation: ListView.Vertical // vertical for compact single-window tooltips// HACK: Avoid blank space in the tooltip after closing a windowListView.onPooled: width = height = 0ListView.onReused: width = height = undefinedreadonly property string title: {if (!toolTipDelegate.isWin) {return toolTipDelegate.genericName;}let text = display;if (toolTipDelegate.isGroup && text === "") {return "";}// Normally the window title will always have " β [app name]" at the end of// the window-provided title. But if it doesn't, this is intentional 100%// of the time because the developer or user has deliberately removed that// part, so just display it with no more fancy processing.if (!text.match(/\s+(β|-|β)/)) {return text;}// 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.text = `${(text.match(/.*(?=\s+(β|-|β))/) || [""])[0]}${(text.match(/<\d+>/) || [""]).pop()}`;// 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;}readonly property bool titleIncludesTrack: toolTipDelegate.playerData !== null && title.includes(toolTipDelegate.playerData.track)// Lots of spacing with no thumbnails looks badspacing: Plasmoid.configuration.showToolTips ? Kirigami.Units.smallSpacing : 0// text labels + close buttonItem {id: headerItemimplicitHeight: header.heightimplicitWidth: header.implicitWidthLayout.fillWidth: true// This number controls the overall size of the window tooltipsLayout.maximumWidth: toolTipDelegate.tooltipInstanceMaximumWidthLayout.minimumWidth: (toolTipDelegate.isWin && Plasmoid.configuration.showToolTips) || toolTipDelegate.isGroup ? Layout.maximumWidth : 0Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter// match margins of DefaultToolTip.qml in plasma-frameworkLayout.margins: toolTipDelegate.isWin && Plasmoid.configuration.showToolTips ? 0 : Kirigami.Units.gridUnit / 2// With thumbnails the header has no margins, so that the thumbnail below it// can span the entire width of the tooltip. That leaves the text labels flush// against the tooltip's edges, so inset them by the same amount the thumbnail// is, to line them upreadonly property int labelInset: toolTipDelegate.isWin && Plasmoid.configuration.showToolTips ? pipeWireLoader.anchors.margins : 0RowLayout {id: headerwidth: parent.width// match spacing of DefaultToolTip.qml in plasma-frameworkspacing: Kirigami.Units.smallSpacing// close button for right edge ltr and left edge rtlLayoutItemProxy {id: closeButtonFlippedItemProxytarget: closeButtonvisible: toolTipDelegate.isWin &&((Plasmoid.location == PlasmaCore.Types.LeftEdge &&Application.layoutDirection == Qt.RightToLeft) ||(Plasmoid.location == PlasmaCore.Types.RightEdge &&Application.layoutDirection == Qt.LeftToRight))}// all textlabelsColumnLayout {spacing: 0// Keep the inset on the side away from the close button, which is meant to sit in the cornerLayout.leftMargin: closeButtonFlippedItemProxy.visible ? 0 : headerItem.labelInsetLayout.rightMargin: closeButtonFlippedItemProxy.visible ? headerItem.labelInset : 0Layout.topMargin: headerItem.labelInset// app nameKirigami.Heading {id: appNameHeadinglevel: 3maximumLineCount: 1Layout.fillWidth: truelineHeight: toolTipDelegate.isWin && Plasmoid.configuration.showToolTips ? 1 : appNameHeading.lineHeightelide: Text.ElideRighttext: toolTipDelegate.appNamecolor: (headerHoverHandler.visible && headerHoverHighlight.pressed) ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColoropacity: root.index === 0 ? 1 : 0visible: (text.length !== 0) && (root.orientation === ListView.Horizontal || root.index === 0)textFormat: Text.PlainText}// window titlePlasmaComponents3.Label {id: winTitleLayout.fillWidth: true// For horizontal grouped tasks, leave room for two lines so thumbnails alignLayout.preferredHeight: root.orientation === ListView.Horizontal && lineCount === 1? implicitHeight * 2: implicitHeightmaximumLineCount: 2wrapMode: Text.Wrapelide: Text.ElideRightverticalAlignment: Text.AlignVCenterproperty bool somethingVisible: (thumbnailSourceItem.visible ||appNameHeading.visible || subtext.visible)text: ((root.titleIncludesTrack && playerController.active) ||(root.title === appNameHeading.text && somethingVisible))? "" : root.titlecolor: (headerHoverHandler.visible && headerHoverHighlight.pressed) ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColoropacity: 0.75font.bold: toolTipDelegate.isGroup && toolTipDelegate.parentTask.model.IsActive && root.index == tasksModel.activeTask.rowvisible: root.orientation === ListView.Horizontal || text.length !== 0textFormat: Text.PlainText}// subtextPlasmaComponents3.Label {id: subtextLayout.fillWidth: true// For horizontal grouped tasks, leave room for two lines so thumbnails alignLayout.preferredHeight: root.orientation === ListView.Horizontal && lineCount === 1? implicitHeight * 2: implicitHeightmaximumLineCount: 2wrapMode: Text.Wrapelide: Text.ElideRightverticalAlignment: Text.AlignVCentertext: toolTipDelegate.isWin ? root.generateSubText() : ""color: (headerHoverHandler.visible && headerHoverHighlight.pressed) ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColoropacity: 0.75visible: 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: !Plasmoid.configuration.showToolTips && !playerController.active && !volumeControls.active ? Qt.AlignVCenter : Qt.AlignTopLayout.preferredHeight: closeButton.heightLayout.preferredWidth: closeButton.widthvisible: root.index === 0 && toolTipDelegate.smartLauncherCountVisibleKirigami.Badge {anchors.centerIn: parenttext: toolTipDelegate.smartLauncherCount}}LayoutItemProxy {target: closeButtonvisible: toolTipDelegate.isWin && !closeButtonFlippedItemProxy.visible}// close buttonPlasmaComponents3.ToolButton {id: closeButtonLayout.alignment: Qt.AlignTopLayout.rightMargin: closeButtonFlippedItemProxy.visible ? headerItem.Layout.margins : -headerItem.Layout.marginsLayout.leftMargin: closeButtonFlippedItemProxy.visible ? -headerItem.Layout.margins : headerItem.Layout.marginsLayout.topMargin: -headerItem.Layout.marginsicon.name: "window-close"onClicked: {tasks.cancelHighlightWindows();tasksModel.requestClose(root.submodelIndex);}PlasmaComponents3.ToolTip.text: i18nc("@info:tooltip Close this window", "Close window")PlasmaComponents3.ToolTip.visible: root.visible && hoveredPlasmaComponents3.ToolTip.delay: Kirigami.Units.toolTipDelay}}// make the header clickable if image tooltips are disabled (and thus there is no other clickable area that activates the window)// headerHoverHandler 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: headerHoverHandleractive: (root.index !== -1) && !Plasmoid.configuration.showToolTipsz: -2anchors.fill: headerItemanchors.margins: -headerItem.Layout.marginssourceComponent: ToolTipWindowMouseArea {rootTask: toolTipDelegate.parentTaskmodelIndex: root.submodelIndexwinId: thumbnailSourceItem.winId}}// There's no PlasmaComponents3 versionPlasmaExtras.Highlight {id: headerHoverHighlightanchors.fill: headerHoverHandlerz: -1visible: (headerHoverHandler.item as MouseArea)?.containsMouse ?? falsepressed: (headerHoverHandler.item as MouseArea)?.containsPress ?? falsehovered: true}}// thumbnail containerItem {id: thumbnailSourceItemLayout.fillWidth: trueLayout.preferredHeight: Kirigami.Units.gridUnit * 8clip: truevisible: Plasmoid.configuration.showToolTips && toolTipDelegate.isWinreadonly property /*undefined|WId where WId = int|string*/ var winId:toolTipDelegate.isWin ? toolTipDelegate.windows[root.index] : undefined// There's no PlasmaComponents3 versionPlasmaExtras.Highlight {anchors.fill: hoverHandlervisible: (hoverHandler.item as MouseArea)?.containsMouse ?? falsepressed: (hoverHandler.item as MouseArea)?.containsPress ?? falsehovered: true}Loader {id: pipeWireLoaderanchors.fill: hoverHandler// Indent a little bit so that neither the thumbnail nor the drop// shadow can cover up the highlightanchors.margins: Kirigami.Units.smallSpacing * 2active: Plasmoid.configuration.showToolTips&& !toolTipDelegate.isLauncher&& !albumArtImage.visible&& (toolTipDelegate.isReadyForPainting || root.isExcludedFromCapture)&& root.index !== -1asynchronous: true//In a loader since we might not have PipeWire available yet (WITH_PIPEWIRE could be undefined in plasma-workspace/libtaskmanager/declarative/taskmanagerplugin.cpp)// Windows excluded from capture must not leak their contents, so show the icon insteadsource: root.isExcludedFromCapture ? "IconThumbnail.qml" : "PipeWireThumbnail.qml"}Loader {active: Plasmoid.configuration.showToolTips&& ((pipeWireLoader.item as PipeWireThumbnail)?.hasThumbnail ?? false)asynchronous: truevisible: activeanchors.fill: pipeWireLoadersourceComponent: GE.DropShadow {horizontalOffset: 0verticalOffset: 3radius: 8samples: Math.round(radius * 1.5)color: "Black"source: pipeWireLoader.item // source could be undefined when albumArt is available, so put it in a Loader.}}Loader {active: Plasmoid.configuration.showToolTips&& albumArtImage.visible&& albumArtImage.status === Image.Ready&& root.index !== -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: GE.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)&& (!(toolTipDelegate.isGroup || backend.applicationCategories(launcherUrl).includes("WebBrowser")) || root.titleIncludesTrack)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: trueretainWhileLoading: truesource: toolTipDelegate.playerData?.artUrl ?? ""fillMode: 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: root.index !== -1anchors.fill: parentsourceComponent: ToolTipWindowMouseArea {rootTask: toolTipDelegate.parentTaskmodelIndex: root.submodelIndexwinId: thumbnailSourceItem.winId}}}// Player controls row, load on demand so group tooltips could be loaded fasterLoader {id: playerController// Only load for one entry, as the controls only apply to one window.// If this is changed in the future, test for index != -1 to avoid loading// when the instance is going to be destroyedactive: (toolTipDelegate.parentTask?.tooltipControlsEnabled&& toolTipDelegate.playerData&& ((root.hasTrackInATitle && albumArtImage.available) || (!root.hasTrackInATitle && root.index == 0))) ?? falseasynchronous: truevisible: activeLayout.fillWidth: trueLayout.maximumWidth: headerItem.Layout.maximumWidthLayout.leftMargin: headerItem.Layout.marginsLayout.rightMargin: headerItem.Layout.marginssource: "PlayerController.qml"}// Volume controlsLoader {id: volumeControlsactive: toolTipDelegate.parentTask !== null&& pulseAudio.item !== null&& toolTipDelegate.parentTask.tooltipControlsEnabled&& toolTipDelegate.parentTask.hasAudioStream// Only load for one entry, as the controls only apply to one window.// If this is changed in the future, test for index != -1 to avoid loading// when the instance is going to be destroyed&& ((hasTrackInATitle && albumArtImage.available) || (!hasTrackInATitle && root.index == 0))asynchronous: truevisible: activeLayout.fillWidth: trueLayout.maximumWidth: headerItem.Layout.maximumWidthLayout.leftMargin: headerItem.Layout.marginsLayout.rightMargin: headerItem.Layout.marginssourceComponent: RowLayout {PlasmaComponents3.ToolButton { // Mute buttonid: muteButtonicon.width: Kirigami.Units.iconSizes.smallicon.height: Kirigami.Units.iconSizes.smallicon.name: {let finalIcon = ""if (checked) {finalIcon = "audio-volume-muted"} else if (slider.displayValue <= 25) {finalIcon = "audio-volume-low"} else if (slider.displayValue <= 75) {finalIcon = "audio-volume-medium"} else {finalIcon = "audio-volume-high"}if (mirrored) {finalIcon = finalIcon + "-rtl"}return finalIcon}onClicked: toolTipDelegate.parentTask.toggleMuted()checked: toolTipDelegate.parentTask.mutedPlasmaComponents3.ToolTip {text: muteButton.checked? i18nc("button to unmute app", "Unmute %1", toolTipDelegate.parentTask.appName): i18nc("button to mute app", "Mute %1", toolTipDelegate.parentTask.appName)}}PlasmaComponents3.Slider {id: sliderreadonly property int displayValue: Math.round(value / to * 100)readonly property int loudestVolume: toolTipDelegate.parentTask.audioStreams.reduce((loudestVolume, stream) => Math.max(loudestVolume, stream.volume), 0)Layout.fillWidth: truefrom: pulseAudio.item.minimalVolumeto: pulseAudio.item.normalVolumevalue: loudestVolumestepSize: to / 100opacity: toolTipDelegate.parentTask.muted ? 0.5 : 1Accessible.name: i18nc("Accessibility data on volume slider", "Adjust volume for %1", toolTipDelegate.parentTask.appName)onMoved: toolTipDelegate.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)textFormat: Text.PlainTextTextMetrics {id: percentMetricstext: i18nc("only used for sizing, should be widest possible string", "100%")}}}}function generateSubText(): string {const subTextEntries = [];if (!Plasmoid.configuration.showOnlyCurrentDesktop && virtualDesktopInfo.numberOfDesktops > 1) {if (!isOnAllVirtualDesktops && virtualDesktops.length > 0) {const virtualDesktopNameList = virtualDesktops.map(virtualDesktop => {const index = virtualDesktopInfo.desktopIds.indexOf(virtualDesktop);return virtualDesktopInfo.desktopNames[index];});subTextEntries.push(i18nc("Comma-separated list of desktops", "On %1",virtualDesktopNameList.join(", ")));} else if (isOnAllVirtualDesktops) {subTextEntries.push(i18nc("Comma-separated list of desktops", "Pinned to all desktops"));}}if (activities.length === 0 && activityInfo.numberOfRunningActivities > 1) {subTextEntries.push(i18nc("Which virtual desktop a window is currently on","Available on all activities"));} else if (activities.length > 0) {const activityNames = activities.filter(activity => activity !== activityInfo.currentActivity).map(activity => activityInfo.activityName(activity)).filter(activityName => 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");}}