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