436362caa6155e4aee8697b8dac02a2d146e2b5f
[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, IResourcesProvider
42     {
43         private readonly Dictionary<string, ViewStyle> map;
44         private string baseTheme;
45         private string resource;
46         private string xamlFile;
47
48         /// <summary>
49         /// The resource file path that is used in the theme.
50         /// </summary>
51         [EditorBrowsable(EditorBrowsableState.Never)]
52         public string Resource
53         {
54             get => resource;
55             set
56             {
57                 if (resource == value) return;
58                 resource = value;
59
60                 Reload();
61             }
62         }
63
64         /// <summary>Create an empty theme.</summary>
65         [EditorBrowsable(EditorBrowsableState.Never)]
66         public Theme()
67         {
68             map = new Dictionary<string, ViewStyle>();
69         }
70
71         /// <summary>Create a new theme from the xaml file.</summary>
72         /// <param name="xamlFile">An absolute path to the xaml file.</param>
73         /// <exception cref="ArgumentNullException">Thrown when the given xamlFile is null or empty string.</exception>
74         /// <exception cref="global::System.IO.IOException">Thrown when there are file IO problems.</exception>
75         /// <exception cref="Exception">Thrown when the content of the xaml file is not valid xaml form.</exception>
76         [EditorBrowsable(EditorBrowsableState.Never)]
77         public Theme(string xamlFile) : this()
78         {
79             if (string.IsNullOrEmpty(xamlFile))
80             {
81                 throw new ArgumentNullException(nameof(xamlFile), "The xaml file path cannot be null or empty string");
82             }
83
84             LoadFromXaml(xamlFile);
85         }
86
87         /// <summary>
88         /// Create a new theme from the xaml file with theme resource.
89         /// </summary>
90         /// <param name="xamlFile">An absolute path to the xaml file.</param>
91         /// <param name="themeResource">An absolute path to the theme resource file.</param>
92         /// <exception cref="ArgumentNullException">Thrown when the given xamlFile or themeResource is null or empty string.</exception>
93         /// <exception cref="System.IO.IOException">Thrown when there are file IO problems.</exception>
94         /// <exception cref="Exception">Thrown when the content of the xaml file is not valid xaml form.</exception>
95         [EditorBrowsable(EditorBrowsableState.Never)]
96         public Theme(string xamlFile, string themeResource) : this()
97         {
98             if (string.IsNullOrEmpty(xamlFile))
99                 throw new ArgumentNullException(nameof(xamlFile), "The xaml file path cannot be null or empty string");
100             if (string.IsNullOrEmpty(themeResource))
101                 throw new ArgumentNullException(nameof(themeResource), "The theme resource file path cannot be null or empty string");
102
103             resource = themeResource;
104             XamlResources.SetAndLoadSource(new Uri(themeResource), themeResource, Assembly.GetAssembly(GetType()), null);
105
106             LoadFromXaml(xamlFile);
107         }
108
109         /// <summary>
110         /// The string key to identify the Theme.
111         /// </summary>
112         [EditorBrowsable(EditorBrowsableState.Never)]
113         public string Id { get; set; }
114
115         /// <summary>
116         /// For Xaml use only.
117         /// The bulit-in theme id that will be used as base of this.
118         /// View styles with same key are merged.
119         /// </summary>
120         internal string BasedOn
121         {
122             get => baseTheme;
123             set
124             {
125                 baseTheme = value;
126
127                 if (string.IsNullOrEmpty(baseTheme)) return;
128
129                 var baseThemeInstance = ThemeManager.GetBuiltinTheme(baseTheme);
130
131                 if (baseThemeInstance != null)
132                 {
133                     foreach (var item in baseThemeInstance)
134                     {
135                         var baseStyle = item.Value?.Clone();
136                         if (map.ContainsKey(item.Key))
137                         {
138                             baseStyle.Merge(map[item.Key]);
139                         }
140                         map[item.Key] = baseStyle;
141                     }
142                 }
143             }
144         }
145
146         /// <inheritdoc/>
147         [EditorBrowsable(EditorBrowsableState.Never)]
148         public bool IsResourcesCreated { get; } = true;
149
150         /// <inheritdoc/>
151         [EditorBrowsable(EditorBrowsableState.Never)]
152         public ResourceDictionary XamlResources { get; set; } = new ResourceDictionary();
153
154         /// <summary>
155         /// For Xaml use only.
156         /// Note that it is not a normal indexer.
157         /// Setter will merge the new value with existing item.
158         /// </summary>
159         internal ViewStyle this[string styleName]
160         {
161             get => map[styleName];
162             set
163             {
164                 if (value == null)
165                 {
166                     map.Remove(styleName);
167                     return;
168                 }
169
170                 if (map.TryGetValue(styleName, out ViewStyle style) && style != null && style.GetType() == value.GetType())
171                 {
172                     style.Merge(value);
173                 }
174                 else
175                 {
176                     map[styleName] = value;
177                 }
178             }
179         }
180
181         internal int Count => map.Count;
182
183         /// <summary>
184         /// Get an enumerator of the theme.
185         /// </summary>
186         [EditorBrowsable(EditorBrowsableState.Never)]
187         public IEnumerator<KeyValuePair<string, ViewStyle>> GetEnumerator() => map.GetEnumerator();
188
189         /// <summary>
190         /// Removes all styles in the theme.
191         /// </summary>
192         [EditorBrowsable(EditorBrowsableState.Never)]
193         public void Clear() => map.Clear();
194
195         /// <summary>
196         /// Determines whether the theme contains the specified style name.
197         /// </summary>
198         /// <exception cref="ArgumentNullException">The given style name is null.</exception>
199         [EditorBrowsable(EditorBrowsableState.Never)]
200         public bool HasStyle(string styleName) => map.ContainsKey(styleName);
201
202         /// <summary>
203         /// Removes the style with the specified style name.
204         /// </summary>
205         /// <exception cref="ArgumentNullException">The given style name is null.</exception>
206         [EditorBrowsable(EditorBrowsableState.Never)]
207         public bool RemoveStyle(string styleName) => map.Remove(styleName);
208
209         /// <summary>
210         /// Gets a style of given style name.
211         /// </summary>
212         /// <param name="styleName">The string key to find a ViewStyle.</param>
213         /// <returns>Founded style instance.</returns>
214         [EditorBrowsable(EditorBrowsableState.Never)]
215         public ViewStyle GetStyle(string styleName) => map.ContainsKey(styleName) ? map[styleName] : null;
216
217         /// <summary>
218         /// Gets a style of given view type.
219         /// </summary>
220         /// <param name="viewType">The type of View.</param>
221         /// <returns>Founded style instance.</returns>
222         /// <exception cref="ArgumentNullException">Thrown when the given viewType is null.</exception>
223         [EditorBrowsable(EditorBrowsableState.Never)]
224         public ViewStyle GetStyle(Type viewType)
225         {
226             var currentType = viewType ?? throw new ArgumentNullException(nameof(viewType));
227             ViewStyle resultStyle = null;
228
229             do
230             {
231                 if (currentType.Equals(typeof(View))) break;
232                 resultStyle = GetStyle(currentType.FullName);
233                 currentType = currentType.BaseType;
234             }
235             while (resultStyle == null && currentType != null);
236
237             return resultStyle;
238         }
239
240         /// <summary>
241         /// Adds the specified style name and value to the theme.
242         /// This replace existing value if the theme already has a style with given name.
243         /// </summary>
244         /// <param name="styleName">The style name to add.</param>
245         /// <param name="value">The style instance to add.</param>
246         [EditorBrowsable(EditorBrowsableState.Never)]
247         public void AddStyle(string styleName, ViewStyle value) => map[styleName] = value?.Clone();
248
249
250         /// <inheritdoc/>
251         [EditorBrowsable(EditorBrowsableState.Never)]
252         public object Clone()
253         {
254             var result = new Theme()
255             {
256                 Id = this.Id,
257             };
258
259             foreach (var item in this)
260             {
261                 result.AddStyle(item.Key, item.Value);
262             }
263             return result;
264         }
265
266         /// <summary>Merge other Theme into this.</summary>
267         /// <param name="xamlFile">An absolute path to the xaml file of the theme.</param>
268         /// <exception cref="ArgumentException">Thrown when the given xamlFile is null or empty string.</exception>
269         /// <exception cref="global::System.IO.IOException">Thrown when there are file IO problems.</exception>
270         /// <exception cref="XamlParseException">Thrown when the content of the xaml file is not valid xaml form.</exception>
271         [EditorBrowsable(EditorBrowsableState.Never)]
272         public void Merge(string xamlFile)
273         {
274             Merge(new Theme(xamlFile));
275         }
276
277         /// <summary>Merge other Theme into this.</summary>
278         /// <param name="theme">The Theme.</param>
279         [EditorBrowsable(EditorBrowsableState.Never)]
280         public void Merge(Theme theme)
281         {
282             if (theme == null)
283                 throw new ArgumentNullException(nameof(theme));
284
285             this.xamlFile = theme.xamlFile;
286
287             foreach (var item in theme)
288             {
289                 if (item.Value == null)
290                 {
291                     map[item.Key] = null;
292                 }
293                 else if (map.ContainsKey(item.Key) && !item.Value.SolidNull)
294                 {
295                     map[item.Key].Merge(theme.GetStyle(item.Key));
296                 }
297                 else
298                 {
299                     map[item.Key] = theme.GetStyle(item.Key).Clone();
300                 }
301             }
302         }
303
304         /// <summary>
305         /// Internal use only.
306         /// </summary>
307         internal void AddStyleWithoutClone(string styleName, ViewStyle value) => map[styleName] = value;
308
309         internal void Reload()
310         {
311             if (xamlFile == null)
312                 throw new InvalidOperationException("Cannot reload without xaml file.");
313
314             map.Clear();
315             if (Resource != null)
316             {
317                 XamlResources.Clear();
318                 XamlResources.SetAndLoadSource(new Uri(Resource), Resource, Assembly.GetAssembly(GetType()), null);
319             }
320
321             LoadFromXaml(xamlFile);
322         }
323
324         private void LoadFromXaml(string xamlFile)
325         {
326             try
327             {
328                 using (var reader = XmlReader.Create(xamlFile))
329                 {
330                     this.xamlFile = xamlFile;
331                     XamlLoader.Load(this, reader);
332                 }
333             }
334             catch (System.IO.IOException)
335             {
336                 Tizen.Log.Error("NUI", $"Could not load \"{xamlFile}\".\n");
337                 throw;
338             }
339             catch (Exception)
340             {
341                 Tizen.Log.Error("NUI", $"Could not parse \"{xamlFile}\".\n");
342                 Tizen.Log.Error("NUI", "Make sure the all used assemblies (e.g. Tizen.NUI.Components) are included in the application project.\n");
343                 Tizen.Log.Error("NUI", "Make sure the type and namespace are correct.\n");
344                 throw;
345             }
346         }
347     }
348 }