[NUI] Fix Pagination bug
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / Pagination.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
18 using System;
19 using System.Collections.Generic;
20 using System.ComponentModel;
21 using System.Diagnostics;
22 using Tizen.NUI.Accessibility;
23 using Tizen.NUI.BaseComponents;
24 using Tizen.NUI.Binding;
25
26 namespace Tizen.NUI.Components
27 {
28     /// <summary>
29     /// Pagination shows the number of pages available and the currently active page.
30     /// </summary>
31     /// <since_tizen> 8 </since_tizen>
32     public partial class Pagination : Control, IAtspiValue
33     {
34         /// <summary>The IndicatorSize bindable property.</summary>
35         [EditorBrowsable(EditorBrowsableState.Never)]
36         public static readonly BindableProperty IndicatorSizeProperty = BindableProperty.Create(nameof(IndicatorSize), typeof(Size), typeof(Pagination), null, propertyChanged: (bindable, oldValue, newValue) =>
37         {
38             if (newValue != null)
39             {
40                 var pagination = (Pagination)bindable;
41                 pagination.indicatorSize = new Size((Size)newValue);
42                 pagination.UpdateVisual();
43                 pagination.UpdateContainer();
44             }
45         },
46         defaultValueCreator: (bindable) =>
47         {
48             return ((Pagination)bindable).indicatorSize;
49         });
50
51         /// <summary>The IndicatorImageUrlSelector bindable property.</summary>
52         [EditorBrowsable(EditorBrowsableState.Never)]
53         public static readonly BindableProperty IndicatorImageUrlProperty = BindableProperty.Create(nameof(IndicatorImageUrl), typeof(Selector<string>), typeof(Pagination), null, propertyChanged: (bindable, oldValue, newValue) =>
54         {
55             var pagination = (Pagination)bindable;
56             pagination.indicatorImageUrl = ((Selector<string>)newValue)?.Clone();
57             pagination.UpdateVisual();
58         },
59         defaultValueCreator: (bindable) =>
60         {
61             return ((Pagination)bindable).indicatorImageUrl;
62         });
63
64         /// <summary>The IndicatorSpacing bindable property.</summary>
65         [EditorBrowsable(EditorBrowsableState.Never)]
66         public static readonly BindableProperty IndicatorSpacingProperty = BindableProperty.Create(nameof(IndicatorSpacing), typeof(int), typeof(Pagination), default(int), propertyChanged: (bindable, oldValue, newValue) =>
67         {
68             var pagination = (Pagination)bindable;
69             pagination.indicatorSpacing = (int)newValue;
70             pagination.UpdateVisual();
71         },
72         defaultValueCreator: (bindable) =>
73         {
74             return ((Pagination)bindable).indicatorSpacing;
75         });
76
77         // Depending on Tizen 7.0 Pagination UX guide
78         private const int DefaultIndicatorWidth = 64;
79         private const int DefaultIndicatorHeight = 8;
80         private const int DefaultIndicatorSpacing = 16;
81         private const string DefaultIndicatorColor = "#FAFAFA";
82         private const string DefaultSelectedIndicatorColor = "#FFA166";
83
84         private View container;
85         private Size indicatorSize = new Size(DefaultIndicatorWidth, DefaultIndicatorHeight);
86         private Selector<string> indicatorImageUrl;
87         private int indicatorSpacing = DefaultIndicatorSpacing;
88         private List<ImageView> indicatorList = new List<ImageView>();
89
90         private int indicatorCount = 0;
91         private int selectedIndex = 0;
92
93         private Color indicatorColor;
94         private Color selectedIndicatorColor;
95         private Selector<string> lastIndicatorImageUrl;
96
97         static Pagination() { }
98
99         /// <summary>
100         /// Creates a new instance of a Pagination.
101         /// </summary>
102         /// <since_tizen> 8 </since_tizen>
103         public Pagination() : base()
104         {
105         }
106
107         /// <summary>
108         /// Creates a new instance of a Pagination using style.
109         /// </summary>
110         /// <param name="style">The string to initialize the Pagination</param>
111         /// <since_tizen> 8 </since_tizen>
112         public Pagination(string style) : base(style)
113         {
114         }
115
116         /// <summary>
117         /// Creates a new instance of a Pagination using style.
118         /// </summary>
119         /// <param name="paginationStyle">The style object to initialize the Pagination</param>
120         /// <since_tizen> 8 </since_tizen>
121         public Pagination(PaginationStyle paginationStyle) : base(paginationStyle)
122         {
123         }
124
125         /// <summary>
126         /// Return currently applied style.
127         /// </summary>
128         /// <remarks>
129         /// Modifying contents in style may cause unexpected behaviour.
130         /// </remarks>
131         /// <since_tizen> 8 </since_tizen>
132         public PaginationStyle Style => (PaginationStyle)(ViewStyle as PaginationStyle)?.Clone();
133
134         /// <summary>
135         /// Gets or sets the size of the indicator.
136         /// </summary>
137         /// <since_tizen> 8 </since_tizen>
138         public Size IndicatorSize
139         {
140             get => (Size)GetValue(IndicatorSizeProperty);
141             set => SetValue(IndicatorSizeProperty, value);
142         }
143
144         /// <summary>
145         /// Gets or sets the background resource of indicator.
146         /// </summary>
147         /// <since_tizen> 8 </since_tizen>
148         public Selector<string> IndicatorImageUrl
149         {
150             get => (Selector<string>)GetValue(IndicatorImageUrlProperty);
151             set => SetValue(IndicatorImageUrlProperty, value);
152         }
153
154         /// <summary>
155         /// This is experimental API.
156         /// Make the last indicator has exceptional image, not common image in the Pagination.
157         /// </summary>
158         [EditorBrowsable(EditorBrowsableState.Never)]
159         public Selector<string> LastIndicatorImageUrl
160         {
161             get
162             {
163                 return GetValue(LastIndicatorImageUrlProperty) as Selector<string>;
164             }
165             set
166             {
167                 SetValue(LastIndicatorImageUrlProperty, value);
168                 NotifyPropertyChanged();
169             }
170         }
171         private Selector<string> InternalLastIndicatorImageUrl
172         {
173             get => lastIndicatorImageUrl;
174             set
175             {
176                 lastIndicatorImageUrl = value;
177                 if (value != null && indicatorCount > 0)
178                 {
179                     indicatorList[LastIndicatorIndex].ResourceUrl = IsLastSelected ? value.Selected : value.Normal;
180                 }
181             }
182         }
183
184         /// <summary>
185         /// Gets or sets the space of the indicator.
186         /// </summary>
187         /// <since_tizen> 8 </since_tizen>
188         public int IndicatorSpacing
189         {
190             get => (int)GetValue(IndicatorSpacingProperty);
191             set => SetValue(IndicatorSpacingProperty, value);
192         }
193
194
195         /// <summary>
196         /// Gets or sets the count of the pages/indicators.
197         /// </summary>
198         /// <since_tizen> 8 </since_tizen>
199         /// <exception cref="ArgumentException">Thrown when the given value is negative.</exception>
200         public int IndicatorCount
201         {
202             get
203             {
204                 return (int)GetValue(IndicatorCountProperty);
205             }
206             set
207             {
208                 SetValue(IndicatorCountProperty, value);
209                 NotifyPropertyChanged();
210             }
211         }
212         private int InternalIndicatorCount
213         {
214             get
215             {
216                 return indicatorCount;
217             }
218             set
219             {
220                 if (value < 0)
221                 {
222                     throw new ArgumentException($"Setting {nameof(IndicatorCount)} to negative is not allowed.");
223                 }
224
225                 if (indicatorCount == value)
226                 {
227                     return;
228                 }
229
230                 int prevLastIndex = -1;
231
232                 if (indicatorCount < value)
233                 {
234                     prevLastIndex = LastIndicatorIndex;
235                     for (int i = indicatorCount; i < value; i++)
236                     {
237                         CreateIndicator(i);
238                     }
239                 }
240                 else
241                 {
242                     for (int i = value; i < indicatorCount; i++)
243                     {
244                         container.Remove(indicatorList[i]);
245                     }
246                     indicatorList.RemoveRange(value, indicatorCount - value);
247
248                     if (selectedIndex >= value)
249                     {
250                         selectedIndex = Math.Max(0, value - 1);
251
252                         if (value > 0)
253                         {
254                             UpdateSelectedIndicator(indicatorList[selectedIndex]);
255                         }
256                     }
257                 }
258                 indicatorCount = value;
259
260                 if (lastIndicatorImageUrl != null && indicatorImageUrl != null && indicatorCount > 0)
261                 {
262                     if (prevLastIndex >= 0)
263                     {
264                         indicatorList[prevLastIndex].ResourceUrl = prevLastIndex == selectedIndex ? indicatorImageUrl.Selected : indicatorImageUrl.Normal;
265                     }
266                     indicatorList[LastIndicatorIndex].ResourceUrl = IsLastSelected ? lastIndicatorImageUrl.Selected : lastIndicatorImageUrl.Normal;
267                 }
268
269                 UpdateContainer();
270             }
271         }
272
273         private void OnIndicatorColorChanged(float r, float g, float b, float a)
274         {
275             IndicatorColor = new Color(r, g, b, a);
276         }
277
278         /// <summary>
279         /// Color of the indicator.
280         /// </summary>
281         /// <since_tizen> 8 </since_tizen>
282         public Color IndicatorColor
283         {
284             get
285             {
286                 return GetValue(IndicatorColorProperty) as Color;
287             }
288             set
289             {
290                 SetValue(IndicatorColorProperty, value);
291                 NotifyPropertyChanged();
292             }
293         }
294         private Color InternalIndicatorColor
295         {
296             get
297             {
298                 return new Color(OnIndicatorColorChanged, indicatorColor);
299             }
300             set
301             {
302                 if (value == null)
303                 {
304                     return;
305                 }
306
307                 if (indicatorColor == null)
308                 {
309                     indicatorColor = new Color((Color)value);
310                 }
311                 else
312                 {
313                     if (indicatorColor == value)
314                     {
315                         return;
316                     }
317
318                     indicatorColor = value;
319                 }
320
321                 if (indicatorCount == 0)
322                 {
323                     return;
324                 }
325
326                 for (int i = 0; i < indicatorCount; i++)
327                 {
328                     if (i == selectedIndex)
329                     {
330                         continue;
331                     }
332
333                     indicatorList[i].Color = indicatorColor;
334                 }
335             }
336         }
337
338         private void OnSelectedIndicatorColorChanged(float r, float g, float b, float a)
339         {
340             SelectedIndicatorColor = new Color(r, g, b, a);
341         }
342
343         /// <summary>
344         /// Color of the selected indicator.
345         /// </summary>
346         /// <since_tizen> 8 </since_tizen>
347         public Color SelectedIndicatorColor
348         {
349             get
350             {
351                 return GetValue(SelectedIndicatorColorProperty) as Color;
352             }
353             set
354             {
355                 SetValue(SelectedIndicatorColorProperty, value);
356                 NotifyPropertyChanged();
357             }
358         }
359         private Color InternalSelectedIndicatorColor
360         {
361             get
362             {
363                 return new Color(OnSelectedIndicatorColorChanged, selectedIndicatorColor);
364             }
365             set
366             {
367                 if (value == null)
368                 {
369                     return;
370                 }
371
372                 if (selectedIndicatorColor == null)
373                 {
374                     selectedIndicatorColor = new Color((Color)value);
375                 }
376                 else
377                 {
378                     if (selectedIndicatorColor == value)
379                     {
380                         return;
381                     }
382
383                     selectedIndicatorColor = value;
384                 }
385
386                 if (indicatorList.Count > selectedIndex)
387                 {
388                     indicatorList[selectedIndex].Color = selectedIndicatorColor;
389                 }
390             }
391         }
392
393         /// <summary>
394         /// Gets or sets the index of the select indicator.
395         /// </summary>
396         /// <since_tizen> 8 </since_tizen>
397         public int SelectedIndex
398         {
399             get
400             {
401                 return (int)GetValue(SelectedIndexProperty);
402             }
403             set
404             {
405                 SetValue(SelectedIndexProperty, value);
406                 NotifyPropertyChanged();
407             }
408         }
409         private int InternalSelectedIndex
410         {
411             get
412             {
413                 return selectedIndex;
414             }
415             set
416             {
417                 var refinedValue = Math.Max(0, Math.Min(value, indicatorCount - 1));
418
419                 if (selectedIndex == refinedValue)
420                 {
421                     return;
422                 }
423
424                 Debug.Assert(refinedValue >= 0 && refinedValue < indicatorCount);
425                 Debug.Assert(selectedIndex >= 0 && selectedIndex < indicatorCount);
426
427                 UpdateUnselectedIndicator(indicatorList[selectedIndex]);
428
429                 selectedIndex = refinedValue;
430
431                 UpdateSelectedIndicator(indicatorList[selectedIndex]);
432
433                 if (Accessibility.Accessibility.IsEnabled && IsHighlighted)
434                 {
435                     EmitAccessibilityEvent(AccessibilityPropertyChangeEvent.Value);
436                 }
437             }
438         }
439
440         /// <summary>
441         /// Retrieves the position of a indicator by index.
442         /// </summary>
443         /// <param name="index">Indicator index</param>
444         /// <returns>The position of a indicator by index.</returns>
445         /// <since_tizen> 8 </since_tizen>
446         public Position GetIndicatorPosition(int index)
447         {
448             if (index < 0 || index >= indicatorList.Count)
449             {
450                 return null;
451             }
452             return new Position(indicatorList[index].Position.X + container.PositionX, indicatorList[index].Position.Y + container.PositionY);
453         }
454
455         /// <summary>
456         /// Minimum value.
457         /// </summary>
458         [EditorBrowsable(EditorBrowsableState.Never)]
459         double IAtspiValue.AccessibilityGetMinimum()
460         {
461             return 0.0;
462         }
463
464         /// <summary>
465         /// Current value.
466         /// </summary>
467         [EditorBrowsable(EditorBrowsableState.Never)]
468         double IAtspiValue.AccessibilityGetCurrent()
469         {
470             return (double)SelectedIndex;
471         }
472
473         /// <summary>
474         /// Maximum value.
475         /// </summary>
476         [EditorBrowsable(EditorBrowsableState.Never)]
477         double IAtspiValue.AccessibilityGetMaximum()
478         {
479             return (double)IndicatorCount;
480         }
481
482         /// <summary>
483         /// Current value.
484         /// </summary>
485         [EditorBrowsable(EditorBrowsableState.Never)]
486         bool IAtspiValue.AccessibilitySetCurrent(double value)
487         {
488             int integerValue = (int)value;
489
490             if (integerValue >= 0 && integerValue <= IndicatorCount)
491             {
492                 SelectedIndex = integerValue;
493                 return true;
494             }
495
496             return false;
497         }
498
499         /// <summary>
500         /// Minimum increment.
501         /// </summary>
502         [EditorBrowsable(EditorBrowsableState.Never)]
503         double IAtspiValue.AccessibilityGetMinimumIncrement()
504         {
505             return 1.0;
506         }
507
508         /// <inheritdoc/>
509         [EditorBrowsable(EditorBrowsableState.Never)]
510         public override void OnInitialize()
511         {
512             base.OnInitialize();
513             AccessibilityRole = Role.ScrollBar;
514             AccessibilityHighlightable = true;
515             AccessibilityAttributes["style"] = "pagecontrolbyvalue";
516
517             container = new View()
518             {
519                 Name = "Container",
520                 ParentOrigin = Tizen.NUI.ParentOrigin.CenterLeft,
521                 PivotPoint = Tizen.NUI.PivotPoint.CenterLeft,
522                 PositionUsesPivotPoint = true,
523                 Layout = new LinearLayout()
524                 {
525                     LinearOrientation = LinearLayout.Orientation.Horizontal,
526                     CellPadding = new Size2D(indicatorSpacing, 0),
527                 },
528                 Padding = new Extents(8, 8, 0, 0),
529             };
530             this.Add(container);
531
532             //TODO: Apply color properties from PaginationStyle class.
533             indicatorColor = new Color(DefaultIndicatorColor);
534             selectedIndicatorColor = new Color(DefaultSelectedIndicatorColor);
535         }
536
537         /// <summary>
538         /// You can override it to do your select out operation.
539         /// </summary>
540         /// <param name="selectOutIndicator">The indicator will be selected out</param>
541         /// <since_tizen> 8 </since_tizen>
542         protected virtual void SelectOut(VisualMap selectOutIndicator)
543         {
544             // Currently, this method is not used in this file anymore.
545             // However, the implementation inside should remain because someone could be used by overriding it.
546             if (!(selectOutIndicator is ImageVisual visual)) return;
547             visual.URL = ((IsLastSelected && lastIndicatorImageUrl != null) ? lastIndicatorImageUrl : indicatorImageUrl)?.Normal;
548
549             if (indicatorColor == null)
550             {
551                 visual.MixColor = new Color(DefaultIndicatorColor);
552                 visual.Opacity = 1.0f;
553             }
554             else
555             {
556                 visual.MixColor = indicatorColor;
557                 visual.Opacity = indicatorColor.A;
558             }
559         }
560
561         /// <summary>
562         /// You can override it to do your select in operation.
563         /// </summary>
564         /// <param name="selectInIndicator">The indicator will be selected in</param>
565         /// <since_tizen> 8 </since_tizen>
566         protected virtual void SelectIn(VisualMap selectInIndicator)
567         {
568             // Currently, this method is not used in this file anymore.
569             // However, the implementation inside should remain because someone could be used by overriding it.
570             if (!(selectInIndicator is ImageVisual visual)) return;
571             visual.URL = ((IsLastSelected && lastIndicatorImageUrl != null) ? lastIndicatorImageUrl : indicatorImageUrl)?.Selected;
572
573             if (selectedIndicatorColor == null)
574             {
575                 visual.MixColor = new Color(DefaultSelectedIndicatorColor);
576                 visual.Opacity = 1.0f;
577             }
578             else
579             {
580                 visual.MixColor = selectedIndicatorColor;
581                 visual.Opacity = selectedIndicatorColor.A;
582             }
583         }
584
585         /// <summary>
586         /// you can override it to create your own default style.
587         /// </summary>
588         /// <returns>The default pagination style.</returns>
589         /// <since_tizen> 8 </since_tizen>
590         protected override ViewStyle CreateViewStyle()
591         {
592             return new PaginationStyle();
593         }
594
595         /// <summary>
596         /// you can override it to clean-up your own resources.
597         /// </summary>
598         /// <param name="type">DisposeTypes</param>
599         /// <since_tizen> 8 </since_tizen>
600         protected override void Dispose(DisposeTypes type)
601         {
602             if (disposed)
603             {
604                 return;
605             }
606
607             if (type == DisposeTypes.Explicit)
608             {
609                 for (int i = 0; i < indicatorCount; i++)
610                 {
611                     container.Remove(indicatorList[i]);
612                 }
613                 indicatorList.Clear();
614
615                 this.Remove(container);
616                 container.Dispose();
617                 container = null;
618             }
619
620             base.Dispose(type);
621         }
622
623         private void UpdateUnselectedIndicator(ImageView indicator)
624         {
625             if (indicator == null) return;
626
627             if (IsLastSelected && lastIndicatorImageUrl != null)
628             {
629                 indicator.ResourceUrl = lastIndicatorImageUrl.Normal;
630             }
631             else
632             {
633                 indicator.ResourceUrl = indicatorImageUrl.Normal;
634             }
635
636             if (indicatorColor == null)
637             {
638                 indicator.Color = new Color(DefaultIndicatorColor);
639             }
640             else
641             {
642                 indicator.Color = indicatorColor;
643             }
644         }
645
646         private void UpdateSelectedIndicator(ImageView indicator)
647         {
648             if (indicator == null) return;
649
650             if (IsLastSelected && lastIndicatorImageUrl != null)
651             {
652                 indicator.ResourceUrl = lastIndicatorImageUrl.Selected;
653             }
654             else
655             {
656                 indicator.ResourceUrl = indicatorImageUrl.Selected;
657             }
658
659             if (indicatorColor == null)
660             {
661                 indicator.Color = new Color(DefaultSelectedIndicatorColor);
662             }
663             else
664             {
665                 indicator.Color = indicatorColor;
666             }            
667         }
668
669         private void CreateIndicator(int index)
670         {
671             Debug.Assert(indicatorSize != null);
672
673             ImageView indicator = new ImageView
674             {
675                 ResourceUrl = indicatorImageUrl?.Normal,
676                 Size = indicatorSize,
677                 Color = (indicatorColor == null) ? new Color(DefaultIndicatorColor) : indicatorColor,
678             };
679             indicatorList.Add(indicator);
680             container.Add(indicator);
681
682             if (index == selectedIndex)
683             {
684                 UpdateSelectedIndicator(indicatorList[selectedIndex]);
685             }
686         }
687
688         private void UpdateContainer()
689         {
690             Debug.Assert(indicatorSize != null);
691
692             if (indicatorList.Count > 0)
693             {
694                 container.SizeWidth = (indicatorSize.Width + indicatorSpacing) * indicatorList.Count - indicatorSpacing;
695             }
696             else
697             {
698                 container.SizeWidth = 0;
699             }
700             container.SizeHeight = indicatorSize.Height;
701             container.PositionX = (int)((this.SizeWidth - container.SizeWidth) / 2);
702         }
703
704         private void UpdateVisual()
705         {
706             Debug.Assert(indicatorSize != null);
707
708             if (indicatorImageUrl == null)
709             {
710                 return;
711             }
712
713             if (container != null && (container.Layout is LinearLayout linearLayout))
714             {
715                 linearLayout.CellPadding = new Size2D(indicatorSpacing, 0);
716             }
717
718             for (int i = 0; i < indicatorList.Count; i++)
719             {
720                 ImageView indicator = indicatorList[i];
721                 indicator.ResourceUrl = selectedIndex == i ? indicatorImageUrl.Selected : indicatorImageUrl.Normal;
722                 indicator.Size = indicatorSize;
723             }
724
725             if (lastIndicatorImageUrl != null && indicatorCount > 0)
726             {
727                 indicatorList[LastIndicatorIndex].ResourceUrl = IsLastSelected ? lastIndicatorImageUrl.Selected : lastIndicatorImageUrl.Normal;
728             }
729         }
730
731         private int LastIndicatorIndex => IndicatorCount - 1;
732         private bool IsLastSelected => LastIndicatorIndex == selectedIndex;
733     }
734 }