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.Reflection;
22 using Tizen.NUI.BaseComponents;
23 using Tizen.NUI.Binding;
30 /// Basically, the Theme is a dictionary of <seealso cref="ViewStyle"/>s that can decorate NUI <seealso cref="View"/>s.
31 /// Each ViewStyle item is identified by a string key that can be matched the <seealso cref="View.StyleName"/>.
34 /// The main purpose of providing Theme is to separate style details from the structure.
35 /// Managing style separately makes it easier to customize the look of application by user context.
36 /// Also since a Theme can be created from xaml file, it can be treated as a resource.
37 /// This enables sharing styles with other applications.
40 [EditorBrowsable(EditorBrowsableState.Never)]
41 public class Theme : BindableObject
43 private readonly Dictionary<string, ViewStyle> map;
44 private IEnumerable<KeyValuePair<string, string>> changedResources = null;
45 private string baseTheme;
46 ResourceDictionary resources;
48 /// <summary>Create an empty theme.</summary>
49 [EditorBrowsable(EditorBrowsableState.Never)]
52 map = new Dictionary<string, ViewStyle>();
55 /// <summary>Create a new theme from the xaml file.</summary>
56 /// <param name="xamlFile">An absolute path to the xaml file.</param>
57 /// <exception cref="ArgumentNullException">Thrown when the given xamlFile is null or empty string.</exception>
58 /// <exception cref="global::System.IO.IOException">Thrown when there are file IO problems.</exception>
59 /// <exception cref="Exception">Thrown when the content of the xaml file is not valid xaml form.</exception>
60 [EditorBrowsable(EditorBrowsableState.Never)]
61 public Theme(string xamlFile) : this()
63 if (string.IsNullOrEmpty(xamlFile))
65 throw new ArgumentNullException(nameof(xamlFile), "The xaml file path cannot be null or empty string");
70 using (var reader = XmlReader.Create(xamlFile))
72 XamlLoader.Load(this, reader);
75 catch (System.IO.IOException)
77 Tizen.Log.Error("NUI", $"Could not load \"{xamlFile}\".\n");
82 Tizen.Log.Error("NUI", $"Could not parse \"{xamlFile}\".\n");
83 Tizen.Log.Error("NUI", "Make sure the all used assemblies (e.g. Tizen.NUI.Components) are included in the application project.\n");
84 Tizen.Log.Error("NUI", "Make sure the type and namespace are correct.\n");
90 /// The string key to identify the Theme.
92 [EditorBrowsable(EditorBrowsableState.Never)]
93 public string Id { get; set; }
96 /// The string key to identify the Theme.
98 [EditorBrowsable(EditorBrowsableState.Never)]
99 public string Version { get; set; } = null;
102 /// For Xaml use only.
103 /// The bulit-in theme id that will be used as base of this.
104 /// View styles with same key are merged.
106 internal string BasedOn
113 if (string.IsNullOrEmpty(baseTheme)) return;
115 var baseThemeInstance = (Theme)ThemeManager.GetBuiltinTheme(baseTheme)?.Clone();
117 if (baseThemeInstance != null)
119 foreach (var item in baseThemeInstance)
121 var baseStyle = item.Value?.Clone();
122 if (map.ContainsKey(item.Key))
124 baseStyle.Merge(map[item.Key]);
126 map[item.Key] = baseStyle;
133 [EditorBrowsable(EditorBrowsableState.Never)]
134 public bool IsResourcesCreated => resources != null;
137 [EditorBrowsable(EditorBrowsableState.Never)]
138 internal ResourceDictionary Resources
142 if (resources != null)
144 resources = new ResourceDictionary();
145 ((IResourceDictionary)resources).ValuesChanged += OnThemeResourcesChanged;
150 if (resources == value)
153 if (resources != null)
155 ((IResourceDictionary)resources).ValuesChanged -= OnThemeResourcesChanged;
158 if (resources != null)
160 // This callback will be removed when Resource.Source is assigned.
161 ((IResourceDictionary)resources).ValuesChanged += OnThemeResourcesChanged;
167 /// For Xaml use only.
168 /// Note that it is not a normal indexer.
169 /// Setter will merge the new value with existing item.
171 internal ViewStyle this[string styleName]
173 get => map[styleName];
178 map.Remove(styleName);
182 if (map.TryGetValue(styleName, out ViewStyle style) && style != null && style.GetType() == value.GetType())
188 map[styleName] = value;
193 internal int Count => map.Count;
195 internal int PackageCount { get; set; } = 0;
198 /// Get an enumerator of the theme.
200 [EditorBrowsable(EditorBrowsableState.Never)]
201 public IEnumerator<KeyValuePair<string, ViewStyle>> GetEnumerator() => map.GetEnumerator();
204 /// Removes all styles in the theme.
206 [EditorBrowsable(EditorBrowsableState.Never)]
207 public void Clear() => map.Clear();
210 /// Determines whether the theme contains the specified style name.
212 /// <exception cref="ArgumentNullException">The given style name is null.</exception>
213 [EditorBrowsable(EditorBrowsableState.Never)]
214 public bool HasStyle(string styleName) => map.ContainsKey(styleName);
217 /// Removes the style with the specified style name.
219 /// <exception cref="ArgumentNullException">The given style name is null.</exception>
220 [EditorBrowsable(EditorBrowsableState.Never)]
221 public bool RemoveStyle(string styleName) => map.Remove(styleName);
224 /// Gets a style of given style name.
226 /// <param name="styleName">The string key to find a ViewStyle.</param>
227 /// <returns>Founded style instance.</returns>
228 [EditorBrowsable(EditorBrowsableState.Never)]
229 public ViewStyle GetStyle(string styleName)
231 map.TryGetValue(styleName, out ViewStyle result);
236 /// Gets a style of given view type.
238 /// <param name="viewType">The type of View.</param>
239 /// <returns>Founded style instance.</returns>
240 /// <exception cref="ArgumentNullException">Thrown when the given viewType is null.</exception>
241 [EditorBrowsable(EditorBrowsableState.Never)]
242 public ViewStyle GetStyle(Type viewType)
244 var currentType = viewType ?? throw new ArgumentNullException(nameof(viewType));
245 ViewStyle resultStyle = null;
249 if (currentType.Equals(typeof(View))) break;
250 resultStyle = GetStyle(currentType.FullName);
251 currentType = currentType.BaseType;
253 while (resultStyle == null && currentType != null);
259 /// Adds the specified style name and value to the theme.
260 /// This replace existing value if the theme already has a style with given name.
262 /// <param name="styleName">The style name to add.</param>
263 /// <param name="value">The style instance to add.</param>
264 [EditorBrowsable(EditorBrowsableState.Never)]
265 public void AddStyle(string styleName, ViewStyle value) => map[styleName] = value?.Clone();
269 [EditorBrowsable(EditorBrowsableState.Never)]
270 public object Clone()
272 var result = new Theme()
275 Resources = Resources
278 foreach (var item in this)
280 result.AddStyle(item.Key, item.Value);
285 /// <summary>Merge other Theme into this.</summary>
286 /// <param name="xamlFile">An absolute path to the xaml file of the theme.</param>
287 /// <exception cref="ArgumentException">Thrown when the given xamlFile is null or empty string.</exception>
288 /// <exception cref="global::System.IO.IOException">Thrown when there are file IO problems.</exception>
289 /// <exception cref="XamlParseException">Thrown when the content of the xaml file is not valid xaml form.</exception>
290 [EditorBrowsable(EditorBrowsableState.Never)]
291 public void Merge(string xamlFile)
293 MergeWithoutClone(new Theme(xamlFile));
296 /// <summary>Merge other Theme into this.</summary>
297 /// <param name="theme">The Theme.</param>
298 [EditorBrowsable(EditorBrowsableState.Never)]
299 public void Merge(Theme theme)
302 throw new ArgumentNullException(nameof(theme));
304 if (Id == null) Id = theme.Id;
306 foreach (var item in theme)
308 if (item.Value == null)
310 map[item.Key] = null;
312 else if (map.ContainsKey(item.Key) && !item.Value.SolidNull)
314 map[item.Key].Merge(item.Value);
318 map[item.Key] = item.Value.Clone();
323 internal void MergeWithoutClone(Theme theme)
326 throw new ArgumentNullException(nameof(theme));
335 Version = theme.Version;
338 foreach (var item in theme)
340 if (item.Value == null)
342 map[item.Key] = null;
344 else if (map.ContainsKey(item.Key) && !item.Value.SolidNull)
346 map[item.Key].Merge(item.Value);
350 map[item.Key] = item.Value;
354 if (theme.resources != null)
356 foreach (var res in theme.resources)
358 Resources[res.Key] = res.Value;
364 /// Internal use only.
366 internal void AddStyleWithoutClone(string styleName, ViewStyle value) => map[styleName] = value;
368 internal void ApplyExternalTheme(IExternalTheme externalTheme, HashSet<ExternalThemeKeyList> keyListSet)
370 Id = externalTheme.Id;
371 Version = externalTheme.Version;
373 if (keyListSet == null)
379 foreach (var keyList in keyListSet)
381 keyList?.ApplyKeyActions(externalTheme, this);
385 internal bool HasSameIdAndVersion(IExternalTheme externalTheme)
387 if (externalTheme == null)
392 return string.Equals(Id, externalTheme.Id, StringComparison.OrdinalIgnoreCase) && string.Equals(Version, externalTheme.Version, StringComparison.OrdinalIgnoreCase);
395 internal void SetChangedResources(IEnumerable<KeyValuePair<string, string>> changedResources)
397 this.changedResources = changedResources;
400 internal void OnThemeResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnThemeResourcesChanged();
402 internal void OnThemeResourcesChanged()
404 if (changedResources != null)
406 // To avoid loop in infinite, remove OnThemeResourcesChanged callback.
407 ((IResourceDictionary)resources).ValuesChanged -= OnThemeResourcesChanged;
408 foreach (var changedResource in changedResources)
410 if (resources.TryGetValue(changedResource.Key, out object resourceValue))
412 string changedValue = changedResource.Value;
414 // check NUIResourcePath
415 string[] changedValues = changedValue.Split('/');
416 if (changedValues[0] == "NUIResourcePath")
418 changedValue = changedValues[1];
421 Type toType = resourceValue.GetType();
422 resources[changedResource.Key] = changedValue.ConvertTo(toType, () => toType.GetTypeInfo(), null);