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: {
355 while (delegateModel.rootIndex.valid) {
356 delegateModel.rootIndex = delegateModel.parentModelIndex()
357 }
358 }
359
360 contentItem: ColumnLayout {
361 spacing: 0
362 ToolBar {
363 Layout.fillWidth: true
364 Layout.minimumHeight: implicitHeight
365 Layout.maximumHeight: implicitHeight
366 contentItem: ColumnLayout {
367
368 Kirigami.SearchField {
369 id: searchField
370 Layout.fillWidth: true
371 Layout.fillHeight: true
372 placeholderText: i18nd("KSysGuardSensorFaces", "Search...")
373 onTextEdited: listView.searchString = text
374 onAccepted: listView.searchString = text
375 KeyNavigation.down: listView
376 }
377
378 RowLayout {
379 visible: delegateModel.rootIndex.valid
380 Layout.maximumHeight: visible ? implicitHeight : 0
381 ToolButton {
382 Layout.fillHeight: true
383 Layout.preferredWidth: height
384 icon.name: "go-previous"
385 text: i18ndc("KSysGuardSensorFaces", "@action:button", "Back")
386 display: Button.IconOnly
387 onClicked: delegateModel.rootIndex = delegateModel.parentModelIndex()
388 }
390 level: 2
391 text: delegateModel.rootIndex.model ? delegateModel.rootIndex.model.data(delegateModel.rootIndex) : ""
392 }
393 }
394 }
395 }
396
397 ScrollView {
398 Layout.fillWidth: true
399 Layout.fillHeight: true
400 ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
401 clip: true
402
403 ListView {
404 id: listView
405
406 // this causes us to load at least one delegate
407 // this is essential in guessing the contentHeight
408 // which is needed to initially resize the popup
409 cacheBuffer: 1
410
411 property string searchString
412
413 implicitHeight: contentHeight
414
415 model: DelegateModel {
416 id: delegateModel
417
418 model: listView.searchString ? sensorsSearchableModel : treeModel
419 delegate: ItemDelegate {
420 id: listItem
421
422 width: listView.width
423
424 text: model.display
425
426 leftPadding: mirrored ? indicator.implicitWidth + Kirigami.Units.largeSpacing * 2 : Kirigami.Units.largeSpacing
427 rightPadding: !mirrored ? indicator.implicitWidth + Kirigami.Units.largeSpacing * 2 : Kirigami.Units.largeSpacing
428
429 indicator: Kirigami.Icon {
430 anchors.right: parent.right
431 anchors.rightMargin: Kirigami.Units.largeSpacing
432 anchors.verticalCenter: parent.verticalCenter
433
434 width: Kirigami.Units.iconSizes.small
435 height: width
436 source: "go-next-symbolic"
437 opacity: model.SensorId.length == 0
438 }
439
440 onClicked: {
441 if (model.SensorId.length == 0) {
442 delegateModel.rootIndex = delegateModel.modelIndex(index);
443 } else {
444 if (control.selected === undefined || control.selected === null) {
445 control.selected = []
446 }
447 const length = control.selected.push(model.SensorId)
448 control.selectedChanged()
449 if (control.maxAllowedSensors == length) {
450 popup.close();
451 }
452 }
453 }
454 }
455 }
456
457 Sensors.SensorTreeModel { id: treeModel }
458
459 KItemModels.KSortFilterProxyModel {
460 id: sensorsSearchableModel
461 filterCaseSensitivity: Qt.CaseInsensitive
462 filterString: listView.searchString
463 sourceModel: KItemModels.KSortFilterProxyModel {
464 filterRowCallback: function(row, parent) {
465 var sensorId = sourceModel.data(sourceModel.index(row, 0), Sensors.SensorTreeModel.SensorId)
466 return sensorId.length > 0
467 }
468 sourceModel: KItemModels.KDescendantsProxyModel {
469 model: listView.searchString ? treeModel : null
470 }
471 }
472 }
473
474 highlightRangeMode: ListView.ApplyRange
475 highlightMoveDuration: 0
476 boundsBehavior: Flickable.StopAtBounds
477 }
478 }
479 }
480
481 background: Item {
482 anchors {
483 fill: parent
484 margins: -1
485 }
486
487 Kirigami.ShadowedRectangle {
488 anchors.fill: parent
489 anchors.margins: 1
490
491 Kirigami.Theme.colorSet: Kirigami.Theme.View
492 Kirigami.Theme.inherit: false
493
494 radius: 2
495 color: Kirigami.Theme.backgroundColor
496
497 property color borderColor: Kirigami.Theme.textColor
498 border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
499 border.width: 1
500
501 shadow.xOffset: 0
502 shadow.yOffset: 2
503 shadow.color: Qt.rgba(0, 0, 0, 0.3)
504 shadow.size: 8
505 }
506 }
507 }
508}
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...)
QString name(GameStandardAction id)
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)
const_pointer data() const const
ClosedHandCursor
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 14 2025 11:51:50 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.