Browse Source

Merge branch 'AvaloniaUI:master' into master

pull/6491/head
Sergey Mikolaytis 5 years ago
committed by GitHub
parent
commit
ca4268cc1b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      src/Avalonia.Controls/ComboBox.cs
  2. 38
      src/Avalonia.Controls/Mixins/DisposableMixin.cs
  3. 41
      src/Avalonia.Controls/Primitives/Popup.cs
  4. 99
      src/Avalonia.Controls/SystemDialog.cs
  5. 11
      src/Avalonia.Input/Navigation/TabNavigation.cs
  6. 1
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  7. 3
      src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs
  8. 1
      src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs
  9. 2
      src/Windows/Avalonia.Win32/FramebufferManager.cs
  10. 7
      src/Windows/Avalonia.Win32/PlatformConstants.cs
  11. 6
      src/Windows/Avalonia.Win32/SystemDialogImpl.cs
  12. 4
      src/Windows/Avalonia.Win32/Win32GlManager.cs
  13. 2
      src/Windows/Avalonia.Win32/Win32Platform.cs
  14. 4
      src/Windows/Avalonia.Win32/WindowImpl.cs
  15. 1
      tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs
  16. 3
      tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs
  17. 27
      tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs
  18. 2
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

30
src/Avalonia.Controls/ComboBox.cs

@ -1,6 +1,8 @@
using System;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
@ -80,7 +82,7 @@ namespace Avalonia.Controls
private bool _isDropDownOpen;
private Popup _popup;
private object _selectionBoxItem;
private IDisposable _subscriptionsOnOpen;
private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable();
/// <summary>
/// Initializes static members of the <see cref="ComboBox"/> class.
@ -291,6 +293,7 @@ namespace Avalonia.Controls
_popup = e.NameScope.Get<Popup>("PART_Popup");
_popup.Opened += PopupOpened;
_popup.Closed += PopupClosed;
}
internal void ItemFocused(ComboBoxItem dropDownItem)
@ -303,8 +306,7 @@ namespace Avalonia.Controls
private void PopupClosed(object sender, EventArgs e)
{
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
_subscriptionsOnOpen.Clear();
if (CanFocus(this))
{
@ -316,20 +318,34 @@ namespace Avalonia.Controls
{
TryFocusSelectedItem();
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
_subscriptionsOnOpen.Clear();
var toplevel = this.GetVisualRoot() as TopLevel;
if (toplevel != null)
{
_subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) =>
toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) =>
{
//eat wheel scroll event outside dropdown popup while it's open
if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)
{
ev.Handled = true;
}
}, Interactivity.RoutingStrategies.Tunnel);
}, Interactivity.RoutingStrategies.Tunnel).DisposeWith(_subscriptionsOnOpen);
}
this.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
foreach (var parent in this.GetVisualAncestors().OfType<IControl>())
{
parent.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
}
}
private void IsVisibleChanged(bool isVisible)
{
if (!isVisible && IsDropDownOpen)
{
IsDropDownOpen = false;
}
}

38
src/Avalonia.Controls/Mixins/DisposableMixin.cs

@ -0,0 +1,38 @@
using System;
using System.Reactive.Disposables;
namespace Avalonia.Controls.Mixins
{
/// <summary>
/// Extension methods associated with the IDisposable interface.
/// </summary>
public static class DisposableMixin
{
/// <summary>
/// Ensures the provided disposable is disposed with the specified <see cref="CompositeDisposable"/>.
/// </summary>
/// <typeparam name="T">
/// The type of the disposable.
/// </typeparam>
/// <param name="item">
/// The disposable we are going to want to be disposed by the CompositeDisposable.
/// </param>
/// <param name="compositeDisposable">
/// The <see cref="CompositeDisposable"/> to which <paramref name="item"/> will be added.
/// </param>
/// <returns>
/// The disposable.
/// </returns>
public static T DisposeWith<T>(this T item, CompositeDisposable compositeDisposable)
where T : IDisposable
{
if (compositeDisposable is null)
{
throw new ArgumentNullException(nameof(compositeDisposable));
}
compositeDisposable.Add(item);
return item;
}
}
}

41
src/Avalonia.Controls/Primitives/Popup.cs

@ -2,6 +2,7 @@ using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
@ -393,18 +394,8 @@ namespace Avalonia.Controls.Primitives
var handlerCleanup = new CompositeDisposable(5);
void DeferCleanup(IDisposable? disposable)
{
if (disposable is null)
{
return;
}
handlerCleanup.Add(disposable);
}
DeferCleanup(popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty));
popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty).DisposeWith(handlerCleanup);
popupHost.SetChild(Child);
((ISetLogicalParent)popupHost).SetParent(this);
@ -418,19 +409,19 @@ namespace Avalonia.Controls.Primitives
PlacementConstraintAdjustment,
PlacementRect);
DeferCleanup(SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,
SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,
(x, handler) => x.TemplateApplied += handler,
(x, handler) => x.TemplateApplied -= handler));
(x, handler) => x.TemplateApplied -= handler).DisposeWith(handlerCleanup);
if (topLevel is Window window)
{
DeferCleanup(SubscribeToEventHandler<Window, EventHandler>(window, WindowDeactivated,
SubscribeToEventHandler<Window, EventHandler>(window, WindowDeactivated,
(x, handler) => x.Deactivated += handler,
(x, handler) => x.Deactivated -= handler));
(x, handler) => x.Deactivated -= handler).DisposeWith(handlerCleanup);
DeferCleanup(SubscribeToEventHandler<IWindowImpl, Action>(window.PlatformImpl, WindowLostFocus,
SubscribeToEventHandler<IWindowImpl, Action>(window.PlatformImpl, WindowLostFocus,
(x, handler) => x.LostFocus += handler,
(x, handler) => x.LostFocus -= handler));
(x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup);
}
else
{
@ -438,13 +429,13 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup)
{
DeferCleanup(SubscribeToEventHandler<Popup, EventHandler<EventArgs>>(popup, ParentClosed,
SubscribeToEventHandler<Popup, EventHandler<EventArgs>>(popup, ParentClosed,
(x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler));
(x, handler) => x.Closed -= handler).DisposeWith(handlerCleanup);
}
}
DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));
InputManager.Instance?.Process.Subscribe(ListenForNonClientClick).DisposeWith(handlerCleanup);
var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state =>
{
@ -466,17 +457,17 @@ namespace Avalonia.Controls.Primitives
dismissLayer.IsVisible = true;
dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement;
DeferCleanup(Disposable.Create(() =>
Disposable.Create(() =>
{
dismissLayer.IsVisible = false;
dismissLayer.InputPassThroughElement = null;
}));
}).DisposeWith(handlerCleanup);
DeferCleanup(SubscribeToEventHandler<LightDismissOverlayLayer, EventHandler<PointerPressedEventArgs>>(
SubscribeToEventHandler<LightDismissOverlayLayer, EventHandler<PointerPressedEventArgs>>(
dismissLayer,
PointerPressedDismissOverlay,
(x, handler) => x.PointerPressed += handler,
(x, handler) => x.PointerPressed -= handler));
(x, handler) => x.PointerPressed -= handler).DisposeWith(handlerCleanup);
}
}

99
src/Avalonia.Controls/SystemDialog.cs

@ -4,30 +4,65 @@ using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Base class for system file dialogs.
/// </summary>
public abstract class FileDialog : FileSystemDialog
{
/// <summary>
/// Gets or sets a collection of filters which determine the types of files displayed in an
/// <see cref="OpenFileDialog"/> or an <see cref="SaveFileDialog"/>.
/// </summary>
public List<FileDialogFilter> Filters { get; set; } = new List<FileDialogFilter>();
public string InitialFileName { get; set; }
/// <summary>
/// Gets or sets initial file name that is displayed when the dialog is opened.
/// </summary>
public string? InitialFileName { get; set; }
}
/// <summary>
/// Base class for system file and directory dialogs.
/// </summary>
public abstract class FileSystemDialog : SystemDialog
{
[Obsolete("Use Directory")]
public string InitialDirectory
public string? InitialDirectory
{
get => Directory;
set => Directory = value;
}
public string Directory { get; set; }
/// <summary>
/// Gets or sets the initial directory that will be displayed when the file system dialog
/// is opened.
/// </summary>
public string? Directory { get; set; }
}
/// <summary>
/// Represents a system dialog that prompts the user to select a location for saving a file.
/// </summary>
public class SaveFileDialog : FileDialog
{
public string DefaultExtension { get; set; }
/// <summary>
/// Gets or sets the default extension to be used to save the file (including the period ".").
/// </summary>
public string? DefaultExtension { get; set; }
public async Task<string> ShowAsync(Window parent)
/// <summary>
/// Shows the save file dialog.
/// </summary>
/// <param name="parent">The parent window.</param>
/// <returns>
/// A task that on completion contains the full path of the save location, or null if the
/// dialog was canceled.
/// </returns>
public async Task<string?> ShowAsync(Window parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
@ -37,11 +72,25 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Represents a system dialog that allows the user to select one or more files to open.
/// </summary>
public class OpenFileDialog : FileDialog
{
/// <summary>
/// Gets or sets a value indicating whether the user can select multiple files.
/// </summary>
public bool AllowMultiple { get; set; }
public Task<string[]> ShowAsync(Window parent)
/// <summary>
/// Shows the open file dialog.
/// </summary>
/// <param name="parent">The parent window.</param>
/// <returns>
/// A task that on completion returns an array containing the full path to the selected
/// files, or null if the dialog was canceled.
/// </returns>
public Task<string[]?> ShowAsync(Window parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
@ -49,15 +98,27 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Represents a system dialog that allows the user to select a directory.
/// </summary>
public class OpenFolderDialog : FileSystemDialog
{
[Obsolete("Use Directory")]
public string DefaultDirectory
public string? DefaultDirectory
{
get => Directory;
set => Directory = value;
}
public Task<string> ShowAsync(Window parent)
/// <summary>
/// Shows the open folder dialog.
/// </summary>
/// <param name="parent">The parent window.</param>
/// <returns>
/// A task that on completion returns the full path of the selected directory, or null if the
/// dialog was canceled.
/// </returns>
public Task<string?> ShowAsync(Window parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
@ -65,14 +126,32 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Base class for system dialogs.
/// </summary>
public abstract class SystemDialog
{
public string Title { get; set; }
/// <summary>
/// Gets or sets the dialog title.
/// </summary>
public string? Title { get; set; }
}
/// <summary>
/// Represents a filter in an <see cref="OpenFileDialog"/> or an <see cref="SaveFileDialog"/>.
/// </summary>
public class FileDialogFilter
{
public string Name { get; set; }
/// <summary>
/// Gets or sets the name of the filter, e.g. ("Text files (.txt)").
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets a list of file extensions matched by the filter (e.g. "txt" or "*" for all
/// files).
/// </summary>
public List<string> Extensions { get; set; } = new List<string>();
}
}

11
src/Avalonia.Input/Navigation/TabNavigation.cs

@ -234,7 +234,7 @@ namespace Avalonia.Input.Navigation
// Return the first visible element.
var uiElement = e as InputElement;
if (uiElement is null || uiElement.IsVisible)
if (uiElement is null || IsVisibleAndEnabled(uiElement))
{
if (e is IVisual elementAsVisual)
{
@ -245,7 +245,7 @@ namespace Avalonia.Input.Navigation
{
if (children[i] is InputElement ie)
{
if (ie.IsVisible)
if (IsVisibleAndEnabled(ie))
return ie;
else
{
@ -270,7 +270,7 @@ namespace Avalonia.Input.Navigation
// Return the last visible element.
var uiElement = e as InputElement;
if (uiElement == null || uiElement.IsVisible)
if (uiElement == null || IsVisibleAndEnabled(uiElement))
{
var elementAsVisual = e as IVisual;
@ -283,7 +283,7 @@ namespace Avalonia.Input.Navigation
{
if (children[i] is InputElement ie)
{
if (ie.IsVisible)
if (IsVisibleAndEnabled(ie))
return ie;
else
{
@ -600,7 +600,7 @@ namespace Avalonia.Input.Navigation
var vchild = children[i];
if (vchild == elementAsVisual)
break;
if (vchild.IsVisible == true && vchild is IInputElement ie)
if (vchild is IInputElement ie && IsVisibleAndEnabled(ie))
prev = ie;
}
return prev;
@ -668,5 +668,6 @@ namespace Avalonia.Input.Navigation
}
private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e);
private static bool IsVisibleAndEnabled(IInputElement e) => e.IsVisible && e.IsEnabled;
}
}

1
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

@ -12,6 +12,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\Avalonia.Win32\Avalonia.Win32.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\Rx.props" />
<Import Project="..\..\..\build\SharpDX.props" />

3
src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs

@ -1,4 +1,5 @@
using Avalonia.Platform;
using Avalonia.Win32;
using Avalonia.Win32.Interop;
using SharpDX;
using SharpDX.DXGI;
@ -21,7 +22,7 @@ namespace Avalonia.Direct2D1
protected override Size2F GetWindowDpi()
{
if (UnmanagedMethods.ShCoreAvailable)
if (UnmanagedMethods.ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8)
{
uint dpix, dpiy;

1
src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs

@ -150,6 +150,7 @@ namespace Avalonia.Win32.Interop.Wpf
if (_image == null || _oldDpi.X != dpi.X || _oldDpi.Y != dpi.Y)
{
_image = new D3DImage(dpi.X, dpi.Y);
_oldDpi = dpi;
}
_impl.ImageSource = _image;

2
src/Windows/Avalonia.Win32/FramebufferManager.cs

@ -87,7 +87,7 @@ namespace Avalonia.Win32
private Vector GetCurrentDpi()
{
if (UnmanagedMethods.ShCoreAvailable)
if (UnmanagedMethods.ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8)
{
var monitor =
UnmanagedMethods.MonitorFromWindow(_hwnd, UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONEAREST);

7
src/Windows/Avalonia.Win32/PlatformConstants.cs

@ -1,8 +1,13 @@
using System;
namespace Avalonia.Win32
{
static class PlatformConstants
public static class PlatformConstants
{
public const string WindowHandleType = "HWND";
public const string CursorHandleType = "HCURSOR";
public static readonly Version Windows8 = new Version(6, 2);
public static readonly Version Windows7 = new Version(6, 1);
}
}

6
src/Windows/Avalonia.Win32/SystemDialogImpl.cs

@ -19,7 +19,7 @@ namespace Avalonia.Win32
var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero;
return Task.Factory.StartNew(() =>
{
var result = Array.Empty<string>();
string[] result = default;
Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
Guid iid = UnmanagedMethods.ShellIds.IFileDialog;
@ -100,7 +100,7 @@ namespace Avalonia.Win32
{
return Task.Factory.StartNew(() =>
{
string result = string.Empty;
string result = default;
var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero;
Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog;
@ -164,7 +164,7 @@ namespace Avalonia.Win32
}
}
}
return "";
return default;
}
}
}

4
src/Windows/Avalonia.Win32/Win32GlManager.cs

@ -1,4 +1,3 @@
using System;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Angle;
using Avalonia.OpenGL.Egl;
@ -9,7 +8,6 @@ namespace Avalonia.Win32
{
static class Win32GlManager
{
private static readonly Version Windows7 = new Version(6, 1);
public static void Initialize()
{
@ -22,7 +20,7 @@ namespace Avalonia.Win32
return wgl;
}
if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > Windows7)
if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7)
{
var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay());

2
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -94,7 +94,7 @@ namespace Avalonia
namespace Avalonia.Win32
{
class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl
public class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl
{
private static readonly Win32Platform s_instance = new Win32Platform();
private static Thread _uiThread;

4
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -764,8 +764,8 @@ namespace Avalonia.Win32
RegisterTouchWindow(_hwnd, 0);
}
if (ShCoreAvailable)
{
if (ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8)
{
var monitor = MonitorFromWindow(
_hwnd,
MONITOR.MONITOR_DEFAULTTONEAREST);

1
tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs

@ -10,7 +10,6 @@ using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
using Xunit;
using System;
using Avalonia.Input.Raw;
using Factory = System.Func<int, System.Action<object>, Avalonia.Controls.Window, Avalonia.AvaloniaObject>;

3
tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs

@ -2,7 +2,6 @@
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.UnitTests;
using Moq;
using Xunit;
@ -126,7 +125,7 @@ namespace Avalonia.Input.UnitTests
{
private readonly Action _action;
public DelegateCommand(Action action) => _action = action;
public event EventHandler CanExecuteChanged;
public event EventHandler CanExecuteChanged { add { } remove { } }
public bool CanExecute(object parameter) => true;
public void Execute(object parameter) => _action();
}

27
tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs

@ -1225,5 +1225,32 @@ namespace Avalonia.Input.UnitTests
"Button2", "Button3", "Button5", "Button1", "Button6", "Button4"
}, result);
}
[Fact]
public void Cannot_Focus_Child_Of_Disabled_Control()
{
Button start;
Button expected;
var top = new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(start = new Button { Name = "Button1" }),
new Border
{
IsEnabled = false,
Child = new Button { Name = "Button2" },
},
(expected = new Button { Name = "Button3" }),
}
};
var current = (IInputElement)start;
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Same(expected, result);
}
}
}

2
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

@ -923,7 +923,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public bool HasResources => true;
public List<object> RequestedResources { get; } = new List<object>();
public event EventHandler OwnerChanged;
public event EventHandler OwnerChanged { add { } remove { } }
public void AddOwner(IResourceHost owner) => Owner = owner;
public void RemoveOwner(IResourceHost owner) => Owner = null;

Loading…
Cancel
Save