[NUI] Add bindable properties to Components.
[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         }
398
399         /// <summary>
400         /// Dismiss the Menu.
401         /// The Menu becomes hidden and disposed.
402         /// </summary>
403         /// <since_tizen> 9 </since_tizen>
404         public void Dismiss()
405         {
406             Hide();
407             Dispose();
408         }
409
410         /// <inheritdoc/>
411         [EditorBrowsable(EditorBrowsableState.Never)]
412         public override void OnRelayout(Vector2 size, RelayoutContainer container)
413         {
414             base.OnRelayout(size, container);
415
416             CalculateSizeAndPosition();
417         }
418
419         private void Initialize()
420         {
421             Layout = new AbsoluteLayout();
422
423             WidthSpecification = LayoutParamPolicies.WrapContent;
424             HeightSpecification = LayoutParamPolicies.WrapContent;
425
426             BackgroundColor = Color.Transparent;
427
428             // Menu is added to Anchor so Menu should exclude layouting because
429             // if Anchor has Layout, then Menu is displayed at an incorrect position.
430             ExcludeLayouting = true;
431
432             Content = CreateDefaultContent();
433
434             Scrim = CreateDefaultScrim();
435
436             menuItemGroup = new MenuItemGroup();
437
438             layer = new Layer();
439             layer.Add(this);
440         }
441
442         private ScrollableBase CreateDefaultContent()
443         {
444             return new ScrollableBase()
445             {
446                 Layout = new LinearLayout()
447                 {
448                     LinearOrientation = LinearLayout.Orientation.Vertical,
449                 },
450                 ScrollingDirection = ScrollableBase.Direction.Vertical,
451                 ScrollEnabled = true,
452                 HideScrollbar = false,
453
454                 // FIXME: This color should be in DefaultThemeCommon.cs.
455                 BackgroundColor = new Color("#EEEFF1"),
456             };
457         }
458
459         private View CreateDefaultScrim()
460         {
461             var scrim = new VisualView()
462             {
463                 // Scrim is added to Menu so Scrim should exclude layouting
464                 // not to enlarge Menu size.
465                 ExcludeLayouting = true,
466                 BackgroundColor = Color.Transparent,
467                 Size = new Size(NUIApplication.GetDefaultWindow().Size),
468             };
469
470             scrim.TouchEvent += (object source, TouchEventArgs e) =>
471             {
472                 if (e.Touch.GetState(0) == PointStateType.Up)
473                 {
474                     this.Dismiss();
475                 }
476                 return true;
477             };
478
479             return scrim;
480         }
481
482         private void CalculateSizeAndPosition()
483         {
484             CalculateMenuSize();
485
486             CalculateMenuPosition();
487
488             CalculateScrimPosition();
489         }
490
491         // Calculate menu's size based on content's size
492         private void CalculateMenuSize()
493         {
494             if (Content == null)
495             {
496                 return;
497             }
498
499             if (Size2D.Equals(Content.Size2D) == false)
500             {
501                 Size2D = new Size2D(Content.Size2D.Width, Content.Size2D.Height);
502             }
503         }
504
505         private View GetRootView()
506         {
507             View root = this;
508             View parent = GetParent() as View;
509
510             while (parent)
511             {
512                 root = parent;
513                 parent = parent?.GetParent() as View;
514             }
515
516             return root;
517         }
518
519         // Calculate menu's position based on Anchor and parent's positions.
520         // If there is not enought space, then menu's size can be also resized.
521         private void CalculateMenuPosition()
522         {
523             if ((Anchor == null) || (Content == null))
524             {
525                 return;
526             }
527
528             if (Items == null)
529             {
530                 return;
531             }
532
533             if ((Size2D.Width == 0) && (Size2D.Height == 0))
534             {
535                 return;
536             }
537
538             int menuScreenPosX = 0;
539             int menuScreenPosY = 0;
540
541             if (HorizontalPositionToAnchor == RelativePosition.Start)
542             {
543                 if (GetRootView().LayoutDirection == ViewLayoutDirectionType.LTR)
544                 {
545                     menuScreenPosX = (int)Anchor.ScreenPosition.X - Size2D.Width;
546                 }
547                 else
548                 {
549                     menuScreenPosX = (int)Anchor.ScreenPosition.X + Anchor.Margin.Start + Anchor.Size2D.Width + Anchor.Margin.End;
550                 }
551             }
552             else if (HorizontalPositionToAnchor == RelativePosition.Center)
553             {
554                 menuScreenPosX = (int)Anchor.ScreenPosition.X + Anchor.Margin.Start + (Anchor.Size2D.Width / 2) - (Size2D.Width / 2);
555             }
556             else
557             {
558                 if (GetRootView().LayoutDirection == ViewLayoutDirectionType.LTR)
559                 {
560                     menuScreenPosX = (int)Anchor.ScreenPosition.X + Anchor.Margin.Start + Anchor.Size2D.Width + Anchor.Margin.End;
561                 }
562                 else
563                 {
564                     menuScreenPosX = (int)Anchor.ScreenPosition.X - Size2D.Width;
565                 }
566             }
567
568             if (VerticalPositionToAnchor == RelativePosition.Start)
569             {
570                 menuScreenPosY = (int)Anchor.ScreenPosition.Y - Size2D.Height;
571             }
572             else if (VerticalPositionToAnchor == RelativePosition.Center)
573             {
574                 menuScreenPosY = (int)Anchor.ScreenPosition.Y + Anchor.Margin.Top + (Anchor.Size2D.Height / 2) - (Size2D.Height / 2);
575             }
576             else
577             {
578                 menuScreenPosY = (int)Anchor.ScreenPosition.Y + Anchor.Margin.Top + Anchor.Size2D.Height + Anchor.Margin.Bottom;
579             }
580
581             int menuSizeW = Size2D.Width;
582             int menuSizeH = Size2D.Height;
583
584             // Check if menu is not inside parent's boundary in x coordinate system.
585             if (menuScreenPosX + Size2D.Width > Window.Size.Width)
586             {
587                 menuScreenPosX = Window.Size.Width - Size2D.Width;
588
589                 if (menuScreenPosX < 0)
590                 {
591                     menuScreenPosX = 0;
592                     menuSizeW = Window.Size.Width;
593                 }
594             }
595             else if (menuScreenPosX < 0)
596             {
597                 menuScreenPosX = 0;
598                 menuSizeW = Window.Size.Width;
599             }
600
601             // Check if menu is not inside parent's boundary in y coordinate system.
602             if (menuScreenPosY + Size2D.Height > Window.Size.Height)
603             {
604                 menuScreenPosY = Window.Size.Height - Size2D.Height;
605
606                 if (menuScreenPosY < 0)
607                 {
608                     menuScreenPosY = 0;
609                     menuSizeH = Window.Size.Height;
610                 }
611             }
612             else if (menuScreenPosY < 0)
613             {
614                 menuScreenPosY = 0;
615                 menuSizeH = Window.Size.Height;
616             }
617
618             // Position is relative to parent's coordinate system.
619             var menuPosX = menuScreenPosX;
620             var menuPosY = menuScreenPosY;
621
622             if ((Position2D.X != menuPosX) || (Position2D.Y != menuPosY) || (Size2D.Width != menuSizeW) || (Size2D.Height != menuSizeH))
623             {
624                 Position2D = new Position2D(menuPosX, menuPosY);
625                 Size2D = new Size2D(menuSizeW, menuSizeH);
626             }
627         }
628
629         // Calculate scrim's position based on menu's position
630         private void CalculateScrimPosition()
631         {
632             if (Scrim == null)
633             {
634                 return;
635             }
636
637             // Menu's Position should be updated before doing this calculation.
638             if ((Scrim.Position2D.X != -Position2D.X) || (Scrim.Position2D.Y != -Position2D.Y))
639             {
640                 Scrim.Position2D = new Position2D(-Position2D.X, -Position2D.Y);
641             }
642         }
643     }
644 }