Merge pull request #3438 from nyalldawson/replace

[FEATURE] Substitution list support for labeling
This commit is contained in:
Nyall Dawson 2016-08-30 08:46:35 +10:00 committed by GitHub
commit 891370fd68
17 changed files with 1363 additions and 268 deletions

View File

@ -464,6 +464,11 @@ class QgsPalLayerSettings
QPainter::CompositionMode blendMode;
QColor previewBkgrdColor;
//! Substitution collection for automatic text substitution with labels
QgsStringReplacementCollection substitutions;
//! True if substitutions should be applied
bool useSubstitutions;
//-- text formatting
QString wrapChar;

View File

@ -1,3 +1,115 @@
/** \ingroup core
* \class QgsStringReplacement
* \brief A representation of a single string replacement.
* \note Added in version 3.0
*/
class QgsStringReplacement
{
%TypeHeaderCode
#include <qgsstringutils.h>
%End
public:
/** Constructor for QgsStringReplacement.
* @param match string to match
* @param replacement string to replace match with
* @param caseSensitive set to true for a case sensitive match
* @param wholeWordOnly set to true to match complete words only, or false to allow partial word matches
*/
QgsStringReplacement( const QString& match,
const QString& replacement,
bool caseSensitive = false,
bool wholeWordOnly = false );
//! Returns the string matched by this object
QString match() const;
//! Returns the string to replace matches with
QString replacement() const;
//! Returns true if match is case sensitive
bool caseSensitive() const;
//! Returns true if match only applies to whole words, or false if partial word matches are permitted
bool wholeWordOnly() const;
/** Processes a given input string, applying any valid replacements which should be made.
* @param input input string
* @returns input string with any matches replaced by replacement string
*/
QString process( const QString& input ) const;
bool operator==( const QgsStringReplacement& other );
/** Returns a map of the replacement properties.
* @see fromProperties()
*/
QgsStringMap properties() const;
/** Creates a new QgsStringReplacement from an encoded properties map.
* @see properties()
*/
static QgsStringReplacement fromProperties( const QgsStringMap& properties );
};
/** \ingroup core
* \class QgsStringReplacementCollection
* \brief A collection of string replacements (specified using QgsStringReplacement objects).
* \note Added in version 3.0
*/
class QgsStringReplacementCollection
{
%TypeHeaderCode
#include <qgsstringutils.h>
%End
public:
/** Constructor for QgsStringReplacementCollection
* @param replacements initial list of string replacements
*/
QgsStringReplacementCollection( const QList< QgsStringReplacement >& replacements = QList< QgsStringReplacement >() );
/** Returns the list of string replacements in this collection.
* @see setReplacements()
*/
QList< QgsStringReplacement > replacements() const;
/** Sets the list of string replacements in this collection.
* @param replacements list of string replacements to apply. Replacements are applied in the
* order they are specified here.
* @see replacements()
*/
void setReplacements( const QList< QgsStringReplacement >& replacements );
/** Processes a given input string, applying any valid replacements which should be made
* using QgsStringReplacement objects contained by this collection. Replacements
* are made in order of the QgsStringReplacement objects contained in the collection.
* @param input input string
* @returns input string with any matches replaced by replacement string
*/
QString process( const QString& input ) const;
/** Writes the collection state to an XML element.
* @param elem target DOM element
* @param doc DOM document
* @see readXml()
*/
void writeXml( QDomElement& elem, QDomDocument& doc ) const;
/** Reads the collection state from an XML element.
* @param elem DOM element
* @see writeXml()
*/
void readXml( const QDomElement& elem );
};
/** \ingroup core
* \class QgsStringUtils
* \brief Utility functions for working with strings.

View File

@ -114,6 +114,7 @@ SET(QGIS_APP_SRCS
qgsrelationadddlg.cpp
qgsselectbyformdialog.cpp
qgsstatisticalsummarydockwidget.cpp
qgssubstitutionlistwidget.cpp
qgstextannotationdialog.cpp
qgssnappingdialog.cpp
qgssvgannotationdialog.cpp
@ -290,6 +291,7 @@ SET (QGIS_APP_MOC_HDRS
qgssnappingdialog.h
qgssponsors.h
qgsstatisticalsummarydockwidget.h
qgssubstitutionlistwidget.h
qgssvgannotationdialog.h
qgstextannotationdialog.h
qgstipgui.h

View File

@ -34,6 +34,7 @@
#include "qgssvgselectorwidget.h"
#include "qgsvectorlayerlabeling.h"
#include "qgslogger.h"
#include "qgssubstitutionlistwidget.h"
#include <QCheckBox>
#include <QSettings>
@ -137,6 +138,7 @@ QgsLabelingGui::QgsLabelingGui( QgsVectorLayer* layer, QgsMapCanvas* mapCanvas,
connect( mShadowTranspSlider, SIGNAL( valueChanged( int ) ), mShadowTranspSpnBx, SLOT( setValue( int ) ) );
connect( mShadowTranspSpnBx, SIGNAL( valueChanged( int ) ), mShadowTranspSlider, SLOT( setValue( int ) ) );
connect( mLimitLabelChkBox, SIGNAL( toggled( bool ) ), mLimitLabelSpinBox, SLOT( setEnabled( bool ) ) );
connect( mCheckBoxSubstituteText, SIGNAL( toggled( bool ) ), mToolButtonConfigureSubstitutes, SLOT( setEnabled( bool ) ) );
//connections to prevent users removing all line placement positions
connect( chkLineAbove, SIGNAL( toggled( bool ) ), this, SLOT( updateLinePlacementOptions() ) );
@ -466,7 +468,8 @@ QgsLabelingGui::QgsLabelingGui( QgsVectorLayer* layer, QgsMapCanvas* mapCanvas,
<< radPolygonPerimeter
<< radPolygonPerimeterCurved
<< radPredefinedOrder
<< mFieldExpressionWidget;
<< mFieldExpressionWidget
<< mCheckBoxSubstituteText;
connectValueChanged( widgets, SLOT( updatePreview() ) );
connect( mQuadrantBtnGrp, SIGNAL( buttonClicked( int ) ), this, SLOT( updatePreview() ) );
@ -623,6 +626,8 @@ void QgsLabelingGui::init()
// set the current field or add the current expression to the bottom of the list
mFieldExpressionWidget->setRow( -1 );
mFieldExpressionWidget->setField( lyr.fieldName );
mCheckBoxSubstituteText->setChecked( lyr.useSubstitutions );
mSubstitutions = lyr.substitutions;
// populate placement options
mCentroidRadioWhole->setChecked( lyr.centroidWhole );
@ -1013,6 +1018,8 @@ QgsPalLayerSettings QgsLabelingGui::layerSettings()
lyr.scaleVisibility = mScaleBasedVisibilityChkBx->isChecked();
lyr.scaleMin = mScaleBasedVisibilityMinSpnBx->value();
lyr.scaleMax = mScaleBasedVisibilityMaxSpnBx->value();
lyr.useSubstitutions = mCheckBoxSubstituteText->isChecked();
lyr.substitutions = mSubstitutions;
// buffer
lyr.bufferDraw = mBufferDrawChkBx->isChecked();
@ -1973,6 +1980,12 @@ void QgsLabelingGui::updateLinePlacementOptions()
}
}
void QgsLabelingGui::onSubstitutionsChanged( const QgsStringReplacementCollection& substitutions )
{
mSubstitutions = substitutions;
emit widgetChanged();
}
void QgsLabelingGui::updateSvgWidgets( const QString& svgPath )
{
if ( mShapeSVGPathLineEdit->text() != svgPath )
@ -2099,6 +2112,28 @@ void QgsLabelingGui::on_mChkNoObstacle_toggled( bool active )
mObstaclePriorityFrame->setEnabled( active );
}
void QgsLabelingGui::on_mToolButtonConfigureSubstitutes_clicked()
{
QgsPanelWidget* panel = QgsPanelWidget::findParentPanel( this );
if ( panel && panel->dockMode() )
{
QgsSubstitutionListWidget* widget = new QgsSubstitutionListWidget( panel );
widget->setPanelTitle( tr( "Substitutions" ) );
widget->setSubstitutions( mSubstitutions );
connect( widget, SIGNAL( substitutionsChanged( QgsStringReplacementCollection ) ), this, SLOT( onSubstitutionsChanged( QgsStringReplacementCollection ) ) );
panel->openPanel( widget );
return;
}
QgsSubstitutionListDialog dlg( this );
dlg.setSubstitutions( mSubstitutions );
if ( dlg.exec() == QDialog::Accepted )
{
mSubstitutions = dlg.substitutions();
emit widgetChanged();
}
}
void QgsLabelingGui::showBackgroundRadius( bool show )
{
mShapeRadiusLabel->setVisible( show );

View File

@ -21,6 +21,7 @@
#include <QDialog>
#include <QFontDatabase>
#include <ui_qgslabelingguibase.h>
#include "qgsstringutils.h"
class QgsVectorLayer;
class QgsMapCanvas;
@ -94,6 +95,8 @@ class APP_EXPORT QgsLabelingGui : public QWidget, private Ui::QgsLabelingGuiBase
void on_mDirectSymbRightToolBtn_clicked();
void on_mChkNoObstacle_toggled( bool active );
void on_mToolButtonConfigureSubstitutes_clicked();
protected:
void blockInitSignals( bool block );
void blockFontChangeSignals( bool blk );
@ -133,6 +136,8 @@ class APP_EXPORT QgsLabelingGui : public QWidget, private Ui::QgsLabelingGuiBase
bool mLoadSvgParams;
QgsStringReplacementCollection mSubstitutions;
void enableDataDefinedAlignment( bool enable );
QgsExpressionContext createExpressionContext() const override;
@ -143,6 +148,7 @@ class APP_EXPORT QgsLabelingGui : public QWidget, private Ui::QgsLabelingGuiBase
void showBackgroundPenStyle( bool show );
void on_mShapeSVGPathLineEdit_textChanged( const QString& text );
void updateLinePlacementOptions();
void onSubstitutionsChanged( const QgsStringReplacementCollection& substitutions );
};
#endif

View File

@ -0,0 +1,230 @@
/***************************************************************************
qgssubstitutionlistwidget.cpp
-----------------------------
begin : August 2016
copyright : (C) 2016 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 "qgssubstitutionlistwidget.h"
#include <QDialogButtonBox>
#include <QCheckBox>
#include <QFileDialog>
#include <QMessageBox>
#include <QTextStream>
QgsSubstitutionListWidget::QgsSubstitutionListWidget( QWidget* parent )
: QgsPanelWidget( parent )
{
setupUi( this );
connect( mTableSubstitutions, SIGNAL( cellChanged( int, int ) ), this, SLOT( tableChanged() ) );
}
void QgsSubstitutionListWidget::setSubstitutions( const QgsStringReplacementCollection& substitutions )
{
mTableSubstitutions->blockSignals( true );
mTableSubstitutions->clearContents();
Q_FOREACH ( const QgsStringReplacement& replacement, substitutions.replacements() )
{
addSubstitution( replacement );
}
mTableSubstitutions->blockSignals( false );
}
QgsStringReplacementCollection QgsSubstitutionListWidget::substitutions() const
{
QList< QgsStringReplacement > result;
for ( int i = 0; i < mTableSubstitutions->rowCount(); ++i )
{
if ( !mTableSubstitutions->item( i, 0 ) )
continue;
if ( mTableSubstitutions->item( i, 0 )->text().isEmpty() )
continue;
QCheckBox* chkCaseSensitive = qobject_cast<QCheckBox*>( mTableSubstitutions->cellWidget( i, 2 ) );
QCheckBox* chkWholeWord = qobject_cast<QCheckBox*>( mTableSubstitutions->cellWidget( i, 3 ) );
QgsStringReplacement replacement( mTableSubstitutions->item( i, 0 )->text(),
mTableSubstitutions->item( i, 1 )->text(),
chkCaseSensitive->isChecked(),
chkWholeWord->isChecked() );
result << replacement;
}
return QgsStringReplacementCollection( result );
}
void QgsSubstitutionListWidget::on_mButtonAdd_clicked()
{
addSubstitution( QgsStringReplacement( QString(), QString(), false, true ) );
mTableSubstitutions->setFocus();
mTableSubstitutions->setCurrentCell( mTableSubstitutions->rowCount() - 1, 0 );
}
void QgsSubstitutionListWidget::on_mButtonRemove_clicked()
{
int currentRow = mTableSubstitutions->currentRow();
mTableSubstitutions->removeRow( currentRow );
tableChanged();
}
void QgsSubstitutionListWidget::tableChanged()
{
emit substitutionsChanged( substitutions() );
}
void QgsSubstitutionListWidget::on_mButtonExport_clicked()
{
QString fileName = QFileDialog::getSaveFileName( this, tr( "Save substitutions" ), QDir::homePath(),
tr( "XML files (*.xml *.XML)" ) );
if ( fileName.isEmpty() )
{
return;
}
// ensure the user never ommited the extension from the file name
if ( !fileName.endsWith( ".xml", Qt::CaseInsensitive ) )
{
fileName += ".xml";
}
QDomDocument doc;
QDomElement root = doc.createElement( "substitutions" );
root.setAttribute( "version", "1.0" );
QgsStringReplacementCollection collection = substitutions();
collection.writeXml( root, doc );
doc.appendChild( root );
QFile file( fileName );
if ( !file.open( QIODevice::WriteOnly | QIODevice::Text ) )
{
QMessageBox::warning( nullptr, tr( "Export substitutions" ),
tr( "Cannot write file %1:\n%2." ).arg( fileName, file.errorString() ),
QMessageBox::Ok,
QMessageBox::Ok );
return;
}
QTextStream out( &file );
doc.save( out, 4 );
}
void QgsSubstitutionListWidget::on_mButtonImport_clicked()
{
QString fileName = QFileDialog::getOpenFileName( this, tr( "Load substitutions" ), QDir::homePath(),
tr( "XML files (*.xml *.XML)" ) );
if ( fileName.isEmpty() )
{
return;
}
QFile file( fileName );
if ( !file.open( QIODevice::ReadOnly | QIODevice::Text ) )
{
QMessageBox::warning( nullptr, tr( "Import substitutions" ),
tr( "Cannot read file %1:\n%2." ).arg( fileName, file.errorString() ),
QMessageBox::Ok,
QMessageBox::Ok );
return;
}
QDomDocument doc;
QString errorStr;
int errorLine;
int errorColumn;
if ( !doc.setContent( &file, true, &errorStr, &errorLine, &errorColumn ) )
{
QMessageBox::warning( nullptr, tr( "Import substitutions" ),
tr( "Parse error at line %1, column %2:\n%3" )
.arg( errorLine )
.arg( errorColumn )
.arg( errorStr ),
QMessageBox::Ok,
QMessageBox::Ok );
return;
}
QDomElement root = doc.documentElement();
if ( root.tagName() != "substitutions" )
{
QMessageBox::warning( nullptr, tr( "Import substitutions" ),
tr( "The selected file in not an substitutions list." ),
QMessageBox::Ok,
QMessageBox::Ok );
return;
}
QgsStringReplacementCollection collection;
collection.readXml( root );
setSubstitutions( collection );
tableChanged();
}
void QgsSubstitutionListWidget::addSubstitution( const QgsStringReplacement& substitution )
{
int row = mTableSubstitutions->rowCount();
mTableSubstitutions->insertRow( row );
Qt::ItemFlags itemFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable
| Qt::ItemIsEditable;
QTableWidgetItem* matchItem = new QTableWidgetItem( substitution.match() );
matchItem->setFlags( itemFlags );
mTableSubstitutions->setItem( row, 0, matchItem );
QTableWidgetItem* replaceItem = new QTableWidgetItem( substitution.replacement() );
replaceItem->setFlags( itemFlags );
mTableSubstitutions->setItem( row, 1, replaceItem );
QCheckBox* caseSensitiveChk = new QCheckBox( this );
caseSensitiveChk->setChecked( substitution.caseSensitive() );
mTableSubstitutions->setCellWidget( row, 2, caseSensitiveChk );
connect( caseSensitiveChk, SIGNAL( toggled( bool ) ), this, SLOT( tableChanged() ) );
QCheckBox* wholeWordChk = new QCheckBox( this );
wholeWordChk->setChecked( substitution.wholeWordOnly() );
mTableSubstitutions->setCellWidget( row, 3, wholeWordChk );
connect( wholeWordChk, SIGNAL( toggled( bool ) ), this, SLOT( tableChanged() ) );
}
//
// QgsSubstitutionListDialog
//
QgsSubstitutionListDialog::QgsSubstitutionListDialog( QWidget* parent )
: QDialog( parent )
, mWidget( nullptr )
{
setWindowTitle( tr( "Substitutions" ) );
QVBoxLayout* vLayout = new QVBoxLayout();
mWidget = new QgsSubstitutionListWidget();
vLayout->addWidget( mWidget );
QDialogButtonBox* bbox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal );
connect( bbox, SIGNAL( accepted() ), this, SLOT( accept() ) );
connect( bbox, SIGNAL( rejected() ), this, SLOT( reject() ) );
vLayout->addWidget( bbox );
setLayout( vLayout );
}
void QgsSubstitutionListDialog::setSubstitutions( const QgsStringReplacementCollection& substitutions )
{
mWidget->setSubstitutions( substitutions );
}
QgsStringReplacementCollection QgsSubstitutionListDialog::substitutions() const
{
return mWidget->substitutions();
}

View File

@ -0,0 +1,111 @@
/***************************************************************************
qgssubstitutionlistwidget.h
---------------------------
begin : August 2016
copyright : (C) 2016 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 QGSSUBSTITUTIONLISTWIDGET_H
#define QGSSUBSTITUTIONLISTWIDGET_H
#include <QDialog>
#include "qgspanelwidget.h"
#include "ui_qgssubstitutionlistwidgetbase.h"
#include "qgsstringutils.h"
/** \class QgsSubstitutionListWidget
* \ingroup app
* A widget which allows users to specify a list of substitutions to apply to a string, with
* options for exporting and importing substitution lists.
* \note added in QGIS 3.0
* \see QgsSubstitutionListDialog
*/
class APP_EXPORT QgsSubstitutionListWidget : public QgsPanelWidget, private Ui::QgsSubstitutionListWidgetBase
{
Q_OBJECT
Q_PROPERTY( QgsStringReplacementCollection substitutions READ substitutions WRITE setSubstitutions NOTIFY substitutionsChanged )
public:
/** Constructor for QgsSubstitutionListWidget.
* @param parent parent widget
*/
QgsSubstitutionListWidget( QWidget* parent = nullptr );
/** Sets the list of substitutions to show in the widget.
* @param substitutions substitution list
* @see substitutions()
*/
void setSubstitutions( const QgsStringReplacementCollection& substitutions );
/** Returns the list of substitutions currently defined by the widget.
* @see setSubstitutions()
*/
QgsStringReplacementCollection substitutions() const;
signals:
//! Emitted when the substitution definitions change.
void substitutionsChanged( const QgsStringReplacementCollection& substitutions );
private slots:
void on_mButtonAdd_clicked();
void on_mButtonRemove_clicked();
void tableChanged();
void on_mButtonExport_clicked();
void on_mButtonImport_clicked();
private:
void addSubstitution( const QgsStringReplacement& substitution );
};
/** \class QgsSubstitutionListDialog
* \ingroup app
* A dialog which allows users to specify a list of substitutions to apply to a string, with
* options for exporting and importing substitution lists.
* \see QgsSubstitutionListWidget
*/
class APP_EXPORT QgsSubstitutionListDialog : public QDialog
{
Q_OBJECT
Q_PROPERTY( QgsStringReplacementCollection substitutions READ substitutions WRITE setSubstitutions )
public:
/** Constructor for QgsSubstitutionListDialog.
* @param parent parent widget
*/
QgsSubstitutionListDialog( QWidget* parent = nullptr );
/** Sets the list of substitutions to show in the dialog.
* @param substitutions substitution list
* @see substitutions()
*/
void setSubstitutions( const QgsStringReplacementCollection& substitutions );
/** Returns the list of substitutions currently defined by the dialog.
* @see setSubstitutions()
*/
QgsStringReplacementCollection substitutions() const;
private:
QgsSubstitutionListWidget* mWidget;
};
#endif // QGSSUBSTITUTIONLISTWIDGET_H

View File

@ -128,6 +128,7 @@ QgsPalLayerSettings::QgsPalLayerSettings()
// font processing info
mTextFontFound = true;
mTextFontFamily = QApplication::font().family();
useSubstitutions = false;
// text formatting
wrapChar = "";
@ -387,6 +388,8 @@ QgsPalLayerSettings& QgsPalLayerSettings::operator=( const QgsPalLayerSettings &
// font processing info
mTextFontFound = s.mTextFontFound;
mTextFontFamily = s.mTextFontFamily;
substitutions = s.substitutions;
useSubstitutions = s.useSubstitutions;
// text formatting
wrapChar = s.wrapChar;
@ -847,7 +850,11 @@ void QgsPalLayerSettings::readFromLayer( QgsVectorLayer* layer )
blendMode = QgsPainting::getCompositionMode(
static_cast< QgsPainting::BlendMode >( layer->customProperty( "labeling/blendMode", QVariant( QgsPainting::BlendNormal ) ).toUInt() ) );
previewBkgrdColor = QColor( layer->customProperty( "labeling/previewBkgrdColor", QVariant( "#ffffff" ) ).toString() );
QDomDocument doc( "substitutions" );
doc.setContent( layer->customProperty( "labeling/substitutions" ).toString() );
QDomElement replacementElem = doc.firstChildElement( "substitutions" );
substitutions.readXml( replacementElem );
useSubstitutions = layer->customProperty( "labeling/useSubstitutions" ).toBool();
// text formatting
wrapChar = layer->customProperty( "labeling/wrapChar" ).toString();
@ -1127,6 +1134,14 @@ void QgsPalLayerSettings::writeToLayer( QgsVectorLayer* layer )
layer->setCustomProperty( "labeling/textTransp", textTransp );
layer->setCustomProperty( "labeling/blendMode", QgsPainting::getBlendModeEnum( blendMode ) );
layer->setCustomProperty( "labeling/previewBkgrdColor", previewBkgrdColor.name() );
QDomDocument doc( "substitutions" );
QDomElement replacementElem = doc.createElement( "substitutions" );
substitutions.writeXml( replacementElem, doc );
QString replacementProps;
QTextStream stream( &replacementProps );
replacementElem.save( stream, -1 );
layer->setCustomProperty( "labeling/substitutions", replacementProps );
layer->setCustomProperty( "labeling/useSubstitutions", useSubstitutions );
// text formatting
layer->setCustomProperty( "labeling/wrapChar", wrapChar );
@ -1298,7 +1313,8 @@ void QgsPalLayerSettings::readXml( QDomElement& elem )
blendMode = QgsPainting::getCompositionMode(
static_cast< QgsPainting::BlendMode >( textStyleElem.attribute( "blendMode", QString::number( QgsPainting::BlendNormal ) ).toUInt() ) );
previewBkgrdColor = QColor( textStyleElem.attribute( "previewBkgrdColor", "#ffffff" ) );
substitutions.readXml( textStyleElem.firstChildElement( "substitutions" ) );
useSubstitutions = textStyleElem.attribute( "useSubstitutions" ).toInt();
// text formatting
QDomElement textFormatElem = elem.firstChildElement( "text-format" );
@ -1564,6 +1580,10 @@ QDomElement QgsPalLayerSettings::writeXml( QDomDocument& doc )
textStyleElem.setAttribute( "textTransp", textTransp );
textStyleElem.setAttribute( "blendMode", QgsPainting::getBlendModeEnum( blendMode ) );
textStyleElem.setAttribute( "previewBkgrdColor", previewBkgrdColor.name() );
QDomElement replacementElem = doc.createElement( "substitutions" );
substitutions.writeXml( replacementElem, doc );
textStyleElem.appendChild( replacementElem );
textStyleElem.setAttribute( "useSubstitutions", useSubstitutions );
// text formatting
QDomElement textFormatElem = doc.createElement( "text-format" );
@ -2314,6 +2334,12 @@ void QgsPalLayerSettings::registerFeature( QgsFeature& f, QgsRenderContext &cont
labelText = v.isNull() ? "" : v.toString();
}
// apply text replacements
if ( useSubstitutions )
{
labelText = substitutions.process( labelText );
}
// data defined format numbers?
bool formatnum = formatNumbers;
if ( dataDefinedEvaluate( QgsPalLayerSettings::NumFormat, exprVal, &context.expressionContext(), formatNumbers ) )

View File

@ -34,6 +34,7 @@
#include "qgsfield.h"
#include "qgspoint.h"
#include "qgsmapunitscale.h"
#include "qgsstringutils.h"
namespace pal
{
@ -480,6 +481,11 @@ class CORE_EXPORT QgsPalLayerSettings
QPainter::CompositionMode blendMode;
QColor previewBkgrdColor;
//! Substitution collection for automatic text substitution with labels
QgsStringReplacementCollection substitutions;
//! True if substitutions should be applied
bool useSubstitutions;
//-- text formatting
QString wrapChar;

View File

@ -337,3 +337,89 @@ QString QgsStringUtils::insertLinks( const QString& string, bool *foundLinks )
return converted;
}
QgsStringReplacement::QgsStringReplacement( const QString& match, const QString& replacement, bool caseSensitive, bool wholeWordOnly )
: mMatch( match )
, mReplacement( replacement )
, mCaseSensitive( caseSensitive )
, mWholeWordOnly( wholeWordOnly )
{
if ( mWholeWordOnly )
mRx = QRegExp( QString( "\\b%1\\b" ).arg( mMatch ),
mCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
}
QString QgsStringReplacement::process( const QString& input ) const
{
QString result = input;
if ( !mWholeWordOnly )
{
return result.replace( mMatch, mReplacement, mCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
}
else
{
return result.replace( mRx, mReplacement );
}
}
QgsStringMap QgsStringReplacement::properties() const
{
QgsStringMap map;
map.insert( "match", mMatch );
map.insert( "replace", mReplacement );
map.insert( "caseSensitive", mCaseSensitive ? "1" : "0" );
map.insert( "wholeWord", mWholeWordOnly ? "1" : "0" );
return map;
}
QgsStringReplacement QgsStringReplacement::fromProperties( const QgsStringMap& properties )
{
return QgsStringReplacement( properties.value( "match" ),
properties.value( "replace" ),
properties.value( "caseSensitive", "0" ) == "1",
properties.value( "wholeWord", "0" ) == "1" );
}
QString QgsStringReplacementCollection::process( const QString& input ) const
{
QString result = input;
Q_FOREACH ( const QgsStringReplacement& r, mReplacements )
{
result = r.process( result );
}
return result;
}
void QgsStringReplacementCollection::writeXml( QDomElement& elem, QDomDocument& doc ) const
{
Q_FOREACH ( const QgsStringReplacement& r, mReplacements )
{
QgsStringMap props = r.properties();
QDomElement propEl = doc.createElement( "replacement" );
QgsStringMap::const_iterator it = props.constBegin();
for ( ; it != props.constEnd(); ++it )
{
propEl.setAttribute( it.key(), it.value() );
}
elem.appendChild( propEl );
}
}
void QgsStringReplacementCollection::readXml( const QDomElement& elem )
{
mReplacements.clear();
QDomNodeList nodelist = elem.elementsByTagName( "replacement" );
for ( int i = 0;i < nodelist.count(); i++ )
{
QDomElement replacementElem = nodelist.at( i ).toElement();
QDomNamedNodeMap nodeMap = replacementElem.attributes();
QgsStringMap props;
for ( int j = 0; j < nodeMap.count(); ++j )
{
props.insert( nodeMap.item( j ).nodeName(), nodeMap.item( j ).nodeValue() );
}
mReplacements << QgsStringReplacement::fromProperties( props );
}
}

View File

@ -14,10 +14,148 @@
***************************************************************************/
#include <QString>
#include <QRegExp>
#include <QList>
#include <QDomDocument>
#include "qgis.h"
#ifndef QGSSTRINGUTILS_H
#define QGSSTRINGUTILS_H
/** \ingroup core
* \class QgsStringReplacement
* \brief A representation of a single string replacement.
* \note Added in version 3.0
*/
class CORE_EXPORT QgsStringReplacement
{
public:
/** Constructor for QgsStringReplacement.
* @param match string to match
* @param replacement string to replace match with
* @param caseSensitive set to true for a case sensitive match
* @param wholeWordOnly set to true to match complete words only, or false to allow partial word matches
*/
QgsStringReplacement( const QString& match,
const QString& replacement,
bool caseSensitive = false,
bool wholeWordOnly = false );
//! Returns the string matched by this object
QString match() const { return mMatch; }
//! Returns the string to replace matches with
QString replacement() const { return mReplacement; }
//! Returns true if match is case sensitive
bool caseSensitive() const { return mCaseSensitive; }
//! Returns true if match only applies to whole words, or false if partial word matches are permitted
bool wholeWordOnly() const { return mWholeWordOnly; }
/** Processes a given input string, applying any valid replacements which should be made.
* @param input input string
* @returns input string with any matches replaced by replacement string
*/
QString process( const QString& input ) const;
bool operator==( const QgsStringReplacement& other )
{
return mMatch == other.mMatch
&& mReplacement == other.mReplacement
&& mCaseSensitive == other.mCaseSensitive
&& mWholeWordOnly == other.mWholeWordOnly;
}
/** Returns a map of the replacement properties.
* @see fromProperties()
*/
QgsStringMap properties() const;
/** Creates a new QgsStringReplacement from an encoded properties map.
* @see properties()
*/
static QgsStringReplacement fromProperties( const QgsStringMap& properties );
private:
QString mMatch;
QString mReplacement;
bool mCaseSensitive;
bool mWholeWordOnly;
QRegExp mRx;
};
/** \ingroup core
* \class QgsStringReplacementCollection
* \brief A collection of string replacements (specified using QgsStringReplacement objects).
* \note Added in version 3.0
*/
class CORE_EXPORT QgsStringReplacementCollection
{
public:
/** Constructor for QgsStringReplacementCollection
* @param replacements initial list of string replacements
*/
QgsStringReplacementCollection( const QList< QgsStringReplacement >& replacements = QList< QgsStringReplacement >() )
: mReplacements( replacements )
{}
/** Returns the list of string replacements in this collection.
* @see setReplacements()
*/
QList< QgsStringReplacement > replacements() const { return mReplacements; }
/** Sets the list of string replacements in this collection.
* @param replacements list of string replacements to apply. Replacements are applied in the
* order they are specified here.
* @see replacements()
*/
void setReplacements( const QList< QgsStringReplacement >& replacements )
{
mReplacements = replacements;
}
/** Processes a given input string, applying any valid replacements which should be made
* using QgsStringReplacement objects contained by this collection. Replacements
* are made in order of the QgsStringReplacement objects contained in the collection.
* @param input input string
* @returns input string with any matches replaced by replacement string
*/
QString process( const QString& input ) const;
/** Writes the collection state to an XML element.
* @param elem target DOM element
* @param doc DOM document
* @see readXml()
*/
void writeXml( QDomElement& elem, QDomDocument& doc ) const;
/** Reads the collection state from an XML element.
* @param elem DOM element
* @see writeXml()
*/
void readXml( const QDomElement& elem );
private:
QList< QgsStringReplacement > mReplacements;
};
/** \ingroup core
* \class QgsStringUtils
* \brief Utility functions for working with strings.

View File

@ -118,6 +118,8 @@ class CORE_EXPORT QgsVectorLayerLabelProvider : public QgsAbstractLabelProvider
//! List of generated
QList<QgsLabelFeature*> mLabels;
friend class TestQgsLabelingEngine;
};
#endif // QGSVECTORLAYERLABELPROVIDER_H

View File

@ -618,7 +618,7 @@
<item>
<widget class="QStackedWidget" name="mLabelStackedWidget">
<property name="currentIndex">
<number>5</number>
<number>0</number>
</property>
<widget class="QWidget" name="mLabelPage_Text">
<layout class="QVBoxLayout" name="verticalLayout_6">
@ -646,9 +646,9 @@
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<y>-78</y>
<width>448</width>
<height>411</height>
<height>442</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
@ -691,6 +691,91 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item row="3" column="2">
<widget class="QgsDataDefinedButton" name="mFontStyleDDBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="mFontStyleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Style</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QgsUnitSelectionWidget" name="mFontSizeUnitWidget" native="true"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="mFontLabel_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Text</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="mFontTranspLabel">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Transparency</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QgsColorButton" name="btnTextColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel" name="labelBlendMode">
<property name="text">
<string>Blend mode</string>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QgsDataDefinedButton" name="mFontColorDDBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="10" column="1" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_14">
<item>
@ -743,73 +828,13 @@
</item>
</layout>
</item>
<item row="5" column="1">
<widget class="QgsDoubleSpinBox" name="mFontSizeSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="decimals">
<number>4</number>
</property>
<property name="maximum">
<double>999999999.000000000000000</double>
</property>
<property name="showClearButton" stdset="0">
<bool>false</bool>
</property>
</widget>
<item row="12" column="1">
<widget class="QgsBlendModeComboBox" name="comboBlendMode"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="mFontStyleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item row="9" column="2">
<widget class="QgsDataDefinedButton" name="mFontCaseDDBtn">
<property name="text">
<string>Style</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QComboBox" name="mFontCapitalsComboBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Capitalization style of text</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="mFontTranspLabel">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Transparency</string>
<string>...</string>
</property>
</widget>
</item>
@ -857,38 +882,36 @@
</property>
</widget>
</item>
<item row="9" column="2">
<widget class="QgsDataDefinedButton" name="mFontCaseDDBtn">
<item row="9" column="0">
<widget class="QLabel" name="mFontCapitalsLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Type case</string>
</property>
</widget>
</item>
<item row="12" column="2">
<widget class="QgsDataDefinedButton" name="mFontBlendModeDDBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QgsDataDefinedButton" name="mFontUnitsDDBtn">
<property name="text">
<string>...</string>
<item row="5" column="0">
<widget class="QLabel" name="mFontSizeLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QgsDataDefinedButton" name="mFontColorDDBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QgsDataDefinedButton" name="mFontDDBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="8" column="2">
<widget class="QgsDataDefinedButton" name="mFontTranspDDBtn">
<property name="text">
<string>...</string>
<string>Size</string>
</property>
</widget>
</item>
@ -960,46 +983,66 @@
</layout>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="mFontCapitalsLabel">
<item row="2" column="1">
<widget class="QFrame" name="mFontFamilyFrame">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="mFontMissingLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">color: #990000;
font-style: italic;</string>
</property>
<property name="text">
<string>Font is missing.</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="5" column="1">
<widget class="QgsDoubleSpinBox" name="mFontSizeSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Type case</string>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item row="12" column="2">
<widget class="QgsDataDefinedButton" name="mFontBlendModeDDBtn">
<property name="text">
<string>...</string>
<property name="decimals">
<number>4</number>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="mFontColorLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<property name="maximum">
<double>999999999.000000000000000</double>
</property>
<property name="text">
<string>Color</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QgsDataDefinedButton" name="mFontSizeDDBtn">
<property name="text">
<string>...</string>
<property name="showClearButton" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
@ -1010,86 +1053,19 @@
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QgsColorButton" name="btnTextColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel" name="labelBlendMode">
<item row="6" column="2">
<widget class="QgsDataDefinedButton" name="mFontUnitsDDBtn">
<property name="text">
<string>Blend mode</string>
<string>...</string>
</property>
</widget>
</item>
<item row="11" column="1" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QLabel" name="mFontWordSpacingLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>word</string>
</property>
</widget>
</item>
<item>
<widget class="QgsDoubleSpinBox" name="mFontWordSpacingSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Space in pixels or map units, relative to size unit choice</string>
</property>
<property name="decimals">
<number>4</number>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>999999999.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="showClearButton" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QgsDataDefinedButton" name="mFontWordSpacingDDBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
<item row="1" column="2">
<widget class="QgsDataDefinedButton" name="mFontDDBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_13">
@ -1273,66 +1249,64 @@
</item>
</layout>
</item>
<item row="12" column="1">
<widget class="QgsBlendModeComboBox" name="comboBlendMode"/>
</item>
<item row="3" column="2">
<widget class="QgsDataDefinedButton" name="mFontStyleDDBtn">
<item row="8" column="2">
<widget class="QgsDataDefinedButton" name="mFontTranspDDBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="mFontSizeLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Size</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QFrame" name="mFontFamilyFrame">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="mFontMissingLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">color: #990000;
font-style: italic;</string>
</property>
<property name="text">
<string>Font is missing.</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<item row="11" column="1" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QLabel" name="mFontWordSpacingLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>word</string>
</property>
</widget>
</item>
<item>
<widget class="QgsDoubleSpinBox" name="mFontWordSpacingSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Space in pixels or map units, relative to size unit choice</string>
</property>
<property name="decimals">
<number>4</number>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>999999999.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="showClearButton" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QgsDataDefinedButton" name="mFontWordSpacingDDBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QFontComboBox" name="mFontFamilyCmbBx">
@ -1341,22 +1315,71 @@ font-style: italic;</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QgsUnitSelectionWidget" name="mFontSizeUnitWidget" native="true"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="mFontLabel_2">
<item row="9" column="1">
<widget class="QComboBox" name="mFontCapitalsComboBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Capitalization style of text</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="mFontColorLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Text</string>
<string>Color</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QgsDataDefinedButton" name="mFontSizeDDBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="13" column="0" colspan="2">
<widget class="QCheckBox" name="mCheckBoxSubstituteText">
<property name="toolTip">
<string>If enabled, the label text will automatically be modified using a preset list of substitutes</string>
</property>
<property name="text">
<string>Apply label text substitutes</string>
</property>
</widget>
</item>
<item row="13" column="2">
<widget class="QToolButton" name="mToolButtonConfigureSubstitutes">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Configure substitutes</string>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
@ -5389,7 +5412,7 @@ font-style: italic;</string>
<rect>
<x>0</x>
<y>0</y>
<width>448</width>
<width>429</width>
<height>799</height>
</rect>
</property>

View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QgsSubstitutionListWidgetBase</class>
<widget class="QgsPanelWidget" name="QgsSubstitutionListWidgetBase">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>291</width>
<height>416</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="mTableSubstitutions">
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<column>
<property name="text">
<string>Text</string>
</property>
</column>
<column>
<property name="text">
<string>Substitution</string>
</property>
</column>
<column>
<property name="text">
<string>Case Sensitive</string>
</property>
</column>
<column>
<property name="text">
<string>Whole Word</string>
</property>
<property name="toolTip">
<string>If checked, only whole word matches are replaced</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QToolButton" name="mButtonAdd">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/symbologyAdd.svg</normaloff>:/images/themes/default/symbologyAdd.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="mButtonRemove">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/symbologyRemove.svg</normaloff>:/images/themes/default/symbologyRemove.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="mButtonImport">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionFileOpen.svg</normaloff>:/images/themes/default/mActionFileOpen.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="mButtonExport">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionFileSave.svg</normaloff>:/images/themes/default/mActionFileSave.svg</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QgsPanelWidget</class>
<extends>QWidget</extends>
<header>qgspanelwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="../../images/images.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -43,6 +43,7 @@ class TestQgsLabelingEngine : public QObject
void testRuleBased();
void zOrder(); //test that labels are stacked correctly
void testEncodeDecodePositionOrder();
void testSubstitutions();
private:
QgsVectorLayer* vl;
@ -413,6 +414,46 @@ void TestQgsLabelingEngine::testEncodeDecodePositionOrder()
QCOMPARE( decoded, expected );
}
void TestQgsLabelingEngine::testSubstitutions()
{
QgsPalLayerSettings settings;
settings.useSubstitutions = false;
QgsStringReplacementCollection collection( QList< QgsStringReplacement >() << QgsStringReplacement( "aa", "bb" ) );
settings.substitutions = collection;
settings.fieldName = QString( "'aa label'" );
settings.isExpression = true;
QgsVectorLayerLabelProvider* provider = new QgsVectorLayerLabelProvider( vl, "test", true, &settings );
QgsFeature f( vl->fields(), 1 );
f.setGeometry( QgsGeometry::fromPoint( QgsPoint( 1, 2 ) ) );
// make a fake render context
QSize size( 640, 480 );
QgsMapSettings mapSettings;
mapSettings.setOutputSize( size );
mapSettings.setExtent( vl->extent() );
mapSettings.setLayers( QStringList() << vl->id() );
mapSettings.setOutputDpi( 96 );
QgsRenderContext context = QgsRenderContext::fromMapSettings( mapSettings );
QStringList attributes;
QgsLabelingEngine engine;
engine.setMapSettings( mapSettings );
engine.addProvider( provider );
provider->prepare( context, attributes );
provider->registerFeature( f, context );
QCOMPARE( provider->mLabels.at( 0 )->labelText(), QString( "aa label" ) );
//with substitution
settings.useSubstitutions = true;
QgsVectorLayerLabelProvider* provider2 = new QgsVectorLayerLabelProvider( vl, "test2", true, &settings );
engine.addProvider( provider2 );
provider2->prepare( context, attributes );
provider2->registerFeature( f, context );
QCOMPARE( provider2->mLabels.at( 0 )->labelText(), QString( "bb label" ) );
}
bool TestQgsLabelingEngine::imageCheck( const QString& testName, QImage &image, int mismatchCount )
{
//draw background

View File

@ -92,6 +92,7 @@ ADD_PYTHON_TEST(PyQgsSymbolLayerCreateSld test_qgssymbollayer_createsld.py)
ADD_PYTHON_TEST(PyQgsArrowSymbolLayer test_qgsarrowsymbollayer.py)
ADD_PYTHON_TEST(PyQgsSymbolExpressionVariables test_qgssymbolexpressionvariables.py)
ADD_PYTHON_TEST(PyQgsSyntacticSugar test_syntactic_sugar.py)
ADD_PYTHON_TEST(PyQgsStringUtils test_qgsstringutils.py)
ADD_PYTHON_TEST(PyQgsSymbol test_qgssymbol.py)
ADD_PYTHON_TEST(PyQgsTreeWidgetItem test_qgstreewidgetitem.py)
ADD_PYTHON_TEST(PyQgsUnitTypes test_qgsunittypes.py)

View File

@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsStringUtils.
.. note:: 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.
"""
__author__ = 'Nyall Dawson'
__date__ = '30/08/2016'
__copyright__ = 'Copyright 2016, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
import qgis # NOQA
from qgis.PyQt.QtXml import (QDomDocument, QDomElement)
from qgis.core import (QgsStringUtils,
QgsStringReplacement,
QgsStringReplacementCollection
)
from qgis.testing import unittest
class PyQgsStringReplacement(unittest.TestCase):
def testBasic(self):
""" basic tests for QgsStringReplacement"""
r = QgsStringReplacement('match', 'replace')
self.assertEqual(r.match(), 'match')
self.assertEqual(r.replacement(), 'replace')
r = QgsStringReplacement('match', 'replace', True, True)
self.assertTrue(r.wholeWordOnly())
self.assertTrue(r.caseSensitive())
def testReplace(self):
""" test applying replacements"""
# case insensitive
r = QgsStringReplacement('match', 'replace', False, False)
self.assertEqual(r.process('one MaTch only'), 'one replace only')
self.assertEqual(r.process('more then one MaTch here match two'), 'more then one replace here replace two')
self.assertEqual(r.process('match start and end MaTch'), 'replace start and end replace')
self.assertEqual(r.process('no hits'), 'no hits')
self.assertEqual(r.process('some exmatches here'), 'some exreplacees here')
self.assertEqual(r.process(''), '')
# case sensitive
r = QgsStringReplacement('match', 'replace', True, False)
self.assertEqual(r.process('one MaTch only'), 'one MaTch only')
self.assertEqual(r.process('one match only'), 'one replace only')
# whole word only, case insensitive
r = QgsStringReplacement('match', 'replace', False, True)
self.assertEqual(r.process('some exmatches here'), 'some exmatches here')
self.assertEqual(r.process('some match here'), 'some replace here')
self.assertEqual(r.process('some exmatches MaTch here'), 'some exmatches replace here')
self.assertEqual(r.process('some match maTCh here'), 'some replace replace here')
self.assertEqual(r.process('some -match. here'), 'some -replace. here')
self.assertEqual(r.process('match here'), 'replace here')
self.assertEqual(r.process('some match'), 'some replace')
# whole word only, case sensitive
r = QgsStringReplacement('match', 'replace', True, True)
self.assertEqual(r.process('some exmatches here'), 'some exmatches here')
self.assertEqual(r.process('some match here'), 'some replace here')
self.assertEqual(r.process('some exmatches MaTch here'), 'some exmatches MaTch here')
self.assertEqual(r.process('some match maTCh here'), 'some replace maTCh here')
def testEquality(self):
""" test equality operator"""
r1 = QgsStringReplacement('a', 'b', True, True)
r2 = QgsStringReplacement('a', 'b', True, True)
self.assertEqual(r1, r2)
r2 = QgsStringReplacement('c', 'b')
self.assertNotEqual(r1, r2)
r2 = QgsStringReplacement('a', 'c')
self.assertNotEqual(r1, r2)
r2 = QgsStringReplacement('a', 'b', False, True)
self.assertNotEqual(r1, r2)
r2 = QgsStringReplacement('c', 'b', True, False)
self.assertNotEqual(r1, r2)
def testSaveRestore(self):
""" test saving/restoring replacement to map"""
r1 = QgsStringReplacement('a', 'b', True, True)
props = r1.properties()
r2 = QgsStringReplacement.fromProperties(props)
self.assertEqual(r1, r2)
r1 = QgsStringReplacement('a', 'b', False, False)
props = r1.properties()
r2 = QgsStringReplacement.fromProperties(props)
self.assertEqual(r1, r2)
class PyQgsStringReplacementCollection(unittest.TestCase):
def testBasic(self):
""" basic QgsStringReplacementCollection tests"""
list = [QgsStringReplacement('aa', '11'),
QgsStringReplacement('bb', '22')]
c = QgsStringReplacementCollection(list)
self.assertEqual(c.replacements(), list)
def testReplacements(self):
""" test replacing using collection of replacements """
c = QgsStringReplacementCollection()
c.setReplacements([QgsStringReplacement('aa', '11'),
QgsStringReplacement('bb', '22')])
self.assertEqual(c.process('here aa bb is aa string bb'), 'here 11 22 is 11 string 22')
self.assertEqual(c.process('no matches'), 'no matches')
self.assertEqual(c.process(''), '')
# test replacements are done in order
c.setReplacements([QgsStringReplacement('aa', '11'),
QgsStringReplacement('11', '22')])
self.assertEqual(c.process('string aa'), 'string 22')
# no replacements
c.setReplacements([])
self.assertEqual(c.process('string aa'), 'string aa')
def testSaveRestore(self):
""" test saving and restoring collections """
c = QgsStringReplacementCollection([QgsStringReplacement('aa', '11', False, False),
QgsStringReplacement('bb', '22', True, True)])
doc = QDomDocument("testdoc")
elem = doc.createElement("replacements")
c.writeXml(elem, doc)
c2 = QgsStringReplacementCollection()
c2.readXml(elem)
self.assertEqual(c2.replacements(), c.replacements())
if __name__ == '__main__':
unittest.main()