/* SPDX-FileCopyrightText: 2012-2016 Eike Hein SPDX-License-Identifier: GPL-2.0-or-later */ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQml 2.15 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.taskmanager 0.1 as TaskManager import org.kde.plasma.private.taskmanager 0.1 as TaskManagerApplet import "code/layout.js" as LayoutManager import "code/tools.js" as TaskTools MouseArea { id: tasks anchors.fill: parent hoverEnabled: true property bool vertical: (plasmoid.formFactor === PlasmaCore.Types.Vertical) // This property tells the plasmoid to render labels next to task icons. // Previously, this property was determined by the value of (plasmoid.pluginName === "org.kde.plasma.icontasks") property bool iconsOnly: !plasmoid.configuration.labelVisible property var toolTipOpenedByClick: null property var toolTipAreaItem: null //property QtObject contextMenuComponent: Qt.createComponent("ContextMenu.qml"); property QtObject tasksMenuComponent: Qt.createComponent("TasksMenu.qml"); property QtObject pulseAudioComponent: Qt.createComponent("PulseAudio.qml"); property bool needLayoutRefresh: false; property variant taskClosedWithMouseMiddleButton: [] Plasmoid.preferredRepresentation: Plasmoid.fullRepresentation Plasmoid.constraintHints: PlasmaCore.Types.CanFillArea Plasmoid.onUserConfiguringChanged: { if (plasmoid.userConfiguring) { LayoutManager.layout(taskRepeater); groupDialog.visible = false; } } Layout.fillWidth: true Layout.fillHeight: true Layout.minimumWidth: tasks.vertical ? 0 : LayoutManager.preferredMinWidth() Layout.minimumHeight: !tasks.vertical ? 0 : LayoutManager.preferredMinHeight() //BEGIN TODO: this is not precise enough: launchers are smaller than full tasks Layout.preferredWidth: tasks.vertical ? PlasmaCore.Units.gridUnit * 10 : ((LayoutManager.logicalTaskCount() * LayoutManager.preferredMaxWidth()) / LayoutManager.calculateStripes()); Layout.preferredHeight: tasks.vertical ? ((LayoutManager.logicalTaskCount() * LayoutManager.preferredMaxHeight()) / LayoutManager.calculateStripes()) : PlasmaCore.Units.gridUnit * 2; //END TODO property Item dragSource: null signal requestLayout signal windowsHovered(variant winIds, bool hovered) signal activateWindowView(variant winIds) signal presentWindows(variant winIds) states: State { // When showing labels is toggled on/off, a weird bug happens where // tasks switch up their dominant colors due to sudden reordering. name: "iconsOnlyChanged"; when: iconsOnly StateChangeScript { script: taskList.updateHoverFunc(); } StateChangeScript { script: LayoutManager.layout(taskRepeater); } PropertyChanges { target: taskList; firstTimeHover: false // Prevents weird tooltip-related bugs from happening. } PropertyChanges { target: tasks; needLayoutRefresh: true } } onWidthChanged: { taskList.width = LayoutManager.layoutWidth(); if (plasmoid.configuration.forceStripes) { taskList.height = LayoutManager.layoutHeight(); } taskList.updateHoverSizes(); } onHeightChanged: { if (plasmoid.configuration.forceStripes) { taskList.width = LayoutManager.layoutWidth(); } taskList.height = LayoutManager.layoutHeight(); taskList.updateHoverSizes(); } onDragSourceChanged: { if (dragSource == null) { tasksModel.syncLaunchers(); } } onExited: { if (needLayoutRefresh) { LayoutManager.layout(taskRepeater) needLayoutRefresh = false; } } /*ContextMenu { id: testMenu }*/ // TasksModel which acts as a data source of currently present tasks. TaskManager.TasksModel { id: tasksModel readonly property int logicalLauncherCount: { if (plasmoid.configuration.separateLaunchers) { return launcherCount; } var startupsWithLaunchers = 0; for (var i = 0; i < taskRepeater.count; ++i) { var item = taskRepeater.itemAt(i); if (item && item.m.IsStartup === true && item.m.HasLauncher === true) { ++startupsWithLaunchers; } } return launcherCount + startupsWithLaunchers; } virtualDesktop: virtualDesktopInfo.currentDesktop screenGeometry: plasmoid.screenGeometry activity: activityInfo.currentActivity filterByVirtualDesktop: plasmoid.configuration.showOnlyCurrentDesktop filterByScreen: plasmoid.configuration.showOnlyCurrentScreen filterByActivity: plasmoid.configuration.showOnlyCurrentActivity filterNotMinimized: plasmoid.configuration.showOnlyMinimized sortMode: sortModeEnumValue(plasmoid.configuration.sortingStrategy) launchInPlace: iconsOnly && plasmoid.configuration.sortingStrategy === 1 separateLaunchers: { if (!iconsOnly && !plasmoid.configuration.separateLaunchers && plasmoid.configuration.sortingStrategy === 1) { return false; } return true; } groupMode: groupModeEnumValue(plasmoid.configuration.groupingStrategy) groupInline: !plasmoid.configuration.groupPopups groupingWindowTasksThreshold: (plasmoid.configuration.onlyGroupWhenFull && !iconsOnly ? LayoutManager.optimumCapacity(width, height) + 1 : -1) onLauncherListChanged: { layoutTimer.restart(); plasmoid.configuration.launchers = launcherList; taskList.updateHoverFunc(); } onGroupingAppIdBlacklistChanged: { plasmoid.configuration.groupingAppIdBlacklist = groupingAppIdBlacklist; } onGroupingLauncherUrlBlacklistChanged: { plasmoid.configuration.groupingLauncherUrlBlacklist = groupingLauncherUrlBlacklist; } function sortModeEnumValue(index) { 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) { 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; } } Connections { target: tasksModel function onActiveTaskChanged() { if (!plasmoid.configuration.groupPopups) { return; } if (tasksModel.activeTask.parent.valid) { groupDialog.activeTask = tasksModel.activeTask; } } } TaskManager.VirtualDesktopInfo { id: virtualDesktopInfo } TaskManager.ActivityInfo { id: activityInfo readonly property string nullUuid: "00000000-0000-0000-0000-000000000000" } TaskManagerApplet.Backend { id: backend taskManagerItem: tasks highlightWindows: plasmoid.configuration.highlightWindows onAddLauncher: { tasks.addLauncher(url); } } // Allows SevenTasks to launch programs from the context menu. See function below as an example. PlasmaCore.DataSource { id: menu_executable engine: "executable" connectedSources: [] onNewData: { var exitCode = data["exit code"] var exitStatus = data["exit status"] var stdout = data["stdout"] var stderr = data["stderr"] exited(sourceName, exitCode, exitStatus, stdout, stderr) disconnectSource(sourceName) } function exec(cmd) { if (cmd) { connectSource(cmd) } } signal exited(string cmd, int exitCode, int exitStatus, string stdout, string stderr) } // Opens KSysGuard from the taskbar context menu. function action_taskman() { menu_executable.exec("ksysguard"); } // Data source of all tasks which are playing media. Allows for playback controls from within SevenTasks. PlasmaCore.DataSource { id: mpris2Source engine: "mpris2" connectedSources: sources function sourceNameForLauncherUrl(launcherUrl, pid) { if (!launcherUrl || launcherUrl === "") { return ""; } // MPRIS spec explicitly mentions that "DesktopEntry" is with .desktop extension trimmed // Moreover, remove URL parameters, like wmClass (part after the question mark) var desktopFileName = launcherUrl.toString().split('/').pop().split('?')[0].replace(".desktop", "") if (desktopFileName.indexOf("applications:") === 0) { desktopFileName = desktopFileName.substr(13) } for (var i = 0, length = connectedSources.length; i < length; ++i) { var source = connectedSources[i]; // we intend to connect directly, otherwise the multiplexer steals the connection away if (source === "@multiplex") { continue; } var sourceData = data[source]; if (!sourceData) { continue; } if (sourceData.DesktopEntry === desktopFileName || (pid && sourceData.InstancePid === pid)) { return source; } var metadata = sourceData.Metadata; if (metadata) { var kdePid = metadata["kde:pid"]; if (kdePid && pid === kdePid) { return source; } } } return "" } function startOperation(source, op) { var service = serviceForSource(source) var operation = service.operationDescription(op) return service.startOperationCall(operation) } function goPrevious(source) { startOperation(source, "Previous"); } function goNext(source) { startOperation(source, "Next"); } function play(source) { startOperation(source, "Play"); } function pause(source) { startOperation(source, "Pause"); } function playPause(source) { startOperation(source, "PlayPause"); } function stop(source) { startOperation(source, "Stop"); } function raise(source) { startOperation(source, "Raise"); } function quit(source) { startOperation(source, "Quit"); } } Loader { id: pulseAudio sourceComponent: pulseAudioComponent active: pulseAudioComponent.status === Component.Ready } Timer { id: iconGeometryTimer interval: 500 repeat: false onTriggered: { TaskTools.publishIconGeometries(taskList.children); } } Binding { target: plasmoid property: "status" value: (tasksModel.anyTaskDemandsAttention && plasmoid.configuration.unhideOnAttention ? PlasmaCore.Types.NeedsAttentionStatus : PlasmaCore.Types.PassiveStatus) restoreMode: Binding.RestoreBinding } Connections { target: plasmoid function onLocationChanged() { // 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.configuration function onLaunchersChanged() { tasksModel.launcherList = plasmoid.configuration.launchers } function onGroupingAppIdBlacklistChanged() { tasksModel.groupingAppIdBlacklist = plasmoid.configuration.groupingAppIdBlacklist; } function onGroupingLauncherUrlBlacklistChanged() { tasksModel.groupingLauncherUrlBlacklist = plasmoid.configuration.groupingLauncherUrlBlacklist; } } TaskManagerApplet.DragHelper { id: dragHelper dragIconSize: PlasmaCore.Units.iconSizes.medium } // Used for getting margin data. PlasmaCore.FrameSvgItem { id: taskFrame visible: false; imagePath: Qt.resolvedUrl("svgs/tasks.svg"); prefix: "normal" } PlasmaCore.Svg { id: taskSvg imagePath: Qt.resolvedUrl("svgs/tasks.svg"); } PlasmaCore.Svg { id: mediaIcons imagePath: Qt.resolvedUrl("svgs/media-icons.svg") } MouseHandler { id: mouseHandler anchors.fill: parent target: taskList onUrlsDropped: { // If all dropped URLs point to application desktop files, we'll add a launcher for each of them. var createLaunchers = urls.every(function (item) { return backend.isApplication(item) }); if (createLaunchers) { urls.forEach(function (item) { addLauncher(item); }); return; } if (!hoveredItem) { return; } // DeclarativeMimeData urls is a QJsonArray but requestOpenUrls expects a proper QList. var urlsList = backend.jsonArrayToUrlList(urls); // 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(), urlsList); } } ToolTipDelegate { id: openWindowToolTipDelegate visible: false } ToolTipDelegate { id: pinnedAppToolTipDelegate visible: false } // This is the main object that contains all of the task icons. /* * A desirable feature would be to implement a transition animation that happens when * a task is removed from the TaskList, but because TaskList is based on QML's Flow view * this makes it impossible (or at least, very difficult) because Flow doesn't have a remove * transition like other view types do. * * Potential solutions include forking Flow or basing TaskList on GridView and implementing a Flow * logic (stacking items left to right and wrapping them around on overflow). Both solutions are * fairly difficult, and while hack solutions exist, I'd prefer to not pollute the code any further. */ TaskList { id: taskList anchors { left: parent.left top: parent.top } // In order to prevent weird bugs regarding hot tracking and how the dominant color is determined, // we prepare the plasmoid for the first time the user hovers over a task icon. property bool firstTimeHover: false onWidthChanged: LayoutManager.layout(taskRepeater) onHeightChanged: LayoutManager.layout(taskRepeater) flow: { if (tasks.vertical) { return plasmoid.configuration.forceStripes ? Flow.LeftToRight : Flow.TopToBottom } return plasmoid.configuration.forceStripes ? Flow.TopToBottom : Flow.LeftToRight } onAnimatingChanged: { if (!animating) { TaskTools.publishIconGeometries(children); } } function layout() { taskList.width = LayoutManager.layoutWidth(); taskList.height = LayoutManager.layoutHeight(); LayoutManager.layout(taskRepeater); //taskList.updateHoverFunc(); } // Updates the dominant color of every task present in the TaskList. // With this, we can update each task icon pretty much globally. function updateHoverFunc() { for(var i = 0; i < taskRepeater.count; i++) { if(taskRepeater.itemAt(i)) { taskRepeater.itemAt(i).updateHoverColor(); } } tasks.state = ""; //console.log("Updated hovers"); } function updateHoverSizes() { for(var i = 0; i < taskRepeater.count; i++) { if(taskRepeater.itemAt(i)) { taskRepeater.itemAt(i).updateHoverSize(); } } tasks.state = ""; //console.log("Updated hovers"); } Timer { id: layoutTimer interval: 0 repeat: false onTriggered: taskList.layout() } Repeater { id: taskRepeater delegate: Task { id: task } onItemAdded: { taskList.layout(); taskList.updateHoverFunc(); } onItemRemoved: { if (tasks.containsMouse && index != taskRepeater.count && item.winIdList && item.winIdList.length > 0 && taskClosedWithMouseMiddleButton.indexOf(item.winIdList[0]) > -1) { needLayoutRefresh = true; } else { taskList.layout(); } taskClosedWithMouseMiddleButton = []; taskList.updateHoverFunc(); } } } GroupDialog { id: groupDialog } function hasLauncher(url) { return tasksModel.launcherPosition(url) != -1; } function addLauncher(url) { if (plasmoid.immutability !== PlasmaCore.Types.SystemImmutable) { tasksModel.requestAddLauncher(url); } } // This is called by plasmashell in response to a Meta+number shortcut. function activateTaskAtIndex(index) { if (typeof index !== "number") { return; } var task = taskRepeater.itemAt(index); if (task) { TaskTools.activateTask(task.modelIndex(), task.m, null, task); } } function resetDragSource() { dragSource = null; } function createTasksMenu(rootTask, modelIndex, args) { var initialArgs = args || {} initialArgs.visualParent = rootTask; initialArgs.modelIndex = modelIndex; initialArgs.mpris2Source = mpris2Source; initialArgs.backend = backend; return tasks.tasksMenuComponent.createObject(rootTask, initialArgs); } function createContextMenu(rootTask, modelIndex, args) { var initialArgs = args || {} initialArgs.visualParent = rootTask; initialArgs.modelIndex = modelIndex; initialArgs.mpris2Source = mpris2Source; initialArgs.backend = backend; return tasks.contextMenuComponent.createObject(rootTask, initialArgs); } Component.onCompleted: { plasmoid.setAction("taskman", i18n("Task Manager")); // Add a context menu entry to SevenTasks. function action_taskman() { menu_executable.exec("ksysguard"); } tasks.requestLayout.connect(layoutTimer.restart); tasks.requestLayout.connect(iconGeometryTimer.restart); tasks.windowsHovered.connect(backend.windowsHovered); //tasks.presentWindows.connect(backend.presentWindows); tasks.activateWindowView.connect(backend.activateWindowView); dragHelper.dropped.connect(resetDragSource); taskList.updateHoverFunc(); } }