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