From 46fba7ce80039d5feb8445410ddf98e485f5951f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 29 Aug 2016 17:18:34 +1000 Subject: [PATCH] [FEATURE] Substitution list support for labeling Adds the ability to specify a list of text substitutes to make which apply to label text. Eg abbrevating street types. Users can export and import lists of substitutes to make reuse and sharing easier. --- python/core/qgspallabeling.sip | 5 + python/core/qgsstringutils.sip | 112 +++++ src/app/CMakeLists.txt | 2 + src/app/qgslabelinggui.cpp | 37 +- src/app/qgslabelinggui.h | 6 + src/app/qgssubstitutionlistwidget.cpp | 230 ++++++++++ src/app/qgssubstitutionlistwidget.h | 111 +++++ src/core/qgspallabeling.cpp | 30 +- src/core/qgspallabeling.h | 6 + src/core/qgsstringutils.cpp | 86 ++++ src/core/qgsstringutils.h | 138 ++++++ src/core/qgsvectorlayerlabelprovider.h | 2 + src/ui/qgslabelingguibase.ui | 553 ++++++++++++----------- src/ui/qgssubstitutionlistwidgetbase.ui | 134 ++++++ tests/src/core/testqgslabelingengine.cpp | 41 ++ tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgsstringutils.py | 137 ++++++ 17 files changed, 1363 insertions(+), 268 deletions(-) create mode 100644 src/app/qgssubstitutionlistwidget.cpp create mode 100644 src/app/qgssubstitutionlistwidget.h create mode 100644 src/ui/qgssubstitutionlistwidgetbase.ui create mode 100644 tests/src/python/test_qgsstringutils.py diff --git a/python/core/qgspallabeling.sip b/python/core/qgspallabeling.sip index 6343f457ebe..8599b8102ca 100644 --- a/python/core/qgspallabeling.sip +++ b/python/core/qgspallabeling.sip @@ -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; diff --git a/python/core/qgsstringutils.sip b/python/core/qgsstringutils.sip index 47ec677fbdc..7235a0cba93 100644 --- a/python/core/qgsstringutils.sip +++ b/python/core/qgsstringutils.sip @@ -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 +%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 +%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. diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 722800cf9e2..36b7b4662ec 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -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 diff --git a/src/app/qgslabelinggui.cpp b/src/app/qgslabelinggui.cpp index 957f7d20198..f2ff675b38e 100644 --- a/src/app/qgslabelinggui.cpp +++ b/src/app/qgslabelinggui.cpp @@ -34,6 +34,7 @@ #include "qgssvgselectorwidget.h" #include "qgsvectorlayerlabeling.h" #include "qgslogger.h" +#include "qgssubstitutionlistwidget.h" #include #include @@ -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 ); diff --git a/src/app/qgslabelinggui.h b/src/app/qgslabelinggui.h index 19f9ddc5953..2ec4bb691f2 100644 --- a/src/app/qgslabelinggui.h +++ b/src/app/qgslabelinggui.h @@ -21,6 +21,7 @@ #include #include #include +#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 diff --git a/src/app/qgssubstitutionlistwidget.cpp b/src/app/qgssubstitutionlistwidget.cpp new file mode 100644 index 00000000000..ff234958e8d --- /dev/null +++ b/src/app/qgssubstitutionlistwidget.cpp @@ -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 +#include +#include +#include +#include + +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( mTableSubstitutions->cellWidget( i, 2 ) ); + QCheckBox* chkWholeWord = qobject_cast( 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(); +} diff --git a/src/app/qgssubstitutionlistwidget.h b/src/app/qgssubstitutionlistwidget.h new file mode 100644 index 00000000000..acd8529a0dc --- /dev/null +++ b/src/app/qgssubstitutionlistwidget.h @@ -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 +#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 diff --git a/src/core/qgspallabeling.cpp b/src/core/qgspallabeling.cpp index 7f32bca2f3f..e73dfa48a94 100644 --- a/src/core/qgspallabeling.cpp +++ b/src/core/qgspallabeling.cpp @@ -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 ) ) diff --git a/src/core/qgspallabeling.h b/src/core/qgspallabeling.h index b9618de7023..b17fde2acff 100644 --- a/src/core/qgspallabeling.h +++ b/src/core/qgspallabeling.h @@ -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; diff --git a/src/core/qgsstringutils.cpp b/src/core/qgsstringutils.cpp index a33062a0e56..d622f16eb4d 100644 --- a/src/core/qgsstringutils.cpp +++ b/src/core/qgsstringutils.cpp @@ -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 ); + } + +} diff --git a/src/core/qgsstringutils.h b/src/core/qgsstringutils.h index 008618b62d9..47eb6de8b7a 100644 --- a/src/core/qgsstringutils.h +++ b/src/core/qgsstringutils.h @@ -14,10 +14,148 @@ ***************************************************************************/ #include +#include +#include +#include +#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. diff --git a/src/core/qgsvectorlayerlabelprovider.h b/src/core/qgsvectorlayerlabelprovider.h index cc5c36daefc..c24adc42225 100644 --- a/src/core/qgsvectorlayerlabelprovider.h +++ b/src/core/qgsvectorlayerlabelprovider.h @@ -118,6 +118,8 @@ class CORE_EXPORT QgsVectorLayerLabelProvider : public QgsAbstractLabelProvider //! List of generated QList mLabels; + + friend class TestQgsLabelingEngine; }; #endif // QGSVECTORLAYERLABELPROVIDER_H diff --git a/src/ui/qgslabelingguibase.ui b/src/ui/qgslabelingguibase.ui index 8b0adf2d238..2e6057b3bf9 100644 --- a/src/ui/qgslabelingguibase.ui +++ b/src/ui/qgslabelingguibase.ui @@ -618,7 +618,7 @@ - 5 + 0 @@ -646,9 +646,9 @@ 0 - 0 + -78 448 - 411 + 442 @@ -691,6 +691,91 @@ 0 + + + + ... + + + + + + + + 0 + 0 + + + + Style + + + + + + + + + + + 0 + 0 + + + + Text + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + true + + + Transparency + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + + + + Blend mode + + + + + + + ... + + + @@ -743,73 +828,13 @@ - - - - - 0 - 0 - - - - - 0 - 0 - - - - 4 - - - 999999999.000000000000000 - - - false - - + + - - - - - 0 - 0 - - + + - Style - - - - - - - true - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Capitalization style of text - - - - - - - true - - - Transparency + ... @@ -857,38 +882,36 @@ - - + + + + + 0 + 0 + + + + Type case + + + + + ... - - - - ... + + + + + 0 + 0 + - - - - - ... - - - - - - - ... - - - - - - - ... + Size @@ -960,46 +983,66 @@ - - + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + color: #990000; +font-style: italic; + + + Font is missing. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + - + 0 0 - - Type case + + + 0 + 0 + - - - - - - ... + + 4 - - - - - - - 0 - 0 - + + 999999999.000000000000000 - - Color - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - ... + + false @@ -1010,86 +1053,19 @@ - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - - + + - Blend mode + ... - - - - - - - 0 - 0 - - - - word - - - - - - - - 0 - 0 - - - - Space in pixels or map units, relative to size unit choice - - - 4 - - - -1000.000000000000000 - - - 999999999.000000000000000 - - - 0.100000000000000 - - - true - - - - - - - ... - - - - + + + + ... + + @@ -1273,66 +1249,64 @@ - - - - - + + ... - - - - - 0 - 0 - - - - Size - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - color: #990000; -font-style: italic; - - - Font is missing. - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - + + + + + + + 0 + 0 + + + + word + + + + + + + + 0 + 0 + + + + Space in pixels or map units, relative to size unit choice + + + 4 + + + -1000.000000000000000 + + + 999999999.000000000000000 + + + 0.100000000000000 + + + true + + + + + + + ... + + + + @@ -1341,22 +1315,71 @@ font-style: italic; - - - - - + + + + true + - + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Capitalization style of text + + + + + + + 0 0 - Text + Color - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + ... + + + + + + + If enabled, the label text will automatically be modified using a preset list of substitutes + + + Apply label text substitutes + + + + + + + false + + + Configure substitutes + + + ... @@ -5389,7 +5412,7 @@ font-style: italic; 0 0 - 448 + 429 799 diff --git a/src/ui/qgssubstitutionlistwidgetbase.ui b/src/ui/qgssubstitutionlistwidgetbase.ui new file mode 100644 index 00000000000..aa186d5a175 --- /dev/null +++ b/src/ui/qgssubstitutionlistwidgetbase.ui @@ -0,0 +1,134 @@ + + + QgsSubstitutionListWidgetBase + + + + 0 + 0 + 291 + 416 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::SelectRows + + + + Text + + + + + Substitution + + + + + Case Sensitive + + + + + Whole Word + + + If checked, only whole word matches are replaced + + + + + + + + + + ... + + + + :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg + + + + + + + ... + + + + :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg + + + + + + + ... + + + + :/images/themes/default/mActionFileOpen.svg:/images/themes/default/mActionFileOpen.svg + + + + + + + ... + + + + :/images/themes/default/mActionFileSave.svg:/images/themes/default/mActionFileSave.svg + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + QgsPanelWidget + QWidget +
qgspanelwidget.h
+ 1 +
+
+ + + + +
diff --git a/tests/src/core/testqgslabelingengine.cpp b/tests/src/core/testqgslabelingengine.cpp index 8a833c31188..a6804cab616 100644 --- a/tests/src/core/testqgslabelingengine.cpp +++ b/tests/src/core/testqgslabelingengine.cpp @@ -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 diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 568e74dd183..52bed0b8a8f 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -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) diff --git a/tests/src/python/test_qgsstringutils.py b/tests/src/python/test_qgsstringutils.py new file mode 100644 index 00000000000..44fa2620386 --- /dev/null +++ b/tests/src/python/test_qgsstringutils.py @@ -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()