[NUI] Modify Menu to help customizing class
[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             CalculateMenuPosition();
532
533             CalculateScrimPosition();
534         }
535
536         private View GetRootView()
537         {
538             View root = this;
539             View parent = GetParent() as View;
540
541             while (parent)
542             {
543                 root = parent;
544                 parent = parent?.GetParent() as View;
545             }
546
547             return root;
548         }
549
550         // Calculate menu's position based on Anchor and parent's positions.
551         // If there is not enought space, then menu's size can be also resized.
552         private void CalculateMenuPosition()
553         {
554             if ((Anchor == null) || (Content == null))
555             {
556                 return;
557             }
558
559             if (Items == null)
560             {
561                 return;
562             }
563
564             if (SizeWidth.Equals(0) && SizeHeight.Equals(0))
565             {
566                 return;
567             }
568
569             float menuScreenPosX = 0;
570             float menuScreenPosY = 0;
571
572             if (HorizontalPositionToAnchor == RelativePosition.Start)
573             {
574                 if (GetRootView().LayoutDirection == ViewLayoutDirectionType.LTR)
575                 {
576                     menuScreenPosX = Anchor.ScreenPosition.X - SizeWidth;
577                 }
578                 else
579                 {
580                     menuScreenPosX = Anchor.ScreenPosition.X + Anchor.Margin.Start + Anchor.SizeWidth + Anchor.Margin.End;
581                 }
582             }
583             else if (HorizontalPositionToAnchor == RelativePosition.Center)
584             {
585                 menuScreenPosX = Anchor.ScreenPosition.X + Anchor.Margin.Start + (Anchor.SizeWidth / 2) - (SizeWidth / 2);
586             }
587             else
588             {
589                 if (GetRootView().LayoutDirection == ViewLayoutDirectionType.LTR)
590                 {
591                     menuScreenPosX = Anchor.ScreenPosition.X + Anchor.Margin.Start + Anchor.SizeWidth + Anchor.Margin.End;
592                 }
593                 else
594                 {
595                     menuScreenPosX = Anchor.ScreenPosition.X - SizeWidth;
596                 }
597             }
598
599             if (VerticalPositionToAnchor == RelativePosition.Start)
600             {
601                 menuScreenPosY = Anchor.ScreenPosition.Y - SizeHeight;
602             }
603             else if (VerticalPositionToAnchor == RelativePosition.Center)
604             {
605                 menuScreenPosY = Anchor.ScreenPosition.Y + Anchor.Margin.Top + (Anchor.SizeHeight / 2) - (SizeHeight / 2);
606             }
607             else
608             {
609                 menuScreenPosY = Anchor.ScreenPosition.Y + Anchor.Margin.Top + Anchor.SizeHeight + Anchor.Margin.Bottom;
610             }
611
612             float menuSizeW = SizeWidth;
613             float menuSizeH = SizeHeight;
614
615             // Check if menu is not inside parent's boundary in x coordinate system.
616             if (menuScreenPosX + SizeWidth > Window.Size.Width)
617             {
618                 if (HorizontalPositionToAnchor == RelativePosition.Center)
619                 {
620                     menuScreenPosX = Window.Size.Width - SizeWidth;
621                 }
622                 else
623                 {
624                     menuSizeW = Window.Size.Width - menuScreenPosX;
625                 }
626             }
627             if (menuScreenPosX < 0)
628             {
629                 menuScreenPosX = 0;
630
631                 if (menuSizeW > Window.Size.Width)
632                 {
633                     menuSizeW = Window.Size.Width;
634                 }
635             }
636
637             // Check if menu is not inside parent's boundary in y coordinate system.
638             if (menuScreenPosY + SizeHeight > Window.Size.Height)
639             {
640                 if (VerticalPositionToAnchor == RelativePosition.Center)
641                 {
642                     menuScreenPosY = Window.Size.Height - SizeHeight;
643                 }
644                 else
645                 {
646                     menuSizeH = Window.Size.Height - menuScreenPosY;
647                 }
648             }
649             if (menuScreenPosY < 0)
650             {
651                 menuScreenPosY = 0;
652
653                 if (menuSizeH > Window.Size.Height)
654                 {
655                     menuSizeH = Window.Size.Height;
656                 }
657             }
658
659             // Position is relative to parent's coordinate system.
660             var menuPosX = menuScreenPosX;
661             var menuPosY = menuScreenPosY;
662
663             if (!PositionX.Equals(menuPosX) || !PositionY.Equals(menuPosY) || !SizeWidth.Equals(menuSizeW) || !SizeHeight.Equals(menuSizeH))
664             {
665                 Position = new Position(menuPosX, menuPosY);
666                 Size = new Size(menuSizeW, menuSizeH);
667             }
668         }
669
670         // Calculate scrim's position based on menu's position
671         private void CalculateScrimPosition()
672         {
673             if (Scrim == null)
674             {
675                 return;
676             }
677
678             // Menu's Position should be updated before doing this calculation.
679             if (!Scrim.PositionX.Equals(-PositionX) || !Scrim.PositionY.Equals(-PositionY))
680             {
681                 Scrim.Position = new Position(-PositionX, -PositionY);
682             }
683         }
684
685         /// <summary>
686         /// Initialize AT-SPI object.
687         /// </summary>
688         [EditorBrowsable(EditorBrowsableState.Never)]
689         public override void OnInitialize()
690         {
691             base.OnInitialize();
692             AccessibilityRole = Role.PopupMenu;
693         }
694
695         /// <summary>
696         /// Informs AT-SPI bridge about the set of AT-SPI states associated with this object.
697         /// </summary>
698         [EditorBrowsable(EditorBrowsableState.Never)]
699         protected override AccessibilityStates AccessibilityCalculateStates()
700         {
701             var states = base.AccessibilityCalculateStates();
702
703             states[AccessibilityState.Modal] = true;
704
705             return states;
706         }
707     }
708 }