/* SPDX-FileCopyrightText: 2012-2016 Eike Hein SPDX-License-Identifier: GPL-2.0-or-later */ import QtQuick import QtQuick.Layouts import org.kde.plasma.plasmoid import org.kde.plasma.components as PlasmaComponents3 import org.kde.plasma.core as PlasmaCore import org.kde.ksvg as KSvg import org.kde.plasma.private.mpris as Mpris import org.kde.kirigami as Kirigami import org.kde.plasma.workspace.trianglemousefilter import org.kde.taskmanager as TaskManager import org.kde.plasma.private.taskmanager as TaskManagerApplet import org.kde.plasma.workspace.dbus as DBus import "code/layoutmetrics.js" as LayoutMetrics import "code/tools.js" as TaskTools PlasmoidItem { id: tasks // Prevent clipping of zoomed icons clip: false // For making a bottom to top layout since qml flow can't do that. // We just hang the task manager upside down to achieve that. // This mirrors the tasks and group dialog as well, so we un-rotate them // to fix that (see Task.qml and GroupDialog.qml). rotation: Plasmoid.configuration.reverseMode && Plasmoid.formFactor === PlasmaCore.Types.Vertical ? 180 : 0 readonly property bool shouldShrinkToZero: tasks.tasksModel.count === 0 && Plasmoid.configuration.fill readonly property bool vertical: Plasmoid.formFactor === PlasmaCore.Types.Vertical readonly property bool iconsOnly: Plasmoid.pluginName === "org.kde.plasma.icontasks" || Plasmoid.pluginName === "org.kde.plasma.icontasks.zoom" property Item toolTipOpenedByClick property Item toolTipAreaItem readonly property Component contextMenuComponent: Qt.createComponent("ContextMenu.qml") readonly property Component pulseAudioComponent: Qt.createComponent("PulseAudio.qml") property bool needLayoutRefresh: false property /*list where WId = int|string*/ var taskClosedWithMouseMiddleButton: [] property alias taskList: taskList // Zoom effect properties - PERFORMANCE OPTIMIZED property bool zoomEffectEnabled: iconsOnly && Plasmoid.configuration.magnifyFactor > 0 preferredRepresentation: fullRepresentation Plasmoid.constraintHints: Plasmoid.CanFillArea Plasmoid.onUserConfiguringChanged: { if (Plasmoid.userConfiguring && groupDialog !== null) { groupDialog.visible = false; } } Layout.fillWidth: vertical ? true : Plasmoid.configuration.fill Layout.fillHeight: !vertical ? true : Plasmoid.configuration.fill Layout.minimumWidth: { if (shouldShrinkToZero) { return Kirigami.Units.gridUnit; // For edit mode } return vertical ? 0 : LayoutMetrics.preferredMinWidth(); } Layout.minimumHeight: { if (shouldShrinkToZero) { return Kirigami.Units.gridUnit; // For edit mode } return !vertical ? 0 : LayoutMetrics.preferredMinHeight(); } //BEGIN TODO: this is not precise enough: launchers are smaller than full tasks Layout.preferredWidth: { if (shouldShrinkToZero) { return 0.01; } // Simple direct calculation - REMOVED CACHING TO FIX BINDING LOOPS return vertical ? (Kirigami.Units.gridUnit * 10) : taskList.Layout.maximumWidth; } Layout.preferredHeight: { if (shouldShrinkToZero) { return 0.01; } // Simple direct calculation - REMOVED CACHING TO FIX BINDING LOOPS return vertical ? taskList.Layout.maximumHeight : (Kirigami.Units.gridUnit * 2); } //END TODO property Item dragSource signal requestLayout signal windowsHovered(var winIds, bool hovered) signal activateWindowView(var winIds) onDragSourceChanged: { if (dragSource === null) { tasksModel.syncLaunchers(); } } function publishIconGeometries(taskItems: /*list*/var): void { if (TaskTools.taskManagerInstanceCount >= 2) { return; } for (let i = 0; i < taskItems.length - 1; ++i) { const task = taskItems[i]; if (!task.model.IsLauncher && !task.model.IsStartup) { tasksModel.requestPublishDelegateGeometry(tasksModel.makeModelIndex(task.index), backend.globalRect(task), task); } } } readonly property TaskManager.TasksModel tasksModel: TaskManager.TasksModel { id: tasksModel readonly property int logicalLauncherCount: { if (Plasmoid.configuration.separateLaunchers) { return launcherCount; } let startupsWithLaunchers = 0; for (let i = 0; i < taskRepeater.count; ++i) { const item = taskRepeater.itemAt(i); // During destruction required properties such as item.model can go null for a while, // so in paths that can trigger on those moments, they need to be guarded if (item?.model?.IsStartup && item.model.HasLauncher) { ++startupsWithLaunchers; } } return launcherCount + startupsWithLaunchers; } virtualDesktop: virtualDesktopInfo.currentDesktop screenGeometry: Plasmoid.containment.screenGeometry activity: activityInfo.currentActivity filterByVirtualDesktop: Plasmoid.configuration.showOnlyCurrentDesktop filterByScreen: Plasmoid.configuration.showOnlyCurrentScreen filterByActivity: Plasmoid.configuration.showOnlyCurrentActivity filterNotMinimized: Plasmoid.configuration.showOnlyMinimized hideActivatedLaunchers: tasks.iconsOnly || Plasmoid.configuration.hideLauncherOnStart sortMode: sortModeEnumValue(Plasmoid.configuration.sortingStrategy) launchInPlace: tasks.iconsOnly && Plasmoid.configuration.sortingStrategy === 1 separateLaunchers: { if (!tasks.iconsOnly && !Plasmoid.configuration.separateLaunchers && Plasmoid.configuration.sortingStrategy === 1) { return false; } return true; } groupMode: groupModeEnumValue(Plasmoid.configuration.groupingStrategy) groupInline: !Plasmoid.configuration.groupPopups && !tasks.iconsOnly groupingWindowTasksThreshold: (Plasmoid.configuration.onlyGroupWhenFull && !tasks.iconsOnly ? LayoutMetrics.optimumCapacity(width, height) + 1 : -1) onLauncherListChanged: { Plasmoid.configuration.launchers = launcherList; } onGroupingAppIdBlacklistChanged: { Plasmoid.configuration.groupingAppIdBlacklist = groupingAppIdBlacklist; } onGroupingLauncherUrlBlacklistChanged: { Plasmoid.configuration.groupingLauncherUrlBlacklist = groupingLauncherUrlBlacklist; } function sortModeEnumValue(index: int): /*TaskManager.TasksModel.SortMode*/ int { switch (index) { case 0: return TaskManager.TasksModel.SortDisabled; case 1: return TaskManager.TasksModel.SortManual; case 2: return TaskManager.TasksModel.SortAlpha; case 3: return TaskManager.TasksModel.SortVirtualDesktop; case 4: return TaskManager.TasksModel.SortActivity; default: return TaskManager.TasksModel.SortDisabled; } } function groupModeEnumValue(index: int): /*TaskManager.TasksModel.GroupMode*/ int { switch (index) { case 0: return TaskManager.TasksModel.GroupDisabled; case 1: return TaskManager.TasksModel.GroupApplications; } } Component.onCompleted: { launcherList = Plasmoid.configuration.launchers; groupingAppIdBlacklist = Plasmoid.configuration.groupingAppIdBlacklist; groupingLauncherUrlBlacklist = Plasmoid.configuration.groupingLauncherUrlBlacklist; // Only hook up view only after the above churn is done. taskRepeater.model = tasksModel; } } readonly property TaskManagerApplet.Backend backend: TaskManagerApplet.Backend { id: backend highlightWindows: Plasmoid.configuration.highlightWindows onAddLauncher: { tasks.addLauncher(url); } } DBus.DBusServiceWatcher { id: effectWatcher busType: DBus.BusType.Session watchedService: "org.kde.KWin.Effect.WindowView1" } readonly property Component taskInitComponent: Component { Timer { interval: 200 running: true onTriggered: { const task = parent as Task; if (task) { tasksModel.requestPublishDelegateGeometry(task.modelIndex(), backend.globalRect(task), task); } destroy(); } } } Connections { target: Plasmoid function onLocationChanged(): void { if (TaskTools.taskManagerInstanceCount >= 2) { return; } // This is on a timer because the panel may not have // settled into position yet when the location prop- // erty updates. iconGeometryTimer.start(); } } Connections { target: Plasmoid.containment function onScreenGeometryChanged(): void { iconGeometryTimer.start(); } } Mpris.Mpris2Model { id: mpris2Source } Item { anchors.fill: parent TaskManager.VirtualDesktopInfo { id: virtualDesktopInfo } TaskManager.ActivityInfo { id: activityInfo readonly property string nullUuid: "00000000-0000-0000-0000-000000000000" } Loader { id: pulseAudio sourceComponent: pulseAudioComponent active: pulseAudioComponent.status === Component.Ready } Timer { id: iconGeometryTimer interval: 500 repeat: false onTriggered: { tasks.publishIconGeometries(taskList.children, tasks); } } Binding { target: Plasmoid property: "status" value: (tasksModel.anyTaskDemandsAttention && Plasmoid.configuration.unhideOnAttention ? PlasmaCore.Types.NeedsAttentionStatus : PlasmaCore.Types.PassiveStatus) restoreMode: Binding.RestoreBinding } Connections { target: Plasmoid.configuration function onLaunchersChanged(): void { tasksModel.launcherList = Plasmoid.configuration.launchers } function onGroupingAppIdBlacklistChanged(): void { tasksModel.groupingAppIdBlacklist = Plasmoid.configuration.groupingAppIdBlacklist; } function onGroupingLauncherUrlBlacklistChanged(): void { tasksModel.groupingLauncherUrlBlacklist = Plasmoid.configuration.groupingLauncherUrlBlacklist; } } Component { id: launchAnimationComponent LaunchAnimation { animationType: Plasmoid.configuration.launchAnimationType animationDuration: Plasmoid.configuration.launchAnimationDuration animationIntensity: Plasmoid.configuration.launchAnimationIntensity } } // Save drag data Item { id: dragHelper Drag.dragType: Drag.Automatic Drag.supportedActions: Qt.CopyAction | Qt.MoveAction | Qt.LinkAction Drag.onDragFinished: dropAction => { tasks.dragSource = null; } } KSvg.FrameSvgItem { id: taskFrame visible: false imagePath: "widgets/tasks" prefix: TaskTools.taskPrefix("normal", Plasmoid.location) } MouseHandler { id: mouseHandler anchors.fill: parent target: taskList onUrlsDropped: urls => { // If all dropped URLs point to application desktop files, we'll add a launcher for each of them. const createLaunchers = urls.every(item => backend.isApplication(item)); if (createLaunchers) { urls.forEach(item => addLauncher(item)); return; } if (!hoveredItem) { return; } // Otherwise we'll just start a new instance of the application with the URLs as argument, // as you probably don't expect some of your files to open in the app and others to spawn launchers. tasksModel.requestOpenUrls(hoveredItem.modelIndex(), urls); } } ToolTipDelegate { id: openWindowToolTipDelegate visible: false } ToolTipDelegate { id: pinnedAppToolTipDelegate visible: false } TriangleMouseFilter { id: tmf filterTimeOut: 300 active: tasks.toolTipAreaItem && tasks.toolTipAreaItem.toolTipOpen blockFirstEnter: false // Prevent clipping of zoomed icons clip: false edge: { switch (Plasmoid.location) { case PlasmaCore.Types.BottomEdge: return Qt.TopEdge; case PlasmaCore.Types.TopEdge: return Qt.BottomEdge; case PlasmaCore.Types.LeftEdge: return Qt.RightEdge; case PlasmaCore.Types.RightEdge: return Qt.LeftEdge; default: return Qt.TopEdge; } } LayoutMirroring.enabled: tasks.shouldBeMirrored(Plasmoid.configuration.reverseMode, Qt.application.layoutDirection, vertical) anchors { left: parent.left top: parent.top } height: taskList.height width: taskList.width TaskList { id: taskList LayoutMirroring.enabled: tasks.shouldBeMirrored(Plasmoid.configuration.reverseMode, Qt.application.layoutDirection, vertical) anchors { left: parent.left top: parent.top } readonly property real widthOccupation: taskRepeater.count / columns readonly property real heightOccupation: taskRepeater.count / rows Layout.maximumWidth: { const totalMaxWidth = children.reduce((accumulator, child) => { if (!isFinite(child.Layout.maximumWidth)) { return accumulator; } return accumulator + child.Layout.maximumWidth }, 0); return Math.round(totalMaxWidth / widthOccupation); } Layout.maximumHeight: { const totalMaxHeight = children.reduce((accumulator, child) => { if (!isFinite(child.Layout.maximumHeight)) { return accumulator; } return accumulator + child.Layout.maximumHeight }, 0); return Math.round(totalMaxHeight / heightOccupation); } width: { if (tasks.shouldShrinkToZero) { return 0; } if (tasks.vertical) { return tasks.width * Math.min(1, widthOccupation); } else { return Math.min(tasks.width, Layout.maximumWidth); } } height: { if (tasks.shouldShrinkToZero) { return 0; } if (tasks.vertical) { return Math.min(tasks.height, Layout.maximumHeight); } else { return tasks.height * Math.min(1, heightOccupation); } } flow: { if (tasks.vertical) { return Plasmoid.configuration.forceStripes ? Grid.LeftToRight : Grid.TopToBottom } return Plasmoid.configuration.forceStripes ? Grid.TopToBottom : Grid.LeftToRight } onAnimatingChanged: { if (!animating) { tasks.publishIconGeometries(children, tasks); } } Repeater { id: taskRepeater delegate: Task { tasksRoot: tasks } onItemRemoved: (index, item) => { if (tasks.containsMouse && index !== taskRepeater.count && item.model.WinIdList.length > 0 && taskClosedWithMouseMiddleButton.includes(item.winIdList[0])) { needLayoutRefresh = true; } taskClosedWithMouseMiddleButton = []; } } } } } readonly property Component groupDialogComponent: Qt.createComponent("GroupDialog.qml") property GroupDialog groupDialog readonly property bool supportsLaunchers: true function hasLauncher(url: url): bool { return tasksModel.launcherPosition(url) !== -1; } function addLauncher(url: url): void { if (Plasmoid.immutability !== PlasmaCore.Types.SystemImmutable) { tasksModel.requestAddLauncher(url); } } function removeLauncher(url: url): void { if (Plasmoid.immutability !== PlasmaCore.Types.SystemImmutable) { tasksModel.requestRemoveLauncher(url); } } // This is called by plasmashell in response to a Meta+number shortcut. // TODO: Change type to int function activateTaskAtIndex(index: var): void { if (typeof index !== "number") { return; } const task = taskRepeater.itemAt(index); if (task) { TaskTools.activateTask(task.modelIndex(), task.model, null, task, Plasmoid, this, effectWatcher.registered); } } function createContextMenu(rootTask, modelIndex, args = {}) { const initialArgs = Object.assign(args, { visualParent: rootTask, modelIndex, mpris2Source, backend, }); return contextMenuComponent.createObject(rootTask, initialArgs); } function shouldBeMirrored(reverseMode, layoutDirection, vertical): bool { // LayoutMirroring is only horizontal if (vertical) { return layoutDirection === Qt.RightToLeft; } if (layoutDirection === Qt.LeftToRight) { return reverseMode; } return !reverseMode; } Component.onCompleted: { TaskTools.taskManagerInstanceCount += 1; requestLayout.connect(iconGeometryTimer.restart); windowsHovered.connect(backend.windowsHovered); activateWindowView.connect(backend.activateWindowView); } Component.onDestruction: { TaskTools.taskManagerInstanceCount -= 1; } // Critical: Invalidate caches when task filtering changes (activity/virtual desktop switches) Connections { target: tasksModel function onCountChanged() { // Simple layout request without caching Qt.callLater(() => { requestLayout(); }); } } // Invalidate caches when virtual desktop info changes Connections { target: virtualDesktopInfo function onCurrentDesktopChanged() { // Delay to ensure tasksModel has processed the change Qt.callLater(() => { requestLayout(); }); } } // Invalidate caches when activity changes Connections { target: activityInfo function onCurrentActivityChanged() { // Delay to ensure tasksModel has processed the change Qt.callLater(() => { requestLayout(); }); } } // Invalidate when filter settings change Connections { target: Plasmoid.configuration function onShowOnlyCurrentDesktopChanged() { Qt.callLater(() => { requestLayout(); }); } function onShowOnlyCurrentActivityChanged() { Qt.callLater(() => { requestLayout(); }); } function onShowOnlyCurrentScreenChanged() { Qt.callLater(() => { requestLayout(); }); } function onShowOnlyMinimizedChanged() { Qt.callLater(() => { requestLayout(); }); } } onVerticalChanged: { } onHeightChanged: { } onWidthChanged: { } // Monitor shouldShrinkToZero changes for immediate layout response onShouldShrinkToZeroChanged: { // Immediate layout request requestLayout(); } }