mirror of
https://github.com/ppy/osu.git
synced 2025-01-30 01:32:55 +08:00
Merge pull request #11182 from angelaz1/keyboard_shortcuts
Add browser-like tab management hotkeys to chat overlay
This commit is contained in:
commit
daff4a1a00
@ -9,6 +9,8 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
@ -36,6 +38,10 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1);
|
private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1);
|
||||||
private Channel channel1 => channels[0];
|
private Channel channel1 => channels[0];
|
||||||
private Channel channel2 => channels[1];
|
private Channel channel2 => channels[1];
|
||||||
|
private Channel channel3 => channels[2];
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private GameHost host { get; set; }
|
||||||
|
|
||||||
public TestSceneChatOverlay()
|
public TestSceneChatOverlay()
|
||||||
{
|
{
|
||||||
@ -44,7 +50,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
Name = $"Channel no. {index}",
|
Name = $"Channel no. {index}",
|
||||||
Topic = index == 3 ? null : $"We talk about the number {index} here",
|
Topic = index == 3 ? null : $"We talk about the number {index} here",
|
||||||
Type = index % 2 == 0 ? ChannelType.PM : ChannelType.Temporary
|
Type = index % 2 == 0 ? ChannelType.PM : ChannelType.Temporary,
|
||||||
|
Id = index
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@ -228,6 +235,92 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddAssert("All channels closed", () => !channelManager.JoinedChannels.Any());
|
AddAssert("All channels closed", () => !channelManager.JoinedChannels.Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCloseTabShortcut()
|
||||||
|
{
|
||||||
|
AddStep("Join 2 channels", () =>
|
||||||
|
{
|
||||||
|
channelManager.JoinChannel(channel1);
|
||||||
|
channelManager.JoinChannel(channel2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Want to close channel 2
|
||||||
|
AddStep("Select channel 2", () => clickDrawable(chatOverlay.TabMap[channel2]));
|
||||||
|
AddStep("Close tab via shortcut", pressCloseDocumentKeys);
|
||||||
|
|
||||||
|
// Channel 2 should be closed
|
||||||
|
AddAssert("Channel 1 open", () => channelManager.JoinedChannels.Contains(channel1));
|
||||||
|
AddAssert("Channel 2 closed", () => !channelManager.JoinedChannels.Contains(channel2));
|
||||||
|
|
||||||
|
// Want to close channel 1
|
||||||
|
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
|
||||||
|
|
||||||
|
AddStep("Close tab via shortcut", pressCloseDocumentKeys);
|
||||||
|
// Channel 1 and channel 2 should be closed
|
||||||
|
AddAssert("All channels closed", () => !channelManager.JoinedChannels.Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNewTabShortcut()
|
||||||
|
{
|
||||||
|
AddStep("Join 2 channels", () =>
|
||||||
|
{
|
||||||
|
channelManager.JoinChannel(channel1);
|
||||||
|
channelManager.JoinChannel(channel2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Want to join another channel
|
||||||
|
AddStep("Press new tab shortcut", pressNewTabKeys);
|
||||||
|
|
||||||
|
// Selector should be visible
|
||||||
|
AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRestoreTabShortcut()
|
||||||
|
{
|
||||||
|
AddStep("Join 3 channels", () =>
|
||||||
|
{
|
||||||
|
channelManager.JoinChannel(channel1);
|
||||||
|
channelManager.JoinChannel(channel2);
|
||||||
|
channelManager.JoinChannel(channel3);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should do nothing
|
||||||
|
AddStep("Restore tab via shortcut", pressRestoreTabKeys);
|
||||||
|
AddAssert("All channels still open", () => channelManager.JoinedChannels.Count == 3);
|
||||||
|
|
||||||
|
// Close channel 1
|
||||||
|
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
|
||||||
|
AddStep("Click normal close button", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child));
|
||||||
|
AddAssert("Channel 1 closed", () => !channelManager.JoinedChannels.Contains(channel1));
|
||||||
|
AddAssert("Other channels still open", () => channelManager.JoinedChannels.Count == 2);
|
||||||
|
|
||||||
|
// Reopen channel 1
|
||||||
|
AddStep("Restore tab via shortcut", pressRestoreTabKeys);
|
||||||
|
AddAssert("All channels now open", () => channelManager.JoinedChannels.Count == 3);
|
||||||
|
AddAssert("Current channel is channel 1", () => currentChannel == channel1);
|
||||||
|
|
||||||
|
// Close two channels
|
||||||
|
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
|
||||||
|
AddStep("Close channel 1", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child));
|
||||||
|
AddStep("Select channel 2", () => clickDrawable(chatOverlay.TabMap[channel2]));
|
||||||
|
AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child));
|
||||||
|
AddAssert("Only one channel open", () => channelManager.JoinedChannels.Count == 1);
|
||||||
|
AddAssert("Current channel is channel 3", () => currentChannel == channel3);
|
||||||
|
|
||||||
|
// Should first re-open channel 2
|
||||||
|
AddStep("Restore tab via shortcut", pressRestoreTabKeys);
|
||||||
|
AddAssert("Channel 1 still closed", () => !channelManager.JoinedChannels.Contains(channel1));
|
||||||
|
AddAssert("Channel 2 now open", () => channelManager.JoinedChannels.Contains(channel2));
|
||||||
|
AddAssert("Current channel is channel 2", () => currentChannel == channel2);
|
||||||
|
|
||||||
|
// Should then re-open channel 1
|
||||||
|
AddStep("Restore tab via shortcut", pressRestoreTabKeys);
|
||||||
|
AddAssert("All channels now open", () => channelManager.JoinedChannels.Count == 3);
|
||||||
|
AddAssert("Current channel is channel 1", () => currentChannel == channel1);
|
||||||
|
}
|
||||||
|
|
||||||
private void pressChannelHotkey(int number)
|
private void pressChannelHotkey(int number)
|
||||||
{
|
{
|
||||||
var channelKey = Key.Number0 + number;
|
var channelKey = Key.Number0 + number;
|
||||||
@ -236,6 +329,23 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
InputManager.ReleaseKey(Key.AltLeft);
|
InputManager.ReleaseKey(Key.AltLeft);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void pressCloseDocumentKeys() => pressKeysFor(PlatformActionType.DocumentClose);
|
||||||
|
|
||||||
|
private void pressNewTabKeys() => pressKeysFor(PlatformActionType.TabNew);
|
||||||
|
|
||||||
|
private void pressRestoreTabKeys() => pressKeysFor(PlatformActionType.TabRestore);
|
||||||
|
|
||||||
|
private void pressKeysFor(PlatformActionType type)
|
||||||
|
{
|
||||||
|
var binding = host.PlatformKeyBindings.First(b => ((PlatformAction)b.Action).ActionType == type);
|
||||||
|
|
||||||
|
foreach (var k in binding.KeyCombination.Keys)
|
||||||
|
InputManager.PressKey((Key)k);
|
||||||
|
|
||||||
|
foreach (var k in binding.KeyCombination.Keys)
|
||||||
|
InputManager.ReleaseKey((Key)k);
|
||||||
|
}
|
||||||
|
|
||||||
private void clickDrawable(Drawable d)
|
private void clickDrawable(Drawable d)
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(d);
|
InputManager.MoveMouseTo(d);
|
||||||
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Overlays.Chat.Tabs;
|
using osu.Game.Overlays.Chat.Tabs;
|
||||||
@ -33,6 +34,16 @@ namespace osu.Game.Online.Chat
|
|||||||
private readonly BindableList<Channel> availableChannels = new BindableList<Channel>();
|
private readonly BindableList<Channel> availableChannels = new BindableList<Channel>();
|
||||||
private readonly BindableList<Channel> joinedChannels = new BindableList<Channel>();
|
private readonly BindableList<Channel> joinedChannels = new BindableList<Channel>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keeps a stack of recently closed channels
|
||||||
|
/// </summary>
|
||||||
|
private readonly List<ClosedChannel> closedChannels = new List<ClosedChannel>();
|
||||||
|
|
||||||
|
// For efficiency purposes, this constant bounds the number of closed channels we store.
|
||||||
|
// This number is somewhat arbitrary; future developers are free to modify it.
|
||||||
|
// Must be a positive number.
|
||||||
|
private const int closed_channels_max_size = 50;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The currently opened channel
|
/// The currently opened channel
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -51,6 +62,9 @@ namespace osu.Game.Online.Chat
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private IAPIProvider api { get; set; }
|
private IAPIProvider api { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private UserLookupCache users { get; set; }
|
||||||
|
|
||||||
public readonly BindableBool HighPollRate = new BindableBool();
|
public readonly BindableBool HighPollRate = new BindableBool();
|
||||||
|
|
||||||
public ChannelManager()
|
public ChannelManager()
|
||||||
@ -420,6 +434,18 @@ namespace osu.Game.Online.Chat
|
|||||||
|
|
||||||
joinedChannels.Remove(channel);
|
joinedChannels.Remove(channel);
|
||||||
|
|
||||||
|
// Prevent the closedChannel list from exceeding the max size
|
||||||
|
// by removing the oldest element
|
||||||
|
if (closedChannels.Count >= closed_channels_max_size)
|
||||||
|
{
|
||||||
|
closedChannels.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For PM channels, we store the user ID; else, we store the channel ID
|
||||||
|
closedChannels.Add(channel.Type == ChannelType.PM
|
||||||
|
? new ClosedChannel(ChannelType.PM, channel.Users.Single().Id)
|
||||||
|
: new ClosedChannel(channel.Type, channel.Id));
|
||||||
|
|
||||||
if (channel.Joined.Value)
|
if (channel.Joined.Value)
|
||||||
{
|
{
|
||||||
api.Queue(new LeaveChannelRequest(channel));
|
api.Queue(new LeaveChannelRequest(channel));
|
||||||
@ -427,6 +453,46 @@ namespace osu.Game.Online.Chat
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens the most recently closed channel that has not already been reopened,
|
||||||
|
/// Works similarly to reopening the last closed tab on a web browser.
|
||||||
|
/// </summary>
|
||||||
|
public void JoinLastClosedChannel()
|
||||||
|
{
|
||||||
|
// This loop could be eliminated if the join channel operation ensured that every channel joined
|
||||||
|
// is removed from the closedChannels list, but it'd require a linear scan of closed channels on every join.
|
||||||
|
// To keep the overhead of joining channels low, just lazily scan the list of closed channels locally.
|
||||||
|
while (closedChannels.Count > 0)
|
||||||
|
{
|
||||||
|
ClosedChannel lastClosedChannel = closedChannels.Last();
|
||||||
|
closedChannels.RemoveAt(closedChannels.Count - 1);
|
||||||
|
|
||||||
|
// If the user has already joined the channel, try the next one
|
||||||
|
if (joinedChannels.FirstOrDefault(lastClosedChannel.Matches) != null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Channel lastChannel = AvailableChannels.FirstOrDefault(lastClosedChannel.Matches);
|
||||||
|
|
||||||
|
if (lastChannel != null)
|
||||||
|
{
|
||||||
|
// Channel exists as an available channel, directly join it
|
||||||
|
CurrentChannel.Value = JoinChannel(lastChannel);
|
||||||
|
}
|
||||||
|
else if (lastClosedChannel.Type == ChannelType.PM)
|
||||||
|
{
|
||||||
|
// Try to get user in order to open PM chat
|
||||||
|
users.GetUserAsync((int)lastClosedChannel.Id).ContinueWith(u =>
|
||||||
|
{
|
||||||
|
if (u.Result == null) return;
|
||||||
|
|
||||||
|
Schedule(() => CurrentChannel.Value = JoinChannel(new Channel(u.Result)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private long lastMessageId;
|
private long lastMessageId;
|
||||||
|
|
||||||
private bool channelsInitialised;
|
private bool channelsInitialised;
|
||||||
@ -511,4 +577,28 @@ namespace osu.Game.Online.Chat
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores information about a closed channel
|
||||||
|
/// </summary>
|
||||||
|
public class ClosedChannel
|
||||||
|
{
|
||||||
|
public readonly ChannelType Type;
|
||||||
|
public readonly long Id;
|
||||||
|
|
||||||
|
public ClosedChannel(ChannelType type, long id)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Matches(Channel channel)
|
||||||
|
{
|
||||||
|
if (channel.Type != Type) return false;
|
||||||
|
|
||||||
|
return Type == ChannelType.PM
|
||||||
|
? channel.Users.Single().Id == Id
|
||||||
|
: channel.Id == Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,9 +81,11 @@ namespace osu.Game.Overlays.Chat.Tabs
|
|||||||
RemoveItem(channel);
|
RemoveItem(channel);
|
||||||
|
|
||||||
if (SelectedTab == null)
|
if (SelectedTab == null)
|
||||||
SelectTab(selectorTab);
|
SelectChannelSelectorTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SelectChannelSelectorTab() => SelectTab(selectorTab);
|
||||||
|
|
||||||
protected override void SelectTab(TabItem<Channel> tab)
|
protected override void SelectTab(TabItem<Channel> tab)
|
||||||
{
|
{
|
||||||
if (tab is ChannelSelectorTabItem)
|
if (tab is ChannelSelectorTabItem)
|
||||||
|
@ -24,13 +24,15 @@ using osu.Game.Overlays.Chat.Tabs;
|
|||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Online;
|
using osu.Game.Online;
|
||||||
|
|
||||||
namespace osu.Game.Overlays
|
namespace osu.Game.Overlays
|
||||||
{
|
{
|
||||||
public class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent
|
public class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent, IKeyBindingHandler<PlatformAction>
|
||||||
{
|
{
|
||||||
public string IconTexture => "Icons/Hexacons/messaging";
|
public string IconTexture => "Icons/Hexacons/messaging";
|
||||||
public LocalisableString Title => ChatStrings.HeaderTitle;
|
public LocalisableString Title => ChatStrings.HeaderTitle;
|
||||||
@ -370,6 +372,30 @@ namespace osu.Game.Overlays
|
|||||||
return base.OnKeyDown(e);
|
return base.OnKeyDown(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool OnPressed(PlatformAction action)
|
||||||
|
{
|
||||||
|
switch (action.ActionType)
|
||||||
|
{
|
||||||
|
case PlatformActionType.TabNew:
|
||||||
|
ChannelTabControl.SelectChannelSelectorTab();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case PlatformActionType.TabRestore:
|
||||||
|
channelManager.JoinLastClosedChannel();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case PlatformActionType.DocumentClose:
|
||||||
|
channelManager.LeaveChannel(channelManager.CurrentChannel.Value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnReleased(PlatformAction action)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public override bool AcceptsFocus => true;
|
public override bool AcceptsFocus => true;
|
||||||
|
|
||||||
protected override void OnFocus(FocusEvent e)
|
protected override void OnFocus(FocusEvent e)
|
||||||
|
Loading…
Reference in New Issue
Block a user