diff --git a/osu.Framework/Graphics/UserInterface/TextBox.cs b/osu.Framework/Graphics/UserInterface/TextBox.cs index 6428bfccab..428bc7e49c 100644 --- a/osu.Framework/Graphics/UserInterface/TextBox.cs +++ b/osu.Framework/Graphics/UserInterface/TextBox.cs @@ -19,6 +19,7 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Platform; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; using osu.Framework.Timing; namespace osu.Framework.Graphics.UserInterface @@ -70,36 +71,43 @@ public class TextBox : TabbableContainer, IHasCurrentValue public TextBox() { - Masking = true; - CornerRadius = 3; - - AddRange(new Drawable[] + // TextBoxes currently require their own top-level PlatformInputManager, as InputManagers + // will not propagate events through other InputManagers. + // Once this restriction has been addressed, we can utilise a single instance of PlatformInputManager + // at the Game level, and TextBoxes need only implement IKeyBindingHandler. + Child = new PlatformInputManager { - Background = new Box - { - Colour = BackgroundUnfocused, - RelativeSizeAxes = Axes.Both, - }, - TextContainer = new Container + Masking = true, + CornerRadius = 3, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Position = new Vector2(LeftRightPadding, 0), - Children = new[] + Background = new Box { - Placeholder = CreatePlaceholder(), - Caret = new DrawableCaret(), - TextFlow = new FillFlowContainer + Colour = BackgroundUnfocused, + RelativeSizeAxes = Axes.Both, + }, + TextContainer = new KeyBindingContainer(this) + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Position = new Vector2(LeftRightPadding, 0), + Children = new[] { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, + Placeholder = CreatePlaceholder(), + Caret = new DrawableCaret(), + TextFlow = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + }, }, }, - }, - }); + } + }; Current.ValueChanged += newValue => { Text = newValue; }; } @@ -262,6 +270,104 @@ private int getCharacterClosestTo(Vector2 pos) private Cached cursorAndLayout = new Cached(); + private bool handleAction(PlatformAction action) + { + int? amount = null; + + switch (action.ActionType) + { + // Clipboard + case PlatformActionType.Cut: + case PlatformActionType.Copy: + if (string.IsNullOrEmpty(SelectedText) || !AllowClipboardExport) return true; + + clipboard?.SetText(SelectedText); + if (action.ActionType == PlatformActionType.Cut) + removeCharacterOrSelection(); + return true; + + case PlatformActionType.Paste: + //the text may get pasted into the hidden textbox, so we don't need any direct clipboard interaction here. + string pending = textInput?.GetPendingText(); + + if (string.IsNullOrEmpty(pending)) + pending = clipboard?.GetText(); + + insertString(pending); + return true; + + case PlatformActionType.SelectAll: + selectionStart = 0; + selectionEnd = text.Length; + cursorAndLayout.Invalidate(); + return true; + + // Cursor Manipulation + case PlatformActionType.CharNext: + if (!HandleLeftRightArrows) return false; + amount = 1; + break; + + case PlatformActionType.CharPrevious: + if (!HandleLeftRightArrows) return false; + amount = -1; + break; + + case PlatformActionType.LineEnd: + amount = text.Length; + break; + + case PlatformActionType.LineStart: + amount = -text.Length; + break; + + case PlatformActionType.WordNext: + { + int searchStart = MathHelper.Clamp(selectionEnd, 0, Text.Length - 1); + while (searchStart < Text.Length && text[searchStart] == ' ') + searchStart++; + int nextSpace = text.IndexOf(' ', searchStart); + amount = (nextSpace >= 0 ? nextSpace : text.Length) - selectionEnd; + } + break; + + case PlatformActionType.WordPrevious: + { + int searchStart = MathHelper.Clamp(selectionEnd - 2, 0, Text.Length - 1); + while (searchStart > 0 && text[searchStart] == ' ') + searchStart--; + int lastSpace = text.LastIndexOf(' ', searchStart); + amount = lastSpace > 0 ? -(selectionEnd - lastSpace - 1) : -selectionEnd; + } + break; + } + + if (amount.HasValue) + { + switch (action.ActionMethod) + { + case PlatformActionMethod.Move: + resetSelection(); + moveSelection(amount.Value, false); + break; + + case PlatformActionMethod.Select: + moveSelection(amount.Value, true); + break; + + case PlatformActionMethod.Delete: + if (selectionLength == 0) + selectionEnd = MathHelper.Clamp(selectionStart + amount.Value, 0, text.Length); + if (selectionLength > 0) + removeCharacterOrSelection(); + break; + } + return true; + } + + return false; + } + private void moveSelection(int offset, bool expand) { if (textInput?.ImeActive == true) return; @@ -486,81 +592,26 @@ protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) if (ReadOnly) return true; - if (state.Keyboard.AltPressed) + if (state.Keyboard.AltPressed || state.Keyboard.ControlPressed || state.Keyboard.SuperPressed) return false; switch (args.Key) { + case Key.Left: + case Key.Right: + case Key.Delete: + case Key.BackSpace: + case Key.Home: + case Key.End: + return false; + case Key.Escape: GetContainingInputManager().ChangeFocus(null); return true; + case Key.Tab: return base.OnKeyDown(state, args); - case Key.End: - moveSelection(text.Length, state.Keyboard.ShiftPressed); - return true; - case Key.Home: - moveSelection(-text.Length, state.Keyboard.ShiftPressed); - return true; - case Key.Left: - { - if (!HandleLeftRightArrows) return false; - - if (selectionEnd == 0) - { - //we only clear if you aren't holding shift - if (!state.Keyboard.ShiftPressed) - resetSelection(); - return true; - } - - int amount = 1; - if (state.Keyboard.ControlPressed) - { - int lastSpace = text.LastIndexOf(' ', Math.Max(0, selectionEnd - 2)); - if (lastSpace >= 0) - { - //if you have something selected and shift is not held down - //A selection reset is required to select a word inside the current selection - if (!state.Keyboard.ShiftPressed) - resetSelection(); - amount = selectionEnd - lastSpace - 1; - } - else - amount = selectionEnd; - } - - moveSelection(-amount, state.Keyboard.ShiftPressed); - return true; - } - case Key.Right: - { - if (!HandleLeftRightArrows) return false; - if (selectionEnd == text.Length) - { - if (!state.Keyboard.ShiftPressed) - resetSelection(); - return true; - } - - int amount = 1; - if (state.Keyboard.ControlPressed) - { - int nextSpace = text.IndexOf(' ', selectionEnd + 1); - if (nextSpace >= 0) - { - if (!state.Keyboard.ShiftPressed) - resetSelection(); - amount = nextSpace - selectionEnd; - } - else - amount = text.Length - selectionEnd; - } - - moveSelection(amount, state.Keyboard.ShiftPressed); - return true; - } case Key.KeypadEnter: case Key.Enter: if (HasFocus) @@ -576,81 +627,6 @@ protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) OnCommit?.Invoke(this, true); } return true; - case Key.Delete: - if (selectionLength == 0) - { - if (text.Length == selectionStart) - return true; - - if (state.Keyboard.ControlPressed) - { - int spacePos = selectionStart; - while (text[spacePos] == ' ' && spacePos < text.Length) - spacePos++; - - spacePos = MathHelper.Clamp(text.IndexOf(' ', spacePos), 0, text.Length); - selectionEnd = spacePos; - - if (selectionStart == 0 && spacePos == 0) - selectionEnd = text.Length; - - if (selectionLength == 0) - return true; - } - else - { - //we're deleting in front of the cursor, so move the cursor forward once first - selectionStart = selectionEnd = selectionStart + 1; - } - } - - removeCharacterOrSelection(); - return true; - case Key.Back: - if (selectionLength == 0 && state.Keyboard.ControlPressed) - { - int spacePos = selectionLeft >= 2 ? Math.Max(0, text.LastIndexOf(' ', selectionLeft - 2) + 1) : 0; - selectionStart = spacePos; - } - - removeCharacterOrSelection(); - return true; - } - - if (state.Keyboard.ControlPressed) - { - //handling of function keys - switch (args.Key) - { - case Key.A: - selectionStart = 0; - selectionEnd = text.Length; - cursorAndLayout.Invalidate(); - return true; - case Key.C: - if (string.IsNullOrEmpty(SelectedText) || !AllowClipboardExport) return true; - - clipboard?.SetText(SelectedText); - return true; - case Key.X: - if (string.IsNullOrEmpty(SelectedText) || !AllowClipboardExport) return true; - - clipboard?.SetText(SelectedText); - removeCharacterOrSelection(); - return true; - case Key.V: - - //the text may get pasted into the hidden textbox, so we don't need any direct clipboard interaction here. - string pending = textInput?.GetPendingText(); - - if (string.IsNullOrEmpty(pending)) - pending = clipboard?.GetText(); - - insertString(pending); - return true; - } - - return false; } return true; @@ -901,5 +877,19 @@ public DrawableCaret() }; } } + + private class KeyBindingContainer : Container, IKeyBindingHandler + { + private readonly TextBox textBox; + + public KeyBindingContainer(TextBox textBox) + { + this.textBox = textBox; + } + + public bool OnPressed(PlatformAction action) => textBox.HasFocus && textBox.handleAction(action); + + public bool OnReleased(PlatformAction action) => false; + } } } diff --git a/osu.Framework/Input/Bindings/KeyBindingInputManager.cs b/osu.Framework/Input/Bindings/KeyBindingInputManager.cs index f2e1b82e5c..dc8427ea42 100644 --- a/osu.Framework/Input/Bindings/KeyBindingInputManager.cs +++ b/osu.Framework/Input/Bindings/KeyBindingInputManager.cs @@ -45,6 +45,12 @@ protected KeyBindingInputManager(SimultaneousBindingMode simultaneousMode = Simu /// protected virtual IEnumerable KeyBindingInputQueue => InputQueue; + /// + /// Override to enable or disable sending of repeated actions (disabled by default). + /// Each repeated action will have its own pressed/released event pair. + /// + protected virtual bool SendRepeats => false; + protected override bool PropagateWheel(IEnumerable drawables, InputState state) { if (base.PropagateWheel(drawables, state)) return true; @@ -59,18 +65,18 @@ protected override bool PropagateWheel(IEnumerable drawables, InputSta InputKey key = state.Mouse.WheelDelta > 0 ? InputKey.MouseWheelUp : InputKey.MouseWheelDown; - return handleNewPressed(state, key) | handleNewReleased(clonedState, key); + return handleNewPressed(state, key, false) | handleNewReleased(clonedState, key); } protected override bool PropagateMouseDown(IEnumerable drawables, InputState state, MouseDownEventArgs args) => - base.PropagateMouseDown(drawables, state, args) || handleNewPressed(state, KeyCombination.FromMouseButton(args.Button)); + base.PropagateMouseDown(drawables, state, args) || handleNewPressed(state, KeyCombination.FromMouseButton(args.Button), false); protected override bool PropagateMouseUp(IEnumerable drawables, InputState state, MouseUpEventArgs args) => base.PropagateMouseUp(drawables, state, args) || handleNewReleased(state, KeyCombination.FromMouseButton(args.Button)); protected override bool PropagateKeyDown(IEnumerable drawables, InputState state, KeyDownEventArgs args) { - if (args.Repeat) + if (args.Repeat && !SendRepeats) { if (pressedBindings.Count > 0) return true; @@ -78,19 +84,19 @@ protected override bool PropagateKeyDown(IEnumerable drawables, InputS return base.PropagateKeyDown(drawables, state, args); } - return base.PropagateKeyDown(drawables, state, args) || handleNewPressed(state, KeyCombination.FromKey(args.Key)); + return base.PropagateKeyDown(drawables, state, args) || handleNewPressed(state, KeyCombination.FromKey(args.Key), args.Repeat); } protected override bool PropagateKeyUp(IEnumerable drawables, InputState state, KeyUpEventArgs args) => base.PropagateKeyUp(drawables, state, args) || handleNewReleased(state, KeyCombination.FromKey(args.Key)); - private bool handleNewPressed(InputState state, InputKey newKey) + private bool handleNewPressed(InputState state, InputKey newKey, bool repeat) { var pressedCombination = KeyCombination.FromInputState(state); bool handled = false; - - var newlyPressed = KeyBindings.Except(pressedBindings).Where(m => + var bindings = repeat ? KeyBindings : KeyBindings.Except(pressedBindings); + var newlyPressed = bindings.Where(m => m.KeyCombination.Keys.Contains(newKey) // only handle bindings matching current key (not required for correct logic) && m.KeyCombination.IsPressed(pressedCombination)); @@ -101,7 +107,8 @@ private bool handleNewPressed(InputState state, InputKey newKey) // we want to always handle bindings with more keys before bindings with less. newlyPressed = newlyPressed.OrderByDescending(b => b.KeyCombination.Keys.Count()).ToList(); - pressedBindings.AddRange(newlyPressed); + if (!repeat) + pressedBindings.AddRange(newlyPressed); foreach (var newBinding in newlyPressed) { diff --git a/osu.Framework/Input/PlatformInputManager.cs b/osu.Framework/Input/PlatformInputManager.cs new file mode 100644 index 0000000000..9a0fc6bb2b --- /dev/null +++ b/osu.Framework/Input/PlatformInputManager.cs @@ -0,0 +1,54 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Input.Bindings; + +namespace osu.Framework.Input +{ + /// + /// Provides actions that are expected to have different key bindings per platform. + /// The framework will always contain one top-level instance of this class, but extra instances + /// can be created to handle events that should trigger specifically on a focused drawable. + /// Will send repeat events by default. + /// + public class PlatformInputManager : KeyBindingInputManager + { + public override IEnumerable DefaultKeyBindings => Host.PlatformKeyBindings; + + protected override bool SendRepeats => true; + } + + public struct PlatformAction + { + public PlatformActionType ActionType; + public PlatformActionMethod? ActionMethod; + + public PlatformAction(PlatformActionType actionType, PlatformActionMethod? actionMethod = null) + { + ActionType = actionType; + ActionMethod = actionMethod; + } + } + + public enum PlatformActionType + { + Cut, + Copy, + Paste, + SelectAll, + CharPrevious, + CharNext, + WordPrevious, + WordNext, + LineStart, + LineEnd + } + + public enum PlatformActionMethod + { + Move, + Select, + Delete + } +} diff --git a/osu.Framework/Platform/GameHost.cs b/osu.Framework/Platform/GameHost.cs index 96f81974aa..1176e64a05 100644 --- a/osu.Framework/Platform/GameHost.cs +++ b/osu.Framework/Platform/GameHost.cs @@ -20,6 +20,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL; using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Handlers; using osu.Framework.Localisation; using osu.Framework.Logging; @@ -579,5 +580,33 @@ public void Dispose() } #endregion + + /// + /// Defines the platform-specific key bindings that will be used by . + /// Should be overridden per-platform to provide native key bindings. + /// + public virtual IEnumerable PlatformKeyBindings => new[] + { + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.X }), new PlatformAction(PlatformActionType.Cut)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.C }), new PlatformAction(PlatformActionType.Copy)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.V }), new PlatformAction(PlatformActionType.Paste)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.A }), new PlatformAction(PlatformActionType.SelectAll)), + new KeyBinding(InputKey.Left, new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Move)), + new KeyBinding(InputKey.Right, new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Move)), + new KeyBinding(InputKey.BackSpace, new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Delete)), + new KeyBinding(InputKey.Delete, new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Left }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Right }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.BackSpace}), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Delete }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Select)), + new KeyBinding(InputKey.Home, new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Move)), + new KeyBinding(InputKey.End, new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Home }), new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.End }), new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Select)), + }; } } diff --git a/osu.Framework/Platform/MacOS/MacOSGameHost.cs b/osu.Framework/Platform/MacOS/MacOSGameHost.cs index 33301592d6..2514ff167d 100644 --- a/osu.Framework/Platform/MacOS/MacOSGameHost.cs +++ b/osu.Framework/Platform/MacOS/MacOSGameHost.cs @@ -1,6 +1,10 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE +using System.Collections.Generic; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; + namespace osu.Framework.Platform.MacOS { public class MacOSGameHost : DesktopGameHost @@ -21,5 +25,31 @@ internal MacOSGameHost(string gameName, bool bindIPC = false) protected override Storage GetStorage(string baseName) => new MacOSStorage(baseName); public override Clipboard GetClipboard() => new MacOSClipboard(); + + public override IEnumerable PlatformKeyBindings => new[] + { + new KeyBinding(new KeyCombination(new[] { InputKey.Win, InputKey.X }), new PlatformAction(PlatformActionType.Cut)), + new KeyBinding(new KeyCombination(new[] { InputKey.Win, InputKey.C }), new PlatformAction(PlatformActionType.Copy)), + new KeyBinding(new KeyCombination(new[] { InputKey.Win, InputKey.V }), new PlatformAction(PlatformActionType.Paste)), + new KeyBinding(new KeyCombination(new[] { InputKey.Win, InputKey.A }), new PlatformAction(PlatformActionType.SelectAll)), + new KeyBinding(InputKey.Left, new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Move)), + new KeyBinding(InputKey.Right, new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Move)), + new KeyBinding(InputKey.BackSpace, new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Delete)), + new KeyBinding(InputKey.Delete, new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Left }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Right }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.BackSpace}), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Delete }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Win, InputKey.Left }), new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Win, InputKey.Right }), new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Win, InputKey.BackSpace }), new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Win, InputKey.Delete }), new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Win, InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Win, InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Select)), + }; } } diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index dfbe6836ba..bbddb41059 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -302,6 +302,7 @@ +