diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs index 121b3bf31..f18bf0a1f 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs @@ -98,6 +98,16 @@ namespace FlaxEditor.GUI.ContextMenu /// public List ExternalPopups = new List(); + /// + /// Optional flag that can disable popup visibility based on window focus and use external control via Hide. + /// + public bool UseVisibilityControl = true; + + /// + /// Optional flag that can disable popup input capturing. Useful for transparent or visual-only popups. + /// + public bool UseInput = true; + /// /// Initializes a new instance of the class. /// @@ -230,8 +240,8 @@ namespace FlaxEditor.GUI.ContextMenu desc.HasBorder = false; desc.SupportsTransparency = false; desc.ShowInTaskbar = false; - desc.ActivateWhenFirstShown = true; - desc.AllowInput = true; + desc.ActivateWhenFirstShown = UseInput; + desc.AllowInput = UseInput; desc.AllowMinimize = false; desc.AllowMaximize = false; desc.AllowDragAndDrop = false; @@ -240,8 +250,11 @@ namespace FlaxEditor.GUI.ContextMenu desc.HasSizingFrame = false; OnWindowCreating(ref desc); _window = Platform.CreateWindow(ref desc); - _window.GotFocus += OnWindowGotFocus; - _window.LostFocus += OnWindowLostFocus; + if (UseVisibilityControl) + { + _window.GotFocus += OnWindowGotFocus; + _window.LostFocus += OnWindowLostFocus; + } // Attach to the window _parentCM = parent as ContextMenuBase; @@ -253,9 +266,12 @@ namespace FlaxEditor.GUI.ContextMenu return; _window.Show(); PerformLayout(); - _previouslyFocused = parentWin.FocusedControl; - Focus(); - OnShow(); + if (UseVisibilityControl) + { + _previouslyFocused = parentWin.FocusedControl; + Focus(); + OnShow(); + } } /// @@ -508,7 +524,7 @@ namespace FlaxEditor.GUI.ContextMenu base.Update(deltaTime); // Let root context menu to check if none of the popup windows - if (_parentCM == null && !IsForeground) + if (_parentCM == null && UseVisibilityControl && !IsForeground) { Hide(); } diff --git a/Source/Editor/GUI/ItemsListContextMenu.cs b/Source/Editor/GUI/ItemsListContextMenu.cs index 8eeccf288..e23bb27f7 100644 --- a/Source/Editor/GUI/ItemsListContextMenu.cs +++ b/Source/Editor/GUI/ItemsListContextMenu.cs @@ -56,6 +56,11 @@ namespace FlaxEditor.GUI /// public event Action Clicked; + /// + /// Occurs when items gets focused. + /// + public event Action Focused; + /// /// The tint color of the text. /// @@ -141,6 +146,10 @@ namespace FlaxEditor.GUI protected virtual void GetTextRect(out Rectangle rect) { rect = new Rectangle(2, 0, Width - 4, Height); + + // Indent for drop panel items is handled by drop panel margin + if (Parent is not DropPanel) + rect.Location += new Float2(Editor.Instance.Icons.ArrowRight12.Size.X + 2, 0); } /// @@ -155,10 +164,6 @@ namespace FlaxEditor.GUI if (IsMouseOver || IsFocused) Render2D.FillRectangle(new Rectangle(Float2.Zero, Size), style.BackgroundHighlighted); - // Indent for drop panel items is handled by drop panel margin - if (Parent is not DropPanel) - textRect.Location += new Float2(Editor.Instance.Icons.ArrowRight12.Size.X + 2, 0); - // Draw all highlights if (_highlights != null) { @@ -207,6 +212,14 @@ namespace FlaxEditor.GUI base.OnMouseLeave(); } + /// + public override void OnGotFocus() + { + base.OnGotFocus(); + + Focused?.Invoke(this); + } + /// public override int Compare(Control other) { @@ -227,6 +240,7 @@ namespace FlaxEditor.GUI private readonly Panel _scrollPanel; private List _categoryPanels; private bool _waitingForInput; + private string _customSearch; /// /// Event fired when any item in this popup menu gets clicked. @@ -290,12 +304,13 @@ namespace FlaxEditor.GUI LockChildrenRecursive(); + var searchText = _searchBox?.Text ?? _customSearch; var items = ItemsPanel.Children; for (int i = 0; i < items.Count; i++) { if (items[i] is Item item) { - item.UpdateFilter(_searchBox.Text); + item.UpdateFilter(searchText); item.UpdateScore(); } } @@ -309,13 +324,13 @@ namespace FlaxEditor.GUI { if (category.Children[j] is Item item2) { - item2.UpdateFilter(_searchBox.Text); + item2.UpdateFilter(searchText); item2.UpdateScore(); anyVisible |= item2.Visible; } } category.Visible = anyVisible; - if (string.IsNullOrEmpty(_searchBox.Text)) + if (string.IsNullOrEmpty(searchText)) category.Close(false); else category.Open(false); @@ -326,8 +341,8 @@ namespace FlaxEditor.GUI UnlockChildrenRecursive(); PerformLayout(true); - _searchBox.Focus(); - TextChanged?.Invoke(_searchBox.Text); + _searchBox?.Focus(); + TextChanged?.Invoke(searchText); } /// @@ -359,6 +374,14 @@ namespace FlaxEditor.GUI } } + /// + /// Removes all added items. + /// + public void ClearItems() + { + ItemsPanel.DisposeChildren(); + } + /// /// Sorts the items list (by item name by default). /// @@ -372,6 +395,34 @@ namespace FlaxEditor.GUI } } + /// + /// Focuses and scroll to the given item to be selected. + /// + /// The item to select. + public void SelectItem(Item item) + { + item.Focus(); + ScrollViewTo(item); + } + + /// + /// Applies custom search text query on the items list. Works even if search field is disabled + /// + /// The custom search text. Null to clear search. + public void Search(string text) + { + if (_searchBox != null) + { + _searchBox.SetText(text); + } + else + { + _customSearch = text; + if (VisibleInHierarchy) + OnSearchFilterChanged(); + } + } + /// /// Adds the item to the view and registers for the click event. /// @@ -453,6 +504,8 @@ namespace FlaxEditor.GUI _searchBox?.Clear(); UnlockChildrenRecursive(); PerformLayout(true); + if (_customSearch != null) + OnSearchFilterChanged(); } private List GetVisibleItems() diff --git a/Source/Editor/Windows/OutputLogWindow.cs b/Source/Editor/Windows/OutputLogWindow.cs index 361f2e4b1..882730fed 100644 --- a/Source/Editor/Windows/OutputLogWindow.cs +++ b/Source/Editor/Windows/OutputLogWindow.cs @@ -129,7 +129,57 @@ namespace FlaxEditor.Windows /// private class CommandLineBox : TextBox { + private sealed class Item : ItemsListContextMenu.Item + { + public CommandLineBox Owner; + + public Item() + { + } + + protected override void GetTextRect(out Rectangle rect) + { + rect = new Rectangle(2, 0, Width - 4, Height); + } + + public override bool OnCharInput(char c) + { + if (Owner != null && (!Owner._searchPopup?.Visible ?? true)) + { + // Redirect input into search textbox while typing and using command history + Owner.Set(Owner.Text + c); + return true; + } + return false; + } + + public override bool OnKeyDown(KeyboardKeys key) + { + switch (key) + { + case KeyboardKeys.Delete: + case KeyboardKeys.Backspace: + if (Owner != null && (!Owner._searchPopup?.Visible ?? true)) + { + // Redirect input into search textbox while typing and using command history + Owner.OnKeyDown(key); + return true; + } + break; + } + return base.OnKeyDown(key); + } + + public override void OnDestroy() + { + Owner = null; + base.OnDestroy(); + } + } + private OutputLogWindow _window; + private ItemsListContextMenu _searchPopup; + private bool _isSettingText; public CommandLineBox(float x, float y, float width, OutputLogWindow window) : base(false, x, y, width) @@ -138,6 +188,100 @@ namespace FlaxEditor.Windows _window = window; } + private void Set(string command) + { + _isSettingText = true; + SetText(command); + SetSelection(command.Length); + _isSettingText = false; + } + + private void ShowPopup(ref ItemsListContextMenu cm, IEnumerable commands, string searchText = null) + { + if (cm == null) + cm = new ItemsListContextMenu(180, 220, false); + else + cm.ClearItems(); + + // Add items + ItemsListContextMenu.Item lastItem = null; + foreach (var command in commands) + { + cm.AddItem(lastItem = new Item + { + Name = command, + Owner = this, + }); + lastItem.Focused += item => + { + // Set command + Set(item.Name); + }; + } + cm.ItemClicked += item => + { + // Execute command + OnKeyDown(KeyboardKeys.Return); + }; + + // Setup popup + var count = commands.Count(); + var totalHeight = count * lastItem.Height + cm.ItemsPanel.Margin.Height + cm.ItemsPanel.Spacing * (count - 1); + cm.Height = 220; + if (cm.Height > totalHeight) + cm.Height = totalHeight; // Limit popup height if list is small + if (searchText != null) + { + cm.SortItems(); + cm.Search(searchText); + cm.UseVisibilityControl = false; + cm.UseInput = false; + } + + // Show popup + cm.Show(this, Float2.Zero, ContextMenuDirection.RightUp); + cm.ScrollViewTo(lastItem); + if (searchText != null) + { + RootWindow.Window.LostFocus += OnRootWindowLostFocus; + } + else + { + lastItem.Focus(); + } + } + + private void OnRootWindowLostFocus() + { + // Prevent popup from staying active when editor window looses focus + _searchPopup?.Hide(); + if (RootWindow?.Window != null) + RootWindow.Window.LostFocus -= OnRootWindowLostFocus; + } + + /// + protected override void OnTextChanged() + { + base.OnTextChanged(); + + // Skip when editing text from code + if (_isSettingText) + return; + + // Show commands search popup based on current text input + var text = Text.Trim(); + if (text.Length != 0) + { + DebugCommands.Search(text, out var matches); + if (matches.Length != 0) + { + ShowPopup(ref _searchPopup, matches, text); + return; + } + } + _searchPopup?.Hide(); + } + /// public override bool OnKeyDown(KeyboardKeys key) { @@ -146,6 +290,7 @@ namespace FlaxEditor.Windows case KeyboardKeys.Return: { // Run command + _searchPopup?.Hide(); var command = Text.Trim(); if (command.Length == 0) return true; @@ -175,8 +320,7 @@ namespace FlaxEditor.Windows else if (matches.Length == 1) { // Exact match - SetText(matches[0]); - SetSelection(Text.Length); + Set(matches[0]); } else { @@ -202,57 +346,60 @@ namespace FlaxEditor.Windows if (sharedLength > minLength) { // Use the largest shared part of all matches - SetText(matches[0].Substring(0, sharedLength)); - SetSelection(sharedLength); + Set(matches[0].Substring(0, sharedLength)); } } return true; } case KeyboardKeys.ArrowUp: { - if (TextLength == 0) + if (_searchPopup != null && _searchPopup.Visible) + { + // Route navigation to active popup + var focusedItem = _searchPopup.RootWindow.FocusedControl as Item; + if (focusedItem == null) + _searchPopup.SelectItem((Item)_searchPopup.ItemsPanel.Children.Last()); + else + _searchPopup.OnKeyDown(key); + } + else if (TextLength == 0) { if (_window._commandHistory != null && _window._commandHistory.Count != 0) { // Show command history popup - var cm = new ItemsListContextMenu(180, 220, false); - ItemsListContextMenu.Item lastItem = null; - var count = _window._commandHistory.Count; - for (int i = 0; i < count; i++) - { - var command = _window._commandHistory[i]; - cm.AddItem(lastItem = new ItemsListContextMenu.Item - { - Name = command, - }); - } - cm.ItemClicked += item => - { - SetText(item.Name); - SetSelection(Text.Length); - }; - var totalHeight = count * lastItem.Height + cm.ItemsPanel.Margin.Height + cm.ItemsPanel.Spacing * (count - 1); - if (cm.Height > totalHeight) - cm.Height = totalHeight; // Limit popup height if history is small - cm.Show(this, Float2.Zero, ContextMenuDirection.RightUp); - lastItem.Focus(); - cm.ScrollViewTo(lastItem); + _searchPopup?.Hide(); + ItemsListContextMenu cm = null; + ShowPopup(ref cm, _window._commandHistory); } } - else - { - // TODO: focus similar commands (via popup) - } return true; } case KeyboardKeys.ArrowDown: { - // Ignore + if (_searchPopup != null && _searchPopup.Visible) + { + // Route navigation to active popup + var focusedItem = _searchPopup.RootWindow.FocusedControl as Item; + if (focusedItem == null) + _searchPopup.SelectItem((Item)_searchPopup.ItemsPanel.Children.First()); + else + _searchPopup.OnKeyDown(key); + } return true; } } + return base.OnKeyDown(key); } + + /// + public override void OnDestroy() + { + _searchPopup?.Dispose(); + _searchPopup = null; + + base.OnDestroy(); + } } private InterfaceOptions.TimestampsFormats _timestampsFormats;