[NUI] Disable ThemeManager preload
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI / src / public / Theme / ThemeManager.cs
1 /*
2  * Copyright(c) 2021 Samsung Electronics Co., Ltd.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  *
16  */
17 using System;
18 using System.Collections.Generic;
19 using System.ComponentModel;
20 using System.Diagnostics;
21 using System.Diagnostics.CodeAnalysis;
22 using Tizen.NUI.BaseComponents;
23
24 #pragma warning disable CS0162 // Unreachable code detected: Some lines can be unreachable in TV profile
25 namespace Tizen.NUI
26 {
27     /// <summary>
28     /// This static module provides methods that can manage NUI <see cref="Theme"/>.
29     /// </summary>
30     /// <example>
31     /// To apply custom theme to the application, try <see cref="ApplyTheme(Theme)"/>.
32     /// <code>
33     /// var customTheme = new Theme(res + "customThemeFile.xaml");
34     /// ThemeManager.ApplyTheme(customTheme);
35     /// </code>
36     /// </example>
37     /// <summary></summary>
38     /// <since_tizen> 9 </since_tizen>
39     public static class ThemeManager
40     {
41         /// <summary>
42         /// The default light theme name preloaded in platform.
43         /// </summary>
44         [EditorBrowsable(EditorBrowsableState.Never)]
45         public const string DefaultLightThemeName = "org.tizen.default-light-theme";
46
47         /// <summary>
48         /// The default dark theme name preloaded in platform.
49         /// </summary>
50         [EditorBrowsable(EditorBrowsableState.Never)]
51         public const string DefaultDarkThemeName = "org.tizen.default-dark-theme";
52
53         private static Theme baseTheme; // The base theme. It includes all styles including structures (Size, Position, Policy) of components.
54         private static Theme platformTheme; // The platform theme. This may include color and image information without structure detail.
55         private static Theme userTheme; // The user custom theme.
56         private static Theme themeForUpdate; // platformTheme + userTheme. It is used when the component need to update according to theme change.
57         private static Theme themeForInitialize; // baseTheme + platformTheme + userTheme. It is used when the component is created.
58         private static readonly List<Theme> cachedPlatformThemes = new List<Theme>(); // Themes provided by framework.
59         private static readonly List<string> packages = new List<string>();// This is to store base theme creators by packages.
60         private static bool platformThemeEnabled = false;
61         private static bool isInEventProgress = false;
62
63         static ThemeManager()
64         {
65             if (InitialThemeDisabled) return;
66
67             ExternalThemeManager.Initialize();
68             ExternalThemeManager.PlatformThemeChanged += OnExternalThemeChanged;
69             AddPackageTheme(DefaultThemeCreator.Instance);
70         }
71
72         /// <summary>
73         /// An event invoked when the theme is about to change (not applied to the views yet).
74         /// </summary>
75         [EditorBrowsable(EditorBrowsableState.Never)]
76         public static event EventHandler<ThemeChangedEventArgs> ThemeChanging;
77
78         /// <summary>
79         /// An event invoked after the theme has changed by <see cref="ApplyTheme(Theme)"/>.
80         /// </summary>
81         /// <since_tizen> 9 </since_tizen>
82         public static event EventHandler<ThemeChangedEventArgs> ThemeChanged;
83
84         /// <summary>
85         /// Internal one should be called before calling public ThemeChanged
86         /// </summary>
87         internal static WeakEvent<EventHandler<ThemeChangedEventArgs>> ThemeChangedInternal = new WeakEvent<EventHandler<ThemeChangedEventArgs>>();
88
89         /// <summary>
90         /// The current theme Id.
91         /// It returns null when no theme is applied.
92         /// </summary>
93         [EditorBrowsable(EditorBrowsableState.Never)]
94         public static string ThemeId
95         {
96             get => userTheme?.Id;
97         }
98
99         /// <summary>
100         /// The current platform theme Id.
101         /// Note that it returns null when the platform theme is disabled.
102         /// If the <seealso cref="NUIApplication.ThemeOptions.PlatformThemeEnabled"/> is given, it can be one of followings in tizen 6.5:
103         /// <list type="bullet">
104         /// <item>
105         /// <description>org.tizen.default-light-theme</description>
106         /// </item>
107         /// <item>
108         /// <description>org.tizen.default-dark-theme</description>
109         /// </item>
110         /// </list>
111         /// </summary>
112         [EditorBrowsable(EditorBrowsableState.Never)]
113         public static string PlatformThemeId
114         {
115             get => platformTheme?.Id ?? (platformThemeEnabled ? baseTheme?.Id : null);
116         }
117
118         /// <summary>
119         /// To support deprecated StyleManager.
120         /// NOTE that, please remove this after remove Tizen.NUI.Components.StyleManager
121         /// </summary>
122         internal static Theme BaseTheme
123         {
124             get
125             {
126                 if (baseTheme == null)
127                 {
128                     baseTheme = new Theme();
129                     UpdateThemeForInitialize();
130                 }
131                 return baseTheme;
132             }
133             set
134             {
135                 baseTheme = value;
136                 UpdateThemeForInitialize();
137             }
138         }
139
140         /// <summary>
141         /// To support deprecated StyleManager.
142         /// NOTE that, please remove this after remove Tizen.NUI.Components.StyleManager
143         /// </summary>
144         internal static Theme CurrentTheme
145         {
146             get => userTheme ?? baseTheme;
147             set
148             {
149                 userTheme = value;
150                 UpdateThemeForInitialize();
151                 NotifyThemeChanged();
152             }
153         }
154
155         internal static bool PlatformThemeEnabled
156         {
157             get => platformThemeEnabled;
158             set
159             {
160                 if (platformThemeEnabled == value) return;
161
162                 platformThemeEnabled = value;
163
164                 if (platformThemeEnabled)
165                 {
166                     ApplyExternalPlatformTheme(ExternalThemeManager.CurrentThemeId, ExternalThemeManager.CurrentThemeVersion);
167                 }
168             }
169         }
170
171         internal static bool ApplicationThemeChangeSensitive { get; set; } = false;
172
173 #if PROFILE_TV
174         internal const bool InitialThemeDisabled = true;
175 #else        
176         internal const bool InitialThemeDisabled = false;
177 #endif
178
179         /// <summary>
180         /// Apply custom theme to the NUI.
181         /// This will change the appearance of the existing components with property <seealso cref="View.ThemeChangeSensitive"/> on.
182         /// This also affects all components created afterwards.
183         /// </summary>
184         /// <param name="theme">The theme instance to be applied.</param>
185         /// <exception cref="ArgumentNullException">Thrown when the given theme is null.</exception>
186         /// <since_tizen> 9 </since_tizen>
187         public static void ApplyTheme(Theme theme)
188         {
189             var newTheme = (Theme)theme?.Clone() ?? throw new ArgumentNullException(nameof(theme));
190
191             if (string.IsNullOrEmpty(newTheme.Id))
192             {
193                 newTheme.Id = "NONAME";
194             }
195
196             StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Small, newTheme.SmallBrokenImageUrl ?? "");
197             StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Normal, newTheme.BrokenImageUrl ?? "");
198             StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Large, newTheme.LargeBrokenImageUrl ?? "");
199
200             userTheme = newTheme;
201             UpdateThemeForInitialize();
202             UpdateThemeForUpdate();
203             NotifyThemeChanged();
204         }
205
206         /// <summary>
207         /// Append a theme to the current theme and apply it.
208         /// This will change the appearance of the existing components with property <seealso cref="View.ThemeChangeSensitive"/> on.
209         /// This also affects all components created afterwards.
210         /// </summary>
211         /// <param name="theme">The theme instance to be appended.</param>
212         /// <exception cref="ArgumentNullException">Thrown when the given theme is null.</exception>
213         [EditorBrowsable(EditorBrowsableState.Never)]
214         public static void AppendTheme(Theme theme)
215         {
216             var newTheme = (Theme)theme?.Clone() ?? throw new ArgumentNullException(nameof(theme));
217
218             if (string.IsNullOrEmpty(newTheme.Id))
219             {
220                 newTheme.Id = "NONAME";
221             }
222
223             StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Small, newTheme.SmallBrokenImageUrl ?? "");
224             StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Normal, newTheme.BrokenImageUrl ?? "");
225             StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Large, newTheme.LargeBrokenImageUrl ?? "");
226
227             if (userTheme == null) userTheme = theme;
228             else
229             {
230                 userTheme = (Theme)userTheme.Clone();
231                 userTheme.MergeWithoutClone(theme);
232             }
233
234             UpdateThemeForInitialize();
235             UpdateThemeForUpdate();
236             NotifyThemeChanged();
237         }
238
239         /// <summary>
240         /// Change tizen theme.
241         /// User may change this to one of platform installed one.
242         /// Note that this is global theme changing which effects all applications.
243         /// </summary>
244         /// <param name="themeId">The installed theme Id.</param>
245         /// <returns>true on success, false when it failed to find installed theme with given themeId.</returns>
246         /// <exception cref="ArgumentNullException">Thrown when the given themeId is null.</exception>
247         [EditorBrowsable(EditorBrowsableState.Never)]
248         public static bool ApplyPlatformTheme(string themeId)
249         {
250             if (themeId == null) throw new ArgumentNullException(nameof(themeId));
251
252             return ExternalThemeManager.SetTheme(themeId);
253         }
254
255         /// <summary>
256         /// Load a style with style name in the current theme.
257         /// For components, the default style name of a component is a component name with namespace (e.g. Tizen.NUI.Components.Button).
258         /// </summary>
259         /// <param name="styleName">The style name.</param>
260         /// <exception cref="ArgumentNullException">Thrown when the given styleName is null.</exception>
261         /// <since_tizen> 9 </since_tizen>
262         public static ViewStyle GetStyle(string styleName)
263         {
264             if (styleName == null) throw new ArgumentNullException(nameof(styleName));
265             return GetInitialStyleWithoutClone(styleName)?.Clone();
266         }
267
268         /// <summary>
269         /// Load a style with view type in the current theme.
270         /// If it failed to find a style with the given type, it will try with it's parent type until it succeeds.
271         /// </summary>
272         /// <param name="viewType"> The type of the view. Full name of the given type will be a key to find a style in the current theme. (e.g. Tizen.NUI.Components.Button) </param>
273         /// <exception cref="ArgumentNullException">Thrown when the given viewType is null.</exception>
274         /// <since_tizen> 9 </since_tizen>
275         public static ViewStyle GetStyle(Type viewType)
276         {
277             if (viewType == null) throw new ArgumentNullException(nameof(viewType));
278             return GetInitialStyleWithoutClone(viewType)?.Clone();
279         }
280
281         /// <summary>
282         /// Load a platform style with style name in the current theme.
283         /// It returns null when the platform theme is disabled. <see cref="NUIApplication.ThemeOptions.PlatformThemeEnabled" />.
284         /// </summary>
285         /// <param name="styleName">The style name.</param>
286         /// <exception cref="ArgumentNullException">Thrown when the given styleName is null.</exception>
287         [EditorBrowsable(EditorBrowsableState.Never)]
288         public static ViewStyle GetPlatformStyle(string styleName)
289         {
290             if (styleName == null) throw new ArgumentNullException(nameof(styleName));
291             return platformTheme?.GetStyle(styleName)?.Clone();
292         }
293
294         /// <summary>
295         /// Load a platform style with view type in the current theme.
296         /// It returns null when the platform theme is disabled. <see cref="NUIApplication.ThemeOptions.PlatformThemeEnabled" />.
297         /// </summary>
298         /// <param name="viewType"> The type of the view. Full name of the given type will be a key to find a style in the current theme. (e.g. Tizen.NUI.Components.Button) </param>
299         /// <exception cref="ArgumentNullException">Thrown when the given viewType is null.</exception>
300         [EditorBrowsable(EditorBrowsableState.Never)]
301         public static ViewStyle GetPlatformStyle(Type viewType)
302         {
303             if (viewType == null) throw new ArgumentNullException(nameof(viewType));
304             return platformTheme?.GetStyle(viewType)?.Clone();
305         }
306
307         /// <summary>
308         /// Load a style with style name in the current theme.
309         /// </summary>
310         /// <param name="styleName">The style name.</param>
311         internal static ViewStyle GetUpdateStyleWithoutClone(string styleName) => themeForUpdate?.GetStyle(styleName);
312
313         /// <summary>
314         /// Load a style with View type in the current theme.
315         /// </summary>
316         /// <param name="viewType">The type of View.</param>
317         internal static ViewStyle GetUpdateStyleWithoutClone(Type viewType) => themeForUpdate?.GetStyle(viewType);
318
319         /// <summary>
320         /// Load a initial component style.
321         /// </summary>
322         internal static ViewStyle GetInitialStyleWithoutClone(string styleName) => themeForInitialize?.GetStyle(styleName);
323
324         /// <summary>
325         /// Load a initial component style.
326         /// </summary>
327         internal static ViewStyle GetInitialStyleWithoutClone(Type viewType) => themeForInitialize?.GetStyle(viewType);
328
329         /// <summary>
330         /// Get a platform installed theme.
331         /// </summary>
332         /// <param name="themeId">The theme id.</param>
333         internal static Theme LoadPlatformTheme(string themeId)
334         {
335             Debug.Assert(themeId != null);
336
337             // Check if it is already loaded.
338             int index = cachedPlatformThemes.FindIndex(x => string.Equals(x.Id, themeId, StringComparison.OrdinalIgnoreCase));
339             if (index >= 0)
340             {
341                 Tizen.Log.Info("NUI", $"Hit cache.");
342                 var found = cachedPlatformThemes[index];
343                 // If the cached is not a full set, update it.
344                 if (found.PackageCount < packages.Count)
345                 {
346                     UpdatePlatformTheme(found);
347                     Tizen.Log.Info("NUI", $"Update cache.");
348                 }
349                 return found;
350             }
351
352             var newTheme = CreatePlatformTheme(themeId);
353             if (newTheme != null)
354             {
355                 cachedPlatformThemes.Add(newTheme);
356                 Tizen.Log.Info("NUI", $"Platform theme has been loaded successfully.");
357             }
358             return newTheme;
359         }
360
361         /// <summary>
362         /// !!! This is for internal use in fhub-nui. Do not open it.
363         /// Set a theme to be used as fallback.
364         /// The fallback theme is set to profile specified theme by default.
365         /// </summary>
366         /// <param name="fallbackTheme">The theme instance to be applied as a fallback.</param>
367         [EditorBrowsable(EditorBrowsableState.Never)]
368         internal static void ApplyFallbackTheme(Theme fallbackTheme)
369         {
370             Debug.Assert(fallbackTheme != null);
371             BaseTheme = (Theme)fallbackTheme?.Clone();
372         }
373
374         /// <summary>
375         /// Apply an external platform theme.
376         /// </summary>
377         /// <param name="id">The external theme id.</param>
378         /// <param name="version">The external theme version.</param>
379         private static void ApplyExternalPlatformTheme(string id, string version)
380         {
381             if (InitialThemeDisabled) return;
382
383             // If the given theme is invalid, do nothing.
384             if (string.IsNullOrEmpty(id))
385             {
386                 return;
387             }
388
389             // If no platform theme has been applied and the base theme can cover the given one, do nothing.
390             if (platformTheme == null && baseTheme != null && baseTheme.HasSameIdAndVersion(id, version))
391             {
392                 Tizen.Log.Info("NUI", "The base theme can cover platform theme: Skip loading.");
393                 return;
394             }
395
396             // If the given theme is already applied, do nothing.
397             if (platformTheme != null && platformTheme.HasSameIdAndVersion(id, version))
398             {
399                 Tizen.Log.Info("NUI", "Platform theme is already applied: Skip loading.");
400                 return;
401             }
402
403             var loaded = LoadPlatformTheme(id);
404
405             if (loaded != null)
406             {
407                 Tizen.Log.Info("NUI", $"{loaded.Id} has been applied successfully.");
408                 platformTheme = loaded;
409                 UpdateThemeForInitialize();
410                 UpdateThemeForUpdate();
411                 NotifyThemeChanged(true);
412             }
413         }
414
415         internal static void AddPackageTheme(IThemeCreator themeCreator)
416         {
417             string packageName;
418             if (InitialThemeDisabled || packages.Contains(packageName = themeCreator.GetType().Assembly.GetName().Name))
419             {
420                 return;
421             }
422
423             Tizen.Log.Debug("NUI", $"AddPackageTheme({themeCreator.GetType().Assembly.GetName().Name})");
424             packages.Add(packageName);
425
426             // Base theme
427             var packageBaseTheme = themeCreator.Create();
428             Debug.Assert(packageBaseTheme != null);
429
430             if (baseTheme == null) baseTheme = packageBaseTheme;
431             else baseTheme.MergeWithoutClone(packageBaseTheme);
432             baseTheme.PackageCount++;
433
434             if (platformThemeEnabled)
435             {
436                 Tizen.Log.Info("NUI", $"Platform theme is enabled");
437                 if (platformTheme != null)
438                 {
439                     UpdatePlatformTheme(platformTheme);
440                 }
441                 else
442                 {
443                     if (!string.IsNullOrEmpty(ExternalThemeManager.CurrentThemeId) && !baseTheme.HasSameIdAndVersion(ExternalThemeManager.CurrentThemeId, ExternalThemeManager.CurrentThemeVersion))
444                     {
445                         var loaded = LoadPlatformTheme(ExternalThemeManager.CurrentThemeId);
446                         if (loaded != null)
447                         {
448                             platformTheme = loaded;
449                         }
450                     }
451                 }
452                 UpdateThemeForUpdate();
453             }
454             UpdateThemeForInitialize();
455         }
456
457         // TODO Please make it private after removing Tizen.NUI.Components.StyleManager.
458         internal static void UpdateThemeForUpdate()
459         {
460             if (userTheme == null)
461             {
462                 themeForUpdate = platformTheme;
463                 return;
464             }
465
466             if (platformTheme == null)
467             {
468                 themeForUpdate = userTheme;
469                 return;
470             }
471
472             themeForUpdate = new Theme();
473             themeForUpdate.Merge(platformTheme);
474             themeForUpdate.MergeWithoutClone(userTheme);
475         }
476
477         // TODO Please make it private after removing Tizen.NUI.Components.StyleManager.
478         internal static void UpdateThemeForInitialize()
479         {
480             if (platformTheme == null && userTheme == null)
481             {
482                 themeForInitialize = baseTheme;
483                 return;
484             }
485
486             themeForInitialize = new Theme();
487
488             if (baseTheme != null) themeForInitialize.Merge(baseTheme);
489
490             if (userTheme == null)
491             {
492                 if (platformTheme != null) themeForInitialize.MergeWithoutClone(platformTheme);
493             }
494             else
495             {
496                 if (platformTheme != null) themeForInitialize.Merge(platformTheme);
497                 themeForInitialize.MergeWithoutClone(userTheme);
498             }
499         }
500
501         private static void UpdatePlatformTheme(Theme theme)
502         {
503             var sharedResourcePath = ExternalThemeManager.GetSharedResourcePath(theme.Id);
504
505             if (sharedResourcePath == null)
506             {
507                 return;
508             }
509
510             for (var i = theme.PackageCount; i < packages.Count; i++)
511             {
512                 theme.MergeWithoutClone(CreatePlatformTheme(sharedResourcePath, packages[i]));
513             }
514             theme.PackageCount = packages.Count;
515         }
516
517         private static Theme CreatePlatformTheme(string id)
518         {
519             var sharedResourcePath = ExternalThemeManager.GetSharedResourcePath(id);
520
521             if (sharedResourcePath == null)
522             {
523                 return null;
524             }
525
526             var newTheme = new Theme()
527             {
528                 Id = id
529             };
530
531             foreach (var packageName in packages)
532             {
533                 newTheme.MergeWithoutClone(CreatePlatformTheme(sharedResourcePath, packageName));
534             }
535             newTheme.PackageCount = packages.Count;
536
537             return newTheme;
538         }
539
540         [SuppressMessage("Microsoft.Design", "CA1031: Do not catch general exception types", Justification = "This method is to handle external resources that may throw an exception but ignorable. This method should not interrupt the main stream.")]
541         private static Theme CreatePlatformTheme(string sharedResourcePath, string assemblyName)
542         {
543             ExternalThemeManager.SharedResourcePath = sharedResourcePath;
544             try
545             {
546                 return new Theme(sharedResourcePath + assemblyName + ".Theme.xaml");
547             }
548             catch (System.IO.FileNotFoundException)
549             {
550                 Tizen.Log.Info("NUI", $"[Ignorable] Current tizen theme does not have NUI theme.");
551             }
552             catch (Exception e)
553             {
554                 Tizen.Log.Info("NUI", $"[Ignorable] {e.GetType().Name} occurred while applying tizen theme to {assemblyName}: {e.Message}");
555             }
556
557             return new Theme();
558         }
559
560         private static void AddToPlatformThemes(Theme theme)
561         {
562             int index = cachedPlatformThemes.FindIndex(x => x.Id.Equals(theme.Id, StringComparison.OrdinalIgnoreCase));
563             if (index >= 0)
564             {
565                 Tizen.Log.Info("NUI", $"Existing {theme.Id} item is overwritten");
566                 cachedPlatformThemes[index] = theme;
567             }
568             else
569             {
570                 cachedPlatformThemes.Add(theme);
571                 Tizen.Log.Info("NUI", $"New {theme.Id} is saved.");
572             }
573         }
574
575         private static void NotifyThemeChanged(bool platformThemeUpdated = false)
576         {
577             if (isInEventProgress) return;
578             isInEventProgress = true;
579
580             var platformThemeId = PlatformThemeId;
581             var userThemeId = userTheme?.Id;
582             ThemeChanging?.Invoke(null, new ThemeChangedEventArgs(userThemeId, platformThemeId, platformThemeUpdated));
583             ThemeChangedInternal.Invoke(null, new ThemeChangedEventArgs(userThemeId, platformThemeId, platformThemeUpdated));
584             ThemeChanged?.Invoke(null, new ThemeChangedEventArgs(userThemeId, platformThemeId, platformThemeUpdated));
585
586             isInEventProgress = false;
587         }
588
589         private static void OnExternalThemeChanged(object sender, EventArgs e)
590         {
591             if (!PlatformThemeEnabled)
592             {
593                 return;
594             }
595
596             ApplyExternalPlatformTheme(ExternalThemeManager.CurrentThemeId, ExternalThemeManager.CurrentThemeVersion);
597         }
598     }
599 }
600 #pragma warning restore CS0162 // Unreachable code detected