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 }