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

0001 /*
0002     SPDX-FileCopyrightText: 2011 Martin Gräßlin <mgraesslin@kde.org>
0003     SPDX-FileCopyrightText: 2012 Gregor Taetzner <gregor@freenet.de>
0004     SPDX-FileCopyrightText: 2015-2018 Eike Hein <hein@kde.org>
0005     SPDX-FileCopyrightText: 2021 Mikel Johnson <mikel5764@gmail.com>
0006     SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 import QtQuick 2.15
0011 import QtQml 2.15
0012 
0013 import org.kde.plasma.plasmoid 2.0
0014 import org.kde.plasma.components 3.0 as PC3
0015 import org.kde.plasma.extras as PlasmaExtras
0016 
0017 import org.kde.ksvg 1.0 as KSvg
0018 import org.kde.kirigami 2.20 as Kirigami
0019 
0020 // ScrollView makes it difficult to control implicit size using the contentItem.
0021 // Using EmptyPage instead.
0022 EmptyPage {
0023     id: root
0024     property alias model: view.model
0025     property alias count: view.count
0026     property alias currentIndex: view.currentIndex
0027     property alias currentItem: view.currentItem
0028     property alias delegate: view.delegate
0029     property alias section: view.section
0030     property alias highlight: view.highlight
0031     property alias view: view
0032 
0033     property bool mainContentView: false
0034     property bool hasSectionView: false
0035 
0036     /**
0037      * Request showing the section view
0038      */
0039     signal showSectionViewRequested(string sectionName)
0040 
0041     clip: view.height < view.contentHeight
0042 
0043     header: MouseArea {
0044         implicitHeight: KickoffSingleton.listItemMetrics.margins.top
0045         hoverEnabled: true
0046         onEntered: {
0047             if (containsMouse) {
0048                 let targetIndex = view.indexAt(mouseX + view.contentX, view.contentY)
0049                 if (targetIndex >= 0) {
0050                     view.currentIndex = targetIndex
0051                     view.forceActiveFocus(Qt.MouseFocusReason)
0052                 }
0053             }
0054         }
0055     }
0056 
0057     footer: MouseArea {
0058         implicitHeight: KickoffSingleton.listItemMetrics.margins.bottom
0059         hoverEnabled: true
0060         onEntered: {
0061             if (containsMouse) {
0062                 let targetIndex = view.indexAt(mouseX + view.contentX, view.height + view.contentY - 1)
0063                 if (targetIndex >= 0) {
0064                     view.currentIndex = targetIndex
0065                     view.forceActiveFocus(Qt.MouseFocusReason)
0066                 }
0067             }
0068         }
0069     }
0070 
0071     implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
0072                             contentWidth, // exclude padding to avoid scrollbars automatically affecting implicitWidth
0073                             implicitHeaderWidth2,
0074                             implicitFooterWidth2)
0075 
0076     leftPadding: verticalScrollBar.visible && root.mirrored ? verticalScrollBar.implicitWidth : 0
0077     rightPadding: verticalScrollBar.visible && !root.mirrored ? verticalScrollBar.implicitWidth : 0
0078 
0079     contentItem: ListView {
0080         id: view
0081 
0082         readonly property real availableWidth: width - leftMargin - rightMargin
0083         readonly property real availableHeight: height - topMargin - bottomMargin
0084         property bool movedWithKeyboard: false
0085         property bool movedWithWheel: false
0086 
0087         Accessible.role: Accessible.List
0088 
0089         implicitWidth: {
0090             let totalMargins = leftMargin + rightMargin
0091             if (mainContentView) {
0092                 if (kickoff.mayHaveGridWithScrollBar) {
0093                     totalMargins += verticalScrollBar.implicitWidth
0094                 }
0095                 return KickoffSingleton.gridCellSize * kickoff.minimumGridRowCount + totalMargins
0096             }
0097             return contentWidth + totalMargins
0098         }
0099         implicitHeight: {
0100             // use grid cells to determine size
0101             let h = KickoffSingleton.gridCellSize * kickoff.minimumGridRowCount
0102             // If no grids are used, use the number of items that would fit in the grid height
0103             if (Plasmoid.configuration.favoritesDisplay !== 0 && Plasmoid.configuration.applicationsDisplay !== 0) {
0104                 h = Math.floor(h / kickoff.listDelegateHeight) * kickoff.listDelegateHeight
0105             }
0106             return h + topMargin + bottomMargin
0107         }
0108 
0109         leftMargin: kickoff.backgroundMetrics.leftPadding
0110         rightMargin: kickoff.backgroundMetrics.rightPadding
0111 
0112         currentIndex: count > 0 ? 0 : -1
0113         focus: true
0114         interactive: height < contentHeight
0115         pixelAligned: true
0116         reuseItems: true
0117         boundsBehavior: Flickable.StopAtBounds
0118         // default keyboard navigation doesn't allow focus reasons to be used
0119         // and eats up/down key events when at the beginning or end of the list.
0120         keyNavigationEnabled: false
0121         keyNavigationWraps: false
0122 
0123         // This is actually needed. The highlight will animate from thin to wide otherwise.
0124         highlightResizeDuration: 0
0125         highlightMoveDuration: 0
0126         highlight: PlasmaExtras.Highlight {
0127             // The default Z value for delegates is 1. The default Z value for the section delegate is 2.
0128             // The highlight gets a value of 3 while the drag is active and then goes back to the default value of 0.
0129             z: root.currentItem && root.currentItem.Drag.active ?
0130                 3 : 0
0131             pressed: view.currentItem && view.currentItem.isPressed && !view.currentItem.isCategoryListItem
0132             active: view.activeFocus
0133                 || (kickoff.contentArea === root
0134                     && kickoff.searchField.activeFocus)
0135         }
0136 
0137         delegate: KickoffListDelegate {
0138             width: view.availableWidth
0139         }
0140 
0141         section {
0142             property: "group"
0143             criteria: ViewSection.FullString
0144             delegate: PC3.AbstractButton {
0145                 width: view.availableWidth
0146                 height: KickoffSingleton.compactListDelegateHeight
0147 
0148                 PC3.Label {
0149                     id: contentLabel
0150                     anchors.left: parent.left
0151                     width: section.length === 1
0152                         ? KickoffSingleton.compactListDelegateContentHeight + leftPadding + rightPadding
0153                         : parent.width
0154                     height: parent.height
0155                     leftPadding: view.effectiveLayoutDirection === Qt.LeftToRight
0156                         ? KickoffSingleton.listItemMetrics.margins.left : 0
0157                     rightPadding: view.effectiveLayoutDirection === Qt.RightToLeft
0158                         ? KickoffSingleton.listItemMetrics.margins.right : 0
0159                     horizontalAlignment: section.length === 1 ? Text.AlignHCenter : Text.AlignLeft
0160                     verticalAlignment: Text.AlignVCenter
0161                     maximumLineCount: 1
0162                     elide: Text.ElideRight
0163                     font.pixelSize: KickoffSingleton.compactListDelegateContentHeight
0164                     enabled: hoverHandler.hovered
0165                     text: section.length === 1 ? section.toUpperCase() : section
0166                     textFormat: Text.PlainText
0167                 }
0168 
0169                 HoverHandler {
0170                     id: hoverHandler
0171                     enabled: root.hasSectionView
0172                     cursorShape: enabled ? Qt.PointingHandCursor : undefined
0173                 }
0174 
0175                 onClicked: root.showSectionViewRequested(contentLabel.text)
0176             }
0177         }
0178 
0179         move: normalTransition
0180         moveDisplaced: normalTransition
0181 
0182         Transition {
0183             id: normalTransition
0184             NumberAnimation {
0185                 duration: Kirigami.Units.shortDuration
0186                 properties: "x, y"
0187                 easing.type: Easing.OutCubic
0188             }
0189         }
0190 
0191         PC3.ScrollBar.vertical: PC3.ScrollBar {
0192             id: verticalScrollBar
0193             parent: root
0194             z: 2
0195             height: root.height
0196             anchors.right: parent.right
0197         }
0198 
0199         Kirigami.WheelHandler {
0200             target: view
0201             filterMouseEvents: true
0202             // `20 * Qt.styleHints.wheelScrollLines` is the default speed.
0203             horizontalStepSize: 20 * Qt.styleHints.wheelScrollLines
0204             verticalStepSize: 20 * Qt.styleHints.wheelScrollLines
0205 
0206             onWheel: wheel => {
0207                 view.movedWithWheel = true
0208                 view.movedWithKeyboard = false
0209                 movedWithWheelTimer.restart()
0210             }
0211         }
0212 
0213         Connections {
0214             target: kickoff
0215             function onExpandedChanged() {
0216                 if (kickoff.expanded) {
0217                     view.currentIndex = 0
0218                     view.positionViewAtBeginning()
0219                 }
0220             }
0221         }
0222 
0223         // Used to block hover events temporarily after using keyboard navigation.
0224         // If you have one hand on the touch pad or mouse and another hand on the keyboard,
0225         // it's easy to accidentally reset the highlight/focus position to the mouse position.
0226         Timer {
0227             id: movedWithKeyboardTimer
0228             interval: 200
0229             onTriggered: view.movedWithKeyboard = false
0230         }
0231 
0232         Timer {
0233             id: movedWithWheelTimer
0234             interval: 200
0235             onTriggered: view.movedWithWheel = false
0236         }
0237 
0238         function focusCurrentItem(event, focusReason) {
0239             currentItem.forceActiveFocus(focusReason)
0240             event.accepted = true
0241         }
0242 
0243         Keys.onMenuPressed: event => {
0244             if (currentItem !== null) {
0245                 currentItem.forceActiveFocus(Qt.ShortcutFocusReason)
0246                 currentItem.openActionMenu()
0247             }
0248         }
0249         Keys.onPressed: event => {
0250             let targetX = currentItem ? currentItem.x : contentX
0251             let targetY = currentItem ? currentItem.y : contentY
0252             let targetIndex = currentIndex
0253             let atFirst = currentIndex === 0
0254             let atLast = currentIndex === count - 1
0255             if (count > 1) {
0256                 switch (event.key) {
0257                     case Qt.Key_Up: if (!atFirst) {
0258                         decrementCurrentIndex()
0259 
0260                         if (currentItem.isSeparator) {
0261                             decrementCurrentIndex()
0262                         }
0263 
0264                         focusCurrentItem(event, Qt.BacktabFocusReason)
0265                     } break
0266                     case Qt.Key_K: if (!atFirst && event.modifiers & Qt.ControlModifier) {
0267                         decrementCurrentIndex()
0268                         focusCurrentItem(event, Qt.BacktabFocusReason)
0269                     } break
0270                     case Qt.Key_Down: if (!atLast) {
0271                         incrementCurrentIndex()
0272 
0273                         if (currentItem.isSeparator) {
0274                             incrementCurrentIndex()
0275                         }
0276 
0277                         focusCurrentItem(event, Qt.TabFocusReason)
0278                     } break
0279                     case Qt.Key_J: if (!atLast && event.modifiers & Qt.ControlModifier) {
0280                         incrementCurrentIndex()
0281                         focusCurrentItem(event, Qt.TabFocusReason)
0282                     } break
0283                     case Qt.Key_Home: if (!atFirst) {
0284                         currentIndex = 0
0285                         focusCurrentItem(event, Qt.BacktabFocusReason)
0286                     } break
0287                     case Qt.Key_End: if (!atLast) {
0288                         currentIndex = count - 1
0289                         focusCurrentItem(event, Qt.TabFocusReason)
0290                     } break
0291                     case Qt.Key_PageUp: if (!atFirst) {
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.max(targetIndex, 0)
0300                         focusCurrentItem(event, Qt.BacktabFocusReason)
0301                     } break
0302                     case Qt.Key_PageDown: if (!atLast) {
0303                         targetY = targetY + height - 1
0304                         targetIndex = indexAt(targetX, targetY)
0305                         // TODO: Find a more efficient, but accurate way to do this
0306                         while (targetIndex === -1) {
0307                             targetY -= 1
0308                             targetIndex = indexAt(targetX, targetY)
0309                         }
0310                         currentIndex = Math.min(targetIndex, count - 1)
0311                         focusCurrentItem(event, Qt.TabFocusReason)
0312                     } break
0313                 }
0314             }
0315             movedWithKeyboard = event.accepted
0316             if (movedWithKeyboard) {
0317                 movedWithKeyboardTimer.restart()
0318             }
0319         }
0320     }
0321 }