Follow formatting NUI
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI / src / public / XamlBinding / ResourceDictionary.cs
1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4 using System.Collections.ObjectModel;
5 using System.Collections.Specialized;
6 using System.ComponentModel;
7 using System.Globalization;
8 using System.Linq;
9 using System.Reflection;
10 using System.Runtime.CompilerServices;
11
12 using Tizen.NUI.Binding.Internals;
13 using Tizen.NUI.Xaml;
14
15 namespace Tizen.NUI.Binding
16 {
17     /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
18     [EditorBrowsable(EditorBrowsableState.Never)]
19     public class ResourceDictionary : IResourceDictionary, IDictionary<string, object>
20     {
21         static ConditionalWeakTable<Type, ResourceDictionary> s_instances = new ConditionalWeakTable<Type, ResourceDictionary>();
22         readonly Dictionary<string, object> _innerDictionary = new Dictionary<string, object>();
23         ResourceDictionary _mergedInstance;
24         Type _mergedWith;
25         Uri _source;
26
27         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
28         [EditorBrowsable(EditorBrowsableState.Never)]
29         public ResourceDictionary()
30         {
31             DependencyService.Register<IResourcesLoader, ResourcesLoader>();
32         }
33
34         /// <summary>
35         /// Gets or sets the type of object with which the resource dictionary is merged.
36         /// </summary>
37         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
38         [EditorBrowsable(EditorBrowsableState.Never)]
39         [TypeConverter(typeof(TypeTypeConverter))]
40         [Obsolete("Use Source")]
41         public Type MergedWith
42         {
43             get { return _mergedWith; }
44             set
45             {
46                 if (_mergedWith == value)
47                     return;
48
49                 if (_source != null)
50                     throw new ArgumentException("MergedWith can not be used with Source");
51
52                 if (!typeof(ResourceDictionary).GetTypeInfo().IsAssignableFrom(value.GetTypeInfo()))
53                     throw new ArgumentException("MergedWith should inherit from ResourceDictionary");
54
55                 _mergedWith = value;
56                 if (_mergedWith == null)
57                     return;
58
59                 _mergedInstance = s_instances.GetValue(_mergedWith, (key) => (ResourceDictionary)Activator.CreateInstance(key));
60                 OnValuesChanged(_mergedInstance.ToArray());
61             }
62         }
63
64         /// <summary>
65         /// Gets or sets the URI of the merged resource dictionary.
66         /// </summary>
67         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
68         [EditorBrowsable(EditorBrowsableState.Never)]
69         [TypeConverter(typeof(RDSourceTypeConverter))]
70         public Uri Source
71         {
72             get { return _source; }
73             set
74             {
75                 if (_source == value)
76                     return;
77                 throw new InvalidOperationException("Source can only be set from XAML."); //through the RDSourceTypeConverter
78             }
79         }
80
81         /// <summary>
82         /// To set and load source.
83         /// </summary>
84         /// <param name="value">The source.</param>
85         /// <param name="resourcePath">The resource path.</param>
86         /// <param name="assembly">The assembly.</param>
87         /// <param name="lineInfo">The xml line info.</param>
88         /// Used by the XamlC compiled converter.
89         [EditorBrowsable(EditorBrowsableState.Never)]
90         public void SetAndLoadSource(Uri value, string resourcePath, Assembly assembly, System.Xml.IXmlLineInfo lineInfo)
91         {
92             _source = value;
93             if (_mergedWith != null)
94                 throw new ArgumentException("Source can not be used with MergedWith");
95
96             //this will return a type if the RD as an x:Class element, and codebehind
97             var type = XamlResourceIdAttribute.GetTypeForPath(assembly, resourcePath);
98             if (type != null)
99                 _mergedInstance = s_instances.GetValue(type, (key) => (ResourceDictionary)Activator.CreateInstance(key));
100             else
101                 _mergedInstance = DependencyService.Get<IResourcesLoader>()?.CreateFromResource<ResourceDictionary>(resourcePath, assembly, lineInfo);
102             OnValuesChanged(_mergedInstance.ToArray());
103         }
104
105         ICollection<ResourceDictionary> _mergedDictionaries;
106
107         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
108         [EditorBrowsable(EditorBrowsableState.Never)]
109         public ICollection<ResourceDictionary> MergedDictionaries
110         {
111             get
112             {
113                 if (_mergedDictionaries == null)
114                 {
115                     var col = new ObservableCollection<ResourceDictionary>();
116                     col.CollectionChanged += MergedDictionaries_CollectionChanged;
117                     _mergedDictionaries = col;
118                 }
119                 return _mergedDictionaries;
120             }
121         }
122
123         internal IList<StyleSheets.StyleSheet> StyleSheets { get; set; }
124
125         void StyleSheetsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
126         {
127             switch (e.Action)
128             {
129                 case NotifyCollectionChangedAction.Add:
130                     ValuesChanged?.Invoke(this, ResourcesChangedEventArgs.StyleSheets);
131                     break;
132             }
133         }
134         IList<ResourceDictionary> _collectionTrack;
135
136         void MergedDictionaries_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
137         {
138             // Move() isn't exposed by ICollection
139             if (e.Action == NotifyCollectionChangedAction.Move)
140                 return;
141
142             _collectionTrack = _collectionTrack ?? new List<ResourceDictionary>();
143             // Collection has been cleared
144             if (e.Action == NotifyCollectionChangedAction.Reset)
145             {
146                 foreach (var dictionary in _collectionTrack)
147                     dictionary.ValuesChanged -= Item_ValuesChanged;
148
149                 _collectionTrack.Clear();
150                 return;
151             }
152
153             // New Items
154             if (e.NewItems != null)
155             {
156                 foreach (var item in e.NewItems)
157                 {
158                     var rd = (ResourceDictionary)item;
159                     _collectionTrack.Add(rd);
160                     rd.ValuesChanged += Item_ValuesChanged;
161                     OnValuesChanged(rd.ToArray());
162                 }
163             }
164
165             // Old Items
166             if (e.OldItems != null)
167             {
168                 foreach (var item in e.OldItems)
169                 {
170                     var rd = (ResourceDictionary)item;
171                     rd.ValuesChanged -= Item_ValuesChanged;
172                     _collectionTrack.Remove(rd);
173                 }
174             }
175         }
176
177         void Item_ValuesChanged(object sender, ResourcesChangedEventArgs e)
178         {
179             OnValuesChanged(e.Values.ToArray());
180         }
181
182         void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
183         {
184             ((ICollection<KeyValuePair<string, object>>)_innerDictionary).Add(item);
185             OnValuesChanged(item);
186         }
187
188         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
189         [EditorBrowsable(EditorBrowsableState.Never)]
190         public void Clear()
191         {
192             _innerDictionary.Clear();
193         }
194
195         bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
196         {
197             return ((ICollection<KeyValuePair<string, object>>)_innerDictionary).Contains(item)
198                 || (_mergedInstance != null && _mergedInstance.Contains(item));
199         }
200
201         void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
202         {
203             ((ICollection<KeyValuePair<string, object>>)_innerDictionary).CopyTo(array, arrayIndex);
204         }
205
206         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
207         [EditorBrowsable(EditorBrowsableState.Never)]
208         public int Count
209         {
210             get { return _innerDictionary.Count; }
211         }
212
213         bool ICollection<KeyValuePair<string, object>>.IsReadOnly
214         {
215             get { return ((ICollection<KeyValuePair<string, object>>)_innerDictionary).IsReadOnly; }
216         }
217
218         bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
219         {
220             return ((ICollection<KeyValuePair<string, object>>)_innerDictionary).Remove(item);
221         }
222
223         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
224         [EditorBrowsable(EditorBrowsableState.Never)]
225         public void Add(string key, object value)
226         {
227             if (ContainsKey(key))
228                 throw new ArgumentException($"A resource with the key '{key}' is already present in the ResourceDictionary.");
229             _innerDictionary.Add(key, value);
230             OnValueChanged(key, value);
231         }
232
233         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
234         [EditorBrowsable(EditorBrowsableState.Never)]
235         public bool ContainsKey(string key)
236         {
237             return _innerDictionary.ContainsKey(key);
238         }
239
240         /// <summary>
241         /// Gets or sets the value according to index.
242         /// </summary>
243         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
244         [EditorBrowsable(EditorBrowsableState.Never)]
245         [IndexerName("Item")]
246         public object this[string index]
247         {
248             get
249             {
250                 if (_innerDictionary.ContainsKey(index))
251                     return _innerDictionary[index];
252                 if (_mergedInstance != null && _mergedInstance.ContainsKey(index))
253                     return _mergedInstance[index];
254                 if (MergedDictionaries != null)
255                     foreach (var dict in MergedDictionaries.Reverse())
256                         if (dict.ContainsKey(index))
257                             return dict[index];
258                 throw new KeyNotFoundException($"The resource '{index}' is not present in the dictionary.");
259             }
260             set
261             {
262                 _innerDictionary[index] = value;
263                 OnValueChanged(index, value);
264             }
265         }
266
267         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
268         [EditorBrowsable(EditorBrowsableState.Never)]
269         public ICollection<string> Keys
270         {
271             get { return _innerDictionary.Keys; }
272         }
273
274         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
275         [EditorBrowsable(EditorBrowsableState.Never)]
276         public bool Remove(string key)
277         {
278             return _innerDictionary.Remove(key);
279         }
280
281         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
282         [EditorBrowsable(EditorBrowsableState.Never)]
283         public ICollection<object> Values
284         {
285             get { return _innerDictionary.Values; }
286         }
287
288         IEnumerator IEnumerable.GetEnumerator()
289         {
290             return GetEnumerator();
291         }
292
293         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
294         [EditorBrowsable(EditorBrowsableState.Never)]
295         public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
296         {
297             return _innerDictionary.GetEnumerator();
298         }
299
300         internal IEnumerable<KeyValuePair<string, object>> MergedResources
301         {
302             get
303             {
304                 if (MergedDictionaries != null)
305                     foreach (var r in MergedDictionaries.Reverse().SelectMany(x => x.MergedResources))
306                         yield return r;
307                 if (_mergedInstance != null)
308                     foreach (var r in _mergedInstance.MergedResources)
309                         yield return r;
310                 foreach (var r in _innerDictionary)
311                     yield return r;
312             }
313         }
314
315         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
316         [EditorBrowsable(EditorBrowsableState.Never)]
317         public bool TryGetValue(string key, out object value)
318         {
319             return _innerDictionary.TryGetValue(key, out value)
320                 || (_mergedInstance != null && _mergedInstance.TryGetValue(key, out value))
321                 || (MergedDictionaries != null && TryGetMergedDictionaryValue(key, out value));
322         }
323
324         bool TryGetMergedDictionaryValue(string key, out object value)
325         {
326             foreach (var dictionary in MergedDictionaries.Reverse())
327                 if (dictionary.TryGetValue(key, out value))
328                     return true;
329
330             value = null;
331             return false;
332         }
333
334         event EventHandler<ResourcesChangedEventArgs> IResourceDictionary.ValuesChanged
335         {
336             add { ValuesChanged += value; }
337             remove { ValuesChanged -= value; }
338         }
339
340         internal void Add(Style style)
341         {
342             if (string.IsNullOrEmpty(style.Class))
343                 Add(style.TargetType.FullName, style);
344             else
345             {
346                 IList<Style> classes;
347                 object outclasses;
348                 if (!TryGetValue(Style.StyleClassPrefix + style.Class, out outclasses) || (classes = outclasses as IList<Style>) == null)
349                     classes = new List<Style>();
350                 classes.Add(style);
351                 this[Style.StyleClassPrefix + style.Class] = classes;
352             }
353         }
354
355         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
356         [EditorBrowsable(EditorBrowsableState.Never)]
357         public void Add(ResourceDictionary mergedResourceDictionary)
358         {
359             MergedDictionaries.Add(mergedResourceDictionary);
360         }
361
362         internal void Add(StyleSheets.StyleSheet styleSheet)
363         {
364             StyleSheets = StyleSheets ?? new List<StyleSheets.StyleSheet>(2);
365             StyleSheets.Add(styleSheet);
366             ValuesChanged?.Invoke(this, ResourcesChangedEventArgs.StyleSheets);
367         }
368
369         void OnValueChanged(string key, object value)
370         {
371             OnValuesChanged(new KeyValuePair<string, object>(key, value));
372         }
373
374         void OnValuesChanged(params KeyValuePair<string, object>[] values)
375         {
376             if (values == null || values.Length == 0)
377                 return;
378             ValuesChanged?.Invoke(this, new ResourcesChangedEventArgs(values));
379         }
380
381         event EventHandler<ResourcesChangedEventArgs> ValuesChanged;
382
383         [Xaml.ProvideCompiled("Tizen.NUI.Xaml.Core.XamlC.RDSourceTypeConverter")]
384         internal class RDSourceTypeConverter : TypeConverter, IExtendedTypeConverter
385         {
386             object IExtendedTypeConverter.ConvertFromInvariantString(string value, IServiceProvider serviceProvider)
387             {
388                 if (serviceProvider == null)
389                     throw new ArgumentNullException(nameof(serviceProvider));
390
391                 var targetRD = (serviceProvider.GetService(typeof(Xaml.IProvideValueTarget)) as Xaml.IProvideValueTarget)?.TargetObject as ResourceDictionary;
392                 if (targetRD == null)
393                     return null;
394
395                 var rootObjectType = (serviceProvider.GetService(typeof(Xaml.IRootObjectProvider)) as Xaml.IRootObjectProvider)?.RootObject.GetType();
396                 if (rootObjectType == null)
397                     return null;
398
399                 var lineInfo = (serviceProvider.GetService(typeof(Xaml.IXmlLineInfoProvider)) as Xaml.IXmlLineInfoProvider)?.XmlLineInfo;
400                 var rootTargetPath = XamlResourceIdAttribute.GetPathForType(rootObjectType);
401                 var uri = new Uri(value, UriKind.Relative); //we don't want file:// uris, even if they start with '/'
402                 var resourcePath = GetResourcePath(uri, rootTargetPath);
403
404                 targetRD.SetAndLoadSource(uri, resourcePath, rootObjectType.GetTypeInfo().Assembly, lineInfo);
405                 return uri;
406             }
407
408             internal static string GetResourcePath(Uri uri, string rootTargetPath)
409             {
410                 //need a fake scheme so it's not seen as file:// uri, and the forward slashes are valid on all plats
411                 var resourceUri = uri.OriginalString.StartsWith("/", StringComparison.Ordinal)
412                                      ? new Uri($"pack://{uri.OriginalString}", UriKind.Absolute)
413                                      : new Uri($"pack:///{rootTargetPath}/../{uri.OriginalString}", UriKind.Absolute);
414
415                 //drop the leading '/'
416                 return resourceUri.AbsolutePath.Substring(1);
417             }
418
419             object IExtendedTypeConverter.ConvertFrom(CultureInfo culture, object value, IServiceProvider serviceProvider)
420             {
421                 throw new NotImplementedException();
422             }
423
424             public override object ConvertFromInvariantString(string value)
425             {
426                 throw new NotImplementedException();
427             }
428         }
429     }
430 }