1/*
2    SPDX-FileCopyrightText: 2020 Marco Martin <mart@kde.org>
3    SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
4    SPDX-FileCopyrightText: 2021 David Redondo <kde@david-redondo.de>
5
6    SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9import QtQuick 2.14
10import QtQuick.Window 2.14
11import QtQuick.Controls 2.14
12import QtQuick.Layouts 1.14
13import QtQml.Models 2.12
14
15import org.kde.kirigami 2.12 as Kirigami
16import org.kde.kitemmodels 1.0 as KItemModels
17import org.kde.ksysguard.sensors 1.0 as Sensors
18
19Control {
20    id: control
21
22    property bool supportsColors: true
23    property int maxAllowedSensors: -1
24    property var selected: []
25    property var colors: {}
26    property var labels: {}
27
28    signal selectColor(string sensorId)
29    signal colorForSensorGenerated(string sensorId, color color)
30    signal sensorLabelChanged(string sensorId, string label)
31
32    onSelectedChanged: {
33        if (!control.selected) {
34            return;
35        }
36        for (let i = 0; i < Math.min(control.selected.length, selectedModel.count); ++i) {
37            selectedModel.set(i, {"sensor": control.selected[i]});
38        }
39        if (selectedModel.count > control.selected.length) {
40            selectedModel.remove(control.selected.length, selectedModel.count - control.selected.length);
41        } else if (selectedModel.count < control.selected.length) {
42            for (let i = selectedModel.count; i < control.selected.length; ++i) {
43                selectedModel.append({"sensor": control.selected[i]});
44            }
45        }
46    }
47
48    background: TextField {
49        readOnly: true
50        hoverEnabled: false
51
52        onFocusChanged: {
53            if (focus && (maxAllowedSensors <= 0 || repeater.count < maxAllowedSensors)) {
54                popup.open()
55            } else {
56                popup.close()
57            }
58        }
59        onReleased: {
60            if (focus && (maxAllowedSensors <= 0 || repeater.count < maxAllowedSensors)) {
61                popup.open()
62            }
63        }
64    }
65
66    contentItem: Flow {
67        spacing: Kirigami.Units.smallSpacing
68
69        move: Transition {
70            NumberAnimation {
71                properties: "x,y"
72                duration: Kirigami.Units.shortDuration
73                easing.type: Easing.InOutQuad
74            }
75        }
76        Repeater {
77            id: repeater
78            model: ListModel {
79                id: selectedModel
80                function writeSelectedSensors() {
81                    let newSelected = [];
82                    for (let i = 0; i < count; ++i) {
83                        newSelected.push(get(i).sensor);
84                    }
85                    control.selected = newSelected;
86                    control.selectedChanged();
87                }
88            }
89
90            delegate: Item {
91                id: delegate
92                implicitHeight: layout.implicitHeight + Kirigami.Units.smallSpacing * 2
93                implicitWidth: Math.min(layout.implicitWidth + Kirigami.Units.smallSpacing * 2,
94                                        control.width - control.leftPadding - control.rightPadding)
95                readonly property int position: index
96                Rectangle {
97                    id: delegateContents
98                    z: 10
99                    color: Qt.rgba(
100                                Kirigami.Theme.highlightColor.r,
101                                Kirigami.Theme.highlightColor.g,
102                                Kirigami.Theme.highlightColor.b,
103                                0.25)
104                    radius: Kirigami.Units.smallSpacing
105                    border.color: Kirigami.Theme.highlightColor
106                    border.width: 1
107                    opacity: (control.maxAllowedSensors <= 0 || index < control.maxAllowedSensors) ? 1 : 0.4
108                    parent: drag.active ? control : delegate
109
110                    width: delegate.width
111                    height: delegate.height
112                    DragHandler {
113                        id: drag
114                        //TODO: uncomment as soon as we can depend from 5.15
115                        //cursorShape: active ? Qt.ClosedHandCursor : Qt.OpenHandCursor
116                        enabled: selectedModel.count > 1
117                        onActiveChanged: {
118                            if (active) {
119                                let pos = delegateContents.mapFromItem(control.contentItem, 0, 0);
120                                delegateContents.x = pos.x;
121                                delegateContents.y = pos.y;
122                            } else {
123                                let pos = delegate.mapFromItem(delegateContents, 0, 0);
124                                delegateContents.x = pos.x;
125                                delegateContents.y = pos.y;
126                                dropAnim.restart();
127                                selectedModel.writeSelectedSensors();
128                            }
129                        }
130                        xAxis {
131                            minimum: 0
132                            maximum: control.width - delegateContents.width
133                        }
134                        yAxis {
135                            minimum: 0
136                            maximum: control.height - delegateContents.height
137                        }
138                        onCentroidChanged: {
139                            if (!active || control.contentItem.move.running) {
140                                return;
141                            }
142                            let pos = control.contentItem.mapFromItem(null, drag.centroid.scenePosition.x, drag.centroid.scenePosition.y);
143                            pos.x = Math.max(0, Math.min(control.contentItem.width - 1, pos.x));
144                            pos.y = Math.max(0, Math.min(control.contentItem.height - 1, pos.y));
145
146                            let child = control.contentItem.childAt(pos.x, pos.y);
147                            if (child === delegate) {
148                                return;
149                            } else if (child) {
150                                let newIndex = -1;
151                                if (pos.x > child.x + child.width/2) {
152                                    newIndex = Math.min(child.position + 1, selectedModel.count - 1);
153                                } else {
154                                    newIndex = child.position;
155                                }
156                                selectedModel.move(index, newIndex, 1);
157                            }
158                        }
159                    }
160                    ParallelAnimation {
161                        id: dropAnim
162                        XAnimator {
163                            target: delegateContents
164                            from: delegateContents.x
165                            to: 0
166                            duration: Kirigami.Units.shortDuration
167                            easing.type: Easing.InOutQuad
168                        }
169                        YAnimator {
170                            target: delegateContents
171                            from: delegateContents.y
172                            to: 0
173                            duration: Kirigami.Units.shortDuration
174                            easing.type: Easing.InOutQuad
175                        }
176                    }
177
178                    Sensors.Sensor { id: sensor; sensorId: model.sensor }
179
180                    Component.onCompleted: {
181                        if (typeof control.colors === "undefined" ||
182                            typeof control.colors[sensor.sensorId] === "undefined") {
183                            let color = Qt.hsva(Math.random(), Kirigami.Theme.highlightColor.hsvSaturation, Kirigami.Theme.highlightColor.hsvValue, 1);
184                            control.colorForSensorGenerated(sensor.sensorId, color)
185                        }
186                    }
187
188                    RowLayout {
189                        id: layout
190
191                        anchors.fill: parent
192                        anchors.margins: Kirigami.Units.smallSpacing
193
194                        ToolButton {
195                            visible: control.supportsColors
196                            Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
197                            Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
198
199                            padding: Kirigami.Units.smallSpacing
200                            flat: false
201
202                            contentItem: Rectangle {
203                                color: typeof control.colors === "undefined"  ? "black" : control.colors[sensor.sensorId]
204                            }
205
206                            onClicked: control.selectColor(sensor.sensorId)
207                        }
208
209                        RowLayout {
210                            id: normalLayout
211                            Label {
212                                id: label
213                                Layout.fillWidth: true
214                                text: control.labels[sensor.sensorId] || sensor.name
215                                elide: Text.ElideRight
216
217                                HoverHandler { id: handler }
218
219                                ToolTip.text: sensor.name
220                                ToolTip.visible: handler.hovered && label.truncated
221                                ToolTip.delay: Kirigami.Units.toolTipDelay
222                            }
223                            ToolButton {
224                                id: editButton
225                                icon.name: "document-edit"
226                                icon.width: Kirigami.Units.iconSizes.small
227                                icon.height: Kirigami.Units.iconSizes.small
228                                Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
229                                Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
230                                onClicked: layout.state = "editing"
231                            }
232                            ToolButton {
233                                id: removeButton
234                                icon.name: "edit-delete-remove"
235                                icon.width: Kirigami.Units.iconSizes.small
236                                icon.height: Kirigami.Units.iconSizes.small
237                                Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
238                                Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
239
240                                onClicked: {
241                                    if (control.selected === undefined || control.selected === null) {
242                                        control.selected = []
243                                    }
244                                    control.selected.splice(control.selected.indexOf(sensor.sensorId), 1)
245                                    control.selectedChanged()
246                                }
247                            }
248                        }
249
250                        Loader {
251                            id: editLoader
252                            active: false
253                            visible: active
254                            focus: active
255                            Layout.fillWidth: true
256                            sourceComponent: RowLayout {
257                                id: editLayout
258                                TextField {
259                                    id: textField
260                                    Layout.fillWidth: true
261                                    text: label.text
262                                    cursorPosition: 0
263                                    focus: true
264                                    onAccepted: {
265                                        if (text == sensor.name) {
266                                            text = ""
267                                        }
268                                        sensorLabelChanged(sensor.sensorId, text)
269                                        layout.state = ""
270                                    }
271                                }
272                                ToolButton {
273                                    icon.name: "checkmark"
274                                    width: Kirigami.Units.iconSizes.smallMedium
275                                    Layout.preferredHeight: textField.implicitHeight
276                                    Layout.preferredWidth: Layout.preferredHeight
277                                    onClicked: textField.accepted()
278                                }
279                            }
280                        }
281
282                        states: State {
283                            name: "editing"
284                            PropertyChanges {
285                                target: normalLayout
286                                visible: false
287                            }
288                            PropertyChanges {
289                                target: editLoader
290                                active: true
291                            }
292                            PropertyChanges {
293                                target: delegate
294                                implicitWidth: control.availableWidth
295                            }
296                        }
297                        transitions: Transition {
298                            PropertyAnimation {
299                                target: delegate
300                                properties: "implicitWidth"
301                                duration: Kirigami.Units.shortDuration
302                                easing.type: Easing.InOutQuad
303                            }
304                        }
305                    }
306                }
307            }
308        }
309
310        Item {
311            width: Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.smallSpacing * 2
312            height: width
313            visible: control.maxAllowedSensors <= 0 || control.selected.length < control.maxAllowedSensors
314        }
315    }
316
317    Popup {
318        id: popup
319
320        // Those bindings will be immediately broken on show, but they're needed to not show the popup at a wrong position for an instant
321        y: (control.Kirigami.ScenePosition.y + control.height + height > control.Window.height)
322            ? - height
323            : control.height
324        implicitHeight: Math.min(contentItem.implicitHeight + 2, Kirigami.Units.gridUnit * 20)
325        width: control.width + 2
326        topMargin: 6
327        bottomMargin: 6
328        Kirigami.Theme.colorSet: Kirigami.Theme.View
329        Kirigami.Theme.inherit: false
330        modal: true
331        dim: false
332        closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
333
334        padding: 1
335
336        onOpened: {
337            if (control.Kirigami.ScenePosition.y + control.height + height > control.Window.height) {
338                y = - height;
339            } else {
340                y = control.height
341            }
342
343            searchField.forceActiveFocus();
344        }
345        onClosed: delegateModel.rootIndex = delegateModel.parentModelIndex()
346
347        contentItem: ColumnLayout {
348            spacing: 0
349            ToolBar {
350                Layout.fillWidth: true
351                Layout.minimumHeight: implicitHeight
352                Layout.maximumHeight: implicitHeight
353                contentItem: ColumnLayout {
354
355                    Kirigami.SearchField {
356                        id: searchField
357                        Layout.fillWidth: true
358                        Layout.fillHeight: true
359                        placeholderText: i18n("Search...")
360                        onTextEdited: listView.searchString = text
361                        onAccepted: listView.searchString = text
362                        KeyNavigation.down: listView
363                    }
364
365                    RowLayout {
366                        visible: delegateModel.rootIndex.valid
367                        Layout.maximumHeight: visible ? implicitHeight : 0
368                        ToolButton {
369                            Layout.fillHeight: true
370                            Layout.preferredWidth: height
371                            icon.name: "go-previous"
372                            text: i18nc("@action:button", "Back")
373                            display: Button.IconOnly
374                            onClicked: delegateModel.rootIndex = delegateModel.parentModelIndex()
375                        }
376                        Kirigami.Heading {
377                            level: 2
378                            text: delegateModel.rootIndex.model ? delegateModel.rootIndex.model.data(delegateModel.rootIndex) : ""
379                        }
380                    }
381                }
382            }
383
384            ScrollView {
385                Layout.fillWidth: true
386                Layout.fillHeight: true
387                ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
388                ListView {
389                    id: listView
390
391                    // this causes us to load at least one delegate
392                    // this is essential in guessing the contentHeight
393                    // which is needed to initially resize the popup
394                    cacheBuffer: 1
395
396                    property string searchString
397
398                    implicitHeight: contentHeight
399
400                    model: DelegateModel {
401                        id: delegateModel
402
403                        model: listView.searchString ? sensorsSearchableModel : treeModel
404                        delegate: Kirigami.BasicListItem {
405                            width: listView.width
406                            text: model.display
407                            reserveSpaceForIcon: false
408
409                            Kirigami.Icon {
410                                source: "go-next-symbolic"
411                                Layout.fillHeight: true
412                                Layout.preferredWidth: Kirigami.Units.iconSizes.small
413                                // Still visible for correct size hints calculation
414                                opacity: model.SensorId.length == 0
415                            }
416                            onClicked: {
417                                if (model.SensorId.length == 0) {
418                                    delegateModel.rootIndex = delegateModel.modelIndex(index);
419                                } else {
420                                    if (control.selected === undefined || control.selected === null) {
421                                        control.selected = []
422                                    }
423                                    const length = control.selected.push(model.SensorId)
424                                    control.selectedChanged()
425                                    if (control.maxAllowedSensors == length) {
426                                        popup.close();
427                                    }
428                                }
429                            }
430                        }
431                    }
432
433                    Sensors.SensorTreeModel { id: treeModel }
434
435                    KItemModels.KSortFilterProxyModel {
436                        id: sensorsSearchableModel
437                        filterCaseSensitivity: Qt.CaseInsensitive
438                        filterString: listView.searchString
439                        sourceModel: KItemModels.KSortFilterProxyModel {
440                            filterRowCallback: function(row, parent) {
441                                var sensorId = sourceModel.data(sourceModel.index(row, 0), Sensors.SensorTreeModel.SensorId)
442                                return sensorId.length > 0
443                            }
444                            sourceModel: KItemModels.KDescendantsProxyModel {
445                                model: listView.searchString ? treeModel : null
446                            }
447                        }
448                    }
449
450                    highlightRangeMode: ListView.ApplyRange
451                    highlightMoveDuration: 0
452                    boundsBehavior: Flickable.StopAtBounds
453                }
454            }
455        }
456
457        background: Item {
458            anchors {
459                fill: parent
460                margins: -1
461            }
462
463            Kirigami.ShadowedRectangle {
464                anchors.fill: parent
465                anchors.margins: 1
466
467                Kirigami.Theme.colorSet: Kirigami.Theme.View
468                Kirigami.Theme.inherit: false
469
470                radius: 2
471                color: Kirigami.Theme.backgroundColor
472
473                property color borderColor: Kirigami.Theme.textColor
474                border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
475                border.width: 1
476
477                shadow.xOffset: 0
478                shadow.yOffset: 2
479                shadow.color: Qt.rgba(0, 0, 0, 0.3)
480                shadow.size: 8
481            }
482        }
483    }
484}
485