Skip to content

Commit

Permalink
Merge pull request #5366 from nekodex/textbox-callbacks
Browse files Browse the repository at this point in the history
Add callbacks to text selection events of TextBox
  • Loading branch information
peppy authored Aug 25, 2022
2 parents 5beb940 + 5c3a92c commit ecdec63
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 4 deletions.
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
}
}
}

0 comments on commit ecdec63

Please sign in to comment.