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