/*SPDX-FileCopyrightText: 2012-2013 Eike Hein <hein@kde.org>SPDX-FileCopyrightText: 2024 Nate Graham <nate@kde.org>SPDX-License-Identifier: GPL-2.0-or-later*/pragma ComponentBehavior: Boundimport QtQuickimport QtQuick.Layoutsimport org.kde.plasma.core as PlasmaCoreimport org.kde.ksvg as KSvgimport org.kde.plasma.extras as PlasmaExtrasimport org.kde.plasma.components as PlasmaComponents3import org.kde.kirigami as Kirigamiimport plasma.applet.org.kde.plasma.taskmanager as TaskManagerAppletimport org.kde.plasma.plasmoidimport org.kde.taskmanager as TaskManagerPlasmaCore.ToolTipArea {id: taskactiveFocusOnTab: true// To achieve a bottom-to-top layout on vertical panels, the task manager// is rotated by 180 degrees(see main.qml). This makes the tasks rotated,// so un-rotate them here to fix that.rotation: Plasmoid.configuration.reverseMode && Plasmoid.formFactor === PlasmaCore.Types.Vertical ? 180 : 0implicitHeight: inPopup? TaskManagerApplet.LayoutMetrics.preferredHeightInPopup(): (tasksRoot.vertical? TaskManagerApplet.LayoutMetrics.preferredMinHeight(): Math.max(tasksRoot.height / Plasmoid.configuration.maxStripes,TaskManagerApplet.LayoutMetrics.preferredMinHeight()))implicitWidth: tasksRoot.vertical? Math.max(TaskManagerApplet.LayoutMetrics.preferredMinWidth(), Math.min(TaskManagerApplet.LayoutMetrics.preferredMaxWidth(), tasksRoot.width / Plasmoid.configuration.maxStripes)): 0Layout.fillWidth: trueLayout.fillHeight: !inPopupLayout.maximumWidth: tasksRoot.vertical? -1: ((model.IsLauncher && !tasksRoot.iconsOnly) ? tasksRoot.height / taskList.rows : TaskManagerApplet.LayoutMetrics.preferredMaxWidth())Layout.maximumHeight: tasksRoot.vertical ? TaskManagerApplet.LayoutMetrics.preferredMaxHeight() : -1required property var modelrequired property int indexrequired property /*main.qml*/ Item tasksRootreadonly property int pid: model.AppPidreadonly property string appName: model.AppNamereadonly property string appId: model.AppIdreadonly property bool isIcon: tasksRoot.iconsOnly || model.IsLauncherproperty bool toolTipOpen: falseproperty bool inPopup: falseproperty bool isWindow: model.IsWindowproperty int childCount: model.ChildCountproperty int previousChildCount: 0property alias labelText: label.textproperty QtObject contextMenu: nullreadonly property bool smartLauncherEnabled: !inPopupproperty QtObject smartLauncherItem: nullproperty Item audioStreamIcon: nullproperty var audioStreams: []property bool delayAudioStreamIndicator: falseproperty bool completed: falsereadonly property bool audioIndicatorsEnabled: Plasmoid.configuration.indicateAudioStreamsreadonly property bool tooltipControlsEnabled: Plasmoid.configuration.tooltipControlsreadonly property bool hasAudioStream: audioStreams.length > 0readonly property bool playingAudio: hasAudioStream && audioStreams.some(item => !item.corked)readonly property bool muted: hasAudioStream && audioStreams.every(item => item.muted)readonly property bool highlighted: (inPopup && activeFocus) || (!inPopup && containsMouse)|| (task.contextMenu && task.contextMenu.status === PlasmaExtras.Menu.Open)|| (!!tasksRoot.groupDialog && tasksRoot.groupDialog.visualParent === task)active: !inPopup && !tasksRoot.groupDialog && task.contextMenu?.status !== PlasmaExtras.Menu.Open && task === (tasksRoot.toolTipOpenedByClick ?? task)interactive: model.IsWindow || mainItem.playerDatahideOnClick: !(hasAudioStream && audioIndicatorsEnabled)location: Plasmoid.locationmainItem: !Plasmoid.configuration.showToolTips || !model.IsWindow ? pinnedAppToolTipDelegate : openWindowToolTipDelegateonXChanged: {if (!completed) {return;}if (oldX < 0) {oldX = x;return;}moveAnim.x = oldX - x + translateTransform.x;moveAnim.y = translateTransform.y;oldX = x;moveAnim.restart();}onYChanged: {if (!completed) {return;}if (oldY < 0) {oldY = y;return;}moveAnim.y = oldY - y + translateTransform.y;moveAnim.x = translateTransform.x;oldY = y;moveAnim.restart();}property real oldX: -1property real oldY: -1SequentialAnimation {id: moveAnimproperty real xproperty real yonRunningChanged: {if (running) {++task.parent.animationsRunning;} else {--task.parent.animationsRunning;}}ParallelAnimation {NumberAnimation {target: translateTransformproperties: "x"from: moveAnim.xto: 0easing.type: Easing.OutQuadduration: Kirigami.Units.longDuration}NumberAnimation {target: translateTransformproperties: "y"from: moveAnim.yto: 0easing.type: Easing.OutQuadduration: Kirigami.Units.longDuration}}}transform: Translate {id: translateTransform}Accessible.name: model.displayAccessible.description: {if (!model.display) {return "";}if (model.IsLauncher) {return i18nc("@info:usagetip %1 application name", "Launch %1", model.display)}let smartLauncherDescription = "";if (iconBox.active) {smartLauncherDescription += i18ncp("@info:tooltip", "There is %1 new message.", "There are %1 new messages.", task.smartLauncherItem.count);}if (model.IsGroupParent) {switch (Plasmoid.configuration.groupedTaskVisualization) {case 0:break; // Use the default descriptioncase 1: {return `${i18nc("@info:usagetip %1 task name", "Show Task tooltip for %1", model.display)}; ${smartLauncherDescription}`;}case 2: {if (effectWatcher.registered) {return `${i18nc("@info:usagetip %1 task name", "Show windows side by side for %1", model.display)}; ${smartLauncherDescription}`;}// fallthrough}default:return `${i18nc("@info:usagetip %1 task name", "Open textual list of windows for %1", model.display)}; ${smartLauncherDescription}`;}}return `${i18nc("@info:usagetip %1 task name", "Activate %1", model.display)}; ${smartLauncherDescription}`;}Accessible.role: Accessible.ButtonAccessible.onPressAction: leftTapHandler.leftClick()onToolTipVisibleChanged: toolTipVisible => {task.toolTipOpen = toolTipVisible;if (!toolTipVisible) {tasksRoot.toolTipOpenedByClick = null;} else {tasksRoot.toolTipAreaItem = task;}}onContainsMouseChanged: {if (tasksRoot.toolTipOpenedByClick && tasksRoot.toolTipOpenedByClick === mainItem.parentTask) {tasksRoot.toolTipOpenedByClick.showToolTip();} else if (containsMouse) {task.forceActiveFocus(Qt.MouseFocusReason);task.updateMainItemBindings();}}onHighlightedChanged: {// ensure it doesn't get stuck with a window highlightedtasksRoot.cancelHighlightWindows();}onPidChanged: updateAudioStreams({delay: false})onAppNameChanged: updateAudioStreams({delay: false})onIsWindowChanged: {if (model.IsWindow) {taskInitComponent.createObject(task);updateAudioStreams({delay: false});}}onChildCountChanged: {if (TaskManagerApplet.TaskTools.taskManagerInstanceCount < 2 && childCount > previousChildCount) {tasksModel.requestPublishDelegateGeometry(modelIndex(), backend.globalRect(task), task);}previousChildCount = childCount;}onIndexChanged: {hideToolTip();if (!inPopup && !tasksRoot.vertical&& !Plasmoid.configuration.separateLaunchers) {tasksRoot.requestLayout();}}onSmartLauncherEnabledChanged: {if (smartLauncherEnabled && !smartLauncherItem) {const component = Qt.createComponent("plasma.applet.org.kde.plasma.taskmanager", "SmartLauncherItem");const smartLauncher = component.createObject(task);component.destroy();smartLauncher.launcherUrl = Qt.binding(() => model.LauncherUrlWithoutIcon);smartLauncherItem = smartLauncher;}}onHasAudioStreamChanged: {const audioStreamIconActive = hasAudioStream && audioIndicatorsEnabled;if (!audioStreamIconActive) {if (audioStreamIcon !== null) {audioStreamIcon.destroy();audioStreamIcon = null;}return;}// Create item on demand instead of using Loader to reduce memory consumption,// because only a few applications have audio streams.const component = Qt.createComponent("AudioStream.qml");audioStreamIcon = component.createObject(task);component.destroy();}onAudioIndicatorsEnabledChanged: task.hasAudioStreamChanged()Keys.onMenuPressed: event => contextMenuTimer.start()Keys.onReturnPressed: event => TaskManagerApplet.TaskTools.activateTask(modelIndex(), model, event.modifiers, task, Plasmoid, tasksRoot, effectWatcher.registered)Keys.onEnterPressed: event => Keys.returnPressed(event);Keys.onSpacePressed: event => Keys.returnPressed(event);Keys.onUpPressed: event => Keys.leftPressed(event)Keys.onDownPressed: event => Keys.rightPressed(event)Keys.onLeftPressed: event => {if (!inPopup && (event.modifiers & Qt.ControlModifier) && (event.modifiers & Qt.ShiftModifier)) {tasksModel.move(task.index, task.index - 1);} else {event.accepted = false;}}Keys.onRightPressed: event => {if (!inPopup && (event.modifiers & Qt.ControlModifier) && (event.modifiers & Qt.ShiftModifier)) {tasksModel.move(task.index, task.index + 1);} else {event.accepted = false;}}function modelIndex(): /*QModelIndex*/ var {return inPopup? tasksModel.makeModelIndex(groupDialog.visualParent.index, index): tasksModel.makeModelIndex(index);}function showContextMenu(args: var): void {task.hideImmediately();contextMenu = tasksRoot.createContextMenu(task, modelIndex(), args) as ContextMenu;contextMenu.show();}function updateAudioStreams(args: var): void {if (args) {// When the task just appeared (e.g. virtual desktop switch), show the audio indicator// right away. Only when audio streams change during the lifetime of this task, delay// showing that to avoid distraction.delayAudioStreamIndicator = !!args.delay;}var pa = pulseAudio.item;if (!pa || !task.isWindow) {task.audioStreams = [];return;}var streams = [];// Check appid first for app using portal// https://docs.pipewire.org/page_portal.htmlif (task.appId) {streams = pa.streamsForAppId(task.appId);}if (!streams.length) {streams = pa.streamsForPid(model.AppPid);if (streams.length) {pa.registerPidMatch(model.AppName);} else {// We only want to fall back to appName matching if we never managed to map// a PID to an audio stream window. Otherwise if you have two instances of// an application, one playing and the other not, it will look up appName// for the non-playing instance and erroneously show an indicator on both.if (!pa.hasPidMatch(model.AppName)) {streams = pa.streamsForAppName(model.AppName);}}}task.audioStreams = streams;}function toggleMuted(): void {if (muted) {task.audioStreams.forEach(item => item.unmute());} else {task.audioStreams.forEach(item => item.mute());}}// Will also be called in activateTaskAtIndex(index)function updateMainItemBindings(): void {if ((mainItem.parentTask === this && mainItem.rootIndex.row === index)|| (tasksRoot.toolTipOpenedByClick === null && !active)|| (tasksRoot.toolTipOpenedByClick !== null && tasksRoot.toolTipOpenedByClick !== this)) {return;}mainItem.blockingUpdates = (mainItem.isGroup !== model.IsGroupParent); // BUG 464597 Force unload the previous componentmainItem.parentTask = this;mainItem.rootIndex = tasksModel.makeModelIndex(index, -1);mainItem.appName = Qt.binding(() => model.AppName);mainItem.pidParent = Qt.binding(() => model.AppPid);mainItem.windows = Qt.binding(() => model.WinIdList);mainItem.isGroup = Qt.binding(() => model.IsGroupParent);mainItem.icon = Qt.binding(() => model.decoration);mainItem.launcherUrl = Qt.binding(() => model.LauncherUrlWithoutIcon);mainItem.isLauncher = Qt.binding(() => model.IsLauncher);mainItem.isMinimized = Qt.binding(() => model.IsMinimized);mainItem.display = Qt.binding(() => model.display);mainItem.genericName = Qt.binding(() => model.GenericName);mainItem.virtualDesktops = Qt.binding(() => model.VirtualDesktops);mainItem.isOnAllVirtualDesktops = Qt.binding(() => model.IsOnAllVirtualDesktops);mainItem.activities = Qt.binding(() => model.Activities);mainItem.isReadyForPainting = Qt.binding(() => model.IsMapped);mainItem.isExcludedFromCapture = Qt.binding(() => model.IsExcludedFromCapture);mainItem.smartLauncherCountVisible = Qt.binding(() => smartLauncherItem?.countVisible ?? false);mainItem.smartLauncherCount = Qt.binding(() => mainItem.smartLauncherCountVisible ? (smartLauncherItem?.count ?? 0) : 0);mainItem.blockingUpdates = false;tasksRoot.toolTipAreaItem = this;}Connections {target: pulseAudio.itemignoreUnknownSignals: true // Plasma-PA might not be availablefunction onStreamsChanged(): void {task.updateAudioStreams({delay: true})}}TapHandler {id: menuTapHandleracceptedButtons: Qt.LeftButtonacceptedDevices: PointerDevice.TouchScreen | PointerDevice.StylusgesturePolicy: TapHandler.ReleaseWithinBoundsonLongPressed: {// When we're a launcher, there's no window controls, so we can show all// places without the menu getting super huge.if (task.model.IsLauncher) {task.showContextMenu({showAllPlaces: true})} else {task.showContextMenu();}}}TapHandler {acceptedButtons: Qt.RightButtonacceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.StylusonPressedChanged: if (pressed) contextMenuTimer.start()}Timer {id: contextMenuTimerinterval: 0onTriggered: menuTapHandler.longPressed()}TapHandler {id: leftTapHandleracceptedButtons: Qt.LeftButtononTapped: (eventPoint, button) => leftClick()function leftClick(): void {if (task.active) {task.hideToolTip();}TaskManagerApplet.TaskTools.activateTask(modelIndex(), model, point.modifiers, task, Plasmoid, tasksRoot, effectWatcher.registered);}}TapHandler {acceptedButtons: Qt.MiddleButton | Qt.BackButton | Qt.ForwardButtononTapped: (eventPoint, button) => {if (button === Qt.MiddleButton) {if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.NewInstance) {tasksModel.requestNewInstance(modelIndex());} else if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.Close) {tasksModel.requestClose(modelIndex());} else if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.ToggleMinimized) {tasksModel.requestToggleMinimized(modelIndex());} else if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.ToggleGrouping) {tasksModel.requestToggleGrouping(modelIndex());} else if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.BringToCurrentDesktop) {TaskManagerApplet.TaskTools.foreachChildTask((childIndex) => {tasksModel.requestVirtualDesktops(childIndex, [virtualDesktopInfo.currentDesktopByScreenGeometry(tasksModel.data(childIndex, TaskManager.AbstractTasksModel.ScreenGeometry))]);}, modelIndex(), tasksModel);}} else if (button === Qt.BackButton || button === Qt.ForwardButton) {const playerData = mpris2Source.playerForLauncherUrl(task.model.LauncherUrlWithoutIcon, task.model.AppPid);if (playerData) {if (button === Qt.BackButton) {playerData.Previous();} else {playerData.Next();}} else {eventPoint.accepted = false;}}task.tasksRoot.cancelHighlightWindows();}}KSvg.FrameSvgItem {id: frameanchors {fill: parenttopMargin: (!task.tasksRoot.vertical && taskList.rows > 1) ? TaskManagerApplet.LayoutMetrics.iconMargin : 0bottomMargin: (!task.tasksRoot.vertical && taskList.rows > 1) ? TaskManagerApplet.LayoutMetrics.iconMargin : 0leftMargin: ((task.inPopup || task.tasksRoot.vertical) && taskList.columns > 1) ? TaskManagerApplet.LayoutMetrics.iconMargin : 0rightMargin: ((task.inPopup || task.tasksRoot.vertical) && taskList.columns > 1) ? TaskManagerApplet.LayoutMetrics.iconMargin : 0}imagePath: "widgets/tasks"property bool isHovered: task.highlighted && Plasmoid.configuration.taskHoverEffectproperty string basePrefix: "normal"prefix: isHovered ? TaskManagerApplet.TaskTools.taskPrefixHovered(basePrefix, Plasmoid.location) : TaskManagerApplet.TaskTools.taskPrefix(basePrefix, Plasmoid.location)// Avoid repositioning delegate item after dragFinishedDragHandler {id: dragHandlergrabPermissions: PointerHandler.CanTakeOverFromHandlersOfDifferentTypefunction setRequestedInhibitDnd(value: bool): void {// This is modifying the value in the panel containment that// inhibits accepting drag and drop, so that we don't accidentally// drop the task on this panel.let item = this;while (item.parent) {item = item.parent;if (item.appletRequestsInhibitDnD !== undefined) {item.appletRequestsInhibitDnD = value}}}onActiveChanged: {if (active) {icon.grabToImage(result => {if (!dragHandler.active) {// BUG 466675 grabToImage is async, so avoid updating dragSource when active is falsereturn;}setRequestedInhibitDnd(true);tasksRoot.dragSource = task;dragHelper.Drag.imageSource = result.url;dragHelper.Drag.mimeData = {"text/x-orgkdeplasmataskmanager_taskurl": backend.tryDecodeApplicationsUrl(model.LauncherUrlWithoutIcon).toString(),[model.MimeType]: model.MimeData,"application/x-orgkdeplasmataskmanager_taskbuttonitem": model.MimeData,};dragHelper.Drag.active = dragHandler.active;});} else {setRequestedInhibitDnd(false);dragHelper.Drag.active = false;dragHelper.Drag.imageSource = "";}}}}Loader {id: taskProgressOverlayLoaderanchors.fill: frameasynchronous: trueactive: task.smartLauncherItem && task.smartLauncherItem.progressVisiblesource: "TaskProgressOverlay.qml"}Loader {id: iconBoxanchors {left: parent.leftleftMargin: adjustMargin(true, parent.width, taskFrame.margins.left)top: parent.toptopMargin: adjustMargin(false, parent.height, taskFrame.margins.top)}width: task.inPopup ? Math.max(Kirigami.Units.iconSizes.sizeForLabels, Kirigami.Units.iconSizes.medium) : Math.min(task.parent?.minimumWidth ?? 0, task.height)height: task.inPopup ? width : (parent.height - adjustMargin(false, parent.height, taskFrame.margins.top)- adjustMargin(false, parent.height, taskFrame.margins.bottom))asynchronous: trueactive: height >= Kirigami.Units.iconSizes.small&& task.smartLauncherItem && task.smartLauncherItem.countVisiblesource: "TaskBadgeOverlay.qml"function adjustMargin(isVertical: bool, size: real, margin: real): real {if (!size) {return margin;}var margins = isVertical ? TaskManagerApplet.LayoutMetrics.horizontalMargins() : TaskManagerApplet.LayoutMetrics.verticalMargins();if ((size - margins) < Kirigami.Units.iconSizes.small) {return Math.ceil((margin * (Kirigami.Units.iconSizes.small / size)) / 2);}return margin;}Kirigami.Icon {id: iconanchors.fill: parentactive: task.highlightedenabled: truesource: task.model.decoration}states: [// Using a state transition avoids a binding loop between label.visible and// the text label margin, which derives from the icon width.State {name: "standalone"when: !label.visible && task.parentAnchorChanges {target: iconBoxanchors.left: undefinedanchors.horizontalCenter: parent.horizontalCenter}PropertyChanges {iconBox.anchors.leftMargin: 0iconBox.width: Math.min(task.parent.minimumWidth, tasksRoot.height)- iconBox.adjustMargin(true, task.width, taskFrame.margins.left)- iconBox.adjustMargin(true, task.width, taskFrame.margins.right)}}]Loader {anchors.centerIn: parentwidth: Math.min(parent.width, parent.height)height: widthactive: task.model.IsStartupsourceComponent: busyIndicator}}PlasmaComponents3.Label {id: labelvisible: (task.inPopup || !task.tasksRoot.iconsOnly && !task.model.IsLauncher&& (parent.width - iconBox.height - Kirigami.Units.smallSpacing) >= TaskManagerApplet.LayoutMetrics.spaceRequiredToShowText())anchors {fill: parentleftMargin: taskFrame.margins.left + iconBox.width + TaskManagerApplet.LayoutMetrics.labelMargintopMargin: taskFrame.margins.toprightMargin: taskFrame.margins.right + (task.audioStreamIcon !== null && task.audioStreamIcon.visible ? (task.audioStreamIcon.width + TaskManagerApplet.LayoutMetrics.labelMargin) : 0)bottomMargin: taskFrame.margins.bottom}wrapMode: (maximumLineCount === 1) ? Text.NoWrap : Text.WrapAnywhereelide: Text.ElideRighttextFormat: Text.PlainTextverticalAlignment: Text.AlignVCentermaximumLineCount: Plasmoid.configuration.maxTextLines || undefined// The accessible item of this element is only used for debugging// purposes, and it will never gain focus (thus it won't interfere// with screenreaders).Accessible.ignored: !visibleAccessible.name: parent.Accessible.name + "-labelhint"// use State to avoid unnecessary re-evaluation when the label is invisiblestates: State {name: "labelVisible"when: label.visiblePropertyChanges {label.text: task.model.display}}}states: [State {name: "launcher"when: task.model.IsLauncherPropertyChanges {frame.basePrefix: ""}},State {name: "attention"when: task.model.IsDemandingAttention || (task.smartLauncherItem && task.smartLauncherItem.urgent)PropertyChanges {frame.basePrefix: "attention"}},State {name: "minimized"when: task.model.IsMinimizedPropertyChanges {frame.basePrefix: "minimized"}},State {name: "active"when: task.model.IsActivePropertyChanges {frame.basePrefix: "focus"}}]Component.onCompleted: {if (!inPopup && model.IsWindow) {const component = Qt.createComponent("GroupExpanderOverlay.qml");component.createObject(task);component.destroy();updateAudioStreams({delay: false});}if (!inPopup && !model.IsWindow) {taskInitComponent.createObject(task);}completed = true;}Component.onDestruction: {if (moveAnim.running) {(task.parent as TaskList).animationsRunning -= 1;}}}