[NUI][AT-SPI] Store AccessibilityAttributes in NUI (#4389)
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / Menu.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.Collections.Generic;
19 using System.ComponentModel;
20 using Tizen.NUI.BaseComponents;
21
22 namespace Tizen.NUI.Components
23 {
24     /// <summary>
25     /// Menu is a class which contains a set of MenuItems and has one of them selected.
26     /// </summary>
27     /// <since_tizen> 9 </since_tizen>
28     public partial class Menu : Control
29     {
30         private Window window = null;
31         private Layer layer = null;
32         private View content = null;
33         private View scrim = null;
34         private View anchor = null;
35         private IEnumerable<MenuItem> menuItems = null;
36         private MenuItemGroup menuItemGroup = null;
37         private RelativePosition horizontalPosition = RelativePosition.Center;
38         private RelativePosition verticalPosition = RelativePosition.Center;
39         private MenuStyle menuStyle = null;
40         private bool styleApplied = false;
41
42         /// <summary>
43         /// Creates a new instance of Menu.
44         /// </summary>
45         /// <since_tizen> 9 </since_tizen>
46         public Menu() : base()
47         {
48             Initialize();
49         }
50
51         /// <summary>
52         /// Creates a new instance of Menu.
53         /// </summary>
54         /// <param name="style">Creates Menu by special style defined in UX.</param>
55         [EditorBrowsable(EditorBrowsableState.Never)]
56         public Menu(string style) : base(style)
57         {
58             Initialize();
59         }
60
61         /// <inheritdoc/>
62         [EditorBrowsable(EditorBrowsableState.Never)]
63         protected override void Dispose(DisposeTypes type)
64         {
65             if (disposed)
66             {
67                 return;
68             }
69
70             if (type == DisposeTypes.Explicit)
71             {
72                 if (Content != null)
73                 {
74                     if (menuItems != null)
75                     {
76                         foreach (MenuItem menuItem in menuItems)
77                         {
78                             Content.Remove(menuItem);
79                         }
80                     }
81
82                     Utility.Dispose(Content);
83                 }
84
85                 Utility.Dispose(Scrim);
86
87                 menuItemGroup = null;
88
89                 layer.Remove(this);
90                 Window.RemoveLayer(layer);
91                 layer.Dispose();
92             }
93
94             base.Dispose(type);
95         }
96
97         /// <summary>
98         /// Applies style to MenuItem.
99         /// </summary>
100         /// <param name="viewStyle">The style to apply.</param>
101         [EditorBrowsable(EditorBrowsableState.Never)]
102         public override void ApplyStyle(ViewStyle viewStyle)
103         {
104             styleApplied = false;
105
106             base.ApplyStyle(viewStyle);
107
108             menuStyle = viewStyle as MenuStyle;
109             if (menuStyle != null)
110             {
111                 Content?.ApplyStyle(menuStyle.Content);
112             }
113
114             styleApplied = true;
115         }
116
117         /// <summary>The Menu's relative position to Anchor.</summary>
118         /// <since_tizen> 9 </since_tizen>
119         public enum RelativePosition
120         {
121             /// <summary>
122             /// At the start of the Anchor.
123             /// If this is used with <see cref="HorizontalPositionToAnchor"/>, then Menu is positioned to the left (LTR) of the Anchor or right (RTL) of the Anchor.
124             /// If this is used with <see cref="VerticalPositionToAnchor"/>, then Menu is positioned to the top of the Anchor.
125             ///</summary>
126             /// <since_tizen> 9 </since_tizen>
127             Start = 0,
128             /// <summary>
129             /// At the center of the Anchor.
130             /// If this is used with <see cref="HorizontalPositionToAnchor"/> or <see cref="VerticalPositionToAnchor"/>, then Menu is positioned to the middle of the Anchor.
131             /// </summary>
132             /// <since_tizen> 9 </since_tizen>
133             Center = 1,
134             /// <summary>
135             /// At the end of the Anchor.
136             /// If this is used with <see cref="HorizontalPositionToAnchor"/>, then Menu is positioned to the right (LTR) of the Anchor or left (RTL) of the Anchor.
137             /// If this is used with <see cref="VerticalPositionToAnchor"/>, then Menu is positioned to the bottom of the Anchor.
138             /// </summary>
139             /// <since_tizen> 9 </since_tizen>
140             End = 2,
141         }
142
143         /// <summary>
144         /// Menu items in Menu.
145         /// Menu items are not automatically disposed when Menu is disposed.
146         /// Therefore, please dispose Menu items when you dispose Menu.
147         /// </summary>
148         /// <since_tizen> 9 </since_tizen>
149         public IEnumerable<MenuItem> Items
150         {
151             get
152             {
153                 return menuItems;
154             }
155
156             set
157             {
158                 if (menuItems != null)
159                 {
160                     foreach (var oldItem in menuItems)
161                     {
162                         if (content.Children?.Contains(oldItem) == true)
163                         {
164                             content.Remove(oldItem);
165                         }
166                     }
167                 }
168
169                 menuItems = value;
170
171                 if (menuItems == null)
172                 {
173                     return;
174                 }
175
176                 foreach (var item in menuItems)
177                 {
178                     content.Add(item);
179                     menuItemGroup.Add(item);
180                 }
181             }
182         }
183
184         /// <summary>
185         /// Anchor of Menu.
186         /// Menu is displayed at the anchor's position.
187         /// If there is not enough space to display menu at the anchor's position,
188         /// then menu is displayed at the proper position near anchor's position.
189         /// </summary>
190         /// <since_tizen> 9 </since_tizen>
191         public View Anchor
192         {
193             get
194             {
195                 return GetValue(AnchorProperty) as View;
196             }
197             set
198             {
199                 SetValue(AnchorProperty, value);
200                 NotifyPropertyChanged();
201             }
202         }
203         private View InternalAnchor
204         {
205             get
206             {
207                 return anchor;
208             }
209
210             set
211             {
212                 if (anchor == value)
213                 {
214                     return;
215                 }
216
217                 anchor = value;
218                 if (anchor == null)
219                 {
220                     return;
221                 }
222
223                 CalculateSizeAndPosition();
224             }
225         }
226
227         /// <summary>
228         /// The horizontal position of Menu relative to Anchor.
229         /// If Anchor is not set, then RelativePosition does not work.
230         /// If RelativePosition is Start, then Menu is displayed at the start of Anchor.
231         /// If RelativePosition is Center, then Menu is displayed at the center of Anchor.
232         /// If RelativePosition is End, then Menu is displayed at the end of Anchor.
233         /// If there is not enough space to display menu at the anchor's position,
234         /// then menu is displayed at the proper position near anchor's position.
235         /// </summary>
236         /// <since_tizen> 9 </since_tizen>
237         public RelativePosition HorizontalPositionToAnchor
238         {
239             get
240             {
241                 return (RelativePosition)GetValue(HorizontalPositionToAnchorProperty);
242             }
243             set
244             {
245                 SetValue(HorizontalPositionToAnchorProperty, value);
246                 NotifyPropertyChanged();
247             }
248         }
249         private RelativePosition InternalHorizontalPositionToAnchor
250         {
251             get
252             {
253                 return horizontalPosition;
254             }
255
256             set
257             {
258                 if (horizontalPosition == value)
259                 {
260                     return;
261                 }
262
263                 horizontalPosition = value;
264
265                 CalculateSizeAndPosition();
266             }
267         }
268
269         /// <summary>
270         /// The vertical position of Menu relative to Anchor.
271         /// If Anchor is not set, then RelativePosition does not work.
272         /// If RelativePosition is Start, then Menu is displayed at the start of Anchor.
273         /// If RelativePosition is Center, then Menu is displayed at the center of Anchor.
274         /// If RelativePosition is End, then Menu is displayed at the end of Anchor.
275         /// If there is not enough space to display menu at the anchor's position,
276         /// then menu is displayed at the proper position near anchor's position.
277         /// </summary>
278         /// <since_tizen> 9 </since_tizen>
279         public RelativePosition VerticalPositionToAnchor
280         {
281             get
282             {
283                 return (RelativePosition)GetValue(VerticalPositionToAnchorProperty);
284             }
285             set
286             {
287                 SetValue(VerticalPositionToAnchorProperty, value);
288                 NotifyPropertyChanged();
289             }
290         }
291         private RelativePosition InternalVerticalPositionToAnchor
292         {
293             get
294             {
295                 return verticalPosition;
296             }
297
298             set
299             {
300                 if (verticalPosition == value)
301                 {
302                     return;
303                 }
304
305                 verticalPosition = value;
306
307                 CalculateSizeAndPosition();
308             }
309         }
310
311         /// <summary>
312         /// Gets Menu style.
313         /// </summary>
314         /// <returns>The default Menu style.</returns>
315         [EditorBrowsable(EditorBrowsableState.Never)]
316         protected override ViewStyle CreateViewStyle()
317         {
318             return new MenuStyle();
319         }
320
321         /// <summary>
322         /// Content of Menu.
323         /// </summary>
324         [EditorBrowsable(EditorBrowsableState.Never)]
325         protected View Content
326         {
327             get
328             {
329                 return content;
330             }
331             set
332             {
333                 if (content == value)
334                 {
335                     return;
336                 }
337
338                 if (content != null)
339                 {
340                     Remove(content);
341                 }
342
343                 content = value;
344                 if (content == null)
345                 {
346                     return;
347                 }
348
349                 Add(content);
350
351                 if (Scrim != null)
352                 {
353                     content.RaiseAbove(Scrim);
354                 }
355             }
356         }
357
358         /// <summary>
359         /// Scrim of Menu.
360         /// Scrim is the screen region outside Menu.
361         /// If Scrim is touched, then Menu is dismissed.
362         /// </summary>
363         [EditorBrowsable(EditorBrowsableState.Never)]
364         protected View Scrim
365         {
366             get
367             {
368                 return scrim;
369             }
370             set
371             {
372                 if (scrim == value)
373                 {
374                     return;
375                 }
376
377                 if (scrim != null)
378                 {
379                     Remove(scrim);
380                 }
381
382                 scrim = value;
383                 if (scrim == null)
384                 {
385                     return;
386                 }
387
388                 Add(scrim);
389
390                 if (Content != null)
391                 {
392                     Content.RaiseAbove(scrim);
393                 }
394             }
395         }
396
397         private Window Window
398         {
399             get
400             {
401                 if (window == null)
402                 {
403                     window = NUIApplication.GetDefaultWindow();
404                 }
405
406                 return window;
407             }
408             set
409             {
410                 if (window == value)
411                 {
412                     return;
413                 }
414
415                 window = value;
416             }
417         }
418
419         /// <summary>
420         /// Post the Menu.
421         /// The Menu is displayed.
422         /// </summary>
423         /// <param name="window">The Window where Menu is displayed.</param>
424         /// <since_tizen> 9 </since_tizen>
425         public void Post(Window window = null)
426         {
427             if (window == null)
428             {
429                 window = NUIApplication.GetDefaultWindow();
430             }
431
432             Window = window;
433
434             Window.AddLayer(layer);
435             layer.RaiseToTop();
436
437             CalculateSizeAndPosition();
438             RegisterDefaultLabel();
439             NotifyAccessibilityStatesChange(new AccessibilityStates(AccessibilityState.Visible, AccessibilityState.Showing), AccessibilityStatesNotifyMode.Recursive);
440         }
441
442         /// <summary>
443         /// Dismiss the Menu.
444         /// The Menu becomes hidden and disposed.
445         /// </summary>
446         /// <since_tizen> 9 </since_tizen>
447         public void Dismiss()
448         {
449             Hide();
450             UnregisterDefaultLabel();
451             NotifyAccessibilityStatesChange(new AccessibilityStates(AccessibilityState.Visible, AccessibilityState.Showing), AccessibilityStatesNotifyMode.Recursive);
452             Dispose();
453         }
454
455         /// <inheritdoc/>
456         [EditorBrowsable(EditorBrowsableState.Never)]
457         public override void OnRelayout(Vector2 size, RelayoutContainer container)
458         {
459             base.OnRelayout(size, container);
460
461             CalculateSizeAndPosition();
462         }
463
464         private void Initialize()
465         {
466             Layout = new AbsoluteLayout();
467
468             WidthSpecification = LayoutParamPolicies.WrapContent;
469             HeightSpecification = LayoutParamPolicies.WrapContent;
470
471             BackgroundColor = Color.Transparent;
472
473             // Menu is added to Anchor so Menu should exclude layouting because
474             // if Anchor has Layout, then Menu is displayed at an incorrect position.
475             ExcludeLayouting = true;
476
477             Content = CreateDefaultContent();
478             if (styleApplied && (menuStyle != null))
479             {
480                 Content.ApplyStyle(menuStyle.Content);
481             }
482
483             Scrim = CreateDefaultScrim();
484
485             menuItemGroup = new MenuItemGroup();
486
487             layer = new Layer();
488             layer.Add(this);
489         }
490
491         private ScrollableBase CreateDefaultContent()
492         {
493             return new ScrollableBase()
494             {
495                 Layout = new LinearLayout()
496                 {
497                     LinearOrientation = LinearLayout.Orientation.Vertical,
498                 },
499                 ScrollingDirection = ScrollableBase.Direction.Vertical,
500                 ScrollEnabled = true,
501                 HideScrollbar = false,
502                 ClippingMode = ClippingModeType.ClipChildren,
503             };
504         }
505
506         private View CreateDefaultScrim()
507         {
508             var scrim = new VisualView()
509             {
510                 // Scrim is added to Menu so Scrim should exclude layouting
511                 // not to enlarge Menu size.
512                 ExcludeLayouting = true,
513                 BackgroundColor = Color.Transparent,
514                 Size = new Size(NUIApplication.GetDefaultWindow().Size),
515             };
516
517             scrim.TouchEvent += (object source, TouchEventArgs e) =>
518             {
519                 if (e.Touch.GetState(0) == PointStateType.Up)
520                 {
521                     this.Dismiss();
522                 }
523                 return true;
524             };
525
526             return scrim;
527         }
528
529         private void CalculateSizeAndPosition()
530         {
531             CalculateMenuSize();
532
533             CalculateMenuPosition();
534
535             CalculateScrimPosition();
536         }
537
538         // Calculate menu's size based on content's size
539         private void CalculateMenuSize()
540         {
541             if (Content == null)
542             {
543                 return;
544             }
545             if (Size.Equals(Content.Size) == false)
546             {
547                 Size = new Size(Content.SizeWidth, Content.SizeHeight);
548             }
549         }
550
551         private View GetRootView()
552         {
553             View root = this;
554             View parent = GetParent() as View;
555
556             while (parent)
557             {
558                 root = parent;
559                 parent = parent?.GetParent() as View;
560             }
561
562             return root;
563         }
564
565         // Calculate menu's position based on Anchor and parent's positions.
566         // If there is not enought space, then menu's size can be also resized.
567         private void CalculateMenuPosition()
568         {
569             if ((Anchor == null) || (Content == null))
570             {
571                 return;
572             }
573
574             if (Items == null)
575             {
576                 return;
577             }
578
579             if (SizeWidth.Equals(0) && SizeHeight.Equals(0))
580             {
581                 return;
582             }
583
584             float menuScreenPosX = 0;
585             float menuScreenPosY = 0;
586
587             if (HorizontalPositionToAnchor == RelativePosition.Start)
588             {
589                 if (GetRootView().LayoutDirection == ViewLayoutDirectionType.LTR)
590                 {
591                     menuScreenPosX = Anchor.ScreenPosition.X - SizeWidth;
592                 }
593                 else
594                 {
595                     menuScreenPosX = Anchor.ScreenPosition.X + Anchor.Margin.Start + Anchor.SizeWidth + Anchor.Margin.End;
596                 }
597             }
598             else if (HorizontalPositionToAnchor == RelativePosition.Center)
599             {
600                 menuScreenPosX = Anchor.ScreenPosition.X + Anchor.Margin.Start + (Anchor.SizeWidth / 2) - (SizeWidth / 2);
601             }
602             else
603             {
604                 if (GetRootView().LayoutDirection == ViewLayoutDirectionType.LTR)
605                 {
606                     menuScreenPosX = Anchor.ScreenPosition.X + Anchor.Margin.Start + Anchor.SizeWidth + Anchor.Margin.End;
607                 }
608                 else
609                 {
610                     menuScreenPosX = Anchor.ScreenPosition.X - SizeWidth;
611                 }
612             }
613
614             if (VerticalPositionToAnchor == RelativePosition.Start)
615             {
616                 menuScreenPosY = Anchor.ScreenPosition.Y - SizeHeight;
617             }
618             else if (VerticalPositionToAnchor == RelativePosition.Center)
619             {
620                 menuScreenPosY = Anchor.ScreenPosition.Y + Anchor.Margin.Top + (Anchor.SizeHeight / 2) - (SizeHeight / 2);
621             }
622             else
623             {
624                 menuScreenPosY = Anchor.ScreenPosition.Y + Anchor.Margin.Top + Anchor.SizeHeight + Anchor.Margin.Bottom;
625             }
626
627             float menuSizeW = SizeWidth;
628             float menuSizeH = SizeHeight;
629
630             // Check if menu is not inside parent's boundary in x coordinate system.
631             if (menuScreenPosX + SizeWidth > Window.Size.Width)
632             {
633                 if (HorizontalPositionToAnchor == RelativePosition.Center)
634                 {
635                     menuScreenPosX = Window.Size.Width - SizeWidth;
636                 }
637                 else
638                 {
639                     menuSizeW = Window.Size.Width - menuScreenPosX;
640                 }
641             }
642             if (menuScreenPosX < 0)
643             {
644                 menuScreenPosX = 0;
645                 menuSizeW = Window.Size.Width;
646             }
647
648             // Check if menu is not inside parent's boundary in y coordinate system.
649             if (menuScreenPosY + SizeHeight > Window.Size.Height)
650             {
651                 if (VerticalPositionToAnchor == RelativePosition.Center)
652                 {
653                     menuScreenPosY = Window.Size.Height - SizeHeight;
654                 }
655                 else
656                 {
657                     menuSizeH = Window.Size.Height - menuScreenPosY;
658                 }
659             }
660             if (menuScreenPosY < 0)
661             {
662                 menuScreenPosY = 0;
663                 menuSizeH = Window.Size.Height;
664             }
665
666             // Position is relative to parent's coordinate system.
667             var menuPosX = menuScreenPosX;
668             var menuPosY = menuScreenPosY;
669
670             if (!PositionX.Equals(menuPosX) || !PositionY.Equals(menuPosY) || !SizeWidth.Equals(menuSizeW) || !SizeHeight.Equals(menuSizeH))
671             {
672                 Position = new Position(menuPosX, menuPosY);
673                 Size = new Size(menuSizeW, menuSizeH);
674             }
675         }
676
677         // Calculate scrim's position based on menu's position
678         private void CalculateScrimPosition()
679         {
680             if (Scrim == null)
681             {
682                 return;
683             }
684
685             // Menu's Position should be updated before doing this calculation.
686             if (!Scrim.PositionX.Equals(-PositionX) || !Scrim.PositionY.Equals(-PositionY))
687             {
688                 Scrim.Position = new Position(-PositionX, -PositionY);
689             }
690         }
691
692         /// <summary>
693         /// Initialize AT-SPI object.
694         /// </summary>
695         [EditorBrowsable(EditorBrowsableState.Never)]
696         public override void OnInitialize()
697         {
698             base.OnInitialize();
699             AccessibilityRole = Role.PopupMenu;
700             AccessibilityAttributes["sub-role"] = "Alert";
701         }
702
703         /// <summary>
704         /// Informs AT-SPI bridge about the set of AT-SPI states associated with this object.
705         /// </summary>
706         [EditorBrowsable(EditorBrowsableState.Never)]
707         protected override AccessibilityStates AccessibilityCalculateStates()
708         {
709             var states = base.AccessibilityCalculateStates();
710
711             states[AccessibilityState.Modal] = true;
712
713             return states;
714         }
715     }
716 }