From 4299e7e0461592e78e832cc4e59d8d4d4921cd2c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Apr 2015 00:54:24 -0400 Subject: [PATCH] Make TextBox handle text input. Instead of TextPresenter - this fixes the problem of focus that we were working around in TextBox.OnGotFocus before. --- Perspex.Controls/Presenters/TextPresenter.cs | 367 +------------------ Perspex.Controls/TextBlock.cs | 14 +- Perspex.Controls/TextBox.cs | 335 ++++++++++++++++- Perspex.Input/MouseDevice.cs | 14 +- Perspex.Interactivity/RoutedEvent.cs | 5 +- Perspex.Themes.Default/TextBoxStyle.cs | 2 - 6 files changed, 365 insertions(+), 372 deletions(-) diff --git a/Perspex.Controls/Presenters/TextPresenter.cs b/Perspex.Controls/Presenters/TextPresenter.cs index a638b5207a..a35c81052f 100644 --- a/Perspex.Controls/Presenters/TextPresenter.cs +++ b/Perspex.Controls/Presenters/TextPresenter.cs @@ -7,25 +7,14 @@ namespace Perspex.Controls.Presenters { using System; - using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; - using Perspex.Controls.Primitives; - using Perspex.Controls.Utils; - using Perspex.Input; - using Perspex.Interactivity; using Perspex.Media; using Perspex.Threading; using Perspex.VisualTree; public class TextPresenter : TextBlock, IPresenter { - public static readonly PerspexProperty AcceptsReturnProperty = - TextBox.AcceptsReturnProperty.AddOwner(); - - public static readonly PerspexProperty AcceptsTabProperty = - TextBox.AcceptsTabProperty.AddOwner(); - public static readonly PerspexProperty CaretIndexProperty = TextBox.CaretIndexProperty.AddOwner(); @@ -41,11 +30,6 @@ namespace Perspex.Controls.Presenters private IObservable canScrollHorizontally; - static TextPresenter() - { - FocusableProperty.OverrideDefaultValue(typeof(TextPresenter), true); - } - public TextPresenter() { this.caretTimer = new DispatcherTimer(); @@ -64,18 +48,6 @@ namespace Perspex.Controls.Presenters .Subscribe(this.CaretIndexChanged); } - public bool AcceptsReturn - { - get { return this.GetValue(AcceptsReturnProperty); } - set { this.SetValue(AcceptsReturnProperty, value); } - } - - public bool AcceptsTab - { - get { return this.GetValue(AcceptsTabProperty); } - set { this.SetValue(AcceptsTabProperty, value); } - } - public int CaretIndex { get { return this.GetValue(CaretIndexProperty); } @@ -121,7 +93,7 @@ namespace Perspex.Controls.Presenters base.Render(context); - if (this.IsFocused && selectionStart == selectionEnd) + if (selectionStart == selectionEnd) { var charPos = this.FormattedText.HitTestTextPosition(this.CaretIndex); Brush caretBrush = Brushes.Black; @@ -133,6 +105,20 @@ namespace Perspex.Controls.Presenters } } + public void ShowCaret() + { + this.caretBlink = true; + this.caretTimer.Start(); + this.InvalidateVisual(); + } + + public void HideCaret() + { + this.caretBlink = false; + this.caretTimer.Stop(); + this.InvalidateVisual(); + } + internal void CaretIndexChanged(int caretIndex) { if (this.GetVisualParent() != null) @@ -186,329 +172,6 @@ namespace Perspex.Controls.Presenters } } - protected override void OnGotFocus(RoutedEventArgs e) - { - base.OnGotFocus(e); - - this.caretBlink = true; - this.caretTimer.Start(); - this.InvalidateVisual(); - } - - protected override void OnLostFocus(RoutedEventArgs e) - { - base.OnLostFocus(e); - - this.SelectionStart = 0; - this.SelectionEnd = 0; - this.caretTimer.Stop(); - this.InvalidateVisual(); - } - - protected override void OnKeyDown(KeyEventArgs e) - { - string text = this.Text ?? string.Empty; - int caretIndex = this.CaretIndex; - bool movement = false; - bool textEntered = false; - var modifiers = e.Device.Modifiers; - - switch (e.Key) - { - case Key.A: - if (modifiers == ModifierKeys.Control) - { - this.SelectAll(); - } - else - { - goto default; - } - - break; - - case Key.Left: - this.MoveHorizontal(-1, modifiers); - movement = true; - break; - - case Key.Right: - this.MoveHorizontal(1, modifiers); - movement = true; - break; - - case Key.Up: - this.MoveVertical(-1, modifiers); - movement = true; - break; - - case Key.Down: - this.MoveVertical(1, modifiers); - movement = true; - break; - - case Key.Home: - this.MoveHome(modifiers); - movement = true; - break; - - case Key.End: - this.MoveEnd(modifiers); - movement = true; - break; - - case Key.Back: - if (!this.DeleteSelection() && this.CaretIndex > 0) - { - this.Text = text.Substring(0, caretIndex - 1) + text.Substring(caretIndex); - --this.CaretIndex; - } - - break; - - case Key.Delete: - if (!this.DeleteSelection() && caretIndex < text.Length) - { - this.Text = text.Substring(0, caretIndex) + text.Substring(caretIndex + 1); - } - - break; - - case Key.Enter: - if (this.AcceptsReturn) - { - goto default; - } - - break; - - case Key.Tab: - if (this.AcceptsTab) - { - goto default; - } - - break; - - default: - if (!string.IsNullOrEmpty(e.Text)) - { - this.DeleteSelection(); - caretIndex = this.CaretIndex; - text = this.Text ?? string.Empty; - this.Text = text.Substring(0, caretIndex) + e.Text + text.Substring(caretIndex); - ++this.CaretIndex; - textEntered = true; - } - - break; - } - - if (movement && ((modifiers & ModifierKeys.Shift) != 0)) - { - this.SelectionEnd = this.CaretIndex; - } - else if (movement || textEntered) - { - this.SelectionStart = this.SelectionEnd = this.CaretIndex; - } - - e.Handled = true; - } - - protected override void OnPointerPressed(PointerPressEventArgs e) - { - var point = e.GetPosition(this); - var index = this.CaretIndex = this.GetCaretIndex(point); - var text = this.Text; - - switch (e.ClickCount) - { - case 1: - this.SelectionStart = this.SelectionEnd = index; - break; - case 2: - if (!StringUtils.IsStartOfWord(text, index)) - { - this.SelectionStart = StringUtils.PreviousWord(text, index, false); - } - - this.SelectionEnd = StringUtils.NextWord(text, index, false); - break; - case 3: - this.SelectionStart = 0; - this.SelectionEnd = text.Length; - break; - } - - e.Device.Capture(this); - } - - protected override void OnPointerMoved(PointerEventArgs e) - { - if (e.Device.Captured == this) - { - var point = e.GetPosition(this); - this.CaretIndex = this.SelectionEnd = this.GetCaretIndex(point); - } - } - - protected override void OnPointerReleased(PointerEventArgs e) - { - if (e.Device.Captured == this) - { - e.Device.Capture(null); - } - } - - private void MoveHorizontal(int count, ModifierKeys modifiers) - { - var text = this.Text ?? string.Empty; - var caretIndex = this.CaretIndex; - - if ((modifiers & ModifierKeys.Control) != 0) - { - if (count > 0) - { - count = StringUtils.NextWord(text, caretIndex, false) - caretIndex; - } - else - { - count = StringUtils.PreviousWord(text, caretIndex, false) - caretIndex; - } - } - - this.CaretIndex = caretIndex += count; - } - - private void MoveVertical(int count, ModifierKeys modifiers) - { - var formattedText = this.FormattedText; - var lines = formattedText.GetLines().ToList(); - var caretIndex = this.CaretIndex; - var lineIndex = this.GetLine(caretIndex, lines) + count; - - if (lineIndex >= 0 && lineIndex < lines.Count) - { - var line = lines[lineIndex]; - var rect = formattedText.HitTestTextPosition(caretIndex); - var y = count < 0 ? rect.Y : rect.Bottom; - var point = new Point(rect.X, y + (count * (line.Height / 2))); - var hit = formattedText.HitTestPoint(point); - this.CaretIndex = caretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0); - } - } - - private void MoveHome(ModifierKeys modifiers) - { - var text = this.Text ?? string.Empty; - var caretIndex = this.CaretIndex; - - if ((modifiers & ModifierKeys.Control) != 0) - { - caretIndex = 0; - } - else - { - var lines = this.FormattedText.GetLines(); - var pos = 0; - - foreach (var line in lines) - { - if (pos + line.Length > caretIndex || pos + line.Length == text.Length) - { - break; - } - - pos += line.Length; - } - - caretIndex = pos; - } - - this.CaretIndex = caretIndex; - } - - private void MoveEnd(ModifierKeys modifiers) - { - var text = this.Text ?? string.Empty; - var caretIndex = this.CaretIndex; - - if ((modifiers & ModifierKeys.Control) != 0) - { - caretIndex = text.Length; - } - else - { - var lines = this.FormattedText.GetLines(); - var pos = 0; - - foreach (var line in lines) - { - pos += line.Length; - - if (pos > caretIndex) - { - if (pos < text.Length) - { - --pos; - } - - break; - } - } - - caretIndex = pos; - } - - this.CaretIndex = caretIndex; - } - - private void SelectAll() - { - this.SelectionStart = 0; - this.SelectionEnd = this.Text.Length; - } - - private bool DeleteSelection() - { - var selectionStart = this.SelectionStart; - var selectionEnd = this.SelectionEnd; - - if (selectionStart != selectionEnd) - { - var start = Math.Min(selectionStart, selectionEnd); - var end = Math.Max(selectionStart, selectionEnd); - var text = this.Text; - this.Text = text.Substring(0, start) + text.Substring(end); - this.SelectionStart = this.SelectionEnd = this.CaretIndex = start; - return true; - } - else - { - return false; - } - } - - private int GetLine(int caretIndex, IList lines) - { - int pos = 0; - int i; - - for (i = 0; i < lines.Count; ++i) - { - var line = lines[i]; - pos += line.Length; - - if (pos > caretIndex) - { - break; - } - } - - return i; - } - private void CaretTimerTick(object sender, EventArgs e) { this.caretBlink = !this.caretBlink; diff --git a/Perspex.Controls/TextBlock.cs b/Perspex.Controls/TextBlock.cs index d7221348ab..52d78d9b46 100644 --- a/Perspex.Controls/TextBlock.cs +++ b/Perspex.Controls/TextBlock.cs @@ -95,13 +95,7 @@ namespace Perspex.Controls set { this.SetValue(ForegroundProperty, value); } } - public TextWrapping TextWrapping - { - get { return this.GetValue(TextWrappingProperty); } - set { this.SetValue(TextWrappingProperty, value); } - } - - protected FormattedText FormattedText + public FormattedText FormattedText { get { @@ -114,6 +108,12 @@ namespace Perspex.Controls } } + public TextWrapping TextWrapping + { + get { return this.GetValue(TextWrappingProperty); } + set { this.SetValue(TextWrappingProperty, value); } + } + public override void Render(IDrawingContext context) { Brush background = this.Background; diff --git a/Perspex.Controls/TextBox.cs b/Perspex.Controls/TextBox.cs index e0b8f864ce..25b50879b6 100644 --- a/Perspex.Controls/TextBox.cs +++ b/Perspex.Controls/TextBox.cs @@ -7,12 +7,16 @@ namespace Perspex.Controls { using System; + using System.Collections.Generic; + using System.Linq; using System.Reactive.Linq; using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; + using Perspex.Controls.Utils; using Perspex.Input; using Perspex.Interactivity; + using Perspex.Media; public class TextBox : TemplatedControl { @@ -105,6 +109,189 @@ namespace Perspex.Controls set { this.SetValue(TextWrappingProperty, value); } } + protected override void OnTemplateApplied() + { + this.presenter = this.GetTemplateChild("textPresenter"); + } + + protected override void OnGotFocus(RoutedEventArgs e) + { + base.OnGotFocus(e); + this.presenter.ShowCaret(); + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + this.SelectionStart = 0; + this.SelectionEnd = 0; + this.presenter.HideCaret(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + string text = this.Text ?? string.Empty; + int caretIndex = this.CaretIndex; + bool movement = false; + bool textEntered = false; + var modifiers = e.Device.Modifiers; + + switch (e.Key) + { + case Key.A: + if (modifiers == ModifierKeys.Control) + { + this.SelectAll(); + } + else + { + goto default; + } + + break; + + case Key.Left: + this.MoveHorizontal(-1, modifiers); + movement = true; + break; + + case Key.Right: + this.MoveHorizontal(1, modifiers); + movement = true; + break; + + case Key.Up: + this.MoveVertical(-1, modifiers); + movement = true; + break; + + case Key.Down: + this.MoveVertical(1, modifiers); + movement = true; + break; + + case Key.Home: + this.MoveHome(modifiers); + movement = true; + break; + + case Key.End: + this.MoveEnd(modifiers); + movement = true; + break; + + case Key.Back: + if (!this.DeleteSelection() && this.CaretIndex > 0) + { + this.Text = text.Substring(0, caretIndex - 1) + text.Substring(caretIndex); + --this.CaretIndex; + } + + break; + + case Key.Delete: + if (!this.DeleteSelection() && caretIndex < text.Length) + { + this.Text = text.Substring(0, caretIndex) + text.Substring(caretIndex + 1); + } + + break; + + case Key.Enter: + if (this.AcceptsReturn) + { + goto default; + } + + break; + + case Key.Tab: + if (this.AcceptsTab) + { + goto default; + } + else + { + base.OnKeyDown(e); + } + + break; + + default: + if (!string.IsNullOrEmpty(e.Text)) + { + this.DeleteSelection(); + caretIndex = this.CaretIndex; + text = this.Text ?? string.Empty; + this.Text = text.Substring(0, caretIndex) + e.Text + text.Substring(caretIndex); + ++this.CaretIndex; + textEntered = true; + } + + break; + } + + if (movement && ((modifiers & ModifierKeys.Shift) != 0)) + { + this.SelectionEnd = this.CaretIndex; + } + else if (movement || textEntered) + { + this.SelectionStart = this.SelectionEnd = this.CaretIndex; + } + + e.Handled = true; + } + + protected override void OnPointerPressed(PointerPressEventArgs e) + { + if (e.Source == this.presenter) + { + var point = e.GetPosition(this.presenter); + var index = this.CaretIndex = this.presenter.GetCaretIndex(point); + var text = this.Text; + + switch (e.ClickCount) + { + case 1: + this.SelectionStart = this.SelectionEnd = index; + break; + case 2: + if (!StringUtils.IsStartOfWord(text, index)) + { + this.SelectionStart = StringUtils.PreviousWord(text, index, false); + } + + this.SelectionEnd = StringUtils.NextWord(text, index, false); + break; + case 3: + this.SelectionStart = 0; + this.SelectionEnd = text.Length; + break; + } + + e.Device.Capture(this.presenter); + e.Handled = true; + } + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + if (e.Device.Captured == this.presenter) + { + var point = e.GetPosition(this.presenter); + this.CaretIndex = this.SelectionEnd = this.presenter.GetCaretIndex(point); + } + } + + protected override void OnPointerReleased(PointerEventArgs e) + { + if (e.Device.Captured == this.presenter) + { + e.Device.Capture(null); + } + } + private static int CoerceCaretIndex(PerspexObject o, int value) { var text = o.GetValue(TextProperty); @@ -112,16 +299,152 @@ namespace Perspex.Controls return Math.Max(0, Math.Min(length, value)); } - protected override void OnTemplateApplied() + private void MoveHorizontal(int count, ModifierKeys modifiers) { - this.presenter = this.GetTemplateChild("textPresenter"); + var text = this.Text ?? string.Empty; + var caretIndex = this.CaretIndex; + + if ((modifiers & ModifierKeys.Control) != 0) + { + if (count > 0) + { + count = StringUtils.NextWord(text, caretIndex, false) - caretIndex; + } + else + { + count = StringUtils.PreviousWord(text, caretIndex, false) - caretIndex; + } + } + + this.CaretIndex = caretIndex += count; } - protected override void OnGotFocus(RoutedEventArgs e) + private void MoveVertical(int count, ModifierKeys modifiers) { - // TODO: There needs to be a better way of setting focus to a templated child. - base.OnGotFocus(e); - FocusManager.Instance.Focus(this.presenter); + var formattedText = this.presenter.FormattedText; + var lines = formattedText.GetLines().ToList(); + var caretIndex = this.CaretIndex; + var lineIndex = this.GetLine(caretIndex, lines) + count; + + if (lineIndex >= 0 && lineIndex < lines.Count) + { + var line = lines[lineIndex]; + var rect = formattedText.HitTestTextPosition(caretIndex); + var y = count < 0 ? rect.Y : rect.Bottom; + var point = new Point(rect.X, y + (count * (line.Height / 2))); + var hit = formattedText.HitTestPoint(point); + this.CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0); + } + } + + private void MoveHome(ModifierKeys modifiers) + { + var text = this.Text ?? string.Empty; + var caretIndex = this.CaretIndex; + + if ((modifiers & ModifierKeys.Control) != 0) + { + caretIndex = 0; + } + else + { + var lines = this.presenter.FormattedText.GetLines(); + var pos = 0; + + foreach (var line in lines) + { + if (pos + line.Length > caretIndex || pos + line.Length == text.Length) + { + break; + } + + pos += line.Length; + } + + caretIndex = pos; + } + + this.CaretIndex = caretIndex; + } + + private void MoveEnd(ModifierKeys modifiers) + { + var text = this.Text ?? string.Empty; + var caretIndex = this.CaretIndex; + + if ((modifiers & ModifierKeys.Control) != 0) + { + caretIndex = text.Length; + } + else + { + var lines = this.presenter.FormattedText.GetLines(); + var pos = 0; + + foreach (var line in lines) + { + pos += line.Length; + + if (pos > caretIndex) + { + if (pos < text.Length) + { + --pos; + } + + break; + } + } + + caretIndex = pos; + } + + this.CaretIndex = caretIndex; + } + + private void SelectAll() + { + this.SelectionStart = 0; + this.SelectionEnd = this.Text.Length; + } + + private bool DeleteSelection() + { + var selectionStart = this.SelectionStart; + var selectionEnd = this.SelectionEnd; + + if (selectionStart != selectionEnd) + { + var start = Math.Min(selectionStart, selectionEnd); + var end = Math.Max(selectionStart, selectionEnd); + var text = this.Text; + this.Text = text.Substring(0, start) + text.Substring(end); + this.SelectionStart = this.SelectionEnd = this.CaretIndex = start; + return true; + } + else + { + return false; + } + } + + private int GetLine(int caretIndex, IList lines) + { + int pos = 0; + int i; + + for (i = 0; i < lines.Count; ++i) + { + var line = lines[i]; + pos += line.Length; + + if (pos > caretIndex) + { + break; + } + } + + return i; } } } diff --git a/Perspex.Input/MouseDevice.cs b/Perspex.Input/MouseDevice.cs index 83b34d126c..0b4b53f9e7 100644 --- a/Perspex.Input/MouseDevice.cs +++ b/Perspex.Input/MouseDevice.cs @@ -228,8 +228,18 @@ namespace Perspex.Input private IInputElement GetFocusable(IVisual hit) { - return this.Captured as IInputElement ?? - hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(x => x.Focusable); + var inputFocus = this.Captured as IInputElement; + + if (inputFocus != null && inputFocus.Focusable) + { + return inputFocus; + } + else + { + return hit.GetSelfAndVisualAncestors() + .OfType() + .FirstOrDefault(x => x.Focusable); + } } private IInteractive GetSource(IVisual hit) diff --git a/Perspex.Interactivity/RoutedEvent.cs b/Perspex.Interactivity/RoutedEvent.cs index 2a7c04cdd5..5a523593f8 100644 --- a/Perspex.Interactivity/RoutedEvent.cs +++ b/Perspex.Interactivity/RoutedEvent.cs @@ -41,8 +41,6 @@ namespace Perspex.Interactivity this.RoutingStrategies = routingStrategies; } - public event EventHandler Raised; - public Type EventArgsType { get; @@ -104,7 +102,8 @@ namespace Perspex.Interactivity foreach (var sub in this.subscriptions) { if (sub.TargetType.GetTypeInfo().IsAssignableFrom(sender.GetType().GetTypeInfo()) && - (e.Route == RoutingStrategies.Direct) || (e.Route & sub.Routes) != 0) + ((e.Route == RoutingStrategies.Direct) || (e.Route & sub.Routes) != 0) && + (!e.Handled || sub.AlsoIfHandled)) { sub.Handler.DynamicInvoke(sender, e); } diff --git a/Perspex.Themes.Default/TextBoxStyle.cs b/Perspex.Themes.Default/TextBoxStyle.cs index ffbabace98..27101a32be 100644 --- a/Perspex.Themes.Default/TextBoxStyle.cs +++ b/Perspex.Themes.Default/TextBoxStyle.cs @@ -56,8 +56,6 @@ namespace Perspex.Themes.Default Content = new TextPresenter { Id = "textPresenter", - [~TextPresenter.AcceptsReturnProperty] = control[~TextBox.AcceptsReturnProperty], - [~TextPresenter.AcceptsTabProperty] = control[~TextBox.AcceptsTabProperty], [~TextPresenter.CaretIndexProperty] = control[~TextBox.CaretIndexProperty], [~TextPresenter.SelectionStartProperty] = control[~TextBox.SelectionStartProperty], [~TextPresenter.SelectionEndProperty] = control[~TextBox.SelectionEndProperty],