diff --git a/app/android/src/uk/co/lutraconsulting/MMActivity.java b/app/android/src/uk/co/lutraconsulting/MMActivity.java index cb26be66a..988530b2d 100644 --- a/app/android/src/uk/co/lutraconsulting/MMActivity.java +++ b/app/android/src/uk/co/lutraconsulting/MMActivity.java @@ -201,6 +201,7 @@ public String importImage(Uri imageUri, String targetPath) { String fileName = getFileName( imageUri ); File newCopyFile = new File( targetPath + "/" + fileName ); try { + newCopyFile.getParentFile().mkdirs(); newCopyFile.createNewFile(); InputStream fileStream = getContentResolver().openInputStream( imageUri ); copyFile( fileStream, newCopyFile ); diff --git a/app/attributes/attributecontroller.cpp b/app/attributes/attributecontroller.cpp index 6c071485a..2acebbe4c 100644 --- a/app/attributes/attributecontroller.cpp +++ b/app/attributes/attributecontroller.cpp @@ -612,7 +612,8 @@ void AttributeController::updateOnFeatureChange() const QVariant newVal = feature.attribute( fieldIndex ); mFormItems[itemData->id()]->setOriginalValue( newVal ); mFormItems[itemData->id()]->setRawValue( newVal ); // we need to set raw value as well, as we use it in form now - if ( mRememberAttributesController && isNewFeature() ) // this is a new feature + itemData->setReusedValue( false ); + if ( mRememberAttributesController && isNewFeature() && newVal.toString().isEmpty() ) { QVariant rememberedValue; bool shouldUseRememberedValue = mRememberAttributesController->rememberedValue( @@ -622,8 +623,35 @@ void AttributeController::updateOnFeatureChange() ); if ( shouldUseRememberedValue ) { - mFeatureLayerPair.featureRef().setAttribute( fieldIndex, rememberedValue ); - itemData->setRawValue( rememberedValue ); + QVariant valueToUse = rememberedValue; + + if ( itemData->editorWidgetType() == QStringLiteral( "ExternalResource" ) && !rememberedValue.toString().isEmpty() ) + { + const QVariantMap config = itemData->editorWidgetConfig(); + const FeatureLayerPair parentPair = mParentController ? mParentController->featureLayerPair() : FeatureLayerPair(); + const QString targetDir = InputUtils::resolveTargetDir( QgsProject::instance()->homePath(), config, mFeatureLayerPair, parentPair, QgsProject::instance() ); + const QString prefix = InputUtils::resolvePrefixForRelativePath( config[ QStringLiteral( "RelativeStorage" ) ].toInt(), QgsProject::instance()->homePath(), targetDir ); + const QString src = InputUtils::getAbsolutePath( rememberedValue.toString(), prefix ); + const QFileInfo fi( src ); + + static const QRegularExpression trailingCounter( QStringLiteral( "\\s\\(\\d+\\)$" ) ); + QString baseName = fi.completeBaseName(); + baseName.remove( trailingCounter ); + const QString canonicalName = fi.suffix().isEmpty() ? baseName : QStringLiteral( "%1.%2" ).arg( baseName, fi.suffix() ); + + const QString dst = CoreUtils::findUniquePath( InputUtils::getAbsolutePath( canonicalName, targetDir ) ); + + if ( InputUtils::copyFile( src, dst ) ) + { + valueToUse = InputUtils::getRelativePath( dst, prefix ); + itemData->setReusedCopyPath( dst ); + } + } + + mFeatureLayerPair.featureRef().setAttribute( fieldIndex, valueToUse ); + itemData->setRawValue( valueToUse ); + itemData->setOriginalValue( valueToUse ); + itemData->setReusedValue( true ); } } } @@ -791,7 +819,8 @@ void AttributeController::recalculateDefaultValues( bool shouldApplyDefaultValue = !defaultDefinition.expression().isEmpty() && - ( isFirstUpdateOfNewFeature || ( isFormValueChange && defaultDefinition.applyOnUpdate() ) ); + ( isFirstUpdateOfNewFeature || ( isFormValueChange && defaultDefinition.applyOnUpdate() ) ) && + !item->isReusedValue(); if ( shouldApplyDefaultValue ) { @@ -1211,6 +1240,8 @@ bool AttributeController::deleteFeature() bool AttributeController::rollback() { + discardReusedPhotoCopies( true ); + if ( !mFeatureLayerPair.layer() ) return false; @@ -1293,6 +1324,11 @@ bool AttributeController::save() disconnect( mFeatureLayerPair.layer(), &QgsVectorLayer::featureAdded, this, &AttributeController::onFeatureAdded ); } + if ( rv ) + { + discardReusedPhotoCopies( false ); + } + // Store the feature attributes for future use if ( featureIsNew && mRememberAttributesController ) { @@ -1497,6 +1533,7 @@ bool AttributeController::setFormValue( const QUuid &id, QVariant value ) QgsField field = item->field(); QVariant val( value ); + item->setReusedValue( false ); item->setRawValue( val ); emit formDataChanged( item->id(), { AttributeFormModel::RawValue } ); @@ -1576,6 +1613,36 @@ void AttributeController::onFeatureAdded( QgsFeatureId newFeatureId ) emit featureIdChanged(); } +void AttributeController::discardReusedPhotoCopies( bool force ) +{ + QMap>::const_iterator formItemsIterator = mFormItems.constBegin(); + while ( formItemsIterator != mFormItems.constEnd() ) + { + std::shared_ptr item = formItemsIterator.value(); + const QString copyPath = item->reusedCopyPath(); + if ( !copyPath.isEmpty() ) + { + bool stillReferenced = false; + if ( !force ) + { + const QVariantMap config = item->editorWidgetConfig(); + const FeatureLayerPair parentPair = mParentController ? mParentController->featureLayerPair() : FeatureLayerPair(); + const QString targetDir = InputUtils::resolveTargetDir( QgsProject::instance()->homePath(), config, mFeatureLayerPair, parentPair, QgsProject::instance() ); + const QString prefix = InputUtils::resolvePrefixForRelativePath( config[ QStringLiteral( "RelativeStorage" ) ].toInt(), QgsProject::instance()->homePath(), targetDir ); + const QString currentPath = InputUtils::getAbsolutePath( mFeatureLayerPair.feature().attribute( item->fieldIndex() ).toString(), prefix ); + stillReferenced = ( currentPath == copyPath ); + } + + if ( force || !stillReferenced ) + { + InputUtils::removeFile( copyPath ); + } + item->setReusedCopyPath( QString() ); + } + ++formItemsIterator; + } +} + void AttributeController::renamePhotos() { const QStringList photoNameFormat = QgsProject::instance()->entryList( QStringLiteral( "Mergin" ), QStringLiteral( "PhotoNaming/%1" ).arg( mFeatureLayerPair.layer()->id() ) ); @@ -1608,7 +1675,7 @@ void AttributeController::renamePhotos() continue; } - if ( item->originalValue() != mFeatureLayerPair.feature().attribute( item->fieldIndex() ) ) + if ( item->isReusedValue() || item->originalValue() != mFeatureLayerPair.feature().attribute( item->fieldIndex() ) ) { const QString expString = QgsProject::instance()->readEntry( QStringLiteral( "Mergin" ), QStringLiteral( "PhotoNaming/%1/%2" ).arg( mFeatureLayerPair.layer()->id(), field.name() ) ); QgsExpression exp( expString ); @@ -1656,6 +1723,7 @@ void AttributeController::renamePhotos() { const QString newValue = InputUtils::getRelativePath( dst, prefix ); setFormValue( item->id(), newValue ); + item->setReusedCopyPath( QString() ); expressionContext.setFeature( featureLayerPair().featureRef() ); } else diff --git a/app/attributes/attributecontroller.h b/app/attributes/attributecontroller.h index 9915b3e46..269b9b2bd 100644 --- a/app/attributes/attributecontroller.h +++ b/app/attributes/attributecontroller.h @@ -222,6 +222,7 @@ class AttributeController : public QObject */ bool allowTabs( QgsAttributeEditorContainer *container ); + void discardReusedPhotoCopies( bool force ); //! renames photos if necessary void renamePhotos(); //! save temporary sketched image to original image diff --git a/app/attributes/attributedata.cpp b/app/attributes/attributedata.cpp index a9e761cda..c19124789 100644 --- a/app/attributes/attributedata.cpp +++ b/app/attributes/attributedata.cpp @@ -300,6 +300,26 @@ void FormItem::setOriginalValue( const QVariant &originalValue ) mOriginalValue = originalValue; } +bool FormItem::isReusedValue() const +{ + return mIsReusedValue; +} + +void FormItem::setReusedValue( bool reused ) +{ + mIsReusedValue = reused; +} + +QString FormItem::reusedCopyPath() const +{ + return mReusedCopyPath; +} + +void FormItem::setReusedCopyPath( const QString &path ) +{ + mReusedCopyPath = path; +} + QgsRelation FormItem::relation() const { return mRelation; diff --git a/app/attributes/attributedata.h b/app/attributes/attributedata.h index 9a5178be3..f9964543c 100644 --- a/app/attributes/attributedata.h +++ b/app/attributes/attributedata.h @@ -152,6 +152,12 @@ class FormItem QVariant rawValue() const; void setRawValue( const QVariant &rawValue ); + bool isReusedValue() const; + void setReusedValue( bool reused ); + + QString reusedCopyPath() const; + void setReusedCopyPath( const QString &path ); + QgsRelation relation() const; QString fieldError() const; @@ -178,6 +184,8 @@ class FormItem bool mVisible = false; QVariant mOriginalValue; // original unmodified value QVariant mRawValue; + bool mIsReusedValue = false; + QString mReusedCopyPath; const QgsRelation mRelation; // Only used for FormItemType::Relation }; diff --git a/app/inpututils.cpp b/app/inpututils.cpp index 49fbb6716..5fd6b5203 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -1018,6 +1018,8 @@ QString InputUtils::resolveTargetDir( const QString &homePath, const QVariantMap { QString result = evaluateExpression( pair, parentPair, activeProject, expression ); sanitizePath( result ); + if ( !result.isEmpty() && !QDir::isAbsolutePath( result ) ) + result = QDir( homePath ).absoluteFilePath( result ); return result; } else @@ -1029,6 +1031,8 @@ QString InputUtils::resolveTargetDir( const QString &homePath, const QVariantMap } else { + if ( !QDir::isAbsolutePath( defaultRoot ) ) + defaultRoot = QDir( homePath ).absoluteFilePath( defaultRoot ); return defaultRoot; } } diff --git a/app/ios/iosviewdelegate.mm b/app/ios/iosviewdelegate.mm index 86d5e243f..3b932df10 100644 --- a/app/ios/iosviewdelegate.mm +++ b/app/ios/iosviewdelegate.mm @@ -94,6 +94,8 @@ - ( void )picker:( PHPickerViewController * )picker didFinishPicking:( NSArray

resolveTargetDir( homePath, config, pair, FeatureLayerPair(), activeProject ); QCOMPARE( resultDir, homePath ); - // case 2: defined default root config, no expression + // case 2: defined default root config as a relative path, no expression - resolved against homePath config.insert( QStringLiteral( "DefaultRoot" ), DEFAULT_ROOT ); QString resultDir2 = mUtils->resolveTargetDir( homePath, config, pair, FeatureLayerPair(), activeProject ); - QCOMPARE( resultDir2, DEFAULT_ROOT ); + QCOMPARE( resultDir2, QStringLiteral( "%1/%2" ).arg( homePath, DEFAULT_ROOT ) ); + config.clear(); + + // case 2b: defined default root config as an already-absolute path, no expression - stays unchanged + config.insert( QStringLiteral( "DefaultRoot" ), ABSOLUTE_DEFAULT_ROOT ); + QString resultDir2b = mUtils->resolveTargetDir( homePath, config, pair, FeatureLayerPair(), activeProject ); + QCOMPARE( resultDir2b, ABSOLUTE_DEFAULT_ROOT ); config.clear(); // case 3: defined expression in config->"PropertyCollection" -> "properties" -> "propertyRootPath" -> "expression"