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

0001 /*
0002     SPDX-FileCopyrightText: 2015 Eike Hein <hein@kde.org>
0003     SPDX-FileCopyrightText: 2021 Mikel Johnson <mikel5764@gmail.com>
0004     SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 import QtQuick 2.15
0010 import QtQml 2.15
0011 
0012 import org.kde.plasma.components 3.0 as PC3
0013 import org.kde.plasma.extras as PlasmaExtras
0014 
0015 import org.kde.ksvg 1.0 as KSvg
0016 import org.kde.kirigami 2.20 as Kirigami
0017 
0018 // ScrollView makes it difficult to control implicit size using the contentItem.
0019 // Using EmptyPage instead.
0020 EmptyPage {
0021     id: root
0022     property alias model: view.model
0023     property alias count: view.count
0024     property alias currentIndex: view.currentIndex
0025     property alias currentItem: view.currentItem
0026     property alias delegate: view.delegate
0027     property alias blockTargetWheel: wheelHandler.blockTargetWheel
0028     property alias view: view
0029 
0030     clip: view.height < view.contentHeight
0031 
0032     header: MouseArea {
0033         implicitHeight: KickoffSingleton.listItemMetrics.margins.top
0034         hoverEnabled: true
0035         onEntered: {
0036             if (containsMouse) {
0037                 let targetIndex = view.indexAt(mouseX + view.contentX, view.contentY)
0038                 if (targetIndex >= 0) {
0039                     view.currentIndex = targetIndex
0040                     view.forceActiveFocus(Qt.MouseFocusReason)
0041                 }
0042             }
0043         }
0044     }
0045 
0046     footer: MouseArea {
0047         implicitHeight: KickoffSingleton.listItemMetrics.margins.bottom
0048         hoverEnabled: true
0049         onEntered: {
0050             if (containsMouse) {
0051                 let targetIndex = view.indexAt(mouseX + view.contentX, view.height + view.contentY - 1)
0052                 if (targetIndex >= 0) {
0053                     view.currentIndex = targetIndex
0054                     view.forceActiveFocus(Qt.MouseFocusReason)
0055                 }
0056             }
0057         }
0058     }
0059 
0060     /* Not setting GridView as the contentItem because GridView has no way to
0061      * set horizontal alignment. I don't want to use leftPadding/rightPadding
0062      * for that because I'd have to change the implicitWidth formula and use a
0063      * more complicated calculation to get the correct padding.
0064      */
0065     GridView {
0066         id: view
0067         readonly property real availableWidth: width - leftMargin - rightMargin
0068         readonly property real availableHeight: height - topMargin - bottomMargin
0069         readonly property int columns: Math.floor(availableWidth / cellWidth)
0070         readonly property int rows: Math.floor(availableHeight / cellHeight)
0071         property bool movedWithKeyboard: false
0072         property bool movedWithWheel: false
0073 
0074         // NOTE: parent is the contentItem that Control subclasses automatically
0075         // create when no contentItem is set, but content is added.
0076         height: parent.height
0077         // There are lots of ways to try to center the content of a GridView
0078         // and many of them have bad visual flaws. This way works pretty well.
0079         // Not center aligning when there might be a scrollbar to keep click target positions consistent.
0080         anchors.horizontalCenter: kickoff.mayHaveGridWithScrollBar ? undefined : parent.horizontalCenter
0081         anchors.horizontalCenterOffset: if (kickoff.mayHaveGridWithScrollBar) {
0082             if (root.mirrored) {
0083                 return verticalScrollBar.implicitWidth/2
0084             } else {
0085                 return -verticalScrollBar.implicitWidth/2
0086             }
0087         } else {
0088             return 0
0089         }
0090         width: Math.min(parent.width, Math.floor((parent.width - leftMargin - rightMargin - (kickoff.mayHaveGridWithScrollBar ? verticalScrollBar.implicitWidth : 0)) / cellWidth) * cellWidth + leftMargin + rightMargin)
0091 
0092         Accessible.description: i18n("Grid with %1 rows, %2 columns", rows, columns) // can't use i18np here
0093 
0094 
0095         implicitWidth: {
0096             let w = view.cellWidth * kickoff.minimumGridRowCount + leftMargin + rightMargin
0097             if (kickoff.mayHaveGridWithScrollBar) {
0098                 w += verticalScrollBar.implicitWidth
0099             }
0100             return w
0101         }
0102         implicitHeight: view.cellHeight * kickoff.minimumGridRowCount + topMargin + bottomMargin
0103 
0104         leftMargin: kickoff.backgroundMetrics.leftPadding
0105         rightMargin: kickoff.backgroundMetrics.rightPadding
0106 
0107         cellHeight: KickoffSingleton.gridCellSize
0108         cellWidth: KickoffSingleton.gridCellSize
0109 
0110         currentIndex: count > 0 ? 0 : -1
0111         focus: true
0112         interactive: height < contentHeight
0113         pixelAligned: true
0114         reuseItems: true
0115         boundsBehavior: Flickable.StopAtBounds
0116         // default keyboard navigation doesn't allow focus reasons to be used
0117         // and eats up/down key events when at the beginning or end of the list.
0118         keyNavigationEnabled: false
0119         keyNavigationWraps: false
0120 
0121         highlightMoveDuration: 0
0122         highlight: PlasmaExtras.Highlight {
0123             // The default Z value for delegates is 1. The default Z value for the section delegate is 2.
0124             // The highlight gets a value of 3 while the drag is active and then goes back to the default value of 0.
0125             z: root.currentItem && root.currentItem.Drag.active ?
0126                 3 : 0
0127             pressed: view.currentItem && view.currentItem.isPressed
0128             active: view.activeFocus
0129                 || (kickoff.contentArea === root
0130                     && kickoff.searchField.activeFocus)
0131             width: view.cellWidth
0132             height: view.cellHeight
0133         }
0134 
0135         delegate: KickoffGridDelegate {
0136             id: itemDelegate
0137             width: view.cellWidth
0138             Accessible.role: Accessible.Cell
0139         }
0140 
0141         move: normalTransition
0142         moveDisplaced: normalTransition
0143 
0144         Transition {
0145             id: normalTransition
0146             NumberAnimation {
0147                 duration: Kirigami.Units.shortDuration
0148                 properties: "x, y"
0149                 easing.type: Easing.OutCubic
0150             }
0151         }
0152 
0153         PC3.ScrollBar.vertical: PC3.ScrollBar {
0154             id: verticalScrollBar
0155             parent: root
0156             z: 2
0157             height: root.height
0158             anchors.right: parent.right
0159         }
0160 
0161         Kirigami.WheelHandler {
0162             id: wheelHandler
0163             target: view
0164             filterMouseEvents: true
0165             // `20 * Qt.styleHints.wheelScrollLines` is the default speed.
0166             horizontalStepSize: 20 * Qt.styleHints.wheelScrollLines
0167             verticalStepSize: 20 * Qt.styleHints.wheelScrollLines
0168 
0169             onWheel: wheel => {
0170                 view.movedWithWheel = true
0171                 view.movedWithKeyboard = false
0172                 movedWithWheelTimer.restart()
0173             }
0174         }
0175 
0176         Connections {
0177             target: kickoff
0178             function onExpandedChanged() {
0179                 if (kickoff.expanded) {
0180                     view.currentIndex = 0
0181                     view.positionViewAtBeginning()
0182                 }
0183             }
0184         }
0185 
0186         // Used to block hover events temporarily after using keyboard navigation.
0187         // If you have one hand on the touch pad or mouse and another hand on the keyboard,
0188         // it's easy to accidentally reset the highlight/focus position to the mouse position.
0189         Timer {
0190             id: movedWithKeyboardTimer
0191             interval: 200
0192             onTriggered: view.movedWithKeyboard = false
0193         }
0194 
0195         Timer {
0196             id: movedWithWheelTimer
0197             interval: 200
0198             onTriggered: view.movedWithWheel = false
0199         }
0200 
0201         function focusCurrentItem(event, focusReason) {
0202             currentItem.forceActiveFocus(focusReason)
0203             event.accepted = true
0204         }
0205 
0206         Keys.onMenuPressed: event => {
0207             if (currentItem !== null) {
0208                 currentItem.forceActiveFocus(Qt.ShortcutFocusReason)
0209                 currentItem.openActionMenu()
0210             }
0211         }
0212 
0213         Keys.onPressed: event => {
0214             let targetX = currentItem ? currentItem.x : contentX
0215             let targetY = currentItem ? currentItem.y : contentY
0216             let targetIndex = currentIndex
0217             // supports mirroring
0218             const atLeft = currentIndex % columns === (Qt.application.layoutDirection == Qt.RightToLeft ? columns - 1 : 0)
0219             // at the beginning of a line
0220             const isLeading = currentIndex % columns === 0
0221             // at the top of a given column and in the top row
0222             let atTop = currentIndex < columns
0223             // supports mirroring
0224             const atRight = currentIndex % columns === (Qt.application.layoutDirection == Qt.RightToLeft ? 0 : columns - 1)
0225             // at the end of a line
0226             const isTrailing = currentIndex % columns === columns - 1
0227             // at bottom of a given column, not necessarily in the last row
0228             let atBottom = currentIndex >= count - columns
0229             // Implements the keyboard navigation described in https://www.w3.org/TR/wai-aria-practices-1.2/#grid
0230             if (count > 1) {
0231                 switch (event.key) {
0232                     case Qt.Key_Left: if (!atLeft && !kickoff.searchField.activeFocus) {
0233                         moveCurrentIndexLeft()
0234                         focusCurrentItem(event, Qt.BacktabFocusReason)
0235                     } break
0236                     case Qt.Key_H: if (!atLeft && !kickoff.searchField.activeFocus && event.modifiers & Qt.ControlModifier) {
0237                         moveCurrentIndexLeft()
0238                         focusCurrentItem(event, Qt.BacktabFocusReason)
0239                     } break
0240                     case Qt.Key_Up: if (!atTop) {
0241                         moveCurrentIndexUp()
0242                         focusCurrentItem(event, Qt.BacktabFocusReason)
0243                     } break
0244                     case Qt.Key_K: if (!atTop && event.modifiers & Qt.ControlModifier) {
0245                         moveCurrentIndexUp()
0246                         focusCurrentItem(event, Qt.BacktabFocusReason)
0247                     } break
0248                     case Qt.Key_Right: if (!atRight && !kickoff.searchField.activeFocus) {
0249                         moveCurrentIndexRight()
0250                         focusCurrentItem(event, Qt.TabFocusReason)
0251                     } break
0252                     case Qt.Key_L: if (!atRight && !kickoff.searchField.activeFocus && event.modifiers & Qt.ControlModifier) {
0253                         moveCurrentIndexRight()
0254                         focusCurrentItem(event, Qt.TabFocusReason)
0255                     } break
0256                     case Qt.Key_Down: if (!atBottom) {
0257                         moveCurrentIndexDown()
0258                         focusCurrentItem(event, Qt.TabFocusReason)
0259                     } break
0260                     case Qt.Key_J: if (!atBottom && event.modifiers & Qt.ControlModifier) {
0261                         moveCurrentIndexDown()
0262                         focusCurrentItem(event, Qt.TabFocusReason)
0263                     } break
0264                     case Qt.Key_Home: if (event.modifiers === Qt.ControlModifier && currentIndex !== 0) {
0265                         currentIndex = 0
0266                         focusCurrentItem(event, Qt.BacktabFocusReason)
0267                     } else if (!isLeading) {
0268                         targetIndex -= currentIndex % columns
0269                         currentIndex = Math.max(targetIndex, 0)
0270                         focusCurrentItem(event, Qt.BacktabFocusReason)
0271                     } break
0272                     case Qt.Key_End: if (event.modifiers === Qt.ControlModifier && currentIndex !== count - 1) {
0273                         currentIndex = count - 1
0274                         focusCurrentItem(event, Qt.TabFocusReason)
0275                     } else if (!isTrailing) {
0276                         targetIndex += columns - 1 - (currentIndex % columns)
0277                         currentIndex = Math.min(targetIndex, count - 1)
0278                         focusCurrentItem(event, Qt.TabFocusReason)
0279                     } break
0280                     case Qt.Key_PageUp: if (!atTop) {
0281                         targetY = targetY - height + 1
0282                         targetIndex = indexAt(targetX, targetY)
0283                         // TODO: Find a more efficient, but accurate way to do this
0284                         while (targetIndex === -1) {
0285                             targetY += 1
0286                             targetIndex = indexAt(targetX, targetY)
0287                         }
0288                         currentIndex = Math.max(targetIndex, 0)
0289                         focusCurrentItem(event, Qt.BacktabFocusReason)
0290                     } break
0291                     case Qt.Key_PageDown: if (!atBottom) {
0292                         targetY = targetY + height - 1
0293                         targetIndex = indexAt(targetX, targetY)
0294                         // TODO: Find a more efficient, but accurate way to do this
0295                         while (targetIndex === -1) {
0296                             targetY -= 1
0297                             targetIndex = indexAt(targetX, targetY)
0298                         }
0299                         currentIndex = Math.min(targetIndex, count - 1)
0300                         focusCurrentItem(event, Qt.TabFocusReason)
0301                     } break
0302                 }
0303             }
0304             movedWithKeyboard = event.accepted
0305             if (movedWithKeyboard) {
0306                 movedWithKeyboardTimer.restart()
0307             }
0308         }
0309     }
0310 }