2 * Copyright(c) 2021 Samsung Electronics Co., Ltd.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 using System.Collections.Generic;
19 using System.ComponentModel;
20 using System.Diagnostics;
21 using System.Diagnostics.CodeAnalysis;
22 using Tizen.NUI.BaseComponents;
24 #pragma warning disable CS0162 // Unreachable code detected: Some lines can be unreachable in TV profile
28 /// This static module provides methods that can manage NUI <see cref="Theme"/>.
31 /// To apply custom theme to the application, try <see cref="ApplyTheme(Theme)"/>.
33 /// var customTheme = new Theme(res + "customThemeFile.xaml");
34 /// ThemeManager.ApplyTheme(customTheme);
37 /// <summary></summary>
38 /// <since_tizen> 9 </since_tizen>
39 public static class ThemeManager
42 /// The default light theme name preloaded in platform.
44 [EditorBrowsable(EditorBrowsableState.Never)]
45 public const string DefaultLightThemeName = "org.tizen.default-light-theme";
48 /// The default dark theme name preloaded in platform.
50 [EditorBrowsable(EditorBrowsableState.Never)]
51 public const string DefaultDarkThemeName = "org.tizen.default-dark-theme";
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;
65 if (InitialThemeDisabled) return;
67 ExternalThemeManager.Initialize();
68 ExternalThemeManager.PlatformThemeChanged += OnExternalThemeChanged;
69 AddPackageTheme(DefaultThemeCreator.Instance);
73 /// An event invoked when the theme is about to change (not applied to the views yet).
75 [EditorBrowsable(EditorBrowsableState.Never)]
76 public static event EventHandler<ThemeChangedEventArgs> ThemeChanging;
79 /// An event invoked after the theme has changed by <see cref="ApplyTheme(Theme)"/>.
81 /// <since_tizen> 9 </since_tizen>
82 public static event EventHandler<ThemeChangedEventArgs> ThemeChanged;
85 /// Internal one should be called before calling public ThemeChanged
87 internal static WeakEvent<EventHandler<ThemeChangedEventArgs>> ThemeChangedInternal = new WeakEvent<EventHandler<ThemeChangedEventArgs>>();
90 /// The current theme Id.
91 /// It returns null when no theme is applied.
93 [EditorBrowsable(EditorBrowsableState.Never)]
94 public static string ThemeId
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">
105 /// <description>org.tizen.default-light-theme</description>
108 /// <description>org.tizen.default-dark-theme</description>
112 [EditorBrowsable(EditorBrowsableState.Never)]
113 public static string PlatformThemeId
115 get => platformTheme?.Id ?? (platformThemeEnabled ? baseTheme?.Id : null);
119 /// To support deprecated StyleManager.
120 /// NOTE that, please remove this after remove Tizen.NUI.Components.StyleManager
122 internal static Theme BaseTheme
126 if (baseTheme == null)
128 baseTheme = new Theme();
129 UpdateThemeForInitialize();
136 UpdateThemeForInitialize();
141 /// To support deprecated StyleManager.
142 /// NOTE that, please remove this after remove Tizen.NUI.Components.StyleManager
144 internal static Theme CurrentTheme
146 get => userTheme ?? baseTheme;
150 UpdateThemeForInitialize();
151 NotifyThemeChanged();
155 internal static bool PlatformThemeEnabled
157 get => platformThemeEnabled;
160 if (platformThemeEnabled == value) return;
162 platformThemeEnabled = value;
164 if (platformThemeEnabled)
166 ApplyExternalPlatformTheme(ExternalThemeManager.CurrentThemeId, ExternalThemeManager.CurrentThemeVersion);
171 internal static bool ApplicationThemeChangeSensitive { get; set; } = false;
174 internal const bool InitialThemeDisabled = true;
176 internal const bool InitialThemeDisabled = false;
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.
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)
189 var newTheme = (Theme)theme?.Clone() ?? throw new ArgumentNullException(nameof(theme));
191 if (string.IsNullOrEmpty(newTheme.Id))
193 newTheme.Id = "NONAME";
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 ?? "");
200 userTheme = newTheme;
201 UpdateThemeForInitialize();
202 UpdateThemeForUpdate();
203 NotifyThemeChanged();
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.
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)
216 var newTheme = (Theme)theme?.Clone() ?? throw new ArgumentNullException(nameof(theme));
218 if (string.IsNullOrEmpty(newTheme.Id))
220 newTheme.Id = "NONAME";
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 ?? "");
227 if (userTheme == null) userTheme = theme;
230 userTheme = (Theme)userTheme.Clone();
231 userTheme.MergeWithoutClone(theme);
234 UpdateThemeForInitialize();
235 UpdateThemeForUpdate();
236 NotifyThemeChanged();
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.
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)
250 if (themeId == null) throw new ArgumentNullException(nameof(themeId));
252 return ExternalThemeManager.SetTheme(themeId);
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).
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)
264 if (styleName == null) throw new ArgumentNullException(nameof(styleName));
265 return GetInitialStyleWithoutClone(styleName)?.Clone();
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.
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)
277 if (viewType == null) throw new ArgumentNullException(nameof(viewType));
278 return GetInitialStyleWithoutClone(viewType)?.Clone();
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" />.
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)
290 if (styleName == null) throw new ArgumentNullException(nameof(styleName));
291 return platformTheme?.GetStyle(styleName)?.Clone();
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" />.
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)
303 if (viewType == null) throw new ArgumentNullException(nameof(viewType));
304 return platformTheme?.GetStyle(viewType)?.Clone();
308 /// Load a style with style name in the current theme.
310 /// <param name="styleName">The style name.</param>
311 internal static ViewStyle GetUpdateStyleWithoutClone(string styleName) => themeForUpdate?.GetStyle(styleName);
314 /// Load a style with View type in the current theme.
316 /// <param name="viewType">The type of View.</param>
317 internal static ViewStyle GetUpdateStyleWithoutClone(Type viewType) => themeForUpdate?.GetStyle(viewType);
320 /// Load a initial component style.
322 internal static ViewStyle GetInitialStyleWithoutClone(string styleName) => themeForInitialize?.GetStyle(styleName);
325 /// Load a initial component style.
327 internal static ViewStyle GetInitialStyleWithoutClone(Type viewType) => themeForInitialize?.GetStyle(viewType);
330 /// Get a platform installed theme.
332 /// <param name="themeId">The theme id.</param>
333 internal static Theme LoadPlatformTheme(string themeId)
335 Debug.Assert(themeId != null);
337 // Check if it is already loaded.
338 int index = cachedPlatformThemes.FindIndex(x => string.Equals(x.Id, themeId, StringComparison.OrdinalIgnoreCase));
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)
346 UpdatePlatformTheme(found);
347 Tizen.Log.Info("NUI", $"Update cache.");
352 var newTheme = CreatePlatformTheme(themeId);
353 if (newTheme != null)
355 cachedPlatformThemes.Add(newTheme);
356 Tizen.Log.Info("NUI", $"Platform theme has been loaded successfully.");
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.
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)
370 Debug.Assert(fallbackTheme != null);
371 BaseTheme = (Theme)fallbackTheme?.Clone();
375 /// Apply an external platform theme.
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)
381 if (InitialThemeDisabled) return;
383 // If the given theme is invalid, do nothing.
384 if (string.IsNullOrEmpty(id))
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))
392 Tizen.Log.Info("NUI", "The base theme can cover platform theme: Skip loading.");
396 // If the given theme is already applied, do nothing.
397 if (platformTheme != null && platformTheme.HasSameIdAndVersion(id, version))
399 Tizen.Log.Info("NUI", "Platform theme is already applied: Skip loading.");
403 var loaded = LoadPlatformTheme(id);
407 Tizen.Log.Info("NUI", $"{loaded.Id} has been applied successfully.");
408 platformTheme = loaded;
409 UpdateThemeForInitialize();
410 UpdateThemeForUpdate();
411 NotifyThemeChanged(true);
415 internal static void AddPackageTheme(IThemeCreator themeCreator)
418 if (InitialThemeDisabled || packages.Contains(packageName = themeCreator.GetType().Assembly.GetName().Name))
423 Tizen.Log.Debug("NUI", $"AddPackageTheme({themeCreator.GetType().Assembly.GetName().Name})");
424 packages.Add(packageName);
427 var packageBaseTheme = themeCreator.Create();
428 Debug.Assert(packageBaseTheme != null);
430 if (baseTheme == null) baseTheme = packageBaseTheme;
431 else baseTheme.MergeWithoutClone(packageBaseTheme);
432 baseTheme.PackageCount++;
434 if (platformThemeEnabled)
436 Tizen.Log.Info("NUI", $"Platform theme is enabled");
437 if (platformTheme != null)
439 UpdatePlatformTheme(platformTheme);
443 if (!string.IsNullOrEmpty(ExternalThemeManager.CurrentThemeId) && !baseTheme.HasSameIdAndVersion(ExternalThemeManager.CurrentThemeId, ExternalThemeManager.CurrentThemeVersion))
445 var loaded = LoadPlatformTheme(ExternalThemeManager.CurrentThemeId);
448 platformTheme = loaded;
452 UpdateThemeForUpdate();
454 UpdateThemeForInitialize();
457 // TODO Please make it private after removing Tizen.NUI.Components.StyleManager.
458 internal static void UpdateThemeForUpdate()
460 if (userTheme == null)
462 themeForUpdate = platformTheme;
466 if (platformTheme == null)
468 themeForUpdate = userTheme;
472 themeForUpdate = new Theme();
473 themeForUpdate.Merge(platformTheme);
474 themeForUpdate.MergeWithoutClone(userTheme);
477 // TODO Please make it private after removing Tizen.NUI.Components.StyleManager.
478 internal static void UpdateThemeForInitialize()
480 if (platformTheme == null && userTheme == null)
482 themeForInitialize = baseTheme;
486 themeForInitialize = new Theme();
488 if (baseTheme != null) themeForInitialize.Merge(baseTheme);
490 if (userTheme == null)
492 if (platformTheme != null) themeForInitialize.MergeWithoutClone(platformTheme);
496 if (platformTheme != null) themeForInitialize.Merge(platformTheme);
497 themeForInitialize.MergeWithoutClone(userTheme);
501 private static void UpdatePlatformTheme(Theme theme)
503 var sharedResourcePath = ExternalThemeManager.GetSharedResourcePath(theme.Id);
505 if (sharedResourcePath == null)
510 for (var i = theme.PackageCount; i < packages.Count; i++)
512 theme.MergeWithoutClone(CreatePlatformTheme(sharedResourcePath, packages[i]));
514 theme.PackageCount = packages.Count;
517 private static Theme CreatePlatformTheme(string id)
519 var sharedResourcePath = ExternalThemeManager.GetSharedResourcePath(id);
521 if (sharedResourcePath == null)
526 var newTheme = new Theme()
531 foreach (var packageName in packages)
533 newTheme.MergeWithoutClone(CreatePlatformTheme(sharedResourcePath, packageName));
535 newTheme.PackageCount = packages.Count;
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)
543 ExternalThemeManager.SharedResourcePath = sharedResourcePath;
546 return new Theme(sharedResourcePath + assemblyName + ".Theme.xaml");
548 catch (System.IO.FileNotFoundException)
550 Tizen.Log.Info("NUI", $"[Ignorable] Current tizen theme does not have NUI theme.");
554 Tizen.Log.Info("NUI", $"[Ignorable] {e.GetType().Name} occurred while applying tizen theme to {assemblyName}: {e.Message}");
560 private static void AddToPlatformThemes(Theme theme)
562 int index = cachedPlatformThemes.FindIndex(x => x.Id.Equals(theme.Id, StringComparison.OrdinalIgnoreCase));
565 Tizen.Log.Info("NUI", $"Existing {theme.Id} item is overwritten");
566 cachedPlatformThemes[index] = theme;
570 cachedPlatformThemes.Add(theme);
571 Tizen.Log.Info("NUI", $"New {theme.Id} is saved.");
575 private static void NotifyThemeChanged(bool platformThemeUpdated = false)
577 if (isInEventProgress) return;
578 isInEventProgress = true;
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));
586 isInEventProgress = false;
589 private static void OnExternalThemeChanged(object sender, EventArgs e)
591 if (!PlatformThemeEnabled)
596 ApplyExternalPlatformTheme(ExternalThemeManager.CurrentThemeId, ExternalThemeManager.CurrentThemeVersion);
600 #pragma warning restore CS0162 // Unreachable code detected