diff --git a/.travis.yml b/.travis.yml index af7d5dbebf..9693d21b7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ os: - linux - osx mono: - - latest + - 4.4.2 script: - ./build.sh --target "Travis" --platform "Mono" --configuration "Release" notifications: diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index f9c55e1f3b..d3cf4e5509 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -219,7 +219,7 @@ namespace Avalonia.Controls.Presenters { var text = Text; - if (!string.IsNullOrWhiteSpace(text)) + if (!string.IsNullOrEmpty(text)) { return base.MeasureOverride(availableSize); } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index ed73472c49..6fd48ceed6 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -148,7 +148,8 @@ namespace Avalonia.Controls { value = CoerceCaretIndex(value); SetAndRaise(CaretIndexProperty, ref _caretIndex, value); - if (_undoRedoHelper.IsLastState && _undoRedoHelper.LastState.Text == Text) + UndoRedoState state; + if (_undoRedoHelper.TryGetLastState(out state) && state.Text == Text) _undoRedoHelper.UpdateLastState(); } } @@ -379,8 +380,20 @@ namespace Avalonia.Controls if (!DeleteSelection() && CaretIndex > 0) { - SetTextInternal(text.Substring(0, caretIndex - 1) + text.Substring(caretIndex)); - --CaretIndex; + var removedCharacters = 1; + // handle deleting /r/n + // you don't ever want to leave a dangling /r around. So, if deleting /n, check to see if + // a /r should also be deleted. + if (CaretIndex > 1 && + text[CaretIndex - 1] == '\n' && + text[CaretIndex - 2] == '\r') + { + removedCharacters = 2; + } + + SetTextInternal(text.Substring(0, caretIndex - removedCharacters) + text.Substring(caretIndex)); + CaretIndex -= removedCharacters; + SelectionStart = SelectionEnd = CaretIndex; } break; @@ -393,7 +406,18 @@ namespace Avalonia.Controls if (!DeleteSelection() && caretIndex < text.Length) { - SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + 1)); + var removedCharacters = 1; + // handle deleting /r/n + // you don't ever want to leave a dangling /r around. So, if deleting /n, check to see if + // a /r should also be deleted. + if (CaretIndex < text.Length - 1 && + text[caretIndex + 1] == '\n' && + text[caretIndex] == '\r') + { + removedCharacters = 2; + } + + SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + removedCharacters)); } break; @@ -695,7 +719,7 @@ namespace Avalonia.Controls private void SelectAll() { SelectionStart = 0; - SelectionEnd = Text.Length; + SelectionEnd = Text?.Length ?? 0; } private bool DeleteSelection() diff --git a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs index 2301198a01..71ba3793d1 100644 --- a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs +++ b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs @@ -8,20 +8,19 @@ using Avalonia.Utilities; namespace Avalonia.Controls.Utils { - class UndoRedoHelper : WeakTimer.IWeakTimerSubscriber where TState : IEquatable + class UndoRedoHelper : WeakTimer.IWeakTimerSubscriber where TState : struct, IEquatable { private readonly IUndoRedoHost _host; public interface IUndoRedoHost { - TState UndoRedoState { get; set; } + TState UndoRedoState { get; set; } } - + private readonly LinkedList _states = new LinkedList(); - [NotNull] private LinkedListNode _currentNode; public int Limit { get; set; } = 10; @@ -29,24 +28,31 @@ namespace Avalonia.Controls.Utils public UndoRedoHelper(IUndoRedoHost host) { _host = host; - _states.AddFirst(_host.UndoRedoState); - _currentNode = _states.First; - WeakTimer.StartWeakTimer(this, new TimeSpan(0, 0, 1)); - + WeakTimer.StartWeakTimer(this, TimeSpan.FromSeconds(1)); } public void Undo() { - if (_currentNode?.Previous != null) - { - _currentNode = _currentNode.Previous; - } - - _host.UndoRedoState = _currentNode.Value; + if (_currentNode?.Previous != null) + { + _currentNode = _currentNode.Previous; + _host.UndoRedoState = _currentNode.Value; + } } - public bool IsLastState => _currentNode.Next == null; + public bool IsLastState => _currentNode != null && _currentNode.Next == null; + + public bool TryGetLastState(out TState _state) + { + _state = default(TState); + if (!IsLastState) + return false; + _state = _currentNode.Value; + return true; + } + + public bool HasState => _currentNode != null; public void UpdateLastState(TState state) { _states.Last.Value = state; @@ -57,34 +63,31 @@ namespace Avalonia.Controls.Utils _states.Last.Value = _host.UndoRedoState; } - public TState LastState => _currentNode.Value; - public void DiscardRedo() { - //Linked list sucks, so we are doing this - while (_currentNode.Next != null) + while (_currentNode?.Next != null) _states.Remove(_currentNode.Next); } public void Redo() - { - if (_currentNode?.Next != null) { - _currentNode = _currentNode.Next; - } - - _host.UndoRedoState = _currentNode.Value; + { + if (_currentNode?.Next != null) + { + _currentNode = _currentNode.Next; + _host.UndoRedoState = _currentNode.Value; + } } public void Snapshot() { var current = _host.UndoRedoState; - if (!_currentNode.Value.Equals(current)) + if (_currentNode == null || !_currentNode.Value.Equals(current)) { - if(_currentNode.Next != null) + if (_currentNode?.Next != null) DiscardRedo(); _states.AddLast(current); _currentNode = _states.Last; - if(_states.Count > Limit) + if (_states.Count > Limit) _states.RemoveFirst(); } } diff --git a/src/Avalonia.SceneGraph/Media/FormattedText.cs b/src/Avalonia.SceneGraph/Media/FormattedText.cs index 1169a56343..f6aeaf5f5f 100644 --- a/src/Avalonia.SceneGraph/Media/FormattedText.cs +++ b/src/Avalonia.SceneGraph/Media/FormattedText.cs @@ -33,7 +33,16 @@ namespace Avalonia.Media { Contract.Requires(text != null); Contract.Requires(fontFamilyName != null); - Contract.Requires(fontSize > 0); + + if (fontSize <= 0) + { + throw new ArgumentException("FontSize must be greater than 0"); + } + + if (fontWeight <= 0) + { + throw new ArgumentException("FontWeight must be greater than 0"); + } Text = text; FontFamilyName = fontFamilyName; diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 5a93538757..e24b6053b0 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -28,7 +28,9 @@ namespace Avalonia.Skia //Paint.TextEncoding = SKTextEncoding.Utf8; _paint.TextEncoding = SKTextEncoding.Utf16; _paint.IsStroke = false; - _paint.IsAntialias = true; + _paint.IsAntialias = true; + _paint.LcdRenderText = true; + _paint.SubpixelText = true; _paint.Typeface = typeface; _paint.TextSize = (float)fontSize; _paint.TextAlign = textAlignment.ToSKTextAlign(); diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 168d9786c2..851657ab6c 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -41,6 +41,58 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Press_Ctrl_A_Select_All_Text() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "1234" + }; + + RaiseKeyEvent(target, Key.A, InputModifiers.Control); + + Assert.Equal(0, target.SelectionStart); + Assert.Equal(4, target.SelectionEnd); + } + } + + [Fact] + public void Press_Ctrl_A_Select_All_Null_Text() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate() + }; + + RaiseKeyEvent(target, Key.A, InputModifiers.Control); + + Assert.Equal(0, target.SelectionStart); + Assert.Equal(0, target.SelectionEnd); + } + } + + [Fact] + public void Press_Ctrl_Z_Will_Not_Modify_Text() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "1234" + }; + + RaiseKeyEvent(target, Key.Z, InputModifiers.Control); + + Assert.Equal("1234", target.Text); + } + } + [Fact] public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int() { diff --git a/tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj b/tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj index 34f12d16ed..9d6f861ddf 100644 --- a/tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj +++ b/tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj @@ -77,6 +77,7 @@ + diff --git a/tests/Avalonia.SceneGraph.UnitTests/Media/FormattedTextTests.cs b/tests/Avalonia.SceneGraph.UnitTests/Media/FormattedTextTests.cs new file mode 100644 index 0000000000..d36e23a6d8 --- /dev/null +++ b/tests/Avalonia.SceneGraph.UnitTests/Media/FormattedTextTests.cs @@ -0,0 +1,28 @@ +using System; +using Avalonia.Media; +using Xunit; + +namespace Avalonia.SceneGraph.UnitTests.Media +{ + public class FormattedTextTests + { + [Fact] + public void Exception_Should_Be_Thrown_If_FontSize_0() + { + Assert.Throws(() => new FormattedText( + "foo", + "Ariel", + 0)); + } + + [Fact] + public void Exception_Should_Be_Thrown_If_FontWeight_0() + { + Assert.Throws(() => new FormattedText( + "foo", + "Ariel", + 12, + fontWeight: 0)); + } + } +}