[NUI] Fix Selector bug and add new clone method (#2584)
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI / src / public / BaseComponents / Style / Selector.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 Tizen.NUI.Binding;
21 using Tizen.NUI.Components;
22
23 namespace Tizen.NUI.BaseComponents
24 {
25     /// <summary>
26     /// Selector class, which is related by Control State, it is base class for other Selector.
27     /// </summary>
28     /// <typeparam name="T">The property type of the selector. if it's reference type, it should be of type <see cref="ICloneable"/> that implement deep copy in <see cref="ICloneable.Clone"/>.</typeparam>
29     /// <since_tizen> 6 </since_tizen>
30     /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
31     [EditorBrowsable(EditorBrowsableState.Never)]
32     public class Selector<T>
33     {
34         private readonly bool cloneable = typeof(ICloneable).IsAssignableFrom(typeof(T));
35
36         /// <summary>
37         /// The list for adding <see cref="SelectorItem{T}"/>.
38         /// </summary>
39         [EditorBrowsable(EditorBrowsableState.Never)]
40         List<SelectorItem<T>> SelectorItems { get; set; } = new List<SelectorItem<T>>();
41
42         /// <summary>
43         /// Adds the specified state and value to the <see cref="SelectorItems"/>.
44         /// </summary>
45         /// <param name="state">The state.</param>
46         /// <param name="value">The value associated with state.</param>
47         [EditorBrowsable(EditorBrowsableState.Never)]
48         public void Add(ControlState state, T value)
49         {
50             SelectorItems.Add(new SelectorItem<T>(state, value));
51             All = default;
52         }
53
54         /// <summary>
55         /// Adds the specified state and value to the <see cref="SelectorItems"/>.
56         /// </summary>
57         /// <param name="selectorItem">The selector item class that stores a state-value pair.</param>
58         [EditorBrowsable(EditorBrowsableState.Never)]
59         public void Add(SelectorItem<T> selectorItem)
60         {
61             // To prevent a state from having multiple values, remove existing state-value pair.
62             int index = SelectorItems.FindIndex(x => x.State == selectorItem.State);
63             if (index != -1)
64                 SelectorItems.RemoveAt(index);
65
66             SelectorItems.Add(selectorItem);
67         }
68
69         /// <since_tizen> 6 </since_tizen>
70         /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
71         public static implicit operator Selector<T>(T value)
72         {
73             return new Selector<T>(value);
74         }
75
76         /// Default Contructor
77         [EditorBrowsable(EditorBrowsableState.Never)]
78         public Selector()
79         {
80         }
81
82         /// Contructor with T
83         [EditorBrowsable(EditorBrowsableState.Never)]
84         public Selector(T value) : this()
85         {
86             All = cloneable ? (T)((ICloneable)value)?.Clone() : value;
87         }
88
89         /// Copy constructor
90         [EditorBrowsable(EditorBrowsableState.Never)]
91         public Selector(Selector<T> value) : this()
92         {
93             Clone(value);
94         }
95
96         /// <summary>
97         /// All State.
98         /// </summary>
99         /// <since_tizen> 6 </since_tizen>
100         /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
101         [EditorBrowsable(EditorBrowsableState.Never)]
102         public T All
103         {
104             get;
105             set;
106         }
107         /// <summary>
108         /// Normal State.
109         /// </summary>
110         /// <since_tizen> 6 </since_tizen>
111         /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
112         [EditorBrowsable(EditorBrowsableState.Never)]
113         public T Normal
114         {
115             get => SelectorItems.Find(x => x.State == ControlState.Normal).Value;
116             set => Add(ControlState.Normal, value);
117         }
118         /// <summary>
119         /// Pressed State.
120         /// </summary>
121         /// <since_tizen> 6 </since_tizen>
122         /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
123         [EditorBrowsable(EditorBrowsableState.Never)]
124         public T Pressed
125         {
126             get => SelectorItems.Find(x => x.State == ControlState.Pressed).Value;
127             set => Add(ControlState.Pressed, value);
128         }
129         /// <summary>
130         /// Focused State.
131         /// </summary>
132         /// <since_tizen> 6 </since_tizen>
133         /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
134         [EditorBrowsable(EditorBrowsableState.Never)]
135         public T Focused
136         {
137             get => SelectorItems.Find(x => x.State == ControlState.Focused).Value;
138             set => Add(ControlState.Focused, value);
139         }
140         /// <summary>
141         /// Selected State.
142         /// </summary>
143         /// <since_tizen> 6 </since_tizen>
144         /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
145         [EditorBrowsable(EditorBrowsableState.Never)]
146         public T Selected
147         {
148             get => SelectorItems.Find(x => x.State == ControlState.Selected).Value;
149             set => Add(ControlState.Selected, value);
150         }
151         /// <summary>
152         /// Disabled State.
153         /// </summary>
154         /// <since_tizen> 6 </since_tizen>
155         /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
156         [EditorBrowsable(EditorBrowsableState.Never)]
157         public T Disabled
158         {
159             get => SelectorItems.Find(x => x.State == ControlState.Disabled).Value;
160             set => Add(ControlState.Disabled, value);
161         }
162         /// <summary>
163         /// DisabledFocused State.
164         /// </summary>
165         /// <since_tizen> 6 </since_tizen>
166         /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
167         [EditorBrowsable(EditorBrowsableState.Never)]
168         public T DisabledFocused
169         {
170             get => SelectorItems.Find(x => x.State == ControlState.DisabledFocused).Value;
171             set => Add(ControlState.DisabledFocused, value);
172         }
173         /// <summary>
174         /// SelectedFocused State.
175         /// </summary>
176         /// <since_tizen> 6 </since_tizen>
177         /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
178         public T SelectedFocused
179         {
180             get => SelectorItems.Find(x => x.State == ControlState.SelectedFocused).Value;
181             set => Add(ControlState.SelectedFocused, value);
182         }
183         /// <summary>
184         /// DisabledSelected State.
185         /// </summary>
186         /// <since_tizen> 6 </since_tizen>
187         /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
188         [EditorBrowsable(EditorBrowsableState.Never)]
189         public T DisabledSelected
190         {
191             get => SelectorItems.Find(x => x.State == ControlState.DisabledSelected).Value;
192             set => Add(ControlState.DisabledSelected, value);
193         }
194
195         /// <summary>
196         /// Other State.
197         /// </summary>
198         /// <since_tizen> 6 </since_tizen>
199         /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
200         [EditorBrowsable(EditorBrowsableState.Never)]
201         public T Other
202         {
203             get => SelectorItems.Find(x => x.State == ControlState.Other).Value;
204             set => Add(ControlState.Other, value);
205         }
206
207         /// <summary>
208         /// Gets the number of elements.
209         /// </summary>
210         [EditorBrowsable(EditorBrowsableState.Never)]
211         public int Count => SelectorItems.Count;
212
213         /// <summary>
214         /// Get value by State.
215         /// </summary>
216         /// <exception cref="ArgumentNullException"> Thrown when state is null. </exception>
217         /// <since_tizen> 6 </since_tizen>
218         /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
219         /// <returns>True if the selector has a given state value, false otherwise.</returns>
220         [EditorBrowsable(EditorBrowsableState.Never)]
221         public bool GetValue(ControlState state, out T result)
222         {
223             if (All != null)
224             {
225                 result = All;
226
227                 return true;
228             }
229
230             if (state == null)
231                 throw new ArgumentNullException(nameof(state));
232
233             result = default;
234
235             int index = SelectorItems.FindIndex(x => x.State == state);
236             if (index >= 0)
237             {
238                 result = SelectorItems[index].Value;
239                 return true;
240             }
241
242             if (null == state)
243             {
244                 throw new ArgumentNullException(nameof(state));
245             }
246             if (state.IsCombined)
247             {
248                 index = SelectorItems.FindIndex(x => state.Contains(x.State));
249                 if (index >= 0)
250                 {
251                     result = SelectorItems[index].Value;
252                     return true;
253                 }
254             }
255
256             index = SelectorItems.FindIndex(x => x.State == ControlState.Other);
257             if (index >= 0)
258             {
259                 result = SelectorItems[index].Value;
260                 return true;
261             }
262
263             return false;
264         }
265
266         /// <summary>
267         /// Removes all elements.
268         /// </summary>
269         [EditorBrowsable(EditorBrowsableState.Never)]
270         public void Clear()
271         {
272             All = default;
273             SelectorItems.Clear();
274         }
275
276         /// <inheritdoc/>
277         [EditorBrowsable(EditorBrowsableState.Never)]
278         public override string ToString()
279         {
280             string result = $"[All, {All}]";
281
282             foreach (var item in SelectorItems)
283             {
284                 result += $", {item}";
285             }
286
287             return result;
288         }
289
290         /// <summary>
291         /// Clone itself.
292         /// If type T implements ICloneable, it calls Clone() method to clone values, otherwise use operator=.
293         /// </summary>
294         /// <since_tizen> 6 </since_tizen>
295         /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
296         [EditorBrowsable(EditorBrowsableState.Never)]
297         public Selector<T> Clone()
298         {
299             var cloned = new Selector<T>();
300             cloned.Clone(this);
301             return cloned;
302         }
303
304         /// <summary>
305         /// Clone with type converter.
306         /// </summary>
307         /// <exception cref="ArgumentNullException"> Thrown when converter is null. </exception>
308         [EditorBrowsable(EditorBrowsableState.Never)]
309         public Selector<TOut> Clone<TOut>(Converter<T, TOut> converter)
310         {
311             if (converter == null) throw new ArgumentNullException(nameof(converter));
312
313             Selector<TOut> result = new Selector<TOut>();
314             result.All = converter(All);
315             result.SelectorItems = SelectorItems.ConvertAll<SelectorItem<TOut>>(m => new SelectorItem<TOut>(m.State, converter(m.Value)));
316
317             return result;
318         }
319
320         /// <summary>
321         /// Copy values from other selector.
322         /// </summary>
323         /// <exception cref="ArgumentNullException"> Thrown when other is null. </exception>
324         [EditorBrowsable(EditorBrowsableState.Never)]
325         public void Clone(Selector<T> other)
326         {
327             if (null == other)
328             {
329                 throw new ArgumentNullException(nameof(other));
330             }
331
332             if (cloneable)
333             {
334                 All = (T)((ICloneable)other.All)?.Clone();
335                 SelectorItems = other.SelectorItems.ConvertAll(m => new SelectorItem<T>(m.State, (T)((ICloneable)m.Value)?.Clone()));
336             }
337             else
338             {
339                 All = other.All;
340                 SelectorItems = other.SelectorItems.ConvertAll(m => m);
341             }
342         }
343
344         internal bool HasMultiValue()
345         {
346             return SelectorItems.Count > 1;
347         }
348     }
349
350     /// <summary>
351     /// This will be attached to a View to detect ControlState change.
352     /// </summary>
353     [EditorBrowsable(EditorBrowsableState.Never)]
354     public class TriggerableSelector<T>
355     {
356         /// <summary>
357         /// Create an TriggerableSelector.
358         /// </summary>
359         [EditorBrowsable(EditorBrowsableState.Never)]
360         public delegate T ValueGetter(View view);
361
362         private readonly BindableProperty targetBindableProperty;
363         private readonly ValueGetter propertyGetter;
364         private bool dirty = true;
365         private Selector<T> selector;
366
367         /// <summary>
368         /// Create an TriggerableSelector.
369         /// </summary>
370         /// <param name="targetBindableProperty">The TriggerableSelector will change this bindable property value when the view's ControlState has changed.</param>
371         /// <param name="propertyGetter">It is optional value in case the target bindable property getter is not proper to use.</param>
372         [EditorBrowsable(EditorBrowsableState.Never)]
373         public TriggerableSelector(BindableProperty targetBindableProperty, ValueGetter propertyGetter = null)
374         {
375             this.targetBindableProperty = targetBindableProperty;
376             this.propertyGetter = propertyGetter;
377         }
378
379         /// <summary>
380         /// Return the containing selector. It can be null.
381         /// </summary>
382         /// <exception cref="ArgumentNullException"> Thrown when view is null. </exception>
383         [EditorBrowsable(EditorBrowsableState.Never)]
384         public Selector<T> Get(View view)
385         {
386             if (!dirty) return selector;
387             if (null == view)
388             {
389                 throw new ArgumentNullException(nameof(view));
390             }
391
392             T value = default;
393
394             if (propertyGetter != null)
395             {
396                 value = propertyGetter(view);
397             }
398             else
399             {
400                 value = (T)view.GetValue(targetBindableProperty);
401             }
402
403             Selector<T> converted = value == null ? null : new Selector<T>(value);
404             Update(view, converted);
405
406             return selector;
407         }
408
409         /// <summary>
410         /// Update containing selector from the other selector.
411         /// </summary>
412         /// <param name="view">The View that is affected by this TriggerableSelector.</param>
413         /// <param name="otherSelector">The copy target.</param>
414         /// <param name="updateView">Whether it updates the target view after update the selector or not.</param>
415         /// <exception cref="ArgumentNullException"> Thrown when view is null. </exception>
416         [EditorBrowsable(EditorBrowsableState.Never)]
417         public void Update(View view, Selector<T> otherSelector, bool updateView = false)
418         {
419             Reset(view);
420
421             if (null == view)
422             {
423                 throw new ArgumentNullException(nameof(view));
424             }
425
426             if (otherSelector == null)
427             {
428                 return;
429             }
430
431             selector = otherSelector.Clone();
432
433             if (otherSelector.HasMultiValue())
434             {
435                 view.ControlStateChangeEventInternal += OnViewControlState;
436             }
437
438             if (updateView && otherSelector.GetValue(view.ControlState, out var value))
439             {
440                 view.SetValue(targetBindableProperty, value);
441             }
442         }
443
444         /// <summary>
445         /// Update containing selector value from a single value.
446         /// Note that, it updates lazily if possible.
447         /// If you need to udpate directly, please use <seealso cref="Update" />.
448         /// </summary>
449         /// <param name="view">The View that is affected by this TriggerableSelector.</param>
450         /// <param name="value">The copy target.</param>
451         [EditorBrowsable(EditorBrowsableState.Never)]
452         public void UpdateIfNeeds(View view, T value)
453         {
454             if (selector != null && selector.HasMultiValue())
455             {
456                 Selector<T> converted = value == null ? null : new Selector<T>(value);
457                 Update(view, converted);
458                 return;
459             }
460
461             dirty = true;
462         }
463
464         /// <summary>
465         /// Reset selector and listeners.
466         /// </summary>
467         /// <param name="view">The View that is affected by this TriggerableSelector.</param>
468         [EditorBrowsable(EditorBrowsableState.Never)]
469         public void Reset(View view)
470         {
471             if (view != null)
472             {
473                 view.ControlStateChangeEventInternal -= OnViewControlState;
474             }
475             selector?.Clear();
476             selector = null;
477             dirty = false;
478         }
479
480         private void OnViewControlState(object obj, View.ControlStateChangedEventArgs controlStateChangedInfo)
481         {
482             View view = obj as View;
483             if (null != view && selector.GetValue(controlStateChangedInfo.CurrentState, out var value))
484             {
485                 view.SetValue(targetBindableProperty, value);
486             }
487         }
488     }
489
490     /// <summary>
491     /// The selector item class that stores a state-value pair.
492     /// </summary>
493     [EditorBrowsable(EditorBrowsableState.Never)]
494     public class SelectorItem<T>
495     {
496         /// <summary>
497         /// The default constructor.
498         /// </summary>
499         [EditorBrowsable(EditorBrowsableState.Never)]
500         public SelectorItem() {}
501
502         /// <summary>
503         /// The constructor with the specified state and value.
504         /// </summary>
505         /// <param name="state">The state</param>
506         /// <param name="value">The value associated with state.</param>
507         [EditorBrowsable(EditorBrowsableState.Never)]
508         public SelectorItem(ControlState state, T value)
509         {
510             State = state;
511             Value = value;
512         }
513
514         /// <summary>
515         /// The state
516         /// </summary>
517         [EditorBrowsable(EditorBrowsableState.Never)]
518         public ControlState State { get; set; }
519
520         /// <summary>
521         /// The value associated with state.
522         /// </summary>
523         [EditorBrowsable(EditorBrowsableState.Never)]
524         public T Value { get; set; }
525
526         ///  <inheritdoc/>
527         [EditorBrowsable(EditorBrowsableState.Never)]
528         public override string ToString() => $"[{State}, {Value}]";
529     }
530
531     /// <summary>
532     /// Extension class for <see cref="Selector{T}"/>.
533     /// </summary>
534     [EditorBrowsable(EditorBrowsableState.Never)]
535     public static class SelectorExtensions
536     {
537         /// <summary>
538         /// Adds the specified state and value to the <see cref="Selector{T}.SelectorItems"/>.
539         /// </summary>
540         /// <param name="list">The list for adding state-value pair.</param>
541         /// <param name="state">The state.</param>
542         /// <param name="value">The value associated with state.</param>
543         /// <exception cref="ArgumentNullException"> Thrown when given list is null. </exception>
544         [EditorBrowsable(EditorBrowsableState.Never)]
545         public static void Add<T>(this IList<SelectorItem<T>> list, ControlState state, T value)
546         {
547             if (list == null) throw new ArgumentNullException(nameof(list));
548
549             list.Add(new SelectorItem<T>(state, value));
550         }
551     }
552 }