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