[NUI] Update theme system
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI / src / public / Theme / Theme.cs
1 /*
2  * Copyright(c) 2021 Samsung Electronics Co., Ltd.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  *
16  */
17 using System;
18 using System.Collections.Generic;
19 using System.ComponentModel;
20 using System.Reflection;
21 using System.Xml;
22 using Tizen.NUI.BaseComponents;
23 using Tizen.NUI.Binding;
24 using Tizen.NUI.Xaml;
25
26 namespace Tizen.NUI
27 {
28     /// <summary>
29     /// <para>
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"/>.
32     /// </para>
33     /// <para>
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.
38     /// </para>
39     /// </summary>
40     /// <since_tizen> 9 </since_tizen>
41     public class Theme : BindableObject
42     {
43         private readonly Dictionary<string, ViewStyle> map;
44         private IEnumerable<KeyValuePair<string, string>> changedResources = null;
45         private string baseTheme;
46         ResourceDictionary resources;
47
48         /// <summary>
49         /// Create an empty theme.
50         /// </summary>
51         /// <since_tizen> 9 </since_tizen>
52         public Theme()
53         {
54             map = new Dictionary<string, ViewStyle>();
55         }
56
57         /// <summary>
58         /// Create a new theme from the xaml file.
59         /// </summary>
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()
66         {
67             if (string.IsNullOrEmpty(xamlFile))
68             {
69                 throw new ArgumentNullException(nameof(xamlFile), "The xaml file path cannot be null or empty string");
70             }
71
72             try
73             {
74                 using (var reader = XmlReader.Create(xamlFile))
75                 {
76                     XamlLoader.Load(this, reader);
77                 }
78             }
79             catch (System.IO.IOException)
80             {
81                 Tizen.Log.Info("NUI", $"Could not load \"{xamlFile}\".\n");
82                 throw;
83             }
84             catch (XamlParseException)
85             {
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");
89                 throw;
90             }
91             catch (Exception e)
92             {
93                 Tizen.Log.Info("NUI", $"Could not parse \"{xamlFile}\".\n");
94                 throw new XamlParseException(e.Message);
95             }
96         }
97
98         /// <summary>
99         /// The string key to identify the Theme.
100         /// </summary>
101         /// <since_tizen> 9 </since_tizen>
102         public string Id { get; set; }
103
104         /// <summary>
105         /// The version of the Theme.
106         /// </summary>
107         /// <since_tizen> 9 </since_tizen>
108         public string Version { get; set; } = null;
109
110         /// <summary>
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.
114         /// </summary>
115         internal string BasedOn
116         {
117             get => baseTheme;
118             set
119             {
120                 baseTheme = value;
121
122                 if (string.IsNullOrEmpty(baseTheme)) return;
123
124                 var baseThemeInstance = (Theme)ThemeManager.LoadPlatformTheme(baseTheme)?.Clone();
125
126                 if (baseThemeInstance != null)
127                 {
128                     foreach (var item in baseThemeInstance)
129                     {
130                         var baseStyle = item.Value?.Clone();
131                         if (map.ContainsKey(item.Key))
132                         {
133                             baseStyle.MergeDirectly(map[item.Key]);
134                         }
135                         map[item.Key] = baseStyle;
136                     }
137                 }
138             }
139         }
140
141         /// <inheritdoc/>
142         [EditorBrowsable(EditorBrowsableState.Never)]
143         public bool IsResourcesCreated => resources != null;
144
145         /// <inheritdoc/>
146         [EditorBrowsable(EditorBrowsableState.Never)]
147         internal ResourceDictionary Resources
148         {
149             get
150             {
151                 if (resources != null)
152                     return resources;
153                 resources = new ResourceDictionary();
154                 ((IResourceDictionary)resources).ValuesChanged += OnThemeResourcesChanged;
155                 return resources;
156             }
157             set
158             {
159                 if (resources == value)
160                     return;
161
162                 if (resources != null)
163                 {
164                     ((IResourceDictionary)resources).ValuesChanged -= OnThemeResourcesChanged;
165                 }
166                 resources = value;
167                 if (resources != null)
168                 {
169                     // This callback will be removed when Resource.Source is assigned.
170                     ((IResourceDictionary)resources).ValuesChanged += OnThemeResourcesChanged;
171                 }
172             }
173         }
174
175         /// <summary>
176         /// For Xaml use only.
177         /// Note that it is not a normal indexer.
178         /// Setter will merge the new value with existing item.
179         /// </summary>
180         internal ViewStyle this[string styleName]
181         {
182             get => map[styleName];
183             set
184             {
185                 if (value == null)
186                 {
187                     map.Remove(styleName);
188                     return;
189                 }
190
191                 if (map.TryGetValue(styleName, out ViewStyle style) && style != null && style.GetType() == value.GetType())
192                 {
193                     style.MergeDirectly(value);
194                 }
195                 else
196                 {
197                     map[styleName] = value;
198                 }
199             }
200         }
201
202         internal int Count => map.Count;
203
204         internal int PackageCount { get; set; } = 0;
205
206         /// <summary>
207         /// Get an enumerator of the theme.
208         /// </summary>
209         [EditorBrowsable(EditorBrowsableState.Never)]
210         public IEnumerator<KeyValuePair<string, ViewStyle>> GetEnumerator() => map.GetEnumerator();
211
212         /// <summary>
213         /// Removes all styles in the theme.
214         /// </summary>
215         [EditorBrowsable(EditorBrowsableState.Never)]
216         public void Clear() => map.Clear();
217
218         /// <summary>
219         /// Determines whether the theme contains the specified style name.
220         /// </summary>
221         /// <exception cref="ArgumentNullException">The given style name is null.</exception>
222         [EditorBrowsable(EditorBrowsableState.Never)]
223         public bool HasStyle(string styleName) => map.ContainsKey(styleName);
224
225         /// <summary>
226         /// Removes the style with the specified style name.
227         /// </summary>
228         /// <exception cref="ArgumentNullException">The given style name is null.</exception>
229         [EditorBrowsable(EditorBrowsableState.Never)]
230         public bool RemoveStyle(string styleName) => map.Remove(styleName);
231
232         /// <summary>
233         /// Gets a style of given style name.
234         /// </summary>
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)
240         {
241             map.TryGetValue(styleName ?? throw new ArgumentNullException(nameof(styleName)), out ViewStyle result);
242             return result;
243         }
244
245         /// <summary>
246         /// Gets a style of given view type.
247         /// </summary>
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)
253         {
254             var currentType = viewType ?? throw new ArgumentNullException(nameof(viewType));
255             ViewStyle resultStyle = null;
256
257             do
258             {
259                 if (currentType.Equals(typeof(View))) break;
260                 resultStyle = GetStyle(currentType.FullName);
261                 currentType = currentType.BaseType;
262             }
263             while (resultStyle == null && currentType != null);
264
265             return resultStyle;
266         }
267
268         /// <summary>
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.
271         /// </summary>
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)
277         {
278             if (styleName == null)
279             {
280                 throw new ArgumentNullException(nameof(styleName));
281             }
282             map[styleName] = value?.Clone();
283         }
284
285         /// <inheritdoc/>
286         /// <since_tizen> 9 </since_tizen>
287         public object Clone()
288         {
289             var result = new Theme()
290             {
291                 Id = this.Id,
292                 Resources = Resources
293             };
294
295             foreach (var item in this)
296             {
297                 result.AddStyle(item.Key, item.Value);
298             }
299             return result;
300         }
301
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)
309         {
310             MergeWithoutClone(new Theme(xamlFile));
311         }
312
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)
317         {
318             if (theme == null)
319                 throw new ArgumentNullException(nameof(theme));
320
321             if (Id == null) Id = theme.Id;
322
323             if (Version == null) Version = theme.Version;
324
325             foreach (var item in theme)
326             {
327                 if (item.Value == null)
328                 {
329                     map[item.Key] = null;
330                 }
331                 else if (map.ContainsKey(item.Key) && !item.Value.SolidNull)
332                 {
333                     map[item.Key].MergeDirectly(item.Value);
334                 }
335                 else
336                 {
337                     map[item.Key] = item.Value.Clone();
338                 }
339             }
340         }
341
342         internal void MergeWithoutClone(Theme theme)
343         {
344             if (theme == null)
345                 throw new ArgumentNullException(nameof(theme));
346
347             if (Id == null)
348             {
349                 Id = theme.Id;
350             }
351
352             if (Version == null)
353             {
354                 Version = theme.Version;
355             }
356
357             foreach (var item in theme)
358             {
359                 if (item.Value == null)
360                 {
361                     map[item.Key] = null;
362                 }
363                 else if (map.ContainsKey(item.Key) && !item.Value.SolidNull)
364                 {
365                     map[item.Key].MergeDirectly(item.Value);
366                 }
367                 else
368                 {
369                     map[item.Key] = item.Value;
370                 }
371             }
372
373             if (theme.resources != null)
374             {
375                 foreach (var res in theme.resources)
376                 {
377                     Resources[res.Key] = res.Value;
378                 }
379             }
380         }
381
382         /// <summary>
383         /// Internal use only.
384         /// </summary>
385         internal void AddStyleWithoutClone(string styleName, ViewStyle value) => map[styleName] = value;
386
387         internal bool HasSameIdAndVersion(string id, string version) => string.Equals(Id, id, StringComparison.OrdinalIgnoreCase) && string.Equals(Version, version, StringComparison.OrdinalIgnoreCase);
388
389         internal void SetChangedResources(IEnumerable<KeyValuePair<string, string>> changedResources)
390         {
391             this.changedResources = changedResources;
392         }
393
394         internal void OnThemeResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnThemeResourcesChanged();
395
396         internal void OnThemeResourcesChanged()
397         {
398             if (changedResources != null)
399             {
400                 // To avoid loop in infinite, remove OnThemeResourcesChanged callback.
401                 ((IResourceDictionary)resources).ValuesChanged -= OnThemeResourcesChanged;
402                 foreach (var changedResource in changedResources)
403                 {
404                     if (resources.TryGetValue(changedResource.Key, out object resourceValue))
405                     {
406                         string changedValue = changedResource.Value;
407
408                         // check NUIResourcePath
409                         string[] changedValues = changedValue.Split('/');
410                         if (changedValues[0] == "NUIResourcePath")
411                         {
412                             changedValue = changedValues[1];
413                         }
414
415                         Type toType = resourceValue.GetType();
416                         resources[changedResource.Key] = changedValue.ConvertTo(toType, () => toType.GetTypeInfo(), null);
417                     }
418                 }
419             }
420         }
421     }
422 }