1/*
2 * This file is part of the KDE Milou Project
3 * SPDX-FileCopyrightText: 2013-2014 Vishesh Handa <me@vhanda.in>
4 *
5 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6 *
7 */
8
9import QtQuick 2.1
10
11import org.kde.plasma.components 2.0 as PlasmaComponents
12import org.kde.plasma.core 2.0 as PlasmaCore
13import org.kde.milou 0.3 as Milou
14
15ListView {
16    id: listView
17    property alias queryString: resultModel.queryString
18    property alias runner: resultModel.runner
19    property alias runnerManager: resultModel.runnerManager
20
21    property alias runnerName: resultModel.runnerName
22    property alias runnerIcon: resultModel.runnerIcon
23    property alias querying: resultModel.querying
24    property alias limit: resultModel.limit
25    property bool reversed
26    signal activated
27    signal updateQueryString(string text, int cursorPosition)
28
29    // NOTE this also flips increment/decrementCurrentIndex (Bug 360789)
30    verticalLayoutDirection: reversed ? ListView.BottomToTop : ListView.TopToBottom
31    keyNavigationWraps: true
32    highlight: PlasmaComponents.Highlight {}
33    highlightMoveDuration: 0
34    activeFocusOnTab: true
35    Accessible.role: Accessible.List
36
37    section {
38        criteria: ViewSection.FullString
39        property: "category"
40    }
41
42    // This is used to keep track if the user has pressed enter before
43    // the first result has been shown, in the case the first result should
44    // be run when the model is populated
45    property bool runAutomatically
46
47    // This is used to disable mouse selection if the user interacts only with keyboard
48    property bool moved: false
49    property point savedMousePosition: Milou.MouseHelper.globalMousePosition()
50    function mouseMovedGlobally() {
51        return savedMousePosition != Milou.MouseHelper.globalMousePosition();
52    }
53
54    Milou.DragHelper {
55        id: dragHelper
56        dragIconSize: units.iconSizes.medium
57    }
58
59    model: Milou.ResultsModel {
60        id: resultModel
61        limit: 15
62        onQueryStringChangeRequested: {
63            listView.updateQueryString(queryString, pos)
64        }
65        Component.onCompleted: {
66            if (typeof runnerWindow !== "undefined") {
67                runnerWindow.runnerManager = runnerManager
68            }
69        }
70        onQueryStringChanged: resetView()
71        onModelReset: resetView()
72
73        onRowsInserted: {
74            // Keep the selection at the top as items inserted to the beginning will shift it downwards
75            // ListView will update its view after this signal is processed and then our callLater will set it back
76            if (listView.currentIndex === 0) {
77                Qt.callLater(function() {
78                    listView.currentIndex = 0;
79                });
80            }
81
82            if (runAutomatically) {
83                // This needs to be delayed as running a result may close the window and clear the query
84                // having us reset the model whilst in the middle of processing the insertion.
85                // The proxy model chain that comes after us really doesn't like this.
86                Qt.callLater(function() {
87                    resultModel.run(resultModel.index(0, 0));
88                    listView.activated();
89                });
90
91                runAutomatically = false;
92            }
93        }
94
95        function resetView() {
96            listView.currentIndex = 0;
97            listView.moved = false;
98            listView.savedMousePosition = Milou.MouseHelper.globalMousePosition();
99        }
100    }
101
102    delegate: ResultDelegate {
103        id: resultDelegate
104        width: listView.width
105        reversed: listView.reversed
106    }
107
108    //
109    // vHanda: Ideally this should have gotten handled in the delagte's onReturnPressed
110    // code, but the ListView doesn't seem forward keyboard events to the delgate when
111    // it is not in activeFocus. Even manually adding Keys.forwardTo: resultDelegate
112    // doesn't make any difference!
113    Keys.onReturnPressed: runCurrentIndex(event);
114    Keys.onEnterPressed: runCurrentIndex(event);
115
116    function runCurrentIndex(event) {
117        if (!currentItem) {
118            runAutomatically = true
119            return;
120        } else {
121            // If user presses Shift+Return to invoke an action, invoke the first runner action
122            if (event && event.modifiers === Qt.ShiftModifier
123                    && currentItem.additionalActions && currentItem.additionalActions.length > 0) {
124                runAction(0)
125                return
126            }
127
128            if (currentItem.activeAction > -1) {
129                runAction(currentItem.activeAction)
130                return
131            }
132
133            if (resultModel.run(resultModel.index(currentIndex, 0))) {
134                activated()
135            }
136            runAutomatically = false
137        }
138    }
139
140    function runAction(index) {
141        if (resultModel.runAction(resultModel.index(currentIndex, 0), index)) {
142            activated()
143        }
144    }
145
146    onActiveFocusChanged: {
147        if (!activeFocus && currentIndex == listView.count-1) {
148            currentIndex = 0;
149        }
150    }
151
152    Keys.onTabPressed: {
153        if (!currentItem || !currentItem.activateNextAction()) {
154            if (reversed) {
155                if (currentIndex == 0) {
156                    listView.nextItemInFocusChain(false).forceActiveFocus();
157                    return;
158                }
159                decrementCurrentIndex()
160            } else {
161                if (currentIndex == listView.count-1) {
162                    listView.nextItemInFocusChain(true).forceActiveFocus();
163                    return;
164                }
165                incrementCurrentIndex()
166            }
167        }
168    }
169    Keys.onBacktabPressed: {
170        if (!currentItem || !currentItem.activatePreviousAction()) {
171            if (reversed) {
172                if (currentIndex == listView.count-1) {
173                    listView.nextItemInFocusChain(true).forceActiveFocus();
174                    return;
175                }
176                incrementCurrentIndex()
177            } else {
178                if (currentIndex == 0) {
179                    listView.nextItemInFocusChain(false).forceActiveFocus();
180                    return;
181                }
182                decrementCurrentIndex()
183            }
184
185            // activate previous action cannot know whether we want to back tab from an action
186            // to the main result or back tab from another search result, so we explicitly highlight
187            // the last action here to provide a consistent navigation experience
188            if (currentItem) {
189                currentItem.activateLastAction()
190            }
191        }
192    }
193    Keys.onUpPressed: reversed ? incrementCurrentIndex() : decrementCurrentIndex();
194    Keys.onDownPressed: reversed ? decrementCurrentIndex() : incrementCurrentIndex();
195
196    boundsBehavior: Flickable.StopAtBounds
197
198    function loadSettings() {
199        resultModel.loadSettings()
200    }
201
202    function setQueryString(queryString) {
203        resultModel.queryString = queryString
204        runAutomatically = false
205    }
206}
207