From ac11e7b3a8f7d0d5ed028ec9c8ce61c1b09976af Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 9 Sep 2018 16:44:31 -0500 Subject: [PATCH 1/9] Detach all styles when a style is removed from a control. --- src/Avalonia.Styling/Styling/IStyle.cs | 2 + src/Avalonia.Styling/Styling/Style.cs | 52 ++++++++++++++----- src/Avalonia.Styling/Styling/Styles.cs | 8 +++ src/Avalonia.Styling/Styling/packages.config | 9 ---- .../Styling/StyleInclude.cs | 10 +++- .../Avalonia.Styling.UnitTests/StyleTests.cs | 25 +++++++++ 6 files changed, 84 insertions(+), 22 deletions(-) delete mode 100644 src/Avalonia.Styling/Styling/packages.config diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Styling/Styling/IStyle.cs index 5f12763825..3cf8491ae3 100644 --- a/src/Avalonia.Styling/Styling/IStyle.cs +++ b/src/Avalonia.Styling/Styling/IStyle.cs @@ -18,5 +18,7 @@ namespace Avalonia.Styling /// The control that contains this style. May be null. /// void Attach(IStyleable control, IStyleHost container); + + void Detach(); } } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 399be5470d..0d079e97da 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Animation; using Avalonia.Controls; @@ -15,9 +16,12 @@ namespace Avalonia.Styling /// public class Style : AvaloniaObject, IStyle, ISetStyleParent { - private static Dictionary> _applied = - new Dictionary>(); + private static Dictionary _applied = + new Dictionary(); private IResourceNode _parent; + + private CompositeDisposable _subscriptions; + private IResourceDictionary _resources; private IList _animations; @@ -88,6 +92,14 @@ namespace Avalonia.Styling } } + private CompositeDisposable Subscriptions + { + get + { + return _subscriptions ?? (_subscriptions = new CompositeDisposable(2)); + } + } + /// IResourceNode IResourceNode.ResourceParent => _parent; @@ -109,7 +121,9 @@ namespace Avalonia.Styling if (match.ImmediateResult != false) { - var subs = GetSubscriptions(control); + var controlSubscriptions = GetSubscriptions(control); + + var subs = new CompositeDisposable(Setters.Count + Animations.Count); foreach (var animation in Animations) { @@ -129,17 +143,25 @@ namespace Avalonia.Styling var sub = setter.Apply(this, control, match.ObservableResult); subs.Add(sub); } + + controlSubscriptions.Add(subs); + Subscriptions.Add(subs); } } else if (control == container) { - var subs = GetSubscriptions(control); + var controlSubscriptions = GetSubscriptions(control); + + var subs = new CompositeDisposable(Setters.Count); foreach (var setter in Setters) { var sub = setter.Apply(this, control, null); subs.Add(sub); } + + controlSubscriptions.Add(subs); + Subscriptions.Add(subs); } } @@ -180,16 +202,25 @@ namespace Avalonia.Styling throw new InvalidOperationException("The Style already has a parent."); } + if (parent == null) + { + Detach(); + } + _parent = parent; } - private static List GetSubscriptions(IStyleable control) + public void Detach() { - List subscriptions; + _subscriptions?.Dispose(); + _subscriptions = null; + } - if (!_applied.TryGetValue(control, out subscriptions)) + private static CompositeDisposable GetSubscriptions(IStyleable control) + { + if (!_applied.TryGetValue(control, out var subscriptions)) { - subscriptions = new List(2); + subscriptions = new CompositeDisposable(2); subscriptions.Add(control.StyleDetach.Subscribe(ControlDetach)); _applied.Add(control, subscriptions); } @@ -206,10 +237,7 @@ namespace Avalonia.Styling { var subscriptions = _applied[control]; - foreach (var subscription in subscriptions) - { - subscription.Dispose(); - } + subscriptions.Dispose(); _applied.Remove(control); } diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 714e7f6def..22ed8bb241 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -105,6 +105,14 @@ namespace Avalonia.Styling } } + public void Detach() + { + foreach (IStyle style in this) + { + style.Detach(); + } + } + /// public bool TryGetResource(string key, out object value) { diff --git a/src/Avalonia.Styling/Styling/packages.config b/src/Avalonia.Styling/Styling/packages.config deleted file mode 100644 index aa2dbd982f..0000000000 --- a/src/Avalonia.Styling/Styling/packages.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 3580b4fcb5..fc2656f236 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -70,6 +70,14 @@ namespace Avalonia.Markup.Xaml.Styling } } + public void Detach() + { + if (Source != null) + { + Loaded.Detach(); + } + } + /// public bool TryGetResource(string key, out object value) => Loaded.TryGetResource(key, out value); @@ -90,4 +98,4 @@ namespace Avalonia.Markup.Xaml.Styling _parent = parent; } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Styling.UnitTests/StyleTests.cs b/tests/Avalonia.Styling.UnitTests/StyleTests.cs index 5ef559b887..53e0ae5db7 100644 --- a/tests/Avalonia.Styling.UnitTests/StyleTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyleTests.cs @@ -167,6 +167,31 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(new Thickness(0), border.BorderThickness); } + [Fact] + public void Style_Should_Detach_Setters_When_Detach_Is_Called() + { + Border border; + + var style = new Style(x => x.OfType()) + { + Setters = new[] + { + new Setter(Border.BorderThicknessProperty, new Thickness(4)), + } + }; + + var root = new TestRoot + { + Child = border = new Border(), + }; + + style.Attach(border, null); + + Assert.Equal(new Thickness(4), border.BorderThickness); + style.Detach(); + Assert.Equal(new Thickness(0), border.BorderThickness); + } + private class Class1 : Control { public static readonly StyledProperty FooProperty = From 81b58e955f06782f2b89af5a8084df5e64a7b558 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 8 Oct 2018 23:40:41 +0300 Subject: [PATCH 2/9] unit tests for textbox OutOfRangeException issue #1954 --- .../TextBoxTests.cs | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 4a8c171ecf..8fbbfdd40b 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -8,7 +8,6 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; -using Avalonia.Markup.Data; using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; @@ -322,6 +321,71 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void SelectionEnd_Dont_Cause_Exception() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123456789" + }; + + target.SelectionStart = 0; + target.SelectionEnd = 9; + + target.Text = "123"; + + RaiseTextEvent(target, "456"); + + Assert.True(true); + } + } + + [Fact] + public void SelectionStart_Dont_Cause_Exception() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123456789" + }; + + target.SelectionStart = 8; + target.SelectionEnd = 9; + + target.Text = "123"; + + RaiseTextEvent(target, "456"); + + Assert.True(true); + } + } + + [Fact] + public void SelectionStartEnd_Are_Valid_AterTextChange() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123456789" + }; + + target.SelectionStart = 8; + target.SelectionEnd = 9; + + target.Text = "123"; + + Assert.True(target.SelectionStart <= "123".Length); + Assert.True(target.SelectionEnd <= "123".Length); + } + } + private static TestServices Services => TestServices.MockThreadingInterface.With( standardCursorFactory: Mock.Of()); @@ -351,6 +415,15 @@ namespace Avalonia.Controls.UnitTests }); } + private void RaiseTextEvent(TextBox textBox, string text) + { + textBox.RaiseEvent(new TextInputEventArgs + { + RoutedEvent = InputElement.TextInputEvent, + Text = text + }); + } + private class Class1 : NotifyingBase { private int _foo; From 0959be5e01ea2e15d83a2f8a6b5179d1bf31465f Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 8 Oct 2018 23:48:19 +0300 Subject: [PATCH 3/9] fix for textbox OutOfRangeException issue #1954 --- src/Avalonia.Controls/TextBox.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 388f984b78..f8f0667759 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -213,6 +213,8 @@ namespace Avalonia.Controls if (!_ignoreTextChanges) { CaretIndex = CoerceCaretIndex(CaretIndex, value?.Length ?? 0); + SelectionStart = CoerceCaretIndex(SelectionStart, value?.Length ?? 0); + SelectionEnd = CoerceCaretIndex(SelectionEnd, value?.Length ?? 0); if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing) { From 3b302006fdd1eae03bf3460e40f2f5a9f6852e23 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 9 Oct 2018 14:48:02 +0300 Subject: [PATCH 4/9] fix failing text e.g. restore carretIndex in textbox when text changed --- src/Avalonia.Controls/TextBox.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index f8f0667759..2f29d229e5 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -212,9 +212,10 @@ namespace Avalonia.Controls { if (!_ignoreTextChanges) { - CaretIndex = CoerceCaretIndex(CaretIndex, value?.Length ?? 0); + var carretIndex = CaretIndex; SelectionStart = CoerceCaretIndex(SelectionStart, value?.Length ?? 0); SelectionEnd = CoerceCaretIndex(SelectionEnd, value?.Length ?? 0); + CaretIndex = CoerceCaretIndex(carretIndex, value?.Length ?? 0); if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing) { From eabf122ce76711aa8a3c12f04e9b1a56ff537e76 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Wed, 10 Oct 2018 18:08:54 +0300 Subject: [PATCH 5/9] fix nits --- src/Avalonia.Controls/TextBox.cs | 4 ++-- tests/Avalonia.Controls.UnitTests/TextBoxTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 2f29d229e5..e2d2f562ec 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -212,10 +212,10 @@ namespace Avalonia.Controls { if (!_ignoreTextChanges) { - var carretIndex = CaretIndex; + var caretIndex = CaretIndex; SelectionStart = CoerceCaretIndex(SelectionStart, value?.Length ?? 0); SelectionEnd = CoerceCaretIndex(SelectionEnd, value?.Length ?? 0); - CaretIndex = CoerceCaretIndex(carretIndex, value?.Length ?? 0); + CaretIndex = CoerceCaretIndex(caretIndex, value?.Length ?? 0); if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing) { diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 8fbbfdd40b..0d87f6d0fe 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -322,7 +322,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void SelectionEnd_Dont_Cause_Exception() + public void SelectionEnd_Doesnt_Cause_Exception() { using (UnitTestApplication.Start(Services)) { @@ -344,7 +344,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void SelectionStart_Dont_Cause_Exception() + public void SelectionStart_Doesnt_Cause_Exception() { using (UnitTestApplication.Start(Services)) { From e00f0f0385ad1b1aa3fd48f036e7730b36fa6fa2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 13 Oct 2018 12:04:05 +0300 Subject: [PATCH 6/9] Platform-specific key gestures --- .../Avalonia.Android/AndroidPlatform.cs | 1 + src/Avalonia.Controls/TextBox.cs | 363 ++++++++++-------- src/Avalonia.Input/Key.cs | 18 + src/Avalonia.Input/KeyGesture.cs | 11 + .../Platform/PlatformHotkeyConfiguration.cs | 94 +++++ src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs | 1 + .../LinuxFramebufferPlatform.cs | 2 + src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs | 1 + src/Windows/Avalonia.Win32/Win32Platform.cs | 1 + src/iOS/Avalonia.iOS/iOSPlatform.cs | 1 + .../Avalonia.UnitTests/UnitTestApplication.cs | 2 + 11 files changed, 340 insertions(+), 155 deletions(-) create mode 100644 src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 5f0edadf63..4e48811c35 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -54,6 +54,7 @@ namespace Avalonia.Android .Bind().ToSingleton() .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new RenderLoop()) + .Bind().ToSingleton() .Bind().ToConstant(new AssetLoader(app.GetType().Assembly)); SkiaPlatform.Initialize(); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index e2d2f562ec..56c5129275 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -368,186 +368,239 @@ namespace Avalonia.Controls string text = Text ?? string.Empty; int caretIndex = CaretIndex; bool movement = false; + bool selection = false; bool handled = false; var modifiers = e.Modifiers; - switch (e.Key) - { - case Key.A: - if (modifiers == InputModifiers.Control) - { - SelectAll(); - handled = true; - } - break; - case Key.C: - if (modifiers == InputModifiers.Control) - { - if (!IsPasswordBox) - { - Copy(); - } - handled = true; - } - break; + var keymap = AvaloniaLocator.Current.GetService(); - case Key.X: - if (modifiers == InputModifiers.Control) - { - if (!IsPasswordBox) - { - Copy(); - DeleteSelection(); - } - handled = true; - } - break; + bool Match(List gestures) => gestures.Any(g => g.Matches(e)); + bool DetectSelection() => e.Modifiers.HasFlag(keymap.SelectionModifiers); - case Key.V: - if (modifiers == InputModifiers.Control) - { - Paste(); - handled = true; - } + if (Match(keymap.SelectAll)) + { + SelectAll(); + handled = true; + } + else if (Match(keymap.Copy)) + { + if (!IsPasswordBox) + { + Copy(); + } - break; + handled = true; + } + else if (Match(keymap.Cut)) + { + if (!IsPasswordBox) + { + Copy(); + DeleteSelection(); + } - case Key.Z: - if (modifiers == InputModifiers.Control) - { - try - { - _isUndoingRedoing = true; - _undoRedoHelper.Undo(); - } - finally - { - _isUndoingRedoing = false; - } - handled = true; - } - break; - case Key.Y: - if (modifiers == InputModifiers.Control) - { - try - { - _isUndoingRedoing = true; - _undoRedoHelper.Redo(); - } - finally - { - _isUndoingRedoing = false; - } - handled = true; - } - break; - case Key.Left: - MoveHorizontal(-1, modifiers); - movement = true; - break; + handled = true; + } + else if (Match(keymap.Paste)) + { - case Key.Right: - MoveHorizontal(1, modifiers); - movement = true; - break; + Paste(); + handled = true; + } + else if (Match(keymap.Undo)) + { - case Key.Up: - movement = MoveVertical(-1, modifiers); - break; + try + { + _isUndoingRedoing = true; + _undoRedoHelper.Undo(); + } + finally + { + _isUndoingRedoing = false; + } - case Key.Down: - movement = MoveVertical(1, modifiers); - break; + handled = true; + } + else if (Match(keymap.Redo)) + { + try + { + _isUndoingRedoing = true; + _undoRedoHelper.Redo(); + } + finally + { + _isUndoingRedoing = false; + } - case Key.Home: - MoveHome(modifiers); - movement = true; - break; + handled = true; + } + else if (Match(keymap.MoveCursorToTheStartOfDocument)) + { + MoveHome(true); + movement = true; + selection = false; + handled = true; + } + else if (Match(keymap.MoveCursorToTheEndOfDocument)) + { + MoveEnd(true); + movement = true; + selection = false; + handled = true; + } + else if (Match(keymap.MoveCursorToTheStartOfLine)) + { + MoveHome(false); + movement = true; + selection = false; + handled = true; + + } + else if (Match(keymap.MoveCursorToTheEndOfLine)) + { + MoveEnd(false); + movement = true; + selection = false; + handled = true; + } + else if (Match(keymap.MoveCursorToTheStartOfDocumentWithSelection)) + { + MoveHome(true); + movement = true; + selection = true; + handled = true; + } + else if (Match(keymap.MoveCursorToTheEndOfDocumentWithSelection)) + { + MoveEnd(true); + movement = true; + selection = true; + handled = true; + } + else if (Match(keymap.MoveCursorToTheStartOfLineWithSelection)) + { + MoveHome(false); + movement = true; + selection = true; + handled = true; + + } + else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection)) + { + MoveEnd(false); + movement = true; + selection = true; + handled = true; + } + else + switch (e.Key) + { + case Key.Left: + MoveHorizontal(-1, modifiers); + movement = true; + selection = DetectSelection(); + break; - case Key.End: - MoveEnd(modifiers); - movement = true; - break; + case Key.Right: + MoveHorizontal(1, modifiers); + movement = true; + selection = DetectSelection(); + break; - case Key.Back: - if (modifiers == InputModifiers.Control && SelectionStart == SelectionEnd) - { - SetSelectionForControlBackspace(modifiers); - } + case Key.Up: + movement = MoveVertical(-1, modifiers); + selection = DetectSelection(); + break; - if (!DeleteSelection() && CaretIndex > 0) - { - 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') + case Key.Down: + movement = MoveVertical(1, modifiers); + selection = DetectSelection(); + break; + + case Key.Back: + if (modifiers == keymap.CommandModifiers && SelectionStart == SelectionEnd) { - removedCharacters = 2; + SetSelectionForControlBackspace(modifiers); } - SetTextInternal(text.Substring(0, caretIndex - removedCharacters) + text.Substring(caretIndex)); - CaretIndex -= removedCharacters; - SelectionStart = SelectionEnd = CaretIndex; - } - handled = true; - break; + if (!DeleteSelection() && CaretIndex > 0) + { + 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; + } - case Key.Delete: - if (modifiers == InputModifiers.Control && SelectionStart == SelectionEnd) - { - SetSelectionForControlDelete(modifiers); - } + SetTextInternal(text.Substring(0, caretIndex - removedCharacters) + + text.Substring(caretIndex)); + CaretIndex -= removedCharacters; + SelectionStart = SelectionEnd = CaretIndex; + } - if (!DeleteSelection() && caretIndex < text.Length) - { - 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') + handled = true; + break; + + case Key.Delete: + if (modifiers == keymap.CommandModifiers && SelectionStart == SelectionEnd) { - removedCharacters = 2; + SetSelectionForControlDelete(modifiers); } - SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + removedCharacters)); - } - handled = true; - break; + if (!DeleteSelection() && caretIndex < text.Length) + { + 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)); + } - case Key.Enter: - if (AcceptsReturn) - { - HandleTextInput(NewLine); handled = true; - } + break; - break; + case Key.Enter: + if (AcceptsReturn) + { + HandleTextInput(NewLine); + handled = true; + } - case Key.Tab: - if (AcceptsTab) - { - HandleTextInput("\t"); - handled = true; - } - else - { - base.OnKeyDown(e); - } + break; - break; + case Key.Tab: + if (AcceptsTab) + { + HandleTextInput("\t"); + handled = true; + } + else + { + base.OnKeyDown(e); + } - default: - handled = false; - break; - } + break; + + default: + handled = false; + break; + } - if (movement && ((modifiers & InputModifiers.Shift) != 0)) + if (movement && selection) { SelectionEnd = CaretIndex; } @@ -732,12 +785,12 @@ namespace Avalonia.Controls } } - private void MoveHome(InputModifiers modifiers) + private void MoveHome(bool document) { var text = Text ?? string.Empty; var caretIndex = CaretIndex; - if ((modifiers & InputModifiers.Control) != 0) + if (document) { caretIndex = 0; } @@ -762,12 +815,12 @@ namespace Avalonia.Controls CaretIndex = caretIndex; } - private void MoveEnd(InputModifiers modifiers) + private void MoveEnd(bool document) { var text = Text ?? string.Empty; var caretIndex = CaretIndex; - if ((modifiers & InputModifiers.Control) != 0) + if (document) { caretIndex = text.Length; } diff --git a/src/Avalonia.Input/Key.cs b/src/Avalonia.Input/Key.cs index d4f95dbd5c..d51b990fd4 100644 --- a/src/Avalonia.Input/Key.cs +++ b/src/Avalonia.Input/Key.cs @@ -1020,5 +1020,23 @@ namespace Avalonia.Input /// The key is used with another key to create a single combined character. /// DeadCharProcessed = 172, + + + /// + /// OSX Platform-specific Fn+Left key + /// + FnLeftArrow = 10001, + /// + /// OSX Platform-specific Fn+Right key + /// + FnRightArrow = 10002, + /// + /// OSX Platform-specific Fn+Up key + /// + FnUpArrow = 10003, + /// + /// OSX Platform-specific Fn+Down key + /// + FnDownArrow = 10004, } } diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index bb3b15c64f..c945902def 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/src/Avalonia.Input/KeyGesture.cs @@ -6,6 +6,17 @@ namespace Avalonia.Input { public sealed class KeyGesture : IEquatable { + public KeyGesture() + { + + } + + public KeyGesture(Key key, InputModifiers modifiers = InputModifiers.None) + { + Key = key; + Modifiers = modifiers; + } + public bool Equals(KeyGesture other) { if (ReferenceEquals(null, other)) return false; diff --git a/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs b/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs new file mode 100644 index 0000000000..5f3093df78 --- /dev/null +++ b/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; + +namespace Avalonia.Input.Platform +{ + public class PlatformHotkeyConfiguration + { + public PlatformHotkeyConfiguration() : this(InputModifiers.Control) + { + + } + + public PlatformHotkeyConfiguration(InputModifiers commandModifiers, InputModifiers selectionModifiers = InputModifiers.Shift) + { + CommandModifiers = commandModifiers; + SelectionModifiers = selectionModifiers; + Copy = new List + { + new KeyGesture(Key.C, commandModifiers) + }; + Cut = new List + { + new KeyGesture(Key.X, commandModifiers) + }; + Paste = new List + { + new KeyGesture(Key.V, commandModifiers) + }; + Undo = new List + { + new KeyGesture(Key.Z, commandModifiers) + }; + Redo = new List + { + new KeyGesture(Key.Y, commandModifiers), + new KeyGesture(Key.Z, commandModifiers | selectionModifiers) + }; + SelectAll = new List + { + new KeyGesture(Key.A, commandModifiers) + }; + MoveCursorToTheStartOfLine = new List + { + new KeyGesture(Key.Home) + }; + MoveCursorToTheEndOfLine = new List + { + new KeyGesture(Key.End) + }; + MoveCursorToTheStartOfDocument = new List + { + new KeyGesture(Key.Home, commandModifiers) + }; + MoveCursorToTheEndOfDocument = new List + { + new KeyGesture(Key.End, commandModifiers) + }; + MoveCursorToTheStartOfLineWithSelection = new List + { + new KeyGesture(Key.Home, selectionModifiers) + }; + MoveCursorToTheEndOfLineWithSelection = new List + { + new KeyGesture(Key.End, selectionModifiers) + }; + MoveCursorToTheStartOfDocumentWithSelection = new List + { + new KeyGesture(Key.Home, commandModifiers | selectionModifiers) + }; + MoveCursorToTheEndOfDocumentWithSelection = new List + { + new KeyGesture(Key.End, commandModifiers | selectionModifiers) + }; + } + + public InputModifiers CommandModifiers { get; set; } + public InputModifiers SelectionModifiers { get; set; } + public List Copy { get; set; } + public List Cut { get; set; } + public List Paste { get; set; } + public List Undo { get; set; } + public List Redo { get; set; } + public List SelectAll { get; set; } + public List MoveCursorToTheStartOfLine { get; set; } + public List MoveCursorToTheEndOfLine { get; set; } + public List MoveCursorToTheStartOfDocument { get; set; } + public List MoveCursorToTheEndOfDocument { get; set; } + public List MoveCursorToTheStartOfLineWithSelection { get; set; } + public List MoveCursorToTheEndOfLineWithSelection { get; set; } + public List MoveCursorToTheStartOfDocumentWithSelection { get; set; } + public List MoveCursorToTheEndOfDocumentWithSelection { get; set; } + + + } +} diff --git a/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs index e2e8a34676..9604e9975b 100644 --- a/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs +++ b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs @@ -81,6 +81,7 @@ namespace Avalonia.Gtk3 .Bind().ToSingleton() .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new DefaultRenderTimer(60)) + .Bind().ToSingleton() .Bind().ToConstant(new PlatformIconLoader()); if (useGpu) EglGlPlatformFeature.TryInitialize(); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 9046c26cee..9ed06c978d 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -5,6 +5,7 @@ using Avalonia.Controls; using Avalonia.Controls.Embedding; using Avalonia.Controls.Platform; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.LinuxFramebuffer; using Avalonia.Platform; using Avalonia.Rendering; @@ -36,6 +37,7 @@ namespace Avalonia.LinuxFramebuffer .Bind().ToSingleton() .Bind().ToConstant(Threading) .Bind().ToConstant(new RenderLoop()) + .Bind().ToSingleton() .Bind().ToConstant(Threading); } diff --git a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs index 5757413b7a..0d2d4d3f9c 100644 --- a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs +++ b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs @@ -37,6 +37,7 @@ namespace Avalonia.MonoMac .Bind().ToSingleton() .Bind().ToConstant(s_renderLoop) .Bind().ToConstant(s_renderTimer) + .Bind().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows)) .Bind().ToConstant(PlatformThreadingInterface.Instance) /*.Bind().ToTransient()*/; } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 5041942c63..33bee4a82a 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -89,6 +89,7 @@ namespace Avalonia.Win32 .Bind().ToConstant(new RenderTimer(60)) .Bind().ToSingleton() .Bind().ToConstant(s_instance) + .Bind().ToSingleton() .Bind().ToConstant(s_instance); Win32GlManager.Initialize(); UseDeferredRendering = deferredRendering; diff --git a/src/iOS/Avalonia.iOS/iOSPlatform.cs b/src/iOS/Avalonia.iOS/iOSPlatform.cs index 7b50040bf2..b0092bc98a 100644 --- a/src/iOS/Avalonia.iOS/iOSPlatform.cs +++ b/src/iOS/Avalonia.iOS/iOSPlatform.cs @@ -42,6 +42,7 @@ namespace Avalonia.iOS .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToSingleton(); } } diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index 359198048b..5c53334f44 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -11,6 +11,7 @@ using Avalonia.Rendering; using Avalonia.Threading; using System.Reactive.Disposables; using System.Reactive.Concurrency; +using Avalonia.Input.Platform; namespace Avalonia.UnitTests { @@ -58,6 +59,7 @@ namespace Avalonia.UnitTests .Bind().ToConstant(Services.StandardCursorFactory) .Bind().ToConstant(Services.Styler) .Bind().ToConstant(Services.WindowingPlatform) + .Bind().ToSingleton() .Bind().ToConstant(this); var styles = Services.Theme?.Invoke(); From 4d2407192ce33e9efee92a4861578fbc5786949a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 13 Oct 2018 19:40:05 +0300 Subject: [PATCH 7/9] Added WholeWordTextActionModifiers --- src/Avalonia.Controls/TextBox.cs | 33 ++++++++++--------- .../Platform/PlatformHotkeyConfiguration.cs | 6 +++- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 56c5129275..d6c3a1041e 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -495,34 +495,36 @@ namespace Avalonia.Controls handled = true; } else + { + bool hasWholeWordModifiers = modifiers.HasFlag(keymap.WholeWordTextActionModifiers); switch (e.Key) { case Key.Left: - MoveHorizontal(-1, modifiers); + MoveHorizontal(-1, hasWholeWordModifiers); movement = true; selection = DetectSelection(); break; case Key.Right: - MoveHorizontal(1, modifiers); + MoveHorizontal(1, hasWholeWordModifiers); movement = true; selection = DetectSelection(); break; case Key.Up: - movement = MoveVertical(-1, modifiers); + movement = MoveVertical(-1); selection = DetectSelection(); break; case Key.Down: - movement = MoveVertical(1, modifiers); + movement = MoveVertical(1); selection = DetectSelection(); break; case Key.Back: - if (modifiers == keymap.CommandModifiers && SelectionStart == SelectionEnd) + if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { - SetSelectionForControlBackspace(modifiers); + SetSelectionForControlBackspace(); } if (!DeleteSelection() && CaretIndex > 0) @@ -548,9 +550,9 @@ namespace Avalonia.Controls break; case Key.Delete: - if (modifiers == keymap.CommandModifiers && SelectionStart == SelectionEnd) + if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { - SetSelectionForControlDelete(modifiers); + SetSelectionForControlDelete(); } if (!DeleteSelection() && caretIndex < text.Length) @@ -599,6 +601,7 @@ namespace Avalonia.Controls handled = false; break; } + } if (movement && selection) { @@ -719,12 +722,12 @@ namespace Avalonia.Controls return result; } - private void MoveHorizontal(int direction, InputModifiers modifiers) + private void MoveHorizontal(int direction, bool wholeWord) { var text = Text ?? string.Empty; var caretIndex = CaretIndex; - if ((modifiers & InputModifiers.Control) == 0) + if (!wholeWord) { var index = caretIndex + direction; @@ -762,7 +765,7 @@ namespace Avalonia.Controls } } - private bool MoveVertical(int count, InputModifiers modifiers) + private bool MoveVertical(int count) { var formattedText = _presenter.FormattedText; var lines = formattedText.GetLines().ToList(); @@ -935,17 +938,17 @@ namespace Avalonia.Controls } } - private void SetSelectionForControlBackspace(InputModifiers modifiers) + private void SetSelectionForControlBackspace() { SelectionStart = CaretIndex; - MoveHorizontal(-1, modifiers); + MoveHorizontal(-1, true); SelectionEnd = CaretIndex; } - private void SetSelectionForControlDelete(InputModifiers modifiers) + private void SetSelectionForControlDelete() { SelectionStart = CaretIndex; - MoveHorizontal(1, modifiers); + MoveHorizontal(1, true); SelectionEnd = CaretIndex; } diff --git a/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs b/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs index 5f3093df78..a758a328be 100644 --- a/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs +++ b/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs @@ -9,10 +9,13 @@ namespace Avalonia.Input.Platform } - public PlatformHotkeyConfiguration(InputModifiers commandModifiers, InputModifiers selectionModifiers = InputModifiers.Shift) + public PlatformHotkeyConfiguration(InputModifiers commandModifiers, + InputModifiers selectionModifiers = InputModifiers.Shift, + InputModifiers wholeWordTextActionModifiers = InputModifiers.Control) { CommandModifiers = commandModifiers; SelectionModifiers = selectionModifiers; + WholeWordTextActionModifiers = wholeWordTextActionModifiers; Copy = new List { new KeyGesture(Key.C, commandModifiers) @@ -73,6 +76,7 @@ namespace Avalonia.Input.Platform } public InputModifiers CommandModifiers { get; set; } + public InputModifiers WholeWordTextActionModifiers { get; set; } public InputModifiers SelectionModifiers { get; set; } public List Copy { get; set; } public List Cut { get; set; } From 94b379096af08d9f7c6b9758600ad734009f1cde Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 13 Oct 2018 19:40:34 +0300 Subject: [PATCH 8/9] Fixed ALT on GTK --- src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index 0ec4047086..6e8cf5cb18 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -126,7 +126,7 @@ namespace Avalonia.Gtk3 if (state.HasFlag(GdkModifierType.ShiftMask)) rv |= InputModifiers.Shift; if (state.HasFlag(GdkModifierType.Mod1Mask)) - rv |= InputModifiers.Control; + rv |= InputModifiers.Alt; if (state.HasFlag(GdkModifierType.Button1Mask)) rv |= InputModifiers.LeftMouseButton; if (state.HasFlag(GdkModifierType.Button2Mask)) From 173ee0037c92beb57d23dda188ae2f3370887266 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 16 Oct 2018 22:51:10 +0100 Subject: [PATCH 9/9] support high dpi on avalonia remote previewer. --- .../Remote/Server/RemoteServerTopLevelImpl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index b302f2f5ec..c1ef9476db 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -102,6 +102,12 @@ namespace Avalonia.Controls.Remote.Server FrameMessage RenderFrame(int width, int height, ProtocolPixelFormat? format) { + var scalingX = _dpi.X / 96.0; + var scalingY = _dpi.Y / 96.0; + + width = (int)(width * scalingX); + height = (int)(height * scalingY); + var fmt = format ?? ProtocolPixelFormat.Rgba8888; var bpp = fmt == ProtocolPixelFormat.Rgb565 ? 2 : 4; var data = new byte[width * height * bpp];