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/Application.cs b/src/Avalonia.Controls/Application.cs index 37796ff9ba..1d4e4cbeaa 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -122,11 +122,11 @@ namespace Avalonia if (_resources != null) { hadResources = _resources.Count > 0; - _resources.ResourcesChanged -= ResourcesChanged; + _resources.ResourcesChanged -= ThisResourcesChanged; } _resources = value; - _resources.ResourcesChanged += ResourcesChanged; + _resources.ResourcesChanged += ThisResourcesChanged; if (hadResources || _resources.Count > 0) { @@ -343,5 +343,10 @@ namespace Avalonia .Bind().ToConstant(clock) .GetService()?.Add(clock); } + + private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, e); + } } } diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index 2d4f2e6b52..106c6fff63 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -11,11 +11,19 @@ namespace Avalonia.Controls.Remote { public class RemoteWidget : Control { + public enum SizingMode + { + Local, + Remote + } + private readonly IAvaloniaRemoteTransportConnection _connection; private FrameMessage _lastFrame; private WriteableBitmap _bitmap; public RemoteWidget(IAvaloniaRemoteTransportConnection connection) { + Mode = SizingMode.Local; + _connection = connection; _connection.OnMessage += (t, msg) => Dispatcher.UIThread.Post(() => OnMessage(msg)); _connection.Send(new ClientSupportedPixelFormatsMessage @@ -28,6 +36,8 @@ namespace Avalonia.Controls.Remote }); } + public SizingMode Mode { get; set; } + private void OnMessage(object msg) { if (msg is FrameMessage frame) @@ -44,13 +54,17 @@ namespace Avalonia.Controls.Remote protected override void ArrangeCore(Rect finalRect) { - _connection.Send(new ClientViewportAllocatedMessage + if (Mode == SizingMode.Local) { - Width = finalRect.Width, - Height = finalRect.Height, - DpiX = 96, - DpiY = 96 //TODO: Somehow detect the actual DPI - }); + _connection.Send(new ClientViewportAllocatedMessage + { + Width = finalRect.Width, + Height = finalRect.Height, + DpiX = 10 * 96, + DpiY = 10 * 96 //TODO: Somehow detect the actual DPI + }); + } + base.ArrangeCore(finalRect); } diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index b302f2f5ec..257e3e9c2a 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -4,11 +4,15 @@ using System.Runtime.InteropServices; using Avalonia.Controls.Embedding.Offscreen; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; +using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Remote.Protocol; +using Avalonia.Remote.Protocol.Input; using Avalonia.Remote.Protocol.Viewport; using Avalonia.Threading; +using InputModifiers = Avalonia.Input.InputModifiers; +using Key = Avalonia.Input.Key; using PixelFormat = Avalonia.Platform.PixelFormat; using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat; @@ -31,6 +35,67 @@ namespace Avalonia.Controls.Remote.Server { _transport = transport; _transport.OnMessage += OnMessage; + + KeyboardDevice = AvaloniaLocator.Current.GetService(); + } + + private static RawMouseEventType GetAvaloniaEventType (Avalonia.Remote.Protocol.Input.MouseButton button, bool pressed) + { + switch (button) + { + case Avalonia.Remote.Protocol.Input.MouseButton.Left: + return pressed ? RawMouseEventType.LeftButtonDown : RawMouseEventType.LeftButtonUp; + + case Avalonia.Remote.Protocol.Input.MouseButton.Middle: + return pressed ? RawMouseEventType.MiddleButtonDown : RawMouseEventType.MiddleButtonUp; + + case Avalonia.Remote.Protocol.Input.MouseButton.Right: + return pressed ? RawMouseEventType.RightButtonDown : RawMouseEventType.RightButtonUp; + + default: + return RawMouseEventType.Move; + } + } + + private static InputModifiers GetAvaloniaInputModifiers (Avalonia.Remote.Protocol.Input.InputModifiers[] modifiers) + { + var result = InputModifiers.None; + + foreach(var modifier in modifiers) + { + switch (modifier) + { + case Avalonia.Remote.Protocol.Input.InputModifiers.Control: + result |= InputModifiers.Control; + break; + + case Avalonia.Remote.Protocol.Input.InputModifiers.Alt: + result |= InputModifiers.Alt; + break; + + case Avalonia.Remote.Protocol.Input.InputModifiers.Shift: + result |= InputModifiers.Shift; + break; + + case Avalonia.Remote.Protocol.Input.InputModifiers.Windows: + result |= InputModifiers.Windows; + break; + + case Avalonia.Remote.Protocol.Input.InputModifiers.LeftMouseButton: + result |= InputModifiers.LeftMouseButton; + break; + + case Avalonia.Remote.Protocol.Input.InputModifiers.MiddleMouseButton: + result |= InputModifiers.MiddleMouseButton; + break; + + case Avalonia.Remote.Protocol.Input.InputModifiers.RightMouseButton: + result |= InputModifiers.RightMouseButton; + break; + } + } + + return result; } protected virtual void OnMessage(IAvaloniaRemoteTransportConnection transport, object obj) @@ -45,6 +110,16 @@ namespace Avalonia.Controls.Remote.Server } Dispatcher.UIThread.Post(RenderIfNeeded); } + if(obj is ClientRenderInfoMessage renderInfo) + { + lock(_lock) + { + _dpi = new Vector(renderInfo.DpiX, renderInfo.DpiY); + _invalidated = true; + } + + Dispatcher.UIThread.Post(RenderIfNeeded); + } if (obj is ClientSupportedPixelFormatsMessage supportedFormats) { lock (_lock) @@ -82,6 +157,84 @@ namespace Avalonia.Controls.Remote.Server _pendingAllocation = allocated; } } + if(obj is PointerMovedEventMessage pointer) + { + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawMouseEventArgs( + MouseDevice, + 0, + InputRoot, + RawMouseEventType.Move, + new Point(pointer.X, pointer.Y), + GetAvaloniaInputModifiers(pointer.Modifiers))); + }, DispatcherPriority.Input); + } + if(obj is PointerPressedEventMessage pressed) + { + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawMouseEventArgs( + MouseDevice, + 0, + InputRoot, + GetAvaloniaEventType(pressed.Button, true), + new Point(pressed.X, pressed.Y), + GetAvaloniaInputModifiers(pressed.Modifiers))); + }, DispatcherPriority.Input); + } + if (obj is PointerPressedEventMessage released) + { + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawMouseEventArgs( + MouseDevice, + 0, + InputRoot, + GetAvaloniaEventType(released.Button, false), + new Point(released.X, released.Y), + GetAvaloniaInputModifiers(released.Modifiers))); + }, DispatcherPriority.Input); + } + if(obj is ScrollEventMessage scroll) + { + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawMouseWheelEventArgs( + MouseDevice, + 0, + InputRoot, + new Point(scroll.X, scroll.Y), + new Vector(scroll.DeltaX, scroll.DeltaY), + GetAvaloniaInputModifiers(scroll.Modifiers))); + }, DispatcherPriority.Input); + } + if(obj is KeyEventMessage key) + { + Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); + + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawKeyEventArgs( + KeyboardDevice, + 0, + key.IsDown ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, + (Key)key.Key, + GetAvaloniaInputModifiers(key.Modifiers))); + }, DispatcherPriority.Input); + } + if(obj is TextInputEventMessage text) + { + Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); + + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawTextInputEventArgs( + KeyboardDevice, + 0, + text.Text)); + }, DispatcherPriority.Input); + } } } @@ -102,6 +255,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]; @@ -169,5 +328,7 @@ namespace Avalonia.Controls.Remote.Server } public override IMouseDevice MouseDevice { get; } = new MouseDevice(); + + public IKeyboardDevice KeyboardDevice { get; } } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index e2d2f562ec..d6c3a1041e 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -368,186 +368,242 @@ 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 + { + bool hasWholeWordModifiers = modifiers.HasFlag(keymap.WholeWordTextActionModifiers); + switch (e.Key) + { + case Key.Left: + MoveHorizontal(-1, hasWholeWordModifiers); + movement = true; + selection = DetectSelection(); + break; - case Key.End: - MoveEnd(modifiers); - movement = true; - break; + case Key.Right: + MoveHorizontal(1, hasWholeWordModifiers); + movement = true; + selection = DetectSelection(); + break; - case Key.Back: - if (modifiers == InputModifiers.Control && SelectionStart == SelectionEnd) - { - SetSelectionForControlBackspace(modifiers); - } + case Key.Up: + movement = MoveVertical(-1); + 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); + selection = DetectSelection(); + break; + + case Key.Back: + if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { - removedCharacters = 2; + SetSelectionForControlBackspace(); } - 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 (hasWholeWordModifiers && SelectionStart == SelectionEnd) { - removedCharacters = 2; + SetSelectionForControlDelete(); } - 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; } @@ -666,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; @@ -709,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(); @@ -732,12 +788,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 +818,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; } @@ -882,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.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index 6dca479d38..f4d9808d38 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -18,7 +18,7 @@ namespace Avalonia.DesignerSupport Control control; using (PlatformManager.DesignerMode()) { - var loader = new AvaloniaXamlLoader(); + var loader = new AvaloniaXamlLoader() {IsDesignMode = true}; var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml)); diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index 20acc30118..3b6d071583 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -57,7 +57,8 @@ namespace Avalonia.DesignerSupport.Remote .Bind().ToConstant(threading) .Bind().ToSingleton() .Bind().ToConstant(instance) - .Bind().ToSingleton(); + .Bind().ToSingleton() + .Bind().ToSingleton(); } diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 4536748836..cf36dd622a 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -15,6 +15,8 @@ namespace Avalonia.DesignerSupport.Remote { private static ClientSupportedPixelFormatsMessage s_supportedPixelFormats; private static ClientViewportAllocatedMessage s_viewportAllocatedMessage; + private static ClientRenderInfoMessage s_renderInfoMessage; + private static IAvaloniaRemoteTransportConnection s_transport; class CommandLineArgs { @@ -161,7 +163,8 @@ namespace Avalonia.DesignerSupport.Remote PreviewerWindowingPlatform.PreFlightMessages = new List { s_supportedPixelFormats, - s_viewportAllocatedMessage + s_viewportAllocatedMessage, + s_renderInfoMessage }; } @@ -173,6 +176,11 @@ namespace Avalonia.DesignerSupport.Remote s_supportedPixelFormats = formats; RebuildPreFlight(); } + if (obj is ClientRenderInfoMessage renderInfo) + { + s_renderInfoMessage = renderInfo; + RebuildPreFlight(); + } if (obj is ClientViewportAllocatedMessage viewport) { s_viewportAllocatedMessage = viewport; diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 43f0e0a95e..3a97b0d799 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Reactive.Disposables; @@ -20,7 +20,7 @@ namespace Avalonia.DesignerSupport.Remote public IPlatformHandle Handle { get; } public Size MaxClientSize { get; } public Size ClientSize { get; } - public double Scaling { get; } + public double Scaling { get; } = 1.0; public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } 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..a758a328be --- /dev/null +++ b/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; + +namespace Avalonia.Input.Platform +{ + public class PlatformHotkeyConfiguration + { + public PlatformHotkeyConfiguration() : this(InputModifiers.Control) + { + + } + + 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) + }; + 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 WholeWordTextActionModifiers { 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/Avalonia.Remote.Protocol/InputMessages.cs b/src/Avalonia.Remote.Protocol/InputMessages.cs index e612eb3730..6b8cdebbcc 100644 --- a/src/Avalonia.Remote.Protocol/InputMessages.cs +++ b/src/Avalonia.Remote.Protocol/InputMessages.cs @@ -75,4 +75,10 @@ namespace Avalonia.Remote.Protocol.Input public Key Key { get; set; } } + [AvaloniaRemoteMessageGuid("C174102E-7405-4594-916F-B10B8248A17D")] + public class TextInputEventMessage : InputEventMessageBase + { + public string Text { get; set; } + } + } diff --git a/src/Avalonia.Remote.Protocol/ViewportMessages.cs b/src/Avalonia.Remote.Protocol/ViewportMessages.cs index 0627e9d629..dc4550b846 100644 --- a/src/Avalonia.Remote.Protocol/ViewportMessages.cs +++ b/src/Avalonia.Remote.Protocol/ViewportMessages.cs @@ -37,6 +37,13 @@ public PixelFormat[] Formats { get; set; } } + [AvaloniaRemoteMessageGuid("7A3c25d3-3652-438D-8EF1-86E942CC96C0")] + public class ClientRenderInfoMessage + { + public double DpiX { get; set; } + public double DpiY { get; set; } + } + [AvaloniaRemoteMessageGuid("68014F8A-289D-4851-8D34-5367EDA7F827")] public class FrameReceivedMessage { 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 067bb59fe9..23c318a809 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); if (control is Animatable animatable) { @@ -132,17 +146,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); } } @@ -183,16 +205,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); } @@ -209,10 +240,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/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/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)) 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/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 473b97b51a..c89be32057 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -21,6 +21,12 @@ namespace Avalonia.Markup.Xaml { private readonly AvaloniaXamlSchemaContext _context = GetContext(); + public bool IsDesignMode + { + get => _context.IsDesignMode; + set => _context.IsDesignMode = value; + } + private static AvaloniaXamlSchemaContext GetContext() { var result = AvaloniaLocator.Current.GetService(); @@ -200,7 +206,7 @@ namespace Avalonia.Markup.Xaml internal static object LoadFromReader(XamlReader reader, AvaloniaXamlContext context = null, IAmbientProvider parentAmbientProvider = null) { var writer = AvaloniaXamlObjectWriter.Create( - reader.SchemaContext, + (AvaloniaXamlSchemaContext)reader.SchemaContext, context, parentAmbientProvider); @@ -234,4 +240,4 @@ namespace Avalonia.Markup.Xaml public static T Parse(string xaml, Assembly localAssembly = null) => (T)Parse(xaml, localAssembly); } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs index 240ca291a8..5d1a98f6f8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs @@ -4,13 +4,27 @@ using Portable.Xaml.ComponentModel; using System.ComponentModel; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using Avalonia.Controls; +using Portable.Xaml.Schema; namespace Avalonia.Markup.Xaml.PortableXaml { - public class AvaloniaXamlObjectWriter : XamlObjectWriter + class AvaloniaXamlObjectWriter : XamlObjectWriter { + private static Dictionary DesignDirectives = new Dictionary + { + ["DataContext"] = "DataContext", + ["DesignWidth"] = "Width", ["DesignHeight"] = "Height", ["PreviewWith"] = "PreviewWith" + } + .ToDictionary(p => new XamlDirective( + new[] {"http://schemas.microsoft.com/expression/blend/2008"}, p.Key, + XamlLanguage.Object, null, AllowedMemberLocations.Attribute), p => p.Value); + + private readonly AvaloniaXamlSchemaContext _schemaContext; + public static AvaloniaXamlObjectWriter Create( - XamlSchemaContext schemaContext, + AvaloniaXamlSchemaContext schemaContext, AvaloniaXamlContext context, IAmbientProvider parentAmbientProvider = null) { @@ -34,13 +48,14 @@ namespace Avalonia.Markup.Xaml.PortableXaml private AvaloniaNameScope _nameScope; private AvaloniaXamlObjectWriter( - XamlSchemaContext schemaContext, + AvaloniaXamlSchemaContext schemaContext, XamlObjectWriterSettings settings, AvaloniaNameScope nameScope, IAmbientProvider parentAmbientProvider) : base(schemaContext, settings, parentAmbientProvider) { _nameScope = nameScope; + _schemaContext = schemaContext; } protected override void Dispose(bool disposing) @@ -122,6 +137,20 @@ namespace Avalonia.Markup.Xaml.PortableXaml (value as Avalonia.ISupportInitialize)?.EndInit(); } + public override void WriteStartMember(XamlMember property) + { + foreach(var d in DesignDirectives) + if (property == d.Key && _schemaContext.IsDesignMode) + { + base.WriteStartMember(new XamlMember(d.Value, + typeof(Design).GetMethod("Get" + d.Value, BindingFlags.Static | BindingFlags.Public), + typeof(Design).GetMethod("Set" + d.Value, BindingFlags.Static | BindingFlags.Public), + SchemaContext)); + return; + } + base.WriteStartMember(property); + } + private class DelayedValuesHelper { private int _cnt; @@ -225,4 +254,4 @@ namespace Avalonia.Markup.Xaml.PortableXaml } } } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs index 70402aa764..2d6e046f51 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs @@ -15,6 +15,7 @@ namespace Avalonia.Markup.Xaml.PortableXaml { internal class AvaloniaXamlSchemaContext : XamlSchemaContext { + public bool IsDesignMode { get; set; } public static AvaloniaXamlSchemaContext Create(IRuntimeTypeProvider typeProvider = null) { return new AvaloniaXamlSchemaContext(typeProvider ?? new AvaloniaRuntimeTypeProvider()); @@ -280,5 +281,20 @@ namespace Avalonia.Markup.Xaml.PortableXaml return $"{MemberType}:{Type.Namespace}:{Type.Name}.{Member}"; } } + + + public override bool TryGetCompatibleXamlNamespace(string xamlNamespace, out string compatibleNamespace) + { + //Forces XamlXmlReader to not ignore our namespace in design mode if mc:Ignorable is set + if (IsDesignMode && + xamlNamespace == "http://schemas.microsoft.com/expression/blend/2008") + { + compatibleNamespace = xamlNamespace; + return true; + } + + return base.TryGetCompatibleXamlNamespace(xamlNamespace, out compatibleNamespace); + } + } -} \ 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/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.Controls.UnitTests/ApplicationTests.cs b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs index 694a6d2278..df14c808db 100644 --- a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs @@ -113,5 +113,21 @@ namespace Avalonia.Controls.UnitTests Assert.Throws(() => { Application.Current.Run(null); }); } } + + [Fact] + public void Raises_ResourcesChanged_When_Event_Handler_Added_After_Resources_Has_Been_Accessed() + { + // Test for #1765. + using (UnitTestApplication.Start()) + { + var resources = Application.Current.Resources; + var raised = false; + + Application.Current.ResourcesChanged += (s, e) => raised = true; + resources["foo"] = "bar"; + + Assert.True(raised); + } + } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 6777171e41..2e67541c1f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -924,6 +924,45 @@ do we need it?")] } } + [Fact] + public void Design_Mode_Properties_Should_Be_Ignored_At_Runtime_And_Set_In_Design_Mode() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var xaml = @" + + +"; + foreach (var designMode in new[] {true, false}) + { + var loader = new AvaloniaXamlLoader {IsDesignMode = designMode}; + var obj = (Window)loader.Load(xaml); + var context = Design.GetDataContext(obj); + var width = Design.GetWidth(obj); + var height = Design.GetHeight(obj); + if (designMode) + { + Assert.Equal("data-context", context); + Assert.Equal(123, width); + Assert.Equal(321, height); + } + else + { + Assert.False(obj.IsSet(Design.DataContextProperty)); + Assert.False(obj.IsSet(Design.WidthProperty)); + Assert.False(obj.IsSet(Design.HeightProperty)); + } + } + } + } + private class SelectedItemsViewModel : INotifyPropertyChanged { public string[] Items { get; set; } @@ -952,4 +991,4 @@ do we need it?")] public static string GetFoo(AvaloniaObject target) => (string)target.GetValue(FooProperty); } -} \ 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 = 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();