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