/* * 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. * */ using System; using System.Collections.Generic; using System.ComponentModel; using System.Reflection; using System.Xml; using Tizen.NUI.BaseComponents; using Tizen.NUI.Binding; using Tizen.NUI.Xaml; namespace Tizen.NUI { /// /// /// Basically, the Theme is a dictionary of s that can decorate NUI s. /// Each ViewStyle item is identified by a string key that can be matched the . /// /// /// The main purpose of providing Theme is to separate style details from the structure. /// Managing style separately makes it easier to customize the look of application by user context. /// Also since a Theme can be created from xaml file, it can be treated as a resource. /// This enables sharing styles with other applications. /// /// [EditorBrowsable(EditorBrowsableState.Never)] public class Theme : BindableObject { private readonly Dictionary map; private IEnumerable> changedResources = null; private string baseTheme; ResourceDictionary resources; /// Create an empty theme. [EditorBrowsable(EditorBrowsableState.Never)] public Theme() { map = new Dictionary(); } /// Create a new theme from the xaml file. /// An absolute path to the xaml file. /// Thrown when the given xamlFile is null or empty string. /// Thrown when there are file IO problems. /// Thrown when the content of the xaml file is not valid xaml form. [EditorBrowsable(EditorBrowsableState.Never)] public Theme(string xamlFile) : this() { if (string.IsNullOrEmpty(xamlFile)) { throw new ArgumentNullException(nameof(xamlFile), "The xaml file path cannot be null or empty string"); } try { using (var reader = XmlReader.Create(xamlFile)) { XamlLoader.Load(this, reader); } } catch (System.IO.IOException) { Tizen.Log.Error("NUI", $"Could not load \"{xamlFile}\".\n"); throw; } catch (Exception) { Tizen.Log.Error("NUI", $"Could not parse \"{xamlFile}\".\n"); Tizen.Log.Error("NUI", "Make sure the all used assemblies (e.g. Tizen.NUI.Components) are included in the application project.\n"); Tizen.Log.Error("NUI", "Make sure the type and namespace are correct.\n"); throw; } } /// /// The string key to identify the Theme. /// [EditorBrowsable(EditorBrowsableState.Never)] public string Id { get; set; } /// /// The string key to identify the Theme. /// [EditorBrowsable(EditorBrowsableState.Never)] public string Version { get; set; } = null; /// /// For Xaml use only. /// The bulit-in theme id that will be used as base of this. /// View styles with same key are merged. /// internal string BasedOn { get => baseTheme; set { baseTheme = value; if (string.IsNullOrEmpty(baseTheme)) return; var baseThemeInstance = (Theme)ThemeManager.GetBuiltinTheme(baseTheme)?.Clone(); if (baseThemeInstance != null) { foreach (var item in baseThemeInstance) { var baseStyle = item.Value?.Clone(); if (map.ContainsKey(item.Key)) { baseStyle.Merge(map[item.Key]); } map[item.Key] = baseStyle; } } } } /// [EditorBrowsable(EditorBrowsableState.Never)] public bool IsResourcesCreated => resources != null; /// [EditorBrowsable(EditorBrowsableState.Never)] internal ResourceDictionary Resources { get { if (resources != null) return resources; resources = new ResourceDictionary(); ((IResourceDictionary)resources).ValuesChanged += OnThemeResourcesChanged; return resources; } set { if (resources == value) return; if (resources != null) { ((IResourceDictionary)resources).ValuesChanged -= OnThemeResourcesChanged; } resources = value; if (resources != null) { // This callback will be removed when Resource.Source is assigned. ((IResourceDictionary)resources).ValuesChanged += OnThemeResourcesChanged; } } } /// /// For Xaml use only. /// Note that it is not a normal indexer. /// Setter will merge the new value with existing item. /// internal ViewStyle this[string styleName] { get => map[styleName]; set { if (value == null) { map.Remove(styleName); return; } if (map.TryGetValue(styleName, out ViewStyle style) && style != null && style.GetType() == value.GetType()) { style.Merge(value); } else { map[styleName] = value; } } } internal int Count => map.Count; internal int PackageCount { get; set; } = 0; /// /// Get an enumerator of the theme. /// [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerator> GetEnumerator() => map.GetEnumerator(); /// /// Removes all styles in the theme. /// [EditorBrowsable(EditorBrowsableState.Never)] public void Clear() => map.Clear(); /// /// Determines whether the theme contains the specified style name. /// /// The given style name is null. [EditorBrowsable(EditorBrowsableState.Never)] public bool HasStyle(string styleName) => map.ContainsKey(styleName); /// /// Removes the style with the specified style name. /// /// The given style name is null. [EditorBrowsable(EditorBrowsableState.Never)] public bool RemoveStyle(string styleName) => map.Remove(styleName); /// /// Gets a style of given style name. /// /// The string key to find a ViewStyle. /// Founded style instance. [EditorBrowsable(EditorBrowsableState.Never)] public ViewStyle GetStyle(string styleName) { map.TryGetValue(styleName, out ViewStyle result); return result; } /// /// Gets a style of given view type. /// /// The type of View. /// Founded style instance. /// Thrown when the given viewType is null. [EditorBrowsable(EditorBrowsableState.Never)] public ViewStyle GetStyle(Type viewType) { var currentType = viewType ?? throw new ArgumentNullException(nameof(viewType)); ViewStyle resultStyle = null; do { if (currentType.Equals(typeof(View))) break; resultStyle = GetStyle(currentType.FullName); currentType = currentType.BaseType; } while (resultStyle == null && currentType != null); return resultStyle; } /// /// Adds the specified style name and value to the theme. /// This replace existing value if the theme already has a style with given name. /// /// The style name to add. /// The style instance to add. [EditorBrowsable(EditorBrowsableState.Never)] public void AddStyle(string styleName, ViewStyle value) => map[styleName] = value?.Clone(); /// [EditorBrowsable(EditorBrowsableState.Never)] public object Clone() { var result = new Theme() { Id = this.Id, Resources = Resources }; foreach (var item in this) { result.AddStyle(item.Key, item.Value); } return result; } /// Merge other Theme into this. /// An absolute path to the xaml file of the theme. /// Thrown when the given xamlFile is null or empty string. /// Thrown when there are file IO problems. /// Thrown when the content of the xaml file is not valid xaml form. [EditorBrowsable(EditorBrowsableState.Never)] public void Merge(string xamlFile) { MergeWithoutClone(new Theme(xamlFile)); } /// Merge other Theme into this. /// The Theme. [EditorBrowsable(EditorBrowsableState.Never)] public void Merge(Theme theme) { if (theme == null) throw new ArgumentNullException(nameof(theme)); if (Id == null) Id = theme.Id; foreach (var item in theme) { if (item.Value == null) { map[item.Key] = null; } else if (map.ContainsKey(item.Key) && !item.Value.SolidNull) { map[item.Key].Merge(item.Value); } else { map[item.Key] = item.Value.Clone(); } } } internal void MergeWithoutClone(Theme theme) { if (theme == null) throw new ArgumentNullException(nameof(theme)); if (Id == null) { Id = theme.Id; } if (Version == null) { Version = theme.Version; } foreach (var item in theme) { if (item.Value == null) { map[item.Key] = null; } else if (map.ContainsKey(item.Key) && !item.Value.SolidNull) { map[item.Key].Merge(item.Value); } else { map[item.Key] = item.Value; } } if (theme.resources != null) { foreach (var res in theme.resources) { Resources[res.Key] = res.Value; } } } /// /// Internal use only. /// internal void AddStyleWithoutClone(string styleName, ViewStyle value) => map[styleName] = value; internal void ApplyExternalTheme(IExternalTheme externalTheme, HashSet keyListSet) { Id = externalTheme.Id; Version = externalTheme.Version; if (keyListSet == null) { // Nothing to apply return; } foreach (var keyList in keyListSet) { keyList?.ApplyKeyActions(externalTheme, this); } } internal bool HasSameIdAndVersion(IExternalTheme externalTheme) { if (externalTheme == null) { return false; } return string.Equals(Id, externalTheme.Id, StringComparison.OrdinalIgnoreCase) && string.Equals(Version, externalTheme.Version, StringComparison.OrdinalIgnoreCase); } internal void SetChangedResources(IEnumerable> changedResources) { this.changedResources = changedResources; } internal void OnThemeResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnThemeResourcesChanged(); internal void OnThemeResourcesChanged() { if (changedResources != null) { // To avoid loop in infinite, remove OnThemeResourcesChanged callback. ((IResourceDictionary)resources).ValuesChanged -= OnThemeResourcesChanged; foreach (var changedResource in changedResources) { if (resources.TryGetValue(changedResource.Key, out object resourceValue)) { string changedValue = changedResource.Value; // check NUIResourcePath string[] changedValues = changedValue.Split('/'); if (changedValues[0] == "NUIResourcePath") { changedValue = changedValues[1]; } Type toType = resourceValue.GetType(); resources[changedResource.Key] = changedValue.ConvertTo(toType, () => toType.GetTypeInfo(), null); } } } } } }