1/*************************************************************************** 2 qgsquickfeatureform.qml 3 -------------------------------------- 4 Date : Nov 2017 5 Copyright : (C) 2017 by Matthias Kuhn 6 Email : matthias@opengis.ch 7 *************************************************************************** 8 * * 9 * This program is free software; you can redistribute it and/or modify * 10 * it under the terms of the GNU General Public License as published by * 11 * the Free Software Foundation; either version 2 of the License, or * 12 * (at your option) any later version. * 13 * * 14 ***************************************************************************/ 15 16import QtQuick 2.6 17import QtQuick.Controls 2.0 18import QtQuick.Dialogs 1.2 19import QtQuick.Layouts 1.3 20import QtGraphicalEffects 1.0 21import QtQml.Models 2.2 22import QtQml 2.2 23 24// We use calendar in datetime widget that is not yet implemented in Controls 2.2 25import QtQuick.Controls 1.4 as Controls1 26 27import QgsQuick 0.1 as QgsQuick 28 29Item { 30 /** 31 * When feature in the form is saved. 32 */ 33 signal saved 34 /** 35 * When the form is about to be closed by closeButton or deleting a feature. 36 */ 37 signal canceled 38 39 /** 40 * A handler for extra events in externalSourceWidget. 41 */ 42 property var externalResourceHandler: QtObject { 43 44 /** 45 * Called when clicked on the camera icon to capture an image. 46 * \param itemWidget editorWidget for modified field to send valueChanged signal. 47 */ 48 property var capturePhoto: function captureImage(itemWidget) { 49 } 50 51 /** 52 * Called when clicked on the gallery icon to choose a file in a gallery. 53 * \param itemWidget editorWidget for modified field to send valueChanged signal. 54 */ 55 property var chooseImage: function chooseImage(itemWidget) { 56 } 57 58 /** 59 * Called when clicked on the photo image. Suppose to be used to bring a bigger preview. 60 * \param imagePath Absolute path to the image. 61 */ 62 property var previewImage: function previewImage(imagePath) { 63 } 64 65 /** 66 * Called when clicked on the trash icon. Suppose to delete the value and optionally also the image. 67 * \param itemWidget editorWidget for modified field to send valueChanged signal. 68 * \param imagePath Absolute path to the image. 69 */ 70 property var removeImage: function removeImage(itemWidget, imagePath) { 71 } 72 73 /** 74 * Called when clicked on the OK icon after taking a photo with the Photo panel. 75 * \param itemWidget editorWidget for modified field to send valueChanged signal. 76 * \param prefixToRelativePath Together with the value creates absolute path 77 * \param value Relative path of taken photo. 78 */ 79 property var confirmImage: function confirmImage(itemWidget, prefixToRelativePath, value) { 80 itemWidget.image.source = prefixToRelativePath + "/" + value 81 itemWidget.valueChanged(value, value === "" || value === null) 82 } 83 } 84 85 /** 86 * Support for custom callback on events happening in widgets 87 */ 88 property var customWidgetCallback: QtObject { 89 90 /** 91 * Called when user clicks on valuerelation widget and combobox shall open 92 * \param widget valuerelation widget for specific field to send valueChanged signal. 93 * \param valueRelationModel model of type FeaturesListModel bears features of related layer. 94 */ 95 property var valueRelationOpened: function valueRelationOpened( widget, valueRelationModel ) { 96 widget.openCombobox() // by default just open combobox 97 } 98 } 99 100 /** 101 * AttributeFormModel binded on a feature supporting auto-generated editor layouts and "tab" layout. 102 */ 103 property QgsQuick.AttributeFormModel model 104 105 /** 106 * Visibility of toolbar. 107 */ 108 property alias toolbarVisible: toolbar.visible 109 110 /** 111 * When adding a new feature, add checkbox to be able to save the same value for the next feature as default. 112 */ 113 property bool allowRememberAttribute: false 114 115 /** 116 * Active project. 117 */ 118 property QgsQuick.Project project 119 120 /** 121 * The function used for a component loader to find qml edit widget components used in form. 122 */ 123 property var loadWidgetFn: QgsQuick.Utils.getEditorComponentSource 124 125 /** 126 * Icon path for save button. 127 */ 128 property string saveButtonIcon: QgsQuick.Utils.getThemeIcon( "ic_save_white" ) 129 /** 130 * Icon path for delete button. 131 */ 132 property string deleteButtonIcon: QgsQuick.Utils.getThemeIcon( "ic_delete_forever_white" ) 133 /** 134 * Icon path for close button 135 */ 136 property string closeButtonIcon: QgsQuick.Utils.getThemeIcon( "ic_clear_white" ) 137 138 /** 139 * Predefined form styling 140 */ 141 property FeatureFormStyling style: FeatureFormStyling {} 142 143 id: form 144 145 states: [ 146 State { 147 name: "ReadOnly" 148 }, 149 State { 150 name: "Edit" 151 }, 152 State { 153 name: "Add" 154 } 155 ] 156 157 function reset() { 158 master.reset() 159 } 160 161 function save() { 162 parent.focus = true 163 if ( form.state === "Add" ) { 164 model.create() 165 state = "Edit" 166 } 167 else 168 { 169 model.save() 170 } 171 172 saved() 173 } 174 175 /** 176 * This is a relay to forward private signals to internal components. 177 */ 178 QtObject { 179 id: master 180 181 /** 182 * This signal is emitted whenever the state of Flickables and TabBars should 183 * be restored. 184 */ 185 signal reset 186 } 187 188 Rectangle { 189 id: container 190 191 clip: true 192 color: form.style.tabs.backgroundColor 193 194 anchors { 195 top: toolbar.bottom 196 bottom: parent.bottom 197 left: parent.left 198 right: parent.right 199 } 200 201 Flickable { 202 id: flickable 203 anchors { 204 left: parent.left 205 right: parent.right 206 } 207 height: form.model.hasTabs ? tabRow.height : 0 208 209 flickableDirection: Flickable.HorizontalFlick 210 contentWidth: tabRow.width 211 212 // Tabs 213 TabBar { 214 id: tabRow 215 visible: model.hasTabs 216 height: form.style.tabs.height 217 spacing: form.style.tabs.spacing 218 219 background: Rectangle { 220 anchors.fill: parent 221 color: form.style.tabs.backgroundColor 222 } 223 224 Connections { 225 target: master 226 onReset: tabRow.currentIndex = 0 227 } 228 229 Connections { 230 target: swipeView 231 onCurrentIndexChanged: tabRow.currentIndex = swipeView.currentIndex 232 } 233 234 Repeater { 235 model: form.model 236 237 TabButton { 238 id: tabButton 239 text: Name 240 leftPadding: 8 * QgsQuick.Utils.dp 241 rightPadding: 8 * QgsQuick.Utils.dp 242 anchors.bottom: parent.bottom 243 244 width: contentItem.width + leftPadding + rightPadding 245 height: form.style.tabs.buttonHeight 246 247 contentItem: Text { 248 // Make sure the width is derived from the text so we can get wider 249 // than the parent item and the Flickable is useful 250 width: paintedWidth 251 text: tabButton.text 252 color: !tabButton.enabled ? form.style.tabs.disabledColor : tabButton.down || 253 tabButton.checked ? form.style.tabs.activeColor : form.style.tabs.normalColor 254 font.weight: tabButton.checked ? Font.DemiBold : Font.Normal 255 256 horizontalAlignment: Text.AlignHCenter 257 verticalAlignment: Text.AlignVCenter 258 } 259 260 background: Rectangle { 261 color: !tabButton.enabled ? form.style.tabs.disabledBackgroundColor : tabButton.down || 262 tabButton.checked ? form.style.tabs.activeBackgroundColor : form.style.tabs.normalBackgroundColor 263 } 264 } 265 } 266 } 267 } 268 269 SwipeView { 270 id: swipeView 271 currentIndex: tabRow.currentIndex 272 anchors { 273 top: flickable.bottom 274 left: parent.left 275 right: parent.right 276 bottom: parent.bottom 277 } 278 279 Repeater { 280 /** 281 * One page per tab in tabbed forms, 1 page in auto forms 282 */ 283 model: form.model.hasTabs ? form.model : 1 284 285 Item { 286 id: formPage 287 property int currentIndex: index 288 289 /** 290 * The main form content area 291 */ 292 Rectangle { 293 anchors.fill: parent 294 color: form.style.backgroundColor 295 opacity: form.style.backgroundOpacity 296 } 297 298 ListView { 299 id: content 300 anchors.fill: parent 301 clip: true 302 spacing: form.style.group.spacing 303 section.property: "Group" 304 section.labelPositioning: ViewSection.CurrentLabelAtStart | ViewSection.InlineLabels 305 section.delegate: Component { 306 307 // section header: group box name 308 Rectangle { 309 width: parent.width 310 height: section === "" ? 0 : form.style.group.height 311 color: form.style.group.marginColor 312 313 Rectangle { 314 anchors.fill: parent 315 anchors { 316 leftMargin: form.style.group.leftMargin 317 rightMargin: form.style.group.rightMargin 318 topMargin: form.style.group.topMargin 319 bottomMargin: form.style.group.bottomMargin 320 } 321 color: form.style.group.backgroundColor 322 323 Text { 324 anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } 325 font.bold: true 326 font.pixelSize: form.style.group.fontPixelSize 327 text: section 328 color: form.style.group.fontColor 329 } 330 } 331 } 332 } 333 334 Connections { 335 target: master 336 onReset: content.contentY = 0 337 } 338 339 model: QgsQuick.SubModel { 340 id: contentModel 341 model: form.model 342 rootIndex: form.model.hasTabs ? form.model.index(currentIndex, 0) : undefined 343 } 344 345 delegate: fieldItem 346 } 347 } 348 } 349 } 350 } 351 352 /** 353 * A field editor 354 */ 355 Component { 356 id: fieldItem 357 358 Item { 359 id: fieldContainer 360 visible: Type === 'field' 361 height: childrenRect.height 362 363 anchors { 364 left: parent.left 365 right: parent.right 366 leftMargin: 12 * QgsQuick.Utils.dp 367 } 368 369 Label { 370 id: fieldLabel 371 372 text: qsTr(Name) || '' 373 font.bold: true 374 color: ConstraintValid ? form.style.constraint.validColor : form.style.constraint.invalidColor 375 } 376 377 Label { 378 id: constraintDescriptionLabel 379 anchors { 380 left: parent.left 381 right: parent.right 382 top: fieldLabel.bottom 383 } 384 385 text: qsTr(ConstraintDescription) 386 height: ConstraintValid ? 0 : undefined 387 visible: !ConstraintValid 388 389 color: form.style.constraint.descriptionColor 390 } 391 392 Item { 393 id: placeholder 394 height: childrenRect.height 395 anchors { left: parent.left; right: rememberCheckbox.left; top: constraintDescriptionLabel.bottom } 396 397 Loader { 398 id: attributeEditorLoader 399 400 height: childrenRect.height 401 anchors { left: parent.left; right: parent.right } 402 403 property var value: AttributeValue 404 property var config: EditorWidgetConfig 405 property var widget: EditorWidget 406 property var field: Field 407 property var constraintValid: ConstraintValid 408 property var homePath: form.project ? form.project.homePath : "" 409 property var customStyle: form.style 410 property var externalResourceHandler: form.externalResourceHandler 411 property bool readOnly: form.state == "ReadOnly" || !AttributeEditable 412 property var featurePair: form.model.attributeModel.featureLayerPair 413 property var activeProject: form.project 414 property var customWidget: form.customWidgetCallback 415 416 active: widget !== 'Hidden' 417 418 source: form.loadWidgetFn(widget.toLowerCase()) 419 } 420 421 Connections { 422 target: attributeEditorLoader.item 423 onValueChanged: { 424 AttributeValue = isNull ? undefined : value 425 } 426 } 427 428 Connections { 429 target: form 430 ignoreUnknownSignals: true 431 onSaved: { 432 if (typeof attributeEditorLoader.item.callbackOnSave === "function") { 433 attributeEditorLoader.item.callbackOnSave() 434 } 435 } 436 } 437 438 Connections { 439 target: form 440 ignoreUnknownSignals: true 441 onCanceled: { 442 if (typeof attributeEditorLoader.item.callbackOnCancel === "function") { 443 attributeEditorLoader.item.callbackOnCancel() 444 } 445 } 446 } 447 } 448 449 CheckBox { 450 id: rememberCheckbox 451 checked: RememberValue ? true : false 452 453 visible: form.allowRememberAttribute && form.state === "Add" && EditorWidget !== "Hidden" 454 width: visible ? undefined : 0 455 456 anchors { right: parent.right; top: fieldLabel.bottom } 457 458 onCheckedChanged: { 459 RememberValue = checked 460 } 461 } 462 } 463 } 464 465 Connections { 466 target: Qt.inputMethod 467 onVisibleChanged: { 468 Qt.inputMethod.commit() 469 } 470 } 471 472 /** The form toolbar **/ 473 Item { 474 id: toolbar 475 height: visible ? 48 * QgsQuick.Utils.dp : 0 476 visible: form.state === 'Add' 477 anchors { 478 top: parent.top 479 left: parent.left 480 right: parent.right 481 } 482 483 RowLayout { 484 anchors.fill: parent 485 Layout.margins: 0 486 487 ToolButton { 488 id: saveButton 489 Layout.preferredWidth: form.style.toolbutton.size 490 Layout.preferredHeight: form.style.toolbutton.size 491 492 visible: form.state !== "ReadOnly" 493 494 contentItem: Image { 495 source: form.saveButtonIcon 496 sourceSize: Qt.size(width, height) 497 } 498 499 background: Rectangle { 500 color: model.constraintsValid ? form.style.toolbutton.backgroundColor : form.style.toolbutton.backgroundColorInvalid 501 } 502 503 enabled: model.constraintsValid 504 505 onClicked: { 506 form.save() 507 } 508 } 509 510 ToolButton { 511 id: deleteButton 512 513 Layout.preferredWidth: form.style.toolbutton.size 514 Layout.preferredHeight: form.style.toolbutton.size 515 516 visible: form.state === "Edit" 517 518 contentItem: Image { 519 source: form.deleteButtonIcon 520 sourceSize: Qt.size(width, height) 521 } 522 523 background: Rectangle { 524 color: form.style.toolbutton.backgroundColor 525 } 526 527 onClicked: deleteDialog.visible = true 528 } 529 530 Label { 531 id: titleLabel 532 533 text: 534 { 535 var currentLayer = model.attributeModel.featureLayerPair.layer 536 var layerName = 'N/A' 537 if (!!currentLayer) 538 layerName = currentLayer.name 539 540 if ( form.state === 'Add' ) 541 qsTr( 'Add feature on <i>%1</i>' ).arg(layerName ) 542 else if ( form.state === 'Edit' ) 543 qsTr( 'Edit feature on <i>%1</i>' ).arg(layerName) 544 else 545 qsTr( 'View feature on <i>%1</i>' ).arg(layerName) 546 } 547 font.bold: true 548 font.pointSize: 16 549 elide: Label.ElideRight 550 horizontalAlignment: Qt.AlignHCenter 551 verticalAlignment: Qt.AlignVCenter 552 Layout.fillWidth: true 553 color: "white" 554 } 555 556 ToolButton { 557 id: closeButton 558 anchors.right: parent.right 559 560 Layout.preferredWidth: form.style.toolbutton.size 561 Layout.preferredHeight: form.style.toolbutton.size 562 563 contentItem: Image { 564 source: form.closeButtonIcon 565 sourceSize: Qt.size(width, height) 566 } 567 568 background: Rectangle { 569 color: form.style.toolbutton.backgroundColor 570 } 571 572 onClicked: { 573 Qt.inputMethod.hide() 574 form.canceled() 575 } 576 } 577 } 578 } 579 580 MessageDialog { 581 id: deleteDialog 582 583 visible: false 584 585 title: qsTr( "Delete feature" ) 586 text: qsTr( "Really delete this feature?" ) 587 icon: StandardIcon.Warning 588 standardButtons: StandardButton.Ok | StandardButton.Cancel 589 onAccepted: { 590 model.attributeModel.deleteFeature() 591 visible = false 592 593 form.canceled() 594 } 595 onRejected: { 596 visible = false 597 } 598 } 599} 600 601