// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. using System.Collections.Generic; using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.GUI; namespace FlaxEditor.GUI.ContextMenu { /// /// Context menu popup directions. /// [HideInEditor] public enum ContextMenuDirection { /// /// The right down. /// RightDown, /// /// The right up. /// RightUp, /// /// The left down. /// LeftDown, /// /// The left up. /// LeftUp, } /// /// Base class for all context menu controls. /// /// [HideInEditor] public class ContextMenuBase : ContainerControl { private ContextMenuDirection _direction; private ContextMenuBase _parentCM; private bool _isSubMenu; private ContextMenuBase _childCM; private Window _window; private Control _previouslyFocused; /// /// Gets a value indicating whether use automatic popup direction fix based on the screen dimensions. /// protected virtual bool UseAutomaticDirectionFix => true; /// /// Returns true if context menu is opened /// public bool IsOpened => Parent != null; /// /// Gets the popup direction. /// public ContextMenuDirection Direction => _direction; /// /// Gets a value indicating whether any child context menu has been opened. /// public bool HasChildCMOpened => _childCM != null; /// /// Gets the topmost context menu. /// public ContextMenuBase TopmostCM { get { var cm = this; while (cm._parentCM != null && cm._isSubMenu) { cm = cm._parentCM; } return cm; } } /// /// Gets a value indicating whether this context menu is a sub-menu. Sub menus are treated like child context menus of the other menu (eg. hierarchy). /// public bool IsSubMenu => _isSubMenu; /// /// External dialog popups opened within the context window (eg. color picker) that should preserve context menu visibility (prevent from closing context menu). /// public List ExternalPopups = new List(); /// /// Initializes a new instance of the class. /// public ContextMenuBase() : base(0, 0, 120, 32) { _direction = ContextMenuDirection.RightDown; Visible = false; _isSubMenu = true; } /// /// Show context menu over given control. /// /// Parent control to attach to it. /// Popup menu origin location in parent control coordinates. public virtual void Show(Control parent, Float2 location) { Assert.IsNotNull(parent); // Ensure to be closed Hide(); // Peek parent control window var parentWin = parent.RootWindow; if (parentWin == null) return; // Check if show menu inside the other menu - then link as a child to prevent closing the calling menu window on lost focus if (_parentCM == null && parentWin.ChildrenCount == 1 && parentWin.Children[0] is ContextMenuBase parentCM) { parentCM.ShowChild(this, parentCM.PointFromScreen(parent.PointToScreen(location)), false); return; } // Unlock and perform controls update UnlockChildrenRecursive(); PerformLayout(); // Calculate popup direction and initial location (fit on a single monitor) var dpiScale = parentWin.DpiScale; var dpiSize = Size * dpiScale; var locationWS = parent.PointToWindow(location); var locationSS = parentWin.PointToScreen(locationWS); Location = Float2.Zero; var monitorBounds = Platform.GetMonitorBounds(locationSS); var rightBottomLocationSS = locationSS + dpiSize; bool isUp = false, isLeft = false; if (UseAutomaticDirectionFix) { if (monitorBounds.Bottom < rightBottomLocationSS.Y) { // Direction: up isUp = true; locationSS.Y -= dpiSize.Y; // Offset to fix sub-menu location if (parent is ContextMenu menu && menu._childCM != null) locationSS.Y += 30.0f * dpiScale; } if (monitorBounds.Right < rightBottomLocationSS.X || _parentCM?.Direction == ContextMenuDirection.LeftDown || _parentCM?.Direction == ContextMenuDirection.LeftUp) { // Direction: left isLeft = true; if (IsSubMenu && _parentCM != null) { locationSS.X -= _parentCM.Width + dpiSize.X; } else { locationSS.X -= dpiSize.X; } } } // Update direction flag if (isUp) _direction = isLeft ? ContextMenuDirection.LeftUp : ContextMenuDirection.RightUp; else _direction = isLeft ? ContextMenuDirection.LeftDown : ContextMenuDirection.RightDown; // Create window var desc = CreateWindowSettings.Default; desc.Position = locationSS; desc.StartPosition = WindowStartPosition.Manual; desc.Size = dpiSize; desc.Fullscreen = false; desc.HasBorder = false; desc.SupportsTransparency = false; desc.ShowInTaskbar = false; desc.ActivateWhenFirstShown = true; desc.AllowInput = true; desc.AllowMinimize = false; desc.AllowMaximize = false; desc.AllowDragAndDrop = false; desc.IsTopmost = true; desc.IsRegularWindow = false; desc.HasSizingFrame = false; OnWindowCreating(ref desc); _window = Platform.CreateWindow(ref desc); _window.LostFocus += OnWindowLostFocus; // Attach to the window _parentCM = parent as ContextMenuBase; Parent = _window.GUI; // Show Visible = true; if (_window == null) return; _window.Show(); PerformLayout(); _previouslyFocused = parentWin.FocusedControl; Focus(); OnShow(); } /// /// Hide popup menu and all child menus. /// public virtual void Hide() { if (!Visible) return; // Lock update IsLayoutLocked = true; // Close child HideChild(); // Unlink from window Parent = null; // Close window if (_window != null) { var win = _window; _window = null; win.Close(); } // Unlink from parent if (_parentCM != null) { _parentCM._childCM = null; _parentCM = null; } // Return focus if (_previouslyFocused != null) { _previouslyFocused.RootWindow?.Focus(); _previouslyFocused.Focus(); _previouslyFocused = null; } // Hide Visible = false; OnHide(); } /// /// Shows new child context menu. /// /// The child menu. /// The child menu initial location. /// True if context menu is a normal sub-menu, otherwise it is a custom menu popup linked as child. public void ShowChild(ContextMenuBase child, Float2 location, bool isSubMenu = true) { // Hide current child HideChild(); // Set child _childCM = child; _childCM._parentCM = this; _childCM._isSubMenu = isSubMenu; // Show child _childCM.Show(this, location); } /// /// Hides child popup menu if any opened. /// public void HideChild() { if (_childCM != null) { _childCM.Hide(); _childCM = null; } } /// /// Updates the size of the window to match context menu dimensions. /// protected void UpdateWindowSize() { if (_window != null) { _window.ClientSize = Size * _window.DpiScale; } } /// /// Called when context menu window setup is performed. Can be used to adjust the popup window options. /// /// The settings. protected virtual void OnWindowCreating(ref CreateWindowSettings settings) { } /// /// Called on context menu show. /// protected virtual void OnShow() { // Nothing to do } /// /// Called on context menu hide. /// protected virtual void OnHide() { // Nothing to do } /// /// Returns true if context menu is in foreground (eg. context window or any child window has user focus or user opened additional popup within this context). /// protected virtual bool IsForeground { get { // Any external popup is focused foreach (var externalPopup in ExternalPopups) { if (externalPopup && externalPopup.IsForegroundWindow) return true; } // Any context menu window is focused var anyForeground = false; var c = this; while (!anyForeground && c != null) { if (c._window != null && c._window.IsForegroundWindow) anyForeground = true; c = c._childCM; } return anyForeground; } } private void OnWindowLostFocus() { // Skip for parent menus (child should handle lost of focus) if (_childCM != null) return; // Check if user stopped using that popup menu if (_parentCM != null) { // Focus parent if user clicked over the parent popup var mouse = _parentCM.PointFromScreen(FlaxEngine.Input.MouseScreenPosition); if (_parentCM.ContainsPoint(ref mouse)) { _parentCM._window.Focus(); } } } /// public override bool IsMouseOver { get { bool result = false; for (int i = 0; i < _children.Count; i++) { var c = _children[i]; if (c.Visible && c.IsMouseOver) { result = true; break; } } return result; } } /// public override void Update(float deltaTime) { base.Update(deltaTime); // Let root context menu to check if none of the popup windows if (_parentCM == null && !IsForeground) { Hide(); } } /// public override void Draw() { // Draw background var style = Style.Current; var bounds = new Rectangle(Float2.Zero, Size); Render2D.FillRectangle(bounds, style.Background); Render2D.DrawRectangle(bounds, Color.Lerp(style.BackgroundSelected, style.Background, 0.6f)); base.Draw(); } /// public override bool OnMouseDown(Float2 location, MouseButton button) { base.OnMouseDown(location, button); return true; } /// public override bool OnMouseUp(Float2 location, MouseButton button) { base.OnMouseUp(location, button); return true; } /// public override void OnDestroy() { // Ensure to be hidden Hide(); base.OnDestroy(); } } }