[FEATURE] Add copy/paste symbol action to right click menu in categorized/graduated/rule based renderers

Allows symbols to be easily copied and pasted between these nodes
This commit is contained in:
Nyall Dawson 2019-07-31 18:17:00 +10:00
parent 47f83b582f
commit 55efeaeb2f
13 changed files with 226 additions and 6 deletions

View File

@ -86,6 +86,11 @@ from the XML file with a matching name.
.. versionadded:: 2.9
%End
protected slots:
virtual void pasteSymbolToSelection();
protected:
void updateUiFromRenderer();

View File

@ -60,6 +60,11 @@ Toggle the link between classes boundaries
void modelDataChanged();
void refreshRanges( bool reset = false );
protected slots:
virtual void pasteSymbolToSelection();
protected:
void updateUiFromRenderer( bool updateCount = true );
void connectUpdateHandlers();

View File

@ -81,6 +81,8 @@ to re-synchronize with the variables.
protected:
virtual QList<QgsSymbol *> selectedSymbols();
%Docstring
Subclasses may provide the capability of changing multiple symbols at once by implementing the following two methods
@ -123,9 +125,17 @@ Change marker sizes of selected symbols
Change marker angles of selected symbols
%End
virtual void copy();
virtual void paste();
virtual void pasteSymbolToSelection();
%Docstring
Pastes the clipboard symbol over selected items.
.. versionadded:: 3.10
%End
private:
virtual void apply();
%Docstring

View File

@ -70,6 +70,14 @@ Constructor for QgsRuleBasedRendererModel, for the specified ``renderer``.
void updateRule( const QModelIndex &index );
void removeRule( const QModelIndex &index );
void setSymbol( const QModelIndex &index, QgsSymbol *symbol /Transfer/ );
%Docstring
Sets the ``symbol`` for the rule at the specified ``index``. Ownership of the symbols is
transferred to the renderer.
.. versionadded:: 3.10
%End
void willAddRules( const QModelIndex &parent, int count ); // call beginInsertRows
void finishedAddingRules(); // call endInsertRows
@ -148,6 +156,8 @@ Opens the dialog for refining a rule using ranges
virtual void paste();
virtual void pasteSymbolToSelection();
};

View File

@ -1066,7 +1066,7 @@ static QString _nameForSymbolType( QgsSymbol::SymbolType type )
}
}
QDomElement QgsSymbolLayerUtils::saveSymbol( const QString &name, QgsSymbol *symbol, QDomDocument &doc, const QgsReadWriteContext &context )
QDomElement QgsSymbolLayerUtils::saveSymbol( const QString &name, const QgsSymbol *symbol, QDomDocument &doc, const QgsReadWriteContext &context )
{
Q_ASSERT( symbol );
QDomElement symEl = doc.createElement( QStringLiteral( "symbol" ) );

View File

@ -48,6 +48,7 @@
#include <QPen>
#include <QPainter>
#include <QFileDialog>
#include <QClipboard>
///@cond PRIVATE
@ -583,6 +584,12 @@ QgsCategorizedSymbolRendererWidget::QgsCategorizedSymbolRendererWidget( QgsVecto
connect( mMergeCategoriesAction, &QAction::triggered, this, &QgsCategorizedSymbolRendererWidget::mergeSelectedCategories );
mUnmergeCategoriesAction = new QAction( tr( "Unmerge Categories" ), this );
connect( mUnmergeCategoriesAction, &QAction::triggered, this, &QgsCategorizedSymbolRendererWidget::unmergeSelectedCategories );
connect( mContextMenu, &QMenu::aboutToShow, this, [ = ]
{
std::unique_ptr< QgsSymbol > tempSymbol( QgsSymbolLayerUtils::symbolFromMimeData( QApplication::clipboard()->mimeData() ) );
mPasteSymbolAction->setEnabled( static_cast< bool >( tempSymbol ) );
} );
}
QgsCategorizedSymbolRendererWidget::~QgsCategorizedSymbolRendererWidget()
@ -1073,6 +1080,32 @@ void QgsCategorizedSymbolRendererWidget::matchToSymbolsFromXml()
}
}
void QgsCategorizedSymbolRendererWidget::pasteSymbolToSelection()
{
std::unique_ptr< QgsSymbol > tempSymbol( QgsSymbolLayerUtils::symbolFromMimeData( QApplication::clipboard()->mimeData() ) );
if ( !tempSymbol )
return;
const QList<int> selectedCats = selectedCategories();
if ( !selectedCats.isEmpty() )
{
for ( int idx : selectedCats )
{
if ( mRenderer->categories().at( idx ).symbol()->type() != tempSymbol->type() )
continue;
std::unique_ptr< QgsSymbol > newCatSymbol( tempSymbol->clone() );
if ( selectedCats.count() > 1 )
{
//if updating multiple categories, retain the existing category colors
newCatSymbol->setColor( mRenderer->categories().at( idx ).symbol()->color() );
}
mRenderer->updateCategorySymbol( idx, newCatSymbol.release() );
}
emit widgetChanged();
}
}
void QgsCategorizedSymbolRendererWidget::cleanUpSymbolSelector( QgsPanelWidget *container )
{
QgsSymbolSelectorWidget *dlg = qobject_cast<QgsSymbolSelectorWidget *>( container );

View File

@ -148,6 +148,10 @@ class GUI_EXPORT QgsCategorizedSymbolRendererWidget : public QgsRendererWidget,
*/
void matchToSymbolsFromXml();
protected slots:
void pasteSymbolToSelection() override;
private slots:
void cleanUpSymbolSelector( QgsPanelWidget *container );

View File

@ -42,7 +42,7 @@
#include <QStandardItem>
#include <QPen>
#include <QPainter>
#include <QClipboard>
// ------------------------------ Model ------------------------------------
///@cond PRIVATE
@ -1402,3 +1402,34 @@ void QgsGraduatedSymbolRendererWidget::dataDefinedSizeLegend()
openPanel( panel ); // takes ownership of the panel
}
}
void QgsGraduatedSymbolRendererWidget::pasteSymbolToSelection()
{
std::unique_ptr< QgsSymbol > tempSymbol( QgsSymbolLayerUtils::symbolFromMimeData( QApplication::clipboard()->mimeData() ) );
if ( !tempSymbol )
return;
const QModelIndexList selectedRows = viewGraduated->selectionModel()->selectedRows();
for ( const QModelIndex &index : selectedRows )
{
if ( !index.isValid() )
continue;
const int row = index.row();
if ( !mRenderer || mRenderer->ranges().size() <= row )
continue;
if ( mRenderer->ranges().at( row ).symbol()->type() != tempSymbol->type() )
continue;
std::unique_ptr< QgsSymbol > newCatSymbol( tempSymbol->clone() );
if ( selectedRows.count() > 1 )
{
//if updating multiple ranges, retain the existing category colors
newCatSymbol->setColor( mRenderer->ranges().at( row ).symbol()->color() );
}
mRenderer->updateRangeSymbol( row, newCatSymbol.release() );
}
emit widgetChanged();
}

View File

@ -130,6 +130,10 @@ class GUI_EXPORT QgsGraduatedSymbolRendererWidget : public QgsRendererWidget, pr
void toggleMethodWidgets( int idx );
void dataDefinedSizeLegend();
protected slots:
void pasteSymbolToSelection() override;
protected:
void updateUiFromRenderer( bool updateCount = true );
void connectUpdateHandlers();

View File

@ -25,10 +25,12 @@
#include "qgspanelwidget.h"
#include "qgsproject.h"
#include "qgsexpressioncontextutils.h"
#include "qgssymbollayerutils.h"
#include <QMessageBox>
#include <QInputDialog>
#include <QMenu>
#include <QClipboard>
QgsRendererWidget::QgsRendererWidget( QgsVectorLayer *layer, QgsStyle *style )
: mLayer( layer )
@ -36,10 +38,19 @@ QgsRendererWidget::QgsRendererWidget( QgsVectorLayer *layer, QgsStyle *style )
{
contextMenu = new QMenu( tr( "Renderer Options" ), this );
mCopyAction = contextMenu->addAction( tr( "Copy" ), this, SLOT( copy() ) );
mCopyAction = new QAction( tr( "Copy" ), this );
connect( mCopyAction, &QAction::triggered, this, &QgsRendererWidget::copy );
mCopyAction->setShortcut( QKeySequence( QKeySequence::Copy ) );
mPasteAction = contextMenu->addAction( tr( "Paste" ), this, SLOT( paste() ) );
mPasteAction = new QAction( tr( "Paste" ), this );
mPasteAction->setShortcut( QKeySequence( QKeySequence::Paste ) );
connect( mPasteAction, &QAction::triggered, this, &QgsRendererWidget::paste );
mCopySymbolAction = new QAction( tr( "Copy Symbol" ), this );
contextMenu->addAction( mCopySymbolAction );
connect( mCopySymbolAction, &QAction::triggered, this, &QgsRendererWidget::copySymbol );
mPasteSymbolAction = new QAction( tr( "Paste Symbol" ), this );
contextMenu->addAction( mPasteSymbolAction );
connect( mPasteSymbolAction, &QAction::triggered, this, &QgsRendererWidget::pasteSymbolToSelection );
contextMenu->addSeparator();
contextMenu->addAction( tr( "Change Color…" ), this, SLOT( changeSymbolColor() ) );
@ -55,6 +66,12 @@ QgsRendererWidget::QgsRendererWidget( QgsVectorLayer *layer, QgsStyle *style )
contextMenu->addAction( tr( "Change Size…" ), this, SLOT( changeSymbolSize() ) );
contextMenu->addAction( tr( "Change Angle…" ), this, SLOT( changeSymbolAngle() ) );
}
connect( contextMenu, &QMenu::aboutToShow, this, [ = ]
{
std::unique_ptr< QgsSymbol > tempSymbol( QgsSymbolLayerUtils::symbolFromMimeData( QApplication::clipboard()->mimeData() ) );
mPasteSymbolAction->setEnabled( static_cast< bool >( tempSymbol ) );
} );
}
void QgsRendererWidget::contextMenuViewCategories( QPoint )
@ -258,6 +275,22 @@ void QgsRendererWidget::changeSymbolAngle()
}
}
void QgsRendererWidget::pasteSymbolToSelection()
{
}
void QgsRendererWidget::copySymbol()
{
QList<QgsSymbol *> symbolList = selectedSymbols();
if ( symbolList.isEmpty() )
{
return;
}
QApplication::clipboard()->setMimeData( QgsSymbolLayerUtils::symbolToMimeData( symbolList.at( 0 ) ) );
}
void QgsRendererWidget::showSymbolLevelsDialog( QgsFeatureRenderer *r )
{
QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this );

View File

@ -95,6 +95,18 @@ class GUI_EXPORT QgsRendererWidget : public QgsPanelWidget
QAction *mCopyAction = nullptr;
QAction *mPasteAction = nullptr;
/**
* Copy symbol action.
* \since QGIS 3.10
*/
QAction *mCopySymbolAction = nullptr;
/**
* Paste symbol action.
* \since QGIS 3.10
*/
QAction *mPasteSymbolAction = nullptr;
//! Context in which widget is shown
QgsSymbolWidgetContext mContext;
@ -126,9 +138,21 @@ class GUI_EXPORT QgsRendererWidget : public QgsPanelWidget
//! Change marker angles of selected symbols
void changeSymbolAngle();
virtual void copy() {}
virtual void paste() {}
/**
* Pastes the clipboard symbol over selected items.
*
* \since QGIS 3.10
*/
virtual void pasteSymbolToSelection();
private slots:
void copySymbol();
private:
/**

View File

@ -38,6 +38,7 @@
#include <QTreeWidgetItem>
#include <QVBoxLayout>
#include <QMessageBox>
#include <QClipboard>
#ifdef ENABLE_MODELTEST
#include "modeltest.h"
@ -51,6 +52,7 @@ QgsRendererWidget *QgsRuleBasedRendererWidget::create( QgsVectorLayer *layer, Qg
QgsRuleBasedRendererWidget::QgsRuleBasedRendererWidget( QgsVectorLayer *layer, QgsStyle *style, QgsFeatureRenderer *renderer )
: QgsRendererWidget( layer, style )
, mContextMenu( new QMenu( this ) )
{
mRenderer = nullptr;
// try to recognize the previous renderer
@ -90,7 +92,6 @@ QgsRuleBasedRendererWidget::QgsRuleBasedRendererWidget( QgsVectorLayer *layer, Q
mRefineMenu->addAction( tr( "Add Categories to Rule" ), this, SLOT( refineRuleCategories() ) );
mRefineMenu->addAction( tr( "Add Ranges to Rule" ), this, SLOT( refineRuleRanges() ) );
btnRefineRule->setMenu( mRefineMenu );
contextMenu->addMenu( mRefineMenu );
btnAddRule->setIcon( QIcon( QgsApplication::iconPath( "symbologyAdd.svg" ) ) );
btnEditRule->setIcon( QIcon( QgsApplication::iconPath( "symbologyEdit.svg" ) ) );
@ -99,7 +100,7 @@ QgsRuleBasedRendererWidget::QgsRuleBasedRendererWidget( QgsVectorLayer *layer, Q
connect( viewRules, &QAbstractItemView::doubleClicked, this, static_cast < void ( QgsRuleBasedRendererWidget::* )( const QModelIndex &index ) > ( &QgsRuleBasedRendererWidget::editRule ) );
// support for context menu (now handled generically)
connect( viewRules, &QWidget::customContextMenuRequested, this, &QgsRuleBasedRendererWidget::contextMenuViewCategories );
connect( viewRules, &QWidget::customContextMenuRequested, this, &QgsRuleBasedRendererWidget::showContextMenu );
connect( viewRules->selectionModel(), &QItemSelectionModel::currentChanged, this, &QgsRuleBasedRendererWidget::currentRuleChanged );
connect( viewRules->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QgsRuleBasedRendererWidget::selectedRulesChanged );
@ -124,6 +125,11 @@ QgsRuleBasedRendererWidget::QgsRuleBasedRendererWidget( QgsVectorLayer *layer, Q
restoreSectionWidths();
connect( mContextMenu, &QMenu::aboutToShow, this, [ = ]
{
std::unique_ptr< QgsSymbol > tempSymbol( QgsSymbolLayerUtils::symbolFromMimeData( QApplication::clipboard()->mimeData() ) );
mPasteSymbolAction->setEnabled( static_cast< bool >( tempSymbol ) );
} );
}
QgsRuleBasedRendererWidget::~QgsRuleBasedRendererWidget()
@ -483,6 +489,26 @@ void QgsRuleBasedRendererWidget::paste()
mModel->dropMimeData( mime, Qt::CopyAction, index.row(), index.column(), index.parent() );
}
void QgsRuleBasedRendererWidget::pasteSymbolToSelection()
{
std::unique_ptr< QgsSymbol > tempSymbol( QgsSymbolLayerUtils::symbolFromMimeData( QApplication::clipboard()->mimeData() ) );
if ( !tempSymbol )
return;
const QModelIndexList indexList = viewRules->selectionModel()->selectedRows();
for ( const QModelIndex &index : indexList )
{
if ( QgsRuleBasedRenderer::Rule *rule = mModel->ruleForIndex( index ) )
{
if ( !rule->symbol() || rule->symbol()->type() != tempSymbol->type() )
continue;
mModel->setSymbol( index, tempSymbol->clone() );
}
}
emit widgetChanged();
}
void QgsRuleBasedRendererWidget::refineRuleCategoriesAccepted( QgsPanelWidget *panel )
{
QgsCategorizedSymbolRendererWidget *w = qobject_cast<QgsCategorizedSymbolRendererWidget *>( panel );
@ -535,6 +561,23 @@ void QgsRuleBasedRendererWidget::liveUpdateRuleFromPanel()
ruleWidgetPanelAccepted( qobject_cast<QgsPanelWidget *>( sender() ) );
}
void QgsRuleBasedRendererWidget::showContextMenu( QPoint )
{
mContextMenu->clear();
mContextMenu->addAction( mCopyAction );
mContextMenu->addAction( mPasteAction );
const QList< QAction * > actions = contextMenu->actions();
for ( QAction *act : actions )
{
mContextMenu->addAction( act );
}
mContextMenu->addMenu( mRefineMenu );
mContextMenu->exec( QCursor::pos() );
}
void QgsRuleBasedRendererWidget::countFeatures()
{
@ -1275,6 +1318,13 @@ void QgsRuleBasedRendererModel::removeRule( const QModelIndex &index )
endRemoveRows();
}
void QgsRuleBasedRendererModel::setSymbol( const QModelIndex &index, QgsSymbol *symbol )
{
QgsRuleBasedRenderer::Rule *rule = ruleForIndex( index );
rule->setSymbol( symbol );
emit dataChanged( index, index );
}
void QgsRuleBasedRendererModel::willAddRules( const QModelIndex &parent, int count )
{
int row = rowCount( parent ); // only consider appending

View File

@ -85,6 +85,14 @@ class GUI_EXPORT QgsRuleBasedRendererModel : public QAbstractItemModel
void updateRule( const QModelIndex &index );
void removeRule( const QModelIndex &index );
/**
* Sets the \a symbol for the rule at the specified \a index. Ownership of the symbols is
* transferred to the renderer.
*
* \since QGIS 3.10
*/
void setSymbol( const QModelIndex &index, QgsSymbol *symbol SIP_TRANSFER );
void willAddRules( const QModelIndex &parent, int count ); // call beginInsertRows
void finishedAddingRules(); // call endInsertRows
@ -162,16 +170,19 @@ class GUI_EXPORT QgsRuleBasedRendererWidget : public QgsRendererWidget, private
QAction *mDeleteAction = nullptr;
QgsRuleBasedRenderer::RuleList mCopyBuffer;
QMenu *mContextMenu = nullptr;
protected slots:
void copy() override;
void paste() override;
void pasteSymbolToSelection() override;
private slots:
void refineRuleCategoriesAccepted( QgsPanelWidget *panel );
void refineRuleRangesAccepted( QgsPanelWidget *panel );
void ruleWidgetPanelAccepted( QgsPanelWidget *panel );
void liveUpdateRuleFromPanel();
void showContextMenu( QPoint p );
};
///////