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