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