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 }