/* * Copyright(c) 2021 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ #if !PROFILE_TV #define ExternalThemeEnabled #endif using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Tizen.NUI.BaseComponents; namespace Tizen.NUI { /// /// This static module provides methods that can manage NUI . /// /// /// To apply custom theme to the application, try . /// /// var customTheme = new Theme(res + "customThemeFile.xaml"); /// ThemeManager.ApplyTheme(customTheme); /// /// /// /// 9 public static class ThemeManager { private static Theme baseTheme; // The base theme. It includes all styles including structures (Size, Position, Policy) of components. private static Theme platformTheme; // The platform theme. This may include color and image information without structure detail. private static Theme userTheme; // The user custom theme. private static Theme themeForUpdate; // platformTheme + userTheme. It is used when the component need to update according to theme change. private static Theme themeForInitialize; // baseTheme + platformTheme + userTheme. It is used when the component is created. private static readonly List cachedPlatformThemes = new List(); // Themes provided by framework. private static readonly List packages = new List();// This is to store base theme creators by packages. private static bool platformThemeEnabled = false; static ThemeManager() { #if ExternalThemeEnabled ExternalThemeManager.Initialize(); #endif AddPackageTheme(DefaultThemeCreator.Instance); } /// /// An event invoked after the theme has changed by . /// /// 9 public static event EventHandler ThemeChanged; /// /// Internal one should be called before calling public ThemeChanged /// internal static WeakEvent> ThemeChangedInternal = new WeakEvent>(); /// /// The current theme Id. /// It returns null when no theme is applied. /// [EditorBrowsable(EditorBrowsableState.Never)] public static string ThemeId { get => userTheme?.Id; } /// /// The current platform theme Id. /// Note that it returns null when the platform theme is disabled. /// If the is given, it can be one of followings in tizen 6.5: /// /// /// org.tizen.default-light-theme /// /// /// org.tizen.default-dark-theme /// /// /// [EditorBrowsable(EditorBrowsableState.Never)] public static string PlatformThemeId { get => platformTheme?.Id ?? (platformThemeEnabled ? baseTheme.Id : null); } /// /// To support deprecated StyleManager. /// NOTE that, please remove this after remove Tizen.NUI.Components.StyleManager /// internal static Theme BaseTheme { get => baseTheme; set { baseTheme = value; UpdateThemeForInitialize(); } } /// /// To support deprecated StyleManager. /// NOTE that, please remove this after remove Tizen.NUI.Components.StyleManager /// internal static Theme CurrentTheme { get => userTheme ?? baseTheme; set { userTheme = value; UpdateThemeForInitialize(); NotifyThemeChanged(); } } internal static bool PlatformThemeEnabled { get => platformThemeEnabled; set { if (platformThemeEnabled == value) return; platformThemeEnabled = value; if (platformThemeEnabled) { ApplyExternalPlatformTheme(ExternalThemeManager.CurrentThemeId, ExternalThemeManager.CurrentThemeVersion); } } } internal static bool ApplicationThemeChangeSensitive { get; set; } = false; /// /// Apply custom theme to the NUI. /// This will change the appearance of the existing components with property on. /// This also affects all components created afterwards. /// /// The theme instance to be applied. /// Thrown when the given theme is null. /// 9 public static void ApplyTheme(Theme theme) { var newTheme = (Theme)theme?.Clone() ?? throw new ArgumentNullException(nameof(theme)); if (string.IsNullOrEmpty(newTheme.Id)) { newTheme.Id = "NONAME"; } StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Small, newTheme.SmallBrokenImageUrl ?? ""); StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Normal, newTheme.BrokenImageUrl ?? ""); StyleManager.Instance.SetBrokenImageUrl(StyleManager.BrokenImageType.Large, newTheme.LargeBrokenImageUrl ?? ""); userTheme = newTheme; UpdateThemeForInitialize(); UpdateThemeForUpdate(); NotifyThemeChanged(); } /// /// Change tizen theme. /// User may change this to one of platform installed one. /// /// The installed theme Id. /// true on success, false when it failed to find installed theme with given themeId. /// Thrown when the given themeId is null. [EditorBrowsable(EditorBrowsableState.Never)] public static bool ApplyPlatformTheme(string themeId) { if (themeId == null) throw new ArgumentNullException(nameof(themeId)); return ExternalThemeManager.SetTheme(themeId); } /// /// Load a style with style name in the current theme. /// For components, the default style name of a component is a component name with namespace (e.g. Tizen.NUI.Components.Button). /// /// The style name. /// Thrown when the given styleName is null. /// 9 public static ViewStyle GetStyle(string styleName) { if (styleName == null) throw new ArgumentNullException(nameof(styleName)); return GetInitialStyleWithoutClone(styleName)?.Clone(); } /// /// Load a style with view type in the current theme. /// If it failed to find a style with the given type, it will try with it's parent type until it succeeds. /// /// 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) /// Thrown when the given viewType is null. /// 9 public static ViewStyle GetStyle(Type viewType) { if (viewType == null) throw new ArgumentNullException(nameof(viewType)); return GetInitialStyleWithoutClone(viewType)?.Clone(); } /// /// Load a platform style with style name in the current theme. /// It returns null when the platform theme is disabled. . /// /// The style name. /// Thrown when the given styleName is null. [EditorBrowsable(EditorBrowsableState.Never)] public static ViewStyle GetPlatformStyle(string styleName) { if (styleName == null) throw new ArgumentNullException(nameof(styleName)); return platformTheme?.GetStyle(styleName)?.Clone(); } /// /// Load a platform style with view type in the current theme. /// It returns null when the platform theme is disabled. . /// /// 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) /// Thrown when the given viewType is null. [EditorBrowsable(EditorBrowsableState.Never)] public static ViewStyle GetPlatformStyle(Type viewType) { if (viewType == null) throw new ArgumentNullException(nameof(viewType)); return platformTheme?.GetStyle(viewType)?.Clone(); } /// /// Load a style with style name in the current theme. /// /// The style name. internal static ViewStyle GetUpdateStyleWithoutClone(string styleName) => themeForUpdate?.GetStyle(styleName); /// /// Load a style with View type in the current theme. /// /// The type of View. internal static ViewStyle GetUpdateStyleWithoutClone(Type viewType) => themeForUpdate?.GetStyle(viewType); /// /// Load a initial component style. /// internal static ViewStyle GetInitialStyleWithoutClone(string styleName) => themeForInitialize.GetStyle(styleName); /// /// Load a initial component style. /// internal static ViewStyle GetInitialStyleWithoutClone(Type viewType) => themeForInitialize.GetStyle(viewType); /// /// Get a platform installed theme. /// /// The theme id. internal static Theme LoadPlatformTheme(string themeId) { Debug.Assert(themeId != null); // Check if it is already loaded. int index = cachedPlatformThemes.FindIndex(x => string.Equals(x.Id, themeId, StringComparison.OrdinalIgnoreCase)); if (index >= 0) { Tizen.Log.Info("NUI", $"Hit cache."); var found = cachedPlatformThemes[index]; // If the cached is not a full set, update it. if (found.PackageCount < packages.Count) { UpdatePlatformTheme(found); Tizen.Log.Info("NUI", $"Update cache."); } return found; } var newTheme = CreatePlatformTheme(themeId); if (newTheme != null) { cachedPlatformThemes.Add(newTheme); Tizen.Log.Info("NUI", $"Platform theme has been loaded successfully."); } return newTheme; } /// /// !!! This is for internal use in fhub-nui. Do not open it. /// Set a theme to be used as fallback. /// The fallback theme is set to profile specified theme by default. /// /// The theme instance to be applied as a fallback. [EditorBrowsable(EditorBrowsableState.Never)] internal static void ApplyFallbackTheme(Theme fallbackTheme) { Debug.Assert(fallbackTheme != null); BaseTheme = (Theme)fallbackTheme?.Clone(); } /// /// Apply an external platform theme. /// /// The external theme id. /// The external theme version. internal static void ApplyExternalPlatformTheme(string id, string version) { #if ExternalThemeEnabled Debug.Assert(baseTheme != null); // If the given theme is invalid, do nothing. if (string.IsNullOrEmpty(id)) { return; } // If no platform theme has been applied and the base theme can cover the given one, do nothing. if (platformTheme == null && baseTheme.HasSameIdAndVersion(id, version)) { Tizen.Log.Info("NUI", "The base theme can cover platform theme: Skip loading."); return; } // If the given theme is already applied, do nothing. if (platformTheme != null && platformTheme.HasSameIdAndVersion(id, version)) { Tizen.Log.Info("NUI", "Platform theme is already applied: Skip loading."); return; } var loaded = LoadPlatformTheme(id); if (loaded != null) { Tizen.Log.Info("NUI", $"{loaded.Id} has been applied successfully."); platformTheme = loaded; UpdateThemeForInitialize(); UpdateThemeForUpdate(); NotifyThemeChanged(true); } #endif } internal static void AddPackageTheme(IThemeCreator themeCreator) { if (packages.Contains(themeCreator)) { return; } Tizen.Log.Info("NUI", $"AddPackageTheme({themeCreator.GetType().Assembly.GetName().Name})"); packages.Add(themeCreator); // Base theme var packageBaseTheme = themeCreator.Create(); Debug.Assert(packageBaseTheme != null); if (baseTheme == null) baseTheme = packageBaseTheme; else baseTheme.MergeWithoutClone(packageBaseTheme); baseTheme.PackageCount++; #if ExternalThemeEnabled if (platformThemeEnabled) { Tizen.Log.Info("NUI", $"Platform theme is enabled"); if (platformTheme != null) { UpdatePlatformTheme(platformTheme); } else { if (!string.IsNullOrEmpty(ExternalThemeManager.CurrentThemeId) && !baseTheme.HasSameIdAndVersion(ExternalThemeManager.CurrentThemeId, ExternalThemeManager.CurrentThemeVersion)) { var loaded = LoadPlatformTheme(ExternalThemeManager.CurrentThemeId); if (loaded != null) { platformTheme = loaded; } } } UpdateThemeForUpdate(); } #endif UpdateThemeForInitialize(); } internal static void Preload() { #if ExternalThemeEnabled Debug.Assert(baseTheme != null); if (string.IsNullOrEmpty(ExternalThemeManager.CurrentThemeId)) return; LoadPlatformTheme(ExternalThemeManager.CurrentThemeId); #endif } // TODO Please make it private after removing Tizen.NUI.Components.StyleManager. internal static void UpdateThemeForUpdate() { if (userTheme == null) { themeForUpdate = platformTheme; return; } if (platformTheme == null) { themeForUpdate = userTheme; return; } themeForUpdate = new Theme(); themeForUpdate.Merge(platformTheme); themeForUpdate.MergeWithoutClone(userTheme); } // TODO Please make it private after removing Tizen.NUI.Components.StyleManager. internal static void UpdateThemeForInitialize() { if (platformTheme == null && userTheme == null) { themeForInitialize = baseTheme; return; } themeForInitialize = new Theme(); themeForInitialize.Merge(baseTheme); if (userTheme == null) { if (platformTheme != null) themeForInitialize.MergeWithoutClone(platformTheme); } else { if (platformTheme != null) themeForInitialize.Merge(platformTheme); themeForInitialize.MergeWithoutClone(userTheme); } } private static void UpdatePlatformTheme(Theme theme) { var sharedResourcePath = ExternalThemeManager.GetSharedResourcePath(theme.Id); if (sharedResourcePath == null) { return; } for (var i = theme.PackageCount; i < packages.Count; i++) { theme.MergeWithoutClone(CreatePlatformTheme(sharedResourcePath, packages[i].GetType().Assembly.GetName().Name)); } theme.PackageCount = packages.Count; } private static Theme CreatePlatformTheme(string id) { var sharedResourcePath = ExternalThemeManager.GetSharedResourcePath(id); if (sharedResourcePath == null) { return null; } var newTheme = new Theme() { Id = id }; foreach (var packageCreator in packages) { newTheme.MergeWithoutClone(CreatePlatformTheme(sharedResourcePath, packageCreator.GetType().Assembly.GetName().Name)); } newTheme.PackageCount = packages.Count; return newTheme; } [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.")] private static Theme CreatePlatformTheme(string sharedResourcePath, string assemblyName) { ExternalThemeManager.SharedResourcePath = sharedResourcePath; try { return new Theme(sharedResourcePath + assemblyName + ".Theme.xaml"); } catch (System.IO.FileNotFoundException) { Tizen.Log.Info("NUI", $"[Ignorable] Current tizen theme does not have NUI theme."); } catch (Exception e) { Tizen.Log.Info("NUI", $"[Ignorable] {e.GetType().Name} occurred while applying tizen theme to {assemblyName}: {e.Message}"); } return new Theme(); } private static void AddToPlatformThemes(Theme theme) { int index = cachedPlatformThemes.FindIndex(x => x.Id.Equals(theme.Id, StringComparison.OrdinalIgnoreCase)); if (index >= 0) { Tizen.Log.Info("NUI", $"Existing {theme.Id} item is overwritten"); cachedPlatformThemes[index] = theme; } else { cachedPlatformThemes.Add(theme); Tizen.Log.Info("NUI", $"New {theme.Id} is saved."); } } private static void NotifyThemeChanged(bool platformThemeUpdated = false) { Debug.Assert(baseTheme != null); var platformThemeId = PlatformThemeId; var userThemeId = userTheme?.Id; ThemeChangedInternal.Invoke(null, new ThemeChangedEventArgs(userThemeId, platformThemeId, platformThemeUpdated)); ThemeChanged?.Invoke(null, new ThemeChangedEventArgs(userThemeId, platformThemeId, platformThemeUpdated)); } } }