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 // Avoid adding pluginPath that is empty or is already contained in the paths.
416 if ( !pluginPath.isEmpty() && !m_data->pluginPaths.contains( pluginPath ) )
417 {
418 m_data->pluginPaths += pluginPath;
419
420 if ( m_data->pluginsRegistered )
421 m_data->registerPlugins( pluginPath );
422 }
423}
424
425void QskSkinManager::removePluginPath( const QString& path )
426{
427 const auto pluginPath = qskResolvedPath( path );
428
429 if ( m_data->pluginPaths.removeOne( pluginPath ) )
430 {
431 if ( m_data->pluginsRegistered )
432 {
433 m_data->factoryMap.reset();
434 m_data->pluginsRegistered = false;
435 }
436 }
437}
438
439void QskSkinManager::setPluginPaths( const QStringList& paths )
440{
441 m_data->pluginPaths.clear();
442
443 QSet< QString > pathSet; // checking for duplicates
444 QStringList pluginPaths;
445
446 for ( const auto& path : paths )
447 {
448 const auto pluginPath = qskResolvedPath( path );
449 if ( !pluginPath.isEmpty() && !pathSet.contains( pluginPath ) )
450 {
451 pathSet += pluginPath;
452 pluginPaths += pluginPath;
453 }
454 }
455
456 if ( pluginPaths != m_data->pluginPaths )
457 {
458 m_data->pluginPaths = pluginPaths;
459 m_data->factoryMap.reset();
460 m_data->pluginsRegistered = false;
461 }
462}
463
464QStringList QskSkinManager::pluginPaths() const
465{
466 return m_data->pluginPaths;
467}
468
469void QskSkinManager::registerFactory(
470 const QString& factoryId, QskSkinFactory* factory )
471{
472 if ( factoryId.isEmpty() || factory == nullptr )
473 return;
474
475 /*
476 Manually registered factories always come first, and we don't need
477 to check the plugins here.
478 */
479
480 m_data->factoryMap.insertFactory( factoryId.toLower(), factory );
481}
482
483void QskSkinManager::unregisterFactory( const QString& factoryId )
484{
485 if ( factoryId.isEmpty() )
486 return;
487
488 /*
489 As this call might be about a factory from a plugin, we need
490 to know about them here.
491 */
492
493 m_data->ensurePlugins();
494 m_data->factoryMap.removeFactory( factoryId.toLower() );
495}
496
497void QskSkinManager::unregisterFactories()
498{
499 m_data->factoryMap.reset();
500}
501
502QStringList QskSkinManager::skinNames() const
503{
504 m_data->ensurePlugins();
505 return m_data->factoryMap.skinNames();
506}
507
508QskSkin* QskSkinManager::createSkin(
509 const QString& skinName, QskSkin::ColorScheme colorScheme ) const
510{
511 m_data->ensurePlugins();
512
513 auto& map = m_data->factoryMap;
514
515 auto name = skinName;
516
517 auto factory = map.factory( name );
518 if ( factory == nullptr )
519 {
520 const auto names = map.skinNames();
521 if ( !names.isEmpty() )
522 {
523 name = names.first();
524 factory = map.factory( name );
525 }
526 }
527
528 QskSkin* skin = nullptr;
529
530 if ( factory )
531 {
532 skin = factory->createSkin( name );
533 if ( skin )
534 {
535 skin->setObjectName( name );
536
537 if ( colorScheme == QskSkin::UnknownScheme )
538 {
539#if QT_VERSION >= QT_VERSION_CHECK( 6, 5, 0 )
540 colorScheme = static_cast< QskSkin::ColorScheme >(
541 QGuiApplication::styleHints()->colorScheme() );
542#else
543 colorScheme = QskSkin::LightScheme;
544#endif
545 }
546
547 skin->setColorScheme( colorScheme );
548 }
549 }
550
551 return skin;
552}
553
554void QskSkinManager::setSkin( QskSkin* skin )
555{
556 if ( m_data->skin == skin )
557 return;
558
559 const auto oldSkin = m_data->skin;
560 m_data->skin = skin;
561
562 if ( skin )
563 {
564 if ( skin->parent() == nullptr )
565 skin->setParent( this );
566
567 connect( skin, &QskSkin::colorSchemeChanged,
568 this, &QskSkinManager::colorSchemeChanged );
569 }
570
571 if ( oldSkin )
572 {
573 disconnect( oldSkin, &QskSkin::colorSchemeChanged,
574 this, &QskSkinManager::colorSchemeChanged );
575 }
576
577 Q_EMIT skinChanged( skin );
578
579 if ( skin && oldSkin && m_data->transitionHint.isValid() )
580 {
581 QskSkinTransition transition;
582 transition.setSourceSkin( oldSkin );
583 transition.setTargetSkin( skin );
584 transition.run( m_data->transitionHint );
585 }
586
587 if ( oldSkin && oldSkin->parent() == this )
588 delete oldSkin;
589}
590
591QskSkin* QskSkinManager::setSkin( const QString& name )
592{
593 if ( !( m_data->skin && ( m_data->skin->objectName() == name ) ) )
594 {
595 auto colorScheme = QskSkin::UnknownScheme;
596 if ( m_data->skin )
597 colorScheme = m_data->skin->colorScheme();
598
599 auto skin = createSkin( name, colorScheme );
600 if ( skin )
601 setSkin( skin );
602 }
603
604 return m_data->skin;
605}
606
607QString QskSkinManager::skinName() const
608{
609 if ( m_data->skin )
610 return m_data->skin->objectName();
611
612 return QString();
613}
614
615QskSkin* QskSkinManager::skin()
616{
617 if ( m_data->skin == nullptr )
618 {
619 m_data->skin = createSkin( QString() );
620 Q_ASSERT( m_data->skin );
621
622 m_data->skin->setParent( this );
623 }
624
625 return m_data->skin;
626}
627
628const QskSkin* QskSkinManager::currentSkin() const
629{
630 return m_data->skin;
631}
632
633void QskSkinManager::setTransitionHint( const QskAnimationHint& hint )
634{
635 m_data->transitionHint = hint;
636}
637
638QskAnimationHint QskSkinManager::transitionHint() const
639{
640 return m_data->transitionHint;
641}
642
643#include "moc_QskSkinManager.cpp"