Libksysguard

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

KDE's Doxygen guidelines are available online.