Libksysguard

Choices.qml
1 /*
2  * Copyright 2020 Marco Martin <[email protected]>
3  * Copyright 2020 Arjen Hiemstra <[email protected]>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU Library General Public License as
7  * published by the Free Software Foundation; either version 2, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details
14  *
15  * You should have received a copy of the GNU Library General Public
16  * License along with this program; if not, write to the
17  * Free Software Foundation, Inc.,
18  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19  */
20 
21 import QtQuick 2.14
22 import QtQuick.Window 2.14
23 import QtQuick.Controls 2.14
24 import QtQuick.Layouts 1.14
25 import QtQml.Models 2.12
26 
27 import org.kde.kirigami 2.12 as Kirigami
28 import org.kde.kitemmodels 1.0 as KItemModels
29 import org.kde.ksysguard.sensors 1.0 as Sensors
30 
31 Control {
32  id: control
33 
34  property bool supportsColors: true
35  property int maxAllowedSensors: -1
36  property var selected: []
37  property var colors: {}
38 
39  signal selectColor(string sensorId)
40  signal colorForSensorGenerated(string sensorId, color color)
41 
42  onSelectedChanged: {
43  if (!control.selected) {
44  return;
45  }
46  for (let i = 0; i < Math.min(control.selected.length, selectedModel.count); ++i) {
47  selectedModel.set(i, {"sensor": control.selected[i]});
48  }
49  if (selectedModel.count > control.selected.length) {
50  selectedModel.remove(control.selected.length, selectedModel.count - control.selected.length);
51  } else if (selectedModel.count < control.selected.length) {
52  for (let i = selectedModel.count; i < control.selected.length; ++i) {
53  selectedModel.append({"sensor": control.selected[i]});
54  }
55  }
56  }
57 
58  background: TextField {
59  readOnly: true
60  hoverEnabled: false
61 
62  onFocusChanged: {
63  if (focus && (maxAllowedSensors <= 0 || repeater.count < maxAllowedSensors)) {
64  popup.open()
65  } else {
66  popup.close()
67  }
68  }
69  onReleased: {
70  if (focus && (maxAllowedSensors <= 0 || repeater.count < maxAllowedSensors)) {
71  popup.open()
72  }
73  }
74  }
75 
76  contentItem: Flow {
77  spacing: Kirigami.Units.smallSpacing
78 
79  move: Transition {
80  NumberAnimation {
81  properties: "x,y"
82  duration: Kirigami.Units.shortDuration
83  easing.type: Easing.InOutQuad
84  }
85  }
86  Repeater {
87  id: repeater
88  model: ListModel {
89  id: selectedModel
90  function writeSelectedSensors() {
91  let newSelected = [];
92  for (let i = 0; i < count; ++i) {
93  newSelected.push(get(i).sensor);
94  }
95  control.selected = newSelected;
96  control.selectedChanged();
97  }
98  }
99 
100  delegate: Item {
101  id: delegate
102  implicitHeight: layout.implicitHeight + Kirigami.Units.smallSpacing * 2
103  implicitWidth: Math.min(layout.implicitWidth + Kirigami.Units.smallSpacing * 2,
104  control.width - control.leftPadding - control.rightPadding)
105  readonly property int position: index
106  Rectangle {
107  id: delegateContents
108  z: 10
109  color: Qt.rgba(
110  Kirigami.Theme.highlightColor.r,
111  Kirigami.Theme.highlightColor.g,
112  Kirigami.Theme.highlightColor.b,
113  0.25)
114  radius: Kirigami.Units.smallSpacing
115  border.color: Kirigami.Theme.highlightColor
116  border.width: 1
117  opacity: (control.maxAllowedSensors <= 0 || index < control.maxAllowedSensors) ? 1 : 0.4
118  parent: drag.active ? control : delegate
119 
120  width: delegate.width
121  height: delegate.height
122  DragHandler {
123  id: drag
124  //TODO: uncomment as soon as we can depend from 5.15
125  //cursorShape: active ? Qt.ClosedHandCursor : Qt.OpenHandCursor
126  enabled: selectedModel.count > 1
127  onActiveChanged: {
128  if (active) {
129  let pos = delegateContents.mapFromItem(control.contentItem, 0, 0);
130  delegateContents.x = pos.x;
131  delegateContents.y = pos.y;
132  } else {
133  let pos = delegate.mapFromItem(delegateContents, 0, 0);
134  delegateContents.x = pos.x;
135  delegateContents.y = pos.y;
136  dropAnim.restart();
137  selectedModel.writeSelectedSensors();
138  }
139  }
140  xAxis {
141  minimum: 0
142  maximum: control.width - delegateContents.width
143  }
144  yAxis {
145  minimum: 0
146  maximum: control.height - delegateContents.height
147  }
148  onCentroidChanged: {
149  if (!active || control.contentItem.move.running) {
150  return;
151  }
152  let pos = control.contentItem.mapFromItem(null, drag.centroid.scenePosition.x, drag.centroid.scenePosition.y);
153  pos.x = Math.max(0, Math.min(control.contentItem.width - 1, pos.x));
154  pos.y = Math.max(0, Math.min(control.contentItem.height - 1, pos.y));
155 
156  let child = control.contentItem.childAt(pos.x, pos.y);
157  if (child === delegate) {
158  return;
159  } else if (child) {
160  let newIndex = -1;
161  if (pos.x > child.x + child.width/2) {
162  newIndex = Math.min(child.position + 1, selectedModel.count - 1);
163  } else {
164  newIndex = child.position;
165  }
166  selectedModel.move(index, newIndex, 1);
167  }
168  }
169  }
170  ParallelAnimation {
171  id: dropAnim
172  XAnimator {
173  target: delegateContents
174  from: delegateContents.x
175  to: 0
176  duration: Kirigami.Units.shortDuration
177  easing.type: Easing.InOutQuad
178  }
179  YAnimator {
180  target: delegateContents
181  from: delegateContents.y
182  to: 0
183  duration: Kirigami.Units.shortDuration
184  easing.type: Easing.InOutQuad
185  }
186  }
187 
188  Sensors.Sensor { id: sensor; sensorId: model.sensor }
189 
190  Component.onCompleted: {
191  if (typeof control.colors === "undefined" ||
192  typeof control.colors[sensor.sensorId] === "undefined") {
193  let color = Qt.hsva(Math.random(), Kirigami.Theme.highlightColor.hsvSaturation, Kirigami.Theme.highlightColor.hsvValue, 1);
194  control.colorForSensorGenerated(sensor.sensorId, color)
195  }
196  }
197 
198  RowLayout {
199  id: layout
200 
201  anchors.fill: parent
202  anchors.margins: Kirigami.Units.smallSpacing
203 
204  ToolButton {
205  visible: control.supportsColors
206  Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
207  Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
208 
209  padding: Kirigami.Units.smallSpacing
210 
211  contentItem: Rectangle {
212  color: typeof control.colors === "undefined" ? "black" : control.colors[sensor.sensorId]
213  }
214 
215  onClicked: control.selectColor(sensor.sensorId)
216  }
217 
218  Label {
219  id: label
220 
221  Layout.fillWidth: true
222  // Have to use a separate metrics object as contentWidth will be the width of the elided text unfortunately
223  // FIXME: why +2 is necessary?
224  Layout.maximumWidth: labelMetrics.boundingRect.width + Kirigami.Units.gridUnit
225  TextMetrics {
226  id: labelMetrics
227  text: sensor.name
228  font: label.font
229  }
230 
231  text: sensor.name
232  elide: Text.ElideRight
233 
234  HoverHandler { id: handler }
235 
236  ToolTip.text: sensor.name
237  ToolTip.visible: handler.hovered && label.truncated
238  ToolTip.delay: Kirigami.Units.toolTipDelay
239  }
240 
241  ToolButton {
242  icon.name: "edit-delete-remove"
243  icon.width: Kirigami.Units.iconSizes.small
244  icon.height: Kirigami.Units.iconSizes.small
245  Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
246  Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
247 
248  onClicked: {
249  if (control.selected === undefined || control.selected === null) {
250  control.selected = []
251  }
252  control.selected.splice(control.selected.indexOf(sensor.sensorId), 1)
253  control.selectedChanged()
254  }
255  }
256  }
257  }
258  }
259  }
260 
261  Item {
262  width: Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.smallSpacing * 2
263  height: width
264  visible: control.maxAllowedSensors <= 0 || control.selected.length < control.maxAllowedSensors
265  }
266  }
267 
268  Popup {
269  id: popup
270 
271  // Those bindings will be immediately broken on show, but they're needed to not show the popup at a wrong position for an instant
272  y: (control.Kirigami.ScenePosition.y + control.height + height > control.Window.height)
273  ? - height
274  : control.height
275  implicitHeight: Math.min(contentItem.implicitHeight + 2, Kirigami.Units.gridUnit * 20)
276  width: control.width + 2
277  topMargin: 6
278  bottomMargin: 6
279  Kirigami.Theme.colorSet: Kirigami.Theme.View
280  Kirigami.Theme.inherit: false
281  modal: true
282  dim: false
283  closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
284 
285  padding: 1
286 
287  onOpened: {
288  if (control.Kirigami.ScenePosition.y + control.height + height > control.Window.height) {
289  y = - height;
290  } else {
291  y = control.height
292  }
293 
294  searchField.forceActiveFocus();
295  }
296  onClosed: delegateModel.rootIndex = delegateModel.parentModelIndex()
297 
298  contentItem: ColumnLayout {
299  spacing: 0
300  RowLayout {
301  Layout.fillWidth: true
302  Layout.minimumHeight: implicitHeight
303  Layout.maximumHeight: implicitHeight
304  Layout.leftMargin: Kirigami.Units.smallSpacing
305  Layout.topMargin: Kirigami.Units.smallSpacing
306  Layout.rightMargin: Kirigami.Units.smallSpacing
307  Layout.bottomMargin: Kirigami.Units.smallSpacing
308 
309  ToolButton {
310  Layout.fillHeight: true
311  Layout.preferredWidth: height
312  icon.name: "go-previous"
313  text: i18nc("@action:button", "Back")
314  display: Button.IconOnly
315  visible: delegateModel.rootIndex.valid
316  onClicked: delegateModel.rootIndex = delegateModel.parentModelIndex()
317  }
318 
319  TextField {
320  id: searchField
321  Layout.fillWidth: true
322  Layout.fillHeight: true
323  placeholderText: i18n("Search...")
324  onTextEdited: listView.searchString = text
325  KeyNavigation.down: listView
326  }
327  }
329  Layout.fillWidth: true
330  }
331  ScrollView {
332  Layout.fillWidth: true
333  Layout.fillHeight: true
334  ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
335  ListView {
336  id: listView
337 
338  // this causes us to load at least one delegate
339  // this is essential in guessing the contentHeight
340  // which is needed to initially resize the popup
341  cacheBuffer: 1
342 
343  property string searchString
344 
345  implicitHeight: contentHeight
346  model: DelegateModel {
347  id: delegateModel
348 
349  model: listView.searchString ? sensorsSearchableModel : treeModel
350  delegate: Kirigami.BasicListItem {
351  width: listView.width
352  text: model.display
353  reserveSpaceForIcon: false
354 
355  onClicked: {
356  if (model.SensorId.length == 0) {
357  delegateModel.rootIndex = delegateModel.modelIndex(index);
358  } else {
359  if (control.selected === undefined || control.selected === null) {
360  control.selected = []
361  }
362  const length = control.selected.push(model.SensorId)
363  control.selectedChanged()
364  if (control.maxAllowedSensors == length) {
365  popup.close();
366  }
367  }
368  }
369  }
370  }
371 
372  Sensors.SensorTreeModel { id: treeModel }
373 
374  KItemModels.KSortFilterProxyModel {
375  id: sensorsSearchableModel
376  filterCaseSensitivity: Qt.CaseInsensitive
377  filterString: listView.searchString
378  sourceModel: KItemModels.KSortFilterProxyModel {
379  filterRowCallback: function(row, parent) {
380  var sensorId = sourceModel.data(sourceModel.index(row, 0), Sensors.SensorTreeModel.SensorId)
381  return sensorId.length > 0
382  }
383  sourceModel: KItemModels.KDescendantsProxyModel {
384  model: listView.searchString ? treeModel : null
385  }
386  }
387  }
388 
389  highlightRangeMode: ListView.ApplyRange
390  highlightMoveDuration: 0
391  boundsBehavior: Flickable.StopAtBounds
392  }
393  }
394  }
395 
396  background: Item {
397  anchors {
398  fill: parent
399  margins: -1
400  }
401 
402  Kirigami.ShadowedRectangle {
403  anchors.fill: parent
404  anchors.margins: 1
405 
406  Kirigami.Theme.colorSet: Kirigami.Theme.View
407  Kirigami.Theme.inherit: false
408 
409  radius: 2
410  color: Kirigami.Theme.backgroundColor
411 
412  property color borderColor: Kirigami.Theme.textColor
413  border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
414  border.width: 1
415 
416  shadow.xOffset: 0
417  shadow.yOffset: 2
418  shadow.color: Qt.rgba(0, 0, 0, 0.3)
419  shadow.size: 8
420  }
421  }
422  }
423 }
QString i18nc(const char *context, const char *text, const TYPE &arg...)
KGuiItem properties()
QString label(StandardShortcut id)
QString i18n(const char *text, const TYPE &arg...)
if(recurs()&&!first)
KIOCORE_EXPORT CopyJob * move(const QUrl &src, const QUrl &dest, JobFlags flags=DefaultFlags)
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Tue Mar 2 2021 02:46:14 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.