1/*
2    SPDX-FileCopyrightText: 2014 Marco Martin <mart@kde.org>
3
4    SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7import QtQuick 2.6
8import QtQuick.Layouts 1.1
9import QtQuick.Window 2.1
10import org.kde.plasma.core 2.0 as PlasmaCore
11import org.kde.plasma.components 2.0 as PlasmaComponents // For Highlight
12import org.kde.plasma.components 3.0 as PlasmaComponents3
13import org.kde.plasma.extras 2.0 as PlasmaExtras
14import org.kde.milou 0.1 as Milou
15
16ColumnLayout {
17    id: root
18    property string query
19    property string runner
20    property bool showHistory: false
21    property alias runnerManager: results.runnerManager
22
23    LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft
24    LayoutMirroring.childrenInherit: true
25
26    onQueryChanged: {
27        queryField.text = query;
28    }
29
30    Connections {
31        target: runnerWindow
32        function onVisibleChanged() {
33            if (runnerWindow.visible) {
34                queryField.forceActiveFocus();
35                listView.currentIndex = -1
36                if (runnerManager.retainPriorSearch) {
37                    // If we manually specified a query(D-Bus invocation) we don't want to retain the prior search
38                    if (!query) {
39                        queryField.text = runnerManager.priorSearch
40                        queryField.select(root.query.length, 0)
41                    }
42                }
43            } else {
44                if (runnerManager.retainPriorSearch) {
45                    runnerManager.priorSearch = root.query
46                }
47                root.runner = ""
48                root.query = ""
49                root.showHistory = false
50            }
51        }
52    }
53
54    Connections {
55        target: root
56        function onShowHistoryChanged() {
57            if (showHistory) {
58                // we store 50 entries in the history but only show 20 in the UI so it doesn't get too huge
59                listView.model = runnerManager.history.slice(0, 20)
60            } else {
61                listView.model = []
62            }
63        }
64    }
65
66    RowLayout {
67        Layout.alignment: Qt.AlignTop
68        PlasmaComponents3.ToolButton {
69            icon.name: "configure"
70            onClicked: {
71                runnerWindow.visible = false
72                runnerWindow.displayConfiguration()
73            }
74            Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Configure")
75            Accessible.description: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Configure Search Plugins")
76            visible: runnerWindow.canConfigure
77            PlasmaComponents3.ToolTip {
78                text: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Configure KRunner…")
79            }
80        }
81        PlasmaComponents3.TextField {
82            id: queryField
83            property bool allowCompletion: false
84
85            clearButtonShown: true
86            Layout.minimumWidth: PlasmaCore.Units.gridUnit * 25
87            Layout.maximumWidth: PlasmaCore.Units.gridUnit * 25
88
89            inputMethodHints: Qt.ImhNoPredictiveText
90
91            activeFocusOnPress: true
92            placeholderText: results.runnerName ? i18ndc("plasma_lookandfeel_org.kde.lookandfeel",
93                                                         "Textfield placeholder text, query specific KRunner",
94                                                         "Search '%1'…", results.runnerName)
95                                                : i18ndc("plasma_lookandfeel_org.kde.lookandfeel",
96                                                         "Textfield placeholder text", "Search…")
97
98            PlasmaComponents3.BusyIndicator {
99                anchors {
100                    right: parent.right
101                    top: parent.top
102                    bottom: parent.bottom
103                    margins: PlasmaCore.Units.smallSpacing
104                    rightMargin: height
105                }
106
107                Timer {
108                    id: queryTimer
109                    property bool queryDisplay: false
110                    running: results.querying
111                    repeat: true
112                    onRunningChanged: if (queryDisplay && !running) {
113                        queryDisplay = false
114                    }
115                    onTriggered: if (!queryDisplay) {
116                        queryDisplay = true
117                    }
118                    interval: 500
119                }
120
121                running: queryTimer.queryDisplay
122            }
123            function move_up() {
124                if (length === 0) {
125                    root.showHistory = true;
126                    if (listView.count > 0) {
127                        listView.forceActiveFocus();
128                    }
129                } else if (results.count > 0) {
130                    results.forceActiveFocus();
131                    results.decrementCurrentIndex();
132                }
133            }
134
135            function move_down() {
136                if (length === 0) {
137                    root.showHistory = true;
138                    if (listView.count > 0) {
139                        listView.forceActiveFocus();
140                    }
141                } else if (results.count > 0) {
142                    results.forceActiveFocus();
143                    results.incrementCurrentIndex();
144                }
145            }
146
147            onTextChanged: {
148                root.query = queryField.text
149                if (allowCompletion && length > 0 && runnerManager.historyEnabled) {
150                    var oldText = text
151                    var suggestedText = runnerManager.getHistorySuggestion(text);
152                    if (suggestedText.length > 0) {
153                        text = text + suggestedText.substr(oldText.length)
154                        select(text.length, oldText.length)
155                    }
156                }
157            }
158            Keys.onPressed: {
159                allowCompletion = (event.key !== Qt.Key_Backspace && event.key !== Qt.Key_Delete)
160
161                if (event.modifiers & Qt.ControlModifier) {
162                    if (event.key === Qt.Key_J) {
163                        move_down()
164                        event.accepted = true;
165                    } else if (event.key === Qt.Key_K) {
166                        move_up()
167                        event.accepted = true;
168                    }
169                }
170            }
171            Keys.onUpPressed: move_up()
172            Keys.onDownPressed: move_down()
173            function closeOrRun(event) {
174                // Close KRunner if no text was typed and enter was pressed, FEATURE: 211225
175                if (!root.query) {
176                    runnerWindow.visible = false
177                } else {
178                    results.runCurrentIndex(event)
179                }
180            }
181            Keys.onEnterPressed: closeOrRun(event)
182            Keys.onReturnPressed: closeOrRun(event)
183
184            Keys.onEscapePressed: {
185                runnerWindow.visible = false
186            }
187
188            PlasmaCore.SvgItem {
189                anchors {
190                    right: parent.right
191                    rightMargin: 6 // from PlasmaStyle TextFieldStyle
192                    verticalCenter: parent.verticalCenter
193                }
194                // match clear button
195                width: Math.max(parent.height * 0.8, PlasmaCore.Units.iconSizes.small)
196                height: width
197                svg: PlasmaCore.Svg {
198                    imagePath: "widgets/arrows"
199                    colorGroup: PlasmaCore.Theme.ButtonColorGroup
200                }
201                elementId: "down-arrow"
202                visible: queryField.length === 0 && runnerManager.historyEnabled
203
204                MouseArea {
205                    anchors.fill: parent
206                    onPressed: {
207                        root.showHistory = !root.showHistory
208                        if (root.showHistory) {
209                            listView.forceActiveFocus(); // is the history list
210                        } else {
211                            queryField.forceActiveFocus();
212                        }
213                    }
214                }
215            }
216        }
217        PlasmaComponents3.ToolButton {
218            checkable: true
219            checked: runnerWindow.pinned
220            onToggled: runnerWindow.pinned = checked
221            icon.name: "window-pin"
222            Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Pin")
223            Accessible.description: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Pin Search")
224            PlasmaComponents3.ToolTip {
225                text: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Keep Open")
226            }
227        }
228    }
229
230    PlasmaExtras.ScrollArea {
231        Layout.alignment: Qt.AlignTop
232        visible: results.count > 0
233        enabled: visible
234        Layout.fillWidth: true
235        Layout.preferredHeight: Math.min(Screen.height, results.contentHeight)
236
237        Milou.ResultsView {
238            id: results
239            queryString: root.query
240            runner: root.runner
241
242            Keys.onPressed: {
243                var ctrl = event.modifiers & Qt.ControlModifier;
244                if (ctrl && event.key === Qt.Key_J) {
245                    incrementCurrentIndex()
246                } else if (ctrl && event.key === Qt.Key_K) {
247                    decrementCurrentIndex()
248                } else if (event.text !== "") {
249                    // This prevents unprintable control characters from being inserted
250                    if (!/[\x00-\x1F\x7F]/.test(event.text)) {
251                        queryField.text += event.text;
252                    }
253                    queryField.cursorPosition = queryField.text.length
254                    queryField.focus = true;
255                }
256            }
257
258            Keys.onEscapePressed: {
259                runnerWindow.visible = false
260            }
261
262            onActivated: {
263                runnerWindow.visible = false
264            }
265
266            onUpdateQueryString: {
267                queryField.text = text
268                queryField.cursorPosition = cursorPosition
269            }
270        }
271    }
272
273    PlasmaExtras.ScrollArea {
274        Layout.alignment: Qt.AlignTop
275        Layout.fillWidth: true
276        visible: root.query.length === 0 && listView.count > 0
277        // don't accept keyboard input when not visible so the keys propagate to the other list
278        enabled: visible
279        Layout.preferredHeight: Math.min(Screen.height, listView.contentHeight)
280
281        ListView {
282            id: listView // needs this id so the delegate can access it
283            keyNavigationWraps: true
284            highlight: PlasmaComponents.Highlight {}
285            highlightMoveDuration: 0
286            activeFocusOnTab: true
287            model: []
288            delegate: Milou.ResultDelegate {
289                id: resultDelegate
290                width: listView.width
291                typeText: index === 0 ? i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Recent Queries") : ""
292                additionalActions: [{
293                    icon: "list-remove",
294                    text: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Remove")
295                }]
296                Accessible.description: i18n("in category recent queries")
297            }
298
299            onActiveFocusChanged: {
300                if (!activeFocus && currentIndex == listView.count-1) {
301                    currentIndex = 0;
302                }
303            }
304            Keys.onReturnPressed: runCurrentIndex(event)
305            Keys.onEnterPressed: runCurrentIndex(event)
306
307            Keys.onTabPressed: {
308                if (currentIndex == listView.count-1) {
309                    listView.nextItemInFocusChain(true).forceActiveFocus();
310                } else {
311                    incrementCurrentIndex()
312                }
313            }
314            Keys.onBacktabPressed: {
315                if (currentIndex == 0) {
316                    listView.nextItemInFocusChain(false).forceActiveFocus();
317                } else {
318                    decrementCurrentIndex()
319                }
320            }
321            Keys.onPressed: {
322                var ctrl = event.modifiers & Qt.ControlModifier;
323                if (ctrl && event.key === Qt.Key_J) {
324                    incrementCurrentIndex()
325                } else if (ctrl && event.key === Qt.Key_K) {
326                    decrementCurrentIndex()
327                } else if (event.text !== "") {
328                    // This prevents unprintable control characters from being inserted
329                    if (event.key == Qt.Key_Escape) {
330                        root.showHistory = false
331                    } else if (!/[\x00-\x1F\x7F]/.test(event.text)) {
332                        queryField.text += event.text;
333                    }
334                    queryField.focus = true;
335                }
336            }
337
338            Keys.onUpPressed: decrementCurrentIndex()
339            Keys.onDownPressed: incrementCurrentIndex()
340
341            function runCurrentIndex(event) {
342                var entry = runnerManager.history[currentIndex]
343                if (entry) {
344                    // If user presses Shift+Return to invoke an action, invoke the first runner action
345                    if (event && event.modifiers === Qt.ShiftModifier
346                            && currentItem.additionalActions && currentItem.additionalActions.length > 0) {
347                        runAction(0);
348                        return
349                    }
350
351                    queryField.text = entry
352                    queryField.forceActiveFocus();
353                }
354            }
355
356            function runAction(actionIndex) {
357                if (actionIndex === 0) {
358                    // QStringList changes just reset the model, so we'll remember the index and set it again
359                    var currentIndex = listView.currentIndex
360                    runnerManager.removeFromHistory(currentIndex)
361                    model = runnerManager.history
362                    listView.currentIndex = currentIndex
363                }
364            }
365        }
366
367    }
368}
369