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 }