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();