Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add callbacks to text selection events of TextBox #5366

Merged
merged 8 commits into from
Aug 25, 2022
106 changes: 106 additions & 0 deletions osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -483,6 +584,8 @@ public class EventQueuesTextBox : TestSceneTextBox.InsertableTextBox
public readonly Queue<bool> CaretMovedQueue = new Queue<bool>();
public readonly Queue<ImeCompositionEvent> ImeCompositionQueue = new Queue<ImeCompositionEvent>();
public readonly Queue<ImeResultEvent> ImeResultQueue = new Queue<ImeResultEvent>();
public readonly Queue<TextSelectionType> TextSelectionQueue = new Queue<TextSelectionType>();
public readonly Queue<bool> TextDeselectionQueue = new Queue<bool>();

protected override void NotifyInputError() => InputErrorQueue.Enqueue(true);
protected override void OnUserTextAdded(string consumed) => UserConsumedTextQueue.Enqueue(consumed);
Expand All @@ -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;
}

Expand Down
87 changes: 83 additions & 4 deletions osu.Framework/Graphics/UserInterface/TextBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ public virtual bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
if (e.Action.IsCommonTextEditingAction() && ImeCompositionActive)
return true;

var lastSelectionBounds = getTextSelectionBounds();

switch (e.Action)
{
// Clipboard
Expand Down Expand Up @@ -263,6 +265,7 @@ public virtual bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
selectionStart = 0;
selectionEnd = text.Length;
cursorAndLayout.Invalidate();
onTextSelectionChanged(TextSelectionType.All, lastSelectionBounds);
return true;

// Cursor Manipulation
Expand Down Expand Up @@ -318,26 +321,34 @@ public virtual bool OnPressed(KeyBindingPressEvent<PlatformAction> 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;
}

Expand Down Expand Up @@ -388,9 +399,11 @@ protected int GetForwardWordAmount()
/// </summary>
protected void MoveCursorBy(int amount)
{
var lastSelectionBounds = getTextSelectionBounds();
selectionStart = selectionEnd;
cursorAndLayout.Invalidate();
moveSelection(amount, false);
onTextDeselected(lastSelectionBounds);
}

/// <summary>
Expand Down Expand Up @@ -832,6 +845,43 @@ protected virtual void OnCaretMoved(bool selecting)
{
}

/// <summary>
/// Invoked whenever text selection changes. For deselection, see <seealso cref="OnTextDeselected"/>.
/// </summary>
/// <param name="selectionType">The type of selection change that occured.</param>
protected virtual void OnTextSelectionChanged(TextSelectionType selectionType)
{
}

/// <summary>
/// Invoked whenever selected text is deselected. For selection, see <seealso cref="OnTextSelectionChanged"/>.
/// </summary>
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);

/// <summary>
/// Invoked whenever the IME composition has changed.
/// </summary>
Expand Down Expand Up @@ -1078,6 +1128,8 @@ protected override void OnDrag(DragEvent e)

FinalizeImeComposition(true);

var lastSelectionBounds = getTextSelectionBounds();

if (doubleClickWord != null)
{
//select words at a time
Expand All @@ -1099,8 +1151,6 @@ protected override void OnDrag(DragEvent e)
selectionStart = doubleClickWord[0];
selectionEnd = doubleClickWord[1];
}

cursorAndLayout.Invalidate();
}
else
{
Expand All @@ -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)
Expand All @@ -1127,6 +1179,8 @@ protected override bool OnDoubleClick(DoubleClickEvent e)
{
FinalizeImeComposition(true);

var lastSelectionBounds = getTextSelectionBounds();

if (text.Length == 0) return true;

if (AllowClipboardExport)
Expand All @@ -1149,6 +1203,9 @@ protected override bool OnDoubleClick(DoubleClickEvent e)
doubleClickWord = new[] { selectionStart, selectionEnd };

cursorAndLayout.Invalidate();

onTextSelectionChanged(TextSelectionType.Word, lastSelectionBounds);

return true;
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -1595,5 +1656,23 @@ private void updateImeWindowPosition()
}

#endregion

public enum TextSelectionType
{
/// <summary>
/// A character was added or removed from the selection.
/// </summary>
Character,

/// <summary>
/// A word was added or removed from the selection.
/// </summary>
Word,

/// <summary>
/// All of the text was selected (i.e. via <see cref="PlatformAction.SelectAll"/>).
/// </summary>
All
}
}
}