diff --git a/.gitignore b/.gitignore index a510c4e49f..d16287cfb4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.suo *.user *.sln.docstates +.vs/ # Build results diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings index bf98899847..ab21d6e50b 100644 --- a/Avalonia.sln.DotSettings +++ b/Avalonia.sln.DotSettings @@ -2,6 +2,24 @@ ExplicitlyExcluded ExplicitlyExcluded HINT + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="set_" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="_" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="False" Prefix="I" Suffix="" Style="AaBb" /> @@ -10,8 +28,9 @@ <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="False" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="s_" Suffix="" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="False" Prefix="T" Suffix="" Style="AaBb" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index 1aaedc4420..dc93808f27 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -34,6 +34,10 @@ Width="100" Height="200" Stretch="UniformToFill"/> - + + + Window Icon as an Image + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index cc35b4d237..792b25963e 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -1,10 +1,14 @@ +using System.IO; +using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; namespace ControlCatalog.Pages { public class ImagePage : UserControl { + private Image iconImage; public ImagePage() { this.InitializeComponent(); @@ -13,6 +17,25 @@ namespace ControlCatalog.Pages private void InitializeComponent() { AvaloniaXamlLoader.Load(this); + iconImage = this.Get("Icon"); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + if (iconImage.Source == null) + { + var windowRoot = e.Root as Window; + if (windowRoot != null) + { + using (var stream = new MemoryStream()) + { + windowRoot.Icon.Save(stream); + stream.Seek(0, SeekOrigin.Begin); + iconImage.Source = new Bitmap(stream); + } + } + } } } } diff --git a/src/Android/Avalonia.Android/PlatformIconLoader.cs b/src/Android/Avalonia.Android/PlatformIconLoader.cs index 11fc5583a5..289025ce1d 100644 --- a/src/Android/Avalonia.Android/PlatformIconLoader.cs +++ b/src/Android/Avalonia.Android/PlatformIconLoader.cs @@ -49,11 +49,9 @@ namespace Avalonia.Android stream.CopyTo(this.stream); } - public Stream Save() + public void Save(Stream outputStream) { - var returnStream = new MemoryStream(); - stream.CopyTo(returnStream); - return returnStream; + stream.CopyTo(outputStream); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/DirectProperty.cs b/src/Avalonia.Base/DirectProperty.cs index fad6cf983a..8352528285 100644 --- a/src/Avalonia.Base/DirectProperty.cs +++ b/src/Avalonia.Base/DirectProperty.cs @@ -30,7 +30,7 @@ namespace Avalonia string name, Func getter, Action setter, - PropertyMetadata metadata) + DirectPropertyMetadata metadata) : base(name, typeof(TOwner), metadata) { Contract.Requires(getter != null); @@ -50,7 +50,7 @@ namespace Avalonia AvaloniaProperty source, Func getter, Action setter, - PropertyMetadata metadata) + DirectPropertyMetadata metadata) : base(source, typeof(TOwner), metadata) { Contract.Requires(getter != null); @@ -93,18 +93,22 @@ namespace Avalonia Func getter, Action setter = null, TValue unsetValue = default(TValue), - BindingMode defaultBindingMode = BindingMode.OneWay, + BindingMode defaultBindingMode = BindingMode.Default, bool enableDataValidation = false) where TNewOwner : AvaloniaObject { + var metadata = new DirectPropertyMetadata( + unsetValue: unsetValue, + defaultBindingMode: defaultBindingMode, + enableDataValidation: enableDataValidation); + + metadata.Merge(GetMetadata(), this); + var result = new DirectProperty( this, getter, setter, - new DirectPropertyMetadata( - unsetValue: unsetValue, - defaultBindingMode: defaultBindingMode, - enableDataValidation: enableDataValidation)); + metadata); AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result); return result; diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs index d22801e35a..26de578a45 100644 --- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs +++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs @@ -23,7 +23,7 @@ namespace Avalonia public DirectPropertyMetadata( TValue unsetValue = default(TValue), BindingMode defaultBindingMode = BindingMode.Default, - bool enableDataValidation = false) + bool? enableDataValidation = null) : base(defaultBindingMode) { UnsetValue = unsetValue; @@ -44,7 +44,7 @@ namespace Avalonia /// control (such as a TextBox's Text property) will be interested in recieving data /// validation messages so this feature must be explicitly enabled by setting this flag. /// - public bool EnableDataValidation { get; } + public bool? EnableDataValidation { get; private set; } /// object IDirectPropertyMetadata.UnsetValue => UnsetValue; @@ -62,6 +62,11 @@ namespace Avalonia { UnsetValue = src.UnsetValue; } + + if (EnableDataValidation == null) + { + EnableDataValidation = src.EnableDataValidation; + } } } } diff --git a/src/Avalonia.Base/IDirectPropertyMetadata.cs b/src/Avalonia.Base/IDirectPropertyMetadata.cs index 9dc014f0b8..c283855e5f 100644 --- a/src/Avalonia.Base/IDirectPropertyMetadata.cs +++ b/src/Avalonia.Base/IDirectPropertyMetadata.cs @@ -16,6 +16,6 @@ namespace Avalonia /// /// Gets a value indicating whether the property is interested in data validation. /// - bool EnableDataValidation { get; } + bool? EnableDataValidation { get; } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 5cd2ddfc35..ef253a28e2 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -671,6 +671,23 @@ namespace Avalonia.Controls if (Name != null) { _nameScope?.Register(Name, this); + + var visualParent = Parent as Visual; + + if (this is INameScope && visualParent != null) + { + // If we have e.g. a named UserControl in a window then we want that control + // to be findable by name from the Window, so register with both name scopes. + // This differs from WPF's behavior in that XAML manually registers controls + // with name scopes based on the XAML file in which the name attribute appears, + // but we're trying to avoid XAML magic in Avalonia in order to made code- + // created UIs easy. This will cause problems if a UserControl declares a name + // in its XAML and that control is included multiple times in a parent control + // (as the name will be duplicated), however at the moment I'm fine with saying + // "don't do that". + var parentNameScope = NameScope.FindNameScope(visualParent); + parentNameScope?.Register(Name, this); + } } } diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index 792dee8ae8..2d8dcb42e3 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -66,6 +66,11 @@ namespace Avalonia.Controls /// double PixelOffset { get; set; } + /// + /// Gets or sets the current scroll offset in the cross axis. + /// + double CrossAxisOffset { get; set; } + /// /// Invalidates the measure of the control and forces a call to /// on the next measure. diff --git a/src/Avalonia.Controls/Platform/IWindowIconImpl.cs b/src/Avalonia.Controls/Platform/IWindowIconImpl.cs index cd339ff404..d106e2a616 100644 --- a/src/Avalonia.Controls/Platform/IWindowIconImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowIconImpl.cs @@ -7,6 +7,6 @@ namespace Avalonia.Platform { public interface IWindowIconImpl { - Stream Save(); + void Save(Stream outputStream); } } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index c17b11a374..40fc2f302c 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -232,6 +232,10 @@ namespace Avalonia.Controls.Presenters { DataContext = content; } + else + { + ClearValue(DataContextProperty); + } // Update the Child. if (newChild == null) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 3a2cb688cb..fee326dacc 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Specialized; +using System.Reactive.Linq; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; @@ -17,6 +18,7 @@ namespace Avalonia.Controls.Presenters internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable { private double _crossAxisOffset; + private IDisposable _subscriptions; /// /// Initializes a new instance of the class. @@ -27,6 +29,15 @@ namespace Avalonia.Controls.Presenters Owner = owner; Items = owner.Items; ItemCount = owner.Items.Count(); + + var panel = VirtualizingPanel; + + if (panel != null) + { + _subscriptions = panel.GetObservable(Panel.BoundsProperty) + .Skip(1) + .Subscribe(_ => InvalidateScroll()); + } } /// @@ -195,8 +206,17 @@ namespace Avalonia.Controls.Presenters /// The actual size used. public virtual Size ArrangeOverride(Size finalSize) { - var origin = Vertical ? new Point(-_crossAxisOffset, 0) : new Point(0, _crossAxisOffset); - Owner.Panel.Arrange(new Rect(origin, finalSize)); + if (VirtualizingPanel != null) + { + VirtualizingPanel.CrossAxisOffset = _crossAxisOffset; + Owner.Panel.Arrange(new Rect(finalSize)); + } + else + { + var origin = Vertical ? new Point(-_crossAxisOffset, 0) : new Point(0, _crossAxisOffset); + Owner.Panel.Arrange(new Rect(origin, finalSize)); + } + return finalSize; } @@ -240,6 +260,9 @@ namespace Avalonia.Controls.Presenters /// public virtual void Dispose() { + _subscriptions?.Dispose(); + _subscriptions = null; + if (VirtualizingPanel != null) { VirtualizingPanel.Controller = null; diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index d29a6aaaef..59f0d551b0 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -173,10 +173,13 @@ namespace Avalonia.Controls.Presenters { if (this.GetVisualParent() != null) { - _caretBlink = true; - _caretTimer.Stop(); - _caretTimer.Start(); - InvalidateVisual(); + if (_caretTimer.IsEnabled) + { + _caretBlink = true; + _caretTimer.Stop(); + _caretTimer.Start(); + InvalidateVisual(); + } if (IsMeasureValid) { diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 396905c26b..ad771d333e 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -157,10 +157,6 @@ namespace Avalonia.Controls /// public ScrollViewer() { - Observable.CombineLatest( - this.GetObservable(ExtentProperty), - this.GetObservable(ViewportProperty)) - .Select(x => new { Extent = x[0], Viewport = x[1] }); } /// diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 2e5afaf170..834f6d218b 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -19,6 +19,7 @@ namespace Avalonia.Controls private double _averageItemSize; private int _averageCount; private double _pixelOffset; + private double _crossAxisOffset; private bool _forceRemeasure; bool IVirtualizingPanel.IsFull @@ -60,6 +61,20 @@ namespace Avalonia.Controls } } + double IVirtualizingPanel.CrossAxisOffset + { + get { return _crossAxisOffset; } + + set + { + if (_crossAxisOffset != value) + { + _crossAxisOffset = value; + InvalidateArrange(); + } + } + } + private IVirtualizingController Controller => ((IVirtualizingPanel)this).Controller; void IVirtualizingPanel.ForceInvalidateMeasure() @@ -140,7 +155,11 @@ namespace Avalonia.Controls { if (orientation == Orientation.Vertical) { - rect = new Rect(rect.X, rect.Y - _pixelOffset, rect.Width, rect.Height); + rect = new Rect( + rect.X - _crossAxisOffset, + rect.Y - _pixelOffset, + rect.Width, + rect.Height); child.Arrange(rect); if (rect.Y >= _availableSpace.Height) @@ -157,7 +176,11 @@ namespace Avalonia.Controls } else { - rect = new Rect(rect.X - _pixelOffset, rect.Y, rect.Width, rect.Height); + rect = new Rect( + rect.X - _pixelOffset, + rect.Y - _crossAxisOffset, + rect.Width, + rect.Height); child.Arrange(rect); if (rect.X >= _availableSpace.Width) diff --git a/src/Avalonia.Controls/WindowIcon.cs b/src/Avalonia.Controls/WindowIcon.cs index debac7c981..dff84ff6ef 100644 --- a/src/Avalonia.Controls/WindowIcon.cs +++ b/src/Avalonia.Controls/WindowIcon.cs @@ -31,6 +31,6 @@ namespace Avalonia.Controls public IWindowIconImpl PlatformImpl { get; } - public Stream Save() => PlatformImpl.Save(); + public void Save(Stream stream) => PlatformImpl.Save(stream); } } diff --git a/src/Avalonia.Styling/Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs index ddfb3ea173..4c5875479e 100644 --- a/src/Avalonia.Styling/Controls/NameScope.cs +++ b/src/Avalonia.Styling/Controls/NameScope.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Avalonia.LogicalTree; namespace Avalonia.Controls { @@ -29,6 +30,32 @@ namespace Avalonia.Controls /// public event EventHandler Unregistered; + /// + /// Finds the containing name scope for a visual. + /// + /// The visual. + /// The containing name scope. + public static INameScope FindNameScope(Visual visual) + { + Contract.Requires(visual != null); + + INameScope result; + + while (visual != null) + { + result = visual as INameScope ?? GetNameScope(visual); + + if (result != null) + { + return result; + } + + visual = (visual as ILogical).LogicalParent as Visual; + } + + return null; + } + /// /// Gets the value of the attached on a visual. /// @@ -36,6 +63,8 @@ namespace Avalonia.Controls /// The value of the NameScope attached property. public static INameScope GetNameScope(Visual visual) { + Contract.Requires(visual != null); + return visual.GetValue(NameScopeProperty); } @@ -46,6 +75,8 @@ namespace Avalonia.Controls /// The value to set. public static void SetNameScope(Visual visual, INameScope value) { + Contract.Requires(visual != null); + visual.SetValue(NameScopeProperty, value); } diff --git a/src/Gtk/Avalonia.Gtk/IconImpl.cs b/src/Gtk/Avalonia.Gtk/IconImpl.cs index a3cf3be47a..3203e59f21 100644 --- a/src/Gtk/Avalonia.Gtk/IconImpl.cs +++ b/src/Gtk/Avalonia.Gtk/IconImpl.cs @@ -18,9 +18,10 @@ namespace Avalonia.Gtk public Pixbuf Pixbuf { get; } - public Stream Save() + public void Save(Stream stream) { - return new MemoryStream(Pixbuf.SaveToBuffer("png")); + var buffer = Pixbuf.SaveToBuffer("png"); + stream.Write(buffer, 0, buffer.Length); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs index 086257f24c..649596a74e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs @@ -115,6 +115,10 @@ namespace Avalonia.Markup.Xaml.Data anchor, enableDataValidation); } + else if (RelativeSource.Mode == RelativeSourceMode.Self) + { + observer = CreateSourceObserver(target, pathInfo.Path, enableDataValidation); + } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { observer = CreateTemplatedParentObserver(target, pathInfo.Path); diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs b/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs index 6771aaf644..c0eb581af2 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs @@ -5,6 +5,7 @@ namespace Avalonia.Markup.Xaml.Data { public enum RelativeSourceMode { + Self, DataContext, TemplatedParent, } diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs index 93f20e4c77..56c0072eaa 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs @@ -131,20 +131,14 @@ namespace Avalonia.Markup.Data } else { - if (notification.Error != null) + if (Next != null) { - _observer.OnNext(notification); + Next.Target = new WeakReference(notification.Value); } - else if (notification.HasValue) + + if (Next == null || notification.Error != null) { - if (Next != null) - { - Next.Target = new WeakReference(notification.Value); - } - else - { - _observer.OnNext(value); - } + _observer.OnNext(value); } } } diff --git a/src/Markup/Avalonia.Markup/Data/MarkupBindingChainException.cs b/src/Markup/Avalonia.Markup/Data/MarkupBindingChainException.cs index dab5756976..51afe1ffbf 100644 --- a/src/Markup/Avalonia.Markup/Data/MarkupBindingChainException.cs +++ b/src/Markup/Avalonia.Markup/Data/MarkupBindingChainException.cs @@ -26,7 +26,7 @@ namespace Avalonia.Markup.Data _nodes = null; } - public bool HasNodes => _nodes.Count > 0; + public bool HasNodes => _nodes?.Count > 0; public void AddNode(string node) => _nodes.Add(node); public void Commit(string expression) diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index 31131a7258..877c4290a4 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -53,6 +53,15 @@ ..\..\..\packages\SharpDX.DXGI.3.1.1\lib\net45\SharpDX.DXGI.dll True + + ..\..\..\packages\SharpDX.Direct2D1.3.1.1\lib\net45\SharpDX.Direct2D1.dll + + + ..\..\..\packages\SharpDX.Direct3D11.3.1.1\lib\net45\SharpDX.Direct3D11.dll + + + ..\..\..\packages\SharpDX.DXGI.3.1.1\lib\net45\SharpDX.DXGI.dll + @@ -66,12 +75,15 @@ + + + @@ -83,8 +95,8 @@ - + diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 683a144bb7..b591dece9a 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -47,13 +47,46 @@ namespace Avalonia.Direct2D1 .Bind().ToConstant(s_d2D1Factory) .Bind().ToConstant(s_d2D1Factory) .BindToSelf(s_dwfactory) - .BindToSelf(s_imagingFactory); + .BindToSelf(s_imagingFactory) + .BindToSelf(s_dxgiDevice) + .BindToSelf(s_d2D1Device); SharpDX.Configuration.EnableReleaseOnFinalizer = true; } + private static readonly SharpDX.DXGI.Device s_dxgiDevice; + + private static readonly SharpDX.Direct2D1.Device s_d2D1Device; + + static Direct2D1Platform() + { + var featureLevels = new[] + { + SharpDX.Direct3D.FeatureLevel.Level_11_1, + SharpDX.Direct3D.FeatureLevel.Level_11_0, + SharpDX.Direct3D.FeatureLevel.Level_10_1, + SharpDX.Direct3D.FeatureLevel.Level_10_0, + SharpDX.Direct3D.FeatureLevel.Level_9_3, + SharpDX.Direct3D.FeatureLevel.Level_9_2, + SharpDX.Direct3D.FeatureLevel.Level_9_1, + }; + + using (var d3dDevice = new SharpDX.Direct3D11.Device( + SharpDX.Direct3D.DriverType.Hardware, + SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport | SharpDX.Direct3D11.DeviceCreationFlags.VideoSupport, + featureLevels)) + { + s_dxgiDevice = d3dDevice.QueryInterface(); + } + + using (var factory1 = s_d2D1Factory.QueryInterface()) + { + s_d2D1Device = new SharpDX.Direct2D1.Device(factory1, s_dxgiDevice); + } + } + public IBitmapImpl CreateBitmap(int width, int height) { - return new BitmapImpl(s_imagingFactory, width, height); + return new WicBitmapImpl(s_imagingFactory, width, height); } public IFormattedTextImpl CreateFormattedText( @@ -126,12 +159,12 @@ namespace Avalonia.Direct2D1 public IBitmapImpl LoadBitmap(string fileName) { - return new BitmapImpl(s_imagingFactory, fileName); + return new WicBitmapImpl(s_imagingFactory, fileName); } public IBitmapImpl LoadBitmap(Stream stream) { - return new BitmapImpl(s_imagingFactory, stream); + return new WicBitmapImpl(s_imagingFactory, stream); } } } diff --git a/src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs index 0121345259..49d4c91c52 100644 --- a/src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs @@ -1,159 +1,29 @@ -// 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 Avalonia.Direct2D1.Media; -using Avalonia.Media; -using Avalonia.Platform; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; using Avalonia.Win32.Interop; using SharpDX; -using SharpDX.Direct2D1; +using SharpDX.DXGI; namespace Avalonia.Direct2D1 { - public class HwndRenderTarget : IRenderTarget + class HwndRenderTarget : SwapChainRenderTarget { private readonly IntPtr _hwnd; - private readonly SharpDX.Direct3D11.Device _d3dDevice; - private DeviceContext _deviceContext; - private SharpDX.DXGI.SwapChain1 _swapChain; - private Size2 _size; - private Size2F _dpi; - /// - /// Initializes a new instance of the class. - /// - /// The window handle. public HwndRenderTarget(IntPtr hwnd) { _hwnd = hwnd; - Direct2DFactory = AvaloniaLocator.Current.GetService(); - DirectWriteFactory = AvaloniaLocator.Current.GetService(); - - var featureLevels = new[] - { - SharpDX.Direct3D.FeatureLevel.Level_12_1, - SharpDX.Direct3D.FeatureLevel.Level_12_0, - SharpDX.Direct3D.FeatureLevel.Level_11_1, - SharpDX.Direct3D.FeatureLevel.Level_11_0, - SharpDX.Direct3D.FeatureLevel.Level_10_1, - SharpDX.Direct3D.FeatureLevel.Level_10_0, - SharpDX.Direct3D.FeatureLevel.Level_9_3, - SharpDX.Direct3D.FeatureLevel.Level_9_2, - SharpDX.Direct3D.FeatureLevel.Level_9_1, - }; - - _d3dDevice = new SharpDX.Direct3D11.Device( - SharpDX.Direct3D.DriverType.Hardware, - SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport, - featureLevels); - - var size = GetWindowSize(); - var dpi = GetWindowDpi(); - UpdateSwapChain(size, dpi); } - /// - /// Gets the Direct2D factory. - /// - public Factory1 Direct2DFactory + protected override SwapChain1 CreateSwapChain(Factory2 dxgiFactory, SwapChainDescription1 swapChainDesc) { - get; + return new SwapChain1(dxgiFactory, DxgiDevice, _hwnd, ref swapChainDesc); } - /// - /// Gets the DirectWrite factory. - /// - public SharpDX.DirectWrite.Factory DirectWriteFactory - { - get; - } - - /// - /// Creates a drawing context for a rendering session. - /// - /// An . - public IDrawingContextImpl CreateDrawingContext() - { - var size = GetWindowSize(); - var dpi = GetWindowDpi(); - - if (size != _size || _dpi != dpi) - { - UpdateSwapChain(size, dpi); - } - - return new DrawingContextImpl(_deviceContext, DirectWriteFactory, _swapChain); - } - - public void Dispose() - { - _deviceContext.Dispose(); - _swapChain.Dispose(); - _d3dDevice.Dispose(); - } - - private void UpdateSwapChain(Size2 size, Size2F dpi) - { - _size = size; - _dpi = dpi; - - using (var dxgiDevice = _d3dDevice.QueryInterface()) - using (var d2dDevice = new Device(Direct2DFactory, dxgiDevice)) - using (var dxgiAdaptor = dxgiDevice.Adapter) - using (var dxgiFactory = dxgiAdaptor.GetParent()) - { - _deviceContext?.Dispose(); - - if (_swapChain == null) - { - var swapChainDesc = new SharpDX.DXGI.SwapChainDescription1 - { - Width = 0, - Height = 0, - Format = SharpDX.DXGI.Format.B8G8R8A8_UNorm, - Stereo = false, - SampleDescription = new SharpDX.DXGI.SampleDescription - { - Count = 1, - Quality = 0, - }, - Usage = SharpDX.DXGI.Usage.RenderTargetOutput, - BufferCount = 2, - Scaling = SharpDX.DXGI.Scaling.None, - SwapEffect = SharpDX.DXGI.SwapEffect.FlipSequential, - Flags = 0, - }; - - _swapChain = new SharpDX.DXGI.SwapChain1(dxgiFactory, _d3dDevice, _hwnd, ref swapChainDesc); - } - else - { - _swapChain.ResizeBuffers(2, 0, 0, SharpDX.DXGI.Format.B8G8R8A8_UNorm, 0); - } - - _deviceContext = new DeviceContext(d2dDevice, DeviceContextOptions.None); - _deviceContext.DotsPerInch = dpi; - - using (var dxgiBackBuffer = _swapChain.GetBackBuffer(0)) - using (var d2dBackBuffer = new Bitmap1( - _deviceContext, - dxgiBackBuffer, - new BitmapProperties1( - new PixelFormat - { - AlphaMode = AlphaMode.Ignore, - Format = SharpDX.DXGI.Format.B8G8R8A8_UNorm - }, - dpi.Width, - dpi.Height, - BitmapOptions.Target | BitmapOptions.CannotDraw))) - { - _deviceContext.Target = d2dBackBuffer; - } - } - } - private Size2F GetWindowDpi() + protected override Size2F GetWindowDpi() { if (UnmanagedMethods.ShCoreAvailable) { @@ -176,7 +46,7 @@ namespace Avalonia.Direct2D1 return new Size2F(96, 96); } - private Size2 GetWindowSize() + protected override Size2 GetWindowSize() { UnmanagedMethods.RECT rc; UnmanagedMethods.GetClientRect(_hwnd, out rc); diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 5b82cfc0c2..fa937ccb2f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -27,6 +27,7 @@ namespace Avalonia.Direct2D1.Media /// /// The render target to draw to. /// The DirectWrite factory. + /// An optional swap chain associated with this drawing context. public DrawingContextImpl( SharpDX.Direct2D1.RenderTarget renderTarget, SharpDX.DirectWrite.Factory directWriteFactory, @@ -35,6 +36,7 @@ namespace Avalonia.Direct2D1.Media _renderTarget = renderTarget; _swapChain = swapChain; _directWriteFactory = directWriteFactory; + _swapChain = swapChain; _renderTarget.BeginDraw(); } @@ -63,11 +65,8 @@ namespace Avalonia.Direct2D1.Media try { _renderTarget.EndDraw(); - - if (_swapChain != null) - { - _swapChain.Present(1, SharpDX.DXGI.PresentFlags.None); - } + + _swapChain?.Present(1, SharpDX.DXGI.PresentFlags.None); } catch (SharpDXException ex) when((uint)ex.HResult == 0x8899000C) // D2DERR_RECREATE_TARGET { diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs index c526f1ed04..63596bdf54 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs @@ -1,130 +1,24 @@ -// 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; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; using Avalonia.Platform; -using SharpDX.WIC; +using SharpDX.Direct2D1; namespace Avalonia.Direct2D1.Media { - /// - /// A Direct2D implementation of a . - /// - public class BitmapImpl : IBitmapImpl + public abstract class BitmapImpl : IBitmapImpl, IDisposable { - private readonly ImagingFactory _factory; - - /// - /// Initializes a new instance of the class. - /// - /// The WIC imaging factory to use. - /// The filename of the bitmap to load. - public BitmapImpl(ImagingFactory factory, string fileName) - { - _factory = factory; - - using (BitmapDecoder decoder = new BitmapDecoder(factory, fileName, DecodeOptions.CacheOnDemand)) - { - WicImpl = new Bitmap(factory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnDemand); - } - } - - /// - /// Initializes a new instance of the class. - /// - /// The WIC imaging factory to use. - /// The stream to read the bitmap from. - public BitmapImpl(ImagingFactory factory, Stream stream) - { - _factory = factory; - - using (BitmapDecoder decoder = new BitmapDecoder(factory, stream, DecodeOptions.CacheOnLoad)) - { - WicImpl = new Bitmap(factory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnLoad); - } - } - - /// - /// Initializes a new instance of the class. - /// - /// The WIC imaging factory to use. - /// The width of the bitmap. - /// The height of the bitmap. - public BitmapImpl(ImagingFactory factory, int width, int height) - { - _factory = factory; - WicImpl = new Bitmap( - factory, - width, - height, - PixelFormat.Format32bppPBGRA, - BitmapCreateCacheOption.CacheOnLoad); - } - - /// - /// Gets the width of the bitmap, in pixels. - /// - public int PixelWidth => WicImpl.Size.Width; - - /// - /// Gets the height of the bitmap, in pixels. - /// - public int PixelHeight => WicImpl.Size.Height; + public abstract Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget target); + public abstract int PixelWidth { get; } + public abstract int PixelHeight { get; } + public abstract void Save(string fileName); + public abstract void Save(Stream stream); public virtual void Dispose() { - WicImpl.Dispose(); - } - - /// - /// Gets the WIC implementation of the bitmap. - /// - public Bitmap WicImpl - { - get; - } - - /// - /// Gets a Direct2D bitmap to use on the specified render target. - /// - /// The render target. - /// The Direct2D bitmap. - public SharpDX.Direct2D1.Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget renderTarget) - { - FormatConverter converter = new FormatConverter(_factory); - converter.Initialize(WicImpl, PixelFormat.Format32bppPBGRA); - return SharpDX.Direct2D1.Bitmap.FromWicBitmap(renderTarget, converter); - } - - /// - /// Saves the bitmap to a file. - /// - /// The filename. - public void Save(string fileName) - { - if (Path.GetExtension(fileName) != ".png") - { - // Yeah, we need to support other formats. - throw new NotSupportedException("Use PNG, stoopid."); - } - - using (FileStream s = new FileStream(fileName, FileMode.Create)) - { - Save(s); - } - } - - public void Save(Stream stream) - { - PngBitmapEncoder encoder = new PngBitmapEncoder(_factory); - encoder.Initialize(stream); - - BitmapFrameEncode frame = new BitmapFrameEncode(encoder); - frame.Initialize(); - frame.WriteSource(WicImpl); - frame.Commit(); - encoder.Commit(); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs new file mode 100644 index 0000000000..5378ae3257 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Platform; +using SharpDX.Direct2D1; + +namespace Avalonia.Direct2D1.Media +{ + /// + /// A Direct2D Bitmap implementation that uses a GPU memory bitmap as its image. + /// + public class D2DBitmapImpl : BitmapImpl + { + private Bitmap _direct2D; + + /// + /// Initialize a new instance of the class + /// with a bitmap backed by GPU memory. + /// + /// The GPU bitmap. + /// + /// This bitmap must be either from the same render target, + /// or if the render target is a , + /// the device associated with this context, to be renderable. + /// + public D2DBitmapImpl(Bitmap d2DBitmap) + { + if (d2DBitmap == null) throw new ArgumentNullException(nameof(d2DBitmap)); + + _direct2D = d2DBitmap; + } + + public override Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget target) => _direct2D; + + public override int PixelWidth => _direct2D.PixelSize.Width; + public override int PixelHeight => _direct2D.PixelSize.Height; + + public override void Save(string fileName) + { + throw new NotImplementedException(); + } + + public override void Save(Stream stream) + { + throw new NotImplementedException(); + } + + public override void Dispose() + { + base.Dispose(); + _direct2D.Dispose(); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs index 66bfb16b50..96b51e4407 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs @@ -10,7 +10,7 @@ using DirectWriteFactory = SharpDX.DirectWrite.Factory; namespace Avalonia.Direct2D1.Media { - public class RenderTargetBitmapImpl : BitmapImpl, IRenderTargetBitmapImpl, IDisposable + public class RenderTargetBitmapImpl : WicBitmapImpl, IRenderTargetBitmapImpl { private readonly DirectWriteFactory _dwriteFactory; private readonly WicRenderTarget _target; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs new file mode 100644 index 0000000000..7c46663748 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -0,0 +1,130 @@ +// 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.IO; +using Avalonia.Platform; +using SharpDX.WIC; + +namespace Avalonia.Direct2D1.Media +{ + /// + /// A WIC implementation of a . + /// + public class WicBitmapImpl : BitmapImpl + { + private readonly ImagingFactory _factory; + + private SharpDX.Direct2D1.Bitmap _direct2D; + + /// + /// Initializes a new instance of the class. + /// + /// The WIC imaging factory to use. + /// The filename of the bitmap to load. + public WicBitmapImpl(ImagingFactory factory, string fileName) + { + _factory = factory; + + using (BitmapDecoder decoder = new BitmapDecoder(factory, fileName, DecodeOptions.CacheOnDemand)) + { + WicImpl = new Bitmap(factory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnDemand); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The WIC imaging factory to use. + /// The stream to read the bitmap from. + public WicBitmapImpl(ImagingFactory factory, Stream stream) + { + _factory = factory; + + using (BitmapDecoder decoder = new BitmapDecoder(factory, stream, DecodeOptions.CacheOnLoad)) + { + WicImpl = new Bitmap(factory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnLoad); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The WIC imaging factory to use. + /// The width of the bitmap. + /// The height of the bitmap. + public WicBitmapImpl(ImagingFactory factory, int width, int height) + { + _factory = factory; + WicImpl = new Bitmap( + factory, + width, + height, + PixelFormat.Format32bppPBGRA, + BitmapCreateCacheOption.CacheOnLoad); + } + + /// + /// Gets the width of the bitmap, in pixels. + /// + public override int PixelWidth => WicImpl.Size.Width; + + /// + /// Gets the height of the bitmap, in pixels. + /// + public override int PixelHeight => WicImpl.Size.Height; + + public override void Dispose() + { + WicImpl.Dispose(); + _direct2D?.Dispose(); + } + + /// + /// Gets the WIC implementation of the bitmap. + /// + public Bitmap WicImpl { get; } + + /// + /// Gets a Direct2D bitmap to use on the specified render target. + /// + /// The render target. + /// The Direct2D bitmap. + public override SharpDX.Direct2D1.Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget renderTarget) + { + FormatConverter converter = new FormatConverter(_factory); + converter.Initialize(WicImpl, PixelFormat.Format32bppPBGRA); + return SharpDX.Direct2D1.Bitmap.FromWicBitmap(renderTarget, converter); + } + + /// + /// Saves the bitmap to a file. + /// + /// The filename. + public override void Save(string fileName) + { + if (Path.GetExtension(fileName) != ".png") + { + // Yeah, we need to support other formats. + throw new NotSupportedException("Use PNG, stoopid."); + } + + using (FileStream s = new FileStream(fileName, FileMode.Create)) + { + Save(s); + } + } + + public override void Save(Stream stream) + { + PngBitmapEncoder encoder = new PngBitmapEncoder(_factory); + encoder.Initialize(stream); + + BitmapFrameEncode frame = new BitmapFrameEncode(encoder); + frame.Initialize(); + frame.WriteSource(WicImpl); + frame.Commit(); + encoder.Commit(); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs new file mode 100644 index 0000000000..4eea7640ed --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Direct2D1.Media; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Win32.Interop; +using SharpDX; +using SharpDX.Direct2D1; +using SharpDX.DXGI; +using AlphaMode = SharpDX.Direct2D1.AlphaMode; +using Device = SharpDX.Direct2D1.Device; +using Factory = SharpDX.Direct2D1.Factory; +using Factory2 = SharpDX.DXGI.Factory2; + +namespace Avalonia.Direct2D1 +{ + public abstract class SwapChainRenderTarget : IRenderTarget + { + private Size2 _savedSize; + private Size2F _savedDpi; + private DeviceContext _deviceContext; + private SwapChain1 _swapChain; + + protected SwapChainRenderTarget() + { + DxgiDevice = AvaloniaLocator.Current.GetService(); + D2DDevice = AvaloniaLocator.Current.GetService(); + Direct2DFactory = AvaloniaLocator.Current.GetService(); + DirectWriteFactory = AvaloniaLocator.Current.GetService(); + } + + + /// + /// Gets the Direct2D factory. + /// + public Factory Direct2DFactory + { + get; + } + + /// + /// Gets the DirectWrite factory. + /// + public SharpDX.DirectWrite.Factory DirectWriteFactory + { + get; + } + + protected SharpDX.DXGI.Device DxgiDevice { get; } + + public Device D2DDevice { get; } + + /// + /// Creates a drawing context for a rendering session. + /// + /// An . + public IDrawingContextImpl CreateDrawingContext() + { + var size = GetWindowSize(); + var dpi = GetWindowDpi(); + + if (size != _savedSize || dpi != _savedDpi) + { + _savedSize = size; + _savedDpi = dpi; + CreateSwapChain(); + } + + return new DrawingContextImpl(_deviceContext, DirectWriteFactory, _swapChain); + } + + public void Dispose() + { + _deviceContext?.Dispose(); + _swapChain?.Dispose(); + } + + private void CreateSwapChain() + { + using (var dxgiAdaptor = DxgiDevice.Adapter) + using (var dxgiFactory = dxgiAdaptor.GetParent()) + { + _deviceContext?.Dispose(); + _deviceContext = new DeviceContext(D2DDevice, DeviceContextOptions.None) {DotsPerInch = _savedDpi}; + + + var swapChainDesc = new SwapChainDescription1 + { + Width = _savedSize.Width, + Height = _savedSize.Height, + Format = Format.B8G8R8A8_UNorm, + Stereo = false, + SampleDescription = new SampleDescription + { + Count = 1, + Quality = 0, + }, + Usage = Usage.RenderTargetOutput, + BufferCount = 2, + Scaling = Scaling.None, + SwapEffect = SwapEffect.FlipSequential, + Flags = 0, + }; + + _swapChain?.Dispose(); + _swapChain = CreateSwapChain(dxgiFactory, swapChainDesc); + + using (var dxgiBackBuffer = _swapChain.GetBackBuffer(0)) + using (var d2dBackBuffer = new Bitmap1( + _deviceContext, + dxgiBackBuffer, + new BitmapProperties1( + new PixelFormat + { + AlphaMode = AlphaMode.Ignore, + Format = Format.B8G8R8A8_UNorm + }, + _savedDpi.Width, + _savedDpi.Height, + BitmapOptions.Target | BitmapOptions.CannotDraw))) + { + _deviceContext.Target = d2dBackBuffer; + } + } + } + + protected abstract SwapChain1 CreateSwapChain(Factory2 dxgiFactory, SwapChainDescription1 swapChainDesc); + + protected abstract Size2F GetWindowDpi(); + + protected abstract Size2 GetWindowSize(); + } +} diff --git a/src/Windows/Avalonia.Win32/IconImpl.cs b/src/Windows/Avalonia.Win32/IconImpl.cs index b9e3378b6f..b7293397d7 100644 --- a/src/Windows/Avalonia.Win32/IconImpl.cs +++ b/src/Windows/Avalonia.Win32/IconImpl.cs @@ -27,18 +27,16 @@ namespace Avalonia.Win32 public IntPtr HIcon => icon?.Handle ?? bitmap.GetHicon(); - public Stream Save() + public void Save(Stream outputStream) { - var stream = new MemoryStream(); if (icon != null) { - icon.Save(stream); + icon.Save(outputStream); } else { - bitmap.Save(stream, ImageFormat.Png); + bitmap.Save(outputStream, ImageFormat.Png); } - return stream; } } } diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index f153b26412..1941d6b60e 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -88,9 +88,16 @@ namespace Avalonia.Win32 var pofn = &ofn; + // We should save the current directory to restore it later. + var currentDirectory = Environment.CurrentDirectory; + var res = dialog is OpenFileDialog ? UnmanagedMethods.GetOpenFileName(new IntPtr(pofn)) : UnmanagedMethods.GetSaveFileName(new IntPtr(pofn)); + + // Restore the old current directory, since GetOpenFileName and GetSaveFileName change it after they're called + Environment.CurrentDirectory = currentDirectory; + if (!res) return null; if (dialog?.Filters.Count > 0) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index b769a8dbdb..73c647abf5 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -709,7 +709,10 @@ namespace Avalonia.Win32 MaximizeWithoutCoveringTaskbar(); } - SetFocus(_hwnd); + if (!Design.IsDesignMode) + { + SetFocus(_hwnd); + } } private void MaximizeWithoutCoveringTaskbar() diff --git a/src/iOS/Avalonia.iOS/PlatformIconLoader.cs b/src/iOS/Avalonia.iOS/PlatformIconLoader.cs index dc9d87660f..ca54a660b3 100644 --- a/src/iOS/Avalonia.iOS/PlatformIconLoader.cs +++ b/src/iOS/Avalonia.iOS/PlatformIconLoader.cs @@ -31,18 +31,16 @@ namespace Avalonia.iOS // Stores the icon created as a stream to support saving even though an icon is never shown public class FakeIcon : IWindowIconImpl { - private Stream stream = new MemoryStream(); + private readonly Stream stream = new MemoryStream(); public FakeIcon(Stream stream) { stream.CopyTo(this.stream); } - public Stream Save() + public void Save(Stream outputStream) { - var returnStream = new MemoryStream(); - stream.CopyTo(returnStream); - return returnStream; + stream.CopyTo(outputStream); } } } \ No newline at end of file diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 7b7a949e7c..ecb555252d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Reactive.Subjects; +using Avalonia; using Avalonia.Data; using Avalonia.Logging; using Avalonia.UnitTests; @@ -410,6 +411,34 @@ namespace Avalonia.Base.UnitTests Assert.True(called); } + [Fact] + public void AddOwner_Should_Inherit_DefaultBindingMode() + { + var foo = new DirectProperty( + "foo", + o => "foo", + null, + new DirectPropertyMetadata(defaultBindingMode: BindingMode.TwoWay)); + var bar = foo.AddOwner(o => "bar"); + + Assert.Equal(BindingMode.TwoWay, bar.GetMetadata().DefaultBindingMode); + Assert.Equal(BindingMode.TwoWay, bar.GetMetadata().DefaultBindingMode); + } + + [Fact] + public void AddOwner_Can_Override_DefaultBindingMode() + { + var foo = new DirectProperty( + "foo", + o => "foo", + null, + new DirectPropertyMetadata(defaultBindingMode: BindingMode.TwoWay)); + var bar = foo.AddOwner(o => "bar", defaultBindingMode: BindingMode.OneWayToSource); + + Assert.Equal(BindingMode.TwoWay, bar.GetMetadata().DefaultBindingMode); + Assert.Equal(BindingMode.OneWayToSource, bar.GetMetadata().DefaultBindingMode); + } + private class Class1 : AvaloniaObject { public static readonly DirectProperty FooProperty = diff --git a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs index 7e40926679..3a37585dc0 100644 --- a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs @@ -34,7 +34,7 @@ namespace Avalonia.Base.UnitTests "test", o => null, null, - new PropertyMetadata()); + new DirectPropertyMetadata()); Assert.True(target.IsDirect); } diff --git a/tests/Avalonia.Controls.UnitTests/ControlTests_NameScope.cs b/tests/Avalonia.Controls.UnitTests/ControlTests_NameScope.cs index ec75c2390b..9f39f7a47a 100644 --- a/tests/Avalonia.Controls.UnitTests/ControlTests_NameScope.cs +++ b/tests/Avalonia.Controls.UnitTests/ControlTests_NameScope.cs @@ -70,5 +70,23 @@ namespace Avalonia.Controls.UnitTests Assert.Null(NameScope.GetNameScope((Control)root.Presenter).Find("foo")); } + + [Fact] + public void Control_That_Is_NameScope_Should_Register_With_Parent_NameScope() + { + UserControl userControl; + var root = new TestTemplatedRoot + { + Content = userControl = new UserControl + { + Name = "foo", + } + }; + + root.ApplyTemplate(); + + Assert.Same(userControl, root.FindControl("foo")); + Assert.Same(userControl, userControl.FindControl("foo")); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index f8eea8c4eb..a588e88eb2 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -153,6 +153,23 @@ namespace Avalonia.Controls.UnitTests Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected); } + [Fact] + public void ScrollViewer_Should_Have_Correct_Extent_And_Viewport() + { + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(), + ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + SelectedIndex = 0, + }; + + Prepare(target); + + Assert.Equal(new Size(20, 20), target.Scroll.Extent); + Assert.Equal(new Size(100, 10), target.Scroll.Viewport); + } + private FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate(parent => @@ -233,6 +250,7 @@ namespace Avalonia.Controls.UnitTests i.InvalidateMeasure(); } + target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs index a54ff022ac..88d26334ed 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs @@ -171,6 +171,22 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal("foo", target.DataContext); } + [Fact] + public void Assigning_Control_To_Content_After_NonControl_Should_Clear_DataContext() + { + var target = new ContentPresenter(); + + target.Content = "foo"; + target.UpdateChild(); + + Assert.True(target.IsSet(Control.DataContextProperty)); + + target.Content = new Border(); + target.UpdateChild(); + + Assert.False(target.IsSet(Control.DataContextProperty)); + } + [Fact] public void Tries_To_Recycle_DataTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index a0192cfab8..58f4127cdc 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; @@ -113,7 +112,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroll = (ScrollContentPresenter)target.Parent; Assert.Equal(new Size(10, 20), scroll.Extent); - Assert.Equal(new Size(0, 10), scroll.Viewport); + Assert.Equal(new Size(100, 10), scroll.Viewport); } [Fact] @@ -255,7 +254,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(10, target.Panel.Children.Count); Assert.Equal(new Size(10, 20), scroll.Extent); - Assert.Equal(new Size(0, 10), scroll.Viewport); + Assert.Equal(new Size(100, 10), scroll.Viewport); target.VirtualizationMode = ItemVirtualizationMode.None; target.Measure(new Size(100, 100)); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 5414fd80da..38b51dfd49 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -786,6 +786,24 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(new Size(10, 20), ((ILogicalScrollable)target).Extent); Assert.Equal(new Size(5, 10), ((ILogicalScrollable)target).Viewport); } + + [Fact] + public void Horizontal_Scroll_Should_Update_Item_Position() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + + target.Measure(new Size(5, 100)); + target.Arrange(new Rect(0, 0, 5, 100)); + + ((ILogicalScrollable)target).Offset = new Vector(5, 0); + + target.Measure(new Size(5, 100)); + target.Arrange(new Rect(0, 0, 5, 100)); + + Assert.Equal(new Rect(-5, 0, 10, 10), target.Panel.Children[0].Bounds); + } } public class Horizontal diff --git a/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs b/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs index 8410c2aa3e..54f9a87f94 100644 --- a/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs +++ b/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs @@ -6,6 +6,7 @@ using Xunit.Abstractions; namespace Avalonia.LeakTests { + [DotMemoryUnit(FailIfRunWithoutSupport = false)] public class AvaloniaObjectTests { public AvaloniaObjectTests(ITestOutputHelper atr) diff --git a/tests/Avalonia.LeakTests/MemberSelectorTests.cs b/tests/Avalonia.LeakTests/MemberSelectorTests.cs index d794e788fd..ffee18ae0a 100644 --- a/tests/Avalonia.LeakTests/MemberSelectorTests.cs +++ b/tests/Avalonia.LeakTests/MemberSelectorTests.cs @@ -4,12 +4,20 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using JetBrains.dotMemoryUnit; using Xunit; +using Xunit.Abstractions; namespace Avalonia.LeakTests { + [DotMemoryUnit(FailIfRunWithoutSupport = false)] public class MemberSelectorTests { + public MemberSelectorTests(ITestOutputHelper atr) + { + DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); + } + [Fact] public void Should_Not_Hold_Reference_To_Object() { diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs index 3238435841..0705ae9c5a 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; using System.Reactive.Linq; +using System.Reactive.Subjects; using Avalonia.Markup.Data; using Avalonia.UnitTests; using Xunit; @@ -54,6 +54,28 @@ namespace Avalonia.Markup.UnitTests.Data Assert.False(target.SetValue("foo")); } + /// + /// Test for #831 - Bound properties are incorrectly updated when changing tab items. + /// + /// + /// There was a bug whereby pushing a null as the ExpressionObserver root didn't update + /// the leaf node, cauing a subsequent SetValue to update an object that should have become + /// unbound. + /// + [Fact] + public void Pushing_Null_To_RootObservable_Updates_Leaf_Node() + { + var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; + var rootObservable = new BehaviorSubject(data); + var target = new ExpressionObserver(rootObservable, "Foo.Bar"); + + target.Subscribe(_ => { }); + rootObservable.OnNext(null); + target.SetValue("baz"); + + Assert.Equal("bar", data.Foo.Bar); + } + private class Class1 : NotifyingBase { private Class2 _foo; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index 4ad740bab0..f820f7d5ab 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -96,6 +96,7 @@ + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Self.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Self.cs new file mode 100644 index 0000000000..e0d16a9563 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Self.cs @@ -0,0 +1,63 @@ +// 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 Moq; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Markup.Xaml.Data; +using Avalonia.Styling; +using Xunit; +using System.Reactive.Disposables; + +namespace Avalonia.Markup.Xaml.UnitTests.Data +{ + public class BindingTests_Self + { + [Fact] + public void Binding_To_Property_On_Self_Should_Work() + { + var target = new TextBlock + { + Tag = "Hello World!", + [!TextBlock.TextProperty] = new Binding("Tag") + { + RelativeSource = new RelativeSource(RelativeSourceMode.Self) + }, + }; + + Assert.Equal("Hello World!", target.Text); + } + + [Fact] + public void TwoWay_Binding_To_Property_On_Self_Should_Work() + { + var target = new TextBlock + { + Tag = "Hello World!", + [!TextBlock.TextProperty] = new Binding("Tag", BindingMode.TwoWay) + { + RelativeSource = new RelativeSource(RelativeSourceMode.Self) + }, + }; + + Assert.Equal("Hello World!", target.Text); + target.Text = "Goodbye cruel world :("; + Assert.Equal("Goodbye cruel world :(", target.Text); + } + + private Mock CreateTarget( + ITemplatedControl templatedParent = null, + string text = null) + { + var result = new Mock(); + + result.Setup(x => x.GetValue(Control.TemplatedParentProperty)).Returns(templatedParent); + result.Setup(x => x.GetValue((AvaloniaProperty)Control.TemplatedParentProperty)).Returns(templatedParent); + result.Setup(x => x.GetValue((AvaloniaProperty)TextBox.TextProperty)).Returns(text); + result.Setup(x => x.Bind(It.IsAny(), It.IsAny>(), It.IsAny())) + .Returns(Disposable.Empty); + return result; + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 868471466a..a66d6ac6d8 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -145,5 +145,49 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal("foo", border.DataContext); } } + + [Fact(Skip = "OmniXaml doesn't support nested markup extensions. #119")] + public void Binding_To_Self_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = (TextBlock)window.Content; + + window.ApplyTemplate(); + + Assert.Equal("foo", textBlock.Text); + } + } + + [Fact] + public void Longform_Binding_To_Self_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = (TextBlock)window.Content; + + window.ApplyTemplate(); + + Assert.Equal("foo", textBlock.Text); + } + } } }