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