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