/*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: trueLayout.minimumWidth: Math.min(Math.max(mainRow.Layout.minimumWidth, mainRow.implicitWidth), Screen.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(Screen.height * 0.8))Layout.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;}RowLayout {id: mainRowanchors.fill: parentspacing: 0readonly property int minimumMainWidth: Math.max(searchField.defaultWidth, runnerColumns.searchResultsPresent ? 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))KSvg.FrameSvgItem {id: sideBarproperty 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.actualScrollBarWidthimplicitHeight: 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: parentPlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOffcontentWidth: availableWidthreadonly property int actualScrollBarWidth: scrollBarVisible ? sideBarScrollView.PlasmaComponents3.ScrollBar.vertical.width : 0property bool scrollBarVisibleBinding on scrollBarVisible {value: Screen.width > 0 && sideBarScrollView.contentHeight > (Screen.width - Kirigami.Units.largeSpacing * 4)delayed: true // this needs to be delayed or it can get stuck in a resize loop}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: sideBarLayoutheight: Math.max(implicitHeight, sideBarScrollView.height - anchors.topMargin - anchors.bottomMargin)anchors.top: parent.topanchors.left: parent.leftanchors.right: parent.rightanchors.topMargin: sideBar.margins.topanchors.leftMargin: sideBar.margins.leftanchors.rightMargin: sideBar.margins.rightAccessible.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 : favoriteSystemActionsLayout.bottomMargin: sideBar.margins.bottom}SideBarSection {id: favoriteAppsLayout.fillHeight: trueLayout.alignment: Qt.AlignHCenterKeyNavigation.up: favoriteSystemActions.bottomSideBarItemKeyNavigation.down: favoriteSystemActionsmodel: root.globalFavoritesonItemFocused: 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.systemFavoritesonItemFocused: 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.rootModelLayoutMirroring.enabled: mainRow.LayoutMirroring.enabledshowSeparators: true // keep even if sorted, the one between recents and categories worksonInteractionConcluded: root.interactionConcluded()onKeyNavigationAtListEnd: {searchField.focus = true;}onNavigateLeftRequested: {if (!sideBar.visible) {return}currentIndex = -1root.focusSideBar()}Component.onCompleted: {rootList.exited.connect(root.reset);}}RowLayout {id: runnerColumnsreadonly property bool searchResultsPresent: runnerColumns.visibleChildren[0] instanceof RunnerResultsListLayout.fillHeight: truevisible: searchField.text !== "" && root.runnerModel.count > 0spacing: Kirigami.Units.smallSpacingLayoutMirroring.enabled: mainRow.LayoutMirroring.enabledRepeater {id: runnerColumnsRepeatermodel: root.runnerModeldelegate: RunnerResultsList {id: runnerMatchesLayout.fillHeight: trueLayout.fillWidth: truevisible: model.count > 0 || (model.querying && visible)model: root.runnerModel.modelForRow(index)mainSearchField: searchFieldLayoutMirroring.enabled: runnerColumns.LayoutMirroring.enabledfunction 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)}}onFocusChanged: {if (!focus) {currentIndex = -1;}}onInteractionConcluded: root.interactionConcluded()onNavigateLeftRequested: navigateToAdjacentColumn(false)onNavigateRightRequested: navigateToAdjacentColumn(true)}}}PlasmaExtras.PlaceholderMessage {id: noMatchesPlaceholderproperty bool searchRunning: falseproperty string lastQuery: "" // copy to avoid timing conflicts with visible bindingLayout.minimumWidth: mainRow.minimumMainWidthLayout.alignment: Qt.AlignHCenter | Qt.AlignVCentervisible: lastQuery !== "" && !runnerColumns.searchResultsPresent && (!searchRunning || visible)iconName: "edit-none"text: i18nc("@info:status", "No matches")Connections {target: root.runnerModelfunction onQueryFinished() {noMatchesPlaceholder.searchRunning = false}}Connections {target: searchFieldfunction onTextChanged() {noMatchesPlaceholder.searchRunning = searchField.text !== ""noMatchesPlaceholder.lastQuery = searchField.text}}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 && runnerColumns.searchResultsPresent? runnerColumns.visibleChildren[0].width: (rootList.visible ? rootList.width : mainRow.minimumMainWidth)focus: !Kirigami.InputMethod.willShowOnActiveonTextChanged: {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 (runnerColumns.visible) {for (let i = 0; i < root.runnerModel.count; ++i) {if (root.runnerModel.modelForRow(i).count) {root.runnerModel.modelForRow(i).trigger(0, "", null);onInteractionConcluded: root.interactionConcluded()break;}}}}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();}MouseArea {id: hoverBlock // don't hover-activate until mouse is moved to not interfere with keyboard useanchors.fill: parenthoverEnabled: truepropagateComposedEvents: true // clicking should still work if hovering is blockedproperty bool mouseMoved: falsefunction reset() {mouseMoved = falseenabled = true}onPositionChanged: if (!mouseMoved) {mouseMoved = true} else {enabled = false // this immediately triggers other hover events when bound to their hoverEnabled}onPressed: event => {enabled = falseevent.accepted = false}}}