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 }