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 AddPackageTheme(DefaultThemeCreator.Instance);
72 /// An event invoked when the theme is about to change (not applied to the views yet).
74 [EditorBrowsable(EditorBrowsableState.Never)]
75 public static event EventHandler<ThemeChangedEventArgs> ThemeChanging;
78 /// An event invoked after the theme has changed by <see cref="ApplyTheme(Theme)"/>.
80 /// <since_tizen> 9 </since_tizen>
81 public static event EventHandler<ThemeChangedEventArgs> ThemeChanged;
84 /// Internal one should be called before calling public ThemeChanged
86 internal static WeakEvent<EventHandler<ThemeChangedEventArgs>> ThemeChangedInternal = new WeakEvent<EventHandler<ThemeChangedEventArgs>>();
89 /// The current theme Id.
90 /// It returns null when no theme is applied.
92 [EditorBrowsable(EditorBrowsableState.Never)]
93 public static string ThemeId
99 /// The current platform theme Id.
100 /// Note that it returns null when the platform theme is disabled.
101 /// If the <seealso cref="NUIApplication.ThemeOptions.PlatformThemeEnabled"/> is given, it can be one of followings in tizen 6.5:
102 /// <list type="bullet">
104 /// <description>org.tizen.default-light-theme</description>
107 /// <description>org.tizen.default-dark-theme</description>
111 [EditorBrowsable(EditorBrowsableState.Never)]
112 public static string PlatformThemeId
114 get => platformTheme?.Id ?? (platformThemeEnabled ? baseTheme?.Id : null);
118 /// To support deprecated StyleManager.
119 /// NOTE that, please remove this after remove Tizen.NUI.Components.StyleManager
121 internal static Theme BaseTheme
125 if (baseTheme == null)
127 baseTheme = new Theme();
128 UpdateThemeForInitialize();
135 UpdateThemeForInitialize();
140 /// To support deprecated StyleManager.
141 /// NOTE that, please remove this after remove Tizen.NUI.Components.StyleManager
143 internal static Theme CurrentTheme
145 get => userTheme ?? baseTheme;
149 UpdateThemeForInitialize();
150 NotifyThemeChanged();
154 internal static bool PlatformThemeEnabled
156 get => platformThemeEnabled;
159 if (platformThemeEnabled == value) return;
161 platformThemeEnabled = value;
163 if (platformThemeEnabled)
165 ApplyExternalPlatformTheme(ExternalThemeManager.CurrentThemeId, ExternalThemeManager.CurrentThemeVersion);
170 internal static bool ApplicationThemeChangeSensitive { get; set; } = false;
173 internal const bool InitialThemeDisabled = true;
175 internal const bool InitialThemeDisabled = false;
179 /// Apply custom theme to the NUI.
180 /// This will change the appearance of the existing components with property <seealso cref="View.ThemeChangeSensitive"/> on.
181 /// This also affects all components created afterwards.
183 /// <param name="theme">The theme instance to be applied.</param>
184 /// <exception cref="ArgumentNullException">Thrown when the given theme is null.</exception>
185 /// <since_tizen> 9 </since_tizen>
186 public static void ApplyTheme(Theme theme)
188 var newTheme = (Theme)theme?.Clone() ?? throw new ArgumentNullException(nameof(theme));
190 if (string.IsNullOrEmpty(newTheme.Id))
192 newTheme.Id = "NONAME";
195 if (newTheme.SmallBrokenImageUrl != null) StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Small, newTheme.SmallBrokenImageUrl);
196 if (newTheme.BrokenImageUrl != null) StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Normal, newTheme.BrokenImageUrl);
197 if (newTheme.LargeBrokenImageUrl != null) StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Large, newTheme.LargeBrokenImageUrl);
199 userTheme = newTheme;
200 UpdateThemeForInitialize();
201 UpdateThemeForUpdate();
202 NotifyThemeChanged();
206 /// Append a theme to the current theme and apply it.
207 /// This will change the appearance of the existing components with property <seealso cref="View.ThemeChangeSensitive"/> on.
208 /// This also affects all components created afterwards.
210 /// <param name="theme">The theme instance to be appended.</param>
211 /// <exception cref="ArgumentNullException">Thrown when the given theme is null.</exception>
212 [EditorBrowsable(EditorBrowsableState.Never)]
213 public static void AppendTheme(Theme theme)
215 var newTheme = (Theme)theme?.Clone() ?? throw new ArgumentNullException(nameof(theme));
217 if (string.IsNullOrEmpty(newTheme.Id))
219 newTheme.Id = "NONAME";
222 if (newTheme.SmallBrokenImageUrl != null) StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Small, newTheme.SmallBrokenImageUrl);
223 if (newTheme.BrokenImageUrl != null) StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Normal, newTheme.BrokenImageUrl);
224 if (newTheme.LargeBrokenImageUrl != null) StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Large, newTheme.LargeBrokenImageUrl);
226 if (userTheme == null) userTheme = newTheme;
229 userTheme = (Theme)userTheme.Clone();
230 userTheme.MergeWithoutClone(newTheme);
233 UpdateThemeForInitialize();
234 UpdateThemeForUpdate();
235 NotifyThemeChanged();
239 /// Append a theme to the current base theme and apply it.
240 /// This will change the appearance of the existing components with property <seealso cref="View.ThemeChangeSensitive"/> on.
241 /// This also affects all components created afterwards.
243 /// <param name="theme">The theme instance to be appended.</param>
244 /// <exception cref="ArgumentNullException">Thrown when the given theme is null.</exception>
245 [EditorBrowsable(EditorBrowsableState.Never)]
246 public static void AppendBaseTheme(Theme theme)
248 var newTheme = (Theme)theme?.Clone() ?? throw new ArgumentNullException(nameof(theme));
250 if (string.IsNullOrEmpty(newTheme.Id))
252 newTheme.Id = "NONAME";
255 if (newTheme.SmallBrokenImageUrl != null) StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Small, newTheme.SmallBrokenImageUrl);
256 if (newTheme.BrokenImageUrl != null) StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Normal, newTheme.BrokenImageUrl);
257 if (newTheme.LargeBrokenImageUrl != null) StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Large, newTheme.LargeBrokenImageUrl);
259 if (baseTheme == null) baseTheme = newTheme;
262 baseTheme = (Theme)baseTheme.Clone();
263 baseTheme.MergeWithoutClone(newTheme);
266 UpdateThemeForInitialize();
267 NotifyThemeChanged();
271 /// Change tizen theme.
272 /// User may change this to one of platform installed one.
273 /// Note that this is global theme changing which effects all applications.
275 /// <param name="themeId">The installed theme Id.</param>
276 /// <returns>true on success, false when it failed to find installed theme with given themeId.</returns>
277 /// <exception cref="ArgumentNullException">Thrown when the given themeId is null.</exception>
278 [EditorBrowsable(EditorBrowsableState.Never)]
279 public static bool ApplyPlatformTheme(string themeId)
281 if (themeId == null) throw new ArgumentNullException(nameof(themeId));
283 return ExternalThemeManager.SetTheme(themeId);
287 /// Load a style with style name in the current theme.
288 /// For components, the default style name of a component is a component name with namespace (e.g. Tizen.NUI.Components.Button).
290 /// <param name="styleName">The style name.</param>
291 /// <exception cref="ArgumentNullException">Thrown when the given styleName is null.</exception>
292 /// <since_tizen> 9 </since_tizen>
293 public static ViewStyle GetStyle(string styleName)
295 if (styleName == null) throw new ArgumentNullException(nameof(styleName));
296 return GetInitialStyleWithoutClone(styleName)?.Clone();
300 /// Load a style with view type in the current theme.
301 /// If it failed to find a style with the given type, it will try with it's parent type until it succeeds.
303 /// <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>
304 /// <exception cref="ArgumentNullException">Thrown when the given viewType is null.</exception>
305 /// <since_tizen> 9 </since_tizen>
306 public static ViewStyle GetStyle(Type viewType)
308 if (viewType == null) throw new ArgumentNullException(nameof(viewType));
309 return GetInitialStyleWithoutClone(viewType)?.Clone();
313 /// Load a platform style with style name in the current theme.
314 /// It returns null when the platform theme is disabled. <see cref="NUIApplication.ThemeOptions.PlatformThemeEnabled" />.
316 /// <param name="styleName">The style name.</param>
317 /// <exception cref="ArgumentNullException">Thrown when the given styleName is null.</exception>
318 [EditorBrowsable(EditorBrowsableState.Never)]
319 public static ViewStyle GetPlatformStyle(string styleName)
321 if (styleName == null) throw new ArgumentNullException(nameof(styleName));
322 return platformTheme?.GetStyle(styleName)?.Clone();
326 /// Load a platform style with view type in the current theme.
327 /// It returns null when the platform theme is disabled. <see cref="NUIApplication.ThemeOptions.PlatformThemeEnabled" />.
329 /// <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>
330 /// <exception cref="ArgumentNullException">Thrown when the given viewType is null.</exception>
331 [EditorBrowsable(EditorBrowsableState.Never)]
332 public static ViewStyle GetPlatformStyle(Type viewType)
334 if (viewType == null) throw new ArgumentNullException(nameof(viewType));
335 return platformTheme?.GetStyle(viewType)?.Clone();
339 /// Load a style with style name in the current theme.
341 /// <param name="styleName">The style name.</param>
342 internal static ViewStyle GetUpdateStyleWithoutClone(string styleName) => themeForUpdate?.GetStyle(styleName);
345 /// Load a style with View type in the current theme.
347 /// <param name="viewType">The type of View.</param>
348 internal static ViewStyle GetUpdateStyleWithoutClone(Type viewType) => themeForUpdate?.GetStyle(viewType);
351 /// Load a initial component style.
353 internal static ViewStyle GetInitialStyleWithoutClone(string styleName) => themeForInitialize?.GetStyle(styleName);
356 /// Load a initial component style.
358 internal static ViewStyle GetInitialStyleWithoutClone(Type viewType) => themeForInitialize?.GetStyle(viewType);
361 /// Get a platform installed theme.
363 /// <param name="themeId">The theme id.</param>
364 internal static Theme LoadPlatformTheme(string themeId)
366 Debug.Assert(themeId != null);
368 // Check if it is already loaded.
369 int index = cachedPlatformThemes.FindIndex(x => string.Equals(x.Id, themeId, StringComparison.OrdinalIgnoreCase));
372 Tizen.Log.Info("NUI", $"Hit cache.");
373 var found = cachedPlatformThemes[index];
374 // If the cached is not a full set, update it.
375 if (found.PackageCount < packages.Count)
377 UpdatePlatformTheme(found);
378 Tizen.Log.Info("NUI", $"Update cache.");
383 var newTheme = CreatePlatformTheme(themeId);
384 if (newTheme != null)
386 cachedPlatformThemes.Add(newTheme);
387 Tizen.Log.Info("NUI", $"Platform theme has been loaded successfully.");
393 /// !!! This is for internal use in fhub-nui. Do not open it.
394 /// Set a theme to be used as fallback.
395 /// The fallback theme is set to profile specified theme by default.
397 /// <param name="fallbackTheme">The theme instance to be applied as a fallback.</param>
398 [EditorBrowsable(EditorBrowsableState.Never)]
399 internal static void ApplyFallbackTheme(Theme fallbackTheme)
401 Debug.Assert(fallbackTheme != null);
402 BaseTheme = (Theme)fallbackTheme?.Clone();
406 /// Apply an external platform theme.
408 /// <param name="id">The external theme id.</param>
409 /// <param name="version">The external theme version.</param>
410 internal static void ApplyExternalPlatformTheme(string id, string version)
412 if (InitialThemeDisabled) return;
414 // If the given theme is invalid, do nothing.
415 if (string.IsNullOrEmpty(id))
420 // If no platform theme has been applied and the base theme can cover the given one, do nothing.
421 if (platformTheme == null && baseTheme != null && baseTheme.HasSameIdAndVersion(id, version))
423 Tizen.Log.Info("NUI", "The base theme can cover platform theme: Skip loading.");
427 // If the given theme is already applied, do nothing.
428 if (platformTheme != null && platformTheme.HasSameIdAndVersion(id, version))
430 Tizen.Log.Info("NUI", "Platform theme is already applied: Skip loading.");
434 var loaded = LoadPlatformTheme(id);
438 Tizen.Log.Info("NUI", $"{loaded.Id} has been applied successfully.");
439 platformTheme = loaded;
440 UpdateThemeForInitialize();
441 UpdateThemeForUpdate();
442 NotifyThemeChanged(true);
446 internal static void AddPackageTheme(IThemeCreator themeCreator)
449 if (InitialThemeDisabled || packages.Contains(packageName = themeCreator.GetType().Assembly.GetName().Name))
454 Tizen.Log.Debug("NUI", $"AddPackageTheme({themeCreator.GetType().Assembly.GetName().Name})");
455 packages.Add(packageName);
458 var packageBaseTheme = themeCreator.Create();
459 Debug.Assert(packageBaseTheme != null);
461 if (baseTheme == null) baseTheme = packageBaseTheme;
462 else baseTheme.MergeWithoutClone(packageBaseTheme);
463 baseTheme.PackageCount++;
465 if (platformThemeEnabled)
467 Tizen.Log.Info("NUI", $"Platform theme is enabled");
468 if (platformTheme != null)
470 UpdatePlatformTheme(platformTheme);
474 if (!string.IsNullOrEmpty(ExternalThemeManager.CurrentThemeId) && !baseTheme.HasSameIdAndVersion(ExternalThemeManager.CurrentThemeId, ExternalThemeManager.CurrentThemeVersion))
476 var loaded = LoadPlatformTheme(ExternalThemeManager.CurrentThemeId);
479 platformTheme = loaded;
483 UpdateThemeForUpdate();
485 UpdateThemeForInitialize();
488 // TODO Please make it private after removing Tizen.NUI.Components.StyleManager.
489 internal static void UpdateThemeForUpdate()
491 if (userTheme == null)
493 themeForUpdate = platformTheme;
497 if (platformTheme == null)
499 themeForUpdate = userTheme;
503 themeForUpdate = new Theme();
504 themeForUpdate.Merge(platformTheme);
505 themeForUpdate.MergeWithoutClone(userTheme);
508 // TODO Please make it private after removing Tizen.NUI.Components.StyleManager.
509 internal static void UpdateThemeForInitialize()
511 if (platformTheme == null && userTheme == null)
513 themeForInitialize = baseTheme;
517 themeForInitialize = new Theme();
519 if (baseTheme != null) themeForInitialize.Merge(baseTheme);
521 if (userTheme == null)
523 if (platformTheme != null) themeForInitialize.MergeWithoutClone(platformTheme);
527 if (platformTheme != null) themeForInitialize.Merge(platformTheme);
528 themeForInitialize.MergeWithoutClone(userTheme);
532 private static void UpdatePlatformTheme(Theme theme)
534 var sharedResourcePath = ExternalThemeManager.GetSharedResourcePath(theme.Id);
536 if (sharedResourcePath == null)
541 for (var i = theme.PackageCount; i < packages.Count; i++)
543 theme.MergeWithoutClone(CreatePlatformTheme(sharedResourcePath, packages[i]));
545 theme.PackageCount = packages.Count;
548 private static Theme CreatePlatformTheme(string id)
550 var sharedResourcePath = ExternalThemeManager.GetSharedResourcePath(id);
552 if (sharedResourcePath == null)
557 var newTheme = new Theme()
562 foreach (var packageName in packages)
564 newTheme.MergeWithoutClone(CreatePlatformTheme(sharedResourcePath, packageName));
566 newTheme.PackageCount = packages.Count;
571 [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.")]
572 private static Theme CreatePlatformTheme(string sharedResourcePath, string assemblyName)
574 ExternalThemeManager.SharedResourcePath = sharedResourcePath;
577 return new Theme(sharedResourcePath + assemblyName + ".Theme.xaml");
579 catch (System.IO.FileNotFoundException)
581 Tizen.Log.Info("NUI", $"[Ignorable] Current tizen theme does not have NUI theme.");
585 Tizen.Log.Info("NUI", $"[Ignorable] {e.GetType().Name} occurred while applying tizen theme to {assemblyName}: {e.Message}");
591 private static void AddToPlatformThemes(Theme theme)
593 int index = cachedPlatformThemes.FindIndex(x => x.Id.Equals(theme.Id, StringComparison.OrdinalIgnoreCase));
596 Tizen.Log.Info("NUI", $"Existing {theme.Id} item is overwritten");
597 cachedPlatformThemes[index] = theme;
601 cachedPlatformThemes.Add(theme);
602 Tizen.Log.Info("NUI", $"New {theme.Id} is saved.");
606 private static void NotifyThemeChanged(bool platformThemeUpdated = false)
608 if (isInEventProgress) return;
609 isInEventProgress = true;
611 var platformThemeId = PlatformThemeId;
612 var userThemeId = userTheme?.Id;
613 ThemeChanging?.Invoke(null, new ThemeChangedEventArgs(userThemeId, platformThemeId, platformThemeUpdated));
614 ThemeChangedInternal.Invoke(null, new ThemeChangedEventArgs(userThemeId, platformThemeId, platformThemeUpdated));
615 ThemeChanged?.Invoke(null, new ThemeChangedEventArgs(userThemeId, platformThemeId, platformThemeUpdated));
617 isInEventProgress = false;
621 #pragma warning restore CS0162 // Unreachable code detected