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 }