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 '>': '>',
0067 '<': '<',
0068 '&': '&',
0069 "'": ''',
0070 '"': '"',
0071 '\u00a0': ' ',
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 }