Warning, /plasma/plasma-desktop/applets/pager/package/contents/ui/main.qml is written in an unsupported language. File is not indexed.

0001 /*
0002     SPDX-FileCopyrightText: 2012 Luís Gabriel Lima <lampih@gmail.com>
0003     SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>
0004     SPDX-FileCopyrightText: 2016 Eike Hein <hein@kde.org>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 import QtQuick 2.15
0010 import QtQuick.Layouts 1.1
0011 import org.kde.plasma.plasmoid 2.0
0012 import org.kde.plasma.core as PlasmaCore
0013 import org.kde.ksvg 1.0 as KSvg
0014 import org.kde.plasma.components 3.0 as PlasmaComponents3
0015 import org.kde.draganddrop 2.0 as DnD
0016 import org.kde.plasma.private.pager 2.0
0017 import org.kde.plasma.activityswitcher as ActivitySwitcher
0018 import org.kde.kirigami 2.20 as Kirigami
0019 
0020 import org.kde.kcmutils as KCM
0021 import org.kde.config as KConfig
0022 
0023 PlasmoidItem {
0024     id: root
0025 
0026     readonly property bool isActivityPager: Plasmoid.pluginName === "org.kde.plasma.activitypager"
0027     readonly property bool vertical: Plasmoid.formFactor === PlasmaCore.Types.Vertical
0028 
0029     readonly property real aspectRatio: (((pagerModel.pagerItemSize.width * pagerItemGrid.effectiveColumns)
0030         + ((pagerItemGrid.effectiveColumns * pagerItemGrid.spacing) - pagerItemGrid.spacing))
0031         / ((pagerModel.pagerItemSize.height * pagerItemGrid.effectiveRows)
0032         + ((pagerItemGrid.effectiveRows * pagerItemGrid.spacing) - pagerItemGrid.spacing)))
0033 
0034     Layout.minimumWidth: !root.vertical ? Math.floor(height * aspectRatio) : 1
0035     Layout.minimumHeight: root.vertical ? Math.floor(width / aspectRatio) : 1
0036 
0037     Layout.maximumWidth: !root.vertical ? Math.floor(height * aspectRatio) : Infinity
0038     Layout.maximumHeight: root.vertical ? Math.floor(width / aspectRatio) : Infinity
0039 
0040     Plasmoid.status: pagerModel.shouldShowPager ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.HiddenStatus
0041 
0042     Layout.fillWidth: root.vertical
0043     Layout.fillHeight: !root.vertical
0044 
0045     property bool dragging: false
0046     property string dragId
0047 
0048     property int dragSwitchDesktopIndex: -1
0049 
0050     property int wheelDelta: 0
0051 
0052     function colorWithAlpha(color: color, alpha: real): color {
0053         return Qt.rgba(color.r, color.g, color.b, alpha)
0054     }
0055 
0056     readonly property color windowActiveOnActiveDesktopColor: colorWithAlpha(Kirigami.Theme.textColor, 0.6)
0057     readonly property color windowInactiveOnActiveDesktopColor: colorWithAlpha(Kirigami.Theme.textColor, 0.35)
0058     readonly property color windowActiveColor: colorWithAlpha(Kirigami.Theme.textColor, 0.5)
0059     readonly property color windowActiveBorderColor: Kirigami.Theme.textColor
0060     readonly property color windowInactiveColor: colorWithAlpha(Kirigami.Theme.textColor, 0.17)
0061     readonly property color windowInactiveBorderColor: colorWithAlpha(Kirigami.Theme.textColor, 0.5)
0062 
0063     function sanitize(input: string): string {
0064         // Based on QQuickStyledTextPrivate::parseEntity
0065         const table = {
0066             '>': '&gt;',
0067             '<': '&lt;',
0068             '&': '&amp;',
0069             "'": '&apos;',
0070             '"': '&quot;',
0071             '\u00a0': '&nbsp;',
0072         };
0073         return input.replace(/[<>&'"\u00a0]/g, c => table[c]);
0074     }
0075 
0076     function generateWindowList(windows) {
0077         // if we have 5 windows, we would show "4 and another one" with the
0078         // hint that there's 1 more taking the same amount of space than just showing it
0079         const maximum = windows.length === 5 ? 5 : 4
0080 
0081         let text = "<ul><li>"
0082             + windows.slice(0, maximum).map(sanitize).join("</li><li>")
0083             + "</li></ul>";
0084 
0085         if (windows.length > maximum) {
0086             text += i18np("…and %1 other window", "…and %1 other windows", windows.length - maximum)
0087         }
0088 
0089         return text
0090     }
0091 
0092     MouseArea {
0093         id: rootMouseArea
0094         anchors.fill: parent
0095 
0096         acceptedButtons: Qt.NoButton
0097         hoverEnabled: true
0098 
0099         onContainsMouseChanged: {
0100             if (!containsMouse && dragging) {
0101                 // Somewhat heavy-handed way to clean up after a window delegate drag
0102                 // exits the window.
0103                 pagerModel.refresh();
0104                 dragging = false;
0105             }
0106         }
0107 
0108         onWheel: wheel => {
0109             // Magic number 120 for common "one click, see:
0110             // https://doc.qt.io/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop
0111             wheelDelta += wheel.angleDelta.y || wheel.angleDelta.x;
0112 
0113             let increment = 0;
0114 
0115             while (wheelDelta >= 120) {
0116                 wheelDelta -= 120;
0117                 increment++;
0118             }
0119 
0120             while (wheelDelta <= -120) {
0121                 wheelDelta += 120;
0122                 increment--;
0123             }
0124 
0125             while (increment !== 0) {
0126                 if (increment < 0) {
0127                     const nextPage = Plasmoid.configuration.wrapPage?
0128                         (pagerModel.currentPage + 1) % repeater.count :
0129                         Math.min(pagerModel.currentPage + 1, repeater.count - 1);
0130                     pagerModel.changePage(nextPage);
0131                 } else {
0132                     const previousPage = Plasmoid.configuration.wrapPage ?
0133                         (repeater.count + pagerModel.currentPage - 1) % repeater.count :
0134                         Math.max(pagerModel.currentPage - 1, 0);
0135                     pagerModel.changePage(previousPage);
0136                 }
0137 
0138                 increment += (increment < 0) ? 1 : -1;
0139                 wheelDelta = 0;
0140             }
0141         }
0142     }
0143 
0144     PagerModel {
0145         id: pagerModel
0146 
0147         enabled: root.visible
0148 
0149         showDesktop: (Plasmoid.configuration.currentDesktopSelected === 1)
0150 
0151         showOnlyCurrentScreen: Plasmoid.configuration.showOnlyCurrentScreen
0152         screenGeometry: Plasmoid.containment.screenGeometry
0153 
0154         pagerType: isActivityPager ? PagerModel.Activities : PagerModel.VirtualDesktops
0155     }
0156 
0157     Connections {
0158         target: Plasmoid.configuration
0159 
0160         function onShowWindowIconsChanged() {
0161             // Causes the model to reset; Component.onCompleted in the
0162             // window delegate now gets a chance to create the icon item,
0163             // which it otherwise will not do.
0164             pagerModel.refresh();
0165         }
0166 
0167         function onDisplayedTextChanged() {
0168             // Causes the model to reset; Component.onCompleted in the
0169             // desktop delegate now gets a chance to create the label item,
0170             // which it otherwise will not do.
0171             pagerModel.refresh();
0172         }
0173     }
0174 
0175     Component {
0176         id: desktopLabelComponent
0177 
0178         PlasmaComponents3.Label {
0179             required property int index
0180             required property var model
0181             required property KSvg.FrameSvgItem desktopFrame
0182 
0183             anchors {
0184                 fill: parent
0185                 topMargin: desktopFrame.margins.top
0186                 leftMargin: desktopFrame.margins.left
0187                 rightMargin: desktopFrame.margins.right
0188                 bottomMargin: desktopFrame.margins.bottom
0189             }
0190 
0191             text: Plasmoid.configuration.displayedText ? model.display : index + 1
0192             textFormat: Text.PlainText
0193 
0194             wrapMode: Text.NoWrap
0195             elide: Text.ElideRight
0196 
0197             horizontalAlignment: Text.AlignHCenter
0198             verticalAlignment: Text.AlignVCenter
0199 
0200             font.pixelSize: Math.min(height, Kirigami.Theme.defaultFont.pixelSize)
0201 
0202             z: 9999 // The label goes above everything
0203         }
0204     }
0205 
0206     Component {
0207         id: windowIconComponent
0208 
0209         Kirigami.Icon {
0210             anchors.centerIn: parent
0211 
0212             height: Math.min(Kirigami.Units.iconSizes.small,
0213                              parent.height,
0214                              Math.max(parent.height - (Kirigami.Units.smallSpacing * 2),
0215                                       Kirigami.Units.smallSpacing * 2))
0216             width: Math.min(Kirigami.Units.iconSizes.small,
0217                             parent.width,
0218                             Math.max(parent.width - (Kirigami.Units.smallSpacing * 2),
0219                                      Kirigami.Units.smallSpacing * 2))
0220 
0221             property var model: null
0222 
0223             source: model ? model.decoration : undefined
0224             roundToIconSize: false
0225             animated: false
0226         }
0227     }
0228 
0229     Timer {
0230         id: dragTimer
0231         interval: 1000
0232         onTriggered: {
0233             if (root.dragSwitchDesktopIndex !== -1 && root.dragSwitchDesktopIndex !== pagerModel.currentPage) {
0234                 pagerModel.changePage(root.dragSwitchDesktopIndex);
0235             }
0236         }
0237     }
0238     onDragSwitchDesktopIndexChanged: if (root.dragSwitchDesktopIndex === -1) {
0239         dragTimer.stop();
0240     } else {
0241         dragTimer.restart();
0242     }
0243 
0244     Grid {
0245         id: pagerItemGrid
0246 
0247         anchors.centerIn: parent
0248         spacing: 1
0249         rows: effectiveRows
0250         columns: effectiveColumns
0251 
0252         z: 1
0253 
0254         readonly property int effectiveRows: {
0255             if (!pagerModel.count) {
0256                 return 1;
0257             }
0258 
0259             let rows = 1;
0260 
0261             if (isActivityPager && Plasmoid.configuration.pagerLayout !== 0 /*No Default*/) {
0262                 if (Plasmoid.configuration.pagerLayout === 1 /*Horizontal*/) {
0263                     rows = 1;
0264                 } else if (Plasmoid.configuration.pagerLayout === 2 /*Vertical*/) {
0265                     rows = pagerModel.count;
0266                 }
0267             } else {
0268                 let columns = Math.floor(pagerModel.count / pagerModel.layoutRows);
0269 
0270                 if (pagerModel.count % pagerModel.layoutRows > 0) {
0271                     columns += 1;
0272                 }
0273 
0274                 rows = Math.floor(pagerModel.count / columns);
0275 
0276                 if (pagerModel.count % columns > 0) {
0277                     rows += 1;
0278                 }
0279             }
0280 
0281             return rows;
0282         }
0283 
0284         readonly property int effectiveColumns: {
0285             if (!pagerModel.count) {
0286                 return 1;
0287             }
0288 
0289             return Math.ceil(pagerModel.count / effectiveRows);
0290         }
0291 
0292         readonly property real pagerItemSizeRatio: pagerModel.pagerItemSize.width / pagerModel.pagerItemSize.height
0293         readonly property real widthScaleFactor: columnWidth / pagerModel.pagerItemSize.width
0294         readonly property real heightScaleFactor: rowHeight / pagerModel.pagerItemSize.height
0295 
0296         states: [
0297             State {
0298                 name: "vertical"
0299                 when: root.vertical
0300                 PropertyChanges {
0301                     target: pagerItemGrid
0302                     innerSpacing: effectiveColumns
0303                     rowHeight: Math.floor(columnWidth / pagerItemSizeRatio)
0304                     columnWidth: Math.floor((root.width - innerSpacing) / effectiveColumns)
0305                 }
0306             }
0307         ]
0308 
0309         property int innerSpacing: (effectiveRows - 1) * spacing
0310         property int rowHeight: Math.floor((root.height - innerSpacing) / effectiveRows)
0311         property int columnWidth: Math.floor(rowHeight * pagerItemSizeRatio)
0312 
0313         Repeater {
0314             id: repeater
0315 
0316             model: pagerModel
0317 
0318             PlasmaCore.ToolTipArea {
0319                 id: desktop
0320 
0321                 readonly property string desktopId: isActivityPager ? model.TasksModel.activity : model.TasksModel.virtualDesktop
0322                 readonly property bool active: (index === pagerModel.currentPage)
0323 
0324                 mainText: model.display
0325                 // our ToolTip has maximumLineCount of 8 which doesn't fit but QML doesn't
0326                 // respect that in RichText so we effectively can put in as much as we like :)
0327                 // it also gives us more flexibility when it comes to styling the <li>
0328                 textFormat: Text.RichText
0329 
0330                 function updateSubTextIfNeeded() {
0331                     if (!containsMouse) {
0332                         return;
0333                     }
0334 
0335                     let text = ""
0336                     let visibleWindows = []
0337                     let minimizedWindows = []
0338 
0339                     for (let i = 0, length = windowRectRepeater.count; i < length; ++i) {
0340                         const window = windowRectRepeater.itemAt(i)
0341                         if (window) {
0342                             if (window.minimized) {
0343                                 minimizedWindows.push(window.visibleName)
0344                             } else {
0345                                 visibleWindows.push(window.visibleName)
0346                             }
0347                         }
0348                     }
0349 
0350                     if (visibleWindows.length === 1) {
0351                         text += visibleWindows[0]
0352                     } else if (visibleWindows.length > 1) {
0353                         text += i18np("%1 Window:", "%1 Windows:", visibleWindows.length)
0354                             + generateWindowList(visibleWindows)
0355                     }
0356 
0357                     if (visibleWindows.length && minimizedWindows.length) {
0358                         if (visibleWindows.length === 1) {
0359                             text += "<br>"
0360                         }
0361                         text += "<br>"
0362                     }
0363 
0364                     if (minimizedWindows.length > 0) {
0365                         text += i18np("%1 Minimized Window:", "%1 Minimized Windows:", minimizedWindows.length)
0366                             + generateWindowList(minimizedWindows)
0367                     }
0368 
0369                     if (text.length) {
0370                         // Get rid of the spacing <ul> would cause
0371                         text = "<style>ul { margin: 0; }</style>" + text
0372                     }
0373 
0374                     subText = text
0375                 }
0376 
0377                 width: pagerItemGrid.columnWidth
0378                 height: pagerItemGrid.rowHeight
0379 
0380                 // These states match the set of SVG prefixes for the "widgets/pager" below.
0381                 state: {
0382                     if ((desktopMouseArea.enabled && (desktopMouseArea.containsMouse || desktopMouseArea.activeFocus))
0383                             || (root.dragging && root.dragId === desktopId)) {
0384                         return "hover";
0385                     } else if (active) {
0386                         return "active";
0387                     } else {
0388                         return "normal";
0389                     }
0390                 }
0391 
0392                 component PagerFrame : KSvg.FrameSvgItem {
0393                     anchors.fill: parent
0394                     imagePath: "widgets/pager"
0395                     opacity: desktop.state === usedPrefix ? 1 : 0
0396                     Behavior on opacity { OpacityAnimator { duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic } }
0397                 }
0398 
0399                 PagerFrame {
0400                     id: desktopFrame
0401                     z: 2 // Above window outlines, but below label
0402                     prefix: "hover"
0403                 }
0404                 PagerFrame {
0405                     z: 3
0406                     prefix: "active"
0407                 }
0408                 PagerFrame {
0409                     z: 4
0410                     prefix: "normal"
0411                 }
0412 
0413                 DnD.DropArea {
0414                     id: droparea
0415                     anchors.fill: parent
0416                     preventStealing: true
0417 
0418                     onDragEnter: event => {
0419                         root.dragSwitchDesktopIndex = index;
0420                     }
0421                     onDragLeave: event => {
0422                         // new onDragEnter may happen before an old onDragLeave
0423                         if (root.dragSwitchDesktopIndex === index) {
0424                             root.dragSwitchDesktopIndex = -1;
0425                         }
0426                     }
0427                     onDrop: event => {
0428                         pagerModel.drop(event.mimeData, event.modifiers, desktop.desktopId);
0429                         root.dragSwitchDesktopIndex = -1;
0430                     }
0431                 }
0432 
0433                 MouseArea {
0434                     id: desktopMouseArea
0435 
0436                     anchors.fill: parent
0437                     hoverEnabled: true
0438                     activeFocusOnTab: true
0439                     onClicked: mouse => {
0440                         pagerModel.changePage(index);
0441                     }
0442                     Accessible.name: Plasmoid.configuration.displayedText ? model.display : i18n("Desktop %1", (index + 1))
0443                     Accessible.description: Plasmoid.configuration.displayedText ? i18nc("@info:tooltip %1 is the name of a virtual desktop or an activity", "Switch to %1", model.display) : i18nc("@info:tooltip %1 is the name of a virtual desktop or an activity", "Switch to %1", (index + 1))
0444                     Accessible.role: Accessible.Button
0445                     Keys.onPressed: event => {
0446                         switch (event.key) {
0447                         case Qt.Key_Space:
0448                         case Qt.Key_Enter:
0449                         case Qt.Key_Return:
0450                         case Qt.Key_Select:
0451                             pagerModel.changePage(index);
0452                             break;
0453                         }
0454                     }
0455                 }
0456 
0457                 Item {
0458                     id: clipRect
0459 
0460                     x: 1
0461                     y: 1
0462                     z: 1 // Below FrameSvg
0463                     width: desktop.width - 2 * x
0464                     height: desktop.height - 2 * y
0465 
0466                     Repeater {
0467                         id: windowRectRepeater
0468 
0469                         model: TasksModel
0470 
0471                         onCountChanged: desktop.updateSubTextIfNeeded()
0472 
0473                         Rectangle {
0474                             id: windowRect
0475 
0476                             z: 1 + model.StackingOrder
0477 
0478                             property rect geometry: model.Geometry
0479                             property string visibleName: model.display
0480                             property bool minimized: (model.IsMinimized === true)
0481                             onMinimizedChanged: desktop.updateSubTextIfNeeded()
0482                             onVisibleNameChanged: desktop.updateSubTextIfNeeded()
0483 
0484                             /* since we move clipRect with 1, move it back */
0485                             x: Math.round(geometry.x * pagerItemGrid.widthScaleFactor) - 1
0486                             y: Math.round(geometry.y * pagerItemGrid.heightScaleFactor) - 1
0487                             width: Math.round(geometry.width * pagerItemGrid.widthScaleFactor)
0488                             height: Math.round(geometry.height * pagerItemGrid.heightScaleFactor)
0489                             visible: model.IsMinimized !== true
0490                             color: {
0491                                 if (desktop.active) {
0492                                     if (model.IsActive === true)
0493                                         return windowActiveOnActiveDesktopColor;
0494                                     else
0495                                         return windowInactiveOnActiveDesktopColor;
0496                                 } else {
0497                                     if (model.IsActive === true)
0498                                         return windowActiveColor;
0499                                     else
0500                                         return windowInactiveColor;
0501                                 }
0502                             }
0503 
0504                             border.width: 1
0505                             border.color: (model.IsActive === true) ? windowActiveBorderColor
0506                                                     : windowInactiveBorderColor
0507 
0508                             Behavior on width  { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic } }
0509                             Behavior on height { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic } }
0510                             Behavior on color        { ColorAnimation  { duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic } }
0511                             Behavior on border.color { ColorAnimation  { duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic } }
0512 
0513                             MouseArea {
0514                                 id: windowMouseArea
0515                                 anchors.fill: parent
0516 
0517                                 drag.target: windowRect
0518                                 drag.threshold: 1
0519                                 drag.axis: Drag.XAndYAxis
0520                                 drag.minimumX: 0
0521                                 drag.maximumX: root.width - windowRect.width
0522                                 drag.minimumY: 0
0523                                 drag.maximumY: root.height - windowRect.height
0524 
0525                                 drag.onActiveChanged: {
0526                                     root.dragging = drag.active;
0527                                     root.dragId = desktop.desktopId;
0528                                     desktopMouseArea.enabled = !drag.active;
0529 
0530                                     if (drag.active) {
0531                                         // Reparent to allow drags outside of this desktop.
0532                                         const value = root.mapFromItem(clipRect, windowRect.x, windowRect.y);
0533                                         windowRect.parent = root;
0534                                         windowRect.x = value.x;
0535                                         windowRect.y = value.y;
0536                                     }
0537                                 }
0538 
0539                                 onReleased: mouse => {
0540                                     if (root.dragging) {
0541                                         windowRect.visible = false;
0542                                         const windowCenter = Qt.point(windowRect.x + windowRect.width / 2, windowRect.y + windowRect.height / 2);
0543                                         const pagerItem = pagerItemGrid.childAt(windowCenter.x, windowCenter.y);
0544 
0545                                         if (pagerItem) {
0546                                             const relativeTopLeft = root.mapToItem(pagerItem, windowRect.x, windowRect.y);
0547 
0548                                             const modelIndex = windowRectRepeater.model.index(index, 0)
0549                                             pagerModel.moveWindow(modelIndex, relativeTopLeft.x, relativeTopLeft.y,
0550                                                 pagerItem.desktopId, root.dragId,
0551                                                 pagerItemGrid.widthScaleFactor, pagerItemGrid.heightScaleFactor);
0552                                         }
0553 
0554                                         // Will reset the model, destroying the reparented drag delegate that
0555                                         // is no longer bound to model.Geometry.
0556                                         root.dragging = false;
0557                                         pagerModel.refresh();
0558                                     } else {
0559                                         // When there is no dragging (just a click), the event is passed
0560                                         // to the desktop MouseArea.
0561                                         desktopMouseArea.clicked(mouse);
0562                                     }
0563                                 }
0564                             }
0565 
0566                             Component.onCompleted: {
0567                                 if (Plasmoid.configuration.showWindowIcons) {
0568                                     windowIconComponent.createObject(windowRect, { model });
0569                                 }
0570                             }
0571                         }
0572                     }
0573                 }
0574 
0575                 Component.onCompleted: {
0576                     if (Plasmoid.configuration.displayedText < 2) {
0577                         desktopLabelComponent.createObject(desktop, { index, model, desktopFrame });
0578                     }
0579                 }
0580 
0581                 onContainsMouseChanged: updateSubTextIfNeeded()
0582             }
0583         }
0584     }
0585 
0586     Plasmoid.contextualActions: [
0587         PlasmaCore.Action {
0588             text: i18n("Show Activity Manager…")
0589             icon.name: "activities"
0590             visible: root.isActivityPager
0591             onTriggered: ActivitySwitcher.Backend.toggleActivityManager()
0592         },
0593         PlasmaCore.Action {
0594             text: i18n("Add Virtual Desktop")
0595             icon.name: "list-add"
0596             visible: !root.isActivityPager && KConfig.KAuthorized.authorize("kcm_kwin_virtualdesktops")
0597             onTriggered: pagerModel.addDesktop()
0598         },
0599         PlasmaCore.Action {
0600             text: i18n("Remove Virtual Desktop")
0601             icon.name: "list-remove"
0602             visible: !root.isActivityPager && KConfig.KAuthorized.authorize("kcm_kwin_virtualdesktops")
0603             enabled: repeater.count > 1
0604             onTriggered: pagerModel.removeDesktop()
0605         },
0606         PlasmaCore.Action {
0607             text: i18n("Configure Virtual Desktops…")
0608             visible: !root.isActivityPager && KConfig.KAuthorized.authorize("kcm_kwin_virtualdesktops")
0609             onTriggered: KCM.KCMLauncher.openSystemSettings("kcm_kwin_virtualdesktops")
0610         }
0611     ]
0612 }