/*
* 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);
}
}
}
}
}
}