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 }