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