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 /// <since_tizen> 9 </since_tizen>
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;
49 /// Create an empty theme.
51 /// <since_tizen> 9 </since_tizen>
54 map = new Dictionary<string, ViewStyle>();
58 /// Create a new theme from the xaml file.
60 /// <param name="xamlFile">An absolute path to the xaml file.</param>
61 /// <exception cref="ArgumentNullException">Thrown when the given xamlFile is null or empty string.</exception>
62 /// <exception cref="global::System.IO.IOException">Thrown when there are file IO problems.</exception>
63 /// <exception cref="XamlParseException">Thrown when the content of the xaml file is not valid xaml form.</exception>
64 /// <since_tizen> 9 </since_tizen>
65 public Theme(string xamlFile) : this()
67 if (string.IsNullOrEmpty(xamlFile))
69 throw new ArgumentNullException(nameof(xamlFile), "The xaml file path cannot be null or empty string");
74 using (var reader = XmlReader.Create(xamlFile))
76 XamlLoader.Load(this, reader);
79 catch (System.IO.IOException)
81 Tizen.Log.Info("NUI", $"Could not load \"{xamlFile}\".\n");
84 catch (XamlParseException)
86 Tizen.Log.Info("NUI", $"Could not parse \"{xamlFile}\".\n");
87 Tizen.Log.Info("NUI", "Make sure the all used assemblies (e.g. Tizen.NUI.Components) are included in the application project.\n");
88 Tizen.Log.Info("NUI", "Make sure the type and namespace are correct.\n");
93 Tizen.Log.Info("NUI", $"Could not parse \"{xamlFile}\".\n");
94 throw new XamlParseException(e.Message);
99 /// The string key to identify the Theme.
101 /// <since_tizen> 9 </since_tizen>
102 public string Id { get; set; }
105 /// The version of the Theme.
107 /// <since_tizen> 9 </since_tizen>
108 public string Version { get; set; } = null;
111 /// For Xaml use only.
112 /// The bulit-in theme id that will be used as base of this.
113 /// View styles with same key are merged.
115 internal string BasedOn
122 if (string.IsNullOrEmpty(baseTheme)) return;
124 var baseThemeInstance = (Theme)ThemeManager.LoadPlatformTheme(baseTheme)?.Clone();
126 if (baseThemeInstance != null)
128 foreach (var item in baseThemeInstance)
130 var baseStyle = item.Value?.Clone();
131 if (map.ContainsKey(item.Key))
133 baseStyle.MergeDirectly(map[item.Key]);
135 map[item.Key] = baseStyle;
142 [EditorBrowsable(EditorBrowsableState.Never)]
143 public bool IsResourcesCreated => resources != null;
146 [EditorBrowsable(EditorBrowsableState.Never)]
147 internal ResourceDictionary Resources
151 if (resources != null)
153 resources = new ResourceDictionary();
154 ((IResourceDictionary)resources).ValuesChanged += OnThemeResourcesChanged;
159 if (resources == value)
162 if (resources != null)
164 ((IResourceDictionary)resources).ValuesChanged -= OnThemeResourcesChanged;
167 if (resources != null)
169 // This callback will be removed when Resource.Source is assigned.
170 ((IResourceDictionary)resources).ValuesChanged += OnThemeResourcesChanged;
176 /// For Xaml use only.
177 /// Note that it is not a normal indexer.
178 /// Setter will merge the new value with existing item.
180 internal ViewStyle this[string styleName]
182 get => map[styleName];
187 map.Remove(styleName);
191 if (map.TryGetValue(styleName, out ViewStyle style) && style != null && style.GetType() == value.GetType())
193 style.MergeDirectly(value);
197 map[styleName] = value;
202 internal int Count => map.Count;
204 internal int PackageCount { get; set; } = 0;
207 /// Get an enumerator of the theme.
209 [EditorBrowsable(EditorBrowsableState.Never)]
210 public IEnumerator<KeyValuePair<string, ViewStyle>> GetEnumerator() => map.GetEnumerator();
213 /// Removes all styles in the theme.
215 [EditorBrowsable(EditorBrowsableState.Never)]
216 public void Clear() => map.Clear();
219 /// Determines whether the theme contains the specified style name.
221 /// <exception cref="ArgumentNullException">The given style name is null.</exception>
222 [EditorBrowsable(EditorBrowsableState.Never)]
223 public bool HasStyle(string styleName) => map.ContainsKey(styleName);
226 /// Removes the style with the specified style name.
228 /// <exception cref="ArgumentNullException">The given style name is null.</exception>
229 [EditorBrowsable(EditorBrowsableState.Never)]
230 public bool RemoveStyle(string styleName) => map.Remove(styleName);
233 /// Gets a style of given style name.
235 /// <param name="styleName">The string key to find a ViewStyle.</param>
236 /// <returns>Found style instance if the style name has been found, otherwise null.</returns>
237 /// <exception cref="ArgumentNullException">Thrown when the given styleName is null.</exception>
238 /// <since_tizen> 9 </since_tizen>
239 public ViewStyle GetStyle(string styleName)
241 map.TryGetValue(styleName ?? throw new ArgumentNullException(nameof(styleName)), out ViewStyle result);
246 /// Gets a style of given view type.
248 /// <param name="viewType">The type of View.</param>
249 /// <returns>Found style instance if the view type is found, otherwise null.</returns>
250 /// <exception cref="ArgumentNullException">Thrown when the given viewType is null.</exception>
251 /// <since_tizen> 9 </since_tizen>
252 public ViewStyle GetStyle(Type viewType)
254 var currentType = viewType ?? throw new ArgumentNullException(nameof(viewType));
255 ViewStyle resultStyle = null;
259 if (currentType.Equals(typeof(View))) break;
260 resultStyle = GetStyle(currentType.FullName);
261 currentType = currentType.BaseType;
263 while (resultStyle == null && currentType != null);
269 /// Adds the specified style name and value to the theme.
270 /// This replace existing value if the theme already has a style with given name.
272 /// <param name="styleName">The style name to add.</param>
273 /// <param name="value">The style instance to add.</param>
274 /// <exception cref="ArgumentNullException">Thrown when the given styleName is null.</exception>
275 /// <since_tizen> 9 </since_tizen>
276 public void AddStyle(string styleName, ViewStyle value)
278 if (styleName == null)
280 throw new ArgumentNullException(nameof(styleName));
282 map[styleName] = value?.Clone();
286 /// <since_tizen> 9 </since_tizen>
287 public object Clone()
289 var result = new Theme()
292 Resources = Resources
295 foreach (var item in this)
297 result.AddStyle(item.Key, item.Value);
302 /// <summary>Merge other theme into this.</summary>
303 /// <param name="xamlFile">An absolute path to the xaml file of the theme.</param>
304 /// <exception cref="ArgumentException">Thrown when the given xamlFile is null or empty string.</exception>
305 /// <exception cref="global::System.IO.IOException">Thrown when there are file IO problems.</exception>
306 /// <exception cref="XamlParseException">Thrown when the content of the xaml file is not valid xaml form.</exception>
307 [EditorBrowsable(EditorBrowsableState.Never)]
308 public void Merge(string xamlFile)
310 MergeWithoutClone(new Theme(xamlFile));
313 /// <summary>Merge other theme into this.</summary>
314 /// <param name="theme">The theme to be merged with this theme.</param>
315 /// <since_tizen> 9 </since_tizen>
316 public void Merge(Theme theme)
319 throw new ArgumentNullException(nameof(theme));
321 if (Id == null) Id = theme.Id;
323 if (Version == null) Version = theme.Version;
325 foreach (var item in theme)
327 if (item.Value == null)
329 map[item.Key] = null;
331 else if (map.ContainsKey(item.Key) && !item.Value.SolidNull)
333 map[item.Key].MergeDirectly(item.Value);
337 map[item.Key] = item.Value.Clone();
342 internal void MergeWithoutClone(Theme theme)
345 throw new ArgumentNullException(nameof(theme));
354 Version = theme.Version;
357 foreach (var item in theme)
359 if (item.Value == null)
361 map[item.Key] = null;
363 else if (map.ContainsKey(item.Key) && !item.Value.SolidNull)
365 map[item.Key].MergeDirectly(item.Value);
369 map[item.Key] = item.Value;
373 if (theme.resources != null)
375 foreach (var res in theme.resources)
377 Resources[res.Key] = res.Value;
383 /// Internal use only.
385 internal void AddStyleWithoutClone(string styleName, ViewStyle value) => map[styleName] = value;
387 internal bool HasSameIdAndVersion(string id, string version) => string.Equals(Id, id, StringComparison.OrdinalIgnoreCase) && string.Equals(Version, version, StringComparison.OrdinalIgnoreCase);
389 internal void SetChangedResources(IEnumerable<KeyValuePair<string, string>> changedResources)
391 this.changedResources = changedResources;
394 internal void OnThemeResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnThemeResourcesChanged();
396 internal void OnThemeResourcesChanged()
398 if (changedResources != null)
400 // To avoid loop in infinite, remove OnThemeResourcesChanged callback.
401 ((IResourceDictionary)resources).ValuesChanged -= OnThemeResourcesChanged;
402 foreach (var changedResource in changedResources)
404 if (resources.TryGetValue(changedResource.Key, out object resourceValue))
406 string changedValue = changedResource.Value;
408 // check NUIResourcePath
409 string[] changedValues = changedValue.Split('/');
410 if (changedValues[0] == "NUIResourcePath")
412 changedValue = changedValues[1];
415 Type toType = resourceValue.GetType();
416 resources[changedResource.Key] = changedValue.ConvertTo(toType, () => toType.GetTypeInfo(), null);