7c200da0c8b5380ede3e180b5ae9e2e4bb8a08c6
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI / src / public / Theme / Theme.cs
1 /*
2  * Copyright(c) 2020 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     [EditorBrowsable(EditorBrowsableState.Never)]
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>Create an empty theme.</summary>
49         [EditorBrowsable(EditorBrowsableState.Never)]
50         public Theme()
51         {
52             map = new Dictionary<string, ViewStyle>();
53         }
54
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()
62         {
63             if (string.IsNullOrEmpty(xamlFile))
64             {
65                 throw new ArgumentNullException(nameof(xamlFile), "The xaml file path cannot be null or empty string");
66             }
67
68             try
69             {
70                 using (var reader = XmlReader.Create(xamlFile))
71                 {
72                     XamlLoader.Load(this, reader);
73                 }
74             }
75             catch (System.IO.IOException)
76             {
77                 Tizen.Log.Error("NUI", $"Could not load \"{xamlFile}\".\n");
78                 throw;
79             }
80             catch (Exception)
81             {
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");
85                 throw;
86             }
87         }
88
89         /// <summary>
90         /// The string key to identify the Theme.
91         /// </summary>
92         [EditorBrowsable(EditorBrowsableState.Never)]
93         public string Id { get; set; }
94
95         /// <summary>
96         /// For Xaml use only.
97         /// The bulit-in theme id that will be used as base of this.
98         /// View styles with same key are merged.
99         /// </summary>
100         internal string BasedOn
101         {
102             get => baseTheme;
103             set
104             {
105                 baseTheme = value;
106
107                 if (string.IsNullOrEmpty(baseTheme)) return;
108
109                 var baseThemeInstance = ThemeManager.GetBuiltinTheme(baseTheme);
110
111                 if (baseThemeInstance != null)
112                 {
113                     foreach (var item in baseThemeInstance)
114                     {
115                         var baseStyle = item.Value?.Clone();
116                         if (map.ContainsKey(item.Key))
117                         {
118                             baseStyle.Merge(map[item.Key]);
119                         }
120                         map[item.Key] = baseStyle;
121                     }
122                 }
123             }
124         }
125
126         /// <inheritdoc/>
127         [EditorBrowsable(EditorBrowsableState.Never)]
128         public bool IsResourcesCreated => resources != null;
129
130         /// <inheritdoc/>
131         [EditorBrowsable(EditorBrowsableState.Never)]
132         internal ResourceDictionary Resources
133         {
134             get
135             {
136                 if (resources != null)
137                     return resources;
138                 resources = new ResourceDictionary();
139                 ((IResourceDictionary)resources).ValuesChanged += OnThemeResourcesChanged;
140                 return resources;
141             }
142             set
143             {
144                 if (resources == value)
145                     return;
146
147                 if (resources != null)
148                 {
149                     ((IResourceDictionary)resources).ValuesChanged -= OnThemeResourcesChanged;
150                 }
151                 resources = value;
152                 if (resources != null)
153                 {
154                     // This callback will be removed when Resource.Source is assigned.
155                     ((IResourceDictionary)resources).ValuesChanged += OnThemeResourcesChanged;
156                 }
157             }
158         }
159
160         /// <summary>
161         /// For Xaml use only.
162         /// Note that it is not a normal indexer.
163         /// Setter will merge the new value with existing item.
164         /// </summary>
165         internal ViewStyle this[string styleName]
166         {
167             get => map[styleName];
168             set
169             {
170                 if (value == null)
171                 {
172                     map.Remove(styleName);
173                     return;
174                 }
175
176                 if (map.TryGetValue(styleName, out ViewStyle style) && style != null && style.GetType() == value.GetType())
177                 {
178                     style.Merge(value);
179                 }
180                 else
181                 {
182                     map[styleName] = value;
183                 }
184             }
185         }
186
187         internal int Count => map.Count;
188
189         /// <summary>
190         /// Get an enumerator of the theme.
191         /// </summary>
192         [EditorBrowsable(EditorBrowsableState.Never)]
193         public IEnumerator<KeyValuePair<string, ViewStyle>> GetEnumerator() => map.GetEnumerator();
194
195         /// <summary>
196         /// Removes all styles in the theme.
197         /// </summary>
198         [EditorBrowsable(EditorBrowsableState.Never)]
199         public void Clear() => map.Clear();
200
201         /// <summary>
202         /// Determines whether the theme contains the specified style name.
203         /// </summary>
204         /// <exception cref="ArgumentNullException">The given style name is null.</exception>
205         [EditorBrowsable(EditorBrowsableState.Never)]
206         public bool HasStyle(string styleName) => map.ContainsKey(styleName);
207
208         /// <summary>
209         /// Removes the style with the specified style name.
210         /// </summary>
211         /// <exception cref="ArgumentNullException">The given style name is null.</exception>
212         [EditorBrowsable(EditorBrowsableState.Never)]
213         public bool RemoveStyle(string styleName) => map.Remove(styleName);
214
215         /// <summary>
216         /// Gets a style of given style name.
217         /// </summary>
218         /// <param name="styleName">The string key to find a ViewStyle.</param>
219         /// <returns>Founded style instance.</returns>
220         [EditorBrowsable(EditorBrowsableState.Never)]
221         public ViewStyle GetStyle(string styleName) => map.ContainsKey(styleName) ? map[styleName] : null;
222
223         /// <summary>
224         /// Gets a style of given view type.
225         /// </summary>
226         /// <param name="viewType">The type of View.</param>
227         /// <returns>Founded style instance.</returns>
228         /// <exception cref="ArgumentNullException">Thrown when the given viewType is null.</exception>
229         [EditorBrowsable(EditorBrowsableState.Never)]
230         public ViewStyle GetStyle(Type viewType)
231         {
232             var currentType = viewType ?? throw new ArgumentNullException(nameof(viewType));
233             ViewStyle resultStyle = null;
234
235             do
236             {
237                 if (currentType.Equals(typeof(View))) break;
238                 resultStyle = GetStyle(currentType.FullName);
239                 currentType = currentType.BaseType;
240             }
241             while (resultStyle == null && currentType != null);
242
243             return resultStyle;
244         }
245
246         /// <summary>
247         /// Adds the specified style name and value to the theme.
248         /// This replace existing value if the theme already has a style with given name.
249         /// </summary>
250         /// <param name="styleName">The style name to add.</param>
251         /// <param name="value">The style instance to add.</param>
252         [EditorBrowsable(EditorBrowsableState.Never)]
253         public void AddStyle(string styleName, ViewStyle value) => map[styleName] = value?.Clone();
254
255
256         /// <inheritdoc/>
257         [EditorBrowsable(EditorBrowsableState.Never)]
258         public object Clone()
259         {
260             var result = new Theme()
261             {
262                 Id = this.Id,
263                 Resources = Resources
264             };
265
266             foreach (var item in this)
267             {
268                 result.AddStyle(item.Key, item.Value);
269             }
270             return result;
271         }
272
273         /// <summary>Merge other Theme into this.</summary>
274         /// <param name="xamlFile">An absolute path to the xaml file of the theme.</param>
275         /// <exception cref="ArgumentException">Thrown when the given xamlFile is null or empty string.</exception>
276         /// <exception cref="global::System.IO.IOException">Thrown when there are file IO problems.</exception>
277         /// <exception cref="XamlParseException">Thrown when the content of the xaml file is not valid xaml form.</exception>
278         [EditorBrowsable(EditorBrowsableState.Never)]
279         public void Merge(string xamlFile)
280         {
281             MergeWithoutClone(new Theme(xamlFile));
282         }
283
284         /// <summary>Merge other Theme into this.</summary>
285         /// <param name="theme">The Theme.</param>
286         [EditorBrowsable(EditorBrowsableState.Never)]
287         public void Merge(Theme theme)
288         {
289             if (theme == null)
290                 throw new ArgumentNullException(nameof(theme));
291
292             if (Id == null) Id = theme.Id;
293
294             foreach (var item in theme)
295             {
296                 if (item.Value == null)
297                 {
298                     map[item.Key] = null;
299                 }
300                 else if (map.ContainsKey(item.Key) && !item.Value.SolidNull)
301                 {
302                     map[item.Key].Merge(item.Value);
303                 }
304                 else
305                 {
306                     map[item.Key] = item.Value.Clone();
307                 }
308             }
309         }
310
311         internal void MergeWithoutClone(Theme theme)
312         {
313             if (theme == null)
314                 throw new ArgumentNullException(nameof(theme));
315
316             if (Id == null) Id = theme.Id;
317
318             foreach (var item in theme)
319             {
320                 if (item.Value == null)
321                 {
322                     map[item.Key] = null;
323                 }
324                 else if (map.ContainsKey(item.Key) && !item.Value.SolidNull)
325                 {
326                     map[item.Key].Merge(item.Value);
327                 }
328                 else
329                 {
330                     map[item.Key] = item.Value;
331                 }
332             }
333
334             if (theme.resources != null)
335             {
336                 foreach (var res in theme.resources)
337                 {
338                     Resources[res.Key] = res.Value;
339                 }
340             }
341         }
342
343         /// <summary>
344         /// Internal use only.
345         /// </summary>
346         internal void AddStyleWithoutClone(string styleName, ViewStyle value) => map[styleName] = value;
347
348         internal void SetChangedResources(IEnumerable<KeyValuePair<string, string>> changedResources)
349         {
350             this.changedResources = changedResources;
351         }
352
353         internal void OnThemeResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnThemeResourcesChanged();
354
355         internal void OnThemeResourcesChanged()
356         {
357             if (changedResources != null)
358             {
359                 // To avoid loop in infinite, remove OnThemeResourcesChanged callback.
360                 ((IResourceDictionary)resources).ValuesChanged -= OnThemeResourcesChanged;
361                 foreach (var changedResource in changedResources)
362                 {
363                     if (resources.TryGetValue(changedResource.Key, out object resourceValue))
364                     {
365                         string changedValue = changedResource.Value;
366
367                         // check NUIResourcePath
368                         string[] changedValues = changedValue.Split('/');
369                         if (changedValues[0] == "NUIResourcePath")
370                         {
371                             changedValue = changedValues[1];
372                         }
373
374                         Type toType = resourceValue.GetType();
375                         resources[changedResource.Key] = changedValue.ConvertTo(toType, () => toType.GetTypeInfo(), null);
376                     }
377                 }
378             }
379         }
380     }
381 }