[NUI] Add Menu, MenuItem, MenuItemGroup classes
authorJaehyun Cho <jae_hyun.cho@samsung.com>
Mon, 12 Apr 2021 12:56:36 +0000 (21:56 +0900)
committerhuiyueun <35286162+huiyueun@users.noreply.github.com>
Tue, 20 Apr 2021 06:13:00 +0000 (15:13 +0900)
To support menu displayed at the given anchor position, Menu, MenuItem,
MenuItemGroup classes are added.

Menu adds MenuItems and also adds MenuItems to MenuItemGroup to select
only one item at a time.

Menu is displayed at the AnchorPosition if it is enough space to display
Menu at the AnchorPosition.
Otherwise, Menu is displayed near AnchorPosition to display Menu.

To support Menu easily, static method ShowMenu() is added to DialogPage.

src/Tizen.NUI.Components/Controls/Menu.cs [new file with mode: 0755]
src/Tizen.NUI.Components/Controls/MenuItem.cs [new file with mode: 0755]
src/Tizen.NUI.Components/Controls/MenuItemGroup.cs [new file with mode: 0755]
src/Tizen.NUI.Components/Theme/DefaultThemeCommon.cs
src/Tizen.NUI.Components/res/nui_component_menu_item_bg.png [new file with mode: 0755]
test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/MenuSample.cs [new file with mode: 0755]

diff --git a/src/Tizen.NUI.Components/Controls/Menu.cs b/src/Tizen.NUI.Components/Controls/Menu.cs
new file mode 100755 (executable)
index 0000000..23fe277
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+ * Copyright(c) 2021 Samsung Electronics Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+using System.Collections.Generic;
+using System.ComponentModel;
+using Tizen.NUI.BaseComponents;
+
+namespace Tizen.NUI.Components
+{
+    /// <summary>
+    /// Menu is a class which contains a set of MenuItems and has one of them selected.
+    /// </summary>
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    public class Menu : Control
+    {
+        private ScrollableBase scrollableBase = null;
+        private IEnumerable<MenuItem> menuItems = null;
+        private MenuItemGroup menuItemGroup = null;
+        private Position2D anchorPosition = new Position2D(0, 0);
+
+        /// <summary>
+        /// Creates a new instance of Menu.
+        /// </summary>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public Menu() : base()
+        {
+            Initialize();
+        }
+
+        /// <summary>
+        /// Dispose Menu and all children on it.
+        /// </summary>
+        /// <param name="type">Dispose type.</param>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        protected override void Dispose(DisposeTypes type)
+        {
+            if (disposed)
+            {
+                return;
+            }
+
+            if (type == DisposeTypes.Explicit)
+            {
+                if (menuItems != null)
+                {
+                    foreach (MenuItem menuItem in menuItems)
+                    {
+                        Utility.Dispose(menuItem);
+                    }
+                }
+
+                menuItemGroup = null;
+
+                if (anchorPosition != null)
+                {
+                    anchorPosition.Dispose();
+                    anchorPosition = null;
+                }
+
+                Utility.Dispose(scrollableBase);
+            }
+
+            base.Dispose(type);
+        }
+
+        /// <summary>
+        /// Menu items in Menu.
+        /// </summary>
+        public IEnumerable<MenuItem> Items
+        {
+            get
+            {
+                return menuItems;
+            }
+
+            set
+            {
+                if (menuItems != null)
+                {
+                    foreach (var oldItem in menuItems)
+                    {
+                        if (scrollableBase.Children?.Contains(oldItem) == true)
+                        {
+                            scrollableBase.Children.Remove(oldItem);
+                        }
+                    }
+                }
+
+                menuItems = value;
+
+                if (menuItems == null)
+                {
+                    return;
+                }
+
+                foreach (var item in menuItems)
+                {
+                    scrollableBase.Add(item);
+                    menuItemGroup.Add(item);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Anchor position of Menu.
+        /// Menu is displayed at the anchor position.
+        /// If there is no enough space to display menu at the anchor position,
+        /// then menu is displayed at the proper position near anchor position.
+        /// </summary>
+        public Position2D AnchorPosition
+        {
+            get
+            {
+                return anchorPosition;
+            }
+
+            set
+            {
+                if (anchorPosition == value)
+                {
+                    return;
+                }
+
+                anchorPosition = value;
+                if (anchorPosition == null)
+                {
+                    return;
+                }
+
+                CalculateSizeAndPosition();
+            }
+        }
+
+        /// <inheritdoc/>
+        public override void OnRelayout(Vector2 size, RelayoutContainer container)
+        {
+            base.OnRelayout(size, container);
+
+            CalculateSizeAndPosition();
+        }
+
+        private void Initialize()
+        {
+            Layout = new AbsoluteLayout();
+
+            WidthSpecification = LayoutParamPolicies.WrapContent;
+
+            scrollableBase = new ScrollableBase()
+            {
+                Layout = new LinearLayout()
+                {
+                    LinearOrientation = LinearLayout.Orientation.Vertical,
+                },
+                ScrollingDirection = ScrollableBase.Direction.Vertical,
+                ScrollEnabled = true,
+                HideScrollbar = false,
+            };
+            Add(scrollableBase);
+
+            menuItemGroup = new MenuItemGroup();
+        }
+
+        private void CalculateSizeAndPosition()
+        {
+            var parent = GetParent() as View;
+
+            if ((AnchorPosition == null) || (parent == null))
+            {
+                return;
+            }
+
+            if (Items == null)
+            {
+                return;
+            }
+
+            if ((Size2D.Width == 0) && (Size2D.Height == 0))
+            {
+                return;
+            }
+
+            var parentPosition = new Position2D((int)parent.ScreenPosition.X, (int)parent.ScreenPosition.Y);
+            var parentSize = new Size2D(parent.Size2D.Width, parent.Size2D.Height);
+
+            int menuPosX = AnchorPosition.X;
+            int menuPosY = AnchorPosition.Y;
+            int menuSizeW = Size2D.Width;
+            int menuSizeH = Size2D.Height;
+
+            // Check if menu is not inside parent's boundary in x coordinate system.
+            if (AnchorPosition.X + Size2D.Width > parentPosition.X + parentSize.Width)
+            {
+                menuPosX = parentPosition.X + parentSize.Width - Size2D.Width;
+
+                if (menuPosX < parentPosition.X)
+                {
+                    menuPosX = parentPosition.X;
+                    menuSizeW = parentSize.Width;
+                }
+            }
+
+            // Check if menu is not inside parent's boundary in y coordinate system.
+            if (AnchorPosition.Y + Size2D.Height > parentPosition.Y + parentSize.Height)
+            {
+                menuPosY = parentPosition.Y + parentSize.Height - Size2D.Height;
+
+                if (menuPosY < parentPosition.Y)
+                {
+                    menuPosY = parentPosition.Y;
+                    menuSizeH = parentSize.Height;
+                }
+            }
+
+            Position2D = new Position2D(menuPosX, menuPosY);
+            Size2D = new Size2D(menuSizeW, menuSizeH);
+
+            parentPosition.Dispose();
+            parentSize.Dispose();
+        }
+    }
+}
diff --git a/src/Tizen.NUI.Components/Controls/MenuItem.cs b/src/Tizen.NUI.Components/Controls/MenuItem.cs
new file mode 100755 (executable)
index 0000000..0d5b131
--- /dev/null
@@ -0,0 +1,243 @@
+/*
+ * Copyright(c) 2021 Samsung Electronics Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using Tizen.NUI.BaseComponents;
+
+namespace Tizen.NUI.Components
+{
+    /// <summary>
+    /// MenuItem is a class which is used to show a list of items in Menu.
+    /// </summary>
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    public class MenuItem : SelectButton
+    {
+        private bool selectedAgain = false;
+
+        private bool styleApplied = false;
+
+        /// <summary>
+        /// Creates a new instance of MenuItem.
+        /// </summary>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public MenuItem()
+        {
+            Initialize();
+        }
+
+        /// <summary>
+        /// Creates a new instance of MenuItem.
+        /// </summary>
+        /// <param name="style">Creates MenuItem by special style defined in UX.</param>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public MenuItem(string style) : base(style)
+        {
+            Initialize();
+        }
+
+        /// <summary>
+        /// Dispose MenuItem and all children on it.
+        /// </summary>
+        /// <param name="type">Dispose type.</param>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        protected override void Dispose(DisposeTypes type)
+        {
+            if (disposed)
+            {
+                return;
+            }
+
+            if (type == DisposeTypes.Explicit)
+            {
+            }
+
+            base.Dispose(type);
+        }
+
+        /// <summary>
+        /// Applies style to MenuItem.
+        /// </summary>
+        /// <param name="viewStyle">The style to apply.</param>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public override void ApplyStyle(ViewStyle viewStyle)
+        {
+            styleApplied = false;
+
+            base.ApplyStyle(viewStyle);
+
+            styleApplied = true;
+
+            //Calculate position based on Achor's position.
+            LayoutItems();
+        }
+
+        /// <inheritdoc/>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        protected override void OnUpdate()
+        {
+            base.OnUpdate();
+            LayoutItems();
+        }
+
+        /// <inheritdoc/>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public override bool OnKey(Key key)
+        {
+            if ((IsEnabled == false) || (key == null))
+            {
+                return false;
+            }
+
+            if (key.State == Key.StateType.Up)
+            {
+                if (key.KeyPressedName == "Return")
+                {
+                    if (IsSelected == true)
+                    {
+                        selectedAgain = true;
+                    }
+                }
+            }
+
+            bool ret = base.OnKey(key);
+
+            if (selectedAgain == true)
+            {
+                IsSelected = true;
+                selectedAgain = false;
+            }
+
+            return ret;
+        }
+
+        /// <inheritdoc/>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        protected override bool HandleControlStateOnTouch(Touch touch)
+        {
+            if ((IsEnabled == false) || (touch == null))
+            {
+                return false;
+            }
+
+            PointStateType state = touch.GetState(0);
+            switch (state)
+            {
+                case PointStateType.Up:
+                    if (IsSelected == true)
+                    {
+                        selectedAgain = true;
+                    }
+                    break;
+                default:
+                    break;
+            }
+
+            bool ret = base.HandleControlStateOnTouch(touch);
+
+            if (selectedAgain == true)
+            {
+                IsSelected = true;
+                selectedAgain = false;
+            }
+
+            return ret;
+        }
+
+        /// <inheritdoc/>
+        [SuppressMessage("Microsoft.Design",
+                         "CA1062: Validate arguments of public methods",
+                         MessageId = "controlStateChangedInfo",
+                         Justification = "OnControlStateChanged is called when controlState is changed so controlStateChangedInfo cannot be null.")]
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        protected override void OnControlStateChanged(ControlStateChangedEventArgs controlStateChangedInfo)
+        {
+            if (controlStateChangedInfo.PreviousState.Contains(ControlState.Selected) != controlStateChangedInfo.CurrentState.Contains(ControlState.Selected))
+            {
+                // MenuItem does not invoke SelectedChanged if button or key is
+                // unpressed while its state is selected.
+                if (selectedAgain == true)
+                {
+                    return;
+                }
+
+                base.OnControlStateChanged(controlStateChangedInfo);
+            }
+        }
+
+        /// <inheritdoc/>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        protected override void LayoutItems()
+        {
+            if (styleApplied == false)
+            {
+                return;
+            }
+
+            if ((Icon == null) && (TextLabel == null))
+            {
+                return;
+            }
+
+            // Icon is added in Button.LayoutItems().
+            if ((Icon != null) && (Children.Contains(Icon) == false))
+            {
+                Add(Icon);
+            }
+
+            // TextLabel is added in Button.LayoutItems().
+            if ((TextLabel != null) && (Children.Contains(TextLabel) == false))
+            {
+                Add(TextLabel);
+            }
+
+            switch (IconRelativeOrientation)
+            {
+                // TODO: Other orientation cases are not implemented yet.
+                case IconOrientation.Left:
+                    if (LayoutDirection == ViewLayoutDirectionType.LTR)
+                    {
+                        int iconPosX = Padding.Start + (Icon?.Margin.Start ?? 0);
+                        int iconPosY = Padding.Top + (Icon?.Margin.Top ?? 0);
+                        int iconSizeW = Icon?.Size2D.Width ?? 0;
+                        int iconSizeH = Icon?.Size2D.Height ?? 0;
+
+                        if (Icon != null)
+                        {
+                            Icon.Position2D = new Position2D(iconPosX, iconPosY);
+                        }
+
+                        if (TextLabel != null)
+                        {
+                            int textPosX = iconPosX + iconSizeW + TextLabel.Margin.Start;
+                            int textPosY = Padding.Top + TextLabel.Margin.Top;
+
+                            TextLabel.Position2D = new Position2D(textPosX, textPosY);
+                        }
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        private void Initialize()
+        {
+            Layout = new AbsoluteLayout();
+        }
+    }
+}
diff --git a/src/Tizen.NUI.Components/Controls/MenuItemGroup.cs b/src/Tizen.NUI.Components/Controls/MenuItemGroup.cs
new file mode 100755 (executable)
index 0000000..c531b0c
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright(c) 2021 Samsung Electronics Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+using System;
+using System.ComponentModel;
+
+namespace Tizen.NUI.Components
+{
+    /// <summary>
+    /// MenuItemGroup class is used to group together a set of MenuItems.
+    /// It enables a user to select exclusively single MenuItem of a group.
+    /// </summary>
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    public class MenuItemGroup : SelectGroup
+    {
+        /// <summary>
+        /// Constructs MenuItemGroup
+        /// </summary>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public MenuItemGroup() : base()
+        {
+            EnableMultiSelection = false;
+        }
+
+        /// <summary>
+        /// Adds a menu item to the end of MenuItemGroup.
+        /// </summary>
+        /// <param name="menuItem">A menu item to be added to the group.</param>
+        /// <exception cref="ArgumentNullException">Thrown when the argument menuItem is null.</exception>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public void Add(MenuItem menuItem)
+        {
+            if (menuItem == null)
+            {
+                throw new ArgumentNullException(nameof(menuItem), "menuItem should not be null.");
+            }
+
+            base.AddSelection(menuItem);
+        }
+
+        /// <summary>
+        /// Removes a menu item from MenuItemGroup.
+        /// </summary>
+        /// <param name="menuItem">A menu item to be removed from the group.</param>
+        /// <exception cref="ArgumentNullException">Thrown when the argument menuItem is null.</exception>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public void Remove(MenuItem menuItem)
+        {
+            if (menuItem == null)
+            {
+                throw new ArgumentNullException(nameof(menuItem), "menuItem should not be null.");
+            }
+
+            base.RemoveSelection(menuItem);
+        }
+    }
+}
index 0e79362..48f98b6 100755 (executable)
@@ -396,13 +396,13 @@ namespace Tizen.NUI.Components
                 },
                 ActionView = new ViewStyle()
                 {
-                    Size = new Size(-1, 48),
+                    Size = new Size(-1, 120),
                     CornerRadius = 0,
                     BackgroundColor = new Color(0, 0, 0, 0),
                 },
                 ActionButton = new ButtonStyle()
                 {
-                    Size = new Size(-1, 48),
+                    Size = new Size(-1, 120),
                     CornerRadius = 0,
                     BackgroundColor = new Color(0, 0, 0, 0),
                     Text = new TextLabelStyle()
@@ -662,6 +662,40 @@ namespace Tizen.NUI.Components
                 },
             });
 
+            theme.AddStyleWithoutClone("Tizen.NUI.Components.MenuItem", new ButtonStyle()
+            {
+                Size = new Size(480, -2),
+                MinimumSize = new Size2D(0, 72),
+                CornerRadius = 0,
+                BackgroundImage = FrameworkInformation.ResourcePath + "nui_component_menu_item_bg.png",
+                Padding = new Extents(16, 16, 16, 16),
+                Text = new TextLabelStyle()
+                {
+                    PixelSize = 32,
+                    MultiLine = true,
+                    HorizontalAlignment = HorizontalAlignment.Begin,
+                    VerticalAlignment = VerticalAlignment.Center,
+                    TextColor = new Selector<Color>()
+                    {
+                        Normal = new Color("#001447FF"),
+                        Focused = new Color("#00338BFF"),
+                        Pressed = new Color("#1B69CAFF"),
+                        Disabled = new Color("#C3CAD2FF"),
+                    },
+                },
+                Icon = new ImageViewStyle()
+                {
+                    Size = new Size(-2, 48),
+                    Color = new Selector<Color>()
+                    {
+                        Normal = new Color("#001447FF"),
+                        Focused = new Color("#00338BFF"),
+                        Pressed = new Color("#1B69CAFF"),
+                        Disabled = new Color("#C3CAD2FF"),
+                    },
+                },
+            });
+
             return theme;
         }
     }
diff --git a/src/Tizen.NUI.Components/res/nui_component_menu_item_bg.png b/src/Tizen.NUI.Components/res/nui_component_menu_item_bg.png
new file mode 100755 (executable)
index 0000000..bb776e7
Binary files /dev/null and b/src/Tizen.NUI.Components/res/nui_component_menu_item_bg.png differ
diff --git a/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/MenuSample.cs b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/MenuSample.cs
new file mode 100755 (executable)
index 0000000..d4dba94
--- /dev/null
@@ -0,0 +1,63 @@
+using Tizen.NUI.BaseComponents;
+using Tizen.NUI.Components;
+
+namespace Tizen.NUI.Samples
+{
+    public class MenuSample : IExample
+    {
+        public void Activate()
+        {
+            var window = NUIApplication.GetDefaultWindow();
+            var navigator = window.GetDefaultNavigator();
+
+            var pageContent = new Button()
+            {
+                Text = "Page Content",
+                CornerRadius = 0,
+                WidthSpecification = LayoutParamPolicies.MatchParent,
+                HeightSpecification = LayoutParamPolicies.MatchParent,
+            };
+
+            var moreButton = new Button()
+            {
+                Text = "More",
+            };
+
+            var appBar = new AppBar()
+            {
+                AutoNavigationContent = false,
+                Title = "Title",
+                Actions = new View[] { moreButton, },
+            };
+
+            var page = new ContentPage()
+            {
+                AppBar = appBar,
+                Content = pageContent,
+            };
+            navigator.Push(page);
+
+            var menuItem = new MenuItem() { Text = "Menu" };
+            var menuItem2 = new MenuItem() { Text = "Menu2" };
+            var menuItem3 = new MenuItem() { Text = "Menu3" };
+            var menuItem4 = new MenuItem() { Text = "Menu4" };
+
+            moreButton.Clicked += (object sender, ClickedEventArgs args) =>
+            {
+                DialogPage.ShowMenu(moreButton, menuItem, menuItem2, menuItem3, menuItem4);
+            };
+        }
+
+        public void Deactivate()
+        {
+            var window = NUIApplication.GetDefaultWindow();
+            var navigator = window.GetDefaultNavigator();
+            var newPageCount = window.GetDefaultNavigator().NavigationPages.Count;
+
+            for (int i = 0; i < newPageCount; i++)
+            {
+                navigator.Pop();
+            }
+        }
+    }
+}