1 /***************************************************************************
2 qgsstylemodel.cpp
3 ---------------
4 begin : September 2018
5 copyright : (C) 2018 by Nyall Dawson
6 email : nyall dot dawson 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 "qgsstylemodel.h"
17 #include "qgsstyle.h"
18 #include "qgssymbollayerutils.h"
19 #include "qgsapplication.h"
20 #include "qgssvgcache.h"
21 #include "qgsimagecache.h"
22 #include "qgsproject.h"
23 #include "qgsexpressioncontextutils.h"
24 #include <QIcon>
25
26 const double ICON_PADDING_FACTOR = 0.16;
27
28 const auto ENTITIES = { QgsStyle::SymbolEntity, QgsStyle::ColorrampEntity, QgsStyle::TextFormatEntity, QgsStyle::LabelSettingsEntity, QgsStyle::LegendPatchShapeEntity, QgsStyle::Symbol3DEntity };
29
30 QgsAbstractStyleEntityIconGenerator *QgsStyleModel::sIconGenerator = nullptr;
31
32 //
33 // QgsAbstractStyleEntityIconGenerator
34 //
35
QgsAbstractStyleEntityIconGenerator(QObject * parent)36 QgsAbstractStyleEntityIconGenerator::QgsAbstractStyleEntityIconGenerator( QObject *parent )
37 : QObject( parent )
38 {
39
40 }
41
setIconSizes(const QList<QSize> & sizes)42 void QgsAbstractStyleEntityIconGenerator::setIconSizes( const QList<QSize> &sizes )
43 {
44 mIconSizes = sizes;
45 }
46
iconSizes() const47 QList<QSize> QgsAbstractStyleEntityIconGenerator::iconSizes() const
48 {
49 return mIconSizes;
50 }
51
52
53 //
54 // QgsStyleModel
55 //
56
QgsStyleModel(QgsStyle * style,QObject * parent)57 QgsStyleModel::QgsStyleModel( QgsStyle *style, QObject *parent )
58 : QAbstractItemModel( parent )
59 , mStyle( style )
60 {
61 Q_ASSERT( mStyle );
62
63 for ( QgsStyle::StyleEntity entity : ENTITIES )
64 {
65 mEntityNames.insert( entity, mStyle->allNames( entity ) );
66 }
67
68 connect( mStyle, &QgsStyle::entityAdded, this, &QgsStyleModel::onEntityAdded );
69 connect( mStyle, &QgsStyle::entityRemoved, this, &QgsStyleModel::onEntityRemoved );
70 connect( mStyle, &QgsStyle::entityRenamed, this, &QgsStyleModel::onEntityRename );
71 connect( mStyle, &QgsStyle::entityChanged, this, &QgsStyleModel::onEntityChanged );
72 connect( mStyle, &QgsStyle::entityTagsChanged, this, &QgsStyleModel::onTagsChanged );
73
74 // when a remote svg or image has been fetched, update the model's decorations.
75 // this is required if a symbol utilizes remote svgs, and the current icons
76 // have been generated using the temporary "downloading" svg. In this case
77 // we require the preview to be regenerated to use the correct fetched
78 // svg
79 connect( QgsApplication::svgCache(), &QgsSvgCache::remoteSvgFetched, this, &QgsStyleModel::rebuildSymbolIcons );
80 connect( QgsApplication::imageCache(), &QgsImageCache::remoteImageFetched, this, &QgsStyleModel::rebuildSymbolIcons );
81
82 // if project color scheme changes, we need to redraw symbols - they may use project colors and accordingly
83 // need updating to reflect the new colors
84 connect( QgsProject::instance(), &QgsProject::projectColorsChanged, this, &QgsStyleModel::rebuildSymbolIcons );
85
86 if ( sIconGenerator )
87 connect( sIconGenerator, &QgsAbstractStyleEntityIconGenerator::iconGenerated, this, &QgsStyleModel::iconGenerated, Qt::QueuedConnection );
88 }
89
data(const QModelIndex & index,int role) const90 QVariant QgsStyleModel::data( const QModelIndex &index, int role ) const
91 {
92 if ( index.row() < 0 || index.row() >= rowCount( QModelIndex() ) )
93 return QVariant();
94
95
96 QgsStyle::StyleEntity entityType = entityTypeFromRow( index.row() );
97
98 QString name;
99 switch ( entityType )
100 {
101 case QgsStyle::TagEntity:
102 case QgsStyle::SmartgroupEntity:
103 break;
104
105 default:
106 name = mEntityNames[ entityType ].value( index.row() - offsetForEntity( entityType ) );
107 break;
108 }
109
110 switch ( role )
111 {
112 case Qt::DisplayRole:
113 case Qt::ToolTipRole:
114 case Qt::EditRole:
115 {
116 switch ( index.column() )
117 {
118 case Name:
119 {
120 const QStringList tags = mStyle->tagsOfSymbol( entityType, name );
121
122 if ( role == Qt::ToolTipRole )
123 {
124 QString tooltip = QStringLiteral( "<h3>%1</h3><p><i>%2</i>" ).arg( name,
125 tags.count() > 0 ? tags.join( QLatin1String( ", " ) ) : tr( "Not tagged" ) );
126
127 switch ( entityType )
128 {
129 case QgsStyle::SymbolEntity:
130 {
131 // create very large preview image
132 std::unique_ptr< QgsSymbol > symbol( mStyle->symbol( name ) );
133 if ( symbol )
134 {
135 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
136 int width = static_cast< int >( Qgis::UI_SCALE_FACTOR * QFontMetrics( data( index, Qt::FontRole ).value< QFont >() ).width( 'X' ) * 23 );
137 #else
138 int width = static_cast< int >( Qgis::UI_SCALE_FACTOR * QFontMetrics( data( index, Qt::FontRole ).value< QFont >() ).horizontalAdvance( 'X' ) * 23 );
139 #endif
140 int height = static_cast< int >( width / 1.61803398875 ); // golden ratio
141 QPixmap pm = QgsSymbolLayerUtils::symbolPreviewPixmap( symbol.get(), QSize( width, height ), height / 20, nullptr, false, mExpressionContext.get() );
142 QByteArray data;
143 QBuffer buffer( &data );
144 pm.save( &buffer, "PNG", 100 );
145 tooltip += QStringLiteral( "<p><img src='data:image/png;base64, %3'>" ).arg( QString( data.toBase64() ) );
146 }
147 break;
148 }
149
150 case QgsStyle::TextFormatEntity:
151 {
152 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
153 int width = static_cast< int >( Qgis::UI_SCALE_FACTOR * QFontMetrics( data( index, Qt::FontRole ).value< QFont >() ).width( 'X' ) * 23 );
154 #else
155 int width = static_cast< int >( Qgis::UI_SCALE_FACTOR * QFontMetrics( data( index, Qt::FontRole ).value< QFont >() ).horizontalAdvance( 'X' ) * 23 );
156 #endif
157 int height = static_cast< int >( width / 1.61803398875 ); // golden ratio
158 const QgsTextFormat format = mStyle->textFormat( name );
159 QPixmap pm = QgsTextFormat::textFormatPreviewPixmap( format, QSize( width, height ), QString(), height / 20 );
160 QByteArray data;
161 QBuffer buffer( &data );
162 pm.save( &buffer, "PNG", 100 );
163 tooltip += QStringLiteral( "<p><img src='data:image/png;base64, %3'>" ).arg( QString( data.toBase64() ) );
164 break;
165 }
166
167 case QgsStyle::LabelSettingsEntity:
168 {
169 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
170 int width = static_cast< int >( Qgis::UI_SCALE_FACTOR * QFontMetrics( data( index, Qt::FontRole ).value< QFont >() ).width( 'X' ) * 23 );
171 #else
172 int width = static_cast< int >( Qgis::UI_SCALE_FACTOR * QFontMetrics( data( index, Qt::FontRole ).value< QFont >() ).horizontalAdvance( 'X' ) * 23 );
173 #endif
174 int height = static_cast< int >( width / 1.61803398875 ); // golden ratio
175 const QgsPalLayerSettings settings = mStyle->labelSettings( name );
176 QPixmap pm = QgsPalLayerSettings::labelSettingsPreviewPixmap( settings, QSize( width, height ), QString(), height / 20 );
177 QByteArray data;
178 QBuffer buffer( &data );
179 pm.save( &buffer, "PNG", 100 );
180 tooltip += QStringLiteral( "<p><img src='data:image/png;base64, %3'>" ).arg( QString( data.toBase64() ) );
181 break;
182 }
183
184 case QgsStyle::LegendPatchShapeEntity:
185 {
186 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
187 int width = static_cast< int >( Qgis::UI_SCALE_FACTOR * QFontMetrics( data( index, Qt::FontRole ).value< QFont >() ).width( 'X' ) * 23 );
188 #else
189 int width = static_cast< int >( Qgis::UI_SCALE_FACTOR * QFontMetrics( data( index, Qt::FontRole ).value< QFont >() ).horizontalAdvance( 'X' ) * 23 );
190 #endif
191 int height = static_cast< int >( width / 1.61803398875 ); // golden ratio
192
193 const QgsLegendPatchShape shape = mStyle->legendPatchShape( name );
194 if ( const QgsSymbol *symbol = mStyle->previewSymbolForPatchShape( shape ) )
195 {
196 QPixmap pm = QgsSymbolLayerUtils::symbolPreviewPixmap( symbol, QSize( width, height ), height / 20, nullptr, false, nullptr, &shape );
197 QByteArray data;
198 QBuffer buffer( &data );
199 pm.save( &buffer, "PNG", 100 );
200 tooltip += QStringLiteral( "<p><img src='data:image/png;base64, %3'>" ).arg( QString( data.toBase64() ) );
201 }
202 break;
203 }
204
205 case QgsStyle::ColorrampEntity:
206 case QgsStyle::TagEntity:
207 case QgsStyle::SmartgroupEntity:
208 case QgsStyle::Symbol3DEntity:
209 break;
210 }
211 return tooltip;
212 }
213 else
214 {
215 return name;
216 }
217 }
218 case Tags:
219 return mStyle->tagsOfSymbol( entityType, name ).join( QLatin1String( ", " ) );
220 }
221 return QVariant();
222 }
223
224 case Qt::DecorationRole:
225 {
226 // Generate icons at all additional sizes specified for the model.
227 // This allows the model to have size responsive icons.
228
229 if ( !mExpressionContext )
230 {
231 // build the expression context once, and keep it around. Usually this is a no-no, but in this
232 // case we want to avoid creating potentially thousands of contexts one-by-one (usually one context
233 // is created for a batch of multiple evalutions like this), and we only use a very minimal context
234 // anyway...
235 mExpressionContext = qgis::make_unique< QgsExpressionContext >();
236 mExpressionContext->appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( nullptr ) );
237 }
238
239 switch ( index.column() )
240 {
241 case Name:
242 switch ( entityType )
243 {
244 case QgsStyle::SymbolEntity:
245 {
246 // use cached icon if possible
247 QIcon icon = mIconCache[ entityType ].value( name );
248 if ( !icon.isNull() )
249 return icon;
250
251 std::unique_ptr< QgsSymbol > symbol( mStyle->symbol( name ) );
252 if ( symbol )
253 {
254 if ( mAdditionalSizes.isEmpty() )
255 icon.addPixmap( QgsSymbolLayerUtils::symbolPreviewPixmap( symbol.get(), QSize( 24, 24 ), 1, nullptr, false, mExpressionContext.get() ) );
256
257 for ( const QSize &s : mAdditionalSizes )
258 {
259 icon.addPixmap( QgsSymbolLayerUtils::symbolPreviewPixmap( symbol.get(), s, static_cast< int >( s.width() * ICON_PADDING_FACTOR ), nullptr, false, mExpressionContext.get() ) );
260 }
261
262 }
263 mIconCache[ entityType ].insert( name, icon );
264 return icon;
265 }
266 case QgsStyle::ColorrampEntity:
267 {
268 // use cached icon if possible
269 QIcon icon = mIconCache[ entityType ].value( name );
270 if ( !icon.isNull() )
271 return icon;
272
273 std::unique_ptr< QgsColorRamp > ramp( mStyle->colorRamp( name ) );
274 if ( ramp )
275 {
276 if ( mAdditionalSizes.isEmpty() )
277 icon.addPixmap( QgsSymbolLayerUtils::colorRampPreviewPixmap( ramp.get(), QSize( 24, 24 ), 1 ) );
278 for ( const QSize &s : mAdditionalSizes )
279 {
280 icon.addPixmap( QgsSymbolLayerUtils::colorRampPreviewPixmap( ramp.get(), s, static_cast< int >( s.width() * ICON_PADDING_FACTOR ) ) );
281 }
282
283 }
284 mIconCache[ entityType ].insert( name, icon );
285 return icon;
286 }
287
288 case QgsStyle::TextFormatEntity:
289 {
290 // use cached icon if possible
291 QIcon icon = mIconCache[ entityType ].value( name );
292 if ( !icon.isNull() )
293 return icon;
294
295 const QgsTextFormat format( mStyle->textFormat( name ) );
296 if ( mAdditionalSizes.isEmpty() )
297 icon.addPixmap( QgsTextFormat::textFormatPreviewPixmap( format, QSize( 24, 24 ), QString(), 1 ) );
298 for ( const QSize &s : mAdditionalSizes )
299 {
300 icon.addPixmap( QgsTextFormat::textFormatPreviewPixmap( format, s, QString(), static_cast< int >( s.width() * ICON_PADDING_FACTOR ) ) );
301 }
302 mIconCache[ entityType ].insert( name, icon );
303 return icon;
304 }
305
306 case QgsStyle::LabelSettingsEntity:
307 {
308 // use cached icon if possible
309 QIcon icon = mIconCache[ entityType ].value( name );
310 if ( !icon.isNull() )
311 return icon;
312
313 const QgsPalLayerSettings settings( mStyle->labelSettings( name ) );
314 if ( mAdditionalSizes.isEmpty() )
315 icon.addPixmap( QgsPalLayerSettings::labelSettingsPreviewPixmap( settings, QSize( 24, 24 ), QString(), 1 ) );
316 for ( const QSize &s : mAdditionalSizes )
317 {
318 icon.addPixmap( QgsPalLayerSettings::labelSettingsPreviewPixmap( settings, s, QString(), static_cast< int >( s.width() * ICON_PADDING_FACTOR ) ) );
319 }
320 mIconCache[ entityType ].insert( name, icon );
321 return icon;
322 }
323
324 case QgsStyle::LegendPatchShapeEntity:
325 {
326 // use cached icon if possible
327 QIcon icon = mIconCache[ entityType ].value( name );
328 if ( !icon.isNull() )
329 return icon;
330
331 const QgsLegendPatchShape shape = mStyle->legendPatchShape( name );
332 if ( !shape.isNull() )
333 {
334 if ( const QgsSymbol *symbol = mStyle->previewSymbolForPatchShape( shape ) )
335 {
336 if ( mAdditionalSizes.isEmpty() )
337 icon.addPixmap( QgsSymbolLayerUtils::symbolPreviewPixmap( symbol, QSize( 24, 24 ), 1, nullptr, false, mExpressionContext.get(), &shape ) );
338
339 for ( const QSize &s : mAdditionalSizes )
340 {
341 icon.addPixmap( QgsSymbolLayerUtils::symbolPreviewPixmap( symbol, s, static_cast< int >( s.width() * ICON_PADDING_FACTOR ), nullptr, false, mExpressionContext.get(), &shape ) );
342 }
343 }
344 }
345 mIconCache[ entityType ].insert( name, icon );
346 return icon;
347 }
348
349 case QgsStyle::Symbol3DEntity:
350 {
351 // hack for now -- we just use a generic "3d icon" svg file.
352 // TODO - render proper thumbnails
353
354 // use cached icon if possible
355 QIcon icon = mIconCache[ entityType ].value( name );
356 if ( !icon.isNull() )
357 return icon;
358
359 if ( sIconGenerator && !mPending3dSymbolIcons.contains( name ) )
360 {
361 mPending3dSymbolIcons.insert( name );
362 sIconGenerator->generateIcon( mStyle, QgsStyle::Symbol3DEntity, name );
363 }
364
365 // TODO - use hourglass icon
366 if ( mAdditionalSizes.isEmpty() )
367 icon.addFile( QgsApplication::defaultThemePath() + QDir::separator() + QStringLiteral( "3d.svg" ), QSize( 24, 24 ) );
368 for ( const QSize &s : mAdditionalSizes )
369 {
370 icon.addFile( QgsApplication::defaultThemePath() + QDir::separator() + QStringLiteral( "3d.svg" ), s );
371 }
372 mIconCache[ entityType ].insert( name, icon );
373 return icon;
374 }
375
376 case QgsStyle::TagEntity:
377 case QgsStyle::SmartgroupEntity:
378 return QVariant();
379 }
380 break;
381
382 case Tags:
383 return QVariant();
384 }
385 return QVariant();
386 }
387
388 case TypeRole:
389 return entityType;
390
391 case TagRole:
392 return mStyle->tagsOfSymbol( entityType, name );
393
394 case IsFavoriteRole:
395 return mStyle->isFavorite( entityType, name );
396
397 case SymbolTypeRole:
398 {
399 switch ( entityType )
400 {
401 case QgsStyle::SymbolEntity:
402 {
403 const QgsSymbol *symbol = mStyle->symbolRef( name );
404 return symbol ? symbol->type() : QVariant();
405 }
406
407 case QgsStyle::LegendPatchShapeEntity:
408 return mStyle->legendPatchShapeSymbolType( name );
409
410 case QgsStyle::TagEntity:
411 case QgsStyle::ColorrampEntity:
412 case QgsStyle::SmartgroupEntity:
413 case QgsStyle::LabelSettingsEntity:
414 case QgsStyle::TextFormatEntity:
415 case QgsStyle::Symbol3DEntity:
416 return QVariant();
417 }
418 return QVariant();
419 }
420
421 case LayerTypeRole:
422 {
423 switch ( entityType )
424 {
425 case QgsStyle::LabelSettingsEntity:
426 return mStyle->labelSettingsLayerType( name );
427
428 case QgsStyle::Symbol3DEntity:
429 case QgsStyle::SymbolEntity:
430 case QgsStyle::LegendPatchShapeEntity:
431 case QgsStyle::TagEntity:
432 case QgsStyle::ColorrampEntity:
433 case QgsStyle::SmartgroupEntity:
434 case QgsStyle::TextFormatEntity:
435 return QVariant();
436 }
437 return QVariant();
438 }
439
440 case CompatibleGeometryTypesRole:
441 {
442 switch ( entityType )
443 {
444 case QgsStyle::Symbol3DEntity:
445 {
446 QVariantList res;
447 const QList< QgsWkbTypes::GeometryType > types = mStyle->symbol3DCompatibleGeometryTypes( name );
448 res.reserve( types.size() );
449 for ( QgsWkbTypes::GeometryType type : types )
450 {
451 res << static_cast< int >( type );
452 }
453 return res;
454 }
455
456 case QgsStyle::LabelSettingsEntity:
457 case QgsStyle::SymbolEntity:
458 case QgsStyle::LegendPatchShapeEntity:
459 case QgsStyle::TagEntity:
460 case QgsStyle::ColorrampEntity:
461 case QgsStyle::SmartgroupEntity:
462 case QgsStyle::TextFormatEntity:
463 return QVariant();
464 }
465 return QVariant();
466 }
467
468 default:
469 return QVariant();
470 }
471 #ifndef _MSC_VER // avoid warning
472 return QVariant(); // avoid warning
473 #endif
474 }
475
setData(const QModelIndex & index,const QVariant & value,int role)476 bool QgsStyleModel::setData( const QModelIndex &index, const QVariant &value, int role )
477 {
478 if ( index.row() < 0 || index.row() >= rowCount( QModelIndex() ) || role != Qt::EditRole )
479 return false;
480
481 switch ( index.column() )
482 {
483 case Name:
484 {
485 QgsStyle::StyleEntity entityType = entityTypeFromRow( index.row() );
486 QString name;
487 switch ( entityType )
488 {
489 case QgsStyle::TagEntity:
490 case QgsStyle::SmartgroupEntity:
491 return false;
492
493 default:
494 name = mEntityNames[ entityType ].value( index.row() - offsetForEntity( entityType ) );
495 break;
496 }
497
498 const QString newName = value.toString();
499 return mStyle->renameEntity( entityType, name, newName );
500 }
501
502 case Tags:
503 return false;
504 }
505
506 return false;
507 }
508
flags(const QModelIndex & index) const509 Qt::ItemFlags QgsStyleModel::flags( const QModelIndex &index ) const
510 {
511 Qt::ItemFlags flags = QAbstractItemModel::flags( index );
512 if ( index.isValid() && index.column() == Name )
513 {
514 return flags | Qt::ItemIsEditable;
515 }
516 else
517 {
518 return flags;
519 }
520 }
521
headerData(int section,Qt::Orientation orientation,int role) const522 QVariant QgsStyleModel::headerData( int section, Qt::Orientation orientation, int role ) const
523 {
524 if ( role == Qt::DisplayRole )
525 {
526 if ( orientation == Qt::Vertical ) //row
527 {
528 return QVariant( section );
529 }
530 else
531 {
532 switch ( section )
533 {
534 case Name:
535 return QVariant( tr( "Name" ) );
536
537 case Tags:
538 return QVariant( tr( "Tags" ) );
539
540 default:
541 return QVariant();
542 }
543 }
544 }
545 else
546 {
547 return QVariant();
548 }
549 }
550
index(int row,int column,const QModelIndex & parent) const551 QModelIndex QgsStyleModel::index( int row, int column, const QModelIndex &parent ) const
552 {
553 if ( !hasIndex( row, column, parent ) )
554 return QModelIndex();
555
556 if ( !parent.isValid() )
557 {
558 return createIndex( row, column );
559 }
560
561 return QModelIndex();
562 }
563
parent(const QModelIndex &) const564 QModelIndex QgsStyleModel::parent( const QModelIndex & ) const
565 {
566 //all items are top level for now
567 return QModelIndex();
568 }
569
rowCount(const QModelIndex & parent) const570 int QgsStyleModel::rowCount( const QModelIndex &parent ) const
571 {
572 if ( !parent.isValid() )
573 {
574 int count = 0;
575 for ( QgsStyle::StyleEntity type : ENTITIES )
576 count += mEntityNames[ type ].size();
577 return count;
578 }
579 return 0;
580 }
581
columnCount(const QModelIndex &) const582 int QgsStyleModel::columnCount( const QModelIndex & ) const
583 {
584 return 2;
585 }
586
addDesiredIconSize(QSize size)587 void QgsStyleModel::addDesiredIconSize( QSize size )
588 {
589 if ( mAdditionalSizes.contains( size ) )
590 return;
591
592 mAdditionalSizes << size;
593
594 if ( sIconGenerator )
595 sIconGenerator->setIconSizes( mAdditionalSizes );
596
597 mIconCache.clear();
598 }
599
setIconGenerator(QgsAbstractStyleEntityIconGenerator * generator)600 void QgsStyleModel::setIconGenerator( QgsAbstractStyleEntityIconGenerator *generator )
601 {
602 sIconGenerator = generator;
603 connect( sIconGenerator, &QgsAbstractStyleEntityIconGenerator::iconGenerated, QgsApplication::defaultStyleModel(), &QgsStyleModel::iconGenerated, Qt::QueuedConnection );
604 }
605
onEntityAdded(QgsStyle::StyleEntity type,const QString & name)606 void QgsStyleModel::onEntityAdded( QgsStyle::StyleEntity type, const QString &name )
607 {
608 mIconCache[ type ].remove( name );
609 const QStringList oldSymbolNames = mEntityNames[ type ];
610 const QStringList newSymbolNames = mStyle->allNames( type );
611
612 // find index of newly added symbol
613 const int newNameIndex = newSymbolNames.indexOf( name );
614 if ( newNameIndex < 0 )
615 return; // shouldn't happen
616
617 const int offset = offsetForEntity( type );
618 beginInsertRows( QModelIndex(), newNameIndex + offset, newNameIndex + offset );
619 mEntityNames[ type ] = newSymbolNames;
620 endInsertRows();
621 }
622
onEntityRemoved(QgsStyle::StyleEntity type,const QString & name)623 void QgsStyleModel::onEntityRemoved( QgsStyle::StyleEntity type, const QString &name )
624 {
625 mIconCache[ type ].remove( name );
626 const QStringList oldSymbolNames = mEntityNames[ type ];
627 const QStringList newSymbolNames = mStyle->allNames( type );
628
629 // find index of removed symbol
630 const int oldNameIndex = oldSymbolNames.indexOf( name );
631 if ( oldNameIndex < 0 )
632 return; // shouldn't happen
633
634 const int offset = offsetForEntity( type );
635 beginRemoveRows( QModelIndex(), oldNameIndex + offset, oldNameIndex + offset );
636 mEntityNames[ type ] = newSymbolNames;
637 endRemoveRows();
638 }
639
onEntityChanged(QgsStyle::StyleEntity type,const QString & name)640 void QgsStyleModel::onEntityChanged( QgsStyle::StyleEntity type, const QString &name )
641 {
642 mIconCache[ type ].remove( name );
643
644 const int offset = offsetForEntity( type );
645 QModelIndex i = index( offset + mEntityNames[ type ].indexOf( name ), Tags );
646 emit dataChanged( i, i, QVector< int >() << Qt::DecorationRole );
647 }
648
onEntityRename(QgsStyle::StyleEntity type,const QString & oldName,const QString & newName)649 void QgsStyleModel::onEntityRename( QgsStyle::StyleEntity type, const QString &oldName, const QString &newName )
650 {
651 mIconCache[ type ].remove( oldName );
652 const QStringList oldSymbolNames = mEntityNames[ type ];
653 const QStringList newSymbolNames = mStyle->allNames( type );
654
655 // find index of removed symbol
656 const int oldNameIndex = oldSymbolNames.indexOf( oldName );
657 if ( oldNameIndex < 0 )
658 return; // shouldn't happen
659
660 // find index of added symbol
661 const int newNameIndex = newSymbolNames.indexOf( newName );
662 if ( newNameIndex < 0 )
663 return; // shouldn't happen
664
665 if ( newNameIndex == oldNameIndex )
666 {
667 mEntityNames[ type ] = newSymbolNames;
668 return;
669 }
670
671 const int offset = offsetForEntity( type );
672 beginMoveRows( QModelIndex(), oldNameIndex + offset, oldNameIndex + offset, QModelIndex(), ( newNameIndex > oldNameIndex ? newNameIndex + 1 : newNameIndex ) + offset );
673 mEntityNames[ type ] = newSymbolNames;
674 endMoveRows();
675 }
676
onTagsChanged(int entity,const QString & name,const QStringList &)677 void QgsStyleModel::onTagsChanged( int entity, const QString &name, const QStringList & )
678 {
679 QgsStyle::StyleEntity type = static_cast< QgsStyle::StyleEntity >( entity );
680 QModelIndex i;
681 int row = mEntityNames[type].indexOf( name ) + offsetForEntity( type );
682 switch ( static_cast< QgsStyle::StyleEntity >( entity ) )
683 {
684 case QgsStyle::TagEntity:
685 case QgsStyle::SmartgroupEntity:
686 return;
687
688 default:
689 i = index( row, Tags );
690 }
691 emit dataChanged( i, i );
692 }
693
rebuildSymbolIcons()694 void QgsStyleModel::rebuildSymbolIcons()
695 {
696 mIconCache[ QgsStyle::SymbolEntity ].clear();
697 mExpressionContext.reset();
698 emit dataChanged( index( 0, 0 ), index( mEntityNames[ QgsStyle::SymbolEntity ].count() - 1, 0 ), QVector<int>() << Qt::DecorationRole );
699 }
700
iconGenerated(QgsStyle::StyleEntity type,const QString & name,const QIcon & icon)701 void QgsStyleModel::iconGenerated( QgsStyle::StyleEntity type, const QString &name, const QIcon &icon )
702 {
703 int row = mEntityNames[type].indexOf( name ) + offsetForEntity( type );
704
705 switch ( type )
706 {
707 case QgsStyle::Symbol3DEntity:
708 mPending3dSymbolIcons.remove( name );
709 mIconCache[ QgsStyle::Symbol3DEntity ].insert( name, icon );
710 emit dataChanged( index( row, 0 ), index( row, 0 ) );
711 break;
712
713 case QgsStyle::SymbolEntity:
714 case QgsStyle::TagEntity:
715 case QgsStyle::ColorrampEntity:
716 case QgsStyle::LegendPatchShapeEntity:
717 case QgsStyle::TextFormatEntity:
718 case QgsStyle::SmartgroupEntity:
719 case QgsStyle::LabelSettingsEntity:
720 break;
721 }
722 }
723
entityTypeFromRow(int row) const724 QgsStyle::StyleEntity QgsStyleModel::entityTypeFromRow( int row ) const
725 {
726 int maxRowForEntity = 0;
727 for ( QgsStyle::StyleEntity type : ENTITIES )
728 {
729 maxRowForEntity += mEntityNames[ type ].size();
730 if ( row < maxRowForEntity )
731 return type;
732 }
733
734 // should never happen
735 Q_ASSERT( false );
736 return QgsStyle::SymbolEntity;
737 }
738
offsetForEntity(QgsStyle::StyleEntity entity) const739 int QgsStyleModel::offsetForEntity( QgsStyle::StyleEntity entity ) const
740 {
741 int offset = 0;
742 for ( QgsStyle::StyleEntity type : ENTITIES )
743 {
744 if ( type == entity )
745 return offset;
746
747 offset += mEntityNames[ type ].size();
748 }
749 return 0;
750 }
751
752 //
753 // QgsStyleProxyModel
754 //
755
QgsStyleProxyModel(QgsStyle * style,QObject * parent)756 QgsStyleProxyModel::QgsStyleProxyModel( QgsStyle *style, QObject *parent )
757 : QSortFilterProxyModel( parent )
758 , mStyle( style )
759 {
760 mModel = new QgsStyleModel( mStyle, this );
761 initialize();
762 }
763
initialize()764 void QgsStyleProxyModel::initialize()
765 {
766 setSortCaseSensitivity( Qt::CaseInsensitive );
767 // setSortLocaleAware( true );
768 setSourceModel( mModel );
769 setDynamicSortFilter( true );
770 sort( 0 );
771
772 connect( mStyle, &QgsStyle::entityTagsChanged, this, [ = ]
773 {
774 // update tagged symbols if filtering by tag
775 if ( mTagId >= 0 )
776 setTagId( mTagId );
777 if ( mSmartGroupId >= 0 )
778 setSmartGroupId( mSmartGroupId );
779 } );
780
781 connect( mStyle, &QgsStyle::favoritedChanged, this, [ = ]
782 {
783 // update favorited symbols if filtering by favorite
784 if ( mFavoritesOnly )
785 setFavoritesOnly( mFavoritesOnly );
786 } );
787
788 connect( mStyle, &QgsStyle::entityRenamed, this, [ = ]( QgsStyle::StyleEntity entity, const QString &, const QString & )
789 {
790 switch ( entity )
791 {
792 case QgsStyle::SmartgroupEntity:
793 case QgsStyle::TagEntity:
794 return;
795
796 default:
797 break;
798 }
799
800 if ( mSmartGroupId >= 0 )
801 setSmartGroupId( mSmartGroupId );
802 } );
803 }
804
QgsStyleProxyModel(QgsStyleModel * model,QObject * parent)805 QgsStyleProxyModel::QgsStyleProxyModel( QgsStyleModel *model, QObject *parent )
806 : QSortFilterProxyModel( parent )
807 , mModel( model )
808 , mStyle( model->style() )
809 {
810 initialize();
811 }
812
filterAcceptsRow(int source_row,const QModelIndex & source_parent) const813 bool QgsStyleProxyModel::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const
814 {
815 if ( mFilterString.isEmpty() && !mEntityFilterEnabled && !mSymbolTypeFilterEnabled && mTagId < 0 && mSmartGroupId < 0 && !mFavoritesOnly )
816 return true;
817
818 QModelIndex index = sourceModel()->index( source_row, 0, source_parent );
819 const QString name = sourceModel()->data( index ).toString();
820 const QStringList tags = sourceModel()->data( index, QgsStyleModel::TagRole ).toStringList();
821
822 QgsStyle::StyleEntity styleEntityType = static_cast< QgsStyle::StyleEntity >( sourceModel()->data( index, QgsStyleModel::TypeRole ).toInt() );
823 if ( mEntityFilterEnabled && ( mEntityFilters.empty() || !mEntityFilters.contains( styleEntityType ) ) )
824 return false;
825
826 QgsSymbol::SymbolType symbolType = static_cast< QgsSymbol::SymbolType >( sourceModel()->data( index, QgsStyleModel::SymbolTypeRole ).toInt() );
827 if ( mSymbolTypeFilterEnabled && symbolType != mSymbolType )
828 return false;
829
830 if ( mLayerType != QgsWkbTypes::UnknownGeometry )
831 {
832 switch ( styleEntityType )
833 {
834 case QgsStyle::SymbolEntity:
835 case QgsStyle::TextFormatEntity:
836 case QgsStyle::TagEntity:
837 case QgsStyle::ColorrampEntity:
838 case QgsStyle::SmartgroupEntity:
839 case QgsStyle::LegendPatchShapeEntity:
840 break;
841
842 case QgsStyle::LabelSettingsEntity:
843 {
844 if ( mLayerType != static_cast< QgsWkbTypes::GeometryType >( sourceModel()->data( index, QgsStyleModel::LayerTypeRole ).toInt() ) )
845 return false;
846 break;
847 }
848
849 case QgsStyle::Symbol3DEntity:
850 {
851 const QVariantList types = sourceModel()->data( index, QgsStyleModel::CompatibleGeometryTypesRole ).toList();
852 if ( !types.empty() && !types.contains( mLayerType ) )
853 return false;
854 break;
855 }
856 }
857 }
858
859 if ( mTagId >= 0 && !mTaggedSymbolNames.contains( name ) )
860 return false;
861
862 if ( mSmartGroupId >= 0 && !mSmartGroupSymbolNames.contains( name ) )
863 return false;
864
865 if ( mFavoritesOnly && !sourceModel()->data( index, QgsStyleModel::IsFavoriteRole ).toBool() )
866 return false;
867
868 if ( !mFilterString.isEmpty() )
869 {
870 // filter by word, in both filter string and style entity name/tags
871 // this allows matching of a filter string "hash line" to the symbol "hashed red lines"
872 const QStringList partsToMatch = mFilterString.trimmed().split( ' ' );
873
874 QStringList partsToSearch = name.split( ' ' );
875 for ( const QString &tag : tags )
876 {
877 partsToSearch.append( tag.split( ' ' ) );
878 }
879
880 for ( const QString &part : partsToMatch )
881 {
882 bool found = false;
883 for ( const QString &partToSearch : qgis::as_const( partsToSearch ) )
884 {
885 if ( partToSearch.contains( part, Qt::CaseInsensitive ) )
886 {
887 found = true;
888 break;
889 }
890 }
891 if ( !found )
892 return false; // couldn't find a match for this word, so hide entity
893 }
894 }
895
896 return true;
897 }
898
setFilterString(const QString & filter)899 void QgsStyleProxyModel::setFilterString( const QString &filter )
900 {
901 mFilterString = filter;
902 invalidateFilter();
903 }
904
905
favoritesOnly() const906 bool QgsStyleProxyModel::favoritesOnly() const
907 {
908 return mFavoritesOnly;
909 }
910
setFavoritesOnly(bool favoritesOnly)911 void QgsStyleProxyModel::setFavoritesOnly( bool favoritesOnly )
912 {
913 mFavoritesOnly = favoritesOnly;
914 invalidateFilter();
915 }
916
addDesiredIconSize(QSize size)917 void QgsStyleProxyModel::addDesiredIconSize( QSize size )
918 {
919 mModel->addDesiredIconSize( size );
920 }
921
symbolTypeFilterEnabled() const922 bool QgsStyleProxyModel::symbolTypeFilterEnabled() const
923 {
924 return mSymbolTypeFilterEnabled;
925 }
926
setSymbolTypeFilterEnabled(bool symbolTypeFilterEnabled)927 void QgsStyleProxyModel::setSymbolTypeFilterEnabled( bool symbolTypeFilterEnabled )
928 {
929 mSymbolTypeFilterEnabled = symbolTypeFilterEnabled;
930 invalidateFilter();
931 }
932
layerType() const933 QgsWkbTypes::GeometryType QgsStyleProxyModel::layerType() const
934 {
935 return mLayerType;
936 }
937
setLayerType(QgsWkbTypes::GeometryType type)938 void QgsStyleProxyModel::setLayerType( QgsWkbTypes::GeometryType type )
939 {
940 mLayerType = type;
941 invalidateFilter();
942 }
943
setTagId(int id)944 void QgsStyleProxyModel::setTagId( int id )
945 {
946 mTagId = id;
947
948 mTaggedSymbolNames.clear();
949 if ( mTagId >= 0 )
950 {
951 for ( QgsStyle::StyleEntity entity : ENTITIES )
952 mTaggedSymbolNames.append( mStyle->symbolsWithTag( entity, mTagId ) );
953 }
954
955 invalidateFilter();
956 }
957
tagId() const958 int QgsStyleProxyModel::tagId() const
959 {
960 return mTagId;
961 }
962
setSmartGroupId(int id)963 void QgsStyleProxyModel::setSmartGroupId( int id )
964 {
965 mSmartGroupId = id;
966
967 mSmartGroupSymbolNames.clear();
968 if ( mSmartGroupId >= 0 )
969 {
970 for ( QgsStyle::StyleEntity entity : ENTITIES )
971 mSmartGroupSymbolNames.append( mStyle->symbolsOfSmartgroup( entity, mSmartGroupId ) );
972 }
973 invalidateFilter();
974 }
975
smartGroupId() const976 int QgsStyleProxyModel::smartGroupId() const
977 {
978 return mSmartGroupId;
979 }
980
symbolType() const981 QgsSymbol::SymbolType QgsStyleProxyModel::symbolType() const
982 {
983 return mSymbolType;
984 }
985
setSymbolType(const QgsSymbol::SymbolType symbolType)986 void QgsStyleProxyModel::setSymbolType( const QgsSymbol::SymbolType symbolType )
987 {
988 mSymbolType = symbolType;
989 invalidateFilter();
990 }
991
entityFilterEnabled() const992 bool QgsStyleProxyModel::entityFilterEnabled() const
993 {
994 return mEntityFilterEnabled;
995 }
996
setEntityFilterEnabled(bool entityFilterEnabled)997 void QgsStyleProxyModel::setEntityFilterEnabled( bool entityFilterEnabled )
998 {
999 mEntityFilterEnabled = entityFilterEnabled;
1000 invalidateFilter();
1001 }
1002
entityFilter() const1003 QgsStyle::StyleEntity QgsStyleProxyModel::entityFilter() const
1004 {
1005 return mEntityFilters.empty() ? QgsStyle::SymbolEntity : mEntityFilters.at( 0 );
1006 }
1007
setEntityFilter(const QgsStyle::StyleEntity entityFilter)1008 void QgsStyleProxyModel::setEntityFilter( const QgsStyle::StyleEntity entityFilter )
1009 {
1010 mEntityFilters = QList< QgsStyle::StyleEntity >() << entityFilter;
1011 invalidateFilter();
1012 }
1013
setEntityFilters(const QList<QgsStyle::StyleEntity> & filters)1014 void QgsStyleProxyModel::setEntityFilters( const QList<QgsStyle::StyleEntity> &filters )
1015 {
1016 mEntityFilters = filters;
1017 invalidateFilter();
1018 }
1019
1020