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

0001 /*
0002     SPDX-FileCopyrightText: 2012-2013 Eike Hein <hein@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 import QtQuick 2.15
0008 
0009 import org.kde.plasma.core as PlasmaCore
0010 import org.kde.ksvg 1.0 as KSvg
0011 import org.kde.plasma.extras 2.0 as PlasmaExtras
0012 import org.kde.plasma.components 3.0 as PlasmaComponents3
0013 import org.kde.kirigami 2.20 as Kirigami
0014 import org.kde.plasma.private.taskmanager 0.1 as TaskManagerApplet
0015 import org.kde.plasma.plasmoid 2.0
0016 
0017 import "code/layout.js" as LayoutManager
0018 import "code/tools.js" as TaskTools
0019 
0020 PlasmaCore.ToolTipArea {
0021     id: task
0022 
0023     activeFocusOnTab: true
0024 
0025     height: Math.max(Kirigami.Units.iconSizes.sizeForLabels, Kirigami.Units.iconSizes.medium) + LayoutManager.verticalMargins()
0026 
0027     visible: false
0028 
0029     // To achieve a bottom to top layout, the task manager is rotated by 180 degrees(see main.qml).
0030     // This makes the tasks mirrored, so we mirror them again to fix that.
0031     rotation: Plasmoid.configuration.reverseMode && Plasmoid.formFactor === PlasmaCore.Types.Vertical ? 180 : 0
0032 
0033     LayoutMirroring.enabled: (Qt.application.layoutDirection == Qt.RightToLeft)
0034     LayoutMirroring.childrenInherit: (Qt.application.layoutDirection == Qt.RightToLeft)
0035 
0036     required property var model
0037     required property int index
0038 
0039     readonly property int pid: model.AppPid
0040     readonly property string appName: model.AppName
0041     readonly property string appId: model.AppId.replace(/\.desktop/, '')
0042     property bool toolTipOpen: false
0043     property bool inPopup: false
0044     property bool isWindow: model.IsWindow
0045     property int childCount: model.ChildCount
0046     property int previousChildCount: 0
0047     property alias labelText: label.text
0048     property QtObject contextMenu: null
0049     readonly property bool smartLauncherEnabled: !inPopup && !model.IsStartup
0050     property QtObject smartLauncherItem: null
0051 
0052     property Item audioStreamIcon: null
0053     property var audioStreams: []
0054     property bool delayAudioStreamIndicator: false
0055     readonly property bool audioIndicatorsEnabled: Plasmoid.configuration.indicateAudioStreams
0056     readonly property bool hasAudioStream: audioStreams.length > 0
0057     readonly property bool playingAudio: hasAudioStream && audioStreams.some(function (item) {
0058         return !item.corked
0059     })
0060     readonly property bool muted: hasAudioStream && audioStreams.every(function (item) {
0061         return item.muted
0062     })
0063 
0064     readonly property bool highlighted: (inPopup && activeFocus) || (!inPopup && containsMouse)
0065         || (task.contextMenu && task.contextMenu.status === PlasmaExtras.Menu.Open)
0066         || (!!tasks.groupDialog && tasks.groupDialog.visualParent === task)
0067 
0068     active: (Plasmoid.configuration.showToolTips || tasks.toolTipOpenedByClick === task) && !inPopup && !tasks.groupDialog
0069     interactive: model.IsWindow || mainItem.playerData
0070     location: Plasmoid.location
0071     mainItem: model.IsWindow ? openWindowToolTipDelegate : pinnedAppToolTipDelegate
0072 
0073     Accessible.name: model.display
0074     Accessible.description: {
0075         if (!model.display) {
0076             return "";
0077         }
0078 
0079         if (model.IsLauncher) {
0080             return i18nc("@info:usagetip %1 application name", "Launch %1", model.display)
0081         }
0082 
0083         let smartLauncherDescription = "";
0084         if (iconBox.active) {
0085             smartLauncherDescription += i18ncp("@info:tooltip", "There is %1 new message.", "There are %1 new messages.", task.smartLauncherItem.count);
0086         }
0087 
0088         if (model.IsGroupParent) {
0089             switch (Plasmoid.configuration.groupedTaskVisualization) {
0090             case 0:
0091                 break; // Use the default description
0092             case 1: {
0093                 if (Plasmoid.configuration.showToolTips) {
0094                     return `${i18nc("@info:usagetip %1 task name", "Show Task tooltip for %1", model.display)}; ${smartLauncherDescription}`;
0095                 }
0096                 // fallthrough
0097             }
0098             case 2: {
0099                 if (backend.windowViewAvailable) {
0100                     return `${i18nc("@info:usagetip %1 task name", "Show windows side by side for %1", model.display)}; ${smartLauncherDescription}`;
0101                 }
0102                 // fallthrough
0103             }
0104             default:
0105                 return `${i18nc("@info:usagetip %1 task name", "Open textual list of windows for %1", model.display)}; ${smartLauncherDescription}`;
0106             }
0107         }
0108 
0109         return `${i18n("Activate %1", model.display)}; ${smartLauncherDescription}`;
0110     }
0111     Accessible.role: Accessible.Button
0112 
0113     onToolTipVisibleChanged: toolTipVisible => {
0114         task.toolTipOpen = toolTipVisible;
0115         if (!toolTipVisible) {
0116             tasks.toolTipOpenedByClick = null;
0117         } else {
0118             tasks.toolTipAreaItem = task;
0119         }
0120     }
0121 
0122     onContainsMouseChanged: if (containsMouse) {
0123         task.forceActiveFocus(Qt.MouseFocusReason);
0124         task.updateMainItemBindings();
0125     } else {
0126         tasks.toolTipOpenedByClick = null;
0127     }
0128 
0129     onHighlightedChanged: {
0130         // ensure it doesn't get stuck with a window highlighted
0131         backend.cancelHighlightWindows();
0132     }
0133 
0134     onPidChanged: updateAudioStreams({delay: false})
0135     onAppNameChanged: updateAudioStreams({delay: false})
0136 
0137     onIsWindowChanged: {
0138         if (model.IsWindow) {
0139             taskInitComponent.createObject(task);
0140             updateAudioStreams({delay: false});
0141         }
0142     }
0143 
0144     onChildCountChanged: {
0145         if (TaskTools.taskManagerInstanceCount < 2 && childCount > previousChildCount) {
0146             tasksModel.requestPublishDelegateGeometry(modelIndex(), backend.globalRect(task), task);
0147         }
0148 
0149         previousChildCount = childCount;
0150     }
0151 
0152     onIndexChanged: {
0153         hideToolTip();
0154 
0155         if (!inPopup && !tasks.vertical
0156             && (LayoutManager.calculateStripes() > 1 || !Plasmoid.configuration.separateLaunchers)) {
0157             tasks.requestLayout();
0158         }
0159     }
0160 
0161     onSmartLauncherEnabledChanged: {
0162         if (smartLauncherEnabled && !smartLauncherItem) {
0163             const smartLauncher = Qt.createQmlObject(`
0164                 import org.kde.plasma.private.taskmanager 0.1 as TaskManagerApplet
0165 
0166                 TaskManagerApplet.SmartLauncherItem { }
0167             `, task);
0168 
0169             smartLauncher.launcherUrl = Qt.binding(() => model.LauncherUrlWithoutIcon);
0170 
0171             smartLauncherItem = smartLauncher;
0172         }
0173     }
0174 
0175     onHasAudioStreamChanged: {
0176         const audioStreamIconActive = hasAudioStream && audioIndicatorsEnabled;
0177         if (!audioStreamIconActive) {
0178             if (audioStreamIcon !== null) {
0179                 audioStreamIcon.destroy();
0180                 audioStreamIcon = null;
0181             }
0182             return;
0183         }
0184         // Create item on demand instead of using Loader to reduce memory consumption,
0185         // because only a few applications have audio streams.
0186         const component = Qt.createComponent("AudioStream.qml");
0187         audioStreamIcon = component.createObject(task);
0188         component.destroy();
0189     }
0190     onAudioIndicatorsEnabledChanged: task.hasAudioStreamChanged()
0191 
0192     Keys.onMenuPressed: contextMenuTimer.start()
0193     Keys.onReturnPressed: TaskTools.activateTask(modelIndex(), model, event.modifiers, task, Plasmoid, tasks)
0194     Keys.onEnterPressed: Keys.returnPressed(event);
0195     Keys.onSpacePressed: Keys.returnPressed(event);
0196     Keys.onUpPressed: Keys.leftPressed(event)
0197     Keys.onDownPressed: Keys.rightPressed(event)
0198     Keys.onLeftPressed: if (!inPopup && (event.modifiers & Qt.ControlModifier) && (event.modifiers & Qt.ShiftModifier)) {
0199         tasksModel.move(task.index, task.index - 1);
0200     } else {
0201         event.accepted = false;
0202     }
0203     Keys.onRightPressed: if (!inPopup && (event.modifiers & Qt.ControlModifier) && (event.modifiers & Qt.ShiftModifier)) {
0204         tasksModel.move(task.index, task.index + 1);
0205     } else {
0206         event.accepted = false;
0207     }
0208 
0209     function modelIndex() {
0210         return (inPopup ? tasksModel.makeModelIndex(groupDialog.visualParent.index, index)
0211             : tasksModel.makeModelIndex(index));
0212     }
0213 
0214     function showContextMenu(args) {
0215         task.hideImmediately();
0216         contextMenu = tasks.createContextMenu(task, modelIndex(), args);
0217         contextMenu.show();
0218     }
0219 
0220     function updateAudioStreams(args) {
0221         if (args) {
0222             // When the task just appeared (e.g. virtual desktop switch), show the audio indicator
0223             // right away. Only when audio streams change during the lifetime of this task, delay
0224             // showing that to avoid distraction.
0225             delayAudioStreamIndicator = !!args.delay;
0226         }
0227 
0228         var pa = pulseAudio.item;
0229         if (!pa || !task.isWindow) {
0230             task.audioStreams = [];
0231             return;
0232         }
0233 
0234         // Check appid first for app using portal
0235         // https://docs.pipewire.org/page_portal.html
0236         var streams = pa.streamsForAppId(task.appId);
0237         if (!streams.length) {
0238             streams = pa.streamsForPid(model.AppPid);
0239             if (streams.length) {
0240                 pa.registerPidMatch(model.AppName);
0241             } else {
0242                 // We only want to fall back to appName matching if we never managed to map
0243                 // a PID to an audio stream window. Otherwise if you have two instances of
0244                 // an application, one playing and the other not, it will look up appName
0245                 // for the non-playing instance and erroneously show an indicator on both.
0246                 if (!pa.hasPidMatch(model.AppName)) {
0247                     streams = pa.streamsForAppName(model.AppName);
0248                 }
0249             }
0250         }
0251 
0252         task.audioStreams = streams;
0253     }
0254 
0255     function toggleMuted() {
0256         if (muted) {
0257             task.audioStreams.forEach(function (item) { item.unmute(); });
0258         } else {
0259             task.audioStreams.forEach(function (item) { item.mute(); });
0260         }
0261     }
0262 
0263     // Will also be called in activateTaskAtIndex(index)
0264     function updateMainItemBindings() {
0265         if ((mainItem.parentTask === task && mainItem.rootIndex.row === task.index) || (tasks.toolTipOpenedByClick === null && !task.active) || (tasks.toolTipOpenedByClick !== null && tasks.toolTipOpenedByClick !== task)) {
0266             return;
0267         }
0268 
0269         mainItem.blockingUpdates = (mainItem.isGroup !== model.IsGroupParent); // BUG 464597 Force unload the previous component
0270 
0271         mainItem.parentTask = task;
0272         mainItem.rootIndex = tasksModel.makeModelIndex(index, -1);
0273 
0274         mainItem.appName = Qt.binding(() => model.AppName);
0275         mainItem.pidParent = Qt.binding(() => model.AppPid);
0276         mainItem.windows = Qt.binding(() => model.WinIdList);
0277         mainItem.isGroup = Qt.binding(() => model.IsGroupParent);
0278         mainItem.icon = Qt.binding(() => model.decoration);
0279         mainItem.launcherUrl = Qt.binding(() => model.LauncherUrlWithoutIcon);
0280         mainItem.isLauncher = Qt.binding(() => model.IsLauncher);
0281         mainItem.isMinimizedParent = Qt.binding(() => model.IsMinimized);
0282         mainItem.displayParent = Qt.binding(() => model.display);
0283         mainItem.genericName = Qt.binding(() => model.GenericName);
0284         mainItem.virtualDesktopParent = Qt.binding(() => model.VirtualDesktops);
0285         mainItem.isOnAllVirtualDesktopsParent = Qt.binding(() => model.IsOnAllVirtualDesktops);
0286         mainItem.activitiesParent = Qt.binding(() => model.Activities);
0287 
0288         mainItem.smartLauncherCountVisible = Qt.binding(() => task.smartLauncherItem && task.smartLauncherItem.countVisible);
0289         mainItem.smartLauncherCount = Qt.binding(() => mainItem.smartLauncherCountVisible ? task.smartLauncherItem.count : 0);
0290 
0291         mainItem.blockingUpdates = false;
0292         tasks.toolTipAreaItem = task;
0293     }
0294 
0295     Connections {
0296         target: pulseAudio.item
0297         ignoreUnknownSignals: true // Plasma-PA might not be available
0298         function onStreamsChanged() {
0299             task.updateAudioStreams({delay: true})
0300         }
0301     }
0302 
0303     TapHandler {
0304         id: menuTapHandler
0305         acceptedButtons: Qt.LeftButton
0306         acceptedDevices: PointerDevice.TouchScreen | PointerDevice.Stylus
0307         onLongPressed: {
0308             // When we're a launcher, there's no window controls, so we can show all
0309             // places without the menu getting super huge.
0310             if (model.IsLauncher) {
0311                 showContextMenu({showAllPlaces: true})
0312             } else {
0313                 showContextMenu();
0314             }
0315         }
0316     }
0317 
0318     TapHandler {
0319         acceptedButtons: Qt.RightButton
0320         acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
0321         gesturePolicy: TapHandler.WithinBounds // Release grab when menu appears
0322         onPressedChanged: if (pressed) contextMenuTimer.start()
0323     }
0324 
0325     Timer {
0326         id: contextMenuTimer
0327         interval: 0
0328         onTriggered: menuTapHandler.longPressed()
0329     }
0330 
0331     TapHandler {
0332         acceptedButtons: Qt.LeftButton
0333         onTapped: {
0334             if (Plasmoid.configuration.showToolTips && task.active) {
0335                 hideToolTip();
0336             }
0337             TaskTools.activateTask(modelIndex(), model, point.modifiers, task, Plasmoid, tasks);
0338         }
0339     }
0340 
0341     TapHandler {
0342         acceptedButtons: Qt.MiddleButton | Qt.BackButton | Qt.ForwardButton
0343         onTapped: (eventPoint, button) => {
0344             if (button === Qt.MiddleButton) {
0345                 if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.NewInstance) {
0346                     tasksModel.requestNewInstance(modelIndex());
0347                 } else if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.Close) {
0348                     tasks.taskClosedWithMouseMiddleButton = model.WinIdList.slice()
0349                     tasksModel.requestClose(modelIndex());
0350                 } else if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.ToggleMinimized) {
0351                     tasksModel.requestToggleMinimized(modelIndex());
0352                 } else if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.ToggleGrouping) {
0353                     tasksModel.requestToggleGrouping(modelIndex());
0354                 } else if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.BringToCurrentDesktop) {
0355                     tasksModel.requestVirtualDesktops(modelIndex(), [virtualDesktopInfo.currentDesktop]);
0356                 }
0357             } else if (button === Qt.BackButton || button === Qt.ForwardButton) {
0358                 const playerData = mpris2Source.playerForLauncherUrl(model.LauncherUrlWithoutIcon, model.AppPid);
0359                 if (playerData) {
0360                     if (button === Qt.BackButton) {
0361                         playerData.Previous();
0362                     } else {
0363                         playerData.Next();
0364                     }
0365                 } else {
0366                     eventPoint.accepted = false;
0367                 }
0368             }
0369 
0370             backend.cancelHighlightWindows();
0371         }
0372     }
0373 
0374     KSvg.FrameSvgItem {
0375         id: frame
0376 
0377         anchors {
0378             fill: parent
0379 
0380             topMargin: (!tasks.vertical && taskList.rows > 1) ? LayoutManager.iconMargin : 0
0381             bottomMargin: (!tasks.vertical && taskList.rows > 1) ? LayoutManager.iconMargin : 0
0382             leftMargin: ((inPopup || tasks.vertical) && taskList.columns > 1) ? LayoutManager.iconMargin : 0
0383             rightMargin: ((inPopup || tasks.vertical) && taskList.columns > 1) ? LayoutManager.iconMargin : 0
0384         }
0385 
0386         imagePath: "widgets/tasks"
0387         property bool isHovered: task.highlighted && Plasmoid.configuration.taskHoverEffect
0388         property string basePrefix: "normal"
0389         prefix: isHovered ? TaskTools.taskPrefixHovered(basePrefix, Plasmoid.location) : TaskTools.taskPrefix(basePrefix, Plasmoid.location)
0390 
0391         // Avoid repositioning delegate item after dragFinished
0392         DragHandler {
0393             id: dragHandler
0394             grabPermissions: PointerHandler.TakeOverForbidden
0395 
0396             function setRequestedInhibitDnd(value) {
0397                 // This is modifying the value in the panel containment that
0398                 // inhibits accepting drag and drop, so that we don't accidentally
0399                 // drop the task on this panel.
0400                 let item = this;
0401                 while (item.parent) {
0402                     item = item.parent;
0403                     if (item.appletRequestsInhibitDnD !== undefined) {
0404                         item.appletRequestsInhibitDnD = value
0405                     }
0406                 }
0407             }
0408 
0409             onActiveChanged: if (active) {
0410                 icon.grabToImage((result) => {
0411                     if (!dragHandler.active) {
0412                         // BUG 466675 grabToImage is async, so avoid updating dragSource when active is false
0413                         return;
0414                     }
0415                     setRequestedInhibitDnd(true);
0416                     tasks.dragSource = task;
0417                     dragHelper.Drag.imageSource = result.url;
0418                     dragHelper.Drag.mimeData = {
0419                         "text/x-orgkdeplasmataskmanager_taskurl": backend.tryDecodeApplicationsUrl(model.LauncherUrlWithoutIcon).toString(),
0420                         [model.MimeType]: model.MimeData,
0421                         "application/x-orgkdeplasmataskmanager_taskbuttonitem": model.MimeData,
0422                     };
0423                     dragHelper.Drag.active = dragHandler.active;
0424                 });
0425             } else {
0426                 setRequestedInhibitDnd(false);
0427                 dragHelper.Drag.active = false;
0428                 dragHelper.Drag.imageSource = "";
0429             }
0430         }
0431     }
0432 
0433     Loader {
0434         id: taskProgressOverlayLoader
0435 
0436         anchors.fill: frame
0437         asynchronous: true
0438         active: model.IsWindow && task.smartLauncherItem && task.smartLauncherItem.progressVisible
0439 
0440         source: "TaskProgressOverlay.qml"
0441     }
0442 
0443     Loader {
0444         id: iconBox
0445 
0446         anchors {
0447             left: parent.left
0448             leftMargin: adjustMargin(true, parent.width, taskFrame.margins.left)
0449             top: parent.top
0450             topMargin: adjustMargin(false, parent.height, taskFrame.margins.top)
0451         }
0452 
0453         width: height
0454         height: (parent.height - adjustMargin(false, parent.height, taskFrame.margins.top)
0455             - adjustMargin(false, parent.height, taskFrame.margins.bottom))
0456 
0457         asynchronous: true
0458         active: height >= Kirigami.Units.iconSizes.small
0459                 && task.smartLauncherItem && task.smartLauncherItem.countVisible
0460         source: "TaskBadgeOverlay.qml"
0461 
0462         function adjustMargin(vert, size, margin) {
0463             if (!size) {
0464                 return margin;
0465             }
0466 
0467             var margins = vert ? LayoutManager.horizontalMargins() : LayoutManager.verticalMargins();
0468 
0469             if ((size - margins) < Kirigami.Units.iconSizes.small) {
0470                 return Math.ceil((margin * (Kirigami.Units.iconSizes.small / size)) / 2);
0471             }
0472 
0473             return margin;
0474         }
0475 
0476         Kirigami.Icon {
0477             id: icon
0478 
0479             anchors.fill: parent
0480 
0481             active: task.highlighted
0482             enabled: true
0483 
0484             source: model.decoration
0485         }
0486 
0487         states: [
0488             // Using a state transition avoids a binding loop between label.visible and
0489             // the text label margin, which derives from the icon width.
0490             State {
0491                 name: "standalone"
0492                 when: !label.visible
0493 
0494                 AnchorChanges {
0495                     target: iconBox
0496                     anchors.left: undefined
0497                     anchors.horizontalCenter: parent.horizontalCenter
0498                 }
0499 
0500                 PropertyChanges {
0501                     target: iconBox
0502                     anchors.leftMargin: 0
0503                     width: parent.width - adjustMargin(true, task.width, taskFrame.margins.left)
0504                                         - adjustMargin(true, task.width, taskFrame.margins.right)
0505                 }
0506             }
0507         ]
0508 
0509         Loader {
0510             anchors.centerIn: parent
0511             width: Math.min(parent.width, parent.height)
0512             height: width
0513             active: model.IsStartup
0514             sourceComponent: busyIndicator
0515         }
0516     }
0517 
0518     PlasmaComponents3.Label {
0519         id: label
0520 
0521         visible: (inPopup || !iconsOnly && !model.IsLauncher
0522             && (parent.width - iconBox.height - Kirigami.Units.smallSpacing) >= (Kirigami.Units.iconSizes.sizeForLabels * LayoutManager.minimumMColumns()))
0523 
0524         anchors {
0525             fill: parent
0526             leftMargin: taskFrame.margins.left + iconBox.width + LayoutManager.labelMargin
0527             topMargin: taskFrame.margins.top
0528             rightMargin: taskFrame.margins.right + (audioStreamIcon !== null && audioStreamIcon.visible ? (audioStreamIcon.width + LayoutManager.labelMargin) : 0)
0529             bottomMargin: taskFrame.margins.bottom
0530         }
0531 
0532         wrapMode: (maximumLineCount == 1) ? Text.NoWrap : Text.Wrap
0533         elide: Text.ElideRight
0534         textFormat: Text.PlainText
0535         verticalAlignment: Text.AlignVCenter
0536         maximumLineCount: Plasmoid.configuration.maxTextLines || undefined
0537 
0538         // use State to avoid unnecessary re-evaluation when the label is invisible
0539         states: State {
0540             name: "labelVisible"
0541             when: label.visible
0542 
0543             PropertyChanges {
0544                 target: label
0545                 text: model.display
0546             }
0547         }
0548     }
0549 
0550     states: [
0551         State {
0552             name: "launcher"
0553             when: model.IsLauncher
0554 
0555             PropertyChanges {
0556                 target: frame
0557                 basePrefix: ""
0558             }
0559         },
0560         State {
0561             name: "attention"
0562             when: model.IsDemandingAttention || (task.smartLauncherItem && task.smartLauncherItem.urgent)
0563 
0564             PropertyChanges {
0565                 target: frame
0566                 basePrefix: "attention"
0567             }
0568         },
0569         State {
0570             name: "minimized"
0571             when: model.IsMinimized
0572 
0573             PropertyChanges {
0574                 target: frame
0575                 basePrefix: "minimized"
0576             }
0577         },
0578         State {
0579             name: "active"
0580             when: model.IsActive
0581 
0582             PropertyChanges {
0583                 target: frame
0584                 basePrefix: "focus"
0585             }
0586         }
0587     ]
0588 
0589     Component.onCompleted: {
0590         if (!inPopup && model.IsWindow) {
0591             var component = Qt.createComponent("GroupExpanderOverlay.qml");
0592             component.createObject(task);
0593             component.destroy();
0594             updateAudioStreams({delay: false});
0595         }
0596 
0597         if (!inPopup && !model.IsWindow) {
0598             taskInitComponent.createObject(task);
0599         }
0600     }
0601 }