plasma-taskmanager-zoom/contents/ui/main.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();
}
}