diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 6b910fc615..4e34d4b132 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,6 @@ about: Create a report to help us improve Avalonia title: '' labels: bug assignees: '' - --- **Describe the bug** @@ -12,6 +11,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,8 +24,9 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. Windows, Mac, Linux (State distribution)] - - Version [e.g. 0.10.0-rc1 or 0.9.12] + +- OS: [e.g. Windows, Mac, Linux (State distribution)] +- Version [e.g. 0.10.0-rc1 or 0.9.12] **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..687355d825 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Questions, Discussions, Ideas + url: https://github.com/AvaloniaUI/Avalonia/discussions/new + about: Please ask and answer questions here. + - name: Avalonia Community Support on Gitter + url: https://gitter.im/AvaloniaUI/Avalonia + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 11fc491ef1..5f0a04cee3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,6 @@ about: Suggest an idea for this project title: '' labels: enhancement assignees: '' - --- **Is your feature request related to a problem? Please describe.** diff --git a/native/Avalonia.Native/src/OSX/Screens.mm b/native/Avalonia.Native/src/OSX/Screens.mm index 455cfa2e41..10f698ff45 100644 --- a/native/Avalonia.Native/src/OSX/Screens.mm +++ b/native/Avalonia.Native/src/OSX/Screens.mm @@ -5,12 +5,6 @@ class Screens : public ComSingleObject public: FORWARD_IUNKNOWN() - private: - CGFloat PrimaryDisplayHeight() - { - return NSMaxY([[[NSScreen screens] firstObject] frame]); - } - public: virtual HRESULT GetScreenCount (int* ret) override { @@ -36,12 +30,12 @@ public: ret->Bounds.Height = [screen frame].size.height; ret->Bounds.Width = [screen frame].size.width; ret->Bounds.X = [screen frame].origin.x; - ret->Bounds.Y = PrimaryDisplayHeight() - [screen frame].origin.y - ret->Bounds.Height; + ret->Bounds.Y = ConvertPointY(ToAvnPoint([screen frame].origin)).Y - ret->Bounds.Height; ret->WorkingArea.Height = [screen visibleFrame].size.height; ret->WorkingArea.Width = [screen visibleFrame].size.width; ret->WorkingArea.X = [screen visibleFrame].origin.x; - ret->WorkingArea.Y = ret->Bounds.Height - [screen visibleFrame].origin.y - ret->WorkingArea.Height; + ret->WorkingArea.Y = ConvertPointY(ToAvnPoint([screen visibleFrame].origin)).Y - ret->WorkingArea.Height; ret->PixelDensity = [screen backingScaleFactor]; diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 871bca086d..ea48a19874 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -34,6 +34,7 @@ extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); extern AvnPoint ToAvnPoint (NSPoint p); extern AvnPoint ConvertPointY (AvnPoint p); +extern CGFloat PrimaryDisplayHeight(); extern NSSize ToNSSize (AvnSize s); #ifdef DEBUG #define NSDebugLog(...) NSLog(__VA_ARGS__) diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index cd6ef73826..72f5aa0a7d 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -299,10 +299,14 @@ AvnPoint ToAvnPoint (NSPoint p) AvnPoint ConvertPointY (AvnPoint p) { - auto sw = [NSScreen.screens objectAtIndex:0].frame; + auto primaryDisplayHeight = NSMaxY([[[NSScreen screens] firstObject] frame]); - auto t = MAX(sw.origin.y, sw.origin.y + sw.size.height); - p.Y = t - p.Y; + p.Y = primaryDisplayHeight - p.Y; return p; } + +CGFloat PrimaryDisplayHeight() +{ + return NSMaxY([[[NSScreen screens] firstObject] frame]); +} diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 8419258fe9..9d49025398 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1391,17 +1391,20 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [super viewDidChangeBackingProperties]; } -- (bool) ignoreUserInput +- (bool) ignoreUserInput:(bool)trigerInputWhenDisabled { auto parentWindow = objc_cast([self window]); if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) { - auto window = dynamic_cast(_parent.getRaw()); - - if(window != nullptr) + if(trigerInputWhenDisabled) { - window->WindowEvents->GotInputWhenDisabled(); + auto window = dynamic_cast(_parent.getRaw()); + + if(window != nullptr) + { + window->WindowEvents->GotInputWhenDisabled(); + } } return TRUE; @@ -1412,7 +1415,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type { - if([self ignoreUserInput]) + bool triggerInputWhenDisabled = type != Move; + + if([self ignoreUserInput: triggerInputWhenDisabled]) { return; } @@ -1578,7 +1583,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type { - if([self ignoreUserInput]) + if([self ignoreUserInput: false]) { return; } diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index c39f414b44..4edb0f137a 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -29,7 +29,7 @@ namespace ControlCatalog.Pages var screens = w.Screens.All; var scaling = ((IRenderRoot)w).RenderScaling; - var drawBrush = Brushes.Green; + var drawBrush = Brushes.Black; Pen p = new Pen(drawBrush); if (screens != null) foreach (Screen screen in screens) @@ -45,18 +45,16 @@ namespace ControlCatalog.Pages screen.Bounds.Height / 10f); Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f, screen.WorkingArea.Height / 10f); + context.DrawRectangle(p, boundsRect); context.DrawRectangle(p, workingAreaRect); - - FormattedText text = new FormattedText() - { - Typeface = Typeface.Default - }; - text.Text = $"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}"; + var text = new FormattedText() { Typeface = new Typeface("Arial"), FontSize = 18 }; + + text.Text = $"Bounds: {screen.Bounds.TopLeft} {screen.Bounds.Width}:{screen.Bounds.Height}"; context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height), text); - text.Text = $"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}"; + text.Text = $"WorkArea: {screen.WorkingArea.TopLeft} {screen.WorkingArea.Width}:{screen.WorkingArea.Height}"; context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text); text.Text = $"Scaling: {screen.PixelDensity * 100}%"; @@ -69,7 +67,7 @@ namespace ControlCatalog.Pages context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text); } - context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10, w.Bounds.Width / 10, w.Bounds.Height / 10)); + context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f, w.Bounds.Width / 10, w.Bounds.Height / 10)); } } } diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 91eef3947b..c779e4b0cb 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -80,6 +80,7 @@ namespace Avalonia.Controls private ICommand _command; private bool _commandCanExecute = true; + private KeyGesture _hotkey; /// /// Initializes static members of the class. @@ -207,6 +208,11 @@ namespace Avalonia.Controls protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { + if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control + { + HotKey = _hotkey; + } + base.OnAttachedToLogicalTree(e); if (Command != null) @@ -217,6 +223,13 @@ namespace Avalonia.Controls protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { + // This will cause the hotkey manager to dispose the observer and the reference to this control + if (HotKey != null) + { + _hotkey = HotKey; + HotKey = null; + } + base.OnDetachedFromLogicalTree(e); if (Command != null) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index d034f54df2..2d91a2ee7e 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -102,6 +102,7 @@ namespace Avalonia.Controls private ICommand? _command; private bool _commandCanExecute = true; private Popup? _popup; + private KeyGesture _hotkey; /// /// Initializes static members of the class. @@ -111,6 +112,7 @@ namespace Avalonia.Controls SelectableMixin.Attach(IsSelectedProperty); PressedMixin.Attach(); CommandProperty.Changed.Subscribe(CommandChanged); + CommandParameterProperty.Changed.Subscribe(CommandParameterChanged); FocusableProperty.OverrideDefaultValue(true); HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); IconProperty.Changed.AddClassHandler((x, e) => x.IconChanged(e)); @@ -337,6 +339,11 @@ namespace Avalonia.Controls protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { + if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control + { + HotKey = _hotkey; + } + base.OnAttachedToLogicalTree(e); if (Command != null) @@ -347,6 +354,13 @@ namespace Avalonia.Controls protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { + // This will cause the hotkey manager to dispose the observer and the reference to this control + if (HotKey != null) + { + _hotkey = HotKey; + HotKey = null; + } + base.OnDetachedFromLogicalTree(e); if (Command != null) @@ -493,6 +507,18 @@ namespace Avalonia.Controls } } + /// + /// Called when the property changes. + /// + /// The event args. + private static void CommandParameterChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is MenuItem menuItem) + { + menuItem.CanExecuteChanged(menuItem, EventArgs.Empty); + } + } + /// /// Called when the event fires. /// diff --git a/src/Avalonia.Controls/Utils/AncestorFinder.cs b/src/Avalonia.Controls/Utils/AncestorFinder.cs index 529aec1393..42e0d85dcd 100644 --- a/src/Avalonia.Controls/Utils/AncestorFinder.cs +++ b/src/Avalonia.Controls/Utils/AncestorFinder.cs @@ -47,6 +47,8 @@ namespace Avalonia.Controls.Utils public void Dispose() { + _child?.Dispose(); + _subject.Dispose(); _disposable.Dispose(); } } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 927b739b0c..11d5801532 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -383,7 +383,7 @@ namespace Avalonia.Native _native.BeginMoveDrag(); } - public Size MaxAutoSizeHint => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity)) + public Size MaxAutoSizeHint => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(1)) .OrderByDescending(x => x.Width + x.Height).FirstOrDefault(); public void SetTopmost(bool value) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs index 1f2508715e..ac78f32231 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs @@ -15,7 +15,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { if (d.Namespace == XamlNamespaces.Xaml2006) { - if (d.Name == "Precompile" || d.Name == "Class") + if (d.Name == "Precompile" || + d.Name == "Class" || + d.Name == "FieldModifier" || + d.Name == "ClassModifier") no.Children.Remove(d); } } diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 89e2c096ee..7b9cd0212e 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -2,6 +2,8 @@ netstandard2.0 Avalonia + Enable + CS8600;CS8602;CS8603 diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index bf43730481..50be598d7f 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -39,17 +39,17 @@ namespace Avalonia.Data /// /// Gets or sets the name of the element to use as the binding source. /// - public string ElementName { get; set; } + public string? ElementName { get; set; } /// /// Gets or sets the relative source for the binding. /// - public RelativeSource RelativeSource { get; set; } + public RelativeSource? RelativeSource { get; set; } /// /// Gets or sets the source for the binding. /// - public object Source { get; set; } + public object? Source { get; set; } /// /// Gets or sets the binding path. @@ -59,24 +59,36 @@ namespace Avalonia.Data /// /// Gets or sets a function used to resolve types from names in the binding path. /// - public Func TypeResolver { get; set; } + public Func? TypeResolver { get; set; } - protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) + protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object? anchor, bool enableDataValidation) { - Contract.Requires(target != null); - anchor = anchor ?? DefaultAnchor?.Target; - + _ = target ?? throw new ArgumentNullException(nameof(target)); + + anchor ??= DefaultAnchor?.Target; enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; - INameScope nameScope = null; + INameScope? nameScope = null; NameScope?.TryGetTarget(out nameScope); var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver, nameScope); + if (node is null) + { + throw new InvalidOperationException("Could not parse binding expression."); + } + + IStyledElement GetSource() + { + return target as IStyledElement ?? + anchor as IStyledElement ?? + throw new ArgumentException("Could not find binding source: either target or anchor must be an IStyledElement."); + } + if (ElementName != null) { return CreateElementObserver( - (target as IStyledElement) ?? (anchor as IStyledElement), + GetSource(), ElementName, node); } @@ -96,9 +108,7 @@ namespace Avalonia.Data } else { - return CreateSourceObserver( - (target as IStyledElement) ?? (anchor as IStyledElement), - node); + return CreateSourceObserver(GetSource(), node); } } else if (RelativeSource.Mode == RelativeSourceMode.DataContext) @@ -111,15 +121,11 @@ namespace Avalonia.Data } else if (RelativeSource.Mode == RelativeSourceMode.Self) { - return CreateSourceObserver( - (target as IStyledElement) ?? (anchor as IStyledElement), - node); + return CreateSourceObserver(GetSource(), node); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - return CreateTemplatedParentObserver( - (target as IStyledElement) ?? (anchor as IStyledElement), - node); + return CreateTemplatedParentObserver(GetSource(), node); } else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor) { @@ -128,10 +134,7 @@ namespace Avalonia.Data throw new InvalidOperationException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree."); } - return CreateFindAncestorObserver( - (target as IStyledElement) ?? (anchor as IStyledElement), - RelativeSource, - node); + return CreateFindAncestorObserver(GetSource(), RelativeSource, node); } else { diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index 3dbc83a7df..c25ef49167 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -38,22 +38,22 @@ namespace Avalonia.Data /// /// Gets or sets the to use. /// - public IValueConverter Converter { get; set; } + public IValueConverter? Converter { get; set; } /// /// Gets or sets a parameter to pass to . /// - public object ConverterParameter { get; set; } + public object? ConverterParameter { get; set; } /// /// Gets or sets the value to use when the binding is unable to produce a value. /// - public object FallbackValue { get; set; } + public object? FallbackValue { get; set; } /// /// Gets or sets the value to use when the binding result is null. /// - public object TargetNullValue { get; set; } + public object? TargetNullValue { get; set; } /// /// Gets or sets the binding mode. @@ -68,26 +68,27 @@ namespace Avalonia.Data /// /// Gets or sets the string format. /// - public string StringFormat { get; set; } + public string? StringFormat { get; set; } - public WeakReference DefaultAnchor { get; set; } + public WeakReference? DefaultAnchor { get; set; } - public WeakReference NameScope { get; set; } + public WeakReference? NameScope { get; set; } protected abstract ExpressionObserver CreateExpressionObserver( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor, + object? anchor, bool enableDataValidation); /// public InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null, + object? anchor = null, bool enableDataValidation = false) { - Contract.Requires(target != null); + _ = target ?? throw new ArgumentNullException(nameof(target)); + anchor = anchor ?? DefaultAnchor?.Target; enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; @@ -133,18 +134,13 @@ namespace Avalonia.Data IAvaloniaObject target, ExpressionNode node, bool targetIsDataContext, - object anchor) + object? anchor) { - Contract.Requires(target != null); + _ = target ?? throw new ArgumentNullException(nameof(target)); if (!(target is IDataContextProvider)) { - target = anchor as IDataContextProvider; - - if (target == null) - { - throw new InvalidOperationException("Cannot find a DataContext to bind to."); - } + target = anchor as IDataContextProvider ?? throw new InvalidOperationException("Cannot find a DataContext to bind to."); } if (!targetIsDataContext) @@ -171,10 +167,9 @@ namespace Avalonia.Data string elementName, ExpressionNode node) { - Contract.Requires(target != null); + _ = target ?? throw new ArgumentNullException(nameof(target)); - NameScope.TryGetTarget(out var scope); - if (scope == null) + if (NameScope is null || !NameScope.TryGetTarget(out var scope) || scope is null) throw new InvalidOperationException("Name scope is null or was already collected"); var result = new ExpressionObserver( NameScopeLocator.Track(scope, elementName), @@ -188,9 +183,9 @@ namespace Avalonia.Data RelativeSource relativeSource, ExpressionNode node) { - Contract.Requires(target != null); + _ = target ?? throw new ArgumentNullException(nameof(target)); - IObservable controlLocator; + IObservable controlLocator; switch (relativeSource.Tree) { @@ -220,7 +215,7 @@ namespace Avalonia.Data object source, ExpressionNode node) { - Contract.Requires(source != null); + _ = source ?? throw new ArgumentNullException(nameof(source)); return new ExpressionObserver(source, node); } @@ -229,7 +224,7 @@ namespace Avalonia.Data IAvaloniaObject target, ExpressionNode node) { - Contract.Requires(target != null); + _ = target ?? throw new ArgumentNullException(nameof(target)); var result = new ExpressionObserver( () => target.GetValue(StyledElement.TemplatedParentProperty), @@ -240,7 +235,7 @@ namespace Avalonia.Data return result; } - protected IObservable GetParentDataContext(IAvaloniaObject target) + protected IObservable GetParentDataContext(IAvaloniaObject target) { // The DataContext is based on the visual parent and not the logical parent: this may // seem counter intuitive considering the fact that property inheritance works on the logical @@ -252,7 +247,7 @@ namespace Avalonia.Data .Select(x => { return (x as IAvaloniaObject)?.GetObservable(StyledElement.DataContextProperty) ?? - Observable.Return((object)null); + Observable.Return((object?)null); }).Switch(); } diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index cbc5f414f2..17b033f238 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -22,12 +22,12 @@ namespace Avalonia.Data /// /// Gets or sets the to use. /// - public IMultiValueConverter Converter { get; set; } + public IMultiValueConverter? Converter { get; set; } /// /// Gets or sets a parameter to pass to . /// - public object ConverterParameter { get; set; } + public object? ConverterParameter { get; set; } /// /// Gets or sets the value to use when the binding is unable to produce a value. @@ -52,12 +52,12 @@ namespace Avalonia.Data /// /// Gets or sets the relative source for the binding. /// - public RelativeSource RelativeSource { get; set; } + public RelativeSource? RelativeSource { get; set; } /// /// Gets or sets the string format. /// - public string StringFormat { get; set; } + public string? StringFormat { get; set; } public MultiBinding() { @@ -69,7 +69,7 @@ namespace Avalonia.Data public InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null, + object? anchor = null, bool enableDataValidation = false) { var targetType = targetProperty?.PropertyType ?? typeof(object); @@ -105,7 +105,7 @@ namespace Avalonia.Data } } - private object ConvertValue(IList values, Type targetType, IMultiValueConverter converter) + private object ConvertValue(IList values, Type targetType, IMultiValueConverter? converter) { for (var i = 0; i < values.Count; ++i) { @@ -116,7 +116,7 @@ namespace Avalonia.Data } var culture = CultureInfo.CurrentCulture; - values = new System.Collections.ObjectModel.ReadOnlyCollection(values); + values = new System.Collections.ObjectModel.ReadOnlyCollection(values); object converted; if (converter != null) { diff --git a/src/Markup/Avalonia.Markup/Data/RelativeSource.cs b/src/Markup/Avalonia.Markup/Data/RelativeSource.cs index e3d2dd4aaa..037175a137 100644 --- a/src/Markup/Avalonia.Markup/Data/RelativeSource.cs +++ b/src/Markup/Avalonia.Markup/Data/RelativeSource.cs @@ -94,7 +94,7 @@ namespace Avalonia.Data /// /// Gets the type of ancestor to look for when in mode. /// - public Type AncestorType { get; set; } + public Type? AncestorType { get; set; } /// /// Gets or sets a value that describes the type of relative source lookup. diff --git a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs index 83f02c52aa..b6c723d68c 100644 --- a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs @@ -17,8 +17,8 @@ namespace Avalonia.Data ISetterValue { private bool _isSetterValue; - private IStyledElement _target; - private Type _targetType; + private IStyledElement _target = default!; + private Type? _targetType; public TemplateBinding() { @@ -33,7 +33,7 @@ namespace Avalonia.Data public InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null, + object? anchor = null, bool enableDataValidation = false) { // Usually each `TemplateBinding` will only be instantiated once; in this case we can @@ -68,12 +68,12 @@ namespace Avalonia.Data /// /// Gets or sets the to use. /// - public IValueConverter Converter { get; set; } + public IValueConverter? Converter { get; set; } /// /// Gets or sets a parameter to pass to . /// - public object ConverterParameter { get; set; } + public object? ConverterParameter { get; set; } /// /// Gets or sets the binding mode. @@ -83,7 +83,7 @@ namespace Avalonia.Data /// /// Gets or sets the name of the source property on the templated parent. /// - public AvaloniaProperty Property { get; set; } + public AvaloniaProperty? Property { get; set; } /// public string Description => "TemplateBinding: " + Property; @@ -164,10 +164,7 @@ namespace Avalonia.Data { if (e.Property == StyledElement.TemplatedParentProperty) { - var oldValue = (IAvaloniaObject)e.OldValue; - var newValue = (IAvaloniaObject)e.OldValue; - - if (oldValue != null) + if (e.OldValue is IAvaloniaObject oldValue) { oldValue.PropertyChanged -= TemplatedParentPropertyChanged; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs index 7c362e24cc..439bc15243 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -6,6 +6,8 @@ using Avalonia.Utilities; using System; using System.Collections.Generic; +#nullable enable + namespace Avalonia.Markup.Parsers { internal enum SourceMode @@ -271,8 +273,8 @@ namespace Avalonia.Markup.Parsers } else if (mode.SequenceEqual("parent".AsSpan())) { - string ancestorNamespace = null; - string ancestorType = null; + string? ancestorNamespace = null; + string? ancestorType = null; var ancestorLevel = 0; if (PeekOpenBracket(ref r)) { @@ -424,19 +426,19 @@ namespace Avalonia.Markup.Parsers public class PropertyNameNode : INode { - public string PropertyName { get; set; } + public string PropertyName { get; set; } = string.Empty; } public class AttachedPropertyNameNode : INode { - public string Namespace { get; set; } - public string TypeName { get; set; } - public string PropertyName { get; set; } + public string Namespace { get; set; } = string.Empty; + public string TypeName { get; set; } = string.Empty; + public string PropertyName { get; set; } = string.Empty; } public class IndexerNode : INode { - public IList Arguments { get; set; } + public IList Arguments { get; set; } = Array.Empty(); } public class NotNode : INode, ITransformNode { } @@ -447,20 +449,20 @@ namespace Avalonia.Markup.Parsers public class NameNode : INode { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; } public class AncestorNode : INode { - public string Namespace { get; set; } - public string TypeName { get; set; } + public string? Namespace { get; set; } + public string? TypeName { get; set; } public int Level { get; set; } } public class TypeCastNode : INode { - public string Namespace { get; set; } - public string TypeName { get; set; } + public string Namespace { get; set; } = string.Empty; + public string TypeName { get; set; } = string.Empty; } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index f957bcab1e..ebdad881c3 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -8,8 +8,8 @@ namespace Avalonia.Markup.Parsers { public static class ExpressionObserverBuilder { - internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func typeResolver = null, - INameScope nameScope = null) + internal static (ExpressionNode? Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func? typeResolver = null, + INameScope? nameScope = null) { if (string.IsNullOrWhiteSpace(expression)) { @@ -32,8 +32,8 @@ namespace Avalonia.Markup.Parsers object root, string expression, bool enableDataValidation = false, - string description = null, - Func typeResolver = null) + string? description = null, + Func? typeResolver = null) { return new ExpressionObserver( root, @@ -45,10 +45,11 @@ namespace Avalonia.Markup.Parsers IObservable rootObservable, string expression, bool enableDataValidation = false, - string description = null, - Func typeResolver = null) + string? description = null, + Func? typeResolver = null) { - Contract.Requires(rootObservable != null); + _ = rootObservable ?? throw new ArgumentNullException(nameof(rootObservable)); + return new ExpressionObserver( rootObservable, Parse(expression, enableDataValidation, typeResolver).Node, @@ -61,10 +62,10 @@ namespace Avalonia.Markup.Parsers string expression, IObservable update, bool enableDataValidation = false, - string description = null, - Func typeResolver = null) + string? description = null, + Func? typeResolver = null) { - Contract.Requires(rootGetter != null); + _ = rootGetter ?? throw new ArgumentNullException(nameof(rootGetter)); return new ExpressionObserver( rootGetter, diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 558130e23f..0a6f3f82ba 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -11,25 +11,25 @@ namespace Avalonia.Markup.Parsers internal class ExpressionParser { private readonly bool _enableValidation; - private readonly Func _typeResolver; - private readonly INameScope _nameScope; + private readonly Func? _typeResolver; + private readonly INameScope? _nameScope; - public ExpressionParser(bool enableValidation, Func typeResolver, INameScope nameScope) + public ExpressionParser(bool enableValidation, Func? typeResolver, INameScope? nameScope) { _typeResolver = typeResolver; _nameScope = nameScope; _enableValidation = enableValidation; } - public (ExpressionNode Node, SourceMode Mode) Parse(ref CharacterReader r) + public (ExpressionNode? Node, SourceMode Mode) Parse(ref CharacterReader r) { - ExpressionNode rootNode = null; - ExpressionNode node = null; + ExpressionNode? rootNode = null; + ExpressionNode? node = null; var (astNodes, mode) = BindingExpressionGrammar.Parse(ref r); foreach (var astNode in astNodes) { - ExpressionNode nextNode = null; + ExpressionNode? nextNode = null; switch (astNode) { case BindingExpressionGrammar.EmptyExpressionNode _: @@ -57,13 +57,13 @@ namespace Avalonia.Markup.Parsers nextNode = ParseFindAncestor(ancestor); break; case BindingExpressionGrammar.NameNode elementName: - nextNode = new ElementNameNode(_nameScope, elementName.Name); + nextNode = new ElementNameNode(_nameScope ?? throw new NotSupportedException("Invalid element name binding with null name scope!"), elementName.Name); break; case BindingExpressionGrammar.TypeCastNode typeCast: nextNode = ParseTypeCastNode(typeCast); break; } - if (rootNode is null) + if (node is null) { rootNode = node = nextNode; } @@ -79,7 +79,7 @@ namespace Avalonia.Markup.Parsers private FindAncestorNode ParseFindAncestor(BindingExpressionGrammar.AncestorNode node) { - Type ancestorType = null; + Type? ancestorType = null; var ancestorLevel = node.Level; if (!(node.Namespace is null) && !(node.TypeName is null)) @@ -97,7 +97,7 @@ namespace Avalonia.Markup.Parsers private TypeCastNode ParseTypeCastNode(BindingExpressionGrammar.TypeCastNode node) { - Type castType = null; + Type? castType = null; if (!(node.Namespace is null) && !(node.TypeName is null)) { if (_typeResolver == null) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index 97198145a8..d6068a15d4 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -9,7 +9,7 @@ namespace Avalonia.Markup.Parsers.Nodes { private readonly WeakReference _nameScope; private readonly string _name; - private IDisposable _subscription; + private IDisposable? _subscription; public ElementNameNode(INameScope nameScope, string name) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index f304d1e9a2..4124032113 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -7,10 +7,10 @@ namespace Avalonia.Markup.Parsers.Nodes public class FindAncestorNode : ExpressionNode { private readonly int _level; - private readonly Type _ancestorType; - private IDisposable _subscription; + private readonly Type? _ancestorType; + private IDisposable? _subscription; - public FindAncestorNode(Type ancestorType, int level) + public FindAncestorNode(Type? ancestorType, int level) { _level = level; _ancestorType = ancestorType; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs index f3abd6a5c5..2f1756fc55 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs @@ -29,15 +29,15 @@ namespace Avalonia.Markup.Parsers.Nodes var list = target as IList; var dictionary = target as IDictionary; var indexerProperty = GetIndexer(typeInfo); - var indexerParameters = indexerProperty?.GetIndexParameters(); + ParameterInfo[] indexerParameters; - if (indexerProperty != null && indexerParameters.Length == Arguments.Count) + if (indexerProperty != null && (indexerParameters = indexerProperty.GetIndexParameters()).Length == Arguments.Count) { var convertedObjectArray = new object[indexerParameters.Length]; for (int i = 0; i < Arguments.Count; i++) { - object temp = null; + object? temp = null; if (!TypeUtilities.TryConvert(indexerParameters[i].ParameterType, Arguments[i], CultureInfo.InvariantCulture, out temp)) { @@ -125,7 +125,7 @@ namespace Avalonia.Markup.Parsers.Nodes public IList Arguments { get; } - public override Type PropertyType + public override Type? PropertyType { get { @@ -144,15 +144,15 @@ namespace Avalonia.Markup.Parsers.Nodes var list = target as IList; var dictionary = target as IDictionary; var indexerProperty = GetIndexer(typeInfo); - var indexerParameters = indexerProperty?.GetIndexParameters(); + ParameterInfo[] indexerParameters; - if (indexerProperty != null && indexerParameters.Length == Arguments.Count) + if (indexerProperty != null && (indexerParameters = indexerProperty.GetIndexParameters()).Length == Arguments.Count) { var convertedObjectArray = new object[indexerParameters.Length]; for (int i = 0; i < Arguments.Count; i++) { - object temp = null; + object? temp = null; if (!TypeUtilities.TryConvert(indexerParameters[i].ParameterType, Arguments[i], CultureInfo.InvariantCulture, out temp)) { @@ -246,7 +246,7 @@ namespace Avalonia.Markup.Parsers.Nodes return true; } - private static PropertyInfo GetIndexer(TypeInfo typeInfo) + private static PropertyInfo? GetIndexer(TypeInfo? typeInfo) { PropertyInfo indexer; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs index c5953b514c..250eca1852 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using Avalonia.Data.Core; using Avalonia.Utilities; +#nullable enable + namespace Avalonia.Markup.Parsers { #if !BUILDTASK @@ -30,7 +32,7 @@ namespace Avalonia.Markup.Parsers var parsed = new List(); while (state != State.End) { - ISyntax syntax = null; + ISyntax? syntax = null; if (state == State.Start) (state, syntax) = ParseStart(ref r); else if (state == State.Next) @@ -53,7 +55,7 @@ namespace Avalonia.Markup.Parsers return parsed; } - private static (State, ISyntax) ParseNext(ref CharacterReader r) + private static (State, ISyntax?) ParseNext(ref CharacterReader r) { r.SkipWhitespace(); if (r.End) @@ -106,7 +108,7 @@ namespace Avalonia.Markup.Parsers }); } - static (string ns, string name) ParseXamlIdentifier(ref CharacterReader r) + static (string? ns, string name) ParseXamlIdentifier(ref CharacterReader r) { var ident = r.ParseIdentifier(); if (ident.IsEmpty) @@ -147,7 +149,7 @@ namespace Avalonia.Markup.Parsers return true; } - private static (State, ISyntax) ParseAfterProperty(ref CharacterReader r) + private static (State, ISyntax?) ParseAfterProperty(ref CharacterReader r) { if (TryParseCasts(ref r, out var rv)) return rv; @@ -184,20 +186,20 @@ namespace Avalonia.Markup.Parsers public class PropertySyntax : ISyntax { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; - public override bool Equals(object obj) + public override bool Equals(object? obj) => obj is PropertySyntax other && other.Name == Name; } public class TypeQualifiedPropertySyntax : ISyntax { - public string Name { get; set; } - public string TypeName { get; set; } - public string TypeNamespace { get; set; } + public string Name { get; set; } = string.Empty; + public string TypeName { get; set; } = string.Empty; + public string? TypeNamespace { get; set; } - public override bool Equals(object obj) + public override bool Equals(object? obj) => obj is TypeQualifiedPropertySyntax other && other.Name == Name && other.TypeName == TypeName @@ -207,14 +209,14 @@ namespace Avalonia.Markup.Parsers public class ChildTraversalSyntax : ISyntax { public static ChildTraversalSyntax Instance { get; } = new ChildTraversalSyntax(); - public override bool Equals(object obj) => obj is ChildTraversalSyntax; + public override bool Equals(object? obj) => obj is ChildTraversalSyntax; } public class EnsureTypeSyntax : ISyntax { - public string TypeName { get; set; } - public string TypeNamespace { get; set; } - public override bool Equals(object obj) + public string TypeName { get; set; } = string.Empty; + public string? TypeNamespace { get; set; } + public override bool Equals(object? obj) => obj is EnsureTypeSyntax other && other.TypeName == TypeName && other.TypeNamespace == TypeNamespace; @@ -222,9 +224,9 @@ namespace Avalonia.Markup.Parsers public class CastTypeSyntax : ISyntax { - public string TypeName { get; set; } - public string TypeNamespace { get; set; } - public override bool Equals(object obj) + public string TypeName { get; set; } = string.Empty; + public string? TypeNamespace { get; set; } + public override bool Equals(object? obj) => obj is CastTypeSyntax other && other.TypeName == TypeName && other.TypeNamespace == TypeNamespace; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index b25e9490cd..9d03341f92 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -4,6 +4,8 @@ using System.Linq; using Avalonia.Data.Core; using Avalonia.Utilities; +#nullable enable + // Don't need to override GetHashCode as the ISyntax objects will not be stored in a hash; the // only reason they have overridden Equals methods is for unit testing. #pragma warning disable 659 @@ -39,7 +41,7 @@ namespace Avalonia.Markup.Parsers var selector = new List(); while (!r.End && state != State.End) { - ISyntax syntax = null; + ISyntax? syntax = null; switch (state) { case State.Start: @@ -110,7 +112,7 @@ namespace Avalonia.Markup.Parsers return State.TypeName; } - private static (State, ISyntax) ParseMiddle(ref CharacterReader r, char? end) + private static (State, ISyntax?) ParseMiddle(ref CharacterReader r, char? end) { if (r.TakeIf(':')) { @@ -190,7 +192,7 @@ namespace Avalonia.Markup.Parsers } } - private static (State, ISyntax) ParseTraversal(ref CharacterReader r) + private static (State, ISyntax?) ParseTraversal(ref CharacterReader r) { r.SkipWhitespace(); if (r.TakeIf('>')) @@ -325,11 +327,11 @@ namespace Avalonia.Markup.Parsers public class OfTypeSyntax : ISyntax, ITypeSyntax { - public string TypeName { get; set; } + public string TypeName { get; set; } = string.Empty; public string Xmlns { get; set; } = string.Empty; - public override bool Equals(object obj) + public override bool Equals(object? obj) { var other = obj as OfTypeSyntax; return other != null && other.TypeName == TypeName && other.Xmlns == Xmlns; @@ -338,11 +340,11 @@ namespace Avalonia.Markup.Parsers public class IsSyntax : ISyntax, ITypeSyntax { - public string TypeName { get; set; } + public string TypeName { get; set; } = string.Empty; public string Xmlns { get; set; } = string.Empty; - public override bool Equals(object obj) + public override bool Equals(object? obj) { var other = obj as IsSyntax; return other != null && other.TypeName == TypeName && other.Xmlns == Xmlns; @@ -351,9 +353,9 @@ namespace Avalonia.Markup.Parsers public class ClassSyntax : ISyntax { - public string Class { get; set; } + public string Class { get; set; } = string.Empty; - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ClassSyntax && ((ClassSyntax)obj).Class == Class; } @@ -361,9 +363,9 @@ namespace Avalonia.Markup.Parsers public class NameSyntax : ISyntax { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is NameSyntax && ((NameSyntax)obj).Name == Name; } @@ -371,11 +373,11 @@ namespace Avalonia.Markup.Parsers public class PropertySyntax : ISyntax { - public string Property { get; set; } + public string Property { get; set; } = string.Empty; - public string Value { get; set; } + public string Value { get; set; } = string.Empty; - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is PropertySyntax && ((PropertySyntax)obj).Property == Property && @@ -385,7 +387,7 @@ namespace Avalonia.Markup.Parsers public class ChildSyntax : ISyntax { - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ChildSyntax; } @@ -393,7 +395,7 @@ namespace Avalonia.Markup.Parsers public class DescendantSyntax : ISyntax { - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is DescendantSyntax; } @@ -401,7 +403,7 @@ namespace Avalonia.Markup.Parsers public class TemplateSyntax : ISyntax { - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is TemplateSyntax; } @@ -409,9 +411,9 @@ namespace Avalonia.Markup.Parsers public class NotSyntax : ISyntax { - public IEnumerable Argument { get; set; } + public IEnumerable Argument { get; set; } = Enumerable.Empty(); - public override bool Equals(object obj) + public override bool Equals(object? obj) { return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument); } @@ -419,7 +421,7 @@ namespace Avalonia.Markup.Parsers public class CommaSyntax : ISyntax { - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is CommaSyntax or; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index 04519bf2bb..92ba744ee1 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -31,13 +31,13 @@ namespace Avalonia.Markup.Parsers /// /// The string. /// The parsed selector. - public Selector Parse(string s) + public Selector? Parse(string s) { var syntax = SelectorGrammar.Parse(s); return Create(syntax); } - private Selector Create(IEnumerable syntax) + private Selector? Create(IEnumerable syntax) { var result = default(Selector); var results = default(List); @@ -110,7 +110,7 @@ namespace Avalonia.Markup.Parsers results = new List(); } - results.Add(result); + results.Add(result ?? throw new NotSupportedException("Invalid selector!")); result = null; break; default: diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 71deb1235f..7c4ff4edc0 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -32,7 +32,7 @@ namespace Avalonia.Skia weight -= weight % 100; // make sure we start at a full weight - for (var i = (int)key.Style; i < 2; i++) + for (var i = 0; i < 2; i++) { // only try 2 font weights in each direction for (var j = 0; j < 200; j += 100) @@ -57,8 +57,8 @@ namespace Avalonia.Skia } } - //Nothing was found so we use the first typeface we can get. - return typefaces.Values.FirstOrDefault(); + //Nothing was found so we try to get a regular typeface. + return typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface) ? typeface : null; } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 136ff63f3d..47a19aad8c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -21,6 +21,7 @@ namespace Avalonia.Direct2D1.Media private readonly ILayerFactory _layerFactory; private readonly SharpDX.Direct2D1.RenderTarget _renderTarget; private readonly DeviceContext _deviceContext; + private readonly bool _ownsDeviceContext; private readonly SharpDX.DXGI.SwapChain1 _swapChain; private readonly Action _finishedCallback; @@ -51,10 +52,12 @@ namespace Avalonia.Direct2D1.Media if (_renderTarget is DeviceContext deviceContext) { _deviceContext = deviceContext; + _ownsDeviceContext = false; } else { _deviceContext = _renderTarget.QueryInterface(); + _ownsDeviceContext = true; } _deviceContext.BeginDraw(); @@ -96,6 +99,13 @@ namespace Avalonia.Direct2D1.Media { throw new RenderTargetCorruptedException(ex); } + finally + { + if (_ownsDeviceContext) + { + _deviceContext.Dispose(); + } + } } /// @@ -151,7 +161,7 @@ namespace Avalonia.Direct2D1.Media using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value)) using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size)) - using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(_deviceContext.Factory, destRect.ToDirect2D())) + using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(Direct2D1Platform.Direct2D1Factory, destRect.ToDirect2D())) { if (d2dOpacityMask.PlatformBrush != null) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index 636309ad1a..d04e2b3110 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -33,13 +33,13 @@ namespace Avalonia.Direct2D1.Media /// public IGeometryImpl Intersect(IGeometryImpl geometry) { - var result = new PathGeometry(Geometry.Factory); - + var result = new PathGeometry(Direct2D1Platform.Direct2D1Factory); using (var sink = result.Open()) { Geometry.Combine(((GeometryImpl)geometry).Geometry, CombineMode.Intersect, sink); - return new StreamGeometryImpl(result); + sink.Close(); } + return new StreamGeometryImpl(result); } /// diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index 49193afd78..90592ea806 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -13,7 +13,7 @@ namespace Avalonia.Direct2D1.Media /// public class WicBitmapImpl : BitmapImpl { - private BitmapDecoder _decoder; + private readonly BitmapDecoder _decoder; private static BitmapInterpolationMode ConvertInterpolationMode(Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) { @@ -41,7 +41,7 @@ namespace Avalonia.Direct2D1.Media /// The filename of the bitmap to load. public WicBitmapImpl(string fileName) { - using (BitmapDecoder decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, fileName, DecodeOptions.CacheOnDemand)) + using (var decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, fileName, DecodeOptions.CacheOnDemand)) { WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnDemand); Dpi = new Vector(96, 96); @@ -177,7 +177,7 @@ namespace Avalonia.Direct2D1.Media /// The Direct2D bitmap. public override OptionalDispose GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget renderTarget) { - FormatConverter converter = new FormatConverter(Direct2D1Platform.ImagingFactory); + using var converter = new FormatConverter(Direct2D1Platform.ImagingFactory); converter.Initialize(WicImpl, SharpDX.WIC.PixelFormat.Format32bppPBGRA); return new OptionalDispose(D2DBitmap.FromWicBitmap(renderTarget, converter), true); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs index 9104be64b2..2bc2b2db71 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs @@ -29,9 +29,12 @@ namespace Avalonia.Direct2D1.Media public IStreamGeometryImpl Clone() { var result = new PathGeometry(Direct2D1Platform.Direct2D1Factory); - var sink = result.Open(); - ((PathGeometry)Geometry).Stream(sink); - sink.Close(); + using (var sink = result.Open()) + { + ((PathGeometry)Geometry).Stream(sink); + sink.Close(); + } + return new StreamGeometryImpl(result); } diff --git a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs index 31e9c260e0..669e139d8f 100644 --- a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs +++ b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs @@ -111,7 +111,7 @@ namespace Avalonia.Direct2D1 /// The Direct2D brush. public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.IPen pen, SharpDX.Direct2D1.RenderTarget renderTarget) { - return pen.ToDirect2DStrokeStyle(renderTarget.Factory); + return pen.ToDirect2DStrokeStyle(Direct2D1Platform.Direct2D1Factory); } /// diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 34371916df..ebe471f303 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -156,15 +156,35 @@ namespace Avalonia.Controls.UnitTests root.Child = null; Assert.Equal(0, command.SubscriptionCount); } + + [Fact] + public void MenuItem_Invokes_CanExecute_When_CommandParameter_Changed() + { + var command = new TestCommand(p => p is bool value && value); + var target = new MenuItem { Command = command }; + + target.CommandParameter = true; + Assert.True(target.IsEffectivelyEnabled); + + target.CommandParameter = false; + Assert.False(target.IsEffectivelyEnabled); + } private class TestCommand : ICommand { - private bool _enabled; + private readonly Func _canExecute; + private readonly Action _execute; private EventHandler _canExecuteChanged; public TestCommand(bool enabled = true) + : this(_ => enabled, _ => { }) + { + } + + public TestCommand(Func canExecute, Action execute = null) { - _enabled = enabled; + _canExecute = canExecute; + _execute = execute ?? (_ => { }); } public int SubscriptionCount { get; private set; } @@ -175,11 +195,9 @@ namespace Avalonia.Controls.UnitTests remove { _canExecuteChanged -= value; --SubscriptionCount; } } - public bool CanExecute(object parameter) => _enabled; + public bool CanExecute(object parameter) => _canExecute(parameter); - public void Execute(object parameter) - { - } + public void Execute(object parameter) => _execute(parameter); } } } diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index 151c3070f4..2a3db9992c 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -1,9 +1,14 @@ -using Moq; +using System; +using System.Collections.Generic; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Styling; +using Avalonia.UnitTests; +using Moq; using Xunit; using System; using Avalonia.Input.Raw; @@ -53,7 +58,115 @@ namespace Avalonia.Controls.UnitTests.Utils HotKeyManager.SetHotKey(button, null); Assert.Empty(tl.KeyBindings); + } + } + + [Fact] + public void HotKeyManager_Should_Release_Reference_When_Control_Detached() + { + using (AvaloniaLocator.EnterScope()) + { + var styler = new Mock(); + + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new WindowingPlatformMock()) + .Bind().ToConstant(styler.Object); + + var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control); + + WeakReference reference = null; + + var tl = new Window(); + new Action(() => + { + var button = new Button(); + reference = new WeakReference(button, true); + tl.Content = button; + tl.Template = CreateWindowTemplate(); + tl.ApplyTemplate(); + tl.Presenter.ApplyTemplate(); + HotKeyManager.SetHotKey(button, gesture1); + + // Detach the button from the logical tree, so there is no reference to it + tl.Content = null; + tl.ApplyTemplate(); + })(); + + + // The button should be collected since it's detached from the listbox + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + Assert.Null(reference?.Target); + } + } + + [Fact] + public void HotKeyManager_Should_Release_Reference_When_Control_In_Item_Template_Detached() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var styler = new Mock(); + + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new WindowingPlatformMock()) + .Bind().ToConstant(styler.Object); + + var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control); + + var weakReferences = new List(); + var tl = new Window { SizeToContent = SizeToContent.WidthAndHeight, IsVisible = true }; + var lm = tl.LayoutManager; + + var keyGestures = new AvaloniaList { gesture1 }; + var listBox = new ListBox + { + Width = 100, + Height = 100, + VirtualizationMode = ItemVirtualizationMode.None, + // Create a button with binding to the KeyGesture in the template and add it to references list + ItemTemplate = new FuncDataTemplate(typeof(KeyGesture), (o, scope) => + { + var keyGesture = o as KeyGesture; + var button = new Button + { + DataContext = keyGesture, [!Button.HotKeyProperty] = new Binding("") + }; + weakReferences.Add(new WeakReference(button, true)); + return button; + }) + }; + // Add the listbox and render it + tl.Content = listBox; + lm.ExecuteInitialLayoutPass(); + listBox.Items = keyGestures; + lm.ExecuteLayoutPass(); + + // Let the button detach when clearing the source items + keyGestures.Clear(); + lm.ExecuteLayoutPass(); + + // Add it again to double check,and render + keyGestures.Add(gesture1); + lm.ExecuteLayoutPass(); + + keyGestures.Clear(); + lm.ExecuteLayoutPass(); + + // The button should be collected since it's detached from the listbox + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + Assert.True(weakReferences.Count > 0); + foreach (var weakReference in weakReferences) + { + Assert.Null(weakReference.Target); + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/IgnoredDirectivesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/IgnoredDirectivesTests.cs new file mode 100644 index 0000000000..de0d9e8bf1 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/IgnoredDirectivesTests.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class IgnoredDirectivesTests : XamlTestBase + { + [Fact] + public void Ignored_Directives_Should_Compile() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + const string xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var target = window.FindControl("target"); + + window.ApplyTemplate(); + target.ApplyTemplate(); + + Assert.Equal("Foo", target.Text); + } + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index a0fe348166..cc4b727bd9 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -48,7 +48,7 @@ namespace Avalonia.Skia.UnitTests.Media continue; } - typeface = new Typeface(customTypeface.FontFamily.Name, fontStyle, fontWeight); + typeface = new Typeface(customTypeface.FontFamily, fontStyle, fontWeight); return true; } @@ -83,7 +83,7 @@ namespace Avalonia.Skia.UnitTests.Media case "Noto Mono": { var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); - skTypeface = typefaceCollection.Get(typeface); + skTypeface = typefaceCollection.Get(_defaultTypeface); break; } default: diff --git a/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs index f9f924e782..68813f28ab 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs @@ -7,25 +7,33 @@ namespace Avalonia.Skia.UnitTests.Media public class SKTypefaceCollectionCacheTests { [Fact] - public void Should_Load_Typefaces_From_Invalid_Name() + public void Should_Get_Near_Matching_Typeface() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) { var notoMono = new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); - var colorEmoji = - new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji"); - var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono); - var typeface = new Typeface("ABC", FontStyle.Italic, FontWeight.Bold); - - Assert.Equal("Noto Mono", notoMonoCollection.Get(typeface).FamilyName); - - var notoColorEmojiCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(colorEmoji); + Assert.Equal("Noto Mono", + notoMonoCollection.Get(new Typeface(notoMono, weight: FontWeight.Bold)).FamilyName); + } + } + + [Fact] + public void Should_Get_Null_For_Invalid_FamilyName() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var notoMono = + new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); + + var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono); - Assert.Equal("Twitter Color Emoji", notoColorEmojiCollection.Get(typeface).FamilyName); + var typeface = notoMonoCollection.Get(new Typeface("ABC")); + + Assert.Null(typeface); } } }