643 lines
21 KiB
QML
643 lines
21 KiB
QML
/*
|
|
SPDX-FileCopyrightText: 2012-2016 Eike Hein <hein@kde.org>
|
|
|
|
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<WId> 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<Item>*/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();
|
|
}
|
|
}
|