d5e866d0d3c62d9640305bfbf3a9690b674119de
[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     [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         /// The string key to identify the Theme.
97         /// </summary>
98         [EditorBrowsable(EditorBrowsableState.Never)]
99         public string Version { get; set; } = null;
100
101         /// <summary>
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.
105         /// </summary>
106         internal string BasedOn
107         {
108             get => baseTheme;
109             set
110             {
111                 baseTheme = value;
112
113                 if (string.IsNullOrEmpty(baseTheme)) return;
114
115                 var baseThemeInstance = (Theme)ThemeManager.GetBuiltinTheme(baseTheme)?.Clone();
116
117                 if (baseThemeInstance != null)
118                 {
119                     foreach (var item in baseThemeInstance)
120                     {
121                         var baseStyle = item.Value?.Clone();
122                         if (map.ContainsKey(item.Key))
123                         {
124                             baseStyle.Merge(map[item.Key]);
125                         }
126                         map[item.Key] = baseStyle;
127                     }
128                 }
129             }
130         }
131
132         /// <inheritdoc/>
133         [EditorBrowsable(EditorBrowsableState.Never)]
134         public bool IsResourcesCreated => resources != null;
135
136         /// <inheritdoc/>
137         [EditorBrowsable(EditorBrowsableState.Never)]
138         internal ResourceDictionary Resources
139         {
140             get
141             {
142                 if (resources != null)
143                     return resources;
144                 resources = new ResourceDictionary();
145                 ((IResourceDictionary)resources).ValuesChanged += OnThemeResourcesChanged;
146                 return resources;
147             }
148             set
149             {
150                 if (resources == value)
151                     return;
152
153                 if (resources != null)
154                 {
155                     ((IResourceDictionary)resources).ValuesChanged -= OnThemeResourcesChanged;
156                 }
157                 resources = value;
158                 if (resources != null)
159                 {
160                     // This callback will be removed when Resource.Source is assigned.
161                     ((IResourceDictionary)resources).ValuesChanged += OnThemeResourcesChanged;
162                 }
163             }
164         }
165
166         /// <summary>
167         /// For Xaml use only.
168         /// Note that it is not a normal indexer.
169         /// Setter will merge the new value with existing item.
170         /// </summary>
171         internal ViewStyle this[string styleName]
172         {
173             get => map[styleName];
174             set
175             {
176                 if (value == null)
177                 {
178                     map.Remove(styleName);
179                     return;
180                 }
181
182                 if (map.TryGetValue(styleName, out ViewStyle style) && style != null && style.GetType() == value.GetType())
183                 {
184                     style.Merge(value);
185                 }
186                 else
187                 {
188                     map[styleName] = value;
189                 }
190             }
191         }
192
193         internal int Count => map.Count;
194
195         internal int PackageCount { get; set; } = 0;
196
197         /// <summary>
198         /// Get an enumerator of the theme.
199         /// </summary>
200         [EditorBrowsable(EditorBrowsableState.Never)]
201         public IEnumerator<KeyValuePair<string, ViewStyle>> GetEnumerator() => map.GetEnumerator();
202
203         /// <summary>
204         /// Removes all styles in the theme.
205         /// </summary>
206         [EditorBrowsable(EditorBrowsableState.Never)]
207         public void Clear() => map.Clear();
208
209         /// <summary>
210         /// Determines whether the theme contains the specified style name.
211         /// </summary>
212         /// <exception cref="ArgumentNullException">The given style name is null.</exception>
213         [EditorBrowsable(EditorBrowsableState.Never)]
214         public bool HasStyle(string styleName) => map.ContainsKey(styleName);
215
216         /// <summary>
217         /// Removes the style with the specified style name.
218         /// </summary>
219         /// <exception cref="ArgumentNullException">The given style name is null.</exception>
220         [EditorBrowsable(EditorBrowsableState.Never)]
221         public bool RemoveStyle(string styleName) => map.Remove(styleName);
222
223         /// <summary>
224         /// Gets a style of given style name.
225         /// </summary>
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)
230         {
231             map.TryGetValue(styleName, out ViewStyle result);
232             return result;
233         }
234
235         /// <summary>
236         /// Gets a style of given view type.
237         /// </summary>
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)
243         {
244             var currentType = viewType ?? throw new ArgumentNullException(nameof(viewType));
245             ViewStyle resultStyle = null;
246
247             do
248             {
249                 if (currentType.Equals(typeof(View))) break;
250                 resultStyle = GetStyle(currentType.FullName);
251                 currentType = currentType.BaseType;
252             }
253             while (resultStyle == null && currentType != null);
254
255             return resultStyle;
256         }
257
258         /// <summary>
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.
261         /// </summary>
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();
266
267
268         /// <inheritdoc/>
269         [EditorBrowsable(EditorBrowsableState.Never)]
270         public object Clone()
271         {
272             var result = new Theme()
273             {
274                 Id = this.Id,
275                 Resources = Resources
276             };
277
278             foreach (var item in this)
279             {
280                 result.AddStyle(item.Key, item.Value);
281             }
282             return result;
283         }
284
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)
292         {
293             MergeWithoutClone(new Theme(xamlFile));
294         }
295
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)
300         {
301             if (theme == null)
302                 throw new ArgumentNullException(nameof(theme));
303
304             if (Id == null) Id = theme.Id;
305
306             foreach (var item in theme)
307             {
308                 if (item.Value == null)
309                 {
310                     map[item.Key] = null;
311                 }
312                 else if (map.ContainsKey(item.Key) && !item.Value.SolidNull)
313                 {
314                     map[item.Key].Merge(item.Value);
315                 }
316                 else
317                 {
318                     map[item.Key] = item.Value.Clone();
319                 }
320             }
321         }
322
323         internal void MergeWithoutClone(Theme theme)
324         {
325             if (theme == null)
326                 throw new ArgumentNullException(nameof(theme));
327
328             if (Id == null)
329             {
330                 Id = theme.Id;
331             }
332
333             if (Version == null)
334             {
335                 Version = theme.Version;
336             }
337
338             foreach (var item in theme)
339             {
340                 if (item.Value == null)
341                 {
342                     map[item.Key] = null;
343                 }
344                 else if (map.ContainsKey(item.Key) && !item.Value.SolidNull)
345                 {
346                     map[item.Key].Merge(item.Value);
347                 }
348                 else
349                 {
350                     map[item.Key] = item.Value;
351                 }
352             }
353
354             if (theme.resources != null)
355             {
356                 foreach (var res in theme.resources)
357                 {
358                     Resources[res.Key] = res.Value;
359                 }
360             }
361         }
362
363         /// <summary>
364         /// Internal use only.
365         /// </summary>
366         internal void AddStyleWithoutClone(string styleName, ViewStyle value) => map[styleName] = value;
367
368         internal void ApplyExternalTheme(IExternalTheme externalTheme, HashSet<ExternalThemeKeyList> keyListSet)
369         {
370             Id = externalTheme.Id;
371             Version = externalTheme.Version;
372
373             if (keyListSet == null)
374             {
375                 // Nothing to apply
376                 return;
377             }
378
379             foreach (var keyList in keyListSet)
380             {
381                 keyList?.ApplyKeyActions(externalTheme, this);
382             }
383         }
384
385         internal bool HasSameIdAndVersion(IExternalTheme externalTheme)
386         {
387             if (externalTheme == null)
388             {
389                 return false;
390             }
391
392             return string.Equals(Id, externalTheme.Id, StringComparison.OrdinalIgnoreCase) && string.Equals(Version, externalTheme.Version, StringComparison.OrdinalIgnoreCase);
393         }
394
395         internal void SetChangedResources(IEnumerable<KeyValuePair<string, string>> changedResources)
396         {
397             this.changedResources = changedResources;
398         }
399
400         internal void OnThemeResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnThemeResourcesChanged();
401
402         internal void OnThemeResourcesChanged()
403         {
404             if (changedResources != null)
405             {
406                 // To avoid loop in infinite, remove OnThemeResourcesChanged callback.
407                 ((IResourceDictionary)resources).ValuesChanged -= OnThemeResourcesChanged;
408                 foreach (var changedResource in changedResources)
409                 {
410                     if (resources.TryGetValue(changedResource.Key, out object resourceValue))
411                     {
412                         string changedValue = changedResource.Value;
413
414                         // check NUIResourcePath
415                         string[] changedValues = changedValue.Split('/');
416                         if (changedValues[0] == "NUIResourcePath")
417                         {
418                             changedValue = changedValues[1];
419                         }
420
421                         Type toType = resourceValue.GetType();
422                         resources[changedResource.Key] = changedValue.ConvertTo(toType, () => toType.GetTypeInfo(), null);
423                     }
424                 }
425             }
426         }
427     }
428 }