diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs index 8c91e03fa0..64bbee063b 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs @@ -135,15 +135,113 @@ public void TestCommittingTextInvokesEvents() [Test] public void TestMovingOrExpandingSelectionInvokesEvent() { + // Selecting Forward AddStep("invoke move action to move caret", () => InputManager.Keys(PlatformAction.MoveBackwardLine)); AddAssert("caret moved event", () => // Ensure dequeued caret move event has selecting = false. textBox.CaretMovedQueue.Dequeue() == false && textBox.CommittedTextQueue.Count == 0); AddStep("invoke select action to expand selection", () => InputManager.Keys(PlatformAction.SelectForwardChar)); + AddAssert("text selection event (character)", () => textBox.TextSelectionQueue.Dequeue() == TextBox.TextSelectionType.Character); AddAssert("caret moved event", () => // Ensure dequeued caret move event has selecting = true. textBox.CaretMovedQueue.Dequeue() && textBox.CommittedTextQueue.Count == 0); + + AddStep("invoke move action to move caret", () => InputManager.Keys(PlatformAction.MoveBackwardLine)); + AddAssert("text deselect event", () => textBox.TextDeselectionQueue.Dequeue()); + AddAssert("caret moved event", () => + // Ensure dequeued caret move event has selecting = false. + textBox.CaretMovedQueue.Dequeue() == false && textBox.CommittedTextQueue.Count == 0); + + AddStep("invoke select action to expand selection", () => InputManager.Keys(PlatformAction.SelectForwardWord)); + AddAssert("text selection event (word)", () => textBox.TextSelectionQueue.Dequeue() == TextBox.TextSelectionType.Word); + AddAssert("caret moved event", () => + // Ensure dequeued caret move event has selecting = true. + textBox.CaretMovedQueue.Dequeue() && textBox.CommittedTextQueue.Count == 0); + + // Selecting Backward + AddStep("invoke move action to move caret", () => InputManager.Keys(PlatformAction.MoveForwardLine)); + AddAssert("text deselect event", () => textBox.TextDeselectionQueue.Dequeue()); + AddAssert("caret moved event", () => + // Ensure dequeued caret move event has selecting = false. + textBox.CaretMovedQueue.Dequeue() == false && textBox.CommittedTextQueue.Count == 0); + + AddStep("invoke select action to expand selection", () => InputManager.Keys(PlatformAction.SelectBackwardChar)); + AddAssert("text selection event (character)", () => textBox.TextSelectionQueue.Dequeue() == TextBox.TextSelectionType.Character); + AddAssert("caret moved event", () => + // Ensure dequeued caret move event has selecting = true. + textBox.CaretMovedQueue.Dequeue() && textBox.CommittedTextQueue.Count == 0); + + AddStep("invoke move action to move caret", () => InputManager.Keys(PlatformAction.MoveForwardLine)); + AddAssert("text deselect event", () => textBox.TextDeselectionQueue.Dequeue()); + AddAssert("caret moved event", () => + // Ensure dequeued caret move event has selecting = false. + textBox.CaretMovedQueue.Dequeue() == false && textBox.CommittedTextQueue.Count == 0); + + AddStep("invoke select action to expand selection", () => InputManager.Keys(PlatformAction.SelectBackwardWord)); + AddAssert("text selection event (word)", () => textBox.TextSelectionQueue.Dequeue() == TextBox.TextSelectionType.Word); + AddAssert("caret moved event", () => + // Ensure dequeued caret move event has selecting = true. + textBox.CaretMovedQueue.Dequeue() && textBox.CommittedTextQueue.Count == 0); + + // Selecting All + AddStep("invoke select action to expand selection", () => InputManager.Keys(PlatformAction.SelectAll)); + AddAssert("text selection event (all)", () => textBox.TextSelectionQueue.Dequeue() == TextBox.TextSelectionType.All); + + AddStep("invoke move action to move caret", () => InputManager.Keys(PlatformAction.MoveBackwardLine)); + AddAssert("text deselect event", () => textBox.TextDeselectionQueue.Dequeue()); + AddAssert("caret moved event", () => + // Ensure dequeued caret move event has selecting = false. + textBox.CaretMovedQueue.Dequeue() == false && textBox.CommittedTextQueue.Count == 0); + + // Selecting via Mouse + AddStep("double-click selection", () => + { + InputManager.MoveMouseTo(textBox); + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + AddAssert("text selection event (word)", () => textBox.TextSelectionQueue.Dequeue() == TextBox.TextSelectionType.Word); + + AddAssert("text input not deactivated", () => textInput.DeactivationQueue.Count == 0); + AddAssert("text input not activated again", () => textInput.ActivationQueue.Count == 0); + AddAssert("text input ensure activated", () => textInput.EnsureActivatedQueue.Dequeue() && textInput.EnsureActivatedQueue.Count == 0); + + AddStep("click deselection", () => + { + InputManager.MoveMouseTo(textBox); + InputManager.Click(MouseButton.Left); + }); + AddAssert("text deselect event", () => textBox.TextDeselectionQueue.Dequeue()); + + AddAssert("text input not deactivated", () => textInput.DeactivationQueue.Count == 0); + AddAssert("text input not activated again", () => textInput.ActivationQueue.Count == 0); + AddAssert("text input ensure activated", () => textInput.EnsureActivatedQueue.Dequeue() && textInput.EnsureActivatedQueue.Count == 0); + + AddStep("click-drag selection", () => + { + InputManager.MoveMouseTo(textBox); + InputManager.PressButton(MouseButton.Left); + InputManager.MoveMouseTo(textInputContainer.ToScreenSpace(textBox.DrawRectangle.Centre + new Vector2(50, 0))); + InputManager.ReleaseButton(MouseButton.Left); + }); + AddAssert("text selection event (character)", () => textBox.TextSelectionQueue.Dequeue() == TextBox.TextSelectionType.Character); + } + + [Test] + public void TestSelectAfterOutOfBandSelectionChange() + { + AddStep("select all text", () => InputManager.Keys(PlatformAction.SelectAll)); + AddAssert("text selection event (all)", () => textBox.TextSelectionQueue.Dequeue() == TextBox.TextSelectionType.All); + + AddStep("delete all text", () => InputManager.Keys(PlatformAction.Delete)); + AddAssert("user text removed event raised", () => textBox.UserRemovedTextQueue.Dequeue() == default_text); + + AddAssert("no text is selected", () => textBox.SelectedText, () => Is.Empty); + AddStep("invoke caret select action", () => InputManager.Keys(PlatformAction.SelectForwardChar)); + AddAssert("no text is selected", () => textBox.SelectedText, () => Is.Empty); + + AddAssert("no text selection event", () => textBox.TextSelectionQueue, () => Has.Exactly(0).Items); } [Test] @@ -382,6 +480,7 @@ public void TestReadOnlyTextBoxDoesntReceiveInput() public void TestStartingCompositionRemovesSelection() { AddStep("select all text", () => InputManager.Keys(PlatformAction.SelectAll)); + AddAssert("text selection event (all)", () => textBox.TextSelectionQueue.Dequeue() == TextBox.TextSelectionType.All); startComposition(); AddAssert("user text removed event not raised", () => textBox.UserRemovedTextQueue.Count == 0); @@ -433,6 +532,8 @@ public void TearDownSteps() textBox.CaretMovedQueue.Count == 0 && textBox.ImeCompositionQueue.Count == 0 && textBox.ImeResultQueue.Count == 0 && + textBox.TextSelectionQueue.Count == 0 && + textBox.TextDeselectionQueue.Count == 0 && textInput.ActivationQueue.Count == 0 && textInput.DeactivationQueue.Count == 0 && textInput.EnsureActivatedQueue.Count == 0); @@ -483,6 +584,8 @@ public class EventQueuesTextBox : TestSceneTextBox.InsertableTextBox public readonly Queue CaretMovedQueue = new Queue(); public readonly Queue ImeCompositionQueue = new Queue(); public readonly Queue ImeResultQueue = new Queue(); + public readonly Queue TextSelectionQueue = new Queue(); + public readonly Queue TextDeselectionQueue = new Queue(); protected override void NotifyInputError() => InputErrorQueue.Enqueue(true); protected override void OnUserTextAdded(string consumed) => UserConsumedTextQueue.Enqueue(consumed); @@ -506,6 +609,9 @@ protected override void OnImeResult(string result, bool successful) => Successful = successful }); + protected override void OnTextSelectionChanged(TextSelectionType selectionType) => TextSelectionQueue.Enqueue(selectionType); + protected override void OnTextDeselected() => TextDeselectionQueue.Enqueue(true); + public new bool ImeCompositionActive => base.ImeCompositionActive; } diff --git a/osu.Framework/Graphics/UserInterface/TextBox.cs b/osu.Framework/Graphics/UserInterface/TextBox.cs index 78c86ec674..230ce33fb0 100644 --- a/osu.Framework/Graphics/UserInterface/TextBox.cs +++ b/osu.Framework/Graphics/UserInterface/TextBox.cs @@ -233,6 +233,8 @@ public virtual bool OnPressed(KeyBindingPressEvent e) if (e.Action.IsCommonTextEditingAction() && ImeCompositionActive) return true; + var lastSelectionBounds = getTextSelectionBounds(); + switch (e.Action) { // Clipboard @@ -263,6 +265,7 @@ public virtual bool OnPressed(KeyBindingPressEvent e) selectionStart = 0; selectionEnd = text.Length; cursorAndLayout.Invalidate(); + onTextSelectionChanged(TextSelectionType.All, lastSelectionBounds); return true; // Cursor Manipulation @@ -318,26 +321,34 @@ public virtual bool OnPressed(KeyBindingPressEvent e) // Expand selection case PlatformAction.SelectBackwardChar: ExpandSelectionBy(-1); + onTextSelectionChanged(TextSelectionType.Character, lastSelectionBounds); return true; case PlatformAction.SelectForwardChar: ExpandSelectionBy(1); + onTextSelectionChanged(TextSelectionType.Character, lastSelectionBounds); return true; case PlatformAction.SelectBackwardWord: ExpandSelectionBy(GetBackwardWordAmount()); + onTextSelectionChanged(TextSelectionType.Word, lastSelectionBounds); return true; case PlatformAction.SelectForwardWord: ExpandSelectionBy(GetForwardWordAmount()); + onTextSelectionChanged(TextSelectionType.Word, lastSelectionBounds); return true; case PlatformAction.SelectBackwardLine: ExpandSelectionBy(GetBackwardLineAmount()); + // TODO: Differentiate 'line' and 'all' selection types if/when multi-line support is added + onTextSelectionChanged(TextSelectionType.All, lastSelectionBounds); return true; case PlatformAction.SelectForwardLine: ExpandSelectionBy(GetForwardLineAmount()); + // TODO: Differentiate 'line' and 'all' selection types if/when multi-line support is added + onTextSelectionChanged(TextSelectionType.All, lastSelectionBounds); return true; } @@ -388,9 +399,11 @@ protected int GetForwardWordAmount() /// protected void MoveCursorBy(int amount) { + var lastSelectionBounds = getTextSelectionBounds(); selectionStart = selectionEnd; cursorAndLayout.Invalidate(); moveSelection(amount, false); + onTextDeselected(lastSelectionBounds); } /// @@ -832,6 +845,43 @@ protected virtual void OnCaretMoved(bool selecting) { } + /// + /// Invoked whenever text selection changes. For deselection, see . + /// + /// The type of selection change that occured. + protected virtual void OnTextSelectionChanged(TextSelectionType selectionType) + { + } + + /// + /// Invoked whenever selected text is deselected. For selection, see . + /// + protected virtual void OnTextDeselected() + { + } + + private void onTextSelectionChanged(TextSelectionType selectionType, (int start, int end) lastSelectionBounds) + { + if (lastSelectionBounds.start == selectionStart && lastSelectionBounds.end == selectionEnd) + return; + + if (selectionLength > 0) + OnTextSelectionChanged(selectionType); + else + onTextDeselected(lastSelectionBounds); + } + + private void onTextDeselected((int start, int end) lastSelectionBounds) + { + if (lastSelectionBounds.start == selectionStart && lastSelectionBounds.end == selectionEnd) + return; + + if (lastSelectionBounds.start != lastSelectionBounds.end) + OnTextDeselected(); + } + + private (int start, int end) getTextSelectionBounds() => (selectionStart, selectionEnd); + /// /// Invoked whenever the IME composition has changed. /// @@ -1078,6 +1128,8 @@ protected override void OnDrag(DragEvent e) FinalizeImeComposition(true); + var lastSelectionBounds = getTextSelectionBounds(); + if (doubleClickWord != null) { //select words at a time @@ -1099,8 +1151,6 @@ protected override void OnDrag(DragEvent e) selectionStart = doubleClickWord[0]; selectionEnd = doubleClickWord[1]; } - - cursorAndLayout.Invalidate(); } else { @@ -1109,9 +1159,11 @@ protected override void OnDrag(DragEvent e) selectionEnd = getCharacterClosestTo(e.MousePosition); if (selectionLength > 0) GetContainingInputManager().ChangeFocus(this); - - cursorAndLayout.Invalidate(); } + + cursorAndLayout.Invalidate(); + + onTextSelectionChanged(doubleClickWord != null ? TextSelectionType.Word : TextSelectionType.Character, lastSelectionBounds); } protected override bool OnDragStart(DragStartEvent e) @@ -1127,6 +1179,8 @@ protected override bool OnDoubleClick(DoubleClickEvent e) { FinalizeImeComposition(true); + var lastSelectionBounds = getTextSelectionBounds(); + if (text.Length == 0) return true; if (AllowClipboardExport) @@ -1149,6 +1203,9 @@ protected override bool OnDoubleClick(DoubleClickEvent e) doubleClickWord = new[] { selectionStart, selectionEnd }; cursorAndLayout.Invalidate(); + + onTextSelectionChanged(TextSelectionType.Word, lastSelectionBounds); + return true; } @@ -1172,10 +1229,14 @@ protected override bool OnMouseDown(MouseDownEvent e) FinalizeImeComposition(true); + var lastSelectionBounds = getTextSelectionBounds(); + selectionStart = selectionEnd = getCharacterClosestTo(e.MousePosition); cursorAndLayout.Invalidate(); + onTextDeselected(lastSelectionBounds); + return false; } @@ -1595,5 +1656,23 @@ private void updateImeWindowPosition() } #endregion + + public enum TextSelectionType + { + /// + /// A character was added or removed from the selection. + /// + Character, + + /// + /// A word was added or removed from the selection. + /// + Word, + + /// + /// All of the text was selected (i.e. via ). + /// + All + } } }