Begin port of HTML item

This commit is contained in:
Nyall Dawson 2017-10-27 17:04:17 +10:00
parent 0e71505fe8
commit f1d1e454d8
7 changed files with 1079 additions and 13 deletions

View File

@ -409,6 +409,7 @@
%Include layout/qgslayoutframe.sip
%Include layout/qgslayoutitem.sip
%Include layout/qgslayoutitemgroup.sip
%Include layout/qgslayoutitemhtml.sip
%Include layout/qgslayoutitemlabel.sip
%Include layout/qgslayoutitemlegend.sip
%Include layout/qgslayoutitemmap.sip

View File

@ -0,0 +1,227 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/layout/qgslayoutitemhtml.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsLayoutItemHtml: QgsLayoutMultiFrame
{
%Docstring
A layout multiframe subclass for HTML content.
.. versionadded:: 3.0
%End
%TypeHeaderCode
#include "qgslayoutitemhtml.h"
%End
public:
enum ContentMode
{
Url,
ManualHtml
};
QgsLayoutItemHtml( QgsLayout *layout );
%Docstring
Constructor for QgsLayoutItemHtml, with the specified parent ``layout``.
%End
~QgsLayoutItemHtml();
void setContentMode( ContentMode mode );
%Docstring
Sets the source ``mode`` for item's HTML content.
.. seealso:: contentMode()
.. seealso:: setUrl()
.. seealso:: setHtml()
%End
ContentMode contentMode() const;
%Docstring
Returns the source mode for item's HTML content.
.. seealso:: setContentMode()
.. seealso:: url()
.. seealso:: html()
:rtype: ContentMode
%End
void setUrl( const QUrl &url );
%Docstring
Sets the ``url`` for content to display in the item when the item is using
the QgsLayoutItemHtml.Url mode. Content is automatically fetched and the
HTML item refreshed after calling this function.
.. seealso:: url()
.. seealso:: contentMode()
%End
QUrl url() const;
%Docstring
Returns the URL of the content displayed in the item if the item is using
the QgsLayoutItemHtml.Url mode.
.. seealso:: setUrl()
.. seealso:: contentMode()
:rtype: QUrl
%End
void setHtml( const QString &html );
%Docstring
Sets the ``html`` to display in the item when the item is using
the QgsLayoutItemHtml.ManualHtml mode. Setting the HTML using this function
does not automatically refresh the item's contents. Call loadHtml to trigger
a refresh of the item after setting the HTML content.
.. seealso:: html()
.. seealso:: contentMode()
.. seealso:: loadHtml()
%End
QString html() const;
%Docstring
Returns the HTML source displayed in the item if the item is using
the QgsLayoutItemHtml.ManualHtml mode.
.. seealso:: setHtml()
.. seealso:: contentMode()
:rtype: str
%End
bool evaluateExpressions() const;
%Docstring
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.
.. seealso:: setEvaluateExpressions()
:rtype: bool
%End
void setEvaluateExpressions( bool evaluateExpressions );
%Docstring
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.
.. seealso:: evaluateExpressions()
%End
bool useSmartBreaks() const;
%Docstring
Returns whether html item is using smart breaks. Smart breaks prevent
the html frame contents from breaking mid-way though a line of text.
.. seealso:: setUseSmartBreaks()
:rtype: bool
%End
void setUseSmartBreaks( bool useSmartBreaks );
%Docstring
Sets whether the html item should use smart breaks. Smart breaks prevent
the html frame contents from breaking mid-way though a line of text.
.. seealso:: useSmartBreaks()
%End
void setMaxBreakDistance( double distance );
%Docstring
Sets the maximum ``distance`` allowed when calculating where to place page breaks
in the html. This distance is the maximum amount of empty space allowed
at the bottom of a frame after calculating the optimum break location. Setting
a larger value will result in better choice of page break location, but more
wasted space at the bottom of frames. This setting is only effective if
useSmartBreaks is true.
.. seealso:: maxBreakDistance()
.. seealso:: setUseSmartBreaks()
%End
double maxBreakDistance() const;
%Docstring
Returns the maximum distance allowed when calculating where to place page breaks
in the html. This distance is the maximum amount of empty space allowed
at the bottom of a frame after calculating the optimum break location. This setting
is only effective if useSmartBreaks is true.
.. seealso:: setMaxBreakDistance()
.. seealso:: useSmartBreaks()
:rtype: float
%End
void setUserStylesheet( const QString &stylesheet );
%Docstring
Sets the user ``stylesheet`` CSS rules to use while rendering the HTML content. These
allow for overriding the styles specified within the HTML source. Setting the stylesheet
using this function does not automatically refresh the item's contents. Call loadHtml
to trigger a refresh of the item after setting the stylesheet rules.
.. seealso:: userStylesheet()
.. seealso:: setUserStylesheetEnabled()
.. seealso:: loadHtml()
%End
QString userStylesheet() const;
%Docstring
Returns the user stylesheet CSS rules used while rendering the HTML content. These
overriding the styles specified within the HTML source.
.. seealso:: setUserStylesheet()
.. seealso:: userStylesheetEnabled()
:rtype: str
%End
void setUserStylesheetEnabled( const bool enabled );
%Docstring
Sets whether user stylesheets are ``enabled`` for the HTML content.
.. seealso:: userStylesheetEnabled()
.. seealso:: setUserStylesheet()
%End
bool userStylesheetEnabled() const;
%Docstring
Returns whether user stylesheets are enabled for the HTML content.
.. seealso:: setUserStylesheetEnabled()
.. seealso:: userStylesheet()
:rtype: bool
%End
virtual QString displayName() const;
virtual QSizeF totalSize() const;
virtual void render( QgsRenderContext &context, const QRectF &renderExtent, const int frameIndex,
const QStyleOptionGraphicsItem *itemStyle = 0 );
virtual bool writeXml( QDomElement &elem, QDomDocument &doc, bool ignoreFrames = false ) const;
virtual bool readXml( const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames = false );
virtual double findNearbyPageBreak( double yPos );
public slots:
void loadHtml( const bool useCache = false, const QgsExpressionContext *context = 0 );
%Docstring
Reloads the html source from the url and redraws the item.
\param useCache set to true to use a cached copy of remote html
content
\param context expression context for evaluating data defined urls and expressions in html
.. seealso:: setUrl
.. seealso:: url
%End
virtual void recalculateFrameSizes();
%Docstring
Recalculates the frame sizes for the current viewport dimensions
%End
void refreshExpressionContext();
void refreshDataDefinedProperty( const QgsLayoutObject::DataDefinedProperty property = QgsLayoutObject::AllProperties );
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/layout/qgslayoutitemhtml.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -373,6 +373,7 @@ SET(QGIS_CORE_SRCS
layout/qgslayoutitem.cpp
layout/qgslayoutitemgroup.cpp
layout/qgslayoutitemgroupundocommand.cpp
layout/qgslayoutitemhtml.cpp
layout/qgslayoutitemlabel.cpp
layout/qgslayoutitemlegend.cpp
layout/qgslayoutitemmap.cpp
@ -735,6 +736,7 @@ SET(QGIS_CORE_MOC_HDRS
layout/qgslayoutitem.h
layout/qgslayoutitemgroup.h
layout/qgslayoutitemgroupundocommand.h
layout/qgslayoutitemhtml.h
layout/qgslayoutitemlabel.h
layout/qgslayoutitemlegend.h
layout/qgslayoutitemmap.h

View File

@ -336,18 +336,6 @@ void QgsComposerHtml::addFrame( QgsComposerFrame *frame, bool recalcFrameSizes )
}
}
bool candidateSort( QPair<int, int> c1, QPair<int, int> c2 )
{
if ( c1.second < c2.second )
return true;
else if ( c1.second > c2.second )
return false;
else if ( c1.first > c2.first )
return true;
else
return false;
}
double QgsComposerHtml::findNearbyPageBreak( double yPos )
{
if ( !mWebPage || !mRenderedPage || !mUseSmartBreaks )
@ -400,7 +388,18 @@ double QgsComposerHtml::findNearbyPageBreak( double yPos )
}
//sort candidate rows by number of changes ascending, row number descending
std::sort( candidates.begin(), candidates.end(), candidateSort );
std::sort( candidates.begin(), candidates.end(),
[]( QPair<int, int> c1, QPair<int, int> c2 )->bool
{
if ( c1.second < c2.second )
return true;
else if ( c1.second > c2.second )
return false;
else if ( c1.first > c2.first )
return true;
else
return false;
} );
//first candidate is now the largest row with smallest number of changes
//OK, now take the mid point of the best candidate position

View File

@ -0,0 +1,567 @@
/***************************************************************************
qgslayoutitemhtml.cpp
------------------------------------------------------------
begin : October 2017
copyright : (C) 2017 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgslayoutitemhtml.h"
#include "qgslayoutframe.h"
#include "qgslayout.h"
#include "qgsnetworkaccessmanager.h"
#include "qgsmessagelog.h"
#include "qgsexpression.h"
#include "qgslogger.h"
#include "qgsnetworkcontentfetcher.h"
#include "qgsvectorlayer.h"
#include "qgsproject.h"
#include "qgsdistancearea.h"
#include "qgsjsonutils.h"
#include "qgsmapsettings.h"
#include "qgswebpage.h"
#include "qgswebframe.h"
#include <QCoreApplication>
#include <QPainter>
#include <QImage>
#include <QNetworkReply>
QgsLayoutItemHtml::QgsLayoutItemHtml( QgsLayout *layout )
: QgsLayoutMultiFrame( layout )
, mContentMode( QgsLayoutItemHtml::Url )
, mLoaded( false )
, mHtmlUnitsToLayoutUnits( 1.0 )
, mEvaluateExpressions( true )
, mUseSmartBreaks( true )
, mMaxBreakDistance( 10 )
, mEnableUserStylesheet( false )
{
mDistanceArea = new QgsDistanceArea();
mHtmlUnitsToLayoutUnits = htmlUnitsToLayoutUnits();
mWebPage = new QgsWebPage();
mWebPage->setIdentifier( tr( "Layout HTML item" ) );
mWebPage->mainFrame()->setScrollBarPolicy( Qt::Horizontal, Qt::ScrollBarAlwaysOff );
mWebPage->mainFrame()->setScrollBarPolicy( Qt::Vertical, Qt::ScrollBarAlwaysOff );
//This makes the background transparent. Found on http://blog.qt.digia.com/blog/2009/06/30/transparent-qwebview-or-qwebpage/
QPalette palette = mWebPage->palette();
palette.setBrush( QPalette::Base, Qt::transparent );
mWebPage->setPalette( palette );
mWebPage->setNetworkAccessManager( QgsNetworkAccessManager::instance() );
connect( mWebPage, &QWebPage::loadFinished, this, &QgsLayoutItemHtml::frameLoaded );
#if 0 //TODO
if ( mLayout )
{
connect( mLayout, &QgsComposition::itemRemoved, this, &QgsComposerMultiFrame::handleFrameRemoval );
}
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().feature(), mComposition->atlasComposition().coverageLayer() );
}
//connect to atlas feature changes
//to update the expression context
connect( &mComposition->atlasComposition(), &QgsAtlasComposition::featureChanged, this, &QgsLayoutItemHtml::refreshExpressionContext );
#endif
mFetcher = new QgsNetworkContentFetcher();
connect( mFetcher, &QgsNetworkContentFetcher::finished, this, [ = ] { frameLoaded(); } );
}
QgsLayoutItemHtml::~QgsLayoutItemHtml()
{
delete mDistanceArea;
delete mWebPage;
delete mRenderedPage;
mFetcher->deleteLater();
}
void QgsLayoutItemHtml::setUrl( const QUrl &url )
{
if ( !mWebPage )
{
return;
}
mUrl = url;
loadHtml( true );
emit changed();
}
void QgsLayoutItemHtml::setHtml( const QString &html )
{
mHtml = html;
//TODO - this signal should be emitted, but without changing the signal which sets the html
//to an equivalent of editingFinished it causes a lot of problems. Need to investigate
//ways of doing this using QScintilla widgets.
//emit changed();
}
void QgsLayoutItemHtml::setEvaluateExpressions( bool evaluateExpressions )
{
mEvaluateExpressions = evaluateExpressions;
loadHtml( true );
emit changed();
}
void QgsLayoutItemHtml::loadHtml( const bool useCache, const QgsExpressionContext *context )
{
if ( !mWebPage )
{
return;
}
QgsExpressionContext scopedContext = createExpressionContext();
const QgsExpressionContext *evalContext = context ? context : &scopedContext;
QString loadedHtml;
switch ( mContentMode )
{
case QgsLayoutItemHtml::Url:
{
QString currentUrl = mUrl.toString();
//data defined url set?
bool ok = false;
currentUrl = mDataDefinedProperties.valueAsString( QgsLayoutObject::SourceUrl, *evalContext, currentUrl, &ok );
if ( ok )
{
currentUrl = currentUrl.trimmed();
QgsDebugMsg( QString( "exprVal Source Url:%1" ).arg( currentUrl ) );
}
if ( currentUrl.isEmpty() )
{
return;
}
if ( !( useCache && currentUrl == mLastFetchedUrl ) )
{
loadedHtml = fetchHtml( QUrl( currentUrl ) );
mLastFetchedUrl = currentUrl;
}
else
{
loadedHtml = mFetchedHtml;
}
break;
}
case QgsLayoutItemHtml::ManualHtml:
loadedHtml = mHtml;
break;
}
//evaluate expressions
if ( mEvaluateExpressions )
{
loadedHtml = QgsExpression::replaceExpressionText( loadedHtml, evalContext, mDistanceArea );
}
mLoaded = false;
//reset page size. otherwise viewport size increases but never decreases again
mWebPage->setViewportSize( QSize( maxFrameWidth() * mHtmlUnitsToLayoutUnits, 0 ) );
//set html, using the specified url as base if in Url mode or the project file if in manual mode
const QUrl baseUrl = mContentMode == QgsLayoutItemHtml::Url ?
QUrl( mActualFetchedUrl ) :
QUrl::fromLocalFile( mLayout->project()->fileInfo().absoluteFilePath() );
mWebPage->mainFrame()->setHtml( loadedHtml, baseUrl );
//set user stylesheet
QWebSettings *settings = mWebPage->settings();
if ( mEnableUserStylesheet && ! mUserStylesheet.isEmpty() )
{
QByteArray ba;
ba.append( mUserStylesheet.toUtf8() );
QUrl cssFileURL = QUrl( "data:text/css;charset=utf-8;base64," + ba.toBase64() );
settings->setUserStyleSheetUrl( cssFileURL );
}
else
{
settings->setUserStyleSheetUrl( QUrl() );
}
while ( !mLoaded )
{
qApp->processEvents();
}
//inject JSON feature
if ( !mAtlasFeatureJSON.isEmpty() )
{
mWebPage->mainFrame()->evaluateJavaScript( QStringLiteral( "if ( typeof setFeature === \"function\" ) { setFeature(%1); }" ).arg( mAtlasFeatureJSON ) );
//needs an extra process events here to give JavaScript a chance to execute
qApp->processEvents();
}
recalculateFrameSizes();
//trigger a repaint
emit contentsChanged();
}
void QgsLayoutItemHtml::frameLoaded( bool ok )
{
Q_UNUSED( ok );
mLoaded = true;
}
double QgsLayoutItemHtml::maxFrameWidth() const
{
double maxWidth = 0;
for ( QgsLayoutFrame *frame : mFrameItems )
{
maxWidth = std::max( maxWidth, static_cast< double >( frame->boundingRect().width() ) );
}
return maxWidth;
}
void QgsLayoutItemHtml::recalculateFrameSizes()
{
if ( frameCount() < 1 ) return;
QSize contentsSize = mWebPage->mainFrame()->contentsSize();
//find maximum frame width
double maxWidth = maxFrameWidth();
//set content width to match maximum frame width
contentsSize.setWidth( maxWidth * mHtmlUnitsToLayoutUnits );
mWebPage->setViewportSize( contentsSize );
mSize.setWidth( contentsSize.width() / mHtmlUnitsToLayoutUnits );
mSize.setHeight( contentsSize.height() / mHtmlUnitsToLayoutUnits );
if ( contentsSize.isValid() )
{
renderCachedImage();
}
QgsLayoutMultiFrame::recalculateFrameSizes();
emit changed();
}
void QgsLayoutItemHtml::renderCachedImage()
{
//render page to cache image
if ( mRenderedPage )
{
delete mRenderedPage;
}
mRenderedPage = new QImage( mWebPage->viewportSize(), QImage::Format_ARGB32 );
if ( mRenderedPage->isNull() )
{
return;
}
mRenderedPage->fill( Qt::transparent );
QPainter painter;
painter.begin( mRenderedPage );
mWebPage->mainFrame()->render( &painter );
painter.end();
}
QString QgsLayoutItemHtml::fetchHtml( const QUrl &url )
{
//pause until HTML fetch
mLoaded = false;
mFetcher->fetchContent( url );
while ( !mLoaded )
{
qApp->processEvents();
}
mFetchedHtml = mFetcher->contentAsString();
mActualFetchedUrl = mFetcher->reply()->url().toString();
return mFetchedHtml;
}
QSizeF QgsLayoutItemHtml::totalSize() const
{
return mSize;
}
void QgsLayoutItemHtml::render( QgsRenderContext &context, const QRectF &renderExtent, const int,
const QStyleOptionGraphicsItem * )
{
if ( !mWebPage )
return;
QPainter *painter = context.painter();
painter->save();
// painter is scaled to dots, so scale back to layout units
painter->scale( context.scaleFactor() / mHtmlUnitsToLayoutUnits, context.scaleFactor() / mHtmlUnitsToLayoutUnits );
painter->translate( 0.0, -renderExtent.top() * mHtmlUnitsToLayoutUnits );
mWebPage->mainFrame()->render( painter, QRegion( renderExtent.left(), renderExtent.top() * mHtmlUnitsToLayoutUnits, renderExtent.width() * mHtmlUnitsToLayoutUnits, renderExtent.height() * mHtmlUnitsToLayoutUnits ) );
painter->restore();
}
double QgsLayoutItemHtml::htmlUnitsToLayoutUnits()
{
if ( !mLayout )
{
return 1.0;
}
return mLayout->convertToLayoutUnits( QgsLayoutMeasurement( mLayout->context().dpi() / 72.0, QgsUnitTypes::LayoutMillimeters ) ); //webkit seems to assume a standard dpi of 96
}
bool candidateSort( QPair<int, int> c1, QPair<int, int> c2 )
{
if ( c1.second < c2.second )
return true;
else if ( c1.second > c2.second )
return false;
else if ( c1.first > c2.first )
return true;
else
return false;
}
double QgsLayoutItemHtml::findNearbyPageBreak( double yPos )
{
if ( !mWebPage || !mRenderedPage || !mUseSmartBreaks )
{
return yPos;
}
//convert yPos to pixels
int idealPos = yPos * htmlUnitsToLayoutUnits();
//if ideal break pos is past end of page, there's nothing we need to do
if ( idealPos >= mRenderedPage->height() )
{
return yPos;
}
int maxSearchDistance = mMaxBreakDistance * htmlUnitsToLayoutUnits();
//loop through all lines just before ideal break location, up to max distance
//of maxSearchDistance
int changes = 0;
QRgb currentColor;
bool currentPixelTransparent = false;
bool previousPixelTransparent = false;
QRgb pixelColor;
QList< QPair<int, int> > candidates;
int minRow = std::max( idealPos - maxSearchDistance, 0 );
for ( int candidateRow = idealPos; candidateRow >= minRow; --candidateRow )
{
changes = 0;
currentColor = qRgba( 0, 0, 0, 0 );
//check all pixels in this line
for ( int col = 0; col < mRenderedPage->width(); ++col )
{
//count how many times the pixels change color in this row
//eventually, we select a row to break at with the minimum number of color changes
//since this is likely a line break, or gap between table cells, etc
//but very unlikely to be midway through a text line or picture
pixelColor = mRenderedPage->pixel( col, candidateRow );
currentPixelTransparent = qAlpha( pixelColor ) == 0;
if ( pixelColor != currentColor && !( currentPixelTransparent && previousPixelTransparent ) )
{
//color has changed
currentColor = pixelColor;
changes++;
}
previousPixelTransparent = currentPixelTransparent;
}
candidates.append( qMakePair( candidateRow, changes ) );
}
//sort candidate rows by number of changes ascending, row number descending
std::sort( candidates.begin(), candidates.end(), candidateSort );
//first candidate is now the largest row with smallest number of changes
//OK, now take the mid point of the best candidate position
//we do this so that the spacing between text lines is likely to be split in half
//otherwise the html will be broken immediately above a line of text, which
//looks a little messy
int maxCandidateRow = candidates[0].first;
int minCandidateRow = maxCandidateRow + 1;
int minCandidateChanges = candidates[0].second;
QList< QPair<int, int> >::iterator it;
for ( it = candidates.begin(); it != candidates.end(); ++it )
{
if ( ( *it ).second != minCandidateChanges || ( *it ).first != minCandidateRow - 1 )
{
//no longer in a consecutive block of rows of minimum pixel color changes
//so return the row mid-way through the block
//first converting back to mm
return ( minCandidateRow + ( maxCandidateRow - minCandidateRow ) / 2 ) / htmlUnitsToLayoutUnits();
}
minCandidateRow = ( *it ).first;
}
//above loop didn't work for some reason
//return first candidate converted to mm
return candidates[0].first / htmlUnitsToLayoutUnits();
}
void QgsLayoutItemHtml::setUseSmartBreaks( bool useSmartBreaks )
{
mUseSmartBreaks = useSmartBreaks;
recalculateFrameSizes();
emit changed();
}
void QgsLayoutItemHtml::setMaxBreakDistance( double maxBreakDistance )
{
mMaxBreakDistance = maxBreakDistance;
recalculateFrameSizes();
emit changed();
}
void QgsLayoutItemHtml::setUserStylesheet( const QString &stylesheet )
{
mUserStylesheet = stylesheet;
//TODO - this signal should be emitted, but without changing the signal which sets the css
//to an equivalent of editingFinished it causes a lot of problems. Need to investigate
//ways of doing this using QScintilla widgets.
//emit changed();
}
void QgsLayoutItemHtml::setUserStylesheetEnabled( const bool stylesheetEnabled )
{
if ( mEnableUserStylesheet != stylesheetEnabled )
{
mEnableUserStylesheet = stylesheetEnabled;
loadHtml( true );
emit changed();
}
}
QString QgsLayoutItemHtml::displayName() const
{
return tr( "<HTML frame>" );
}
bool QgsLayoutItemHtml::writeXml( QDomElement &elem, QDomDocument &doc, bool ignoreFrames ) const
{
QDomElement htmlElem = doc.createElement( QStringLiteral( "ComposerHtml" ) );
htmlElem.setAttribute( QStringLiteral( "contentMode" ), QString::number( static_cast< int >( mContentMode ) ) );
htmlElem.setAttribute( QStringLiteral( "url" ), mUrl.toString() );
htmlElem.setAttribute( QStringLiteral( "html" ), mHtml );
htmlElem.setAttribute( QStringLiteral( "evaluateExpressions" ), mEvaluateExpressions ? "true" : "false" );
htmlElem.setAttribute( QStringLiteral( "useSmartBreaks" ), mUseSmartBreaks ? "true" : "false" );
htmlElem.setAttribute( QStringLiteral( "maxBreakDistance" ), QString::number( mMaxBreakDistance ) );
htmlElem.setAttribute( QStringLiteral( "stylesheet" ), mUserStylesheet );
htmlElem.setAttribute( QStringLiteral( "stylesheetEnabled" ), mEnableUserStylesheet ? "true" : "false" );
bool state = _writeXml( htmlElem, doc, ignoreFrames );
elem.appendChild( htmlElem );
return state;
}
bool QgsLayoutItemHtml::readXml( const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames )
{
if ( !ignoreFrames )
{
deleteFrames();
}
//first create the frames
if ( !_readXml( itemElem, doc, ignoreFrames ) )
{
return false;
}
bool contentModeOK;
mContentMode = static_cast< QgsLayoutItemHtml::ContentMode >( itemElem.attribute( QStringLiteral( "contentMode" ) ).toInt( &contentModeOK ) );
if ( !contentModeOK )
{
mContentMode = QgsLayoutItemHtml::Url;
}
mEvaluateExpressions = itemElem.attribute( QStringLiteral( "evaluateExpressions" ), QStringLiteral( "true" ) ) == QLatin1String( "true" );
mUseSmartBreaks = itemElem.attribute( QStringLiteral( "useSmartBreaks" ), QStringLiteral( "true" ) ) == QLatin1String( "true" );
mMaxBreakDistance = itemElem.attribute( QStringLiteral( "maxBreakDistance" ), QStringLiteral( "10" ) ).toDouble();
mHtml = itemElem.attribute( QStringLiteral( "html" ) );
mUserStylesheet = itemElem.attribute( QStringLiteral( "stylesheet" ) );
mEnableUserStylesheet = itemElem.attribute( QStringLiteral( "stylesheetEnabled" ), QStringLiteral( "false" ) ) == QLatin1String( "true" );
//finally load the set url
QString urlString = itemElem.attribute( QStringLiteral( "url" ) );
if ( !urlString.isEmpty() )
{
mUrl = urlString;
}
loadHtml( true );
//since frames had to be created before, we need to emit a changed signal to refresh the widget
emit changed();
return true;
}
void QgsLayoutItemHtml::setExpressionContext( const QgsFeature &feature, QgsVectorLayer *layer )
{
mExpressionFeature = feature;
mExpressionLayer = layer;
//setup distance area conversion
if ( layer )
{
mDistanceArea->setSourceCrs( layer->crs() );
}
else if ( mLayout )
{
#if 0 //TODO
//set to composition's mapsettings' crs
QgsComposerMap *referenceMap = mComposition->referenceMap();
if ( referenceMap )
mDistanceArea->setSourceCrs( referenceMap->crs() );
#endif
}
if ( mLayout )
{
mDistanceArea->setEllipsoid( mLayout->project()->ellipsoid() );
}
// create JSON representation of feature
QgsJsonExporter exporter( layer );
exporter.setIncludeRelated( true );
mAtlasFeatureJSON = exporter.exportFeature( feature );
}
void QgsLayoutItemHtml::refreshExpressionContext()
{
QgsVectorLayer *vl = nullptr;
QgsFeature feature;
#if 0 //TODO
if ( mComposition->atlasComposition().enabled() )
{
vl = mComposition->atlasComposition().coverageLayer();
}
if ( mComposition->atlasMode() != QgsComposition::AtlasOff )
{
feature = mComposition->atlasComposition().feature();
}
#endif
setExpressionContext( feature, vl );
loadHtml( true );
}
void QgsLayoutItemHtml::refreshDataDefinedProperty( const QgsLayoutObject::DataDefinedProperty property )
{
QgsExpressionContext context = createExpressionContext();
//updates data defined properties and redraws item to match
if ( property == QgsLayoutObject::SourceUrl || property == QgsLayoutObject::AllProperties )
{
loadHtml( true, &context );
}
}

View File

@ -0,0 +1,269 @@
/***************************************************************************
qgslayoutitemhtml.h
------------------------------------------------------------
begin : October 2017
copyright : (C) 2017 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSLAYOUTITEMHTML_H
#define QGSLAYOUTITEMHTML_H
#include "qgis_core.h"
#include "qgis.h"
#include "qgslayoutmultiframe.h"
#include "qgsfeature.h"
#include <QUrl>
class QgsWebPage;
class QImage;
class QgsVectorLayer;
class QgsNetworkContentFetcher;
class QgsDistanceArea;
/**
* \ingroup core
* A layout multiframe subclass for HTML content.
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayoutItemHtml: public QgsLayoutMultiFrame
{
Q_OBJECT
public:
//! Source modes for the HTML content to render in the item
enum ContentMode
{
Url, //!< Using this mode item fetches its content via a url
ManualHtml //!< HTML content is manually set for the item
};
/**
* Constructor for QgsLayoutItemHtml, with the specified parent \a layout.
*/
QgsLayoutItemHtml( QgsLayout *layout );
~QgsLayoutItemHtml();
/**
* Sets the source \a mode for item's HTML content.
* \see contentMode()
* \see setUrl()
* \see setHtml()
*/
void setContentMode( ContentMode mode ) { mContentMode = mode; }
/**
* Returns the source mode for item's HTML content.
* \see setContentMode()
* \see url()
* \see html()
*/
ContentMode contentMode() const { return mContentMode; }
/**
* Sets the \a url for content to display in the item when the item is using
* the QgsLayoutItemHtml::Url mode. Content is automatically fetched and the
* HTML item refreshed after calling this function.
* \see url()
* \see contentMode()
*/
void setUrl( const QUrl &url );
/**
* Returns the URL of the content displayed in the item if the item is using
* the QgsLayoutItemHtml::Url mode.
* \see setUrl()
* \see contentMode()
*/
QUrl url() const { return mUrl; }
/**
* Sets the \a html to display in the item when the item is using
* the QgsLayoutItemHtml::ManualHtml mode. Setting the HTML using this function
* does not automatically refresh the item's contents. Call loadHtml to trigger
* a refresh of the item after setting the HTML content.
* \see html()
* \see contentMode()
* \see loadHtml()
*/
void setHtml( const QString &html );
/**
* Returns the HTML source displayed in the item if the item is using
* the QgsLayoutItemHtml::ManualHtml mode.
* \see setHtml()
* \see contentMode()
*/
QString html() const { return mHtml; }
/**
* 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.
* \see setEvaluateExpressions()
*/
bool evaluateExpressions() const { return mEvaluateExpressions; }
/**
* 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.
* \see evaluateExpressions()
*/
void setEvaluateExpressions( bool evaluateExpressions );
/**
* Returns whether html item is using smart breaks. Smart breaks prevent
* the html frame contents from breaking mid-way though a line of text.
* \see setUseSmartBreaks()
*/
bool useSmartBreaks() const { return mUseSmartBreaks; }
/**
* Sets whether the html item should use smart breaks. Smart breaks prevent
* the html frame contents from breaking mid-way though a line of text.
* \see useSmartBreaks()
*/
void setUseSmartBreaks( bool useSmartBreaks );
/**
* Sets the maximum \a distance allowed when calculating where to place page breaks
* in the html. This distance is the maximum amount of empty space allowed
* at the bottom of a frame after calculating the optimum break location. Setting
* a larger value will result in better choice of page break location, but more
* wasted space at the bottom of frames. This setting is only effective if
* useSmartBreaks is true.
* \see maxBreakDistance()
* \see setUseSmartBreaks()
*/
void setMaxBreakDistance( double distance );
/**
* Returns the maximum distance allowed when calculating where to place page breaks
* in the html. This distance is the maximum amount of empty space allowed
* at the bottom of a frame after calculating the optimum break location. This setting
* is only effective if useSmartBreaks is true.
* \see setMaxBreakDistance()
* \see useSmartBreaks()
*/
double maxBreakDistance() const { return mMaxBreakDistance; }
/**
* Sets the user \a stylesheet CSS rules to use while rendering the HTML content. These
* allow for overriding the styles specified within the HTML source. Setting the stylesheet
* using this function does not automatically refresh the item's contents. Call loadHtml
* to trigger a refresh of the item after setting the stylesheet rules.
* \see userStylesheet()
* \see setUserStylesheetEnabled()
* \see loadHtml()
*/
void setUserStylesheet( const QString &stylesheet );
/**
* Returns the user stylesheet CSS rules used while rendering the HTML content. These
* overriding the styles specified within the HTML source.
* \see setUserStylesheet()
* \see userStylesheetEnabled()
*/
QString userStylesheet() const { return mUserStylesheet; }
/**
* Sets whether user stylesheets are \a enabled for the HTML content.
* \see userStylesheetEnabled()
* \see setUserStylesheet()
*/
void setUserStylesheetEnabled( const bool enabled );
/**
* Returns whether user stylesheets are enabled for the HTML content.
* \see setUserStylesheetEnabled()
* \see userStylesheet()
*/
bool userStylesheetEnabled() const { return mEnableUserStylesheet; }
QString displayName() const override;
QSizeF totalSize() const override;
void render( QgsRenderContext &context, const QRectF &renderExtent, const int frameIndex,
const QStyleOptionGraphicsItem *itemStyle = nullptr ) override;
bool writeXml( QDomElement &elem, QDomDocument &doc, bool ignoreFrames = false ) const override;
bool readXml( const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames = false ) override;
//overridden to break frames without dividing lines of text
double findNearbyPageBreak( double yPos ) override;
public slots:
/**
* Reloads the html source from the url and redraws the item.
* \param useCache set to true to use a cached copy of remote html
* content
* \param context expression context for evaluating data defined urls and expressions in html
* \see setUrl
* \see url
*/
void loadHtml( const bool useCache = false, const QgsExpressionContext *context = nullptr );
//! Recalculates the frame sizes for the current viewport dimensions
void recalculateFrameSizes() override;
void refreshExpressionContext();
void refreshDataDefinedProperty( const QgsLayoutObject::DataDefinedProperty property = QgsLayoutObject::AllProperties );
private slots:
void frameLoaded( bool ok = true );
private:
ContentMode mContentMode;
QUrl mUrl;
QgsWebPage *mWebPage = nullptr;
QString mHtml;
QString mFetchedHtml;
QString mLastFetchedUrl;
QString mActualFetchedUrl; //may be different if page was redirected
bool mLoaded;
QSizeF mSize; //total size in mm
double mHtmlUnitsToLayoutUnits;
QImage *mRenderedPage = nullptr;
bool mEvaluateExpressions;
bool mUseSmartBreaks;
double mMaxBreakDistance;
QgsFeature mExpressionFeature;
QgsVectorLayer *mExpressionLayer = nullptr;
QgsDistanceArea *mDistanceArea = nullptr;
QString mUserStylesheet;
bool mEnableUserStylesheet;
//! JSON string representation of current atlas feature
QString mAtlasFeatureJSON;
QgsNetworkContentFetcher *mFetcher = nullptr;
double htmlUnitsToLayoutUnits(); //calculate scale factor
//renders a snapshot of the page to a cached image
void renderCachedImage();
//fetches html content from a url and returns it as a string
QString fetchHtml( const QUrl &url );
//! Sets the current feature, the current layer and a list of local variable substitutions for evaluating expressions
void setExpressionContext( const QgsFeature &feature, QgsVectorLayer *layer );
//! Calculates the max width of frames in the html multiframe
double maxFrameWidth() const;
};
#endif // QGSLAYOUTITEMHTML_H

View File

@ -55,6 +55,7 @@ void QgsLayoutMultiFrame::addFrame( QgsLayoutFrame *frame, bool recalcFrameSizes
mFrameItems.push_back( frame );
frame->mMultiFrame = this;
connect( frame, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutMultiFrame::recalculateFrameSizes );
connect( frame, &QgsLayoutFrame::destroyed, this, &QgsLayoutMultiFrame::handleFrameRemoval );
if ( mLayout )
{