Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature form redesign #3037

Merged
merged 30 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1e4e35a
organise inputs and form editors
tomasMizera Jan 31, 2024
3734e89
Create MMTextFormEditor.qml
tomasMizera Jan 31, 2024
80a4f2c
Move private properties in MMNumberFormEditor to private QtObject
tomasMizera Jan 31, 2024
95566d3
Update inputUtils to support text, number and spinner form editors
tomasMizera Jan 31, 2024
3c68d8e
do not deal with unused editor for now
tomasMizera Jan 31, 2024
8e3e0a3
MMScannerFormEditor in the app
tomasMizera Feb 1, 2024
a56de6b
MMTextMultilinFormEditor in the app
tomasMizera Feb 1, 2024
524e2e4
MMValueRelationFormEditor and MMValueMapFormEditor in the app, withou…
tomasMizera Feb 1, 2024
c49faae
Complete MMValueMapFormEditor
tomasMizera Feb 2, 2024
52dfb22
MMCalendarFormEditor in the app + fixed its ANR
tomasMizera Feb 2, 2024
fe5b0c4
Complete MMValueRelationFormEditor
tomasMizera Feb 2, 2024
d0b0dd2
Use new editors in the form
tomasMizera Feb 2, 2024
9ed8dbc
Small fixes and comments
tomasMizera Feb 2, 2024
2703451
MMSwitchFormEditor in the app
tomasMizera Feb 3, 2024
288fa35
Rename editors to better name convention
tomasMizera Feb 3, 2024
8ab6d6c
Update inputstyle to follow the new naming
tomasMizera Feb 3, 2024
399253a
MMFormPhotoEditor in the app + bye bye External Resource Bundle
tomasMizera Feb 4, 2024
1d5f348
Added remember last value option
tomasMizera Feb 5, 2024
667d16b
Feature form redesigned + skeleton for preview panel
tomasMizera Feb 5, 2024
77428d1
Merge branch 'master' into feature-form-redesign
tomasMizera Feb 5, 2024
ae3940c
MMFormRelationEditor in the app (word cloud)
tomasMizera Feb 5, 2024
9bb1aae
MMFormGalleryRow in the app
tomasMizera Feb 5, 2024
ce73c39
Fix test and formatting
tomasMizera Feb 6, 2024
87fba44
Update visual of MMPreviewPanel
tomasMizera Feb 6, 2024
40541d0
Rename nogallery hack
tomasMizera Feb 6, 2024
573a39d
Remove old form editors
tomasMizera Feb 6, 2024
ba955b8
Merge branch 'master' into feature-form-redesign
tomasMizera Feb 6, 2024
184fb6c
fix gallery
PeterPetrik Feb 7, 2024
cf27024
rich text and spacer
PeterPetrik Feb 7, 2024
7f20480
fix styling
PeterPetrik Feb 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions app/androidutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ bool AndroidUtils::requestMediaLocationPermission()
return true;
}

void AndroidUtils::callImagePicker()
void AndroidUtils::callImagePicker( const QString &code )
{
#ifdef ANDROID

Expand All @@ -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();
Expand All @@ -273,14 +275,16 @@ void AndroidUtils::callImagePicker()
#endif
}

void AndroidUtils::callCamera( const QString &targetPath )
void AndroidUtils::callCamera( const QString &targetPath, const QString &code )
{
#ifdef ANDROID
if ( !requestCameraPermission() )
{
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();
Expand Down Expand Up @@ -363,7 +367,7 @@ void AndroidUtils::handleActivityResult( int receiverRequestCode, int resultCode
cursor.callMethod<jboolean>( "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 )
{
Expand All @@ -373,7 +377,7 @@ void AndroidUtils::handleActivityResult( int receiverRequestCode, int resultCode

QString selectedImagePath = "file://" + absolutePath;

emit imageSelected( absolutePath );
emit imageSelected( absolutePath, mLastCode );
}
else
{
Expand Down
11 changes: 7 additions & 4 deletions app/androidutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -74,7 +75,7 @@ class AndroidUtils: public QObject
#endif

signals:
void imageSelected( QString imagePath );
void imageSelected( QString imagePath, QString code );

void bluetoothEnabled( bool state );

Expand All @@ -83,6 +84,8 @@ class AndroidUtils: public QObject

private:

QString mLastCode;

#ifdef ANDROID
QBluetoothLocalDevice mBluetooth;
#endif
Expand Down
2 changes: 1 addition & 1 deletion app/attributes/attributecontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
5 changes: 3 additions & 2 deletions app/attributes/attributeformmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,9 @@ QHash<int, QByteArray> 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" );
Expand Down
6 changes: 6 additions & 0 deletions app/featuresmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ void FeaturesModel::onFutureFinished()
mFeatures << FeatureLayerPair( f, mLayer );
}
emit layerFeaturesCountChanged( layerFeaturesCount() );
emit countChanged( rowCount() );
endResetModel();
mFetchingResults = false;
emit fetchingResultsChanged( mFetchingResults );
Expand Down Expand Up @@ -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 )
Expand Down
8 changes: 8 additions & 0 deletions app/featuresmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 );

Expand All @@ -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 );
Expand Down
108 changes: 65 additions & 43 deletions app/inpututils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1020,80 +1020,102 @@ 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" ) )
{
if ( config.contains( "Style" ) )
{
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<const QgsVectorLayer *>( 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 )
Expand Down
2 changes: 1 addition & 1 deletion app/inpututils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
11 changes: 8 additions & 3 deletions app/ios/iosutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}

Expand All @@ -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 );
}

Expand Down
8 changes: 5 additions & 3 deletions app/ios/iosutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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.
*/
Expand Down
Loading
Loading