Browse Source

move clipboard to TopLevel

Emmanuel Hansen 2 years ago
parent
commit
04c8b652c8
28 changed files with 259 additions and 94 deletions
  1. 8 8
      samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
  2. 0 2
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  3. 0 1
      src/Android/Avalonia.Android/AndroidPlatform.cs
  4. 17 13
      src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
  5. 12 7
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  6. 4 2
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  7. 0 7
      src/Avalonia.Controls/Application.cs
  8. 1 1
      src/Avalonia.Controls/MaskedTextBox.cs
  9. 5 2
      src/Avalonia.Controls/SelectableTextBlock.cs
  10. 18 5
      src/Avalonia.Controls/TextBox.cs
  11. 5 0
      src/Avalonia.Controls/TopLevel.cs
  12. 0 1
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  13. 0 6
      src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs
  14. 2 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs
  15. 5 3
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  16. 3 2
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
  17. 7 5
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
  18. 6 2
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  19. 6 0
      src/Avalonia.Native/WindowImplBase.cs
  20. 6 0
      src/Avalonia.X11/X11Window.cs
  21. 8 1
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  22. 0 1
      src/Browser/Avalonia.Browser/WindowingPlatform.cs
  23. 6 0
      src/Windows/Avalonia.Win32/WindowImpl.cs
  24. 9 0
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  25. 0 1
      src/iOS/Avalonia.iOS/Platform.cs
  26. 60 9
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  27. 69 14
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  28. 2 0
      tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

+ 8 - 8
samples/ControlCatalog/Pages/ClipboardPage.xaml.cs

@@ -32,13 +32,13 @@ namespace ControlCatalog.Pages
 
         private async void CopyText(object? sender, RoutedEventArgs args)
         {
-            if (Application.Current!.Clipboard is { } clipboard && ClipboardContent is { } clipboardContent)
+            if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard && ClipboardContent is { } clipboardContent)
                 await clipboard.SetTextAsync(clipboardContent.Text ?? String.Empty);
         }
 
         private async void PasteText(object? sender, RoutedEventArgs args)
         {
-            if (Application.Current!.Clipboard is { } clipboard)
+            if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
             {
                 ClipboardContent.Text = await clipboard.GetTextAsync();
             }
@@ -46,7 +46,7 @@ namespace ControlCatalog.Pages
 
         private async void CopyTextDataObject(object? sender, RoutedEventArgs args)
         {
-            if (Application.Current!.Clipboard is { } clipboard)
+            if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
             {
                 var dataObject = new DataObject();
                 dataObject.Set(DataFormats.Text, ClipboardContent.Text ?? string.Empty);
@@ -56,7 +56,7 @@ namespace ControlCatalog.Pages
 
         private async void PasteTextDataObject(object? sender, RoutedEventArgs args)
         {
-            if (Application.Current!.Clipboard is { } clipboard)
+            if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
             {
                 ClipboardContent.Text = await clipboard.GetDataAsync(DataFormats.Text) as string ?? string.Empty;
             }
@@ -64,7 +64,7 @@ namespace ControlCatalog.Pages
 
         private async void CopyFilesDataObject(object? sender, RoutedEventArgs args)
         {
-            if (Application.Current!.Clipboard is { } clipboard)
+            if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
             {
                 var storageProvider = TopLevel.GetTopLevel(this)!.StorageProvider;
                 var filesPath = (ClipboardContent.Text ?? string.Empty)
@@ -110,7 +110,7 @@ namespace ControlCatalog.Pages
 
         private async void PasteFilesDataObject(object? sender, RoutedEventArgs args)
         {
-            if (Application.Current!.Clipboard is { } clipboard)
+            if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
             {
                 var files = await clipboard.GetDataAsync(DataFormats.Files) as IEnumerable<Avalonia.Platform.Storage.IStorageItem>;
 
@@ -120,7 +120,7 @@ namespace ControlCatalog.Pages
 
         private async void GetFormats(object sender, RoutedEventArgs args)
         {
-            if (Application.Current!.Clipboard is { } clipboard)
+            if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
             {
                 var formats = await clipboard.GetFormatsAsync();
                 ClipboardContent.Text = string.Join(Environment.NewLine, formats);
@@ -129,7 +129,7 @@ namespace ControlCatalog.Pages
 
         private async void Clear(object sender, RoutedEventArgs args)
         {
-            if (Application.Current!.Clipboard is { } clipboard)
+            if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
             {
                 await clipboard.ClearAsync();
             }

+ 0 - 2
src/Android/Avalonia.Android/AndroidInputMethod.cs

@@ -6,9 +6,7 @@ using Android.Views;
 using Android.Views.InputMethods;
 using Avalonia.Android.Platform.SkiaPlatform;
 using Avalonia.Controls.Presenters;
-using Avalonia.Input;
 using Avalonia.Input.TextInput;
-using Avalonia.Reactive;
 
 namespace Avalonia.Android
 {

+ 0 - 1
src/Android/Avalonia.Android/AndroidPlatform.cs

@@ -38,7 +38,6 @@ namespace Avalonia.Android
             Options = AvaloniaLocator.Current.GetService<AndroidPlatformOptions>() ?? new AndroidPlatformOptions();
 
             AvaloniaLocator.CurrentMutable
-                .Bind<IClipboard>().ToTransient<ClipboardImpl>()
                 .Bind<ICursorFactory>().ToTransient<CursorFactory>()
                 .Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
                 .Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()

+ 17 - 13
src/Android/Avalonia.Android/Platform/ClipboardImpl.cs

@@ -2,32 +2,26 @@ using System;
 using System.Threading.Tasks;
 
 using Android.Content;
-using Android.Runtime;
-using Android.Views;
 
 using Avalonia.Input;
 using Avalonia.Input.Platform;
-using Avalonia.Platform;
 
 namespace Avalonia.Android.Platform
 {
     internal class ClipboardImpl : IClipboard
     {
-        private Context context = (AvaloniaLocator.Current.GetService<IWindowImpl>() as View).Context;
+        private ClipboardManager? _clipboardManager;
 
-        private ClipboardManager ClipboardManager
+        internal ClipboardImpl(ClipboardManager? value)
         {
-            get
-            {
-                return this.context.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>();
-            }
+            _clipboardManager = value;
         }
 
         public Task<string> GetTextAsync()
         {
-            if (ClipboardManager.HasPrimaryClip)
+            if (_clipboardManager?.HasPrimaryClip == true)
             {
-                return Task.FromResult<string>(ClipboardManager.PrimaryClip.GetItemAt(0).Text);
+                return Task.FromResult<string>(_clipboardManager.PrimaryClip.GetItemAt(0).Text);
             }
 
             return Task.FromResult<string>(null);
@@ -35,15 +29,25 @@ namespace Avalonia.Android.Platform
 
         public Task SetTextAsync(string text)
         {
+            if(_clipboardManager == null)
+            {
+                return Task.CompletedTask;
+            }
+
             ClipData clip = ClipData.NewPlainText("text", text);
-            ClipboardManager.PrimaryClip = clip;
+            _clipboardManager.PrimaryClip = clip;
 
             return Task.FromResult<object>(null);
         }
 
         public Task ClearAsync()
         {
-            ClipboardManager.PrimaryClip = null;
+            if (_clipboardManager == null)
+            {
+                return Task.CompletedTask;
+            }
+
+            _clipboardManager.PrimaryClip = null;
 
             return Task.FromResult<object>(null);
         }

+ 12 - 7
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -3,7 +3,10 @@ using System.Collections.Generic;
 using Android.App;
 using Android.Content;
 using Android.Graphics;
+using Android.Graphics.Drawables;
+using Android.OS;
 using Android.Runtime;
+using Android.Text;
 using Android.Views;
 using Android.Views.InputMethods;
 using Avalonia.Android.Platform.Specific;
@@ -13,6 +16,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.Platform;
 using Avalonia.Controls.Platform.Surfaces;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Input.Raw;
 using Avalonia.Input.TextInput;
 using Avalonia.OpenGL.Egl;
@@ -22,13 +26,7 @@ using Avalonia.Platform.Storage;
 using Avalonia.Rendering;
 using Avalonia.Rendering.Composition;
 using Java.Lang;
-using Java.Util;
-using Math = System.Math;
-using AndroidRect = Android.Graphics.Rect;
-using Window = Android.Views.Window;
-using Android.Graphics.Drawables;
-using Android.OS;
-using Android.Text;
+using ClipboardManager = Android.Content.ClipboardManager;
 
 namespace Avalonia.Android.Platform.SkiaPlatform
 {
@@ -44,6 +42,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         private readonly IStorageProvider _storageProvider;
         private readonly ISystemNavigationManagerImpl _systemNavigationManager;
         private readonly AndroidInsetsManager _insetsManager;
+        private readonly ClipboardImpl _clipboard;
         private ViewImpl _view;
 
         public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@@ -54,6 +53,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             _pointerHelper = new AndroidMotionEventsHelper(this);
             _gl = new EglGlPlatformSurface(this);
             _framebuffer = new FramebufferManager(this);
+            _clipboard = new ClipboardImpl(avaloniaView.Context?.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>());
 
             RenderScaling = _view.Scaling;
 
@@ -408,6 +408,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
                 return _insetsManager;
             }
 
+            if(featureType == typeof(IClipboard))
+            {
+                return _clipboard;
+            }
+
             return null;
         }
     }

+ 4 - 2
src/Avalonia.Controls.DataGrid/DataGrid.cs

@@ -6133,8 +6133,10 @@ namespace Avalonia.Controls
 
         private async void CopyToClipboard(string text)
         {
-            var clipboard = ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)));
-            await clipboard.SetTextAsync(text);
+            var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
+
+            if (clipboard != null)
+                await clipboard.SetTextAsync(text);
         }
 
         /// <summary>

+ 0 - 7
src/Avalonia.Controls/Application.cs

@@ -36,8 +36,6 @@ namespace Avalonia
         /// </summary>
         private DataTemplates? _dataTemplates;
 
-        private readonly Lazy<IClipboard?> _clipboard =
-            new Lazy<IClipboard?>(() => (IClipboard?)AvaloniaLocator.Current.GetService(typeof(IClipboard)));
         private Styles? _styles;
         private IResourceDictionary? _resources;
         private bool _notifyingResourcesChanged;
@@ -141,11 +139,6 @@ namespace Avalonia
             private set;
         }
 
-        /// <summary>
-        /// Gets the application clipboard.
-        /// </summary>
-        public IClipboard? Clipboard => _clipboard.Value;
-
         /// <summary>
         /// Gets the application's global resource dictionary.
         /// </summary>

+ 1 - 1
src/Avalonia.Controls/MaskedTextBox.cs

@@ -210,7 +210,7 @@ namespace Avalonia.Controls
 
             if (keymap is not null && Match(keymap.Paste))
             {
-                var clipboard = (IClipboard?)AvaloniaLocator.Current.GetService(typeof(IClipboard));
+                var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
 
                 if (clipboard is null)
                     return;

+ 5 - 2
src/Avalonia.Controls/SelectableTextBlock.cs

@@ -8,6 +8,7 @@ using Avalonia.Input.Platform;
 using Avalonia.Interactivity;
 using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
+using Avalonia.Platform;
 using Avalonia.Utilities;
 
 namespace Avalonia.Controls
@@ -120,8 +121,10 @@ namespace Avalonia.Controls
 
             if (!eventArgs.Handled)
             {
-                await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
-                    .SetTextAsync(text);
+                var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
+
+                if (clipboard != null)
+                    await clipboard.SetTextAsync(text);
             }
         }
 

+ 18 - 5
src/Avalonia.Controls/TextBox.cs

@@ -18,6 +18,7 @@ using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Automation.Peers;
 using Avalonia.Threading;
+using Avalonia.Platform;
 
 namespace Avalonia.Controls
 {
@@ -1044,8 +1045,13 @@ namespace Avalonia.Controls
             if (!eventArgs.Handled)
             {
                 SnapshotUndoRedo();
-                await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
-                    .SetTextAsync(text);
+
+                var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
+
+                if (clipboard == null)
+                    return;
+                
+                await clipboard.SetTextAsync(text);
                 DeleteSelection();
             }
         }
@@ -1066,8 +1072,10 @@ namespace Avalonia.Controls
             RaiseEvent(eventArgs);
             if (!eventArgs.Handled)
             {
-                await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
-                    .SetTextAsync(text);
+                var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
+
+                if (clipboard != null)
+                    await clipboard.SetTextAsync(text);
             }
         }
 
@@ -1083,7 +1091,12 @@ namespace Avalonia.Controls
                 return;
             }
 
-            var text = await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard))).GetTextAsync();
+            string? text = null;
+
+            var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
+
+            if (clipboard != null)
+                text = await clipboard.GetTextAsync();
 
             if (string.IsNullOrEmpty(text))
             {

+ 5 - 0
src/Avalonia.Controls/TopLevel.cs

@@ -394,6 +394,11 @@ namespace Avalonia.Controls
 
         public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature<IInsetsManager>();
 
+        /// <summary>
+        /// Gets the platform's clipboard implementation
+        /// </summary>
+        public IClipboard? Clipboard => PlatformImpl?.TryGetFeature<IClipboard>();
+
         /// <inheritdoc/>
         Point IRenderRoot.PointToClient(PixelPoint p)
         {

+ 0 - 1
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@@ -48,7 +48,6 @@ namespace Avalonia.DesignerSupport.Remote
             s_transport = transport;
             var instance = new PreviewerWindowingPlatform();
             AvaloniaLocator.CurrentMutable
-                .Bind<IClipboard>().ToSingleton<ClipboardStub>()
                 .Bind<ICursorFactory>().ToSingleton<CursorFactoryStub>()
                 .Bind<IKeyboardDevice>().ToConstant(Keyboard)
                 .Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()

+ 0 - 6
src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs

@@ -76,12 +76,6 @@ namespace Avalonia.Diagnostics.Controls
         public Input.InputManager? InputManager =>
             _application.InputManager;
 
-        /// <summary>
-        /// Gets the application clipboard.
-        /// </summary>
-        public Input.Platform.IClipboard? Clipboard =>
-            _application.Clipboard;
-
         /// <summary>
         /// Gets the application's global resource dictionary.
         /// </summary>

+ 2 - 1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Data;
+using Avalonia.Input.Platform;
 using Avalonia.Markup.Xaml.MarkupExtensions;
 using Avalonia.Media;
 
@@ -7,7 +8,7 @@ namespace Avalonia.Diagnostics.ViewModels
 {
     internal class BindingSetterViewModel : SetterViewModel
     {
-        public BindingSetterViewModel(AvaloniaProperty property, object? value) : base(property, value)
+        public BindingSetterViewModel(AvaloniaProperty property, object? value, IClipboard? clipboard) : base(property, value, clipboard)
         {
             switch (value)
             {

+ 5 - 3
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@@ -59,6 +59,8 @@ namespace Avalonia.Diagnostics.ViewModels
 
                 var styleDiagnostics = styledElement.GetStyleDiagnostics();
 
+                var clipboard = TopLevel.GetTopLevel(_avaloniaObject as Visual)?.Clipboard;
+
                 // We need to place styles without activator first, such styles will be overwritten by ones with activators.
                 foreach (var appliedStyle in styleDiagnostics.AppliedStyles.OrderBy(s => s.HasActivator))
                 {
@@ -91,7 +93,7 @@ namespace Avalonia.Diagnostics.ViewModels
                                     var resourceKey = resourceInfo.Value.resourceKey;
                                     var resourceValue = styledElement.FindResource(resourceKey);
 
-                                    setterVm = new ResourceSetterViewModel(regularSetter.Property, resourceKey, resourceValue, resourceInfo.Value.isDynamic);
+                                    setterVm = new ResourceSetterViewModel(regularSetter.Property, resourceKey, resourceValue, resourceInfo.Value.isDynamic, clipboard);
                                 }
                                 else
                                 {
@@ -99,11 +101,11 @@ namespace Avalonia.Diagnostics.ViewModels
 
                                     if (isBinding)
                                     {
-                                        setterVm = new BindingSetterViewModel(regularSetter.Property, setterValue);
+                                        setterVm = new BindingSetterViewModel(regularSetter.Property, setterValue, clipboard);
                                     }
                                     else
                                     {
-                                        setterVm = new SetterViewModel(regularSetter.Property, setterValue);
+                                        setterVm = new SetterViewModel(regularSetter.Property, setterValue, clipboard);
                                     }
                                 }
 

+ 3 - 2
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs

@@ -1,4 +1,5 @@
-using Avalonia.Media;
+using Avalonia.Input.Platform;
+using Avalonia.Media;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
@@ -10,7 +11,7 @@ namespace Avalonia.Diagnostics.ViewModels
         
         public string ValueTypeTooltip { get; }
 
-        public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object? resourceValue, bool isDynamic) : base(property, resourceValue)
+        public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object? resourceValue, bool isDynamic, IClipboard? clipboard) : base(property, resourceValue, clipboard)
         {
             Key = resourceKey;
             Tint = isDynamic ? Brushes.Orange : Brushes.Brown;

+ 7 - 5
src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs

@@ -25,13 +25,17 @@ namespace Avalonia.Diagnostics.ViewModels
             set => RaiseAndSetIfChanged(ref _isVisible, value);
         }
 
-        public SetterViewModel(AvaloniaProperty property, object? value)
+        private IClipboard? _clipboard;
+
+        public SetterViewModel(AvaloniaProperty property, object? value, IClipboard? clipboard)
         {
             Property = property;
             Name = property.Name;
             Value = value;
             IsActive = true;
             IsVisible = true;
+
+            _clipboard = clipboard;
         }
 
         public virtual void CopyValue()
@@ -51,11 +55,9 @@ namespace Avalonia.Diagnostics.ViewModels
             CopyToClipboard(Property.Name);
         }
 
-        protected static void CopyToClipboard(string value)
+        protected void CopyToClipboard(string value)
         {
-            var clipboard = AvaloniaLocator.Current.GetService<IClipboard>();
-
-            clipboard?.SetTextAsync(value);
+            _clipboard?.SetTextAsync(value);
         }
     }
 }

+ 6 - 2
src/Avalonia.Headless/HeadlessWindowImpl.cs

@@ -1,12 +1,11 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
-using Avalonia.Automation.Peers;
 using Avalonia.Controls;
-using Avalonia.Controls.Platform;
 using Avalonia.Controls.Platform.Surfaces;
 using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Input.Raw;
 using Avalonia.Media.Imaging;
 using Avalonia.Platform;
@@ -254,6 +253,11 @@ namespace Avalonia.Headless
                 return new NoopStorageProvider();
             }
 
+            if(featureType == typeof(IClipboard))
+            {
+                return AvaloniaLocator.Current.GetRequiredService<IClipboard>();
+            }
+
             return null;
         }
 

+ 6 - 0
src/Avalonia.Native/WindowImplBase.cs

@@ -7,6 +7,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.Platform;
 using Avalonia.Controls.Platform.Surfaces;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Input.Raw;
 using Avalonia.Native.Interop;
 using Avalonia.OpenGL;
@@ -528,6 +529,11 @@ namespace Avalonia.Native
                 return _platformBehaviorInhibition;
             }
 
+            if (featureType == typeof(IClipboard))
+            {
+                return AvaloniaLocator.Current.GetRequiredService<IClipboard>();
+            }
+
             return null;
         }
 

+ 6 - 0
src/Avalonia.X11/X11Window.cs

@@ -23,6 +23,7 @@ using Avalonia.Threading;
 using Avalonia.X11.Glx;
 using Avalonia.X11.NativeDialogs;
 using static Avalonia.X11.XLib;
+using Avalonia.Input.Platform;
 // ReSharper disable IdentifierTypo
 // ReSharper disable StringLiteralTypo
 
@@ -828,6 +829,11 @@ namespace Avalonia.X11
                 return _nativeControlHost;
             }
 
+            if (featureType == typeof(IClipboard))
+            {
+                return AvaloniaLocator.Current.GetRequiredService<IClipboard>();
+            }
+
             return null;
         }
 

+ 8 - 1
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@@ -8,6 +8,7 @@ using Avalonia.Browser.Storage;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Input.Raw;
 using Avalonia.Input.TextInput;
 using Avalonia.Platform;
@@ -31,6 +32,7 @@ namespace Avalonia.Browser
         private readonly INativeControlHostImpl _nativeControlHost;
         private readonly IStorageProvider _storageProvider;
         private readonly ISystemNavigationManagerImpl _systemNavigationManager;
+        private readonly ClipboardImpl _clipboard;
         private readonly IInsetsManager? _insetsManager;
 
         public BrowserTopLevelImpl(AvaloniaView avaloniaView)
@@ -46,7 +48,7 @@ namespace Avalonia.Browser
             _nativeControlHost = _avaloniaView.GetNativeControlHostImpl();
             _storageProvider = new BrowserStorageProvider();
             _systemNavigationManager = new BrowserSystemNavigationManagerImpl();
-
+            _clipboard = new ClipboardImpl();
         }
 
         public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
@@ -282,6 +284,11 @@ namespace Avalonia.Browser
                 return _insetsManager;
             }
 
+            if (featureType == typeof(IClipboard))
+            {
+                return _clipboard;
+            }
+
             return null;
         }
     }

+ 0 - 1
src/Browser/Avalonia.Browser/WindowingPlatform.cs

@@ -36,7 +36,6 @@ namespace Avalonia.Browser
             s_keyboard = new KeyboardDevice();
             AvaloniaLocator.CurrentMutable
                 .Bind<IRuntimePlatform>().ToSingleton<BrowserRuntimePlatform>()
-                .Bind<IClipboard>().ToSingleton<ClipboardImpl>()
                 .Bind<ICursorFactory>().ToSingleton<CssCursorFactory>()
                 .Bind<IKeyboardDevice>().ToConstant(s_keyboard)
                 .Bind<IPlatformSettings>().ToSingleton<BrowserPlatformSettings>()

+ 6 - 0
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -24,6 +24,7 @@ using Avalonia.Win32.OpenGl;
 using Avalonia.Win32.WinRT.Composition;
 using Avalonia.Win32.WinRT;
 using static Avalonia.Win32.Interop.UnmanagedMethods;
+using Avalonia.Input.Platform;
 
 namespace Avalonia.Win32
 {
@@ -332,6 +333,11 @@ namespace Avalonia.Win32
                 return _storageProvider;
             }
 
+            if (featureType == typeof(IClipboard))
+            {
+                return AvaloniaLocator.Current.GetRequiredService<IClipboard>();
+            }
+
             return null;
         }
 

+ 9 - 0
src/iOS/Avalonia.iOS/AvaloniaView.cs

@@ -4,6 +4,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.Embedding;
 using Avalonia.Controls.Platform;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Input.Raw;
 using Avalonia.Input.TextInput;
 using Avalonia.iOS.Storage;
@@ -87,6 +88,8 @@ namespace Avalonia.iOS
             private readonly INativeControlHostImpl _nativeControlHost;
             private readonly IStorageProvider _storageProvider;
             internal readonly InsetsManager _insetsManager;
+            private readonly ClipboardImpl _clipboard;
+
             public AvaloniaView View => _view;
 
             public TopLevelImpl(AvaloniaView view)
@@ -99,6 +102,7 @@ namespace Avalonia.iOS
                 {
                     view._topLevel.Padding = b ? default : _insetsManager.SafeAreaPadding;
                 };
+                _clipboard = new ClipboardImpl();
             }
 
             public void Dispose()
@@ -196,6 +200,11 @@ namespace Avalonia.iOS
                     return _insetsManager;
                 }
 
+                if (featureType == typeof(IClipboard))
+                {
+                    return _clipboard;
+                }
+
                 return null;
             }
         }

+ 0 - 1
src/iOS/Avalonia.iOS/Platform.cs

@@ -39,7 +39,6 @@ namespace Avalonia.iOS
                 .Bind<IPlatformGraphics>().ToConstant(GlFeature)
                 .Bind<ICursorFactory>().ToConstant(new CursorFactoryStub())
                 .Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
-                .Bind<IClipboard>().ToConstant(new ClipboardImpl())
                 .Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
                 .Bind<IPlatformIconLoader>().ToConstant(new PlatformIconLoaderStub())
                 .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()

+ 60 - 9
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@@ -9,8 +9,10 @@ using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
+using Avalonia.Layout;
 using Avalonia.Media;
 using Avalonia.Platform;
+using Avalonia.Rendering;
 using Avalonia.UnitTests;
 using Moq;
 using Xunit;
@@ -820,18 +822,25 @@ namespace Avalonia.Controls.UnitTests
                     SelectionStart = selectionStart,
                     SelectionEnd = selectionEnd
                 };
-                
+
+                var impl = CreateMockTopLevelImpl();
+                var topLevel = new TestTopLevel(impl.Object)
+                {
+                    Template = CreateTopLevelTemplate()
+                };
+                topLevel.Content = target;
+
+                topLevel.ApplyTemplate();
+                topLevel.LayoutManager.ExecuteInitialLayoutPass();
+
                 target.ApplyTemplate();
 
                 if (fromClipboard)
                 {
-                    AvaloniaLocator.CurrentMutable.Bind<IClipboard>().ToSingleton<ClipboardStub>();
-
-                    var clipboard = AvaloniaLocator.CurrentMutable.GetRequiredService<IClipboard>();
-                    clipboard.SetTextAsync(textInput).GetAwaiter().GetResult();
+                    topLevel.Clipboard?.SetTextAsync(textInput).GetAwaiter().GetResult();
 
                     RaiseKeyEvent(target, Key.V, KeyModifiers.Control);
-                    clipboard.ClearAsync().GetAwaiter().GetResult();
+                    topLevel.Clipboard?.ClearAsync().GetAwaiter().GetResult();
                 }
                 else
                 {
@@ -859,10 +868,18 @@ namespace Avalonia.Controls.UnitTests
                     AcceptsReturn = true,
                     AcceptsTab = true
                 };
+
+                var impl = CreateMockTopLevelImpl();
+                var topLevel = new TestTopLevel(impl.Object)
+                {
+                    Template = CreateTopLevelTemplate()
+                };
+                topLevel.Content = target;
+                topLevel.ApplyTemplate();
+                topLevel.LayoutManager.ExecuteInitialLayoutPass();
+
                 target.SelectionStart = 1;
                 target.SelectionEnd = 3;
-                AvaloniaLocator.CurrentMutable
-                    .Bind<Input.Platform.IClipboard>().ToSingleton<ClipboardStub>();
 
                 RaiseKeyEvent(target, key, modifiers);
                 RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); // undo
@@ -951,7 +968,7 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
-        private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard
+        internal class ClipboardStub : IClipboard // in order to get tests working that use the clipboard
         {
             private string _text;
 
@@ -976,6 +993,40 @@ namespace Avalonia.Controls.UnitTests
             public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
         }
 
+        private class TestTopLevel : TopLevel
+        {
+            private readonly ILayoutManager _layoutManager;
+
+            public TestTopLevel(ITopLevelImpl impl, ILayoutManager layoutManager = null)
+                : base(impl)
+            {
+                _layoutManager = layoutManager ?? new LayoutManager(this);
+            }
+
+            protected override ILayoutManager CreateLayoutManager() => _layoutManager;
+        }
+
+        private static Mock<ITopLevelImpl> CreateMockTopLevelImpl()
+        {
+            var clipboard = new Mock<ITopLevelImpl>();
+            clipboard.Setup(r => r.CreateRenderer(It.IsAny<IRenderRoot>()))
+                .Returns(RendererMocks.CreateRenderer().Object);
+            clipboard.Setup(r => r.TryGetFeature(typeof(IClipboard)))
+                .Returns(new ClipboardStub());
+            clipboard.SetupGet(x => x.RenderScaling).Returns(1);
+            return clipboard;
+        }
+
+        private static FuncControlTemplate<TestTopLevel> CreateTopLevelTemplate()
+        {
+            return new FuncControlTemplate<TestTopLevel>((x, scope) =>
+                new ContentPresenter
+                {
+                    Name = "PART_ContentPresenter",
+                    [!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty],
+                }.RegisterInNameScope(scope));
+        }
+
         private class TestContextMenu : ContextMenu
         {
             public TestContextMenu()

+ 69 - 14
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@@ -11,6 +11,7 @@ using Avalonia.Input.Platform;
 using Avalonia.Layout;
 using Avalonia.Media;
 using Avalonia.Platform;
+using Avalonia.Rendering;
 using Avalonia.UnitTests;
 using Moq;
 using Xunit;
@@ -760,18 +761,24 @@ namespace Avalonia.Controls.UnitTests
                     SelectionStart = selectionStart,
                     SelectionEnd = selectionEnd
                 };
-                
+
+                var impl = CreateMockTopLevelImpl();
+                var topLevel = new TestTopLevel(impl.Object)
+                {
+                    Template = CreateTopLevelTemplate()
+                };
+                topLevel.Content = target;
+                topLevel.ApplyTemplate();
+                topLevel.LayoutManager.ExecuteInitialLayoutPass();
+
                 target.Measure(Size.Infinity);
                 
                 if (fromClipboard)
                 {
-                    AvaloniaLocator.CurrentMutable.Bind<IClipboard>().ToSingleton<ClipboardStub>();
-                    
-                    var clipboard = AvaloniaLocator.CurrentMutable.GetRequiredService<IClipboard>();
-                    clipboard.SetTextAsync(textInput).GetAwaiter().GetResult();
+                    topLevel.Clipboard?.SetTextAsync(textInput).GetAwaiter().GetResult();
 
                     RaiseKeyEvent(target, Key.V, KeyModifiers.Control);
-                    clipboard.ClearAsync().GetAwaiter().GetResult();
+                    topLevel.Clipboard?.ClearAsync().GetAwaiter().GetResult();
                 }
                 else
                 {
@@ -799,11 +806,19 @@ namespace Avalonia.Controls.UnitTests
                     AcceptsReturn = true,
                     AcceptsTab = true
                 };
+
+                var impl = CreateMockTopLevelImpl();
+                var topLevel = new TestTopLevel(impl.Object)
+                {
+                    Template = CreateTopLevelTemplate()
+                };
+                topLevel.Content = target;
+                topLevel.ApplyTemplate();
+                topLevel.LayoutManager.ExecuteInitialLayoutPass();
+
                 target.ApplyTemplate();
                 target.SelectionStart = 1;
                 target.SelectionEnd = 3;
-                AvaloniaLocator.CurrentMutable
-                    .Bind<Input.Platform.IClipboard>().ToSingleton<ClipboardStub>();
 
                 RaiseKeyEvent(target, key, modifiers);
                 RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); // undo
@@ -872,15 +887,21 @@ namespace Avalonia.Controls.UnitTests
                     AcceptsReturn= true
                 };
 
-                target.Measure(Size.Infinity);
+                var impl = CreateMockTopLevelImpl();
+                var topLevel = new TestTopLevel(impl.Object)
+                {
+                    Template = CreateTopLevelTemplate()
+                };
+                topLevel.Content = target;
+                topLevel.ApplyTemplate();
+                topLevel.LayoutManager.ExecuteInitialLayoutPass();
 
-                AvaloniaLocator.CurrentMutable.Bind<IClipboard>().ToSingleton<ClipboardStub>();
+                target.Measure(Size.Infinity);
 
-                var clipboard = AvaloniaLocator.CurrentMutable.GetRequiredService<IClipboard>();
-                clipboard.SetTextAsync(Environment.NewLine).GetAwaiter().GetResult();
+                topLevel.Clipboard?.SetTextAsync(Environment.NewLine).GetAwaiter().GetResult();
 
                 RaiseKeyEvent(target, Key.V, KeyModifiers.Control);
-                clipboard.ClearAsync().GetAwaiter().GetResult();
+                topLevel.Clipboard?.ClearAsync().GetAwaiter().GetResult();
 
                 RaiseTextEvent(target, Environment.NewLine);
 
@@ -1176,7 +1197,41 @@ namespace Avalonia.Controls.UnitTests
 
             public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
         }
-        
+
+        private class TestTopLevel : TopLevel
+        {
+            private readonly ILayoutManager _layoutManager;
+
+            public TestTopLevel(ITopLevelImpl impl, ILayoutManager layoutManager = null)
+                : base(impl)
+            {
+                _layoutManager = layoutManager ?? new LayoutManager(this);
+            }
+
+            protected override ILayoutManager CreateLayoutManager() => _layoutManager;
+        }
+
+        private static Mock<ITopLevelImpl> CreateMockTopLevelImpl()
+        {
+            var clipboard = new Mock<ITopLevelImpl>();
+            clipboard.Setup(r => r.CreateRenderer(It.IsAny<IRenderRoot>()))
+                .Returns(RendererMocks.CreateRenderer().Object);
+            clipboard.Setup(r => r.TryGetFeature(typeof(IClipboard)))
+                .Returns(new ClipboardStub());
+            clipboard.SetupGet(x => x.RenderScaling).Returns(1);
+            return clipboard;
+        }
+
+        private static FuncControlTemplate<TestTopLevel> CreateTopLevelTemplate()
+        {
+            return new FuncControlTemplate<TestTopLevel>((x, scope) =>
+                new ContentPresenter
+                {
+                    Name = "PART_ContentPresenter",
+                    [!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty],
+                }.RegisterInNameScope(scope));
+        }
+
         private class TestContextMenu : ContextMenu
         {
             public TestContextMenu()

+ 2 - 0
tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

@@ -2,6 +2,7 @@ using System;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Input.Raw;
 using Avalonia.Layout;
 using Avalonia.LogicalTree;
@@ -11,6 +12,7 @@ using Avalonia.Styling;
 using Avalonia.UnitTests;
 using Moq;
 using Xunit;
+using static Avalonia.Controls.UnitTests.MaskedTextBoxTests;
 
 namespace Avalonia.Controls.UnitTests
 {