/*SPDX-FileCopyrightText: 2012 LuΓs Gabriel Lima <lampih@gmail.com>SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>SPDX-FileCopyrightText: 2016 Eike Hein <hein@kde.org>SPDX-License-Identifier: GPL-2.0-or-later*/pragma ComponentBehavior: Boundimport QtQuickimport QtQuick.Layoutsimport org.kde.plasma.plasmoidimport org.kde.plasma.core as PlasmaCoreimport org.kde.ksvg as KSvgimport org.kde.plasma.components as PlasmaComponents3import org.kde.draganddrop as DnDimport plasma.applet.org.kde.plasma.pagerimport org.kde.plasma.activityswitcher as ActivitySwitcherimport org.kde.kirigami as Kirigamiimport org.kde.kcmutils as KCMimport org.kde.config as KConfigPlasmoidItem {id: rootreadonly property bool isActivityPager: Plasmoid.pluginName === "org.kde.plasma.activitypager"readonly property bool vertical: Plasmoid.formFactor === PlasmaCore.Types.Verticalreadonly property real aspectRatio: (((pagerModel.pagerItemSize.width * pagerItemGrid.effectiveColumns)+ ((pagerItemGrid.effectiveColumns * pagerItemGrid.spacing) - pagerItemGrid.spacing))/ ((pagerModel.pagerItemSize.height * pagerItemGrid.effectiveRows)+ ((pagerItemGrid.effectiveRows * pagerItemGrid.spacing) - pagerItemGrid.spacing)))Layout.minimumWidth: !root.vertical ? Math.floor(height * aspectRatio) : 1Layout.minimumHeight: root.vertical ? Math.floor(width / aspectRatio) : 1Layout.maximumWidth: !root.vertical ? Math.floor(height * aspectRatio) : InfinityLayout.maximumHeight: root.vertical ? Math.floor(width / aspectRatio) : InfinityPlasmoid.status: pagerModel.shouldShowPager ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.HiddenStatusLayout.fillWidth: root.verticalLayout.fillHeight: !root.verticalproperty bool dragging: falseproperty string dragIdproperty int dragSwitchDesktopIndex: -1property int wheelDelta: 0function colorWithAlpha(color: color, alpha: real): color {return Qt.rgba(color.r, color.g, color.b, alpha)}readonly property color windowActiveOnActiveDesktopColor: colorWithAlpha(Kirigami.Theme.textColor, 0.6)readonly property color windowInactiveOnActiveDesktopColor: colorWithAlpha(Kirigami.Theme.textColor, 0.35)readonly property color windowActiveColor: colorWithAlpha(Kirigami.Theme.textColor, 0.5)readonly property color windowActiveBorderColor: Kirigami.Theme.textColorreadonly property color windowInactiveColor: colorWithAlpha(Kirigami.Theme.textColor, 0.17)readonly property color windowInactiveBorderColor: colorWithAlpha(Kirigami.Theme.textColor, 0.5)function sanitize(input: string): string {// Based on QQuickStyledTextPrivate::parseEntityconst table = {'>': '>','<': '<','&': '&',"'": ''','"': '"','\u00a0': ' ',};return input.replace(/[<>&'"\u00a0]/g, c => table[c]);}function generateWindowList(windows) {// if we have 5 windows, we would show "4 and another one" with the// hint that there's 1 more taking the same amount of space than just showing itconst maximum = windows.length === 5 ? 5 : 4let text = "<ul><li>"+ windows.slice(0, maximum).map(sanitize).join("</li><li>")+ "</li></ul>";if (windows.length > maximum) {text += i18ncp("@info:tooltip overflow label", "β¦and %1 other window", "β¦and %1 other windows", windows.length - maximum)}return text}MouseArea {id: rootMouseAreaanchors.fill: parentacceptedButtons: Qt.NoButtonhoverEnabled: trueonWheel: wheel => {// Magic number 120 for common "one click, see:// https://doc.qt.io/qt-5/qml-qtquick-wheelevent.html#angleDelta-proproot.wheelDelta += wheel.angleDelta.y || wheel.angleDelta.x;let increment = 0;while (root.wheelDelta >= 120) {root.wheelDelta -= 120;increment++;}while (root.wheelDelta <= -120) {root.wheelDelta += 120;increment--;}while (increment !== 0) {if (increment < 0) {const nextPage = Plasmoid.configuration.wrapPage?(pagerModel.currentPage + 1) % repeater.count :Math.min(pagerModel.currentPage + 1, repeater.count - 1);// At the last desktop: changePage() on the current one would toggle Show Desktopif (nextPage !== pagerModel.currentPage) {pagerModel.changePage(nextPage);}} else {const previousPage = Plasmoid.configuration.wrapPage ?(repeater.count + pagerModel.currentPage - 1) % repeater.count :Math.max(pagerModel.currentPage - 1, 0);if (previousPage !== pagerModel.currentPage) {pagerModel.changePage(previousPage);}}increment += (increment < 0) ? 1 : -1;root.wheelDelta = 0;}}}PagerModel {id: pagerModelenabled: root.visibleshowDesktop: (Plasmoid.configuration.currentDesktopSelected === 1)showOnlyCurrentScreen: Plasmoid.configuration.showOnlyCurrentScreenscreenName: root.Screen.namescreenGeometry: Plasmoid.containment.screenGeometrypagerType: root.isActivityPager ? PagerModel.Activities : PagerModel.VirtualDesktops}Connections {target: Plasmoid.configurationfunction onShowWindowIconsChanged() {// Causes the model to reset; Component.onCompleted in the// window delegate now gets a chance to create the icon item,// which it otherwise will not do.pagerModel.refresh();}function onDisplayedTextChanged() {// Causes the model to reset; Component.onCompleted in the// desktop delegate now gets a chance to create the label item,// which it otherwise will not do.pagerModel.refresh();}}Component {id: desktopLabelComponentPlasmaComponents3.Label {required property int indexrequired property string displayrequired property KSvg.FrameSvgItem desktopFrameanchors {fill: parenttopMargin: desktopFrame.margins.topleftMargin: desktopFrame.margins.leftrightMargin: desktopFrame.margins.rightbottomMargin: desktopFrame.margins.bottom}text: Plasmoid.configuration.displayedText ? display : index + 1textFormat: Text.PlainTextwrapMode: Text.NoWrapelide: Text.ElideRighthorizontalAlignment: Text.AlignHCenterverticalAlignment: Text.AlignVCenterfont.pixelSize: Math.min(height, Kirigami.Theme.defaultFont.pixelSize)z: 9999 // The label goes above everything}}Component {id: windowIconComponentKirigami.Icon {anchors.centerIn: parentreadonly property real availableSize: Math.min(parent.width, parent.height)width: Math.min(Kirigami.Units.iconSizes.medium,availableSize,Math.max(availableSize - (Kirigami.Units.smallSpacing * 2),Kirigami.Units.smallSpacing * 2))height: widthrequired property var decoration//: nullsource: decoration ?? undefinedroundToIconSize: falseanimated: false}}Timer {id: dragTimerinterval: 1000onTriggered: {if (root.dragSwitchDesktopIndex !== -1 && root.dragSwitchDesktopIndex !== pagerModel.currentPage) {pagerModel.changePage(root.dragSwitchDesktopIndex);}}}onDragSwitchDesktopIndexChanged: if (root.dragSwitchDesktopIndex === -1) {dragTimer.stop();} else {dragTimer.restart();}Grid {id: pagerItemGridanchors.centerIn: parentspacing: 1rows: effectiveRows * effectiveColumns >= pagerModel.count ? effectiveRows : pagerModel.countcolumns: effectiveRows * effectiveColumns >= pagerModel.count ? effectiveColumns : pagerModel.countz: 1readonly property int effectiveRows: {if (!pagerModel.count) {return 1;}let rows = 1;if (root.isActivityPager && Plasmoid.configuration.pagerLayout !== 0 /*No Default*/) {if (Plasmoid.configuration.pagerLayout === 1 /*Horizontal*/) {rows = 1;} else if (Plasmoid.configuration.pagerLayout === 2 /*Vertical*/) {rows = pagerModel.count;}} else {let columns = Math.floor(pagerModel.count / pagerModel.layoutRows);if (pagerModel.count % pagerModel.layoutRows > 0) {columns += 1;}rows = Math.floor(pagerModel.count / columns);if (pagerModel.count % columns > 0) {rows += 1;}}return rows;}readonly property int effectiveColumns: {if (!pagerModel.count) {return 1;}return Math.ceil(pagerModel.count / effectiveRows);}readonly property real pagerItemSizeRatio: pagerModel.pagerItemSize.width / pagerModel.pagerItemSize.heightreadonly property real widthScaleFactor: columnWidth / pagerModel.pagerItemSize.widthreadonly property real heightScaleFactor: rowHeight / pagerModel.pagerItemSize.heightstates: [State {name: "vertical"when: root.verticalPropertyChanges {pagerItemGrid.innerSpacing: pagerItemGrid.effectiveColumnspagerItemGrid.rowHeight: Math.floor(pagerItemGrid.columnWidth / pagerItemGrid.pagerItemSizeRatio)pagerItemGrid.columnWidth: Math.floor((root.width - pagerItemGrid.innerSpacing) / pagerItemGrid.effectiveColumns)}}]property int innerSpacing: (effectiveRows - 1) * spacingproperty int rowHeight: Math.floor((root.height - innerSpacing) / effectiveRows)property int columnWidth: Math.floor(rowHeight * pagerItemSizeRatio)Repeater {id: repeatermodel: pagerModelcomponent DesktopDelegate: PlasmaCore.ToolTipArea {required property string displayrequired property int indexrequired property var modelreadonly property /*WindowModel*/ var tasksModel: model.TasksModelreadonly property string desktopId: (root.isActivityPager ? tasksModel?.activity : tasksModel?.virtualDesktop) ?? ""readonly property bool isCurrent: (index === pagerModel.currentPage)}delegate: DesktopDelegate {id: desktopmainText: display// our ToolTip has maximumLineCount of 8 which doesn't fit but QML doesn't// respect that in RichText so we effectively can put in as much as we like :)// it also gives us more flexibility when it comes to styling the <li>textFormat: Text.RichTextfunction updateSubTextIfNeeded() {if (!containsMouse) {return;}let text = ""let visibleWindows = []let minimizedWindows = []for (let i = 0, length = windowRectRepeater.count; i < length; ++i) {const window = windowRectRepeater.itemAt(i) as WindowDelegateif (window) {if (window.minimized) {minimizedWindows.push(window.display)} else {visibleWindows.push(window.display)}}}if (visibleWindows.length === 1) {text += visibleWindows[0]} else if (visibleWindows.length > 1) {text += i18ncp("@info:tooltip start of list", "%1 Window:", "%1 Windows:", visibleWindows.length)+ root.generateWindowList(visibleWindows)}if (visibleWindows.length && minimizedWindows.length) {if (visibleWindows.length === 1) {text += "<br>"}text += "<br>"}if (minimizedWindows.length > 0) {text += i18ncp("@info:tooltip", "%1 Minimized Window:", "%1 Minimized Windows:", minimizedWindows.length)+ root.generateWindowList(minimizedWindows)}if (text.length) {// Get rid of the spacing <ul> would causetext = "<style>ul { margin: 0; }</style>" + text}subText = text}width: pagerItemGrid.columnWidthheight: pagerItemGrid.rowHeight// These states match the set of SVG prefixes for the "widgets/pager" below.state: {if ((desktopMouseArea.enabled && (desktopMouseArea.containsMouse || desktopMouseArea.activeFocus))|| (root.dragging && root.dragId === desktopId)) {return "hover";} else if (isCurrent) {return "active";} else {return "normal";}}component PagerFrame : KSvg.FrameSvgItem {anchors.fill: parentimagePath: "widgets/pager"opacity: desktop.state === usedPrefix ? 1 : 0}PagerFrame {id: desktopFramez: 2 // Above window outlines, but below labelprefix: "hover"}PagerFrame {z: 3prefix: "active"}PagerFrame {z: 4prefix: "normal"}DnD.DropArea {id: dropareaanchors.fill: parentpreventStealing: trueonDragEnter: event => {root.dragSwitchDesktopIndex = desktop.index;}onDragLeave: event => {// new onDragEnter may happen before an old onDragLeaveif (root.dragSwitchDesktopIndex === desktop.index) {root.dragSwitchDesktopIndex = -1;}}onDrop: event => {pagerModel.drop(event.mimeData, event.modifiers, desktop.desktopId);root.dragSwitchDesktopIndex = -1;}}MouseArea {id: desktopMouseAreaanchors.fill: parenthoverEnabled: trueactiveFocusOnTab: trueonClicked: mouse => {pagerModel.changePage(desktop.index);}Accessible.name: Plasmoid.configuration.displayedText ? desktop.display : i18nc("@info:whatsthis Accessible name for pager section", "Desktop %1", (desktop.index + 1))Accessible.description: Plasmoid.configuration.displayedText ? i18nc("@info:tooltip %1 is the name of a virtual desktop or an activity", "Switch to %1", desktop.display) : i18nc("@info:tooltip %1 is the name of a virtual desktop or an activity", "Switch to %1", (desktop.index + 1))Accessible.role: Accessible.ButtonKeys.onPressed: event => {switch (event.key) {case Qt.Key_Space:case Qt.Key_Enter:case Qt.Key_Return:case Qt.Key_Select:pagerModel.changePage(desktop.index);break;}}}Item {id: clipRectx: 1y: 1z: 1 // Below FrameSvgwidth: desktop.width - 2 * xheight: desktop.height - 2 * yRepeater {id: windowRectRepeatermodel: desktop.tasksModelonCountChanged: desktop.updateSubTextIfNeeded()component WindowDelegate: Rectangle {required property string displayrequired property var modelrequired property var decorationrequired property int index// These can't be required due to their role namesreadonly property rect geometry: model.Geometryreadonly property bool minimized: model.IsMinimizedreadonly property bool isActive: model.IsActivereadonly property int stackingOrder: model.PagerStackingOrder}delegate: WindowDelegate {id: windowRectonMinimizedChanged: desktop.updateSubTextIfNeeded()onDisplayChanged: desktop.updateSubTextIfNeeded()z: 1 + stackingOrder/* since we move clipRect with 1, move it back */x: Math.round(geometry.x * pagerItemGrid.widthScaleFactor) - 1y: Math.round(geometry.y * pagerItemGrid.heightScaleFactor) - 1width: Math.round(geometry.width * pagerItemGrid.widthScaleFactor)height: Math.round(geometry.height * pagerItemGrid.heightScaleFactor)visible: Plasmoid.configuration.showWindowOutlines && !minimizedcolor: {if (desktop.isCurrent) {if (isActive) {return root.windowActiveOnActiveDesktopColor;} else {return root.windowInactiveOnActiveDesktopColor;}} else {if (isActive) {return root.windowActiveColor;} else {return root.windowInactiveColor;}}}border.width: 1border.color: isActive? root.windowActiveBorderColor: root.windowInactiveBorderColorMouseArea {id: windowMouseAreaanchors.fill: parentdrag.target: windowRectdrag.threshold: 1drag.axis: Drag.XAndYAxisdrag.minimumX: 0drag.maximumX: root.width - windowRect.widthdrag.minimumY: 0drag.maximumY: root.height - windowRect.heightdrag.onActiveChanged: {root.dragging = drag.active;root.dragId = desktop.desktopId;desktopMouseArea.enabled = !drag.active;if (drag.active) {// Reparent to allow drags outside of this desktop.const value = root.mapFromItem(clipRect, windowRect.x, windowRect.y);windowRect.parent = root;windowRect.x = value.x;windowRect.y = value.y;}}onReleased: mouse => {if (root.dragging) {windowRect.visible = false;const windowCenter = Qt.point(windowRect.x + windowRect.width / 2, windowRect.y + windowRect.height / 2);const pagerItem = pagerItemGrid.childAt(windowCenter.x, windowCenter.y) as DesktopDelegate;if (pagerItem) {const relativeTopLeft = root.mapToItem(pagerItem, windowRect.x, windowRect.y);const modelIndex = windowRectRepeater.model.index(windowRect.index, 0)pagerModel.moveWindow(modelIndex, relativeTopLeft.x, relativeTopLeft.y,pagerItem.desktopId, root.dragId,pagerItemGrid.widthScaleFactor, pagerItemGrid.heightScaleFactor);}// Will reset the model, destroying the reparented drag delegate that// is no longer bound to model.Geometry.root.dragging = false;} else {// When there is no dragging (just a click), the event is passed// to the desktop MouseArea.desktopMouseArea.clicked(mouse);}pagerModel.refresh();}}Component.onCompleted: {if (Plasmoid.configuration.showWindowIcons) {windowIconComponent.createObject(windowRect, { decoration: Qt.binding(() => windowRect.decoration) });}}}}}Component.onCompleted: {if (Plasmoid.configuration.displayedText < 2) {desktopLabelComponent.createObject(desktop, {index: Qt.binding(() => desktop.index),display: Qt.binding(() => desktop.display),desktopFrame,});}}onContainsMouseChanged: updateSubTextIfNeeded()}}}Plasmoid.contextualActions: [PlasmaCore.Action {text: i18nc("@action:inmenu widget context menu", "Show Activity Manager")icon.name: "activities"visible: root.isActivityPageronTriggered: ActivitySwitcher.Backend.toggleActivityManager()},PlasmaCore.Action {text: i18nc("@action:inmenu widget context menu", "Add Virtual Desktop")icon.name: "list-add"visible: !root.isActivityPager && KConfig.KAuthorized.authorize("kcm_kwin_virtualdesktops")onTriggered: pagerModel.addDesktop()},PlasmaCore.Action {text: i18nc("@action:inmenu widget context menu", "Remove Virtual Desktop")icon.name: "list-remove"visible: !root.isActivityPager && KConfig.KAuthorized.authorize("kcm_kwin_virtualdesktops")enabled: repeater.count > 1onTriggered: pagerModel.removeDesktop()},PlasmaCore.Action {text: i18nc("@action:inmenu widget context menu", "&Configure Activitiesβ¦")visible: root.isActivityPager && KConfig.KAuthorized.authorize("kcm_activities")onTriggered: KCM.KCMLauncher.openSystemSettings("kcm_activities")},PlasmaCore.Action {text: i18nc("@action:inmenu widget context menu", "Configure Virtual Desktopsβ¦")visible: !root.isActivityPager && KConfig.KAuthorized.authorize("kcm_kwin_virtualdesktops")onTriggered: {if (Qt.platform.pluginName.includes("wayland"))KCM.KCMLauncher.openSystemSettings("kcm_kwin_virtualdesktops")elseKCM.KCMLauncher.openSystemSettings("kcm_kwin_virtualdesktops_x11")}}]}