From b165258e6d852856ab5380b4c0cacf7f89e32480 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Sat, 23 Mar 2019 16:41:16 +0100 Subject: [PATCH] [feature][needs-docs] HTML form widget Shameless clone of QML widget, with some webview quirks. Funded by ARPA Piemonte --- .../qgsattributeeditorelement.sip.in | 45 +++++- .../core/qgswidgetwrapper.sip.in | 3 + python/gui/gui_auto.sip | 1 + src/app/qgsattributesformproperties.cpp | 136 +++++++++++++++++- src/app/qgsattributesformproperties.h | 12 +- src/core/qgsattributeeditorelement.cpp | 30 ++++ src/core/qgsattributeeditorelement.h | 47 +++++- src/core/qgseditformconfig.cpp | 6 + src/gui/CMakeLists.txt | 2 + src/gui/editorwidgets/core/qgswidgetwrapper.h | 3 + src/gui/qgsattributeform.cpp | 20 +++ 11 files changed, 300 insertions(+), 5 deletions(-) diff --git a/python/core/auto_generated/qgsattributeeditorelement.sip.in b/python/core/auto_generated/qgsattributeeditorelement.sip.in index 93c26137e0d..9d7f0a0bfb7 100644 --- a/python/core/auto_generated/qgsattributeeditorelement.sip.in +++ b/python/core/auto_generated/qgsattributeeditorelement.sip.in @@ -48,7 +48,8 @@ layer. AeTypeField, AeTypeRelation, AeTypeInvalid, - AeTypeQmlElement + AeTypeQmlElement, + AeTypeHtmlElement }; QgsAttributeEditorElement( AttributeEditorType type, const QString &name, QgsAttributeEditorElement *parent = 0 ); @@ -401,6 +402,48 @@ The QML code that will be represented within this widget. }; + +class QgsAttributeEditorHtmlElement : QgsAttributeEditorElement +{ +%Docstring +An attribute editor widget that will represent arbitrary HTML code. + +.. versionadded:: 3.10 +%End + +%TypeHeaderCode +#include "qgsattributeeditorelement.h" +%End + public: + + QgsAttributeEditorHtmlElement( const QString &name, QgsAttributeEditorElement *parent ); +%Docstring +Creates a new element which can display HTML + +:param name: The name of the widget +:param parent: The parent (used as container) +%End + + virtual QgsAttributeEditorElement *clone( QgsAttributeEditorElement *parent ) const /Factory/; + + + QString htmlCode() const; +%Docstring +The QML code that will be represented within this widget. + +.. versionadded:: 3.4 +%End + + void setHtmlCode( const QString &htmlCode ); +%Docstring +The HTML code that will be represented within this widget. + +@param htmlCode +%End + +}; + + /************************************************************************ * This file has been generated automatically from * * * diff --git a/python/gui/auto_generated/editorwidgets/core/qgswidgetwrapper.sip.in b/python/gui/auto_generated/editorwidgets/core/qgswidgetwrapper.sip.in index ce5409677cc..3f8fe79c612 100644 --- a/python/gui/auto_generated/editorwidgets/core/qgswidgetwrapper.sip.in +++ b/python/gui/auto_generated/editorwidgets/core/qgswidgetwrapper.sip.in @@ -15,6 +15,7 @@ %ModuleCode #include "qgsrelationwidgetwrapper.h" #include "qgsqmlwidgetwrapper.h" +#include "qgshtmlwidgetwrapper.h" %End class QgsWidgetWrapper : QObject @@ -40,6 +41,8 @@ changed status of the widget will be saved. sipType = sipType_QgsRelationWidgetWrapper; else if ( qobject_cast( sipCpp ) ) sipType = sipType_QgsQmlWidgetWrapper; + else if ( qobject_cast( sipCpp ) ) + sipType = sipType_QgsHtmlWidgetWrapper; else sipType = 0; %End diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip index ada56cad049..ec5e54e6aa7 100644 --- a/python/gui/gui_auto.sip +++ b/python/gui/gui_auto.sip @@ -287,6 +287,7 @@ %Include auto_generated/editorwidgets/qgsdatetimesearchwidgetwrapper.sip %Include auto_generated/editorwidgets/qgsdefaultsearchwidgetwrapper.sip %Include auto_generated/editorwidgets/qgsdoublespinbox.sip +%Include auto_generated/editorwidgets/qgshtmlwidgetwrapper.sip %Include auto_generated/editorwidgets/qgsmultiedittoolbutton.sip %Include auto_generated/editorwidgets/qgsrelationreferencesearchwidgetwrapper.sip %Include auto_generated/editorwidgets/qgsrelationreferencewidget.sip diff --git a/src/app/qgsattributesformproperties.cpp b/src/app/qgsattributesformproperties.cpp index 2c60e430a93..cfc2e39cdb5 100644 --- a/src/app/qgsattributesformproperties.cpp +++ b/src/app/qgsattributesformproperties.cpp @@ -20,8 +20,10 @@ #include "qgisapp.h" #include "qgsfieldcombobox.h" #include "qgsqmlwidgetwrapper.h" +#include "qgshtmlwidgetwrapper.h" #include "qgsapplication.h" #include "qgscolorbutton.h" +#include "qgscodeeditorhtml.h" QgsAttributesFormProperties::QgsAttributesFormProperties( QgsVectorLayer *layer, QWidget *parent ) : QWidget( parent ) @@ -139,14 +141,17 @@ void QgsAttributesFormProperties::initAvailableWidgetsTree() } catitem->setExpanded( true ); - // QML widget + // QML/HTML widget catItemData = DnDTreeItemData( DnDTreeItemData::Container, QStringLiteral( "Other" ), tr( "Other Widgets" ) ); catitem = mAvailableWidgetsTree->addItem( mAvailableWidgetsTree->invisibleRootItem(), catItemData ); DnDTreeItemData itemData = DnDTreeItemData( DnDTreeItemData::QmlWidget, QStringLiteral( "QmlWidget" ), tr( "QML Widget" ) ); itemData.setShowLabel( true ); - mAvailableWidgetsTree->addItem( catitem, itemData ); + + auto itemDataHtml { DnDTreeItemData( DnDTreeItemData::HtmlWidget, QStringLiteral( "HtmlWidget" ), tr( "HTML Widget" ) ) }; + itemDataHtml.setShowLabel( true ); + mAvailableWidgetsTree->addItem( catitem, itemDataHtml ); catitem ->setExpanded( true ); } @@ -478,6 +483,19 @@ QTreeWidgetItem *QgsAttributesFormProperties::loadAttributeEditorTreeItem( QgsAt newWidget = tree->addItem( parent, itemData ); break; } + + case QgsAttributeEditorElement::AeTypeHtmlElement: + { + const QgsAttributeEditorHtmlElement *htmlElementEditor = static_cast( widgetDef ); + DnDTreeItemData itemData = DnDTreeItemData( DnDTreeItemData::HtmlWidget, widgetDef->name(), widgetDef->name() ); + itemData.setShowLabel( widgetDef->showLabel() ); + HtmlElementEditorConfiguration htmlEdConfig; + htmlEdConfig.htmlCode = htmlElementEditor->htmlCode(); + itemData.setHtmlElementEditorConfiguration( htmlEdConfig ); + newWidget = tree->addItem( parent, itemData ); + break; + } + case QgsAttributeEditorElement::AeTypeInvalid: { QgsDebugMsg( QStringLiteral( "Not loading invalid attribute editor type..." ) ); @@ -519,6 +537,12 @@ void QgsAttributesFormProperties::onAttributeSelectionChanged() mAttributeTypeDialog->setVisible( false ); break; } + case DnDTreeItemData::HtmlWidget: + { + mAttributeRelationEdit->setVisible( false ); + mAttributeTypeDialog->setVisible( false ); + break; + } } } @@ -609,6 +633,15 @@ QgsAttributeEditorElement *QgsAttributesFormProperties::createAttributeEditorWid widgetDef = element; break; } + + case DnDTreeItemData::HtmlWidget: + { + QgsAttributeEditorHtmlElement *element = new QgsAttributeEditorHtmlElement( item->text( 0 ), parent ); + element->setHtmlCode( itemData.htmlElementEditorConfiguration().htmlCode ); + widgetDef = element; + break; + } + } widgetDef->setShowLabel( itemData.showLabel() ); @@ -861,6 +894,10 @@ QTreeWidgetItem *DnDTree::addItem( QTreeWidgetItem *parent, QgsAttributesFormPro case QgsAttributesFormProperties::DnDTreeItemData::QmlWidget: //no icon for QmlWidget break; + + case QgsAttributesFormProperties::DnDTreeItemData::HtmlWidget: + //no icon for HtmlWidget + break; } } newItem->setData( 0, QgsAttributesFormProperties::DnDTreeRole, data ); @@ -941,6 +978,11 @@ bool DnDTree::dropMimeData( QTreeWidgetItem *parent, int index, const QMimeData { onItemDoubleClicked( newItem, 0 ); } + + if ( itemElement.type() == QgsAttributesFormProperties::DnDTreeItemData::HtmlWidget ) + { + onItemDoubleClicked( newItem, 0 ); + } } } @@ -1281,6 +1323,85 @@ void DnDTree::onItemDoubleClicked( QTreeWidgetItem *item, int column ) } break; + case QgsAttributesFormProperties::DnDTreeItemData::HtmlWidget: + { + QDialog dlg; + dlg.setWindowTitle( tr( "Configure HTML Widget" ) ); + + QVBoxLayout *mainLayout = new QVBoxLayout(); + QHBoxLayout *htmlLayout = new QHBoxLayout(); + QVBoxLayout *layout = new QVBoxLayout(); + mainLayout->addLayout( htmlLayout ); + htmlLayout->addLayout( layout ); + dlg.setLayout( mainLayout ); + layout->addWidget( baseWidget ); + + QLineEdit *title = new QLineEdit( itemData.name() ); + + //htmlCode + QgsCodeEditorHTML *htmlCode = new QgsCodeEditorHTML( ); + htmlCode->setSizePolicy( QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding ); + htmlCode->setText( itemData.htmlElementEditorConfiguration().htmlCode ); + + QgsHtmlWidgetWrapper *htmlWrapper = new QgsHtmlWidgetWrapper( mLayer, nullptr, this ); + QgsFeature previewFeature; + mLayer->getFeatures().nextFeature( previewFeature ); + + //update preview on text change + connect( htmlCode, &QgsCodeEditorHTML::textChanged, this, [ = ] + { + htmlWrapper->setHtmlCode( htmlCode->text( ) ); + htmlWrapper->reinitWidget(); + htmlWrapper->setFeature( previewFeature ); + } ); + + QgsFieldExpressionWidget *expressionWidget = new QgsFieldExpressionWidget; + expressionWidget->setLayer( mLayer ); + QToolButton *addExpressionButton = new QToolButton(); + addExpressionButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/symbologyAdd.svg" ) ) ); + + connect( addExpressionButton, &QAbstractButton::clicked, this, [ = ] + { + htmlCode->insertText( QStringLiteral( "" ).arg( expressionWidget->expression().replace( '"', QLatin1String( "\\\"" ) ) ) ); + } ); + + layout->addWidget( new QLabel( tr( "Title" ) ) ); + layout->addWidget( title ); + QGroupBox *expressionWidgetBox = new QGroupBox( tr( "HTML Code" ) ); + layout->addWidget( expressionWidgetBox ); + expressionWidgetBox->setLayout( new QHBoxLayout ); + expressionWidgetBox->layout()->addWidget( expressionWidget ); + expressionWidgetBox->layout()->addWidget( addExpressionButton ); + layout->addWidget( htmlCode ); + QScrollArea *htmlPreviewBox = new QScrollArea(); + htmlPreviewBox->setLayout( new QGridLayout ); + htmlPreviewBox->setMinimumWidth( 400 ); + htmlPreviewBox->layout()->addWidget( htmlWrapper->widget() ); + //emit to load preview for the first time + emit htmlCode->textChanged(); + htmlLayout->addWidget( htmlPreviewBox ); + + QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel ); + + connect( buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept ); + connect( buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject ); + + mainLayout->addWidget( buttonBox ); + + if ( dlg.exec() ) + { + QgsAttributesFormProperties::HtmlElementEditorConfiguration htmlEdCfg; + htmlEdCfg.htmlCode = htmlCode->text(); + itemData.setName( title->text() ); + itemData.setHtmlElementEditorConfiguration( htmlEdCfg ); + itemData.setShowLabel( showLabelCheckbox->isChecked() ); + + item->setData( 0, QgsAttributesFormProperties::DnDTreeRole, itemData ); + item->setText( 0, title->text() ); + } + } + break; + case QgsAttributesFormProperties::DnDTreeItemData::Field: { QDialog dlg; @@ -1393,6 +1514,17 @@ void QgsAttributesFormProperties::DnDTreeItemData::setQmlElementEditorConfigurat mQmlElementEditorConfiguration = qmlElementEditorConfiguration; } + +QgsAttributesFormProperties::HtmlElementEditorConfiguration QgsAttributesFormProperties::DnDTreeItemData::htmlElementEditorConfiguration() const +{ + return mHtmlElementEditorConfiguration; +} + +void QgsAttributesFormProperties::DnDTreeItemData::setHtmlElementEditorConfiguration( QgsAttributesFormProperties::HtmlElementEditorConfiguration htmlElementEditorConfiguration ) +{ + mHtmlElementEditorConfiguration = htmlElementEditorConfiguration; +} + QColor QgsAttributesFormProperties::DnDTreeItemData::backgroundColor() const { return mBackgroundColor; diff --git a/src/app/qgsattributesformproperties.h b/src/app/qgsattributesformproperties.h index de3c3429c7c..606f8ba4ca8 100644 --- a/src/app/qgsattributesformproperties.h +++ b/src/app/qgsattributesformproperties.h @@ -74,6 +74,11 @@ class APP_EXPORT QgsAttributesFormProperties : public QWidget, private Ui_QgsAtt QString qmlCode; }; + struct HtmlElementEditorConfiguration + { + QString htmlCode; + }; + class DnDTreeItemData : public QTreeWidgetItem { public: @@ -82,7 +87,8 @@ class APP_EXPORT QgsAttributesFormProperties : public QWidget, private Ui_QgsAtt Field, Relation, Container, - QmlWidget + QmlWidget, + HtmlWidget }; //do we need that @@ -124,6 +130,9 @@ class APP_EXPORT QgsAttributesFormProperties : public QWidget, private Ui_QgsAtt QmlElementEditorConfiguration qmlElementEditorConfiguration() const; void setQmlElementEditorConfiguration( QmlElementEditorConfiguration qmlElementEditorConfiguration ); + HtmlElementEditorConfiguration htmlElementEditorConfiguration() const; + void setHtmlElementEditorConfiguration( HtmlElementEditorConfiguration htmlElementEditorConfiguration ); + QColor backgroundColor() const; void setBackgroundColor( const QColor &backgroundColor ); @@ -138,6 +147,7 @@ class APP_EXPORT QgsAttributesFormProperties : public QWidget, private Ui_QgsAtt QgsOptionalExpression mVisibilityExpression; RelationEditorConfiguration mRelationEditorConfiguration; QmlElementEditorConfiguration mQmlElementEditorConfiguration; + HtmlElementEditorConfiguration mHtmlElementEditorConfiguration; QColor mBackgroundColor; }; diff --git a/src/core/qgsattributeeditorelement.cpp b/src/core/qgsattributeeditorelement.cpp index 4ab73da95e4..e9d62021f9b 100644 --- a/src/core/qgsattributeeditorelement.cpp +++ b/src/core/qgsattributeeditorelement.cpp @@ -189,3 +189,33 @@ QString QgsAttributeEditorQmlElement::typeIdentifier() const { return QStringLiteral( "attributeEditorQmlElement" ); } + +QgsAttributeEditorElement *QgsAttributeEditorHtmlElement::clone( QgsAttributeEditorElement *parent ) const +{ + QgsAttributeEditorHtmlElement *element = new QgsAttributeEditorHtmlElement( name(), parent ); + element->setHtmlCode( mHtmlCode ); + + return element; +} + +QString QgsAttributeEditorHtmlElement::htmlCode() const +{ + return mHtmlCode; +} + +void QgsAttributeEditorHtmlElement::setHtmlCode( const QString &htmlCode ) +{ + mHtmlCode = htmlCode; +} + +void QgsAttributeEditorHtmlElement::saveConfiguration( QDomElement &elem ) const +{ + QDomText codeElem = elem.ownerDocument().createTextNode( mHtmlCode ); + elem.appendChild( codeElem ); +} + +QString QgsAttributeEditorHtmlElement::typeIdentifier() const +{ + return QStringLiteral( "attributeEditorHtmlElement" ); +} + diff --git a/src/core/qgsattributeeditorelement.h b/src/core/qgsattributeeditorelement.h index ef290c14053..903ec87c8ca 100644 --- a/src/core/qgsattributeeditorelement.h +++ b/src/core/qgsattributeeditorelement.h @@ -63,7 +63,8 @@ class CORE_EXPORT QgsAttributeEditorElement SIP_ABSTRACT AeTypeField, //!< A field AeTypeRelation, //!< A relation AeTypeInvalid, //!< Invalid - AeTypeQmlElement //!< A QML element + AeTypeQmlElement, //!< A QML element + AeTypeHtmlElement //!< A HTML element }; /** @@ -470,4 +471,48 @@ class CORE_EXPORT QgsAttributeEditorQmlElement : public QgsAttributeEditorElemen QString mQmlCode; }; + +/** + * \ingroup core + * An attribute editor widget that will represent arbitrary HTML code. + * + * \since QGIS 3.10 + */ +class CORE_EXPORT QgsAttributeEditorHtmlElement : public QgsAttributeEditorElement +{ + public: + + /** + * Creates a new element which can display HTML + * + * \param name The name of the widget + * \param parent The parent (used as container) + */ + QgsAttributeEditorHtmlElement( const QString &name, QgsAttributeEditorElement *parent ) + : QgsAttributeEditorElement( AeTypeHtmlElement, name, parent ) + {} + + QgsAttributeEditorElement *clone( QgsAttributeEditorElement *parent ) const override SIP_FACTORY; + + /** + * The QML code that will be represented within this widget. + * + * \since QGIS 3.4 + */ + QString htmlCode() const; + + /** + * The HTML code that will be represented within this widget. + * + * @param htmlCode + */ + void setHtmlCode( const QString &htmlCode ); + + private: + void saveConfiguration( QDomElement &elem ) const override; + QString typeIdentifier() const override; + QString mHtmlCode; +}; + + #endif // QGSATTRIBUTEEDITORELEMENT_H diff --git a/src/core/qgseditformconfig.cpp b/src/core/qgseditformconfig.cpp index 1d700f3b3bd..20bfad36f0e 100644 --- a/src/core/qgseditformconfig.cpp +++ b/src/core/qgseditformconfig.cpp @@ -582,6 +582,12 @@ QgsAttributeEditorElement *QgsEditFormConfig::attributeEditorElementFromDomEleme qmlElement->setQmlCode( elem.text() ); newElement = qmlElement; } + else if ( elem.tagName() == QLatin1String( "attributeEditorHtmlElement" ) ) + { + QgsAttributeEditorHtmlElement *htmlElement = new QgsAttributeEditorHtmlElement( elem.attribute( QStringLiteral( "name" ) ), parent ); + htmlElement->setHtmlCode( elem.text() ); + newElement = htmlElement; + } if ( newElement ) { diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 35f31930f7f..2058c64ffcf 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -120,6 +120,7 @@ SET(QGIS_GUI_SRCS editorwidgets/qgsexternalresourcewidgetfactory.cpp editorwidgets/qgshiddenwidgetwrapper.cpp editorwidgets/qgshiddenwidgetfactory.cpp + editorwidgets/qgshtmlwidgetwrapper.cpp editorwidgets/qgskeyvaluewidgetfactory.cpp editorwidgets/qgskeyvaluewidgetwrapper.cpp editorwidgets/qgslistwidgetfactory.cpp @@ -681,6 +682,7 @@ SET(QGIS_GUI_MOC_HDRS editorwidgets/qgsexternalresourceconfigdlg.h editorwidgets/qgsexternalresourcewidgetwrapper.h editorwidgets/qgshiddenwidgetwrapper.h + editorwidgets/qgshtmlwidgetwrapper.h editorwidgets/qgskeyvaluewidgetwrapper.h editorwidgets/qgslistwidgetwrapper.h editorwidgets/qgsmultiedittoolbutton.h diff --git a/src/gui/editorwidgets/core/qgswidgetwrapper.h b/src/gui/editorwidgets/core/qgswidgetwrapper.h index f03c0c1d420..037b32be5d5 100644 --- a/src/gui/editorwidgets/core/qgswidgetwrapper.h +++ b/src/gui/editorwidgets/core/qgswidgetwrapper.h @@ -33,6 +33,7 @@ class QgsVectorLayer; % ModuleCode #include "qgsrelationwidgetwrapper.h" #include "qgsqmlwidgetwrapper.h" +#include "qgshtmlwidgetwrapper.h" % End #endif @@ -59,6 +60,8 @@ class GUI_EXPORT QgsWidgetWrapper : public QObject sipType = sipType_QgsRelationWidgetWrapper; else if ( qobject_cast( sipCpp ) ) sipType = sipType_QgsQmlWidgetWrapper; + else if ( qobject_cast( sipCpp ) ) + sipType = sipType_QgsHtmlWidgetWrapper; else sipType = 0; SIP_END diff --git a/src/gui/qgsattributeform.cpp b/src/gui/qgsattributeform.cpp index 7f0fca3e00a..9f729fa0ad6 100644 --- a/src/gui/qgsattributeform.cpp +++ b/src/gui/qgsattributeform.cpp @@ -38,6 +38,7 @@ #include "qgsvectorlayerjoinbuffer.h" #include "qgsvectorlayerutils.h" #include "qgsqmlwidgetwrapper.h" +#include "qgshtmlwidgetwrapper.h" #include "qgsapplication.h" #include "qgsexpressioncontextutils.h" @@ -1841,6 +1842,25 @@ QgsAttributeForm::WidgetInfo QgsAttributeForm::createWidgetFromDef( const QgsAtt break; } + case QgsAttributeEditorElement::AeTypeHtmlElement: + { + const QgsAttributeEditorHtmlElement *elementDef = static_cast( widgetDef ); + + QgsHtmlWidgetWrapper *htmlWrapper = new QgsHtmlWidgetWrapper( mLayer, nullptr, this ); + htmlWrapper->setHtmlCode( elementDef->htmlCode() ); + htmlWrapper->setConfig( mLayer->editFormConfig().widgetConfig( elementDef->name() ) ); + context.setAttributeFormMode( mMode ); + htmlWrapper->setContext( context ); + + mWidgets.append( htmlWrapper ); + + newWidgetInfo.widget = htmlWrapper->widget(); + newWidgetInfo.labelText = elementDef->name(); + newWidgetInfo.labelOnTop = true; + newWidgetInfo.showLabel = widgetDef->showLabel(); + break; + } + default: QgsDebugMsg( QStringLiteral( "Unknown attribute editor widget type encountered..." ) ); break;