2017-08-09 10:50:34 +08:00
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
2017-08-09 11:37:47 +08:00
using System ;
2017-08-09 10:50:34 +08:00
using System.Collections.Generic ;
2017-08-10 15:08:43 +08:00
using System.Linq ;
2017-08-09 11:37:47 +08:00
using osu.Framework.Allocation ;
2017-08-09 10:50:34 +08:00
using osu.Framework.Input ;
2017-08-09 11:37:47 +08:00
using osu.Game.Rulesets ;
2017-08-09 10:50:34 +08:00
namespace osu.Game.Input
{
2017-08-10 17:28:22 +08:00
public enum ConcurrentActionMode
{
/// <summary>
/// One action can be actuated at once. The first action matching a chord will take precedence and no other action will be actuated until it has been released.
/// </summary>
None ,
/// <summary>
/// Unique actions are allowed to be fired at the same time. There may therefore be more than one action in an actuated state at once.
/// If one action has multiple bindings, only the first will add actuation data, and the last to be released will add de-actuation data.
/// </summary>
UniqueActions ,
/// <summary>
/// Both unique actions and the same action can be concurrently actuated.
/// Same as <see cref="UniqueActions"/>, but multiple bindings for the same action will individually add actuation and de-actuation data to events.
/// </summary>
UniqueAndSameActions ,
}
2017-08-09 13:50:10 +08:00
/// <summary>
/// Maps custom action data of type <see cref="T"/> and stores to <see cref="InputState.Data"/>.
/// </summary>
/// <typeparam name="T">The type of the custom action.</typeparam>
2017-08-09 16:10:32 +08:00
public abstract class ActionMappingInputManager < T > : PassThroughInputManager
2017-08-09 10:50:34 +08:00
where T : struct
{
2017-08-09 12:04:11 +08:00
private readonly RulesetInfo ruleset ;
2017-08-09 12:23:23 +08:00
private readonly int? variant ;
2017-08-10 17:28:22 +08:00
private readonly ConcurrentActionMode concurrencyMode ;
2017-08-10 15:08:43 +08:00
private readonly List < Binding > mappings = new List < Binding > ( ) ;
2017-08-09 13:50:10 +08:00
/// <summary>
/// Create a new instance.
/// </summary>
/// <param name="ruleset">A reference to identify the current <see cref="Ruleset"/>. Used to lookup mappings. Null for global mappings.</param>
/// <param name="variant">An optional variant for the specified <see cref="Ruleset"/>. Used when a ruleset has more than one possible keyboard layouts.</param>
2017-08-10 17:28:22 +08:00
/// <param name="concurrencyMode">Specify how to deal with multiple matches of combinations and actions.</param>
protected ActionMappingInputManager ( RulesetInfo ruleset = null , int? variant = null , ConcurrentActionMode concurrencyMode = ConcurrentActionMode . None )
2017-08-09 12:04:11 +08:00
{
this . ruleset = ruleset ;
2017-08-09 12:23:23 +08:00
this . variant = variant ;
2017-08-10 17:28:22 +08:00
this . concurrencyMode = concurrencyMode ;
2017-08-09 12:04:11 +08:00
}
2017-08-10 15:08:43 +08:00
protected abstract IDictionary < KeyCombination , T > CreateDefaultMappings ( ) ;
2017-08-09 16:10:32 +08:00
2017-08-10 15:08:43 +08:00
private BindingStore store ;
2017-08-09 10:50:34 +08:00
2017-08-09 11:37:47 +08:00
[BackgroundDependencyLoader]
private void load ( BindingStore bindings )
{
2017-08-10 15:08:43 +08:00
store = bindings ;
ReloadMappings ( ) ;
2017-08-09 11:37:47 +08:00
}
2017-08-10 15:08:43 +08:00
protected void ReloadMappings ( )
2017-08-09 10:50:34 +08:00
{
2017-08-10 15:08:43 +08:00
var rulesetId = ruleset ? . ID ;
2017-08-09 10:50:34 +08:00
2017-08-10 15:08:43 +08:00
mappings . Clear ( ) ;
foreach ( var kvp in CreateDefaultMappings ( ) )
mappings . Add ( new Binding ( kvp . Key , kvp . Value ) ) ;
if ( store ! = null )
{
foreach ( var b in store . Query < Binding > ( b = > b . RulesetID = = rulesetId & & b . Variant = = variant ) )
mappings . Add ( b ) ;
}
2017-08-10 17:28:22 +08:00
if ( concurrencyMode > ConcurrentActionMode . None )
2017-08-10 15:08:43 +08:00
{
// ensure we have no overlapping bindings.
foreach ( var m in mappings )
foreach ( var colliding in mappings . Where ( k = > ! k . Keys . Equals ( m . Keys ) & & k . Keys . CheckValid ( m . Keys . Keys ) ) )
throw new InvalidOperationException ( $"Multiple partially overlapping bindings are not supported ({m} and {colliding} are colliding)!" ) ;
}
2017-08-09 10:50:34 +08:00
}
2017-08-10 15:08:43 +08:00
private readonly List < Binding > pressedBindings = new List < Binding > ( ) ;
protected override bool OnKeyDown ( InputState state , KeyDownEventArgs args )
2017-08-09 10:50:34 +08:00
{
2017-08-10 17:28:22 +08:00
if ( ! args . Repeat & & ( concurrencyMode > ConcurrentActionMode . None | | pressedBindings . Count = = 0 ) )
2017-08-10 15:08:43 +08:00
{
Binding validBinding ;
2017-08-10 17:28:22 +08:00
if ( ( validBinding = mappings . Except ( pressedBindings ) . LastOrDefault ( m = > m . Keys . CheckValid ( state . Keyboard . Keys , concurrencyMode = = ConcurrentActionMode . None ) ) ) ! = null )
2017-08-10 15:08:43 +08:00
{
2017-08-10 17:28:22 +08:00
if ( concurrencyMode = = ConcurrentActionMode . UniqueAndSameActions | | pressedBindings . All ( p = > p . Action ! = validBinding . Action ) )
state . Data = validBinding . GetAction < T > ( ) ;
2017-08-10 15:08:43 +08:00
// store both the pressed combination and the resulting action, just in case the assignments change while we are actuated.
pressedBindings . Add ( validBinding ) ;
}
}
return base . OnKeyDown ( state , args ) ;
2017-08-09 10:50:34 +08:00
}
2017-08-09 11:37:47 +08:00
2017-08-10 15:08:43 +08:00
protected override bool OnKeyUp ( InputState state , KeyUpEventArgs args )
2017-08-09 11:37:47 +08:00
{
2017-08-10 15:08:43 +08:00
foreach ( var binding in pressedBindings . ToList ( ) )
{
2017-08-10 17:28:22 +08:00
if ( ! binding . Keys . CheckValid ( state . Keyboard . Keys , concurrencyMode = = ConcurrentActionMode . None ) )
2017-08-10 15:08:43 +08:00
{
2017-08-10 17:28:22 +08:00
// clear the no-longer-valid combination/action.
2017-08-10 15:08:43 +08:00
pressedBindings . Remove ( binding ) ;
2017-08-10 17:28:22 +08:00
if ( concurrencyMode = = ConcurrentActionMode . UniqueAndSameActions | | pressedBindings . All ( p = > p . Action ! = binding . Action ) )
// set data as KeyUp if we're all done with this action.
state . Data = binding . GetAction < T > ( ) ;
2017-08-10 15:08:43 +08:00
}
}
2017-08-09 11:37:47 +08:00
2017-08-10 15:08:43 +08:00
return base . OnKeyUp ( state , args ) ;
2017-08-09 11:37:47 +08:00
}
}
2017-08-09 10:50:34 +08:00
}