QSkinny 0.8.0
C++/Qt UI toolkit
Loading...
Searching...
No Matches
QskSkinManager.cpp
1/******************************************************************************
2 * QSkinny - Copyright (C) The authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *****************************************************************************/
5
6#include "QskSkinManager.h"
7#include "QskSkinFactory.h"
8#include "QskSkin.h"
9#include "QskSkinTransition.h"
10#include "QskAnimationHint.h"
11
12#include <qdir.h>
13#include <qglobalstatic.h>
14#include <qjsonarray.h>
15#include <qjsonobject.h>
16#include <qmap.h>
17#include <qpluginloader.h>
18#include <qpointer.h>
19#include <qset.h>
20
21#if QT_VERSION >= QT_VERSION_CHECK( 6, 5, 0 )
22#include <qguiapplication.h>
23#include <qstylehints.h>
24#endif
25
26
27namespace
28{
29 class SkinManager final : public QskSkinManager
30 {
31 public:
32 SkinManager()
33 {
34#if QT_VERSION >= QT_VERSION_CHECK( 6, 5, 0 )
35 connect( QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged,
36 this, &SkinManager::updateColorScheme );
37#endif
38 }
39
40 private:
41#if QT_VERSION >= QT_VERSION_CHECK( 6, 5, 0 )
42 void updateColorScheme( Qt::ColorScheme scheme )
43 {
44 if ( QGuiApplication::desktopSettingsAware() )
45 {
46 skin()->setColorScheme(
47 static_cast< QskSkin::ColorScheme >( scheme ) );
48 }
49 }
50#endif
51 };
52}
53
54Q_GLOBAL_STATIC( SkinManager, qskGlobalSkinManager )
55
56static QStringList qskPathList( const char* envName )
57{
58 const auto env = qgetenv( envName );
59 if ( env.isEmpty() )
60 return QStringList();
61
62 const auto name = QFile::decodeName( env );
63 return name.split( QDir::listSeparator(), Qt::SkipEmptyParts );
64}
65
66static inline QString qskResolvedPath( const QString& path )
67{
68 return QDir( path ).canonicalPath();
69}
70
71namespace
72{
73 /*
74 We could use QFactoryLoader, but as it is again a "private" class
75 and does a couple of hardcoded things we don't need ( like always resolving
76 from the application library path ) we prefer having our own code.
77 */
78 class FactoryLoader final : public QPluginLoader
79 {
80 public:
81 FactoryLoader( QObject* parent = nullptr )
82 : QPluginLoader( parent )
83 {
84 }
85
86 bool setPlugin( const QString& fileName )
87 {
88 QPluginLoader::setFileName( fileName );
89
90 /*
91 FactoryId and names of the skins can be found in the metadata
92 without having to load the plugin itself
93 */
94
95 const QLatin1String TokenInterfaceId( "IID" );
96 const QLatin1String TokenData( "MetaData" );
97 const QLatin1String TokenFactoryId( "FactoryId" );
98 const QLatin1String TokenSkins( "Skins" );
99
100 const QLatin1String InterfaceId( QskSkinFactoryIID );
101
102 const auto pluginData = metaData();
103 if ( pluginData.value( TokenInterfaceId ) == InterfaceId )
104 {
105 const auto factoryData = pluginData.value( TokenData ).toObject();
106
107 m_factoryId = factoryData.value( TokenFactoryId ).toString().toLower();
108 if ( m_factoryId.isEmpty() )
109 {
110 // Creating a dummy factory id
111 static int i = 0;
112 m_factoryId = QStringLiteral( "skin_factory_" ) + QString::number( i++ );
113 }
114
115 const auto skins = factoryData.value( TokenSkins ).toArray();
116
117 for ( const auto& skin : skins )
118 m_skinNames += skin.toString();
119 }
120
121 return !m_skinNames.isEmpty();
122 }
123
124 inline QString factoryId() const
125 {
126 return m_factoryId;
127 }
128
129 inline QskSkinFactory* factory()
130 {
131 auto factory = qobject_cast< QskSkinFactory* >( QPluginLoader::instance() );
132 if ( factory )
133 {
134 factory->setParent( nullptr );
135 factory->setObjectName( m_factoryId );
136 }
137
138 return factory;
139 }
140
141 inline QStringList skinNames() const
142 {
143 return m_skinNames;
144 }
145
146 private:
147 void setFileName( const QString& ) = delete;
148 QObject* instance() = delete;
149
150 QString m_factoryId;
151 QStringList m_skinNames;
152 };
153
154 class FactoryMap
155 {
156 private:
157 class Data
158 {
159 public:
160 Data()
161 : loader( nullptr )
162 {
163 }
164
165 ~Data()
166 {
167 reset();
168 }
169
170 void reset()
171 {
172 if ( factory && factory->parent() == nullptr )
173 delete factory;
174
175 factory = nullptr;
176
177 delete loader;
178 loader = nullptr;
179 }
180
181 FactoryLoader* loader;
182 QPointer< QskSkinFactory > factory;
183 };
184
185 public:
186 FactoryMap()
187 : m_isValid( false )
188 {
189 }
190
191 void reset()
192 {
193 m_skinNames.clear();
194 m_skinMap.clear();
195 m_factoryMap.clear();
196 }
197
198 QskSkinFactory* factory( const QString& skinName )
199 {
200 if ( !m_isValid )
201 rebuild();
202
203 const auto it = m_skinMap.constFind( skinName );
204 if ( it != m_skinMap.constEnd() )
205 {
206 auto it2 = m_factoryMap.find( it.value() );
207 if ( it2 != m_factoryMap.end() )
208 {
209 auto& data = it2.value();
210 if ( ( data.factory == nullptr ) && data.loader != nullptr )
211 data.factory = data.loader->factory();
212
213 return data.factory;
214 }
215 }
216
217 return nullptr;
218 }
219
220 QStringList skinNames() const
221 {
222 if ( !m_isValid )
223 const_cast< FactoryMap* >( this )->rebuild();
224
225 return m_skinNames;
226 }
227
228 void insertFactory( FactoryLoader* loader )
229 {
230 auto& data = m_factoryMap[ loader->factoryId() ];
231
232 if ( data.loader != loader )
233 {
234 data.reset();
235 data.loader = loader;
236
237 m_skinMap.clear();
238 m_isValid = false;
239 }
240 }
241
242 void insertFactory( const QString& factoryId, QskSkinFactory* factory )
243 {
244 auto& data = m_factoryMap[ factoryId ];
245
246 if ( data.factory != factory )
247 {
248 data.reset();
249 data.factory = factory;
250
251 m_skinMap.clear();
252 m_skinNames.clear();
253 m_isValid = false;
254 }
255 }
256
257 void removeFactory( const QString& factoryId )
258 {
259 const auto itFactory = m_factoryMap.find( factoryId );
260 if ( itFactory == m_factoryMap.end() )
261 return;
262
263 m_factoryMap.erase( itFactory );
264
265 if ( m_isValid )
266 {
267 for ( auto it = m_skinMap.constBegin();
268 it != m_skinMap.constEnd(); ++it )
269 {
270 if ( it.key() == factoryId )
271 {
272 m_isValid = false;
273 break;
274 }
275 }
276
277 if ( !m_isValid )
278 {
279 m_skinNames.clear();
280 m_skinMap.clear();
281 }
282 }
283 }
284
285 inline bool hasFactory( const QString& factoryId ) const
286 {
287 return m_factoryMap.contains( factoryId );
288 }
289
290 private:
291 void rebuild()
292 {
293 m_skinMap.clear();
294 m_skinNames.clear();
295
296 // first we try all factories, that have been added manually
297 for ( auto it = m_factoryMap.constBegin(); it != m_factoryMap.constEnd(); ++it )
298 {
299 const auto& data = it.value();
300
301 if ( data.loader == nullptr && data.factory )
302 rebuild( it.key(), data.factory->skinNames() );
303 }
304
305 // all factories from plugins are following
306 for ( auto it = m_factoryMap.constBegin(); it != m_factoryMap.constEnd(); ++it )
307 {
308 const auto& data = it.value();
309 if ( data.loader )
310 rebuild( it.key(), data.loader->skinNames() );
311 }
312
313 m_isValid = true;
314 }
315
316 void rebuild( const QString& factoryId, const QStringList& skinNames )
317 {
318 for ( const auto& name : skinNames )
319 {
320 if ( !m_skinMap.contains( name ) )
321 {
322 m_skinMap.insert( name, factoryId );
323 m_skinNames += name;
324 }
325 }
326 }
327
328 QMap< QString, Data > m_factoryMap; // factoryId -> data
329 QMap< QString, QString > m_skinMap; // skinName -> factoryId
330 QStringList m_skinNames;
331
332 bool m_isValid;
333 };
334}
335
336class QskSkinManager::PrivateData
337{
338 public:
339 PrivateData()
340 : pluginsRegistered( false )
341 {
342 }
343
344 inline void ensurePlugins()
345 {
346 if ( !pluginsRegistered )
347 {
348 for ( const auto& path : std::as_const( pluginPaths ) )
349 registerPlugins( path + QStringLiteral( "/skins" ) );
350
351 pluginsRegistered = true;
352 }
353 }
354
355 void registerPlugins( const QString& path )
356 {
357 /*
358 We only detect plugins, but don't load them before being needed.
359 Static plugins are not supported as QskSkinmanager::registerFactory
360 offers a better solution for this use case.
361 */
362
363 QDir dir( path );
364
365 FactoryLoader* loader = nullptr;
366
367 const auto dirEntries = dir.entryList( QDir::Files );
368
369 for ( const auto& fileName : dirEntries )
370 {
371 if ( loader == nullptr )
372 loader = new FactoryLoader();
373
374 bool ok = loader->setPlugin( dir.absoluteFilePath( fileName ) );
375 if ( ok && !factoryMap.hasFactory( loader->factoryId() ) )
376 {
377 factoryMap.insertFactory( loader );
378 loader = nullptr;
379 }
380 }
381
382 delete loader;
383 }
384
385 public:
386 QStringList pluginPaths;
387 FactoryMap factoryMap;
388
389 QPointer< QskSkin > skin;
390 QskAnimationHint transitionHint = 500;
391
392 bool pluginsRegistered : 1;
393};
394
395QskSkinManager* QskSkinManager::instance()
396{
397 return qskGlobalSkinManager;
398}
399
400QskSkinManager::QskSkinManager()
401 : m_data( new PrivateData() )
402{
403 setPluginPaths( qskPathList( "QSK_PLUGIN_PATH" ) +
404 qskPathList( "QT_PLUGIN_PATH" ) );
405}
406
407QskSkinManager::~QskSkinManager()
408{
409}
410
411void QskSkinManager::addPluginPath( const QString& path )
412{
413 const auto pluginPath = qskResolvedPath( path );
414
415 if ( !pluginPath.isEmpty() && !pluginPath.contains( pluginPath ) )
416 {
417 m_data->pluginPaths += pluginPath;
418
419 if ( m_data->pluginsRegistered )
420 m_data->registerPlugins( pluginPath );
421 }
422}
423
424void QskSkinManager::removePluginPath( const QString& path )
425{
426 const auto pluginPath = qskResolvedPath( path );
427
428 if ( m_data->pluginPaths.removeOne( pluginPath ) )
429 {
430 if ( m_data->pluginsRegistered )
431 {
432 m_data->factoryMap.reset();
433 m_data->pluginsRegistered = false;
434 }
435 }
436}
437
438void QskSkinManager::setPluginPaths( const QStringList& paths )
439{
440 m_data->pluginPaths.clear();
441
442 QSet< QString > pathSet; // checking for duplicates
443 QStringList pluginPaths;
444
445 for ( const auto& path : paths )
446 {
447 const auto pluginPath = qskResolvedPath( path );
448 if ( !pluginPath.isEmpty() && !pathSet.contains( pluginPath ) )
449 {
450 pathSet += pluginPath;
451 pluginPaths += pluginPath;
452 }
453 }
454
455 if ( pluginPaths != m_data->pluginPaths )
456 {
457 m_data->pluginPaths = pluginPaths;
458 m_data->factoryMap.reset();
459 m_data->pluginsRegistered = false;
460 }
461}
462
463QStringList QskSkinManager::pluginPaths() const
464{
465 return m_data->pluginPaths;
466}
467
468void QskSkinManager::registerFactory(
469 const QString& factoryId, QskSkinFactory* factory )
470{
471 if ( factoryId.isEmpty() || factory == nullptr )
472 return;
473
474 /*
475 Manually registered factories always come first, and we don't need
476 to check the plugins here.
477 */
478
479 m_data->factoryMap.insertFactory( factoryId.toLower(), factory );
480}
481
482void QskSkinManager::unregisterFactory( const QString& factoryId )
483{
484 if ( factoryId.isEmpty() )
485 return;
486
487 /*
488 As this call might be about a factory from a plugin, we need
489 to know about them here.
490 */
491
492 m_data->ensurePlugins();
493 m_data->factoryMap.removeFactory( factoryId.toLower() );
494}
495
496void QskSkinManager::unregisterFactories()
497{
498 m_data->factoryMap.reset();
499}
500
501QStringList QskSkinManager::skinNames() const
502{
503 m_data->ensurePlugins();
504 return m_data->factoryMap.skinNames();
505}
506
507QskSkin* QskSkinManager::createSkin(
508 const QString& skinName, QskSkin::ColorScheme colorScheme ) const
509{
510 m_data->ensurePlugins();
511
512 auto& map = m_data->factoryMap;
513
514 auto name = skinName;
515
516 auto factory = map.factory( name );
517 if ( factory == nullptr )
518 {
519 const auto names = map.skinNames();
520 if ( !names.isEmpty() )
521 {
522 name = names.first();
523 factory = map.factory( name );
524 }
525 }
526
527 QskSkin* skin = nullptr;
528
529 if ( factory )
530 {
531 skin = factory->createSkin( name );
532 if ( skin )
533 {
534 skin->setObjectName( name );
535
536 if ( colorScheme == QskSkin::UnknownScheme )
537 {
538#if QT_VERSION >= QT_VERSION_CHECK( 6, 5, 0 )
539 colorScheme = static_cast< QskSkin::ColorScheme >(
540 QGuiApplication::styleHints()->colorScheme() );
541#else
542 colorScheme = QskSkin::LightScheme;
543#endif
544 }
545
546 skin->setColorScheme( colorScheme );
547 }
548 }
549
550 return skin;
551}
552
553void QskSkinManager::setSkin( QskSkin* skin )
554{
555 if ( m_data->skin == skin )
556 return;
557
558 const auto oldSkin = m_data->skin;
559 m_data->skin = skin;
560
561 if ( skin )
562 {
563 if ( skin->parent() == nullptr )
564 skin->setParent( this );
565
566 connect( skin, &QskSkin::colorSchemeChanged,
567 this, &QskSkinManager::colorSchemeChanged );
568 }
569
570 if ( oldSkin )
571 {
572 disconnect( oldSkin, &QskSkin::colorSchemeChanged,
573 this, &QskSkinManager::colorSchemeChanged );
574 }
575
576 Q_EMIT skinChanged( skin );
577
578 if ( skin && oldSkin && m_data->transitionHint.isValid() )
579 {
580 QskSkinTransition transition;
581 transition.setSourceSkin( oldSkin );
582 transition.setTargetSkin( skin );
583 transition.run( m_data->transitionHint );
584 }
585
586 if ( oldSkin && oldSkin->parent() == this )
587 delete oldSkin;
588}
589
590QskSkin* QskSkinManager::setSkin( const QString& name )
591{
592 if ( !( m_data->skin && ( m_data->skin->objectName() == name ) ) )
593 {
594 auto colorScheme = QskSkin::UnknownScheme;
595 if ( m_data->skin )
596 colorScheme = m_data->skin->colorScheme();
597
598 auto skin = createSkin( name, colorScheme );
599 if ( skin )
600 setSkin( skin );
601 }
602
603 return m_data->skin;
604}
605
606QString QskSkinManager::skinName() const
607{
608 if ( m_data->skin )
609 return m_data->skin->objectName();
610
611 return QString();
612}
613
614QskSkin* QskSkinManager::skin()
615{
616 if ( m_data->skin == nullptr )
617 {
618 m_data->skin = createSkin( QString() );
619 Q_ASSERT( m_data->skin );
620
621 m_data->skin->setParent( this );
622 }
623
624 return m_data->skin;
625}
626
627void QskSkinManager::setTransitionHint( const QskAnimationHint& hint )
628{
629 m_data->transitionHint = hint;
630}
631
632QskAnimationHint QskSkinManager::transitionHint() const
633{
634 return m_data->transitionHint;
635}
636
637#include "moc_QskSkinManager.cpp"