1 /***************************************************************************
2     qgscategorizedsymbolrendererwidget.cpp
3     ---------------------
4     begin                : November 2009
5     copyright            : (C) 2009 by Martin Dobias
6     email                : wonder dot sk at gmail dot com
7  ***************************************************************************
8  *                                                                         *
9  *   This program is free software; you can redistribute it and/or modify  *
10  *   it under the terms of the GNU General Public License as published by  *
11  *   the Free Software Foundation; either version 2 of the License, or     *
12  *   (at your option) any later version.                                   *
13  *                                                                         *
14  ***************************************************************************/
15 
16 #include "qgscategorizedsymbolrendererwidget.h"
17 #include "qgspanelwidget.h"
18 
19 #include "qgscategorizedsymbolrenderer.h"
20 
21 #include "qgsdatadefinedsizelegend.h"
22 #include "qgsdatadefinedsizelegendwidget.h"
23 #include "qgssymbol.h"
24 #include "qgssymbollayerutils.h"
25 #include "qgscolorramp.h"
26 #include "qgscolorrampbutton.h"
27 #include "qgsstyle.h"
28 #include "qgslogger.h"
29 #include "qgsexpressioncontextutils.h"
30 #include "qgstemporalcontroller.h"
31 
32 #include "qgssymbolselectordialog.h"
33 #include "qgsexpressionbuilderdialog.h"
34 
35 #include "qgsvectorlayer.h"
36 #include "qgsfeatureiterator.h"
37 
38 #include "qgsproject.h"
39 #include "qgsexpression.h"
40 #include "qgsmapcanvas.h"
41 #include "qgssettings.h"
42 #include "qgsguiutils.h"
43 #include "qgsmarkersymbol.h"
44 
45 #include <QKeyEvent>
46 #include <QMenu>
47 #include <QMessageBox>
48 #include <QStandardItemModel>
49 #include <QStandardItem>
50 #include <QPen>
51 #include <QPainter>
52 #include <QFileDialog>
53 #include <QClipboard>
54 
55 ///@cond PRIVATE
56 
QgsCategorizedSymbolRendererModel(QObject * parent)57 QgsCategorizedSymbolRendererModel::QgsCategorizedSymbolRendererModel( QObject *parent ) : QAbstractItemModel( parent )
58   , mMimeFormat( QStringLiteral( "application/x-qgscategorizedsymbolrendererv2model" ) )
59 {
60 }
61 
setRenderer(QgsCategorizedSymbolRenderer * renderer)62 void QgsCategorizedSymbolRendererModel::setRenderer( QgsCategorizedSymbolRenderer *renderer )
63 {
64   if ( mRenderer )
65   {
66     beginRemoveRows( QModelIndex(), 0, std::max< int >( mRenderer->categories().size() - 1, 0 ) );
67     mRenderer = nullptr;
68     endRemoveRows();
69   }
70   if ( renderer )
71   {
72     mRenderer = renderer;
73     if ( renderer->categories().size() > 0 )
74     {
75       beginInsertRows( QModelIndex(), 0, renderer->categories().size() - 1 );
76       endInsertRows();
77     }
78   }
79 }
80 
addCategory(const QgsRendererCategory & cat)81 void QgsCategorizedSymbolRendererModel::addCategory( const QgsRendererCategory &cat )
82 {
83   if ( !mRenderer ) return;
84   const int idx = mRenderer->categories().size();
85   beginInsertRows( QModelIndex(), idx, idx );
86   mRenderer->addCategory( cat );
87   endInsertRows();
88 }
89 
category(const QModelIndex & index)90 QgsRendererCategory QgsCategorizedSymbolRendererModel::category( const QModelIndex &index )
91 {
92   if ( !mRenderer )
93   {
94     return QgsRendererCategory();
95   }
96   const QgsCategoryList &catList = mRenderer->categories();
97   const int row = index.row();
98   if ( row >= catList.size() )
99   {
100     return QgsRendererCategory();
101   }
102   return catList.at( row );
103 }
104 
105 
flags(const QModelIndex & index) const106 Qt::ItemFlags QgsCategorizedSymbolRendererModel::flags( const QModelIndex &index ) const
107 {
108   if ( !index.isValid() || !mRenderer )
109   {
110     return Qt::ItemIsDropEnabled;
111   }
112 
113   Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsUserCheckable;
114   if ( index.column() == 1 )
115   {
116     const QgsRendererCategory category = mRenderer->categories().value( index.row() );
117     if ( category.value().type() != QVariant::List )
118     {
119       flags |= Qt::ItemIsEditable;
120     }
121   }
122   else if ( index.column() == 2 )
123   {
124     flags |= Qt::ItemIsEditable;
125   }
126   return flags;
127 }
128 
supportedDropActions() const129 Qt::DropActions QgsCategorizedSymbolRendererModel::supportedDropActions() const
130 {
131   return Qt::MoveAction;
132 }
133 
data(const QModelIndex & index,int role) const134 QVariant QgsCategorizedSymbolRendererModel::data( const QModelIndex &index, int role ) const
135 {
136   if ( !index.isValid() || !mRenderer )
137     return QVariant();
138 
139   const QgsRendererCategory category = mRenderer->categories().value( index.row() );
140 
141   switch ( role )
142   {
143     case Qt::CheckStateRole:
144     {
145       if ( index.column() == 0 )
146       {
147         return category.renderState() ? Qt::Checked : Qt::Unchecked;
148       }
149       break;
150     }
151 
152     case Qt::DisplayRole:
153     case Qt::ToolTipRole:
154     {
155       switch ( index.column() )
156       {
157         case 1:
158         {
159           if ( category.value().type() == QVariant::List )
160           {
161             QStringList res;
162             const QVariantList list = category.value().toList();
163             res.reserve( list.size() );
164             for ( const QVariant &v : list )
165               res << v.toString();
166 
167             if ( role == Qt::DisplayRole )
168               return res.join( ';' );
169             else // tooltip
170               return res.join( '\n' );
171           }
172           else if ( !category.value().isValid() || category.value().isNull() || category.value().toString().isEmpty() )
173           {
174             return tr( "all other values" );
175           }
176           else
177           {
178             return category.value().toString();
179           }
180         }
181         case 2:
182           return category.label();
183       }
184       break;
185     }
186 
187     case Qt::FontRole:
188     {
189       if ( index.column() == 1 && category.value().type() != QVariant::List && ( !category.value().isValid() || category.value().isNull() || category.value().toString().isEmpty() ) )
190       {
191         QFont italicFont;
192         italicFont.setItalic( true );
193         return italicFont;
194       }
195       return QVariant();
196     }
197 
198     case Qt::DecorationRole:
199     {
200       if ( index.column() == 0 && category.symbol() )
201       {
202         const int iconSize = QgsGuiUtils::scaleIconSize( 16 );
203         return QgsSymbolLayerUtils::symbolPreviewIcon( category.symbol(), QSize( iconSize, iconSize ) );
204       }
205       break;
206     }
207 
208     case Qt::ForegroundRole:
209     {
210       QBrush brush( qApp->palette().color( QPalette::Text ), Qt::SolidPattern );
211       if ( index.column() == 1 && ( category.value().type() == QVariant::List
212                                     || !category.value().isValid() || category.value().isNull() || category.value().toString().isEmpty() ) )
213       {
214         QColor fadedTextColor = brush.color();
215         fadedTextColor.setAlpha( 128 );
216         brush.setColor( fadedTextColor );
217       }
218       return brush;
219     }
220 
221     case Qt::TextAlignmentRole:
222     {
223       return ( index.column() == 0 ) ? static_cast<Qt::Alignment::Int>( Qt::AlignHCenter ) : static_cast<Qt::Alignment::Int>( Qt::AlignLeft );
224     }
225 
226     case Qt::EditRole:
227     {
228       switch ( index.column() )
229       {
230         case 1:
231         {
232           if ( category.value().type() == QVariant::List )
233           {
234             QStringList res;
235             const QVariantList list = category.value().toList();
236             res.reserve( list.size() );
237             for ( const QVariant &v : list )
238               res << v.toString();
239 
240             return res.join( ';' );
241           }
242           else
243           {
244             return category.value();
245           }
246         }
247 
248         case 2:
249           return category.label();
250       }
251       break;
252     }
253   }
254 
255   return QVariant();
256 }
257 
setData(const QModelIndex & index,const QVariant & value,int role)258 bool QgsCategorizedSymbolRendererModel::setData( const QModelIndex &index, const QVariant &value, int role )
259 {
260   if ( !index.isValid() )
261     return false;
262 
263   if ( index.column() == 0 && role == Qt::CheckStateRole )
264   {
265     mRenderer->updateCategoryRenderState( index.row(), value == Qt::Checked );
266     emit dataChanged( index, index );
267     return true;
268   }
269 
270   if ( role != Qt::EditRole )
271     return false;
272 
273   switch ( index.column() )
274   {
275     case 1: // value
276     {
277       // try to preserve variant type for this value
278       QVariant val;
279       switch ( mRenderer->categories().value( index.row() ).value().type() )
280       {
281         case QVariant::Int:
282           val = value.toInt();
283           break;
284         case QVariant::Double:
285           val = value.toDouble();
286           break;
287         case QVariant::List:
288         {
289           const QStringList parts = value.toString().split( ';' );
290           QVariantList list;
291           list.reserve( parts.count() );
292           for ( const QString &p : parts )
293             list << p;
294 
295           if ( list.count() == 1 )
296             val = list.at( 0 );
297           else
298             val = list;
299           break;
300         }
301         default:
302           val = value.toString();
303           break;
304       }
305       mRenderer->updateCategoryValue( index.row(), val );
306       break;
307     }
308     case 2: // label
309       mRenderer->updateCategoryLabel( index.row(), value.toString() );
310       break;
311     default:
312       return false;
313   }
314 
315   emit dataChanged( index, index );
316   return true;
317 }
318 
headerData(int section,Qt::Orientation orientation,int role) const319 QVariant QgsCategorizedSymbolRendererModel::headerData( int section, Qt::Orientation orientation, int role ) const
320 {
321   if ( orientation == Qt::Horizontal && role == Qt::DisplayRole && section >= 0 && section < 3 )
322   {
323     QStringList lst;
324     lst << tr( "Symbol" ) << tr( "Value" ) << tr( "Legend" );
325     return lst.value( section );
326   }
327   return QVariant();
328 }
329 
rowCount(const QModelIndex & parent) const330 int QgsCategorizedSymbolRendererModel::rowCount( const QModelIndex &parent ) const
331 {
332   if ( parent.isValid() || !mRenderer )
333   {
334     return 0;
335   }
336   return mRenderer->categories().size();
337 }
338 
columnCount(const QModelIndex & index) const339 int QgsCategorizedSymbolRendererModel::columnCount( const QModelIndex &index ) const
340 {
341   Q_UNUSED( index )
342   return 3;
343 }
344 
index(int row,int column,const QModelIndex & parent) const345 QModelIndex QgsCategorizedSymbolRendererModel::index( int row, int column, const QModelIndex &parent ) const
346 {
347   if ( hasIndex( row, column, parent ) )
348   {
349     return createIndex( row, column );
350   }
351   return QModelIndex();
352 }
353 
parent(const QModelIndex & index) const354 QModelIndex QgsCategorizedSymbolRendererModel::parent( const QModelIndex &index ) const
355 {
356   Q_UNUSED( index )
357   return QModelIndex();
358 }
359 
mimeTypes() const360 QStringList QgsCategorizedSymbolRendererModel::mimeTypes() const
361 {
362   QStringList types;
363   types << mMimeFormat;
364   return types;
365 }
366 
mimeData(const QModelIndexList & indexes) const367 QMimeData *QgsCategorizedSymbolRendererModel::mimeData( const QModelIndexList &indexes ) const
368 {
369   QMimeData *mimeData = new QMimeData();
370   QByteArray encodedData;
371 
372   QDataStream stream( &encodedData, QIODevice::WriteOnly );
373 
374   // Create list of rows
375   const auto constIndexes = indexes;
376   for ( const QModelIndex &index : constIndexes )
377   {
378     if ( !index.isValid() || index.column() != 0 )
379       continue;
380 
381     stream << index.row();
382   }
383   mimeData->setData( mMimeFormat, encodedData );
384   return mimeData;
385 }
386 
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)387 bool QgsCategorizedSymbolRendererModel::dropMimeData( const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent )
388 {
389   Q_UNUSED( row )
390   Q_UNUSED( column )
391   if ( action != Qt::MoveAction ) return true;
392 
393   if ( !data->hasFormat( mMimeFormat ) ) return false;
394 
395   QByteArray encodedData = data->data( mMimeFormat );
396   QDataStream stream( &encodedData, QIODevice::ReadOnly );
397 
398   QVector<int> rows;
399   while ( !stream.atEnd() )
400   {
401     int r;
402     stream >> r;
403     rows.append( r );
404   }
405 
406   int to = parent.row();
407   // to is -1 if dragged outside items, i.e. below any item,
408   // then move to the last position
409   if ( to == -1 ) to = mRenderer->categories().size(); // out of rang ok, will be decreased
410   for ( int i = rows.size() - 1; i >= 0; i-- )
411   {
412     QgsDebugMsg( QStringLiteral( "move %1 to %2" ).arg( rows[i] ).arg( to ) );
413     int t = to;
414     // moveCategory first removes and then inserts
415     if ( rows[i] < t ) t--;
416     mRenderer->moveCategory( rows[i], t );
417     // current moved under another, shift its index up
418     for ( int j = 0; j < i; j++ )
419     {
420       if ( to < rows[j] && rows[i] > rows[j] ) rows[j] += 1;
421     }
422     // removed under 'to' so the target shifted down
423     if ( rows[i] < to ) to--;
424   }
425   emit dataChanged( createIndex( 0, 0 ), createIndex( mRenderer->categories().size(), 0 ) );
426   emit rowsMoved();
427   return false;
428 }
429 
deleteRows(QList<int> rows)430 void QgsCategorizedSymbolRendererModel::deleteRows( QList<int> rows )
431 {
432   std::sort( rows.begin(), rows.end() ); // list might be unsorted, depending on how the user selected the rows
433   for ( int i = rows.size() - 1; i >= 0; i-- )
434   {
435     beginRemoveRows( QModelIndex(), rows[i], rows[i] );
436     mRenderer->deleteCategory( rows[i] );
437     endRemoveRows();
438   }
439 }
440 
removeAllRows()441 void QgsCategorizedSymbolRendererModel::removeAllRows()
442 {
443   beginRemoveRows( QModelIndex(), 0, mRenderer->categories().size() - 1 );
444   mRenderer->deleteAllCategories();
445   endRemoveRows();
446 }
447 
sort(int column,Qt::SortOrder order)448 void QgsCategorizedSymbolRendererModel::sort( int column, Qt::SortOrder order )
449 {
450   if ( column == 0 )
451   {
452     return;
453   }
454   if ( column == 1 )
455   {
456     mRenderer->sortByValue( order );
457   }
458   else if ( column == 2 )
459   {
460     mRenderer->sortByLabel( order );
461   }
462   emit dataChanged( createIndex( 0, 0 ), createIndex( mRenderer->categories().size(), 0 ) );
463 }
464 
updateSymbology()465 void QgsCategorizedSymbolRendererModel::updateSymbology()
466 {
467   emit dataChanged( createIndex( 0, 0 ), createIndex( mRenderer->categories().size(), 0 ) );
468 }
469 
470 // ------------------------------ View style --------------------------------
QgsCategorizedSymbolRendererViewStyle(QWidget * parent)471 QgsCategorizedSymbolRendererViewStyle::QgsCategorizedSymbolRendererViewStyle( QWidget *parent )
472   : QgsProxyStyle( parent )
473 {}
474 
drawPrimitive(PrimitiveElement element,const QStyleOption * option,QPainter * painter,const QWidget * widget) const475 void QgsCategorizedSymbolRendererViewStyle::drawPrimitive( PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget ) const
476 {
477   if ( element == QStyle::PE_IndicatorItemViewItemDrop && !option->rect.isNull() )
478   {
479     QStyleOption opt( *option );
480     opt.rect.setLeft( 0 );
481     // draw always as line above, because we move item to that index
482     opt.rect.setHeight( 0 );
483     if ( widget ) opt.rect.setRight( widget->width() );
484     QProxyStyle::drawPrimitive( element, &opt, painter, widget );
485     return;
486   }
487   QProxyStyle::drawPrimitive( element, option, painter, widget );
488 }
489 
490 ///@endcond
491 
492 // ------------------------------ Widget ------------------------------------
create(QgsVectorLayer * layer,QgsStyle * style,QgsFeatureRenderer * renderer)493 QgsRendererWidget *QgsCategorizedSymbolRendererWidget::create( QgsVectorLayer *layer, QgsStyle *style, QgsFeatureRenderer *renderer )
494 {
495   return new QgsCategorizedSymbolRendererWidget( layer, style, renderer );
496 }
497 
QgsCategorizedSymbolRendererWidget(QgsVectorLayer * layer,QgsStyle * style,QgsFeatureRenderer * renderer)498 QgsCategorizedSymbolRendererWidget::QgsCategorizedSymbolRendererWidget( QgsVectorLayer *layer, QgsStyle *style, QgsFeatureRenderer *renderer )
499   : QgsRendererWidget( layer, style )
500   , mContextMenu( new QMenu( this ) )
501 {
502 
503   // try to recognize the previous renderer
504   // (null renderer means "no previous renderer")
505   if ( renderer )
506   {
507     mRenderer.reset( QgsCategorizedSymbolRenderer::convertFromRenderer( renderer, layer ) );
508   }
509   if ( !mRenderer )
510   {
511     mRenderer = std::make_unique< QgsCategorizedSymbolRenderer >( QString(), QgsCategoryList() );
512     if ( renderer )
513       renderer->copyRendererData( mRenderer.get() );
514   }
515 
516   const QString attrName = mRenderer->classAttribute();
517   mOldClassificationAttribute = attrName;
518 
519   // setup user interface
520   setupUi( this );
521   layout()->setContentsMargins( 0, 0, 0, 0 );
522 
523   mExpressionWidget->setLayer( mLayer );
524   btnChangeCategorizedSymbol->setLayer( mLayer );
525   btnChangeCategorizedSymbol->registerExpressionContextGenerator( this );
526 
527   // initiate color ramp button to random
528   btnColorRamp->setShowRandomColorRamp( true );
529 
530   // set project default color ramp
531   const QString defaultColorRamp = QgsProject::instance()->readEntry( QStringLiteral( "DefaultStyles" ), QStringLiteral( "/ColorRamp" ), QString() );
532   if ( !defaultColorRamp.isEmpty() )
533   {
534     btnColorRamp->setColorRampFromName( defaultColorRamp );
535   }
536   else
537   {
538     btnColorRamp->setRandomColorRamp();
539   }
540 
541   mCategorizedSymbol.reset( QgsSymbol::defaultSymbol( mLayer->geometryType() ) );
542   if ( mCategorizedSymbol )
543   {
544     btnChangeCategorizedSymbol->setSymbolType( mCategorizedSymbol->type() );
545     btnChangeCategorizedSymbol->setSymbol( mCategorizedSymbol->clone() );
546   }
547 
548   mModel = new QgsCategorizedSymbolRendererModel( this );
549   mModel->setRenderer( mRenderer.get() );
550 
551   // update GUI from renderer
552   updateUiFromRenderer();
553 
554   viewCategories->setModel( mModel );
555   viewCategories->resizeColumnToContents( 0 );
556   viewCategories->resizeColumnToContents( 1 );
557   viewCategories->resizeColumnToContents( 2 );
558 
559   viewCategories->setStyle( new QgsCategorizedSymbolRendererViewStyle( viewCategories ) );
560   connect( viewCategories->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QgsCategorizedSymbolRendererWidget::selectionChanged );
561 
562   connect( mModel, &QgsCategorizedSymbolRendererModel::rowsMoved, this, &QgsCategorizedSymbolRendererWidget::rowsMoved );
563   connect( mModel, &QAbstractItemModel::dataChanged, this, &QgsPanelWidget::widgetChanged );
564 
565   connect( mExpressionWidget, static_cast < void ( QgsFieldExpressionWidget::* )( const QString & ) >( &QgsFieldExpressionWidget::fieldChanged ), this, &QgsCategorizedSymbolRendererWidget::categoryColumnChanged );
566 
567   connect( viewCategories, &QAbstractItemView::doubleClicked, this, &QgsCategorizedSymbolRendererWidget::categoriesDoubleClicked );
568   connect( viewCategories, &QTreeView::customContextMenuRequested, this, &QgsCategorizedSymbolRendererWidget::showContextMenu );
569 
570   connect( btnChangeCategorizedSymbol, &QgsSymbolButton::changed, this, &QgsCategorizedSymbolRendererWidget::updateSymbolsFromButton );
571 
572   connect( btnAddCategories, &QAbstractButton::clicked, this, &QgsCategorizedSymbolRendererWidget::addCategories );
573   connect( btnDeleteCategories, &QAbstractButton::clicked, this, &QgsCategorizedSymbolRendererWidget::deleteCategories );
574   connect( btnDeleteAllCategories, &QAbstractButton::clicked, this, &QgsCategorizedSymbolRendererWidget::deleteAllCategories );
575   connect( btnAddCategory, &QAbstractButton::clicked, this, &QgsCategorizedSymbolRendererWidget::addCategory );
576 
577   connect( btnColorRamp, &QgsColorRampButton::colorRampChanged, this, &QgsCategorizedSymbolRendererWidget::applyColorRamp );
578 
579   // menus for data-defined rotation/size
580   QMenu *advMenu = new QMenu;
581 
582   advMenu->addAction( tr( "Match to Saved Symbols" ), this, &QgsCategorizedSymbolRendererWidget::matchToSymbolsFromLibrary );
583   advMenu->addAction( tr( "Match to Symbols from File…" ), this, &QgsCategorizedSymbolRendererWidget::matchToSymbolsFromXml );
584   mActionLevels = advMenu->addAction( tr( "Symbol Levels…" ), this, &QgsCategorizedSymbolRendererWidget::showSymbolLevels );
585   if ( mCategorizedSymbol && mCategorizedSymbol->type() == Qgis::SymbolType::Marker )
586   {
587     QAction *actionDdsLegend = advMenu->addAction( tr( "Data-defined Size Legend…" ) );
588     // only from Qt 5.6 there is convenience addAction() with new style connection
589     connect( actionDdsLegend, &QAction::triggered, this, &QgsCategorizedSymbolRendererWidget::dataDefinedSizeLegend );
590   }
591 
592   btnAdvanced->setMenu( advMenu );
593 
594   mExpressionWidget->registerExpressionContextGenerator( this );
595 
596   mMergeCategoriesAction = new QAction( tr( "Merge Categories" ), this );
597   connect( mMergeCategoriesAction, &QAction::triggered, this, &QgsCategorizedSymbolRendererWidget::mergeSelectedCategories );
598   mUnmergeCategoriesAction = new QAction( tr( "Unmerge Categories" ), this );
599   connect( mUnmergeCategoriesAction, &QAction::triggered, this, &QgsCategorizedSymbolRendererWidget::unmergeSelectedCategories );
600 
601   connect( mContextMenu, &QMenu::aboutToShow, this, [ = ]
602   {
603     const std::unique_ptr< QgsSymbol > tempSymbol( QgsSymbolLayerUtils::symbolFromMimeData( QApplication::clipboard()->mimeData() ) );
604     mPasteSymbolAction->setEnabled( static_cast< bool >( tempSymbol ) );
605   } );
606 }
607 
~QgsCategorizedSymbolRendererWidget()608 QgsCategorizedSymbolRendererWidget::~QgsCategorizedSymbolRendererWidget()
609 {
610   delete mModel;
611 }
612 
updateUiFromRenderer()613 void QgsCategorizedSymbolRendererWidget::updateUiFromRenderer()
614 {
615   // Note: This assumes that the signals for UI element changes have not
616   // yet been connected, so that the updates to color ramp, symbol, etc
617   // don't override existing customizations.
618 
619   //mModel->setRenderer ( mRenderer ); // necessary?
620 
621   // set column
622   const QString attrName = mRenderer->classAttribute();
623   mExpressionWidget->setField( attrName );
624 
625   // set source symbol
626   if ( mRenderer->sourceSymbol() )
627   {
628     mCategorizedSymbol.reset( mRenderer->sourceSymbol()->clone() );
629     whileBlocking( btnChangeCategorizedSymbol )->setSymbol( mCategorizedSymbol->clone() );
630   }
631 
632   // if a color ramp attached to the renderer, enable the color ramp button
633   if ( mRenderer->sourceColorRamp() )
634   {
635     btnColorRamp->setColorRamp( mRenderer->sourceColorRamp() );
636   }
637 }
638 
renderer()639 QgsFeatureRenderer *QgsCategorizedSymbolRendererWidget::renderer()
640 {
641   return mRenderer.get();
642 }
643 
setContext(const QgsSymbolWidgetContext & context)644 void QgsCategorizedSymbolRendererWidget::setContext( const QgsSymbolWidgetContext &context )
645 {
646   QgsRendererWidget::setContext( context );
647   btnChangeCategorizedSymbol->setMapCanvas( context.mapCanvas() );
648   btnChangeCategorizedSymbol->setMessageBar( context.messageBar() );
649 }
650 
disableSymbolLevels()651 void QgsCategorizedSymbolRendererWidget::disableSymbolLevels()
652 {
653   delete mActionLevels;
654   mActionLevels = nullptr;
655 }
656 
changeSelectedSymbols()657 void QgsCategorizedSymbolRendererWidget::changeSelectedSymbols()
658 {
659   const QList<int> selectedCats = selectedCategories();
660 
661   if ( !selectedCats.isEmpty() )
662   {
663     QgsSymbol *newSymbol = mCategorizedSymbol->clone();
664     QgsSymbolSelectorDialog dlg( newSymbol, mStyle, mLayer, this );
665     dlg.setContext( context() );
666     if ( !dlg.exec() )
667     {
668       delete newSymbol;
669       return;
670     }
671 
672     const auto constSelectedCats = selectedCats;
673     for ( const int idx : constSelectedCats )
674     {
675       const QgsRendererCategory category = mRenderer->categories().value( idx );
676 
677       QgsSymbol *newCatSymbol = newSymbol->clone();
678       newCatSymbol->setColor( mRenderer->categories()[idx].symbol()->color() );
679       mRenderer->updateCategorySymbol( idx, newCatSymbol );
680     }
681   }
682 }
683 
changeCategorizedSymbol()684 void QgsCategorizedSymbolRendererWidget::changeCategorizedSymbol()
685 {
686   QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this );
687   std::unique_ptr<QgsSymbol> newSymbol( mCategorizedSymbol->clone() );
688   if ( panel && panel->dockMode() )
689   {
690     // bit tricky here - the widget doesn't take ownership of the symbol. So we need it to last for the duration of the
691     // panel's existence. Accordingly, just kinda give it ownership here, and clean up in cleanUpSymbolSelector
692     QgsSymbolSelectorWidget *dlg = new QgsSymbolSelectorWidget( newSymbol.release(), mStyle, mLayer, panel );
693     dlg->setContext( mContext );
694     connect( dlg, &QgsPanelWidget::widgetChanged, this, &QgsCategorizedSymbolRendererWidget::updateSymbolsFromWidget );
695     connect( dlg, &QgsPanelWidget::panelAccepted, this, &QgsCategorizedSymbolRendererWidget::cleanUpSymbolSelector );
696     openPanel( dlg );
697   }
698   else
699   {
700     QgsSymbolSelectorDialog dlg( newSymbol.get(), mStyle, mLayer, panel );
701     dlg.setContext( mContext );
702     if ( !dlg.exec() || !newSymbol )
703     {
704       return;
705     }
706 
707     mCategorizedSymbol = std::move( newSymbol );
708     applyChangeToSymbol();
709   }
710 }
711 
712 
populateCategories()713 void QgsCategorizedSymbolRendererWidget::populateCategories()
714 {
715 }
716 
categoryColumnChanged(const QString & field)717 void QgsCategorizedSymbolRendererWidget::categoryColumnChanged( const QString &field )
718 {
719   mRenderer->setClassAttribute( field );
720   emit widgetChanged();
721 }
722 
categoriesDoubleClicked(const QModelIndex & idx)723 void QgsCategorizedSymbolRendererWidget::categoriesDoubleClicked( const QModelIndex &idx )
724 {
725   if ( idx.isValid() && idx.column() == 0 )
726     changeCategorySymbol();
727 }
728 
changeCategorySymbol()729 void QgsCategorizedSymbolRendererWidget::changeCategorySymbol()
730 {
731   const QgsRendererCategory category = mRenderer->categories().value( currentCategoryRow() );
732 
733   std::unique_ptr< QgsSymbol > symbol;
734 
735   if ( auto *lSymbol = category.symbol() )
736   {
737     symbol.reset( lSymbol->clone() );
738   }
739   else
740   {
741     symbol.reset( QgsSymbol::defaultSymbol( mLayer->geometryType() ) );
742   }
743 
744   QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this );
745   if ( panel && panel->dockMode() )
746   {
747     QgsSymbolSelectorWidget *dlg = new QgsSymbolSelectorWidget( symbol.release(), mStyle, mLayer, panel );
748     dlg->setContext( mContext );
749     dlg->setPanelTitle( category.label() );
750     connect( dlg, &QgsPanelWidget::widgetChanged, this, &QgsCategorizedSymbolRendererWidget::updateSymbolsFromWidget );
751     connect( dlg, &QgsPanelWidget::panelAccepted, this, &QgsCategorizedSymbolRendererWidget::cleanUpSymbolSelector );
752     openPanel( dlg );
753   }
754   else
755   {
756     QgsSymbolSelectorDialog dlg( symbol.get(), mStyle, mLayer, panel );
757     dlg.setContext( mContext );
758     if ( !dlg.exec() || !symbol )
759     {
760       return;
761     }
762 
763     mCategorizedSymbol = std::move( symbol );
764     applyChangeToSymbol();
765   }
766 }
767 
768 
addCategories()769 void QgsCategorizedSymbolRendererWidget::addCategories()
770 {
771   const QString attrName = mExpressionWidget->currentField();
772   const int idx = mLayer->fields().lookupField( attrName );
773   QList<QVariant> uniqueValues;
774   if ( idx == -1 )
775   {
776     // Lets assume it's an expression
777     QgsExpression *expression = new QgsExpression( attrName );
778     QgsExpressionContext context;
779     context << QgsExpressionContextUtils::globalScope()
780             << QgsExpressionContextUtils::projectScope( QgsProject::instance() )
781             << QgsExpressionContextUtils::atlasScope( nullptr )
782             << QgsExpressionContextUtils::layerScope( mLayer );
783 
784     expression->prepare( &context );
785     QgsFeatureIterator fit = mLayer->getFeatures();
786     QgsFeature feature;
787     while ( fit.nextFeature( feature ) )
788     {
789       context.setFeature( feature );
790       const QVariant value = expression->evaluate( &context );
791       if ( uniqueValues.contains( value ) )
792         continue;
793       uniqueValues << value;
794     }
795   }
796   else
797   {
798     uniqueValues = qgis::setToList( mLayer->uniqueValues( idx ) );
799   }
800 
801   // ask to abort if too many classes
802   if ( uniqueValues.size() >= 1000 )
803   {
804     const int res = QMessageBox::warning( nullptr, tr( "Classify Categories" ),
805                                           tr( "High number of classes. Classification would yield %1 entries which might not be expected. Continue?" ).arg( uniqueValues.size() ),
806                                           QMessageBox::Ok | QMessageBox::Cancel,
807                                           QMessageBox::Cancel );
808     if ( res == QMessageBox::Cancel )
809     {
810       return;
811     }
812   }
813 
814 #if 0
815   DlgAddCategories dlg( mStyle, createDefaultSymbol(), unique_vals, this );
816   if ( !dlg.exec() )
817     return;
818 #endif
819 
820   QgsCategoryList cats = QgsCategorizedSymbolRenderer::createCategories( uniqueValues, mCategorizedSymbol.get(), mLayer, attrName );
821   bool deleteExisting = false;
822 
823   if ( !mOldClassificationAttribute.isEmpty() &&
824        attrName != mOldClassificationAttribute &&
825        !mRenderer->categories().isEmpty() )
826   {
827     const int res = QMessageBox::question( this,
828                                            tr( "Delete Classification" ),
829                                            tr( "The classification field was changed from '%1' to '%2'.\n"
830                                                "Should the existing classes be deleted before classification?" )
831                                            .arg( mOldClassificationAttribute, attrName ),
832                                            QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel );
833     if ( res == QMessageBox::Cancel )
834     {
835       return;
836     }
837 
838     deleteExisting = ( res == QMessageBox::Yes );
839   }
840 
841   // First element to apply coloring to
842   bool keepExistingColors = false;
843   if ( !deleteExisting )
844   {
845     QgsCategoryList prevCats = mRenderer->categories();
846     keepExistingColors = !prevCats.isEmpty();
847     QgsRandomColorRamp randomColors;
848     if ( keepExistingColors && btnColorRamp->isRandomColorRamp() )
849       randomColors.setTotalColorCount( cats.size() );
850     for ( int i = 0; i < cats.size(); ++i )
851     {
852       bool contains = false;
853       const QVariant value = cats.at( i ).value();
854       for ( int j = 0; j < prevCats.size() && !contains; ++j )
855       {
856         const QVariant prevCatValue = prevCats.at( j ).value();
857         if ( prevCatValue.type() == QVariant::List )
858         {
859           const QVariantList list = prevCatValue.toList();
860           for ( const QVariant &v : list )
861           {
862             if ( v == value )
863             {
864               contains = true;
865               break;
866             }
867           }
868         }
869         else
870         {
871           if ( prevCats.at( j ).value() == value )
872           {
873             contains = true;
874           }
875         }
876         if ( contains )
877           break;
878       }
879 
880       if ( !contains )
881       {
882         if ( keepExistingColors && btnColorRamp->isRandomColorRamp() )
883         {
884           // insure that append symbols have random colors
885           cats.at( i ).symbol()->setColor( randomColors.color( i ) );
886         }
887         prevCats.append( cats.at( i ) );
888       }
889     }
890     cats = prevCats;
891   }
892 
893   mOldClassificationAttribute = attrName;
894 
895   // TODO: if not all categories are desired, delete some!
896   /*
897   if (not dlg.readAllCats.isChecked())
898   {
899     cats2 = {}
900     for item in dlg.listCategories.selectedItems():
901       for k,c in cats.iteritems():
902         if item.text() == k.toString():
903           break
904       cats2[k] = c
905     cats = cats2
906   }
907   */
908 
909   // recreate renderer
910   std::unique_ptr< QgsCategorizedSymbolRenderer > r = std::make_unique< QgsCategorizedSymbolRenderer >( attrName, cats );
911   r->setSourceSymbol( mCategorizedSymbol->clone() );
912   std::unique_ptr< QgsColorRamp > ramp( btnColorRamp->colorRamp() );
913   if ( ramp )
914     r->setSourceColorRamp( ramp->clone() );
915 
916   if ( mModel )
917   {
918     mModel->setRenderer( r.get() );
919   }
920   mRenderer = std::move( r );
921   if ( ! keepExistingColors && ramp )
922     applyColorRamp();
923   emit widgetChanged();
924 }
925 
applyColorRamp()926 void QgsCategorizedSymbolRendererWidget::applyColorRamp()
927 {
928   if ( !btnColorRamp->isNull() )
929   {
930     mRenderer->updateColorRamp( btnColorRamp->colorRamp() );
931   }
932   mModel->updateSymbology();
933 }
934 
currentCategoryRow()935 int QgsCategorizedSymbolRendererWidget::currentCategoryRow()
936 {
937   const QModelIndex idx = viewCategories->selectionModel()->currentIndex();
938   if ( !idx.isValid() )
939     return -1;
940   return idx.row();
941 }
942 
selectedCategories()943 QList<int> QgsCategorizedSymbolRendererWidget::selectedCategories()
944 {
945   QList<int> rows;
946   const QModelIndexList selectedRows = viewCategories->selectionModel()->selectedRows();
947 
948   const auto constSelectedRows = selectedRows;
949   for ( const QModelIndex &r : constSelectedRows )
950   {
951     if ( r.isValid() )
952     {
953       rows.append( r.row() );
954     }
955   }
956   return rows;
957 }
958 
deleteCategories()959 void QgsCategorizedSymbolRendererWidget::deleteCategories()
960 {
961   const QList<int> categoryIndexes = selectedCategories();
962   mModel->deleteRows( categoryIndexes );
963   emit widgetChanged();
964 }
965 
deleteAllCategories()966 void QgsCategorizedSymbolRendererWidget::deleteAllCategories()
967 {
968   mModel->removeAllRows();
969   emit widgetChanged();
970 }
971 
addCategory()972 void QgsCategorizedSymbolRendererWidget::addCategory()
973 {
974   if ( !mModel ) return;
975   QgsSymbol *symbol = QgsSymbol::defaultSymbol( mLayer->geometryType() );
976   const QgsRendererCategory cat( QString(), symbol, QString(), true );
977   mModel->addCategory( cat );
978   emit widgetChanged();
979 }
980 
selectedSymbols()981 QList<QgsSymbol *> QgsCategorizedSymbolRendererWidget::selectedSymbols()
982 {
983   QList<QgsSymbol *> selectedSymbols;
984 
985   QItemSelectionModel *m = viewCategories->selectionModel();
986   const QModelIndexList selectedIndexes = m->selectedRows( 1 );
987 
988   if ( !selectedIndexes.isEmpty() )
989   {
990     const QgsCategoryList &categories = mRenderer->categories();
991     QModelIndexList::const_iterator indexIt = selectedIndexes.constBegin();
992     for ( ; indexIt != selectedIndexes.constEnd(); ++indexIt )
993     {
994       const int row = ( *indexIt ).row();
995       QgsSymbol *s = categories[row].symbol();
996       if ( s )
997       {
998         selectedSymbols.append( s );
999       }
1000     }
1001   }
1002   return selectedSymbols;
1003 }
1004 
selectedCategoryList()1005 QgsCategoryList QgsCategorizedSymbolRendererWidget::selectedCategoryList()
1006 {
1007   QgsCategoryList cl;
1008 
1009   QItemSelectionModel *m = viewCategories->selectionModel();
1010   const QModelIndexList selectedIndexes = m->selectedRows( 1 );
1011 
1012   if ( !selectedIndexes.isEmpty() )
1013   {
1014     QModelIndexList::const_iterator indexIt = selectedIndexes.constBegin();
1015     for ( ; indexIt != selectedIndexes.constEnd(); ++indexIt )
1016     {
1017       cl.append( mModel->category( *indexIt ) );
1018     }
1019   }
1020   return cl;
1021 }
1022 
refreshSymbolView()1023 void QgsCategorizedSymbolRendererWidget::refreshSymbolView()
1024 {
1025   populateCategories();
1026   emit widgetChanged();
1027 }
1028 
showSymbolLevels()1029 void QgsCategorizedSymbolRendererWidget::showSymbolLevels()
1030 {
1031   showSymbolLevelsDialog( mRenderer.get() );
1032 }
1033 
rowsMoved()1034 void QgsCategorizedSymbolRendererWidget::rowsMoved()
1035 {
1036   viewCategories->selectionModel()->clear();
1037 }
1038 
matchToSymbolsFromLibrary()1039 void QgsCategorizedSymbolRendererWidget::matchToSymbolsFromLibrary()
1040 {
1041   const int matched = matchToSymbols( QgsStyle::defaultStyle() );
1042   if ( matched > 0 )
1043   {
1044     QMessageBox::information( this, tr( "Matched Symbols" ),
1045                               tr( "Matched %1 categories to symbols." ).arg( matched ) );
1046   }
1047   else
1048   {
1049     QMessageBox::warning( this, tr( "Matched Symbols" ),
1050                           tr( "No categories could be matched to symbols in library." ) );
1051   }
1052 }
1053 
matchToSymbols(QgsStyle * style)1054 int QgsCategorizedSymbolRendererWidget::matchToSymbols( QgsStyle *style )
1055 {
1056   if ( !mLayer || !style )
1057     return 0;
1058 
1059   const Qgis::SymbolType type = mLayer->geometryType() == QgsWkbTypes::PointGeometry ? Qgis::SymbolType::Marker
1060                                 : mLayer->geometryType() == QgsWkbTypes::LineGeometry ? Qgis::SymbolType::Line
1061                                 : Qgis::SymbolType::Fill;
1062 
1063   QVariantList unmatchedCategories;
1064   QStringList unmatchedSymbols;
1065   const int matched = mRenderer->matchToSymbols( style, type, unmatchedCategories, unmatchedSymbols );
1066 
1067   mModel->updateSymbology();
1068   return matched;
1069 }
1070 
matchToSymbolsFromXml()1071 void QgsCategorizedSymbolRendererWidget::matchToSymbolsFromXml()
1072 {
1073   QgsSettings settings;
1074   const QString openFileDir = settings.value( QStringLiteral( "UI/lastMatchToSymbolsDir" ), QDir::homePath() ).toString();
1075 
1076   const QString fileName = QFileDialog::getOpenFileName( this, tr( "Match to Symbols from File" ), openFileDir,
1077                            tr( "XML files (*.xml *.XML)" ) );
1078   if ( fileName.isEmpty() )
1079   {
1080     return;
1081   }
1082 
1083   const QFileInfo openFileInfo( fileName );
1084   settings.setValue( QStringLiteral( "UI/lastMatchToSymbolsDir" ), openFileInfo.absolutePath() );
1085 
1086   QgsStyle importedStyle;
1087   if ( !importedStyle.importXml( fileName ) )
1088   {
1089     QMessageBox::warning( this, tr( "Match to Symbols from File" ),
1090                           tr( "An error occurred while reading file:\n%1" ).arg( importedStyle.errorString() ) );
1091     return;
1092   }
1093 
1094   const int matched = matchToSymbols( &importedStyle );
1095   if ( matched > 0 )
1096   {
1097     QMessageBox::information( this, tr( "Match to Symbols from File" ),
1098                               tr( "Matched %1 categories to symbols from file." ).arg( matched ) );
1099   }
1100   else
1101   {
1102     QMessageBox::warning( this, tr( "Match to Symbols from File" ),
1103                           tr( "No categories could be matched to symbols in file." ) );
1104   }
1105 }
1106 
setSymbolLevels(const QgsLegendSymbolList & levels,bool enabled)1107 void QgsCategorizedSymbolRendererWidget::setSymbolLevels( const QgsLegendSymbolList &levels, bool enabled )
1108 {
1109   for ( const QgsLegendSymbolItem &legendSymbol : levels )
1110   {
1111     QgsSymbol *sym = legendSymbol.symbol();
1112     for ( int layer = 0; layer < sym->symbolLayerCount(); layer++ )
1113     {
1114       mRenderer->setLegendSymbolItem( legendSymbol.ruleKey(), sym->clone() );
1115     }
1116   }
1117   mRenderer->setUsingSymbolLevels( enabled );
1118   mModel->updateSymbology();
1119   emit widgetChanged();
1120 }
1121 
pasteSymbolToSelection()1122 void QgsCategorizedSymbolRendererWidget::pasteSymbolToSelection()
1123 {
1124   std::unique_ptr< QgsSymbol > tempSymbol( QgsSymbolLayerUtils::symbolFromMimeData( QApplication::clipboard()->mimeData() ) );
1125   if ( !tempSymbol )
1126     return;
1127 
1128   const QList<int> selectedCats = selectedCategories();
1129   if ( !selectedCats.isEmpty() )
1130   {
1131     for ( const int idx : selectedCats )
1132     {
1133       if ( mRenderer->categories().at( idx ).symbol()->type() != tempSymbol->type() )
1134         continue;
1135 
1136       std::unique_ptr< QgsSymbol > newCatSymbol( tempSymbol->clone() );
1137       if ( selectedCats.count() > 1 )
1138       {
1139         //if updating multiple categories, retain the existing category colors
1140         newCatSymbol->setColor( mRenderer->categories().at( idx ).symbol()->color() );
1141       }
1142       mRenderer->updateCategorySymbol( idx, newCatSymbol.release() );
1143     }
1144     emit widgetChanged();
1145   }
1146 }
1147 
cleanUpSymbolSelector(QgsPanelWidget * container)1148 void QgsCategorizedSymbolRendererWidget::cleanUpSymbolSelector( QgsPanelWidget *container )
1149 {
1150   QgsSymbolSelectorWidget *dlg = qobject_cast<QgsSymbolSelectorWidget *>( container );
1151   if ( !dlg )
1152     return;
1153 
1154   delete dlg->symbol();
1155 }
1156 
updateSymbolsFromWidget()1157 void QgsCategorizedSymbolRendererWidget::updateSymbolsFromWidget()
1158 {
1159   QgsSymbolSelectorWidget *dlg = qobject_cast<QgsSymbolSelectorWidget *>( sender() );
1160   mCategorizedSymbol.reset( dlg->symbol()->clone() );
1161 
1162   applyChangeToSymbol();
1163 }
1164 
updateSymbolsFromButton()1165 void QgsCategorizedSymbolRendererWidget::updateSymbolsFromButton()
1166 {
1167   mCategorizedSymbol.reset( btnChangeCategorizedSymbol->symbol()->clone() );
1168 
1169   applyChangeToSymbol();
1170 }
1171 
applyChangeToSymbol()1172 void QgsCategorizedSymbolRendererWidget::applyChangeToSymbol()
1173 {
1174   // When there is a selection, change the selected symbols only
1175   QItemSelectionModel *m = viewCategories->selectionModel();
1176   const QModelIndexList i = m->selectedRows();
1177 
1178   if ( !i.isEmpty() )
1179   {
1180     const QList<int> selectedCats = selectedCategories();
1181 
1182     if ( !selectedCats.isEmpty() )
1183     {
1184       const auto constSelectedCats = selectedCats;
1185       for ( const int idx : constSelectedCats )
1186       {
1187         QgsSymbol *newCatSymbol = mCategorizedSymbol->clone();
1188         if ( selectedCats.count() > 1 )
1189         {
1190           //if updating multiple categories, retain the existing category colors
1191           newCatSymbol->setColor( mRenderer->categories().at( idx ).symbol()->color() );
1192         }
1193         mRenderer->updateCategorySymbol( idx, newCatSymbol );
1194       }
1195     }
1196   }
1197   else
1198   {
1199     mRenderer->updateSymbols( mCategorizedSymbol.get() );
1200   }
1201 
1202   mModel->updateSymbology();
1203   emit widgetChanged();
1204 }
1205 
keyPressEvent(QKeyEvent * event)1206 void QgsCategorizedSymbolRendererWidget::keyPressEvent( QKeyEvent *event )
1207 {
1208   if ( !event )
1209   {
1210     return;
1211   }
1212 
1213   if ( event->key() == Qt::Key_C && event->modifiers() == Qt::ControlModifier )
1214   {
1215     mCopyBuffer.clear();
1216     mCopyBuffer = selectedCategoryList();
1217   }
1218   else if ( event->key() == Qt::Key_V && event->modifiers() == Qt::ControlModifier )
1219   {
1220     QgsCategoryList::const_iterator rIt = mCopyBuffer.constBegin();
1221     for ( ; rIt != mCopyBuffer.constEnd(); ++rIt )
1222     {
1223       mModel->addCategory( *rIt );
1224     }
1225   }
1226 }
1227 
createExpressionContext() const1228 QgsExpressionContext QgsCategorizedSymbolRendererWidget::createExpressionContext() const
1229 {
1230   QgsExpressionContext expContext;
1231   expContext << QgsExpressionContextUtils::globalScope()
1232              << QgsExpressionContextUtils::projectScope( QgsProject::instance() )
1233              << QgsExpressionContextUtils::atlasScope( nullptr );
1234 
1235   if ( auto *lMapCanvas = mContext.mapCanvas() )
1236   {
1237     expContext << QgsExpressionContextUtils::mapSettingsScope( lMapCanvas->mapSettings() )
1238                << new QgsExpressionContextScope( lMapCanvas->expressionContextScope() );
1239     if ( const QgsExpressionContextScopeGenerator *generator = dynamic_cast< const QgsExpressionContextScopeGenerator * >( lMapCanvas->temporalController() ) )
1240     {
1241       expContext << generator->createExpressionContextScope();
1242     }
1243   }
1244   else
1245   {
1246     expContext << QgsExpressionContextUtils::mapSettingsScope( QgsMapSettings() );
1247   }
1248 
1249   if ( auto *lVectorLayer = vectorLayer() )
1250     expContext << QgsExpressionContextUtils::layerScope( lVectorLayer );
1251 
1252   // additional scopes
1253   const auto constAdditionalExpressionContextScopes = mContext.additionalExpressionContextScopes();
1254   for ( const QgsExpressionContextScope &scope : constAdditionalExpressionContextScopes )
1255   {
1256     expContext.appendScope( new QgsExpressionContextScope( scope ) );
1257   }
1258 
1259   return expContext;
1260 }
1261 
dataDefinedSizeLegend()1262 void QgsCategorizedSymbolRendererWidget::dataDefinedSizeLegend()
1263 {
1264   QgsMarkerSymbol *s = static_cast<QgsMarkerSymbol *>( mCategorizedSymbol.get() ); // this should be only enabled for marker symbols
1265   QgsDataDefinedSizeLegendWidget *panel = createDataDefinedSizeLegendWidget( s, mRenderer->dataDefinedSizeLegend() );
1266   if ( panel )
1267   {
1268     connect( panel, &QgsPanelWidget::widgetChanged, this, [ = ]
1269     {
1270       mRenderer->setDataDefinedSizeLegend( panel->dataDefinedSizeLegend() );
1271       emit widgetChanged();
1272     } );
1273     openPanel( panel );  // takes ownership of the panel
1274   }
1275 }
1276 
mergeSelectedCategories()1277 void QgsCategorizedSymbolRendererWidget::mergeSelectedCategories()
1278 {
1279   const QgsCategoryList &categories = mRenderer->categories();
1280 
1281   const QList<int> selectedCategoryIndexes = selectedCategories();
1282   QList< int > categoryIndexes;
1283 
1284   // filter out "" entry
1285   for ( const int i : selectedCategoryIndexes )
1286   {
1287     const QVariant v = categories.at( i ).value();
1288 
1289     if ( !v.isValid() || v == "" )
1290     {
1291       continue;
1292     }
1293 
1294     categoryIndexes.append( i );
1295   }
1296 
1297   if ( categoryIndexes.count() < 2 )
1298     return;
1299 
1300   QStringList labels;
1301   QVariantList values;
1302   values.reserve( categoryIndexes.count() );
1303   labels.reserve( categoryIndexes.count() );
1304   for ( const int i : categoryIndexes )
1305   {
1306     const QVariant v = categories.at( i ).value();
1307 
1308     if ( v.type() == QVariant::List )
1309     {
1310       values.append( v.toList() );
1311     }
1312     else
1313       values << v;
1314 
1315     labels << categories.at( i ).label();
1316   }
1317 
1318   // modify first category (basically we "merge up" into the first selected category)
1319   mRenderer->updateCategoryLabel( categoryIndexes.at( 0 ), labels.join( ',' ) );
1320   mRenderer->updateCategoryValue( categoryIndexes.at( 0 ), values );
1321 
1322   categoryIndexes.pop_front();
1323   mModel->deleteRows( categoryIndexes );
1324 
1325   emit widgetChanged();
1326 }
1327 
unmergeSelectedCategories()1328 void QgsCategorizedSymbolRendererWidget::unmergeSelectedCategories()
1329 {
1330   const QList<int> categoryIndexes = selectedCategories();
1331   if ( categoryIndexes.isEmpty() )
1332     return;
1333 
1334   const QgsCategoryList &categories = mRenderer->categories();
1335   for ( const int i : categoryIndexes )
1336   {
1337     const QVariant v = categories.at( i ).value();
1338     if ( v.type() != QVariant::List )
1339       continue;
1340 
1341     const QVariantList list = v.toList();
1342     for ( int j = 1; j < list.count(); ++j )
1343     {
1344       mModel->addCategory( QgsRendererCategory( list.at( j ), categories.at( i ).symbol()->clone(), list.at( j ).toString(), categories.at( i ).renderState() ) );
1345     }
1346     mRenderer->updateCategoryValue( i, list.at( 0 ) );
1347     mRenderer->updateCategoryLabel( i, list.at( 0 ).toString() );
1348   }
1349 
1350   emit widgetChanged();
1351 }
1352 
showContextMenu(QPoint)1353 void QgsCategorizedSymbolRendererWidget::showContextMenu( QPoint )
1354 {
1355   mContextMenu->clear();
1356   const QList< QAction * > actions = contextMenu->actions();
1357   for ( QAction *act : actions )
1358   {
1359     mContextMenu->addAction( act );
1360   }
1361 
1362   mContextMenu->addSeparator();
1363 
1364   if ( viewCategories->selectionModel()->selectedRows().count() > 1 )
1365   {
1366     mContextMenu->addAction( mMergeCategoriesAction );
1367   }
1368   if ( viewCategories->selectionModel()->selectedRows().count() == 1 )
1369   {
1370     const QList<int> categoryIndexes = selectedCategories();
1371     const QgsCategoryList &categories = mRenderer->categories();
1372     const QVariant v = categories.at( categoryIndexes.at( 0 ) ).value();
1373     if ( v.type() == QVariant::List )
1374       mContextMenu->addAction( mUnmergeCategoriesAction );
1375   }
1376   else if ( viewCategories->selectionModel()->selectedRows().count() > 1 )
1377   {
1378     mContextMenu->addAction( mUnmergeCategoriesAction );
1379   }
1380 
1381   mContextMenu->exec( QCursor::pos() );
1382 }
1383 
selectionChanged(const QItemSelection &,const QItemSelection &)1384 void QgsCategorizedSymbolRendererWidget::selectionChanged( const QItemSelection &, const QItemSelection & )
1385 {
1386   const QList<int> selectedCats = selectedCategories();
1387   if ( !selectedCats.isEmpty() )
1388   {
1389     whileBlocking( btnChangeCategorizedSymbol )->setSymbol( mRenderer->categories().at( selectedCats.at( 0 ) ).symbol()->clone() );
1390   }
1391   else if ( mRenderer->sourceSymbol() )
1392   {
1393     whileBlocking( btnChangeCategorizedSymbol )->setSymbol( mRenderer->sourceSymbol()->clone() );
1394   }
1395   btnChangeCategorizedSymbol->setDialogTitle( selectedCats.size() == 1 ? mRenderer->categories().at( selectedCats.at( 0 ) ).label() : tr( "Symbol Settings" ) );
1396 }
1397