1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 15:33:05 +08:00

Merge pull request #18796 from bdach/mod-overlay/legacy-key-bindings

Add setting option to toggle between mod overlay hotkey styles
This commit is contained in:
Dean Herbert 2022-06-22 20:28:18 +09:00 committed by GitHub
commit b660119de7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 406 additions and 37 deletions

View File

@ -9,9 +9,11 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Mods.Input;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Utils; using osu.Game.Utils;
@ -25,6 +27,9 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Resolved]
private OsuConfigManager configManager { get; set; } = null!;
[TestCase(ModType.DifficultyReduction)] [TestCase(ModType.DifficultyReduction)]
[TestCase(ModType.DifficultyIncrease)] [TestCase(ModType.DifficultyIncrease)]
[TestCase(ModType.Conversion)] [TestCase(ModType.Conversion)]
@ -132,14 +137,16 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
[Test] [Test]
public void TestKeyboardSelection() public void TestSequentialKeyboardSelection()
{ {
AddStep("set sequential hotkey mode", () => configManager.SetValue(OsuSetting.ModSelectHotkeyStyle, ModSelectHotkeyStyle.Sequential));
ModColumn column = null!; ModColumn column = null!;
AddStep("create content", () => Child = new Container AddStep("create content", () => Child = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(30), Padding = new MarginPadding(30),
Child = column = new ModColumn(ModType.DifficultyReduction, true, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }) Child = column = new ModColumn(ModType.DifficultyReduction, true)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -158,9 +165,12 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set filter to NF", () => setFilter(mod => mod.Acronym == "NF")); AddStep("set filter to NF", () => setFilter(mod => mod.Acronym == "NF"));
AddStep("press W", () => InputManager.Key(Key.W)); AddStep("press W", () => InputManager.Key(Key.W));
AddAssert("NF panel not selected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
AddStep("press Q", () => InputManager.Key(Key.Q));
AddAssert("NF panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value); AddAssert("NF panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
AddStep("press W again", () => InputManager.Key(Key.W)); AddStep("press Q again", () => InputManager.Key(Key.Q));
AddAssert("NF panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value); AddAssert("NF panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
AddStep("filter out everything", () => setFilter(_ => false)); AddStep("filter out everything", () => setFilter(_ => false));
@ -171,6 +181,113 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("clear filter", () => setFilter(null)); AddStep("clear filter", () => setFilter(null));
} }
[Test]
public void TestClassicKeyboardExclusiveSelection()
{
AddStep("set classic hotkey mode", () => configManager.SetValue(OsuSetting.ModSelectHotkeyStyle, ModSelectHotkeyStyle.Classic));
ModColumn column = null!;
AddStep("create content", () => Child = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(30),
Child = column = new ModColumn(ModType.DifficultyIncrease, false)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AvailableMods = getExampleModsFor(ModType.DifficultyIncrease)
}
});
AddUntilStep("wait for panel load", () => column.IsLoaded && column.ItemsLoaded);
AddStep("press A", () => InputManager.Key(Key.A));
AddAssert("HR panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "HR").Active.Value);
AddStep("press A again", () => InputManager.Key(Key.A));
AddAssert("HR panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "HR").Active.Value);
AddStep("press D", () => InputManager.Key(Key.D));
AddAssert("DT panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "DT").Active.Value);
AddStep("press D again", () => InputManager.Key(Key.D));
AddAssert("DT panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "DT").Active.Value);
AddAssert("NC panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NC").Active.Value);
AddStep("press D again", () => InputManager.Key(Key.D));
AddAssert("DT panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "DT").Active.Value);
AddAssert("NC panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NC").Active.Value);
AddStep("press Shift-D", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.D);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddAssert("DT panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "DT").Active.Value);
AddAssert("NC panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NC").Active.Value);
AddStep("press J", () => InputManager.Key(Key.J));
AddAssert("no change", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Active.Value).Mod.Acronym == "NC");
AddStep("filter everything but NC", () => setFilter(mod => mod.Acronym == "NC"));
AddStep("press A", () => InputManager.Key(Key.A));
AddAssert("no change", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Active.Value).Mod.Acronym == "NC");
}
[Test]
public void TestClassicKeyboardIncompatibleSelection()
{
AddStep("set classic hotkey mode", () => configManager.SetValue(OsuSetting.ModSelectHotkeyStyle, ModSelectHotkeyStyle.Classic));
ModColumn column = null!;
AddStep("create content", () => Child = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(30),
Child = column = new ModColumn(ModType.DifficultyIncrease, true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AvailableMods = getExampleModsFor(ModType.DifficultyIncrease)
}
});
AddUntilStep("wait for panel load", () => column.IsLoaded && column.ItemsLoaded);
AddStep("press A", () => InputManager.Key(Key.A));
AddAssert("HR panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "HR").Active.Value);
AddStep("press A again", () => InputManager.Key(Key.A));
AddAssert("HR panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "HR").Active.Value);
AddStep("press D", () => InputManager.Key(Key.D));
AddAssert("DT panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "DT").Active.Value);
AddAssert("NC panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NC").Active.Value);
AddStep("press D again", () => InputManager.Key(Key.D));
AddAssert("DT panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "DT").Active.Value);
AddAssert("NC panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NC").Active.Value);
AddStep("press Shift-D", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.D);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddAssert("DT panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "DT").Active.Value);
AddAssert("NC panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NC").Active.Value);
AddStep("press J", () => InputManager.Key(Key.J));
AddAssert("no change", () => this.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddStep("filter everything but NC", () => setFilter(mod => mod.Acronym == "NC"));
AddStep("press A", () => InputManager.Key(Key.A));
AddAssert("no change", () => this.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
}
private void setFilter(Func<Mod, bool>? filter) private void setFilter(Func<Mod, bool>? filter)
{ {
foreach (var modState in this.ChildrenOfType<ModColumn>().Single().AvailableMods) foreach (var modState in this.ChildrenOfType<ModColumn>().Single().AvailableMods)
@ -181,8 +298,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public new bool SelectionAnimationRunning => base.SelectionAnimationRunning; public new bool SelectionAnimationRunning => base.SelectionAnimationRunning;
public TestModColumn(ModType modType, bool allowBulkSelection) public TestModColumn(ModType modType, bool allowIncompatibleSelection)
: base(modType, allowBulkSelection) : base(modType, allowIncompatibleSelection)
{ {
} }
} }

View File

@ -20,6 +20,7 @@ using osu.Game.Input;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods.Input;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
@ -47,6 +48,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.SongSelectSortingMode, SortMode.Title); SetDefault(OsuSetting.SongSelectSortingMode, SortMode.Title);
SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation); SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation);
SetDefault(OsuSetting.ModSelectHotkeyStyle, ModSelectHotkeyStyle.Sequential);
SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f); SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f);
@ -324,6 +326,7 @@ namespace osu.Game.Configuration
SongSelectGroupingMode, SongSelectGroupingMode,
SongSelectSortingMode, SongSelectSortingMode,
RandomSelectAlgorithm, RandomSelectAlgorithm,
ModSelectHotkeyStyle,
ShowFpsDisplay, ShowFpsDisplay,
ChatDisplayHeight, ChatDisplayHeight,
BeatmapListingCardSize, BeatmapListingCardSize,

View File

@ -106,6 +106,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString RandomSelectionAlgorithm => new TranslatableString(getKey(@"random_selection_algorithm"), @"Random selection algorithm"); public static LocalisableString RandomSelectionAlgorithm => new TranslatableString(getKey(@"random_selection_algorithm"), @"Random selection algorithm");
/// <summary>
/// "Mod select hotkey style"
/// </summary>
public static LocalisableString ModSelectHotkeyStyle => new TranslatableString(getKey(@"mod_select_hotkey_style"), @"Mod select hotkey style");
/// <summary> /// <summary>
/// "no limit" /// "no limit"
/// </summary> /// </summary>

View File

@ -0,0 +1,98 @@
// 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.Diagnostics;
using System.Linq;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mods;
using osuTK.Input;
namespace osu.Game.Overlays.Mods.Input
{
/// <summary>
/// Uses bindings from stable 1:1.
/// </summary>
public class ClassicModHotkeyHandler : IModHotkeyHandler
{
private static readonly Dictionary<Key, Type[]> mod_type_lookup = new Dictionary<Key, Type[]>
{
[Key.Q] = new[] { typeof(ModEasy) },
[Key.W] = new[] { typeof(ModNoFail) },
[Key.E] = new[] { typeof(ModHalfTime) },
[Key.A] = new[] { typeof(ModHardRock) },
[Key.S] = new[] { typeof(ModSuddenDeath), typeof(ModPerfect) },
[Key.D] = new[] { typeof(ModDoubleTime), typeof(ModNightcore) },
[Key.F] = new[] { typeof(ModHidden) },
[Key.G] = new[] { typeof(ModFlashlight) },
[Key.Z] = new[] { typeof(ModRelax) },
[Key.V] = new[] { typeof(ModAutoplay), typeof(ModCinema) }
};
private readonly bool allowIncompatibleSelection;
public ClassicModHotkeyHandler(bool allowIncompatibleSelection)
{
this.allowIncompatibleSelection = allowIncompatibleSelection;
}
public bool HandleHotkeyPressed(KeyDownEvent e, IEnumerable<ModState> availableMods)
{
if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch))
return false;
var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && !modState.Filtered.Value).ToArray();
if (matchingMods.Length == 0)
return false;
if (matchingMods.Length == 1)
{
matchingMods.Single().Active.Toggle();
return true;
}
if (allowIncompatibleSelection)
{
// easier path - multiple incompatible mods can be active at a time.
// this is used in the free mod select overlay.
// in this case, just toggle everything.
bool anyActive = matchingMods.Any(mod => mod.Active.Value);
foreach (var mod in matchingMods)
mod.Active.Value = !anyActive;
return true;
}
// we now know there are multiple possible mods to handle, and only one of them can be active at a time.
// let's make sure of this just in case.
Debug.Assert(matchingMods.Count(modState => modState.Active.Value) <= 1);
int currentSelectedIndex = Array.FindIndex(matchingMods, modState => modState.Active.Value);
// `FindIndex` will return -1 if it doesn't find the item.
// this is convenient in the forward direction, since if we add 1 then we'll end up at the first item,
// but less so in the backwards direction.
// for convenience, detect this situation and set the index to one index past the last item.
// this makes it so that if we subtract 1 then we'll end up at the last item again.
if (currentSelectedIndex < 0 && e.ShiftPressed)
currentSelectedIndex = matchingMods.Length;
int indexToSelect = e.ShiftPressed ? currentSelectedIndex - 1 : currentSelectedIndex + 1;
// `currentSelectedIndex` and `indexToSelect` can both be equal to -1 or `matchingMods.Length`.
// if the former is beyond array range, it means nothing was previously selected and so there's nothing to deselect.
// if the latter is beyond array range, it means that either the previous selection was first and we're going backwards,
// or it was last and we're going forwards.
// in either case there is nothing to select.
if (currentSelectedIndex >= 0 && currentSelectedIndex <= matchingMods.Length - 1)
matchingMods[currentSelectedIndex].Active.Value = false;
if (indexToSelect >= 0 && indexToSelect <= matchingMods.Length - 1)
matchingMods[indexToSelect].Active.Value = true;
return true;
}
private static bool matches(ModState modState, Type[] typesToMatch)
=> typesToMatch.Any(typeToMatch => typeToMatch.IsInstanceOfType(modState.Mod));
}
}

View File

@ -0,0 +1,22 @@
// 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.Collections.Generic;
using osu.Framework.Input.Events;
namespace osu.Game.Overlays.Mods.Input
{
/// <summary>
/// Encapsulates strategies of handling mod hotkeys on the <see cref="ModSelectOverlay"/>.
/// </summary>
public interface IModHotkeyHandler
{
/// <summary>
/// Attempt to handle the supplied <see cref="KeyDownEvent"/> as a selection of one of the mods in <paramref name="availableMods"/>.
/// </summary>
/// <param name="e">The event representing the user's keypress.</param>
/// <param name="availableMods">The list of currently available mods.</param>
/// <returns>Whether the supplied event was handled as a mod selection/deselection.</returns>
bool HandleHotkeyPressed(KeyDownEvent e, IEnumerable<ModState> availableMods);
}
}

View File

@ -0,0 +1,27 @@
// 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 osu.Game.Rulesets.Mods;
namespace osu.Game.Overlays.Mods.Input
{
/// <summary>
/// The style of hotkey handling to use on the mod select screen.
/// </summary>
public enum ModSelectHotkeyStyle
{
/// <summary>
/// Each letter row on the keyboard controls one of the three first <see cref="ModColumn"/>s.
/// Individual letters in a row trigger the mods in a sequential fashion.
/// Uses <see cref="SequentialModHotkeyHandler"/>.
/// </summary>
Sequential,
/// <summary>
/// Matches keybindings from stable 1:1.
/// One keybinding can toggle between what used to be <see cref="MultiMod"/>s on stable,
/// and some mods in a column may not have any hotkeys at all.
/// </summary>
Classic
}
}

View File

@ -0,0 +1,17 @@
// 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.Collections.Generic;
using osu.Framework.Input.Events;
namespace osu.Game.Overlays.Mods.Input
{
/// <summary>
/// A no-op implementation of <see cref="IModHotkeyHandler"/>.
/// Used when a column is not handling any hotkeys at all.
/// </summary>
public class NoopModHotkeyHandler : IModHotkeyHandler
{
public bool HandleHotkeyPressed(KeyDownEvent e, IEnumerable<ModState> availableMods) => false;
}
}

View File

@ -0,0 +1,59 @@
// 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 osu.Framework.Input.Events;
using osu.Game.Rulesets.Mods;
using osuTK.Input;
namespace osu.Game.Overlays.Mods.Input
{
/// <summary>
/// This implementation of <see cref="IModHotkeyHandler"/> receives a sequence of <see cref="Key"/>s,
/// and maps the sequence of keys onto the items it is provided in <see cref="HandleHotkeyPressed"/>.
/// In this case, particular mods are not bound to particular keys, the hotkeys are a byproduct of mod ordering.
/// </summary>
public class SequentialModHotkeyHandler : IModHotkeyHandler
{
public static SequentialModHotkeyHandler Create(ModType modType)
{
switch (modType)
{
case ModType.DifficultyReduction:
return new SequentialModHotkeyHandler(new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P });
case ModType.DifficultyIncrease:
return new SequentialModHotkeyHandler(new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L });
case ModType.Automation:
return new SequentialModHotkeyHandler(new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M });
default:
throw new ArgumentOutOfRangeException(nameof(modType), modType, $"Cannot create {nameof(SequentialModHotkeyHandler)} for provided mod type");
}
}
private readonly Key[] toggleKeys;
private SequentialModHotkeyHandler(Key[] keys)
{
toggleKeys = keys;
}
public bool HandleHotkeyPressed(KeyDownEvent e, IEnumerable<ModState> availableMods)
{
int index = Array.IndexOf(toggleKeys, e.Key);
if (index < 0)
return false;
var modState = availableMods.Where(modState => !modState.Filtered.Value).ElementAtOrDefault(index);
if (modState == null)
return false;
modState.Active.Toggle();
return true;
}
}
}

View File

@ -17,14 +17,15 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Overlays.Mods.Input;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
@ -70,7 +71,7 @@ namespace osu.Game.Overlays.Mods
protected virtual ModPanel CreateModPanel(ModState mod) => new ModPanel(mod); protected virtual ModPanel CreateModPanel(ModState mod) => new ModPanel(mod);
private readonly Key[]? toggleKeys; private readonly bool allowIncompatibleSelection;
private readonly TextFlowContainer headerText; private readonly TextFlowContainer headerText;
private readonly Box headerBackground; private readonly Box headerBackground;
@ -81,15 +82,18 @@ namespace osu.Game.Overlays.Mods
private Colour4 accentColour; private Colour4 accentColour;
private Bindable<ModSelectHotkeyStyle> hotkeyStyle = null!;
private IModHotkeyHandler hotkeyHandler = null!;
private Task? latestLoadTask; private Task? latestLoadTask;
internal bool ItemsLoaded => latestLoadTask == null; internal bool ItemsLoaded => latestLoadTask == null;
private const float header_height = 42; private const float header_height = 42;
public ModColumn(ModType modType, bool allowBulkSelection, Key[]? toggleKeys = null) public ModColumn(ModType modType, bool allowIncompatibleSelection)
{ {
ModType = modType; ModType = modType;
this.toggleKeys = toggleKeys; this.allowIncompatibleSelection = allowIncompatibleSelection;
Width = 320; Width = 320;
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
@ -197,7 +201,7 @@ namespace osu.Game.Overlays.Mods
createHeaderText(); createHeaderText();
if (allowBulkSelection) if (allowIncompatibleSelection)
{ {
controlContainer.Height = 35; controlContainer.Height = 35;
controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this) controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this)
@ -231,7 +235,7 @@ namespace osu.Game.Overlays.Mods
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colours) private void load(OverlayColourProvider colourProvider, OsuColour colours, OsuConfigManager configManager)
{ {
headerBackground.Colour = accentColour = colours.ForModType(ModType); headerBackground.Colour = accentColour = colours.ForModType(ModType);
@ -243,6 +247,8 @@ namespace osu.Game.Overlays.Mods
contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background3); contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background3);
contentBackground.Colour = colourProvider.Background4; contentBackground.Colour = colourProvider.Background4;
hotkeyStyle = configManager.GetBindable<ModSelectHotkeyStyle>(OsuSetting.ModSelectHotkeyStyle);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -250,6 +256,7 @@ namespace osu.Game.Overlays.Mods
base.LoadComplete(); base.LoadComplete();
toggleAllCheckbox?.Current.BindValueChanged(_ => updateToggleAllText(), true); toggleAllCheckbox?.Current.BindValueChanged(_ => updateToggleAllText(), true);
hotkeyStyle.BindValueChanged(val => hotkeyHandler = createHotkeyHandler(val.NewValue), true);
asyncLoadPanels(); asyncLoadPanels();
} }
@ -423,19 +430,32 @@ namespace osu.Game.Overlays.Mods
#region Keyboard selection support #region Keyboard selection support
/// <summary>
/// Creates an appropriate <see cref="IModHotkeyHandler"/> for this column's <see cref="ModType"/> and
/// the supplied <paramref name="hotkeyStyle"/>.
/// </summary>
private IModHotkeyHandler createHotkeyHandler(ModSelectHotkeyStyle hotkeyStyle)
{
switch (ModType)
{
case ModType.DifficultyReduction:
case ModType.DifficultyIncrease:
case ModType.Automation:
return hotkeyStyle == ModSelectHotkeyStyle.Sequential
? (IModHotkeyHandler)SequentialModHotkeyHandler.Create(ModType)
: new ClassicModHotkeyHandler(allowIncompatibleSelection);
default:
return new NoopModHotkeyHandler();
}
}
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
{ {
if (e.ControlPressed || e.AltPressed || e.SuperPressed) return false; if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.Repeat)
if (toggleKeys == null) return false; return false;
int index = Array.IndexOf(toggleKeys, e.Key); return hotkeyHandler.HandleHotkeyPressed(e, availableMods);
if (index < 0) return false;
var modState = availableMods.ElementAtOrDefault(index);
if (modState == null || modState.Filtered.Value) return false;
modState.Active.Toggle();
return true;
} }
#endregion #endregion

View File

@ -21,7 +21,6 @@ using osu.Game.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
@ -68,7 +67,7 @@ namespace osu.Game.Overlays.Mods
/// </summary> /// </summary>
protected virtual bool AllowCustomisation => true; protected virtual bool AllowCustomisation => true;
protected virtual ModColumn CreateModColumn(ModType modType, Key[]? toggleKeys = null) => new ModColumn(modType, false, toggleKeys); protected virtual ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, false);
protected virtual IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection) => newSelection; protected virtual IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection) => newSelection;
@ -160,9 +159,9 @@ namespace osu.Game.Overlays.Mods
Padding = new MarginPadding { Bottom = 10 }, Padding = new MarginPadding { Bottom = 10 },
Children = new[] Children = new[]
{ {
createModColumnContent(ModType.DifficultyReduction, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }), createModColumnContent(ModType.DifficultyReduction),
createModColumnContent(ModType.DifficultyIncrease, new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }), createModColumnContent(ModType.DifficultyIncrease),
createModColumnContent(ModType.Automation, new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }), createModColumnContent(ModType.Automation),
createModColumnContent(ModType.Conversion), createModColumnContent(ModType.Conversion),
createModColumnContent(ModType.Fun) createModColumnContent(ModType.Fun)
} }
@ -264,9 +263,9 @@ namespace osu.Game.Overlays.Mods
column.DeselectAll(); column.DeselectAll();
} }
private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null) private ColumnDimContainer createModColumnContent(ModType modType)
{ {
var column = CreateModColumn(modType, toggleKeys).With(column => var column = CreateModColumn(modType).With(column =>
{ {
// spacing applied here rather than via `columnFlow.Spacing` to avoid uneven gaps when some of the columns are hidden. // spacing applied here rather than via `columnFlow.Spacing` to avoid uneven gaps when some of the columns are hidden.
column.Margin = new MarginPadding { Right = 10 }; column.Margin = new MarginPadding { Right = 10 };

View File

@ -1,14 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
@ -19,7 +15,7 @@ namespace osu.Game.Overlays.Mods
{ {
} }
protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new UserModColumn(modType, false, toggleKeys); protected override ModColumn CreateModColumn(ModType modType) => new UserModColumn(modType, false);
protected override IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection) protected override IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection)
{ {
@ -44,8 +40,8 @@ namespace osu.Game.Overlays.Mods
private class UserModColumn : ModColumn private class UserModColumn : ModColumn
{ {
public UserModColumn(ModType modType, bool allowBulkSelection, [CanBeNull] Key[] toggleKeys = null) public UserModColumn(ModType modType, bool allowIncompatibleSelection)
: base(modType, allowBulkSelection, toggleKeys) : base(modType, allowIncompatibleSelection)
{ {
} }

View File

@ -11,6 +11,7 @@ using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Overlays.Mods.Input;
namespace osu.Game.Overlays.Settings.Sections.UserInterface namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
@ -61,6 +62,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
LabelText = UserInterfaceStrings.RandomSelectionAlgorithm, LabelText = UserInterfaceStrings.RandomSelectionAlgorithm,
Current = config.GetBindable<RandomSelectAlgorithm>(OsuSetting.RandomSelectAlgorithm), Current = config.GetBindable<RandomSelectAlgorithm>(OsuSetting.RandomSelectAlgorithm),
},
new SettingsEnumDropdown<ModSelectHotkeyStyle>
{
LabelText = UserInterfaceStrings.ModSelectHotkeyStyle,
Current = config.GetBindable<ModSelectHotkeyStyle>(OsuSetting.ModSelectHotkeyStyle),
ClassicDefault = ModSelectHotkeyStyle.Classic
} }
}; };
} }

View File

@ -11,7 +11,6 @@ using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osuTK.Input;
namespace osu.Game.Screens.OnlinePlay namespace osu.Game.Screens.OnlinePlay
{ {
@ -33,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay
IsValidMod = _ => true; IsValidMod = _ => true;
} }
protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new ModColumn(modType, true, toggleKeys); protected override ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, true);
protected override IEnumerable<ShearedButton> CreateFooterButtons() => base.CreateFooterButtons().Prepend( protected override IEnumerable<ShearedButton> CreateFooterButtons() => base.CreateFooterButtons().Prepend(
new SelectAllModsButton(this) new SelectAllModsButton(this)