RunCommand.qml 13.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/*
 * Copyright 2014 Marco Martin <mart@kde.org>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

Marco Martin's avatar
Marco Martin committed
18
import QtQuick 2.6
19
import QtQuick.Layouts 1.1
20
import QtQuick.Window 2.1
21
import org.kde.plasma.core 2.0 as PlasmaCore
22 23
import org.kde.plasma.components 2.0 as PlasmaComponents // For Highlight
import org.kde.plasma.components 3.0 as PlasmaComponents3
24 25 26 27 28 29 30
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.milou 0.1 as Milou

ColumnLayout {
    id: root
    property string query
    property string runner
31
    property bool showHistory: false
32

33 34 35
    LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft
    LayoutMirroring.childrenInherit: true

36 37 38 39 40 41 42 43 44
    onQueryChanged: {
        queryField.text = query;
    }

    Connections {
        target: runnerWindow
        onVisibleChanged: {
            if (runnerWindow.visible) {
                queryField.forceActiveFocus();
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
45
                listView.currentIndex = -1
46 47 48 49 50 51 52 53
                if (runnerWindow.retainPriorSearch) {
                    // Hack: We want to retain the prior seach, but are in a different match session
                    // This way we ensure that the prior search gets executed as a new query
                    const priorSearch = root.query
                    queryField.text = ""
                    queryField.text = priorSearch
                }
                queryField.select(root.query.length, 0)
54
            } else {
55
                root.runner = ""
56
                if (runnerWindow.retainPriorSearch) {
57 58
                    // Same effect as setting empty string, but we want to retain this string for the next session
                    results.model.clear()
59 60 61
                } else {
                    root.query = ""
                }
62
                root.showHistory = false
63 64 65 66 67
            }
        }
    }

    RowLayout {
Marco Martin's avatar
Marco Martin committed
68
        Layout.alignment: Qt.AlignTop
69 70
        PlasmaComponents3.ToolButton {
            icon.name: "configure"
71 72 73 74
            onClicked: {
                runnerWindow.visible = false
                runnerWindow.displayConfiguration()
            }
75 76
            Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Configure")
            Accessible.description: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Configure Search Plugins")
77
            visible: runnerWindow.canConfigure
78 79 80
            PlasmaComponents3.ToolTip {
                text: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Configure KRunner...")
            }
81
        }
82
        PlasmaComponents3.TextField {
83
            id: queryField
84 85
            property bool allowCompletion: false

86 87
            clearButtonShown: true
            Layout.minimumWidth: units.gridUnit * 25
88
            Layout.maximumWidth: units.gridUnit * 25
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
89 90

            activeFocusOnPress: true
91 92 93 94 95
            placeholderText: results.runnerName ? i18ndc("plasma_lookandfeel_org.kde.lookandfeel",
                                                         "Textfield placeholder text, query specific KRunner",
                                                         "Search '%1'...", results.runnerName)
                                                : i18ndc("plasma_lookandfeel_org.kde.lookandfeel",
                                                         "Textfield placeholder text", "Search...")
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
96

97
            PlasmaComponents3.BusyIndicator {
98 99 100 101 102 103 104 105 106 107
                anchors {
                    right: parent.right
                    top: parent.top
                    bottom: parent.bottom
                    margins: units.smallSpacing
                    rightMargin: height
                }

                Timer {
                    id: queryTimer
108 109 110 111 112 113 114 115
                    property bool queryDisplay: false
                    running: results.querying || queryDisplay
                    onRunningChanged: if (running) {
                        queryDisplay = true
                    }
                    onTriggered: if (!results.querying) {
                        queryDisplay = false
                    }
116 117 118
                    interval: 500
                }

119
                running: queryTimer.queryDisplay
120
            }
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
            function move_up() {
                if (length === 0) {
                    root.showHistory = true;
                    listView.forceActiveFocus();
                } else if (results.count > 0) {
                    results.forceActiveFocus();
                    results.decrementCurrentIndex();
                }
            }

            function move_down() {
                if (length === 0) {
                    root.showHistory = true;
                    listView.forceActiveFocus();
                } else if (results.count > 0) {
                    results.forceActiveFocus();
                    results.incrementCurrentIndex();
                }
            }

141
            onTextChanged: {
142
                root.query = queryField.text
143 144 145
                if (allowCompletion && length > 0) {
                    var history = runnerWindow.history

146 147
                    // search the first item in the history rather than the shortest matching one
                    // this way more recently used entries take precedence over older ones (Bug 358985)
148 149 150 151
                    for (var i = 0, j = history.length; i < j; ++i) {
                        var item = history[i]

                        if (item.toLowerCase().indexOf(text.toLowerCase()) === 0) {
152
                            var oldText = text
153
                            text = text + item.substr(oldText.length)
154 155
                            select(text.length, oldText.length)
                            break
156 157 158
                        }
                    }
                }
159
            }
160 161 162 163
            Keys.onPressed: {
                allowCompletion = (event.key !== Qt.Key_Backspace && event.key !== Qt.Key_Delete)

                if (event.modifiers & Qt.ControlModifier) {
Laurent Montel's avatar
Laurent Montel committed
164
                    if (event.key === Qt.Key_J) {
165 166
                        move_down()
                        event.accepted = true;
Laurent Montel's avatar
Laurent Montel committed
167
                    } else if (event.key === Qt.Key_K) {
168 169 170
                        move_up()
                        event.accepted = true;
                    }
171 172
                }
            }
173 174
            Keys.onUpPressed: move_up()
            Keys.onDownPressed: move_down()
175 176
            Keys.onEnterPressed: results.runCurrentIndex(event)
            Keys.onReturnPressed: results.runCurrentIndex(event)
177

178 179 180
            Keys.onEscapePressed: {
                runnerWindow.visible = false
            }
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195

            PlasmaCore.SvgItem {
                anchors {
                    right: parent.right
                    rightMargin: 6 // from PlasmaStyle TextFieldStyle
                    verticalCenter: parent.verticalCenter
                }
                // match clear button
                width: Math.max(parent.height * 0.8, units.iconSizes.small)
                height: width
                svg: PlasmaCore.Svg {
                    imagePath: "widgets/arrows"
                    colorGroup: PlasmaCore.Theme.ButtonColorGroup
                }
                elementId: "down-arrow"
196
                visible: queryField.length === 0 && runnerWindow.history.length > 0
197 198 199 200 201 202 203 204 205 206 207 208 209

                MouseArea {
                    anchors.fill: parent
                    onPressed: {
                        root.showHistory = !root.showHistory
                        if (root.showHistory) {
                            listView.forceActiveFocus(); // is the history list
                        } else {
                            queryField.forceActiveFocus();
                        }
                    }
                }
            }
210
        }
211 212
        PlasmaComponents3.ToolButton {
            icon.name: "window-close"
213 214 215
            onClicked: {
                runnerWindow.visible = false
            }
216 217
            Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Close")
            Accessible.description: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Close Search")
218 219 220
            PlasmaComponents3.ToolTip {
                text: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Close")
            }
221 222 223 224
        }
    }

    PlasmaExtras.ScrollArea {
Marco Martin's avatar
Marco Martin committed
225
        Layout.alignment: Qt.AlignTop
226
        visible: results.count > 0
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
227
        enabled: visible
228
        Layout.fillWidth: true
229
        Layout.preferredHeight: Math.min(Screen.height, results.contentHeight)
230

231 232 233
        Milou.ResultsView {
            id: results
            queryString: root.query
234
            runner: root.runner
235

Marco Martin's avatar
Marco Martin committed
236
            Keys.onPressed: {
237
                var ctrl = event.modifiers & Qt.ControlModifier;
Laurent Montel's avatar
Laurent Montel committed
238
                if (ctrl && event.key === Qt.Key_J) {
239
                    incrementCurrentIndex()
Laurent Montel's avatar
Laurent Montel committed
240
                } else if (ctrl && event.key === Qt.Key_K) {
241
                    decrementCurrentIndex()
Laurent Montel's avatar
Laurent Montel committed
242
                } else if (event.text !== "") {
243 244 245 246
                    // This prevents unprintable control characters from being inserted
                    if (!/[\x00-\x1F\x7F]/.test(event.text)) {
                        queryField.text += event.text;
                    }
Marco Martin's avatar
Marco Martin committed
247 248 249 250
                    queryField.focus = true;
                }
            }

251
            onActivated: {
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
252
                runnerWindow.addToHistory(queryString)
253 254
                runnerWindow.visible = false
            }
255 256 257 258 259

            onUpdateQueryString: {
                queryField.text = text
                queryField.cursorPosition = cursorPosition
            }
260 261
        }
    }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
262 263 264 265 266 267 268 269 270 271 272 273 274 275

    PlasmaExtras.ScrollArea {
        Layout.alignment: Qt.AlignTop
        Layout.fillWidth: true
        visible: root.query.length === 0 && listView.count > 0
        // don't accept keyboard input when not visible so the keys propagate to the other list
        enabled: visible
        Layout.preferredHeight: Math.min(Screen.height, listView.contentHeight)

        ListView {
            id: listView // needs this id so the delegate can access it
            keyNavigationWraps: true
            highlight: PlasmaComponents.Highlight {}
            highlightMoveDuration: 0
Marco Martin's avatar
Marco Martin committed
276
            activeFocusOnTab: true
277 278
            // we store 50 entries in the history but only show 20 in the UI so it doesn't get too huge
            model: root.showHistory ? runnerWindow.history.slice(0, 20) : []
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
279 280 281 282
            delegate: Milou.ResultDelegate {
                id: resultDelegate
                width: listView.width
                typeText: index === 0 ? i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Recent Queries") : ""
283 284 285 286
                additionalActions: [{
                    icon: "list-remove",
                    text: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Remove")
                }]
Marco Martin's avatar
Marco Martin committed
287
                Accessible.description: i18n("in category recent queries")
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
288 289
            }

Marco Martin's avatar
Marco Martin committed
290 291 292 293 294
            onActiveFocusChanged: {
                if (!activeFocus && currentIndex == listView.count-1) {
                    currentIndex = 0;
                }
            }
295 296
            Keys.onReturnPressed: runCurrentIndex(event)
            Keys.onEnterPressed: runCurrentIndex(event)
Marco Martin's avatar
Marco Martin committed
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
            
            Keys.onTabPressed: {
                if (currentIndex == listView.count-1) {
                    listView.nextItemInFocusChain(true).forceActiveFocus();
                } else {
                    incrementCurrentIndex()
                }
            }
            Keys.onBacktabPressed: {
                if (currentIndex == 0) {
                    listView.nextItemInFocusChain(false).forceActiveFocus();
                } else {
                    decrementCurrentIndex()
                }
            }
            Keys.onPressed: {
313
                var ctrl = event.modifiers & Qt.ControlModifier;
Laurent Montel's avatar
Laurent Montel committed
314
                if (ctrl && event.key === Qt.Key_J) {
315
                    incrementCurrentIndex()
Laurent Montel's avatar
Laurent Montel committed
316
                } else if (ctrl && event.key === Qt.Key_K) {
317
                    decrementCurrentIndex()
Laurent Montel's avatar
Laurent Montel committed
318
                } else if (event.text !== "") {
Marco Martin's avatar
Marco Martin committed
319 320 321 322 323
                    queryField.text += event.text;
                    queryField.focus = true;
                }
            }
  
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
324 325 326
            Keys.onUpPressed: decrementCurrentIndex()
            Keys.onDownPressed: incrementCurrentIndex()

327
            function runCurrentIndex(event) {
328 329
                var entry = runnerWindow.history[currentIndex]
                if (entry) {
330 331 332 333 334 335 336
                    // If user presses Shift+Return to invoke an action, invoke the first runner action
                    if (event && event.modifiers === Qt.ShiftModifier
                            && currentItem.additionalActions && currentItem.additionalActions.length > 0) {
                        runAction(0);
                        return
                    }

337
                    queryField.text = entry
Marco Martin's avatar
Marco Martin committed
338
                    queryField.forceActiveFocus();
339 340 341 342 343 344 345 346 347 348
                }
            }

            function runAction(actionIndex) {
                if (actionIndex === 0) {
                    // QStringList changes just reset the model, so we'll remember the index and set it again
                    var currentIndex = listView.currentIndex
                    runnerWindow.removeFromHistory(currentIndex)
                    listView.currentIndex = currentIndex
                }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
349 350 351 352
            }
        }

    }
353
}