From 1e4e35ab1c6381ce037f23e6d93b722db62bf094 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Wed, 31 Jan 2024 21:52:03 +0100 Subject: [PATCH 01/28] organise inputs and form editors --- app/qml/CMakeLists.txt | 38 +++++++-- app/qml/{inputs => components}/MMCheckBox.qml | 0 app/qml/components/MMComboBoxDrawer.qml | 4 +- app/qml/components/MMDrawer.qml | 2 +- .../editors/MMButtonFormEditor.qml} | 5 +- .../editors/MMCalendarFormEditor.qml} | 5 +- .../editors/MMDropdownFormEditor.qml} | 5 +- .../editors/MMGalleryFormEditor.qml} | 2 + .../editors/MMNumberFormEditor.qml} | 81 +++++++++++++------ .../editors/MMPhotoFormEditor.qml} | 5 +- .../editors/MMScannerFormEditor.qml} | 7 +- .../editors/MMSliderFormEditor.qml} | 78 +++++++++++++----- .../editors/MMSwitchFormEditor.qml} | 5 +- .../editors/MMTextMultilineFormEditor.qml} | 5 +- .../{MMAbstractEditor.qml => MMBaseInput.qml} | 2 + ...PasswordEditor.qml => MMPasswordInput.qml} | 2 +- .../{MMSearchEditor.qml => MMSearchInput.qml} | 2 +- .../{MMInputEditor.qml => MMTextInput.qml} | 38 ++++++--- app/qml/onboarding/MMCreateWorkspace.qml | 2 +- app/qml/onboarding/MMHowYouFoundUs.qml | 2 +- app/qml/onboarding/MMLogin.qml | 8 +- app/qml/onboarding/MMSignUp.qml | 8 +- app/qml/onboarding/MMWhichIndustry.qml | 4 +- gallery/qml.qrc | 79 +++++++++--------- gallery/qml/pages/CalendarPage.qml | 8 +- gallery/qml/pages/DrawerPage.qml | 19 +++-- gallery/qml/pages/EditorsPage.qml | 34 ++++---- gallery/qml/pages/FormPage.qml | 2 +- gallery/qml/pages/NotificationPage.qml | 12 +-- 29 files changed, 298 insertions(+), 166 deletions(-) rename app/qml/{inputs => components}/MMCheckBox.qml (100%) rename app/qml/{inputs/MMButtonInputEditor.qml => form/editors/MMButtonFormEditor.qml} (97%) rename app/qml/{inputs/MMCalendarEditor.qml => form/editors/MMCalendarFormEditor.qml} (98%) rename app/qml/{inputs/MMComboBoxEditor.qml => form/editors/MMDropdownFormEditor.qml} (97%) rename app/qml/{components/MMPhotoGallery.qml => form/editors/MMGalleryFormEditor.qml} (99%) rename app/qml/{inputs/MMNumberEditor.qml => form/editors/MMNumberFormEditor.qml} (62%) rename app/qml/{inputs/MMPhotoEditor.qml => form/editors/MMPhotoFormEditor.qml} (96%) rename app/qml/{inputs/MMQrCodeEditor.qml => form/editors/MMScannerFormEditor.qml} (94%) rename app/qml/{inputs/MMSliderEditor.qml => form/editors/MMSliderFormEditor.qml} (51%) rename app/qml/{inputs/MMSwitchEditor.qml => form/editors/MMSwitchFormEditor.qml} (95%) rename app/qml/{inputs/MMTextAreaEditor.qml => form/editors/MMTextMultilineFormEditor.qml} (96%) rename app/qml/inputs/{MMAbstractEditor.qml => MMBaseInput.qml} (98%) rename app/qml/inputs/{MMPasswordEditor.qml => MMPasswordInput.qml} (98%) rename app/qml/inputs/{MMSearchEditor.qml => MMSearchInput.qml} (99%) rename app/qml/inputs/{MMInputEditor.qml => MMTextInput.qml} (67%) diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index 9477e47f6..667f6a8a2 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -36,6 +36,7 @@ set(MM_QML components/MMRoundButton.qml components/MMButton.qml components/MMComponent_reachedDataLimit.qml + components/MMCheckBox.qml components/MMDrawer.qml components/MMHeader.qml components/MMHlineText.qml @@ -54,7 +55,6 @@ set(MM_QML components/MMNotification.qml components/MMNotificationView.qml components/MMPhoto.qml - components/MMPhotoGallery.qml components/MMProgressBar.qml components/MMProjectItem.qml components/MMRadioButton.qml @@ -100,11 +100,20 @@ set(MM_QML editor/inputvaluerelationcombobox.qml editor/inputvaluerelationpage.qml editor/inputspacer.qml - inputs/MMAbstractEditor.qml - inputs/MMCheckBox.qml - inputs/MMInputEditor.qml - inputs/MMPasswordEditor.qml - inputs/MMSliderEditor.qml + + # + # Inputs + # + + inputs/MMBaseInput.qml + inputs/MMPasswordInput.qml + inputs/MMTextInput.qml + inputs/MMSearchInput.qml + + # + # Forms + # + form/FeatureForm.qml form/FeatureFormPage.qml form/FeatureFormStyling.qml @@ -112,6 +121,23 @@ set(MM_QML form/FormWrapper.qml form/MMFormTabBar.qml form/PreviewPanel.qml + form/editors/MMButtonFormEditor.qml + form/editors/MMCalendarFormEditor.qml + form/editors/MMDropdownFormEditor.qml + form/editors/MMGalleryFormEditor.qml + form/editors/MMNumberFormEditor.qml + form/editors/MMPhotoFormEditor.qml + form/editors/MMScannerFormEditor.qml + form/editors/MMSliderFormEditor.qml + form/editors/MMSwitchFormEditor.qml + form/editors/MMTextMultilineFormEditor.qml + form/editors/MMTextFormEditor.qml + # we are missing the following form editors: + # - Spacer + # - Text / HTML widget + # - Relation reference + # - Relations + layers/FeaturesListPageV2.qml layers/LayerDetail.qml layers/LayersListPageV2.qml diff --git a/app/qml/inputs/MMCheckBox.qml b/app/qml/components/MMCheckBox.qml similarity index 100% rename from app/qml/inputs/MMCheckBox.qml rename to app/qml/components/MMCheckBox.qml diff --git a/app/qml/components/MMComboBoxDrawer.qml b/app/qml/components/MMComboBoxDrawer.qml index b67219933..70252952c 100644 --- a/app/qml/components/MMComboBoxDrawer.qml +++ b/app/qml/components/MMComboBoxDrawer.qml @@ -12,6 +12,8 @@ import QtQuick.Controls import QtQuick.Controls.Basic import "../inputs" + +// TODO: rename to MMDropdownDrawer Drawer { id: root @@ -84,7 +86,7 @@ Drawer { } } - MMSearchEditor { + MMSearchInput { id: searchBar width: parent.width - 2 * root.padding diff --git a/app/qml/components/MMDrawer.qml b/app/qml/components/MMDrawer.qml index 7cd47c7b5..f0333bfb0 100644 --- a/app/qml/components/MMDrawer.qml +++ b/app/qml/components/MMDrawer.qml @@ -89,7 +89,7 @@ Drawer { MouseArea { anchors.fill: parent - onClicked: control.visible = false + onClicked: control.close() } } } diff --git a/app/qml/inputs/MMButtonInputEditor.qml b/app/qml/form/editors/MMButtonFormEditor.qml similarity index 97% rename from app/qml/inputs/MMButtonInputEditor.qml rename to app/qml/form/editors/MMButtonFormEditor.qml index be1e1aa3b..eacc6c34d 100644 --- a/app/qml/inputs/MMButtonInputEditor.qml +++ b/app/qml/form/editors/MMButtonFormEditor.qml @@ -10,9 +10,10 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../components" +import "../../components" +import "../../inputs" -MMAbstractEditor { +MMBaseInput { id: root property var parentValue: parent.value ?? "" diff --git a/app/qml/inputs/MMCalendarEditor.qml b/app/qml/form/editors/MMCalendarFormEditor.qml similarity index 98% rename from app/qml/inputs/MMCalendarEditor.qml rename to app/qml/form/editors/MMCalendarFormEditor.qml index 22803149f..9b8bacc2a 100644 --- a/app/qml/inputs/MMCalendarEditor.qml +++ b/app/qml/form/editors/MMCalendarFormEditor.qml @@ -10,9 +10,10 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../components" +import "../../components" +import "../../inputs" -MMAbstractEditor { +MMBaseInput { id: root property var parentField: parent.field ?? "" diff --git a/app/qml/inputs/MMComboBoxEditor.qml b/app/qml/form/editors/MMDropdownFormEditor.qml similarity index 97% rename from app/qml/inputs/MMComboBoxEditor.qml rename to app/qml/form/editors/MMDropdownFormEditor.qml index 2fdc39db1..11e817fcd 100644 --- a/app/qml/inputs/MMComboBoxEditor.qml +++ b/app/qml/form/editors/MMDropdownFormEditor.qml @@ -10,9 +10,10 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../components" +import "../../components" +import "../../inputs" -MMAbstractEditor { +MMBaseInput { id: root property alias placeholderText: textField.placeholderText diff --git a/app/qml/components/MMPhotoGallery.qml b/app/qml/form/editors/MMGalleryFormEditor.qml similarity index 99% rename from app/qml/components/MMPhotoGallery.qml rename to app/qml/form/editors/MMGalleryFormEditor.qml index 015753c1a..7c6c6e6de 100644 --- a/app/qml/components/MMPhotoGallery.qml +++ b/app/qml/form/editors/MMGalleryFormEditor.qml @@ -10,6 +10,8 @@ import QtQuick import QtQuick.Controls +import "../../components" + Item { id: root diff --git a/app/qml/inputs/MMNumberEditor.qml b/app/qml/form/editors/MMNumberFormEditor.qml similarity index 62% rename from app/qml/inputs/MMNumberEditor.qml rename to app/qml/form/editors/MMNumberFormEditor.qml index 8a137bad2..61fa9585d 100644 --- a/app/qml/inputs/MMNumberEditor.qml +++ b/app/qml/form/editors/MMNumberFormEditor.qml @@ -10,32 +10,52 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../components" -MMAbstractEditor { +import "../../components" +import "../../inputs" + +/* + * Number (range editable) editor for QGIS Attribute Form + * Requires various global properties set to function, see featureform Loader section. + * These properties are injected here via 'fieldXYZ' properties and captured with underscore `_`. + * + * Should be used only within feature form. + */ + +MMBaseInput { id: root - property var parentValue: parent.value ?? 0 - property bool parentValueIsNull: parent.valueIsNull ?? false - property bool isReadOnly: parent.readOnly ?? false + property var _fieldValue: parent.fieldValue + property var _fieldConfig: parent.fieldConfig + property bool _fieldValueIsNull: parent.fieldValueIsNull + + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle + property bool _fieldIsReadOnly: parent.fieldIsReadOnly + + property string _fieldTitle: parent.fieldTitle + property string _fieldErrorMessage: parent.fieldErrorMessage + property string _fieldWarningMessage: parent.fieldWarningMessage - property var locale: Qt.locale() - // TODO: uncomment in Input app - property real precision//: config['Precision'] ? config['Precision'] : 0 - property string suffix//: config['Suffix'] ? config['Suffix'] : '' - property real from //: config["Min"] - property real to //: config["Max"] + property real to: _fieldConfig["Max"] + property real from: _fieldConfig["Min"] + property string suffix: _fieldConfig['Suffix'] ? _fieldConfig['Suffix'] : '' + property real precision: _fieldConfig['Precision'] ? _fieldConfig['Precision'] : 0 property alias placeholderText: numberInput.placeholderText // don't ever use a step smaller than would be visible in the widget // i.e. if showing 2 decimals, smallest increment will be 0.01 // https://github.com/qgis/QGIS/blob/a038a79997fb560e797daf3903d94c7d68e25f42/src/gui/editorwidgets/qgsdoublespinbox.cpp#L83-L87 - property real step//: Math.max(config["Step"], Math.pow( 10.0, 0.0 - precision )) + property real step: Math.max(_fieldConfig["Step"], Math.pow( 10.0, 0.0 - precision )) signal editorValueChanged( var newValue, var isNull ) - enabled: !isReadOnly + title: _fieldShouldShowTitle ? _fieldTitle : "" + + errorMsg: _fieldErrorMessage + warningMsg: _fieldWarningMessage + + enabled: !_fieldIsReadOnly hasFocus: numberInput.activeFocus leftAction: MMIcon { @@ -48,8 +68,17 @@ MMAbstractEditor { enabled: Number( numberInput.text ) - root.step >= root.from } + onLeftActionClicked: { + if ( leftIcon.enabled ) + { + let decremented = Number( numberInput.text ) - root.step + root.editorValueChanged( decremented.toFixed( root.precision ), false ) + } + } + content: Item { anchors.fill: parent + Row { height: parent.height anchors.horizontalCenter: parent.horizontalCenter @@ -60,12 +89,16 @@ MMAbstractEditor { height: parent.height - clip: true - text: root.parentValue === undefined || root.parentValueIsNull ? "" : root.parentValue - color: root.enabled ? __style.nightColor : __style.mediumGreenColor + text: root._fieldValue === undefined || root._fieldValueIsNull ? '' : root._fieldValue + placeholderTextColor: __style.nightAlphaColor + color: root.enabled ? __style.nightColor : __style.mediumGreenColor + font: __style.p5 + + clip: true hoverEnabled: true + verticalAlignment: Qt.AlignVCenter inputMethodHints: Qt.ImhFormattedNumbersOnly @@ -83,7 +116,7 @@ MMAbstractEditor { Text { id: suffix - text: root.suffix + text: root.suffix ? ' ' + root.suffix : "" // to make sure there is a space between the number and the suffix visible: root.suffix !== "" && numberInput.text !== "" @@ -106,18 +139,14 @@ MMAbstractEditor { enabled: Number( numberInput.text ) + root.step <= root.to } - onLeftActionClicked: { - numberInput.forceActiveFocus() - if ( leftIcon.enabled ) { - let decremented = Number( numberInput.text ) - root.step - root.editorValueChanged( decremented.toFixed( root.precision ), false ) - } - } onRightActionClicked: { - numberInput.forceActiveFocus(); - if ( rightIcon.enabled ) { + if ( rightIcon.enabled ) + { let incremented = Number( numberInput.text ) + root.step root.editorValueChanged( incremented.toFixed( root.precision ), false ) } } + + // on press and hold behavior can be used from here: + // https://github.com/mburakov/qt5/blob/93bfa3874c10f6cb5aa376f24363513ba8264117/qtquickcontrols/src/controls/SpinBox.qml#L306-L309 } diff --git a/app/qml/inputs/MMPhotoEditor.qml b/app/qml/form/editors/MMPhotoFormEditor.qml similarity index 96% rename from app/qml/inputs/MMPhotoEditor.qml rename to app/qml/form/editors/MMPhotoFormEditor.qml index 6b2e8c257..5fceb033b 100644 --- a/app/qml/inputs/MMPhotoEditor.qml +++ b/app/qml/form/editors/MMPhotoFormEditor.qml @@ -10,9 +10,10 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../components" +import "../../components" +import "../../inputs" -MMAbstractEditor { +MMBaseInput { id: root property var parentValue: parent.value ?? "" diff --git a/app/qml/inputs/MMQrCodeEditor.qml b/app/qml/form/editors/MMScannerFormEditor.qml similarity index 94% rename from app/qml/inputs/MMQrCodeEditor.qml rename to app/qml/form/editors/MMScannerFormEditor.qml index 32a9aa1dc..ebcc3d052 100644 --- a/app/qml/inputs/MMQrCodeEditor.qml +++ b/app/qml/form/editors/MMScannerFormEditor.qml @@ -10,9 +10,10 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../components" +import "../../components" +import "../../inputs" -MMAbstractEditor { +MMBaseInput { id: root property var parentValue: parent.value @@ -41,6 +42,8 @@ MMAbstractEditor { background: Rectangle { color: __style.transparentColor } + + onTextChanged: root.editorValueChanged( text, text === "" ) } rightAction: MMIcon { diff --git a/app/qml/inputs/MMSliderEditor.qml b/app/qml/form/editors/MMSliderFormEditor.qml similarity index 51% rename from app/qml/inputs/MMSliderEditor.qml rename to app/qml/form/editors/MMSliderFormEditor.qml index 35545f411..e4fde5055 100644 --- a/app/qml/inputs/MMSliderEditor.qml +++ b/app/qml/form/editors/MMSliderFormEditor.qml @@ -11,27 +11,41 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic import QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import ".." -MMAbstractEditor { +import "../../inputs" + +/* + * Number slider editor for QGIS Attribute Form + * Requires various global properties set to function, see featureform Loader section. + * These properties are injected here via 'fieldXYZ' properties and captured with underscore `_`. + * + * Should be used only within feature form. + */ + +MMBaseInput { id: root - property var parentValue: parent.value - property bool parentValueIsNull: parent.valueIsNull ?? false - property bool isReadOnly: parent.readOnly ?? false + property var _fieldValue: parent.fieldValue + property var _fieldConfig: parent.fieldConfig + + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle + property bool _fieldIsReadOnly: parent.fieldIsReadOnly - property int precision: 1 //config["Precision"] - required property real from //getRange(config["Min"], -max_range) - required property real to //getRange(config["Max"], max_range) - property real step: 1 //config["Step"] ? config["Step"] : 1 - property string suffix: "" //config["Suffix"] ? config["Suffix"] : "" - property var locale: Qt.locale() + property string _fieldTitle: parent.fieldTitle + property string _fieldErrorMessage: parent.fieldErrorMessage + property string _fieldWarningMessage: parent.fieldWarningMessage signal editorValueChanged( var newValue, var isNull ) + title: _fieldShouldShowTitle ? _fieldTitle : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage + hasFocus: slider.activeFocus + enabled: !_fieldIsReadOnly + content: Item { id: input @@ -51,12 +65,12 @@ MMAbstractEditor { Layout.maximumHeight: input.height elide: Text.ElideRight - text: Number( slider.value ).toFixed( precision ).toLocaleString( root.locale ) + root.suffix + text: Number( slider.value ).toFixed( internal.precision ).toLocaleString( root.locale ) + ' ' + internal.suffix verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignLeft font: __style.p5 - color: __style.nightColor + color: root.enabled ? __style.nightColor : __style.mediumGreenColor } Slider { @@ -66,12 +80,12 @@ MMAbstractEditor { Layout.maximumHeight: input.height Layout.preferredHeight: input.height - to: root.to - from: root.from - stepSize: root.step - value: root.parentValue ? root.parentValue : 0 + to: internal.to + from: internal.from + stepSize: internal.step + value: root._fieldValue ? root._fieldValue : 0 - onValueChanged: { root.editorValueChanged( slider.value, false ); forceActiveFocus() } + onValueChanged: root.editorValueChanged( slider.value, false ) background: Rectangle { x: slider.leftPadding @@ -95,4 +109,30 @@ MMAbstractEditor { } } } + + QtObject { + id: internal + + property var locale: Qt.locale() + + property real from: fixRange( _fieldConfig["Min"] ) + property real to: fixRange( _fieldConfig["Max"] ) + + property int precision: _fieldConfig["Precision"] + property real step: _fieldConfig["Step"] ? _fieldConfig["Step"] : 1 + property string suffix: _fieldConfig["Suffix"] ? _fieldConfig["Suffix"] : "" + + readonly property int intMax: 2000000000 // https://doc.qt.io/qt-5/qml-int.html + + function fixRange( rangeValue ) { + if ( typeof rangeValue !== 'undefined' ) { + + if ( rangeValue >= -internal.intMax && rangeValue <= internal.intMax ) { + return rangeValue + } + } + + return internal.intMax + } + } } diff --git a/app/qml/inputs/MMSwitchEditor.qml b/app/qml/form/editors/MMSwitchFormEditor.qml similarity index 95% rename from app/qml/inputs/MMSwitchEditor.qml rename to app/qml/form/editors/MMSwitchFormEditor.qml index bc88f8f9f..ad53cc9be 100644 --- a/app/qml/inputs/MMSwitchEditor.qml +++ b/app/qml/form/editors/MMSwitchFormEditor.qml @@ -10,9 +10,10 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../components" +import "../../components" +import "../../inputs" -MMAbstractEditor { +MMBaseInput { id: root property var parentValue: parent.value ?? false diff --git a/app/qml/inputs/MMTextAreaEditor.qml b/app/qml/form/editors/MMTextMultilineFormEditor.qml similarity index 96% rename from app/qml/inputs/MMTextAreaEditor.qml rename to app/qml/form/editors/MMTextMultilineFormEditor.qml index d16d78d6c..ba39396db 100644 --- a/app/qml/inputs/MMTextAreaEditor.qml +++ b/app/qml/form/editors/MMTextMultilineFormEditor.qml @@ -10,9 +10,10 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../components" +import "../../components" +import "../../inputs" -MMAbstractEditor { +MMBaseInput { id: root property var parentValue: parent.value ?? "" diff --git a/app/qml/inputs/MMAbstractEditor.qml b/app/qml/inputs/MMBaseInput.qml similarity index 98% rename from app/qml/inputs/MMAbstractEditor.qml rename to app/qml/inputs/MMBaseInput.qml index 311edb07c..476f3dee9 100644 --- a/app/qml/inputs/MMAbstractEditor.qml +++ b/app/qml/inputs/MMBaseInput.qml @@ -15,6 +15,8 @@ import QtQuick.Layouts import "../components" import "." +//! This is a base class for all inputs/form editors, do not use this in the app directly + Item { id: root diff --git a/app/qml/inputs/MMPasswordEditor.qml b/app/qml/inputs/MMPasswordInput.qml similarity index 98% rename from app/qml/inputs/MMPasswordEditor.qml rename to app/qml/inputs/MMPasswordInput.qml index 94c550ab3..4d47ef4a0 100644 --- a/app/qml/inputs/MMPasswordEditor.qml +++ b/app/qml/inputs/MMPasswordInput.qml @@ -12,7 +12,7 @@ import QtQuick.Controls import QtQuick.Controls.Basic import "../components" -MMAbstractEditor { +MMBaseInput { id: root property alias placeholderText: textField.placeholderText diff --git a/app/qml/inputs/MMSearchEditor.qml b/app/qml/inputs/MMSearchInput.qml similarity index 99% rename from app/qml/inputs/MMSearchEditor.qml rename to app/qml/inputs/MMSearchInput.qml index 6dfa38355..bd2ae6558 100644 --- a/app/qml/inputs/MMSearchEditor.qml +++ b/app/qml/inputs/MMSearchInput.qml @@ -12,7 +12,7 @@ import QtQuick.Controls import QtQuick.Controls.Basic import "../components" -MMAbstractEditor { +MMBaseInput { id: root property alias placeholderText: textField.placeholderText diff --git a/app/qml/inputs/MMInputEditor.qml b/app/qml/inputs/MMTextInput.qml similarity index 67% rename from app/qml/inputs/MMInputEditor.qml rename to app/qml/inputs/MMTextInput.qml index 75e5631a1..88ab34400 100644 --- a/app/qml/inputs/MMInputEditor.qml +++ b/app/qml/inputs/MMTextInput.qml @@ -12,17 +12,23 @@ import QtQuick.Controls import QtQuick.Controls.Basic import "../components" -MMAbstractEditor { - id: root +/* + * Common text input to use in the app. + * Disabled state can be achieved by setting `enabled: false`. + * + * See MMBaseInput for more properties. + */ - property var parentValue: parent.value ?? "" - property bool parentValueIsNull: parent.valueIsNull ?? false - property bool isReadOnly: parent.readOnly ?? false +MMBaseInput { + id: root - property alias placeholderText: textField.placeholderText + property bool showClearIcon: true property alias text: textField.text + property alias placeholderText: textField.placeholderText + + property alias textFieldComponent: textField - signal editorValueChanged( var newValue, var isNull ) + signal textEdited( string text ) hasFocus: textField.activeFocus @@ -31,15 +37,17 @@ MMAbstractEditor { anchors.fill: parent - text: root.parentValue - color: root.enabled ? __style.nightColor : __style.mediumGreenColor placeholderTextColor: __style.nightAlphaColor + color: root.enabled ? __style.nightColor : __style.mediumGreenColor + font: __style.p5 hoverEnabled: true background: Rectangle { color: __style.transparentColor } + + onTextEdited: root.textEdited( textField.text ) } rightAction: MMIcon { @@ -49,8 +57,16 @@ MMAbstractEditor { source: __style.xMarkIcon color: root.enabled ? __style.forestColor : __style.mediumGreenColor - visible: textField.activeFocus && textField.text.length>0 + visible: root.showClearIcon && textField.activeFocus && textField.text.length > 0 } - onRightActionClicked: textField.text = "" + onRightActionClicked: { + if (root.showClearIcon) { + textField.clear() + } + else { + // if the clear button should not be there, let's open keyboard instead + textField.forceActiveFocus() + } + } } diff --git a/app/qml/onboarding/MMCreateWorkspace.qml b/app/qml/onboarding/MMCreateWorkspace.qml index ea94d0d08..46cdf5620 100644 --- a/app/qml/onboarding/MMCreateWorkspace.qml +++ b/app/qml/onboarding/MMCreateWorkspace.qml @@ -98,7 +98,7 @@ Page { Item { width: 1; height: 1 } - MMInputEditor { + MMTextInput { id: workspaceName width: parent.width - 2 * root.hPadding title: qsTr("Workspace name") diff --git a/app/qml/onboarding/MMHowYouFoundUs.qml b/app/qml/onboarding/MMHowYouFoundUs.qml index 22ddaf1ce..d3b1c44bd 100644 --- a/app/qml/onboarding/MMHowYouFoundUs.qml +++ b/app/qml/onboarding/MMHowYouFoundUs.qml @@ -145,7 +145,7 @@ Page { topPadding: 20 * __dp visible: listView.model.count === listView.currentIndex + 1 // === Other - MMInputEditor { + MMTextInput { id: otherSourceText title: qsTr("Source") placeholderText: root.specifySourceText diff --git a/app/qml/onboarding/MMLogin.qml b/app/qml/onboarding/MMLogin.qml index 73fc8b51b..a44d2a297 100644 --- a/app/qml/onboarding/MMLogin.qml +++ b/app/qml/onboarding/MMLogin.qml @@ -106,14 +106,14 @@ Page { width: root.width - 2 * root.hPadding } - MMInputEditor { + MMTextInput { id: username width: parent.width - 2 * root.hPadding - title: qsTr("Username") + title: qsTr("Email or username") bgColor: __style.lightGreenColor } - MMPasswordEditor { + MMPasswordInput { id: password width: parent.width - 2 * root.hPadding title: qsTr("Password") @@ -187,7 +187,7 @@ Page { title: qsTr("Change server") primaryButton: qsTr("Confirm") visible: false - specialComponent: MMInputEditor { + specialComponent: MMTextInput { width: changeServerDrawer.width - 40 * __dp title: qsTr("Server address") bgColor: __style.lightGreenColor diff --git a/app/qml/onboarding/MMSignUp.qml b/app/qml/onboarding/MMSignUp.qml index 6d1b8f6f5..b005e1b23 100644 --- a/app/qml/onboarding/MMSignUp.qml +++ b/app/qml/onboarding/MMSignUp.qml @@ -134,28 +134,28 @@ Page { topPadding: 20 * __dp bottomPadding: 20 * __dp - MMInputEditor { + MMTextInput { id: username width: parent.width - 2 * root.hPadding title: qsTr("Username") bgColor: __style.lightGreenColor } - MMInputEditor { + MMTextInput { id: email width: parent.width - 2 * root.hPadding title: qsTr("Email address") bgColor: __style.lightGreenColor } - MMPasswordEditor { + MMPasswordInput { id: password width: parent.width - 2 * root.hPadding title: qsTr("Password") bgColor: __style.lightGreenColor } - MMPasswordEditor { + MMPasswordInput { id: passwordConfirm width: parent.width - 2 * root.hPadding title: qsTr("Confirm password") diff --git a/app/qml/onboarding/MMWhichIndustry.qml b/app/qml/onboarding/MMWhichIndustry.qml index 9005ef579..6b7c81f5b 100644 --- a/app/qml/onboarding/MMWhichIndustry.qml +++ b/app/qml/onboarding/MMWhichIndustry.qml @@ -94,7 +94,7 @@ Page { Component.onCompleted: { listView.model.append({name: qsTr("Agriculture"), icon: __style.tractorIcon, colorx: __style.sunColor, color: "#F4CB46"}) listView.model.append({name: qsTr("Archaeology"), icon: __style.archaeologyIcon, colorx: __style.sandColor, color: "#FFF4E2"}) - listView.model.append({name: qsTr("onstruction and engineering"), icon: __style.engineeringIcon, colorx: __style.roseColor, color: "#FFBABC"}) + listView.model.append({name: qsTr("Construction and engineering"), icon: __style.engineeringIcon, colorx: __style.roseColor, color: "#FFBABC"}) listView.model.append({name: qsTr("Electric utilities"), icon: __style.electricityIcon, colorx: __style.nightColor, color: "#12181F"}) listView.model.append({name: qsTr("Environmental protection"), icon: __style.environmentalIcon, colorx: __style.fieldColor, color: "#9BD1A9"}) listView.model.append({name: qsTr("Local governments"), icon: __style.stateAndLocalIcon, colorx: __style.purpleColor, color: "#CCBDF5"}) @@ -143,7 +143,7 @@ Page { topPadding: 20 * __dp visible: listView.model.count === listView.currentIndex + 1 - MMInputEditor { + MMTextInput { title: qsTr("Source") placeholderText: root.specifyIndustryText onTextChanged: root.selectedText = text diff --git a/gallery/qml.qrc b/gallery/qml.qrc index 9deb76599..7d96a378c 100644 --- a/gallery/qml.qrc +++ b/gallery/qml.qrc @@ -1,91 +1,98 @@ qml/Main.qml - qml/pages/IconsPage.qml qml/IconBox.qml + + qml/pages/IconsPage.qml qml/pages/StylePage.qml qml/pages/MiscPage.qml qml/pages/InitialGalleryPage.qml qml/pages/ButtonsPage.qml + qml/pages/NotificationPage.qml + qml/pages/DrawerPage.qml + qml/pages/ChecksPage.qml + qml/pages/MapPage.qml + qml/pages/ToolbarPage.qml + qml/pages/ProjectItemsPage.qml + qml/pages/PhotosPage.qml + qml/pages/EditorsPage.qml + qml/pages/OnboardingPage.qml + qml/pages/CalendarPage.qml + qml/pages/FormPage.qml + ../app/qml/components/MMButton.qml ../app/qml/components/MMLinkButton.qml - ../app/qml/components/MMLink.qml ../app/qml/components/MMIcon.qml ../app/qml/components/MMRoundButton.qml + ../app/qml/components/MMLink.qml ../app/qml/components/MMRoundLinkButton.qml ../app/qml/components/MMRadioButton.qml ../app/qml/components/MMSwitch.qml - qml/pages/NotificationPage.qml ../app/qml/components/MMNotification.qml ../app/qml/components/MMNotificationView.qml ../app/qml/components/MMDrawer.qml - qml/pages/DrawerPage.qml - qml/pages/ChecksPage.qml ../app/qml/components/MMComponent_reachedDataLimit.qml ../app/qml/components/MMProgressBar.qml - qml/pages/MapPage.qml ../app/qml/components/MMMapButton.qml ../app/qml/components/MMShadow.qml ../app/qml/components/MMMapLabel.qml ../app/qml/components/MMToolbarButton.qml ../app/qml/components/MMToolbar.qml - qml/pages/ToolbarPage.qml ../app/qml/components/MMMenuDrawer.qml ../app/qml/components/MMToolbarMenuButton.qml ../app/qml/components/MMToolbarLongButton.qml - qml/pages/ProjectItemsPage.qml ../app/qml/components/MMListDrawer.qml ../app/qml/components/MMListDrawerItem.qml ../app/qml/components/MMProjectItem.qml ../app/qml/components/MMMorePhoto.qml ../app/qml/components/MMPhoto.qml - ../app/qml/components/MMPhotoGallery.qml - qml/pages/PhotosPage.qml - qml/pages/EditorsPage.qml - ../app/qml/inputs/MMAbstractEditor.qml - ../app/qml/inputs/MMCheckBox.qml - ../app/qml/inputs/MMInputEditor.qml - ../app/qml/inputs/MMPasswordEditor.qml - ../app/qml/inputs/MMSliderEditor.qml - qml/pages/OnboardingPage.qml - ../app/qml/onboarding/MMAcceptInvitation.qml - ../app/qml/onboarding/MMCreateWorkspace.qml - ../app/qml/onboarding/MMHowYouFoundUs.qml - ../app/qml/onboarding/MMLogin.qml - ../app/qml/onboarding/MMSignUp.qml - ../app/qml/onboarding/MMWhichIndustry.qml + ../app/qml/components/MMCheckBox.qml ../app/qml/components/MMHeader.qml ../app/qml/components/MMHlineText.qml ../app/qml/components/MMTextBubble.qml ../app/qml/components/MMIconCheckBoxHorizontal.qml ../app/qml/components/MMIconCheckBoxVertical.qml - ../app/qml/inputs/MMCalendarEditor.qml ../app/qml/components/calendar/MMDateTimePicker.qml ../app/qml/components/calendar/MMDayOfWeekRow.qml ../app/qml/components/calendar/MMMonthGrid.qml ../app/qml/components/calendar/MMAmPmSwitch.qml ../app/qml/components/MMCalendarDrawer.qml - ../app/qml/inputs/MMTextAreaEditor.qml - ../app/qml/inputs/MMSwitchEditor.qml ../app/qml/components/MMSelectableToolbar.qml ../app/qml/components/MMSelectableToolbarButton.qml - ../app/qml/inputs/MMNumberEditor.qml - ../app/qml/inputs/MMButtonInputEditor.qml ../app/qml/components/MMWarningBubble.qml ../app/qml/components/MMTumbler.qml ../app/qml/components/calendar/MMTimeTumbler.qml ../app/qml/components/calendar/MMDateTumbler.qml - qml/pages/CalendarPage.qml - ../app/qml/inputs/MMComboBoxEditor.qml ../app/qml/components/MMComboBoxDrawer.qml - ../app/qml/inputs/MMSearchEditor.qml - ../app/qml/map/MMPositionMarker.qml ../app/qml/components/MMMapBlurLabel.qml - ../app/qml/inputs/MMQrCodeEditor.qml ../app/qml/components/MMCodeScanner.qml - ../app/qml/map/MMMapScaleBar.qml - ../app/qml/inputs/MMPhotoEditor.qml - qml/pages/FormPage.qml + + ../app/qml/inputs/MMBaseInput.qml + ../app/qml/inputs/MMPasswordInput.qml + ../app/qml/inputs/MMTextInput.qml + ../app/qml/inputs/MMSearchInput.qml + ../app/qml/form/MMFormTabBar.qml + ../app/qml/form/editors/MMButtonFormEditor.qml + ../app/qml/form/editors/MMCalendarFormEditor.qml + ../app/qml/form/editors/MMDropdownFormEditor.qml + ../app/qml/form/editors/MMGalleryFormEditor.qml + ../app/qml/form/editors/MMNumberFormEditor.qml + ../app/qml/form/editors/MMPhotoFormEditor.qml + ../app/qml/form/editors/MMScannerFormEditor.qml + ../app/qml/form/editors/MMSliderFormEditor.qml + ../app/qml/form/editors/MMSwitchFormEditor.qml + ../app/qml/form/editors/MMTextFormEditor.qml + ../app/qml/form/editors/MMTextMultilineFormEditor.qml + + ../app/qml/onboarding/MMAcceptInvitation.qml + ../app/qml/onboarding/MMCreateWorkspace.qml + ../app/qml/onboarding/MMHowYouFoundUs.qml + ../app/qml/onboarding/MMLogin.qml + ../app/qml/onboarding/MMSignUp.qml + ../app/qml/onboarding/MMWhichIndustry.qml + + ../app/qml/map/MMPositionMarker.qml + ../app/qml/map/MMMapScaleBar.qml diff --git a/gallery/qml/pages/CalendarPage.qml b/gallery/qml/pages/CalendarPage.qml index 48835988f..5af8422ee 100644 --- a/gallery/qml/pages/CalendarPage.qml +++ b/gallery/qml/pages/CalendarPage.qml @@ -11,7 +11,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../../app/qml/inputs" +import "../../app/qml/form/editors" import "../../app/qml/components" ScrollView { @@ -41,7 +41,7 @@ ScrollView { checked: true } - MMCalendarEditor { + MMCalendarFormEditor { title: "Date & Time" placeholderText: "YYYY/MM/DD HH:MM" enabled: checkbox.checked @@ -57,7 +57,7 @@ ScrollView { onSelected: function(newDateTime) { dateTime = newDateTime; text = Qt.formatDateTime(newDateTime, "yyyy/MM/dd hh:mm") } } - MMCalendarEditor { + MMCalendarFormEditor { title: "Date" placeholderText: "YYYY/MM/DD" enabled: checkbox.checked @@ -73,7 +73,7 @@ ScrollView { onSelected: function(newDateTime) { dateTime = newDateTime; text = Qt.formatDateTime(newDateTime, "yyyy/MM/dd") } } - MMCalendarEditor { + MMCalendarFormEditor { title: "Time" placeholderText: "HH:MM:SS" enabled: checkbox.checked diff --git a/gallery/qml/pages/DrawerPage.qml b/gallery/qml/pages/DrawerPage.qml index c0f31ea92..4f72f672c 100644 --- a/gallery/qml/pages/DrawerPage.qml +++ b/gallery/qml/pages/DrawerPage.qml @@ -23,15 +23,15 @@ Page { MMButton { text: "Upload" - onClicked: drawer1.visible = true + onClicked: drawer1.open() } MMButton { text: "Reached Data Limit" - onClicked: drawer2.visible = true + onClicked: drawer2.open() } MMButton { text: "Synchronization Failed" - onClicked: drawer3.visible = true + onClicked: drawer3.open() } } @@ -44,8 +44,8 @@ Page { primaryButton: "Yes, Upload Project" secondaryButton: "No Cancel" - onPrimaryButtonClicked: visible = false - onSecondaryButtonClicked: visible = false + onPrimaryButtonClicked: close() + onSecondaryButtonClicked: close() } MMDrawer { @@ -55,7 +55,6 @@ Page { bigTitle: "You have reached a data limit" primaryButton: "Manage Subscription" specialComponent: component.comp - visible: true MMComponent_reachedDataLimit { id: component @@ -65,8 +64,8 @@ Page { usedData: 0.923 } - onPrimaryButtonClicked: visible = false - onSecondaryButtonClicked: visible = false + onPrimaryButtonClicked: close() + onSecondaryButtonClicked: close() } MMDrawer { @@ -78,7 +77,7 @@ Page { primaryButton: "Ok, I understand" boundedDescription: "Failed to push changes. Ask the project workspace owner to log in to their Mergin Maps dashboard for more information." - onPrimaryButtonClicked: visible = false - onSecondaryButtonClicked: visible = false + onPrimaryButtonClicked: close() + onSecondaryButtonClicked: close() } } diff --git a/gallery/qml/pages/EditorsPage.qml b/gallery/qml/pages/EditorsPage.qml index f02d5c744..3c2745377 100644 --- a/gallery/qml/pages/EditorsPage.qml +++ b/gallery/qml/pages/EditorsPage.qml @@ -12,6 +12,7 @@ import QtQuick.Controls import QtQuick.Controls.Basic import "../../app/qml/inputs" +import "../../app/qml/form/editors" import "../../app/qml/components" ScrollView { @@ -20,7 +21,7 @@ ScrollView { spacing: 20 GroupBox { - title: "Items based on MMAbstractEditor" + title: "Items based on MMBaseInput" background: Rectangle { color: "lightGray" border.color: "gray" @@ -41,13 +42,13 @@ ScrollView { checked: true } - MMSearchEditor { + MMSearchInput { title: "MMSearchEditor" placeholderText: "Text value" onSearchTextChanged: function(text) { console.log("Searched string: " + text) } } - MMComboBoxEditor { + MMDropdownFormEditor { title: "MMComboBoxEditor" placeholderText: "Select one" dropDownTitle: "Select one" @@ -79,7 +80,7 @@ ScrollView { onFeatureClicked: function(selectedFeatures) { text = selectedFeatures } } - MMComboBoxEditor { + MMDropdownFormEditor { title: "MMComboBoxEditor Multi select" placeholderText: "Select multiple" dropDownTitle: "Multi select" @@ -147,7 +148,7 @@ ScrollView { } } - MMSliderEditor { + MMSliderFormEditor { title: "MMSliderEditor" from: -100 to: 100 @@ -160,7 +161,7 @@ ScrollView { checkboxChecked: true } - MMNumberEditor { + MMNumberFormEditor { title: "MMNumberEditor" parentValue: "2.0" from: 1.0 @@ -173,16 +174,16 @@ ScrollView { onEditorValueChanged: function(newValue) { parentValue = newValue } } - MMInputEditor { + MMTextInput { title: "MMInputEditor" - parentValue: "Text" + text: "Text" enabled: checkbox.checked width: parent.width hasCheckbox: true checkboxChecked: false } - MMQrCodeEditor { + MMScannerFormEditor { title: "MMQrCodeEditor" placeholderText: "QR code" warningMsg: text.length > 0 ? "" : "Click to icon and scan the code" @@ -192,7 +193,7 @@ ScrollView { onEditorValueChanged: function(newValue, isNull) { console.log("QR code: " + newValue) } } - MMPhotoEditor { + MMPhotoFormEditor { title: "MMPhotoEditor" width: parent.width photoUrl: "https://images.pexels.com/photos/615348/forest-fog-sunny-nature-615348.jpeg" @@ -201,7 +202,7 @@ ScrollView { onContentClicked: console.log("Open photo") } - MMButtonInputEditor { + MMButtonFormEditor { title: "MMButtonInputEditor" placeholderText: "Write something" text: "Text to copy" @@ -212,7 +213,7 @@ ScrollView { buttonEnabled: text.length > 0 } - MMButtonInputEditor { + MMButtonFormEditor { title: "MMButtonInputEditor" placeholderText: "Píš" buttonText: "Kopíruj" @@ -221,7 +222,7 @@ ScrollView { buttonEnabled: text.length > 0 } - MMInputEditor { + MMTextInput { title: "MMInputEditor" placeholderText: "Placeholder" enabled: checkbox.checked @@ -229,7 +230,7 @@ ScrollView { warningMsg: text.length > 0 ? "" : "Write something" } - MMTextAreaEditor { + MMTextMultilineFormEditor { title: "MMTextAreaEditor" placeholderText: "Place for multi row text" enabled: checkbox.checked @@ -237,7 +238,7 @@ ScrollView { warningMsg: text.length > 0 ? "" : "Write something" } - MMPasswordEditor { + MMPasswordInput { title: "MMPasswordEditor" text: "Password" //regexp: '(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{6,})' @@ -246,7 +247,7 @@ ScrollView { width: parent.width } - MMSwitchEditor { + MMSwitchFormEditor { title: "MMSwitchEditor" checked: true text: checked ? "True" : "False" @@ -256,6 +257,5 @@ ScrollView { } } } - } } diff --git a/gallery/qml/pages/FormPage.qml b/gallery/qml/pages/FormPage.qml index 8c696c6b0..3f1962090 100644 --- a/gallery/qml/pages/FormPage.qml +++ b/gallery/qml/pages/FormPage.qml @@ -171,7 +171,7 @@ Page { Component { id: fieldDelegate - MMInputEditor { + MMTextInput { width: ListView.view.width title: qsTr("Field title") placeholderText: qsTr("placeholder...") diff --git a/gallery/qml/pages/NotificationPage.qml b/gallery/qml/pages/NotificationPage.qml index f726aa3eb..dd63755d9 100644 --- a/gallery/qml/pages/NotificationPage.qml +++ b/gallery/qml/pages/NotificationPage.qml @@ -12,7 +12,7 @@ import QtQuick.Controls import QtQuick.Controls.Basic import "../../app/qml/components" -import "../../app/qml/inputs" +import "../../app/qml/form/editors" import notificationType 1.0 Page { @@ -28,31 +28,31 @@ Page { spacing: 20 anchors.centerIn: parent - MMButtonInputEditor { + MMButtonFormEditor { buttonText: "Send" anchors.horizontalCenter: parent.horizontalCenter placeholderText: "Write an informative message" onButtonClicked: { __notificationModel.add(text, 60, NotificationType.Information, NotificationType.None); text = "" } } - MMButtonInputEditor { + MMButtonFormEditor { buttonText: "Send" anchors.horizontalCenter: parent.horizontalCenter placeholderText: "Write a success message" onButtonClicked: { __notificationModel.add(text, 60, NotificationType.Success, NotificationType.Check); text = "" } } - MMButtonInputEditor { + MMButtonFormEditor { buttonText: "Send" anchors.horizontalCenter: parent.horizontalCenter placeholderText: "Write a warning message" onButtonClicked: { __notificationModel.add(text, 60, NotificationType.Warning, NotificationType.Waiting); text = "" } } - MMButtonInputEditor { + MMButtonFormEditor { buttonText: "Send" anchors.horizontalCenter: parent.horizontalCenter placeholderText: "Write an error message" onButtonClicked: { __notificationModel.add(text, 60, NotificationType.Error, NotificationType.None); text = "" } } - MMButtonInputEditor { + MMButtonFormEditor { buttonText: "Send" anchors.horizontalCenter: parent.horizontalCenter text: "Stojí, stojí mohyla, Na mohyle zlá chvíľa, Na mohyle tŕnie chrastie A v tom tŕní, chrastí rastie, Rastie, kvety rozvíja Jedna žltá ľalia. Tá ľalia smutno vzdychá: „Hlávku moju tŕnie pichá A nožičky oheň páli – Pomôžte mi v mojom žiali!“ " From 3734e89d6fbd93d719db11339b5ba699dc063b09 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Wed, 31 Jan 2024 21:52:26 +0100 Subject: [PATCH 02/28] Create MMTextFormEditor.qml --- app/qml/form/editors/MMTextFormEditor.qml | 78 +++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 app/qml/form/editors/MMTextFormEditor.qml diff --git a/app/qml/form/editors/MMTextFormEditor.qml b/app/qml/form/editors/MMTextFormEditor.qml new file mode 100644 index 000000000..501cac778 --- /dev/null +++ b/app/qml/form/editors/MMTextFormEditor.qml @@ -0,0 +1,78 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick +import QtQuick.Controls + +import "../../inputs" + +/* + * Text Edit for QGIS Attribute Form + * Requires various global properties set to function, see featureform Loader section. + * These properties are injected here via 'fieldXYZ' properties and captured with underscore `_`. + * + * Should be used only within feature form. + * See MMTextInput + */ + +MMTextInput { + id: root + + property var _field: parent.field + property var _fieldValue: parent.fieldValue + property bool _fieldValueIsNull: parent.fieldValueIsNull + + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle + property bool _fieldIsReadOnly: parent.fieldIsReadOnly + + property string _fieldTitle: parent.fieldTitle + property string _fieldErrorMessage: parent.fieldErrorMessage + property string _fieldWarningMessage: parent.fieldWarningMessage + + signal editorValueChanged( var newValue, bool isNull ) + + text: _fieldValue === undefined || _fieldValueIsNull ? '' : _fieldValue + + showClearIcon: false + + enabled: !_fieldIsReadOnly + textFieldComponent.readOnly: _fieldIsReadOnly + textFieldComponent.inputMethodHints: root._field.isNumeric ? Qt.ImhFormattedNumbersOnly : Qt.ImhNone + + title: _fieldShouldShowTitle ? _fieldTitle : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage + + textFieldComponent.maximumLength: { + if ( ( !root._field.isNumeric ) && ( root._field.length > 0 ) ) { + return root._field.length + } + return internal.textMaxCharactersLimit + } + + onTextEdited: function ( text ) { + let val = text + if ( root._field.isNumeric ) + { + val = val.replace( ",", "." ).replace( / /g, '' ) // replace comma with dot and remove spaces + } + + root.editorValueChanged( val, val === "" ) + } + + // Avoid Android's uncommited text + textFieldComponent.onPreeditTextChanged: if ( __androidUtils.isAndroid ) Qt.inputMethod.commit() + + QtObject { + id: internal + + property int textMaxCharactersLimit: 32767 // Qt default + } +} From 80a4f2c3fa39e8ec7d763e0348863c8a218d0c40 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Wed, 31 Jan 2024 21:57:35 +0100 Subject: [PATCH 03/28] Move private properties in MMNumberFormEditor to private QtObject --- app/qml/form/editors/MMNumberFormEditor.qml | 41 ++++++++++++--------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/app/qml/form/editors/MMNumberFormEditor.qml b/app/qml/form/editors/MMNumberFormEditor.qml index 61fa9585d..3c92d4dec 100644 --- a/app/qml/form/editors/MMNumberFormEditor.qml +++ b/app/qml/form/editors/MMNumberFormEditor.qml @@ -36,18 +36,8 @@ MMBaseInput { property string _fieldErrorMessage: parent.fieldErrorMessage property string _fieldWarningMessage: parent.fieldWarningMessage - property real to: _fieldConfig["Max"] - property real from: _fieldConfig["Min"] - property string suffix: _fieldConfig['Suffix'] ? _fieldConfig['Suffix'] : '' - property real precision: _fieldConfig['Precision'] ? _fieldConfig['Precision'] : 0 - property alias placeholderText: numberInput.placeholderText - // don't ever use a step smaller than would be visible in the widget - // i.e. if showing 2 decimals, smallest increment will be 0.01 - // https://github.com/qgis/QGIS/blob/a038a79997fb560e797daf3903d94c7d68e25f42/src/gui/editorwidgets/qgsdoublespinbox.cpp#L83-L87 - property real step: Math.max(_fieldConfig["Step"], Math.pow( 10.0, 0.0 - precision )) - signal editorValueChanged( var newValue, var isNull ) title: _fieldShouldShowTitle ? _fieldTitle : "" @@ -65,14 +55,14 @@ MMBaseInput { source: __style.minusIcon color: enabled ? __style.forestColor : __style.mediumGreenColor - enabled: Number( numberInput.text ) - root.step >= root.from + enabled: Number( numberInput.text ) - internal.step >= internal.from } onLeftActionClicked: { if ( leftIcon.enabled ) { - let decremented = Number( numberInput.text ) - root.step - root.editorValueChanged( decremented.toFixed( root.precision ), false ) + let decremented = Number( numberInput.text ) - internal.step + root.editorValueChanged( decremented.toFixed( internal.precision ), false ) } } @@ -116,9 +106,9 @@ MMBaseInput { Text { id: suffix - text: root.suffix ? ' ' + root.suffix : "" // to make sure there is a space between the number and the suffix + text: internal.suffix ? ' ' + internal.suffix : "" // to make sure there is a space between the number and the suffix - visible: root.suffix !== "" && numberInput.text !== "" + visible: internal.suffix !== "" && numberInput.text !== "" height: parent.height verticalAlignment: Qt.AlignVCenter @@ -136,17 +126,32 @@ MMBaseInput { source: __style.plusIcon color: enabled ? __style.forestColor : __style.mediumGreenColor - enabled: Number( numberInput.text ) + root.step <= root.to + enabled: Number( numberInput.text ) + internal.step <= internal.to } onRightActionClicked: { if ( rightIcon.enabled ) { - let incremented = Number( numberInput.text ) + root.step - root.editorValueChanged( incremented.toFixed( root.precision ), false ) + let incremented = Number( numberInput.text ) + internal.step + root.editorValueChanged( incremented.toFixed( internal.precision ), false ) } } + QtObject { + id: internal + + property real to: _fieldConfig["Max"] + property real from: _fieldConfig["Min"] + property string suffix: _fieldConfig['Suffix'] ? _fieldConfig['Suffix'] : '' + property real precision: _fieldConfig['Precision'] ? _fieldConfig['Precision'] : 0 + + + // don't ever use a step smaller than would be visible in the widget + // i.e. if showing 2 decimals, smallest increment will be 0.01 + // https://github.com/qgis/QGIS/blob/a038a79997fb560e797daf3903d94c7d68e25f42/src/gui/editorwidgets/qgsdoublespinbox.cpp#L83-L87 + property real step: Math.max(_fieldConfig["Step"], Math.pow( 10.0, 0.0 - precision )) + } + // on press and hold behavior can be used from here: // https://github.com/mburakov/qt5/blob/93bfa3874c10f6cb5aa376f24363513ba8264117/qtquickcontrols/src/controls/SpinBox.qml#L306-L309 } From 95566d3f40261cb75e9a562360d90a94e29f59d6 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Wed, 31 Jan 2024 21:58:44 +0100 Subject: [PATCH 04/28] Update inputUtils to support text, number and spinner form editors --- app/inpututils.cpp | 14 +++++++++----- app/inpututils.h | 2 +- app/test/testutilsfunctions.cpp | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index c11b7ee8d..61b04beb8 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -1020,9 +1020,11 @@ const QUrl InputUtils::getThemeIcon( const QString &name ) return QUrl( path ); } -const QUrl InputUtils::getEditorComponentSource( const QString &widgetName, const QVariantMap &config, const QgsField &field ) +const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVariantMap &config, const QgsField &field ) { - QString path( "../editor/input%1.qml" ); + QString widgetName = widgetNameIn.toLower(); + + QString path( "../form/editors/%1.qml" ); if ( widgetName == QStringLiteral( "range" ) ) { @@ -1030,16 +1032,18 @@ const QUrl InputUtils::getEditorComponentSource( const QString &widgetName, cons { if ( config["Style"] == QStringLiteral( "Slider" ) ) { - return QUrl( path.arg( QLatin1String( "rangeslider" ) ) ); + return QUrl( path.arg( QLatin1String( "MMSliderFormEditor" ) ) ); } else if ( config["Style"] == QStringLiteral( "SpinBox" ) ) { - return QUrl( path.arg( QLatin1String( "rangeeditable" ) ) ); + return QUrl( path.arg( QLatin1String( "MMNumberFormEditor" ) ) ); } } - return QUrl( path.arg( QLatin1String( "textedit" ) ) ); + return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); } + return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); // <<------ Mind! + if ( field.name().contains( "qrcode", Qt::CaseInsensitive ) || field.alias().contains( "qrcode", Qt::CaseInsensitive ) ) { return QUrl( path.arg( QStringLiteral( "qrcodereader" ) ) ); diff --git a/app/inpututils.h b/app/inpututils.h index d9bfda744..f36f4de7c 100644 --- a/app/inpututils.h +++ b/app/inpututils.h @@ -346,7 +346,7 @@ class InputUtils: public QObject * \param config map coming from QGIS describing this field * \param field qgsfield instance of this field */ - Q_INVOKABLE static const QUrl getEditorComponentSource( const QString &widgetName, const QVariantMap &config = QVariantMap(), const QgsField &field = QgsField() ); + Q_INVOKABLE static const QUrl getFormEditorType( const QString &widgetNameIn, const QVariantMap &config = QVariantMap(), const QgsField &field = QgsField() ); /** * \copydoc QgsCoordinateFormatter::format() diff --git a/app/test/testutilsfunctions.cpp b/app/test/testutilsfunctions.cpp index f737751f4..06981af71 100644 --- a/app/test/testutilsfunctions.cpp +++ b/app/test/testutilsfunctions.cpp @@ -221,10 +221,10 @@ void TestUtilsFunctions::fileExists() void TestUtilsFunctions::loadQmlComponent() { - QUrl dummy = mUtils->getEditorComponentSource( "dummy" ); + QUrl dummy = mUtils->getFormEditorType( "dummy" ); QCOMPARE( dummy.path(), QString( "../editor/inputtextedit.qml" ) ); - QUrl valuemap = mUtils->getEditorComponentSource( "valuemap" ); + QUrl valuemap = mUtils->getFormEditorType( "valuemap" ); QCOMPARE( valuemap.path(), QString( "../editor/inputvaluemap.qml" ) ); } From 3c68d8e4b8b16bf46fc2ffbea493b772ad9e67dd Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Wed, 31 Jan 2024 22:05:19 +0100 Subject: [PATCH 05/28] do not deal with unused editor for now --- app/qml/form/editors/MMButtonFormEditor.qml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/qml/form/editors/MMButtonFormEditor.qml b/app/qml/form/editors/MMButtonFormEditor.qml index eacc6c34d..8800bc67c 100644 --- a/app/qml/form/editors/MMButtonFormEditor.qml +++ b/app/qml/form/editors/MMButtonFormEditor.qml @@ -13,6 +13,11 @@ import QtQuick.Controls.Basic import "../../components" import "../../inputs" +/* + * This editor is not maintaned as it is not used in the app at the moment. + * We might need it in the future though. + */ + MMBaseInput { id: root From 8e3e0a38a75dea46e7b8b93b8a95ae600e916fa1 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Thu, 1 Feb 2024 19:48:50 +0100 Subject: [PATCH 06/28] MMScannerFormEditor in the app --- app/inpututils.cpp | 8 ++-- app/qml/CMakeLists.txt | 1 + app/qml/form/editors/MMScannerFormEditor.qml | 39 +++++++++++++++----- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index 61b04beb8..e074fb0f0 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -1041,13 +1041,13 @@ const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVa } return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); } + else if ( field.name().contains( "qrcode", Qt::CaseInsensitive ) || field.alias().contains( "qrcode", Qt::CaseInsensitive ) ) + { + return QUrl( path.arg( QStringLiteral( "MMScannerFormEditor" ) ) ); + } return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); // <<------ Mind! - if ( field.name().contains( "qrcode", Qt::CaseInsensitive ) || field.alias().contains( "qrcode", Qt::CaseInsensitive ) ) - { - return QUrl( path.arg( QStringLiteral( "qrcodereader" ) ) ); - } if ( widgetName == QStringLiteral( "textedit" ) ) { diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index 667f6a8a2..f99986d0c 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -35,6 +35,7 @@ set(MM_QML components/ToolbarButton.qml components/MMRoundButton.qml components/MMButton.qml + components/MMCodeScanner.qml components/MMComponent_reachedDataLimit.qml components/MMCheckBox.qml components/MMDrawer.qml diff --git a/app/qml/form/editors/MMScannerFormEditor.qml b/app/qml/form/editors/MMScannerFormEditor.qml index ebcc3d052..07c62ca55 100644 --- a/app/qml/form/editors/MMScannerFormEditor.qml +++ b/app/qml/form/editors/MMScannerFormEditor.qml @@ -16,16 +16,29 @@ import "../../inputs" MMBaseInput { id: root - property var parentValue: parent.value - property bool isReadOnly: parent.readOnly ?? false + property var _fieldValue: parent.fieldValue + property var _fieldConfig: parent.fieldConfig + property bool _fieldValueIsNull: parent.fieldValueIsNull + + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle + property bool _fieldIsReadOnly: parent.fieldIsReadOnly + + property string _fieldTitle: parent.fieldTitle + property string _fieldErrorMessage: parent.fieldErrorMessage + property string _fieldWarningMessage: parent.fieldWarningMessage property alias placeholderText: textField.placeholderText property alias text: textField.text signal editorValueChanged( var newValue, bool isNull ) + title: _fieldShouldShowTitle ? _fieldTitle : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage + hasFocus: textField.activeFocus - enabled: !root.isReadOnly + enabled: !_fieldIsReadOnly content: TextField { id: textField @@ -33,17 +46,19 @@ MMBaseInput { anchors.fill: parent anchors.verticalCenter: parent.verticalCenter - text: root.parentValue !== undefined ? root.parentValue : '' readOnly: !root.enabled + + text: root._fieldValue === undefined || root._fieldValueIsNull ? '' : root._fieldValue + color: root.enabled ? __style.nightColor : __style.mediumGreenColor placeholderTextColor: __style.nightAlphaColor + font: __style.p5 hoverEnabled: true - background: Rectangle { - color: __style.transparentColor - } - onTextChanged: root.editorValueChanged( text, text === "" ) + background: Rectangle { color: __style.transparentColor } + + onTextEdited: root.editorValueChanged( textField.text, textField.text === "" ) } rightAction: MMIcon { @@ -58,8 +73,10 @@ MMBaseInput { onRightActionClicked: { if ( !root.enabled ) return + if (!__inputUtils.acquireCameraPermission()) return + codeScannerLoader.active = true codeScannerLoader.focus = true } @@ -78,11 +95,13 @@ MMBaseInput { MMCodeScanner { focus: true - Component.onCompleted: open() onClosed: codeScannerLoader.active = false + + Component.onCompleted: open() + onScanFinished: function( captured ) { root.editorValueChanged( captured, false ) - codeScannerLoader.active = false + close() } } } From a56de6b90beb0b43c122fd58956065b8b34f4903 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Thu, 1 Feb 2024 20:07:45 +0100 Subject: [PATCH 07/28] MMTextMultilinFormEditor in the app --- app/inpututils.cpp | 9 ++-- app/qml/form/editors/MMScannerFormEditor.qml | 8 ++++ .../editors/MMTextMultilineFormEditor.qml | 41 +++++++++++++++++-- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index e074fb0f0..f731c0bc4 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -1046,18 +1046,17 @@ const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVa return QUrl( path.arg( QStringLiteral( "MMScannerFormEditor" ) ) ); } - return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); // <<------ Mind! - - if ( widgetName == QStringLiteral( "textedit" ) ) { if ( config.value( "IsMultiline" ).toBool() ) { - return QUrl( path.arg( QStringLiteral( "texteditmultiline" ) ) ); + return QUrl( path.arg( QStringLiteral( "MMTextMultilineFormEditor" ) ) ); } - return QUrl( path.arg( QLatin1String( "textedit" ) ) ); + return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); } + return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); // <<------ Mind! + if ( widgetName == QStringLiteral( "valuerelation" ) ) { const QgsMapLayer *referencedLayer = QgsProject::instance()->mapLayer( config.value( "Layer" ).toString() ); diff --git a/app/qml/form/editors/MMScannerFormEditor.qml b/app/qml/form/editors/MMScannerFormEditor.qml index 07c62ca55..c031d14b2 100644 --- a/app/qml/form/editors/MMScannerFormEditor.qml +++ b/app/qml/form/editors/MMScannerFormEditor.qml @@ -13,6 +13,14 @@ import QtQuick.Controls.Basic import "../../components" import "../../inputs" + +/* + * QR/Barcode scanner editor for QGIS Attribute Form + * Requires various global properties set to function, see featureform Loader section. + * These properties are injected here via 'fieldXYZ' properties and captured with underscore `_`. + * + * Should be used only within feature form. + */ MMBaseInput { id: root diff --git a/app/qml/form/editors/MMTextMultilineFormEditor.qml b/app/qml/form/editors/MMTextMultilineFormEditor.qml index ba39396db..a2cab86a0 100644 --- a/app/qml/form/editors/MMTextMultilineFormEditor.qml +++ b/app/qml/form/editors/MMTextMultilineFormEditor.qml @@ -13,20 +13,43 @@ import QtQuick.Controls.Basic import "../../components" import "../../inputs" +/* + * Text multiline editor for QGIS Attribute Form + * Requires various global properties set to function, see featureform Loader section. + * These properties are injected here via 'fieldXYZ' properties and captured with underscore `_`. + * + * Should be used only within feature form. + */ MMBaseInput { id: root - property var parentValue: parent.value ?? "" - property bool parentValueIsNull: parent.valueIsNull ?? false - property bool isReadOnly: parent.readOnly ?? false + property var _fieldValue: parent.fieldValue + property var _fieldConfig: parent.fieldConfig + property bool _fieldValueIsNull: parent.fieldValueIsNull + + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle + property bool _fieldIsReadOnly: parent.fieldIsReadOnly + + property string _fieldTitle: parent.fieldTitle + property string _fieldErrorMessage: parent.fieldErrorMessage + property string _fieldWarningMessage: parent.fieldWarningMessage property alias placeholderText: textArea.placeholderText property alias text: textArea.text + property int minimumRows: 3 signal editorValueChanged( var newValue, var isNull ) + title: _fieldShouldShowTitle ? _fieldTitle : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage + + enabled: !_fieldIsReadOnly + hasFocus: textArea.activeFocus + contentItemHeight: { const minHeight = 34 * __dp + metrics.height * root.minimumRows var realHeight = textArea.y + textArea.contentHeight + 2 * textArea.verticalPadding @@ -42,11 +65,23 @@ MMBaseInput { height: contentHeight + textArea.verticalPadding width: parent.width + text: root._fieldValue === undefined || root._fieldValueIsNull ? '' : root._fieldValue + textFormat: root._fieldConfig['UseHtml'] ? TextEdit.RichText : TextEdit.PlainText + hoverEnabled: true placeholderTextColor: __style.nightAlphaColor color: root.enabled ? __style.nightColor : __style.mediumGreenColor + font: __style.p5 wrapMode: Text.WordWrap + + onLinkActivated: function( link ) { + Qt.openUrlExternally( link ) + } + + onTextChanged: root.editorValueChanged( text, text === "" ) + + onPreeditTextChanged: Qt.inputMethod.commit() // to avoid Android's uncommited text } FontMetrics { From 524e2e4252fb62038bad81a64fa566054712367f Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Thu, 1 Feb 2024 22:38:04 +0100 Subject: [PATCH 08/28] MMValueRelationFormEditor and MMValueMapFormEditor in the app, without drawers --- app/inpututils.cpp | 35 +++--- app/qml/CMakeLists.txt | 5 +- ...omboBoxDrawer.qml => MMDropdownDrawer.qml} | 3 +- app/qml/form/editors/MMValueMapFormEditor.qml | 108 ++++++++++++++++++ .../editors/MMValueRelationFormEditor.qml | 97 ++++++++++++++++ .../MMDropdownInput.qml} | 75 +++++++----- gallery/qml.qrc | 4 +- gallery/qml/Main.qml | 4 - 8 files changed, 270 insertions(+), 61 deletions(-) rename app/qml/components/{MMComboBoxDrawer.qml => MMDropdownDrawer.qml} (99%) create mode 100644 app/qml/form/editors/MMValueMapFormEditor.qml create mode 100644 app/qml/form/editors/MMValueRelationFormEditor.qml rename app/qml/{form/editors/MMDropdownFormEditor.qml => inputs/MMDropdownInput.qml} (63%) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index f731c0bc4..8194f8c8a 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -1045,8 +1045,7 @@ const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVa { return QUrl( path.arg( QStringLiteral( "MMScannerFormEditor" ) ) ); } - - if ( widgetName == QStringLiteral( "textedit" ) ) + else if ( widgetName == QStringLiteral( "textedit" ) ) { if ( config.value( "IsMultiline" ).toBool() ) { @@ -1054,28 +1053,20 @@ const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVa } return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); } - - return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); // <<------ Mind! - - if ( widgetName == QStringLiteral( "valuerelation" ) ) + else if ( widgetName == QStringLiteral( "checkbox" ) ) { - const QgsMapLayer *referencedLayer = QgsProject::instance()->mapLayer( config.value( "Layer" ).toString() ); - const QgsVectorLayer *layer = qobject_cast( referencedLayer ); - - if ( layer ) - { - int featuresCount = layer->dataProvider()->featureCount(); - if ( featuresCount > 4 ) - return QUrl( path.arg( QLatin1String( "valuerelationpage" ) ) ); - } - - if ( config.value( "AllowMulti" ).toBool() ) - { - return QUrl( path.arg( QLatin1String( "valuerelationpage" ) ) ); - } - - return QUrl( path.arg( QLatin1String( "valuerelationcombobox" ) ) ); + return QUrl( path.arg( QLatin1String( "MMSwitchFormEditor" ) ) ); + } + else if ( widgetName == QStringLiteral( "valuerelation" ) ) + { + return QUrl( path.arg( QLatin1String( "MMValueRelationFormEditor" ) ) ); } + else if ( widgetName == QStringLiteral( "valuemap" ) ) + { + return QUrl( path.arg( QLatin1String( "MMValueMapFormEditor" ) ) ); + } + + return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); // <<------ Mind! QStringList supportedWidgets = { QStringLiteral( "richtext" ), QStringLiteral( "textedit" ), diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index f99986d0c..eee02839f 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -39,6 +39,7 @@ set(MM_QML components/MMComponent_reachedDataLimit.qml components/MMCheckBox.qml components/MMDrawer.qml + components/MMDropdownDrawer.qml components/MMHeader.qml components/MMHlineText.qml components/MMIcon.qml @@ -110,6 +111,7 @@ set(MM_QML inputs/MMPasswordInput.qml inputs/MMTextInput.qml inputs/MMSearchInput.qml + inputs/MMDropdownInput.qml # # Forms @@ -124,7 +126,6 @@ set(MM_QML form/PreviewPanel.qml form/editors/MMButtonFormEditor.qml form/editors/MMCalendarFormEditor.qml - form/editors/MMDropdownFormEditor.qml form/editors/MMGalleryFormEditor.qml form/editors/MMNumberFormEditor.qml form/editors/MMPhotoFormEditor.qml @@ -133,6 +134,8 @@ set(MM_QML form/editors/MMSwitchFormEditor.qml form/editors/MMTextMultilineFormEditor.qml form/editors/MMTextFormEditor.qml + form/editors/MMValueMapFormEditor.qml + form/editors/MMValueRelationFormEditor.qml # we are missing the following form editors: # - Spacer # - Text / HTML widget diff --git a/app/qml/components/MMComboBoxDrawer.qml b/app/qml/components/MMDropdownDrawer.qml similarity index 99% rename from app/qml/components/MMComboBoxDrawer.qml rename to app/qml/components/MMDropdownDrawer.qml index 70252952c..1a318b0ae 100644 --- a/app/qml/components/MMComboBoxDrawer.qml +++ b/app/qml/components/MMDropdownDrawer.qml @@ -10,10 +10,9 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../inputs" +import "../inputs" -// TODO: rename to MMDropdownDrawer Drawer { id: root diff --git a/app/qml/form/editors/MMValueMapFormEditor.qml b/app/qml/form/editors/MMValueMapFormEditor.qml new file mode 100644 index 000000000..6802dcfea --- /dev/null +++ b/app/qml/form/editors/MMValueMapFormEditor.qml @@ -0,0 +1,108 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick + +import "../../inputs" + +/* + * Dropdown (value map) editor for QGIS Attribute Form + * Requires various global properties set to function, see featureform Loader section. + * These properties are injected here via 'fieldXYZ' properties and captured with underscore `_`. + * + * Should be used only within feature form. + * See MMDropdownInput for more info. + */ +MMDropdownInput { + id: root + + property var _fieldValue: parent.fieldValue + property var _fieldConfig: parent.fieldConfig + property bool _fieldValueIsNull: parent.fieldValueIsNull + + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle + property bool _fieldIsReadOnly: parent.fieldIsReadOnly + + property string _fieldTitle: parent.fieldTitle + property string _fieldErrorMessage: parent.fieldErrorMessage + property string _fieldWarningMessage: parent.fieldWarningMessage + + signal editorValueChanged( var newValue, bool isNull ) + + title: _fieldShouldShowTitle ? _fieldTitle : "" + + dropDownTitle: _fieldTitle + + errorMsg: _fieldErrorMessage + warningMsg: _fieldWarningMessage + + enabled: !_fieldIsReadOnly + + on_FieldValueChanged: { + + if ( _fieldValueIsNull ) { + text = "" + } + + // let's find the new value in the model + for ( let i = 0; i < internal.modelData.count; i++ ) { + let item_i = internal.modelData.get( i ) + + if ( _fieldValue.toString() === item_i.data.toString() ) { + text = item_i.display + } + } + } + + QtObject { + id: internal + + property ListModel modelData: ListModel { id: listModel } + + Component.onCompleted: { + + // + // Parses value map options from config into ListModel. + // This functionality should be moved to FeaturesListModel(?) in order to support search. + // + + if ( !root._fieldConfig['map'] ) { + __inputUtils.log( "Value map", root._fieldTitle + " config is not configured properly" ) + } + + let config = root._fieldConfig['map'] + + if ( config.length ) + { + //it's a list (>=QGIS3.0) + for ( var i = 0; i < config.length; i++ ) + { + let modelItem = { + display: Object.keys( config[i] )[0], + data: Object.values( config[i] )[0] + } + + listModel.append( modelItem ) + + // Is this the current item? If so, set the text + if ( !root._fieldValueIsNull ) { + if ( root._fieldValue.toString() === modelItem.data.toString() ) { + root.text = modelItem.display + } + } + } + } + else + { + //it's a map (<=QGIS2.18) <--- sorry, dropped support for that in 2024.1.0 + __inputUtils.log( "Value map", root._fieldTitle + " is using unsupported format (list, <=QGIS2.18)" ) + } + } + } +} diff --git a/app/qml/form/editors/MMValueRelationFormEditor.qml b/app/qml/form/editors/MMValueRelationFormEditor.qml new file mode 100644 index 000000000..ef55a7723 --- /dev/null +++ b/app/qml/form/editors/MMValueRelationFormEditor.qml @@ -0,0 +1,97 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick + +import "../../inputs" +import lc 1.0 + +/* + * Dropdown (value relation) editor for QGIS Attribute Form + * Requires various global properties set to function, see featureform Loader section. + * These properties are injected here via 'fieldXYZ' properties and captured with underscore `_`. + * + * Should be used only within feature form. + * See MMDropdownInput for more info. + */ +MMDropdownInput { + id: root + + property var _fieldValue: parent.fieldValue + property var _fieldConfig: parent.fieldConfig + property bool _fieldValueIsNull: parent.fieldValueIsNull + property var _fieldFeatureLayerPair: parent.fieldFeatureLayerPair + + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle + property bool _fieldIsReadOnly: parent.fieldIsReadOnly + + property string _fieldTitle: parent.fieldTitle + property string _fieldErrorMessage: parent.fieldErrorMessage + property string _fieldWarningMessage: parent.fieldWarningMessage + + signal editorValueChanged( var newValue, bool isNull ) + + title: _fieldShouldShowTitle ? _fieldTitle : "" + + dropDownTitle: _fieldTitle + + errorMsg: _fieldErrorMessage + warningMsg: _fieldWarningMessage + + enabled: !_fieldIsReadOnly + + on_FieldValueChanged: { + vrModel.pair = root._fieldFeatureLayerPair + } + + ValueRelationFeaturesModel { + id: vrModel + + config: root._fieldConfig + pair: root._fieldFeatureLayerPair + + onInvalidate: { + if ( root._fieldValueIsNull ) + { + return // ignore invalidate signal if value is already NULL + } + if ( root._fieldIsReadOnly ) + { + return // ignore invalidate signal if form is not in edit mode + } + root.editorValueChanged( "", true ) + } + + onFetchingResultsChanged: function ( isFetching ) { + if ( !isFetching ) + { + setText() + } + } + } + + function reload() + { + if ( !root.isReadOnly ) + { + vrModel.pair = root._fieldFeatureLayerPair + } + } + + function setText() + { + root.text = vrModel.convertFromQgisType( root._fieldValue, FeaturesModel.FeatureTitle ).join( ', ' ) + } + + QtObject { + id: internal + + property bool allowMultivalue: root._fieldConfig["AllowMulti"] + } +} diff --git a/app/qml/form/editors/MMDropdownFormEditor.qml b/app/qml/inputs/MMDropdownInput.qml similarity index 63% rename from app/qml/form/editors/MMDropdownFormEditor.qml rename to app/qml/inputs/MMDropdownInput.qml index 11e817fcd..0e03c1f5e 100644 --- a/app/qml/form/editors/MMDropdownFormEditor.qml +++ b/app/qml/inputs/MMDropdownInput.qml @@ -10,17 +10,17 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../../components" -import "../../inputs" +import "../components" MMBaseInput { id: root property alias placeholderText: textField.placeholderText property alias text: textField.text + property bool multiSelect: false - required property ListModel featuresModel - required property string dropDownTitle + property var featuresModel + property string dropDownTitle property var preselectedFeatures: [] hasFocus: textField.activeFocus @@ -33,13 +33,26 @@ MMBaseInput { anchors.fill: parent anchors.verticalCenter: parent.verticalCenter + readOnly: true + color: root.enabled ? __style.nightColor : __style.mediumGreenColor placeholderTextColor: __style.nightAlphaColor + font: __style.p5 hoverEnabled: true + background: Rectangle { color: __style.transparentColor } + + MouseArea { + anchors.fill: parent + onClicked: function( mouse ) { + mouse.accepted = true + console.log( "Open draweeer!" ) + } + } + } rightAction: MMIcon { @@ -56,33 +69,35 @@ MMBaseInput { onRightActionClicked: { if ( !root.enabled ) return - listLoader.active = true - listLoader.focus = true - } - - Loader { - id: listLoader - asynchronous: true - active: false - sourceComponent: listComponent + console.log( "Open draweeer!" ) +// listLoader.active = true +// listLoader.focus = true } - Component { - id: listComponent - - MMComboBoxDrawer { - focus: true - model: root.featuresModel - title: root.dropDownTitle - multiSelect: root.multiSelect - preselectedFeatures: root.preselectedFeatures - - Component.onCompleted: open() - onClosed: listLoader.active = false - onFeatureClicked: function(selectedFeatures) { - root.featureClicked( selectedFeatures ) - } - } - } +// Loader { +// id: listLoader + +// asynchronous: true +// active: false +// sourceComponent: listComponent +// } + +// Component { +// id: listComponent + +// MMDropdownDrawer { +// focus: true +// model: root.featuresModel +// title: root.dropDownTitle +// multiSelect: root.multiSelect +// preselectedFeatures: root.preselectedFeatures + +// Component.onCompleted: open() +// onClosed: listLoader.active = false +// onFeatureClicked: function(selectedFeatures) { +// root.featureClicked( selectedFeatures ) +// } +// } +// } } diff --git a/gallery/qml.qrc b/gallery/qml.qrc index 7d96a378c..79440da4c 100644 --- a/gallery/qml.qrc +++ b/gallery/qml.qrc @@ -63,11 +63,12 @@ ../app/qml/components/MMTumbler.qml ../app/qml/components/calendar/MMTimeTumbler.qml ../app/qml/components/calendar/MMDateTumbler.qml - ../app/qml/components/MMComboBoxDrawer.qml + ../app/qml/components/MMDropdownDrawer.qml ../app/qml/components/MMMapBlurLabel.qml ../app/qml/components/MMCodeScanner.qml ../app/qml/inputs/MMBaseInput.qml + ../app/qml/inputs/MMDropdownInput.qml ../app/qml/inputs/MMPasswordInput.qml ../app/qml/inputs/MMTextInput.qml ../app/qml/inputs/MMSearchInput.qml @@ -75,7 +76,6 @@ ../app/qml/form/MMFormTabBar.qml ../app/qml/form/editors/MMButtonFormEditor.qml ../app/qml/form/editors/MMCalendarFormEditor.qml - ../app/qml/form/editors/MMDropdownFormEditor.qml ../app/qml/form/editors/MMGalleryFormEditor.qml ../app/qml/form/editors/MMNumberFormEditor.qml ../app/qml/form/editors/MMPhotoFormEditor.qml diff --git a/gallery/qml/Main.qml b/gallery/qml/Main.qml index 8ef151678..adf096990 100644 --- a/gallery/qml/Main.qml +++ b/gallery/qml/Main.qml @@ -138,10 +138,6 @@ ApplicationWindow { title: "Text areas" source: "TextAreaPage.qml" } - ListElement { - title: "Combo boxes" - source: "ComboBoxPage.qml" - } ListElement { title: "Checks" source: "ChecksPage.qml" From c49faae3b4b873a4cac53c6f05678de4d0f1e513 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Fri, 2 Feb 2024 10:58:19 +0100 Subject: [PATCH 09/28] Complete MMValueMapFormEditor --- app/qml/components/MMDropdownDrawer.qml | 49 +++++++---- app/qml/form/editors/MMValueMapFormEditor.qml | 88 +++++++++++-------- app/qml/inputs/MMDropdownInput.qml | 85 +++++++++++------- 3 files changed, 138 insertions(+), 84 deletions(-) diff --git a/app/qml/components/MMDropdownDrawer.qml b/app/qml/components/MMDropdownDrawer.qml index 1a318b0ae..05e083707 100644 --- a/app/qml/components/MMDropdownDrawer.qml +++ b/app/qml/components/MMDropdownDrawer.qml @@ -18,13 +18,16 @@ Drawer { property alias title: title.text property alias model: listView.model + + property bool withSearchbar: true + property bool multiSelect: false - property int minFeaturesCountToFullScreenMode: 4 - property var preselectedFeatures: [] + property var selectedFeatures: [] // in/out property, contains list of selected feature ids + property int minFeaturesCountToFullScreenMode: 6 padding: 20 * __dp - signal featureClicked( var selectedFeatures ) + signal selectionFinished( var selectedFeatures ) width: ApplicationWindow.window.width height: (mainColumn.height > ApplicationWindow.window.height ? ApplicationWindow.window.height : mainColumn.height) - 20 * __dp @@ -80,7 +83,7 @@ Drawer { MouseArea { anchors.fill: parent - onClicked: root.visible = false + onClicked: root.close() } } } @@ -91,7 +94,7 @@ Drawer { width: parent.width - 2 * root.padding placeholderText: qsTr("Text value") bgColor: __style.lightGreenColor - visible: root.model.count >= root.minFeaturesCountToFullScreenMode + visible: root.withSearchbar onSearchTextChanged: function(text) { root.model.searchExpression = text @@ -114,12 +117,11 @@ Drawer { return 0 } clip: true - currentIndex: -1 delegate: Item { id: delegate - property bool checked: root.multiSelect ? root.preselectedFeatures.includes(model.FeatureId) : listView.currentIndex === model.index + property bool checked: root.selectedFeatures.includes( model.FeatureId ) width: listView.width height: internal.comboBoxItemHeight @@ -160,14 +162,15 @@ Drawer { MouseArea { anchors.fill: parent onClicked: { - listView.currentIndex = model.index - if(root.multiSelect) { + if ( root.multiSelect ) { delegate.checked = !delegate.checked - delegate.forceActiveFocus() + + // add or remove the item from the selected features list + addOrRemoveFeature( model.FeatureId ) } else { - root.featureClicked(model.FeatureId) - close() + root.selectionFinished( [model.FeatureId] ) + root.close() } } } @@ -188,16 +191,30 @@ Drawer { onClicked: { let selectedFeatures = [] - for(let i=0; i=QGIS3.0) + for ( var i = 0; i < config.length; i++ ) { - //it's a list (>=QGIS3.0) - for ( var i = 0; i < config.length; i++ ) - { - let modelItem = { - display: Object.keys( config[i] )[0], - data: Object.values( config[i] )[0] - } + // Intentionally using roles "FeatureXYZ" here so that it mimics + // the FeaturesListModel and can be used in the DropdownDrawer + let modelItem = { + FeatureTitle: Object.keys( config[i] )[0], + FeatureId: Object.values( config[i] )[0] + } - listModel.append( modelItem ) + listModel.append( modelItem ) - // Is this the current item? If so, set the text - if ( !root._fieldValueIsNull ) { - if ( root._fieldValue.toString() === modelItem.data.toString() ) { - root.text = modelItem.display - } + // Is this the current item? If so, set the text + if ( !root._fieldValueIsNull ) { + if ( root._fieldValue.toString() === modelItem.FeatureId.toString() ) { + root.text = modelItem.FeatureTitle + root.preselectedFeatures = [modelItem.FeatureId] } } } - else - { - //it's a map (<=QGIS2.18) <--- sorry, dropped support for that in 2024.1.0 - __inputUtils.log( "Value map", root._fieldTitle + " is using unsupported format (list, <=QGIS2.18)" ) - } + } + else + { + //it's a map (<=QGIS2.18) <--- sorry, dropped support for that in 2024.1.0 + __inputUtils.log( "Value map", root._fieldTitle + " is using unsupported format (list, <=QGIS2.18)" ) } } } diff --git a/app/qml/inputs/MMDropdownInput.qml b/app/qml/inputs/MMDropdownInput.qml index 0e03c1f5e..28c7f5741 100644 --- a/app/qml/inputs/MMDropdownInput.qml +++ b/app/qml/inputs/MMDropdownInput.qml @@ -12,20 +12,35 @@ import QtQuick.Controls import QtQuick.Controls.Basic import "../components" +/* + * Common dropdown input to use in the app. + * Disabled state can be achieved by setting `enabled: false`. + * + * See MMDropdownDrawer to see required roles for dataModel. + * See MMBaseInput for more properties. + */ + MMBaseInput { id: root property alias placeholderText: textField.placeholderText property alias text: textField.text + // dataModel is used in the drawer. It must have these roles: + // - FeatureId + // - FeatureTitle + // and it must have `count` function to get the number of items + // one can use qml's ListModel or our FeaturesModel.h + property var dataModel + + property string dropDownTitle: "" property bool multiSelect: false - property var featuresModel - property string dropDownTitle + property bool withSearchbar: false property var preselectedFeatures: [] hasFocus: textField.activeFocus - signal featureClicked( var selectedFeatures ) + signal selectionFinished( var selectedFeatures ) content: TextField { id: textField @@ -49,7 +64,7 @@ MMBaseInput { anchors.fill: parent onClicked: function( mouse ) { mouse.accepted = true - console.log( "Open draweeer!" ) + openDrawer() } } @@ -70,34 +85,40 @@ MMBaseInput { if ( !root.enabled ) return - console.log( "Open draweeer!" ) -// listLoader.active = true -// listLoader.focus = true + openDrawer() + } + + Loader { + id: drawerLoader + + asynchronous: true + active: false + sourceComponent: listComponent + } + + Component { + id: listComponent + + MMDropdownDrawer { + focus: true + model: root.dataModel + title: root.dropDownTitle + multiSelect: root.multiSelect + withSearchbar: root.withSearchbar + selectedFeatures: root.preselectedFeatures + + onClosed: drawerLoader.active = false + + onSelectionFinished: function ( selectedFeatures ) { + root.selectionFinished( selectedFeatures ) + } + + Component.onCompleted: open() + } } -// Loader { -// id: listLoader - -// asynchronous: true -// active: false -// sourceComponent: listComponent -// } - -// Component { -// id: listComponent - -// MMDropdownDrawer { -// focus: true -// model: root.featuresModel -// title: root.dropDownTitle -// multiSelect: root.multiSelect -// preselectedFeatures: root.preselectedFeatures - -// Component.onCompleted: open() -// onClosed: listLoader.active = false -// onFeatureClicked: function(selectedFeatures) { -// root.featureClicked( selectedFeatures ) -// } -// } -// } + function openDrawer() { + drawerLoader.active = true + drawerLoader.focus = true + } } From 52dfb22417291e14515b1be89ab6b34fd9943f4c Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Fri, 2 Feb 2024 16:21:33 +0100 Subject: [PATCH 10/28] MMCalendarFormEditor in the app + fixed its ANR --- app/qml/CMakeLists.txt | 10 +++ app/qml/components/MMTumbler.qml | 12 ---- .../components/calendar/MMDateTimePicker.qml | 22 +++--- app/qml/components/calendar/MMDateTumbler.qml | 15 ++++ app/qml/components/calendar/MMTimeTumbler.qml | 15 ++++ app/qml/form/editors/MMCalendarFormEditor.qml | 72 ++++++++++++------- 6 files changed, 100 insertions(+), 46 deletions(-) diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index eee02839f..4cef6bdbd 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -69,6 +69,16 @@ set(MM_QML components/MMToolbarLongButton.qml components/MMToolbarMenuButton.qml components/MMWarningBubble.qml + + components/MMCalendarDrawer.qml + components/MMTumbler.qml + components/calendar/MMDateTimePicker.qml + components/calendar/MMTimeTumbler.qml + components/calendar/MMMonthGrid.qml + components/calendar/MMDayOfWeekRow.qml + components/calendar/MMDateTumbler.qml + components/calendar/MMAmPmSwitch.qml + onboarding/MMAcceptInvitation.qml onboarding/MMCreateWorkspace.qml onboarding/MMHowYouFoundUs.qml diff --git a/app/qml/components/MMTumbler.qml b/app/qml/components/MMTumbler.qml index 63d0b865a..59ba71e17 100644 --- a/app/qml/components/MMTumbler.qml +++ b/app/qml/components/MMTumbler.qml @@ -22,17 +22,5 @@ Tumbler { verticalAlignment: Text.AlignVCenter opacity: 1.0 - Math.abs(Tumbler.displacement) / (control.visibleItemCount / 2) color: Math.abs(Tumbler.displacement) < 0.4 ? __style.forestColor : __style.nightColor - - required property var modelData - required property int index - } - - Rectangle { - anchors.horizontalCenter: control.horizontalCenter - y: (control.height - height) / 2 - width: text.width + 2 * radius - height: control.height * 0.26 - color: __style.lightGreenColor - radius: 8 * __dp } } diff --git a/app/qml/components/calendar/MMDateTimePicker.qml b/app/qml/components/calendar/MMDateTimePicker.qml index cd6f74cee..2ab5fb8dc 100644 --- a/app/qml/components/calendar/MMDateTimePicker.qml +++ b/app/qml/components/calendar/MMDateTimePicker.qml @@ -82,7 +82,6 @@ Item { MouseArea { anchors.fill: monthYearRow onClicked: { - tumblerBgArea.visible = true dateYearTumbler.visible = true } } @@ -148,9 +147,16 @@ Item { bottomPadding: 70 * __dp width: parent.width height: { - if(Window.height > 680) + + // ApplicationWindow.window might not be attached when calculating this for the first time, + // it will get recalculated automatically once the window is attached so we just need to + // make sure this won't fail the first time. + + if ( ApplicationWindow.window?.height ?? 0 > 680 ) { return 330 * __dp - return 330 - (680 - Window.height) + } + + return 330 - ( 680 - ApplicationWindow.window?.height ?? 0 ) } locale: root.locale @@ -234,7 +240,6 @@ Item { MouseArea { anchors.fill: parent onClicked: { - tumblerBgArea.visible = true timeTumbler.visible = true } } @@ -259,14 +264,13 @@ Item { } } - // visible area when a tumbler is visible. Waits for a click to cloase the tumbler + // Area to close tumbler when clicked away from it - does not propagate the click further to the calendar MouseArea { - id: tumblerBgArea - anchors.fill: mainColumn - visible: false + + enabled: dateYearTumbler.visible || timeTumbler.visible + onClicked: { - visible = false dateYearTumbler.visible = false timeTumbler.visible = false } diff --git a/app/qml/components/calendar/MMDateTumbler.qml b/app/qml/components/calendar/MMDateTumbler.qml index fc1ac30b4..0d81bd141 100644 --- a/app/qml/components/calendar/MMDateTumbler.qml +++ b/app/qml/components/calendar/MMDateTumbler.qml @@ -43,6 +43,21 @@ Item { } } + Rectangle { + anchors { + left: parent.left + leftMargin: 12 * __dp + right: parent.right + rightMargin: 12 * __dp + verticalCenter: parent.verticalCenter + } + + height: 54 * __dp + radius: 8 * __dp + + color: __style.lightGreenColor + } + Row { id: row diff --git a/app/qml/components/calendar/MMTimeTumbler.qml b/app/qml/components/calendar/MMTimeTumbler.qml index 4f6738b55..ff160fbc6 100644 --- a/app/qml/components/calendar/MMTimeTumbler.qml +++ b/app/qml/components/calendar/MMTimeTumbler.qml @@ -40,6 +40,21 @@ Item { } } + Rectangle { + anchors { + left: parent.left + leftMargin: 12 * __dp + right: parent.right + rightMargin: 12 * __dp + verticalCenter: parent.verticalCenter + } + + height: 54 * __dp + radius: 8 * __dp + + color: __style.lightGreenColor + } + Row { id: row diff --git a/app/qml/form/editors/MMCalendarFormEditor.qml b/app/qml/form/editors/MMCalendarFormEditor.qml index 9b8bacc2a..b6c8a57fe 100644 --- a/app/qml/form/editors/MMCalendarFormEditor.qml +++ b/app/qml/form/editors/MMCalendarFormEditor.qml @@ -16,26 +16,36 @@ import "../../inputs" MMBaseInput { id: root - property var parentField: parent.field ?? "" - property var parentValue: parent.value ?? "" - property bool parentValueIsNull: parent.valueIsNull ?? true - property bool isReadOnly: parent.readOnly ?? false - - property var config - property bool fieldIsDate: __inputUtils.fieldType( field ) === 'QDate' - property var typeFromFieldFormat: __inputUtils.dateTimeFieldFormat( config['field_format'] ) + property var _field: parent.field + property var _fieldValue: parent.fieldValue + property var _fieldConfig: parent.fieldConfig + property bool _fieldValueIsNull: parent.fieldValueIsNull + + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle + property bool _fieldIsReadOnly: parent.fieldIsReadOnly + + property string _fieldTitle: parent.fieldTitle + property string _fieldErrorMessage: parent.fieldErrorMessage + property string _fieldWarningMessage: parent.fieldWarningMessage + + signal editorValueChanged( var newValue, var isNull ) + + property bool fieldIsDate: __inputUtils.fieldType( _field ) === 'QDate' + property var typeFromFieldFormat: __inputUtils.dateTimeFieldFormat( _fieldConfig['field_format'] ) property bool includesTime: typeFromFieldFormat.includes("Time") property bool includesDate: typeFromFieldFormat.includes("Date") - property bool showSeconds: false + property bool showSeconds: true property date dateTime property alias placeholderText: textField.placeholderText property alias text: textField.text - signal editorValueChanged( var newValue, var isNull ) - signal selected(date newDateTime) + title: _fieldShouldShowTitle ? _fieldTitle : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage - enabled: !isReadOnly + enabled: !_fieldIsReadOnly hasFocus: textField.activeFocus content: TextField { @@ -43,12 +53,19 @@ MMBaseInput { anchors.fill: parent - text: root.parentValue + text: formatText( root._fieldValue ) color: root.enabled ? __style.nightColor : __style.mediumGreenColor placeholderTextColor: __style.nightAlphaColor font: __style.p5 hoverEnabled: true + // Not decided yet if we want to keep this editable or not... test and see! + inputMethodHints: Qt.ImhDate | Qt.ImhTime + + onTextEdited: { + root.editorValueChanged( textField.text, textField.text === "" ) + } + background: Rectangle { color: __style.transparentColor } @@ -64,11 +81,11 @@ MMBaseInput { } onRightActionClicked: { - if (root.parentValueIsNull) { + if (root._fieldValueIsNull) { root.openPicker( new Date() ) } else { - root.openPicker( dateTransformer.toJsDate(root.parentValue) ) + root.openPicker( dateTransformer.toJsDate(root._fieldValue) ) } } @@ -86,14 +103,18 @@ MMBaseInput { MMCalendarDrawer { id: dateTimeDrawer - title: root.fieldIsDate ? qsTr("Date") : qsTr("Date & Time") - dateTime: root.dateTime + title: root._fieldTitle + dateTime: root._fieldValueIsNull ? new Date() : dateTransformer.toJsDate( root._fieldValue ) hasDatePicker: root.includesDate hasTimePicker: root.includesTime showSeconds: root.showSeconds - onPrimaryButtonClicked: root.selected(dateTimeDrawer.dateTime) + onPrimaryButtonClicked: { + root.newDateSelected( dateTime ) + } + onClosed: dateTimeDrawerLoader.active = false + Component.onCompleted: open() } } @@ -103,7 +124,7 @@ MMBaseInput { // When changing this function, test with various timezones! // On desktop, use environment variable TZ, e.g. TZ=America/Mexico_City (UTC-5) function toJsDate(qtDate) { - if ( root.parentField.isDateOrTime ) { + if ( root._field.isDateOrTime ) { if (root.fieldIsDate) { if (qtDate.getUTCHours() === 0) { @@ -133,32 +154,33 @@ MMBaseInput { else { // This is the case when the date coming from C++ is pure string, so we // need to convert it to JS Date ourselves - return Date.fromLocaleString(Qt.locale(), qtDate, config['field_format']) + return Date.fromLocaleString(Qt.locale(), qtDate, root._fieldConfig['field_format']) } } } function newDateSelected( jsDate ) { + if ( jsDate ) { - if ( root.parentField.isDateOrTime ) { + if ( root._field.isDateOrTime ) { // For QDate, the year, month and day is clipped based on // the local timezone in QgsFeature.convertCompatible - root.editorValueChanged( jsDate, false ) + root.editorValueChanged( jsDate, false ) } else { - let qtDate = jsDate.toLocaleString(Qt.locale(), config['field_format']) + let qtDate = jsDate.toLocaleString(Qt.locale(), root._fieldConfig['field_format']) root.editorValueChanged(qtDate, false) } } } function formatText( qtDate ) { - if ( qtDate === undefined || root.parentValueIsNull ) { + if ( qtDate === undefined || root._fieldValueIsNull ) { return '' } else { let jsDate = dateTransformer.toJsDate(qtDate) - return Qt.formatDateTime(jsDate, config['display_format']) + return Qt.formatDateTime(jsDate, root._fieldConfig['display_format']) } } From fe5b0c4249441d996c1fa42d4524b6031efee6a8 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Fri, 2 Feb 2024 20:15:04 +0100 Subject: [PATCH 11/28] Complete MMValueRelationFormEditor --- app/featuresmodel.cpp | 6 ++ app/featuresmodel.h | 8 +++ app/qml/components/MMDropdownDrawer.qml | 23 ++++---- .../editors/MMValueRelationFormEditor.qml | 58 ++++++++++++++++++- app/qml/inputs/MMDropdownInput.qml | 2 + 5 files changed, 83 insertions(+), 14 deletions(-) diff --git a/app/featuresmodel.cpp b/app/featuresmodel.cpp index cd3d766fc..c480e179e 100644 --- a/app/featuresmodel.cpp +++ b/app/featuresmodel.cpp @@ -97,6 +97,7 @@ void FeaturesModel::onFutureFinished() mFeatures << FeatureLayerPair( f, mLayer ); } emit layerFeaturesCountChanged( layerFeaturesCount() ); + emit countChanged( rowCount() ); endResetModel(); mFetchingResults = false; emit fetchingResultsChanged( mFetchingResults ); @@ -301,6 +302,11 @@ QString FeaturesModel::searchExpression() const return mSearchExpression; } +int FeaturesModel::count() const +{ + return rowCount(); +} + void FeaturesModel::setSearchExpression( const QString &searchExpression ) { if ( mSearchExpression != searchExpression ) diff --git a/app/featuresmodel.h b/app/featuresmodel.h index 316f3d75a..34ee30b90 100644 --- a/app/featuresmodel.h +++ b/app/featuresmodel.h @@ -47,6 +47,11 @@ class FeaturesModel : public QAbstractListModel // Returns if there is a pending feature request that will populate the model Q_PROPERTY( bool fetchingResults MEMBER mFetchingResults NOTIFY fetchingResultsChanged ) + // Returns a number of fetched features currently in the model + // It is different from layerFeaturesCount -> it says how many features are in the layer + // Name of the property is intentionally `count` so that it matches ListModel's count property + Q_PROPERTY( int count READ count NOTIFY countChanged ) + public: enum ModelRoles @@ -95,6 +100,8 @@ class FeaturesModel : public QAbstractListModel QgsVectorLayer *layer() const; QString searchExpression() const; + int count() const; + void setSearchExpression( const QString &searchExpression ); void setLayer( QgsVectorLayer *newLayer ); @@ -107,6 +114,7 @@ class FeaturesModel : public QAbstractListModel void layerChanged( QgsVectorLayer *layer ); void layerFeaturesCountChanged( int layerFeaturesCount ); + void countChanged( int featuresCount ); //! \a isFetching is TRUE when still fetching results, FALSE when done fetching bool fetchingResultsChanged( bool isFetching ); diff --git a/app/qml/components/MMDropdownDrawer.qml b/app/qml/components/MMDropdownDrawer.qml index 05e083707..80e1d81ca 100644 --- a/app/qml/components/MMDropdownDrawer.qml +++ b/app/qml/components/MMDropdownDrawer.qml @@ -92,11 +92,17 @@ Drawer { id: searchBar width: parent.width - 2 * root.padding - placeholderText: qsTr("Text value") + placeholderText: qsTr("Search") bgColor: __style.lightGreenColor visible: root.withSearchbar onSearchTextChanged: function(text) { + + // Listview height is derived from the number of items in the model. + // We intenionally break the binding here in order to stop evaluating height - + // the drawer would jump places otherwise + listView.height = listView.height + root.model.searchExpression = text } } @@ -107,12 +113,12 @@ Drawer { bottomMargin: primaryButton.visible ? primaryButton.height + 20 * __dp : 0 width: parent.width - 2 * root.padding height: { - if(root.model.count >= root.minFeaturesCountToFullScreenMode) { - if(ApplicationWindow.window) + if ( root.model.count >= root.minFeaturesCountToFullScreenMode ) { + if ( ApplicationWindow.window ) return ApplicationWindow.window.height - searchBar.height - 100 * __dp else return 0 } - if(root.model) + if ( root.model ) return root.model.count * internal.comboBoxItemHeight return 0 } @@ -190,14 +196,7 @@ Drawer { visible: root.multiSelect onClicked: { - let selectedFeatures = [] - - for ( let i = 0; i < listView.model.count; i++ ) { - if ( listView.itemAtIndex(i).checked ) - selectedFeatures.push( listView.model.get(i).FeatureId ) - } - - root.selectionFinished( selectedFeatures ) + root.selectionFinished( root.selectedFeatures ) close() } } diff --git a/app/qml/form/editors/MMValueRelationFormEditor.qml b/app/qml/form/editors/MMValueRelationFormEditor.qml index ef55a7723..30b5cecf9 100644 --- a/app/qml/form/editors/MMValueRelationFormEditor.qml +++ b/app/qml/form/editors/MMValueRelationFormEditor.qml @@ -9,6 +9,7 @@ import QtQuick +import "../../components" import "../../inputs" import lc 1.0 @@ -39,8 +40,6 @@ MMDropdownInput { title: _fieldShouldShowTitle ? _fieldTitle : "" - dropDownTitle: _fieldTitle - errorMsg: _fieldErrorMessage warningMsg: _fieldWarningMessage @@ -50,6 +49,61 @@ MMDropdownInput { vrModel.pair = root._fieldFeatureLayerPair } + dropdownLoader.sourceComponent: Component { + MMDropdownDrawer { + focus: true + + title: root._fieldTitle + + multiSelect: internal.allowMultivalue + withSearchbar: vrModel.count > 5 + + selectedFeatures: { + if ( internal.allowMultivalue ) { + root.preselectedFeatures = vrModel.convertFromQgisType( root._fieldValue, FeaturesModel.FeatureId ) + } + else { + root.preselectedFeatures = [root._fieldValue] + } + } + + model: ValueRelationFeaturesModel { + id: vrDropdownModel + + config: root._fieldConfig + pair: root._fieldFeatureLayerPair + } + + onClosed: dropdownLoader.active = false + + onSelectionFinished: function ( selectedFeatures ) { + + if ( internal.allowMultivalue ) + { + let isNull = selectedFeatures.length === 0 + + if ( !isNull ) + { + // We need to convert feature id to string prior to sending it to C++ in order to + // avoid conversion to scientific notation. + selectedFeatures = selectedFeatures.map( function(x) { return x.toString() } ) + } + root.editorValueChanged( vrModel.convertToQgisType( selectedFeatures ), isNull ) + } + else + { + // We need to convert feature id to string prior to sending it to C++ in order to + // avoid conversion to scientific notation. + selectedFeatures = selectedFeatures.toString() + + root.editorValueChanged( vrModel.convertToKey( selectedFeatures ), false ) + } + } + + Component.onCompleted: open() + } + } + ValueRelationFeaturesModel { id: vrModel diff --git a/app/qml/inputs/MMDropdownInput.qml b/app/qml/inputs/MMDropdownInput.qml index 28c7f5741..13c033289 100644 --- a/app/qml/inputs/MMDropdownInput.qml +++ b/app/qml/inputs/MMDropdownInput.qml @@ -26,6 +26,8 @@ MMBaseInput { property alias placeholderText: textField.placeholderText property alias text: textField.text + property alias dropdownLoader: drawerLoader + // dataModel is used in the drawer. It must have these roles: // - FeatureId // - FeatureTitle From d0b0dd2989866f9ad5b9a8fbc19409f5171cdced Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Fri, 2 Feb 2024 20:15:50 +0100 Subject: [PATCH 12/28] Use new editors in the form --- app/inpututils.cpp | 4 ++ app/qml/form/FeatureForm.qml | 105 +++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index 8194f8c8a..f3bb814e7 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -1041,6 +1041,10 @@ const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVa } return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); } + else if ( widgetName == QStringLiteral( "datetime" ) ) + { + return QUrl( path.arg( QLatin1String( "MMCalendarFormEditor" ) ) ); + } else if ( field.name().contains( "qrcode", Qt::CaseInsensitive ) || field.alias().contains( "qrcode", Qt::CaseInsensitive ) ) { return QUrl( path.arg( QStringLiteral( "MMScannerFormEditor" ) ) ); diff --git a/app/qml/form/FeatureForm.qml b/app/qml/form/FeatureForm.qml index b0459f38c..e0cd494db 100644 --- a/app/qml/form/FeatureForm.qml +++ b/app/qml/form/FeatureForm.qml @@ -388,6 +388,111 @@ Item { } } + Component { + id: editorComponent + + Item { + + width: ListView.view.width + implicitHeight: childrenRect.height + + // TODO: filter such fields in field proxy model instead +// property bool shouldBeVisible: Type !== FormItem.Invalid && Type !== FormItem.Container + visible: Type !== FormItem.Invalid && Type !== FormItem.Container + + + Loader { + id: formEditorsLoader + + // + // Maybe one day we could use DelegateChooser instead of this hack-ish approach, see: + // https://doc.qt.io/qt-6/qml-qt-labs-qmlmodels-delegatechooser.html + // + + width: parent.width + + property var fieldValue: model.RawValue + property bool fieldValueIsNull: model.RawValueIsNull + + property var field: model.Field + property var fieldWidget: model.EditorWidget + property var fieldConfig: model.EditorWidgetConfig + +// property var homePath: form.project ? form.project.homePath : "" +// property var externalResourceHandler: form.externalResourceHandler + + property bool fieldIsReadOnly: form.state === "readOnly" || !AttributeEditable + property bool fieldShouldShowTitle: model.ShowName + + property string fieldTitle: model.Name + property string fieldErrorMessage: model.ValidationStatus === FieldValidator.Error ? model.ValidationMessage : "" + property string fieldWarningMessage: model.ValidationStatus === FieldValidator.Warning ? model.ValidationMessage : "" + + property var fieldActiveProject: form.project + property var fieldAssociatedRelation: model.Relation + property var fieldFeatureLayerPair: form.controller.featureLayerPair + + active: fieldWidget !== 'Hidden' + + Keys.forwardTo: backHandler + + source: { + if ( model.EditorWidget !== undefined ) { + return __inputUtils.getFormEditorType( model.EditorWidget, model.EditorWidgetConfig, model.Field ) + } + + return '' + } + } + + Connections { + target: formEditorsLoader.item + ignoreUnknownSignals: true + + function onEditorValueChanged( newVal, isNull ) { + model.AttributeValue = isNull ? undefined : newVal + } + } + + Connections { + target: form.controller + + // Important for relation form editors + function onFeatureLayerPairChanged() { + if ( formEditorsLoader.item && formEditorsLoader.item.featureLayerPairChanged ) + { + formEditorsLoader.item.featureLayerPairChanged() + } + } + + // Important for value relation form editors + function onFormRecalculated() { + if ( formEditorsLoader.item && formEditorsLoader.item.reload ) + { + formEditorsLoader.item.reload() + } + } + } + + Connections { + target: form + ignoreUnknownSignals: true + + function onSaved() { + if (formEditorsLoader.item && typeof formEditorsLoader.item.callbackOnSave === "function") { + formEditorsLoader.item.callbackOnSave() + } + } + + function onCanceled() { + if (formEditorsLoader.item && typeof formEditorsLoader.item.callbackOnCancel === "function") { + formEditorsLoader.item.callbackOnCancel() + } + } + } + } + } + /** * A field editor */ From 9ed8dbc90e431fd67cf162c8765eb0102a089533 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Fri, 2 Feb 2024 20:16:18 +0100 Subject: [PATCH 13/28] Small fixes and comments --- app/qml/components/MMHeader.qml | 2 +- app/qml/components/NumberInputField.qml | 1 + app/qml/form/FeatureForm.qml | 2 +- app/qml/form/MMFormTabBar.qml | 2 +- app/qml/form/editors/MMTextFormEditor.qml | 1 + app/qml/form/editors/MMTextMultilineFormEditor.qml | 1 + 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/qml/components/MMHeader.qml b/app/qml/components/MMHeader.qml index 3e9a74f56..cf1ef20bb 100644 --- a/app/qml/components/MMHeader.qml +++ b/app/qml/components/MMHeader.qml @@ -31,7 +31,7 @@ Item { signal backClicked implicitHeight: 60 * __dp - implicitWidth: Window.width + implicitWidth: ApplicationWindow.window.width Text { // If there is a right or a left icon, we need to shift the margin diff --git a/app/qml/components/NumberInputField.qml b/app/qml/components/NumberInputField.qml index 1064f7bd7..23cd025c0 100644 --- a/app/qml/components/NumberInputField.qml +++ b/app/qml/components/NumberInputField.qml @@ -36,6 +36,7 @@ Item { } } + // Could in theory be fixed with `inputMethodComposing` TextInput property instead onPreeditTextChanged: if ( __androidUtils.isAndroid ) Qt.inputMethod.commit() // to avoid Android's uncommited text text: root.number diff --git a/app/qml/form/FeatureForm.qml b/app/qml/form/FeatureForm.qml index e0cd494db..30169c644 100644 --- a/app/qml/form/FeatureForm.qml +++ b/app/qml/form/FeatureForm.qml @@ -354,7 +354,7 @@ Item { model: swipeViewRepeater.model.attributeFormProxyModel(formPage.tabIndex) - delegate: fieldItem + delegate: editorComponent header: Rectangle { opacity: 1 diff --git a/app/qml/form/MMFormTabBar.qml b/app/qml/form/MMFormTabBar.qml index 8936297fc..b8ed9bcb8 100644 --- a/app/qml/form/MMFormTabBar.qml +++ b/app/qml/form/MMFormTabBar.qml @@ -16,7 +16,7 @@ TabBar { property alias tabButtonsModel: tabBarRepeater.model implicitHeight: 56 * __dp - implicitWidth: Window.width + implicitWidth: ApplicationWindow.window.width spacing: 20 * __dp diff --git a/app/qml/form/editors/MMTextFormEditor.qml b/app/qml/form/editors/MMTextFormEditor.qml index 501cac778..b54711925 100644 --- a/app/qml/form/editors/MMTextFormEditor.qml +++ b/app/qml/form/editors/MMTextFormEditor.qml @@ -68,6 +68,7 @@ MMTextInput { } // Avoid Android's uncommited text + // Could in theory be fixed with `inputMethodComposing` TextInput property instead textFieldComponent.onPreeditTextChanged: if ( __androidUtils.isAndroid ) Qt.inputMethod.commit() QtObject { diff --git a/app/qml/form/editors/MMTextMultilineFormEditor.qml b/app/qml/form/editors/MMTextMultilineFormEditor.qml index a2cab86a0..55c11891f 100644 --- a/app/qml/form/editors/MMTextMultilineFormEditor.qml +++ b/app/qml/form/editors/MMTextMultilineFormEditor.qml @@ -81,6 +81,7 @@ MMBaseInput { onTextChanged: root.editorValueChanged( text, text === "" ) + // Could in theory be fixed with `inputMethodComposing` TextInput property instead onPreeditTextChanged: Qt.inputMethod.commit() // to avoid Android's uncommited text } From 2703451ccfef4436cf9b71d7e3eab9e73dd76d9b Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Sat, 3 Feb 2024 22:59:15 +0100 Subject: [PATCH 14/28] MMSwitchFormEditor in the app --- app/qml/form/editors/MMSwitchFormEditor.qml | 56 ++++++++++++++++++--- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/app/qml/form/editors/MMSwitchFormEditor.qml b/app/qml/form/editors/MMSwitchFormEditor.qml index ad53cc9be..09c4429f3 100644 --- a/app/qml/form/editors/MMSwitchFormEditor.qml +++ b/app/qml/form/editors/MMSwitchFormEditor.qml @@ -9,22 +9,39 @@ import QtQuick import QtQuick.Controls -import QtQuick.Controls.Basic import "../../components" import "../../inputs" +/* + * Switch (boolean) editor for QGIS Attribute Form + * Requires various global properties set to function, see featureform Loader section. + * These properties are injected here via 'fieldXYZ' properties and captured with underscore `_`. + * + * Should be used only within feature form. + */ MMBaseInput { id: root - property var parentValue: parent.value ?? false - property bool parentValueIsNull: parent.valueIsNull ?? false - property bool isReadOnly: parent.readOnly ?? false + property var _field: parent.field + property var _fieldValue: parent.fieldValue + property var _fieldConfig: parent.fieldConfig - property alias text: textField.text - property alias checked: rightSwitch.checked + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle + property bool _fieldIsReadOnly: parent.fieldIsReadOnly + + property string _fieldTitle: parent.fieldTitle + property string _fieldErrorMessage: parent.fieldErrorMessage + property string _fieldWarningMessage: parent.fieldWarningMessage signal editorValueChanged( var newValue, var isNull ) + title: _fieldShouldShowTitle ? _fieldTitle : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage + + enabled: !_fieldIsReadOnly + hasFocus: rightSwitch.focus content: Text { @@ -33,11 +50,17 @@ MMBaseInput { width: parent.width + rightSwitch.x anchors.verticalCenter: parent.verticalCenter + text: rightSwitch.checked ? internal.checkedStateValue : internal.uncheckedStateValue + color: root.enabled ? __style.nightColor : __style.mediumGreenColor font: __style.p5 elide: Text.ElideRight } + onContentClicked: { + rightSwitch.toggle() + } + rightAction: MMSwitch { id: rightSwitch @@ -45,8 +68,25 @@ MMBaseInput { height: parent.height x: -30 * __dp - checked: root.checked + checked: root._fieldValue === internal.checkedStateValue + + onCheckedChanged: { + let newVal = rightSwitch.checked ? internal.checkedStateValue : internal.uncheckedStateValue + root.editorValueChanged( newVal, false ) + } + } + + function getConfigValue( configValue, defaultValue ) { + if ( !configValue && root._field.type + "" === internal.booleanEnum ) { + return defaultValue + } else return configValue + } + + QtObject { + id: internal - onCheckedChanged: focus = true + property var checkedStateValue: getConfigValue( root._fieldConfig['CheckedState'], true ) + property var uncheckedStateValue: getConfigValue( root._fieldConfig['UncheckedState'], false ) + property string booleanEnum: "1" // QMetaType::Bool Enum of Qvariant::Type } } From 288fa35986ba18bd5fc67a6a590951982c3c5475 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Sat, 3 Feb 2024 23:57:11 +0100 Subject: [PATCH 15/28] Rename editors to better name convention --- app/qml/CMakeLists.txt | 24 +- ...nFormEditor.qml => MMFormButtonEditor.qml} | 0 ...ormEditor.qml => MMFormCalendarEditor.qml} | 0 ...FormEditor.qml => MMFormGalleryEditor.qml} | 0 ...rFormEditor.qml => MMFormNumberEditor.qml} | 0 app/qml/form/editors/MMFormPhotoViewer.qml | 256 ++++++++++++++++++ ...FormEditor.qml => MMFormScannerEditor.qml} | 0 ...rFormEditor.qml => MMFormSliderEditor.qml} | 0 ...hFormEditor.qml => MMFormSwitchEditor.qml} | 0 ...extFormEditor.qml => MMFormTextEditor.qml} | 0 ...itor.qml => MMFormTextMultilineEditor.qml} | 0 ...ormEditor.qml => MMFormValueMapEditor.qml} | 0 ...itor.qml => MMFormValueRelationEditor.qml} | 0 app/qml/form/editors/MMPhotoFormEditor.qml | 71 ----- gallery/qml.qrc | 20 +- 15 files changed, 278 insertions(+), 93 deletions(-) rename app/qml/form/editors/{MMButtonFormEditor.qml => MMFormButtonEditor.qml} (100%) rename app/qml/form/editors/{MMCalendarFormEditor.qml => MMFormCalendarEditor.qml} (100%) rename app/qml/form/editors/{MMGalleryFormEditor.qml => MMFormGalleryEditor.qml} (100%) rename app/qml/form/editors/{MMNumberFormEditor.qml => MMFormNumberEditor.qml} (100%) create mode 100644 app/qml/form/editors/MMFormPhotoViewer.qml rename app/qml/form/editors/{MMScannerFormEditor.qml => MMFormScannerEditor.qml} (100%) rename app/qml/form/editors/{MMSliderFormEditor.qml => MMFormSliderEditor.qml} (100%) rename app/qml/form/editors/{MMSwitchFormEditor.qml => MMFormSwitchEditor.qml} (100%) rename app/qml/form/editors/{MMTextFormEditor.qml => MMFormTextEditor.qml} (100%) rename app/qml/form/editors/{MMTextMultilineFormEditor.qml => MMFormTextMultilineEditor.qml} (100%) rename app/qml/form/editors/{MMValueMapFormEditor.qml => MMFormValueMapEditor.qml} (100%) rename app/qml/form/editors/{MMValueRelationFormEditor.qml => MMFormValueRelationEditor.qml} (100%) delete mode 100644 app/qml/form/editors/MMPhotoFormEditor.qml diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index 4cef6bdbd..d7ea09472 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -134,18 +134,18 @@ set(MM_QML form/FormWrapper.qml form/MMFormTabBar.qml form/PreviewPanel.qml - form/editors/MMButtonFormEditor.qml - form/editors/MMCalendarFormEditor.qml - form/editors/MMGalleryFormEditor.qml - form/editors/MMNumberFormEditor.qml - form/editors/MMPhotoFormEditor.qml - form/editors/MMScannerFormEditor.qml - form/editors/MMSliderFormEditor.qml - form/editors/MMSwitchFormEditor.qml - form/editors/MMTextMultilineFormEditor.qml - form/editors/MMTextFormEditor.qml - form/editors/MMValueMapFormEditor.qml - form/editors/MMValueRelationFormEditor.qml + form/editors/MMFormButtonEditor.qml + form/editors/MMFormCalendarEditor.qml + form/editors/MMFormGalleryEditor.qml + form/editors/MMFormNumberEditor.qml + form/editors/MMFormPhotoViewer.qml + form/editors/MMFormScannerEditor.qml + form/editors/MMFormSliderEditor.qml + form/editors/MMFormSwitchEditor.qml + form/editors/MMFormTextMultilineEditor.qml + form/editors/MMFormTextEditor.qml + form/editors/MMFormValueMapEditor.qml + form/editors/MMFormValueRelationEditor.qml # we are missing the following form editors: # - Spacer # - Text / HTML widget diff --git a/app/qml/form/editors/MMButtonFormEditor.qml b/app/qml/form/editors/MMFormButtonEditor.qml similarity index 100% rename from app/qml/form/editors/MMButtonFormEditor.qml rename to app/qml/form/editors/MMFormButtonEditor.qml diff --git a/app/qml/form/editors/MMCalendarFormEditor.qml b/app/qml/form/editors/MMFormCalendarEditor.qml similarity index 100% rename from app/qml/form/editors/MMCalendarFormEditor.qml rename to app/qml/form/editors/MMFormCalendarEditor.qml diff --git a/app/qml/form/editors/MMGalleryFormEditor.qml b/app/qml/form/editors/MMFormGalleryEditor.qml similarity index 100% rename from app/qml/form/editors/MMGalleryFormEditor.qml rename to app/qml/form/editors/MMFormGalleryEditor.qml diff --git a/app/qml/form/editors/MMNumberFormEditor.qml b/app/qml/form/editors/MMFormNumberEditor.qml similarity index 100% rename from app/qml/form/editors/MMNumberFormEditor.qml rename to app/qml/form/editors/MMFormNumberEditor.qml diff --git a/app/qml/form/editors/MMFormPhotoViewer.qml b/app/qml/form/editors/MMFormPhotoViewer.qml new file mode 100644 index 000000000..56f8aa6f9 --- /dev/null +++ b/app/qml/form/editors/MMFormPhotoViewer.qml @@ -0,0 +1,256 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import "../../components" +import "../../inputs" + +// TODO: turn into MMPhotoFormViewer and make it as a base class to MMPhotoFormEditor + +MMBaseInput { + id: root + + property var _field: parent.field + property var _fieldValue: parent.fieldValue + property bool _fieldValueIsNull: parent.fieldValueIsNull + + property var _fieldConfig: parent.fieldConfig + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle + property bool _fieldIsReadOnly: parent.fieldIsReadOnly + + property string _fieldTitle: parent.fieldTitle + property string _fieldErrorMessage: parent.fieldErrorMessage + property string _fieldWarningMessage: parent.fieldWarningMessage + + signal editorValueChanged( var newValue, var isNull ) + + title: _fieldShouldShowTitle ? _fieldTitle : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage + + enabled: !_fieldIsReadOnly + + hasFocus: rightSwitch.focus + + +// property var parentValue: parent.value ?? "" +// property bool parentValueIsNull: parent.valueIsNull ?? false +// property bool isReadOnly: parent.readOnly ?? false + +// property url photoUrl + +// signal trashClicked() + + contentItemHeight: 160 * __dp + spacing: 0 + radius: 20 * __dp + + content: MMPhoto { + id: photo + + width: root.width + height: root.contentItemHeight + photoUrl: root.photoUrl + + MouseArea { + anchors.fill: parent + onClicked: root.contentClicked() + } + + Rectangle { + width: 40 * __dp + height: width + radius: width / 2 + color: __style.negativeColor + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: 10 * __dp + anchors.bottomMargin: 10 * __dp + visible: photo.status === Image.Ready + + MMIcon { + anchors.centerIn: parent + source: __style.deleteIcon + useCustomSize: true + width: 30 * __dp + height: width + color: __style.grapeColor + } + + MouseArea { + anchors.centerIn: parent + width: parent.width + 20 * __dp + height: width + onClicked: root.trashClicked() + } + } + } + + QtObject { + id: externalResourceHandler + + // Has to be set for actions with callbacks + property var itemWidget + + // Whether we have camera available on this platform + property var hasCameraCapability:__androidUtils.isAndroid || __iosUtils.isIos + + /** + * Called when clicked on the camera icon to capture an image. + * \param itemWidget editorWidget for modified field to send valueChanged signal. + */ + property var capturePhoto: function capturePhoto(itemWidget) { + externalResourceHandler.itemWidget = itemWidget + if ( !__inputUtils.createDirectory( itemWidget.targetDir ) ) + { + __inputUtils.log("Capture photo", "Could not create directory " + itemWidget.targetDir); + errorDialog.errorText = qsTr( "Could not create directory %1." ).arg( itemWidget.targetDir ) + errorDialog.open() + } + + if (__androidUtils.isAndroid) { + __androidUtils.callCamera(itemWidget.targetDir) + } else if (__iosUtils.isIos) { + __iosUtils.callCamera(itemWidget.targetDir) + } else { + // This should never happen + console.log("Camera not implemented on this platform.") + } + } + + /** + * Called when clicked on the gallery icon to choose a file from a gallery. + * ItemWidget reference is set here and kept for the whole workflow to avoid ambiguity in case of + * multiple external resource (attachment) fields. All usecases and bundle itself counts with one interaction + * per one time. + * + * The workflow of choosing an image from a gallery starts here and goes as follows: + * Android gallery even is evoked. When a user chooses image, "imageSelected( selectedImagePath )" is emitted. + * Then "imageSelected" caught the signal, handles changes and sends signal "valueChanged". + * \param itemWidget editorWidget for modified field to send valueChanged signal. + */ + property var chooseImage: function chooseImage(itemWidget) { + externalResourceHandler.itemWidget = itemWidget + if (__androidUtils.isAndroid) { + __androidUtils.callImagePicker() + } else if (__iosUtils.isIos) { + __iosUtils.callImagePicker(itemWidget.targetDir) + } else { + desktopGalleryPicker.open() + } + } + + /** + * Called to show an image preview. + * \param imagePath Absolute path to an image. + */ + property var previewImage: function previewImage(imagePath) { + imagePreview.source = "file://" + imagePath + imagePreview.width = window.width + previewImageWrapper.open() + } + + /** + * Called to remove an image from a widget. A confirmation dialog is open first if a file exists. + * ItemWidget reference is set here to delete an image for certain widget. + * \param itemWidget editorWidget for modified field to send valueChanged signal. + * \param imagePath Absolute path to an image. + */ + property var removeImage: function removeImage(itemWidget, imagePath) { + if (__inputUtils.fileExists(imagePath)) { + externalResourceHandler.itemWidget = itemWidget + imageDeleteDialog.imagePath = imagePath + imageDeleteDialog.open() + } else { + itemWidget.editorValueChanged("", false) + } + } + + /** + * Called when a photo is taken and confirmed (clicked on check/ok button). + * Original photo file is renamed with current date time to avoid name conflicts. + * ItemWidget reference is always set here to avoid ambiguity in case of + * multiple external resource (attachment) fields. + * \param itemWidget editorWidget for a modified field to send valueChanged signal. + * \param prefixToRelativePath depends on widget's config, see more inputexternalwidget.qml + * \param value depends on widget's config, see more in inputexternalwidget.qml + */ + property var confirmImage: function confirmImage(itemWidget, prefixToRelativePath, value) { + if (value) { + __inputUtils.rescaleImage(value, __activeProject.qgsProject) + var newCurrentValue = __inputUtils.getRelativePath(value, prefixToRelativePath) + itemWidget.editorValueChanged(newCurrentValue, newCurrentValue === "" || newCurrentValue === null) + } + } + + /** + * Called when an image is either selected from a gallery or captured by native camera. If the image doesn't exist in a folder + * set in widget's config, it is copied to the destination and value is set according a new copy (only when chosen from gallery). + * \param imagePath Absolute path to a selected image + */ + property var imageSelected: function imageSelected(imagePath) { + var filename = __inputUtils.getFileName(imagePath) + //! final absolute location of an image. + var absolutePath = __inputUtils.getAbsolutePath( filename, externalResourceHandler.itemWidget.targetDir ) + + if (!__inputUtils.fileExists(absolutePath)) { // we need to copy it! + var success = __inputUtils.copyFile(imagePath, absolutePath) + if (!success) + { + __inputUtils.log("Select image", "Failed to copy image file to " + absolutePath); + errorDialog.errorText = qsTr( "Failed to copy image file to %1." ).arg( absolutePath ) + errorDialog.open() + } + } + externalResourceHandler.confirmImage(externalResourceHandler.itemWidget, externalResourceHandler.itemWidget.prefixToRelativePath, absolutePath) + } + + /** + * Called when an image is captured by a camera. Method sets proper value according given absolute path of the image + * and prefixPath set in thd project settings. + * \param imagePath Absolute path to a captured image + */ + property var imageCaptured: function imageCaptured(absoluteImagePath) { + if (absoluteImagePath) { + var prefixPath = externalResourceHandler.itemWidget.prefixToRelativePath.endsWith("/") ? + externalResourceHandler.itemWidget.prefixToRelativePath : + externalResourceHandler.itemWidget.prefixToRelativePath + "/" + externalResourceHandler.confirmImage(externalResourceHandler.itemWidget, prefixPath, absoluteImagePath) + } + } + + property var onFormSave: function onFormSave(itemWidget) { + __inputUtils.removeFile(itemWidget.sourceToDelete) + itemWidget.sourceToDelete = "" + } + + property var onFormCanceled: function onFormCanceled(itemWidget) { + itemWidget.sourceToDelete = "" + } + } + + Connections { + target: __androidUtils + // used for both gallery and camera + function onImageSelected( imagePath ) { + externalResourceHandler.imageSelected(imagePath) + } + } + + Connections { + target: __iosUtils + // used for both gallery and camera + function onImageSelected( imagePath ) { + externalResourceHandler.imageCaptured(imagePath) + } + } +} diff --git a/app/qml/form/editors/MMScannerFormEditor.qml b/app/qml/form/editors/MMFormScannerEditor.qml similarity index 100% rename from app/qml/form/editors/MMScannerFormEditor.qml rename to app/qml/form/editors/MMFormScannerEditor.qml diff --git a/app/qml/form/editors/MMSliderFormEditor.qml b/app/qml/form/editors/MMFormSliderEditor.qml similarity index 100% rename from app/qml/form/editors/MMSliderFormEditor.qml rename to app/qml/form/editors/MMFormSliderEditor.qml diff --git a/app/qml/form/editors/MMSwitchFormEditor.qml b/app/qml/form/editors/MMFormSwitchEditor.qml similarity index 100% rename from app/qml/form/editors/MMSwitchFormEditor.qml rename to app/qml/form/editors/MMFormSwitchEditor.qml diff --git a/app/qml/form/editors/MMTextFormEditor.qml b/app/qml/form/editors/MMFormTextEditor.qml similarity index 100% rename from app/qml/form/editors/MMTextFormEditor.qml rename to app/qml/form/editors/MMFormTextEditor.qml diff --git a/app/qml/form/editors/MMTextMultilineFormEditor.qml b/app/qml/form/editors/MMFormTextMultilineEditor.qml similarity index 100% rename from app/qml/form/editors/MMTextMultilineFormEditor.qml rename to app/qml/form/editors/MMFormTextMultilineEditor.qml diff --git a/app/qml/form/editors/MMValueMapFormEditor.qml b/app/qml/form/editors/MMFormValueMapEditor.qml similarity index 100% rename from app/qml/form/editors/MMValueMapFormEditor.qml rename to app/qml/form/editors/MMFormValueMapEditor.qml diff --git a/app/qml/form/editors/MMValueRelationFormEditor.qml b/app/qml/form/editors/MMFormValueRelationEditor.qml similarity index 100% rename from app/qml/form/editors/MMValueRelationFormEditor.qml rename to app/qml/form/editors/MMFormValueRelationEditor.qml diff --git a/app/qml/form/editors/MMPhotoFormEditor.qml b/app/qml/form/editors/MMPhotoFormEditor.qml deleted file mode 100644 index 5fceb033b..000000000 --- a/app/qml/form/editors/MMPhotoFormEditor.qml +++ /dev/null @@ -1,71 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import QtQuick.Controls.Basic -import "../../components" -import "../../inputs" - -MMBaseInput { - id: root - - property var parentValue: parent.value ?? "" - property bool parentValueIsNull: parent.valueIsNull ?? false - property bool isReadOnly: parent.readOnly ?? false - - property url photoUrl - - signal trashClicked() - - contentItemHeight: 160 * __dp - spacing: 0 - radius: 20 * __dp - - content: MMPhoto { - id: photo - - width: root.width - height: root.contentItemHeight - photoUrl: root.photoUrl - - MouseArea { - anchors.fill: parent - onClicked: root.contentClicked() - } - - Rectangle { - width: 40 * __dp - height: width - radius: width / 2 - color: __style.negativeColor - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.rightMargin: 10 * __dp - anchors.bottomMargin: 10 * __dp - visible: photo.status === Image.Ready - - MMIcon { - anchors.centerIn: parent - source: __style.deleteIcon - useCustomSize: true - width: 30 * __dp - height: width - color: __style.grapeColor - } - - MouseArea { - anchors.centerIn: parent - width: parent.width + 20 * __dp - height: width - onClicked: root.trashClicked() - } - } - } -} diff --git a/gallery/qml.qrc b/gallery/qml.qrc index 79440da4c..b6b391126 100644 --- a/gallery/qml.qrc +++ b/gallery/qml.qrc @@ -74,16 +74,16 @@ ../app/qml/inputs/MMSearchInput.qml ../app/qml/form/MMFormTabBar.qml - ../app/qml/form/editors/MMButtonFormEditor.qml - ../app/qml/form/editors/MMCalendarFormEditor.qml - ../app/qml/form/editors/MMGalleryFormEditor.qml - ../app/qml/form/editors/MMNumberFormEditor.qml - ../app/qml/form/editors/MMPhotoFormEditor.qml - ../app/qml/form/editors/MMScannerFormEditor.qml - ../app/qml/form/editors/MMSliderFormEditor.qml - ../app/qml/form/editors/MMSwitchFormEditor.qml - ../app/qml/form/editors/MMTextFormEditor.qml - ../app/qml/form/editors/MMTextMultilineFormEditor.qml + ../app/qml/form/editors/MMFormButtonEditor.qml + ../app/qml/form/editors/MMFormCalendarEditor.qml + ../app/qml/form/editors/MMFormGalleryEditor.qml + ../app/qml/form/editors/MMFormNumberEditor.qml + ../app/qml/form/editors/MMFormPhotoViewer.qml + ../app/qml/form/editors/MMFormScannerEditor.qml + ../app/qml/form/editors/MMFormSliderEditor.qml + ../app/qml/form/editors/MMFormSwitchEditor.qml + ../app/qml/form/editors/MMFormTextEditor.qml + ../app/qml/form/editors/MMFormTextMultilineEditor.qml ../app/qml/onboarding/MMAcceptInvitation.qml ../app/qml/onboarding/MMCreateWorkspace.qml From 8ab6d6c279fa242ba2ad3223c3b79d456495c7cb Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Sat, 3 Feb 2024 23:58:40 +0100 Subject: [PATCH 16/28] Update inputstyle to follow the new naming --- app/inpututils.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index f3bb814e7..27dcf71b9 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -1032,45 +1032,45 @@ const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVa { if ( config["Style"] == QStringLiteral( "Slider" ) ) { - return QUrl( path.arg( QLatin1String( "MMSliderFormEditor" ) ) ); + return QUrl( path.arg( QLatin1String( "MMFormSliderEditor" ) ) ); } else if ( config["Style"] == QStringLiteral( "SpinBox" ) ) { - return QUrl( path.arg( QLatin1String( "MMNumberFormEditor" ) ) ); + return QUrl( path.arg( QLatin1String( "MMFormNumberEditor" ) ) ); } } - return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); + return QUrl( path.arg( QLatin1String( "MMFormTextEditor" ) ) ); } else if ( widgetName == QStringLiteral( "datetime" ) ) { - return QUrl( path.arg( QLatin1String( "MMCalendarFormEditor" ) ) ); + return QUrl( path.arg( QLatin1String( "MMFormCalendarEditor" ) ) ); } else if ( field.name().contains( "qrcode", Qt::CaseInsensitive ) || field.alias().contains( "qrcode", Qt::CaseInsensitive ) ) { - return QUrl( path.arg( QStringLiteral( "MMScannerFormEditor" ) ) ); + return QUrl( path.arg( QStringLiteral( "MMFormScannerEditor" ) ) ); } else if ( widgetName == QStringLiteral( "textedit" ) ) { if ( config.value( "IsMultiline" ).toBool() ) { - return QUrl( path.arg( QStringLiteral( "MMTextMultilineFormEditor" ) ) ); + return QUrl( path.arg( QStringLiteral( "MMFormTextMultilineEditor" ) ) ); } - return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); + return QUrl( path.arg( QLatin1String( "MMFormTextEditor" ) ) ); } else if ( widgetName == QStringLiteral( "checkbox" ) ) { - return QUrl( path.arg( QLatin1String( "MMSwitchFormEditor" ) ) ); + return QUrl( path.arg( QLatin1String( "MMFormSwitchEditor" ) ) ); } else if ( widgetName == QStringLiteral( "valuerelation" ) ) { - return QUrl( path.arg( QLatin1String( "MMValueRelationFormEditor" ) ) ); + return QUrl( path.arg( QLatin1String( "MMFormValueRelationEditor" ) ) ); } else if ( widgetName == QStringLiteral( "valuemap" ) ) { - return QUrl( path.arg( QLatin1String( "MMValueMapFormEditor" ) ) ); + return QUrl( path.arg( QLatin1String( "MMFormValueMapEditor" ) ) ); } - return QUrl( path.arg( QLatin1String( "MMTextFormEditor" ) ) ); // <<------ Mind! + return QUrl( path.arg( QLatin1String( "MMFormTextEditor" ) ) ); // <<------ Mind! QStringList supportedWidgets = { QStringLiteral( "richtext" ), QStringLiteral( "textedit" ), From 399253a3e27b5ee0dbc2920f7be38d868545a3dc Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Sun, 4 Feb 2024 15:41:50 +0100 Subject: [PATCH 17/28] MMFormPhotoEditor in the app + bye bye External Resource Bundle --- app/androidutils.cpp | 12 +- app/androidutils.h | 11 +- app/attributes/attributeformmodel.cpp | 5 +- app/inpututils.cpp | 4 + app/ios/iosutils.cpp | 11 +- app/ios/iosutils.h | 8 +- app/qml/CMakeLists.txt | 1 + app/qml/form/FeatureForm.qml | 55 +--- app/qml/form/FeatureFormPage.qml | 5 - app/qml/form/editors/MMFormPhotoEditor.qml | 342 +++++++++++++++++++++ app/qml/form/editors/MMFormPhotoViewer.qml | 227 +++----------- 11 files changed, 419 insertions(+), 262 deletions(-) create mode 100644 app/qml/form/editors/MMFormPhotoEditor.qml diff --git a/app/androidutils.cpp b/app/androidutils.cpp index 32be6cc6f..d6094f09e 100644 --- a/app/androidutils.cpp +++ b/app/androidutils.cpp @@ -247,7 +247,7 @@ bool AndroidUtils::requestMediaLocationPermission() return true; } -void AndroidUtils::callImagePicker() +void AndroidUtils::callImagePicker( const QString &code ) { #ifdef ANDROID @@ -256,6 +256,8 @@ void AndroidUtils::callImagePicker() return; } + mLastCode = code; + // request media location permission to be able to read EXIF metadata from gallery image // it is not a mandatory permission, so continue even if it is rejected requestMediaLocationPermission(); @@ -273,7 +275,7 @@ void AndroidUtils::callImagePicker() #endif } -void AndroidUtils::callCamera( const QString &targetPath ) +void AndroidUtils::callCamera( const QString &targetPath, const QString &code ) { #ifdef ANDROID if ( !requestCameraPermission() ) @@ -281,6 +283,8 @@ void AndroidUtils::callCamera( const QString &targetPath ) return; } + mLastCode = code; + // request media location permission to be able to read EXIF metadata from captured image // it is not a mandatory permission, so continue even if it is rejected requestMediaLocationPermission(); @@ -363,7 +367,7 @@ void AndroidUtils::handleActivityResult( int receiverRequestCode, int resultCode cursor.callMethod( "moveToFirst", "()Z" ); QJniObject result = cursor.callObjectMethod( "getString", "(I)Ljava/lang/String;", columnIndex ); QString selectedImagePath = "file://" + result.toString(); - emit imageSelected( selectedImagePath ); + emit imageSelected( selectedImagePath, mLastCode ); } else if ( receiverRequestCode == CAMERA_CODE && resultCode == RESULT_OK ) { @@ -373,7 +377,7 @@ void AndroidUtils::handleActivityResult( int receiverRequestCode, int resultCode QString selectedImagePath = "file://" + absolutePath; - emit imageSelected( absolutePath ); + emit imageSelected( absolutePath, mLastCode ); } else { diff --git a/app/androidutils.h b/app/androidutils.h index bcbc9b68a..69e261e45 100644 --- a/app/androidutils.h +++ b/app/androidutils.h @@ -59,9 +59,10 @@ class AndroidUtils: public QObject /** * Starts ACTION_PICK activity which opens a gallery. If an image is selected, * handler of the activity emits imageSelected signal. - * */ - Q_INVOKABLE void callImagePicker(); - Q_INVOKABLE void callCamera( const QString &targetPath ); + * The code parameter will be used in response (signal) + */ + Q_INVOKABLE void callImagePicker( const QString &code = "" ); + Q_INVOKABLE void callCamera( const QString &targetPath, const QString &code = "" ); #ifdef ANDROID const static int MEDIA_CODE = 101; @@ -74,7 +75,7 @@ class AndroidUtils: public QObject #endif signals: - void imageSelected( QString imagePath ); + void imageSelected( QString imagePath, QString code ); void bluetoothEnabled( bool state ); @@ -83,6 +84,8 @@ class AndroidUtils: public QObject private: + QString mLastCode; + #ifdef ANDROID QBluetoothLocalDevice mBluetooth; #endif diff --git a/app/attributes/attributeformmodel.cpp b/app/attributes/attributeformmodel.cpp index 3314c9b06..1c15c5524 100644 --- a/app/attributes/attributeformmodel.cpp +++ b/app/attributes/attributeformmodel.cpp @@ -125,8 +125,9 @@ QHash AttributeFormModel::roleNames() const roles[EditorWidget] = QByteArray( "EditorWidget" ); roles[EditorWidgetConfig] = QByteArray( "EditorWidgetConfig" ); roles[RememberValue] = QByteArray( "RememberValue" ); - roles[AttributeFormModel::Field] = QByteArray( "Field" ); - roles[AttributeFormModel::Group] = QByteArray( "Group" ); + roles[Field] = QByteArray( "Field" ); + roles[FieldIndex] = QByteArray( "FieldIndex" ); + roles[Group] = QByteArray( "Group" ); roles[ValidationMessage] = QByteArray( "ValidationMessage" ); roles[ValidationStatus] = QByteArray( "ValidationStatus" ); roles[Relation] = QByteArray( "Relation" ); diff --git a/app/inpututils.cpp b/app/inpututils.cpp index 27dcf71b9..abee1e5f2 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -1069,6 +1069,10 @@ const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVa { return QUrl( path.arg( QLatin1String( "MMFormValueMapEditor" ) ) ); } + else if ( widgetName == QStringLiteral( "externalresource" ) ) + { + return QUrl( path.arg( QLatin1String( "MMFormPhotoEditor" ) ) ); + } return QUrl( path.arg( QLatin1String( "MMFormTextEditor" ) ) ); // <<------ Mind! diff --git a/app/ios/iosutils.cpp b/app/ios/iosutils.cpp index 32c9facaa..382657bc7 100644 --- a/app/ios/iosutils.cpp +++ b/app/ios/iosutils.cpp @@ -15,7 +15,10 @@ IosUtils::IosUtils( QObject *parent ): QObject( parent ) setIdleTimerDisabled(); #endif mImagePicker = new IOSImagePicker(); - QObject::connect( mImagePicker, &IOSImagePicker::imageCaptured, this, &IosUtils::imageSelected ); + QObject::connect( mImagePicker, &IOSImagePicker::imageCaptured, this, [this]( const QString & absoluteImagePath ) + { + emit imageSelected( absoluteImagePath, mLastCode ); + } ); QObject::connect( mImagePicker, &IOSImagePicker::notify, this, &IosUtils::showToast ); } @@ -28,13 +31,15 @@ bool IosUtils::isIos() const #endif } -void IosUtils::callImagePicker( const QString &targetPath ) +void IosUtils::callImagePicker( const QString &targetPath, const QString &code ) { + mLastCode = code; mImagePicker->showImagePicker( targetPath ); } -void IosUtils::callCamera( const QString &targetPath ) +void IosUtils::callCamera( const QString &targetPath, const QString &code ) { + mLastCode = code; mImagePicker->callCamera( targetPath, mPositionKit, mCompass ); } diff --git a/app/ios/iosutils.h b/app/ios/iosutils.h index 6704af484..d59b7db78 100644 --- a/app/ios/iosutils.h +++ b/app/ios/iosutils.h @@ -36,13 +36,13 @@ class IosUtils: public QObject explicit IosUtils( QObject *parent = nullptr ); bool isIos() const; - Q_INVOKABLE void callImagePicker( const QString &targetPath ); - Q_INVOKABLE void callCamera( const QString &targetPath ); + Q_INVOKABLE void callImagePicker( const QString &targetPath, const QString &code = "" ); + Q_INVOKABLE void callCamera( const QString &targetPath, const QString &code = "" ); IOSImagePicker *imagePicker() const; static QString readExif( const QString &filepath, const QString &tag ); signals: - void imageSelected( const QString &imagePath ); + void imageSelected( const QString &imagePath, const QString &code ); //! Used to show a notification to a user. Can be replaced by slot function similar to AndroidUtils::showToast using native Alert dialog. void showToast( const QString &message ); void positionKitChanged(); @@ -52,6 +52,8 @@ class IosUtils: public QObject IOSImagePicker *mImagePicker = nullptr; PositionKit *mPositionKit = nullptr; Compass *mCompass = nullptr; + + QString mLastCode; /** * Calls the objective-c function to disable idle timer to prevent screen from sleeping. */ diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index d7ea09472..c9aa02730 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -138,6 +138,7 @@ set(MM_QML form/editors/MMFormCalendarEditor.qml form/editors/MMFormGalleryEditor.qml form/editors/MMFormNumberEditor.qml + form/editors/MMFormPhotoEditor.qml form/editors/MMFormPhotoViewer.qml form/editors/MMFormScannerEditor.qml form/editors/MMFormSliderEditor.qml diff --git a/app/qml/form/FeatureForm.qml b/app/qml/form/FeatureForm.qml index 30169c644..09533c620 100644 --- a/app/qml/form/FeatureForm.qml +++ b/app/qml/form/FeatureForm.qml @@ -42,52 +42,6 @@ Item { */ signal createLinkedFeature( var parentController, var relation ) - /** - * A handler for extra events in externalSourceWidget. - */ - property var externalResourceHandler: QtObject { - - /** - * Called when clicked on the camera icon to capture an image. - * \param itemWidget editorWidget for modified field to send valueChanged signal. - */ - property var capturePhoto: function captureImage(itemWidget) { - } - - /** - * Called when clicked on the gallery icon to choose a file in a gallery. - * \param itemWidget editorWidget for modified field to send valueChanged signal. - */ - property var chooseImage: function chooseImage(itemWidget) { - } - - /** - * Called when clicked on the photo image. Suppose to be used to bring a bigger preview. - * \param imagePath Absolute path to the image. - */ - property var previewImage: function previewImage(imagePath) { - } - - /** - * Called when clicked on the trash icon. Suppose to delete the value and optionally also the image. - * \param itemWidget editorWidget for modified field to send valueChanged signal. - * \param imagePath Absolute path to the image. - */ - property var removeImage: function removeImage(itemWidget, imagePath) { - } - - /** - * Called when clicked on the OK icon after taking a photo with the Photo panel. - * \param itemWidget editorWidget for modified field to send valueChanged signal. - * \param prefixToRelativePath Together with the value creates absolute path - * \param value Relative path of taken photo. - */ - property var confirmImage: function confirmImage(itemWidget, prefixToRelativePath, value) { - itemWidget.image.source = prefixToRelativePath + "/" + value - itemWidget.editorValueChanged(value, value === "" || value === null) - } - } - /** * Active project. */ @@ -415,12 +369,10 @@ Item { property bool fieldValueIsNull: model.RawValueIsNull property var field: model.Field + property var fieldIndex: model.FieldIndex property var fieldWidget: model.EditorWidget property var fieldConfig: model.EditorWidgetConfig -// property var homePath: form.project ? form.project.homePath : "" -// property var externalResourceHandler: form.externalResourceHandler - property bool fieldIsReadOnly: form.state === "readOnly" || !AttributeEditable property bool fieldShouldShowTitle: model.ShowName @@ -431,6 +383,7 @@ Item { property var fieldActiveProject: form.project property var fieldAssociatedRelation: model.Relation property var fieldFeatureLayerPair: form.controller.featureLayerPair + property string fieldHomePath: form.project ? form.project.homePath : "" // for photo editor active: fieldWidget !== 'Hidden' @@ -480,13 +433,13 @@ Item { function onSaved() { if (formEditorsLoader.item && typeof formEditorsLoader.item.callbackOnSave === "function") { - formEditorsLoader.item.callbackOnSave() + formEditorsLoader.item.callbackOnFormSaved() } } function onCanceled() { if (formEditorsLoader.item && typeof formEditorsLoader.item.callbackOnCancel === "function") { - formEditorsLoader.item.callbackOnCancel() + formEditorsLoader.item.callbackOnFormCanceled() } } } diff --git a/app/qml/form/FeatureFormPage.qml b/app/qml/form/FeatureFormPage.qml index 663c3811e..e665b2a7c 100644 --- a/app/qml/form/FeatureFormPage.qml +++ b/app/qml/form/FeatureFormPage.qml @@ -151,7 +151,6 @@ Item { /*required*/ featureLayerPair: root.featureLayerPair } - externalResourceHandler: externalResourceBundle.handler state: root.formState onSaved: root.close() @@ -248,10 +247,6 @@ Item { onButtonClicked: close() } - - ExternalResourceBundle { - id: externalResourceBundle - } } } } diff --git a/app/qml/form/editors/MMFormPhotoEditor.qml b/app/qml/form/editors/MMFormPhotoEditor.qml new file mode 100644 index 000000000..a43361509 --- /dev/null +++ b/app/qml/form/editors/MMFormPhotoEditor.qml @@ -0,0 +1,342 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick +import QtQuick.Dialogs + +import notificationType 1.0 + +/* + * Photo form editor (external resource) for QGIS Attribute Form + * Requires various global properties set to function, see featureform Loader section. + * These properties are injected here via 'fieldXYZ' properties and captured with underscore `_`. + * + * Should be used only within feature form. + * Replaced previous ExternalResourceBundle and some code from inputexternalresource. + * See MMFormPhotoViewer. + * + * + * Overview of path handling + * ------------------------- + * + * Project home path: comes from QgsProject::homePath() - by default it points to the folder where + * the .qgs/.qgz project file is stored, but can be changed manually by the user. + * + * Default path: defined in the field's configuration. This is the path where newly captured images will be stored. + * It has to be an absolute path. It can be defined as an expression (e.g. @project_home || '/photos') or + * by plain path (e.g. /home/john/photos). If not defined, project home path is used. + * + * In the field's configuration, there are three ways how path to pictures is stored in field values: + * absolute paths, relative to default path and relative to project path. Below is an example of how + * the final field values of paths are calculated. + * + * variable | value + * -------------------+-------------------------------- + * project home path | /home/john + * default path | /home/john/photos + * image path | /home/john/photos/img0001.jpg + * + * + * storage type | calculation of field value | final field value + * -------------------------+---------------------------------+-------------------------------- + * absolute path | image path | /home/john/photos/img0001.jpg + * relative to default path | image path - default path | img0001.jpg + * relative to project path | image path - project home path | photos/img0001.jpg + */ + +MMFormPhotoViewer { + id: root + + property var _fieldValue: parent.fieldValue + property var _fieldConfig: parent.fieldConfig + property var _fieldIndex: parent.fieldIndex + property bool _fieldValueIsNull: parent.fieldValueIsNull + + property string _fieldHomePath: parent.fieldHomePath + property var _fieldActiveProject: parent.fieldActiveProject + property var _fieldFeatureLayerPair: parent.fieldFeatureLayerPair + + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle + property bool _fieldIsReadOnly: parent.fieldIsReadOnly + + property string _fieldTitle: parent.fieldTitle + property string _fieldErrorMessage: parent.fieldErrorMessage + property string _fieldWarningMessage: parent.fieldWarningMessage + + signal editorValueChanged( var newValue, bool isNull ) + + title: _fieldShouldShowTitle ? _fieldTitle : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage + + enabled: !_fieldIsReadOnly + + photoUrl: internal.absoluteImagePath + hasCameraCapability: __androidUtils.isAndroid || __iosUtils.isIos + + on_FieldValueChanged: internal.calculateAbsoluteImagePath() + + onCapturePhotoClicked: internal.capturePhoto() + onChooseFromGalleryClicked: internal.chooseFromGallery() + onTrashClicked: internal.removeImage( __inputUtils.getAbsolutePath( root._fieldValue, internal.prefixToRelativePath ) ) + + // used only on desktop builds + FileDialog { + id: desktopGalleryPicker + + title: qsTr( "Open Image" ) + + nameFilters: [ qsTr( "Image files (*.gif *.png *.jpg)" ) ] + + currentFolder: __inputUtils.imageGalleryLocation() + onAccepted: { + internal.imageSelected( selectedFile ) + } + } + + MessageDialog { // TODO: We might want to redesign this dialogue + id: imageDeleteDialog + + property string imagePath + + title: qsTr( "Remove photo reference" ) + text: qsTr( "Also permanently delete photo from device?" ) + + buttons: MessageDialog.Yes | MessageDialog.No | MessageDialog.Cancel + + onButtonClicked: function( clickedButton ) { + if ( clickedButton === MessageDialog.Yes ) { + internal.imageSourceToDelete = imageDeleteDialog.imagePath + root.editorValueChanged( "", false ) // Shouldn't this be true? + } + else if ( clickedButton === MessageDialog.No ) { + root.editorValueChanged( "", false ) // Shouldn't this be true? + } + + close() + } + } + + Connections { + target: __androidUtils + + // used for both gallery and camera + function onImageSelected( imagePath, index ) { + console.log( "Returned from Android activity, imagePath", imagePath, "indexes:", root._fieldIndex, index ) + if ( root._fieldIndex.toString() === index.toString() ) { + console.log( "It is the same!" ) + internal.imageSelected( imagePath ) + } + } + } + + Connections { + target: __iosUtils + + // used for both gallery and camera + function onImageSelected( imagePath, index ) { + console.log( "Returned from iOS activity, imagePath", imagePath, "indexes:", root._fieldIndex, index ) + if ( root._fieldIndex.toString() === index.toString() ) { + internal.imageCaptured( imagePath ) + } + } + } + + QtObject { + id: internal + //! This object is a combination of previous "ExternalResourceBundle" and some functions from "inputexternalresource" editor + + /** + * 0 - Relative path disabled + * 1 - Relative path to project + * 2 - Relative path to defaultRoot defined in the config - Default path field in the widget configuration form + */ + property int relativeStorageMode: root._fieldConfig["RelativeStorage"] + + /** + * This evaluates the "default path" with the following order: + * 1. evaluate default path expression if defined, + * 2. use default path value if not empty, + * 3. use project home folder + */ + property string targetDir: __inputUtils.resolveTargetDir( + root._fieldHomePath, + root._fieldConfig, + root._fieldFeatureLayerPair, + root._fieldActiveProject + ) + + property string prefixToRelativePath: __inputUtils.resolvePrefixForRelativePath( + relativeStorageMode, + root._fieldHomePath, + targetDir + ) + + property string absoluteImagePath + + property string imageSourceToDelete // used to postpone image deletion to when the form is saved + + function callbackOnFormSaved() { + __inputUtils.removeFile( imageSourceToDelete ) + imageSourceToDelete = "" + } + + function callbackOnFormCanceled() { + imageSourceToDelete = "" + } + + function calculateAbsoluteImagePath() { + let absolutePath = __inputUtils.getAbsolutePath( root._fieldValue, internal.prefixToRelativePath ) + + if ( root.photoComponent.status === Image.Error ) { // <--- this looks dodgy to calculate it from the image.status + root.state = "notAvailable" + absoluteImagePath = "" + return + } + else if ( root._fieldValue && __inputUtils.fileExists( absolutePath ) ) { + root.state = "valid" + absoluteImagePath = "file://" + absolutePath + return + } + else if ( !root._fieldValue || root._fieldValueIsNullfield ) { + root.state = "notSet" + absoluteImagePath = "" + return + } + + root.state = "notAvailable" + absoluteImagePath = "file://" + absolutePath + } + + /** + * Called when clicked on the camera icon to capture an image. + */ + function capturePhoto() { + if ( !__inputUtils.createDirectory( targetDir ) ) + { + __inputUtils.log( "Capture photo", "Could not create directory " + targetDir ); + __notificationModel.addError( qsTr( "Could not create directory %1." ).arg( targetDir ) ) + } + + if ( __androidUtils.isAndroid ) { + __androidUtils.callCamera( targetDir, root._fieldIndex ) + } + else if ( __iosUtils.isIos ) { + __iosUtils.callCamera( targetDir, root._fieldIndex ) + } + else { + // This should never happen + console.error( "Camera not implemented on this platform." ) + } + } + + /** + * Called when clicked on the gallery icon to choose a file from a gallery. + * Ambiguity issues are handled with Connection's enabled property. We enable it + * only when waiting for image. All usecases and bundle itself counts with one interaction + * per one time. + * + * The workflow of choosing an image from a gallery starts here and goes as follows: + * Android gallery even is evoked. When a user chooses image, "imageSelected( selectedImagePath )" is emitted. + * Then "imageSelected" caught the signal, handles changes and sends signal "valueChanged". + */ + function chooseFromGallery() { + if ( __androidUtils.isAndroid ) { + __androidUtils.callImagePicker( root._fieldIndex ) + } + else if ( __iosUtils.isIos ) { + __iosUtils.callImagePicker( targetDir, root._fieldIndex ) + } + else { + desktopGalleryPicker.open() + } + } + + /** + * Called to remove an image from a widget. A confirmation dialog is open first if a file exists. + * ItemWidget reference is set here to delete an image for certain widget. + * \param itemWidget editorWidget for modified field to send valueChanged signal. + * \param imagePath Absolute path to an image. + */ + function removeImage( path ) { + console.info("removeImage 1") + if ( __inputUtils.fileExists( path ) ) { + console.info("removeImage 2") + imageDeleteDialog.imagePath = path + imageDeleteDialog.open() + console.info("removeImage 3") + } + else { + console.info("removeImage 4") + root.editorValueChanged( "", false ) + } + } + + /** + * Called when an image is either selected from a gallery or captured by native camera. If the image doesn't exist in a folder + * set in widget's config, it is copied to the destination and value is set according a new copy (only when chosen from gallery). + * \param imgPath Absolute path to a selected image + * + * Used for Android and desktop builds + */ + function imageSelected( imgPath ) { + let filename = __inputUtils.getFileName( imgPath ) + + //! final absolute location of an image. + let absolutePath = __inputUtils.getAbsolutePath( filename, targetDir ) + + if ( !__inputUtils.fileExists( absolutePath ) ) { // we need to copy it! + + let success = __inputUtils.copyFile( imgPath, absolutePath ) + + if ( !success ) { + __inputUtils.log( "Select image", "Failed to copy image file to " + absolutePath ) + __notificationModel.addError( qsTr( "Failed to process the image" ) ) + } + } + confirmImage( prefixToRelativePath, absolutePath ) + } + + /** + * Called when an image is captured by a camera. Method sets proper value according given absolute path of the image + * and prefixPath set in thd project settings. + * \param imgPath Absolute path to a captured image + * + * Only used for iOS! + */ + function imageCaptured( imgPath ) { + if ( imgPath ) { + + let prefixPath = prefixToRelativePath.endsWith("/") ? prefixToRelativePath : prefixToRelativePath + "/" + + confirmImage( prefixPath, imgPath ) + } + } + + /** + * Called when a photo is taken and confirmed (clicked on check/ok button). + * Original photo file is renamed with current date time to avoid name conflicts. + * ItemWidget reference is always set here to avoid ambiguity in case of + * multiple external resource (attachment) fields. + * \param prefixToRelativePath depends on widget's config, see more inputexternalwidget.qml + * \param imgPath + */ + function confirmImage( prefixToRelativePath, imgPath ) { + console.log( "Confirming image", imgPath, prefixToRelativePath ) + if ( imgPath ) { + __inputUtils.rescaleImage( imgPath, __activeProject.qgsProject ) + let newImgPath = __inputUtils.getRelativePath( imgPath, prefixToRelativePath ) + + console.log( "Sending changed signale", newImgPath ) + root.editorValueChanged( newImgPath, newImgPath === "" || newImgPath === null ) + } + } + } +} diff --git a/app/qml/form/editors/MMFormPhotoViewer.qml b/app/qml/form/editors/MMFormPhotoViewer.qml index 56f8aa6f9..735c1835d 100644 --- a/app/qml/form/editors/MMFormPhotoViewer.qml +++ b/app/qml/form/editors/MMFormPhotoViewer.qml @@ -9,51 +9,57 @@ import QtQuick import QtQuick.Controls -import QtQuick.Controls.Basic import "../../components" import "../../inputs" -// TODO: turn into MMPhotoFormViewer and make it as a base class to MMPhotoFormEditor +/* + * Photo viewer for feature form. + * Its purpose is to show image based on the provided URL. + * It is not combined with MMPhotoFormEditor as it would be not possible to use it in the gallery then. + * + * Serves as a base class for MMPhotoFormEditor. + */ MMBaseInput { id: root - property var _field: parent.field - property var _fieldValue: parent.fieldValue - property bool _fieldValueIsNull: parent.fieldValueIsNull + // TODO: + // - add photo preview panel (zoom), + // - handle "photo notAvailable" state + // - handle empty state - add "capture photo" and "choose from gallery" signals + // - scale images well - based on the root.size - property var _fieldConfig: parent.fieldConfig - property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle - property bool _fieldIsReadOnly: parent.fieldIsReadOnly + property url photoUrl: "" + property bool hasCameraCapability: true - property string _fieldTitle: parent.fieldTitle - property string _fieldErrorMessage: parent.fieldErrorMessage - property string _fieldWarningMessage: parent.fieldWarningMessage + property alias photoComponent: photo - signal editorValueChanged( var newValue, var isNull ) - - title: _fieldShouldShowTitle ? _fieldTitle : "" - - warningMsg: _fieldWarningMessage - errorMsg: _fieldErrorMessage - - enabled: !_fieldIsReadOnly - - hasFocus: rightSwitch.focus - - -// property var parentValue: parent.value ?? "" -// property bool parentValueIsNull: parent.valueIsNull ?? false -// property bool isReadOnly: parent.readOnly ?? false - -// property url photoUrl - -// signal trashClicked() + signal trashClicked() + signal capturePhotoClicked() + signal chooseFromGalleryClicked() contentItemHeight: 160 * __dp spacing: 0 radius: 20 * __dp + states: [ + State { + name: "valid" + }, + State { + name: "notSet" + }, + State { + name: "notAvailable" + } + ] + + state: "notSet" + + onContentClicked: { + // TODO: open preview + } + content: MMPhoto { id: photo @@ -75,7 +81,7 @@ MMBaseInput { anchors.bottom: parent.bottom anchors.rightMargin: 10 * __dp anchors.bottomMargin: 10 * __dp - visible: photo.status === Image.Ready + visible: enabled && ( photo.status === Image.Ready || photo.status === Image.Error ) MMIcon { anchors.centerIn: parent @@ -94,163 +100,4 @@ MMBaseInput { } } } - - QtObject { - id: externalResourceHandler - - // Has to be set for actions with callbacks - property var itemWidget - - // Whether we have camera available on this platform - property var hasCameraCapability:__androidUtils.isAndroid || __iosUtils.isIos - - /** - * Called when clicked on the camera icon to capture an image. - * \param itemWidget editorWidget for modified field to send valueChanged signal. - */ - property var capturePhoto: function capturePhoto(itemWidget) { - externalResourceHandler.itemWidget = itemWidget - if ( !__inputUtils.createDirectory( itemWidget.targetDir ) ) - { - __inputUtils.log("Capture photo", "Could not create directory " + itemWidget.targetDir); - errorDialog.errorText = qsTr( "Could not create directory %1." ).arg( itemWidget.targetDir ) - errorDialog.open() - } - - if (__androidUtils.isAndroid) { - __androidUtils.callCamera(itemWidget.targetDir) - } else if (__iosUtils.isIos) { - __iosUtils.callCamera(itemWidget.targetDir) - } else { - // This should never happen - console.log("Camera not implemented on this platform.") - } - } - - /** - * Called when clicked on the gallery icon to choose a file from a gallery. - * ItemWidget reference is set here and kept for the whole workflow to avoid ambiguity in case of - * multiple external resource (attachment) fields. All usecases and bundle itself counts with one interaction - * per one time. - * - * The workflow of choosing an image from a gallery starts here and goes as follows: - * Android gallery even is evoked. When a user chooses image, "imageSelected( selectedImagePath )" is emitted. - * Then "imageSelected" caught the signal, handles changes and sends signal "valueChanged". - * \param itemWidget editorWidget for modified field to send valueChanged signal. - */ - property var chooseImage: function chooseImage(itemWidget) { - externalResourceHandler.itemWidget = itemWidget - if (__androidUtils.isAndroid) { - __androidUtils.callImagePicker() - } else if (__iosUtils.isIos) { - __iosUtils.callImagePicker(itemWidget.targetDir) - } else { - desktopGalleryPicker.open() - } - } - - /** - * Called to show an image preview. - * \param imagePath Absolute path to an image. - */ - property var previewImage: function previewImage(imagePath) { - imagePreview.source = "file://" + imagePath - imagePreview.width = window.width - previewImageWrapper.open() - } - - /** - * Called to remove an image from a widget. A confirmation dialog is open first if a file exists. - * ItemWidget reference is set here to delete an image for certain widget. - * \param itemWidget editorWidget for modified field to send valueChanged signal. - * \param imagePath Absolute path to an image. - */ - property var removeImage: function removeImage(itemWidget, imagePath) { - if (__inputUtils.fileExists(imagePath)) { - externalResourceHandler.itemWidget = itemWidget - imageDeleteDialog.imagePath = imagePath - imageDeleteDialog.open() - } else { - itemWidget.editorValueChanged("", false) - } - } - - /** - * Called when a photo is taken and confirmed (clicked on check/ok button). - * Original photo file is renamed with current date time to avoid name conflicts. - * ItemWidget reference is always set here to avoid ambiguity in case of - * multiple external resource (attachment) fields. - * \param itemWidget editorWidget for a modified field to send valueChanged signal. - * \param prefixToRelativePath depends on widget's config, see more inputexternalwidget.qml - * \param value depends on widget's config, see more in inputexternalwidget.qml - */ - property var confirmImage: function confirmImage(itemWidget, prefixToRelativePath, value) { - if (value) { - __inputUtils.rescaleImage(value, __activeProject.qgsProject) - var newCurrentValue = __inputUtils.getRelativePath(value, prefixToRelativePath) - itemWidget.editorValueChanged(newCurrentValue, newCurrentValue === "" || newCurrentValue === null) - } - } - - /** - * Called when an image is either selected from a gallery or captured by native camera. If the image doesn't exist in a folder - * set in widget's config, it is copied to the destination and value is set according a new copy (only when chosen from gallery). - * \param imagePath Absolute path to a selected image - */ - property var imageSelected: function imageSelected(imagePath) { - var filename = __inputUtils.getFileName(imagePath) - //! final absolute location of an image. - var absolutePath = __inputUtils.getAbsolutePath( filename, externalResourceHandler.itemWidget.targetDir ) - - if (!__inputUtils.fileExists(absolutePath)) { // we need to copy it! - var success = __inputUtils.copyFile(imagePath, absolutePath) - if (!success) - { - __inputUtils.log("Select image", "Failed to copy image file to " + absolutePath); - errorDialog.errorText = qsTr( "Failed to copy image file to %1." ).arg( absolutePath ) - errorDialog.open() - } - } - externalResourceHandler.confirmImage(externalResourceHandler.itemWidget, externalResourceHandler.itemWidget.prefixToRelativePath, absolutePath) - } - - /** - * Called when an image is captured by a camera. Method sets proper value according given absolute path of the image - * and prefixPath set in thd project settings. - * \param imagePath Absolute path to a captured image - */ - property var imageCaptured: function imageCaptured(absoluteImagePath) { - if (absoluteImagePath) { - var prefixPath = externalResourceHandler.itemWidget.prefixToRelativePath.endsWith("/") ? - externalResourceHandler.itemWidget.prefixToRelativePath : - externalResourceHandler.itemWidget.prefixToRelativePath + "/" - externalResourceHandler.confirmImage(externalResourceHandler.itemWidget, prefixPath, absoluteImagePath) - } - } - - property var onFormSave: function onFormSave(itemWidget) { - __inputUtils.removeFile(itemWidget.sourceToDelete) - itemWidget.sourceToDelete = "" - } - - property var onFormCanceled: function onFormCanceled(itemWidget) { - itemWidget.sourceToDelete = "" - } - } - - Connections { - target: __androidUtils - // used for both gallery and camera - function onImageSelected( imagePath ) { - externalResourceHandler.imageSelected(imagePath) - } - } - - Connections { - target: __iosUtils - // used for both gallery and camera - function onImageSelected( imagePath ) { - externalResourceHandler.imageCaptured(imagePath) - } - } } From 1d5f348dc9f5296ac9ac48839f66ad5f1fc8c256 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Mon, 5 Feb 2024 08:46:55 +0100 Subject: [PATCH 18/28] Added remember last value option --- app/qml/form/FeatureForm.qml | 7 +++++++ app/qml/form/editors/MMFormCalendarEditor.qml | 19 +++++++++++++++++++ app/qml/form/editors/MMFormNumberEditor.qml | 12 ++++++++++++ app/qml/form/editors/MMFormPhotoEditor.qml | 11 +++++++++++ app/qml/form/editors/MMFormScannerEditor.qml | 13 ++++++++++++- app/qml/form/editors/MMFormSliderEditor.qml | 12 +++++++++++- app/qml/form/editors/MMFormSwitchEditor.qml | 12 ++++++++++++ app/qml/form/editors/MMFormTextEditor.qml | 11 +++++++++++ .../editors/MMFormTextMultilineEditor.qml | 11 +++++++++++ app/qml/form/editors/MMFormValueMapEditor.qml | 11 +++++++++++ .../editors/MMFormValueRelationEditor.qml | 11 +++++++++++ app/qml/inputs/MMBaseInput.qml | 3 +-- 12 files changed, 129 insertions(+), 4 deletions(-) diff --git a/app/qml/form/FeatureForm.qml b/app/qml/form/FeatureForm.qml index 09533c620..0e29735df 100644 --- a/app/qml/form/FeatureForm.qml +++ b/app/qml/form/FeatureForm.qml @@ -385,6 +385,9 @@ Item { property var fieldFeatureLayerPair: form.controller.featureLayerPair property string fieldHomePath: form.project ? form.project.homePath : "" // for photo editor + property bool fieldRememberValueSupported: form.controller.rememberAttributesController.rememberValuesAllowed && form.state === "add" && model.EditorWidget !== "Hidden" && Type === FormItem.Field + property bool fieldRememberValueState: model.RememberValue ? true : false + active: fieldWidget !== 'Hidden' Keys.forwardTo: backHandler @@ -405,6 +408,10 @@ Item { function onEditorValueChanged( newVal, isNull ) { model.AttributeValue = isNull ? undefined : newVal } + + function onRememberValueBoxClicked( state ) { + model.RememberValue = state + } } Connections { diff --git a/app/qml/form/editors/MMFormCalendarEditor.qml b/app/qml/form/editors/MMFormCalendarEditor.qml index b6c8a57fe..9caa81d51 100644 --- a/app/qml/form/editors/MMFormCalendarEditor.qml +++ b/app/qml/form/editors/MMFormCalendarEditor.qml @@ -13,6 +13,14 @@ import QtQuick.Controls.Basic import "../../components" import "../../inputs" +/* + * Calendar (datetime) editor for QGIS Attribute Form + * Requires various global properties set to function, see featureform Loader section. + * These properties are injected here via 'fieldXYZ' properties and captured with underscore `_`. + * + * Should be used only within feature form. + */ + MMBaseInput { id: root @@ -28,7 +36,11 @@ MMBaseInput { property string _fieldErrorMessage: parent.fieldErrorMessage property string _fieldWarningMessage: parent.fieldWarningMessage + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + signal editorValueChanged( var newValue, var isNull ) + signal rememberValueBoxClicked( bool state ) property bool fieldIsDate: __inputUtils.fieldType( _field ) === 'QDate' property var typeFromFieldFormat: __inputUtils.dateTimeFieldFormat( _fieldConfig['field_format'] ) @@ -45,9 +57,16 @@ MMBaseInput { warningMsg: _fieldWarningMessage errorMsg: _fieldErrorMessage + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + enabled: !_fieldIsReadOnly hasFocus: textField.activeFocus + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + content: TextField { id: textField diff --git a/app/qml/form/editors/MMFormNumberEditor.qml b/app/qml/form/editors/MMFormNumberEditor.qml index 3c92d4dec..1205cddf4 100644 --- a/app/qml/form/editors/MMFormNumberEditor.qml +++ b/app/qml/form/editors/MMFormNumberEditor.qml @@ -36,9 +36,14 @@ MMBaseInput { property string _fieldErrorMessage: parent.fieldErrorMessage property string _fieldWarningMessage: parent.fieldWarningMessage + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + property alias placeholderText: numberInput.placeholderText signal editorValueChanged( var newValue, var isNull ) + signal rememberValueBoxClicked( bool state ) + title: _fieldShouldShowTitle ? _fieldTitle : "" @@ -48,6 +53,13 @@ MMBaseInput { enabled: !_fieldIsReadOnly hasFocus: numberInput.activeFocus + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + leftAction: MMIcon { id: leftIcon diff --git a/app/qml/form/editors/MMFormPhotoEditor.qml b/app/qml/form/editors/MMFormPhotoEditor.qml index a43361509..23e7f2489 100644 --- a/app/qml/form/editors/MMFormPhotoEditor.qml +++ b/app/qml/form/editors/MMFormPhotoEditor.qml @@ -69,7 +69,11 @@ MMFormPhotoViewer { property string _fieldErrorMessage: parent.fieldErrorMessage property string _fieldWarningMessage: parent.fieldWarningMessage + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + signal editorValueChanged( var newValue, bool isNull ) + signal rememberValueBoxClicked( bool state ) title: _fieldShouldShowTitle ? _fieldTitle : "" @@ -78,6 +82,9 @@ MMFormPhotoViewer { enabled: !_fieldIsReadOnly + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + photoUrl: internal.absoluteImagePath hasCameraCapability: __androidUtils.isAndroid || __iosUtils.isIos @@ -87,6 +94,10 @@ MMFormPhotoViewer { onChooseFromGalleryClicked: internal.chooseFromGallery() onTrashClicked: internal.removeImage( __inputUtils.getAbsolutePath( root._fieldValue, internal.prefixToRelativePath ) ) + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + // used only on desktop builds FileDialog { id: desktopGalleryPicker diff --git a/app/qml/form/editors/MMFormScannerEditor.qml b/app/qml/form/editors/MMFormScannerEditor.qml index c031d14b2..05f00b524 100644 --- a/app/qml/form/editors/MMFormScannerEditor.qml +++ b/app/qml/form/editors/MMFormScannerEditor.qml @@ -13,7 +13,6 @@ import QtQuick.Controls.Basic import "../../components" import "../../inputs" - /* * QR/Barcode scanner editor for QGIS Attribute Form * Requires various global properties set to function, see featureform Loader section. @@ -21,6 +20,7 @@ import "../../inputs" * * Should be used only within feature form. */ + MMBaseInput { id: root @@ -35,10 +35,14 @@ MMBaseInput { property string _fieldErrorMessage: parent.fieldErrorMessage property string _fieldWarningMessage: parent.fieldWarningMessage + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + property alias placeholderText: textField.placeholderText property alias text: textField.text signal editorValueChanged( var newValue, bool isNull ) + signal rememberValueBoxClicked( bool state ) title: _fieldShouldShowTitle ? _fieldTitle : "" @@ -48,6 +52,13 @@ MMBaseInput { hasFocus: textField.activeFocus enabled: !_fieldIsReadOnly + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + content: TextField { id: textField diff --git a/app/qml/form/editors/MMFormSliderEditor.qml b/app/qml/form/editors/MMFormSliderEditor.qml index e4fde5055..7941fe5d3 100644 --- a/app/qml/form/editors/MMFormSliderEditor.qml +++ b/app/qml/form/editors/MMFormSliderEditor.qml @@ -35,7 +35,11 @@ MMBaseInput { property string _fieldErrorMessage: parent.fieldErrorMessage property string _fieldWarningMessage: parent.fieldWarningMessage + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + signal editorValueChanged( var newValue, var isNull ) + signal rememberValueBoxClicked( bool state ) title: _fieldShouldShowTitle ? _fieldTitle : "" @@ -43,9 +47,15 @@ MMBaseInput { errorMsg: _fieldErrorMessage hasFocus: slider.activeFocus - enabled: !_fieldIsReadOnly + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + content: Item { id: input diff --git a/app/qml/form/editors/MMFormSwitchEditor.qml b/app/qml/form/editors/MMFormSwitchEditor.qml index 09c4429f3..c80adb6d1 100644 --- a/app/qml/form/editors/MMFormSwitchEditor.qml +++ b/app/qml/form/editors/MMFormSwitchEditor.qml @@ -19,6 +19,7 @@ import "../../inputs" * * Should be used only within feature form. */ + MMBaseInput { id: root @@ -33,7 +34,11 @@ MMBaseInput { property string _fieldErrorMessage: parent.fieldErrorMessage property string _fieldWarningMessage: parent.fieldWarningMessage + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + signal editorValueChanged( var newValue, var isNull ) + signal rememberValueBoxClicked( bool state ) title: _fieldShouldShowTitle ? _fieldTitle : "" @@ -44,6 +49,13 @@ MMBaseInput { hasFocus: rightSwitch.focus + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + content: Text { id: textField diff --git a/app/qml/form/editors/MMFormTextEditor.qml b/app/qml/form/editors/MMFormTextEditor.qml index b54711925..b9da08df4 100644 --- a/app/qml/form/editors/MMFormTextEditor.qml +++ b/app/qml/form/editors/MMFormTextEditor.qml @@ -35,7 +35,11 @@ MMTextInput { property string _fieldErrorMessage: parent.fieldErrorMessage property string _fieldWarningMessage: parent.fieldWarningMessage + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + signal editorValueChanged( var newValue, bool isNull ) + signal rememberValueBoxClicked( bool state ) text: _fieldValue === undefined || _fieldValueIsNull ? '' : _fieldValue @@ -50,6 +54,9 @@ MMTextInput { warningMsg: _fieldWarningMessage errorMsg: _fieldErrorMessage + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + textFieldComponent.maximumLength: { if ( ( !root._field.isNumeric ) && ( root._field.length > 0 ) ) { return root._field.length @@ -57,6 +64,10 @@ MMTextInput { return internal.textMaxCharactersLimit } + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + onTextEdited: function ( text ) { let val = text if ( root._field.isNumeric ) diff --git a/app/qml/form/editors/MMFormTextMultilineEditor.qml b/app/qml/form/editors/MMFormTextMultilineEditor.qml index 55c11891f..134a87b80 100644 --- a/app/qml/form/editors/MMFormTextMultilineEditor.qml +++ b/app/qml/form/editors/MMFormTextMultilineEditor.qml @@ -34,12 +34,16 @@ MMBaseInput { property string _fieldErrorMessage: parent.fieldErrorMessage property string _fieldWarningMessage: parent.fieldWarningMessage + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + property alias placeholderText: textArea.placeholderText property alias text: textArea.text property int minimumRows: 3 signal editorValueChanged( var newValue, var isNull ) + signal rememberValueBoxClicked( bool state ) title: _fieldShouldShowTitle ? _fieldTitle : "" @@ -50,6 +54,13 @@ MMBaseInput { hasFocus: textArea.activeFocus + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + contentItemHeight: { const minHeight = 34 * __dp + metrics.height * root.minimumRows var realHeight = textArea.y + textArea.contentHeight + 2 * textArea.verticalPadding diff --git a/app/qml/form/editors/MMFormValueMapEditor.qml b/app/qml/form/editors/MMFormValueMapEditor.qml index 8fafaa5bd..662c4439e 100644 --- a/app/qml/form/editors/MMFormValueMapEditor.qml +++ b/app/qml/form/editors/MMFormValueMapEditor.qml @@ -33,7 +33,11 @@ MMDropdownInput { property string _fieldErrorMessage: parent.fieldErrorMessage property string _fieldWarningMessage: parent.fieldWarningMessage + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + signal editorValueChanged( var newValue, bool isNull ) + signal rememberValueBoxClicked( bool state ) title: _fieldShouldShowTitle ? _fieldTitle : "" @@ -44,10 +48,17 @@ MMDropdownInput { enabled: !_fieldIsReadOnly + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + dataModel: listModel multiSelect: false withSearchbar: false + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + onSelectionFinished: function ( selectedFeatures ) { if ( !selectedFeatures || ( Array.isArray( selectedFeatures ) && selectedFeatures.length !== 1 ) ) { diff --git a/app/qml/form/editors/MMFormValueRelationEditor.qml b/app/qml/form/editors/MMFormValueRelationEditor.qml index 30b5cecf9..d7bbc2af1 100644 --- a/app/qml/form/editors/MMFormValueRelationEditor.qml +++ b/app/qml/form/editors/MMFormValueRelationEditor.qml @@ -36,7 +36,11 @@ MMDropdownInput { property string _fieldErrorMessage: parent.fieldErrorMessage property string _fieldWarningMessage: parent.fieldWarningMessage + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + signal editorValueChanged( var newValue, bool isNull ) + signal rememberValueBoxClicked( bool state ) title: _fieldShouldShowTitle ? _fieldTitle : "" @@ -45,10 +49,17 @@ MMDropdownInput { enabled: !_fieldIsReadOnly + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + on_FieldValueChanged: { vrModel.pair = root._fieldFeatureLayerPair } + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + dropdownLoader.sourceComponent: Component { MMDropdownDrawer { focus: true diff --git a/app/qml/inputs/MMBaseInput.qml b/app/qml/inputs/MMBaseInput.qml index 476f3dee9..3d53f1dd4 100644 --- a/app/qml/inputs/MMBaseInput.qml +++ b/app/qml/inputs/MMBaseInput.qml @@ -33,7 +33,7 @@ Item { property bool hasFocus: false property color bgColor: __style.whiteColor property bool hasCheckbox: false - property bool checkboxChecked: false + property alias checkboxChecked: checkbox.checked property real contentItemHeight: 50 * __dp @@ -62,7 +62,6 @@ Item { small: true visible: root.hasCheckbox - checked: root.checkboxChecked } Text { id: titleItem From 667d16b5483c4ecd928a3f5845711c079a3c68e3 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Mon, 5 Feb 2024 22:34:27 +0100 Subject: [PATCH 19/28] Feature form redesigned + skeleton for preview panel --- app/attributes/attributecontroller.cpp | 2 +- app/qml/CMakeLists.txt | 11 +- app/qml/components/MMHeader.qml | 2 +- app/qml/components/MMToolbar.qml | 16 +- app/qml/form/FeatureForm.qml | 703 ------------------ app/qml/form/FeatureFormPage.qml | 252 ------- app/qml/form/FeatureFormStyling.qml | 122 --- app/qml/form/FeatureToolbar.qml | 203 ----- app/qml/form/MMForm.qml | 516 +++++++++++++ .../{FormWrapper.qml => MMFormWrapper.qml} | 111 ++- app/qml/form/MMPreviewPanel.qml | 285 +++++++ app/qml/form/PreviewPanel.qml | 213 ------ .../form/{ => components}/MMFormTabBar.qml | 2 +- app/qml/form/editors/MMFormPhotoEditor.qml | 9 - app/qml/map/MapWrapper.qml | 4 + 15 files changed, 907 insertions(+), 1544 deletions(-) delete mode 100644 app/qml/form/FeatureForm.qml delete mode 100644 app/qml/form/FeatureFormPage.qml delete mode 100644 app/qml/form/FeatureFormStyling.qml delete mode 100644 app/qml/form/FeatureToolbar.qml create mode 100644 app/qml/form/MMForm.qml rename app/qml/form/{FormWrapper.qml => MMFormWrapper.qml} (65%) create mode 100644 app/qml/form/MMPreviewPanel.qml delete mode 100644 app/qml/form/PreviewPanel.qml rename app/qml/form/{ => components}/MMFormTabBar.qml (96%) diff --git a/app/attributes/attributecontroller.cpp b/app/attributes/attributecontroller.cpp index fa5c4c2b7..6760f6ece 100644 --- a/app/attributes/attributecontroller.cpp +++ b/app/attributes/attributecontroller.cpp @@ -1328,7 +1328,7 @@ bool AttributeController::setFormShouldRememberValue( const QUuid &id, bool shou bool changed = mRememberAttributesController->setShouldRememberValue( mFeatureLayerPair.layer(), data->fieldIndex(), shouldRememberValue ); if ( changed ) { - emit formDataChanged( id ); + emit formDataChanged( id ); // It _should_ be enough to emit only the RememberValue role here, not all of them } return true; } diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index c9aa02730..846ed5e54 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -127,13 +127,10 @@ set(MM_QML # Forms # - form/FeatureForm.qml - form/FeatureFormPage.qml - form/FeatureFormStyling.qml - form/FeatureToolbar.qml - form/FormWrapper.qml - form/MMFormTabBar.qml - form/PreviewPanel.qml + form/MMFormWrapper.qml + form/MMForm.qml + form/MMPreviewPanel.qml + form/componentMMFormTabBar.qml form/editors/MMFormButtonEditor.qml form/editors/MMFormCalendarEditor.qml form/editors/MMFormGalleryEditor.qml diff --git a/app/qml/components/MMHeader.qml b/app/qml/components/MMHeader.qml index cf1ef20bb..33f86a2c1 100644 --- a/app/qml/components/MMHeader.qml +++ b/app/qml/components/MMHeader.qml @@ -31,7 +31,7 @@ Item { signal backClicked implicitHeight: 60 * __dp - implicitWidth: ApplicationWindow.window.width + implicitWidth: ApplicationWindow.window?.width ?? 0 Text { // If there is a right or a left icon, we need to shift the margin diff --git a/app/qml/components/MMToolbar.qml b/app/qml/components/MMToolbar.qml index 204c367e6..6bbed94db 100644 --- a/app/qml/components/MMToolbar.qml +++ b/app/qml/components/MMToolbar.qml @@ -19,6 +19,8 @@ Rectangle { readonly property double minimumToolbarButtonWidth: 100 * __dp + property int maxButtonsInToolbar: 4 + height: __style.toolbarHeight color: __style.forestColor @@ -72,9 +74,9 @@ Rectangle { var w = control.width var button - // add all buttons (max 4) into toolbar + // add all buttons (max maxButtonsInToolbar) into toolbar visibleButtonModel.clear() - if(c <= 4 || w >= c*control.minimumToolbarButtonWidth) { + if(c <= maxButtonsInToolbar || w >= c*control.minimumToolbarButtonWidth) { for( var i = 0; i < c; i++ ) { button = m.get(i) if(button.isMenuButton !== undefined) @@ -88,10 +90,10 @@ Rectangle { // not all buttons are visible in toolbar due to width // the past of them will apper in the menu inside '...' button var maxVisible = Math.floor(w/control.minimumToolbarButtonWidth) - if(maxVisible<4) - maxVisible = 4 + if(maxVisible= i*control.minimumToolbarButtonWidth) { + if(maxVisible===maxButtonsInToolbar || w >= i*control.minimumToolbarButtonWidth) { button = m.get(i) button.isMenuButton = false button.width = Math.floor(w / maxVisible) @@ -117,4 +119,8 @@ Rectangle { } } } + + function closeMenu() { + menu.close() + } } diff --git a/app/qml/form/FeatureForm.qml b/app/qml/form/FeatureForm.qml deleted file mode 100644 index 0e29735df..000000000 --- a/app/qml/form/FeatureForm.qml +++ /dev/null @@ -1,703 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import QtQml.Models -import QtQml - -import lc 1.0 -import "../components" -import Input 0.1 as Input - -Item { - /** - * When feature in the form is saved. - */ - signal saved - - /** - * When the form is about to be closed by closeButton or deleting a feature. - */ - signal canceled - - /** - * When edit operation failed. - */ - signal editingFailed - - /** - * Signal emited when relation editor requests to open child feature form - */ - signal openLinkedFeature( var linkedFeature ) - - /** - * Signal emited when relation editor requests to create child feature and open its form - */ - signal createLinkedFeature( var parentController, var relation ) - - /** - * Active project. - */ - property Input.Project project - - /** - * Controller - */ - property AttributeController controller - - /** - * View for extra components like value relation page, relations page, etc. - */ - property StackView extraView - - /** - * Predefined form styling - */ - property FeatureFormStyling style: FeatureFormStyling {} - - id: form - - states: [ - State { - name: "readOnly" - }, - State { - name: "edit" - }, - State { - name: "add" - } - ] - - function reset() { - master.reset() - } - - function save() { - if ( controller.hasValidationErrors ) - { - console.log( qsTr( 'Can not save the form, there are validation errors' ) ) - __inputUtils.showNotification( qsTr( 'Feature could not be saved, please check all required fields' ) ) - - // In future we could navigate user to a field that contains validation error - return - } - - parent.focus = true - controller.save() - } - - function cancel() { - // remove feature if we are in "add" mode and it already has valid ID - // it was saved to prefill relation reference field in child layer - let featureId = form.controller.featureLayerPair.feature.id - let shouldRemoveFeature = form.state === "add" && __inputUtils.isFeatureIdValid( featureId ) - - if ( shouldRemoveFeature ) { - form.controller.deleteFeature() - } - - parent.focus = true - - // rollback all changes if the layer is still editable - form.controller.rollback() - - canceled() - } - - /** - * This is a relay to forward private signals to internal components. - */ - QtObject { - id: master - - /** - * This signal is emitted whenever the state of Flickables and TabBars should - * be restored. - */ - signal reset - } - - StackView { - id: formView - - anchors.fill: parent - - initialItem: container - } - - Rectangle { - id: container - - clip: true - color: form.style.tabs.backgroundColor - - width: formView.width - height: formView.height - - Flickable { - id: flickable - anchors { - left: container.left - right: container.right - leftMargin: form.style.fields.outerMargin - rightMargin: form.style.fields.outerMargin - } - height: form.controller.hasTabs ? tabRow.height : 0 - - flickableDirection: Flickable.HorizontalFlick - contentWidth: tabRow.width - - // Tabs - TabBar { - id: tabRow - visible: form.controller.hasTabs - height: form.style.tabs.height - spacing: form.style.tabs.spacing - - background: Rectangle { - anchors.fill: parent - color: form.style.tabs.backgroundColor - } - - Connections { - target: master - function onReset() { - tabRow.currentIndex = 0 - } - } - - Connections { - target: swipeView - function onCurrentIndexChanged() { - tabRow.currentIndex = swipeView.currentIndex - } - } - - Repeater { - model: form.controller.attributeTabProxyModel - - TabButton { - id: tabButton - text: Name - leftPadding: 8 * __dp - rightPadding: 8 * __dp - anchors.bottom: parent.bottom - focusPolicy: Qt.NoFocus - - width: leftPadding + rightPadding - height: form.style.tabs.buttonHeight - - contentItem: Text { - // Make sure the width is derived from the text so we can get wider - // than the parent item and the Flickable is useful - Component.onCompleted: { - tabButton.width = tabButton.width + paintedWidth - if (tabRow.currentIndex == index) - tabButton.checked = true - } - - width: paintedWidth - text: tabButton.text - color: !tabButton.enabled ? form.style.tabs.disabledColor : tabButton.down || - tabButton.checked ? form.style.tabs.activeColor : form.style.tabs.normalColor - font.weight: Font.DemiBold - font.underline: tabButton.checked ? true : false - font.pixelSize: form.style.tabs.tabLabelPixelSize - opacity: tabButton.checked ? 1 : 0.5 - - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - background: Rectangle { - color: !tabButton.enabled ? form.style.tabs.disabledBackgroundColor : tabButton.down || - tabButton.checked ? form.style.tabs.activeBackgroundColor : form.style.tabs.normalBackgroundColor - } - } - } - } - } - - SwipeView { - id: swipeView - currentIndex: form.controller.hasTabs ? tabRow.currentIndex : 0 - - anchors { - top: flickable.bottom - left: container.left - right: container.right - bottom: container.bottom - } - - Repeater { - //One page per tab in tabbed forms, 1 page in auto forms - - model: form.controller.attributeTabProxyModel - id: swipeViewRepeater - - Item { - id: formPage - property int tabIndex: model.TabIndex - - // The main form content area - Rectangle { - anchors.fill: formPage - color: form.style.backgroundColor - opacity: form.style.backgroundOpacity - } - - ListView { - id: content - anchors.fill: formPage - clip: true - spacing: form.style.group.spacing - section.property: "Group" - section.labelPositioning: ViewSection.CurrentLabelAtStart | ViewSection.InlineLabels - section.delegate: Component { - - // section header: group box name - Item { - id: headerContainer - width: ListView.view.width - height: section === "" ? 0 : form.style.group.height + form.style.group.spacing // add space after section header - - Rectangle { - width: headerContainer.width - height: section === "" ? 0 : form.style.group.height - color: form.style.group.marginColor - anchors.top: headerContainer.top - - Rectangle { - anchors.fill: parent - anchors { - leftMargin: form.style.group.leftMargin - rightMargin: form.style.group.rightMargin - topMargin: form.style.group.topMargin - bottomMargin: form.style.group.bottomMargin - } - color: form.style.group.backgroundColor - - Text { - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - font.bold: true - font.pixelSize: form.style.group.fontPixelSize - text: section - color: form.style.group.fontColor - } - } - } - } - } - - - Connections { - target: master - function onReset() { - content.contentY = 0 - } - } - - - model: swipeViewRepeater.model.attributeFormProxyModel(formPage.tabIndex) - - delegate: editorComponent - - header: Rectangle { - opacity: 1 - height: form.style.group.spacing - } - - footer: Rectangle { - opacity: 1 - height: 2 * form.style.group.spacing - } - } - } - } - } - - // Borders - Rectangle { - width: container.width - height: form.style.tabs.borderWidth - anchors.top: flickable.top - color: form.style.tabs.borderColor - visible: flickable.height - } - - Rectangle { - width: container.width - height: form.style.tabs.borderWidth - anchors.bottom: flickable.bottom - color: form.style.tabs.borderColor - visible: flickable.height - } - } - - Component { - id: editorComponent - - Item { - - width: ListView.view.width - implicitHeight: childrenRect.height - - // TODO: filter such fields in field proxy model instead -// property bool shouldBeVisible: Type !== FormItem.Invalid && Type !== FormItem.Container - visible: Type !== FormItem.Invalid && Type !== FormItem.Container - - - Loader { - id: formEditorsLoader - - // - // Maybe one day we could use DelegateChooser instead of this hack-ish approach, see: - // https://doc.qt.io/qt-6/qml-qt-labs-qmlmodels-delegatechooser.html - // - - width: parent.width - - property var fieldValue: model.RawValue - property bool fieldValueIsNull: model.RawValueIsNull - - property var field: model.Field - property var fieldIndex: model.FieldIndex - property var fieldWidget: model.EditorWidget - property var fieldConfig: model.EditorWidgetConfig - - property bool fieldIsReadOnly: form.state === "readOnly" || !AttributeEditable - property bool fieldShouldShowTitle: model.ShowName - - property string fieldTitle: model.Name - property string fieldErrorMessage: model.ValidationStatus === FieldValidator.Error ? model.ValidationMessage : "" - property string fieldWarningMessage: model.ValidationStatus === FieldValidator.Warning ? model.ValidationMessage : "" - - property var fieldActiveProject: form.project - property var fieldAssociatedRelation: model.Relation - property var fieldFeatureLayerPair: form.controller.featureLayerPair - property string fieldHomePath: form.project ? form.project.homePath : "" // for photo editor - - property bool fieldRememberValueSupported: form.controller.rememberAttributesController.rememberValuesAllowed && form.state === "add" && model.EditorWidget !== "Hidden" && Type === FormItem.Field - property bool fieldRememberValueState: model.RememberValue ? true : false - - active: fieldWidget !== 'Hidden' - - Keys.forwardTo: backHandler - - source: { - if ( model.EditorWidget !== undefined ) { - return __inputUtils.getFormEditorType( model.EditorWidget, model.EditorWidgetConfig, model.Field ) - } - - return '' - } - } - - Connections { - target: formEditorsLoader.item - ignoreUnknownSignals: true - - function onEditorValueChanged( newVal, isNull ) { - model.AttributeValue = isNull ? undefined : newVal - } - - function onRememberValueBoxClicked( state ) { - model.RememberValue = state - } - } - - Connections { - target: form.controller - - // Important for relation form editors - function onFeatureLayerPairChanged() { - if ( formEditorsLoader.item && formEditorsLoader.item.featureLayerPairChanged ) - { - formEditorsLoader.item.featureLayerPairChanged() - } - } - - // Important for value relation form editors - function onFormRecalculated() { - if ( formEditorsLoader.item && formEditorsLoader.item.reload ) - { - formEditorsLoader.item.reload() - } - } - } - - Connections { - target: form - ignoreUnknownSignals: true - - function onSaved() { - if (formEditorsLoader.item && typeof formEditorsLoader.item.callbackOnSave === "function") { - formEditorsLoader.item.callbackOnFormSaved() - } - } - - function onCanceled() { - if (formEditorsLoader.item && typeof formEditorsLoader.item.callbackOnCancel === "function") { - formEditorsLoader.item.callbackOnFormCanceled() - } - } - } - } - } - - /** - * A field editor - */ - Component { - id: fieldItem - - Item { - id: fieldContainer - - // TODO: filter such fields in field proxy model instead - property bool shouldBeVisible: Type !== FormItem.Invalid && Type !== FormItem.Container - - visible: shouldBeVisible - - // We also need to set height to zero if Type is not field otherwise children created blank space in form - height: shouldBeVisible ? childrenRect.height : 0 - width: ListView.view.width - - Item { - id: paddedEditorField - - anchors { - left: fieldContainer.left - right: fieldContainer.right - leftMargin: form.style.fields.outerMargin - rightMargin: form.style.fields.outerMargin - } - - height: fieldContainer.shouldBeVisible ? childrenRect.height : 0 - - Item { - id: fieldLabelContainer - - height: fieldLabel.height + fieldValidationText.height + form.style.fields.sideMargin - - anchors { - left: paddedEditorField.left - right: paddedEditorField.right - topMargin: form.style.fields.sideMargin - bottomMargin: form.style.fields.sideMargin - } - - Label { - id: fieldLabel - - text: Name - height: visible ? paintedHeight : 0 - visible: ShowName - color: form.style.constraint.validColor - leftPadding: form.style.fields.sideMargin - font.pixelSize: form.style.fields.labelPixelSize - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - anchors.top: fieldLabelContainer.top - } - - Label { - id: fieldValidationText - - anchors { - left: fieldLabelContainer.left - right: fieldLabelContainer.right - top: fieldLabel.bottom - leftMargin: form.style.fields.sideMargin - } - - text: ValidationMessage - visible: ValidationMessage // show if there is something - height: visible ? paintedHeight : 0 - wrapMode: Text.WordWrap - opacity: visible ? 1 : 0 - color: ValidationStatus === FieldValidator.Warning ? form.style.constraint.descriptionColor : form.style.constraint.invalidColor - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - - Behavior on height { - NumberAnimation { duration: 100 } - } - - Behavior on opacity { - NumberAnimation { duration: 100 } - } - } - } - - Item { - id: placeholder - height: childrenRect.height - anchors { - left: paddedEditorField.left - right: rememberCheckboxContainer.left - top: fieldLabelContainer.bottom - } - - Loader { - id: attributeEditorLoader - - height: childrenRect.height - anchors { left: placeholder.left; right: placeholder.right } - - property var value: RawValue - property bool valueIsNull: RawValueIsNull - - property var field: Field - property var widget: EditorWidget - property var config: EditorWidgetConfig - - property var homePath: form.project ? form.project.homePath : "" - property var externalResourceHandler: form.externalResourceHandler - - property var customStyle: form.style - property bool readOnly: form.state === "readOnly" || !AttributeEditable - - property var labelAlias: Name - property var activeProject: form.project - property var associatedRelation: Relation - property var featurePair: form.controller.featureLayerPair - - property var formView: extraView //! passes StackView to editor, so that editors can show fullpage views (VR page, camera,..) - - active: widget !== 'Hidden' - Keys.forwardTo: backHandler - - source: { - if ( widget !== undefined ) - return __inputUtils.getEditorComponentSource( widget.toLowerCase(), config, field ) - else - return '' - } - } - - Connections { - target: attributeEditorLoader.item - ignoreUnknownSignals: true - - function onEditorValueChanged( newValue, isNull ) { - AttributeValue = isNull ? undefined : newValue - } - - function onOpenLinkedFeature( linkedFeature ) { - form.openLinkedFeature( linkedFeature ) - } - - function onCreateLinkedFeature( parentFeature, relation ) { - let parentHasValidId = __inputUtils.isFeatureIdValid( parentFeature.feature.id ) - - if ( parentHasValidId ) { - // parent feature in this case already have valid id, so we can open new form - form.createLinkedFeature( form.controller, relation ) - } - else { - // parent feature do not have a valid ID yet, we need to save it and acquire ID - form.controller.acquireId() - form.createLinkedFeature( form.controller, relation ) - } - } - } - - Connections { - target: form.controller - - function onFeatureLayerPairChanged() { - if ( attributeEditorLoader.item && attributeEditorLoader.item.featureLayerPairChanged ) - { - attributeEditorLoader.item.featureLayerPairChanged() - } - } - - function onFormRecalculated() { - if ( attributeEditorLoader.item && attributeEditorLoader.item.reload ) - { - attributeEditorLoader.item.reload() - } - } - } - - Connections { - target: form - ignoreUnknownSignals: true - - function onSaved() { - if (attributeEditorLoader.item && typeof attributeEditorLoader.item.callbackOnSave === "function") { - attributeEditorLoader.item.callbackOnSave() - } - } - - function onCanceled() { - if (attributeEditorLoader.item && typeof attributeEditorLoader.item.callbackOnCancel === "function") { - attributeEditorLoader.item.callbackOnCancel() - } - } - } - } - - Item { - id: rememberCheckboxContainer - visible: form.controller.rememberAttributesController.rememberValuesAllowed && form.state === "add" && EditorWidget !== "Hidden" && Type === FormItem.Field - - implicitWidth: visible ? 35 * __dp : 0 - implicitHeight: placeholder.height - - anchors { - top: fieldLabelContainer.bottom - right: paddedEditorField.right - } - - CheckboxComponent { - id: rememberCheckbox - visible: rememberCheckboxContainer.visible - baseColor: form.style.checkboxComponent.baseColor - - implicitWidth: 40 * __dp - implicitHeight: width - y: rememberCheckboxContainer.height/2 - rememberCheckbox.height/2 - x: (rememberCheckboxContainer.width + form.style.fields.outerMargin) / 7 - - onCheckboxClicked: function( buttonState ) { - RememberValue = buttonState - } - checked: RememberValue ? true : false - } - - MouseArea { - anchors.fill: rememberCheckboxContainer - onClicked: rememberCheckbox.checkboxClicked( !rememberCheckbox.checkState ) - } - } - } - } - } - - Connections { - target: Qt.inputMethod - function onVisibleChanged() { - Qt.inputMethod.commit() - } - } - - Connections { - target: form.controller - function onChangesCommited() { - form.saved() - } - function onCommitFailed() { - form.editingFailed() - } - } -} diff --git a/app/qml/form/FeatureFormPage.qml b/app/qml/form/FeatureFormPage.qml deleted file mode 100644 index e665b2a7c..000000000 --- a/app/qml/form/FeatureFormPage.qml +++ /dev/null @@ -1,252 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import QtQuick.Dialogs - -import lc 1.0 -import ".." -import "../components" - -Item { - id: root - - property var project - property var featureLayerPair - - property var linkedRelation - property var parentController - - property string formState - - signal close() - signal opened() - signal editGeometryClicked( var pair ) - signal splitGeometryClicked() - signal redrawGeometryClicked( var pair ) - signal openLinkedFeature( var linkedFeature ) - signal createLinkedFeature( var parentController, var relation ) - - onOpened: { - formStackView.forceActiveFocus() - } - - StackView { - id: formStackView - - /** - * StackView handling navigation in one FeatureForm - * Initial page is the form itself and any other extra - * needed pages (like value relation page, relations page, ..) - * should be pushed to this view. - * - * View is attached to Feature Form, - * so editors can push their components to it - */ - - anchors.fill: parent - - initialItem: formPageComponent - focus: true - - onCurrentItemChanged: { - currentItem.forceActiveFocus() - } - } - - Component { - id: formPageComponent - - Page { - id: formPage - - property alias form: featureForm - - header: PanelHeader { - id: header - - - height: InputStyle.rowHeightHeader - rowHeight: InputStyle.rowHeightHeader - color: InputStyle.clrPanelMain - fontBtnColor: InputStyle.highlightColor - - titleText: featureForm.state === "edit" ? qsTr("Edit Feature") : qsTr("Feature") - - backIconVisible: !saveButtonText.visible - backTextVisible: saveButtonText.visible - - onBack: featureForm.cancel() - - Text { - id: saveButtonText - - text: qsTr("Save") - - height: header.rowHeight - visible: featureForm.state === "edit" || featureForm.state === "add" - - color: featureForm.controller.hasValidationErrors ? InputStyle.invalidButtonColor : InputStyle.highlightColor - font.pixelSize: InputStyle.fontPixelSizeNormal - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignLeft - - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.top: parent.top - anchors.rightMargin: InputStyle.panelMargin // same as back button - - MouseArea { - anchors.fill: parent - onClicked: featureForm.save() - } - } - } - - Item { - id: backHandler - focus: true - Keys.onReleased: function( event ) { - if (event.key === Qt.Key_Back || event.key === Qt.Key_Escape) { - if ( featureForm.controller.hasAnyChanges ) { - saveChangesDialog.open() - } - else { - featureForm.cancel() - } - event.accepted = true; - } - } - - onVisibleChanged: function( visible ) { - if ( visible ) - backHandler.forceActiveFocus() - } - } - - // content - FeatureForm { - id: featureForm - - anchors.fill: parent - - project: root.project - - controller: AttributeController { - /*required*/ variablesManager: __variablesManager - rememberAttributesController: RememberAttributesController { - rememberValuesAllowed: __appSettings.reuseLastEnteredValues - } - // NOTE: order matters, we want to init variables manager before - // assingning FeatureLayerPair, as VariablesManager required for - // correct expression evaluation - /*required*/ featureLayerPair: root.featureLayerPair - } - - state: root.formState - - onSaved: root.close() - onCanceled: root.close() - onEditingFailed: editingFailedDialog.open() - onOpenLinkedFeature: function( linkedFeature ) { - root.openLinkedFeature( linkedFeature ) - } - onCreateLinkedFeature: function( parentController, relation ) { - root.createLinkedFeature( parentController, relation ) - } - - extraView: formPage.StackView.view - - Connections { - target: root - function onFormStateChanged() { - featureForm.state = root.formState - } - } - - Component.onCompleted: { - if ( root.parentController && root.linkedRelation ) { - featureForm.controller.parentController = root.parentController - featureForm.controller.linkedRelation = root.linkedRelation - } - } - } - - footer: FeatureToolbar { - id: toolbar - - height: InputStyle.rowHeightHeader - - state: featureForm.state - - visible: !root.readOnly - isFeaturePoint: __inputUtils.isPointLayer( root.featureLayerPair.layer ) - isSpatialLayer: __inputUtils.isSpatialLayer( root.featureLayerPair.layer ) - - onEditClicked: root.formState = "edit" - onDeleteClicked: deleteDialog.visible = true - onEditGeometryClicked: root.editGeometryClicked( featureForm.controller.featureLayerPair ) - onSplitGeometryClicked: root.splitGeometryClicked() - onRedrawGeometryClicked: root.redrawGeometryClicked( featureForm.controller.featureLayerPair ) - } - - MessageDialog { - id: deleteDialog - - visible: false - title: qsTr( "Delete feature" ) - text: qsTr( "Are you sure you want to delete this feature?" ) - buttons: MessageDialog.Ok | MessageDialog.Cancel - - onButtonClicked: function(clickedButton) { - if ( clickedButton === MessageDialog.Ok ) { - featureForm.controller.deleteFeature() - featureForm.canceled() - deleteDialog.close() - root.close() - } - - deleteDialog.close() - } - } - - MessageDialog { - id: saveChangesDialog - - visible: false - title: qsTr( "Unsaved changes" ) - text: qsTr( "Do you want to save changes?" ) - buttons: MessageDialog.Yes | MessageDialog.No | MessageDialog.Cancel - - onButtonClicked: function(clickedButton) { - if (clickedButton === MessageDialog.Yes) { - featureForm.save() - } - else if (clickedButton === MessageDialog.No) { - featureForm.cancel() - } - saveChangesDialog.close() - } - } - - MessageDialog { - id: editingFailedDialog - - visible: false - title: qsTr( "Saving failed" ) - text: qsTr( "Failed to save changes. This should not happen normally. Please restart the app and try again — if that does not help, please contact support." ) - buttons: MessageDialog.Close - - onButtonClicked: close() - } - } - } -} diff --git a/app/qml/form/FeatureFormStyling.qml b/app/qml/form/FeatureFormStyling.qml deleted file mode 100644 index 2b43cf936..000000000 --- a/app/qml/form/FeatureFormStyling.qml +++ /dev/null @@ -1,122 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick - -import ".." - -QtObject { - property color backgroundColor: "white" - property real backgroundOpacity: 1 - property real titleLabelPixelSize: InputStyle.fontPixelSizeNormal - - property QtObject group: QtObject { - property color backgroundColor: InputStyle.panelBackgroundLight - property color marginColor: InputStyle.panelBackgroundDark - property real leftMargin: 0 * __dp - property real rightMargin: 0 * __dp - property real topMargin: 1 * __dp - property real bottomMargin: 1 * __dp - property real height: 64 * __dp - property color fontColor: InputStyle.fontColor - property int spacing: InputStyle.formSpacing - property int fontPixelSize: InputStyle.fontPixelSizeNormal - } - - property QtObject tabs: QtObject { - property color normalColor: InputStyle.fontColor - property color activeColor: InputStyle.fontColor - property color disabledColor: InputStyle.fontColor - property color backgroundColor: InputStyle.panelBackgroundLight - property color normalBackgroundColor: InputStyle.panelBackgroundLight - property color activeBackgroundColor: InputStyle.panelBackgroundLight - property color disabledBackgroundColor: InputStyle.panelBackgroundDark - property real height: InputStyle.rowHeight * 0.9 - property real buttonHeight: height - property real spacing: 0 - property int tabLabelPixelSize: InputStyle.fontPixelSizeSmall - property real borderWidth: 1 * __dp - property color borderColor: InputStyle.labelColor - } - - property QtObject constraint: QtObject { - property color validColor: InputStyle.labelColor - property color invalidColor: "#c0392b" - property color descriptionColor: "#e67e22" - } - - property QtObject toolbutton: QtObject { - property color backgroundColor: "transparent" - property color backgroundColorInvalid: "#bdc3c7" - property color activeButtonColor: InputStyle.activeButtonColor - property real size: 80 * __dp - } - - property QtObject fields: QtObject { - property color backgroundColor: InputStyle.panelBackgroundLight - property color backgroundColorDark: InputStyle.panelBackgroundDark - property color backgroundColorDarker: InputStyle.panelBackgroundDarker - property color backgroundColorInactive: "grey" - property color fontColor: InputStyle.fontColor - property color activeColor: InputStyle.fontColor - property color attentionColor: "#aa0000" - property color normalColor: InputStyle.panelBackgroundLight - property real cornerRadius: 8 * __dp - property real height: InputStyle.fieldHeight - property int fontPixelStyle: InputStyle.fontPixelSizeSmall - property real sideMargin: InputStyle.innerFieldMargin - property real outerMargin: InputStyle.outerFieldMargin - property int fontPixelSize: InputStyle.fontPixelSizeNormal - property int labelPixelSize: InputStyle.fontPixelSizeSmall - } - - property QtObject icons: QtObject { - property var camera: InputStyle.cameraIcon - property var remove: InputStyle.removeIcon - property var gallery:InputStyle.galleryIcon - property var notAvailable: __inputUtils.getThemeIcon("no-image") - property var today: __inputUtils.getThemeIcon("ic_today") - property var back: InputStyle.backIcon - property var combobox: InputStyle.comboboxIcon - property var valueRelationMore: InputStyle.valueRelationIcon - property var minus: __inputUtils.getThemeIcon("minus") - property var plus: __inputUtils.getThemeIcon("plus-big") - property string relationsLink: InputStyle.linkIcon - property string relationsUnlink: InputStyle.unlinkIcon - } - - property QtObject checkboxComponent: QtObject { - property color baseColor: InputStyle.panelBackgroundDarker - } - - property QtObject relationComponent: QtObject { - property real textDelegateHeight: fields.height * 0.8 - property int flowSpacing: 8 * __dp - - // photo mode - property color photoBorderColor: InputStyle.darkGreen - property color photoBorderColorButton: InputStyle.darkOrange - property color iconColor: InputStyle.darkGreen - property color iconColorButton: InputStyle.darkOrange - property color textColorButton: InputStyle.darkOrange - property real photoBorderWidth: 2 * __dp - - // tag cloud (text mode) - property color tagBackgroundColor: InputStyle.darkGreen - property color tagBackgroundColorButton: InputStyle.darkOrange - property color tagBackgroundColorButtonAlt: InputStyle.panelBackgroundLight - property color tagBorderColor: InputStyle.darkGreen - property color tagBorderColorButton: InputStyle.darkOrange - property color tagTextColor: InputStyle.panelBackgroundLight - property color tagTextColorButton: InputStyle.darkOrange - property real tagBorderWidth: 2 * __dp - property real tagRadius: 10 * __dp - property real tagInnerSpacing: 30 * __dp - } -} diff --git a/app/qml/form/FeatureToolbar.qml b/app/qml/form/FeatureToolbar.qml deleted file mode 100644 index 94bb172c9..000000000 --- a/app/qml/form/FeatureToolbar.qml +++ /dev/null @@ -1,203 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import ".." -import "../components" - -Item { - id: toolbar - - property int itemSize: toolbar.height * 0.8 - property bool isFeaturePoint: false - property bool isSpatialLayer: false - - signal editClicked() - signal deleteClicked() - signal editGeometryClicked() - signal splitGeometryClicked() - signal redrawGeometryClicked() - - states: [ - State { - name: "edit" // edit existing feature - PropertyChanges { target: editRow; visible: true } - PropertyChanges { target: readOnlyRow; visible: false } - PropertyChanges { target: addRow; visible: false } - } - ,State { - name: "add" // add new feature - PropertyChanges { target: editRow; visible: false } - PropertyChanges { target: readOnlyRow; visible: false } - PropertyChanges { target: addRow; visible: true } - } - ,State { - name: "readOnly" - PropertyChanges { target: editRow; visible: false } - PropertyChanges { target: readOnlyRow; visible: true } - PropertyChanges { target: addRow; visible: false } - } - ] - - Rectangle { - anchors.fill: parent - color: InputStyle.clrPanelBackground - } - - Row { - id: readOnlyRow - height: parent.height - width: parent.width - anchors.fill: parent - - Item { - width: parent.width/parent.children.length - height: parent.height - - MainPanelButton { - id: openProjectBtn - width: toolbar.itemSize - text: qsTr("Edit") - imageSource: InputStyle.editIcon - - onActivated: { - toolbar.editClicked() - } - } - } - } - - Row { - id: editRow - height: parent.height - width: parent.width - anchors.fill: parent - - Item { - width: parent.width/parent.children.length - height: parent.height - - MainPanelButton { - width: visible ? toolbar.itemSize : 0 - text: qsTr("Delete") - imageSource: InputStyle.removeIcon - - onActivated: { - toolbar.deleteClicked() - } - } - } - - Item { - width: visible ? parent.width/parent.children.length : 0 - height: parent.height - visible: isSpatialLayer - - MainPanelButton { - width: parent.visible ? toolbar.itemSize : 0 - text: qsTr("Edit geometry") - imageSource: InputStyle.editIcon - - onActivated: { - toolbar.editGeometryClicked() - } - } - } - - Item { - width: visible ? parent.width/parent.children.length : 0 - height: parent.height - visible: isSpatialLayer - - MainPanelButton { - id: menuBtn - width: parent.visible ? toolbar.itemSize : 0 - text: qsTr("Advanced") - imageSource: InputStyle.moreMenuIcon - - onActivated: { - if ( !rootMenu.visible ) rootMenu.open() - else rootMenu.close() - } - } - } - } - - Row { - id: addRow - height: parent.height - width: parent.width - anchors.fill: parent - - - Item { - width: parent.width/parent.children.length - height: parent.height - visible: isSpatialLayer - - MainPanelButton { - width: toolbar.itemSize - text: qsTr("Edit geometry") - imageSource: InputStyle.editIcon - - onActivated: { - toolbar.editGeometryClicked() - } - } - } - } - - Menu { - id: rootMenu - title: qsTr("Advanced") - x:parent.width - rootMenu.width - y: -rootMenu.height - width: parent.width < 300 * __dp ? parent.width : 300 * __dp - closePolicy: Popup.CloseOnReleaseOutsideParent | Popup.CloseOnEscape - - MenuItem { - width: parent.width - height: visible ? toolbar.itemSize : 0 - visible: !isFeaturePoint - - ExtendedMenuItem { - height: toolbar.itemSize - rowHeight: height - width: parent.width - contentText: qsTr("Split geometry") - imageSource: InputStyle.scissorsIcon - } - - onClicked: { - toolbar.splitGeometryClicked() - rootMenu.close() - } - } - - MenuItem { - width: parent.width - height: toolbar.itemSize - - ExtendedMenuItem { - height: toolbar.itemSize - rowHeight: height - width: parent.width - contentText: qsTr("Redraw geometry") - imageSource: InputStyle.eraserIcon - } - - onClicked: { - toolbar.redrawGeometryClicked() - rootMenu.close() - } - } - } - -} diff --git a/app/qml/form/MMForm.qml b/app/qml/form/MMForm.qml new file mode 100644 index 000000000..40a51c132 --- /dev/null +++ b/app/qml/form/MMForm.qml @@ -0,0 +1,516 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQml +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +import lc 1.0 + +import "../components" +import Input 0.1 as Input + +Page { + id: root + + /** + * When feature in the form is saved. + */ + signal saved + + /** + * When the form is about to be closed by closeButton or deleting a feature. + */ + signal canceled + + /** + * Signal emited when relation editor requests to open child feature form + */ + signal openLinkedFeature( var linkedFeature ) + + /** + * Signal emited when relation editor requests to create child feature and open its form + */ + signal createLinkedFeature( var parentController, var relation ) + + signal splitGeometryRequested() + signal redrawGeometryRequested( var layerPair ) + signal editGeometryRequested( var layerPair ) + + /** + * Active project. + */ + property Input.Project project + + /** + * Controller + */ + property AttributeController controller + + implicitWidth: ApplicationWindow.window?.width ?? 0 + implicitHeight: ApplicationWindow.window?.height ?? 0 + + states: [ + State { + name: "readOnly" + }, + State { + name: "edit" + }, + State { + name: "add" + } + ] + + background: Rectangle { + color: __style.lightGreenColor + } + + header: MMHeader { + title: { + if ( root.state === "add" ) return qsTr( "New feature" ) + else if ( root.state === "edit" ) return qsTr( "Edit feature" ) + return __inputUtils.featureTitle( root.controller.featureLayerPair, __activeProject.qgsProject ) + } + + rightMarginShift: saveButton.visible ? saveButton.width : 0 + + onBackClicked: root.rollbackAndClose() + + MMRoundButton { + id: saveButton + + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: __style.pageMargins + + visible: root.state === "add" || root.state === "edit" + + iconSource: __style.checkmarkIcon + iconColor: __style.forestColor + + bgndColor: __style.grassColor + + onClicked: root.save() + } + } + + ColumnLayout { + anchors.fill: parent + + MMFormTabBar { + id: tabBar + + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: __style.maxPageWidth + + visible: root.controller.hasTabs + + tabButtonsModel: root.controller.attributeTabProxyModel + + onCurrentIndexChanged: formSwipe.setCurrentIndex( tabBar.currentIndex ) + } + + SwipeView { + id: formSwipe + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: __style.maxPageWidth + + clip: true + + onCurrentIndexChanged: tabBar.setCurrentIndex( formSwipe.currentIndex ) + + Repeater { + id: swipeViewRepeater + + model: root.controller.attributeTabProxyModel + + Item { + id: pageDelegate + + property int tabIndex: model.TabIndex // from the repeater + + ListView { + + anchors { + fill: parent + leftMargin: __style.pageMargins + rightMargin: __style.pageMargins + } + + model: swipeViewRepeater.model.attributeFormProxyModel( pageDelegate.tabIndex ) + + clip: true + spacing: internal.formSpacing + + header: Rectangle { + opacity: 1 // invisible + height: 20 * __dp + } + + section { + property: "Group" + delegate: sectionDelegate + labelPositioning: ViewSection.CurrentLabelAtStart | ViewSection.InlineLabels + } + + delegate: editorDelegate + + footer: Rectangle { + opacity: 1 // invisible + height: 20 * __dp + } + } + + } + } + } + } + + footer: MMToolbar { + +// TODO: visible: !root.layerIsReadOnly + + maxButtonsInToolbar: 3 + + ObjectModel { + id: readStateButtons + + MMToolbarLongButton { + text: qsTr( "Edit feature" ); + + iconSource: __style.editIcon; + + onClicked: { + root.state = "edit" + } + } + } + + ObjectModel { + id: addStateButtons // edit buttons are the same + + MMToolbarButton { + text: qsTr( "Delete" ) + iconSource: __style.deleteIcon + onClicked: deleteDialog.open() + } + + MMToolbarButton { + text: qsTr( "Edit geometry" ) + iconSource: __style.editIcon + onClicked: root.editGeometryRequested( root.controller.featureLayerPair ) + } + + MMToolbarButton { + text: qsTr( "Redraw geometry" ) + iconSource: __style.editIcon + onClicked: root.redrawGeometryRequested( root.controller.featureLayerPair ) + } + + MMToolbarButton { + text: qsTr( "Split geometry" ) + iconSource: __style.editIcon + onClicked: root.splitGeometryRequested() + } + } + + + model: root.state === "readOnly" ? readStateButtons : addStateButtons + } + + Component { + id: sectionDelegate + + Item { + + property string sectionTitle: section + + height: section ? 76 * __dp : 0 + width: ListView.view.width + + // section bgnd + Rectangle { + anchors.fill: parent; + color: __style.lightGreenColor; + } + + Text { + id: sectionTitle + + text: section + font: __style.h3 + color: __style.forestColor + + topPadding: internal.formSpacing + bottomPadding: internal.formSpacing + } + } + } + + Component { + id: editorDelegate + + Item { + + width: ListView.view.width + implicitHeight: childrenRect.height + + // In future, better to filter such fields in the field proxy model instead + visible: Type !== FormItem.Invalid && Type !== FormItem.Container + + Loader { + id: formEditorsLoader + + // + // Maybe one day we could use DelegateChooser instead of this hack-ish approach, see: + // https://doc.qt.io/qt-6/qml-qt-labs-qmlmodels-delegatechooser.html + // + + width: parent.width + + property var fieldValue: model.RawValue + property bool fieldValueIsNull: model.RawValueIsNull + + property var field: model.Field + property var fieldIndex: model.FieldIndex + property var fieldWidget: model.EditorWidget + property var fieldConfig: model.EditorWidgetConfig + + property bool fieldIsReadOnly: root.state === "readOnly" || !AttributeEditable + property bool fieldShouldShowTitle: model.ShowName + + property string fieldTitle: model.Name + property string fieldErrorMessage: model.ValidationStatus === FieldValidator.Error ? model.ValidationMessage : "" + property string fieldWarningMessage: model.ValidationStatus === FieldValidator.Warning ? model.ValidationMessage : "" + + property var fieldActiveProject: root.project + property var fieldAssociatedRelation: model.Relation + property var fieldFeatureLayerPair: root.controller.featureLayerPair + property string fieldHomePath: root.project ? root.project.homePath : "" // for photo editor + + property bool fieldRememberValueSupported: root.controller.rememberAttributesController.rememberValuesAllowed && root.state === "add" && model.EditorWidget !== "Hidden" && Type === FormItem.Field + property bool fieldRememberValueState: model.RememberValue ? true : false + + active: fieldWidget !== 'Hidden' + + Keys.forwardTo: backHandler + + source: { + if ( model.EditorWidget !== undefined ) { + return __inputUtils.getFormEditorType( model.EditorWidget, model.EditorWidgetConfig, model.Field ) + } + + return '' + } + } + + Connections { + target: formEditorsLoader.item + ignoreUnknownSignals: true + + function onEditorValueChanged( newVal, isNull ) { + model.AttributeValue = isNull ? undefined : newVal + } + + function onRememberValueBoxClicked( state ) { + model.RememberValue = state + } + + // TODO: support for relations + } + + Connections { + target: root.controller + + // Important for relation form editors + function onFeatureLayerPairChanged() { + if ( formEditorsLoader.item && formEditorsLoader.item.featureLayerPairChanged ) + { + formEditorsLoader.item.featureLayerPairChanged() + } + } + + // Important for value relation form editors + function onFormRecalculated() { + if ( formEditorsLoader.item && formEditorsLoader.item.reload ) + { + formEditorsLoader.item.reload() + } + } + } + + Connections { + target: root + ignoreUnknownSignals: true + + function onSaved() { + if ( formEditorsLoader.item && typeof formEditorsLoader.item.callbackOnSave === "function" ) { + formEditorsLoader.item.callbackOnFormSaved() + } + } + + function onCanceled() { + if ( formEditorsLoader.item && typeof formEditorsLoader.item.callbackOnCancel === "function" ) { + formEditorsLoader.item.callbackOnFormCanceled() + } + } + } + } + } + + MessageDialog { + id: saveChangesDialog + + visible: false + title: qsTr( "Unsaved changes" ) + text: qsTr( "Do you want to save changes?" ) + buttons: MessageDialog.Yes | MessageDialog.No | MessageDialog.Cancel + + onButtonClicked: function( clickedButton ) { + if ( clickedButton === MessageDialog.Yes ) { + root.save() + } + else if ( clickedButton === MessageDialog.No ) { + root.rollbackAndClose() + } + saveChangesDialog.close() + } + } + + MessageDialog { + id: deleteDialog + + title: qsTr( "Delete feature" ) + text: qsTr( "Are you sure you want to delete this feature?" ) + buttons: MessageDialog.Yes | MessageDialog.No + + onButtonClicked: function( clickedButton ) { + if ( clickedButton === MessageDialog.Yes ) { + root.controller.deleteFeature() + root.canceled() + deleteDialog.close() + } + + deleteDialog.close() + } + } + + MessageDialog { + id: editingFailedDialog + + title: qsTr( "Saving failed" ) + text: qsTr( "Failed to save changes. This should not happen normally. Please restart the app and try again — if that does not help, please contact support." ) + buttons: MessageDialog.Close + + onButtonClicked: close() + } + + Item { + id: backHandler + + focus: true + Keys.onReleased: function( event ) { + if ( event.key === Qt.Key_Back || event.key === Qt.Key_Escape ) { + if ( root.controller.hasAnyChanges ) { + saveChangesDialog.open() + } + else { + root.rollbackAndClose() + } + event.accepted = true; + } + } + + onVisibleChanged: function( visible ) { + if ( visible ) + backHandler.forceActiveFocus() + } + } + + Connections { + target: Qt.inputMethod + + function onVisibleChanged() { + Qt.inputMethod.commit() + } + } + + Connections { + target: root.controller + + function onChangesCommited() { + root.saved() + } + + function onCommitFailed() { + editingFailedDialog.open() + } + } + + function reset() { + master.reset() + } + + function save() { + if ( controller.hasValidationErrors ) + { + console.log( qsTr( 'Can not save the form, there are validation errors' ) ) + __inputUtils.showNotification( qsTr( 'Feature could not be saved, please check all required fields' ) ) + + // In future we could navigate user to a field that contains validation error + return + } + + parent.focus = true + controller.save() + } + + function rollbackAndClose() { + // remove feature if we are in "add" mode and it already has valid ID + // it was saved to prefill relation reference field in child layer + let featureId = root.controller.featureLayerPair.feature.id + let shouldRemoveFeature = root.state === "add" && __inputUtils.isFeatureIdValid( featureId ) + + if ( shouldRemoveFeature ) { + root.controller.deleteFeature() + } + + parent.focus = true + + // rollback all changes if the layer is still editable + root.controller.rollback() + + root.canceled() + } + + /** + * This is a relay to forward private signals to internal components. + */ + QtObject { + id: master + + /** + * This signal is emitted whenever the state of Flickables and TabBars should + * be restored. + */ + signal reset + } + + QtObject { + id: internal + + property real formSpacing: 20 * __dp + } +} diff --git a/app/qml/form/FormWrapper.qml b/app/qml/form/MMFormWrapper.qml similarity index 65% rename from app/qml/form/FormWrapper.qml rename to app/qml/form/MMFormWrapper.qml index a64586949..c5e67f197 100644 --- a/app/qml/form/FormWrapper.qml +++ b/app/qml/form/MMFormWrapper.qml @@ -13,6 +13,8 @@ import QtQuick.Controls import lc 1.0 import ".." +// Wraps preview panel and feature form + Item { id: root @@ -28,13 +30,13 @@ Item { property var relationToApply property var controllerToApply - property alias formState: formContainer.formState // add, edit or ReadOnly + property alias formState: featureForm.state // add, edit or ReadOnly property alias panelState: statesManager.state property real previewHeight property real panelHeight - property bool isReadOnly: featureLayerPair?.layer?.readOnly ?? false + property bool layerIsReadOnly: featureLayerPair?.layer?.readOnly ?? false signal closed() signal editGeometry( var pair ) @@ -64,18 +66,18 @@ Item { name: "preview" PropertyChanges { target: drawer; height: root.previewHeight } PropertyChanges { target: drawer; interactive: true } - PropertyChanges { target: formContainer; visible: false } + PropertyChanges { target: featureForm; visible: false } PropertyChanges { target: previewPanel; visible: true } }, State { name: "form" PropertyChanges { target: drawer; height: root.height } PropertyChanges { target: drawer; interactive: false } - PropertyChanges { target: formContainer; visible: true } + PropertyChanges { target: featureForm; visible: true } PropertyChanges { target: previewPanel; visible: false } StateChangeScript { script: { - formContainer.opened() + featureForm.forceActiveFocus() } } }, @@ -106,10 +108,36 @@ Item { PropertyAnimation { properties: "height"; easing.type: Easing.InOutQuad } } + background: Rectangle { // rounded drawer + color: __style.whiteColor + radius: 20 * __dp + + Rectangle { + width: parent.width / 10 + height: 4 * __dp + + anchors.top: parent.top + anchors.topMargin: 8 * __dp + anchors.horizontalCenter: parent.horizontalCenter + + radius: 20 * __dp + + color: __style.lightGreenColor + } + + Rectangle { + color: __style.whiteColor + width: parent.width + height: parent.height + y: parent.height / 2 + } + } + width: parent.width - z: 0 + modal: false dragMargin: 0 // prevents opening the drawer by dragging. + edge: Qt.BottomEdge closePolicy: Popup.CloseOnEscape // prevents the drawer closing while moving canvas @@ -121,56 +149,85 @@ Item { PreviewPanel { id: previewPanel - onStakeoutFeature: function( feature ) { - root.stakeoutFeature( feature ) - } - - isReadOnly: root.isReadOnly + layerIsReadOnly: root.layerIsReadOnly controller: AttributePreviewController { project: root.project; featureLayerPair: root.featureLayerPair } height: root.previewHeight width: root.width - onContentClicked: root.panelState = "form" + onStakeoutClicked: function( feature ) { + root.stakeoutFeature( feature ) + } + +// onContentClicked: root.panelState = "form" + onEditClicked: { root.panelState = "form" - formContainer.formState = "edit" + featureForm.state = "edit" } } - FeatureFormPage { - id: formContainer + MMForm { + id: featureForm anchors.fill: parent project: root.project - featureLayerPair: root.featureLayerPair - linkedRelation: root.linkedRelation - parentController: root.parentController + controller: AttributeController { + variablesManager: __variablesManager - formState: root.formState + rememberAttributesController: RememberAttributesController { + rememberValuesAllowed: __appSettings.reuseLastEnteredValues + } + // NOTE: order matters, we want to init variables manager before + // assingning FeatureLayerPair, as VariablesManager is required + // for correct expression evaluation + featureLayerPair: root.featureLayerPair + } - onClose: root.panelState = "closed" - onEditGeometryClicked: function( pair ) { + state: root.formState + + onSaved: root.panelState = "closed" + onCanceled: root.panelState = "closed" + + onEditGeometryRequested: function( pair ) { root.panelState = "hidden" root.editGeometry( pair ) } + + onRedrawGeometryRequested: function( pair ) { + root.panelState = "hidden" + root.redrawGeometry( pair ) + } + + onSplitGeometryRequested: { + root.panelState = "hidden" + root.splitGeometry( root.featureLayerPair ) + } + onOpenLinkedFeature: function( linkedFeature ) { root.openLinkedFeature( linkedFeature ) } + onCreateLinkedFeature: function( parentController, relation ) { root.controllerToApply = parentController root.relationToApply = relation root.createLinkedFeature( relation.referencingLayer, root.featureLayerPair ) } - onSplitGeometryClicked: { - root.panelState = "hidden" - root.splitGeometry( root.featureLayerPair ) + + Connections { + target: root + function onFormStateChanged() { + featureForm.state = root.formState + } } - onRedrawGeometryClicked: function( pair ) { - root.panelState = "hidden" - root.redrawGeometry( pair ) + + Component.onCompleted: { + if ( root.parentController && root.linkedRelation ) { + featureForm.controller.parentController = root.parentController + featureForm.controller.linkedRelation = root.linkedRelation + } } } } diff --git a/app/qml/form/MMPreviewPanel.qml b/app/qml/form/MMPreviewPanel.qml new file mode 100644 index 000000000..3d0a9a1ae --- /dev/null +++ b/app/qml/form/MMPreviewPanel.qml @@ -0,0 +1,285 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +//import Qt5Compat.GraphicalEffects + +//import ".." // import InputStyle singleton +import "../components" as Components +import lc 1.0 + +Item { + id: root + + property bool layerIsReadOnly: false + property AttributePreviewController controller + +// signal contentClicked() + signal editClicked() + signal stakeoutClicked( var feature ) // TODO: remove the feature + + ColumnLayout { + anchors { + fill: parent + margins: __style.pageMargins + } + + spacing: 20 * __dp + + Item { + id: photoContainer + + Layout.fillWidth: true + Layout.preferredHeight: 160 * __dp + + visible: root.controller.type === AttributePreviewController.Photo + + Components.MMPhoto { + width: parent.width + height: parent.height + + photoUrl: root.controller.photo + + fillMode: Image.PreserveAspectCrop + } + } + + Text { + Layout.fillWidth: true + Layout.preferredHeight: paintedHeight + + text: root.controller.title + + font: __style.t1 + color: __style.forestColor + + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + + maximumLineCount: 2 + elide: Text.ElideRight + wrapMode: Text.WordWrap + } + + // TODO: HTML type + + Item { + id: buttonGroup + + Layout.fillWidth: true + Layout.preferredHeight: 40 * __dp + + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + } + + + + +// property real rowHeight: InputStyle.rowHeight + + + +// MouseArea { +// anchors.fill: parent +// onClicked: { +// contentClicked() +// } +// } + +// layer.enabled: true +// layer.effect: Components.Shadow {} + +// Rectangle { +// anchors.fill: parent +// color: InputStyle.clrPanelMain + +// Rectangle { +// anchors.fill: parent +// anchors.margins: InputStyle.panelMargin +// anchors.topMargin: 0 + +// Item { +// id: header +// width: parent.width +// height: previewPanel.rowHeight + +// Row { +// id: title +// height: rowHeight +// width: parent.width + +// Text { +// id: titleText + +//// height: rowHeight +// width: parent.width + +// text: controller.title + +// wrapMode: Text.WordWrap +// maximumLineCount: 2 +// elide: Text.ElideRight + +// font: __style.t1 + +// color: __style.forestColor + +// horizontalAlignment: Text.AlignLeft +// verticalAlignment: Text.AlignVCenter +// } + +//// Button { +//// id: stakeoutIconContainerSpace +//// height: rowHeight +//// width: rowHeight + +//// background: Item { +//// visible: __inputUtils.isPointLayerFeature( controller.featureLayerPair ) +//// enabled: visible + +//// anchors.fill: parent + +//// Image { +//// id: stakeoutIcon + +//// anchors.fill: parent +//// anchors.margins: rowHeight/8 +//// anchors.rightMargin: 0 +//// source: InputStyle.stakeoutIcon +//// sourceSize.width: width +//// sourceSize.height: height +//// fillMode: Image.PreserveAspectFit +//// } + +//// ColorOverlay { +//// anchors.fill: stakeoutIcon +//// source: stakeoutIcon +//// color: InputStyle.fontColor +//// } +//// } + +//// onClicked: previewPanel.stakeoutFeature( controller.featureLayerPair ) +//// } + +//// Button { +//// id: iconContainer +//// height: rowHeight +//// width: rowHeight +//// visible: !previewPanel.isReadOnly + +//// background: Item { +//// anchors.fill: parent + +//// Image { +//// id: icon +//// anchors.fill: parent +//// anchors.margins: rowHeight/4 +//// anchors.rightMargin: 0 +//// source: InputStyle.editIcon +//// sourceSize.width: width +//// sourceSize.height: height +//// fillMode: Image.PreserveAspectFit +//// } + +//// ColorOverlay { +//// anchors.fill: icon +//// source: icon +//// color: InputStyle.fontColor +//// } +//// } + +//// onClicked: editClicked() +//// } +// } + +//// Rectangle { +//// id: titleBorder +//// width: parent.width +//// height: 1 +//// color: InputStyle.fontColor +//// anchors.bottom: title.bottom +//// } +// } + +// Item { +// id: content +// width: parent.width +// anchors.top: header.bottom +// anchors.bottom: parent.bottom + +// // we have three options what will be in the preview content: html content, image or field values + +// Text { +// visible: controller.type == AttributePreviewController.Empty +// text: qsTr("No map tip available.") +// anchors.fill: parent +// anchors.topMargin: InputStyle.panelMargin +// } + +// Text { +// visible: controller.type == AttributePreviewController.HTML +// text: controller.html +// anchors.fill: parent +// anchors.topMargin: InputStyle.panelMargin +// } + +// Image { +// visible: controller.type == AttributePreviewController.Photo +// source: controller.photo +// sourceSize: Qt.size(width, height) +// fillMode: Image.PreserveAspectFit +// autoTransform : true +// anchors.fill: parent +// anchors.topMargin: InputStyle.panelMargin +// } + +// ListView { +// visible: controller.type == AttributePreviewController.Fields +// model: controller.fieldModel +// anchors.fill: parent +// anchors.topMargin: InputStyle.panelMargin +// spacing: 2 * __dp +// interactive: false + +// delegate: Row { +// id: root +// spacing: InputStyle.panelMargin +// width: ListView.view.width + +// Text { +// id: fieldName +// text: Name +// width: root.width / 2 +// font.pixelSize: InputStyle.fontPixelSizeNormal +// color: InputStyle.fontColorBright +// elide: Text.ElideRight +// } + +// Text { +// id: fieldValue +// text: Value ? Value : "" +// width: root.width / 2 - root.spacing +// font.pixelSize: InputStyle.fontPixelSizeNormal +// color: InputStyle.fontColor +// elide: Text.ElideRight + +// } +// } +// } +// } +// } +// } +} diff --git a/app/qml/form/PreviewPanel.qml b/app/qml/form/PreviewPanel.qml deleted file mode 100644 index 1af636020..000000000 --- a/app/qml/form/PreviewPanel.qml +++ /dev/null @@ -1,213 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Qt5Compat.GraphicalEffects - -import ".." // import InputStyle singleton -import "../components" as Components -import lc 1.0 - -Item { - id: previewPanel - property real rowHeight: InputStyle.rowHeight - property AttributePreviewController controller - - property bool isReadOnly - - signal contentClicked() - signal editClicked() - signal stakeoutFeature( var feature ) - - MouseArea { - anchors.fill: parent - onClicked: { - contentClicked() - } - } - - layer.enabled: true - layer.effect: Components.Shadow {} - - Rectangle { - anchors.fill: parent - color: InputStyle.clrPanelMain - - Rectangle { - anchors.fill: parent - anchors.margins: InputStyle.panelMargin - anchors.topMargin: 0 - - Item { - id: header - width: parent.width - height: previewPanel.rowHeight - - Row { - id: title - height: rowHeight - width: parent.width - - Text { - id: titleText - height: rowHeight - width: parent.width - 2 * rowHeight - text: controller.title - font.pixelSize: InputStyle.fontPixelSizeBig - color: InputStyle.fontColor - font.bold: true - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - elide: Qt.ElideRight - } - - Button { - id: stakeoutIconContainerSpace - height: rowHeight - width: rowHeight - - background: Item { - visible: __inputUtils.isPointLayerFeature( controller.featureLayerPair ) - enabled: visible - - anchors.fill: parent - - Image { - id: stakeoutIcon - - anchors.fill: parent - anchors.margins: rowHeight/8 - anchors.rightMargin: 0 - source: InputStyle.stakeoutIcon - sourceSize.width: width - sourceSize.height: height - fillMode: Image.PreserveAspectFit - } - - ColorOverlay { - anchors.fill: stakeoutIcon - source: stakeoutIcon - color: InputStyle.fontColor - } - } - - onClicked: previewPanel.stakeoutFeature( controller.featureLayerPair ) - } - - Button { - id: iconContainer - height: rowHeight - width: rowHeight - visible: !previewPanel.isReadOnly - - background: Item { - anchors.fill: parent - - Image { - id: icon - anchors.fill: parent - anchors.margins: rowHeight/4 - anchors.rightMargin: 0 - source: InputStyle.editIcon - sourceSize.width: width - sourceSize.height: height - fillMode: Image.PreserveAspectFit - } - - ColorOverlay { - anchors.fill: icon - source: icon - color: InputStyle.fontColor - } - } - - onClicked: editClicked() - } - } - - Rectangle { - id: titleBorder - width: parent.width - height: 1 - color: InputStyle.fontColor - anchors.bottom: title.bottom - } - } - - Item { - id: content - width: parent.width - anchors.top: header.bottom - anchors.bottom: parent.bottom - - // we have three options what will be in the preview content: html content, image or field values - - Text { - visible: controller.type == AttributePreviewController.Empty - text: qsTr("No map tip available.") - anchors.fill: parent - anchors.topMargin: InputStyle.panelMargin - } - - Text { - visible: controller.type == AttributePreviewController.HTML - text: controller.html - anchors.fill: parent - anchors.topMargin: InputStyle.panelMargin - } - - Image { - visible: controller.type == AttributePreviewController.Photo - source: controller.photo - sourceSize: Qt.size(width, height) - fillMode: Image.PreserveAspectFit - autoTransform : true - anchors.fill: parent - anchors.topMargin: InputStyle.panelMargin - } - - ListView { - visible: controller.type == AttributePreviewController.Fields - model: controller.fieldModel - anchors.fill: parent - anchors.topMargin: InputStyle.panelMargin - spacing: 2 * __dp - interactive: false - - delegate: Row { - id: root - spacing: InputStyle.panelMargin - width: ListView.view.width - - Text { - id: fieldName - text: Name - width: root.width / 2 - font.pixelSize: InputStyle.fontPixelSizeNormal - color: InputStyle.fontColorBright - elide: Text.ElideRight - } - - Text { - id: fieldValue - text: Value ? Value : "" - width: root.width / 2 - root.spacing - font.pixelSize: InputStyle.fontPixelSizeNormal - color: InputStyle.fontColor - elide: Text.ElideRight - - } - } - } - } - } - } -} diff --git a/app/qml/form/MMFormTabBar.qml b/app/qml/form/components/MMFormTabBar.qml similarity index 96% rename from app/qml/form/MMFormTabBar.qml rename to app/qml/form/components/MMFormTabBar.qml index b8ed9bcb8..7ed19f2c3 100644 --- a/app/qml/form/MMFormTabBar.qml +++ b/app/qml/form/components/MMFormTabBar.qml @@ -16,7 +16,7 @@ TabBar { property alias tabButtonsModel: tabBarRepeater.model implicitHeight: 56 * __dp - implicitWidth: ApplicationWindow.window.width + implicitWidth: ApplicationWindow.window?.width ?? 0 spacing: 20 * __dp diff --git a/app/qml/form/editors/MMFormPhotoEditor.qml b/app/qml/form/editors/MMFormPhotoEditor.qml index 23e7f2489..ec97981a4 100644 --- a/app/qml/form/editors/MMFormPhotoEditor.qml +++ b/app/qml/form/editors/MMFormPhotoEditor.qml @@ -140,9 +140,7 @@ MMFormPhotoViewer { // used for both gallery and camera function onImageSelected( imagePath, index ) { - console.log( "Returned from Android activity, imagePath", imagePath, "indexes:", root._fieldIndex, index ) if ( root._fieldIndex.toString() === index.toString() ) { - console.log( "It is the same!" ) internal.imageSelected( imagePath ) } } @@ -153,7 +151,6 @@ MMFormPhotoViewer { // used for both gallery and camera function onImageSelected( imagePath, index ) { - console.log( "Returned from iOS activity, imagePath", imagePath, "indexes:", root._fieldIndex, index ) if ( root._fieldIndex.toString() === index.toString() ) { internal.imageCaptured( imagePath ) } @@ -277,15 +274,11 @@ MMFormPhotoViewer { * \param imagePath Absolute path to an image. */ function removeImage( path ) { - console.info("removeImage 1") if ( __inputUtils.fileExists( path ) ) { - console.info("removeImage 2") imageDeleteDialog.imagePath = path imageDeleteDialog.open() - console.info("removeImage 3") } else { - console.info("removeImage 4") root.editorValueChanged( "", false ) } } @@ -340,12 +333,10 @@ MMFormPhotoViewer { * \param imgPath */ function confirmImage( prefixToRelativePath, imgPath ) { - console.log( "Confirming image", imgPath, prefixToRelativePath ) if ( imgPath ) { __inputUtils.rescaleImage( imgPath, __activeProject.qgsProject ) let newImgPath = __inputUtils.getRelativePath( imgPath, prefixToRelativePath ) - console.log( "Sending changed signale", newImgPath ) root.editorValueChanged( newImgPath, newImgPath === "" || newImgPath === null ) } } diff --git a/app/qml/map/MapWrapper.qml b/app/qml/map/MapWrapper.qml index dbe588fb3..b3009e6ee 100644 --- a/app/qml/map/MapWrapper.qml +++ b/app/qml/map/MapWrapper.qml @@ -484,6 +484,8 @@ Item { bottomMargin: internal.bottomMapButtonsMargin } + visible: root.mapExtentOffset > 0 ? false : true + iconSource: __style.gpsIcon onClicked: { @@ -630,6 +632,8 @@ Item { } visible: { + if ( root.mapExtentOffset > 0 ) return false + if ( __positionKit.positionProvider && __positionKit.positionProvider.type() === "external" ) { // for external receivers we want to show gps panel and accuracy button // even when the GPS receiver is not sending position data From ae3940c795dd406d386d1c1caf44c85ff7b84360 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Mon, 5 Feb 2024 23:31:45 +0100 Subject: [PATCH 20/28] MMFormRelationEditor in the app (word cloud) --- app/inpututils.cpp | 4 ++ app/qml/CMakeLists.txt | 4 +- app/qml/FormsStackManager.qml | 2 +- ...resDrawer.qml => MMFeaturesListDrawer.qml} | 51 +++++++---------- app/qml/form/MMForm.qml | 21 ++++++- app/qml/form/MMFormWrapper.qml | 4 +- app/qml/form/MMPreviewPanel.qml | 10 +++- app/qml/form/editors/MMFormPhotoViewer.qml | 2 + .../editors/MMFormRelationEditor.qml} | 55 ++++++++++++++----- gallery/qml.qrc | 3 +- gallery/qml/pages/EditorsPage.qml | 2 +- 11 files changed, 100 insertions(+), 58 deletions(-) rename app/qml/components/{MMLinkedFeaturesDrawer.qml => MMFeaturesListDrawer.qml} (80%) rename app/qml/{inputs/MMRelationsEditor.qml => form/editors/MMFormRelationEditor.qml} (71%) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index abee1e5f2..9625ebc15 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -1073,6 +1073,10 @@ const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVa { return QUrl( path.arg( QLatin1String( "MMFormPhotoEditor" ) ) ); } + else if ( widgetName == QStringLiteral( "relation" ) ) + { + return QUrl( path.arg( QLatin1String( "MMFormRelationEditor" ) ) ); + } return QUrl( path.arg( QLatin1String( "MMFormTextEditor" ) ) ); // <<------ Mind! diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index 846ed5e54..e1388b2fc 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -47,6 +47,7 @@ set(MM_QML components/MMIconCheckBoxVertical.qml components/MMLink.qml components/MMLinkButton.qml + components/MMFeaturesListDrawer.qml components/MMListDrawer.qml components/MMListDrawerItem.qml components/MMMapButton.qml @@ -130,13 +131,14 @@ set(MM_QML form/MMFormWrapper.qml form/MMForm.qml form/MMPreviewPanel.qml - form/componentMMFormTabBar.qml + form/components/MMFormTabBar.qml form/editors/MMFormButtonEditor.qml form/editors/MMFormCalendarEditor.qml form/editors/MMFormGalleryEditor.qml form/editors/MMFormNumberEditor.qml form/editors/MMFormPhotoEditor.qml form/editors/MMFormPhotoViewer.qml + form/editors/MMFormRelationEditor.qml form/editors/MMFormScannerEditor.qml form/editors/MMFormSliderEditor.qml form/editors/MMFormSwitchEditor.qml diff --git a/app/qml/FormsStackManager.qml b/app/qml/FormsStackManager.qml index 5c4fd8aec..8469ad8ed 100644 --- a/app/qml/FormsStackManager.qml +++ b/app/qml/FormsStackManager.qml @@ -281,7 +281,7 @@ Item { Component { id: formComponent - Forms.FormWrapper { + Forms.MMFormWrapper { id: wrapper project: root.project diff --git a/app/qml/components/MMLinkedFeaturesDrawer.qml b/app/qml/components/MMFeaturesListDrawer.qml similarity index 80% rename from app/qml/components/MMLinkedFeaturesDrawer.qml rename to app/qml/components/MMFeaturesListDrawer.qml index 7f52f3c81..a602b37f9 100644 --- a/app/qml/components/MMLinkedFeaturesDrawer.qml +++ b/app/qml/components/MMFeaturesListDrawer.qml @@ -15,7 +15,7 @@ import "../inputs" Drawer { id: root - property alias title: title.text + property alias title: header.title property alias model: listView.model property bool withSearch: false @@ -28,6 +28,10 @@ Drawer { height: ApplicationWindow.window.height edge: Qt.BottomEdge + background: Rectangle { + color: __style.lightGreenColor + } + Rectangle { color: __style.lightGreenColor anchors.top: parent.top @@ -37,8 +41,19 @@ Drawer { anchors.topMargin: -height } + MMHeader { + id: header + onBackClicked: root.close() + } + Rectangle { - anchors.fill: parent + anchors { + top: header.bottom + left: parent.left + right: parent.right + bottom: parent.bottom + } + color: __style.lightGreenColor Column { @@ -50,33 +65,7 @@ Drawer { rightPadding: root.padding bottomPadding: root.padding - Item { width: 1; height: 1 } - - Row { - width: parent.width - 2 * root.padding - anchors.horizontalCenter: parent.horizontalCenter - - MMBackButton { - id: closeButton - onClicked: root.close() - } - - Text { - id: title - - anchors.verticalCenter: parent.verticalCenter - font: __style.t2 - width: parent.width - closeButton.width * 2 - color: __style.forestColor - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - Item { width: closeButton.width; height: 1 } - } - - MMSearchEditor { + MMSearchInput { id: searchBar width: parent.width - 2 * root.padding @@ -140,8 +129,7 @@ Drawer { MouseArea { anchors.fill: parent onClicked: { - root.featureClicked(model.FeaturePair) - close() + root.featureClicked( model.FeaturePair ) } } } @@ -160,7 +148,6 @@ Drawer { onClicked: { root.createLinkedFeature() - close() } } } diff --git a/app/qml/form/MMForm.qml b/app/qml/form/MMForm.qml index 40a51c132..0c446ab1d 100644 --- a/app/qml/form/MMForm.qml +++ b/app/qml/form/MMForm.qml @@ -16,6 +16,7 @@ import QtQuick.Dialogs import lc 1.0 import "../components" +import "./components" import Input 0.1 as Input Page { @@ -328,13 +329,29 @@ Page { model.RememberValue = state } - // TODO: support for relations + function onCreateLinkedFeature( parentFeature, relation ) { + let parentHasValidId = __inputUtils.isFeatureIdValid( parentFeature.feature.id ) + + if ( parentHasValidId ) { + // parent feature in this case already have valid id, so we can open new form + root.createLinkedFeature( root.controller, relation ) + } + else { + // parent feature does not have a valid ID yet, we need to save it and acquire ID + root.controller.acquireId() + root.createLinkedFeature( root.controller, relation ) + } + } + + function onOpenLinkedFeature( linkedFeature ) { + root.openLinkedFeature( linkedFeature ) + } } Connections { target: root.controller - // Important for relation form editors + // Important for relation form editors // <--- TODO: remove me if all works, unused function onFeatureLayerPairChanged() { if ( formEditorsLoader.item && formEditorsLoader.item.featureLayerPairChanged ) { diff --git a/app/qml/form/MMFormWrapper.qml b/app/qml/form/MMFormWrapper.qml index c5e67f197..5dfadc0fd 100644 --- a/app/qml/form/MMFormWrapper.qml +++ b/app/qml/form/MMFormWrapper.qml @@ -146,7 +146,7 @@ Item { statesManager.state = "closed" } - PreviewPanel { + MMPreviewPanel { id: previewPanel layerIsReadOnly: root.layerIsReadOnly @@ -159,7 +159,7 @@ Item { root.stakeoutFeature( feature ) } -// onContentClicked: root.panelState = "form" + onContentClicked: root.panelState = "form" onEditClicked: { root.panelState = "form" diff --git a/app/qml/form/MMPreviewPanel.qml b/app/qml/form/MMPreviewPanel.qml index 3d0a9a1ae..5cdcf69e9 100644 --- a/app/qml/form/MMPreviewPanel.qml +++ b/app/qml/form/MMPreviewPanel.qml @@ -22,7 +22,7 @@ Item { property bool layerIsReadOnly: false property AttributePreviewController controller -// signal contentClicked() + signal contentClicked() signal editClicked() signal stakeoutClicked( var feature ) // TODO: remove the feature @@ -85,7 +85,13 @@ Item { } } - + MouseArea { + anchors.fill: parent + onClicked: function( mouse ) { + mouse.accepted = true + root.contentClicked() + } + } // property real rowHeight: InputStyle.rowHeight diff --git a/app/qml/form/editors/MMFormPhotoViewer.qml b/app/qml/form/editors/MMFormPhotoViewer.qml index 735c1835d..6e533f0e7 100644 --- a/app/qml/form/editors/MMFormPhotoViewer.qml +++ b/app/qml/form/editors/MMFormPhotoViewer.qml @@ -67,6 +67,8 @@ MMBaseInput { height: root.contentItemHeight photoUrl: root.photoUrl + fillMode: Image.PreserveAspectCrop + MouseArea { anchors.fill: parent onClicked: root.contentClicked() diff --git a/app/qml/inputs/MMRelationsEditor.qml b/app/qml/form/editors/MMFormRelationEditor.qml similarity index 71% rename from app/qml/inputs/MMRelationsEditor.qml rename to app/qml/form/editors/MMFormRelationEditor.qml index 4e688bb37..054dc1b21 100644 --- a/app/qml/inputs/MMRelationsEditor.qml +++ b/app/qml/form/editors/MMFormRelationEditor.qml @@ -10,18 +10,30 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../components" -MMAbstractEditor { +import lc 1.0 + +import "../../components" +import "../../inputs" + +/* + * Relation editor (text mode ~~ bubbles/word cloud) for QGIS Attribute Form + * Requires various global properties set to function, see featureform Loader section. + * These properties are injected here via 'fieldXYZ' properties and captured with underscore `_`. + * + * Should be used only within feature form. + */ + +MMBaseInput { id: root - property var parentValue: parent.value ?? "" - property bool parentValueIsNull: parent.valueIsNull ?? false - property bool isReadOnly: parent.readOnly ?? false + property var _fieldAssociatedRelation: parent.fieldAssociatedRelation + property var _fieldFeatureLayerPair: parent.fieldFeatureLayerPair + property var _fieldActiveProject: parent.fieldActiveProject + property string _fieldTitle: parent.fieldTitle - required property ListModel featuresModel + property ListModel featuresModel // <---- what to do here? - signal editorValueChanged( var newValue, var isNull ) signal openLinkedFeature( var linkedFeature ) signal createLinkedFeature( var parentFeature, var relation ) @@ -59,7 +71,7 @@ MMAbstractEditor { MouseArea { anchors.fill: parent - onClicked: root.createLinkedFeature( root.parent.featurePair, root.parent.associatedRelation ) + onClicked: root.createLinkedFeature( root._fieldFeatureLayerPair, root._fieldAssociatedRelation ) } } @@ -68,7 +80,20 @@ MMAbstractEditor { property var invisibleIds: 0 - model: root.featuresModel + model: RelationFeaturesModel { + id: rmodel + + relation: root._fieldAssociatedRelation + parentFeatureLayerPair: root._fieldFeatureLayerPair + homePath: root._fieldActiveProject.homePath + + onModelReset: { + // Repeater does not necesarry clear delegates immediately if they are invisible, + // we need to do hard reload in this case so that recalculateVisibleItems() is triggered + + root.recalculate() + } + } delegate: Rectangle { width: text.contentWidth + 24 * __dp @@ -137,7 +162,7 @@ MMAbstractEditor { function recalculate() { repeater.invisibleIds = 0 repeater.model = null - repeater.model = root.featuresModel + repeater.model = rmodel } Loader { @@ -151,18 +176,18 @@ MMAbstractEditor { Component { id: listComponent - MMLinkedFeaturesDrawer { + MMFeaturesListDrawer { focus: true - model: root.featuresModel - title: qsTr("Linked features") - withSearch: true + model: rmodel + title: root._fieldTitle + withSearch: false Component.onCompleted: open() onClosed: listLoader.active = false onFeatureClicked: function(selectedFeatures) { root.openLinkedFeature( selectedFeatures ) } - onCreateLinkedFeature: root.createLinkedFeature( root.parent.featurePair, root.parent.associatedRelation ) + onCreateLinkedFeature: root.createLinkedFeature( root._fieldFeatureLayerPair, root._fieldAssociatedRelation ) } } diff --git a/gallery/qml.qrc b/gallery/qml.qrc index 4de83f0d8..b8a17e9a5 100644 --- a/gallery/qml.qrc +++ b/gallery/qml.qrc @@ -73,8 +73,6 @@ ../app/qml/inputs/MMPasswordInput.qml ../app/qml/inputs/MMTextInput.qml ../app/qml/inputs/MMSearchInput.qml - ../app/qml/inputs/MMRelationsEditor.qml - ../app/qml/inputs/MMPhotoEditor.qml ../app/qml/form/MMFormTabBar.qml ../app/qml/form/editors/MMFormButtonEditor.qml @@ -82,6 +80,7 @@ ../app/qml/form/editors/MMFormGalleryEditor.qml ../app/qml/form/editors/MMFormNumberEditor.qml ../app/qml/form/editors/MMFormPhotoViewer.qml + ../app/qml/form/editors/MMFormRelationEditor.qml ../app/qml/form/editors/MMFormScannerEditor.qml ../app/qml/form/editors/MMFormSliderEditor.qml ../app/qml/form/editors/MMFormSwitchEditor.qml diff --git a/gallery/qml/pages/EditorsPage.qml b/gallery/qml/pages/EditorsPage.qml index ace48a19d..516abfaeb 100644 --- a/gallery/qml/pages/EditorsPage.qml +++ b/gallery/qml/pages/EditorsPage.qml @@ -42,7 +42,7 @@ ScrollView { checked: true } - MMRelationsEditor { + MMFormRelationEditor { title: "MMTextAreaEditor" enabled: checkbox.checked width: parent.width From 9bb1aae613de94afbbdef509ea2bf6cf0492b525 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Tue, 6 Feb 2024 00:12:38 +0100 Subject: [PATCH 21/28] MMFormGalleryRow in the app --- app/inpututils.cpp | 46 ++++++++++--- app/inpututils.h | 2 +- app/qml/form/MMForm.qml | 2 +- app/qml/form/editors/MMFormGalleryEditor.qml | 68 +++++++++++++++---- app/qml/form/editors/MMFormRelationEditor.qml | 4 ++ app/relationfeaturesmodel.cpp | 16 ----- app/relationfeaturesmodel.h | 11 --- 7 files changed, 95 insertions(+), 54 deletions(-) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index 9625ebc15..caa9e88ba 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -1020,7 +1020,7 @@ const QUrl InputUtils::getThemeIcon( const QString &name ) return QUrl( path ); } -const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVariantMap &config, const QgsField &field ) +const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVariantMap &config, const QgsField &field, const QgsRelation &relation ) { QString widgetName = widgetNameIn.toLower(); @@ -1075,23 +1075,49 @@ const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVa } else if ( widgetName == QStringLiteral( "relation" ) ) { - return QUrl( path.arg( QLatin1String( "MMFormRelationEditor" ) ) ); + // check if we should use gallery or word tags + bool useGallery = false; + + QgsVectorLayer *layer = relation.referencingLayer(); + if ( layer && layer->isValid() ) + { + QgsFields fields = layer->fields(); + for ( int i = 0; i < fields.size(); i++ ) + { + // Lets try by widget type + QgsEditorWidgetSetup setup = layer->editorWidgetSetup( i ); + if ( setup.type() == QStringLiteral( "ExternalResource" ) ) + { + useGallery = true; + break; + } + } + } + + // Mind this hack - fields with `no-gallery-use` won't use gallery, but normal word tags instead + if ( field.name().contains( "no-gallery-use", Qt::CaseInsensitive ) || field.alias().contains( "no-gallery-use", Qt::CaseInsensitive ) ) + { + useGallery = false; + } + + if ( useGallery ) + { + return QUrl( path.arg( QLatin1String( "MMFormGalleryEditor" ) ) ); + } + else + { + return QUrl( path.arg( QLatin1String( "MMFormRelationEditor" ) ) ); + } } return QUrl( path.arg( QLatin1String( "MMFormTextEditor" ) ) ); // <<------ Mind! + // Missing editors: QStringList supportedWidgets = { QStringLiteral( "richtext" ), - QStringLiteral( "textedit" ), - QStringLiteral( "valuemap" ), - QStringLiteral( "valuerelation" ), - QStringLiteral( "checkbox" ), - QStringLiteral( "externalresource" ), - QStringLiteral( "datetime" ), - QStringLiteral( "range" ), - QStringLiteral( "relation" ), QStringLiteral( "spacer" ), QStringLiteral( "relationreference" ) }; + if ( supportedWidgets.contains( widgetName ) ) { return QUrl( path.arg( widgetName ) ); diff --git a/app/inpututils.h b/app/inpututils.h index f36f4de7c..57ab7ba15 100644 --- a/app/inpututils.h +++ b/app/inpututils.h @@ -346,7 +346,7 @@ class InputUtils: public QObject * \param config map coming from QGIS describing this field * \param field qgsfield instance of this field */ - Q_INVOKABLE static const QUrl getFormEditorType( const QString &widgetNameIn, const QVariantMap &config = QVariantMap(), const QgsField &field = QgsField() ); + Q_INVOKABLE static const QUrl getFormEditorType( const QString &widgetNameIn, const QVariantMap &config = QVariantMap(), const QgsField &field = QgsField(), const QgsRelation &relation = QgsRelation() ); /** * \copydoc QgsCoordinateFormatter::format() diff --git a/app/qml/form/MMForm.qml b/app/qml/form/MMForm.qml index 0c446ab1d..80832f4c8 100644 --- a/app/qml/form/MMForm.qml +++ b/app/qml/form/MMForm.qml @@ -310,7 +310,7 @@ Page { source: { if ( model.EditorWidget !== undefined ) { - return __inputUtils.getFormEditorType( model.EditorWidget, model.EditorWidgetConfig, model.Field ) + return __inputUtils.getFormEditorType( model.EditorWidget, model.EditorWidgetConfig, model.Field, model.Relation ) } return '' diff --git a/app/qml/form/editors/MMFormGalleryEditor.qml b/app/qml/form/editors/MMFormGalleryEditor.qml index 7c6c6e6de..43e33dc6c 100644 --- a/app/qml/form/editors/MMFormGalleryEditor.qml +++ b/app/qml/form/editors/MMFormGalleryEditor.qml @@ -10,6 +10,7 @@ import QtQuick import QtQuick.Controls +import lc 1.0 import "../../components" Item { @@ -18,17 +19,27 @@ Item { width: parent.width height: column.height - required property var model - property string title + property var _fieldAssociatedRelation: parent.fieldAssociatedRelation + property var _fieldFeatureLayerPair: parent.fieldFeatureLayerPair + property var _fieldActiveProject: parent.fieldActiveProject + + property string _fieldTitle: parent.fieldTitle + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle + + property var model + property string title: _fieldShouldShowTitle ? _fieldTitle : "" property string warningMsg property string errorMsg property int maxVisiblePhotos: -1 // -1 for showing all photos - property bool showAddImage: false + property bool showAddImage: true signal showAll() signal clicked( var path ) signal addImage() + signal openLinkedFeature( var linkedFeature ) + signal createLinkedFeature( var parentFeature, var relation ) + Column { id: column @@ -54,6 +65,8 @@ Item { anchors.right: parent.right + visible: false // for now + text: qsTr("Show all") font: __style.t4 color: __style.forestColor @@ -73,19 +86,39 @@ Item { spacing: root.maxVisiblePhotos !== 0 ? 20 * __dp : 0 orientation: ListView.Horizontal - model: { - if(root.maxVisiblePhotos >= 0 && root.model.length > root.maxVisiblePhotos) { - return root.model.slice(0, root.maxVisiblePhotos) - } - return root.model +// model: { +// if(root.maxVisiblePhotos >= 0 && root.model.length > root.maxVisiblePhotos) { +// return root.model.slice(0, root.maxVisiblePhotos) +// } +// return root.model +// } + + model: RelationFeaturesModel { + id: rmodel + + relation: root._fieldAssociatedRelation + parentFeatureLayerPair: root._fieldFeatureLayerPair + homePath: root._fieldActiveProject.homePath } delegate: MMPhoto { width: rowView.height - photoUrl: model.modelData + fillMode: Image.PreserveAspectCrop - onClicked: function(path) { root.clicked(path) } + photoUrl: { + let absolutePath = model.PhotoPath + + if ( absolutePath !== '' && __inputUtils.fileExists( absolutePath ) ) { + return "file://" + absolutePath + } + return '' + } + + onClicked: function(path) { + root.clicked(path) + root.openLinkedFeature( model.FeaturePair ) + } } header: Row { @@ -107,7 +140,10 @@ Item { MouseArea { anchors.fill: parent - onClicked: root.addImage() + onClicked: { + root.addImage() + root.createLinkedFeature( root._fieldFeatureLayerPair, root._fieldAssociatedRelation ) + } } } @@ -120,10 +156,12 @@ Item { footer: MMMorePhoto { width: visible ? rowView.height + rowView.spacing: 0 - hiddenPhotoCount: root.model.length - root.maxVisiblePhotos - visible: root.maxVisiblePhotos >= 0 && root.model.length > root.maxVisiblePhotos - photoUrl: visible ? model[root.maxVisiblePhotos] : "" - space: visible ? rowView.spacing : 0 +// hiddenPhotoCount: root.model.length - root.maxVisiblePhotos +// visible: root.maxVisiblePhotos >= 0 && root.model.length > root.maxVisiblePhotos +// photoUrl: visible ? model[root.maxVisiblePhotos] : "" +// space: visible ? rowView.spacing : 0 + + visible: false onClicked: root.showAll() } diff --git a/app/qml/form/editors/MMFormRelationEditor.qml b/app/qml/form/editors/MMFormRelationEditor.qml index 054dc1b21..6e145be1c 100644 --- a/app/qml/form/editors/MMFormRelationEditor.qml +++ b/app/qml/form/editors/MMFormRelationEditor.qml @@ -30,7 +30,9 @@ MMBaseInput { property var _fieldAssociatedRelation: parent.fieldAssociatedRelation property var _fieldFeatureLayerPair: parent.fieldFeatureLayerPair property var _fieldActiveProject: parent.fieldActiveProject + property string _fieldTitle: parent.fieldTitle + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle property ListModel featuresModel // <---- what to do here? @@ -42,6 +44,8 @@ MMBaseInput { Component.onCompleted: root.recalculate() onWidthChanged: root.recalculate() + title: _fieldShouldShowTitle ? _fieldTitle : "" + content: Rectangle { width: root.width - 2 * root.spacing height: root.contentItemHeight diff --git a/app/relationfeaturesmodel.cpp b/app/relationfeaturesmodel.cpp index 16365aabe..ac53c24bb 100644 --- a/app/relationfeaturesmodel.cpp +++ b/app/relationfeaturesmodel.cpp @@ -49,8 +49,6 @@ void RelationFeaturesModel::setup() if ( !mRelation.isValid() || !mParentFeatureLayerPair.isValid() ) return; - setIsTextType( photoFieldIndex( mRelation.referencingLayer() ) == -1 ); - QObject::connect( mRelation.referencingLayer(), &QgsVectorLayer::afterCommitChanges, this, &RelationFeaturesModel::populate ); FeaturesModel::setLayer( mRelation.referencingLayer() ); @@ -144,20 +142,6 @@ int RelationFeaturesModel::photoFieldIndex( QgsVectorLayer *layer ) const return -1; } -bool RelationFeaturesModel::isTextType() const -{ - return mIsTextType; -} - -void RelationFeaturesModel::setIsTextType( bool isTextType ) -{ - if ( isTextType != mIsTextType ) - { - mIsTextType = isTextType; - emit isTextTypeChanged(); - } -} - QString RelationFeaturesModel::homePath() const { return mHomePath; diff --git a/app/relationfeaturesmodel.h b/app/relationfeaturesmodel.h index 95c4576f5..e8f5059ff 100644 --- a/app/relationfeaturesmodel.h +++ b/app/relationfeaturesmodel.h @@ -34,12 +34,6 @@ class RelationFeaturesModel : public FeaturesModel //! parent feature layer pair represents a feature from parent relation layer for which we gather related child features Q_PROPERTY( FeatureLayerPair parentFeatureLayerPair READ parentFeatureLayerPair WRITE setParentFeatureLayerPair NOTIFY parentFeatureLayerPairChanged ) - /** - * Flag to distinguish what data type suppose to be displayed. By default text data type is expected, otherwise image. - * Property is set to False only if there is any photo field (see more: RelationFeaturesModel::photoFieldIndex) - */ - Q_PROPERTY( bool isTextType READ isTextType WRITE setIsTextType NOTIFY isTextTypeChanged ) - public: enum relationModelRoles @@ -66,14 +60,10 @@ class RelationFeaturesModel : public FeaturesModel QString homePath() const; void setHomePath( const QString &homePath ); - bool isTextType() const; - void setIsTextType( bool isTextType ); - signals: void parentFeatureLayerPairChanged( FeatureLayerPair pair ); void relationChanged( QgsRelation relation ); void homePathChanged(); - void isTextTypeChanged(); private: QVariant relationPhotoPath( const FeatureLayerPair &featurePair ) const; @@ -88,7 +78,6 @@ class RelationFeaturesModel : public FeaturesModel QgsRelation mRelation; // associated relation FeatureLayerPair mParentFeatureLayerPair; // parent feature (with relation widget in form) QString mHomePath; - bool mIsTextType = true; }; #endif // RELATIONFEATURESMODEL_H From ce73c39cd3faaf899f40156360c89c3d42729bb0 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Tue, 6 Feb 2024 13:42:54 +0100 Subject: [PATCH 22/28] Fix test and formatting --- app/inpututils.cpp | 20 +++++--------------- app/qml/CMakeLists.txt | 18 ------------------ app/test/testutilsfunctions.cpp | 4 ++-- 3 files changed, 7 insertions(+), 35 deletions(-) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index caa9e88ba..2a2ddc399 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -1110,22 +1110,12 @@ const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVa } } - return QUrl( path.arg( QLatin1String( "MMFormTextEditor" ) ) ); // <<------ Mind! + // TODO == Missing editors: + // - QStringLiteral( "richtext" ) -> text and HTML form widget + // - QStringLiteral( "spacer" ) + // - QStringLiteral( "relationreference" ) - // Missing editors: - QStringList supportedWidgets = { QStringLiteral( "richtext" ), - QStringLiteral( "spacer" ), - QStringLiteral( "relationreference" ) - }; - - if ( supportedWidgets.contains( widgetName ) ) - { - return QUrl( path.arg( widgetName ) ); - } - else - { - return QUrl( path.arg( QLatin1String( "textedit" ) ) ); - } + return QUrl( path.arg( QLatin1String( "MMFormTextEditor" ) ) ); } const QgsEditorWidgetSetup InputUtils::getEditorWidgetSetup( const QgsField &field ) diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index e1388b2fc..85bf94da7 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -70,7 +70,6 @@ set(MM_QML components/MMToolbarLongButton.qml components/MMToolbarMenuButton.qml components/MMWarningBubble.qml - components/MMCalendarDrawer.qml components/MMTumbler.qml components/calendar/MMDateTimePicker.qml @@ -79,7 +78,6 @@ set(MM_QML components/calendar/MMDayOfWeekRow.qml components/calendar/MMDateTumbler.qml components/calendar/MMAmPmSwitch.qml - onboarding/MMAcceptInvitation.qml onboarding/MMCreateWorkspace.qml onboarding/MMHowYouFoundUs.qml @@ -113,21 +111,11 @@ set(MM_QML editor/inputvaluerelationcombobox.qml editor/inputvaluerelationpage.qml editor/inputspacer.qml - - # - # Inputs - # - inputs/MMBaseInput.qml inputs/MMPasswordInput.qml inputs/MMTextInput.qml inputs/MMSearchInput.qml inputs/MMDropdownInput.qml - - # - # Forms - # - form/MMFormWrapper.qml form/MMForm.qml form/MMPreviewPanel.qml @@ -146,12 +134,6 @@ set(MM_QML form/editors/MMFormTextEditor.qml form/editors/MMFormValueMapEditor.qml form/editors/MMFormValueRelationEditor.qml - # we are missing the following form editors: - # - Spacer - # - Text / HTML widget - # - Relation reference - # - Relations - layers/FeaturesListPageV2.qml layers/LayerDetail.qml layers/LayersListPageV2.qml diff --git a/app/test/testutilsfunctions.cpp b/app/test/testutilsfunctions.cpp index 06981af71..71b56682e 100644 --- a/app/test/testutilsfunctions.cpp +++ b/app/test/testutilsfunctions.cpp @@ -222,10 +222,10 @@ void TestUtilsFunctions::fileExists() void TestUtilsFunctions::loadQmlComponent() { QUrl dummy = mUtils->getFormEditorType( "dummy" ); - QCOMPARE( dummy.path(), QString( "../editor/inputtextedit.qml" ) ); + QCOMPARE( dummy.path(), QString( "../editor/MMFormTextEditor.qml" ) ); QUrl valuemap = mUtils->getFormEditorType( "valuemap" ); - QCOMPARE( valuemap.path(), QString( "../editor/inputvaluemap.qml" ) ); + QCOMPARE( valuemap.path(), QString( "../editor/MMFormValueMapEditor.qml" ) ); } void TestUtilsFunctions::getRelativePath() From 87fba44ad9c4e8a4d5363a7ef57ba9a624de8c8f Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Tue, 6 Feb 2024 17:49:42 +0100 Subject: [PATCH 23/28] Update visual of MMPreviewPanel --- app/qml/CMakeLists.txt | 1 + app/qml/form/MMPreviewPanel.qml | 390 +++++++----------- .../components/MMPreviewPanelActionButton.qml | 65 +++ app/qml/main.qml | 2 +- gallery/qml.qrc | 4 +- 5 files changed, 222 insertions(+), 240 deletions(-) create mode 100644 app/qml/form/components/MMPreviewPanelActionButton.qml diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index 85bf94da7..99d91ed73 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -120,6 +120,7 @@ set(MM_QML form/MMForm.qml form/MMPreviewPanel.qml form/components/MMFormTabBar.qml + form/components/MMPreviewPanelActionButton.qml form/editors/MMFormButtonEditor.qml form/editors/MMFormCalendarEditor.qml form/editors/MMFormGalleryEditor.qml diff --git a/app/qml/form/MMPreviewPanel.qml b/app/qml/form/MMPreviewPanel.qml index 5cdcf69e9..b868a74fe 100644 --- a/app/qml/form/MMPreviewPanel.qml +++ b/app/qml/form/MMPreviewPanel.qml @@ -10,10 +10,9 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls -//import Qt5Compat.GraphicalEffects -//import ".." // import InputStyle singleton import "../components" as Components +import "./components" as FormComponents import lc 1.0 Item { @@ -24,268 +23,185 @@ Item { signal contentClicked() signal editClicked() - signal stakeoutClicked( var feature ) // TODO: remove the feature + signal stakeoutClicked( var feature ) - ColumnLayout { - anchors { - fill: parent - margins: __style.pageMargins + MouseArea { + anchors.fill: parent + onClicked: function( mouse ) { + mouse.accepted = true + root.contentClicked() } + } - spacing: 20 * __dp + // TODO: this needs to be revisited, the layout does not work very well - Item { - id: photoContainer + Item { + x: parent.width / 2 - width / 2 + width: parent.width > __style.maxPageWidth ? __style.maxPageWidth : parent.width + height: parent.height - Layout.fillWidth: true - Layout.preferredHeight: 160 * __dp + ColumnLayout { + id: layout - visible: root.controller.type === AttributePreviewController.Photo + anchors { + fill: parent + leftMargin: __style.pageMargins + topMargin: __style.pageMargins + rightMargin: __style.pageMargins + bottomMargin: __style.pageMargins + } - Components.MMPhoto { - width: parent.width - height: parent.height + spacing: 20 * __dp - photoUrl: root.controller.photo + Item { + id: photoContainer - fillMode: Image.PreserveAspectCrop + Layout.fillWidth: true + Layout.preferredHeight: 160 * __dp + + visible: root.controller.type === AttributePreviewController.Photo + + Components.MMPhoto { + width: parent.width + height: parent.height + + photoUrl: root.controller.photo + + fillMode: Image.PreserveAspectCrop + } } - } - Text { - Layout.fillWidth: true - Layout.preferredHeight: paintedHeight + Text { + Layout.fillWidth: true + Layout.preferredHeight: paintedHeight - text: root.controller.title + text: root.controller.title - font: __style.t1 - color: __style.forestColor + font: __style.t1 + color: __style.forestColor - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter - maximumLineCount: 2 - elide: Text.ElideRight - wrapMode: Text.WordWrap - } + maximumLineCount: 2 + elide: Text.ElideRight + wrapMode: Text.WordWrap + } - // TODO: HTML type + Text { + Layout.fillWidth: true + Layout.fillHeight: true - Item { - id: buttonGroup + text: controller.html + visible: root.controller.type === AttributePreviewController.HTML - Layout.fillWidth: true - Layout.preferredHeight: 40 * __dp + clip: true + } - } + Text { + Layout.fillWidth: true + Layout.fillHeight: true - Item { - Layout.fillHeight: true - Layout.fillWidth: true - } - } + text: qsTr("No map tip available.") + visible: root.controller.type === AttributePreviewController.Empty - MouseArea { - anchors.fill: parent - onClicked: function( mouse ) { - mouse.accepted = true - root.contentClicked() - } - } + font: __style.p6 + color: __style.nightColor + } + Item { + id: fieldsContainer -// property real rowHeight: InputStyle.rowHeight + Layout.fillWidth: true + Layout.fillHeight: true + visible: root.controller.type === AttributePreviewController.Fields + ListView { + anchors.fill: parent -// MouseArea { -// anchors.fill: parent -// onClicked: { -// contentClicked() -// } -// } + spacing: 20 * __dp + interactive: false -// layer.enabled: true -// layer.effect: Components.Shadow {} + model: root.controller.fieldModel -// Rectangle { -// anchors.fill: parent -// color: InputStyle.clrPanelMain + delegate: Item { + width: ListView.view.width + height: childrenRect.height -// Rectangle { -// anchors.fill: parent -// anchors.margins: InputStyle.panelMargin -// anchors.topMargin: 0 - -// Item { -// id: header -// width: parent.width -// height: previewPanel.rowHeight - -// Row { -// id: title -// height: rowHeight -// width: parent.width - -// Text { -// id: titleText - -//// height: rowHeight -// width: parent.width - -// text: controller.title - -// wrapMode: Text.WordWrap -// maximumLineCount: 2 -// elide: Text.ElideRight - -// font: __style.t1 - -// color: __style.forestColor - -// horizontalAlignment: Text.AlignLeft -// verticalAlignment: Text.AlignVCenter -// } - -//// Button { -//// id: stakeoutIconContainerSpace -//// height: rowHeight -//// width: rowHeight - -//// background: Item { -//// visible: __inputUtils.isPointLayerFeature( controller.featureLayerPair ) -//// enabled: visible - -//// anchors.fill: parent - -//// Image { -//// id: stakeoutIcon - -//// anchors.fill: parent -//// anchors.margins: rowHeight/8 -//// anchors.rightMargin: 0 -//// source: InputStyle.stakeoutIcon -//// sourceSize.width: width -//// sourceSize.height: height -//// fillMode: Image.PreserveAspectFit -//// } - -//// ColorOverlay { -//// anchors.fill: stakeoutIcon -//// source: stakeoutIcon -//// color: InputStyle.fontColor -//// } -//// } - -//// onClicked: previewPanel.stakeoutFeature( controller.featureLayerPair ) -//// } - -//// Button { -//// id: iconContainer -//// height: rowHeight -//// width: rowHeight -//// visible: !previewPanel.isReadOnly - -//// background: Item { -//// anchors.fill: parent - -//// Image { -//// id: icon -//// anchors.fill: parent -//// anchors.margins: rowHeight/4 -//// anchors.rightMargin: 0 -//// source: InputStyle.editIcon -//// sourceSize.width: width -//// sourceSize.height: height -//// fillMode: Image.PreserveAspectFit -//// } - -//// ColorOverlay { -//// anchors.fill: icon -//// source: icon -//// color: InputStyle.fontColor -//// } -//// } - -//// onClicked: editClicked() -//// } -// } - -//// Rectangle { -//// id: titleBorder -//// width: parent.width -//// height: 1 -//// color: InputStyle.fontColor -//// anchors.bottom: title.bottom -//// } -// } - -// Item { -// id: content -// width: parent.width -// anchors.top: header.bottom -// anchors.bottom: parent.bottom - -// // we have three options what will be in the preview content: html content, image or field values - -// Text { -// visible: controller.type == AttributePreviewController.Empty -// text: qsTr("No map tip available.") -// anchors.fill: parent -// anchors.topMargin: InputStyle.panelMargin -// } - -// Text { -// visible: controller.type == AttributePreviewController.HTML -// text: controller.html -// anchors.fill: parent -// anchors.topMargin: InputStyle.panelMargin -// } - -// Image { -// visible: controller.type == AttributePreviewController.Photo -// source: controller.photo -// sourceSize: Qt.size(width, height) -// fillMode: Image.PreserveAspectFit -// autoTransform : true -// anchors.fill: parent -// anchors.topMargin: InputStyle.panelMargin -// } - -// ListView { -// visible: controller.type == AttributePreviewController.Fields -// model: controller.fieldModel -// anchors.fill: parent -// anchors.topMargin: InputStyle.panelMargin -// spacing: 2 * __dp -// interactive: false - -// delegate: Row { -// id: root -// spacing: InputStyle.panelMargin -// width: ListView.view.width - -// Text { -// id: fieldName -// text: Name -// width: root.width / 2 -// font.pixelSize: InputStyle.fontPixelSizeNormal -// color: InputStyle.fontColorBright -// elide: Text.ElideRight -// } - -// Text { -// id: fieldValue -// text: Value ? Value : "" -// width: root.width / 2 - root.spacing -// font.pixelSize: InputStyle.fontPixelSizeNormal -// color: InputStyle.fontColor -// elide: Text.ElideRight - -// } -// } -// } -// } -// } -// } + Column { + width: parent.width + spacing: 0 + + Text { + width: parent.width + + text: model.Name + font: __style.p6 + color: __style.nightColor + + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + Text { + width: parent.width + + text: model.Value + font: __style.p5 + color: __style.nightColor + + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + } + } + } + + Item { + // Vertical spacer to keep action buttons on the bottom with photo type + Layout.fillWidth: true + Layout.fillHeight: true + + visible: root.controller.type === AttributePreviewController.Photo + } + + ScrollView { + + Layout.fillWidth: true + Layout.preferredHeight: 40 * __dp + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AlwaysOff + + Row { + height: parent.height + spacing: 12 * __dp + + FormComponents.MMPreviewPanelActionButton { + height: parent.height + + visible: !root.layerIsReadOnly + + buttonText: qsTr( "Edit" ) + iconSource: __style.editIcon + + onClicked: root.editClicked() + } + + FormComponents.MMPreviewPanelActionButton { + height: parent.height + + visible: __inputUtils.isPointLayerFeature( controller.featureLayerPair ) + + buttonText: qsTr( "Stake out" ) + iconSource: __style.positionTrackingIcon // TODO: change to stakeout icon + + onClicked: root.editClicked() + } + } + } + } + } } diff --git a/app/qml/form/components/MMPreviewPanelActionButton.qml b/app/qml/form/components/MMPreviewPanelActionButton.qml new file mode 100644 index 000000000..c7c80a143 --- /dev/null +++ b/app/qml/form/components/MMPreviewPanelActionButton.qml @@ -0,0 +1,65 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick +import "../../components" + +Rectangle { + id: root + + property alias iconSource: icon.source + property alias buttonText: txt.text + + signal clicked() + + width: childrenRect.width + + color: __style.lightGreenColor + radius: 30 * __dp + + Row { + height: parent.height + + leftPadding: 20 * __dp + rightPadding: 20 * __dp + + spacing: 10 * __dp + + MMIcon { + id: icon + + y: parent.height / 2 - height / 2 + width: 24 * __dp + height: 24 * __dp + + color: __style.forestColor + + useCustomSize: true + } + + Text { + id: txt + + height: parent.height + + font: __style.t3 + color: __style.forestColor + + verticalAlignment: Text.AlignVCenter + } + } + + MouseArea { + anchors.fill: parent + onClicked: function( mouse ) { + mouse.accepted = true + root.clicked() + } + } +} diff --git a/app/qml/main.qml b/app/qml/main.qml index a476d7c95..fec8cf6a3 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -664,7 +664,7 @@ ApplicationWindow { height: window.height width: window.width - previewHeight: window.height / 3 + previewHeight: window.height * 0.4 project: __activeProject.qgsProject diff --git a/gallery/qml.qrc b/gallery/qml.qrc index b8a17e9a5..478c18e73 100644 --- a/gallery/qml.qrc +++ b/gallery/qml.qrc @@ -66,7 +66,7 @@ ../app/qml/components/MMDropdownDrawer.qml ../app/qml/components/MMMapBlurLabel.qml ../app/qml/components/MMCodeScanner.qml - ../app/qml/components/MMLinkedFeaturesDrawer.qml + ../app/qml/components/MMFeaturesListDrawer.qml ../app/qml/inputs/MMBaseInput.qml ../app/qml/inputs/MMDropdownInput.qml @@ -74,7 +74,7 @@ ../app/qml/inputs/MMTextInput.qml ../app/qml/inputs/MMSearchInput.qml - ../app/qml/form/MMFormTabBar.qml + ../app/qml/form/components/MMFormTabBar.qml ../app/qml/form/editors/MMFormButtonEditor.qml ../app/qml/form/editors/MMFormCalendarEditor.qml ../app/qml/form/editors/MMFormGalleryEditor.qml From 40541d0705f9b25ecff52bbbad878b965d1e5056 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Tue, 6 Feb 2024 17:49:49 +0100 Subject: [PATCH 24/28] Rename nogallery hack --- app/inpututils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index 2a2ddc399..cb233c403 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -1095,7 +1095,7 @@ const QUrl InputUtils::getFormEditorType( const QString &widgetNameIn, const QVa } // Mind this hack - fields with `no-gallery-use` won't use gallery, but normal word tags instead - if ( field.name().contains( "no-gallery-use", Qt::CaseInsensitive ) || field.alias().contains( "no-gallery-use", Qt::CaseInsensitive ) ) + if ( field.name().contains( "nogallery", Qt::CaseInsensitive ) || field.alias().contains( "nogallery", Qt::CaseInsensitive ) ) { useGallery = false; } From 573a39dadc681997f1ccd5a030bf7b88728d6d0e Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Tue, 6 Feb 2024 17:54:02 +0100 Subject: [PATCH 25/28] Remove old form editors --- app/qml/CMakeLists.txt | 16 - app/qml/editor/AbstractEditor.qml | 116 -------- app/qml/editor/RelationPhotoDelegate.qml | 77 ----- .../editor/RelationPhotoFooterDelegate.qml | 112 ------- app/qml/editor/RelationTextDelegate.qml | 90 ------ app/qml/editor/inputcheckbox.qml | 98 ------ app/qml/editor/inputdatetime.qml | 219 -------------- app/qml/editor/inputexternalresource.qml | 278 ------------------ app/qml/editor/inputqrcodereader.qml | 105 ------- app/qml/editor/inputrangeeditable.qml | 185 ------------ app/qml/editor/inputrangeslider.qml | 132 --------- app/qml/editor/inputrelation.qml | 250 ---------------- app/qml/editor/inputtextedit.qml | 76 ----- app/qml/editor/inputtexteditmultiline.qml | 51 ---- app/qml/editor/inputvaluemap.qml | 95 ------ app/qml/editor/inputvaluerelationcombobox.qml | 99 ------- app/qml/editor/inputvaluerelationpage.qml | 174 ----------- 17 files changed, 2173 deletions(-) delete mode 100644 app/qml/editor/AbstractEditor.qml delete mode 100644 app/qml/editor/RelationPhotoDelegate.qml delete mode 100644 app/qml/editor/RelationPhotoFooterDelegate.qml delete mode 100644 app/qml/editor/RelationTextDelegate.qml delete mode 100644 app/qml/editor/inputcheckbox.qml delete mode 100644 app/qml/editor/inputdatetime.qml delete mode 100644 app/qml/editor/inputexternalresource.qml delete mode 100644 app/qml/editor/inputqrcodereader.qml delete mode 100644 app/qml/editor/inputrangeeditable.qml delete mode 100644 app/qml/editor/inputrangeslider.qml delete mode 100644 app/qml/editor/inputrelation.qml delete mode 100644 app/qml/editor/inputtextedit.qml delete mode 100644 app/qml/editor/inputtexteditmultiline.qml delete mode 100644 app/qml/editor/inputvaluemap.qml delete mode 100644 app/qml/editor/inputvaluerelationcombobox.qml delete mode 100644 app/qml/editor/inputvaluerelationpage.qml diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index 99d91ed73..f18bb35aa 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -92,24 +92,8 @@ set(MM_QML dialogs/NoPermissionsDialog.qml dialogs/SplittingFailedDialog.qml dialogs/SyncFailedDialog.qml - editor/AbstractEditor.qml - editor/RelationPhotoDelegate.qml - editor/RelationPhotoFooterDelegate.qml - editor/RelationTextDelegate.qml - editor/inputcheckbox.qml - editor/inputdatetime.qml - editor/inputexternalresource.qml - editor/inputqrcodereader.qml - editor/inputrangeeditable.qml - editor/inputrangeslider.qml - editor/inputrelation.qml editor/inputrelationreference.qml editor/inputrichtext.qml - editor/inputtextedit.qml - editor/inputtexteditmultiline.qml - editor/inputvaluemap.qml - editor/inputvaluerelationcombobox.qml - editor/inputvaluerelationpage.qml editor/inputspacer.qml inputs/MMBaseInput.qml inputs/MMPasswordInput.qml diff --git a/app/qml/editor/AbstractEditor.qml b/app/qml/editor/AbstractEditor.qml deleted file mode 100644 index c5b256edf..000000000 --- a/app/qml/editor/AbstractEditor.qml +++ /dev/null @@ -1,116 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import QtQml.Models -import QtQuick.Layouts - -import lc 1.0 -import ".." - -Item { - id: root - - signal contentClicked() - signal leftActionClicked() - signal rightActionClicked() - - property alias content: contentContainer.children - property alias leftAction: leftActionContainer.children - property alias rightAction: rightActionContainer.children - - width: parent.width - height: customStyle.fields.height - - Rectangle { // background - width: parent.width - height: parent.height - border.color: customStyle.fields.normalColor - border.width: 1 * __dp < 1 ? 1 : 1 * __dp - color: customStyle.fields.backgroundColor - radius: customStyle.fields.cornerRadius - } - - Item { - id: rowlayout - - anchors { - fill: parent - leftMargin: customStyle.fields.sideMargin - rightMargin: customStyle.fields.sideMargin - } - - Item { - id: leftActionContainer - - property bool actionAllowed: leftActionContainer.children.length > 1 - - height: parent.height - width: actionAllowed ? parent.height : 0 - - Item { - width: leftActionContainer.actionAllowed ? parent.width + customStyle.fields.sideMargin : parent.width - x: leftActionContainer.actionAllowed ? parent.x - customStyle.fields.sideMargin : parent.x - height: parent.height - - MouseArea { - anchors.fill: parent - - onReleased: { - root.leftActionClicked() - } - } - } - } - - Item { - id: contentContainer - - y: leftActionContainer.y - x: leftActionContainer.width - - height: parent.height - width: parent.width - ( leftActionContainer.width + rightActionContainer.width ) - - MouseArea { - anchors.fill: parent - - onClicked: { - root.contentClicked() - } - } - } - - Item { - id: rightActionContainer - - property bool actionAllowed: rightActionContainer.children.length > 1 - - y: contentContainer.y - x: contentContainer.x + contentContainer.width - - height: parent.height - width: actionAllowed > 0 ? parent.height : 0 - - Item { - width: rightActionContainer.actionAllowed ? parent.width + customStyle.fields.sideMargin : parent.width - height: parent.height - - MouseArea { - anchors.fill: parent - - onReleased: { - root.rightActionClicked() - } - } - } - } - } -} diff --git a/app/qml/editor/RelationPhotoDelegate.qml b/app/qml/editor/RelationPhotoDelegate.qml deleted file mode 100644 index 13485c9c1..000000000 --- a/app/qml/editor/RelationPhotoDelegate.qml +++ /dev/null @@ -1,77 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import QtQml.Models -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects - -import lc 1.0 -import ".." - -Item { - id: root - - signal clicked( var feature ) - - Image { - id: image - - property bool imageValid: true - - anchors.centerIn: parent - width: imageValid ? parent.width : parent.width * 0.4 - height: imageValid ? parent.width : parent.width * 0.4 - sourceSize.width: image.width - sourceSize.height: image.height - visible: imageValid - - autoTransform: true - source: { - let absolutePath = model.PhotoPath - - if (image.status === Image.Error) { - image.imageValid = false - customStyle.icons.notAvailable - } - else if (absolutePath !== '' && __inputUtils.fileExists(absolutePath)) { - "file://" + absolutePath - } - else { - image.imageValid = false - customStyle.icons.notAvailable - } - } - - horizontalAlignment: Image.AlignHCenter - verticalAlignment: Image.AlignVCenter - mipmap: true - fillMode: Image.PreserveAspectFit - } - - ColorOverlay { - source: image.imageValid ? undefined : image - anchors.fill: image.imageValid ? undefined : image - color: customStyle.relationComponent.iconColor - } - - Rectangle { // border - anchors.fill: parent - - border.color: customStyle.relationComponent.photoBorderColor - border.width: customStyle.relationComponent.photoBorderWidth - color: "transparent" - } - - MouseArea { - anchors.fill: parent - onClicked: root.clicked( model.FeaturePair ) - } -} diff --git a/app/qml/editor/RelationPhotoFooterDelegate.qml b/app/qml/editor/RelationPhotoFooterDelegate.qml deleted file mode 100644 index 3d9f176c3..000000000 --- a/app/qml/editor/RelationPhotoFooterDelegate.qml +++ /dev/null @@ -1,112 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import QtQml.Models -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects - -import lc 1.0 -import ".." - -Item { - id: root - - property bool isReadOnly: false - - signal clicked() - - height: parent.height - width: parent.height + spacingRect.width - visible: !isReadOnly - - Row { - height: parent.height - width: parent.width - - Rectangle { // listview does not add space before footer, add it here - id: spacingRect - - width: customStyle.group.spacing - height: width - color: "transparent" - - } - - Rectangle { - width: root.width - spacingRect.width - height: root.height - - border.width: customStyle.relationComponent.photoBorderWidth - border.color: customStyle.relationComponent.photoBorderColorButton - color: "transparent" - - - Item { - width: parent.width / 2 - height: parent.height / 2 - - anchors.centerIn: parent - - Column { - anchors.fill: parent - - Item { - id: iconContainer - - width: parent.width - height: parent.height - textContainer.height - - Image { - id: icon - - width: parent.width - height: parent.height - - sourceSize: Qt.size( width, height ) - - source: customStyle.icons.plus - fillMode: Image.PreserveAspectFit - } - - ColorOverlay { - anchors.fill: icon - source: icon - color: customStyle.relationComponent.iconColorButton - } - } - - Item { - id: textContainer - - width: parent.width - height: txt.paintedHeight - - Text { - id: txt - text: qsTr( "Add" ) - - anchors.centerIn: parent - - font.pixelSize: customStyle.fields.labelPixelSize - color: customStyle.relationComponent.textColorButton - clip: true - } - } - } - } - } - } - - MouseArea { - anchors.fill: parent - onClicked: root.clicked( model.FeaturePair ) - } -} diff --git a/app/qml/editor/RelationTextDelegate.qml b/app/qml/editor/RelationTextDelegate.qml deleted file mode 100644 index 6ee579256..000000000 --- a/app/qml/editor/RelationTextDelegate.qml +++ /dev/null @@ -1,90 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls - -import lc 1.0 -import ".." - -Item { - id: root - - property string text - property real firstLinesMaxWidth - property real lastLineMaxWidth - - property alias backgroundContent: textDelegateContent - property alias textContent: txt - - property int itemsLine: { - // figure out which line am I from Y - if ( y < 2 * height ) return 0 // first and second line - if ( y < 3 * height ) return 1 // last line - return -1 // after last line ~> invisible - } - - signal clicked( var feature ) - - height: customStyle.relationComponent.textDelegateHeight - width: childrenRect.width - - visible: { - if ( itemsLine === 0 ) return true - if ( itemsLine === 1 ) { - // this is last line, we want to make sure that I can fit to line with "Add" icon and "More" icon - if ( x + width <= lastLineMaxWidth ) - return true - } - - return false - } - - Rectangle { - id: textDelegateContent - - property real requestedWidth: txt.paintedWidth + customStyle.relationComponent.tagInnerSpacing - - height: parent.height - width: { - if ( root.itemsLine === 0 ) - var comparedWidth = root.firstLinesMaxWidth - else - comparedWidth = root.lastLineMaxWidth - - return requestedWidth > comparedWidth ? comparedWidth : requestedWidth - } - - radius: customStyle.relationComponent.tagRadius - color: customStyle.relationComponent.tagBackgroundColor - border.color: customStyle.relationComponent.tagBorderColor - border.width: customStyle.relationComponent.tagBorderWidth - - Text { - id: txt - - text: root.text ? root.text : model.FeatureTitle - - width: parent.width - height: parent.height - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - - clip: true - font.bold: true - font.pixelSize: customStyle.fields.fontPixelSize - color: customStyle.relationComponent.tagTextColor - } - } - - MouseArea { - anchors.fill: parent - onClicked: root.clicked( model.FeaturePair ) - } -} diff --git a/app/qml/editor/inputcheckbox.qml b/app/qml/editor/inputcheckbox.qml deleted file mode 100644 index 649172b52..000000000 --- a/app/qml/editor/inputcheckbox.qml +++ /dev/null @@ -1,98 +0,0 @@ -/*************************************************************************** - checkbox.qml - -------------------------------------- - Date : 2017 - Copyright : (C) 2017 by Matthias Kuhn - Email : matthias@opengis.ch - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import "../components" - -/** - * Checkbox for QGIS Attribute Form - * Requires various global properties set to function, see featureform Loader section - * Do not use directly from Application QML - */ -Item { - id: fieldItem - - property var checkedState: getConfigValue(config['CheckedState'], true) - property var uncheckedState: getConfigValue(config['UncheckedState'], false) - property string booleanEnum: "1" // QMetaType::Bool Enum of Qvariant::Type - property bool isReadOnly: readOnly - - signal editorValueChanged( var newValue, bool isNull ) - - function getConfigValue(configValue, defaultValue) { - if (!configValue && field.type + "" === fieldItem.booleanEnum) { - return defaultValue - } else return configValue - } - - enabled: !readOnly - height: childrenRect.height - anchors { - right: parent.right - left: parent.left - } - - Rectangle { - id: fieldContainer - height: customStyle.fields.height - color: customStyle.fields.backgroundColor - radius: customStyle.fields.cornerRadius - anchors { right: parent.right; left: parent.left } - - MouseArea { - anchors.fill: parent - onClicked: switchComp.toggle() - } - - Text { - text: switchComp.checked ? fieldItem.checkedState : fieldItem.uncheckedState - font.pixelSize: customStyle.fields.fontPixelSize - color: customStyle.fields.fontColor - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - leftPadding: customStyle.fields.sideMargin - } - - Switch { - id: switchComp - - property var currentValue: value - - isReadOnly: fieldItem.isReadOnly - bgndColorActive: customStyle.toolbutton.activeButtonColor - bgndColorInactive: customStyle.toolbutton.backgroundColorInvalid - - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.rightMargin: customStyle.fields.sideMargin - - implicitHeight: fieldContainer.height * 0.6 - - checked: value === fieldItem.checkedState - - onSwitchChecked: function( isChecked ) { - editorValueChanged( isChecked ? fieldItem.checkedState : fieldItem.uncheckedState, false ) - } - - // Workaround to get a signal when the value has changed - onCurrentValueChanged: { - switchComp.checked = currentValue === fieldItem.checkedState - } - } - } -} diff --git a/app/qml/editor/inputdatetime.qml b/app/qml/editor/inputdatetime.qml deleted file mode 100644 index ea4b4db61..000000000 --- a/app/qml/editor/inputdatetime.qml +++ /dev/null @@ -1,219 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import QtQuick.Window - -import "../components" as Components - -AbstractEditor { - id: root - - property var parentField: parent.field - property var parentValue: parent.value - property bool parentValueIsNull: parent.valueIsNull - - property bool isReadOnly: parent.readOnly - - property bool fieldIsDate: __inputUtils.fieldType( field ) === 'QDate' - property var typeFromFieldFormat: __inputUtils.dateTimeFieldFormat( config['field_format'] ) - - property bool includesTime: typeFromFieldFormat.includes("Time") - property bool includesDate: typeFromFieldFormat.includes("Date") - - signal editorValueChanged(var newValue, bool isNull) - - enabled: !isReadOnly - - QtObject { - id: dateTransformer - // When changing this function, test with various timezones! - // On desktop, use environment variable TZ, e.g. TZ=America/Mexico_City (UTC-5) - function toJsDate(qtDate) { - if ( root.parentField.isDateOrTime ) { - if (root.fieldIsDate) { - if (qtDate.getUTCHours() === 0) - { - // on cold start of this editor widget, the JS date coming from C++ QDate is shifted. - // As [1] docs say: "converting a QDate will result in UTC's start of the - // day, which falls on a different date in some other time-zones" - // So for example if 2001-01-01 is stored in date file, - // it will become 2000-12-31 19:00:00 -05 in QML/JS in UTC -05 zone. - // However, we need 2001-01-01 00:00:00 in local timezone. - // [1] https://doc.qt.io/qt-6/qml-date.html - let date = new Date(qtDate.getUTCFullYear(), qtDate.getUTCMonth(), qtDate.getUTCDate() ) - return date - } else { - // - // Other issue is that when we already set NEW value by our calendar picker, - // the JS date coming from C++ already has correct (local) timezone... - // We can distinguish between these two by checking if the UTC hour is midnight - // or not and based on that apply or not apply the timezone shift - // - return qtDate - } - } - else { - return qtDate - } - } - else { - // This is the case when the date coming from C++ is pure string, so we - // need to convert it to JS Date ourselves - return Date.fromLocaleString(Qt.locale(), qtDate, config['field_format']) - } - } - } - - function newDateSelected( jsDate ) { - if ( jsDate ) { - if ( root.parentField.isDateOrTime ) { - // For QDate, the year, month and day is clipped based on - // the local timezone in QgsFeature.convertCompatible - root.editorValueChanged( jsDate, false ) - } - else { - let qtDate = jsDate.toLocaleString(Qt.locale(), config['field_format']) - root.editorValueChanged(qtDate, false) - } - } - } - - function formatText( qtDate ) { - if ( qtDate === undefined || root.parentValueIsNull ) { - return '' - } - else { - let jsDate = dateTransformer.toJsDate(qtDate) - return Qt.formatDateTime(jsDate, config['display_format']) - } - } - - function openPicker(requestedDate) { - dateTimeDrawerLoader.active = true - dateTimeDrawerLoader.focus = true - dateTimeDrawerLoader.item.dateToOpen = requestedDate - } - - onContentClicked: { - if (root.parentValueIsNull) { - // open calendar for today when no date is set - root.openPicker( new Date() ) - } - else { - root.openPicker( dateTransformer.toJsDate(root.parentValue) ) - } - } - - onRightActionClicked: { - root.newDateSelected( new Date() ) - } - - content: Text { - id: dateText - - text: formatText( root.parentValue ) - - anchors.fill: parent - - verticalAlignment: Text.AlignVCenter - - topPadding: customStyle.fields.height * 0.25 - bottomPadding: customStyle.fields.height * 0.25 - leftPadding: customStyle.fields.sideMargin - rightPadding: customStyle.fields.sideMargin - - color: customStyle.fields.fontColor - font.pixelSize: customStyle.fields.fontPixelSize - } - - rightAction: Item { - id: todayBtnContainer - - anchors.fill: parent - - Image { - id: todayBtn - - anchors.centerIn: parent - width: parent.width / 2 - sourceSize.width: parent.width / 2 - - source: customStyle.icons.today - } - - ColorOverlay { - anchors.fill: todayBtn - source: todayBtn - color: todayBtn.enabled ? customStyle.toolbutton.activeButtonColor : customStyle.toolbutton.backgroundColorInvalid - } - } - - Loader { - id: dateTimeDrawerLoader - - asynchronous: true - active: false - sourceComponent: dateTimeDrawerBlueprint - } - - Component { - id: dateTimeDrawerBlueprint - - Drawer { - id: dateTimeDrawer - - property alias dateToOpen: picker.dateToSelect - - dim: true - edge: Qt.BottomEdge - interactive: false - dragMargin: 0 - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - - height: { - if (root.includesDate) { - if ( Screen.primaryOrientation === Qt.PortraitOrientation ) { - return parent.height * 2/3 - } - return parent.height // for landscape mode - } - - return parent.height * 1/3 - } - width: formView.width - - onClosed: dateTimeDrawerLoader.active = false - - Component.onCompleted: open() - - Components.DateTimePicker { - id: picker - - width: parent.width - height: parent.height - - hasDatePicker: root.includesDate - hasTimePicker: root.includesTime - - onSelected: function( selectedDate ) { - root.newDateSelected( selectedDate ) - dateTimeDrawer.close() - } - - onCanceled: { - dateTimeDrawer.close() - } - } - } - } -} diff --git a/app/qml/editor/inputexternalresource.qml b/app/qml/editor/inputexternalresource.qml deleted file mode 100644 index db7bbe4da..000000000 --- a/app/qml/editor/inputexternalresource.qml +++ /dev/null @@ -1,278 +0,0 @@ -/*************************************************************************** - externalresource.qml - -------------------------------------- - Date : 2017 - Copyright : (C) 2017 by Matthias Kuhn - Email : matthias@opengis.ch - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import Qt5Compat.GraphicalEffects -import QtQuick.Layouts - -import "../components" - -/** - * External Resource (Photo capture) for QGIS Attribute Form - * Requires various global properties set to function, see featureform Loader section - * Do not use directly from Application QML - * The widget is interactive which allows interactions even in readOnly state (e.g showing preview), but no edit! - * - * Overview of path handling - * ------------------------- - * - * Project home path: comes from QgsProject::homePath() - by default it points to the folder where - * the .qgs/.qgz project file is stored, but can be changed manually by the user. - * - * Default path: defined in the field's configuration. This is the path where newly captured images will be stored. - * It has to be an absolute path. It can be defined as an expression (e.g. @project_home || '/photos') or - * by plain path (e.g. /home/john/photos). If not defined, project home path is used. - * - * In the field's configuration, there are three ways how path to pictures is stored in field values: - * absolute paths, relative to default path and relative to project path. Below is an example of how - * the final field values of paths are calculated. - * - * variable | value - * -------------------+-------------------------------- - * project home path | /home/john - * default path | /home/john/photos - * image path | /home/john/photos/img0001.jpg - * - * - * storage type | calculation of field value | final field value - * -------------------------+---------------------------------+-------------------------------- - * absolute path | image path | /home/john/photos/img0001.jpg - * relative to default path | image path - default path | img0001.jpg - * relative to project path | image path - project home path | photos/img0001.jpg - */ -Item { - signal editorValueChanged(var newValue, bool isNull) - - property var image: image - property var cameraIcon: customStyle.icons.camera - property var deleteIcon: customStyle.icons.remove - property var galleryIcon: customStyle.icons.gallery - property var backIcon: customStyle.icons.back - property real iconSize: customStyle.fields.height - property real textMargin: 10 * __dp - /** - * 0 - Relative path disabled - * 1 - Relative path to project - * 2 - Relative path to defaultRoot defined in the config - Default path field in the widget configuration form - */ - property int relativeStorageMode: config["RelativeStorage"] - - /** - * This evaluates the "default path" with the following order: - * 1. evaluate default path expression if defined, - * 2. use default path value if not empty, - * 3. use project home folder - */ - property string targetDir: __inputUtils.resolveTargetDir(homePath, config, featurePair, activeProject) - - property string prefixToRelativePath: __inputUtils.resolvePrefixForRelativePath(relativeStorageMode, homePath, targetDir) - - // Meant to be use with the save callback - stores image source - property string sourceToDelete - - function callbackOnSave() { - externalResourceHandler.onFormSave(fieldItem) - } - function callbackOnCancel() { - externalResourceHandler.onFormCanceled(fieldItem) - } - - id: fieldItem - enabled: true // its interactive widget - height: customStyle.fields.height * 3 - anchors { - left: parent.left - right: parent.right - } - - states: [ - State { - name: "valid" - }, - State { - name: "notSet" - }, - State { - name: "notAvailable" - } - ] - - Rectangle { - id: imageContainer - width: parent.width - height: parent.height - color: customStyle.fields.backgroundColor - radius: customStyle.fields.cornerRadius - - Image { - property var currentValue: value - - id: image - height: imageContainer.height - sourceSize.height: imageContainer.height - autoTransform: true - fillMode: Image.PreserveAspectFit - visible: fieldItem.state === "valid" - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - - MouseArea { - anchors.fill: parent - onClicked: externalResourceHandler.previewImage( __inputUtils.getAbsolutePath( image.currentValue, prefixToRelativePath ) ) - } - - onCurrentValueChanged: { - image.source = image.getSource() - } - - function getSource() { - var absolutePath = __inputUtils.getAbsolutePath( image.currentValue, prefixToRelativePath ) - if (image.status === Image.Error) { - fieldItem.state = "notAvailable" - return "" - } - else if (image.currentValue && __inputUtils.fileExists(absolutePath)) { - fieldItem.state = "valid" - return "file://" + absolutePath - } - else if (!image.currentValue) { - fieldItem.state = "notSet" - return "" - } - fieldItem.state = "notAvailable" - return "file://" + absolutePath - } - } - } - - Button { - id: deleteButton - visible: !readOnly && fieldItem.state !== "notSet" - width: buttonsContainer.itemHeight - height: width - padding: 0 - - anchors.right: imageContainer.right - anchors.bottom: imageContainer.bottom - anchors.margins: buttonsContainer.itemHeight/4 - - onClicked: externalResourceHandler.removeImage( fieldItem, __inputUtils.getAbsolutePath( image.currentValue, prefixToRelativePath ) ) - - background: Image { - id: deleteIcon - source: fieldItem.deleteIcon - width: deleteButton.width - height: deleteButton.height - sourceSize.width: width - sourceSize.height: height - fillMode: Image.PreserveAspectFit - } - - ColorOverlay { - anchors.fill: deleteIcon - source: deleteIcon - color: customStyle.fields.attentionColor - } - } - - Item { - property real itemHeight: fieldItem.height * 0.2 - - id: buttonsContainer - anchors.centerIn: imageContainer - anchors.fill: imageContainer - anchors.margins: fieldItem.textMargin - visible: fieldItem.state === "notSet" - - RowLayout { - anchors.fill: parent - - IconTextItem { - id: photoButton - fontColor: customStyle.fields.fontColor - fontPixelSize: customStyle.fields.fontPixelSize - iconSource: fieldItem.cameraIcon - iconSize: buttonsContainer.itemHeight - labelText: qsTr("Take a photo") - visible: !readOnly && fieldItem.state !== " valid" && externalResourceHandler.hasCameraCapability - - Layout.preferredHeight: parent.height - Layout.fillWidth: true - Layout.preferredWidth: ( parent.width - lineContainer.width ) / 2 - - MouseArea { - anchors.fill: parent - onClicked: { - externalResourceHandler.capturePhoto(fieldItem) - } - } - } - - Item { - id: lineContainer - visible: photoButton.visible - Layout.fillWidth: true - Layout.preferredHeight: parent.height - Layout.preferredWidth: line.width * 2 - - Rectangle { - id: line - - height: parent.height * 0.7 - color: customStyle.fields.fontColor - width: 1.5 * __dp - anchors.centerIn: parent - } - } - - IconTextItem { - id: browseButton - fontColor: customStyle.fields.fontColor - fontPixelSize: customStyle.fields.fontPixelSize - iconSource: fieldItem.galleryIcon - iconSize: buttonsContainer.itemHeight - labelText: qsTr("From gallery") - - visible: !readOnly && fieldItem.state !== " valid" - - Layout.preferredHeight: parent.height - Layout.fillWidth: true - Layout.preferredWidth: ( parent.width - lineContainer.width ) / 2 - - MouseArea { - anchors.fill: parent - onClicked: externalResourceHandler.chooseImage(fieldItem) - } - } - } - } - - Text { - id: text - height: parent.height - width: imageContainer.width - 2* fieldItem.textMargin - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - text: qsTr("Image is not available: ") + image.currentValue - font.pixelSize: customStyle.fields.fontPixelSize - color: customStyle.fields.fontColor - anchors.leftMargin: buttonsContainer.itemHeight + fieldItem.textMargin - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - visible: fieldItem.state === "notAvailable" - } - -} diff --git a/app/qml/editor/inputqrcodereader.qml b/app/qml/editor/inputqrcodereader.qml deleted file mode 100644 index dcf098116..000000000 --- a/app/qml/editor/inputqrcodereader.qml +++ /dev/null @@ -1,105 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import Qt5Compat.GraphicalEffects - -import lc 1.0 -import ".." - -AbstractEditor { - id: root - - /*required*/ property var config: parent.config - /*required*/ property var parentValue: parent.value - /*required*/ property bool isReadOnly: parent.readOnly - property StackView formView: parent.formView - - signal editorValueChanged( var newValue, bool isNull ) - - height: textArea.topPadding + textArea.bottomPadding + textArea.contentHeight - - content: TextArea { - id: textArea - - readOnly: root.isReadOnly - - anchors.fill: parent - - topPadding: customStyle.fields.height * 0.25 - bottomPadding: customStyle.fields.height * 0.25 - leftPadding: customStyle.fields.sideMargin - rightPadding: customStyle.fields.sideMargin - - wrapMode: Text.Wrap - color: customStyle.fields.fontColor - font.pixelSize: customStyle.fields.fontPixelSize - - text: root.parentValue !== undefined ? root.parentValue : '' - textFormat: config['UseHtml'] ? TextEdit.RichText : TextEdit.PlainText - - onLinkActivated: function( link ) { - Qt.openUrlExternally( link ) - } - - onTextChanged: root.editorValueChanged( text, text === "" ) - } - - rightAction: Item { - anchors.fill: parent - - Image { - id: importDataBtnIcon - - y: parent.y + parent.height / 2 - height / 2 - x: parent.x + parent.width - 1.5 * width - - width: parent.width * 0.6 - sourceSize.width: parent.width * 0.6 - - source: InputStyle.qrCodeIcon - visible: !root.isReadOnly - } - - ColorOverlay { - source: importDataBtnIcon - color: root.parent.readOnly ? customStyle.toolbutton.backgroundColorInvalid : customStyle.fields.fontColor - anchors.fill: importDataBtnIcon - } - } - - onRightActionClicked: { - if ( root.parent.readOnly ) return - - if (!__inputUtils.acquireCameraPermission()) return - - let page = root.formView.push( readerComponent, {} ) - page.forceActiveFocus() - } - - Component { - id: readerComponent - - CodeScanner { - id: codeScannerPage - - focus: true - - onBackButtonClicked: { - root.formView.pop() - } - - onScanFinished: function( captured ) { - root.editorValueChanged( captured, false ) - root.formView.pop() - } - } - } -} diff --git a/app/qml/editor/inputrangeeditable.qml b/app/qml/editor/inputrangeeditable.qml deleted file mode 100644 index 283fe65e4..000000000 --- a/app/qml/editor/inputrangeeditable.qml +++ /dev/null @@ -1,185 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects - -import lc 1.0 - -AbstractEditor { - id: root - - /*required*/ property var parentValue: parent.value - /*required*/ property bool parentValueIsNull: parent.valueIsNull - /*required*/ property bool isReadOnly: parent.readOnly - - property var locale: Qt.locale() - property real precision: config['Precision'] ? config['Precision'] : 0 - property string suffix: config['Suffix'] ? config['Suffix'] : '' - - // don't ever use a step smaller than would be visible in the widget - // i.e. if showing 2 decimals, smallest increment will be 0.01 - // https://github.com/qgis/QGIS/blob/a038a79997fb560e797daf3903d94c7d68e25f42/src/gui/editorwidgets/qgsdoublespinbox.cpp#L83-L87 - property real step: Math.max(config["Step"], Math.pow( 10.0, 0.0 - precision )) - - signal editorValueChanged( var newValue, bool isNull ) - - enabled: !isReadOnly - - leftAction: Item { - id: minusSign - - anchors.fill: parent - enabled: Number( numberInput.text ) - root.step >= config["Min"] - - Image { - id: imgMinus - - anchors.centerIn: parent - - width: parent.width / 3 - sourceSize.width: parent.width / 3 - - source: customStyle.icons.minus - } - - ColorOverlay { - source: imgMinus - color: minusSign.enabled ? customStyle.fields.fontColor : customStyle.toolbutton.backgroundColorInvalid - anchors.fill: imgMinus - } - } - - onLeftActionClicked: { - if ( minusSign.enabled ) - { - let decremented = Number( numberInput.text ) - root.step - root.editorValueChanged( decremented.toFixed( root.precision ), false ) - } - } - - content: Item { - id: contentContainer - - anchors.fill: parent - - Row { - id: inputAndSuffixContainer - - x: parent.width / 2 - width / 2 - - width: childrenRect.width - - height: parent.height - - TextInput { - id: numberInput - - property real maxWidth: contentContainer.width - suffix.width - - onTextEdited: { - let val = text.replace( ",", "." ).replace( / /g, '' ) // replace comma with dot - - root.editorValueChanged( val, val === "" ) - } - - text: root.parentValue === undefined || root.parentValueIsNull ? "" : root.parentValue - - height: parent.height - width: { - // set parent width if the number exceeds width of the field - if ( contentWidth > numberInput.maxWidth ) { - return numberInput.maxWidth - } - - if ( contentWidth > 0 ) { - return contentWidth - } - - return 1 // TextInput must have width set at least to one, otherwise listview would not scroll to this element - } - - inputMethodHints: Qt.ImhFormattedNumbersOnly - - font.pixelSize: customStyle.fields.fontPixelSize - color: customStyle.fields.fontColor - selectionColor: customStyle.fields.fontColor - selectedTextColor: "#ffffff" - - horizontalAlignment: Qt.AlignRight - verticalAlignment: Qt.AlignVCenter - - clip: true - } - - Text { - id: suffix - - text: root.suffix - - visible: root.suffix !== "" && numberInput.text !== "" - - height: parent.height - width: paintedWidth - horizontalAlignment: Qt.AlignLeft - verticalAlignment: Qt.AlignVCenter - - color: customStyle.fields.fontColor - font.pixelSize: customStyle.fields.fontPixelSize - } - } - } - - onContentClicked: { - if ( numberInput.activeFocus ) { - Qt.inputMethod.show() // only show keyboard if we already have active focus - } - else { - numberInput.forceActiveFocus() - } - } - - rightAction: Item { - id: plusSign - - anchors.fill: parent - - enabled: Number( numberInput.text ) + root.step <= config["Max"] - - Image { - id: imgPlus - - anchors.centerIn: parent - - width: parent.width / 3 - sourceSize.width: parent.width / 3 - - source: customStyle.icons.plus - } - - ColorOverlay { - source: imgPlus - color: plusSign.enabled ? customStyle.fields.fontColor : customStyle.toolbutton.backgroundColorInvalid - anchors.fill: imgPlus - } - } - - onRightActionClicked: { - if ( plusSign.enabled ) - { - let incremented = Number( numberInput.text ) + root.step - root.editorValueChanged( incremented.toFixed( root.precision ), false ) - } - - // on press and hold behavior can be used from here: - // https://github.com/mburakov/qt5/blob/93bfa3874c10f6cb5aa376f24363513ba8264117/qtquickcontrols/src/controls/SpinBox.qml#L306-L309 - } -} diff --git a/app/qml/editor/inputrangeslider.qml b/app/qml/editor/inputrangeslider.qml deleted file mode 100644 index b31119bfe..000000000 --- a/app/qml/editor/inputrangeslider.qml +++ /dev/null @@ -1,132 +0,0 @@ -/*************************************************************************** - range.qml - -------------------------------------- - Date : 2019 - Copyright : (C) 2019 by Viktor Sklencar - Email : viktor.sklencar@lutraconsulting.co.uk - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects - -import lc 1.0 - -Item { - id: root - - /*required*/ property var parentValue: parent.value - - readonly property int max_range: 2000000000 // https://doc.qt.io/qt-5/qml-int.html - - property int precision: config["Precision"] - property real from: getRange(config["Min"], -max_range) - property real to: getRange(config["Max"], max_range) - property real step: config["Step"] ? config["Step"] : 1 - property var locale: Qt.locale() - property string suffix: config["Suffix"] ? config["Suffix"] : "" - - signal editorValueChanged( var newValue, bool isNull ) - - function getRange(rangeValue, defaultRange) { - if ( typeof rangeValue !== 'undefined' && rangeValue >= -max_range && rangeValue <= max_range ) - return rangeValue - else - return defaultRange - } - - enabled: !readOnly - height: customStyle.fields.height - - anchors { - left: parent.left - right: parent.right - } - - // background - Rectangle { - anchors.fill: parent - border.color: customStyle.fields.normalColor - border.width: 1 * __dp - color: customStyle.fields.backgroundColor - radius: customStyle.fields.cornerRadius - } - - Item { - id: sliderContainer - - anchors.fill: parent - - RowLayout { - id: rowLayout - - anchors.fill: parent - - Text { - id: valueLabel - - Layout.preferredWidth: rowLayout.width / 3 - Layout.maximumWidth: rowLayout.width / 3 - Layout.preferredHeight: root.height - Layout.maximumHeight: root.height - - elide: Text.ElideRight - text: Number( slider.value ).toFixed( precision ).toLocaleString( root.locale ) + root.suffix - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignLeft - font.pixelSize: customStyle.fields.fontPixelSize - color: customStyle.fields.fontColor - padding: 10 * __dp - leftPadding: customStyle.fields.sideMargin - } - - Slider { - id: slider - - to: root.to - from: root.from - stepSize: root.step - value: root.parent.value ? root.parent.value : 0 - - Layout.fillWidth: true - Layout.maximumHeight: root.height - Layout.preferredHeight: root.height - rightPadding: customStyle.fields.sideMargin - - onValueChanged: root.editorValueChanged( slider.value, false ) - - background: Rectangle { - x: slider.leftPadding - y: slider.topPadding + slider.availableHeight / 2 - height / 2 - width: slider.availableWidth - height: slider.height * 0.1 - radius: 2 * __dp - - color: root.enabled ? customStyle.fields.fontColor : customStyle.fields.backgroundColorInactive - } - - handle: Rectangle { - x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) - y: slider.topPadding + slider.availableHeight / 2 - height / 2 - - width: slider.height * 0.6 * 0.66 + (2 * border.width) // Similar to indicator SwitchWidget of CheckBox widget - height: width - - radius: height * 0.5 - - color: "white" - border.color: customStyle.fields.backgroundColorInactive - } - } - } - } -} diff --git a/app/qml/editor/inputrelation.qml b/app/qml/editor/inputrelation.qml deleted file mode 100644 index 15c559a69..000000000 --- a/app/qml/editor/inputrelation.qml +++ /dev/null @@ -1,250 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import QtQml.Models -import QtQuick.Layouts - -import lc 1.0 -import ".." - -Item { - id: root - - property real widgetHeight: customStyle.fields.height * 3 // three rows - - signal featureLayerPairChanged() - - signal openLinkedFeature( var linkedFeature ) - signal createLinkedFeature( var parentFeature, var relation ) - - RelationFeaturesModel { - id: rmodel - - relation: associatedRelation - parentFeatureLayerPair: featurePair - homePath: activeProject.homePath - - onModelReset: { - // Repeater does not necesarry clear delegates immediately if they are invisible, - // we need to do hard reload in this case so that recalculateVisibleItems() is triggered - generator.model = null - generator.model = rmodel - - generator.recalculateVisibleItems() - } - } - - anchors { - left: parent.left - right: parent.right - } - - height: content.height - - Item { - id: content - - height: rmodel.isTextType ? textModeContainer.height : photoModeContainer.height - anchors { - left: parent.left - right: parent.right - } - - // Text Mode Widget - Rectangle { - id: textModeContainer - - property real fullLineWidth: flowItemView.width // full line width - first lines - property real lastLineShorterWidth: flowItemView.width - addChildButton.width - ( showMoreButton.visible ? showMoreButton.width : 0 ) - - states: [ - State { - name: "initial" - }, - State { - name: "page" - StateChangeScript { - script: { - let page = root.parent.formView.push( relationsPageComponent, { featuresModel: rmodel } ) - page.forceActiveFocus() - } - } - } - ] - - visible: rmodel.isTextType - height: root.widgetHeight - width: parent.width - state: "initial" - - Layout.maximumHeight: root.widgetHeight - Layout.minimumHeight: customStyle.fields.height - - border.color: customStyle.fields.normalColor - border.width: 1 * __dp - color: customStyle.fields.backgroundColor - radius: customStyle.fields.cornerRadius - - MouseArea { - anchors.fill: parent - onClicked: { - if ( textModeContainer.state === "initial" ) - textModeContainer.state = "page" - } - } - - Flow { - id: flowItemView - - anchors.fill: parent - anchors.margins: customStyle.fields.sideMargin - - spacing: customStyle.relationComponent.flowSpacing - - Repeater { - id: generator - - model: rmodel - - property int invisibleItemsCount: 0 - - delegate: RelationTextDelegate { - firstLinesMaxWidth: flowItemView.width - lastLineMaxWidth: flowItemView.width / 2 - - onClicked: function( feature ) { - root.openLinkedFeature( feature ) - } - - onVisibleChanged: generator.recalculateVisibleItems() - } - - function recalculateVisibleItems() { - let invisibles_count = 0 - - for ( let i = 0; i < generator.count; i++ ) { - let delegate_i = generator.itemAt( i ) - if ( delegate_i && !delegate_i.visible ) { - invisibles_count++ - } - } - - generator.invisibleItemsCount = invisibles_count - } - } - - RelationTextDelegate { - id: showMoreButton - - visible: generator.invisibleItemsCount > 0 - text: qsTr( "%1 more" ).arg( generator.invisibleItemsCount ) - - firstLinesMaxWidth: textModeContainer.fullLineWidth - lastLineMaxWidth: firstLinesMaxWidth - - backgroundContent.color: customStyle.relationComponent.tagBackgroundColorButton - backgroundContent.border.color: customStyle.relationComponent.tagBorderColorButton - textContent.color: customStyle.relationComponent.tagTextColor - - onClicked: textModeContainer.state = "page" - } - - RelationTextDelegate { - id: addChildButton - - text: "+ " + qsTr( "Add" ) - visible: !root.parent.readOnly - - backgroundContent.color: customStyle.relationComponent.tagBackgroundColorButtonAlt - backgroundContent.border.color: customStyle.relationComponent.tagBorderColorButton - textContent.color: customStyle.relationComponent.tagTextColorButton - - firstLinesMaxWidth: textModeContainer.fullLineWidth - lastLineMaxWidth: firstLinesMaxWidth - - onClicked: root.createLinkedFeature( root.parent.featurePair, root.parent.associatedRelation ) - } - } - } - - // Photo Mode Widget - Rectangle { - id: photoModeContainer - - visible: !rmodel.isTextType - height: widgetHeight - width: parent.width - - border.color: customStyle.fields.normalColor - border.width: 1 * __dp - color: customStyle.fields.backgroundColor - radius: customStyle.fields.cornerRadius - - ListView { - height: widgetHeight - width: parent.width - anchors.margins: customStyle.fields.sideMargin - anchors.fill: parent - spacing: customStyle.group.spacing - orientation: ListView.Horizontal - clip: true - - model: rmodel - delegate: RelationPhotoDelegate { - onClicked: function ( feature ) { - root.openLinkedFeature( feature ) - } - - height: ListView.view.height - width: height - } - - footer: RelationPhotoFooterDelegate { - isReadOnly: root.parent.readOnly - onClicked: root.createLinkedFeature( root.parent.featurePair, root.parent.associatedRelation ) - } - } - } - } - - Component { - id: relationsPageComponent - - FeaturesListPage { - id: relationsPage - - pageTitle: qsTr( "Linked features" ) - allowSearch: false //TODO search - toolbarButtons: ["add"] - toolbarVisible: !root.parent.readOnly - focus: true - - onBackButtonClicked: { - root.parent.formView.pop() - textModeContainer.state = "initial" - } - - onAddFeatureClicked: root.createLinkedFeature( root.parent.featurePair, root.parent.associatedRelation ) - onSelectionFinished: function( featureIds ) { - let clickedFeature = featuresModel.convertRoleValue( FeaturesModel.FeatureId, featureIds, FeaturesModel.FeaturePair ) - root.openLinkedFeature( clickedFeature ) - } - - Keys.onReleased: function( event ) { - if ( event.key === Qt.Key_Back || event.key === Qt.Key_Escape ) { - event.accepted = true - root.parent.formView.pop() - textModeContainer.state = "initial" - } - } - } - } -} diff --git a/app/qml/editor/inputtextedit.qml b/app/qml/editor/inputtextedit.qml deleted file mode 100644 index 9b6d14ed1..000000000 --- a/app/qml/editor/inputtextedit.qml +++ /dev/null @@ -1,76 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls - -/** - * Text Edit for QGIS Attribute Form - * Requires various global properties set to function, see featureform Loader section - * Do not use directly from Application QML - */ - -AbstractEditor { - id: root - - /*required*/ property var parentValue: parent.value - /*required*/ property bool parentValueIsNull: parent.valueIsNull - /*required*/ property bool isReadOnly: parent.readOnly - - signal editorValueChanged( var newValue, bool isNull ) - - content: TextField { - id: textField - - anchors.fill: parent - - topPadding: customStyle.fields.height * 0.25 - bottomPadding: customStyle.fields.height * 0.25 - leftPadding: customStyle.fields.sideMargin - rightPadding: customStyle.fields.sideMargin - - readOnly: root.isReadOnly - - font.pixelSize: customStyle.fields.fontPixelSize - color: customStyle.fields.fontColor - - text: root.parentValue === undefined || root.parentValueIsNull ? '' : root.parentValue - inputMethodHints: field.isNumeric ? Qt.ImhFormattedNumbersOnly : Qt.ImhNone - - states: [ - State { - name: "limitedTextLengthState" // Make sure we do not input more characters than allowed for strings - when: ( !field.isNumeric ) && ( field.length > 0 ) - PropertyChanges { - target: textField - maximumLength: field.length - } - } - ] - - background: Rectangle { - anchors.fill: parent - - radius: customStyle.fields.cornerRadius - color: customStyle.fields.backgroundColor - } - - onTextEdited: { - let val = text - if ( field.isNumeric ) - { - val = val.replace( ",", "." ).replace( / /g, '' ) // replace comma with dot and remove spaces - } - - editorValueChanged( val, val === "" ) - } - - onPreeditTextChanged: if ( __androidUtils.isAndroid ) Qt.inputMethod.commit() // to avoid Android's uncommited text - } -} diff --git a/app/qml/editor/inputtexteditmultiline.qml b/app/qml/editor/inputtexteditmultiline.qml deleted file mode 100644 index 01b8dfc77..000000000 --- a/app/qml/editor/inputtexteditmultiline.qml +++ /dev/null @@ -1,51 +0,0 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls - -AbstractEditor { - id: root - - /*required*/ property bool isReadOnly: parent.readOnly - /*required*/ property var parentValue: parent.value - /*required*/ property var config: parent.config - - signal editorValueChanged( var newValue, bool isNull ) - - height: textArea.topPadding + textArea.bottomPadding + textArea.contentHeight - - content: TextArea { - id: textArea - - readOnly: root.isReadOnly - - anchors.fill: parent - - topPadding: customStyle.fields.height * 0.25 - bottomPadding: customStyle.fields.height * 0.25 - leftPadding: customStyle.fields.sideMargin - rightPadding: customStyle.fields.sideMargin - - wrapMode: Text.Wrap - color: customStyle.fields.fontColor - font.pixelSize: customStyle.fields.fontPixelSize - - text: root.parentValue !== undefined ? root.parentValue : '' - textFormat: config['UseHtml'] ? TextEdit.RichText : TextEdit.PlainText - - onLinkActivated: function( link ) { - Qt.openUrlExternally( link ) - } - - onTextChanged: root.editorValueChanged( text, text === "" ) - - onPreeditTextChanged: Qt.inputMethod.commit() // to avoid Android's uncommited text - } -} diff --git a/app/qml/editor/inputvaluemap.qml b/app/qml/editor/inputvaluemap.qml deleted file mode 100644 index 81ad38329..000000000 --- a/app/qml/editor/inputvaluemap.qml +++ /dev/null @@ -1,95 +0,0 @@ -/*************************************************************************** - valuemap.qml - -------------------------------------- - Date : 2017 - Copyright : (C) 2017 by Matthias Kuhn - Email : matthias@opengis.ch - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -import QtQuick -import QtQuick.Controls -import Qt5Compat.GraphicalEffects -import "../components" - -/** - * Value Map for QGIS Attribute Form - * Requires various global properties set to function, see featureform Loader section - * Do not use directly from Application QML - */ -Item { - signal editorValueChanged(var newValue, bool isNull) - property bool isReadOnly: readOnly - - id: fieldItem - enabled: !readOnly - height: customStyle.fields.height - anchors { - left: parent.left - right: parent.right - } - - InputComboBox { - // Reversed to model's key-value map. It is used to find index according current value - property var reverseConfig: ({}) - property var currentEditorValue: value - - comboStyle: customStyle.fields - textRole: 'display' - height: parent.height - readOnly: isReadOnly - iconSize: fieldItem.height * 0.50 - model: ListModel { - id: listModel - } - - Component.onCompleted: { - var currentMap; - var currentKey; - - if( config['map'] ) - { - if( config['map'].length ) - { - //it's a list (>=QGIS3.0) - for(var i=0; i Date: Wed, 7 Feb 2024 11:46:29 +0100 Subject: [PATCH 26/28] fix gallery --- app/qml/CMakeLists.txt | 2 +- app/qml/form/editors/MMFormRelationEditor.qml | 2 - .../MMTextWithButtonInput.qml} | 18 +- gallery/CMakeLists.txt | 1 + gallery/main.cpp | 2 + gallery/qml.qrc | 6 +- gallery/qml/Main.qml | 4 + gallery/qml/pages/EditorsPage.qml | 417 +++++++----------- gallery/qml/pages/FormPage.qml | 5 +- gallery/qml/pages/InputsPage.qml | 187 ++++++++ gallery/relationfeaturesmodel.h | 173 ++++++++ 11 files changed, 553 insertions(+), 264 deletions(-) rename app/qml/{form/editors/MMFormButtonEditor.qml => inputs/MMTextWithButtonInput.qml} (84%) create mode 100644 gallery/qml/pages/InputsPage.qml create mode 100644 gallery/relationfeaturesmodel.h diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index 9a34c2f2d..8bdd4a6f4 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -101,12 +101,12 @@ set(MM_QML inputs/MMTextInput.qml inputs/MMSearchInput.qml inputs/MMDropdownInput.qml + inputs/MMTextWithButtonInput.qml form/MMFormWrapper.qml form/MMForm.qml form/MMPreviewPanel.qml form/components/MMFormTabBar.qml form/components/MMPreviewPanelActionButton.qml - form/editors/MMFormButtonEditor.qml form/editors/MMFormCalendarEditor.qml form/editors/MMFormGalleryEditor.qml form/editors/MMFormNumberEditor.qml diff --git a/app/qml/form/editors/MMFormRelationEditor.qml b/app/qml/form/editors/MMFormRelationEditor.qml index 6e145be1c..26b3263d0 100644 --- a/app/qml/form/editors/MMFormRelationEditor.qml +++ b/app/qml/form/editors/MMFormRelationEditor.qml @@ -34,8 +34,6 @@ MMBaseInput { property string _fieldTitle: parent.fieldTitle property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle - property ListModel featuresModel // <---- what to do here? - signal openLinkedFeature( var linkedFeature ) signal createLinkedFeature( var parentFeature, var relation ) diff --git a/app/qml/form/editors/MMFormButtonEditor.qml b/app/qml/inputs/MMTextWithButtonInput.qml similarity index 84% rename from app/qml/form/editors/MMFormButtonEditor.qml rename to app/qml/inputs/MMTextWithButtonInput.qml index 8800bc67c..36ae9ec0c 100644 --- a/app/qml/form/editors/MMFormButtonEditor.qml +++ b/app/qml/inputs/MMTextWithButtonInput.qml @@ -10,27 +10,24 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../../components" -import "../../inputs" +import "../components" /* - * This editor is not maintaned as it is not used in the app at the moment. - * We might need it in the future though. + * Common text input to use in the app, with button on right + * Disabled state can be achieved by setting `enabled: false`. + * + * See MMBaseInput for more properties. */ MMBaseInput { id: root - property var parentValue: parent.value ?? "" - property bool parentValueIsNull: parent.valueIsNull ?? false - property bool isReadOnly: parent.readOnly ?? false - property alias placeholderText: textField.placeholderText property alias text: textField.text property alias buttonText: buttonText.text property alias buttonEnabled: rightButton.enabled - signal editorValueChanged( var newValue, var isNull ) + signal textEdited( string text ) signal buttonClicked() hasFocus: textField.activeFocus @@ -41,7 +38,6 @@ MMBaseInput { anchors.verticalCenter: parent.verticalCenter width: parent.width + rightButton.x - text: root.parentValue color: root.enabled ? __style.nightColor : __style.mediumGreenColor placeholderTextColor: __style.nightAlphaColor font: __style.p5 @@ -50,6 +46,8 @@ MMBaseInput { background: Rectangle { color: __style.transparentColor } + + onTextEdited: root.textEdited( textField.text ) } rightAction: Button { diff --git a/gallery/CMakeLists.txt b/gallery/CMakeLists.txt index db08061c6..0afc939bf 100644 --- a/gallery/CMakeLists.txt +++ b/gallery/CMakeLists.txt @@ -36,6 +36,7 @@ set(GALLERY_HDRS inpututils.h scalebarkit.h positionkit.h + relationfeaturesmodel.h ../app/notificationmodel.h ../app/mmstyle.h ../core/merginerrortypes.h diff --git a/gallery/main.cpp b/gallery/main.cpp index 08ad70526..501c582b3 100644 --- a/gallery/main.cpp +++ b/gallery/main.cpp @@ -23,6 +23,7 @@ #include "inpututils.h" #include "scalebarkit.h" #include "positionkit.h" +#include "relationfeaturesmodel.h" int main( int argc, char *argv[] ) { @@ -42,6 +43,7 @@ int main( int argc, char *argv[] ) qmlRegisterUncreatableType( "lc", 1, 0, "RegistrationError", "RegistrationError Enum" ); qmlRegisterType( "lc", 1, 0, "QrCodeDecoder" ); qmlRegisterType( "lc", 1, 0, "ScaleBarKit" ); + qmlRegisterType( "lc", 1, 0, "RelationFeaturesModel" ); #ifdef DESKTOP_OS HotReload hotReload( engine ); diff --git a/gallery/qml.qrc b/gallery/qml.qrc index d28436774..841a4082a 100644 --- a/gallery/qml.qrc +++ b/gallery/qml.qrc @@ -20,6 +20,7 @@ qml/pages/CalendarPage.qml qml/pages/FormPage.qml qml/pages/GpsInfoPage.qml + qml/pages/InputsPage.qml ../app/qml/components/MMButton.qml ../app/qml/components/MMLinkButton.qml @@ -78,13 +79,14 @@ ../app/qml/inputs/MMPasswordInput.qml ../app/qml/inputs/MMTextInput.qml ../app/qml/inputs/MMSearchInput.qml - + ../app/qml/inputs/MMTextWithButtonInput.qml + ../app/qml/form/components/MMFormTabBar.qml - ../app/qml/form/editors/MMFormButtonEditor.qml ../app/qml/form/editors/MMFormCalendarEditor.qml ../app/qml/form/editors/MMFormGalleryEditor.qml ../app/qml/form/editors/MMFormNumberEditor.qml ../app/qml/form/editors/MMFormPhotoViewer.qml + ../app/qml/form/editors/MMFormPhotoEditor.qml ../app/qml/form/editors/MMFormRelationEditor.qml ../app/qml/form/editors/MMFormScannerEditor.qml ../app/qml/form/editors/MMFormSliderEditor.qml diff --git a/gallery/qml/Main.qml b/gallery/qml/Main.qml index 03c2607da..1a5f40545 100644 --- a/gallery/qml/Main.qml +++ b/gallery/qml/Main.qml @@ -130,6 +130,10 @@ ApplicationWindow { title: "Editors" source: "EditorsPage.qml" } + ListElement { + title: "Inputs" + source: "InputsPage.qml" + } ListElement { title: "Calendar" source: "CalendarPage.qml" diff --git a/gallery/qml/pages/EditorsPage.qml b/gallery/qml/pages/EditorsPage.qml index 516abfaeb..594f67ab3 100644 --- a/gallery/qml/pages/EditorsPage.qml +++ b/gallery/qml/pages/EditorsPage.qml @@ -38,297 +38,220 @@ ScrollView { MMCheckBox { id: checkbox - text: checked ? "enabled" : "disabled" + text: checked ? "enabled: yes" : "enabled: no" checked: true } - MMFormRelationEditor { - title: "MMTextAreaEditor" - enabled: checkbox.checked - width: parent.width - - onCreateLinkedFeature: function(parentFeature, relation) { - console.log("Add feature: " + parentFeature + " " + relation) - } - - onOpenLinkedFeature: function(linkedFeature) { - console.log("Feature: " + linkedFeature) - } + MMCheckBox { + id: checkboxRemember + text: checked ? "remeber: yes" : "remember: no" + checked: false + } - featuresModel: ListModel { - property string searchExpression + MMCheckBox { + id: checkboxTitle + text: checked ? "show title: yes" : "show title: no" + checked: true + } - ListElement { - FeatureId: 1 - FeatureTitle: "Title 1" - FeaturePair: "Pair 1" - Description: "Description 1" - SearchResult: "SearchResult 1" - Feature: "Feature 1" - } - ListElement { - FeatureId: 2 - FeatureTitle: "Title 2" - FeaturePair: "Pair 2" - Description: "Description 2" - SearchResult: "SearchResult 2" - Feature: "Feature 2" - } - ListElement { - FeatureId: 3 - FeatureTitle: "Title 3" - FeaturePair: "Pair 3" - Description: "Description 3" - SearchResult: "SearchResult 3" - Feature: "Feature 3" - } - ListElement { - FeatureId: 4 - FeatureTitle: "Title 4" - FeaturePair: "Pair 4" - Description: "Description 4" - SearchResult: "SearchResult 4" - Feature: "Feature 4" - } - ListElement { - FeatureId: 5 - FeatureTitle: "Title 5" - FeaturePair: "Pair 5" - Description: "Description 5" - SearchResult: "SearchResult 5" - Feature: "Feature 5" - } - ListElement { - FeatureId: 6 - FeatureTitle: "Title 6" - FeaturePair: "Pair 6" - Description: "Description 6" - SearchResult: "SearchResult 6" - Feature: "Feature 6" - } - ListElement { - FeatureId: 7 - FeatureTitle: "Title 7" - FeaturePair: "Pair 7" - Description: "Description 7" - SearchResult: "SearchResult 7" - Feature: "Feature 7" - } - } + MMCheckBox { + id: checkboxWarning + text: checked ? "show warning: yes" : "show warning: no" + checked: false } - MMSearchEditor { - title: "MMSearchEditor" - placeholderText: "Text value" - onSearchTextChanged: function(text) { console.log("Searched string: " + text) } + MMCheckBox { + id: checkboxError + text: checked ? "show error: yes" : "show error: no" + checked: false } - MMDropdownFormEditor { - title: "MMComboBoxEditor" - placeholderText: "Select one" - dropDownTitle: "Select one" - enabled: checkbox.checked + Item { width: parent.width - featuresModel: ListModel { - ListElement { - FeatureId: 1 - FeatureTitle: "Title 1" - Description: "Description 1" - SearchResult: "SearchResult 1" - Feature: "Feature 1" - } - ListElement { - FeatureId: 2 - FeatureTitle: "Title 2" - Description: "Description 2" - SearchResult: "SearchResult 2" - Feature: "Feature 2" + height: relationEditor.height + + property var fieldValue: "" + property var fieldConfig: ({}) + property bool fieldShouldShowTitle: checkboxTitle.checked + property bool fieldIsReadOnly: !checkbox.checked + property string fieldTitle: "MMFormRelationEditor" + property string fieldErrorMessage: checkboxError.checked ? "error" : "" + property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" + property bool fieldRememberValueSupported: checkboxRemember.checked + property bool fieldRememberValueState: false + + MMFormRelationEditor { + id: relationEditor + width: parent.width + + onCreateLinkedFeature: function(parentFeature, relation) { + console.log("Add feature: " + parentFeature + " " + relation) } - ListElement { - FeatureId: 3 - FeatureTitle: "Title 3" - Description: "Description 3" - SearchResult: "SearchResult 3" - Feature: "Feature 3" + + onOpenLinkedFeature: function(linkedFeature) { + console.log("Feature: " + linkedFeature) } } - onFeatureClicked: function(selectedFeatures) { text = selectedFeatures } } - MMDropdownFormEditor { - title: "MMComboBoxEditor Multi select" - placeholderText: "Select multiple" - dropDownTitle: "Multi select" - enabled: checkbox.checked + Item { width: parent.width - multiSelect: true - preselectedFeatures: [] - - featuresModel: ListModel { - property string searchExpression - - ListElement { - FeatureId: 1 - FeatureTitle: "Title 1" - Description: "Description 1" - SearchResult: "SearchResult 1" - Feature: "Feature 1" - } - ListElement { - FeatureId: 2 - FeatureTitle: "Title 2" - Description: "Description 2" - SearchResult: "SearchResult 2" - Feature: "Feature 2" - } - ListElement { - FeatureId: 3 - FeatureTitle: "Title 3" - Description: "Description 3" - SearchResult: "SearchResult 3" - Feature: "Feature 3" - } - ListElement { - FeatureId: 4 - FeatureTitle: "Title 4" - Description: "Description 4" - SearchResult: "SearchResult 4" - Feature: "Feature 4" - } - ListElement { - FeatureId: 5 - FeatureTitle: "Title 5" - Description: "Description 5" - SearchResult: "SearchResult 5" - Feature: "Feature 5" - } - ListElement { - FeatureId: 6 - FeatureTitle: "Title 6" - Description: "Description 6" - SearchResult: "SearchResult 6" - Feature: "Feature 6" + height: galleryEditor.height + + property var fieldValue: "" + property var fieldConfig: ({}) + property bool fieldShouldShowTitle: checkboxTitle.checked + property bool fieldIsReadOnly: !checkbox.checked + property string fieldTitle: "MMFormGalleryEditor" + property string fieldErrorMessage: checkboxError.checked ? "error" : "" + property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" + property bool fieldRememberValueSupported: checkboxRemember.checked + property bool fieldRememberValueState: false + + MMFormGalleryEditor { + id: galleryEditor + width: parent.width + + onCreateLinkedFeature: function(parentFeature, relation) { + console.log("Add feature: " + parentFeature + " " + relation) } - ListElement { - FeatureId: 7 - FeatureTitle: "Title 7" - Description: "Description 7" - SearchResult: "SearchResult 7" - Feature: "Feature 7" + + onOpenLinkedFeature: function(linkedFeature) { + console.log("Feature: " + linkedFeature) } } - onFeatureClicked: function(selectedFeatures) { - preselectedFeatures = selectedFeatures - text = selectedFeatures.toString() - } } - MMSliderFormEditor { - title: "MMSliderEditor" - from: -100 - to: 100 - parentValue: -100 - suffix: " s" + Item { width: parent.width - enabled: checkbox.checked - onEditorValueChanged: function(newValue) { errorMsg = newValue > 0 ? "" : "Set positive value!" } - hasCheckbox: true - checkboxChecked: true - } + height: sliderEditor.height - MMNumberFormEditor { - title: "MMNumberEditor" - parentValue: "2.0" - from: 1.0 - to: 3.0 - width: parent.width - enabled: checkbox.checked - precision: 1 - suffix: "s." - step: Math.pow( 10.0, 0.0 - precision ) - onEditorValueChanged: function(newValue) { parentValue = newValue } - } + property var fieldValue: -100 + property var fieldConfig: {["Min",-100],["Max", 100], ["Suffix", "s"], ["Precision", 1]} + property bool fieldShouldShowTitle: checkboxTitle.checked + property bool fieldIsReadOnly: !checkbox.checked + property string fieldTitle: "MMFormSliderEditor" + property string fieldErrorMessage: checkboxError.checked ? "error" : "" + property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" + property bool fieldRememberValueSupported: checkboxRemember.checked + property bool fieldRememberValueState: false - MMTextInput { - title: "MMInputEditor" - text: "Text" - enabled: checkbox.checked - width: parent.width - hasCheckbox: true - checkboxChecked: false + MMFormSliderEditor { + id: sliderEditor + width: parent.width + onEditorValueChanged: function(newValue, isNull ) { parent.fieldValue = newValue } + } } - MMScannerFormEditor { - title: "MMQrCodeEditor" - placeholderText: "QR code" - warningMsg: text.length > 0 ? "" : "Click to icon and scan the code" - enabled: checkbox.checked + Item { width: parent.width + height: numberEditor.height - onEditorValueChanged: function(newValue, isNull) { console.log("QR code: " + newValue) } + property var fieldValue: 2 + property var fieldConfig: {["Min",1.0], ["Max", 3.0], ["Precition", 1], ["Suffix", "s."], ["Step", 0.1]} + property bool fieldShouldShowTitle: checkboxTitle.checked + property bool fieldIsReadOnly: !checkbox.checked + property string fieldTitle: "MMFormNumberEditor" + property string fieldErrorMessage: checkboxError.checked ? "error" : "" + property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" + property bool fieldRememberValueSupported: checkboxRemember.checked + property bool fieldRememberValueState: false + property bool fieldValueIsNull: false + + MMFormNumberEditor { + id: numberEditor + width: parent.width + onEditorValueChanged: function(newValue, isNull) { parent.fieldValue = newValue } + } } - MMPhotoFormEditor { - title: "MMPhotoEditor" + Item { width: parent.width - photoUrl: "https://images.pexels.com/photos/615348/forest-fog-sunny-nature-615348.jpeg" + height: scannerEditor.height - onTrashClicked: console.log("Move to trash") - onContentClicked: console.log("Open photo") - } + property var fieldValue: "" + property var fieldConfig: ({}) + property bool fieldShouldShowTitle: checkboxTitle.checked + property bool fieldIsReadOnly: !checkbox.checked + property string fieldTitle: "MMFormScannerEditor" + property string fieldErrorMessage: checkboxError.checked ? "error" : "" + property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" + property bool fieldRememberValueSupported: checkboxRemember.checked + property bool fieldRememberValueState: false + property bool fieldValueIsNull: false - MMButtonFormEditor { - title: "MMButtonInputEditor" - placeholderText: "Write something" - text: "Text to copy" - buttonText: "Copy" - enabled: checkbox.checked - width: parent.width - onButtonClicked: console.log("Copy pressed") - buttonEnabled: text.length > 0 + MMFormScannerEditor { + id: scannerEditor + placeholderText: "QR code" + width: parent.width + onEditorValueChanged: function(newValue, isNull) { parent.fieldValue = newValue } + } } - MMButtonFormEditor { - title: "MMButtonInputEditor" - placeholderText: "Píš" - buttonText: "Kopíruj" - enabled: checkbox.checked + Item { width: parent.width - buttonEnabled: text.length > 0 - } + height: photoEditor.height + property var fieldValue: "" + property var fieldConfig: {["RelativeStorage", ""]} + property bool fieldShouldShowTitle: checkboxTitle.checked + property bool fieldIsReadOnly: !checkbox.checked + property string fieldTitle: "MMFormPhotoEditor" + property string fieldErrorMessage: checkboxError.checked ? "error" : "" + property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" + property bool fieldRememberValueSupported: checkboxRemember.checked + property bool fieldRememberValueState: false + property bool fieldValueIsNull: false - MMTextInput { - title: "MMInputEditor" - placeholderText: "Placeholder" - enabled: checkbox.checked - width: parent.width - warningMsg: text.length > 0 ? "" : "Write something" - } + MMFormPhotoEditor { + id: photoEditor + width: parent.width - MMTextMultilineFormEditor { - title: "MMTextAreaEditor" - placeholderText: "Place for multi row text" - enabled: checkbox.checked - width: parent.width - warningMsg: text.length > 0 ? "" : "Write something" + onTrashClicked: console.log("Move to trash") + onContentClicked: console.log("Open photo") + } } - MMPasswordInput { - title: "MMPasswordEditor" - text: "Password" - //regexp: '(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{6,})' - errorMsg: "Password must contain at least 6 characters\nMinimum 1 number, uppercase and lowercase letter and special character" - enabled: checkbox.checked + Item { width: parent.width + height: textMultilineEditor.height + + property var fieldValue: "" + property var fieldConfig: ({}) + property bool fieldShouldShowTitle: checkboxTitle.checked + property bool fieldIsReadOnly: !checkbox.checked + property string fieldTitle: "MMFormTextMultilineEditor" + property string fieldErrorMessage: checkboxError.checked ? "error" : "" + property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" + property bool fieldRememberValueSupported: checkboxRemember.checked + property bool fieldRememberValueState: false + property bool fieldValueIsNull: false + + MMFormTextMultilineEditor { + id: textMultilineEditor + placeholderText: "Place for multi row text" + width: parent.width + } } - MMSwitchFormEditor { - title: "MMSwitchEditor" - checked: true - text: checked ? "True" : "False" - warningMsg: checked ? "" : "Should be checked :)" - enabled: checkbox.checked + Item { width: parent.width + height: switchEditor.height + + property var fieldValue: "" + property var fieldConfig: {["CheckedState", "checked"], ["UncheckedState", "unchecked"]} + property bool fieldShouldShowTitle: checkboxTitle.checked + property bool fieldIsReadOnly: !checkbox.checked + property string fieldTitle: "MMFormSwitchEditor" + property string fieldErrorMessage: checkboxError.checked ? "error" : "" + property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" + property bool fieldRememberValueSupported: checkboxRemember.checked + property bool fieldRememberValueState: false + property bool fieldValueIsNull: false + + MMFormSwitchEditor { + id: switchEditor + width: parent.width + } } } } diff --git a/gallery/qml/pages/FormPage.qml b/gallery/qml/pages/FormPage.qml index 3f1962090..990855f39 100644 --- a/gallery/qml/pages/FormPage.qml +++ b/gallery/qml/pages/FormPage.qml @@ -14,6 +14,7 @@ import QtQuick.Layouts import "../../app/qml/components" import "../../app/qml/inputs" import "../../app/qml/form" +import "../../app/qml/form/components" Page { id: root @@ -173,8 +174,8 @@ Page { MMTextInput { width: ListView.view.width - title: qsTr("Field title") - placeholderText: qsTr("placeholder...") + title: "Field title" + placeholderText: "placeholder..." } } diff --git a/gallery/qml/pages/InputsPage.qml b/gallery/qml/pages/InputsPage.qml new file mode 100644 index 000000000..9101ea77c --- /dev/null +++ b/gallery/qml/pages/InputsPage.qml @@ -0,0 +1,187 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic + +import "../../app/qml/inputs" +import "../../app/qml/components" + +ScrollView { + Column { + padding: 20 + spacing: 20 + + MMCheckBox { + id: checkbox + text: checked ? "enabled" : "disabled" + checked: true + } + + GroupBox { + title: "Items based on MMBaseInput" + background: Rectangle { + color: "lightGray" + border.color: "gray" + } + label: Label { + color: "black" + text: parent.title + padding: 5 + } + + Column { + spacing: 10 + width: ApplicationWindow.window ? ApplicationWindow.window.width - 40 : 0 + + MMSearchInput { + title: "MMSearchInput" + placeholderText: "Text value" + onSearchTextChanged: function(text) { console.log("Searched string: " + text) } + } + + MMDropdownInput { + title: "MMDropdownInput" + placeholderText: "Select one" + dropDownTitle: "Select one" + enabled: checkbox.checked + width: parent.width + dataModel: ListModel { + ListElement { + FeatureId: 1 + FeatureTitle: "Title 1" + Description: "Description 1" + SearchResult: "SearchResult 1" + Feature: "Feature 1" + } + ListElement { + FeatureId: 2 + FeatureTitle: "Title 2" + Description: "Description 2" + SearchResult: "SearchResult 2" + Feature: "Feature 2" + } + ListElement { + FeatureId: 3 + FeatureTitle: "Title 3" + Description: "Description 3" + SearchResult: "SearchResult 3" + Feature: "Feature 3" + } + } + onSelectionFinished: function(selectedFeatures) { text = selectedFeatures } + } + + MMDropdownInput { + title: "MMDropdownInput Multi select" + placeholderText: "Select multiple" + dropDownTitle: "Multi select" + enabled: checkbox.checked + width: parent.width + multiSelect: true + preselectedFeatures: [] + + dataModel: ListModel { + property string searchExpression + + ListElement { + FeatureId: 1 + FeatureTitle: "Title 1" + Description: "Description 1" + SearchResult: "SearchResult 1" + Feature: "Feature 1" + } + ListElement { + FeatureId: 2 + FeatureTitle: "Title 2" + Description: "Description 2" + SearchResult: "SearchResult 2" + Feature: "Feature 2" + } + ListElement { + FeatureId: 3 + FeatureTitle: "Title 3" + Description: "Description 3" + SearchResult: "SearchResult 3" + Feature: "Feature 3" + } + ListElement { + FeatureId: 4 + FeatureTitle: "Title 4" + Description: "Description 4" + SearchResult: "SearchResult 4" + Feature: "Feature 4" + } + ListElement { + FeatureId: 5 + FeatureTitle: "Title 5" + Description: "Description 5" + SearchResult: "SearchResult 5" + Feature: "Feature 5" + } + ListElement { + FeatureId: 6 + FeatureTitle: "Title 6" + Description: "Description 6" + SearchResult: "SearchResult 6" + Feature: "Feature 6" + } + ListElement { + FeatureId: 7 + FeatureTitle: "Title 7" + Description: "Description 7" + SearchResult: "SearchResult 7" + Feature: "Feature 7" + } + } + onSelectionFinished: function(selectedFeatures) { + preselectedFeatures = selectedFeatures + text = selectedFeatures.toString() + } + } + + MMTextInput { + title: "MMTextInput" + text: "Text" + enabled: checkbox.checked + width: parent.width + hasCheckbox: true + checkboxChecked: false + } + + MMTextInput { + title: "MMTextInput" + placeholderText: "Placeholder" + enabled: checkbox.checked + width: parent.width + warningMsg: text.length > 0 ? "" : "Write something" + } + + MMTextWithButtonInput { + title: "MMTextWithButtonInput" + placeholderText: "Write something" + buttonText: "Copy" + width: parent.width + onButtonClicked: console.log("Copy pressed") + buttonEnabled: text.length > 0 + } + + MMPasswordInput { + title: "MMPasswordInput" + text: "Password" + //regexp: '(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{6,})' + errorMsg: "Password must contain at least 6 characters\nMinimum 1 number, uppercase and lowercase letter and special character" + enabled: checkbox.checked + width: parent.width + } + } + } + } +} diff --git a/gallery/relationfeaturesmodel.h b/gallery/relationfeaturesmodel.h new file mode 100644 index 000000000..e22409a85 --- /dev/null +++ b/gallery/relationfeaturesmodel.h @@ -0,0 +1,173 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef RELATIONFEATURESMODEL_H +#define RELATIONFEATURESMODEL_H + +#include + +#include +#include +#include + +/** + * Mockup of RelationFeaturesModel class + */ +class RelationFeaturesModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY( /*QgsRelation*/ QString relation READ relation WRITE setRelation NOTIFY relationChanged ) + Q_PROPERTY( QString homePath READ homePath WRITE setHomePath NOTIFY homePathChanged ) + Q_PROPERTY( /*FeatureLayerPair*/ QString parentFeatureLayerPair READ parentFeatureLayerPair WRITE setParentFeatureLayerPair NOTIFY parentFeatureLayerPairChanged ) + + public: + + enum relationModelRoles + { + FeatureTitle = Qt::UserRole + 10, + FeatureId, + Feature, + FeaturePair, + Description, // secondary text in list view + SearchResult, + PhotoPath = Qt::UserRole + 100, + }; + Q_ENUM( relationModelRoles ); + + explicit RelationFeaturesModel( QObject *parent = nullptr ) + { + for ( int i = 0; i <= mRows; ++i ) + { + QString it = QString::number( i ); + mData << FeatureItem( i, + "title" + it, + "pair" + it, + "desc" + it, "search" + it, "feat" + it, "photo" + it ); + } + } + + ~RelationFeaturesModel() {}; + + QVariant data( const QModelIndex &index, int role ) const override + { + int row = index.row(); + if ( row < 0 || row >= rowCount() ) + return QVariant(); + + if ( !index.isValid() ) + return QVariant(); + + const FeatureItem &item = mData.at( row ); + + if ( role == PhotoPath ) + { + return item.photoPath; + } + else if ( role == FeatureTitle ) + { + return item.featureTitle; + } + else if ( role == FeatureId ) + { + return item.featureId; + } + else if ( role == Feature ) + { + return item.feature; + } + else if ( role == FeaturePair ) + { + return item.featurePair; + } + else if ( role == Description ) + { + return item.description; + } + else if ( role == SearchResult ) + { + return item.searchResult; + } + else + return QVariant(); + } + QHash roleNames() const override + { + QHash roleNames = QAbstractListModel::roleNames(); + roleNames[PhotoPath] = QStringLiteral( "PhotoPath" ).toLatin1(); + roleNames[FeatureTitle] = QStringLiteral( "FeatureTitle" ).toLatin1(); + roleNames[FeatureId] = QStringLiteral( "FeatureId" ).toLatin1(); + roleNames[Feature] = QStringLiteral( "Feature" ).toLatin1(); + roleNames[FeaturePair] = QStringLiteral( "FeaturePair" ).toLatin1(); + roleNames[Description] = QStringLiteral( "Description" ).toLatin1(); + roleNames[SearchResult] = QStringLiteral( "SearchResult" ).toLatin1(); + return roleNames; + } + + int rowCount( const QModelIndex &parent = QModelIndex() ) const override {return mRows;} + + // void setup(); + // void setupFeatureRequest( QgsFeatureRequest &request ) override; + + void setParentFeatureLayerPair( QString pair ) { mParentFeatureLayerPair = pair; emit parentFeatureLayerPairChanged( pair ); } + void setRelation( QString relation ) {mRelation = relation; emit relationChanged( mRelation );} + + QString parentFeatureLayerPair() const {return mParentFeatureLayerPair;} + QString relation() const {return mRelation;} + + QString homePath() const {return mHomePath;} + void setHomePath( const QString &homePath ) {mHomePath = homePath; emit homePathChanged();} + + signals: + void parentFeatureLayerPairChanged( QString pair ); + void relationChanged( QString relation ); + void homePathChanged(); + + private: + /** + * Searches and returns first index of photo field if the field is type of 'ExternalResource'. + * @param layer Referencing layer of the relation. + * @return Index of photo field, otherwise -1 if not found any. + */ + int photoFieldIndex( ) const {return mPhotoIndex;} + + struct FeatureItem + { + FeatureItem( int id, QString title, QString pair, QString desc, QString search, QString feat, QString photo ) + : featureId( id ) + , featureTitle( title ) + , featurePair( pair ) + , description( desc ) + , searchResult( search ) + , feature( feat ), + photoPath( photo ) + { + } + + int featureId; + QString featureTitle; + QString featurePair; + QString description; + QString searchResult; + QString feature; + QString photoPath; + }; + + QList mData; + + QString mRelation; // associated relation + QString mParentFeatureLayerPair; // parent feature (with relation widget in form) + QString mHomePath; + + int mPhotoIndex = -1; + int mRows = 10; + +}; + +#endif // RELATIONFEATURESMODEL_H From cf27024f7ff6fcfa7e80ba49183fcd8c56381f03 Mon Sep 17 00:00:00 2001 From: PeterPetrik Date: Wed, 7 Feb 2024 16:00:57 +0100 Subject: [PATCH 27/28] rich text and spacer --- app/qml/CMakeLists.txt | 4 +- .../editors/MMFormRichTextViewer.qml} | 23 +-- .../editors/MMFormSpacer.qml} | 27 +++- gallery/inpututils.h | 11 +- gallery/main.cpp | 2 + gallery/qml.qrc | 3 + gallery/qml/EditorItem.qml | 30 ++++ gallery/qml/pages/EditorsPage.qml | 151 ++++++++---------- 8 files changed, 143 insertions(+), 108 deletions(-) rename app/qml/{editor/inputrichtext.qml => form/editors/MMFormRichTextViewer.qml} (68%) rename app/qml/{editor/inputspacer.qml => form/editors/MMFormSpacer.qml} (60%) create mode 100644 gallery/qml/EditorItem.qml diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index 8bdd4a6f4..cff523e10 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -94,8 +94,6 @@ set(MM_QML dialogs/SplittingFailedDialog.qml dialogs/SyncFailedDialog.qml editor/inputrelationreference.qml - editor/inputrichtext.qml - editor/inputspacer.qml inputs/MMBaseInput.qml inputs/MMPasswordInput.qml inputs/MMTextInput.qml @@ -120,6 +118,8 @@ set(MM_QML form/editors/MMFormTextEditor.qml form/editors/MMFormValueMapEditor.qml form/editors/MMFormValueRelationEditor.qml + form/editors/MMFormSpacer.qml + form/editors/MMFormRichTextViewer.qml layers/FeaturesListPageV2.qml layers/LayerDetail.qml layers/LayersListPageV2.qml diff --git a/app/qml/editor/inputrichtext.qml b/app/qml/form/editors/MMFormRichTextViewer.qml similarity index 68% rename from app/qml/editor/inputrichtext.qml rename to app/qml/form/editors/MMFormRichTextViewer.qml index b2eb6fe4e..8e20c8b32 100644 --- a/app/qml/editor/inputrichtext.qml +++ b/app/qml/form/editors/MMFormRichTextViewer.qml @@ -13,8 +13,11 @@ import "../components" Item { id: root - /*required*/ property var parentValue: parent.value - /*required*/ property var config: parent.config + + property var _fieldValue: parent.fieldValue + property var _fieldConfig: parent.fieldConfig + + property real padding: 11 * __dp height: textArea.height width: parent.width @@ -32,18 +35,18 @@ Item { id: textArea wrapMode: Text.Wrap - color: customStyle.fields.fontColor - font.pixelSize: customStyle.fields.fontPixelSize + font: __style.p5 + color: __style.nightColor - text: root.parentValue !== undefined ? root.parentValue : '' - textFormat: config['UseHtml'] ? TextEdit.RichText : TextEdit.PlainText + text: root._fieldValue !== undefined ? root._fieldValue : '' + textFormat: _fieldConfig['UseHtml'] ? TextEdit.RichText : TextEdit.PlainText width: root.width - topPadding: customStyle.fields.height * 0.25 - bottomPadding: customStyle.fields.height * 0.25 - leftPadding: customStyle.fields.sideMargin - rightPadding: customStyle.fields.sideMargin + topPadding: root.padding + bottomPadding: root.padding + leftPadding: root.padding + rightPadding: root.padding onLinkActivated: function( link ) { Qt.openUrlExternally( link ) diff --git a/app/qml/editor/inputspacer.qml b/app/qml/form/editors/MMFormSpacer.qml similarity index 60% rename from app/qml/editor/inputspacer.qml rename to app/qml/form/editors/MMFormSpacer.qml index 01b6bb49c..60758a51d 100644 --- a/app/qml/editor/inputspacer.qml +++ b/app/qml/form/editors/MMFormSpacer.qml @@ -9,20 +9,35 @@ import QtQuick import QtQuick.Controls -import "../components" -Rectangle { - id: spacer +import "../../components" +import "../../inputs" + +/* + * Spacer for QGIS Attribute Form + * Has 2 forms: with HLine and without + * It does not have title + * Read-Only, user cannot modify the content + * + * Requires various global properties set to function, see featureform Loader section. + * These properties are injected here via 'fieldXYZ' properties and captured with underscore `_`. + * + * Should be used only within feature form. + */ - /*required*/ property var config: parent.config +Rectangle { + id: root - property bool isHLine: config["IsHLine"] + property var _fieldConfig: parent.fieldConfig height: 1 * __dp < 1 ? 1 : 1 * __dp // parent form's list inserts space between each 2 elements width: parent.width - color: isHLine ? customStyle.fields.backgroundColor : "transparent" + + color: _fieldConfig["IsHLine"] ? __style.forestColor : "transparent" + anchors { right: parent.right left: parent.left } + } diff --git a/gallery/inpututils.h b/gallery/inpututils.h index d5a5ee96a..b28741099 100644 --- a/gallery/inpututils.h +++ b/gallery/inpututils.h @@ -8,11 +8,16 @@ class InputUtils: public QObject Q_OBJECT public: - explicit InputUtils( QObject *parent = nullptr ) {}; + explicit InputUtils( QObject *parent = nullptr ) {} Q_INVOKABLE bool acquireCameraPermission() { return true; } - Q_INVOKABLE static QString fieldType( const QObject &field ) { return "QDate"; }; - Q_INVOKABLE static QString dateTimeFieldFormat( const QString &fieldFormat ) { return "QDateTime"; }; + Q_INVOKABLE static QString fieldType( const QObject &field ) { return "QDate"; } + Q_INVOKABLE static QString dateTimeFieldFormat( const QString &fieldFormat ) { return "QDateTime"; } + Q_INVOKABLE bool fileExists( const QString &path ) { return false; } + Q_INVOKABLE static QString resolveTargetDir( const QString &homePath, const QVariantMap &config, const QString &pair, QString activeProject ) { return ""; } + Q_INVOKABLE static QString resolvePrefixForRelativePath( int relativeStorageMode, const QString &homePath, const QString &targetDir ) { return ""; } + + }; #endif // INPUTUTILS_H diff --git a/gallery/main.cpp b/gallery/main.cpp index 501c582b3..8c4152c14 100644 --- a/gallery/main.cpp +++ b/gallery/main.cpp @@ -51,6 +51,8 @@ int main( int argc, char *argv[] ) #endif InputUtils iu; engine.rootContext()->setContextProperty( "__inputUtils", &iu ); + engine.rootContext()->setContextProperty( "__androidUtils", &iu ); + engine.rootContext()->setContextProperty( "__iosUtils", &iu ); qreal dp = Helper::calculateDpRatio(); MMStyle style( dp ); diff --git a/gallery/qml.qrc b/gallery/qml.qrc index 841a4082a..1bbe0c009 100644 --- a/gallery/qml.qrc +++ b/gallery/qml.qrc @@ -2,6 +2,7 @@ qml/Main.qml qml/IconBox.qml + qml/EditorItem.qml qml/pages/IconsPage.qml qml/pages/StylePage.qml @@ -93,6 +94,8 @@ ../app/qml/form/editors/MMFormSwitchEditor.qml ../app/qml/form/editors/MMFormTextEditor.qml ../app/qml/form/editors/MMFormTextMultilineEditor.qml + ../app/qml/form/editors/MMFormSpacer.qml + ../app/qml/form/editors/MMFormRichTextViewer.qml ../app/qml/onboarding/MMAcceptInvitation.qml ../app/qml/onboarding/MMCreateWorkspace.qml diff --git a/gallery/qml/EditorItem.qml b/gallery/qml/EditorItem.qml new file mode 100644 index 000000000..6f7436766 --- /dev/null +++ b/gallery/qml/EditorItem.qml @@ -0,0 +1,30 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic + +import "../app/qml/components" +import "../app/qml/inputs" +import "../app/qml/form/editors" + +Item { + required property string fieldTitle + + property variant fieldValue: "" + property var fieldConfig: ({}) + property bool fieldShouldShowTitle: checkboxTitle.checked + property bool fieldIsReadOnly: !checkbox.checked + property string fieldErrorMessage: checkboxError.checked ? "error" : "" + property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" + property bool fieldRememberValueSupported: checkboxRemember.checked + property bool fieldRememberValueState: false + +} diff --git a/gallery/qml/pages/EditorsPage.qml b/gallery/qml/pages/EditorsPage.qml index 594f67ab3..08ce40b58 100644 --- a/gallery/qml/pages/EditorsPage.qml +++ b/gallery/qml/pages/EditorsPage.qml @@ -14,6 +14,7 @@ import QtQuick.Controls.Basic import "../../app/qml/inputs" import "../../app/qml/form/editors" import "../../app/qml/components" +import "../" ScrollView { Column { @@ -66,19 +67,10 @@ ScrollView { checked: false } - Item { + EditorItem { width: parent.width height: relationEditor.height - - property var fieldValue: "" - property var fieldConfig: ({}) - property bool fieldShouldShowTitle: checkboxTitle.checked - property bool fieldIsReadOnly: !checkbox.checked - property string fieldTitle: "MMFormRelationEditor" - property string fieldErrorMessage: checkboxError.checked ? "error" : "" - property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" - property bool fieldRememberValueSupported: checkboxRemember.checked - property bool fieldRememberValueState: false + fieldTitle: "MMFormRelationEditor" MMFormRelationEditor { id: relationEditor @@ -94,19 +86,10 @@ ScrollView { } } - Item { + EditorItem { width: parent.width height: galleryEditor.height - - property var fieldValue: "" - property var fieldConfig: ({}) - property bool fieldShouldShowTitle: checkboxTitle.checked - property bool fieldIsReadOnly: !checkbox.checked - property string fieldTitle: "MMFormGalleryEditor" - property string fieldErrorMessage: checkboxError.checked ? "error" : "" - property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" - property bool fieldRememberValueSupported: checkboxRemember.checked - property bool fieldRememberValueState: false + fieldTitle: "MMFormGalleryEditor" MMFormGalleryEditor { id: galleryEditor @@ -122,19 +105,13 @@ ScrollView { } } - Item { + EditorItem { width: parent.width height: sliderEditor.height - property var fieldValue: -100 - property var fieldConfig: {["Min",-100],["Max", 100], ["Suffix", "s"], ["Precision", 1]} - property bool fieldShouldShowTitle: checkboxTitle.checked - property bool fieldIsReadOnly: !checkbox.checked - property string fieldTitle: "MMFormSliderEditor" - property string fieldErrorMessage: checkboxError.checked ? "error" : "" - property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" - property bool fieldRememberValueSupported: checkboxRemember.checked - property bool fieldRememberValueState: false + fieldValue: "100" + fieldConfig: ({Min:-100, Max: 100, Suffix: "s", Precision: 1}) + fieldTitle: "MMFormSliderEditor" MMFormSliderEditor { id: sliderEditor @@ -143,20 +120,13 @@ ScrollView { } } - Item { + EditorItem { width: parent.width height: numberEditor.height - property var fieldValue: 2 - property var fieldConfig: {["Min",1.0], ["Max", 3.0], ["Precition", 1], ["Suffix", "s."], ["Step", 0.1]} - property bool fieldShouldShowTitle: checkboxTitle.checked - property bool fieldIsReadOnly: !checkbox.checked - property string fieldTitle: "MMFormNumberEditor" - property string fieldErrorMessage: checkboxError.checked ? "error" : "" - property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" - property bool fieldRememberValueSupported: checkboxRemember.checked - property bool fieldRememberValueState: false - property bool fieldValueIsNull: false + fieldValue: "2" + fieldConfig: ({Min: 1.0, Max: 3.0, Precition: 1, Suffix: "s.", Step: 0.1}) + fieldTitle: "MMFormNumberEditor" MMFormNumberEditor { id: numberEditor @@ -165,20 +135,11 @@ ScrollView { } } - Item { + EditorItem { width: parent.width height: scannerEditor.height - property var fieldValue: "" - property var fieldConfig: ({}) - property bool fieldShouldShowTitle: checkboxTitle.checked - property bool fieldIsReadOnly: !checkbox.checked - property string fieldTitle: "MMFormScannerEditor" - property string fieldErrorMessage: checkboxError.checked ? "error" : "" - property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" - property bool fieldRememberValueSupported: checkboxRemember.checked - property bool fieldRememberValueState: false - property bool fieldValueIsNull: false + fieldTitle: "MMFormScannerEditor" MMFormScannerEditor { id: scannerEditor @@ -188,19 +149,12 @@ ScrollView { } } - Item { + EditorItem { width: parent.width height: photoEditor.height - property var fieldValue: "" - property var fieldConfig: {["RelativeStorage", ""]} - property bool fieldShouldShowTitle: checkboxTitle.checked - property bool fieldIsReadOnly: !checkbox.checked - property string fieldTitle: "MMFormPhotoEditor" - property string fieldErrorMessage: checkboxError.checked ? "error" : "" - property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" - property bool fieldRememberValueSupported: checkboxRemember.checked - property bool fieldRememberValueState: false - property bool fieldValueIsNull: false + + fieldConfig: ({RelativeStorage: ""}) + fieldTitle: "MMFormPhotoEditor" MMFormPhotoEditor { id: photoEditor @@ -211,20 +165,11 @@ ScrollView { } } - Item { + EditorItem { width: parent.width height: textMultilineEditor.height - property var fieldValue: "" - property var fieldConfig: ({}) - property bool fieldShouldShowTitle: checkboxTitle.checked - property bool fieldIsReadOnly: !checkbox.checked - property string fieldTitle: "MMFormTextMultilineEditor" - property string fieldErrorMessage: checkboxError.checked ? "error" : "" - property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" - property bool fieldRememberValueSupported: checkboxRemember.checked - property bool fieldRememberValueState: false - property bool fieldValueIsNull: false + fieldTitle: "MMFormTextMultilineEditor" MMFormTextMultilineEditor { id: textMultilineEditor @@ -233,26 +178,58 @@ ScrollView { } } - Item { + EditorItem { width: parent.width height: switchEditor.height - property var fieldValue: "" - property var fieldConfig: {["CheckedState", "checked"], ["UncheckedState", "unchecked"]} - property bool fieldShouldShowTitle: checkboxTitle.checked - property bool fieldIsReadOnly: !checkbox.checked - property string fieldTitle: "MMFormSwitchEditor" - property string fieldErrorMessage: checkboxError.checked ? "error" : "" - property string fieldWarningMessage: checkboxWarning.checked ? "warning" : "" - property bool fieldRememberValueSupported: checkboxRemember.checked - property bool fieldRememberValueState: false - property bool fieldValueIsNull: false + fieldConfig: ({ CheckedState: "checked", UncheckedState: "unchecked"}) + fieldTitle: "MMFormSwitchEditor" MMFormSwitchEditor { id: switchEditor width: parent.width } } + + Label { + text: "MMFormSpacer - HLine" + } + + EditorItem { + width: parent.width + height: spacer2.height + + fieldConfig: ({IsHLine: true}) + fieldTitle: "title not shown for spacer" + + MMFormSpacer { + id: spacer2 + width: parent.width + } + } + + + Label { + text: "MMFormRichTextViewer - Text" + } + + EditorItem { + width: parent.width + height: richTextViewer.height + + fieldValue: "Lorem ipsum dolor sit amet, consectetur adipiscing elit," + + " sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n\n\n" + + " Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris" + + " nisi ut aliquip ex ea commodo consequat." + + fieldConfig: ({UseHtml: false}) + fieldTitle: "" + + MMFormRichTextViewer { + id: richTextViewer + width: parent.width + } + } } } } From 7f20480c433908fe49bdd2dc38d5286ac82d68cc Mon Sep 17 00:00:00 2001 From: PeterPetrik Date: Wed, 7 Feb 2024 16:13:08 +0100 Subject: [PATCH 28/28] fix styling --- app/qml/form/editors/MMFormRichTextViewer.qml | 8 ++++---- app/qml/form/editors/MMFormSpacer.qml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/qml/form/editors/MMFormRichTextViewer.qml b/app/qml/form/editors/MMFormRichTextViewer.qml index 8e20c8b32..5c04f7689 100644 --- a/app/qml/form/editors/MMFormRichTextViewer.qml +++ b/app/qml/form/editors/MMFormRichTextViewer.qml @@ -25,10 +25,10 @@ Item { Rectangle { // background width: root.width height: root.height - border.color: customStyle.fields.normalColor - border.width: 1 * __dp - color: customStyle.fields.backgroundColor - radius: customStyle.fields.cornerRadius + border.width: 2 * __dp + border.color: __style.transparentColor + color: __style.whiteColor + radius: __style.inputRadius } Text { diff --git a/app/qml/form/editors/MMFormSpacer.qml b/app/qml/form/editors/MMFormSpacer.qml index 60758a51d..2a4c3a97f 100644 --- a/app/qml/form/editors/MMFormSpacer.qml +++ b/app/qml/form/editors/MMFormSpacer.qml @@ -33,7 +33,7 @@ Rectangle { height: 1 * __dp < 1 ? 1 : 1 * __dp // parent form's list inserts space between each 2 elements width: parent.width - color: _fieldConfig["IsHLine"] ? __style.forestColor : "transparent" + color: _fieldConfig["IsHLine"] ? __style.forestColor : __style.transparentColor anchors { right: parent.right