Browse Source
Instead of being a private implementation detail of TextBox, make it use a presenter control that list linked to the main control using binding. Should give a lot more fexibility.pull/12/head
5 changed files with 480 additions and 450 deletions
@ -0,0 +1,471 @@ |
|||||
|
// -----------------------------------------------------------------------
|
||||
|
// <copyright file="TextPresenter.cs" company="Steven Kirk">
|
||||
|
// Copyright 2013 MIT Licence. See licence.md for more information.
|
||||
|
// </copyright>
|
||||
|
// -----------------------------------------------------------------------
|
||||
|
|
||||
|
namespace Perspex.Controls |
||||
|
{ |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Reactive.Linq; |
||||
|
using Perspex.Controls.Utils; |
||||
|
using Perspex.Input; |
||||
|
using Perspex.Media; |
||||
|
using Perspex.Threading; |
||||
|
|
||||
|
public class TextPresenter : TextBlock |
||||
|
{ |
||||
|
public static readonly PerspexProperty<bool> AcceptsReturnProperty = |
||||
|
TextBox.AcceptsReturnProperty.AddOwner<TextPresenter>(); |
||||
|
|
||||
|
public static readonly PerspexProperty<bool> AcceptsTabProperty = |
||||
|
TextBox.AcceptsTabProperty.AddOwner<TextPresenter>(); |
||||
|
|
||||
|
public static readonly PerspexProperty<int> CaretIndexProperty = |
||||
|
TextBox.CaretIndexProperty.AddOwner<TextPresenter>(); |
||||
|
|
||||
|
public static readonly PerspexProperty<int> SelectionStartProperty = |
||||
|
TextBox.SelectionStartProperty.AddOwner<TextPresenter>(); |
||||
|
|
||||
|
public static readonly PerspexProperty<int> SelectionEndProperty = |
||||
|
TextBox.SelectionEndProperty.AddOwner<TextPresenter>(); |
||||
|
|
||||
|
private DispatcherTimer caretTimer; |
||||
|
|
||||
|
private bool caretBlink; |
||||
|
|
||||
|
static TextPresenter() |
||||
|
{ |
||||
|
FocusableProperty.OverrideDefaultValue(typeof(TextPresenter), true); |
||||
|
} |
||||
|
|
||||
|
public TextPresenter() |
||||
|
{ |
||||
|
this.caretTimer = new DispatcherTimer(); |
||||
|
this.caretTimer.Interval = TimeSpan.FromMilliseconds(500); |
||||
|
this.caretTimer.Tick += this.CaretTimerTick; |
||||
|
|
||||
|
Observable.Merge( |
||||
|
this.GetObservable(SelectionStartProperty), |
||||
|
this.GetObservable(SelectionEndProperty)) |
||||
|
.Subscribe(_ => this.InvalidateFormattedText()); |
||||
|
|
||||
|
this.GetObservable(TextBox.CaretIndexProperty) |
||||
|
.Subscribe(_ => this.CaretMoved()); |
||||
|
} |
||||
|
|
||||
|
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); } |
||||
|
set { this.SetValue(CaretIndexProperty, value); } |
||||
|
} |
||||
|
|
||||
|
public int SelectionStart |
||||
|
{ |
||||
|
get { return this.GetValue(SelectionStartProperty); } |
||||
|
set { this.SetValue(SelectionStartProperty, value); } |
||||
|
} |
||||
|
|
||||
|
public int SelectionEnd |
||||
|
{ |
||||
|
get { return this.GetValue(SelectionEndProperty); } |
||||
|
set { this.SetValue(SelectionEndProperty, value); } |
||||
|
} |
||||
|
|
||||
|
public new FormattedText FormattedText |
||||
|
{ |
||||
|
get { return base.FormattedText; } |
||||
|
} |
||||
|
|
||||
|
public int GetCaretIndex(Point point) |
||||
|
{ |
||||
|
var hit = this.FormattedText.HitTestPoint(point); |
||||
|
return hit.TextPosition + (hit.IsTrailing ? 1 : 0); |
||||
|
} |
||||
|
|
||||
|
public new void GotFocus() |
||||
|
{ |
||||
|
this.caretBlink = true; |
||||
|
this.caretTimer.Start(); |
||||
|
} |
||||
|
|
||||
|
public new void LostFocus() |
||||
|
{ |
||||
|
this.SelectionStart = 0; |
||||
|
this.SelectionEnd = 0; |
||||
|
|
||||
|
this.caretTimer.Stop(); |
||||
|
this.InvalidateVisual(); |
||||
|
} |
||||
|
|
||||
|
public override void Render(IDrawingContext context) |
||||
|
{ |
||||
|
var selectionStart = this.SelectionStart; |
||||
|
var selectionEnd = this.SelectionEnd; |
||||
|
|
||||
|
if (selectionStart != selectionEnd) |
||||
|
{ |
||||
|
var start = Math.Min(selectionStart, selectionEnd); |
||||
|
var length = Math.Max(selectionStart, selectionEnd) - start; |
||||
|
var rects = this.FormattedText.HitTestTextRange(start, length); |
||||
|
|
||||
|
var brush = new SolidColorBrush(0xff086f9e); |
||||
|
|
||||
|
foreach (var rect in rects) |
||||
|
{ |
||||
|
context.FillRectange(brush, rect); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
base.Render(context); |
||||
|
|
||||
|
if (this.IsFocused && selectionStart == selectionEnd) |
||||
|
{ |
||||
|
var charPos = this.FormattedText.HitTestTextPosition(this.CaretIndex); |
||||
|
Brush caretBrush = Brushes.Black; |
||||
|
|
||||
|
if (this.caretBlink) |
||||
|
{ |
||||
|
context.DrawLine(new Pen(caretBrush, 1), charPos.TopLeft, charPos.BottomLeft); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override FormattedText CreateFormattedText() |
||||
|
{ |
||||
|
var result = base.CreateFormattedText(); |
||||
|
var selectionStart = this.SelectionStart; |
||||
|
var selectionEnd = this.SelectionEnd; |
||||
|
var start = Math.Min(selectionStart, selectionEnd); |
||||
|
var length = Math.Max(selectionStart, selectionEnd) - start; |
||||
|
|
||||
|
if (length > 0) |
||||
|
{ |
||||
|
result.SetForegroundBrush(Brushes.White, start, length); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
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.SelectionStart = 0; |
||||
|
this.SelectionEnd = this.Text.Length; |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal void CaretMoved() |
||||
|
{ |
||||
|
this.caretBlink = true; |
||||
|
this.caretTimer.Stop(); |
||||
|
this.caretTimer.Start(); |
||||
|
this.InvalidateVisual(); |
||||
|
} |
||||
|
|
||||
|
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 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<FormattedTextLine> 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; |
||||
|
this.InvalidateVisual(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,127 +0,0 @@ |
|||||
// -----------------------------------------------------------------------
|
|
||||
// <copyright file="TextBoxView.cs" company="Steven Kirk">
|
|
||||
// Copyright 2013 MIT Licence. See licence.md for more information.
|
|
||||
// </copyright>
|
|
||||
// -----------------------------------------------------------------------
|
|
||||
|
|
||||
namespace Perspex.Controls |
|
||||
{ |
|
||||
using System; |
|
||||
using System.Reactive.Linq; |
|
||||
using Perspex.Media; |
|
||||
using Perspex.Threading; |
|
||||
|
|
||||
internal class TextBoxView : TextBlock |
|
||||
{ |
|
||||
private TextBox parent; |
|
||||
|
|
||||
private DispatcherTimer caretTimer; |
|
||||
|
|
||||
private bool caretBlink; |
|
||||
|
|
||||
public TextBoxView(TextBox parent) |
|
||||
{ |
|
||||
this.caretTimer = new DispatcherTimer(); |
|
||||
this.caretTimer.Interval = TimeSpan.FromMilliseconds(500); |
|
||||
this.caretTimer.Tick += this.CaretTimerTick; |
|
||||
this.parent = parent; |
|
||||
this[!TextProperty] = parent[!TextProperty]; |
|
||||
this[!TextWrappingProperty] = parent[!TextWrappingProperty]; |
|
||||
|
|
||||
Observable.Merge( |
|
||||
this.parent.GetObservable(TextBox.SelectionStartProperty), |
|
||||
this.parent.GetObservable(TextBox.SelectionEndProperty)) |
|
||||
.Subscribe(_ => this.InvalidateFormattedText()); |
|
||||
|
|
||||
parent.GetObservable(TextBox.CaretIndexProperty).Subscribe(_ => this.CaretMoved()); |
|
||||
} |
|
||||
|
|
||||
public new FormattedText FormattedText |
|
||||
{ |
|
||||
get { return base.FormattedText; } |
|
||||
} |
|
||||
|
|
||||
public int GetCaretIndex(Point point) |
|
||||
{ |
|
||||
var hit = this.FormattedText.HitTestPoint(point); |
|
||||
return hit.TextPosition + (hit.IsTrailing ? 1 : 0); |
|
||||
} |
|
||||
|
|
||||
public new void GotFocus() |
|
||||
{ |
|
||||
this.caretBlink = true; |
|
||||
this.caretTimer.Start(); |
|
||||
} |
|
||||
|
|
||||
public new void LostFocus() |
|
||||
{ |
|
||||
this.parent.SelectionStart = 0; |
|
||||
this.parent.SelectionEnd = 0; |
|
||||
|
|
||||
this.caretTimer.Stop(); |
|
||||
this.InvalidateVisual(); |
|
||||
} |
|
||||
|
|
||||
public override void Render(IDrawingContext context) |
|
||||
{ |
|
||||
var selectionStart = this.parent.SelectionStart; |
|
||||
var selectionEnd = this.parent.SelectionEnd; |
|
||||
|
|
||||
if (selectionStart != selectionEnd) |
|
||||
{ |
|
||||
var start = Math.Min(selectionStart, selectionEnd); |
|
||||
var length = Math.Max(selectionStart, selectionEnd) - start; |
|
||||
var rects = this.FormattedText.HitTestTextRange(start, length); |
|
||||
|
|
||||
var brush = new SolidColorBrush(0xff086f9e); |
|
||||
|
|
||||
foreach (var rect in rects) |
|
||||
{ |
|
||||
context.FillRectange(brush, rect); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
base.Render(context); |
|
||||
|
|
||||
if (this.parent.IsFocused && selectionStart == selectionEnd) |
|
||||
{ |
|
||||
var charPos = this.FormattedText.HitTestTextPosition(this.parent.CaretIndex); |
|
||||
Brush caretBrush = Brushes.Black; |
|
||||
|
|
||||
if (this.caretBlink) |
|
||||
{ |
|
||||
context.DrawLine(new Pen(caretBrush, 1), charPos.TopLeft, charPos.BottomLeft); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected override FormattedText CreateFormattedText() |
|
||||
{ |
|
||||
var result = base.CreateFormattedText(); |
|
||||
var selectionStart = this.parent.SelectionStart; |
|
||||
var selectionEnd = this.parent.SelectionEnd; |
|
||||
var start = Math.Min(selectionStart, selectionEnd); |
|
||||
var length = Math.Max(selectionStart, selectionEnd) - start; |
|
||||
|
|
||||
if (length > 0) |
|
||||
{ |
|
||||
result.SetForegroundBrush(Brushes.White, start, length); |
|
||||
} |
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
internal void CaretMoved() |
|
||||
{ |
|
||||
this.caretBlink = true; |
|
||||
this.caretTimer.Stop(); |
|
||||
this.caretTimer.Start(); |
|
||||
this.InvalidateVisual(); |
|
||||
} |
|
||||
|
|
||||
private void CaretTimerTick(object sender, EventArgs e) |
|
||||
{ |
|
||||
this.caretBlink = !this.caretBlink; |
|
||||
this.InvalidateVisual(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue