Browse Source

Merge branch 'master' into fixes/3245-scrollviewer-large-small-scroll

pull/3725/head
Dariusz Komosiński 6 years ago
committed by GitHub
parent
commit
6a3e3bb91e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      build/Rx.props
  2. 7
      samples/ControlCatalog/MainView.xaml.cs
  3. 1
      samples/ControlCatalog/Pages/DialogsPage.xaml
  4. 36
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  5. 18
      src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs
  6. 59
      src/Avalonia.Controls/ContextMenu.cs
  7. 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  8. 6
      src/Avalonia.Controls/Remote/RemoteWidget.cs
  9. 2
      src/Avalonia.Controls/Window.cs
  10. 2
      src/Avalonia.Controls/WindowBase.cs
  11. 2
      src/Avalonia.Input/FocusManager.cs
  12. 5
      src/Avalonia.Visuals/Media/Brush.cs
  13. 3
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  14. 2
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  15. 107
      src/Avalonia.X11/X11ImmediateRendererProxy.cs
  16. 1
      src/Avalonia.X11/X11Platform.cs
  17. 39
      src/Avalonia.X11/X11Window.cs
  18. 527
      src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs
  19. 1031
      src/Windows/Avalonia.Win32/WindowImpl.cs
  20. 178
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs
  21. 17
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  22. 78
      tests/Avalonia.LeakTests/ControlTests.cs
  23. 4
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs
  24. 12
      tests/Avalonia.Visuals.UnitTests/Media/BrushTests.cs

2
build/Rx.props

@ -1,5 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="System.Reactive" Version="4.1.6" />
<PackageReference Include="System.Reactive" Version="4.4.1" />
</ItemGroup>
</Project>

7
samples/ControlCatalog/MainView.xaml.cs

@ -60,8 +60,8 @@ namespace ControlCatalog
var decorations = this.Find<ComboBox>("Decorations");
decorations.SelectionChanged += (sender, e) =>
{
Window window = (Window)VisualRoot;
window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex;
if (VisualRoot is Window window)
window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex;
};
}
@ -69,7 +69,8 @@ namespace ControlCatalog
{
base.OnAttachedToVisualTree(e);
var decorations = this.Find<ComboBox>("Decorations");
decorations.SelectedIndex = (int)((Window)VisualRoot).SystemDecorations;
if (VisualRoot is Window window)
decorations.SelectedIndex = (int)window.SystemDecorations;
}
}
}

1
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -9,5 +9,6 @@
<Button Name="DecoratedWindow">Decorated window</Button>
<Button Name="DecoratedWindowDialog">Decorated window (dialog)</Button>
<Button Name="Dialog">Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (No taskbar icon)</Button>
</StackPanel>
</UserControl>

36
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -42,14 +42,15 @@ namespace ControlCatalog.Pages
new SaveFileDialog()
{
Title = "Save file",
Filters = GetFilters()
Filters = GetFilters(),
InitialFileName = "test.txt"
}.ShowAsync(GetWindow());
};
this.FindControl<Button>("SelectFolder").Click += delegate
{
new OpenFolderDialog()
{
Title = "Select folder"
Title = "Select folder",
}.ShowAsync(GetWindow());
};
this.FindControl<Button>("DecoratedWindow").Click += delegate
@ -61,14 +62,29 @@ namespace ControlCatalog.Pages
new DecoratedWindow().ShowDialog(GetWindow());
};
this.FindControl<Button>("Dialog").Click += delegate
{
var window = new Window();
window.Height = 200;
window.Width = 200;
window.Content = new TextBlock { Text = "Hello world!" };
window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
window.ShowDialog(GetWindow());
};
{
var window = CreateSampleWindow();
window.Height = 200;
window.ShowDialog(GetWindow());
};
this.FindControl<Button>("DialogNoTaskbar").Click += delegate
{
var window = CreateSampleWindow();
window.Height = 200;
window.ShowInTaskbar = false;
window.ShowDialog(GetWindow());
};
}
private Window CreateSampleWindow()
{
var window = new Window();
window.Height = 200;
window.Width = 200;
window.Content = new TextBlock { Text = "Hello world!" };
window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
return window;
}
Window GetWindow() => (Window)this.VisualRoot;

18
src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs

@ -27,7 +27,23 @@ namespace Avalonia.Data.Converters
/// <inheritdoc/>
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
{
var converted = values.OfType<TIn>().ToList();
//standard OfType skip null values, even they are valid for the Type
static IEnumerable<TIn> OfTypeWithDefaultSupport(IList<object> list)
{
foreach (object obj in list)
{
if (obj is TIn result)
{
yield return result;
}
else if (Equals(obj, default(TIn)))
{
yield return default(TIn);
}
}
}
var converted = OfTypeWithDefaultSupport(values).ToList();
if (converted.Count == values.Count)
{

59
src/Avalonia.Controls/ContextMenu.cs

@ -20,6 +20,8 @@ namespace Avalonia.Controls
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup;
private Control _attachedControl;
private IInputElement _previousFocus;
/// <summary>
/// Initializes a new instance of the <see cref="ContextMenu"/> class.
@ -69,13 +71,16 @@ namespace Avalonia.Controls
{
var control = (Control)e.Sender;
if (e.OldValue != null)
if (e.OldValue is ContextMenu oldMenu)
{
control.PointerReleased -= ControlPointerReleased;
oldMenu._attachedControl = null;
((ISetLogicalParent)oldMenu._popup)?.SetParent(null);
}
if (e.NewValue != null)
if (e.NewValue is ContextMenu newMenu)
{
newMenu._attachedControl = control;
control.PointerReleased += ControlPointerReleased;
}
}
@ -91,8 +96,18 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param>
public void Open(Control control)
{
if (control == null)
if (control is null && _attachedControl is null)
{
throw new ArgumentNullException(nameof(control));
}
if (control is object && _attachedControl is object && control != _attachedControl)
{
throw new ArgumentException(
"Cannot show ContentMenu on a different control to the one it is attached to.",
nameof(control));
}
if (IsOpen)
{
return;
@ -145,36 +160,38 @@ namespace Avalonia.Controls
return new MenuItemContainerGenerator(this);
}
private void CloseCore()
{
SelectedIndex = -1;
IsOpen = false;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
}
private void PopupOpened(object sender, EventArgs e)
{
_previousFocus = FocusManager.Instance?.Current;
Focus();
}
private void PopupClosed(object sender, EventArgs e)
{
var contextMenu = (sender as Popup)?.Child as ContextMenu;
if (contextMenu != null)
foreach (var i in LogicalChildren)
{
foreach (var i in contextMenu.GetLogicalChildren().OfType<MenuItem>())
if (i is MenuItem menuItem)
{
i.IsSubMenuOpen = false;
menuItem.IsSubMenuOpen = false;
}
}
contextMenu.CloseCore();
SelectedIndex = -1;
IsOpen = false;
if (_attachedControl is null)
{
((ISetLogicalParent)_popup).SetParent(null);
}
// HACK: Reset the focus when the popup is closed. We need to fix this so it's automatic.
FocusManager.Instance?.Focus(_previousFocus);
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
}
private static void ControlPointerReleased(object sender, PointerReleasedEventArgs e)

2
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -96,7 +96,7 @@ namespace Avalonia.Controls.Platform
root.Deactivated -= WindowDeactivated;
}
_inputManagerSubscription!.Dispose();
_inputManagerSubscription?.Dispose();
Menu = null;
_root = null;

6
src/Avalonia.Controls/Remote/RemoteWidget.cs

@ -75,7 +75,11 @@ namespace Avalonia.Controls.Remote
var fmt = (PixelFormat) _lastFrame.Format;
if (_bitmap == null || _bitmap.PixelSize.Width != _lastFrame.Width ||
_bitmap.PixelSize.Height != _lastFrame.Height)
_bitmap = new WriteableBitmap(new PixelSize(_lastFrame.Width, _lastFrame.Height), new Vector(96, 96), fmt);
{
_bitmap?.Dispose();
_bitmap = new WriteableBitmap(new PixelSize(_lastFrame.Width, _lastFrame.Height),
new Vector(96, 96), fmt);
}
using (var l = _bitmap.Lock())
{
var lineLen = (fmt == PixelFormat.Rgb565 ? 2 : 4) * _lastFrame.Width;

2
src/Avalonia.Controls/Window.cs

@ -186,6 +186,8 @@ namespace Avalonia.Controls
impl.WindowStateChanged = HandleWindowStateChanged;
_maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x));
PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar);
}
/// <summary>

2
src/Avalonia.Controls/WindowBase.cs

@ -255,7 +255,7 @@ namespace Avalonia.Controls
if (scope != null)
{
FocusManager.Instance.SetFocusScope(scope);
FocusManager.Instance?.SetFocusScope(scope);
}
IsActive = true;

2
src/Avalonia.Input/FocusManager.cs

@ -168,7 +168,7 @@ namespace Avalonia.Input
{
var scope = control as IFocusScope;
if (scope != null)
if (scope != null && control.VisualRoot?.IsVisible == true)
{
yield return scope;
}

5
src/Avalonia.Visuals/Media/Brush.cs

@ -19,6 +19,11 @@ namespace Avalonia.Media
/// <inheritdoc/>
public event EventHandler Invalidated;
static Brush()
{
AffectsRender<Brush>(OpacityProperty);
}
/// <summary>
/// Gets or sets the opacity of the brush.
/// </summary>

3
src/Avalonia.X11/NativeDialogs/Gtk.cs

@ -190,6 +190,9 @@ namespace Avalonia.X11.NativeDialogs
[DllImport(GtkName)]
public static extern void gtk_file_chooser_set_filename(IntPtr chooser, Utf8Buffer file);
[DllImport(GtkName)]
public static extern void gtk_file_chooser_set_current_name(IntPtr chooser, Utf8Buffer file);
[DllImport(GtkName)]
public static extern IntPtr gtk_file_filter_new();

2
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -91,7 +91,7 @@ namespace Avalonia.X11.NativeDialogs
gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel);
if (initialFileName != null)
using (var fn = new Utf8Buffer(initialFileName))
gtk_file_chooser_set_filename(dlg, fn);
gtk_file_chooser_set_current_name(dlg, fn);
gtk_window_present(dlg);
return tcs.Task;
}

107
src/Avalonia.X11/X11ImmediateRendererProxy.cs

@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.X11
{
public class X11ImmediateRendererProxy : IRenderer, IRenderLoopTask
{
private readonly IRenderLoop _loop;
private ImmediateRenderer _renderer;
private bool _invalidated;
private object _lock = new object();
public X11ImmediateRendererProxy(IVisual root, IRenderLoop loop)
{
_loop = loop;
_renderer = new ImmediateRenderer(root);
}
public void Dispose()
{
_renderer.Dispose();
}
public bool DrawFps
{
get => _renderer.DrawFps;
set => _renderer.DrawFps = value;
}
public bool DrawDirtyRects
{
get => _renderer.DrawDirtyRects;
set => _renderer.DrawDirtyRects = value;
}
public event EventHandler<SceneInvalidatedEventArgs> SceneInvalidated
{
add => _renderer.SceneInvalidated += value;
remove => _renderer.SceneInvalidated -= value;
}
public void AddDirty(IVisual visual)
{
lock (_lock)
_invalidated = true;
_renderer.AddDirty(visual);
}
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter)
{
return _renderer.HitTest(p, root, filter);
}
public IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter)
{
return _renderer.HitTestFirst(p, root, filter);
}
public void RecalculateChildren(IVisual visual)
{
_renderer.RecalculateChildren(visual);
}
public void Resized(Size size)
{
_renderer.Resized(size);
}
public void Paint(Rect rect)
{
_invalidated = false;
_renderer.Paint(rect);
}
public void Start()
{
_loop.Add(this);
_renderer.Start();
}
public void Stop()
{
_loop.Remove(this);
_renderer.Stop();
}
public bool NeedsUpdate => false;
public void Update(TimeSpan time)
{
}
public void Render()
{
if (_invalidated)
{
lock (_lock)
_invalidated = false;
Dispatcher.UIThread.Post(() => Paint(new Rect(0, 0, 100000, 100000)));
}
}
}
}

1
src/Avalonia.X11/X11Platform.cs

@ -96,6 +96,7 @@ namespace Avalonia
public bool UseGpu { get; set; } = true;
public bool OverlayPopups { get; set; }
public bool UseDBusMenu { get; set; }
public bool UseDeferredRendering { get; set; } = true;
public List<string> GlxRendererBlacklist { get; set; } = new List<string>
{

39
src/Avalonia.X11/X11Window.cs

@ -27,7 +27,6 @@ namespace Avalonia.X11
private readonly IWindowImpl _popupParent;
private readonly bool _popup;
private readonly X11Info _x11;
private bool _invalidated;
private XConfigureEvent? _configure;
private PixelPoint? _configurePoint;
private bool _triggeredExpose;
@ -41,6 +40,7 @@ namespace Avalonia.X11
private IntPtr _xic;
private IntPtr _renderHandle;
private bool _mapped;
private bool _wasMappedAtLeastOnce = false;
private HashSet<X11Window> _transientChildren = new HashSet<X11Window>();
private X11Window _transientParent;
private double? _scalingOverride;
@ -308,8 +308,13 @@ namespace Avalonia.X11
public Action Closed { get; set; }
public Action<PixelPoint> PositionChanged { get; set; }
public IRenderer CreateRenderer(IRenderRoot root) =>
new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>());
public IRenderer CreateRenderer(IRenderRoot root)
{
var loop = AvaloniaLocator.Current.GetService<IRenderLoop>();
return _platform.Options.UseDeferredRendering ?
new DeferredRenderer(root, loop) :
(IRenderer)new X11ImmediateRendererProxy(root, loop);
}
void OnEvent(XEvent ev)
{
@ -683,20 +688,12 @@ namespace Avalonia.X11
void DoPaint()
{
_invalidated = false;
Paint?.Invoke(new Rect());
}
public void Invalidate(Rect rect)
{
if(_invalidated)
return;
_invalidated = true;
Dispatcher.UIThread.InvokeAsync(() =>
{
if (_mapped)
DoPaint();
});
}
public IInputRoot InputRoot => _inputRoot;
@ -758,7 +755,15 @@ namespace Avalonia.X11
_transientParent = window;
_transientParent?._transientChildren.Add(this);
if (informServer)
XSetTransientForHint(_x11.Display, _handle, _transientParent?._handle ?? IntPtr.Zero);
SetTransientForHint(_transientParent?._handle);
}
void SetTransientForHint(IntPtr? parent)
{
if (parent == null || parent == IntPtr.Zero)
XDeleteProperty(_x11.Display, _handle, _x11.Atoms.XA_WM_TRANSIENT_FOR);
else
XSetTransientForHint(_x11.Display, _handle, parent.Value);
}
public void Show()
@ -769,6 +774,7 @@ namespace Avalonia.X11
void ShowCore()
{
_wasMappedAtLeastOnce = true;
XMapWindow(_x11.Display, _handle);
XFlush(_x11.Display);
}
@ -816,7 +822,7 @@ namespace Avalonia.X11
XConfigureResizeWindow(_x11.Display, _renderHandle, pixelSize);
XFlush(_x11.Display);
if (force || (_popup && needImmediatePopupResize))
if (force || !_wasMappedAtLeastOnce || (_popup && needImmediatePopupResize))
{
_realSize = pixelSize;
Resized?.Invoke(ClientSize);
@ -857,6 +863,11 @@ namespace Avalonia.X11
XConfigureWindow(_x11.Display, _handle, ChangeWindowFlags.CWX | ChangeWindowFlags.CWY,
ref changes);
XFlush(_x11.Display);
if (!_wasMappedAtLeastOnce)
{
_position = value;
PositionChanged?.Invoke(value);
}
}
}

527
src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs

@ -0,0 +1,527 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Win32.Input;
using static Avalonia.Win32.Interop.UnmanagedMethods;
namespace Avalonia.Win32
{
public partial class WindowImpl
{
[SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation",
Justification = "Using Win32 naming for consistency.")]
protected virtual unsafe IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
const double wheelDelta = 120.0;
uint timestamp = unchecked((uint)GetMessageTime());
RawInputEventArgs e = null;
switch ((WindowsMessage)msg)
{
case WindowsMessage.WM_ACTIVATE:
{
var wa = (WindowActivate)(ToInt32(wParam) & 0xffff);
switch (wa)
{
case WindowActivate.WA_ACTIVE:
case WindowActivate.WA_CLICKACTIVE:
{
Activated?.Invoke();
break;
}
case WindowActivate.WA_INACTIVE:
{
Deactivated?.Invoke();
break;
}
}
return IntPtr.Zero;
}
case WindowsMessage.WM_NCCALCSIZE:
{
if (ToInt32(wParam) == 1 && !HasFullDecorations)
{
return IntPtr.Zero;
}
break;
}
case WindowsMessage.WM_CLOSE:
{
bool? preventClosing = Closing?.Invoke();
if (preventClosing == true)
{
return IntPtr.Zero;
}
break;
}
case WindowsMessage.WM_DESTROY:
{
//Window doesn't exist anymore
_hwnd = IntPtr.Zero;
//Remove root reference to this class, so unmanaged delegate can be collected
s_instances.Remove(this);
Closed?.Invoke();
if (_parent != null)
{
_parent._disabledBy.Remove(this);
_parent.UpdateEnabled();
}
_mouseDevice.Dispose();
_touchDevice?.Dispose();
//Free other resources
Dispose();
return IntPtr.Zero;
}
case WindowsMessage.WM_DPICHANGED:
{
var dpi = ToInt32(wParam) & 0xffff;
var newDisplayRect = Marshal.PtrToStructure<RECT>(lParam);
_scaling = dpi / 96.0;
ScalingChanged?.Invoke(_scaling);
SetWindowPos(hWnd,
IntPtr.Zero,
newDisplayRect.left,
newDisplayRect.top,
newDisplayRect.right - newDisplayRect.left,
newDisplayRect.bottom - newDisplayRect.top,
SetWindowPosFlags.SWP_NOZORDER |
SetWindowPosFlags.SWP_NOACTIVATE);
return IntPtr.Zero;
}
case WindowsMessage.WM_KEYDOWN:
case WindowsMessage.WM_SYSKEYDOWN:
{
e = new RawKeyEventArgs(
WindowsKeyboardDevice.Instance,
timestamp,
_owner,
RawKeyEventType.KeyDown,
KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)),
WindowsKeyboardDevice.Instance.Modifiers);
break;
}
case WindowsMessage.WM_MENUCHAR:
{
// mute the system beep
return (IntPtr)((int)MenuCharParam.MNC_CLOSE << 16);
}
case WindowsMessage.WM_KEYUP:
case WindowsMessage.WM_SYSKEYUP:
{
e = new RawKeyEventArgs(
WindowsKeyboardDevice.Instance,
timestamp,
_owner,
RawKeyEventType.KeyUp,
KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)),
WindowsKeyboardDevice.Instance.Modifiers);
break;
}
case WindowsMessage.WM_CHAR:
{
// Ignore control chars
if (ToInt32(wParam) >= 32)
{
e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, _owner,
new string((char)ToInt32(wParam), 1));
}
break;
}
case WindowsMessage.WM_LBUTTONDOWN:
case WindowsMessage.WM_RBUTTONDOWN:
case WindowsMessage.WM_MBUTTONDOWN:
case WindowsMessage.WM_XBUTTONDOWN:
{
if (ShouldIgnoreTouchEmulatedMessage())
{
break;
}
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
(WindowsMessage)msg switch
{
WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown,
WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown,
WindowsMessage.WM_MBUTTONDOWN => RawPointerEventType.MiddleButtonDown,
WindowsMessage.WM_XBUTTONDOWN =>
HighWord(ToInt32(wParam)) == 1 ?
RawPointerEventType.XButton1Down :
RawPointerEventType.XButton2Down
},
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_LBUTTONUP:
case WindowsMessage.WM_RBUTTONUP:
case WindowsMessage.WM_MBUTTONUP:
case WindowsMessage.WM_XBUTTONUP:
{
if (ShouldIgnoreTouchEmulatedMessage())
{
break;
}
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
(WindowsMessage)msg switch
{
WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp,
WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp,
WindowsMessage.WM_MBUTTONUP => RawPointerEventType.MiddleButtonUp,
WindowsMessage.WM_XBUTTONUP =>
HighWord(ToInt32(wParam)) == 1 ?
RawPointerEventType.XButton1Up :
RawPointerEventType.XButton2Up,
},
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSEMOVE:
{
if (ShouldIgnoreTouchEmulatedMessage())
{
break;
}
if (!_trackingMouse)
{
var tm = new TRACKMOUSEEVENT
{
cbSize = Marshal.SizeOf<TRACKMOUSEEVENT>(),
dwFlags = 2,
hwndTrack = _hwnd,
dwHoverTime = 0,
};
TrackMouseEvent(ref tm);
}
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
RawPointerEventType.Move,
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSEWHEEL:
{
e = new RawMouseWheelEventArgs(
_mouseDevice,
timestamp,
_owner,
PointToClient(PointFromLParam(lParam)),
new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSEHWHEEL:
{
e = new RawMouseWheelEventArgs(
_mouseDevice,
timestamp,
_owner,
PointToClient(PointFromLParam(lParam)),
new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSELEAVE:
{
_trackingMouse = false;
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
RawPointerEventType.LeaveWindow,
new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers);
break;
}
case WindowsMessage.WM_NCLBUTTONDOWN:
case WindowsMessage.WM_NCRBUTTONDOWN:
case WindowsMessage.WM_NCMBUTTONDOWN:
case WindowsMessage.WM_NCXBUTTONDOWN:
{
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
(WindowsMessage)msg switch
{
WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType
.NonClientLeftButtonDown,
WindowsMessage.WM_NCRBUTTONDOWN => RawPointerEventType.RightButtonDown,
WindowsMessage.WM_NCMBUTTONDOWN => RawPointerEventType.MiddleButtonDown,
WindowsMessage.WM_NCXBUTTONDOWN =>
HighWord(ToInt32(wParam)) == 1 ?
RawPointerEventType.XButton1Down :
RawPointerEventType.XButton2Down,
},
PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_TOUCH:
{
var touchInputCount = wParam.ToInt32();
var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount];
var touchInputs = new Span<TOUCHINPUT>(pTouchInputs, touchInputCount);
if (GetTouchInputInfo(lParam, (uint)touchInputCount, pTouchInputs, Marshal.SizeOf<TOUCHINPUT>()))
{
foreach (var touchInput in touchInputs)
{
Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
_owner,
touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ?
RawPointerEventType.TouchEnd :
touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ?
RawPointerEventType.TouchBegin :
RawPointerEventType.TouchUpdate,
PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)),
WindowsKeyboardDevice.Instance.Modifiers,
touchInput.Id));
}
CloseTouchInputHandle(lParam);
return IntPtr.Zero;
}
break;
}
case WindowsMessage.WM_NCPAINT:
{
if (!HasFullDecorations)
{
return IntPtr.Zero;
}
break;
}
case WindowsMessage.WM_NCACTIVATE:
{
if (!HasFullDecorations)
{
return new IntPtr(1);
}
break;
}
case WindowsMessage.WM_PAINT:
{
using (_rendererLock.Lock())
{
if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero)
{
var f = Scaling;
var r = ps.rcPaint;
Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f,
(r.bottom - r.top) / f));
EndPaint(_hwnd, ref ps);
}
}
return IntPtr.Zero;
}
case WindowsMessage.WM_SIZE:
{
using (_rendererLock.Lock())
{
// Do nothing here, just block until the pending frame render is completed on the render thread
}
var size = (SizeCommand)wParam;
if (Resized != null &&
(size == SizeCommand.Restored ||
size == SizeCommand.Maximized))
{
var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16);
Resized(clientSize / Scaling);
}
var windowState = size == SizeCommand.Maximized ?
WindowState.Maximized :
(size == SizeCommand.Minimized ? WindowState.Minimized : WindowState.Normal);
if (windowState != _lastWindowState)
{
_lastWindowState = windowState;
WindowStateChanged?.Invoke(windowState);
}
return IntPtr.Zero;
}
case WindowsMessage.WM_MOVE:
{
PositionChanged?.Invoke(new PixelPoint((short)(ToInt32(lParam) & 0xffff),
(short)(ToInt32(lParam) >> 16)));
return IntPtr.Zero;
}
case WindowsMessage.WM_GETMINMAXINFO:
{
MINMAXINFO mmi = Marshal.PtrToStructure<MINMAXINFO>(lParam);
if (_minSize.Width > 0)
{
mmi.ptMinTrackSize.X =
(int)((_minSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right);
}
if (_minSize.Height > 0)
{
mmi.ptMinTrackSize.Y =
(int)((_minSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom);
}
if (!double.IsInfinity(_maxSize.Width) && _maxSize.Width > 0)
{
mmi.ptMaxTrackSize.X =
(int)((_maxSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right);
}
if (!double.IsInfinity(_maxSize.Height) && _maxSize.Height > 0)
{
mmi.ptMaxTrackSize.Y =
(int)((_maxSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom);
}
Marshal.StructureToPtr(mmi, lParam, true);
return IntPtr.Zero;
}
case WindowsMessage.WM_DISPLAYCHANGE:
{
(Screen as ScreenImpl)?.InvalidateScreensCache();
return IntPtr.Zero;
}
}
#if USE_MANAGED_DRAG
if (_managedDrag.PreprocessInputEvent(ref e))
return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam);
#endif
if (e != null && Input != null)
{
Input(e);
if (e.Handled)
{
return IntPtr.Zero;
}
}
using (_rendererLock.Lock())
{
return DefWindowProc(hWnd, msg, wParam, lParam);
}
}
private static int ToInt32(IntPtr ptr)
{
if (IntPtr.Size == 4)
return ptr.ToInt32();
return (int)(ptr.ToInt64() & 0xffffffff);
}
private static int HighWord(int param) => param >> 16;
private Point DipFromLParam(IntPtr lParam)
{
return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling;
}
private PixelPoint PointFromLParam(IntPtr lParam)
{
return new PixelPoint((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16));
}
private bool ShouldIgnoreTouchEmulatedMessage()
{
if (!_multitouch)
{
return false;
}
// MI_WP_SIGNATURE
// https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
const long marker = 0xFF515700L;
var info = GetMessageExtraInfo().ToInt64();
return (info & marker) == marker;
}
private static RawInputModifiers GetMouseModifiers(IntPtr wParam)
{
var keys = (ModifierKeys)ToInt32(wParam);
var modifiers = WindowsKeyboardDevice.Instance.Modifiers;
if (keys.HasFlagCustom(ModifierKeys.MK_LBUTTON))
{
modifiers |= RawInputModifiers.LeftMouseButton;
}
if (keys.HasFlagCustom(ModifierKeys.MK_RBUTTON))
{
modifiers |= RawInputModifiers.RightMouseButton;
}
if (keys.HasFlagCustom(ModifierKeys.MK_MBUTTON))
{
modifiers |= RawInputModifiers.MiddleMouseButton;
}
if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON1))
{
modifiers |= RawInputModifiers.XButton1MouseButton;
}
if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON2))
{
modifiers |= RawInputModifiers.XButton2MouseButton;
}
return modifiers;
}
}
}

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

File diff suppressed because it is too large

178
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs

@ -0,0 +1,178 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_MultiBinding
{
[Fact]
public void Should_Update()
{
var target = new Class1();
var b = new Subject<object>();
var mb = new MultiBinding()
{
Converter = StringJoinConverter,
Bindings = new[]
{
b.ToBinding()
}
};
target.Bind(Class1.FooProperty, mb);
Assert.Equal(null, target.Foo);
b.OnNext("Foo");
Assert.Equal("Foo", target.Foo);
b.OnNext("Bar");
Assert.Equal("Bar", target.Foo);
}
[Fact]
public void Should_Update_With_Multiple_Bindings()
{
var target = new Class1();
var bindings = Enumerable.Range(0, 3).Select(i => new BehaviorSubject<object>("Empty")).ToArray();
var mb = new MultiBinding()
{
Converter = StringJoinConverter,
Bindings = bindings.Select(b => b.ToBinding()).ToArray()
};
target.Bind(Class1.FooProperty, mb);
Assert.Equal("Empty,Empty,Empty", target.Foo);
bindings[0].OnNext("Foo");
Assert.Equal("Foo,Empty,Empty", target.Foo);
bindings[1].OnNext("Bar");
Assert.Equal("Foo,Bar,Empty", target.Foo);
bindings[2].OnNext("Baz");
Assert.Equal("Foo,Bar,Baz", target.Foo);
}
[Fact]
public void Should_Update_When_Null_Value_In_Bindings()
{
var target = new Class1();
var b = new Subject<object>();
var mb = new MultiBinding()
{
Converter = StringJoinConverter,
Bindings = new[]
{
b.ToBinding()
}
};
target.Bind(Class1.FooProperty, mb);
Assert.Equal(null, target.Foo);
b.OnNext("Foo");
Assert.Equal("Foo", target.Foo);
b.OnNext(null);
Assert.Equal("", target.Foo);
}
[Fact]
public void Should_Update_When_Null_Value_In_Bindings_With_StringFormat()
{
var target = new Class1();
var b = new Subject<object>();
var mb = new MultiBinding()
{
StringFormat = "Converted: {0}",
Bindings = new[]
{
b.ToBinding()
}
};
target.Bind(Class1.FooProperty, mb);
Assert.Equal(null, target.Foo);
b.OnNext("Foo");
Assert.Equal("Converted: Foo", target.Foo);
b.OnNext(null);
Assert.Equal("Converted: ", target.Foo);
}
[Fact]
public void MultiValueConverter_Should_Not_Skip_Valid_Null_ReferenceType_Value()
{
var target = new FuncMultiValueConverter<string, string>(v => string.Join(",", v.ToArray()));
object value = target.Convert(new[] { "Foo", "Bar", "Baz" }, typeof(string), null, CultureInfo.InvariantCulture);
Assert.Equal("Foo,Bar,Baz", value);
value = target.Convert(new[] { null, "Bar", "Baz" }, typeof(string), null, CultureInfo.InvariantCulture);
Assert.Equal(",Bar,Baz", value);
}
[Fact]
public void MultiValueConverter_Should_Not_Skip_Valid_Default_ValueType_Value()
{
var target = new FuncMultiValueConverter<StringValueTypeWrapper, string>(v => string.Join(",", v.ToArray()));
IList<object> create(string[] values) =>
values.Select(v => (object)(v != null ? new StringValueTypeWrapper() { Value = v } : default)).ToList();
object value = target.Convert(create(new[] { "Foo", "Bar", "Baz" }), typeof(string), null, CultureInfo.InvariantCulture);
Assert.Equal("Foo,Bar,Baz", value);
value = target.Convert(create(new[] { null, "Bar", "Baz" }), typeof(string), null, CultureInfo.InvariantCulture);
Assert.Equal(",Bar,Baz", value);
}
private struct StringValueTypeWrapper
{
public string Value;
public override string ToString() => Value;
}
private static IMultiValueConverter StringJoinConverter = new FuncMultiValueConverter<object, string>(v => string.Join(",", v.ToArray()));
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>("Foo");
public string Foo
{
get => GetValue(FooProperty);
set => SetValue(FooProperty, value);
}
}
}
}

17
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -1,9 +1,5 @@
using System;
using System.Windows.Input;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Markup.Data;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
@ -159,6 +155,19 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Can_Set_Clear_ContextMenu_Property()
{
using (Application())
{
var target = new ContextMenu();
var control = new Panel();
control.ContextMenu = target;
control.ContextMenu = null;
}
}
[Fact(Skip = "The only reason this test was 'passing' before was that the author forgot to call Window.ApplyTemplate()")]
public void Cancelling_Closing_Leaves_ContextMenuOpen()
{

78
tests/Avalonia.LeakTests/ControlTests.cs

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Contexts;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Diagnostics;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
@ -419,9 +421,83 @@ namespace Avalonia.LeakTests
}
}
[Fact]
public void Attached_ContextMenu_Is_Freed()
{
using (Start())
{
void AttachShowAndDetachContextMenu(Control control)
{
var contextMenu = new ContextMenu
{
Items = new[]
{
new MenuItem { Header = "Foo" },
new MenuItem { Header = "Foo" },
}
};
control.ContextMenu = contextMenu;
contextMenu.Open(control);
contextMenu.Close();
control.ContextMenu = null;
}
var window = new Window();
window.Show();
Assert.Same(window, FocusManager.Instance.Current);
AttachShowAndDetachContextMenu(window);
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount));
}
}
[Fact]
public void Standalone_ContextMenu_Is_Freed()
{
using (Start())
{
void BuildAndShowContextMenu(Control control)
{
var contextMenu = new ContextMenu
{
Items = new[]
{
new MenuItem { Header = "Foo" },
new MenuItem { Header = "Foo" },
}
};
contextMenu.Open(control);
contextMenu.Close();
}
var window = new Window();
window.Show();
Assert.Same(window, FocusManager.Instance.Current);
BuildAndShowContextMenu(window);
BuildAndShowContextMenu(window);
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount));
}
}
private IDisposable Start()
{
return UnitTestApplication.Start(TestServices.StyledWindow);
return UnitTestApplication.Start(TestServices.StyledWindow.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
inputManager: new InputManager()));
}
private class Node

4
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@ -21,6 +21,10 @@ namespace Avalonia.UnitTests
{
var win = Mock.Of<IWindowImpl>(x => x.Scaling == 1);
var mock = Mock.Get(win);
mock.Setup(x => x.Show()).Callback(() =>
{
mock.Object.Activated?.Invoke();
});
mock.Setup(x => x.CreatePopup()).Returns(() =>
{
if (popupImpl != null)

12
tests/Avalonia.Visuals.UnitTests/Media/BrushTests.cs

@ -77,5 +77,17 @@ namespace Avalonia.Visuals.UnitTests.Media
{
Assert.Throws<FormatException>(() => Brush.Parse("#ff808g80"));
}
[Fact]
public void Changing_Opacity_Raises_Invalidated()
{
var target = new SolidColorBrush();
var raised = false;
target.Invalidated += (s, e) => raised = true;
target.Opacity = 0.5;
Assert.True(raised);
}
}
}

Loading…
Cancel
Save