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/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/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/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/inpututils.cpp b/app/inpututils.cpp index c11b7ee8d..cb233c403 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, const QgsRelation &relation ) { - QString path( "../editor/input%1.qml" ); + QString widgetName = widgetNameIn.toLower(); + + QString path( "../form/editors/%1.qml" ); if ( widgetName == QStringLiteral( "range" ) ) { @@ -1030,70 +1032,90 @@ const QUrl InputUtils::getEditorComponentSource( const QString &widgetName, cons { if ( config["Style"] == QStringLiteral( "Slider" ) ) { - return QUrl( path.arg( QLatin1String( "rangeslider" ) ) ); + return QUrl( path.arg( QLatin1String( "MMFormSliderEditor" ) ) ); } else if ( config["Style"] == QStringLiteral( "SpinBox" ) ) { - return QUrl( path.arg( QLatin1String( "rangeeditable" ) ) ); + return QUrl( path.arg( QLatin1String( "MMFormNumberEditor" ) ) ); } } - return QUrl( path.arg( QLatin1String( "textedit" ) ) ); + return QUrl( path.arg( QLatin1String( "MMFormTextEditor" ) ) ); } - - if ( field.name().contains( "qrcode", Qt::CaseInsensitive ) || field.alias().contains( "qrcode", Qt::CaseInsensitive ) ) + else if ( widgetName == QStringLiteral( "datetime" ) ) { - return QUrl( path.arg( QStringLiteral( "qrcodereader" ) ) ); + return QUrl( path.arg( QLatin1String( "MMFormCalendarEditor" ) ) ); } - - if ( widgetName == QStringLiteral( "textedit" ) ) + else if ( field.name().contains( "qrcode", Qt::CaseInsensitive ) || field.alias().contains( "qrcode", Qt::CaseInsensitive ) ) + { + return QUrl( path.arg( QStringLiteral( "MMFormScannerEditor" ) ) ); + } + else if ( widgetName == QStringLiteral( "textedit" ) ) { if ( config.value( "IsMultiline" ).toBool() ) { - return QUrl( path.arg( QStringLiteral( "texteditmultiline" ) ) ); + return QUrl( path.arg( QStringLiteral( "MMFormTextMultilineEditor" ) ) ); } - return QUrl( path.arg( QLatin1String( "textedit" ) ) ); + return QUrl( path.arg( QLatin1String( "MMFormTextEditor" ) ) ); } - - if ( widgetName == QStringLiteral( "valuerelation" ) ) + else if ( widgetName == QStringLiteral( "checkbox" ) ) + { + return QUrl( path.arg( QLatin1String( "MMFormSwitchEditor" ) ) ); + } + else if ( widgetName == QStringLiteral( "valuerelation" ) ) { - const QgsMapLayer *referencedLayer = QgsProject::instance()->mapLayer( config.value( "Layer" ).toString() ); - const QgsVectorLayer *layer = qobject_cast( referencedLayer ); + return QUrl( path.arg( QLatin1String( "MMFormValueRelationEditor" ) ) ); + } + else if ( widgetName == QStringLiteral( "valuemap" ) ) + { + return QUrl( path.arg( QLatin1String( "MMFormValueMapEditor" ) ) ); + } + else if ( widgetName == QStringLiteral( "externalresource" ) ) + { + return QUrl( path.arg( QLatin1String( "MMFormPhotoEditor" ) ) ); + } + else if ( widgetName == QStringLiteral( "relation" ) ) + { + // check if we should use gallery or word tags + bool useGallery = false; - if ( layer ) + QgsVectorLayer *layer = relation.referencingLayer(); + if ( layer && layer->isValid() ) { - int featuresCount = layer->dataProvider()->featureCount(); - if ( featuresCount > 4 ) - return QUrl( path.arg( QLatin1String( "valuerelationpage" ) ) ); + 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; + } + } } - if ( config.value( "AllowMulti" ).toBool() ) + // Mind this hack - fields with `no-gallery-use` won't use gallery, but normal word tags instead + if ( field.name().contains( "nogallery", Qt::CaseInsensitive ) || field.alias().contains( "nogallery", Qt::CaseInsensitive ) ) { - return QUrl( path.arg( QLatin1String( "valuerelationpage" ) ) ); + useGallery = false; } - return QUrl( path.arg( QLatin1String( "valuerelationcombobox" ) ) ); + if ( useGallery ) + { + return QUrl( path.arg( QLatin1String( "MMFormGalleryEditor" ) ) ); + } + else + { + return QUrl( path.arg( QLatin1String( "MMFormRelationEditor" ) ) ); + } } - 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 ) ); - } - else - { - return QUrl( path.arg( QLatin1String( "textedit" ) ) ); - } + // TODO == Missing editors: + // - QStringLiteral( "richtext" ) -> text and HTML form widget + // - QStringLiteral( "spacer" ) + // - QStringLiteral( "relationreference" ) + + return QUrl( path.arg( QLatin1String( "MMFormTextEditor" ) ) ); } const QgsEditorWidgetSetup InputUtils::getEditorWidgetSetup( const QgsField &field ) diff --git a/app/inpututils.h b/app/inpututils.h index d9bfda744..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 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(), const QgsRelation &relation = QgsRelation() ); /** * \copydoc QgsCoordinateFormatter::format() 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 bf0f78404..cff523e10 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -35,8 +35,11 @@ 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 + components/MMDropdownDrawer.qml components/MMGpsDataDrawer.qml components/MMHeader.qml components/MMHlineText.qml @@ -45,6 +48,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 @@ -55,7 +59,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 @@ -68,6 +71,14 @@ 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 @@ -82,37 +93,33 @@ 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/MMAbstractEditor.qml - inputs/MMCheckBox.qml - inputs/MMInputEditor.qml - inputs/MMPasswordEditor.qml - inputs/MMSliderEditor.qml - form/FeatureForm.qml - form/FeatureFormPage.qml - form/FeatureFormStyling.qml - form/FeatureToolbar.qml - form/FormWrapper.qml - form/MMFormTabBar.qml - form/PreviewPanel.qml + inputs/MMBaseInput.qml + inputs/MMPasswordInput.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/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 + form/editors/MMFormTextMultilineEditor.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/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/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/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/components/MMComboBoxDrawer.qml b/app/qml/components/MMDropdownDrawer.qml similarity index 75% rename from app/qml/components/MMComboBoxDrawer.qml rename to app/qml/components/MMDropdownDrawer.qml index b67219933..80e1d81ca 100644 --- a/app/qml/components/MMComboBoxDrawer.qml +++ b/app/qml/components/MMDropdownDrawer.qml @@ -10,6 +10,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic + import "../inputs" Drawer { @@ -17,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 @@ -79,20 +83,26 @@ Drawer { MouseArea { anchors.fill: parent - onClicked: root.visible = false + onClicked: root.close() } } } - MMSearchEditor { + MMSearchInput { id: searchBar width: parent.width - 2 * root.padding - placeholderText: qsTr("Text value") + placeholderText: qsTr("Search") bgColor: __style.lightGreenColor - visible: root.model.count >= root.minFeaturesCountToFullScreenMode + 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 } } @@ -103,22 +113,21 @@ 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 } 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 @@ -159,14 +168,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() } } } @@ -186,17 +196,24 @@ Drawer { visible: root.multiSelect onClicked: { - let selectedFeatures = [] - for(let i=0; i= 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/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/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/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/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 __style.maxPageWidth ? __style.maxPageWidth : parent.width + height: parent.height + + ColumnLayout { + id: layout + + anchors { + fill: parent + leftMargin: __style.pageMargins + topMargin: __style.pageMargins + rightMargin: __style.pageMargins + bottomMargin: __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 + } + + Text { + Layout.fillWidth: true + Layout.fillHeight: true + + text: controller.html + visible: root.controller.type === AttributePreviewController.HTML + + clip: true + } + + Text { + Layout.fillWidth: true + Layout.fillHeight: true + + text: qsTr("No map tip available.") + visible: root.controller.type === AttributePreviewController.Empty + + font: __style.p6 + color: __style.nightColor + } + + Item { + id: fieldsContainer + + Layout.fillWidth: true + Layout.fillHeight: true + + visible: root.controller.type === AttributePreviewController.Fields + + ListView { + anchors.fill: parent + + spacing: 20 * __dp + interactive: false + + model: root.controller.fieldModel + + delegate: Item { + width: ListView.view.width + height: childrenRect.height + + 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/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 8936297fc..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: Window.width + implicitWidth: ApplicationWindow.window?.width ?? 0 spacing: 20 * __dp 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/inputs/MMCalendarEditor.qml b/app/qml/form/editors/MMFormCalendarEditor.qml similarity index 62% rename from app/qml/inputs/MMCalendarEditor.qml rename to app/qml/form/editors/MMFormCalendarEditor.qml index 22803149f..9caa81d51 100644 --- a/app/qml/inputs/MMCalendarEditor.qml +++ b/app/qml/form/editors/MMFormCalendarEditor.qml @@ -10,44 +10,81 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../components" - -MMAbstractEditor { +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 - property var parentField: parent.field ?? "" - property var parentValue: parent.value ?? "" - property bool parentValueIsNull: parent.valueIsNull ?? true - property bool isReadOnly: parent.readOnly ?? false + 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 - property var config - property bool fieldIsDate: __inputUtils.fieldType( field ) === 'QDate' - property var typeFromFieldFormat: __inputUtils.dateTimeFieldFormat( config['field_format'] ) + 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'] ) 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 + + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState - enabled: !isReadOnly + enabled: !_fieldIsReadOnly hasFocus: textField.activeFocus + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + content: TextField { id: textField 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 } @@ -63,11 +100,11 @@ MMAbstractEditor { } onRightActionClicked: { - if (root.parentValueIsNull) { + if (root._fieldValueIsNull) { root.openPicker( new Date() ) } else { - root.openPicker( dateTransformer.toJsDate(root.parentValue) ) + root.openPicker( dateTransformer.toJsDate(root._fieldValue) ) } } @@ -85,14 +122,18 @@ MMAbstractEditor { 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() } } @@ -102,7 +143,7 @@ MMAbstractEditor { // 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) { @@ -132,32 +173,33 @@ MMAbstractEditor { 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']) } } diff --git a/app/qml/components/MMPhotoGallery.qml b/app/qml/form/editors/MMFormGalleryEditor.qml similarity index 63% rename from app/qml/components/MMPhotoGallery.qml rename to app/qml/form/editors/MMFormGalleryEditor.qml index 015753c1a..43e33dc6c 100644 --- a/app/qml/components/MMPhotoGallery.qml +++ b/app/qml/form/editors/MMFormGalleryEditor.qml @@ -10,23 +10,36 @@ import QtQuick import QtQuick.Controls +import lc 1.0 +import "../../components" + Item { id: root 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 @@ -52,6 +65,8 @@ Item { anchors.right: parent.right + visible: false // for now + text: qsTr("Show all") font: __style.t4 color: __style.forestColor @@ -71,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 + + photoUrl: { + let absolutePath = model.PhotoPath - onClicked: function(path) { root.clicked(path) } + if ( absolutePath !== '' && __inputUtils.fileExists( absolutePath ) ) { + return "file://" + absolutePath + } + return '' + } + + onClicked: function(path) { + root.clicked(path) + root.openLinkedFeature( model.FeaturePair ) + } } header: Row { @@ -105,7 +140,10 @@ Item { MouseArea { anchors.fill: parent - onClicked: root.addImage() + onClicked: { + root.addImage() + root.createLinkedFeature( root._fieldFeatureLayerPair, root._fieldAssociatedRelation ) + } } } @@ -118,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/MMFormNumberEditor.qml b/app/qml/form/editors/MMFormNumberEditor.qml new file mode 100644 index 000000000..1205cddf4 --- /dev/null +++ b/app/qml/form/editors/MMFormNumberEditor.qml @@ -0,0 +1,169 @@ +/*************************************************************************** + * * + * 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" + +/* + * 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 _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 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 : "" + + errorMsg: _fieldErrorMessage + warningMsg: _fieldWarningMessage + + enabled: !_fieldIsReadOnly + hasFocus: numberInput.activeFocus + + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + + leftAction: MMIcon { + id: leftIcon + + height: parent.height + + source: __style.minusIcon + color: enabled ? __style.forestColor : __style.mediumGreenColor + enabled: Number( numberInput.text ) - internal.step >= internal.from + } + + onLeftActionClicked: { + if ( leftIcon.enabled ) + { + let decremented = Number( numberInput.text ) - internal.step + root.editorValueChanged( decremented.toFixed( internal.precision ), false ) + } + } + + content: Item { + anchors.fill: parent + + Row { + height: parent.height + anchors.horizontalCenter: parent.horizontalCenter + clip: true + + TextField { + id: numberInput + + height: parent.height + + 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 + + onTextEdited: { + let val = text.replace( ",", "." ).replace( / /g, '' ) // replace comma with dot + + root.editorValueChanged( val, val === "" ) + } + + background: Rectangle { + color: __style.transparentColor + } + } + + Text { + id: suffix + + text: internal.suffix ? ' ' + internal.suffix : "" // to make sure there is a space between the number and the suffix + + visible: internal.suffix !== "" && numberInput.text !== "" + + height: parent.height + verticalAlignment: Qt.AlignVCenter + + font: __style.p5 + color: numberInput.color + } + } + } + + rightAction: MMIcon { + id: rightIcon + + height: parent.height + + source: __style.plusIcon + color: enabled ? __style.forestColor : __style.mediumGreenColor + enabled: Number( numberInput.text ) + internal.step <= internal.to + } + + onRightActionClicked: { + if ( rightIcon.enabled ) + { + 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 +} diff --git a/app/qml/form/editors/MMFormPhotoEditor.qml b/app/qml/form/editors/MMFormPhotoEditor.qml new file mode 100644 index 000000000..ec97981a4 --- /dev/null +++ b/app/qml/form/editors/MMFormPhotoEditor.qml @@ -0,0 +1,344 @@ +/*************************************************************************** + * * + * 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 + + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + + signal editorValueChanged( var newValue, bool isNull ) + signal rememberValueBoxClicked( bool state ) + + title: _fieldShouldShowTitle ? _fieldTitle : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage + + enabled: !_fieldIsReadOnly + + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + + 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 ) ) + + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + + // 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 ) { + if ( root._fieldIndex.toString() === index.toString() ) { + internal.imageSelected( imagePath ) + } + } + } + + Connections { + target: __iosUtils + + // used for both gallery and camera + function onImageSelected( imagePath, 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 ) { + if ( __inputUtils.fileExists( path ) ) { + imageDeleteDialog.imagePath = path + imageDeleteDialog.open() + } + else { + 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 ) { + if ( imgPath ) { + __inputUtils.rescaleImage( imgPath, __activeProject.qgsProject ) + let newImgPath = __inputUtils.getRelativePath( imgPath, prefixToRelativePath ) + + root.editorValueChanged( newImgPath, newImgPath === "" || newImgPath === null ) + } + } + } +} diff --git a/app/qml/inputs/MMPhotoEditor.qml b/app/qml/form/editors/MMFormPhotoViewer.qml similarity index 60% rename from app/qml/inputs/MMPhotoEditor.qml rename to app/qml/form/editors/MMFormPhotoViewer.qml index 6b2e8c257..6e533f0e7 100644 --- a/app/qml/inputs/MMPhotoEditor.qml +++ b/app/qml/form/editors/MMFormPhotoViewer.qml @@ -9,24 +9,57 @@ import QtQuick import QtQuick.Controls -import QtQuick.Controls.Basic -import "../components" +import "../../components" +import "../../inputs" -MMAbstractEditor { +/* + * 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 parentValue: parent.value ?? "" - property bool parentValueIsNull: parent.valueIsNull ?? false - property bool isReadOnly: parent.readOnly ?? false + // 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 url photoUrl: "" + property bool hasCameraCapability: true - property url photoUrl + property alias photoComponent: photo 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 @@ -34,6 +67,8 @@ MMAbstractEditor { height: root.contentItemHeight photoUrl: root.photoUrl + fillMode: Image.PreserveAspectCrop + MouseArea { anchors.fill: parent onClicked: root.contentClicked() @@ -48,7 +83,7 @@ MMAbstractEditor { 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 diff --git a/app/qml/inputs/MMRelationsEditor.qml b/app/qml/form/editors/MMFormRelationEditor.qml similarity index 70% rename from app/qml/inputs/MMRelationsEditor.qml rename to app/qml/form/editors/MMFormRelationEditor.qml index 4e688bb37..26b3263d0 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 - required property ListModel featuresModel + property string _fieldTitle: parent.fieldTitle + property bool _fieldShouldShowTitle: parent.fieldShouldShowTitle - signal editorValueChanged( var newValue, var isNull ) signal openLinkedFeature( var linkedFeature ) signal createLinkedFeature( var parentFeature, var relation ) @@ -30,6 +42,8 @@ MMAbstractEditor { Component.onCompleted: root.recalculate() onWidthChanged: root.recalculate() + title: _fieldShouldShowTitle ? _fieldTitle : "" + content: Rectangle { width: root.width - 2 * root.spacing height: root.contentItemHeight @@ -59,7 +73,7 @@ MMAbstractEditor { MouseArea { anchors.fill: parent - onClicked: root.createLinkedFeature( root.parent.featurePair, root.parent.associatedRelation ) + onClicked: root.createLinkedFeature( root._fieldFeatureLayerPair, root._fieldAssociatedRelation ) } } @@ -68,7 +82,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 +164,7 @@ MMAbstractEditor { function recalculate() { repeater.invisibleIds = 0 repeater.model = null - repeater.model = root.featuresModel + repeater.model = rmodel } Loader { @@ -151,18 +178,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/app/qml/editor/inputrichtext.qml b/app/qml/form/editors/MMFormRichTextViewer.qml similarity index 58% rename from app/qml/editor/inputrichtext.qml rename to app/qml/form/editors/MMFormRichTextViewer.qml index b2eb6fe4e..5c04f7689 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 @@ -22,28 +25,28 @@ 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 { 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/inputs/MMQrCodeEditor.qml b/app/qml/form/editors/MMFormScannerEditor.qml similarity index 55% rename from app/qml/inputs/MMQrCodeEditor.qml rename to app/qml/form/editors/MMFormScannerEditor.qml index 32a9aa1dc..05f00b524 100644 --- a/app/qml/inputs/MMQrCodeEditor.qml +++ b/app/qml/form/editors/MMFormScannerEditor.qml @@ -10,21 +10,54 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../components" - -MMAbstractEditor { +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 - 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 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 : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage hasFocus: textField.activeFocus - enabled: !root.isReadOnly + enabled: !_fieldIsReadOnly + + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } content: TextField { id: textField @@ -32,15 +65,19 @@ MMAbstractEditor { 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 - } + + background: Rectangle { color: __style.transparentColor } + + onTextEdited: root.editorValueChanged( textField.text, textField.text === "" ) } rightAction: MMIcon { @@ -55,8 +92,10 @@ MMAbstractEditor { onRightActionClicked: { if ( !root.enabled ) return + if (!__inputUtils.acquireCameraPermission()) return + codeScannerLoader.active = true codeScannerLoader.focus = true } @@ -75,11 +114,13 @@ MMAbstractEditor { MMCodeScanner { focus: true - Component.onCompleted: open() onClosed: codeScannerLoader.active = false + + Component.onCompleted: open() + onScanFinished: function( captured ) { root.editorValueChanged( captured, false ) - codeScannerLoader.active = false + close() } } } diff --git a/app/qml/form/editors/MMFormSliderEditor.qml b/app/qml/form/editors/MMFormSliderEditor.qml new file mode 100644 index 000000000..7941fe5d3 --- /dev/null +++ b/app/qml/form/editors/MMFormSliderEditor.qml @@ -0,0 +1,148 @@ +/*************************************************************************** + * * + * 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 QtQuick.Layouts + +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 _fieldValue: parent.fieldValue + 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 + + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + + signal editorValueChanged( var newValue, var isNull ) + signal rememberValueBoxClicked( bool state ) + + title: _fieldShouldShowTitle ? _fieldTitle : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage + + hasFocus: slider.activeFocus + enabled: !_fieldIsReadOnly + + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + + content: Item { + id: input + + anchors.fill: parent + + RowLayout { + id: rowLayout + + anchors.fill: parent + + Text { + id: valueLabel + + Layout.preferredWidth: rowLayout.width / 2 - root.spacing + Layout.maximumWidth: rowLayout.width / 2 - root.spacing + Layout.preferredHeight: input.height + Layout.maximumHeight: input.height + + elide: Text.ElideRight + text: Number( slider.value ).toFixed( internal.precision ).toLocaleString( root.locale ) + ' ' + internal.suffix + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + font: __style.p5 + color: root.enabled ? __style.nightColor : __style.mediumGreenColor + } + + Slider { + id: slider + + Layout.fillWidth: true + Layout.maximumHeight: input.height + Layout.preferredHeight: input.height + + to: internal.to + from: internal.from + stepSize: internal.step + value: root._fieldValue ? root._fieldValue : 0 + + onValueChanged: root.editorValueChanged( slider.value, false ) + + background: Rectangle { + x: slider.leftPadding + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + width: slider.availableWidth + height: 4 * __dp + radius: 2 * __dp + + color: __style.lightGreenColor + } + + handle: Rectangle { + x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + width: 20 * __dp + height: width + radius: height / 2 + + color: root.enabled ? __style.forestColor : __style.lightGreenColor + } + } + } + } + + 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/editor/inputspacer.qml b/app/qml/form/editors/MMFormSpacer.qml similarity index 59% rename from app/qml/editor/inputspacer.qml rename to app/qml/form/editors/MMFormSpacer.qml index 01b6bb49c..2a4c3a97f 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 : __style.transparentColor + anchors { right: parent.right left: parent.left } + } diff --git a/app/qml/form/editors/MMFormSwitchEditor.qml b/app/qml/form/editors/MMFormSwitchEditor.qml new file mode 100644 index 000000000..c80adb6d1 --- /dev/null +++ b/app/qml/form/editors/MMFormSwitchEditor.qml @@ -0,0 +1,104 @@ +/*************************************************************************** + * * + * 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" +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 _field: parent.field + property var _fieldValue: parent.fieldValue + 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 + + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + + signal editorValueChanged( var newValue, var isNull ) + signal rememberValueBoxClicked( bool state ) + + title: _fieldShouldShowTitle ? _fieldTitle : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage + + enabled: !_fieldIsReadOnly + + hasFocus: rightSwitch.focus + + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + + content: Text { + id: textField + + 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 + + width: 50 + height: parent.height + x: -30 * __dp + + 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 + + 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 + } +} diff --git a/app/qml/form/editors/MMFormTextEditor.qml b/app/qml/form/editors/MMFormTextEditor.qml new file mode 100644 index 000000000..b9da08df4 --- /dev/null +++ b/app/qml/form/editors/MMFormTextEditor.qml @@ -0,0 +1,90 @@ +/*************************************************************************** + * * + * 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 + + 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 + + showClearIcon: false + + enabled: !_fieldIsReadOnly + textFieldComponent.readOnly: _fieldIsReadOnly + textFieldComponent.inputMethodHints: root._field.isNumeric ? Qt.ImhFormattedNumbersOnly : Qt.ImhNone + + title: _fieldShouldShowTitle ? _fieldTitle : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage + + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + + textFieldComponent.maximumLength: { + if ( ( !root._field.isNumeric ) && ( root._field.length > 0 ) ) { + return root._field.length + } + return internal.textMaxCharactersLimit + } + + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + + 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 + // Could in theory be fixed with `inputMethodComposing` TextInput property instead + textFieldComponent.onPreeditTextChanged: if ( __androidUtils.isAndroid ) Qt.inputMethod.commit() + + QtObject { + id: internal + + property int textMaxCharactersLimit: 32767 // Qt default + } +} diff --git a/app/qml/form/editors/MMFormTextMultilineEditor.qml b/app/qml/form/editors/MMFormTextMultilineEditor.qml new file mode 100644 index 000000000..134a87b80 --- /dev/null +++ b/app/qml/form/editors/MMFormTextMultilineEditor.qml @@ -0,0 +1,103 @@ +/*************************************************************************** + * * + * 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" + +/* + * 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 _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 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 : "" + + warningMsg: _fieldWarningMessage + errorMsg: _fieldErrorMessage + + enabled: !_fieldIsReadOnly + + 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 + return realHeight < minHeight ? minHeight : realHeight + } + + content: TextArea { + id: textArea + + property real verticalPadding: 11 * __dp + + y: textArea.verticalPadding + 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 === "" ) + + // Could in theory be fixed with `inputMethodComposing` TextInput property instead + onPreeditTextChanged: Qt.inputMethod.commit() // to avoid Android's uncommited text + } + + FontMetrics { + id: metrics + font: textArea.font + } +} diff --git a/app/qml/form/editors/MMFormValueMapEditor.qml b/app/qml/form/editors/MMFormValueMapEditor.qml new file mode 100644 index 000000000..662c4439e --- /dev/null +++ b/app/qml/form/editors/MMFormValueMapEditor.qml @@ -0,0 +1,135 @@ +/*************************************************************************** + * * + * 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 + + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + + signal editorValueChanged( var newValue, bool isNull ) + signal rememberValueBoxClicked( bool state ) + + title: _fieldShouldShowTitle ? _fieldTitle : "" + + dropDownTitle: _fieldTitle + + errorMsg: _fieldErrorMessage + warningMsg: _fieldWarningMessage + + 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 ) ) { + // should not happen... + __inputUtils.log( "Value map", root._fieldTitle + " received unexpected values" ) + return + } + + root.editorValueChanged( selectedFeatures[0], selectedFeatures[0] === null ) + } + + on_FieldValueChanged: { + + if ( _fieldValueIsNull ) { + text = "" + preselectedFeatures = [] + } + + // let's find the new value in the model + for ( let i = 0; i < listModel.count; i++ ) { + let item_i = listModel.get( i ) + + if ( _fieldValue.toString() === item_i.FeatureId.toString() ) { + text = item_i.FeatureTitle + preselectedFeatures = [item_i.FeatureId] + } + } + } + + 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++ ) + { + // 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 ) + + // 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)" ) + } + } +} diff --git a/app/qml/form/editors/MMFormValueRelationEditor.qml b/app/qml/form/editors/MMFormValueRelationEditor.qml new file mode 100644 index 000000000..d7bbc2af1 --- /dev/null +++ b/app/qml/form/editors/MMFormValueRelationEditor.qml @@ -0,0 +1,162 @@ +/*************************************************************************** + * * + * 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" +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 + + property bool _fieldRememberValueSupported: parent.fieldRememberValueSupported + property bool _fieldRememberValueState: parent.fieldRememberValueState + + signal editorValueChanged( var newValue, bool isNull ) + signal rememberValueBoxClicked( bool state ) + + title: _fieldShouldShowTitle ? _fieldTitle : "" + + errorMsg: _fieldErrorMessage + warningMsg: _fieldWarningMessage + + enabled: !_fieldIsReadOnly + + hasCheckbox: _fieldRememberValueSupported + checkboxChecked: _fieldRememberValueState + + on_FieldValueChanged: { + vrModel.pair = root._fieldFeatureLayerPair + } + + onCheckboxCheckedChanged: { + root.rememberValueBoxClicked( checkboxChecked ) + } + + 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 + + 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/inputs/MMAbstractEditor.qml b/app/qml/inputs/MMBaseInput.qml similarity index 97% rename from app/qml/inputs/MMAbstractEditor.qml rename to app/qml/inputs/MMBaseInput.qml index 311edb07c..3d53f1dd4 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 @@ -31,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 @@ -60,7 +62,6 @@ Item { small: true visible: root.hasCheckbox - checked: root.checkboxChecked } Text { id: titleItem diff --git a/app/qml/inputs/MMComboBoxEditor.qml b/app/qml/inputs/MMDropdownInput.qml similarity index 59% rename from app/qml/inputs/MMComboBoxEditor.qml rename to app/qml/inputs/MMDropdownInput.qml index 2fdc39db1..13c033289 100644 --- a/app/qml/inputs/MMComboBoxEditor.qml +++ b/app/qml/inputs/MMDropdownInput.qml @@ -12,19 +12,37 @@ import QtQuick.Controls import QtQuick.Controls.Basic import "../components" -MMAbstractEditor { +/* + * 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 + + property alias dropdownLoader: drawerLoader + + // 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 - required property ListModel featuresModel - required 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 @@ -32,13 +50,26 @@ MMAbstractEditor { 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 + openDrawer() + } + } + } rightAction: MMIcon { @@ -55,12 +86,12 @@ MMAbstractEditor { onRightActionClicked: { if ( !root.enabled ) return - listLoader.active = true - listLoader.focus = true + + openDrawer() } Loader { - id: listLoader + id: drawerLoader asynchronous: true active: false @@ -70,18 +101,26 @@ MMAbstractEditor { Component { id: listComponent - MMComboBoxDrawer { + MMDropdownDrawer { focus: true - model: root.featuresModel + model: root.dataModel title: root.dropDownTitle multiSelect: root.multiSelect - preselectedFeatures: root.preselectedFeatures + withSearchbar: root.withSearchbar + selectedFeatures: root.preselectedFeatures - Component.onCompleted: open() - onClosed: listLoader.active = false - onFeatureClicked: function(selectedFeatures) { - root.featureClicked( selectedFeatures ) + onClosed: drawerLoader.active = false + + onSelectionFinished: function ( selectedFeatures ) { + root.selectionFinished( selectedFeatures ) } + + Component.onCompleted: open() } } + + function openDrawer() { + drawerLoader.active = true + drawerLoader.focus = true + } } diff --git a/app/qml/inputs/MMNumberEditor.qml b/app/qml/inputs/MMNumberEditor.qml deleted file mode 100644 index 8a137bad2..000000000 --- a/app/qml/inputs/MMNumberEditor.qml +++ /dev/null @@ -1,123 +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" - -MMAbstractEditor { - id: root - - property var parentValue: parent.value ?? 0 - property bool parentValueIsNull: parent.valueIsNull ?? false - property bool isReadOnly: parent.readOnly ?? false - - 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 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 )) - - signal editorValueChanged( var newValue, var isNull ) - - enabled: !isReadOnly - hasFocus: numberInput.activeFocus - - leftAction: MMIcon { - id: leftIcon - - height: parent.height - - source: __style.minusIcon - color: enabled ? __style.forestColor : __style.mediumGreenColor - enabled: Number( numberInput.text ) - root.step >= root.from - } - - content: Item { - anchors.fill: parent - Row { - height: parent.height - anchors.horizontalCenter: parent.horizontalCenter - clip: true - - TextField { - id: numberInput - - height: parent.height - - clip: true - text: root.parentValue === undefined || root.parentValueIsNull ? "" : root.parentValue - color: root.enabled ? __style.nightColor : __style.mediumGreenColor - placeholderTextColor: __style.nightAlphaColor - font: __style.p5 - hoverEnabled: true - verticalAlignment: Qt.AlignVCenter - inputMethodHints: Qt.ImhFormattedNumbersOnly - - onTextEdited: { - let val = text.replace( ",", "." ).replace( / /g, '' ) // replace comma with dot - - root.editorValueChanged( val, val === "" ) - } - - background: Rectangle { - color: __style.transparentColor - } - } - - Text { - id: suffix - - text: root.suffix - - visible: root.suffix !== "" && numberInput.text !== "" - - height: parent.height - verticalAlignment: Qt.AlignVCenter - - font: __style.p5 - color: numberInput.color - } - } - } - - rightAction: MMIcon { - id: rightIcon - - height: parent.height - - source: __style.plusIcon - color: enabled ? __style.forestColor : __style.mediumGreenColor - 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 ) { - let incremented = Number( numberInput.text ) + root.step - root.editorValueChanged( incremented.toFixed( root.precision ), false ) - } - } -} 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/MMSliderEditor.qml b/app/qml/inputs/MMSliderEditor.qml deleted file mode 100644 index 35545f411..000000000 --- a/app/qml/inputs/MMSliderEditor.qml +++ /dev/null @@ -1,98 +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 QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import ".." - -MMAbstractEditor { - id: root - - property var parentValue: parent.value - property bool parentValueIsNull: parent.valueIsNull ?? false - property bool isReadOnly: parent.readOnly ?? false - - 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() - - signal editorValueChanged( var newValue, var isNull ) - - hasFocus: slider.activeFocus - - content: Item { - id: input - - anchors.fill: parent - - RowLayout { - id: rowLayout - - anchors.fill: parent - - Text { - id: valueLabel - - Layout.preferredWidth: rowLayout.width / 2 - root.spacing - Layout.maximumWidth: rowLayout.width / 2 - root.spacing - Layout.preferredHeight: input.height - Layout.maximumHeight: input.height - - elide: Text.ElideRight - text: Number( slider.value ).toFixed( precision ).toLocaleString( root.locale ) + root.suffix - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignLeft - font: __style.p5 - color: __style.nightColor - } - - Slider { - id: slider - - Layout.fillWidth: true - Layout.maximumHeight: input.height - Layout.preferredHeight: input.height - - to: root.to - from: root.from - stepSize: root.step - value: root.parentValue ? root.parentValue : 0 - - onValueChanged: { root.editorValueChanged( slider.value, false ); forceActiveFocus() } - - background: Rectangle { - x: slider.leftPadding - y: slider.topPadding + slider.availableHeight / 2 - height / 2 - width: slider.availableWidth - height: 4 * __dp - radius: 2 * __dp - - color: __style.lightGreenColor - } - - handle: Rectangle { - x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) - y: slider.topPadding + slider.availableHeight / 2 - height / 2 - width: 20 * __dp - height: width - radius: height / 2 - - color: root.enabled ? __style.forestColor : __style.lightGreenColor - } - } - } - } -} diff --git a/app/qml/inputs/MMSwitchEditor.qml b/app/qml/inputs/MMSwitchEditor.qml deleted file mode 100644 index bc88f8f9f..000000000 --- a/app/qml/inputs/MMSwitchEditor.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 -import QtQuick.Controls.Basic -import "../components" - -MMAbstractEditor { - id: root - - property var parentValue: parent.value ?? false - property bool parentValueIsNull: parent.valueIsNull ?? false - property bool isReadOnly: parent.readOnly ?? false - - property alias text: textField.text - property alias checked: rightSwitch.checked - - signal editorValueChanged( var newValue, var isNull ) - - hasFocus: rightSwitch.focus - - content: Text { - id: textField - - width: parent.width + rightSwitch.x - anchors.verticalCenter: parent.verticalCenter - - color: root.enabled ? __style.nightColor : __style.mediumGreenColor - font: __style.p5 - elide: Text.ElideRight - } - - rightAction: MMSwitch { - id: rightSwitch - - width: 50 - height: parent.height - x: -30 * __dp - - checked: root.checked - - onCheckedChanged: focus = true - } -} diff --git a/app/qml/inputs/MMTextAreaEditor.qml b/app/qml/inputs/MMTextAreaEditor.qml deleted file mode 100644 index d16d78d6c..000000000 --- a/app/qml/inputs/MMTextAreaEditor.qml +++ /dev/null @@ -1,55 +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" - -MMAbstractEditor { - id: root - - property var parentValue: parent.value ?? "" - property bool parentValueIsNull: parent.valueIsNull ?? false - property bool isReadOnly: parent.readOnly ?? false - - property alias placeholderText: textArea.placeholderText - property alias text: textArea.text - property int minimumRows: 3 - - signal editorValueChanged( var newValue, var isNull ) - - hasFocus: textArea.activeFocus - contentItemHeight: { - const minHeight = 34 * __dp + metrics.height * root.minimumRows - var realHeight = textArea.y + textArea.contentHeight + 2 * textArea.verticalPadding - return realHeight < minHeight ? minHeight : realHeight - } - - content: TextArea { - id: textArea - - property real verticalPadding: 11 * __dp - - y: textArea.verticalPadding - height: contentHeight + textArea.verticalPadding - width: parent.width - - hoverEnabled: true - placeholderTextColor: __style.nightAlphaColor - color: root.enabled ? __style.nightColor : __style.mediumGreenColor - font: __style.p5 - wrapMode: Text.WordWrap - } - - FontMetrics { - id: metrics - font: textArea.font - } -} 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/inputs/MMButtonInputEditor.qml b/app/qml/inputs/MMTextWithButtonInput.qml similarity index 88% rename from app/qml/inputs/MMButtonInputEditor.qml rename to app/qml/inputs/MMTextWithButtonInput.qml index be1e1aa3b..36ae9ec0c 100644 --- a/app/qml/inputs/MMButtonInputEditor.qml +++ b/app/qml/inputs/MMTextWithButtonInput.qml @@ -12,19 +12,22 @@ import QtQuick.Controls import QtQuick.Controls.Basic import "../components" -MMAbstractEditor { +/* + * 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 @@ -35,7 +38,6 @@ MMAbstractEditor { 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 @@ -44,6 +46,8 @@ MMAbstractEditor { background: Rectangle { color: __style.transparentColor } + + onTextEdited: root.textEdited( textField.text ) } rightAction: Button { 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/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 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/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 diff --git a/app/test/testutilsfunctions.cpp b/app/test/testutilsfunctions.cpp index f737751f4..71b56682e 100644 --- a/app/test/testutilsfunctions.cpp +++ b/app/test/testutilsfunctions.cpp @@ -221,11 +221,11 @@ void TestUtilsFunctions::fileExists() void TestUtilsFunctions::loadQmlComponent() { - QUrl dummy = mUtils->getEditorComponentSource( "dummy" ); - QCOMPARE( dummy.path(), QString( "../editor/inputtextedit.qml" ) ); + QUrl dummy = mUtils->getFormEditorType( "dummy" ); + QCOMPARE( dummy.path(), QString( "../editor/MMFormTextEditor.qml" ) ); - QUrl valuemap = mUtils->getEditorComponentSource( "valuemap" ); - QCOMPARE( valuemap.path(), QString( "../editor/inputvaluemap.qml" ) ); + QUrl valuemap = mUtils->getFormEditorType( "valuemap" ); + QCOMPARE( valuemap.path(), QString( "../editor/MMFormValueMapEditor.qml" ) ); } void TestUtilsFunctions::getRelativePath() 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/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 08ad70526..8c4152c14 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 ); @@ -49,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 d0e759ade..1bbe0c009 100644 --- a/gallery/qml.qrc +++ b/gallery/qml.qrc @@ -1,97 +1,110 @@ qml/Main.qml - qml/pages/IconsPage.qml qml/IconBox.qml + qml/EditorItem.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 + qml/pages/GpsInfoPage.qml + qml/pages/InputsPage.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/MMDropdownDrawer.qml ../app/qml/components/MMMapBlurLabel.qml - ../app/qml/inputs/MMQrCodeEditor.qml ../app/qml/components/MMCodeScanner.qml - ../app/qml/map/MMMapScaleBar.qml - ../app/qml/inputs/MMRelationsEditor.qml - ../app/qml/inputs/MMPhotoEditor.qml - qml/pages/FormPage.qml - ../app/qml/form/MMFormTabBar.qml + ../app/qml/components/MMFeaturesListDrawer.qml ../app/qml/components/MMGpsDataDrawer.qml ../app/qml/components/MMGpsDataText.qml ../app/qml/components/MMLine.qml - ../app/qml/components/MMLinkedFeaturesDrawer.qml - qml/pages/GpsInfoPage.qml + ../app/qml/components/MMFeaturesListDrawer.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 + ../app/qml/inputs/MMTextWithButtonInput.qml + + ../app/qml/form/components/MMFormTabBar.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 + ../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 + ../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/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/Main.qml b/gallery/qml/Main.qml index e81ede966..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" @@ -138,10 +142,6 @@ ApplicationWindow { title: "Text areas" source: "TextAreaPage.qml" } - ListElement { - title: "Combo boxes" - source: "ComboBoxPage.qml" - } ListElement { title: "Checks" source: "ChecksPage.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 6dea33250..859971cf7 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 { @@ -64,8 +64,8 @@ Page { usedData: 0.923 } - onPrimaryButtonClicked: visible = false - onSecondaryButtonClicked: visible = false + onPrimaryButtonClicked: close() + onSecondaryButtonClicked: close() } MMDrawer { @@ -77,8 +77,8 @@ 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() } MMGpsDataDrawer { diff --git a/gallery/qml/pages/EditorsPage.qml b/gallery/qml/pages/EditorsPage.qml index 19bb668a0..08ce40b58 100644 --- a/gallery/qml/pages/EditorsPage.qml +++ b/gallery/qml/pages/EditorsPage.qml @@ -12,7 +12,9 @@ import QtQuick.Controls import QtQuick.Controls.Basic import "../../app/qml/inputs" +import "../../app/qml/form/editors" import "../../app/qml/components" +import "../" ScrollView { Column { @@ -20,7 +22,7 @@ ScrollView { spacing: 20 GroupBox { - title: "Items based on MMAbstractEditor" + title: "Items based on MMBaseInput" background: Rectangle { color: "lightGray" border.color: "gray" @@ -37,303 +39,198 @@ ScrollView { MMCheckBox { id: checkbox - text: checked ? "enabled" : "disabled" + text: checked ? "enabled: yes" : "enabled: no" checked: true } - property var featurePair: "parentFeature" - property var associatedRelation: "associatedRelation" - - MMRelationsEditor { - 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 } - MMComboBoxEditor { - title: "MMComboBoxEditor" - placeholderText: "Select one" - dropDownTitle: "Select one" - enabled: checkbox.checked + EditorItem { 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 + fieldTitle: "MMFormRelationEditor" + + 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 } } - MMComboBoxEditor { - title: "MMComboBoxEditor Multi select" - placeholderText: "Select multiple" - dropDownTitle: "Multi select" - enabled: checkbox.checked + EditorItem { 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 + fieldTitle: "MMFormGalleryEditor" + + 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() - } } - MMSliderEditor { - title: "MMSliderEditor" - from: -100 - to: 100 - parentValue: -100 - suffix: " s" + EditorItem { width: parent.width - enabled: checkbox.checked - onEditorValueChanged: function(newValue) { errorMsg = newValue > 0 ? "" : "Set positive value!" } - hasCheckbox: true - checkboxChecked: true - } + height: sliderEditor.height - MMNumberEditor { - 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 } + fieldValue: "100" + fieldConfig: ({Min:-100, Max: 100, Suffix: "s", Precision: 1}) + fieldTitle: "MMFormSliderEditor" + + MMFormSliderEditor { + id: sliderEditor + width: parent.width + onEditorValueChanged: function(newValue, isNull ) { parent.fieldValue = newValue } + } } - MMInputEditor { - title: "MMInputEditor" - parentValue: "Text" - enabled: checkbox.checked + EditorItem { width: parent.width - hasCheckbox: true - checkboxChecked: false + height: numberEditor.height + + fieldValue: "2" + fieldConfig: ({Min: 1.0, Max: 3.0, Precition: 1, Suffix: "s.", Step: 0.1}) + fieldTitle: "MMFormNumberEditor" + + MMFormNumberEditor { + id: numberEditor + width: parent.width + onEditorValueChanged: function(newValue, isNull) { parent.fieldValue = newValue } + } } - MMQrCodeEditor { - title: "MMQrCodeEditor" - placeholderText: "QR code" - warningMsg: text.length > 0 ? "" : "Click to icon and scan the code" - enabled: checkbox.checked + EditorItem { width: parent.width + height: scannerEditor.height + + fieldTitle: "MMFormScannerEditor" - onEditorValueChanged: function(newValue, isNull) { console.log("QR code: " + newValue) } + MMFormScannerEditor { + id: scannerEditor + placeholderText: "QR code" + width: parent.width + onEditorValueChanged: function(newValue, isNull) { parent.fieldValue = newValue } + } } - MMPhotoEditor { - title: "MMPhotoEditor" + EditorItem { width: parent.width - photoUrl: "https://images.pexels.com/photos/615348/forest-fog-sunny-nature-615348.jpeg" + height: photoEditor.height + + fieldConfig: ({RelativeStorage: ""}) + fieldTitle: "MMFormPhotoEditor" - onTrashClicked: console.log("Move to trash") - onContentClicked: console.log("Open photo") + MMFormPhotoEditor { + id: photoEditor + width: parent.width + + onTrashClicked: console.log("Move to trash") + onContentClicked: console.log("Open photo") + } } - MMButtonInputEditor { - title: "MMButtonInputEditor" - placeholderText: "Write something" - text: "Text to copy" - buttonText: "Copy" - enabled: checkbox.checked + EditorItem { width: parent.width - onButtonClicked: console.log("Copy pressed") - buttonEnabled: text.length > 0 + height: textMultilineEditor.height + + fieldTitle: "MMFormTextMultilineEditor" + + MMFormTextMultilineEditor { + id: textMultilineEditor + placeholderText: "Place for multi row text" + width: parent.width + } } - MMButtonInputEditor { - title: "MMButtonInputEditor" - placeholderText: "Píš" - buttonText: "Kopíruj" - enabled: checkbox.checked + EditorItem { width: parent.width - buttonEnabled: text.length > 0 + height: switchEditor.height + + fieldConfig: ({ CheckedState: "checked", UncheckedState: "unchecked"}) + fieldTitle: "MMFormSwitchEditor" + + MMFormSwitchEditor { + id: switchEditor + width: parent.width + } } - MMInputEditor { - title: "MMInputEditor" - placeholderText: "Placeholder" - enabled: checkbox.checked - width: parent.width - warningMsg: text.length > 0 ? "" : "Write something" + Label { + text: "MMFormSpacer - HLine" } - MMTextAreaEditor { - title: "MMTextAreaEditor" - placeholderText: "Place for multi row text" - enabled: checkbox.checked + EditorItem { width: parent.width - warningMsg: text.length > 0 ? "" : "Write something" + height: spacer2.height + + fieldConfig: ({IsHLine: true}) + fieldTitle: "title not shown for spacer" + + MMFormSpacer { + id: spacer2 + width: parent.width + } } - MMPasswordEditor { - 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 - width: parent.width + + Label { + text: "MMFormRichTextViewer - Text" } - MMSwitchEditor { - title: "MMSwitchEditor" - checked: true - text: checked ? "True" : "False" - warningMsg: checked ? "" : "Should be checked :)" - enabled: checkbox.checked + 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 + } } } } - } } diff --git a/gallery/qml/pages/FormPage.qml b/gallery/qml/pages/FormPage.qml index 8c696c6b0..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 @@ -171,10 +172,10 @@ Page { Component { id: fieldDelegate - MMInputEditor { + 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/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!“ " 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