/*SPDX-FileCopyrightText: 2015 Eike Hein <hein@kde.org>SPDX-FileCopyrightText: 2021 Mikel Johnson <mikel5764@gmail.com>SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>SPDX-License-Identifier: GPL-2.0-or-later*/pragma ComponentBehavior: Boundimport QtQuickimport QtQuick.Templates as Timport org.kde.plasma.components as PC3import org.kde.plasma.extras as PlasmaExtrasimport org.kde.kirigami as Kirigami// ScrollView makes it difficult to control implicit size using the contentItem.// Using EmptyPage instead.EmptyPage {id: rootproperty alias model: view.modelproperty alias count: view.countproperty alias currentIndex: view.currentIndexproperty alias currentItem: view.currentItemproperty alias delegate: view.delegateproperty alias flickableToScroll: wheelHandler.targetproperty alias view: viewclip: view.height < view.contentHeightheader: MouseArea {implicitHeight: KickoffSingleton.listItemMetrics.margins.tophoverEnabled: trueonEntered: {if (containsMouse) {const targetIndex = view.indexAt(mouseX + view.contentX, view.contentY)if (targetIndex >= 0) {view.currentIndex = targetIndexview.forceActiveFocus(Qt.MouseFocusReason)}}}}footer: MouseArea {implicitHeight: KickoffSingleton.listItemMetrics.margins.bottomhoverEnabled: trueonEntered: {if (containsMouse) {const targetIndex = view.indexAt(mouseX + view.contentX, view.height + view.contentY - 1)if (targetIndex >= 0) {view.currentIndex = targetIndexview.forceActiveFocus(Qt.MouseFocusReason)}}}}/* Not setting GridView as the contentItem because GridView has no way to* set horizontal alignment. I don't want to use leftPadding/rightPadding* for that because I'd have to change the implicitWidth formula and use a* more complicated calculation to get the correct padding.*/GridView {id: view// Not storing them as properties somehow avoids a dubious warning// about *Binding loop detected for property "rows"*function availableWidth(): real {return width - leftMargin - rightMargin;}function availableHeight(): real {return height - topMargin - bottomMargin;}readonly property int columns: Math.floor(availableWidth() / cellWidth)property int rowsBinding on rows {value: Math.floor(view.availableHeight() / view.cellHeight)delayed: true}property bool movedWithKeyboard: falseproperty bool movedWithWheel: false// NOTE: parent is the contentItem that Control subclasses automatically// create when no contentItem is set, but content is added.height: parent.height// There are lots of ways to try to center the content of a GridView// and many of them have bad visual flaws. This way works pretty well.// Not center aligning when there might be a scrollbar to keep click target positions consistent.anchors.horizontalCenter: undefinedanchors.horizontalCenterOffset: if (kickoff.mayHaveGridWithScrollBar) {if (root.mirrored) {return verticalScrollBar.implicitWidth/2} else {return -verticalScrollBar.implicitWidth/2}} else {return 0}width: Math.min(parent.width, Math.floor((parent.width - leftMargin - rightMargin - (kickoff.mayHaveGridWithScrollBar ? verticalScrollBar.implicitWidth : 0)) / cellWidth) * cellWidth + leftMargin + rightMargin)states: [State {name: "centered"when: !kickoff.mayHaveGridWithScrollBarAnchorChanges {target: viewanchors.horizontalCenter : parent.horizontalCenter}}]Accessible.description: i18nc("@info:whatsthis Accessible description", "Grid with %1 rows, %2 columns", rows, columns) // can't use i18np hereimplicitWidth: {let w = view.cellWidth * kickoff.minimumGridRowCount + leftMargin + rightMarginif (kickoff.mayHaveGridWithScrollBar) {w += verticalScrollBar.implicitWidth}return w}implicitHeight: view.cellHeight * kickoff.minimumGridRowCount + topMargin + bottomMarginleftMargin: kickoff.backgroundMetrics.leftPaddingrightMargin: kickoff.backgroundMetrics.rightPaddingcellHeight: KickoffSingleton.gridCellSizecellWidth: KickoffSingleton.gridCellSizecurrentIndex: count > 0 ? 0 : -1focus: trueinteractive: height < contentHeightpixelAligned: truereuseItems: false // explicitly disabled because it doesn't work correctly with switching models like we doboundsBehavior: Flickable.StopAtBounds// default keyboard navigation doesn't allow focus reasons to be used// and eats up/down key events when at the beginning or end of the list.keyNavigationEnabled: falsekeyNavigationWraps: falsehighlightFollowsCurrentItem: falsedelegate: KickoffGridDelegate {id: itemDelegatewidth: view.cellWidthAccessible.role: Accessible.Cell}move: normalTransitionmoveDisplaced: normalTransitionTransition {id: normalTransitionNumberAnimation {duration: Kirigami.Units.shortDurationproperties: "x, y"easing.type: Easing.OutCubic}}PC3.ScrollBar.vertical: PC3.ScrollBar {id: verticalScrollBarparent: rootz: 2height: root.heightanchors.right: parent.right}Kirigami.WheelHandler {id: wheelHandlertarget: viewfilterMouseEvents: true// `20 * Qt.styleHints.wheelScrollLines` is the default speed.horizontalStepSize: 20 * Qt.styleHints.wheelScrollLinesverticalStepSize: 20 * Qt.styleHints.wheelScrollLinesonWheel: wheel => {view.movedWithWheel = trueview.movedWithKeyboard = falsemovedWithWheelTimer.restart()}}Connections {target: kickofffunction onExpandedChanged() {if (!kickoff.expanded) {view.currentIndex = 0view.positionViewAtBeginning()}}}// Used to block hover events temporarily after using keyboard navigation.// If you have one hand on the touch pad or mouse and another hand on the keyboard,// it's easy to accidentally reset the highlight/focus position to the mouse position.Timer {id: movedWithKeyboardTimerinterval: 200onTriggered: view.movedWithKeyboard = false}Timer {id: movedWithWheelTimerinterval: 200onTriggered: view.movedWithWheel = false}function focusCurrentItem(event, focusReason) {currentItem.forceActiveFocus(focusReason)positionViewAtIndex(currentIndex, GridView.Contain)event.accepted = true}Keys.onMenuPressed: event => {const delegate = currentItem as AbstractKickoffItemDelegate;if (delegate !== null) {delegate.forceActiveFocus(Qt.ShortcutFocusReason)delegate.openActionMenu()}}Keys.onPressed: event => {const targetX = currentItem ? currentItem.x : contentXlet targetY = currentItem ? currentItem.y : contentYlet targetIndex = currentIndex// supports mirroringconst atLeft = currentIndex % columns === (Application.layoutDirection == Qt.RightToLeft ? columns - 1 : 0)// at the beginning of a lineconst isLeading = currentIndex % columns === 0// at the top of a given column and in the top rowconst atTop = currentIndex < columns// supports mirroringconst atRight = currentIndex % columns === (Application.layoutDirection == Qt.RightToLeft ? 0 : columns - 1)// at the end of a lineconst isTrailing = currentIndex % columns === columns - 1// at bottom of a given column, not necessarily in the last rowlet atBottom = currentIndex >= count - columns// Implements the keyboard navigation described in https://www.w3.org/TR/wai-aria-practices-1.2/#gridif (count > 0 && (event.key == Qt.Key_Return || event.key == Qt.Key_Enter)) {(root.currentItem as AbstractKickoffItemDelegate).action.triggered();root.currentItem.forceActiveFocus(Qt.ShortcutFocusReason);event.accepted = true;} else if (count > 1) {switch (event.key) {case Qt.Key_Left: if (!atLeft && !kickoff.searchField.activeFocus) {moveCurrentIndexLeft()focusCurrentItem(event, Qt.BacktabFocusReason)} breakcase Qt.Key_H: if (!atLeft && !kickoff.searchField.activeFocus && event.modifiers & Qt.ControlModifier) {moveCurrentIndexLeft()focusCurrentItem(event, Qt.BacktabFocusReason)} breakcase Qt.Key_Up: if (!atTop) {moveCurrentIndexUp()focusCurrentItem(event, Qt.BacktabFocusReason)} breakcase Qt.Key_K: if (!atTop && event.modifiers & Qt.ControlModifier) {moveCurrentIndexUp()focusCurrentItem(event, Qt.BacktabFocusReason)} breakcase Qt.Key_Right: if (!atRight && !kickoff.searchField.activeFocus) {moveCurrentIndexRight()focusCurrentItem(event, Qt.TabFocusReason)} breakcase Qt.Key_L: if (!atRight && !kickoff.searchField.activeFocus && event.modifiers & Qt.ControlModifier) {moveCurrentIndexRight()focusCurrentItem(event, Qt.TabFocusReason)} breakcase Qt.Key_Down: if (!atBottom) {moveCurrentIndexDown()focusCurrentItem(event, Qt.TabFocusReason)} breakcase Qt.Key_J: if (!atBottom && event.modifiers & Qt.ControlModifier) {moveCurrentIndexDown()focusCurrentItem(event, Qt.TabFocusReason)} breakcase Qt.Key_Home: if (event.modifiers === Qt.ControlModifier && currentIndex !== 0) {currentIndex = 0focusCurrentItem(event, Qt.BacktabFocusReason)} else if (!isLeading) {targetIndex -= currentIndex % columnscurrentIndex = Math.max(targetIndex, 0)focusCurrentItem(event, Qt.BacktabFocusReason)} breakcase Qt.Key_End: if (event.modifiers === Qt.ControlModifier && currentIndex !== count - 1) {currentIndex = count - 1focusCurrentItem(event, Qt.TabFocusReason)} else if (!isTrailing) {targetIndex += columns - 1 - (currentIndex % columns)currentIndex = Math.min(targetIndex, count - 1)focusCurrentItem(event, Qt.TabFocusReason)} breakcase Qt.Key_PageUp: if (!atTop) {targetY = targetY - height + 1targetIndex = indexAt(targetX, targetY)// TODO: Find a more efficient, but accurate way to do thiswhile (targetIndex === -1) {targetY += 1targetIndex = indexAt(targetX, targetY)}currentIndex = Math.max(targetIndex, 0)focusCurrentItem(event, Qt.BacktabFocusReason)} breakcase Qt.Key_PageDown: if (!atBottom) {targetY = targetY + height - 1targetIndex = indexAt(targetX, targetY)// TODO: Find a more efficient, but accurate way to do thiswhile (targetIndex === -1) {targetY -= 1targetIndex = indexAt(targetX, targetY)}currentIndex = Math.min(targetIndex, count - 1)focusCurrentItem(event, Qt.TabFocusReason)} break}}movedWithKeyboard = event.acceptedif (movedWithKeyboard) {movedWithKeyboardTimer.restart()}}}}