Skip to content

Commit

Permalink
[FEATURE][composer] Allow evaluation of QGIS expressions inside html …
Browse files Browse the repository at this point in the history
…item source. Expressions are evaluated before HTML is rendered, allowing the expression results to modify how the HTML content is rendered. Sponsored by City of Uster, Switzerland.
  • Loading branch information
nyalldawson committed Jul 16, 2014
1 parent 5e4510f commit 19708be
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 19 deletions.
20 changes: 20 additions & 0 deletions python/core/composer/qgscomposerhtml.sip
Expand Up @@ -73,6 +73,26 @@ class QgsComposerHtml: QgsComposerMultiFrame
* @note added in 2.5
*/
QString html() const;

/**Returns whether html item will evaluate QGIS expressions prior to rendering
* the HTML content. If set, any content inside [% %] tags will be
* treated as a QGIS expression and evaluated against the current atlas
* feature.
* @returns true if html item will evaluate expressions in the content
* @see setEvaluateExpressions
* @note added in QGIS 2.5
*/
bool evaluateExpressions() const;

/**Sets whether the html item will evaluate QGIS expressions prior to rendering
* the HTML content. If set, any content inside [% %] tags will be
* treated as a QGIS expression and evaluated against the current atlas
* feature.
* @param evaluateExpressions set to true to evaluate expressions in the HTML content
* @see evaluateExpressions
* @note added in QGIS 2.5
*/
void setEvaluateExpressions( bool evaluateExpressions );

QSizeF totalSize() const;
void render( QPainter* p, const QRectF& renderExtent );
Expand Down
115 changes: 111 additions & 4 deletions src/app/composer/qgscomposerhtmlwidget.cpp
Expand Up @@ -18,6 +18,7 @@
#include "qgscomposermultiframecommand.h"
#include "qgscomposerhtml.h"
#include "qgscomposition.h"
#include "qgsexpressionbuilderdialog.h"
#include <QFileDialog>
#include <QSettings>
#include <Qsci/qsciscintilla.h>
Expand Down Expand Up @@ -77,6 +78,7 @@ void QgsComposerHtmlWidget::blockSignals( bool block )
mHtmlEditor->blockSignals( block );
mRadioManualSource->blockSignals( block );
mRadioUrlSource->blockSignals( block );
mEvaluateExpressionsCheckbox->blockSignals( block );
}

void QgsComposerHtmlWidget::on_mUrlLineEdit_editingFinished()
Expand Down Expand Up @@ -133,6 +135,24 @@ void QgsComposerHtmlWidget::on_mResizeModeComboBox_currentIndexChanged( int inde
mAddFramePushButton->setEnabled( mHtml->resizeMode() == QgsComposerMultiFrame::UseExistingFrames );
}

void QgsComposerHtmlWidget::on_mEvaluateExpressionsCheckbox_toggled( bool checked )
{
if ( !mHtml )
{
return;
}

QgsComposition* composition = mHtml->composition();
if ( composition )
{
blockSignals( true );
composition->beginMultiFrameCommand( mHtml, tr( "Evaluate expressions changed" ) );
mHtml->setEvaluateExpressions( checked );
composition->endMultiFrameCommand();
blockSignals( false );
}
}

void QgsComposerHtmlWidget::on_mUseSmartBreaksCheckBox_toggled( bool checked )
{
if ( !mHtml )
Expand All @@ -158,7 +178,15 @@ void QgsComposerHtmlWidget::on_mMaxDistanceSpinBox_valueChanged( double val )
return;
}

mHtml->setMaxBreakDistance( val );
QgsComposition* composition = mHtml->composition();
if ( composition )
{
blockSignals( true );
composition->beginMultiFrameCommand( mHtml, tr( "Page break distance changed" ) );
mHtml->setMaxBreakDistance( val );
composition->endMultiFrameCommand();
blockSignals( false );
}
}

void QgsComposerHtmlWidget::htmlEditorChanged()
Expand All @@ -168,7 +196,16 @@ void QgsComposerHtmlWidget::htmlEditorChanged()
return;
}

mHtml->setHtml( mHtmlEditor->text() );
QgsComposition* composition = mHtml->composition();
if ( composition )
{
blockSignals( true );
composition->beginMultiFrameCommand( mHtml, tr( "HTML changed" ) );
mHtml->setHtml( mHtmlEditor->text() );
composition->endMultiFrameCommand();
blockSignals( false );
}

}

void QgsComposerHtmlWidget::on_mRadioManualSource_clicked( bool checked )
Expand All @@ -178,8 +215,17 @@ void QgsComposerHtmlWidget::on_mRadioManualSource_clicked( bool checked )
return;
}

mHtml->setContentMode( checked ? QgsComposerHtml::ManualHtml : QgsComposerHtml::Url );
QgsComposition* composition = mHtml->composition();
if ( composition )
{
blockSignals( true );
composition->beginMultiFrameCommand( mHtml, tr( "HTML source changed" ) );
mHtml->setContentMode( checked ? QgsComposerHtml::ManualHtml : QgsComposerHtml::Url );
composition->endMultiFrameCommand();
blockSignals( false );
}
mHtmlEditor->setEnabled( checked );
mInsertExpressionButton->setEnabled( checked );
mUrlLineEdit->setEnabled( !checked );
mFileToolButton->setEnabled( !checked );

Expand All @@ -193,14 +239,73 @@ void QgsComposerHtmlWidget::on_mRadioUrlSource_clicked( bool checked )
return;
}

mHtml->setContentMode( checked ? QgsComposerHtml::Url : QgsComposerHtml::ManualHtml );
QgsComposition* composition = mHtml->composition();
if ( composition )
{
blockSignals( true );
composition->beginMultiFrameCommand( mHtml, tr( "HTML source changed" ) );
mHtml->setContentMode( checked ? QgsComposerHtml::Url : QgsComposerHtml::ManualHtml );
composition->endMultiFrameCommand();
blockSignals( false );
}
mHtmlEditor->setEnabled( !checked );
mInsertExpressionButton->setEnabled( !checked );
mUrlLineEdit->setEnabled( checked );
mFileToolButton->setEnabled( checked );

mHtml->loadHtml();
}

void QgsComposerHtmlWidget::on_mInsertExpressionButton_clicked()
{
if ( !mHtml )
{
return;
}

int line = 0;
int index = 0;
QString selText;
if ( mHtmlEditor->hasSelectedText() )
{
selText = mHtmlEditor->selectedText();

// edit the selected expression if there's one
if ( selText.startsWith( "[%" ) && selText.endsWith( "%]" ) )
selText = selText.mid( 2, selText.size() - 4 );
}
else
{
mHtmlEditor->getCursorPosition( &line, &index );
}

// use the atlas coverage layer, if any
QgsVectorLayer* coverageLayer = atlasCoverageLayer();
QgsExpressionBuilderDialog exprDlg( coverageLayer, selText, this );
exprDlg.setWindowTitle( tr( "Insert expression" ) );
if ( exprDlg.exec() == QDialog::Accepted )
{
QString expression = exprDlg.expressionText();
QgsComposition* composition = mHtml->composition();
if ( !expression.isEmpty() && composition )
{
blockSignals( true );
composition->beginMultiFrameCommand( mHtml, tr( "HTML source changed" ) );
if ( mHtmlEditor->hasSelectedText() )
{
mHtmlEditor->replaceSelectedText( "[%" + expression + "%]" );
}
else
{
mHtmlEditor->insertAt( "[%" + expression + "%]", line, index );
}
composition->endMultiFrameCommand();
blockSignals( false );
}
}

}

void QgsComposerHtmlWidget::on_mReloadPushButton_clicked()
{
if ( !mHtml )
Expand Down Expand Up @@ -244,6 +349,7 @@ void QgsComposerHtmlWidget::setGuiElementValues()
blockSignals( true );
mUrlLineEdit->setText( mHtml->url().toString() );
mResizeModeComboBox->setCurrentIndex( mResizeModeComboBox->findData( mHtml->resizeMode() ) );
mEvaluateExpressionsCheckbox->setChecked( mHtml->evaluateExpressions() );
mUseSmartBreaksCheckBox->setChecked( mHtml->useSmartBreaks() );
mMaxDistanceSpinBox->setValue( mHtml->maxBreakDistance() );

Expand All @@ -255,5 +361,6 @@ void QgsComposerHtmlWidget::setGuiElementValues()
mFileToolButton->setEnabled( mHtml->contentMode() == QgsComposerHtml::Url );
mRadioManualSource->setChecked( mHtml->contentMode() == QgsComposerHtml::ManualHtml );
mHtmlEditor->setEnabled( mHtml->contentMode() == QgsComposerHtml::ManualHtml );
mInsertExpressionButton->setEnabled( mHtml->contentMode() == QgsComposerHtml::ManualHtml );
blockSignals( false );
}
2 changes: 2 additions & 0 deletions src/app/composer/qgscomposerhtmlwidget.h
Expand Up @@ -33,11 +33,13 @@ class QgsComposerHtmlWidget: public QgsComposerItemBaseWidget, private Ui::QgsCo
void on_mUrlLineEdit_editingFinished();
void on_mFileToolButton_clicked();
void on_mResizeModeComboBox_currentIndexChanged( int index );
void on_mEvaluateExpressionsCheckbox_toggled( bool checked );
void on_mUseSmartBreaksCheckBox_toggled( bool checked );
void on_mMaxDistanceSpinBox_valueChanged( double val );
void htmlEditorChanged();
void on_mRadioManualSource_clicked( bool checked );
void on_mRadioUrlSource_clicked( bool checked );
void on_mInsertExpressionButton_clicked();

void on_mReloadPushButton_clicked();
void on_mAddFramePushButton_clicked();
Expand Down
60 changes: 58 additions & 2 deletions src/core/composer/qgscomposerhtml.cpp
Expand Up @@ -19,6 +19,7 @@
#include "qgsaddremovemultiframecommand.h"
#include "qgsnetworkaccessmanager.h"
#include "qgsmessagelog.h"
#include "qgsexpression.h"

#include <QCoreApplication>
#include <QPainter>
Expand All @@ -33,8 +34,11 @@ QgsComposerHtml::QgsComposerHtml( QgsComposition* c, bool createUndoCommands ):
mLoaded( false ),
mHtmlUnitsToMM( 1.0 ),
mRenderedPage( 0 ),
mEvaluateExpressions( true ),
mUseSmartBreaks( true ),
mMaxBreakDistance( 10 )
mMaxBreakDistance( 10 ),
mExpressionFeature( 0 ),
mExpressionLayer( 0 )
{
mHtmlUnitsToMM = htmlUnitsToMM();
mWebPage = new QWebPage();
Expand All @@ -45,6 +49,18 @@ QgsComposerHtml::QgsComposerHtml( QgsComposition* c, bool createUndoCommands ):
QObject::connect( mComposition, SIGNAL( itemRemoved( QgsComposerItem* ) ), this, SLOT( handleFrameRemoval( QgsComposerItem* ) ) );
connect( mComposition, SIGNAL( refreshItemsTriggered() ), this, SLOT( loadHtml() ) );
}

if ( mComposition && mComposition->atlasMode() == QgsComposition::PreviewAtlas )
{
//a html item added while atlas preview is enabled needs to have the expression context set,
//otherwise fields in the html aren't correctly evaluated until atlas preview feature changes (#9457)
setExpressionContext( mComposition->atlasComposition().currentFeature(), mComposition->atlasComposition().coverageLayer() );
}

//connect to atlas feature changes
//to update the expression context
connect( &mComposition->atlasComposition(), SIGNAL( featureChanged( QgsFeature* ) ), this, SLOT( refreshExpressionContext() ) );

}

QgsComposerHtml::QgsComposerHtml(): QgsComposerMultiFrame( 0, false ),
Expand All @@ -54,7 +70,9 @@ QgsComposerHtml::QgsComposerHtml(): QgsComposerMultiFrame( 0, false ),
mHtmlUnitsToMM( 1.0 ),
mRenderedPage( 0 ),
mUseSmartBreaks( true ),
mMaxBreakDistance( 10 )
mMaxBreakDistance( 10 ),
mExpressionFeature( 0 ),
mExpressionLayer( 0 )
{
}

Expand All @@ -80,6 +98,12 @@ void QgsComposerHtml::setHtml( const QString html )
mHtml = html;
}

void QgsComposerHtml::setEvaluateExpressions( bool evaluateExpressions )
{
mEvaluateExpressions = evaluateExpressions;
loadHtml();
}

QString QgsComposerHtml::fetchHtml( QUrl url )
{
QUrl nextUrlToFetch = url;
Expand Down Expand Up @@ -151,6 +175,12 @@ void QgsComposerHtml::loadHtml()
break;
}

//evaluate expressions
if ( mEvaluateExpressions )
{
loadedHtml = QgsExpression::replaceExpressionText( loadedHtml, mExpressionFeature, mExpressionLayer );
}

mLoaded = false;
//set html, using the specified url as base if in Url mode
mWebPage->mainFrame()->setHtml( loadedHtml, mContentMode == QgsComposerHtml::Url ? QUrl( mUrl ) : QUrl() );
Expand Down Expand Up @@ -360,6 +390,7 @@ bool QgsComposerHtml::writeXML( QDomElement& elem, QDomDocument & doc, bool igno
htmlElem.setAttribute( "contentMode", QString::number(( int ) mContentMode ) );
htmlElem.setAttribute( "url", mUrl.toString() );
htmlElem.setAttribute( "html", mHtml );
htmlElem.setAttribute( "evaluateExpressions", mEvaluateExpressions ? "true" : "false" );
htmlElem.setAttribute( "useSmartBreaks", mUseSmartBreaks ? "true" : "false" );
htmlElem.setAttribute( "maxBreakDistance", QString::number( mMaxBreakDistance ) );

Expand All @@ -384,6 +415,7 @@ bool QgsComposerHtml::readXML( const QDomElement& itemElem, const QDomDocument&
{
mContentMode = QgsComposerHtml::Url;
}
mEvaluateExpressions = itemElem.attribute( "evaluateExpressions", "true" ) == "true" ? true : false;
mUseSmartBreaks = itemElem.attribute( "useSmartBreaks", "true" ) == "true" ? true : false;
mMaxBreakDistance = itemElem.attribute( "maxBreakDistance", "10" ).toDouble();
mHtml = itemElem.attribute( "html" );
Expand All @@ -399,3 +431,27 @@ bool QgsComposerHtml::readXML( const QDomElement& itemElem, const QDomDocument&
emit changed();
return true;
}

void QgsComposerHtml::setExpressionContext( QgsFeature* feature, QgsVectorLayer* layer )
{
mExpressionFeature = feature;
mExpressionLayer = layer;
}

void QgsComposerHtml::refreshExpressionContext()
{
QgsVectorLayer * vl = 0;
QgsFeature* feature = 0;

if ( mComposition->atlasComposition().enabled() )
{
vl = mComposition->atlasComposition().coverageLayer();
}
if ( mComposition->atlasMode() != QgsComposition::AtlasOff )
{
feature = mComposition->atlasComposition().currentFeature();
}

setExpressionContext( feature, vl );
loadHtml();
}

0 comments on commit 19708be

Please sign in to comment.