Warning, /plasma/plasma-desktop/applets/taskmanager/package/contents/ui/ToolTipInstance.qml is written in an unsupported language. File is not indexed.

0001 /*
0002     SPDX-FileCopyrightText: 2013 Sebastian Kügler <sebas@kde.org>
0003     SPDX-FileCopyrightText: 2014 Martin Gräßlin <mgraesslin@kde.org>
0004     SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>
0005     SPDX-FileCopyrightText: 2017 Roman Gilg <subdiff@gmail.com>
0006     SPDX-FileCopyrightText: 2020 Nate Graham <nate@kde.org>
0007 
0008     SPDX-License-Identifier: LGPL-2.0-or-later
0009 */
0010 
0011 import QtQuick
0012 import QtQuick.Layouts
0013 import Qt5Compat.GraphicalEffects
0014 
0015 import org.kde.plasma.plasmoid 2.0
0016 import org.kde.plasma.core as PlasmaCore
0017 import org.kde.plasma.components 3.0 as PlasmaComponents3
0018 import org.kde.plasma.extras 2.0 as PlasmaExtras
0019 import org.kde.kirigami 2 as Kirigami
0020 import org.kde.kwindowsystem 1.0
0021 
0022 ColumnLayout {
0023     property var submodelIndex
0024     property int flatIndex: isGroup && index != undefined ? index : 0
0025     readonly property int appPid: isGroup ? model.AppPid : pidParent
0026 
0027     // HACK: Avoid blank space in the tooltip after closing a window
0028     ListView.onPooled: width = height = 0
0029     ListView.onReused: width = height = undefined
0030 
0031     readonly property string title: {
0032         if (!isWin) {
0033             return genericName || "";
0034         }
0035 
0036         let text;
0037         if (isGroup) {
0038             if (model.display.length === 0) {
0039                 return "";
0040             }
0041             text = model.display;
0042         } else {
0043             text = displayParent;
0044         }
0045 
0046         // Normally the window title will always have " — [app name]" at the end of
0047         // the window-provided title. But if it doesn't, this is intentional 100%
0048         // of the time because the developer or user has deliberately removed that
0049         // part, so just display it with no more fancy processing.
0050         if (!text.match(/\s+(—|-|–)/)) {
0051             return text;
0052         }
0053 
0054         // KWin appends increasing integers in between pointy brackets to otherwise equal window titles.
0055         // In this case save <#number> as counter and delete it at the end of text.
0056         text = `${(text.match(/.*(?=\s+(—|-|–))/) || [""])[0]}${(text.match(/<\d+>/) || [""]).pop()}`;
0057 
0058         // In case the window title had only redundant information (i.e. appName), text is now empty.
0059         // Add a hyphen to indicate that and avoid empty space.
0060         if (text === "") {
0061             text = "—";
0062         }
0063         return text;
0064     }
0065     readonly property bool titleIncludesTrack: toolTipDelegate.playerData && title.includes(toolTipDelegate.playerData.track)
0066 
0067     spacing: Kirigami.Units.smallSpacing
0068 
0069     // text labels + close button
0070     RowLayout {
0071         id: header
0072         // match spacing of DefaultToolTip.qml in plasma-framework
0073         spacing: isWin ? Kirigami.Units.smallSpacing : Kirigami.Units.gridUnit
0074 
0075         // This number controls the overall size of the window tooltips
0076         Layout.maximumWidth: toolTipDelegate.tooltipInstanceMaximumWidth
0077         Layout.minimumWidth: isWin ? Layout.maximumWidth : 0
0078         Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
0079         // match margins of DefaultToolTip.qml in plasma-framework
0080         Layout.margins: isWin ? 0 : Kirigami.Units.gridUnit / 2
0081 
0082         // all textlabels
0083         ColumnLayout {
0084             spacing: 0
0085             // app name
0086             Kirigami.Heading {
0087                 id: appNameHeading
0088                 level: 3
0089                 maximumLineCount: 1
0090                 lineHeight: isWin ? 1 : appNameHeading.lineHeight
0091                 Layout.fillWidth: true
0092                 elide: Text.ElideRight
0093                 text: appName
0094                 opacity: flatIndex == 0
0095                 visible: text.length !== 0
0096                 textFormat: Text.PlainText
0097             }
0098             // window title
0099             PlasmaComponents3.Label {
0100                 id: winTitle
0101                 maximumLineCount: 1
0102                 Layout.fillWidth: true
0103                 elide: Text.ElideRight
0104                 text: titleIncludesTrack ? "" : title
0105                 opacity: 0.75
0106                 visible: title.length !== 0 && title !== appNameHeading.text
0107                 textFormat: Text.PlainText
0108             }
0109             // subtext
0110             PlasmaComponents3.Label {
0111                 id: subtext
0112                 maximumLineCount: 1
0113                 Layout.fillWidth: true
0114                 elide: Text.ElideRight
0115                 text: isWin ? generateSubText() : ""
0116                 opacity: 0.6
0117                 visible: text.length !== 0 && text !== appNameHeading.text
0118                 textFormat: Text.PlainText
0119             }
0120         }
0121 
0122         // Count badge.
0123         // The badge itself is inside an item to better center the text in the bubble
0124         Item {
0125             Layout.alignment: Qt.AlignRight | Qt.AlignTop
0126             Layout.preferredHeight: closeButton.height
0127             Layout.preferredWidth: closeButton.width
0128             visible: flatIndex === 0 && smartLauncherCountVisible
0129 
0130             Badge {
0131                 anchors.centerIn: parent
0132                 height: Kirigami.Units.iconSizes.smallMedium
0133                 number: smartLauncherCount
0134             }
0135         }
0136 
0137         // close button
0138         PlasmaComponents3.ToolButton {
0139             id: closeButton
0140             Layout.alignment: Qt.AlignRight | Qt.AlignTop
0141             visible: isWin
0142             icon.name: "window-close"
0143             onClicked: {
0144                 backend.cancelHighlightWindows();
0145                 tasksModel.requestClose(submodelIndex);
0146             }
0147         }
0148     }
0149 
0150     // thumbnail container
0151     Item {
0152         id: thumbnailSourceItem
0153 
0154         Layout.minimumWidth: header.width
0155         Layout.preferredHeight: header.width / 2
0156 
0157         clip: true
0158         visible: toolTipDelegate.isWin
0159 
0160         readonly property bool isMinimized: isGroup ? IsMinimized : isMinimizedParent
0161         // TODO: this causes XCB error message when being visible the first time
0162         readonly property var winId: toolTipDelegate.isWin && toolTipDelegate.windows[flatIndex] !== undefined ? toolTipDelegate.windows[flatIndex] : 0
0163 
0164         // There's no PlasmaComponents3 version
0165         PlasmaExtras.Highlight {
0166             anchors.fill: hoverHandler
0167             visible: Boolean(hoverHandler.item?.containsMouse)
0168             pressed: Boolean(hoverHandler.item?.containsPress)
0169             hovered: true
0170         }
0171 
0172         Loader {
0173             id: thumbnailLoader
0174             active: !toolTipDelegate.isLauncher
0175                 && !albumArtImage.visible
0176                 && (Number.isInteger(thumbnailSourceItem.winId) || pipeWireLoader.item && !pipeWireLoader.item.hasThumbnail)
0177                 && flatIndex !== -1 // Avoid loading when the instance is going to be destroyed
0178             asynchronous: true
0179             visible: active
0180             anchors.fill: hoverHandler
0181             // Indent a little bit so that neither the thumbnail nor the drop
0182             // shadow can cover up the highlight
0183             anchors.margins: Kirigami.Units.smallSpacing * 2
0184 
0185             sourceComponent: thumbnailSourceItem.isMinimized || pipeWireLoader.active ? iconItem : x11Thumbnail
0186 
0187             Component {
0188                 id: x11Thumbnail
0189 
0190                 PlasmaCore.WindowThumbnail {
0191                     winId: thumbnailSourceItem.winId
0192                 }
0193             }
0194 
0195             // when minimized, we don't have a preview on X11, so show the icon
0196             Component {
0197                 id: iconItem
0198 
0199                 Kirigami.Icon {
0200                     id: realIconItem
0201                     source: icon
0202                     animated: false
0203                     visible: valid
0204                     opacity: pipeWireLoader.active ? 0 : 1
0205 
0206                     SequentialAnimation {
0207                         running: true
0208 
0209                         PauseAnimation {
0210                             duration: Kirigami.Units.humanMoment
0211                         }
0212 
0213                         NumberAnimation {
0214                             id: showAnimation
0215                             duration: Kirigami.Units.longDuration
0216                             easing.type: Easing.OutCubic
0217                             property: "opacity"
0218                             target: realIconItem
0219                             to: 1
0220                         }
0221                     }
0222 
0223                 }
0224             }
0225         }
0226 
0227         Loader {
0228             id: pipeWireLoader
0229             anchors.fill: hoverHandler
0230             // Indent a little bit so that neither the thumbnail nor the drop
0231             // shadow can cover up the highlight
0232             anchors.margins: thumbnailLoader.anchors.margins
0233 
0234             active: !toolTipDelegate.isLauncher && !albumArtImage.visible && KWindowSystem.isPlatformWayland && flatIndex !== -1
0235             asynchronous: true
0236             //In a loader since we might not have PipeWire available yet (WITH_PIPEWIRE could be undefined in plasma-workspace/libtaskmanager/declarative/taskmanagerplugin.cpp)
0237             source: "PipeWireThumbnail.qml"
0238         }
0239 
0240         Loader {
0241             active: (pipeWireLoader.item && pipeWireLoader.item.hasThumbnail) || (thumbnailLoader.status === Loader.Ready && !thumbnailSourceItem.isMinimized)
0242             asynchronous: true
0243             visible: active
0244             anchors.fill: pipeWireLoader.active ? pipeWireLoader : thumbnailLoader
0245 
0246             sourceComponent: DropShadow {
0247                 horizontalOffset: 0
0248                 verticalOffset: 3
0249                 radius: 8
0250                 samples: Math.round(radius * 1.5)
0251                 color: "Black"
0252                 source: pipeWireLoader.active ? pipeWireLoader.item : thumbnailLoader.item // source could be undefined when albumArt is available, so put it in a Loader.
0253             }
0254         }
0255 
0256         Loader {
0257             active: albumArtImage.visible && albumArtImage.status === Image.Ready && flatIndex !== -1 // Avoid loading when the instance is going to be destroyed
0258             asynchronous: true
0259             visible: active
0260             anchors.centerIn: hoverHandler
0261 
0262             sourceComponent: ShaderEffect {
0263                 id: albumArtBackground
0264                 readonly property Image source: albumArtImage
0265 
0266                 // Manual implementation of Image.PreserveAspectCrop
0267                 readonly property real scaleFactor: Math.max(hoverHandler.width / source.paintedWidth, hoverHandler.height / source.paintedHeight)
0268                 width: Math.round(source.paintedWidth * scaleFactor)
0269                 height: Math.round(source.paintedHeight * scaleFactor)
0270                 layer.enabled: true
0271                 opacity: 0.25
0272                 layer.effect: FastBlur {
0273                     source: albumArtBackground
0274                     anchors.fill: source
0275                     radius: 30
0276                 }
0277             }
0278         }
0279 
0280         Image {
0281             id: albumArtImage
0282             // also Image.Loading to prevent loading thumbnails just because the album art takes a split second to load
0283             // if this is a group tooltip, we check if window title and track match, to allow distinguishing the different windows
0284             // 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
0285             // in all other cases we can safely show the album art without checking the title
0286             readonly property bool available: (status === Image.Ready || status === Image.Loading)
0287                 && (!(isGroup || backend.applicationCategories(launcherUrl).includes("WebBrowser")) || titleIncludesTrack)
0288 
0289             anchors.fill: hoverHandler
0290             // Indent by one pixel to make sure we never cover up the entire highlight
0291             anchors.margins: 1
0292             sourceSize: Qt.size(parent.width, parent.height)
0293 
0294             asynchronous: true
0295             source: toolTipDelegate.playerData?.artUrl ?? ""
0296             fillMode: Image.PreserveAspectFit
0297             visible: available
0298         }
0299 
0300         // 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.
0301         Loader {
0302             id: hoverHandler
0303             active: flatIndex !== -1
0304             anchors.fill: parent
0305             sourceComponent: ToolTipWindowMouseArea {
0306                 rootTask: parentTask
0307                 modelIndex: submodelIndex
0308                 winId: thumbnailSourceItem.winId
0309             }
0310         }
0311     }
0312 
0313     // Player controls row, load on demand so group tooltips could be loaded faster
0314     Loader {
0315         id: playerController
0316         active: toolTipDelegate.playerData && flatIndex !== -1 // Avoid loading when the instance is going to be destroyed
0317         asynchronous: true
0318         visible: active
0319         Layout.fillWidth: true
0320         Layout.maximumWidth: header.Layout.maximumWidth
0321         Layout.leftMargin: header.Layout.margins
0322         Layout.rightMargin: header.Layout.margins
0323 
0324         source: "PlayerController.qml"
0325     }
0326 
0327     // Volume controls
0328     Loader {
0329         active: parentTask
0330              && pulseAudio.item
0331              && parentTask.audioIndicatorsEnabled
0332              && parentTask.hasAudioStream
0333              && flatIndex !== -1 // Avoid loading when the instance is going to be destroyed
0334         asynchronous: true
0335         visible: active
0336         Layout.fillWidth: true
0337         Layout.maximumWidth: header.Layout.maximumWidth
0338         Layout.leftMargin: header.Layout.margins
0339         Layout.rightMargin: header.Layout.margins
0340         sourceComponent: RowLayout {
0341             PlasmaComponents3.ToolButton { // Mute button
0342                 icon.width: Kirigami.Units.iconSizes.small
0343                 icon.height: Kirigami.Units.iconSizes.small
0344                 icon.name: if (checked) {
0345                     "audio-volume-muted"
0346                 } else if (slider.displayValue <= 25) {
0347                     "audio-volume-low"
0348                 } else if (slider.displayValue <= 75) {
0349                     "audio-volume-medium"
0350                 } else {
0351                     "audio-volume-high"
0352                 }
0353                 onClicked: parentTask.toggleMuted()
0354                 checked: parentTask.muted
0355 
0356                 PlasmaComponents3.ToolTip {
0357                     text: parent.checked ?
0358                         i18nc("button to unmute app", "Unmute %1", parentTask.appName)
0359                         : i18nc("button to mute app", "Mute %1", parentTask.appName)
0360                 }
0361             }
0362 
0363             PlasmaComponents3.Slider {
0364                 id: slider
0365 
0366                 readonly property int displayValue: Math.round(value / to * 100)
0367                 readonly property int loudestVolume: {
0368                     let v = 0
0369                     parentTask.audioStreams.forEach((stream) => {
0370                         v = Math.max(v, stream.volume)
0371                     })
0372                     return v
0373                 }
0374 
0375                 Layout.fillWidth: true
0376                 from: pulseAudio.item.minimalVolume
0377                 to: pulseAudio.item.normalVolume
0378                 value: loudestVolume
0379                 stepSize: to / 100
0380                 opacity: parentTask.muted ? 0.5 : 1
0381 
0382                 Accessible.name: i18nc("Accessibility data on volume slider", "Adjust volume for %1", parentTask.appName)
0383 
0384                 onMoved: parentTask.audioStreams.forEach((stream) => {
0385                     let v = Math.max(from, value)
0386                     if (v > 0 && loudestVolume > 0) { // prevent divide by 0
0387                         // adjust volume relative to the loudest stream
0388                         v = Math.min(Math.round(stream.volume / loudestVolume * v), to)
0389                     }
0390                     stream.model.Volume = v
0391                     stream.model.Muted = v === 0
0392                 })
0393             }
0394             PlasmaComponents3.Label { // percent label
0395                 Layout.alignment: Qt.AlignHCenter
0396                 Layout.minimumWidth: percentMetrics.advanceWidth
0397                 horizontalAlignment: Qt.AlignRight
0398                 text: i18nc("volume percentage", "%1%", slider.displayValue)
0399                 textFormat: Text.PlainText
0400                 TextMetrics {
0401                     id: percentMetrics
0402                     text: i18nc("only used for sizing, should be widest possible string", "100%")
0403                 }
0404             }
0405         }
0406     }
0407 
0408     function generateSubText() {
0409         if (activitiesParent === undefined) {
0410             return "";
0411         }
0412 
0413         let subTextEntries = [];
0414 
0415         const onAllDesktops = isGroup ? IsOnAllVirtualDesktops : isOnAllVirtualDesktopsParent;
0416         if (!Plasmoid.configuration.showOnlyCurrentDesktop && virtualDesktopInfo.numberOfDesktops > 1) {
0417             const virtualDesktops = isGroup ? VirtualDesktops : virtualDesktopParent;
0418 
0419             if (!onAllDesktops && virtualDesktops !== undefined && virtualDesktops.length > 0) {
0420                 let virtualDesktopNameList = new Array();
0421 
0422                 for (let i = 0; i < virtualDesktops.length; ++i) {
0423                     virtualDesktopNameList.push(virtualDesktopInfo.desktopNames[virtualDesktopInfo.desktopIds.indexOf(virtualDesktops[i])]);
0424                 }
0425 
0426                 subTextEntries.push(i18nc("Comma-separated list of desktops", "On %1",
0427                     virtualDesktopNameList.join(", ")));
0428             } else if (onAllDesktops) {
0429                 subTextEntries.push(i18nc("Comma-separated list of desktops", "Pinned to all desktops"));
0430             }
0431         }
0432 
0433         const act = isGroup ? Activities : activitiesParent;
0434         if (act === undefined) {
0435             return subTextEntries.join("\n");
0436         }
0437 
0438         if (act.length === 0 && activityInfo.numberOfRunningActivities > 1) {
0439             subTextEntries.push(i18nc("Which virtual desktop a window is currently on",
0440                 "Available on all activities"));
0441         } else if (act.length > 0) {
0442             let activityNames = [];
0443 
0444             for (let i = 0; i < act.length; i++) {
0445                 const activity = act[i];
0446                 const activityName = activityInfo.activityName(act[i]);
0447                 if (activityName === "") {
0448                     continue;
0449                 }
0450                 if (Plasmoid.configuration.showOnlyCurrentActivity) {
0451                     if (activity !== activityInfo.currentActivity) {
0452                         activityNames.push(activityName);
0453                     }
0454                 } else if (activity !== activityInfo.currentActivity) {
0455                     activityNames.push(activityName);
0456                 }
0457             }
0458 
0459             if (Plasmoid.configuration.showOnlyCurrentActivity) {
0460                 if (activityNames.length > 0) {
0461                     subTextEntries.push(i18nc("Activities a window is currently on (apart from the current one)",
0462                         "Also available on %1", activityNames.join(", ")));
0463                 }
0464             } else if (activityNames.length > 0) {
0465                 subTextEntries.push(i18nc("Which activities a window is currently on",
0466                     "Available on %1", activityNames.join(", ")));
0467             }
0468         }
0469 
0470         return subTextEntries.join("\n");
0471     }
0472 }