Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add event handler for user change of TabControl tab selection #6218

Merged
merged 12 commits into from Mar 21, 2024
148 changes: 148 additions & 0 deletions osu.Framework.Tests/Visual/UserInterface/TestSceneTabControlEvents.cs
@@ -0,0 +1,148 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Testing;
using osuTK;
using osuTK.Input;

namespace osu.Framework.Tests.Visual.UserInterface
{
public partial class TestSceneTabControlEvents : ManualInputManagerTestScene
{
private EventQueuesTabControl tabControl = null!;

[SetUpSteps]
public void SetUpSteps()
{
AddStep("add tab control", () => Child = tabControl = new EventQueuesTabControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(150, 40),
Items = Enum.GetValues<TestEnum>(),
});

AddAssert("selected tab queue empty", () => tabControl.UserTabSelectionChangedQueue.Count == 0);
}

[Test]
public void TestClickSendsEvent()
{
AddStep("click second tab", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<TabItem<TestEnum>>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});

AddAssert("selected tab = second", () => tabControl.Current.Value == TestEnum.Second);
AddAssert("selected tab queue has \"second\"", () => tabControl.UserTabSelectionChangedQueue.Dequeue().Value == TestEnum.Second);
}

[Test]
public void TestClickSameTabDoesNotSendEvent()
{
AddAssert("first tab selected", () => tabControl.Current.Value == TestEnum.First);
AddStep("click first tab", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<TabItem<TestEnum>>().First());
InputManager.Click(MouseButton.Left);
});

AddAssert("first tab still selected", () => tabControl.Current.Value == TestEnum.First);
AddAssert("selected tab queue empty", () => tabControl.UserTabSelectionChangedQueue.Count == 0);
}

[Test]
public void TestSelectItemMethodSendsEvent()
{
AddStep("call select item", () => tabControl.SelectItem(TestEnum.Second));
AddAssert("selected tab queue has \"second\"", () => tabControl.UserTabSelectionChangedQueue.Dequeue().Value == TestEnum.Second);
}

[Test]
public void TestSwitchTabMethodSendsEvent()
{
AddStep("set switchable", () => tabControl.IsSwitchable = true);
AddStep("call switch tab", () => tabControl.SwitchTab(1));
AddAssert("selected tab = second", () => tabControl.Current.Value == TestEnum.Second);
AddAssert("selected tab queue has \"second\"", () => tabControl.UserTabSelectionChangedQueue.Dequeue().Value == TestEnum.Second);
AddStep("call switch tab", () => tabControl.SwitchTab(-1));
AddAssert("selected tab = second", () => tabControl.Current.Value == TestEnum.First);
AddAssert("selected tab queue has \"second\"", () => tabControl.UserTabSelectionChangedQueue.Dequeue().Value == TestEnum.First);
}

[Test]
public void TestSwitchUsingKeyBindingSendsEvent()
{
AddStep("set switchable", () => tabControl.IsSwitchable = true);
AddStep("switch forward", () => InputManager.Keys(PlatformAction.DocumentNext));
AddAssert("selected tab = second", () => tabControl.Current.Value == TestEnum.Second);
AddAssert("selected tab queue has \"second\"", () => tabControl.UserTabSelectionChangedQueue.Dequeue().Value == TestEnum.Second);
AddStep("switch backward", () => InputManager.Keys(PlatformAction.DocumentPrevious));
AddAssert("selected tab = second", () => tabControl.Current.Value == TestEnum.First);
AddAssert("selected tab queue has \"second\"", () => tabControl.UserTabSelectionChangedQueue.Dequeue().Value == TestEnum.First);
}

[Test]
public void TestSwitchOnRemovalDoesNotSendEvent()
{
AddStep("set switchable", () => tabControl.IsSwitchable = true);
AddStep("remove first tab", () => tabControl.RemoveItem(TestEnum.First));

AddAssert("selected tab = second", () => tabControl.Current.Value == TestEnum.Second);
AddAssert("selected tab queue still empty", () => tabControl.UserTabSelectionChangedQueue.Count == 0);
}

[Test]
public void TestBindableChangeDoesNotSendEvent()
{
AddStep("set selected tab = second", () => tabControl.Current.Value = TestEnum.Second);
AddAssert("selected tab queue still empty", () => tabControl.UserTabSelectionChangedQueue.Count == 0);
}

[TearDownSteps]
public void TearDownSteps()
{
AddAssert("selected tab queue empty", () => tabControl.UserTabSelectionChangedQueue.Count == 0);
}

private partial class EventQueuesTabControl : BasicTabControl<TestEnum>
{
public readonly Queue<TabItem<TestEnum>> UserTabSelectionChangedQueue = new Queue<TabItem<TestEnum>>();

private Box background = null!;

[BackgroundDependencyLoader]
private void load()
{
AddInternal(background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = FrameworkColour.YellowGreen,
Alpha = 0,
});
}

protected override void OnUserTabSelectionChanged(TabItem<TestEnum> item)
{
UserTabSelectionChangedQueue.Enqueue(item);
background.FadeOutFromOne(500);
}
}

private enum TestEnum
{
First,
Second,
}
}
}
94 changes: 64 additions & 30 deletions osu.Framework/Graphics/UserInterface/TabControl.cs
Expand Up @@ -208,10 +208,22 @@ protected override void LoadComplete()

Current.BindValueChanged(v =>
{
if (v.NewValue != null && tabMap.TryGetValue(v.NewValue, out var found))
selectTab(found);
else
selectTab(null);
TabItem<T> tab = null;

if (v.NewValue != null)
tabMap.TryGetValue(v.NewValue, out tab);

// Only reorder if not pinned and not showing
if (AutoSort && tab != null && !tab.IsPresent && !tab.Pinned)
performTabSort(tab);

// Deactivate previously selected tab
if (SelectedTab != null && SelectedTab != tab) SelectedTab.Active.Value = false;

SelectedTab = tab;

if (SelectedTab != null)
SelectedTab.Active.Value = true;
}, true);

// TabContainer doesn't have valid layout yet, so TabItems all have y=0 and selectTab() didn't call performTabSort() so we call it here instead
Expand Down Expand Up @@ -263,24 +275,22 @@ public void UnpinItem(T item)
/// </summary>
public void Clear() => Items = Array.Empty<T>();

private TabItem<T> addTab(T value, bool addToDropdown = true)
private void addTab(T value, bool addToDropdown = true)
{
// Do not allow duplicate adding
if (tabMap.ContainsKey(value))
throw new InvalidOperationException($"Item {value} has already been added to this {nameof(TabControl<T>)}");

var tab = CreateTabItem(value);
AddTabItem(tab, addToDropdown);

return tab;
}

private void removeTab(T value, bool removeFromDropdown = true)
{
if (!tabMap.ContainsKey(value))
if (!tabMap.TryGetValue(value, out var tab))
throw new InvalidOperationException($"Item {value} doesn't exist in this {nameof(TabControl<T>)}.");

RemoveTabItem(tabMap[value], removeFromDropdown);
RemoveTabItem(tab, removeFromDropdown);
}

/// <summary>
Expand Down Expand Up @@ -323,7 +333,10 @@ protected virtual void RemoveTabItem(TabItem<T> tab, bool removeFromDropdown = t
{
// check all tabs as to include self (in correct iteration order)
bool anySwitchableTabsToRight = AllTabs.SkipWhile(t => t != tab).Skip(1).Any(t => t.IsSwitchable);
SwitchTab(anySwitchableTabsToRight ? 1 : -1);

// switching tab on removal is not directly caused by the user.
// call the private method to not trigger a user change event.
switchTab(anySwitchableTabsToRight ? 1 : -1);
}
}

Expand All @@ -349,36 +362,40 @@ private void updateDropdown(TabItem<T> tab, bool isVisible)
}

/// <summary>
/// Selects a <see cref="TabItem{T}"/>.
/// Selects the tab representing the provided item.
/// </summary>
/// <param name="tab">The tab to select.</param>
protected virtual void SelectTab(TabItem<T> tab)
/// <param name="item">The item to select.</param>
/// <exception cref="InvalidOperationException">Thrown when the provided item doesn't exist in the tab control.</exception>
public void SelectItem(T item)
{
selectTab(tab);
Current.Value = SelectedTab != null ? SelectedTab.Value : default;
if (!tabMap.TryGetValue(item, out var tab))
throw new InvalidOperationException($"Item {item} cannot be selected as it does not exist in this {nameof(TabControl<T>)}");

SelectTab(tab);
}

private void selectTab(TabItem<T> tab)
/// <summary>
/// Selects a <see cref="TabItem{T}"/> and signals an event that the user selected the given tab via <see cref="OnUserTabSelectionChanged"/>.
/// </summary>
/// <param name="tab">The tab to select.</param>
protected void SelectTab(TabItem<T> tab)
peppy marked this conversation as resolved.
Show resolved Hide resolved
{
// Only reorder if not pinned and not showing
if (AutoSort && tab != null && !tab.IsPresent && !tab.Pinned)
performTabSort(tab);

// Deactivate previously selected tab
if (SelectedTab != null && SelectedTab != tab) SelectedTab.Active.Value = false;

SelectedTab = tab;

if (SelectedTab != null)
SelectedTab.Active.Value = true;
if (selectTab(tab))
OnUserTabSelectionChanged(tab);
}

/// <summary>
/// Switches the currently selected tab forward or backward one index, optionally wrapping.
/// </summary>
/// <param name="direction">Pass 1 to move to the next tab, or -1 to move to the previous tab.</param>
/// <param name="wrap">If <c>true</c>, moving past the start or the end of the tab list will wrap to the opposite end.</param>
public virtual void SwitchTab(int direction, bool wrap = true)
public void SwitchTab(int direction, bool wrap = true)
{
if (switchTab(direction, wrap))
OnUserTabSelectionChanged(SelectedTab);
}

private bool switchTab(int direction, bool wrap = true)
{
if (Math.Abs(direction) != 1) throw new ArgumentException("value must be -1 or 1", nameof(direction));

Expand All @@ -394,8 +411,16 @@ public virtual void SwitchTab(int direction, bool wrap = true)
if (found == null && wrap)
found = allTabs.FirstOrDefault(t => t != SelectedTab);

if (found != null)
SelectTab(found);
return found != null && selectTab(found);
}

private bool selectTab(TabItem<T> tab)
{
if (tab == SelectedTab)
return false;

Current.Value = tab != null ? tab.Value : default;
return true;
}

private void activationRequested(TabItem<T> tab)
Expand Down Expand Up @@ -440,6 +465,15 @@ public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
{
}

/// <summary>
/// Invoked when the user directly changed tab selection, either by clicking on another tab or switching to the tab using <see cref="PlatformAction"/> key bindings.
/// Note that this does not get invoked when tab selection is changed as a result of a user closing the currently selected tab (see <see cref="SwitchTabOnRemove"/>).
/// </summary>
/// <param name="item">The new tab item.</param>
protected virtual void OnUserTabSelectionChanged(TabItem<T> item)
{
}

/// <summary>
/// Creates the <see cref="TabFillFlowContainer"/> to contain the <see cref="TabItem{T}"/>s.
/// </summary>
Expand Down