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