Added text selection and copy support to TextFieldWidget.

Use Shift and navigation key (cursor, home, end) to select a portion of
text, and replace/delete/cut as appropriate.
Also provides support for selection with mouse (click and drag)
This commit is contained in:
Joe Alam
2018-04-07 17:45:11 +00:00
committed by reaperrr
parent b5893d4c6d
commit 7221c29d9b
7 changed files with 148 additions and 10 deletions

View File

@@ -80,6 +80,7 @@ Also thanks to:
* Jefri Sevkin (Arular) * Jefri Sevkin (Arular)
* Jes * Jes
* Joakim Lindberg (booom3) * Joakim Lindberg (booom3)
* Joe Alam (joealam)
* John Turner (whinis) * John Turner (whinis)
* Jonas A. Lind (SoScared) * Jonas A. Lind (SoScared)
* Joppy Furr * Joppy Furr

View File

@@ -45,6 +45,11 @@ namespace OpenRA.Mods.Common.Widgets
public Color TextColor = ChromeMetrics.Get<Color>("TextfieldColor"); public Color TextColor = ChromeMetrics.Get<Color>("TextfieldColor");
public Color TextColorDisabled = ChromeMetrics.Get<Color>("TextfieldColorDisabled"); public Color TextColorDisabled = ChromeMetrics.Get<Color>("TextfieldColorDisabled");
public Color TextColorInvalid = ChromeMetrics.Get<Color>("TextfieldColorInvalid"); public Color TextColorInvalid = ChromeMetrics.Get<Color>("TextfieldColorInvalid");
public Color TextColorHighlight = ChromeMetrics.Get<Color>("TextfieldColorHighlight");
protected int selectionStartIndex = -1;
protected int selectionEndIndex = -1;
protected bool mouseSelectionActive = false;
public TextFieldWidget() public TextFieldWidget()
{ {
@@ -60,6 +65,7 @@ namespace OpenRA.Mods.Common.Widgets
TextColor = widget.TextColor; TextColor = widget.TextColor;
TextColorDisabled = widget.TextColorDisabled; TextColorDisabled = widget.TextColorDisabled;
TextColorInvalid = widget.TextColorInvalid; TextColorInvalid = widget.TextColorInvalid;
TextColorHighlight = widget.TextColorHighlight;
VisualHeight = widget.VisualHeight; VisualHeight = widget.VisualHeight;
IsDisabled = widget.IsDisabled; IsDisabled = widget.IsDisabled;
} }
@@ -81,15 +87,35 @@ namespace OpenRA.Mods.Common.Widgets
if (IsDisabled()) if (IsDisabled())
return false; return false;
if (mi.Event != MouseInputEvent.Down) if (mouseSelectionActive)
{
if (mi.Event == MouseInputEvent.Up)
{
mouseSelectionActive = false;
return true;
}
else if (mi.Event != MouseInputEvent.Move)
return false;
}
else if (mi.Event != MouseInputEvent.Down)
return false; return false;
// Attempt to take keyboard focus // Attempt to take keyboard focus
if (!RenderBounds.Contains(mi.Location) || !TakeKeyboardFocus()) if (!RenderBounds.Contains(mi.Location) || !TakeKeyboardFocus())
return false; return false;
mouseSelectionActive = true;
ResetBlinkCycle(); ResetBlinkCycle();
var cachedCursorPos = CursorPosition;
CursorPosition = ClosestCursorPosition(mi.Location.X); CursorPosition = ClosestCursorPosition(mi.Location.X);
if (mi.Modifiers.HasModifier(Modifiers.Shift) || (mi.Event == MouseInputEvent.Move && mouseSelectionActive))
HandleSelectionUpdate(cachedCursorPos, CursorPosition);
else
ClearSelection();
return true; return true;
} }
@@ -147,7 +173,8 @@ namespace OpenRA.Mods.Common.Widgets
var isOSX = Platform.CurrentPlatform == PlatformType.OSX; var isOSX = Platform.CurrentPlatform == PlatformType.OSX;
switch (e.Key) { switch (e.Key)
{
case Keycode.RETURN: case Keycode.RETURN:
case Keycode.KP_ENTER: case Keycode.KP_ENTER:
if (OnEnterKey()) if (OnEnterKey())
@@ -160,6 +187,7 @@ namespace OpenRA.Mods.Common.Widgets
break; break;
case Keycode.ESCAPE: case Keycode.ESCAPE:
ClearSelection();
if (OnEscKey()) if (OnEscKey())
return true; return true;
break; break;
@@ -173,12 +201,19 @@ namespace OpenRA.Mods.Common.Widgets
ResetBlinkCycle(); ResetBlinkCycle();
if (CursorPosition > 0) if (CursorPosition > 0)
{ {
var cachedCurrentCursorPos = CursorPosition;
if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Alt))) if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Alt)))
CursorPosition = GetPrevWhitespaceIndex(); CursorPosition = GetPrevWhitespaceIndex();
else if (isOSX && e.Modifiers.HasModifier(Modifiers.Meta)) else if (isOSX && e.Modifiers.HasModifier(Modifiers.Meta))
CursorPosition = 0; CursorPosition = 0;
else else
CursorPosition--; CursorPosition--;
if (e.Modifiers.HasModifier(Modifiers.Shift))
HandleSelectionUpdate(cachedCurrentCursorPos, CursorPosition);
else
ClearSelection();
} }
break; break;
@@ -187,23 +222,41 @@ namespace OpenRA.Mods.Common.Widgets
ResetBlinkCycle(); ResetBlinkCycle();
if (CursorPosition <= Text.Length - 1) if (CursorPosition <= Text.Length - 1)
{ {
var cachedCurrentCursorPos = CursorPosition;
if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Alt))) if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Alt)))
CursorPosition = GetNextWhitespaceIndex(); CursorPosition = GetNextWhitespaceIndex();
else if (isOSX && e.Modifiers.HasModifier(Modifiers.Meta)) else if (isOSX && e.Modifiers.HasModifier(Modifiers.Meta))
CursorPosition = Text.Length; CursorPosition = Text.Length;
else else
CursorPosition++; CursorPosition++;
if (e.Modifiers.HasModifier(Modifiers.Shift))
HandleSelectionUpdate(cachedCurrentCursorPos, CursorPosition);
else
ClearSelection();
} }
break; break;
case Keycode.HOME: case Keycode.HOME:
ResetBlinkCycle(); ResetBlinkCycle();
if (e.Modifiers.HasModifier(Modifiers.Shift))
HandleSelectionUpdate(CursorPosition, 0);
else
ClearSelection();
CursorPosition = 0; CursorPosition = 0;
break; break;
case Keycode.END: case Keycode.END:
ResetBlinkCycle(); ResetBlinkCycle();
if (e.Modifiers.HasModifier(Modifiers.Shift))
HandleSelectionUpdate(CursorPosition, Text.Length);
else
ClearSelection();
CursorPosition = Text.Length; CursorPosition = Text.Length;
break; break;
@@ -234,6 +287,7 @@ namespace OpenRA.Mods.Common.Widgets
{ {
Text = Text.Substring(CursorPosition); Text = Text.Substring(CursorPosition);
CursorPosition = 0; CursorPosition = 0;
ClearSelection();
OnTextEdited(); OnTextEdited();
} }
@@ -242,20 +296,35 @@ namespace OpenRA.Mods.Common.Widgets
case Keycode.X: case Keycode.X:
ResetBlinkCycle(); ResetBlinkCycle();
if (((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Meta))) && if (((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Meta))) &&
!string.IsNullOrEmpty(Text)) !string.IsNullOrEmpty(Text) && selectionStartIndex != -1)
{ {
Game.Renderer.SetClipboardText(Text); var lowestIndex = selectionStartIndex < selectionEndIndex ? selectionStartIndex : selectionEndIndex;
Text = Text.Remove(0); var highestIndex = selectionStartIndex < selectionEndIndex ? selectionEndIndex : selectionStartIndex;
CursorPosition = 0; Game.Renderer.SetClipboardText(Text.Substring(lowestIndex, highestIndex - lowestIndex));
RemoveSelectedText();
OnTextEdited(); OnTextEdited();
} }
break;
case Keycode.C:
ResetBlinkCycle();
if (((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Meta)))
&& !string.IsNullOrEmpty(Text) && selectionStartIndex != -1)
{
var lowestIndex = selectionStartIndex < selectionEndIndex ? selectionStartIndex : selectionEndIndex;
var highestIndex = selectionStartIndex < selectionEndIndex ? selectionEndIndex : selectionStartIndex;
Game.Renderer.SetClipboardText(Text.Substring(lowestIndex, highestIndex - lowestIndex));
}
break; break;
case Keycode.DELETE: case Keycode.DELETE:
// cmd+delete is equivalent to ctrl+k on non-osx // cmd+delete is equivalent to ctrl+k on non-osx
ResetBlinkCycle(); ResetBlinkCycle();
if (CursorPosition < Text.Length) if (selectionStartIndex != -1)
RemoveSelectedText();
else if (CursorPosition < Text.Length)
{ {
if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Alt))) if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Alt)))
Text = Text.Substring(0, CursorPosition) + Text.Substring(GetNextWhitespaceIndex()); Text = Text.Substring(0, CursorPosition) + Text.Substring(GetNextWhitespaceIndex());
@@ -272,7 +341,9 @@ namespace OpenRA.Mods.Common.Widgets
case Keycode.BACKSPACE: case Keycode.BACKSPACE:
// cmd+backspace is equivalent to ctrl+u on non-osx // cmd+backspace is equivalent to ctrl+u on non-osx
ResetBlinkCycle(); ResetBlinkCycle();
if (CursorPosition > 0) if (selectionStartIndex != -1)
RemoveSelectedText();
else if (CursorPosition > 0)
{ {
if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Alt))) if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Alt)))
{ {
@@ -298,6 +369,10 @@ namespace OpenRA.Mods.Common.Widgets
case Keycode.V: case Keycode.V:
ResetBlinkCycle(); ResetBlinkCycle();
if (selectionStartIndex != -1)
RemoveSelectedText();
if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Meta))) if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Meta)))
{ {
var clipboardText = Game.Renderer.GetClipboardText(); var clipboardText = Game.Renderer.GetClipboardText();
@@ -313,10 +388,18 @@ namespace OpenRA.Mods.Common.Widgets
} }
break; break;
case Keycode.A:
// Ctrl+A as Select-All, or Cmd+A on OSX
if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Meta)))
{
ClearSelection();
HandleSelectionUpdate(0, Text.Length);
}
break;
default: default:
break; break;
} }
return true; return true;
} }
@@ -326,6 +409,9 @@ namespace OpenRA.Mods.Common.Widgets
if (!HasKeyboardFocus || IsDisabled()) if (!HasKeyboardFocus || IsDisabled())
return false; return false;
if (selectionStartIndex != -1)
RemoveSelectedText();
if (MaxLength > 0 && Text.Length >= MaxLength) if (MaxLength > 0 && Text.Length >= MaxLength)
return true; return true;
@@ -337,11 +423,44 @@ namespace OpenRA.Mods.Common.Widgets
Text = Text.Insert(CursorPosition, text.Substring(0, pasteLength)); Text = Text.Insert(CursorPosition, text.Substring(0, pasteLength));
CursorPosition += pasteLength; CursorPosition += pasteLength;
ClearSelection();
OnTextEdited(); OnTextEdited();
return true; return true;
} }
void HandleSelectionUpdate(int prevCursorPos, int newCursorPos)
{
// If selection index is -1, there's no selection already open so create one
if (selectionStartIndex == -1)
selectionStartIndex = prevCursorPos;
selectionEndIndex = newCursorPos;
if (selectionStartIndex == selectionEndIndex)
ClearSelection();
}
void ClearSelection()
{
selectionStartIndex = -1;
selectionEndIndex = -1;
}
void RemoveSelectedText()
{
if (selectionStartIndex != -1)
{
var lowestIndex = selectionStartIndex < selectionEndIndex ? selectionStartIndex : selectionEndIndex;
var highestIndex = selectionStartIndex < selectionEndIndex ? selectionEndIndex : selectionStartIndex;
Text = Text.Remove(lowestIndex, highestIndex - lowestIndex);
ClearSelection();
CursorPosition = lowestIndex;
}
}
protected int blinkCycle = 10; protected int blinkCycle = 10;
protected bool showCursor = true; protected bool showCursor = true;
@@ -383,7 +502,8 @@ namespace OpenRA.Mods.Common.Widgets
new Rectangle(pos.X, pos.Y, Bounds.Width, Bounds.Height)); new Rectangle(pos.X, pos.Y, Bounds.Width, Bounds.Height));
// Inset text by the margin and center vertically // Inset text by the margin and center vertically
var textPos = pos + new int2(LeftMargin, (Bounds.Height - textSize.Y) / 2 - VisualHeight); var verticalMargin = (Bounds.Height - textSize.Y) / 2 - VisualHeight;
var textPos = pos + new int2(LeftMargin, verticalMargin);
// Right align when editing and scissor when the text overflows // Right align when editing and scissor when the text overflows
if (textSize.X > Bounds.Width - LeftMargin - RightMargin) if (textSize.X > Bounds.Width - LeftMargin - RightMargin)
@@ -395,6 +515,18 @@ namespace OpenRA.Mods.Common.Widgets
Bounds.Width - LeftMargin - RightMargin, Bounds.Bottom)); Bounds.Width - LeftMargin - RightMargin, Bounds.Bottom));
} }
// Draw the highlight around the selected area
if (selectionStartIndex != -1)
{
var visualSelectionStartIndex = selectionStartIndex < selectionEndIndex ? selectionStartIndex : selectionEndIndex;
var visualSelectionEndIndex = selectionStartIndex < selectionEndIndex ? selectionEndIndex : selectionStartIndex;
var highlightStartX = font.Measure(apparentText.Substring(0, visualSelectionStartIndex)).X;
var highlightEndX = font.Measure(apparentText.Substring(0, visualSelectionEndIndex)).X;
WidgetUtils.FillRectWithColor(
new Rectangle(textPos.X + highlightStartX, textPos.Y, highlightEndX - highlightStartX, Bounds.Height - (verticalMargin * 2)), TextColorHighlight);
}
var color = var color =
disabled ? TextColorDisabled disabled ? TextColorDisabled
: IsValid() ? TextColor : IsValid() ? TextColor

View File

@@ -6,3 +6,4 @@ Metrics:
CheckboxPressedState: true CheckboxPressedState: true
ColorPickerActorType: fact.colorpicker ColorPickerActorType: fact.colorpicker
ColorPickerRemapIndices: 176, 178, 180, 182, 184, 186, 189, 191, 177, 179, 181, 183, 185, 187, 188, 190 ColorPickerRemapIndices: 176, 178, 180, 182, 184, 186, 189, 191, 177, 179, 181, 183, 185, 187, 188, 190
TextfieldColorHighlight: 800000

View File

@@ -36,6 +36,7 @@ Metrics:
TextfieldColor: FFFFFF TextfieldColor: FFFFFF
TextfieldColorDisabled: 808080 TextfieldColorDisabled: 808080
TextfieldColorInvalid: FFC0C0 TextfieldColorInvalid: FFC0C0
TextfieldColorHighlight: 195BC4
TextfieldFont: Regular TextfieldFont: Regular
WaitingGameColor: 00FF00 WaitingGameColor: 00FF00
UPnPDisabledColor: FFFFFF UPnPDisabledColor: FFFFFF

View File

@@ -6,3 +6,4 @@ Metrics:
FactionSuffix-corrino: harkonnen FactionSuffix-corrino: harkonnen
FactionSuffix-smuggler: ordos FactionSuffix-smuggler: ordos
FactionSuffix-mercenary: ordos FactionSuffix-mercenary: ordos
TextfieldColorHighlight: 7f4d29

View File

@@ -15,3 +15,4 @@ Metrics:
FactionSuffix-ukraine: soviet FactionSuffix-ukraine: soviet
IncompatibleProtectedGameColor: B22222 IncompatibleProtectedGameColor: B22222
IncompatibleVersionColor: D3D3D3 IncompatibleVersionColor: D3D3D3
TextfieldColorHighlight: 562020

View File

@@ -2,3 +2,4 @@
Metrics: Metrics:
ColorPickerActorType: mmch.colorpicker ColorPickerActorType: mmch.colorpicker
ColorPickerRemapIndices: 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 ColorPickerRemapIndices: 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31
TextfieldColorHighlight: 1a1a1a