diff --git a/AUTHORS b/AUTHORS index 39a8e69067..b40047ddae 100644 --- a/AUTHORS +++ b/AUTHORS @@ -80,6 +80,7 @@ Also thanks to: * Jefri Sevkin (Arular) * Jes * Joakim Lindberg (booom3) + * Joe Alam (joealam) * John Turner (whinis) * Jonas A. Lind (SoScared) * Joppy Furr diff --git a/OpenRA.Mods.Common/Widgets/TextFieldWidget.cs b/OpenRA.Mods.Common/Widgets/TextFieldWidget.cs index 26b253b482..2257b3a7df 100644 --- a/OpenRA.Mods.Common/Widgets/TextFieldWidget.cs +++ b/OpenRA.Mods.Common/Widgets/TextFieldWidget.cs @@ -45,6 +45,11 @@ namespace OpenRA.Mods.Common.Widgets public Color TextColor = ChromeMetrics.Get("TextfieldColor"); public Color TextColorDisabled = ChromeMetrics.Get("TextfieldColorDisabled"); public Color TextColorInvalid = ChromeMetrics.Get("TextfieldColorInvalid"); + public Color TextColorHighlight = ChromeMetrics.Get("TextfieldColorHighlight"); + + protected int selectionStartIndex = -1; + protected int selectionEndIndex = -1; + protected bool mouseSelectionActive = false; public TextFieldWidget() { @@ -60,6 +65,7 @@ namespace OpenRA.Mods.Common.Widgets TextColor = widget.TextColor; TextColorDisabled = widget.TextColorDisabled; TextColorInvalid = widget.TextColorInvalid; + TextColorHighlight = widget.TextColorHighlight; VisualHeight = widget.VisualHeight; IsDisabled = widget.IsDisabled; } @@ -81,15 +87,35 @@ namespace OpenRA.Mods.Common.Widgets if (IsDisabled()) 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; // Attempt to take keyboard focus if (!RenderBounds.Contains(mi.Location) || !TakeKeyboardFocus()) return false; + mouseSelectionActive = true; + ResetBlinkCycle(); + + var cachedCursorPos = CursorPosition; CursorPosition = ClosestCursorPosition(mi.Location.X); + + if (mi.Modifiers.HasModifier(Modifiers.Shift) || (mi.Event == MouseInputEvent.Move && mouseSelectionActive)) + HandleSelectionUpdate(cachedCursorPos, CursorPosition); + else + ClearSelection(); + return true; } @@ -147,7 +173,8 @@ namespace OpenRA.Mods.Common.Widgets var isOSX = Platform.CurrentPlatform == PlatformType.OSX; - switch (e.Key) { + switch (e.Key) + { case Keycode.RETURN: case Keycode.KP_ENTER: if (OnEnterKey()) @@ -160,6 +187,7 @@ namespace OpenRA.Mods.Common.Widgets break; case Keycode.ESCAPE: + ClearSelection(); if (OnEscKey()) return true; break; @@ -173,12 +201,19 @@ namespace OpenRA.Mods.Common.Widgets ResetBlinkCycle(); if (CursorPosition > 0) { + var cachedCurrentCursorPos = CursorPosition; + if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Alt))) CursorPosition = GetPrevWhitespaceIndex(); else if (isOSX && e.Modifiers.HasModifier(Modifiers.Meta)) CursorPosition = 0; else CursorPosition--; + + if (e.Modifiers.HasModifier(Modifiers.Shift)) + HandleSelectionUpdate(cachedCurrentCursorPos, CursorPosition); + else + ClearSelection(); } break; @@ -187,23 +222,41 @@ namespace OpenRA.Mods.Common.Widgets ResetBlinkCycle(); if (CursorPosition <= Text.Length - 1) { + var cachedCurrentCursorPos = CursorPosition; + if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Alt))) CursorPosition = GetNextWhitespaceIndex(); else if (isOSX && e.Modifiers.HasModifier(Modifiers.Meta)) CursorPosition = Text.Length; else CursorPosition++; + + if (e.Modifiers.HasModifier(Modifiers.Shift)) + HandleSelectionUpdate(cachedCurrentCursorPos, CursorPosition); + else + ClearSelection(); } break; case Keycode.HOME: ResetBlinkCycle(); + if (e.Modifiers.HasModifier(Modifiers.Shift)) + HandleSelectionUpdate(CursorPosition, 0); + else + ClearSelection(); + CursorPosition = 0; break; case Keycode.END: ResetBlinkCycle(); + + if (e.Modifiers.HasModifier(Modifiers.Shift)) + HandleSelectionUpdate(CursorPosition, Text.Length); + else + ClearSelection(); + CursorPosition = Text.Length; break; @@ -234,6 +287,7 @@ namespace OpenRA.Mods.Common.Widgets { Text = Text.Substring(CursorPosition); CursorPosition = 0; + ClearSelection(); OnTextEdited(); } @@ -242,20 +296,35 @@ namespace OpenRA.Mods.Common.Widgets case Keycode.X: ResetBlinkCycle(); 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); - Text = Text.Remove(0); - CursorPosition = 0; + var lowestIndex = selectionStartIndex < selectionEndIndex ? selectionStartIndex : selectionEndIndex; + var highestIndex = selectionStartIndex < selectionEndIndex ? selectionEndIndex : selectionStartIndex; + Game.Renderer.SetClipboardText(Text.Substring(lowestIndex, highestIndex - lowestIndex)); + + RemoveSelectedText(); 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; case Keycode.DELETE: // cmd+delete is equivalent to ctrl+k on non-osx 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))) Text = Text.Substring(0, CursorPosition) + Text.Substring(GetNextWhitespaceIndex()); @@ -272,7 +341,9 @@ namespace OpenRA.Mods.Common.Widgets case Keycode.BACKSPACE: // cmd+backspace is equivalent to ctrl+u on non-osx 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))) { @@ -298,6 +369,10 @@ namespace OpenRA.Mods.Common.Widgets case Keycode.V: ResetBlinkCycle(); + + if (selectionStartIndex != -1) + RemoveSelectedText(); + if ((!isOSX && e.Modifiers.HasModifier(Modifiers.Ctrl)) || (isOSX && e.Modifiers.HasModifier(Modifiers.Meta))) { var clipboardText = Game.Renderer.GetClipboardText(); @@ -313,10 +388,18 @@ namespace OpenRA.Mods.Common.Widgets } 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: break; - } + } return true; } @@ -326,6 +409,9 @@ namespace OpenRA.Mods.Common.Widgets if (!HasKeyboardFocus || IsDisabled()) return false; + if (selectionStartIndex != -1) + RemoveSelectedText(); + if (MaxLength > 0 && Text.Length >= MaxLength) return true; @@ -337,11 +423,44 @@ namespace OpenRA.Mods.Common.Widgets Text = Text.Insert(CursorPosition, text.Substring(0, pasteLength)); CursorPosition += pasteLength; + ClearSelection(); OnTextEdited(); 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 bool showCursor = true; @@ -383,7 +502,8 @@ namespace OpenRA.Mods.Common.Widgets new Rectangle(pos.X, pos.Y, Bounds.Width, Bounds.Height)); // 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 if (textSize.X > Bounds.Width - LeftMargin - RightMargin) @@ -395,6 +515,18 @@ namespace OpenRA.Mods.Common.Widgets 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 = disabled ? TextColorDisabled : IsValid() ? TextColor diff --git a/mods/cnc/metrics.yaml b/mods/cnc/metrics.yaml index eba2223b0a..49380eabed 100644 --- a/mods/cnc/metrics.yaml +++ b/mods/cnc/metrics.yaml @@ -6,3 +6,4 @@ Metrics: CheckboxPressedState: true ColorPickerActorType: fact.colorpicker ColorPickerRemapIndices: 176, 178, 180, 182, 184, 186, 189, 191, 177, 179, 181, 183, 185, 187, 188, 190 + TextfieldColorHighlight: 800000 diff --git a/mods/common/metrics.yaml b/mods/common/metrics.yaml index 605352ed54..3d4c68a01c 100644 --- a/mods/common/metrics.yaml +++ b/mods/common/metrics.yaml @@ -36,6 +36,7 @@ Metrics: TextfieldColor: FFFFFF TextfieldColorDisabled: 808080 TextfieldColorInvalid: FFC0C0 + TextfieldColorHighlight: 195BC4 TextfieldFont: Regular WaitingGameColor: 00FF00 UPnPDisabledColor: FFFFFF diff --git a/mods/d2k/metrics.yaml b/mods/d2k/metrics.yaml index 913609c8d1..019bb57634 100644 --- a/mods/d2k/metrics.yaml +++ b/mods/d2k/metrics.yaml @@ -6,3 +6,4 @@ Metrics: FactionSuffix-corrino: harkonnen FactionSuffix-smuggler: ordos FactionSuffix-mercenary: ordos + TextfieldColorHighlight: 7f4d29 diff --git a/mods/ra/metrics.yaml b/mods/ra/metrics.yaml index 0bb45fcffa..3f3c77b4e2 100644 --- a/mods/ra/metrics.yaml +++ b/mods/ra/metrics.yaml @@ -15,3 +15,4 @@ Metrics: FactionSuffix-ukraine: soviet IncompatibleProtectedGameColor: B22222 IncompatibleVersionColor: D3D3D3 + TextfieldColorHighlight: 562020 diff --git a/mods/ts/metrics.yaml b/mods/ts/metrics.yaml index 35fe75f1b1..fe7d275abb 100644 --- a/mods/ts/metrics.yaml +++ b/mods/ts/metrics.yaml @@ -2,3 +2,4 @@ Metrics: ColorPickerActorType: mmch.colorpicker ColorPickerRemapIndices: 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 + TextfieldColorHighlight: 1a1a1a