1/*
2    SPDX-FileCopyrightText: 2015 Eike Hein <hein@kde.org>
3
4    SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7import QtQuick 2.4
8import QtGraphicalEffects 1.0
9import QtQml 2.15
10
11import org.kde.plasma.core 2.1 as PlasmaCore
12import org.kde.plasma.components 2.0 as PlasmaComponents
13import org.kde.plasma.core 2.0 as PlasmaCore
14import org.kde.plasma.extras 2.0 as PlasmaExtras
15import org.kde.kquickcontrolsaddons 2.0
16import org.kde.kwindowsystem 1.0
17
18import org.kde.plasma.private.shell 2.0
19
20import org.kde.plasma.private.kicker 0.1 as Kicker
21
22import "code/tools.js" as Tools
23
24/* TODO
25 * Reverse middleRow layout + keyboard nav + filter list text alignment in rtl locales.
26 * Keep cursor column when arrow'ing down past non-full trailing rows into a lower grid.
27 * Make DND transitions cleaner by performing an item swap instead of index reinsertion.
28*/
29
30Kicker.DashboardWindow {
31    id: root
32
33    property bool smallScreen: ((Math.floor(width / PlasmaCore.Units.iconSizes.huge) <= 22) || (Math.floor(height / PlasmaCore.Units.iconSizes.huge) <= 14))
34
35    property int iconSize: smallScreen ? PlasmaCore.Units.iconSizes.large : PlasmaCore.Units.iconSizes.huge
36    property int cellSize: iconSize + PlasmaCore.Theme.mSize(PlasmaCore.Theme.defaultFont).height
37        + (2 * PlasmaCore.Units.smallSpacing)
38        + (2 * Math.max(highlightItemSvg.margins.top + highlightItemSvg.margins.bottom,
39                        highlightItemSvg.margins.left + highlightItemSvg.margins.right))
40    property int columns: Math.floor(((smallScreen ? 85 : 80)/100) * Math.ceil(width / cellSize))
41    property bool searching: (searchField.text != "")
42    property var widgetExplorer: null
43
44    keyEventProxy: searchField
45    backgroundColor: Qt.rgba(0, 0, 0, 0.737)
46
47    onKeyEscapePressed: {
48        if (searching) {
49            searchField.clear();
50        } else {
51            root.toggle();
52        }
53    }
54
55    onVisibleChanged: {
56        tabBar.activeTab = 0;
57        reset();
58
59        if (visible) {
60            preloadAllAppsTimer.restart();
61        }
62    }
63
64    onSearchingChanged: {
65        if (!searching) {
66            reset();
67        } else {
68            filterList.currentIndex = -1;
69
70            if (tabBar.activeTab == 1) {
71                widgetExplorer.widgetsModel.filterQuery = "";
72                widgetExplorer.widgetsModel.filterType = "";
73            }
74        }
75    }
76
77    function reset() {
78        searchField.clear();
79        globalFavoritesGrid.currentIndex = -1;
80        systemFavoritesGrid.currentIndex = -1;
81        filterList.currentIndex = 0;
82        funnelModel.sourceModel = rootModel.modelForRow(0);
83        mainGrid.model = (tabBar.activeTab == 0) ? funnelModel : root.widgetExplorer.widgetsModel;
84        mainGrid.currentIndex = -1;
85        filterListScrollArea.focus = true;
86        filterList.model = (tabBar.activeTab == 0) ? rootModel : root.widgetExplorer.filterModel;
87    }
88
89    function updateWidgetExplorer() {
90        if (tabBar.activeTab == 1 /* Widgets */ || tabBar.hoveredTab == 1) {
91            if (!root.widgetExplorer) {
92                root.widgetExplorer = widgetExplorerComponent.createObject(root, {
93                    containment: containmentInterface.screenContainment(plasmoid)
94                });
95            }
96        } else if (root.widgetExplorer) {
97            root.widgetExplorer.destroy();
98            root.widgetExplorer = null;
99        }
100    }
101
102    mainItem: MouseArea {
103        id: rootItem
104
105        anchors.fill: parent
106
107        acceptedButtons: Qt.LeftButton | Qt.RightButton
108
109        LayoutMirroring.enabled: Qt.application.layoutDirection == Qt.RightToLeft
110        LayoutMirroring.childrenInherit: true
111
112        Connections {
113            target: kicker
114
115            function onReset() {
116                if (!root.searching) {
117                    filterList.applyFilter();
118
119                    if (tabBar.activeTab == 0) {
120                        funnelModel.reset();
121                    }
122                }
123            }
124
125            function onDragSourceChanged() {
126                if (!kicker.dragSource) {
127                    // FIXME TODO HACK: Reset all views post-DND to work around
128                    // mouse grab bug despite QQuickWindow::mouseGrabberItem==0x0.
129                    // Needs a more involved hunt through Qt Quick sources later since
130                    // it's not happening with near-identical code in the menu repr.
131                    rootModel.refresh();
132                } else if (tabBar.activeTab == 1) {
133                    root.toggle();
134                    containmentInterface.ensureMutable(containmentInterface.screenContainment(plasmoid));
135                    kwindowsystem.showingDesktop = true;
136                }
137            }
138        }
139
140        KWindowSystem {
141            id: kwindowsystem
142        }
143
144        Component {
145            id: widgetExplorerComponent
146
147            WidgetExplorer { showSpecialFilters: false }
148        }
149
150        Connections {
151            target: plasmoid
152            function onUserConfiguringChanged() {
153                if (plasmoid.userConfiguring) {
154                    root.hide()
155                }
156            }
157        }
158
159        PlasmaComponents.Menu {
160            id: contextMenu
161
162            PlasmaComponents.MenuItem {
163                action: plasmoid.action("configure")
164            }
165        }
166
167        PlasmaExtras.Heading {
168            id: dummyHeading
169
170            visible: false
171
172            width: 0
173
174            level: 1
175        }
176
177        TextMetrics {
178            id: headingMetrics
179
180            font: dummyHeading.font
181        }
182
183        Kicker.FunnelModel {
184            id: funnelModel
185
186            onSourceModelChanged: {
187                if (mainColumn.visible) {
188                    mainGrid.currentIndex = -1;
189                    mainGrid.forceLayout();
190                }
191            }
192        }
193
194        Timer {
195            id: preloadAllAppsTimer
196
197            property bool done: false
198
199            interval: 1000
200            repeat: false
201
202            onTriggered: {
203                if (done || root.searching) {
204                    return;
205                }
206
207                for (var i = 0; i < rootModel.count; ++i) {
208                    var model = rootModel.modelForRow(i);
209
210                    if (model.description === "KICKER_ALL_MODEL") {
211                        allAppsGrid.model = model;
212                        done = true;
213                        break;
214                    }
215                }
216            }
217
218            function defer() {
219                if (running && !done) {
220                    restart();
221                }
222            }
223        }
224
225        Kicker.ContainmentInterface {
226            id: containmentInterface
227        }
228
229        DashboardTabBar {
230            id: tabBar
231
232            y: 0
233
234            anchors.horizontalCenter: parent.horizontalCenter
235
236            visible: (plasmoid.immutability !== PlasmaCore.Types.SystemImmutable)
237
238            onActiveTabChanged: {
239                root.updateWidgetExplorer();
240                root.reset();
241            }
242
243            onHoveredTabChanged: root.updateWidgetExplorer()
244
245            Keys.onDownPressed: {
246                mainColumn.tryActivate(0, 0);
247            }
248        }
249
250        TextEdit {
251            id: searchField
252
253            width: 0
254            height: 0
255
256            visible: false
257
258            persistentSelection: true
259
260            onTextChanged: {
261                if (tabBar.activeTab == 0) {
262                    runnerModel.query = searchField.text;
263                } else {
264                    root.widgetExplorer.widgetsModel.searchTerm = searchField.text;
265                }
266            }
267
268            function clear() {
269                text = "";
270            }
271
272            onSelectionStartChanged: Qt.callLater(searchHeading.updateSelection)
273            onSelectionEndChanged: Qt.callLater(searchHeading.updateSelection)
274        }
275
276        TextEdit {
277            id: searchHeading
278
279            anchors {
280                horizontalCenter: parent.horizontalCenter
281            }
282
283            y: (middleRow.anchors.topMargin / 2) - (root.smallScreen ? (height/10) : 0)
284
285            font.pointSize: dummyHeading.font.pointSize * 1.5
286            wrapMode: Text.NoWrap
287            opacity: 1.0
288
289            selectByMouse: false
290            cursorVisible: false
291
292            color: "white"
293
294            text: root.searching ? i18n("Searching for '%1'", searchField.text) : i18n("Type to search…")
295
296            function updateSelection() {
297                if (!searchField.selectedText) {
298                    return;
299                }
300
301                var delta = text.lastIndexOf(searchField.text, text.length - 2);
302                searchHeading.select(searchField.selectionStart + delta, searchField.selectionEnd + delta);
303            }
304        }
305
306        PlasmaComponents.ToolButton {
307            id: cancelSearchButton
308
309            anchors {
310                left: searchHeading.right
311                leftMargin: PlasmaCore.Units.largeSpacing
312                verticalCenter: searchHeading.verticalCenter
313            }
314
315            width: PlasmaCore.Units.iconSizes.large
316            height: width
317
318            visible: (searchField.text != "")
319
320            iconName: "dialog-close"
321            flat: false
322
323            onClicked: searchField.clear();
324
325            Keys.onPressed: event => {
326                if (event.key === Qt.Key_Tab) {
327                    event.accepted = true;
328
329                    if (runnerModel.count) {
330                        mainColumn.tryActivate(0, 0);
331                    } else {
332                        systemFavoritesGrid.tryActivate(0, 0);
333                    }
334                } else if (event.key === Qt.Key_Backtab) {
335                    event.accepted = true;
336
337                    if (tabBar.visible) {
338                        tabBar.focus = true;
339                    } else if (globalFavoritesGrid.enabled) {
340                        globalFavoritesGrid.tryActivate(0, 0);
341                    } else {
342                        systemFavoritesGrid.tryActivate(0, 0);
343                    }
344                }
345            }
346        }
347
348        Row {
349            id: middleRow
350
351            anchors {
352                top: parent.top
353                topMargin: PlasmaCore.Units.gridUnit * (smallScreen ? 8 : 10)
354                bottom: parent.bottom
355                bottomMargin: (PlasmaCore.Units.gridUnit * 2)
356                horizontalCenter: parent.horizontalCenter
357            }
358
359            width: (root.columns * root.cellSize) + (2 * spacing)
360
361            spacing: PlasmaCore.Units.gridUnit * 2
362
363            Item {
364                id: favoritesColumn
365
366                anchors {
367                    top: parent.top
368                    bottom: parent.bottom
369                }
370
371                width: (columns * root.cellSize) + PlasmaCore.Units.gridUnit
372
373                property int columns: 3
374
375                PlasmaExtras.Heading {
376                    id: favoritesColumnLabel
377
378                    enabled: (tabBar.activeTab == 0)
379
380                    anchors {
381                        top: parent.top
382                    }
383
384                    x: PlasmaCore.Units.smallSpacing
385                    width: parent.width - x
386
387                    elide: Text.ElideRight
388                    wrapMode: Text.NoWrap
389
390                    color: "white"
391
392                    level: 1
393
394                    text: i18n("Favorites")
395
396                    opacity: (enabled ? 1.0 : 0.3)
397
398                    Behavior on opacity { SmoothedAnimation { duration: PlasmaCore.Units.longDuration; velocity: 0.01 } }
399                }
400
401                PlasmaCore.SvgItem {
402                    id: favoritesColumnLabelUnderline
403
404                    enabled: (tabBar.activeTab == 0)
405
406                    anchors {
407                        top: favoritesColumnLabel.bottom
408                    }
409
410                    width: parent.width - PlasmaCore.Units.gridUnit
411                    height: lineSvg.horLineHeight
412
413                    svg: lineSvg
414                    elementId: "horizontal-line"
415
416                    opacity: (enabled ? 1.0 : 0.3)
417
418                    Behavior on opacity { SmoothedAnimation { duration: PlasmaCore.Units.longDuration; velocity: 0.01 } }
419                }
420
421                ItemGridView {
422                    id: globalFavoritesGrid
423
424                    enabled: (tabBar.activeTab == 0)
425
426                    anchors {
427                        top: favoritesColumnLabelUnderline.bottom
428                        topMargin: PlasmaCore.Units.largeSpacing
429                    }
430
431                    property int rows: (Math.floor((parent.height - favoritesColumnLabel.height
432                        - favoritesColumnLabelUnderline.height - PlasmaCore.Units.largeSpacing) / root.cellSize)
433                        - systemFavoritesGrid.rows)
434
435                    width: parent.width
436                    height: rows * root.cellSize
437
438                    cellWidth: root.cellSize
439                    cellHeight: root.cellSize
440                    iconSize: root.iconSize
441
442                    model: globalFavorites
443
444                    dropEnabled: true
445                    usesPlasmaTheme: false
446
447                    opacity: (enabled ? 1.0 : 0.3)
448
449                    Behavior on opacity { SmoothedAnimation { duration: PlasmaCore.Units.longDuration; velocity: 0.01 } }
450
451                    onCurrentIndexChanged: {
452                        preloadAllAppsTimer.defer();
453                    }
454
455                    onKeyNavRight: {
456                        mainColumn.tryActivate(currentRow(), 0);
457                    }
458
459                    onKeyNavDown: {
460                        systemFavoritesGrid.tryActivate(0, currentCol());
461                    }
462
463                    Keys.onPressed: event => {
464                        if (event.key === Qt.Key_Tab) {
465                            event.accepted = true;
466
467                            if (tabBar.visible) {
468                                tabBar.focus = true;
469                            } else if (root.searching) {
470                                cancelSearchButton.focus = true;
471                            } else {
472                                mainColumn.tryActivate(0, 0);
473                            }
474                        } else if (event.key === Qt.Key_Backtab) {
475                            event.accepted = true;
476                            systemFavoritesGrid.tryActivate(0, 0);
477                        }
478                    }
479
480                    Binding {
481                        target: globalFavorites
482                        property: "iconSize"
483                        value: root.iconSize
484                        restoreMode: Binding.RestoreBinding
485                    }
486                }
487
488                ItemGridView {
489                    id: systemFavoritesGrid
490
491                    anchors {
492                        top: globalFavoritesGrid.bottom
493                    }
494
495                    property int rows: Math.ceil(count / Math.floor(width / root.cellSize))
496
497                    width: parent.width
498                    height: rows * root.cellSize
499
500                    cellWidth: root.cellSize
501                    cellHeight: root.cellSize
502                    iconSize: root.iconSize
503
504                    model: systemFavorites
505
506                    dropEnabled: true
507                    usesPlasmaTheme: true
508
509                    onCurrentIndexChanged: {
510                        preloadAllAppsTimer.defer();
511                    }
512
513                    onKeyNavRight: {
514                        mainColumn.tryActivate(globalFavoritesGrid.rows + currentRow(), 0);
515                    }
516
517                    onKeyNavUp: {
518                        globalFavoritesGrid.tryActivate(globalFavoritesGrid.rows - 1, currentCol());
519                    }
520
521                    Keys.onPressed: event => {
522                        if (event.key === Qt.Key_Tab) {
523                            event.accepted = true;
524
525                            if (globalFavoritesGrid.enabled) {
526                                globalFavoritesGrid.tryActivate(0, 0);
527                            } else if (tabBar.visible) {
528                                tabBar.focus = true;
529                            } else if (root.searching && !runnerModel.count) {
530                                cancelSearchButton.focus = true;
531                            } else {
532                                mainColumn.tryActivate(0, 0);
533                            }
534                        } else if (event.key === Qt.Key_Backtab) {
535                            event.accepted = true;
536
537                            if (filterList.enabled) {
538                                filterList.forceActiveFocus();
539                            } else if (root.searching && !runnerModel.count) {
540                                cancelSearchButton.focus = true;
541                            } else {
542                                mainColumn.tryActivate(0, 0);
543                            }
544                        }
545                    }
546                }
547            }
548
549            Item {
550                id: mainColumn
551
552                anchors.top: parent.top
553
554                width: (columns * root.cellSize) + PlasmaCore.Units.gridUnit
555                height: Math.floor(parent.height / root.cellSize) * root.cellSize + mainGridContainer.headerHeight
556
557                property int columns: root.columns - favoritesColumn.columns - filterListColumn.columns
558                property Item visibleGrid: mainGrid
559
560                function tryActivate(row, col) {
561                    if (visibleGrid) {
562                        visibleGrid.tryActivate(row, col);
563                    }
564                }
565
566                Item {
567                    id: mainGridContainer
568
569                    anchors.fill: parent
570                    z: (opacity == 1.0) ? 1 : 0
571
572                    visible: opacity != 0.0
573
574                    property int headerHeight: mainColumnLabel.height + mainColumnLabelUnderline.height + PlasmaCore.Units.largeSpacing
575
576                    opacity: {
577                        if (tabBar.activeTab == 0 && root.searching) {
578                            return 0.0;
579                        }
580
581                        if (filterList.allApps) {
582                            return 0.0;
583                        }
584
585                        return 1.0;
586                    }
587
588                    onOpacityChanged: {
589                        if (opacity == 1.0) {
590                            mainColumn.visibleGrid = mainGrid;
591                        }
592                    }
593
594                    PlasmaExtras.Heading {
595                        id: mainColumnLabel
596
597                        anchors {
598                            top: parent.top
599                        }
600
601                        x: PlasmaCore.Units.smallSpacing
602                        width: parent.width - x
603
604                        elide: Text.ElideRight
605                        wrapMode: Text.NoWrap
606                        opacity: 1.0
607
608                        color: "white"
609
610                        level: 1
611
612                        text: (tabBar.activeTab == 0) ? funnelModel.description : i18n("Widgets")
613                    }
614
615                    PlasmaCore.SvgItem {
616                        id: mainColumnLabelUnderline
617
618                        visible: mainGrid.count
619
620                        anchors {
621                            top: mainColumnLabel.bottom
622                        }
623
624                        width: parent.width - PlasmaCore.Units.gridUnit
625                        height: lineSvg.horLineHeight
626
627                        svg: lineSvg
628                        elementId: "horizontal-line"
629                    }
630
631                    ItemGridView {
632                        id: mainGrid
633
634                        anchors {
635                            top: mainColumnLabelUnderline.bottom
636                            topMargin: PlasmaCore.Units.largeSpacing
637                        }
638
639                        width: parent.width
640                        height: systemFavoritesGrid.y + systemFavoritesGrid.height - mainGridContainer.headerHeight
641
642                        cellWidth: (tabBar.activeTab == 0 ? root.cellSize : root.cellSize * 2)
643                        cellHeight: cellWidth
644                        iconSize: (tabBar.activeTab == 0 ? root.iconSize : cellWidth - (PlasmaCore.Units.largeSpacing * 2))
645
646                        model: funnelModel
647
648                        onCurrentIndexChanged: {
649                            preloadAllAppsTimer.defer();
650                        }
651
652                        onKeyNavLeft: {
653                            if (tabBar.activeTab == 0) {
654                                var row = currentRow();
655                                var target = row + 1 > globalFavoritesGrid.rows ? systemFavoritesGrid : globalFavoritesGrid;
656                                var targetRow = row + 1 > globalFavoritesGrid.rows ? row - globalFavoritesGrid.rows : row;
657                                target.tryActivate(targetRow, favoritesColumn.columns - 1);
658                            }
659                        }
660
661                        onKeyNavRight: {
662                            filterListScrollArea.focus = true;
663                        }
664
665                        onKeyNavUp: {
666                            if (tabBar.visible) {
667                                tabBar.focus = true;
668                            }
669                        }
670
671                        onItemActivated: {
672                            if (tabBar.activeTab == 1) {
673                                containmentInterface.ensureMutable(containmentInterface.screenContainment(plasmoid));
674                                root.widgetExplorer.addApplet(currentItem.m.pluginName);
675                                root.toggle();
676                                kwindowsystem.showingDesktop = true;
677                            }
678                        }
679                    }
680                }
681
682                ItemMultiGridView {
683                    id: allAppsGrid
684
685                    anchors {
686                        top: parent.top
687                    }
688
689                    z: (opacity == 1.0) ? 1 : 0
690                    width: parent.width
691                    height: systemFavoritesGrid.y + systemFavoritesGrid.height
692
693                    visible: opacity != 0.0
694
695                    opacity: filterList.allApps ? 1.0 : 0.0
696
697                    onOpacityChanged: {
698                        if (opacity == 1.0) {
699                            allAppsGrid.flickableItem.contentY = 0;
700                            mainColumn.visibleGrid = allAppsGrid;
701                        }
702                    }
703
704                    onKeyNavLeft: {
705                        var row = 0;
706
707                        for (var i = 0; i < subGridIndex; i++) {
708                            row += subGridAt(i).lastRow() + 2; // Header counts as one.
709                        }
710
711                        row += subGridAt(subGridIndex).currentRow();
712
713                        var target = row + 1 > globalFavoritesGrid.rows ? systemFavoritesGrid : globalFavoritesGrid;
714                        var targetRow = row + 1 > globalFavoritesGrid.rows ? row - globalFavoritesGrid.rows : row;
715                        target.tryActivate(targetRow, favoritesColumn.columns - 1);
716                    }
717
718                    onKeyNavRight: {
719                        filterListScrollArea.focus = true;
720                    }
721                }
722
723                ItemMultiGridView {
724                    id: runnerGrid
725
726                    anchors {
727                        top: parent.top
728                    }
729
730                    z: (opacity == 1.0) ? 1 : 0
731                    width: parent.width
732                    height: Math.min(implicitHeight, systemFavoritesGrid.y + systemFavoritesGrid.height)
733
734                    visible: opacity != 0.0
735
736                    model: runnerModel
737
738                    grabFocus: true
739
740                    opacity: (tabBar.activeTab == 0 && root.searching) ? 1.0 : 0.0
741
742                    onOpacityChanged: {
743                        if (opacity == 1.0) {
744                            mainColumn.visibleGrid = runnerGrid;
745                        }
746                    }
747
748                    onKeyNavLeft: {
749                        var row = 0;
750
751                        for (var i = 0; i < subGridIndex; i++) {
752                            row += subGridAt(i).lastRow() + 2; // Header counts as one.
753                        }
754
755                        row += subGridAt(subGridIndex).currentRow();
756
757                        var target = row + 1 > globalFavoritesGrid.rows ? systemFavoritesGrid : globalFavoritesGrid;
758                        var targetRow = row + 1 > globalFavoritesGrid.rows ? row - globalFavoritesGrid.rows : row;
759                        target.tryActivate(targetRow, favoritesColumn.columns - 1);
760                    }
761                }
762
763                Keys.onPressed: event => {
764                    if (event.key === Qt.Key_Tab) {
765                        event.accepted = true;
766
767                        if (filterList.enabled) {
768                            filterList.forceActiveFocus();
769                        } else {
770                            systemFavoritesGrid.tryActivate(0, 0);
771                        }
772                    } else if (event.key === Qt.Key_Backtab) {
773                        event.accepted = true;
774
775                        if (root.searching) {
776                            cancelSearchButton.focus = true;
777                        } else if (tabBar.visible) {
778                            tabBar.focus = true;
779                        } else if (globalFavoritesGrid.enabled) {
780                            globalFavoritesGrid.tryActivate(0, 0);
781                        } else {
782                            systemFavoritesGrid.tryActivate(0, 0);
783                        }
784                    }
785                }
786            }
787
788            Item {
789                id: filterListColumn
790
791                anchors {
792                    top: parent.top
793                    topMargin: mainColumnLabelUnderline.y + mainColumnLabelUnderline.height + PlasmaCore.Units.largeSpacing
794                    bottom: parent.bottom
795                }
796
797                width: columns * root.cellSize
798
799                property int columns: 3
800
801                PlasmaExtras.ScrollArea {
802                    id: filterListScrollArea
803
804                    x: root.visible ? 0 : PlasmaCore.Units.gridUnit
805
806                    Behavior on x { SmoothedAnimation { duration: PlasmaCore.Units.longDuration; velocity: 0.01 } }
807
808                    width: parent.width
809                    height: mainGrid.height
810
811                    enabled: !root.searching
812
813                    property alias currentIndex: filterList.currentIndex
814
815                    opacity: root.visible ? (root.searching ? 0.30 : 1.0) : 0.3
816
817                    Behavior on opacity { SmoothedAnimation { duration: PlasmaCore.Units.longDuration; velocity: 0.01 } }
818
819                    verticalScrollBarPolicy: (opacity == 1.0) ? Qt.ScrollBarAsNeeded : Qt.ScrollBarAlwaysOff
820
821                    onEnabledChanged: {
822                        if (!enabled) {
823                            filterList.currentIndex = -1;
824                        }
825                    }
826
827                    onCurrentIndexChanged: {
828                        focus = (currentIndex != -1);
829                    }
830
831                    ListView {
832                        id: filterList
833
834                        focus: true
835
836                        property bool allApps: false
837                        property int eligibleWidth: width
838                        property int hItemMargins: Math.max(highlightItemSvg.margins.left + highlightItemSvg.margins.right,
839                            listItemSvg.margins.left + listItemSvg.margins.right)
840
841                        model: rootModel
842
843                        boundsBehavior: Flickable.StopAtBounds
844                        snapMode: ListView.SnapToItem
845                        spacing: 0
846                        keyNavigationWraps: true
847
848                        delegate: MouseArea {
849                            id: item
850
851                            signal actionTriggered(string actionId, variant actionArgument)
852                            signal aboutToShowActionMenu(variant actionMenu)
853
854                            property var m: model
855                            property int textWidth: label.contentWidth
856                            property int mouseCol
857                            property bool hasActionList: ((model.favoriteId !== null)
858                                || (("hasActionList" in model) && (model.hasActionList === true)))
859                            property Item menu: actionMenu
860
861                            width: parent.width
862                            height: Math.ceil((label.paintedHeight
863                                + Math.max(highlightItemSvg.margins.top + highlightItemSvg.margins.bottom,
864                                listItemSvg.margins.top + listItemSvg.margins.bottom)) / 2) * 2
865
866                            Accessible.role: Accessible.MenuItem
867                            Accessible.name: model.display
868
869                            acceptedButtons: Qt.LeftButton | Qt.RightButton
870
871                            hoverEnabled: true
872
873                            onContainsMouseChanged: {
874                                if (!containsMouse) {
875                                    updateCurrentItemTimer.stop();
876                                }
877                            }
878
879                            onPositionChanged: mouse => { // Lazy menu implementation.
880                                mouseCol = mouse.x;
881
882                                if (justOpenedTimer.running || ListView.view.currentIndex === 0 || index === ListView.view.currentIndex) {
883                                    updateCurrentItem();
884                                } else if ((index == ListView.view.currentIndex - 1) && mouse.y < (height - 6)
885                                    || (index == ListView.view.currentIndex + 1) && mouse.y > 5) {
886
887                                    if (mouse.x > ListView.view.eligibleWidth - 5) {
888                                        updateCurrentItem();
889                                    }
890                                } else if (mouse.x > ListView.view.eligibleWidth) {
891                                    updateCurrentItem();
892                                }
893
894                                updateCurrentItemTimer.restart();
895                            }
896
897                            onPressed: mouse => {
898                                if (mouse.buttons & Qt.RightButton) {
899                                    if (hasActionList) {
900                                        openActionMenu(item, mouse.x, mouse.y);
901                                    }
902                                }
903                            }
904
905                            onClicked: mouse => {
906                                if (mouse.button == Qt.LeftButton) {
907                                    updateCurrentItem();
908                                }
909                            }
910
911                            onAboutToShowActionMenu: {
912                                var actionList = hasActionList ? model.actionList : [];
913                                Tools.fillActionMenu(i18n, actionMenu, actionList, ListView.view.model.favoritesModel, model.favoriteId);
914                            }
915
916                            onActionTriggered: {
917                                if (Tools.triggerAction(ListView.view.model, model.index, actionId, actionArgument) === true) {
918                                    plasmoid.expanded = false;
919                                }
920                            }
921
922                            function openActionMenu(visualParent, x, y) {
923                                aboutToShowActionMenu(actionMenu);
924                                actionMenu.visualParent = visualParent;
925                                actionMenu.open(x, y);
926                            }
927
928                            function updateCurrentItem() {
929                                ListView.view.currentIndex = index;
930                                ListView.view.eligibleWidth = Math.min(width, mouseCol);
931                            }
932
933                            ActionMenu {
934                                id: actionMenu
935
936                                onActionClicked: {
937                                    actionTriggered(actionId, actionArgument);
938                                }
939                            }
940
941                            Timer {
942                                id: updateCurrentItemTimer
943
944                                interval: 50
945                                repeat: false
946
947                                onTriggered: parent.updateCurrentItem()
948                            }
949
950                            PlasmaExtras.Heading {
951                                id: label
952
953                                anchors {
954                                    fill: parent
955                                    leftMargin: highlightItemSvg.margins.left
956                                    rightMargin: highlightItemSvg.margins.right
957                                }
958
959                                elide: Text.ElideRight
960                                wrapMode: Text.NoWrap
961                                opacity: 1.0
962
963                                color: "white"
964
965                                level: 1
966
967                                text: model.display
968                            }
969                        }
970
971                        highlight: PlasmaComponents.Highlight {
972                            anchors {
973                                top: filterList.currentItem ? filterList.currentItem.top : undefined
974                                left: filterList.currentItem ? filterList.currentItem.left : undefined
975                                bottom: filterList.currentItem ? filterList.currentItem.bottom : undefined
976                            }
977
978                            opacity: filterListScrollArea.focus ? 1.0 : 0.7
979
980                            width: (highlightItemSvg.margins.left
981                                + filterList.currentItem.textWidth
982                                + highlightItemSvg.margins.right
983                                + PlasmaCore.Units.smallSpacing)
984
985                            visible: filterList.currentItem
986                        }
987
988                        highlightFollowsCurrentItem: false
989                        highlightMoveDuration: 0
990                        highlightResizeDuration: 0
991
992                        onCurrentIndexChanged: applyFilter()
993
994                        onCountChanged: {
995                            var width = 0;
996
997                            for (var i = 0; i < rootModel.count; ++i) {
998                                headingMetrics.text = rootModel.labelForRow(i);
999
1000                                if (headingMetrics.width > width) {
1001                                    width = headingMetrics.width;
1002                                }
1003                            }
1004
1005                            filterListColumn.columns = Math.ceil(width / root.cellSize);
1006                            filterListScrollArea.width = width + hItemMargins + (PlasmaCore.Units.gridUnit * 2);
1007                        }
1008
1009                        function applyFilter() {
1010                            if (!root.searching && currentIndex >= 0) {
1011                                if (tabBar.activeTab == 1) {
1012                                    root.widgetExplorer.widgetsModel.filterQuery = currentItem.m.filterData;
1013                                    root.widgetExplorer.widgetsModel.filterType = currentItem.m.filterType;
1014
1015                                    allApps = false;
1016                                    funnelModel.sourceModel = model;
1017
1018                                    return;
1019                                }
1020
1021                                if (preloadAllAppsTimer.running) {
1022                                    preloadAllAppsTimer.stop();
1023                                }
1024
1025                                var model = rootModel.modelForRow(currentIndex);
1026
1027                                if (model.description === "KICKER_ALL_MODEL") {
1028                                    allAppsGrid.model = model;
1029                                    allApps = true;
1030                                    funnelModel.sourceModel = null;
1031                                    preloadAllAppsTimer.done = true;
1032                                } else {
1033                                    funnelModel.sourceModel = model;
1034                                    allApps = false;
1035                                }
1036                            } else {
1037                                funnelModel.sourceModel = null;
1038                                allApps = false;
1039                            }
1040                        }
1041
1042                        Keys.onPressed: event => {
1043                            if (event.key === Qt.Key_Left) {
1044                                event.accepted = true;
1045
1046                                var currentRow = Math.max(0, Math.ceil(currentItem.y / mainGrid.cellHeight) - 1);
1047                                mainColumn.tryActivate(currentRow, mainColumn.columns - 1);
1048                            } else if (event.key === Qt.Key_Tab) {
1049                                event.accepted = true;
1050                                systemFavoritesGrid.tryActivate(0, 0);
1051                            } else if (event.key === Qt.Key_Backtab) {
1052                                event.accepted = true;
1053                                mainColumn.tryActivate(0, 0);
1054                            }
1055                        }
1056                    }
1057                }
1058            }
1059        }
1060
1061        onPressed: mouse => {
1062            if (mouse.button == Qt.RightButton) {
1063                contextMenu.open(mouse.x, mouse.y);
1064            }
1065        }
1066
1067        onClicked: mouse => {
1068            if (mouse.button == Qt.LeftButton) {
1069                root.toggle();
1070            }
1071        }
1072    }
1073}
1074