/*SPDX-FileCopyrightText: 2013-2015 Eike Hein <hein@kde.org>SPDX-License-Identifier: GPL-2.0-or-later*/pragma ComponentBehavior: Boundimport QtQuickimport QtQuick.Layoutsimport org.kde.kirigami as Kirigamiimport org.kde.ksvg as KSvgimport org.kde.plasma.components as PlasmaComponents3import org.kde.plasma.core as PlasmaCoreimport org.kde.plasma.extras as PlasmaExtrasimport org.kde.plasma.plasmoidimport org.kde.plasma.private.kicker as KickerPlasmaComponents3.ScrollView {id: rootrequired property Kicker.KAStatsFavoritesModel globalFavoritesrequired property Kicker.SimpleFavoritesModel systemFavoritesrequired property Kicker.RootModel rootModelrequired property Kicker.RunnerModel runnerModelsignal interactionConcludedfocus: true// we never want a vertical scrollbar, the components handle those.PlasmaComponents3.ScrollBar.vertical.policy: PlasmaComponents3.ScrollBar.AlwaysOff// needs to be set explicitly as the default can cause loopsPlasmaComponents3.ScrollBar.horizontal.visible: Plasmoid.containment.screenGeometry.width > 0 && contentWidth > (Plasmoid.containment.screenGeometry.width - Kirigami.Units.largeSpacing * 4)// ScrollBar's padding is only to make space for the scrollbar, so we don't need it hereLayout.minimumWidth: Math.min(Math.max(mainRow.Layout.minimumWidth, mainRow.implicitWidth), Plasmoid.containment.screenGeometry.width - Kirigami.Units.largeSpacing * 4)Layout.maximumWidth: Layout.minimumWidthcontentWidth: mainRow.implicitWidthLayout.minimumHeight: Math.min(Math.max(sideBar.implicitHeight, rootList.implicitHeight + rootList.Layout.bottomMargin), Math.round(Plasmoid.containment.screenGeometry.height * 0.9)) + topPadding + bottomPaddingLayout.maximumHeight: Layout.minimumHeightfunction ensureVisible(item: Item) : void {const actualItemX = item.xconst viewXPosition = (item.width <= contentItem.width)? Math.round(actualItemX + item.width / 2 - contentItem.width / 2): actualItemXconst flickable = contentItem as Flickableif (actualItemX < flickable.contentX) {flickable.contentX = Math.max(0, viewXPosition)} else if ((actualItemX + item.width) > (flickable.contentX + flickable.width)) {flickable.contentX = Math.min(flickable.contentWidth - flickable.width, viewXPosition)}flickable.returnToBounds()}function focusRunnerColumn(column, focusTopElement = true) : void {if (runnerColumns.visibleChildren.length <= column + 1) { // visibleChildren includes repeaterreturn}const targetList = runnerColumns.visibleChildren[column]targetList.currentIndex = focusTopElement ? 0 : (targetList.count - 1)targetList.giveFocus(Qt.TabFocusReason)root.ensureVisible(targetList)}function focusRootList(focusTopElement = true) : void {rootList.currentIndex = focusTopElement ? 0 : (rootList.model.count - 1)rootList.forceActiveFocus(Qt.TabFocusReason)}function focusSideBar() : void {sideBar.forceActiveFocus(Qt.TabFocusReason)root.ensureVisible(sideBar)}function reset() {rootList.currentIndex = -1;hoverBlock.reset();searchField.text = "";searchField.focus = true;}DropArea {id: unfavoriteDropAreaanchors.fill: parentkeys: ["favoritedrag"]onDropped: drop => {let draggedItem = drag.source as SideBarItemif (draggedItem && draggedItem.favoritesModel.isFavorite(draggedItem.favoriteId)) {draggedItem.showUnfavoritePlaceholder = truedraggedItem.favoritesModel.removeFavorite(draggedItem.favoriteId)drop.accept(Qt.MoveAction)}}}RowLayout {id: mainRowanchors.fill: parentspacing: 0readonly property int minimumMainWidth: Math.max(searchField.defaultWidth, root.runnerModel.resultsPresent ? 0 : Math.min(rootList.implicitWidth, rootList.Layout.maximumWidth))Layout.minimumWidth: (sideBar.visible ? sideBar.implicitWidth + sideBar.Layout.rightMargin : 0) + minimumMainWidthLayoutMirroring.enabled: ((Plasmoid.location === PlasmaCore.Types.RightEdge)|| (Application.layoutDirection === Qt.RightToLeft && Plasmoid.location !== PlasmaCore.Types.LeftEdge))LayoutMirroring.childrenInherit: trueKSvg.FrameSvgItem {id: sideBarreadonly property real innerMargin: 2readonly property bool onTopPanel: Plasmoid.location === PlasmaCore.Types.TopEdgevisible: (root.globalFavorites.count + root.systemFavorites.count) > 0Layout.fillHeight: trueLayout.rightMargin: Kirigami.Units.smallSpacingimplicitWidth: Math.max(favoriteApps.implicitWidth, favoriteSystemActions.implicitWidth) + margins.left + margins.right + sideBarScrollView.leftPadding + sideBarScrollView.rightPaddingimplicitHeight: sideBarLayout.implicitHeight + margins.top + margins.bottomimagePath: "widgets/frame"prefix: "plain"function handleLeftRightArrow(event: KeyEvent) : void {let backArrowKey = (event.key === Qt.Key_Left && !mainRow.LayoutMirroring.enabled) ||(event.key === Qt.Key_Right && mainRow.LayoutMirroring.enabled)let forwardArrowKey = (event.key === Qt.Key_Right && !mainRow.LayoutMirroring.enabled) ||(event.key === Qt.Key_Left && mainRow.LayoutMirroring.enabled)if (backArrowKey & runnerColumns.visibleChildren.length > 1) {root.focusRunnerColumn(runnerColumns.visibleChildren.length - 2, true)} else if (forwardArrowKey) {if (runnerColumns.visibleChildren.length > 1) {root.focusRunnerColumn(0, true)} else if (rootList.visible) {root.focusRootList(true)} else {searchField.forceActiveFocus(Qt.TabFocusReason)}} else {event.accepted = false}}activeFocusOnTab: trueonActiveFocusChanged: if (activeFocus) {let target = (onTopPanel && favoriteSystemActions.model.count) ? favoriteSystemActions : favoriteAppstarget.forceActiveFocus(Qt.TabFocusReason)}KeyNavigation.right: rootListPlasmaComponents3.ScrollView {id: sideBarScrollViewanchors {fill: parenttopMargin: sideBar.innerMarginbottomMargin: sideBar.innerMargin}PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOffPlasmaComponents3.ScrollBar.vertical.visible: Plasmoid.containment.screenGeometry.height > 0 && sideBarScrollView.contentHeight > Math.round(Plasmoid.containment.screenGeometry.height * 0.9)contentWidth: availableWidthcontentHeight: Math.max(sideBarLayout.implicitHeight + sideBarLayout.anchors.topMargin + sideBarLayout.anchors.bottomMargin, sideBar.height - 2 * sideBar.innerMargin)function ensureVisible(item: Item) {let flickable = (contentItem as Flickable)let actualItemY = flickable.mapFromItem(item, 0, 0).ylet viewYPosition = (item.height <= contentItem.height)? Math.round(actualItemY + item.height / 2 - contentItem.height / 2): actualItemYif (actualItemY < flickable.contentY) {flickable.contentY = Math.max(0, viewYPosition)} else if ((actualItemY + item.height) > (flickable.contentY + flickable.height)) {flickable.contentY = Math.min(flickable.contentHeight - flickable.height, viewYPosition)}flickable.returnToBounds()}ColumnLayout {id: sideBarLayoutanchors {fill: parenttopMargin: sideBar.margins.top - sideBar.innerMarginleftMargin: sideBar.margins.leftrightMargin: sideBar.margins.rightbottomMargin: sideBar.margins.bottom - sideBar.innerMargin}Accessible.role: Accessible.ListAccessible.name: i18nc("@title:group accessible name for favorite group in sidebar", "Favorites")Keys.onLeftPressed: event => sideBar.handleLeftRightArrow(event)Keys.onRightPressed: event => sideBar.handleLeftRightArrow(event)LayoutItemProxy {target: sideBar.onTopPanel ? favoriteSystemActions : favoriteApps}KSvg.SvgItem {id: sidebarSeparatorLayout.fillWidth: trueLayout.leftMargin: Kirigami.Units.smallSpacingLayout.rightMargin: Layout.leftMarginLayout.alignment: Qt.AlignHCentervisible: (favoriteApps.model && favoriteApps.model.count&& favoriteSystemActions.model && favoriteSystemActions.model.count)imagePath: "widgets/line"elementId: "horizontal-line"}LayoutItemProxy {target: sideBar.onTopPanel ? favoriteApps : favoriteSystemActions}SideBarSection {id: favoriteAppsLayout.fillHeight: trueLayout.alignment: Qt.AlignHCenterKeyNavigation.up: favoriteSystemActions.bottomSideBarItemKeyNavigation.down: favoriteSystemActionsmodel: root.globalFavoritesfavoriteType: SideBarSection.FavoriteType.KAStatsonItemFocused: item => sideBarScrollView.ensureVisible(item)onInteractionConcluded: root.interactionConcluded()Binding {target: root.globalFavoritesproperty: "iconSize"value: Kirigami.Units.iconSizes.mediumrestoreMode: Binding.RestoreBinding}}SideBarSection {id: favoriteSystemActionsLayout.alignment: Qt.AlignHCentermodel: root.systemFavoritesfavoriteType: SideBarSection.FavoriteType.SimpleonItemFocused: item => sideBarScrollView.ensureVisible(item)onInteractionConcluded: root.interactionConcluded()KeyNavigation.up: favoriteApps.bottomSideBarItemKeyNavigation.down: favoriteApps}}}}ItemListView {id: rootListLayout.alignment: Qt.AlignTopLayout.bottomMargin: searchField.implicitHeight + Kirigami.Units.smallSpacingLayout.fillHeight: trueLayout.fillWidth: trueLayout.maximumHeight: implicitHeightvisible: searchField.text === ""iconsEnabled: Plasmoid.configuration.showIconsRootLevelhoverEnabled: !hoverBlock.enabledmainSearchField: searchFieldmodel: root.rootModelonExited: rootList.currentIndex = -1onInteractionConcluded: root.interactionConcluded()onKeyNavigationAtListEnd: {searchField.focus = true;}onNavigateLeftRequested: {if (!sideBar.visible) {return}currentIndex = -1root.focusSideBar()}}RowLayout {id: runnerColumnsLayout.fillHeight: truevisible: searchField.text !== "" && root.runnerModel.count > 0 && !initialDelayTimer.active && root.runnerModel.resultsPresentspacing: 0Timer {property bool active: falseid: initialDelayTimerinterval: 250 // match KRunner's delay for multi-runner queriesonRunningChanged: if (running && !root.runnerModel.resultsPresent) { active = true }onTriggered: active = false}Repeater {id: runnerColumnsRepeatermodel: root.runnerModeldelegate: RunnerResultsList {id: runnerMatchesLayout.fillHeight: trueLayout.fillWidth: truevisible: model.count > 0 || (model.querying && visible)hoverEnabled: !hoverBlock.enabledmodel: root.runnerModel.modelForRow(index)mainSearchField: searchFieldfunction navigateToAdjacentColumn(forward = true) {let currentColumnIndex = runnerColumns.visibleChildren.indexOf(runnerMatches)let targetColumnIndex = currentColumnIndex + (forward ? 1 : -1)if (currentColumnIndex < 0 || runnerColumns.visibleChildren.length <= 2) {return}currentIndex = -1if (targetColumnIndex >= 0 && targetColumnIndex < (runnerColumns.visibleChildren.length - 1)) {root.focusRunnerColumn(targetColumnIndex)} else if (sideBar.visible) {root.focusSideBar()} else {root.focusRunnerColumn(forward ? 0 : runnerColumns.visibleChildren.length - 2)}}onListActiveFocusChanged: {if (!listActiveFocus) {currentIndex = -1;}}onInteractionConcluded: root.interactionConcluded()onNavigateLeftRequested: navigateToAdjacentColumn(false)onNavigateRightRequested: navigateToAdjacentColumn(true)}}}PlasmaExtras.PlaceholderMessage {id: noMatchesPlaceholderLayout.minimumWidth: mainRow.minimumMainWidthLayout.alignment: Qt.AlignHCenter | Qt.AlignVCentervisible: root.runnerModel.query !== "" && !root.runnerModel.resultsPresent && ((!root.runnerModel.querying && !initialDelayTimer.active) || visible)iconName: "edit-none"text: i18nc("@info:status", "No matches")Binding {searchField.width: noMatchesPlaceholder.widthwhen: noMatchesPlaceholder.visible}}}PlasmaExtras.SearchField {id: searchFieldreadonly property int spacing: sideBar.width ? sideBar.Layout.rightMargin : 0anchors.bottom: mainRow.bottomanchors.left: parent.leftanchors.leftMargin: sideBar.visible ? sideBar.width + spacing : 0readonly property real defaultWidth: Kirigami.Units.gridUnit * 14width: runnerColumns.visible && root.runnerModel.resultsPresent? runnerColumns.visibleChildren[0].width - (runnerColumns.visibleChildren.length > 2 ? Kirigami.Units.smallSpacing : 0): (rootList.visible ? rootList.width : mainRow.minimumMainWidth)focus: !Kirigami.InputMethod.willShowOnActiveonTextChanged: {initialDelayTimer.restart()root.runnerModel.query = text;}onFocusChanged: {if (focus) {// FIXME: Cleanup arbitration between rootList/runnerCols here and in Keys.if (rootList.visible) {rootList.currentIndex = -1;}if (runnerColumns.visible) {runnerColumnsRepeater.itemAt(0).currentIndex = -1;}}}states: [State {name: "right"when: (Plasmoid.location === PlasmaCore.Types.RightEdge && Application.layoutDirection === Qt.LeftToRight)|| (Plasmoid.location === PlasmaCore.Types.LeftEdge && Application.layoutDirection === Qt.RightToLeft)AnchorChanges {target: searchFieldanchors.top: undefinedanchors.bottom: mainRow.bottomanchors.left: undefinedanchors.right: parent.right}PropertyChanges {searchField.anchors.leftMargin: undefinedsearchField.anchors.rightMargin: sideBar.visible ? sideBar.width + spacing : 0}}]function handleUpDownArrow(event: KeyEvent) : void {if (rootList.visible) {root.focusRootList(event.key === Qt.Key_Down);} else if (runnerColumns.visible) {let index = runnerColumns.visibleChildren[0].currentIndexroot.focusRunnerColumn(0, event.key === Qt.Key_Down)// First column, first entry is initially selected even when focus is on the search// field, as Return will activate it. Down should immediately go to the second entry.if (index === 0 && event.key === Qt.Key_Down) {runnerColumns.visibleChildren[0].currentIndex = Math.min(index + 1, runnerColumns.visibleChildren[0].count - 1)}} else {event.accepted = false}}function handleLeftRightArrow(event: KeyEvent) : void {let backArrowKey = (event.key === Qt.Key_Left && !mainRow.LayoutMirroring.enabled) ||(event.key === Qt.Key_Right && mainRow.LayoutMirroring.enabled)let forwardArrowKey = (event.key === Qt.Key_Right && !mainRow.LayoutMirroring.enabled) ||(event.key === Qt.Key_Left && mainRow.LayoutMirroring.enabled)if (backArrowKey) {if (!sideBar.visible && !runnerColumns.visible) {return;}if (runnerColumns.visibleChildren[0]?.length > 1) {runnerColumns.visibleChildren[0].currentIndex = -1;}if (sideBar.visible) {root.focusSideBar(true)} else {root.focusRunnerColumn(runnerColumns.visibleChildren.length - 2, true)}} else if (forwardArrowKey && runnerColumns.visibleChildren.length > 2) {runnerColumns.visibleChildren[0].currentIndex = -1;root.focusRunnerColumn(1, true)} else {event.accepted = false}}function launchBestMatch() : void {if (launchMatchTimer.running) {launchMatchTimer.stop()launchMatchTimer.triggered()return}if (!root.runnerModel.querying || runnerColumns.visibleChildren[0]?.currentItem?.text.toLowerCase().includes(root.runnerModel.query.toLowerCase())) {launchMatchTimer.triggered()return}launchMatchTimer.start()}Timer {id: launchMatchTimerinterval: 750onTriggered: runnerColumns.visibleChildren[0]?.currentItem?.action.trigger()}Connections {target: runnerColumns.visibleChildren[0] instanceof RunnerResultsList ? runnerColumns.visibleChildren[0] : nullenabled: launchMatchTimer.runningfunction onCurrentItemChanged() : void {if (runnerColumns.visibleChildren[0]?.currentItem?.text.toLowerCase().includes(root.runnerModel.query.toLowerCase())) {launchMatchTimer.stop()Qt.callLater(launchMatchTimer.triggered) // callLater to resolve spurious binding loop}}}Connections {target: root.runnerModelenabled: launchMatchTimer.running || initialDelayTimer.activefunction onQueryFinished() : void {// queryFinish may be emitted for intermediate queries, ignore thoseif (root.runnerModel.querying) { return }initialDelayTimer.triggered()if (!launchMatchTimer.running) { return }launchMatchTimer.stop()Qt.callLater(launchMatchTimer.triggered) // callLater to give bindings time to update}}Keys.priority: Keys.AfterItem // arrow keys should move cursor firstKeys.onUpPressed: event => handleUpDownArrow(event)Keys.onDownPressed: event => handleUpDownArrow(event)Keys.onLeftPressed: event => handleLeftRightArrow(event)Keys.onRightPressed: event => handleLeftRightArrow(event)Keys.onEnterPressed: launchBestMatch()Keys.onReturnPressed: launchBestMatch()Keys.onEscapePressed: root.interactionConcluded()function appendText(newText) {focus = true;text = text + newText;}}Component.onCompleted: {kicker.modelRefreshed.connect(() => {const searchTerm = searchField.textroot.reset()runnerModel.clear() // runnerModel is on a timer, clearing and setting query is too fast to worksearchField.text = searchTerm});kicker.reset.connect(reset)rootModel.refresh();}HoverBlocker {id: hoverBlockanchors.fill: parentonWidthChanged: hoverBlock.reset()Connections {target: root.runnerModelfunction onQueryChanged() : void {hoverBlock.reset()}}}}