Skip to content

Commit

Permalink
[FEATURE ] allow to use URLs for attribute forms
Browse files Browse the repository at this point in the history
  • Loading branch information
3nids committed May 9, 2018
1 parent d06043f commit 1869929
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 29 deletions.
18 changes: 14 additions & 4 deletions python/core/qgseditformconfig.sip.in
Expand Up @@ -64,6 +64,12 @@ Constructor for TabData
CodeSourceEnvironment
};

enum FormPath
{
Original,
LocalCopy
};

QgsEditFormConfig( const QgsEditFormConfig &o );
%Docstring
Copy constructor
Expand Down Expand Up @@ -109,17 +115,21 @@ Get the active layout style for the attribute editor for this layer
Set the active layout style for the attribute editor for this layer
%End

QString uiForm() const;
QString uiForm( FormPath path = LocalCopy ) const;
%Docstring
Get path to the .ui form. Only meaningful with EditorLayout.UiFileLayout.
Get path to the .ui form. Only meaningful with EditorLayout.UiFileLayout
If the form is from a URL and ``path`` is Original, the original URL
of the UI form is returned instead of the local copy.
%End

void setUiForm( const QString &ui );
bool setUiForm( const QString &ui, QString *errMsg /Out/ = 0 );
%Docstring
Set path to the .ui form.
When a string is provided, the layout style will be set to EditorLayout.UiFileLayout,
When a string is provided in ``ui``, the layout style will be set to EditorLayout.UiFileLayout,
if an empty or a null string is provided, the layout style will be set to
EditorLayout.GeneratedLayout.
If ``ui`` is a URL, a local copy of the file will be made and will be used to create the forms
``context`` is provided to save error messages
%End

bool setWidgetConfig( const QString &widgetName, const QVariantMap &config );
Expand Down
10 changes: 10 additions & 0 deletions python/core/qgsproject.sip.in
Expand Up @@ -930,6 +930,16 @@ provider.
Returns the current auxiliary storage.

.. versionadded:: 3.0
%End

void addUiFormLocalCopy( QTemporaryFile *file );
%Docstring
Save a pointer to temporary file to keep local copy
available of downloaded UI forms during project's life

:param file: the pointer to the temporary file

.. versionadded:: 3.2
%End

const QgsProjectMetadata &metadata() const;
Expand Down
7 changes: 5 additions & 2 deletions src/app/qgsattributesformproperties.cpp
Expand Up @@ -17,6 +17,7 @@
#include "qgsattributetypedialog.h"
#include "qgsattributerelationedit.h"
#include "qgsattributesforminitcode.h"
#include "qgisapp.h"

QgsAttributesFormProperties::QgsAttributesFormProperties( QgsVectorLayer *layer, QWidget *parent )
: QWidget( parent )
Expand Down Expand Up @@ -168,7 +169,7 @@ void QgsAttributesFormProperties::initLayoutConfig()
mEditorLayoutComboBox_currentIndexChanged( mEditorLayoutComboBox->currentIndex() );

QgsEditFormConfig cfg = mLayer->editFormConfig();
mEditFormLineEdit->setText( cfg.uiForm() );
mEditFormLineEdit->setText( cfg.uiForm( QgsEditFormConfig::Original ) );
}

void QgsAttributesFormProperties::initInitPython()
Expand Down Expand Up @@ -683,7 +684,9 @@ void QgsAttributesFormProperties::apply()
editFormConfig.addTab( createAttributeEditorWidget( tabItem, nullptr, false ) );
}

editFormConfig.setUiForm( mEditFormLineEdit->text() );
QString *errMsg = new QString();
if ( !editFormConfig.setUiForm( mEditFormLineEdit->text(), errMsg ) )
QgisApp::instance()->messageBar()->pushMessage( *errMsg, Qgis::Warning );

editFormConfig.setLayout( ( QgsEditFormConfig::EditorLayout ) mEditorLayoutComboBox->currentIndex() );

Expand Down
70 changes: 63 additions & 7 deletions src/core/qgseditformconfig.cpp
Expand Up @@ -14,6 +14,7 @@
***************************************************************************/
#include "qgseditformconfig_p.h"
#include "qgseditformconfig.h"
#include "qgsnetworkcontentfetcher.h"
#include "qgspathresolver.h"
#include "qgsproject.h"
#include "qgsreadwritecontext.h"
Expand Down Expand Up @@ -146,22 +147,75 @@ void QgsEditFormConfig::setLayout( QgsEditFormConfig::EditorLayout editorLayout
d->mConfiguredRootContainer = true;
}

QString QgsEditFormConfig::uiForm() const
QString QgsEditFormConfig::uiForm( FormPath path ) const
{
return d->mUiFormPath;
if ( path == Original && !d->mUiFormUrl.isEmpty() )
return d->mUiFormUrl;
else
return d->mUiFormPath;
}

void QgsEditFormConfig::setUiForm( const QString &ui )
bool QgsEditFormConfig::setUiForm( const QString &ui, QString *errMsg )
{
if ( ui.isEmpty() || ui.isNull() )
bool success = false;

if ( !ui.isEmpty() && ui == d->mUiFormUrl && !d->mUiFormPath.isEmpty() )
{
// do not download again if URL did not change and was correctly loaded before
return success;
}

// if the ui points to a URL make a local copy
QString formPath = ui;
QString formUrl = QString();
if ( !ui.isEmpty() && !QUrl::fromUserInput( ui ).isLocalFile() )
{
formPath = QString();
formUrl = ui;

QgsNetworkContentFetcher fetcher;
QEventLoop loop;
QObject::connect( &fetcher, &QgsNetworkContentFetcher::finished, &loop, &QEventLoop::quit );
fetcher.fetchContent( QUrl( ui ) );

//wait until form is fetched
loop.exec( QEventLoop::ExcludeUserInputEvents );

QNetworkReply *reply = fetcher.reply();
if ( reply )
{
QTemporaryFile *localFile = new QTemporaryFile( QStringLiteral( "XXXXXX.ui" ) );
if ( localFile->open() )
{
localFile->write( reply->readAll() );
localFile->close();
success = true;
QgsProject::instance()->addUiFormLocalCopy( localFile );
formPath = localFile->fileName();
}
}
if ( !success && errMsg )
{
*errMsg = QString( "Could not load UI from %1" ).arg( ui );
}
}
else
{
success = true;
}

if ( formPath.isEmpty() )
{
setLayout( GeneratedLayout );
}
else
{
setLayout( UiFileLayout );
}
d->mUiFormPath = ui;
d->mUiFormPath = formPath;
d->mUiFormUrl = formUrl;

return success;
}

bool QgsEditFormConfig::readOnly( int idx ) const
Expand Down Expand Up @@ -268,7 +322,9 @@ void QgsEditFormConfig::readXml( const QDomNode &node, QgsReadWriteContext &cont
if ( !editFormNode.isNull() )
{
QDomElement e = editFormNode.toElement();
d->mUiFormPath = context.pathResolver().readPath( e.text() );
QString *errMsg = new QString();
if ( !setUiForm( context.pathResolver().readPath( e.text() ), errMsg ) )
context.pushMessage( *errMsg, Qgis::Warning );
}

QDomNode editFormInitNode = node.namedItem( QStringLiteral( "editforminit" ) );
Expand Down Expand Up @@ -396,7 +452,7 @@ void QgsEditFormConfig::writeXml( QDomNode &node, const QgsReadWriteContext &con
QDomDocument doc( node.ownerDocument() );

QDomElement efField = doc.createElement( QStringLiteral( "editform" ) );
QDomText efText = doc.createTextNode( context.pathResolver().writePath( uiForm() ) );
QDomText efText = doc.createTextNode( context.pathResolver().writePath( uiForm( Original ) ) );
efField.appendChild( efText );
node.appendChild( efField );

Expand Down
25 changes: 20 additions & 5 deletions src/core/qgseditformconfig.h
Expand Up @@ -25,8 +25,8 @@
#include <QDomDocument>

#include "qgsattributeeditorelement.h"
#include "qgsreadwritecontext.h"

class QgsReadWriteContext;
class QgsRelationManager;
class QgsEditFormConfigPrivate;

Expand Down Expand Up @@ -93,6 +93,15 @@ class CORE_EXPORT QgsEditFormConfig
CodeSourceEnvironment = 3 //!< Use the Python code available in the Python environment
};

/**
* The FormPath enum determins the path of the custom UI form
*/
enum FormPath
{
Original, //!< User entered directory or URL
LocalCopy //!< If the Original is an URL, this is for the local copy of the file
};

/**
* Copy constructor
*
Expand Down Expand Up @@ -135,16 +144,22 @@ class CORE_EXPORT QgsEditFormConfig
//! Set the active layout style for the attribute editor for this layer
void setLayout( EditorLayout editorLayout );

//! Get path to the .ui form. Only meaningful with EditorLayout::UiFileLayout.
QString uiForm() const;
/**
* \brief Get path to the .ui form. Only meaningful with EditorLayout::UiFileLayout
* If the form is from a URL and \a path is Original, the original URL
* of the UI form is returned instead of the local copy.
*/
QString uiForm( FormPath path = LocalCopy ) const;

/**
* Set path to the .ui form.
* When a string is provided, the layout style will be set to EditorLayout::UiFileLayout,
* When a string is provided in \a ui, the layout style will be set to EditorLayout::UiFileLayout,
* if an empty or a null string is provided, the layout style will be set to
* EditorLayout::GeneratedLayout.
* If \a ui is a URL, a local copy of the file will be made and will be used to create the forms
* \a context is provided to save error messages
*/
void setUiForm( const QString &ui );
bool setUiForm( const QString &ui, QString *errMsg SIP_OUT = nullptr );

/**
* Set the editor widget config for a widget which is not for a simple field.
Expand Down
4 changes: 3 additions & 1 deletion src/core/qgseditformconfig_p.h
Expand Up @@ -65,8 +65,10 @@ class QgsEditFormConfigPrivate : public QSharedData
//! Defines the default layout to use for the attribute editor (Drag and drop, UI File, Generated)
QgsEditFormConfig::EditorLayout mEditorLayout = QgsEditFormConfig::GeneratedLayout;

//! Init form instance
//! Path to the UI form
QString mUiFormPath;
//! URL of the UI form if taken from the web
QString mUiFormUrl;
//! Name of the Python form init function
QString mInitFunction;
//! Path of the Python external file to be loaded
Expand Down
5 changes: 5 additions & 0 deletions src/core/qgsproject.cpp
Expand Up @@ -2687,3 +2687,8 @@ void QgsProject::setRequiredLayers( const QSet<QgsMapLayer *> &layers )
}
writeEntry( QStringLiteral( "RequiredLayers" ), QStringLiteral( "Layers" ), layerIds );
}

void QgsProject::addUiFormLocalCopy( QTemporaryFile *file )
{
mUiFormLocalCopies.append( file );
}
3 changes: 3 additions & 0 deletions src/core/qgsproject.h
Expand Up @@ -1351,6 +1351,9 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera

QgsCoordinateTransformContext mTransformContext;

// local temporary copies of downloaded UI form files
QList<QPointer<QTemporaryFile>> mUiFormLocalCopies;

QgsProjectMetadata mMetadata;

friend class QgsProjectDirtyBlocker;
Expand Down
24 changes: 14 additions & 10 deletions src/gui/qgsattributeform.cpp
Expand Up @@ -1117,23 +1117,27 @@ void QgsAttributeForm::init()
if ( mContext.allowCustomUi() && mLayer->editFormConfig().layout() == QgsEditFormConfig::UiFileLayout &&
!mLayer->editFormConfig().uiForm().isEmpty() )
{
QFile file( mLayer->editFormConfig().uiForm() );
QgsDebugMsg( QString( "loading form: %1" ).arg( mLayer->editFormConfig().uiForm() ) );
QFile file( mLayer->editFormConfig().uiForm( QgsEditFormConfig::LocalCopy ) );

if ( file.open( QFile::ReadOnly ) )
{
QUiLoader loader;

QFileInfo fi( mLayer->editFormConfig().uiForm() );
QFileInfo fi( file );
loader.setWorkingDirectory( fi.dir() );
formWidget = loader.load( &file, this );
formWidget->setWindowFlags( Qt::Widget );
layout->addWidget( formWidget );
formWidget->show();
file.close();
mButtonBox = findChild<QDialogButtonBox *>();
createWrappers();

formWidget->installEventFilter( this );
if ( formWidget )
{
formWidget->setWindowFlags( Qt::Widget );
layout->addWidget( formWidget );
formWidget->show();
file.close();
mButtonBox = findChild<QDialogButtonBox *>();
createWrappers();

formWidget->installEventFilter( this );
}
}
}

Expand Down

0 comments on commit 1869929

Please sign in to comment.