From 372faa8ad985ac0a7730e271a31b6d20978996b4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 29 Nov 2023 16:42:25 +0300 Subject: [PATCH 01/22] Expose dropdown menu state from `Dropdown` --- osu.Framework/Graphics/UserInterface/Dropdown.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Framework/Graphics/UserInterface/Dropdown.cs b/osu.Framework/Graphics/UserInterface/Dropdown.cs index da63f6494d..70d0a82000 100644 --- a/osu.Framework/Graphics/UserInterface/Dropdown.cs +++ b/osu.Framework/Graphics/UserInterface/Dropdown.cs @@ -30,6 +30,8 @@ public abstract partial class Dropdown : CompositeDrawable, IHasCurrentValue< protected internal DropdownHeader Header; protected internal DropdownMenu Menu; + public Bindable State { get; } = new Bindable(); + /// /// Creates the header part of the control. /// @@ -114,7 +116,7 @@ private void addDropdownItem(T value) if (!Current.Disabled) Current.Value = value; - Menu.State = MenuState.Closed; + State.Value = MenuState.Closed; }); // inheritors expect that `virtual GenerateItemText` is only called when this dropdown's BDL has run to completion. @@ -220,13 +222,16 @@ protected Dropdown() Header.Action = Menu.Toggle; Header.ChangeSelection += selectionKeyPressed; + Header.State.BindTo(State); Menu.PreselectionConfirmed += preselectionConfirmed; + Menu.StateChanged += state => State.Value = state; + State.ValueChanged += state => Menu.State = state.NewValue; Current.ValueChanged += val => Scheduler.AddOnce(selectionChanged, val); Current.DisabledChanged += disabled => { Header.Enabled.Value = !disabled; - if (disabled && Menu.State == MenuState.Open) - Menu.State = MenuState.Closed; + if (disabled && State.Value == MenuState.Open) + State.Value = MenuState.Closed; }; ItemSource.CollectionChanged += (_, _) => setItems(itemSource); @@ -235,7 +240,7 @@ protected Dropdown() private void preselectionConfirmed(int selectedIndex) { SelectedItem = MenuItems.ElementAtOrDefault(selectedIndex); - Menu.State = MenuState.Closed; + State.Value = MenuState.Closed; } private void selectionKeyPressed(DropdownHeader.DropdownSelectionAction action) From 43ad6b1ad2d6aac3ff77c138f9f1c26d24b63b78 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 29 Nov 2023 16:43:49 +0300 Subject: [PATCH 02/22] Add implementation for dropdown search bars --- .../Layout/TestSceneFillFlowContainer.cs | 11 ++++ .../UserInterface/TestSceneTabControl.cs | 11 ++++ .../Graphics/UserInterface/BasicDropdown.cs | 26 ++++++++- .../Graphics/UserInterface/Dropdown.cs | 2 +- .../Graphics/UserInterface/DropdownHeader.cs | 45 +++++++++++++++- .../UserInterface/DropdownSearchBar.cs | 53 +++++++++++++++++++ 6 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 osu.Framework/Graphics/UserInterface/DropdownSearchBar.cs diff --git a/osu.Framework.Tests/Visual/Layout/TestSceneFillFlowContainer.cs b/osu.Framework.Tests/Visual/Layout/TestSceneFillFlowContainer.cs index 659eb55abe..a536412ead 100644 --- a/osu.Framework.Tests/Visual/Layout/TestSceneFillFlowContainer.cs +++ b/osu.Framework.Tests/Visual/Layout/TestSceneFillFlowContainer.cs @@ -397,6 +397,17 @@ public TestSceneDropdownHeader() label = new SpriteText(), }; } + + protected override DropdownSearchBar CreateSearchBar() => new BasicDropdownSearchBar(); + + private partial class BasicDropdownSearchBar : DropdownSearchBar + { + protected override void PopIn() => this.FadeIn(); + + protected override void PopOut() => this.FadeOut(); + + protected override TextBox CreateTextBox() => new BasicTextBox(); + } } private partial class AnchorDropdown : BasicDropdown diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneTabControl.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneTabControl.cs index a962c4a5e8..c83d51df50 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneTabControl.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneTabControl.cs @@ -505,6 +505,17 @@ public StyledDropdownHeader() new Box { Width = 20, Height = 20 } }; } + + protected override DropdownSearchBar CreateSearchBar() => new BasicDropdownSearchBar(); + + private partial class BasicDropdownSearchBar : DropdownSearchBar + { + protected override void PopIn() => this.FadeIn(); + + protected override void PopOut() => this.FadeOut(); + + protected override TextBox CreateTextBox() => new BasicTextBox(); + } } private partial class TabControlWithNoDropdown : BasicTabControl diff --git a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs index 651b475b74..7926f3d890 100644 --- a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs +++ b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs @@ -15,6 +15,8 @@ public partial class BasicDropdown : Dropdown public partial class BasicDropdownHeader : DropdownHeader { + private static FontUsage font => FrameworkFont.Condensed; + private readonly SpriteText label; protected internal override LocalisableString Label @@ -25,8 +27,6 @@ protected internal override LocalisableString Label public BasicDropdownHeader() { - var font = FrameworkFont.Condensed; - Foreground.Padding = new MarginPadding(5); BackgroundColour = FrameworkColour.Green; BackgroundColourHover = FrameworkColour.YellowGreen; @@ -41,6 +41,28 @@ public BasicDropdownHeader() }, }; } + + protected override DropdownSearchBar CreateSearchBar() => new BasicDropdownSearchBar(); + + public partial class BasicDropdownSearchBar : DropdownSearchBar + { + protected override void PopIn() => this.FadeIn(); + + protected override void PopOut() => this.FadeOut(); + + protected override TextBox CreateTextBox() => new SearchTextBox + { + PlaceholderText = "type to search", + }; + + private partial class SearchTextBox : BasicTextBox + { + public SearchTextBox() + { + TextContainer.Margin = new MarginPadding { Top = 2 }; + } + } + } } public partial class BasicDropdownMenu : DropdownMenu diff --git a/osu.Framework/Graphics/UserInterface/Dropdown.cs b/osu.Framework/Graphics/UserInterface/Dropdown.cs index 70d0a82000..6afc0a29d9 100644 --- a/osu.Framework/Graphics/UserInterface/Dropdown.cs +++ b/osu.Framework/Graphics/UserInterface/Dropdown.cs @@ -220,7 +220,7 @@ protected Dropdown() Menu.RelativeSizeAxes = Axes.X; - Header.Action = Menu.Toggle; + Header.ToggleMenu = Menu.Toggle; Header.ChangeSelection += selectionKeyPressed; Header.State.BindTo(State); Menu.PreselectionConfirmed += preselectionConfirmed; diff --git a/osu.Framework/Graphics/UserInterface/DropdownHeader.cs b/osu.Framework/Graphics/UserInterface/DropdownHeader.cs index 1603fd1c3c..6af2abcc83 100644 --- a/osu.Framework/Graphics/UserInterface/DropdownHeader.cs +++ b/osu.Framework/Graphics/UserInterface/DropdownHeader.cs @@ -5,6 +5,7 @@ using osuTK.Graphics; using System; +using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; @@ -15,13 +16,17 @@ namespace osu.Framework.Graphics.UserInterface { - public abstract partial class DropdownHeader : ClickableContainer, IKeyBindingHandler + public abstract partial class DropdownHeader : Container, IKeyBindingHandler { public event Action ChangeSelection; protected Container Background; protected Container Foreground; + private readonly DropdownSearchBar searchBar; + + public Bindable SearchTerm => searchBar.SearchTerm; + private Color4 backgroundColour = Color4.DarkGray; protected Color4 BackgroundColour @@ -52,6 +57,12 @@ protected Color4 DisabledColour protected internal abstract LocalisableString Label { get; set; } + public BindableBool Enabled { get; } = new BindableBool(true); + + public IBindable State { get; } = new Bindable(); + + public Action ToggleMenu; + protected DropdownHeader() { Masking = true; @@ -79,13 +90,45 @@ protected DropdownHeader() RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y }, + searchBar = CreateSearchBar().With(s => + { + s.AlwaysPresent = true; + }), }; } + protected abstract DropdownSearchBar CreateSearchBar(); + protected override void LoadComplete() { base.LoadComplete(); + Enabled.BindValueChanged(_ => updateState(), true); + + State.BindValueChanged(v => + { + if (v.NewValue == MenuState.Open) + searchBar.Focus(); + else + searchBar.Reset(); + }, true); + + searchBar.SearchTerm.BindValueChanged(t => + { + if (!string.IsNullOrEmpty(t.NewValue) && string.IsNullOrEmpty(t.OldValue)) + searchBar.Show(); + else if (string.IsNullOrEmpty(t.NewValue) && !string.IsNullOrEmpty(t.OldValue)) + searchBar.Hide(); + }, true); + } + + protected override bool OnClick(ClickEvent e) + { + if (!Enabled.Value) + return false; + + ToggleMenu?.Invoke(); + return false; } protected override bool OnHover(HoverEvent e) diff --git a/osu.Framework/Graphics/UserInterface/DropdownSearchBar.cs b/osu.Framework/Graphics/UserInterface/DropdownSearchBar.cs new file mode 100644 index 0000000000..e3c1f5ebed --- /dev/null +++ b/osu.Framework/Graphics/UserInterface/DropdownSearchBar.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; + +namespace osu.Framework.Graphics.UserInterface +{ + public abstract partial class DropdownSearchBar : VisibilityContainer + { + private TextBox textBox = null!; + private PassThroughInputManager textBoxInputManager = null!; + + public Bindable SearchTerm { get; } = new Bindable(); + + // handling mouse input on dropdown header is not easy, since the menu would lose focus on release and automatically close + public override bool HandlePositionalInput => false; + public override bool PropagatePositionalInputSubTree => false; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + // Dropdown menus rely on their focus state to determine when they should be closed. + // On the other hand, text boxes require to be focused in order for the user to interact with them. + // To handle that matter, we'll wrap the search text box inside a local input manager, and manage its focus state accordingly. + InternalChild = textBoxInputManager = new PassThroughInputManager + { + RelativeSizeAxes = Axes.Both, + Child = textBox = CreateTextBox().With(t => + { + t.RelativeSizeAxes = Axes.Both; + t.Current = SearchTerm; + }) + }; + } + + public void Focus() => textBoxInputManager.ChangeFocus(textBox); + + public void Reset() + { + textBoxInputManager.ChangeFocus(null); + textBox.Text = string.Empty; + + Hide(); + } + + protected abstract TextBox CreateTextBox(); + } +} From 33e2e46b7d292e11864b128c6b961a39c5acfd19 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 29 Nov 2023 20:37:04 +0300 Subject: [PATCH 03/22] Hook up dropdown search bar implementation --- .../Graphics/UserInterface/BasicDropdown.cs | 17 +++++++++ .../Graphics/UserInterface/BasicMenu.cs | 17 +++++++++ .../Graphics/UserInterface/Dropdown.cs | 38 ++++++++++++++----- osu.Framework/Graphics/UserInterface/Menu.cs | 26 ++++++++++++- 4 files changed, 87 insertions(+), 11 deletions(-) diff --git a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs index 7926f3d890..c816036408 100644 --- a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs +++ b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs @@ -75,6 +75,23 @@ public partial class BasicDropdownMenu : DropdownMenu private partial class DrawableBasicDropdownMenuItem : DrawableDropdownMenuItem { + private bool matchingFilter; + + public override bool MatchingFilter + { + get => matchingFilter; + set + { + matchingFilter = value; + this.FadeTo(value ? 1 : 0); + } + } + + public override bool FilteringActive + { + set { } + } + public DrawableBasicDropdownMenuItem(MenuItem item) : base(item) { diff --git a/osu.Framework/Graphics/UserInterface/BasicMenu.cs b/osu.Framework/Graphics/UserInterface/BasicMenu.cs index 38c5401944..9217733385 100644 --- a/osu.Framework/Graphics/UserInterface/BasicMenu.cs +++ b/osu.Framework/Graphics/UserInterface/BasicMenu.cs @@ -25,6 +25,23 @@ public BasicMenu(Direction direction, bool topLevelMenu = false) public partial class BasicDrawableMenuItem : DrawableMenuItem { + private bool matchingFilter; + + public override bool MatchingFilter + { + get => matchingFilter; + set + { + matchingFilter = value; + this.FadeTo(value ? 1 : 0); + } + } + + public override bool FilteringActive + { + set { } + } + public BasicDrawableMenuItem(MenuItem item) : base(item) { diff --git a/osu.Framework/Graphics/UserInterface/Dropdown.cs b/osu.Framework/Graphics/UserInterface/Dropdown.cs index 6afc0a29d9..69e55d350c 100644 --- a/osu.Framework/Graphics/UserInterface/Dropdown.cs +++ b/osu.Framework/Graphics/UserInterface/Dropdown.cs @@ -218,14 +218,20 @@ protected Dropdown() AutoSizeAxes = Axes.Y }; - Menu.RelativeSizeAxes = Axes.X; - Header.ToggleMenu = Menu.Toggle; Header.ChangeSelection += selectionKeyPressed; + + Header.SearchTerm.ValueChanged += t => Menu.SearchTerm = t.NewValue; + Header.State.BindTo(State); + + Menu.RelativeSizeAxes = Axes.X; Menu.PreselectionConfirmed += preselectionConfirmed; + Menu.FilterCompleted += menuFilterCompleted; Menu.StateChanged += state => State.Value = state; + State.ValueChanged += state => Menu.State = state.NewValue; + Current.ValueChanged += val => Scheduler.AddOnce(selectionChanged, val); Current.DisabledChanged += disabled => { @@ -237,9 +243,17 @@ protected Dropdown() ItemSource.CollectionChanged += (_, _) => setItems(itemSource); } - private void preselectionConfirmed(int selectedIndex) + private void menuFilterCompleted() { - SelectedItem = MenuItems.ElementAtOrDefault(selectedIndex); + if (!string.IsNullOrEmpty(Menu.SearchTerm)) + Menu.PreselectItem(0); + else + Menu.PreselectItem(null); + } + + private void preselectionConfirmed(DropdownMenuItem item) + { + SelectedItem = item; State.Value = MenuState.Closed; } @@ -385,13 +399,13 @@ private void clearPreselection(MenuState obj) PreselectItem(null); } - protected internal IEnumerable DrawableMenuItems => Children.OfType(); + protected internal IEnumerable DrawableMenuItems => Children.OfType().Where(i => i.MatchingFilter); protected internal IEnumerable VisibleMenuItems => DrawableMenuItems.Where(item => !item.IsMaskedAway); public DrawableDropdownMenuItem PreselectedItem => DrawableMenuItems.FirstOrDefault(c => c.IsPreSelected) ?? DrawableMenuItems.FirstOrDefault(c => c.IsSelected); - public event Action PreselectionConfirmed; + public event Action> PreselectionConfirmed; /// /// Selects an item from this . @@ -424,13 +438,18 @@ public void SelectItem(DropdownMenuItem item) /// public bool AnyPresent => Children.Any(c => c.IsPresent); - protected void PreselectItem(int index) => PreselectItem(Items[Math.Clamp(index, 0, DrawableMenuItems.Count() - 1)]); + protected internal void PreselectItem(int index) + { + PreselectItem(DrawableMenuItems.Any() + ? DrawableMenuItems.ElementAt(Math.Clamp(index, 0, DrawableMenuItems.Count() - 1)).Item + : null); + } /// /// Preselects an item from this . /// /// The item to select. - protected void PreselectItem(MenuItem item) + protected internal void PreselectItem(MenuItem item) { Children.OfType().ForEach(c => { @@ -586,7 +605,8 @@ protected override bool OnKeyDown(KeyDownEvent e) return true; case Key.Enter: - PreselectionConfirmed?.Invoke(targetPreselectionIndex); + var preselectedItem = DrawableMenuItems.ElementAt(targetPreselectionIndex); + PreselectionConfirmed?.Invoke((DropdownMenuItem)preselectedItem.Item); return true; case Key.Escape: diff --git a/osu.Framework/Graphics/UserInterface/Menu.cs b/osu.Framework/Graphics/UserInterface/Menu.cs index cc7823d39e..61f250db4e 100644 --- a/osu.Framework/Graphics/UserInterface/Menu.cs +++ b/osu.Framework/Graphics/UserInterface/Menu.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Layout; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Framework.Threading; using osuTK; @@ -70,6 +71,21 @@ public abstract partial class Menu : CompositeDrawable, IStateful private readonly Container submenuContainer; private readonly LayoutValue positionLayout = new LayoutValue(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit); + /// + /// Search terms to filter items displayed in this menu. + /// + public string SearchTerm + { + get => itemsFlow.SearchTerm; + set => itemsFlow.SearchTerm = value; + } + + public event Action FilterCompleted + { + add => itemsFlow.FilterCompleted += value; + remove => itemsFlow.FilterCompleted -= value; + } + /// /// Constructs a menu. /// @@ -656,7 +672,7 @@ private void closeFromChild(MenuItem source) #region DrawableMenuItem // must be public due to mono bug(?) https://github.com/ppy/osu/issues/1204 - public abstract partial class DrawableMenuItem : CompositeDrawable, IStateful + public abstract partial class DrawableMenuItem : CompositeDrawable, IStateful, IFilterable { /// /// Invoked when this 's changes. @@ -698,6 +714,12 @@ public abstract partial class DrawableMenuItem : CompositeDrawable, IStateful public virtual bool CloseMenuOnClick => true; + public IEnumerable FilterTerms => Item.Text.Value.Yield(); + + public abstract bool MatchingFilter { get; set; } + + public abstract bool FilteringActive { set; } + protected DrawableMenuItem(MenuItem item) { Item = item; @@ -912,7 +934,7 @@ protected override bool OnClick(ClickEvent e) #endregion - private partial class ItemsFlow : FillFlowContainer + private partial class ItemsFlow : SearchContainer { public readonly LayoutValue SizeCache = new LayoutValue(Invalidation.RequiredParentSizeToFit, InvalidationSource.Self); From 6d215cf9303c547f57056b69d7f6afcd12f0b66a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 1 Dec 2023 16:44:20 +0300 Subject: [PATCH 04/22] Change search bar display implementation to work better --- .../Graphics/UserInterface/Dropdown.cs | 50 +++++++++++++-- .../Graphics/UserInterface/DropdownHeader.cs | 33 +++++----- .../UserInterface/DropdownSearchBar.cs | 62 +++++++++++++++++-- osu.Framework/Graphics/UserInterface/Menu.cs | 10 ++- 4 files changed, 128 insertions(+), 27 deletions(-) diff --git a/osu.Framework/Graphics/UserInterface/Dropdown.cs b/osu.Framework/Graphics/UserInterface/Dropdown.cs index 69e55d350c..40db6a3629 100644 --- a/osu.Framework/Graphics/UserInterface/Dropdown.cs +++ b/osu.Framework/Graphics/UserInterface/Dropdown.cs @@ -30,6 +30,21 @@ public abstract partial class Dropdown : CompositeDrawable, IHasCurrentValue< protected internal DropdownHeader Header; protected internal DropdownMenu Menu; + /// + /// Whether this should always have a search bar displayed in the header when opened. + /// + public bool AlwaysShowSearchBar + { + get => Header.AlwaysShowSearchBar; + set => Header.AlwaysShowSearchBar = value; + } + + public bool AllowNonContiguousMatching + { + get => Menu.AllowNonContiguousMatching; + set => Menu.AllowNonContiguousMatching = value; + } + public Bindable State { get; } = new Bindable(); /// @@ -177,6 +192,25 @@ protected virtual LocalisableString GenerateItemText(T item) } } + /// + /// Puts the state of this one level back: + /// - If the dropdown search bar contains text, this method will reset it. + /// - If the dropdown is open, this method wil close it. + /// + public bool Back() + { + if (Header.SearchBar.Back()) + return true; + + if (State.Value == MenuState.Open) + { + State.Value = MenuState.Closed; + return true; + } + + return false; + } + private readonly BindableWithCurrent current = new BindableWithCurrent(); public Bindable Current @@ -305,6 +339,14 @@ protected override void LoadComplete() Header.Label = SelectedItem?.Text.Value ?? default; } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Key == Key.Escape) + return Back(); + + return false; + } + private void selectionChanged(ValueChangedEvent args) { // refresh if SelectedItem and SelectedValue mismatched @@ -567,6 +609,10 @@ protected override bool OnHover(HoverEvent e) #endregion + // we'll handle closing the menu in Dropdown instead, + // since a search bar may be active and we want to reset it rather than closing the menu. + protected override bool CloseOnEscape => false; + protected override bool OnKeyDown(KeyDownEvent e) { var drawableMenuItemsList = DrawableMenuItems.ToList(); @@ -609,10 +655,6 @@ protected override bool OnKeyDown(KeyDownEvent e) PreselectionConfirmed?.Invoke((DropdownMenuItem)preselectedItem.Item); return true; - case Key.Escape: - State = MenuState.Closed; - return true; - default: return base.OnKeyDown(e); } diff --git a/osu.Framework/Graphics/UserInterface/DropdownHeader.cs b/osu.Framework/Graphics/UserInterface/DropdownHeader.cs index 6af2abcc83..7b83666178 100644 --- a/osu.Framework/Graphics/UserInterface/DropdownHeader.cs +++ b/osu.Framework/Graphics/UserInterface/DropdownHeader.cs @@ -23,9 +23,15 @@ public abstract partial class DropdownHeader : Container, IKeyBindingHandler SearchBar.AlwaysDisplayOnFocus; + set => SearchBar.AlwaysDisplayOnFocus = value; + } + + protected internal DropdownSearchBar SearchBar { get; } - public Bindable SearchTerm => searchBar.SearchTerm; + public Bindable SearchTerm => SearchBar.SearchTerm; private Color4 backgroundColour = Color4.DarkGray; @@ -69,6 +75,7 @@ protected DropdownHeader() RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Width = 1; + InternalChildren = new Drawable[] { Background = new Container @@ -90,10 +97,7 @@ protected DropdownHeader() RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y }, - searchBar = CreateSearchBar().With(s => - { - s.AlwaysPresent = true; - }), + SearchBar = CreateSearchBar(), }; } @@ -108,17 +112,9 @@ protected override void LoadComplete() State.BindValueChanged(v => { if (v.NewValue == MenuState.Open) - searchBar.Focus(); + SearchBar.ObtainFocus(); else - searchBar.Reset(); - }, true); - - searchBar.SearchTerm.BindValueChanged(t => - { - if (!string.IsNullOrEmpty(t.NewValue) && string.IsNullOrEmpty(t.OldValue)) - searchBar.Show(); - else if (string.IsNullOrEmpty(t.NewValue) && !string.IsNullOrEmpty(t.OldValue)) - searchBar.Hide(); + SearchBar.ReleaseFocus(); }, true); } @@ -149,10 +145,11 @@ private void updateState() Background.Colour = IsHovered && Enabled.Value ? BackgroundColourHover : BackgroundColour; } - public override bool HandleNonPositionalInput => IsHovered; - protected override bool OnKeyDown(KeyDownEvent e) { + if (!IsHovered) + return false; + if (!Enabled.Value) return true; diff --git a/osu.Framework/Graphics/UserInterface/DropdownSearchBar.cs b/osu.Framework/Graphics/UserInterface/DropdownSearchBar.cs index e3c1f5ebed..a95560da50 100644 --- a/osu.Framework/Graphics/UserInterface/DropdownSearchBar.cs +++ b/osu.Framework/Graphics/UserInterface/DropdownSearchBar.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Framework.Input; +using osuTK; namespace osu.Framework.Graphics.UserInterface { @@ -19,9 +20,26 @@ public abstract partial class DropdownSearchBar : VisibilityContainer public override bool HandlePositionalInput => false; public override bool PropagatePositionalInputSubTree => false; + private bool obtainedFocus; + + private bool alwaysDisplayOnFocus; + + public bool AlwaysDisplayOnFocus + { + get => alwaysDisplayOnFocus; + set + { + alwaysDisplayOnFocus = value; + + if (IsLoaded) + updateVisibility(); + } + } + [BackgroundDependencyLoader] private void load() { + AlwaysPresent = true; RelativeSizeAxes = Axes.Both; // Dropdown menus rely on their focus state to determine when they should be closed. @@ -32,22 +50,58 @@ private void load() RelativeSizeAxes = Axes.Both, Child = textBox = CreateTextBox().With(t => { + t.ReleaseFocusOnCommit = false; t.RelativeSizeAxes = Axes.Both; + t.Size = new Vector2(1f); t.Current = SearchTerm; }) }; } - public void Focus() => textBoxInputManager.ChangeFocus(textBox); + protected override void LoadComplete() + { + base.LoadComplete(); - public void Reset() + SearchTerm.BindValueChanged(v => updateVisibility()); + updateVisibility(); + } + + public void ObtainFocus() + { + textBoxInputManager.ChangeFocus(textBox); + obtainedFocus = true; + + updateVisibility(); + } + + public void ReleaseFocus() { textBoxInputManager.ChangeFocus(null); - textBox.Text = string.Empty; + SearchTerm.Value = string.Empty; + obtainedFocus = false; + + updateVisibility(); + } - Hide(); + public bool Back() + { + // text box may have lost focus from pressing escape, retain it. + if (obtainedFocus && !textBox.HasFocus) + ObtainFocus(); + + if (!string.IsNullOrEmpty(SearchTerm.Value)) + { + SearchTerm.Value = string.Empty; + return true; + } + + return false; } + private void updateVisibility() => State.Value = obtainedFocus && (AlwaysDisplayOnFocus || !string.IsNullOrEmpty(SearchTerm.Value)) + ? Visibility.Visible + : Visibility.Hidden; + protected abstract TextBox CreateTextBox(); } } diff --git a/osu.Framework/Graphics/UserInterface/Menu.cs b/osu.Framework/Graphics/UserInterface/Menu.cs index 61f250db4e..f1d30c9a60 100644 --- a/osu.Framework/Graphics/UserInterface/Menu.cs +++ b/osu.Framework/Graphics/UserInterface/Menu.cs @@ -80,6 +80,12 @@ public string SearchTerm set => itemsFlow.SearchTerm = value; } + public bool AllowNonContiguousMatching + { + get => itemsFlow.AllowNonContiguousMatching; + set => itemsFlow.AllowNonContiguousMatching = value; + } + public event Action FilterCompleted { add => itemsFlow.FilterCompleted += value; @@ -598,9 +604,11 @@ private void menuItemHovered(DrawableMenuItem item) public override bool HandleNonPositionalInput => State == MenuState.Open; + protected virtual bool CloseOnEscape => !TopLevelMenu; + protected override bool OnKeyDown(KeyDownEvent e) { - if (e.Key == Key.Escape && !TopLevelMenu) + if (e.Key == Key.Escape && CloseOnEscape) { Close(); return true; From 09a14e1262bcddf5825ad44313b21633c21b4e8d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Dec 2023 22:07:45 +0300 Subject: [PATCH 05/22] Rename menu item properties in `Dropdown` to make sense with searching --- .../Visual/UserInterface/TestSceneDropdown.cs | 8 +++---- .../Graphics/UserInterface/Dropdown.cs | 24 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs index 2aae971425..1094b56326 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs @@ -143,10 +143,10 @@ public void TestKeyboardSelection(bool cleanSelection) AddAssert("previous item is selected", () => testDropdown.SelectedIndex == Math.Max(0, previousIndex - 1)); AddStep("select last item", () => InputManager.Keys(PlatformAction.MoveToListEnd)); - AddAssert("last item selected", () => testDropdown.SelectedItem == testDropdown.Menu.DrawableMenuItems.Last().Item); + AddAssert("last item selected", () => testDropdown.SelectedItem == testDropdown.Menu.VisibleMenuItems.Last().Item); AddStep("select last item", () => InputManager.Keys(PlatformAction.MoveToListStart)); - AddAssert("first item selected", () => testDropdown.SelectedItem == testDropdown.Menu.DrawableMenuItems.First().Item); + AddAssert("first item selected", () => testDropdown.SelectedItem == testDropdown.Menu.VisibleMenuItems.First().Item); AddStep("select next item when empty", () => InputManager.Key(Key.Up)); AddStep("select previous item when empty", () => InputManager.Key(Key.Down)); @@ -302,7 +302,7 @@ public void TestAddItemBeforeDropdownLoad() }; }); - AddAssert("text is expected", () => dropdown.Menu.DrawableMenuItems.First().ChildrenOfType().First().Text.ToString(), () => Is.EqualTo("loaded: test")); + AddAssert("text is expected", () => dropdown.Menu.VisibleMenuItems.First().ChildrenOfType().First().Text.ToString(), () => Is.EqualTo("loaded: test")); } /// @@ -324,7 +324,7 @@ public void TestAddItemWhileDropdownIsInReadyState() dropdown.Items = new TestModel("test").Yield(); }); - AddAssert("text is expected", () => dropdown.Menu.DrawableMenuItems.First(d => d.IsSelected).ChildrenOfType().First().Text.ToString(), () => Is.EqualTo("loaded: test")); + AddAssert("text is expected", () => dropdown.Menu.VisibleMenuItems.First(d => d.IsSelected).ChildrenOfType().First().Text.ToString(), () => Is.EqualTo("loaded: test")); } /// diff --git a/osu.Framework/Graphics/UserInterface/Dropdown.cs b/osu.Framework/Graphics/UserInterface/Dropdown.cs index 40db6a3629..585c97bbf3 100644 --- a/osu.Framework/Graphics/UserInterface/Dropdown.cs +++ b/osu.Framework/Graphics/UserInterface/Dropdown.cs @@ -441,11 +441,11 @@ private void clearPreselection(MenuState obj) PreselectItem(null); } - protected internal IEnumerable DrawableMenuItems => Children.OfType().Where(i => i.MatchingFilter); - protected internal IEnumerable VisibleMenuItems => DrawableMenuItems.Where(item => !item.IsMaskedAway); + protected internal IEnumerable VisibleMenuItems => Children.OfType().Where(i => i.MatchingFilter); + protected internal IEnumerable MenuItemsInView => VisibleMenuItems.Where(item => !item.IsMaskedAway); - public DrawableDropdownMenuItem PreselectedItem => DrawableMenuItems.FirstOrDefault(c => c.IsPreSelected) - ?? DrawableMenuItems.FirstOrDefault(c => c.IsSelected); + public DrawableDropdownMenuItem PreselectedItem => VisibleMenuItems.FirstOrDefault(c => c.IsPreSelected) + ?? VisibleMenuItems.FirstOrDefault(c => c.IsSelected); public event Action> PreselectionConfirmed; @@ -482,8 +482,8 @@ public void SelectItem(DropdownMenuItem item) protected internal void PreselectItem(int index) { - PreselectItem(DrawableMenuItems.Any() - ? DrawableMenuItems.ElementAt(Math.Clamp(index, 0, DrawableMenuItems.Count() - 1)).Item + PreselectItem(VisibleMenuItems.Any() + ? VisibleMenuItems.ElementAt(Math.Clamp(index, 0, VisibleMenuItems.Count() - 1)).Item : null); } @@ -615,12 +615,12 @@ protected override bool OnHover(HoverEvent e) protected override bool OnKeyDown(KeyDownEvent e) { - var drawableMenuItemsList = DrawableMenuItems.ToList(); - if (!drawableMenuItemsList.Any()) + var visibleMenuItemsList = VisibleMenuItems.ToList(); + if (!visibleMenuItemsList.Any()) return base.OnKeyDown(e); var currentPreselected = PreselectedItem; - int targetPreselectionIndex = drawableMenuItemsList.IndexOf(currentPreselected); + int targetPreselectionIndex = visibleMenuItemsList.IndexOf(currentPreselected); switch (e.Key) { @@ -638,7 +638,7 @@ protected override bool OnKeyDown(KeyDownEvent e) if (currentPreselected == firstVisibleItem) PreselectItem(targetPreselectionIndex - VisibleMenuItems.Count()); else - PreselectItem(drawableMenuItemsList.IndexOf(firstVisibleItem)); + PreselectItem(visibleMenuItemsList.IndexOf(firstVisibleItem)); return true; case Key.PageDown: @@ -647,11 +647,11 @@ protected override bool OnKeyDown(KeyDownEvent e) if (currentPreselected == lastVisibleItem) PreselectItem(targetPreselectionIndex + VisibleMenuItems.Count()); else - PreselectItem(drawableMenuItemsList.IndexOf(lastVisibleItem)); + PreselectItem(visibleMenuItemsList.IndexOf(lastVisibleItem)); return true; case Key.Enter: - var preselectedItem = DrawableMenuItems.ElementAt(targetPreselectionIndex); + var preselectedItem = VisibleMenuItems.ElementAt(targetPreselectionIndex); PreselectionConfirmed?.Invoke((DropdownMenuItem)preselectedItem.Item); return true; From 54a23fb71d273d48d4936c84492e2a48ec1e6dce Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Dec 2023 21:59:30 +0300 Subject: [PATCH 06/22] Move `ManualTextInput` to a separate class --- .../UserInterface/TestSceneTextBoxEvents.cs | 52 ++---------------- .../TestSceneTextBoxKeyEvents.cs | 3 +- .../Testing/Input/ManualTextInputSource.cs | 53 +++++++++++++++++++ 3 files changed, 59 insertions(+), 49 deletions(-) create mode 100644 osu.Framework/Testing/Input/ManualTextInputSource.cs diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs index 008df0226c..643924de69 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Testing; +using osu.Framework.Testing.Input; using osuTK; using osuTK.Input; @@ -20,7 +21,7 @@ namespace osu.Framework.Tests.Visual.UserInterface public partial class TestSceneTextBoxEvents : ManualInputManagerTestScene { private EventQueuesTextBox textBox; - private ManualTextInput textInput; + private ManualTextInputSource textInput; private ManualTextInputContainer textInputContainer; private const string default_text = "some default text"; @@ -618,57 +619,12 @@ protected override void OnImeResult(string result, bool successful) => public partial class ManualTextInputContainer : Container { [Cached(typeof(TextInputSource))] - public readonly ManualTextInput TextInput; + public readonly ManualTextInputSource TextInput; public ManualTextInputContainer() { RelativeSizeAxes = Axes.Both; - TextInput = new ManualTextInput(); - } - } - - public class ManualTextInput : TextInputSource - { - public void Text(string text) => TriggerTextInput(text); - - public new void TriggerImeComposition(string text, int start, int length) - { - base.TriggerImeComposition(text, start, length); - } - - public new void TriggerImeResult(string text) - { - base.TriggerImeResult(text); - } - - public override void ResetIme() - { - base.ResetIme(); - - // this call will be somewhat delayed in a real world scenario, but let's run it immediately for simplicity. - base.TriggerImeComposition(string.Empty, 0, 0); - } - - public readonly Queue ActivationQueue = new Queue(); - public readonly Queue EnsureActivatedQueue = new Queue(); - public readonly Queue DeactivationQueue = new Queue(); - - protected override void ActivateTextInput(bool allowIme) - { - base.ActivateTextInput(allowIme); - ActivationQueue.Enqueue(allowIme); - } - - protected override void EnsureTextInputActivated(bool allowIme) - { - base.EnsureTextInputActivated(allowIme); - EnsureActivatedQueue.Enqueue(allowIme); - } - - protected override void DeactivateTextInput() - { - base.DeactivateTextInput(); - DeactivationQueue.Enqueue(true); + TextInput = new ManualTextInputSource(); } } diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxKeyEvents.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxKeyEvents.cs index 0d8ae811e4..b1281b4a43 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxKeyEvents.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxKeyEvents.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Events; using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Framework.Testing.Input; using osuTK; using osuTK.Input; @@ -20,7 +21,7 @@ public partial class TestSceneTextBoxKeyEvents : ManualInputManagerTestScene { private KeyEventQueuesTextBox textBox; - private TestSceneTextBoxEvents.ManualTextInput textInput; + private ManualTextInputSource textInput; [Resolved] private GameHost host { get; set; } diff --git a/osu.Framework/Testing/Input/ManualTextInputSource.cs b/osu.Framework/Testing/Input/ManualTextInputSource.cs new file mode 100644 index 0000000000..def56cb23b --- /dev/null +++ b/osu.Framework/Testing/Input/ManualTextInputSource.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Input; + +namespace osu.Framework.Testing.Input +{ + public class ManualTextInputSource : TextInputSource + { + public readonly Queue ActivationQueue = new Queue(); + public readonly Queue EnsureActivatedQueue = new Queue(); + public readonly Queue DeactivationQueue = new Queue(); + + public void Text(string text) => TriggerTextInput(text); + + public new void TriggerImeComposition(string text, int start, int length) + { + base.TriggerImeComposition(text, start, length); + } + + public new void TriggerImeResult(string text) + { + base.TriggerImeResult(text); + } + + public override void ResetIme() + { + base.ResetIme(); + + // this call will be somewhat delayed in a real world scenario, but let's run it immediately for simplicity. + base.TriggerImeComposition(string.Empty, 0, 0); + } + + protected override void ActivateTextInput(bool allowIme) + { + base.ActivateTextInput(allowIme); + ActivationQueue.Enqueue(allowIme); + } + + protected override void EnsureTextInputActivated(bool allowIme) + { + base.EnsureTextInputActivated(allowIme); + EnsureActivatedQueue.Enqueue(allowIme); + } + + protected override void DeactivateTextInput() + { + base.DeactivateTextInput(); + DeactivationQueue.Enqueue(true); + } + } +} From 739d7d69cdc70200f8a8672ab75c1d69708df1a7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Dec 2023 22:10:50 +0300 Subject: [PATCH 07/22] Add test coverage --- .../Visual/UserInterface/TestSceneDropdown.cs | 120 +++++++++++++++++- 1 file changed, 115 insertions(+), 5 deletions(-) diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs index 1094b56326..ae7473f8f8 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs @@ -15,6 +15,7 @@ using osu.Framework.Input; using osu.Framework.Localisation; using osu.Framework.Testing; +using osu.Framework.Testing.Input; using osuTK; using osuTK.Input; @@ -365,11 +366,114 @@ public void TestSetNonExistentItem([Values] bool afterBdl) AddAssert("text is expected", () => dropdown.SelectedItem.Text.Value.ToString(), () => Is.EqualTo("loaded: non-existent item")); } + #region Searching + + [Test] + public void TestSearching() + { + ManualTextDropdown dropdown = null!; + + AddStep("setup dropdown", () => dropdown = createDropdowns(1)[0]); + AddAssert("search bar hidden", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + + toggleDropdownViaClick(() => dropdown); + + AddAssert("search bar still hidden", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + + AddStep("trigger text", () => dropdown.TextInput.Text("test 4")); + AddAssert("search bar visible", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + AddAssert("items filtered", () => + { + var drawableItem = dropdown.Menu.VisibleMenuItems.Single(i => i.IsPresent); + return drawableItem.Item.Text.Value == "test 4"; + }); + AddAssert("item preselected", () => dropdown.Menu.VisibleMenuItems.Single().IsPreSelected); + + AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddAssert("item selected", () => dropdown.SelectedItem.Text.Value == "test 4"); + } + + [Test] + public void TestReleaseFocusAfterSearching() + { + ManualTextDropdown dropdown = null!; + + AddStep("setup dropdown", () => dropdown = createDropdowns(1)[0]); + toggleDropdownViaClick(() => dropdown); + + AddStep("trigger text", () => dropdown.TextInput.Text("test 4")); + AddAssert("search bar visible", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + + AddStep("press escape", () => InputManager.Key(Key.Escape)); + AddAssert("search bar hidden", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + AddAssert("dropdown still open", () => dropdown.State.Value == MenuState.Open); + + AddStep("press escape again", () => InputManager.Key(Key.Escape)); + AddAssert("dropdown closed", () => dropdown.State.Value == MenuState.Closed); + + toggleDropdownViaClick(() => dropdown); + AddStep("trigger text", () => dropdown.TextInput.Text("test 4")); + AddAssert("search bar visible", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + + AddStep("click away", () => + { + InputManager.MoveMouseTo(Vector2.Zero); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("search bar hidden", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + } + + [Test] + public void TestAlwaysShowSearchBar() + { + ManualTextDropdown dropdown = null!; + + AddStep("setup dropdown", () => + { + dropdown = createDropdowns(1)[0]; + dropdown.AlwaysShowSearchBar = true; + }); + + AddAssert("search bar hidden", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + toggleDropdownViaClick(() => dropdown); + + AddAssert("search bar visible", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + + AddStep("trigger text", () => dropdown.TextInput.Text("test 4")); + AddAssert("search bar still visible", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + + AddStep("press escape", () => InputManager.Key(Key.Escape)); + AddAssert("search bar still visible", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + AddAssert("dropdown still open", () => dropdown.State.Value == MenuState.Open); + + AddStep("press escape again", () => InputManager.Key(Key.Escape)); + AddAssert("dropdown closed", () => dropdown.State.Value == MenuState.Closed); + AddAssert("search bar hidden", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + + toggleDropdownViaClick(() => dropdown); + AddStep("trigger text", () => dropdown.TextInput.Text("test 4")); + AddAssert("search bar visible", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + + AddStep("click away", () => + { + InputManager.MoveMouseTo(Vector2.Zero); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("search bar hidden", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + } + + #endregion + private TestDropdown createDropdown() => createDropdowns(1).Single(); - private TestDropdown[] createDropdowns(int count) + private TestDropdown[] createDropdowns(int count) => createDropdowns(count); + + private TDropdown[] createDropdowns(int count) + where TDropdown : TestDropdown, new() { - TestDropdown[] dropdowns = new TestDropdown[count]; + TDropdown[] dropdowns = new TDropdown[count]; for (int dropdownIndex = 0; dropdownIndex < count; dropdownIndex++) { @@ -377,7 +481,7 @@ private TestDropdown[] createDropdowns(int count) for (int itemIndex = 0; itemIndex < items_to_add; itemIndex++) testItems[itemIndex] = "test " + itemIndex; - dropdowns[dropdownIndex] = new TestDropdown + dropdowns[dropdownIndex] = new TDropdown { Position = new Vector2(50f, 50f), Width = 150, @@ -435,8 +539,14 @@ private partial class TestDropdown : BasicDropdown { internal new DropdownMenuItem SelectedItem => base.SelectedItem; - public int SelectedIndex => Menu.DrawableMenuItems.Select(d => d.Item).ToList().IndexOf(SelectedItem); - public int PreselectedIndex => Menu.DrawableMenuItems.ToList().IndexOf(Menu.PreselectedItem); + public int SelectedIndex => Menu.VisibleMenuItems.Select(d => d.Item).ToList().IndexOf(SelectedItem); + public int PreselectedIndex => Menu.VisibleMenuItems.ToList().IndexOf(Menu.PreselectedItem); + } + + private partial class ManualTextDropdown : TestDropdown + { + [Cached(typeof(TextInputSource))] + public readonly ManualTextInputSource TextInput = new ManualTextInputSource(); } /// From eae6d6790794b780e1b7315d1fb7af7ea9966782 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Dec 2023 22:12:58 +0300 Subject: [PATCH 08/22] Allow specifying custom text size in `TextBox` --- .../Visual/UserInterface/TestSceneTextBox.cs | 2 +- .../Graphics/UserInterface/BasicTextBox.cs | 2 +- osu.Framework/Graphics/UserInterface/TextBox.cs | 16 +++++++++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs index f3f09e793e..b7cad8fe19 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs @@ -878,7 +878,7 @@ private partial class NumberTextBox : BasicTextBox private partial class CustomTextBox : BasicTextBox { - protected override Drawable GetDrawableCharacter(char c) => new ScalingText(c, CalculatedTextSize); + protected override Drawable GetDrawableCharacter(char c) => new ScalingText(c, DisplayedTextSize); private partial class ScalingText : CompositeDrawable { diff --git a/osu.Framework/Graphics/UserInterface/BasicTextBox.cs b/osu.Framework/Graphics/UserInterface/BasicTextBox.cs index 69609ee23b..b9c92487fb 100644 --- a/osu.Framework/Graphics/UserInterface/BasicTextBox.cs +++ b/osu.Framework/Graphics/UserInterface/BasicTextBox.cs @@ -96,7 +96,7 @@ protected override void OnFocus(FocusEvent e) protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer { AutoSizeAxes = Axes.Both, - Child = new SpriteText { Text = c.ToString(), Font = FrameworkFont.Condensed.With(size: CalculatedTextSize) } + Child = new SpriteText { Text = c.ToString(), Font = FrameworkFont.Condensed.With(size: DisplayedTextSize) } }; protected override SpriteText CreatePlaceholder() => new FadingPlaceholderText diff --git a/osu.Framework/Graphics/UserInterface/TextBox.cs b/osu.Framework/Graphics/UserInterface/TextBox.cs index d9a74aecab..7f76b8d4df 100644 --- a/osu.Framework/Graphics/UserInterface/TextBox.cs +++ b/osu.Framework/Graphics/UserInterface/TextBox.cs @@ -507,7 +507,7 @@ protected override void Dispose(bool isDisposing) private void updateCursorAndLayout() { - Placeholder.Font = Placeholder.Font.With(size: CalculatedTextSize); + Placeholder.Font = Placeholder.Font.With(size: DisplayedTextSize); float cursorPos = 0; if (text.Length > 0) @@ -740,7 +740,7 @@ private string removeCharacters(int number = 1) /// /// The character that this should represent. /// A that represents the character - protected virtual Drawable GetDrawableCharacter(char c) => new SpriteText { Text = c.ToString(), Font = new FontUsage(size: CalculatedTextSize) }; + protected virtual Drawable GetDrawableCharacter(char c) => new SpriteText { Text = c.ToString(), Font = new FontUsage(size: DisplayedTextSize) }; protected virtual Drawable AddCharacterToFlow(char c) { @@ -770,7 +770,17 @@ protected virtual Drawable AddCharacterToFlow(char c) private float getDepthForCharacterIndex(int index) => -index; - protected float CalculatedTextSize => TextFlow.DrawSize.Y - (TextFlow.Padding.Top + TextFlow.Padding.Bottom); + private readonly float? customTextSize; + + /// + /// A fixed size for the text displayed in this . If left unset, text size will be computed based on the dimensions of the . + /// + public float TextSize + { + init => customTextSize = value; + } + + protected float DisplayedTextSize => customTextSize ?? (TextFlow.DrawSize.Y - (TextFlow.Padding.Top + TextFlow.Padding.Bottom)); protected void InsertString(string value) { From a399c1bc0f8bcb71af48c331d3a8a84627de1045 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Dec 2023 22:14:49 +0300 Subject: [PATCH 09/22] Make text size of search bar match dropdown header --- osu.Framework/Graphics/UserInterface/BasicDropdown.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs index c816036408..b35bbf5c16 100644 --- a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs +++ b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs @@ -53,6 +53,7 @@ public partial class BasicDropdownSearchBar : DropdownSearchBar protected override TextBox CreateTextBox() => new SearchTextBox { PlaceholderText = "type to search", + TextSize = font.Size, }; private partial class SearchTextBox : BasicTextBox From a385e2af129fc4433bff4bd720de06e6eac1291f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Dec 2023 22:23:50 +0300 Subject: [PATCH 10/22] Set correct default state of `matchingFilter` --- osu.Framework/Graphics/UserInterface/BasicDropdown.cs | 2 +- osu.Framework/Graphics/UserInterface/BasicMenu.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs index b35bbf5c16..8eb1ebfd76 100644 --- a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs +++ b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs @@ -76,7 +76,7 @@ public partial class BasicDropdownMenu : DropdownMenu private partial class DrawableBasicDropdownMenuItem : DrawableDropdownMenuItem { - private bool matchingFilter; + private bool matchingFilter = true; public override bool MatchingFilter { diff --git a/osu.Framework/Graphics/UserInterface/BasicMenu.cs b/osu.Framework/Graphics/UserInterface/BasicMenu.cs index 9217733385..7f7d3e881a 100644 --- a/osu.Framework/Graphics/UserInterface/BasicMenu.cs +++ b/osu.Framework/Graphics/UserInterface/BasicMenu.cs @@ -25,7 +25,7 @@ public BasicMenu(Direction direction, bool topLevelMenu = false) public partial class BasicDrawableMenuItem : DrawableMenuItem { - private bool matchingFilter; + private bool matchingFilter = true; public override bool MatchingFilter { From c6c97eed36ea3b0b6756fc1661e3bfe86a9e64bf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Dec 2023 22:54:29 +0300 Subject: [PATCH 11/22] Add basic test case --- .../Visual/UserInterface/TestSceneDropdown.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs index ae7473f8f8..3938919ff9 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs @@ -25,6 +25,16 @@ public partial class TestSceneDropdown : ManualInputManagerTestScene { private const int items_to_add = 10; + [Test] + public void TestBasic() + { + AddStep("setup dropdowns", () => + { + TestDropdown[] dropdowns = createDropdowns(2); + dropdowns[1].AlwaysShowSearchBar = true; + }); + } + [Test] public void TestSelectByUserInteraction() { From 0200dfa7c23c70683d58da10007063d44b806442 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 5 Dec 2023 00:55:54 +0300 Subject: [PATCH 12/22] Move filtering entirely from `Menu` to `DropdownMenu` --- .../Graphics/UserInterface/BasicDropdown.cs | 17 ------ .../Graphics/UserInterface/BasicMenu.cs | 17 ------ .../Graphics/UserInterface/Dropdown.cs | 59 ++++++++++++++++++- osu.Framework/Graphics/UserInterface/Menu.cs | 56 ++++++------------ 4 files changed, 77 insertions(+), 72 deletions(-) diff --git a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs index 8eb1ebfd76..4ca92fec03 100644 --- a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs +++ b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs @@ -76,23 +76,6 @@ public partial class BasicDropdownMenu : DropdownMenu private partial class DrawableBasicDropdownMenuItem : DrawableDropdownMenuItem { - private bool matchingFilter = true; - - public override bool MatchingFilter - { - get => matchingFilter; - set - { - matchingFilter = value; - this.FadeTo(value ? 1 : 0); - } - } - - public override bool FilteringActive - { - set { } - } - public DrawableBasicDropdownMenuItem(MenuItem item) : base(item) { diff --git a/osu.Framework/Graphics/UserInterface/BasicMenu.cs b/osu.Framework/Graphics/UserInterface/BasicMenu.cs index 7f7d3e881a..38c5401944 100644 --- a/osu.Framework/Graphics/UserInterface/BasicMenu.cs +++ b/osu.Framework/Graphics/UserInterface/BasicMenu.cs @@ -25,23 +25,6 @@ public BasicMenu(Direction direction, bool topLevelMenu = false) public partial class BasicDrawableMenuItem : DrawableMenuItem { - private bool matchingFilter = true; - - public override bool MatchingFilter - { - get => matchingFilter; - set - { - matchingFilter = value; - this.FadeTo(value ? 1 : 0); - } - } - - public override bool FilteringActive - { - set { } - } - public BasicDrawableMenuItem(MenuItem item) : base(item) { diff --git a/osu.Framework/Graphics/UserInterface/Dropdown.cs b/osu.Framework/Graphics/UserInterface/Dropdown.cs index 585c97bbf3..8f3da76506 100644 --- a/osu.Framework/Graphics/UserInterface/Dropdown.cs +++ b/osu.Framework/Graphics/UserInterface/Dropdown.cs @@ -421,6 +421,29 @@ internal void ShowItem(T val) public abstract partial class DropdownMenu : Menu, IKeyBindingHandler { + private SearchContainer itemsFlow; + + /// + /// Search terms to filter items displayed in this menu. + /// + public string SearchTerm + { + get => itemsFlow.SearchTerm; + set => itemsFlow.SearchTerm = value; + } + + public bool AllowNonContiguousMatching + { + get => itemsFlow.AllowNonContiguousMatching; + set => itemsFlow.AllowNonContiguousMatching = value; + } + + public event Action FilterCompleted + { + add => itemsFlow.FilterCompleted += value; + remove => itemsFlow.FilterCompleted -= value; + } + protected DropdownMenu() : base(Direction.Vertical) { @@ -515,10 +538,27 @@ private static bool compareItemEquality(MenuItem a, MenuItem b) #region DrawableDropdownMenuItem - public abstract partial class DrawableDropdownMenuItem : DrawableMenuItem + public abstract partial class DrawableDropdownMenuItem : DrawableMenuItem, IFilterable { public event Action> PreselectionRequested; + private bool matchingFilter = true; + + public bool MatchingFilter + { + get => matchingFilter; + set + { + matchingFilter = value; + UpdateFilteringState(value); + } + } + + public virtual bool FilteringActive + { + set { } + } + protected DrawableDropdownMenuItem(MenuItem item) : base(item) { @@ -600,6 +640,8 @@ protected override void UpdateForegroundColour() Foreground.FadeColour(IsPreSelected ? ForegroundColourHover : IsSelected ? ForegroundColourSelected : ForegroundColour); } + protected virtual void UpdateFilteringState(bool filtered) => this.FadeTo(filtered ? 1 : 0); + protected override bool OnHover(HoverEvent e) { PreselectionRequested?.Invoke(Item as DropdownMenuItem); @@ -680,6 +722,21 @@ public bool OnPressed(KeyBindingPressEvent e) public void OnReleased(KeyBindingReleaseEvent e) { } + + internal override IItemsFlow CreateItemsFlow(FillDirection direction) => (IItemsFlow)(itemsFlow = new SearchableItemsFlow + { + Direction = direction, + }); + + private partial class SearchableItemsFlow : SearchContainer, IItemsFlow + { + public LayoutValue SizeCache { get; } = new LayoutValue(Invalidation.RequiredParentSizeToFit, InvalidationSource.Self); + + public SearchableItemsFlow() + { + AddLayout(SizeCache); + } + } } #endregion diff --git a/osu.Framework/Graphics/UserInterface/Menu.cs b/osu.Framework/Graphics/UserInterface/Menu.cs index f1d30c9a60..c67759cb92 100644 --- a/osu.Framework/Graphics/UserInterface/Menu.cs +++ b/osu.Framework/Graphics/UserInterface/Menu.cs @@ -62,7 +62,7 @@ public abstract partial class Menu : CompositeDrawable, IStateful protected readonly Direction Direction; - private ItemsFlow itemsFlow; + private FillFlowContainer itemsFlow; private Menu parentMenu; private Menu submenu; @@ -71,27 +71,6 @@ public abstract partial class Menu : CompositeDrawable, IStateful private readonly Container submenuContainer; private readonly LayoutValue positionLayout = new LayoutValue(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit); - /// - /// Search terms to filter items displayed in this menu. - /// - public string SearchTerm - { - get => itemsFlow.SearchTerm; - set => itemsFlow.SearchTerm = value; - } - - public bool AllowNonContiguousMatching - { - get => itemsFlow.AllowNonContiguousMatching; - set => itemsFlow.AllowNonContiguousMatching = value; - } - - public event Action FilterCompleted - { - add => itemsFlow.FilterCompleted += value; - remove => itemsFlow.FilterCompleted -= value; - } - /// /// Constructs a menu. /// @@ -123,7 +102,7 @@ protected Menu(Direction direction, bool topLevelMenu = false) { d.RelativeSizeAxes = Axes.Both; d.Masking = false; - d.Child = itemsFlow = new ItemsFlow { Direction = direction == Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical }; + d.Child = itemsFlow = (FillFlowContainer)CreateItemsFlow(direction == Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical); }) } }, @@ -203,7 +182,7 @@ public float MaxWidth maxWidth = value; - itemsFlow.SizeCache.Invalidate(); + ((IItemsFlow)itemsFlow).SizeCache.Invalidate(); } } @@ -222,7 +201,7 @@ public float MaxHeight maxHeight = value; - itemsFlow.SizeCache.Invalidate(); + ((IItemsFlow)itemsFlow).SizeCache.Invalidate(); } } @@ -292,7 +271,7 @@ private void resetState() return; submenu?.Close(); - itemsFlow.SizeCache.Invalidate(); + ((IItemsFlow)itemsFlow).SizeCache.Invalidate(); } /// @@ -309,7 +288,7 @@ public virtual void Add(MenuItem item) drawableItem.SetFlowDirection(Direction); ItemsContainer.Add(drawableItem); - itemsFlow.SizeCache.Invalidate(); + ((IItemsFlow)itemsFlow).SizeCache.Invalidate(); } private void itemStateChanged(DrawableMenuItem item, MenuItemState state) @@ -329,7 +308,7 @@ private void itemStateChanged(DrawableMenuItem item, MenuItemState state) public bool Remove(MenuItem item) { bool result = ItemsContainer.RemoveAll(d => d.Item == item, true) > 0; - itemsFlow.SizeCache.Invalidate(); + ((IItemsFlow)itemsFlow).SizeCache.Invalidate(); return result; } @@ -461,7 +440,7 @@ protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); - if (!itemsFlow.SizeCache.IsValid) + if (!((IItemsFlow)itemsFlow).SizeCache.IsValid) { // Our children will be relatively-sized on the axis separate to the menu direction, so we need to compute // that size ourselves, based on the content size of our children, to give them a valid relative size @@ -495,7 +474,7 @@ protected override void UpdateAfterChildren() UpdateSize(new Vector2(width, height)); - itemsFlow.SizeCache.Validate(); + ((IItemsFlow)itemsFlow).SizeCache.Validate(); } } @@ -677,10 +656,12 @@ private void closeFromChild(MenuItem source) /// The . protected abstract ScrollContainer CreateScrollContainer(Direction direction); + internal virtual IItemsFlow CreateItemsFlow(FillDirection direction) => new ItemsFlow { Direction = direction }; + #region DrawableMenuItem // must be public due to mono bug(?) https://github.com/ppy/osu/issues/1204 - public abstract partial class DrawableMenuItem : CompositeDrawable, IStateful, IFilterable + public abstract partial class DrawableMenuItem : CompositeDrawable, IStateful { /// /// Invoked when this 's changes. @@ -724,10 +705,6 @@ public abstract partial class DrawableMenuItem : CompositeDrawable, IStateful FilterTerms => Item.Text.Value.Yield(); - public abstract bool MatchingFilter { get; set; } - - public abstract bool FilteringActive { set; } - protected DrawableMenuItem(MenuItem item) { Item = item; @@ -942,9 +919,14 @@ protected override bool OnClick(ClickEvent e) #endregion - private partial class ItemsFlow : SearchContainer + internal interface IItemsFlow : IFillFlowContainer + { + LayoutValue SizeCache { get; } + } + + private partial class ItemsFlow : FillFlowContainer, IItemsFlow { - public readonly LayoutValue SizeCache = new LayoutValue(Invalidation.RequiredParentSizeToFit, InvalidationSource.Self); + public LayoutValue SizeCache { get; } = new LayoutValue(Invalidation.RequiredParentSizeToFit, InvalidationSource.Self); public ItemsFlow() { From d5744b513f50a91c14bd8bc49dd1065d2ce53a8a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 5 Dec 2023 00:58:26 +0300 Subject: [PATCH 13/22] Revert existence of general bindable for dropdown state --- .../Visual/UserInterface/TestSceneDropdown.cs | 8 ++-- .../Graphics/UserInterface/Dropdown.cs | 38 +++++++++---------- .../Graphics/UserInterface/DropdownHeader.cs | 18 ++++----- 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs index 3938919ff9..f73c4437b0 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs @@ -416,10 +416,10 @@ public void TestReleaseFocusAfterSearching() AddStep("press escape", () => InputManager.Key(Key.Escape)); AddAssert("search bar hidden", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); - AddAssert("dropdown still open", () => dropdown.State.Value == MenuState.Open); + AddAssert("dropdown still open", () => dropdown.Menu.State == MenuState.Open); AddStep("press escape again", () => InputManager.Key(Key.Escape)); - AddAssert("dropdown closed", () => dropdown.State.Value == MenuState.Closed); + AddAssert("dropdown closed", () => dropdown.Menu.State == MenuState.Closed); toggleDropdownViaClick(() => dropdown); AddStep("trigger text", () => dropdown.TextInput.Text("test 4")); @@ -455,10 +455,10 @@ public void TestAlwaysShowSearchBar() AddStep("press escape", () => InputManager.Key(Key.Escape)); AddAssert("search bar still visible", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); - AddAssert("dropdown still open", () => dropdown.State.Value == MenuState.Open); + AddAssert("dropdown still open", () => dropdown.Menu.State == MenuState.Open); AddStep("press escape again", () => InputManager.Key(Key.Escape)); - AddAssert("dropdown closed", () => dropdown.State.Value == MenuState.Closed); + AddAssert("dropdown closed", () => dropdown.Menu.State == MenuState.Closed); AddAssert("search bar hidden", () => dropdown.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); toggleDropdownViaClick(() => dropdown); diff --git a/osu.Framework/Graphics/UserInterface/Dropdown.cs b/osu.Framework/Graphics/UserInterface/Dropdown.cs index 8f3da76506..5cc8b27d92 100644 --- a/osu.Framework/Graphics/UserInterface/Dropdown.cs +++ b/osu.Framework/Graphics/UserInterface/Dropdown.cs @@ -15,6 +15,7 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Layout; using osu.Framework.Localisation; using osuTK.Graphics; using osuTK.Input; @@ -45,8 +46,6 @@ public bool AllowNonContiguousMatching set => Menu.AllowNonContiguousMatching = value; } - public Bindable State { get; } = new Bindable(); - /// /// Creates the header part of the control. /// @@ -131,7 +130,7 @@ private void addDropdownItem(T value) if (!Current.Disabled) Current.Value = value; - State.Value = MenuState.Closed; + Menu.Close(); }); // inheritors expect that `virtual GenerateItemText` is only called when this dropdown's BDL has run to completion. @@ -202,9 +201,9 @@ public bool Back() if (Header.SearchBar.Back()) return true; - if (State.Value == MenuState.Open) + if (Menu.State == MenuState.Open) { - State.Value = MenuState.Closed; + Menu.Close(); return true; } @@ -257,27 +256,34 @@ protected Dropdown() Header.SearchTerm.ValueChanged += t => Menu.SearchTerm = t.NewValue; - Header.State.BindTo(State); - Menu.RelativeSizeAxes = Axes.X; Menu.PreselectionConfirmed += preselectionConfirmed; - Menu.FilterCompleted += menuFilterCompleted; - Menu.StateChanged += state => State.Value = state; + Menu.FilterCompleted += filterCompleted; - State.ValueChanged += state => Menu.State = state.NewValue; + Menu.StateChanged += state => + { + Menu.State = state; + Header.UpdateSearchBarFocus(state); + }; Current.ValueChanged += val => Scheduler.AddOnce(selectionChanged, val); Current.DisabledChanged += disabled => { Header.Enabled.Value = !disabled; - if (disabled && State.Value == MenuState.Open) - State.Value = MenuState.Closed; + if (disabled && Menu.State == MenuState.Open) + Menu.State = MenuState.Closed; }; ItemSource.CollectionChanged += (_, _) => setItems(itemSource); } - private void menuFilterCompleted() + private void preselectionConfirmed(DropdownMenuItem item) + { + SelectedItem = item; + Menu.State = MenuState.Closed; + } + + private void filterCompleted() { if (!string.IsNullOrEmpty(Menu.SearchTerm)) Menu.PreselectItem(0); @@ -285,12 +291,6 @@ private void menuFilterCompleted() Menu.PreselectItem(null); } - private void preselectionConfirmed(DropdownMenuItem item) - { - SelectedItem = item; - State.Value = MenuState.Closed; - } - private void selectionKeyPressed(DropdownHeader.DropdownSelectionAction action) { if (!MenuItems.Any()) diff --git a/osu.Framework/Graphics/UserInterface/DropdownHeader.cs b/osu.Framework/Graphics/UserInterface/DropdownHeader.cs index 7b83666178..2981c8017c 100644 --- a/osu.Framework/Graphics/UserInterface/DropdownHeader.cs +++ b/osu.Framework/Graphics/UserInterface/DropdownHeader.cs @@ -65,8 +65,6 @@ protected Color4 DisabledColour public BindableBool Enabled { get; } = new BindableBool(true); - public IBindable State { get; } = new Bindable(); - public Action ToggleMenu; protected DropdownHeader() @@ -108,14 +106,6 @@ protected override void LoadComplete() base.LoadComplete(); Enabled.BindValueChanged(_ => updateState(), true); - - State.BindValueChanged(v => - { - if (v.NewValue == MenuState.Open) - SearchBar.ObtainFocus(); - else - SearchBar.ReleaseFocus(); - }, true); } protected override bool OnClick(ClickEvent e) @@ -139,6 +129,14 @@ protected override void OnHoverLost(HoverLostEvent e) base.OnHoverLost(e); } + public void UpdateSearchBarFocus(MenuState state) + { + if (state == MenuState.Open) + SearchBar.ObtainFocus(); + else + SearchBar.ReleaseFocus(); + } + private void updateState() { Colour = Enabled.Value ? Color4.White : DisabledColour; From 1833630542b567be58d6978c735d7dc2ac5430ff Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 5 Dec 2023 00:59:31 +0300 Subject: [PATCH 14/22] Remove `CloseOnEscape` and add local handling in `DropdownMenu` --- .../Graphics/UserInterface/Dropdown.cs | 83 ++++++++++--------- osu.Framework/Graphics/UserInterface/Menu.cs | 4 +- 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/osu.Framework/Graphics/UserInterface/Dropdown.cs b/osu.Framework/Graphics/UserInterface/Dropdown.cs index 5cc8b27d92..a7d5147fd6 100644 --- a/osu.Framework/Graphics/UserInterface/Dropdown.cs +++ b/osu.Framework/Graphics/UserInterface/Dropdown.cs @@ -651,55 +651,56 @@ protected override bool OnHover(HoverEvent e) #endregion - // we'll handle closing the menu in Dropdown instead, - // since a search bar may be active and we want to reset it rather than closing the menu. - protected override bool CloseOnEscape => false; - protected override bool OnKeyDown(KeyDownEvent e) { var visibleMenuItemsList = VisibleMenuItems.ToList(); - if (!visibleMenuItemsList.Any()) - return base.OnKeyDown(e); - - var currentPreselected = PreselectedItem; - int targetPreselectionIndex = visibleMenuItemsList.IndexOf(currentPreselected); - switch (e.Key) + if (visibleMenuItemsList.Any()) { - case Key.Up: - PreselectItem(targetPreselectionIndex - 1); - return true; - - case Key.Down: - PreselectItem(targetPreselectionIndex + 1); - return true; - - case Key.PageUp: - var firstVisibleItem = VisibleMenuItems.First(); - - if (currentPreselected == firstVisibleItem) - PreselectItem(targetPreselectionIndex - VisibleMenuItems.Count()); - else - PreselectItem(visibleMenuItemsList.IndexOf(firstVisibleItem)); - return true; - - case Key.PageDown: - var lastVisibleItem = VisibleMenuItems.Last(); + var currentPreselected = PreselectedItem; + int targetPreselectionIndex = visibleMenuItemsList.IndexOf(currentPreselected); - if (currentPreselected == lastVisibleItem) - PreselectItem(targetPreselectionIndex + VisibleMenuItems.Count()); - else - PreselectItem(visibleMenuItemsList.IndexOf(lastVisibleItem)); - return true; + switch (e.Key) + { + case Key.Up: + PreselectItem(targetPreselectionIndex - 1); + return true; + + case Key.Down: + PreselectItem(targetPreselectionIndex + 1); + return true; + + case Key.PageUp: + var firstVisibleItem = VisibleMenuItems.First(); + + if (currentPreselected == firstVisibleItem) + PreselectItem(targetPreselectionIndex - VisibleMenuItems.Count()); + else + PreselectItem(visibleMenuItemsList.IndexOf(firstVisibleItem)); + return true; + + case Key.PageDown: + var lastVisibleItem = VisibleMenuItems.Last(); + + if (currentPreselected == lastVisibleItem) + PreselectItem(targetPreselectionIndex + VisibleMenuItems.Count()); + else + PreselectItem(visibleMenuItemsList.IndexOf(lastVisibleItem)); + return true; + + case Key.Enter: + var preselectedItem = VisibleMenuItems.ElementAt(targetPreselectionIndex); + PreselectionConfirmed?.Invoke((DropdownMenuItem)preselectedItem.Item); + return true; + } + } - case Key.Enter: - var preselectedItem = VisibleMenuItems.ElementAt(targetPreselectionIndex); - PreselectionConfirmed?.Invoke((DropdownMenuItem)preselectedItem.Item); - return true; + if (e.Key == Key.Escape) + // we'll handle closing the menu in Dropdown instead, + // since a search bar may be active and we want to reset it rather than closing the menu. + return false; - default: - return base.OnKeyDown(e); - } + return base.OnKeyDown(e); } public bool OnPressed(KeyBindingPressEvent e) diff --git a/osu.Framework/Graphics/UserInterface/Menu.cs b/osu.Framework/Graphics/UserInterface/Menu.cs index c67759cb92..d46426893d 100644 --- a/osu.Framework/Graphics/UserInterface/Menu.cs +++ b/osu.Framework/Graphics/UserInterface/Menu.cs @@ -583,11 +583,9 @@ private void menuItemHovered(DrawableMenuItem item) public override bool HandleNonPositionalInput => State == MenuState.Open; - protected virtual bool CloseOnEscape => !TopLevelMenu; - protected override bool OnKeyDown(KeyDownEvent e) { - if (e.Key == Key.Escape && CloseOnEscape) + if (e.Key == Key.Escape && !TopLevelMenu) { Close(); return true; From 5bfe0a57743ef59aedf1ccfef4c537b3ce280335 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 5 Dec 2023 00:59:37 +0300 Subject: [PATCH 15/22] Convert to auto-property --- osu.Framework/Graphics/UserInterface/Menu.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Framework/Graphics/UserInterface/Menu.cs b/osu.Framework/Graphics/UserInterface/Menu.cs index d46426893d..e52273a63e 100644 --- a/osu.Framework/Graphics/UserInterface/Menu.cs +++ b/osu.Framework/Graphics/UserInterface/Menu.cs @@ -48,7 +48,7 @@ public abstract partial class Menu : CompositeDrawable, IStateful /// /// The that contains the items of this . /// - protected FillFlowContainer ItemsContainer => itemsFlow; + protected FillFlowContainer ItemsContainer { get; private set; } /// /// The container that provides the masking effects for this . @@ -62,7 +62,6 @@ public abstract partial class Menu : CompositeDrawable, IStateful protected readonly Direction Direction; - private FillFlowContainer itemsFlow; private Menu parentMenu; private Menu submenu; @@ -102,7 +101,7 @@ protected Menu(Direction direction, bool topLevelMenu = false) { d.RelativeSizeAxes = Axes.Both; d.Masking = false; - d.Child = itemsFlow = (FillFlowContainer)CreateItemsFlow(direction == Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical); + d.Child = ItemsContainer = (FillFlowContainer)CreateItemsFlow(direction == Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical); }) } }, @@ -182,7 +181,7 @@ public float MaxWidth maxWidth = value; - ((IItemsFlow)itemsFlow).SizeCache.Invalidate(); + ((IItemsFlow)ItemsContainer).SizeCache.Invalidate(); } } @@ -201,7 +200,7 @@ public float MaxHeight maxHeight = value; - ((IItemsFlow)itemsFlow).SizeCache.Invalidate(); + ((IItemsFlow)ItemsContainer).SizeCache.Invalidate(); } } @@ -271,7 +270,7 @@ private void resetState() return; submenu?.Close(); - ((IItemsFlow)itemsFlow).SizeCache.Invalidate(); + ((IItemsFlow)ItemsContainer).SizeCache.Invalidate(); } /// @@ -288,7 +287,7 @@ public virtual void Add(MenuItem item) drawableItem.SetFlowDirection(Direction); ItemsContainer.Add(drawableItem); - ((IItemsFlow)itemsFlow).SizeCache.Invalidate(); + ((IItemsFlow)ItemsContainer).SizeCache.Invalidate(); } private void itemStateChanged(DrawableMenuItem item, MenuItemState state) @@ -308,7 +307,7 @@ private void itemStateChanged(DrawableMenuItem item, MenuItemState state) public bool Remove(MenuItem item) { bool result = ItemsContainer.RemoveAll(d => d.Item == item, true) > 0; - ((IItemsFlow)itemsFlow).SizeCache.Invalidate(); + ((IItemsFlow)ItemsContainer).SizeCache.Invalidate(); return result; } @@ -440,7 +439,7 @@ protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); - if (!((IItemsFlow)itemsFlow).SizeCache.IsValid) + if (!((IItemsFlow)ItemsContainer).SizeCache.IsValid) { // Our children will be relatively-sized on the axis separate to the menu direction, so we need to compute // that size ourselves, based on the content size of our children, to give them a valid relative size @@ -474,7 +473,7 @@ protected override void UpdateAfterChildren() UpdateSize(new Vector2(width, height)); - ((IItemsFlow)itemsFlow).SizeCache.Validate(); + ((IItemsFlow)ItemsContainer).SizeCache.Validate(); } } From f0df44dfec826dab92bf5b89aa00dc64561d12ec Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 5 Dec 2023 01:04:58 +0300 Subject: [PATCH 16/22] Simplify custom textbox font size logic --- .../Visual/UserInterface/TestSceneTextBox.cs | 2 +- .../Graphics/UserInterface/BasicDropdown.cs | 2 +- .../Graphics/UserInterface/BasicTextBox.cs | 2 +- osu.Framework/Graphics/UserInterface/TextBox.cs | 13 ++++++------- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs index b7cad8fe19..f012f96ca6 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs @@ -878,7 +878,7 @@ private partial class NumberTextBox : BasicTextBox private partial class CustomTextBox : BasicTextBox { - protected override Drawable GetDrawableCharacter(char c) => new ScalingText(c, DisplayedTextSize); + protected override Drawable GetDrawableCharacter(char c) => new ScalingText(c, FontSize); private partial class ScalingText : CompositeDrawable { diff --git a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs index 4ca92fec03..b84cdcfdb6 100644 --- a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs +++ b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs @@ -53,7 +53,7 @@ public partial class BasicDropdownSearchBar : DropdownSearchBar protected override TextBox CreateTextBox() => new SearchTextBox { PlaceholderText = "type to search", - TextSize = font.Size, + FontSize = font.Size, }; private partial class SearchTextBox : BasicTextBox diff --git a/osu.Framework/Graphics/UserInterface/BasicTextBox.cs b/osu.Framework/Graphics/UserInterface/BasicTextBox.cs index b9c92487fb..1a81b67224 100644 --- a/osu.Framework/Graphics/UserInterface/BasicTextBox.cs +++ b/osu.Framework/Graphics/UserInterface/BasicTextBox.cs @@ -96,7 +96,7 @@ protected override void OnFocus(FocusEvent e) protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer { AutoSizeAxes = Axes.Both, - Child = new SpriteText { Text = c.ToString(), Font = FrameworkFont.Condensed.With(size: DisplayedTextSize) } + Child = new SpriteText { Text = c.ToString(), Font = FrameworkFont.Condensed.With(size: FontSize) } }; protected override SpriteText CreatePlaceholder() => new FadingPlaceholderText diff --git a/osu.Framework/Graphics/UserInterface/TextBox.cs b/osu.Framework/Graphics/UserInterface/TextBox.cs index 7f76b8d4df..c969c909a4 100644 --- a/osu.Framework/Graphics/UserInterface/TextBox.cs +++ b/osu.Framework/Graphics/UserInterface/TextBox.cs @@ -507,7 +507,7 @@ protected override void Dispose(bool isDisposing) private void updateCursorAndLayout() { - Placeholder.Font = Placeholder.Font.With(size: DisplayedTextSize); + Placeholder.Font = Placeholder.Font.With(size: FontSize); float cursorPos = 0; if (text.Length > 0) @@ -740,7 +740,7 @@ private string removeCharacters(int number = 1) /// /// The character that this should represent. /// A that represents the character - protected virtual Drawable GetDrawableCharacter(char c) => new SpriteText { Text = c.ToString(), Font = new FontUsage(size: DisplayedTextSize) }; + protected virtual Drawable GetDrawableCharacter(char c) => new SpriteText { Text = c.ToString(), Font = new FontUsage(size: FontSize) }; protected virtual Drawable AddCharacterToFlow(char c) { @@ -770,18 +770,17 @@ protected virtual Drawable AddCharacterToFlow(char c) private float getDepthForCharacterIndex(int index) => -index; - private readonly float? customTextSize; + private readonly float? customFontSize; /// /// A fixed size for the text displayed in this . If left unset, text size will be computed based on the dimensions of the . /// - public float TextSize + public float FontSize { - init => customTextSize = value; + get => customFontSize ?? TextFlow.DrawSize.Y - (TextFlow.Padding.Top + TextFlow.Padding.Bottom); + init => customFontSize = value; } - protected float DisplayedTextSize => customTextSize ?? (TextFlow.DrawSize.Y - (TextFlow.Padding.Top + TextFlow.Padding.Bottom)); - protected void InsertString(string value) { // inserting text could insert it in the middle of an active composition, leading to an invalid state. From 393a87d276088e834b5f09f2320b4483b81002da Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 12 Dec 2023 20:53:50 +0300 Subject: [PATCH 17/22] Fix text box layout when using arbitrary font sizes --- .../Graphics/UserInterface/BasicDropdown.cs | 10 +--------- .../Graphics/UserInterface/BasicTextBox.cs | 17 +++++++---------- osu.Framework/Graphics/UserInterface/TextBox.cs | 10 +++++++--- osu.Framework/Testing/TestBrowser.cs | 2 +- 4 files changed, 16 insertions(+), 23 deletions(-) diff --git a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs index b84cdcfdb6..843e1e57cb 100644 --- a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs +++ b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs @@ -50,19 +50,11 @@ public partial class BasicDropdownSearchBar : DropdownSearchBar protected override void PopOut() => this.FadeOut(); - protected override TextBox CreateTextBox() => new SearchTextBox + protected override TextBox CreateTextBox() => new BasicTextBox { PlaceholderText = "type to search", FontSize = font.Size, }; - - private partial class SearchTextBox : BasicTextBox - { - public SearchTextBox() - { - TextContainer.Margin = new MarginPadding { Top = 2 }; - } - } } } diff --git a/osu.Framework/Graphics/UserInterface/BasicTextBox.cs b/osu.Framework/Graphics/UserInterface/BasicTextBox.cs index 1a81b67224..46802d8cbb 100644 --- a/osu.Framework/Graphics/UserInterface/BasicTextBox.cs +++ b/osu.Framework/Graphics/UserInterface/BasicTextBox.cs @@ -140,20 +140,17 @@ public partial class BasicCaret : Caret { public BasicCaret() { - RelativeSizeAxes = Axes.Y; - Size = new Vector2(1, 0.9f); - Colour = Color4.Transparent; - Anchor = Anchor.CentreLeft; - Origin = Anchor.CentreLeft; - - Masking = true; - CornerRadius = 1; - InternalChild = new Box + InternalChild = new Container { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, - Colour = Color4.White, + Height = 0.9f, + CornerRadius = 1f, + Masking = true, + Child = new Box { RelativeSizeAxes = Axes.Both }, }; } diff --git a/osu.Framework/Graphics/UserInterface/TextBox.cs b/osu.Framework/Graphics/UserInterface/TextBox.cs index c969c909a4..fa3f0ead8a 100644 --- a/osu.Framework/Graphics/UserInterface/TextBox.cs +++ b/osu.Framework/Graphics/UserInterface/TextBox.cs @@ -170,14 +170,17 @@ protected TextBox() Children = new Drawable[] { Placeholder = CreatePlaceholder(), - caret = CreateCaret(), + caret = CreateCaret().With(c => + { + c.Anchor = Anchor.CentreLeft; + c.Origin = Anchor.CentreLeft; + }), TextFlow = new FillFlowContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.Both, }, }, }, @@ -507,6 +510,7 @@ protected override void Dispose(bool isDisposing) private void updateCursorAndLayout() { + caret.Height = FontSize; Placeholder.Font = Placeholder.Font.With(size: FontSize); float cursorPos = 0; diff --git a/osu.Framework/Testing/TestBrowser.cs b/osu.Framework/Testing/TestBrowser.cs index ee68420c1c..5d5e9a97d9 100644 --- a/osu.Framework/Testing/TestBrowser.cs +++ b/osu.Framework/Testing/TestBrowser.cs @@ -655,7 +655,7 @@ private partial class TestBrowserTextBox : BasicTextBox public TestBrowserTextBox() { - TextFlow.Height = 0.75f; + FontSize = 14f; } } } From bb9f45e537dea5c33e6cadcb6156c1704b01a204 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 12 Dec 2023 21:05:49 +0300 Subject: [PATCH 18/22] Fix compile error --- osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs index 7481f9a3a9..1c7d75a780 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs @@ -341,7 +341,7 @@ public void TestClearItemsInBindableWhileNotPresent() AddStep("hide dropdown", () => testDropdown.Hide()); AddStep("clear items", () => bindableList.Clear()); AddStep("show dropdown", () => testDropdown.Show()); - AddAssert("dropdown menu empty", () => !testDropdown.Menu.DrawableMenuItems.Any()); + AddAssert("dropdown menu empty", () => !testDropdown.Menu.Children.Any()); } /// From 09589659f4f263b9c2da9c99b9dedf46ba49222d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 12 Dec 2023 21:05:34 +0300 Subject: [PATCH 19/22] Fix automatic textbox font size broken by layout changes --- osu.Framework/Graphics/UserInterface/TextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Graphics/UserInterface/TextBox.cs b/osu.Framework/Graphics/UserInterface/TextBox.cs index fa3f0ead8a..4f92c9612c 100644 --- a/osu.Framework/Graphics/UserInterface/TextBox.cs +++ b/osu.Framework/Graphics/UserInterface/TextBox.cs @@ -781,7 +781,7 @@ protected virtual Drawable AddCharacterToFlow(char c) /// public float FontSize { - get => customFontSize ?? TextFlow.DrawSize.Y - (TextFlow.Padding.Top + TextFlow.Padding.Bottom); + get => customFontSize ?? TextContainer.DrawSize.Y; init => customFontSize = value; } From 1876a7146a6ad1feeb6d85f15b1b85521b209946 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 12 Dec 2023 21:11:51 +0300 Subject: [PATCH 20/22] Update test border caret --- .../Visual/UserInterface/TestSceneTextBox.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs index f012f96ca6..4eb7c5ba5d 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs @@ -923,16 +923,19 @@ private partial class BorderCaret : Caret public BorderCaret() { - RelativeSizeAxes = Axes.Y; - - Masking = true; - BorderColour = Color4.White; - BorderThickness = 3; - - InternalChild = new Box + InternalChild = new Container { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, - Colour = Color4.Transparent + Masking = true, + BorderColour = Colour4.White, + BorderThickness = 3f, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Transparent, + }, }; } From fe0b532c9f692379ad39b8addb93fd954065f58c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 12 Dec 2023 21:12:13 +0300 Subject: [PATCH 21/22] Remove unused using directive --- osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs index 4eb7c5ba5d..1da11b240d 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs @@ -16,7 +16,6 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osuTK; -using osuTK.Graphics; using osuTK.Input; namespace osu.Framework.Tests.Visual.UserInterface From d5c2e65a32c50ef953df13faa5543906c796baff Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 12 Dec 2023 21:22:54 +0300 Subject: [PATCH 22/22] Enforce centre-left anchor/origin on placeholder text --- osu.Framework/Graphics/UserInterface/TextBox.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Framework/Graphics/UserInterface/TextBox.cs b/osu.Framework/Graphics/UserInterface/TextBox.cs index 4f92c9612c..d0331a5a24 100644 --- a/osu.Framework/Graphics/UserInterface/TextBox.cs +++ b/osu.Framework/Graphics/UserInterface/TextBox.cs @@ -169,7 +169,11 @@ protected TextBox() Position = new Vector2(LeftRightPadding, 0), Children = new Drawable[] { - Placeholder = CreatePlaceholder(), + Placeholder = CreatePlaceholder().With(p => + { + p.Anchor = Anchor.CentreLeft; + p.Origin = Anchor.CentreLeft; + }), caret = CreateCaret().With(c => { c.Anchor = Anchor.CentreLeft;