/* SPDX-FileCopyrightText: 2016 Eike Hein SPDX-FileCopyrightText: 2022 Carson Black SPDX-License-Identifier: GPL-2.0-or-later */ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import org.kde.plasma.plasmoid import org.kde.plasma.core as PlasmaCore import org.kde.plasma.components as PlasmaComponents import org.kde.plasma.extras as PlasmaExtras import org.kde.kirigami as Kirigami import org.kde.taskmanager as TaskManager PlasmoidItem { id: root property string lastActiveTaskName: "" property /*QIcon*/ var lastActiveTaskIcon: "" Plasmoid.constraintHints: Plasmoid.CanFillArea compactRepresentation: windowListButton fullRepresentation: windowList switchWidth: Kirigami.Units.gridUnit * 8 switchHeight: Kirigami.Units.gridUnit * 6 readonly property bool inPanel: [ PlasmaCore.Types.TopEdge, PlasmaCore.Types.RightEdge, PlasmaCore.Types.BottomEdge, PlasmaCore.Types.LeftEdge, ].includes(Plasmoid.location) TextMetrics { id: placeholderMetrics font: Kirigami.Theme.defaultFont text: i18nc("@info:placeholder", "No open windows") } property ListModel noWindowModel: ListModel { ListElement { display: "" decoration: "edit-none" } Component.onCompleted: { noWindowModel.setProperty(0, "display", placeholderMetrics.text) } } TaskManager.VirtualDesktopInfo { id: virtualDesktopInfo } TaskManager.ActivityInfo { id: activityInfo readonly property string nullUuid: "00000000-0000-0000-0000-000000000000" } TaskManager.TasksModel { id: tasksModel screenGeometry: Plasmoid.containment.screenGeometry activity: activityInfo.currentActivity sortMode: Plasmoid.configuration.sortingStrategy groupMode: TaskManager.TasksModel.GroupDisabled filterByCurrentVirtualDesktop: Plasmoid.configuration.showOnlyCurrentDesktop filterByScreen: Plasmoid.configuration.showOnlyCurrentScreen filterByActivity: Plasmoid.configuration.showOnlyCurrentActivity filterNotMinimized: Plasmoid.configuration.showOnlyMinimized } property string longestWindowCaption: "" TextMetrics { id: longestTextMetrics elide: Text.ElideRight } property int fullRepresentationDynamicWidth: 0 function updateLongestWindowTitle() { if (!tasksModel || !tasksModel.count) { longestWindowCaption = ""; fullRepresentationDynamicWidth = Math.ceil(placeholderMetrics.width) + Kirigami.Units.iconSizes.sizeForLabels * 2 + Kirigami.Units.smallSpacing * 2; return; } let maxWidth = 0; let longest = ""; for (let i = 0; i < tasksModel.count; ++i) { let idx = tasksModel.makeModelIndex(i); longestTextMetrics.text = tasksModel.data(idx, 0) || tasksModel.data(idx, TaskManager.AbstractTasksModel.AppName) || ""; if (longestTextMetrics.width > maxWidth) { maxWidth = longestTextMetrics.width; longest = longestTextMetrics.text; } } root.longestWindowCaption = longest; fullRepresentationDynamicWidth = Math.ceil(maxWidth) + Kirigami.Units.iconSizes.sizeForLabels * 2 + Kirigami.Units.smallSpacing * 2; } Connections { target: tasksModel function onModelReset() { root.updateLongestWindowTitle(); } } Component { id: windowList PlasmaComponents.ScrollView { id: windowListScrollView PlasmaComponents.ScrollBar.horizontal.policy: PlasmaComponents.ScrollBar.AlwaysOff ListView { id: windowListView property int maxDelegateWidth: 0 clip: true width: windowListScrollView.availableWidth // Set preferred size when on desktop containment. // Size set arbitrarily to fit approximately 12-14 items. Binding { target: windowListScrollView property: "Layout.preferredWidth" when: !root.inPanel value: Kirigami.Units.gridUnit * 28 } Binding { target: windowListScrollView property: "Layout.preferredHeight" when: !root.inPanel value: Kirigami.Units.gridUnit * 24 } Binding { target: windowListScrollView property: "Layout.maximumHeight" when: root.inPanel value: windowListView.contentHeight } Binding { target: windowListScrollView property: "Layout.minimumHeight" when: root.inPanel value: windowListView.contentHeight } Binding { target: windowListScrollView property: "Layout.maximumWidth" when: root.inPanel value: root.fullRepresentationDynamicWidth } Binding { target: windowListScrollView property: "Layout.minimumWidth" when: root.inPanel value: root.fullRepresentationDynamicWidth } model: root.inPanel && tasksModel.count === 0 ? root.noWindowModel : tasksModel Connections { target: root function onExpandedChanged() { if (root.expanded) { windowListView.currentIndex = -1 // Needed for when for expanded with Global Shortcut if (tasksModel.activeTask.valid) { root.lastActiveTaskName = tasksModel.data(tasksModel.activeTask, TaskManager.AbstractTasksModel.AppName) || tasksModel.data(tasksModel.activeTask, 0 /* display name, window title if app name not present */) root.lastActiveTaskIcon = tasksModel.data(tasksModel.activeTask, 1 /* decorationrole */) } root.updateLongestWindowTitle(); } else { Qt.callLater( () => { // give the previous window a chance to gain focus root.lastActiveTaskName = "" root.lastActiveTaskIcon = "" root.updateLongestWindowTitle(); }) } } } // focus is needed to receive key events on desktop containment works in panel without this. focus: true keyNavigationWraps: true highlight: PlasmaExtras.Highlight { visible: windowListView.currentItem active: windowListView.focus pressed: windowListView.currentItem && windowListView.currentItem.pressed } highlightMoveDuration: 0 highlightResizeDuration: 0 Keys.onTabPressed: { incrementCurrentIndex(); } Keys.onBacktabPressed: { decrementCurrentIndex(); } section { property: switch (Plasmoid.configuration.sortingStrategy) { case TaskManager.TasksModel.SortVirtualDesktop: return "VirtualDesktops"; // AbstractTasksModel::AdditionalRoles::VirtualDesktops case TaskManager.TasksModel.SortActivity: return "Activities"; // AbstractTasksModel::AdditionalRoles::Activities default: return ""; } delegate: Kirigami.ListSectionHeader { required property var section width: windowListView.width text: { switch (Plasmoid.configuration.sortingStrategy) { case TaskManager.TasksModel.SortVirtualDesktop: // Section contains the virtual desktop id. In case the application is on multiple desktops, it is empty. return section ? virtualDesktopInfo.desktopNames[virtualDesktopInfo.desktopIds.indexOf(section)] : ""; case TaskManager.TasksModel.SortActivity: // Section contains the activity id. In case the application is on multiple activities, it is empty. return section ? activityInfo.activityName(section) : ""; } return ""; } } } // Helps with performance otherwise scrolling is very laggy and stuttery // with low fps reuseItems: true delegate: PlasmaComponents.ItemDelegate { id: delegate required property var model required property var decoration width: { if (root.inPanel) { return root.fullRepresentationDynamicWidth } else { return ListView.view.width; } } highlighted: ListView.isCurrentItem contentItem: RowLayout { spacing: Kirigami.Units.smallSpacing Kirigami.Icon { id: iconItem source: delegate.decoration visible: source !== "" && iconItem.valid implicitWidth: Kirigami.Units.iconSizes.sizeForLabels implicitHeight: Kirigami.Units.iconSizes.sizeForLabels } // Fall back to a generic icon if the application doesn't provide a valid one Kirigami.Icon { source: "preferences-system-windows" visible: !iconItem.valid implicitWidth: Kirigami.Units.iconSizes.sizeForLabels implicitHeight: Kirigami.Units.iconSizes.sizeForLabels } PlasmaComponents.Label { Layout.fillWidth: true text: delegate.model.display textFormat: Text.PlainText elide: Text.ElideRight } } onHoveredChanged: { if (hovered) { windowListView.currentIndex = model.index } } onClicked: { windowListView.currentIndex = model.index tasksModel.requestActivate(tasksModel.makeModelIndex(model.index)) focus = false // or it persists invisibly after closing } Keys.onReturnPressed: animateClick() Keys.onEnterPressed: animateClick() TapHandler { acceptedButtons: Qt.RightButton onTapped: { /* Empty to disable right click menu provided by applet on the delegate */ } } } Kirigami.PlaceholderMessage { anchors.centerIn: parent width: parent.width - (Kirigami.Units.largeSpacing * 2) visible: !root.inPanel && windowListView.count === 0 icon.source: "edit-none" text: placeholderMetrics.text } } } } // Only exists because the default CompactRepresentation doesn't expose the // ability to show text below or beside the icon. // TODO remove once it gains that feature. Component { id: windowListButton MenuButton { id: menuButton Layout.minimumWidth: Math.min(implicitWidth, Kirigami.Units.gridUnit * 20) Layout.maximumWidth: Layout.minimumWidth Layout.fillHeight: Plasmoid.formFactor === PlasmaCore.Types.Horizontal Layout.fillWidth: Plasmoid.formFactor === PlasmaCore.Types.Vertical onClicked: root.expanded = !root.expanded down: pressed || root.expanded Accessible.name: Plasmoid.title Accessible.description: root.toolTipSubText text: if (root.lastActiveTaskName !== "") { return root.lastActiveTaskName } else if (tasksModel.activeTask.valid) { return tasksModel.data(tasksModel.activeTask, TaskManager.AbstractTasksModel.AppName) || tasksModel.data(tasksModel.activeTask, 0 /* display name, window title if app name not present */) } else { return i18nc("@title:window title shown e.g. for desktop and expanded widgets", "Plasma Desktop") } iconSource: if (root.lastActiveTaskIcon) { return root.lastActiveTaskIcon } else if (tasksModel.activeTask.valid) { return tasksModel.data(tasksModel.activeTask, 1 /* decorationrole */) } else { return "start-here-kde-symbolic" } Timer { id: hoverOpenTimer interval: Plasmoid?.configuration?.hoverOpenDelay ?? 300 repeat: false onTriggered: { root.expanded = true } } onHoveredChanged: { if (hovered) { if (Plasmoid.configuration.openOnHover) { hoverOpenTimer.start() } } else { hoverOpenTimer.stop() } } } } }