Browse Source

Merge branch 'master' into effectiveViewportChangedListeners

pull/8455/head
Steven Kirk 4 years ago
committed by GitHub
parent
commit
e18254a23c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Avalonia.sln
  2. 27
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  3. 53
      src/Avalonia.Base/Controls/Classes.cs
  4. 14
      src/Avalonia.Base/Controls/IClassesChangedListener.cs
  5. 2
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  6. 26
      src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs
  7. 3
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  8. 27
      src/Avalonia.Controls/Converters/StringFormatConverter.cs
  9. 3
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  10. 23
      src/Avalonia.Controls/ProgressBar.cs
  11. 1
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  12. 5
      src/Avalonia.Controls/TrayIcon.cs
  13. 2
      src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml
  14. 4
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  15. 20
      src/Avalonia.FreeDesktop/DBusCallQueue.cs
  16. 4
      src/Avalonia.FreeDesktop/DBusHelper.cs
  17. 46
      src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
  18. 28
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
  19. 31
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
  20. 38
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
  21. 38
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs
  22. 30
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
  23. 2
      src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
  24. 16
      src/Avalonia.FreeDesktop/DBusMenu.cs
  25. 36
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  26. 2
      src/Avalonia.FreeDesktop/DBusRequest.cs
  27. 2
      src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs
  28. 1
      src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj
  29. 21
      src/Avalonia.Themes.Default/Controls/ProgressBar.xaml
  30. 2
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  31. 18
      src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml
  32. 2
      src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
  33. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  34. 23
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs
  35. 5
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs
  36. 16
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
  37. 2
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs
  38. 0
      src/Shared/IsExternalInit.cs
  39. 5
      tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs
  40. 85
      tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs
  41. 44
      tests/Avalonia.Controls.UnitTests/MenuItemTests.cs
  42. 147
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  43. 2
      tests/Avalonia.LeakTests/ControlTests.cs
  44. 11
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  45. 225
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

1
Avalonia.sln

@ -38,6 +38,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig .editorconfig = .editorconfig
src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
EndProjectSection EndProjectSection

27
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@ -1,22 +1,37 @@
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.ProgressBarPage"> <UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.ProgressBarPage">
<StackPanel Orientation="Vertical" Spacing="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h2">A progress bar control</TextBlock> <TextBlock Classes="h2">A progress bar control</TextBlock>
<StackPanel> <StackPanel Spacing="5">
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock VerticalAlignment="Center">Maximum</TextBlock>
<NumericUpDown x:Name="maximum" Value="100" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock VerticalAlignment="Center">Minimum</TextBlock>
<NumericUpDown x:Name="minimum" Value="0" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock VerticalAlignment="Center">Progress Text Format</TextBlock>
<TextBox x:Name="stringFormat" Text="{}{0:0}%" VerticalAlignment="Center"/>
</StackPanel>
<CheckBox x:Name="showProgress" Margin="10,16,0,0" Content="Show Progress Text" /> <CheckBox x:Name="showProgress" Margin="10,16,0,0" Content="Show Progress Text" />
<CheckBox x:Name="isIndeterminate" Margin="10,16,0,0" Content="Toggle Indeterminate" /> <CheckBox x:Name="isIndeterminate" Margin="10,16,0,0" Content="Toggle Indeterminate" />
<StackPanel Orientation="Horizontal" Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> <StackPanel Orientation="Horizontal" Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16">
<StackPanel Spacing="16"> <StackPanel Spacing="16">
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}" /> <ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}"
Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" ProgressTextFormat="{Binding #stringFormat.Text}"/>
</StackPanel> </StackPanel>
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical" /> <ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical"
Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" ProgressTextFormat="{Binding #stringFormat.Text}"/>
</StackPanel> </StackPanel>
<StackPanel Margin="16"> <StackPanel Margin="16">
<Slider Name="hprogress" Maximum="100" Value="40" /> <Slider Name="hprogress" Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" Value="40" />
<Slider Name="vprogress" Maximum="100" Value="60" /> <Slider Name="vprogress" Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" Value="60" />
</StackPanel> </StackPanel>
<StackPanel Spacing="10"> <StackPanel Spacing="10">
<ProgressBar VerticalAlignment="Center" IsIndeterminate="True" /> <ProgressBar VerticalAlignment="Center" IsIndeterminate="True"
Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.value}"/>
<ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" /> <ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
<ProgressBar VerticalAlignment="Center" Value="50" /> <ProgressBar VerticalAlignment="Center" Value="50" />
<ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" /> <ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />

53
src/Avalonia.Base/Controls/Classes.cs

@ -14,6 +14,8 @@ namespace Avalonia.Controls
/// </remarks> /// </remarks>
public class Classes : AvaloniaList<string>, IPseudoClasses public class Classes : AvaloniaList<string>, IPseudoClasses
{ {
private List<IClassesChangedListener>? _listeners;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Classes"/> class. /// Initializes a new instance of the <see cref="Classes"/> class.
/// </summary> /// </summary>
@ -39,6 +41,11 @@ namespace Avalonia.Controls
{ {
} }
/// <summary>
/// Gets the number of listeners subscribed to this collection for unit testing purposes.
/// </summary>
internal int ListenerCount => _listeners?.Count ?? 0;
/// <summary> /// <summary>
/// Parses a classes string. /// Parses a classes string.
/// </summary> /// </summary>
@ -62,6 +69,7 @@ namespace Avalonia.Controls
if (!Contains(name)) if (!Contains(name))
{ {
base.Add(name); base.Add(name);
NotifyChanged();
} }
} }
@ -89,6 +97,7 @@ namespace Avalonia.Controls
} }
base.AddRange(c); base.AddRange(c);
NotifyChanged();
} }
/// <summary> /// <summary>
@ -103,6 +112,8 @@ namespace Avalonia.Controls
RemoveAt(i); RemoveAt(i);
} }
} }
NotifyChanged();
} }
/// <summary> /// <summary>
@ -122,6 +133,7 @@ namespace Avalonia.Controls
if (!Contains(name)) if (!Contains(name))
{ {
base.Insert(index, name); base.Insert(index, name);
NotifyChanged();
} }
} }
@ -154,6 +166,7 @@ namespace Avalonia.Controls
if (toInsert != null) if (toInsert != null)
{ {
base.InsertRange(index, toInsert); base.InsertRange(index, toInsert);
NotifyChanged();
} }
} }
@ -169,7 +182,14 @@ namespace Avalonia.Controls
public override bool Remove(string name) public override bool Remove(string name)
{ {
ThrowIfPseudoclass(name, "removed"); ThrowIfPseudoclass(name, "removed");
return base.Remove(name);
if (base.Remove(name))
{
NotifyChanged();
return true;
}
return false;
} }
/// <summary> /// <summary>
@ -197,6 +217,7 @@ namespace Avalonia.Controls
if (toRemove != null) if (toRemove != null)
{ {
base.RemoveAll(toRemove); base.RemoveAll(toRemove);
NotifyChanged();
} }
} }
@ -214,6 +235,7 @@ namespace Avalonia.Controls
var name = this[index]; var name = this[index];
ThrowIfPseudoclass(name, "removed"); ThrowIfPseudoclass(name, "removed");
base.RemoveAt(index); base.RemoveAt(index);
NotifyChanged();
} }
/// <summary> /// <summary>
@ -224,6 +246,7 @@ namespace Avalonia.Controls
public override void RemoveRange(int index, int count) public override void RemoveRange(int index, int count)
{ {
base.RemoveRange(index, count); base.RemoveRange(index, count);
NotifyChanged();
} }
/// <summary> /// <summary>
@ -255,6 +278,7 @@ namespace Avalonia.Controls
} }
base.AddRange(source); base.AddRange(source);
NotifyChanged();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -263,13 +287,38 @@ namespace Avalonia.Controls
if (!Contains(name)) if (!Contains(name))
{ {
base.Add(name); base.Add(name);
NotifyChanged();
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
bool IPseudoClasses.Remove(string name) bool IPseudoClasses.Remove(string name)
{ {
return base.Remove(name); if (base.Remove(name))
{
NotifyChanged();
return true;
}
return false;
}
internal void AddListener(IClassesChangedListener listener)
{
(_listeners ??= new()).Add(listener);
}
internal void RemoveListener(IClassesChangedListener listener)
{
_listeners?.Remove(listener);
}
private void NotifyChanged()
{
if (_listeners is null)
return;
foreach (var listener in _listeners)
listener.Changed();
} }
private void ThrowIfPseudoclass(string name, string operation) private void ThrowIfPseudoclass(string name, string operation)

14
src/Avalonia.Base/Controls/IClassesChangedListener.cs

@ -0,0 +1,14 @@
namespace Avalonia.Controls
{
/// <summary>
/// Internal interface for listening to changes in <see cref="Classes"/> in a more
/// performant manner than subscribing to CollectionChanged.
/// </summary>
internal interface IClassesChangedListener
{
/// <summary>
/// Notifies the listener that the <see cref="Classes"/> collection has changed.
/// </summary>
void Changed();
}
}

2
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -55,7 +55,7 @@ namespace Avalonia.Data.Core.Plugins
private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName) private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName)
{ {
if (instance is IReflectableType reflectableType) if (instance is IReflectableType reflectableType && instance is not Type)
return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags); return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags);
var type = instance.GetType(); var type = instance.GetType();

26
src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls;
#nullable enable #nullable enable
@ -10,21 +11,17 @@ namespace Avalonia.Styling.Activators
/// An <see cref="IStyleActivator"/> which is active when a set of classes match those on a /// An <see cref="IStyleActivator"/> which is active when a set of classes match those on a
/// control. /// control.
/// </summary> /// </summary>
internal sealed class StyleClassActivator : StyleActivatorBase internal sealed class StyleClassActivator : StyleActivatorBase, IClassesChangedListener
{ {
private readonly IList<string> _match; private readonly IList<string> _match;
private readonly IAvaloniaReadOnlyList<string> _classes; private readonly Classes _classes;
private NotifyCollectionChangedEventHandler? _classesChangedHandler;
public StyleClassActivator(IAvaloniaReadOnlyList<string> classes, IList<string> match) public StyleClassActivator(Classes classes, IList<string> match)
{ {
_classes = classes; _classes = classes;
_match = match; _match = match;
} }
private NotifyCollectionChangedEventHandler ClassesChangedHandler =>
_classesChangedHandler ??= ClassesChanged;
public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch) public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch)
{ {
int remainingMatches = toMatch.Count; int remainingMatches = toMatch.Count;
@ -55,23 +52,20 @@ namespace Avalonia.Styling.Activators
return remainingMatches == 0; return remainingMatches == 0;
} }
protected override void Initialize() void IClassesChangedListener.Changed()
{ {
PublishNext(IsMatching()); PublishNext(IsMatching());
_classes.CollectionChanged += ClassesChangedHandler;
} }
protected override void Deinitialize() protected override void Initialize()
{ {
_classes.CollectionChanged -= ClassesChangedHandler; PublishNext(IsMatching());
_classes.AddListener(this);
} }
private void ClassesChanged(object? sender, NotifyCollectionChangedEventArgs e) protected override void Deinitialize()
{ {
if (e.Action != NotifyCollectionChangedAction.Move) _classes.RemoveListener(this);
{
PublishNext(IsMatching());
}
} }
private bool IsMatching() => AreClassesMatching(_classes, _match); private bool IsMatching() => AreClassesMatching(_classes, _match);

3
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Avalonia.Controls;
using Avalonia.Styling.Activators; using Avalonia.Styling.Activators;
#nullable enable #nullable enable
@ -125,7 +126,7 @@ namespace Avalonia.Styling
{ {
if (subscribe) if (subscribe)
{ {
var observable = new StyleClassActivator(control.Classes, _classes.Value); var observable = new StyleClassActivator((Classes)control.Classes, _classes.Value);
return new SelectorMatch(observable); return new SelectorMatch(observable);
} }

27
src/Avalonia.Controls/Converters/StringFormatConverter.cs

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Data;
using Avalonia.Data.Converters;
namespace Avalonia.Controls.Converters;
/// <summary>
/// Calls <see cref="string.Format(string, object[])"/> on the passed in values, where the first element in the list
/// is the string, and everything after it is passed into the object array in order.
/// </summary>
public class StringFormatConverter : IMultiValueConverter
{
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
{
try
{
return string.Format((string)values[0]!, values.Skip(1).ToArray());
}
catch (Exception e)
{
return new BindingNotification(e, BindingErrorType.Error);
}
}
}

3
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -387,6 +387,7 @@ namespace Avalonia.Controls.Primitives
/// Sets the TemplatedParent property for the created template children. /// Sets the TemplatedParent property for the created template children.
/// </summary> /// </summary>
/// <param name="control">The control.</param> /// <param name="control">The control.</param>
/// <param name="templatedParent">The templated parent to apply.</param>
internal static void ApplyTemplatedParent(IStyledElement control, ITemplatedControl? templatedParent) internal static void ApplyTemplatedParent(IStyledElement control, ITemplatedControl? templatedParent)
{ {
control.SetValue(TemplatedParentProperty, templatedParent); control.SetValue(TemplatedParentProperty, templatedParent);
@ -396,7 +397,7 @@ namespace Avalonia.Controls.Primitives
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
if (children[i] is IStyledElement child) if (children[i] is IStyledElement child && child.TemplatedParent is null)
{ {
ApplyTemplatedParent(child, templatedParent); ApplyTemplatedParent(child, templatedParent);
} }

23
src/Avalonia.Controls/ProgressBar.cs

@ -96,6 +96,7 @@ namespace Avalonia.Controls
} }
} }
private double _percentage;
private double _indeterminateStartingOffset; private double _indeterminateStartingOffset;
private double _indeterminateEndingOffset; private double _indeterminateEndingOffset;
private Border? _indicator; private Border? _indicator;
@ -106,9 +107,17 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> ShowProgressTextProperty = public static readonly StyledProperty<bool> ShowProgressTextProperty =
AvaloniaProperty.Register<ProgressBar, bool>(nameof(ShowProgressText)); AvaloniaProperty.Register<ProgressBar, bool>(nameof(ShowProgressText));
public static readonly StyledProperty<string> ProgressTextFormatProperty =
AvaloniaProperty.Register<ProgressBar, string>(nameof(ProgressTextFormat), "{1:0}%");
public static readonly StyledProperty<Orientation> OrientationProperty = public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal); AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal);
public static readonly DirectProperty<ProgressBar, double> PercentageProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>(
nameof(Percentage),
o => o.Percentage);
[Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
public static readonly DirectProperty<ProgressBar, double> IndeterminateStartingOffsetProperty = public static readonly DirectProperty<ProgressBar, double> IndeterminateStartingOffsetProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>( AvaloniaProperty.RegisterDirect<ProgressBar, double>(
@ -123,6 +132,12 @@ namespace Avalonia.Controls
p => p.IndeterminateEndingOffset, p => p.IndeterminateEndingOffset,
(p, o) => p.IndeterminateEndingOffset = o); (p, o) => p.IndeterminateEndingOffset = o);
public double Percentage
{
get { return _percentage; }
private set { SetAndRaise(PercentageProperty, ref _percentage, value); }
}
[Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
public double IndeterminateStartingOffset public double IndeterminateStartingOffset
{ {
@ -165,6 +180,12 @@ namespace Avalonia.Controls
set => SetValue(ShowProgressTextProperty, value); set => SetValue(ShowProgressTextProperty, value);
} }
public string ProgressTextFormat
{
get => GetValue(ProgressTextFormatProperty);
set => SetValue(ProgressTextFormatProperty, value);
}
public Orientation Orientation public Orientation Orientation
{ {
get => GetValue(OrientationProperty); get => GetValue(OrientationProperty);
@ -245,6 +266,8 @@ namespace Avalonia.Controls
_indicator.Width = bounds.Width * percent; _indicator.Width = bounds.Width * percent;
else else
_indicator.Height = bounds.Height * percent; _indicator.Height = bounds.Height * percent;
Percentage = percent * 100;
} }
} }
} }

1
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -421,6 +421,7 @@ namespace Avalonia.Controls
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnAttachedToVisualTree(e);
InvalidateMeasure(); InvalidateMeasure();
_viewportManager.ResetScrollers(); _viewportManager.ResetScrollers();
} }

5
src/Avalonia.Controls/TrayIcon.cs

@ -189,7 +189,10 @@ namespace Avalonia.Controls
var app = Application.Current ?? throw new InvalidOperationException("Application not yet initialized."); var app = Application.Current ?? throw new InvalidOperationException("Application not yet initialized.");
var trayIcons = GetIcons(app); var trayIcons = GetIcons(app);
RemoveIcons(trayIcons); if (trayIcons != null)
{
RemoveIcons(trayIcons);
}
} }
private static void Icons_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) private static void Icons_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)

2
src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml

@ -19,7 +19,7 @@
Classes="textBoxClearButton" Classes="textBoxClearButton"
ToolTip.Tip="Clear" ToolTip.Tip="Clear"
Cursor="Hand" Cursor="Hand"
Command="{ReflectionBinding $parent[TextBox].Clear}" Command="{Binding $parent[TextBox].Clear}"
Opacity="0.5" /> Opacity="0.5" />
<ToggleButton Classes="filter-text-box-toggle" <ToggleButton Classes="filter-text-box-toggle"
ToolTip.Tip="Match Case" ToolTip.Tip="Match Case"

4
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -5,6 +5,10 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Include="..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" /> <ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" /> <ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />

20
src/Avalonia.FreeDesktop/DBusCallQueue.cs

@ -8,10 +8,9 @@ namespace Avalonia.FreeDesktop
{ {
private readonly Func<Exception, Task> _errorHandler; private readonly Func<Exception, Task> _errorHandler;
class Item record Item(Func<Task> Callback)
{ {
public Func<Task> Callback; public Action<Exception?>? OnFinish;
public Action<Exception> OnFinish;
} }
private Queue<Item> _q = new Queue<Item>(); private Queue<Item> _q = new Queue<Item>();
private bool _processing; private bool _processing;
@ -23,19 +22,15 @@ namespace Avalonia.FreeDesktop
public void Enqueue(Func<Task> cb) public void Enqueue(Func<Task> cb)
{ {
_q.Enqueue(new Item _q.Enqueue(new Item(cb));
{
Callback = cb
});
Process(); Process();
} }
public Task EnqueueAsync(Func<Task> cb) public Task EnqueueAsync(Func<Task> cb)
{ {
var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously); var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
_q.Enqueue(new Item _q.Enqueue(new Item(cb)
{ {
Callback = cb,
OnFinish = e => OnFinish = e =>
{ {
if (e == null) if (e == null)
@ -51,13 +46,12 @@ namespace Avalonia.FreeDesktop
public Task<T> EnqueueAsync<T>(Func<Task<T>> cb) public Task<T> EnqueueAsync<T>(Func<Task<T>> cb)
{ {
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously); var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
_q.Enqueue(new Item _q.Enqueue(new Item(async () =>
{
Callback = async () =>
{ {
var res = await cb(); var res = await cb();
tcs.TrySetResult(res); tcs.TrySetResult(res);
}, })
{
OnFinish = e => OnFinish = e =>
{ {
if (e != null) if (e != null)

4
src/Avalonia.FreeDesktop/DBusHelper.cs

@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop
private readonly object _lock = new(); private readonly object _lock = new();
private SynchronizationContext? _ctx; private SynchronizationContext? _ctx;
public override void Post(SendOrPostCallback d, object state) public override void Post(SendOrPostCallback d, object? state)
{ {
lock (_lock) lock (_lock)
{ {
@ -29,7 +29,7 @@ namespace Avalonia.FreeDesktop
} }
} }
public override void Send(SendOrPostCallback d, object state) public override void Send(SendOrPostCallback d, object? state)
{ {
lock (_lock) lock (_lock)
{ {

46
src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs

@ -1,9 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.FreeDesktop.DBusIme.Fcitx;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Input.TextInput; using Avalonia.Input.TextInput;
using Avalonia.Logging; using Avalonia.Logging;
@ -26,7 +24,7 @@ namespace Avalonia.FreeDesktop.DBusIme
return (im, im); return (im, im);
} }
} }
internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl
{ {
private List<IDisposable> _disposables = new List<IDisposable>(); private List<IDisposable> _disposables = new List<IDisposable>();
@ -34,7 +32,7 @@ namespace Avalonia.FreeDesktop.DBusIme
protected Connection Connection { get; } protected Connection Connection { get; }
private readonly string[] _knownNames; private readonly string[] _knownNames;
private bool _connecting; private bool _connecting;
private string _currentName; private string? _currentName;
private DBusCallQueue _queue; private DBusCallQueue _queue;
private bool _controlActive, _windowActive; private bool _controlActive, _windowActive;
private bool? _imeActive; private bool? _imeActive;
@ -42,9 +40,9 @@ namespace Avalonia.FreeDesktop.DBusIme
private PixelRect? _lastReportedRect; private PixelRect? _lastReportedRect;
private double _scaling = 1; private double _scaling = 1;
private PixelPoint _windowPosition; private PixelPoint _windowPosition;
protected bool IsConnected => _currentName != null; protected bool IsConnected => _currentName != null;
public DBusTextInputMethodBase(Connection connection, params string[] knownNames) public DBusTextInputMethodBase(Connection connection, params string[] knownNames)
{ {
_queue = new DBusCallQueue(QueueOnError); _queue = new DBusCallQueue(QueueOnError);
@ -58,18 +56,18 @@ namespace Avalonia.FreeDesktop.DBusIme
foreach (var name in _knownNames) foreach (var name in _knownNames)
_disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange)); _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange));
} }
protected abstract Task<bool> Connect(string name); protected abstract Task<bool> Connect(string name);
protected string GetAppName() => protected string GetAppName() =>
Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; Application.Current?.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia";
private async void OnNameChange(ServiceOwnerChangedEventArgs args) private async void OnNameChange(ServiceOwnerChangedEventArgs args)
{ {
if (args.NewOwner != null && _currentName == null) if (args.NewOwner != null && _currentName == null)
{ {
_onlineNamesQueue.Enqueue(args.ServiceName); _onlineNamesQueue.Enqueue(args.ServiceName);
if(!_connecting) if (!_connecting)
{ {
_connecting = true; _connecting = true;
try try
@ -98,25 +96,25 @@ namespace Avalonia.FreeDesktop.DBusIme
_connecting = false; _connecting = false;
} }
} }
} }
// IME has crashed // IME has crashed
if (args.NewOwner == null && args.ServiceName == _currentName) if (args.NewOwner == null && args.ServiceName == _currentName)
{ {
_currentName = null; _currentName = null;
foreach(var s in _disposables) foreach (var s in _disposables)
s.Dispose(); s.Dispose();
_disposables.Clear(); _disposables.Clear();
OnDisconnected(); OnDisconnected();
Reset(); Reset();
// Watch again // Watch again
Watch(); Watch();
} }
} }
protected virtual Task Disconnect() protected virtual Task Disconnect()
{ {
return Task.CompletedTask; return Task.CompletedTask;
@ -124,7 +122,7 @@ namespace Avalonia.FreeDesktop.DBusIme
protected virtual void OnDisconnected() protected virtual void OnDisconnected()
{ {
} }
protected virtual void Reset() protected virtual void Reset()
@ -149,10 +147,14 @@ namespace Avalonia.FreeDesktop.DBusIme
OnDisconnected(); OnDisconnected();
_currentName = null; _currentName = null;
} }
protected void Enqueue(Func<Task> cb) => _queue.Enqueue(cb); protected void Enqueue(Func<Task> cb) => _queue.Enqueue(cb);
protected void AddDisposable(IDisposable d) => _disposables.Add(d); protected void AddDisposable(IDisposable? d)
{
if(d is { })
_disposables.Add(d);
}
public void Dispose() public void Dispose()
{ {
@ -198,7 +200,7 @@ namespace Avalonia.FreeDesktop.DBusIme
UpdateActive(); UpdateActive();
} }
void ITextInputMethodImpl.SetClient(ITextInputMethodClient client) void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
{ {
_controlActive = client is { }; _controlActive = client is { };
UpdateActive(); UpdateActive();
@ -225,7 +227,7 @@ namespace Avalonia.FreeDesktop.DBusIme
} }
} }
private Action<string> _onCommit; private Action<string>? _onCommit;
event Action<string> IX11InputMethodControl.Commit event Action<string> IX11InputMethodControl.Commit
{ {
add => _onCommit += value; add => _onCommit += value;
@ -234,7 +236,7 @@ namespace Avalonia.FreeDesktop.DBusIme
protected void FireCommit(string s) => _onCommit?.Invoke(s); protected void FireCommit(string s) => _onCommit?.Invoke(s);
private Action<X11InputMethodForwardedKey> _onForward; private Action<X11InputMethodForwardedKey>? _onForward;
event Action<X11InputMethodForwardedKey> IX11InputMethodControl.ForwardKey event Action<X11InputMethodForwardedKey> IX11InputMethodControl.ForwardKey
{ {
add => _onForward += value; add => _onForward += value;

28
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs

@ -31,15 +31,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
Task DestroyICAsync(); Task DestroyICAsync();
Task<int> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time); Task<int> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time);
Task<IDisposable> WatchEnableIMAsync(Action handler, Action<Exception> onError = null); Task<IDisposable> WatchEnableIMAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCloseIMAsync(Action handler, Action<Exception> onError = null); Task<IDisposable> WatchCloseIMAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCommitStringAsync(Action<string> handler, Action<Exception> onError = null); Task<IDisposable?> WatchCommitStringAsync(Action<string> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception> onError = null); Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action<Exception> onError = null); Task<IDisposable> WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception> onError = null); Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action<Exception> onError = null); Task<IDisposable> WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action<Exception> onError = null); Task<IDisposable?> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception> onError = null); Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception>? onError = null);
} }
[DBusInterface("org.fcitx.Fcitx.InputContext1")] [DBusInterface("org.fcitx.Fcitx.InputContext1")]
@ -54,11 +54,11 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
Task DestroyICAsync(); Task DestroyICAsync();
Task<bool> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time); Task<bool> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time);
Task<IDisposable> WatchCommitStringAsync(Action<string> handler, Action<Exception> onError = null); Task<IDisposable?> WatchCommitStringAsync(Action<string> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception> onError = null); Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception> onError = null); Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action<Exception> onError = null); Task<IDisposable?> WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception> onError = null); Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception>? onError = null);
} }
[DBusInterface("org.fcitx.Fcitx.InputMethod1")] [DBusInterface("org.fcitx.Fcitx.InputMethod1")]

31
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs

@ -5,8 +5,8 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
{ {
internal class FcitxICWrapper internal class FcitxICWrapper
{ {
private readonly IFcitxInputContext1 _modern; private readonly IFcitxInputContext1? _modern;
private readonly IFcitxInputContext _old; private readonly IFcitxInputContext? _old;
public FcitxICWrapper(IFcitxInputContext old) public FcitxICWrapper(IFcitxInputContext old)
{ {
@ -18,34 +18,37 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
_modern = modern; _modern = modern;
} }
public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern.FocusInAsync(); public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern?.FocusInAsync() ?? Task.CompletedTask;
public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern.FocusOutAsync(); public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern?.FocusOutAsync() ?? Task.CompletedTask;
public Task ResetAsync() => _old?.ResetAsync() ?? _modern.ResetAsync(); public Task ResetAsync() => _old?.ResetAsync() ?? _modern?.ResetAsync() ?? Task.CompletedTask;
public Task SetCursorRectAsync(int x, int y, int w, int h) => public Task SetCursorRectAsync(int x, int y, int w, int h) =>
_old?.SetCursorRectAsync(x, y, w, h) ?? _modern.SetCursorRectAsync(x, y, w, h); _old?.SetCursorRectAsync(x, y, w, h) ?? _modern?.SetCursorRectAsync(x, y, w, h) ?? Task.CompletedTask;
public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern.DestroyICAsync(); public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern?.DestroyICAsync() ?? Task.CompletedTask;
public async Task<bool> ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time) public async Task<bool> ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time)
{ {
if(_old!=null) if(_old!=null)
return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0; return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0;
return await _modern.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time); return await (_modern?.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time) ?? Task.FromResult(false));
} }
public Task<IDisposable> WatchCommitStringAsync(Action<string> handler) => public Task<IDisposable?> WatchCommitStringAsync(Action<string> handler) =>
_old?.WatchCommitStringAsync(handler) ?? _modern.WatchCommitStringAsync(handler); _old?.WatchCommitStringAsync(handler)
?? _modern?.WatchCommitStringAsync(handler)
?? Task.FromResult(default(IDisposable?));
public Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) public Task<IDisposable?> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler)
{ {
return _old?.WatchForwardKeyAsync(handler) return _old?.WatchForwardKeyAsync(handler)
?? _modern.WatchForwardKeyAsync(ev => ?? _modern?.WatchForwardKeyAsync(ev =>
handler((ev.keyval, ev.state, ev.type ? 1 : 0))); handler((ev.keyval, ev.state, ev.type ? 1 : 0)))
?? Task.FromResult(default(IDisposable?));
} }
public Task SetCapacityAsync(uint flags) => public Task SetCapacityAsync(uint flags) =>
_old?.SetCapacityAsync(flags) ?? _modern.SetCapabilityAsync(flags); _old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask;
} }
} }

38
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs

@ -12,7 +12,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
{ {
internal class FcitxX11TextInputMethod : DBusTextInputMethodBase internal class FcitxX11TextInputMethod : DBusTextInputMethodBase
{ {
private FcitxICWrapper _context; private FcitxICWrapper? _context;
private FcitxCapabilityFlags? _lastReportedFlags; private FcitxCapabilityFlags? _lastReportedFlags;
public FcitxX11TextInputMethod(Connection connection) : base(connection, public FcitxX11TextInputMethod(Connection connection) : base(connection,
@ -49,7 +49,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
return true; return true;
} }
protected override Task Disconnect() => _context.DestroyICAsync(); protected override Task Disconnect() => _context?.DestroyICAsync() ?? Task.CompletedTask;
protected override void OnDisconnected() => _context = null; protected override void OnDisconnected() => _context = null;
@ -60,18 +60,18 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
} }
protected override Task SetCursorRectCore(PixelRect cursorRect) => protected override Task SetCursorRectCore(PixelRect cursorRect) =>
_context.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width), _context?.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width),
Math.Max(1, cursorRect.Height)); Math.Max(1, cursorRect.Height))
?? Task.CompletedTask;
protected override Task SetActiveCore(bool active)
{ protected override Task SetActiveCore(bool active)=> (active
if (active) ? _context?.FocusInAsync()
return _context.FocusInAsync(); : _context?.FocusOutAsync())
else ?? Task.CompletedTask;
return _context.FocusOutAsync();
}
protected override Task ResetContextCore() => _context.ResetAsync(); protected override Task ResetContextCore() => _context?.ResetAsync()
?? Task.CompletedTask;
protected override async Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) protected override async Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
{ {
@ -88,9 +88,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
var type = args.Type == RawKeyEventType.KeyDown ? var type = args.Type == RawKeyEventType.KeyDown ?
FcitxKeyEventType.FCITX_PRESS_KEY : FcitxKeyEventType.FCITX_PRESS_KEY :
FcitxKeyEventType.FCITX_RELEASE_KEY; FcitxKeyEventType.FCITX_RELEASE_KEY;
if (_context is { })
return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, {
(uint)args.Timestamp).ConfigureAwait(false); return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type,
(uint)args.Timestamp).ConfigureAwait(false);
}
else
{
return false;
}
} }
public override void SetOptions(TextInputOptions options) => public override void SetOptions(TextInputOptions options) =>

38
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs

@ -22,25 +22,25 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
Task<object> GetEngineAsync(); Task<object> GetEngineAsync();
Task DestroyAsync(); Task DestroyAsync();
Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos); Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos);
Task<IDisposable> WatchCommitTextAsync(Action<object> cb, Action<Exception> onError = null); Task<IDisposable> WatchCommitTextAsync(Action<object> cb, Action<Exception>? onError = null);
Task<IDisposable> WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action<Exception> onError = null); Task<IDisposable> WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchRequireSurroundingTextAsync(Action handler, Action<Exception> onError = null); Task<IDisposable> WatchRequireSurroundingTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action<Exception> onError = null); Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action<Exception> onError = null); Task<IDisposable> WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchShowPreeditTextAsync(Action handler, Action<Exception> onError = null); Task<IDisposable> WatchShowPreeditTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchHidePreeditTextAsync(Action handler, Action<Exception> onError = null); Task<IDisposable> WatchHidePreeditTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action<Exception> onError = null); Task<IDisposable> WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchShowAuxiliaryTextAsync(Action handler, Action<Exception> onError = null); Task<IDisposable> WatchShowAuxiliaryTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchHideAuxiliaryTextAsync(Action handler, Action<Exception> onError = null); Task<IDisposable> WatchHideAuxiliaryTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action<Exception> onError = null); Task<IDisposable> WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchShowLookupTableAsync(Action handler, Action<Exception> onError = null); Task<IDisposable> WatchShowLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchHideLookupTableAsync(Action handler, Action<Exception> onError = null); Task<IDisposable> WatchHideLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchPageUpLookupTableAsync(Action handler, Action<Exception> onError = null); Task<IDisposable> WatchPageUpLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchPageDownLookupTableAsync(Action handler, Action<Exception> onError = null); Task<IDisposable> WatchPageDownLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCursorUpLookupTableAsync(Action handler, Action<Exception> onError = null); Task<IDisposable> WatchCursorUpLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCursorDownLookupTableAsync(Action handler, Action<Exception> onError = null); Task<IDisposable> WatchCursorDownLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchRegisterPropertiesAsync(Action<object> handler, Action<Exception> onError = null); Task<IDisposable> WatchRegisterPropertiesAsync(Action<object> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdatePropertyAsync(Action<object> handler, Action<Exception> onError = null); Task<IDisposable> WatchUpdatePropertyAsync(Action<object> handler, Action<Exception>? onError = null);
} }

30
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs

@ -9,7 +9,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
{ {
internal class IBusX11TextInputMethod : DBusTextInputMethodBase internal class IBusX11TextInputMethod : DBusTextInputMethodBase
{ {
private IIBusInputContext _context; private IIBusInputContext? _context;
public IBusX11TextInputMethod(Connection connection) : base(connection, public IBusX11TextInputMethod(Connection connection) : base(connection,
"org.freedesktop.portal.IBus") "org.freedesktop.portal.IBus")
@ -53,16 +53,16 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
private void OnCommitText(object wtf) private void OnCommitText(object wtf)
{ {
// Hello darkness, my old friend // Hello darkness, my old friend
var prop = wtf.GetType().GetField("Item3"); if (wtf.GetType().GetField("Item3") is { } prop)
if (prop != null)
{ {
var text = (string)prop.GetValue(wtf); var text = prop.GetValue(wtf) as string;
if (!string.IsNullOrEmpty(text)) if (!string.IsNullOrEmpty(text))
FireCommit(text); FireCommit(text!);
} }
} }
protected override Task Disconnect() => _context.DestroyAsync(); protected override Task Disconnect() => _context?.DestroyAsync()
?? Task.CompletedTask;
protected override void OnDisconnected() protected override void OnDisconnected()
{ {
@ -71,13 +71,15 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
} }
protected override Task SetCursorRectCore(PixelRect rect) protected override Task SetCursorRectCore(PixelRect rect)
=> _context.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height); => _context?.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height)
?? Task.CompletedTask;
protected override Task SetActiveCore(bool active) protected override Task SetActiveCore(bool active)
=> active ? _context.FocusInAsync() : _context.FocusOutAsync(); => (active ? _context?.FocusInAsync() : _context?.FocusOutAsync())
?? Task.CompletedTask;
protected override Task ResetContextCore() protected override Task ResetContextCore()
=> _context.ResetAsync(); => _context?.ResetAsync() ?? Task.CompletedTask;
protected override Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) protected override Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
{ {
@ -94,7 +96,15 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
if (args.Type == RawKeyEventType.KeyUp) if (args.Type == RawKeyEventType.KeyUp)
state |= IBusModifierMask.ReleaseMask; state |= IBusModifierMask.ReleaseMask;
return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); if(_context is { })
{
return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state);
}
else
{
return Task.FromResult(false);
}
} }
public override void SetOptions(TextInputOptions options) public override void SetOptions(TextInputOptions options)

2
src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs

@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop.DBusIme
new DBusInputMethodFactory<IBusX11TextInputMethod>(_ => new IBusX11TextInputMethod(conn)) new DBusInputMethodFactory<IBusX11TextInputMethod>(_ => new IBusX11TextInputMethod(conn))
}; };
static Func<Connection, IX11InputMethodFactory> DetectInputMethod() static Func<Connection, IX11InputMethodFactory>? DetectInputMethod()
{ {
foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" }) foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" })
{ {

16
src/Avalonia.FreeDesktop/DBusMenu.cs

@ -28,18 +28,18 @@ namespace Avalonia.FreeDesktop.DBusMenu
Task<int[]> EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events); Task<int[]> EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events);
Task<bool> AboutToShowAsync(int Id); Task<bool> AboutToShowAsync(int Id);
Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids); Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids);
Task<IDisposable> WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception> onError = null); Task<IDisposable> WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception> onError = null); Task<IDisposable> WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception> onError = null); Task<IDisposable> WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception>? onError = null);
} }
[Dictionary] [Dictionary]
class DBusMenuProperties class DBusMenuProperties
{ {
public uint Version { get; set; } = default (uint); public uint Version { get; set; } = default (uint);
public string TextDirection { get; set; } = default (string); public string? TextDirection { get; set; } = default (string);
public string Status { get; set; } = default (string); public string? Status { get; set; } = default (string);
public string[] IconThemePath { get; set; } = default (string[]); public string[]? IconThemePath { get; set; } = default (string[]);
} }
@ -50,7 +50,7 @@ namespace Avalonia.FreeDesktop.DBusMenu
Task UnregisterWindowAsync(uint WindowId); Task UnregisterWindowAsync(uint WindowId);
Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId); Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId);
Task<(uint, string, ObjectPath)[]> GetMenusAsync(); Task<(uint, string, ObjectPath)[]> GetMenusAsync();
Task<IDisposable> WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action<Exception> onError = null); Task<IDisposable> WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchWindowUnregisteredAsync(Action<uint> handler, Action<Exception> onError = null); Task<IDisposable> WatchWindowUnregisteredAsync(Action<uint> handler, Action<Exception>? onError = null);
} }
} }

36
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop
{ {
public class DBusMenuExporter public class DBusMenuExporter
{ {
public static ITopLevelNativeMenuExporter TryCreateTopLevelNativeMenu(IntPtr xid) public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid)
{ {
if (DBusHelper.Connection == null) if (DBusHelper.Connection == null)
return null; return null;
@ -37,10 +37,10 @@ namespace Avalonia.FreeDesktop
{ {
private readonly Connection _dbus; private readonly Connection _dbus;
private readonly uint _xid; private readonly uint _xid;
private IRegistrar _registrar; private IRegistrar? _registrar;
private bool _disposed; private bool _disposed;
private uint _revision = 1; private uint _revision = 1;
private NativeMenu _menu; private NativeMenu? _menu;
private readonly Dictionary<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>(); private readonly Dictionary<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>();
private readonly Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>(); private readonly Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>();
private readonly HashSet<NativeMenu> _menus = new HashSet<NativeMenu>(); private readonly HashSet<NativeMenu> _menus = new HashSet<NativeMenu>();
@ -73,10 +73,10 @@ namespace Avalonia.FreeDesktop
if (_appMenu) if (_appMenu)
{ {
await _dbus.RegisterObjectAsync(this); await _dbus.RegisterObjectAsync(this);
_registrar = DBusHelper.Connection.CreateProxy<IRegistrar>( _registrar = DBusHelper.Connection?.CreateProxy<IRegistrar>(
"com.canonical.AppMenu.Registrar", "com.canonical.AppMenu.Registrar",
"/com/canonical/AppMenu/Registrar"); "/com/canonical/AppMenu/Registrar");
if (!_disposed) if (!_disposed && _registrar is { })
await _registrar.RegisterWindowAsync(_xid, ObjectPath); await _registrar.RegisterWindowAsync(_xid, ObjectPath);
} }
else else
@ -109,9 +109,9 @@ namespace Avalonia.FreeDesktop
public bool IsNativeMenuExported { get; private set; } public bool IsNativeMenuExported { get; private set; }
public event EventHandler OnIsNativeMenuExportedChanged; public event EventHandler? OnIsNativeMenuExportedChanged;
public void SetNativeMenu(NativeMenu menu) public void SetNativeMenu(NativeMenu? menu)
{ {
if (menu == null) if (menu == null)
menu = new NativeMenu(); menu = new NativeMenu();
@ -153,7 +153,7 @@ namespace Avalonia.FreeDesktop
Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background);
} }
private (NativeMenuItemBase item, NativeMenu menu) GetMenu(int id) private (NativeMenuItemBase? item, NativeMenu? menu) GetMenu(int id)
{ {
if (id == 0) if (id == 0)
return (null, _menu); return (null, _menu);
@ -161,7 +161,7 @@ namespace Avalonia.FreeDesktop
return (item, (item as NativeMenuItem)?.Menu); return (item, (item as NativeMenuItem)?.Menu);
} }
private void EnsureSubscribed(NativeMenu menu) private void EnsureSubscribed(NativeMenu? menu)
{ {
if(menu!=null && _menus.Add(menu)) if(menu!=null && _menus.Add(menu))
((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged; ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged;
@ -180,12 +180,12 @@ namespace Avalonia.FreeDesktop
return id; return id;
} }
private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) private void OnMenuItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
QueueReset(); QueueReset();
} }
private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) private void OnItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{ {
QueueReset(); QueueReset();
} }
@ -216,7 +216,7 @@ namespace Avalonia.FreeDesktop
"type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data"
}; };
object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name) object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name)
{ {
var (it, menu) = i; var (it, menu) = i;
@ -302,7 +302,7 @@ namespace Avalonia.FreeDesktop
} }
private List<KeyValuePair<string, object>> _reusablePropertyList = new List<KeyValuePair<string, object>>(); private List<KeyValuePair<string, object>> _reusablePropertyList = new List<KeyValuePair<string, object>>();
KeyValuePair<string, object>[] GetProperties((NativeMenuItemBase item, NativeMenu menu) i, string[] names) KeyValuePair<string, object>[] GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names)
{ {
if (names?.Length > 0 != true) if (names?.Length > 0 != true)
names = AllProperties; names = AllProperties;
@ -336,7 +336,7 @@ namespace Avalonia.FreeDesktop
return Task.FromResult(rv); return Task.FromResult(rv);
} }
(int, KeyValuePair<string, object>[], object[]) GetLayout(NativeMenuItemBase item, NativeMenu menu, int depth, string[] propertyNames) (int, KeyValuePair<string, object>[], object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames)
{ {
var id = item == null ? 0 : GetId(item); var id = item == null ? 0 : GetId(item);
var props = GetProperties((item, menu), propertyNames); var props = GetProperties((item, menu), propertyNames);
@ -414,22 +414,22 @@ namespace Avalonia.FreeDesktop
private event Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> private event Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)>
ItemsPropertiesUpdated { add { } remove { } } ItemsPropertiesUpdated { add { } remove { } }
private event Action<(uint revision, int parent)> LayoutUpdated; private event Action<(uint revision, int parent)>? LayoutUpdated;
private event Action<(int id, uint timestamp)> ItemActivationRequested { add { } remove { } } private event Action<(int id, uint timestamp)> ItemActivationRequested { add { } remove { } }
private event Action<PropertyChanges> PropertiesChanged { add { } remove { } } private event Action<PropertyChanges> PropertiesChanged { add { } remove { } }
async Task<IDisposable> IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception> onError) async Task<IDisposable> IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception>? onError)
{ {
ItemsPropertiesUpdated += handler; ItemsPropertiesUpdated += handler;
return Disposable.Create(() => ItemsPropertiesUpdated -= handler); return Disposable.Create(() => ItemsPropertiesUpdated -= handler);
} }
async Task<IDisposable> IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception> onError) async Task<IDisposable> IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception>? onError)
{ {
LayoutUpdated += handler; LayoutUpdated += handler;
return Disposable.Create(() => LayoutUpdated -= handler); return Disposable.Create(() => LayoutUpdated -= handler);
} }
async Task<IDisposable> IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception> onError) async Task<IDisposable> IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception>? onError)
{ {
ItemActivationRequested+= handler; ItemActivationRequested+= handler;
return Disposable.Create(() => ItemActivationRequested -= handler); return Disposable.Create(() => ItemActivationRequested -= handler);

2
src/Avalonia.FreeDesktop/DBusRequest.cs

@ -11,6 +11,6 @@ namespace Avalonia.FreeDesktop
internal interface IRequest : IDBusObject internal interface IRequest : IDBusObject
{ {
Task CloseAsync(); Task CloseAsync();
Task<IDisposable> WatchResponseAsync(Action<(uint response, IDictionary<string, object> results)> handler, Action<Exception> onError = null); Task<IDisposable> WatchResponseAsync(Action<(uint response, IDictionary<string, object> results)> handler, Action<Exception>? onError = null);
} }
} }

2
src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs

@ -10,7 +10,7 @@ namespace Avalonia.FreeDesktop
public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives) public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives)
{ {
Contract.Requires<ArgumentNullException>(mountedDrives != null); Contract.Requires<ArgumentNullException>(mountedDrives != null);
return new LinuxMountedVolumeInfoListener(ref mountedDrives); return new LinuxMountedVolumeInfoListener(ref mountedDrives!);
} }
} }
} }

1
src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj

@ -11,6 +11,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" />
<Compile Include="..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
<Compile Include="..\Shared\SourceGeneratorAttributes.cs" /> <Compile Include="..\Shared\SourceGeneratorAttributes.cs" />
</ItemGroup> </ItemGroup>

21
src/Avalonia.Themes.Default/Controls/ProgressBar.xaml

@ -1,4 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui"> <Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls">
<Design.PreviewWith> <Design.PreviewWith>
<Border Padding="20"> <Border Padding="20">
<StackPanel Spacing="10"> <StackPanel Spacing="10">
@ -11,10 +13,13 @@
</Border> </Border>
</Design.PreviewWith> </Design.PreviewWith>
<Style Selector="ProgressBar"> <Style Selector="ProgressBar">
<Style.Resources>
<converters:StringFormatConverter x:Key="StringFormatConverter"/>
</Style.Resources>
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/> <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}"/> <Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate TargetType="ProgressBar">
<Grid> <Grid>
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}">
<Panel> <Panel>
@ -23,7 +28,17 @@
</Panel> </Panel>
</Border> </Border>
<LayoutTransformControl HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{Binding ShowProgressText, RelativeSource={RelativeSource TemplatedParent}}" Name="PART_LayoutTransformControl"> <LayoutTransformControl HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{Binding ShowProgressText, RelativeSource={RelativeSource TemplatedParent}}" Name="PART_LayoutTransformControl">
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}" Text="{Binding Value, RelativeSource={RelativeSource TemplatedParent}, StringFormat={}{0:0}%}" /> <TextBlock Foreground="{DynamicResource ThemeForegroundBrush}">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource StringFormatConverter}">
<TemplateBinding Property="ProgressTextFormat"/>
<Binding Path="Value" RelativeSource="{RelativeSource TemplatedParent}"/>
<TemplateBinding Property="Percentage"/>
<TemplateBinding Property="Minimum"/>
<TemplateBinding Property="Maximum"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</LayoutTransformControl> </LayoutTransformControl>
</Grid> </Grid>
</ControlTemplate> </ControlTemplate>

2
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

@ -160,7 +160,7 @@
</ContentPresenter> </ContentPresenter>
<Popup Name="PART_Popup" <Popup Name="PART_Popup"
WindowManagerAddShadowHint="False" WindowManagerAddShadowHint="False"
MinWidth="{ReflectionBinding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}" MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
IsLightDismissEnabled="True" IsLightDismissEnabled="True"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}" IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
OverlayInputPassThroughElement="{Binding $parent[Menu]}"> OverlayInputPassThroughElement="{Binding $parent[Menu]}">

18
src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml

@ -1,5 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui" <Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
x:CompileBindings="True"> x:CompileBindings="True">
<Design.PreviewWith> <Design.PreviewWith>
<Border Padding="20"> <Border Padding="20">
@ -13,6 +14,9 @@
</Border> </Border>
</Design.PreviewWith> </Design.PreviewWith>
<Style Selector="ProgressBar"> <Style Selector="ProgressBar">
<Style.Resources>
<converters:StringFormatConverter x:Key="StringFormatConverter"/>
</Style.Resources>
<Setter Property="Foreground" Value="{DynamicResource SystemControlHighlightAccentBrush}" /> <Setter Property="Foreground" Value="{DynamicResource SystemControlHighlightAccentBrush}" />
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" /> <Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource ProgressBarBorderThemeThickness}" /> <Setter Property="BorderThickness" Value="{DynamicResource ProgressBarBorderThemeThickness}" />
@ -21,7 +25,7 @@
<Setter Property="MinHeight" Value="{DynamicResource ProgressBarThemeMinHeight}" /> <Setter Property="MinHeight" Value="{DynamicResource ProgressBarThemeMinHeight}" />
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate TargetType="ProgressBar">
<Border x:Name="ProgressBarRoot" ClipToBounds="True" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}"> <Border x:Name="ProgressBarRoot" ClipToBounds="True" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}">
<Panel> <Panel>
<Panel x:Name="DeterminateRoot"> <Panel x:Name="DeterminateRoot">
@ -32,7 +36,17 @@
<Border x:Name="IndeterminateProgressBarIndicator2" CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" /> <Border x:Name="IndeterminateProgressBarIndicator2" CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
</Panel> </Panel>
<LayoutTransformControl x:Name="PART_LayoutTransformControl" HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{TemplateBinding ShowProgressText}"> <LayoutTransformControl x:Name="PART_LayoutTransformControl" HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{TemplateBinding ShowProgressText}">
<TextBlock Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" Text="{ReflectionBinding Value, RelativeSource={RelativeSource TemplatedParent}, StringFormat={}{0:0}%}" /> <TextBlock Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource StringFormatConverter}">
<TemplateBinding Property="ProgressTextFormat"/>
<Binding Path="Value" RelativeSource="{RelativeSource TemplatedParent}"/>
<TemplateBinding Property="Percentage"/>
<TemplateBinding Property="Minimum"/>
<TemplateBinding Property="Maximum"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</LayoutTransformControl> </LayoutTransformControl>
</Panel> </Panel>
</Border> </Border>

2
src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs

@ -133,7 +133,7 @@ namespace Avalonia.LinuxFramebuffer.Output
var device = gbm_create_device(card.Fd); var device = gbm_create_device(card.Fd);
_gbmTargetSurface = gbm_surface_create(device, modeInfo.Resolution.Width, modeInfo.Resolution.Height, _gbmTargetSurface = gbm_surface_create(device, modeInfo.Resolution.Width, modeInfo.Resolution.Height,
GbmColorFormats.GBM_FORMAT_XRGB8888, GbmBoFlags.GBM_BO_USE_SCANOUT | GbmBoFlags.GBM_BO_USE_RENDERING); GbmColorFormats.GBM_FORMAT_XRGB8888, GbmBoFlags.GBM_BO_USE_SCANOUT | GbmBoFlags.GBM_BO_USE_RENDERING);
if(_gbmTargetSurface == null) if(_gbmTargetSurface == IntPtr.Zero)
throw new InvalidOperationException("Unable to create GBM surface"); throw new InvalidOperationException("Unable to create GBM surface");
_eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), false, 0x31D7, device, null); _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), false, 0x31D7, device, null);

6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -45,10 +45,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlReorderClassesPropertiesTransformer() new AvaloniaXamlIlReorderClassesPropertiesTransformer()
); );
InsertBefore<ContentConvertTransformer>( InsertBefore<ContentConvertTransformer>(
new AvaloniaXamlIlBindingPathParser(),
new AvaloniaXamlIlSelectorTransformer(), new AvaloniaXamlIlSelectorTransformer(),
new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
new AvaloniaXamlIlBindingPathParser(),
new AvaloniaXamlIlPropertyPathTransformer(), new AvaloniaXamlIlPropertyPathTransformer(),
new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlSetterTransformer(),
new AvaloniaXamlIlConstructorServiceProviderTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(),

23
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs

@ -122,10 +122,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding);
} }
var mode = relativeSourceObject.Children var modeProperty = relativeSourceObject.Children
.OfType<XamlAstXamlPropertyValueNode>() .OfType<XamlAstXamlPropertyValueNode>()
.FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode") .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode")?
?.Values[0] is XamlAstTextNode modeAssignedValue ? modeAssignedValue.Text : null; .Values.FirstOrDefault() as XamlAstTextNode
?? relativeSourceObject.Arguments.OfType<XamlAstTextNode>().FirstOrDefault();
var mode = modeProperty?.Text;
if (relativeSourceObject.Arguments.Count == 0 && mode == null) if (relativeSourceObject.Arguments.Count == 0 && mode == null)
{ {
mode = "FindAncestor"; mode = "FindAncestor";
@ -212,16 +215,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
} }
else if (mode == "TemplatedParent") else if (mode == "TemplatedParent")
{ {
var parentType = context.ParentNodes().OfType<AvaloniaXamlIlTargetTypeMetadataNode>() var contentTemplateNode = context.ParentNodes().OfType<AvaloniaXamlIlTargetTypeMetadataNode>()
.FirstOrDefault(x => .FirstOrDefault(x =>
x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate) x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate);
?.TargetType.GetClrType(); if (contentTemplateNode is null)
if (parentType is null)
{ {
throw new XamlParseException("A binding with a TemplatedParent RelativeSource has to be in a ControlTemplate.", binding); throw new XamlParseException("A binding with a TemplatedParent RelativeSource has to be in a ControlTemplate.", binding);
} }
var parentType = contentTemplateNode.TargetType.GetClrType();
if (parentType is null)
{
throw new XamlParseException("TargetType has to be set on ControlTemplate or it should be defined inside of a Style.", binding);
}
convertedNode = new TemplatedParentBindingExpressionNode { Type = parentType }; convertedNode = new TemplatedParentBindingExpressionNode { Type = parentType };
} }
else else

5
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs

@ -105,6 +105,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{ {
startType = TypeReferenceResolver.ResolveType(context, text.Text, isMarkupExtension: false, text, strict: true).Type; startType = TypeReferenceResolver.ResolveType(context, text.Text, isMarkupExtension: false, text, strict: true).Type;
} }
if (dataTypeProperty?.Values.Count is 1 && dataTypeProperty.Values[0] is XamlTypeExtensionNode typeNode)
{
startType = typeNode.Value.GetClrType();
}
Func<IXamlType> startTypeResolver = startType is not null ? () => startType : () => Func<IXamlType> startTypeResolver = startType is not null ? () => startType : () =>
{ {

16
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs

@ -47,7 +47,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
return startTypeResolver(); return startTypeResolver();
} }
if (bindingPathAssignment.Values[0] is ParsedBindingPathNode bindingPathNode) if (bindingPathAssignment.Values[0] is XamlIlBindingPathNode pathNode)
{
bindingResultType = pathNode.BindingResultType;
}
else if (bindingPathAssignment.Values[0] is ParsedBindingPathNode bindingPathNode)
{ {
var transformed = TransformBindingPath( var transformed = TransformBindingPath(
context, context,
@ -63,7 +67,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
} }
else else
{ {
throw new InvalidOperationException(); throw new InvalidOperationException("Invalid state of Path property");
} }
} }
@ -240,6 +244,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
case TemplatedParentBindingExpressionNode templatedParent: case TemplatedParentBindingExpressionNode templatedParent:
var templatedParentField = context.GetAvaloniaTypes().StyledElement.GetAllFields() var templatedParentField = context.GetAvaloniaTypes().StyledElement.GetAllFields()
.FirstOrDefault(f => f.IsStatic && f.IsPublic && f.Name == "TemplatedParentProperty"); .FirstOrDefault(f => f.IsStatic && f.IsPublic && f.Name == "TemplatedParentProperty");
nodes.Add(new SelfPathElementNode(selfType));
nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode( nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(
templatedParentField, templatedParentField,
templatedParent.Type)); templatedParent.Type));
@ -374,6 +379,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
public static IXamlType GetTargetType(IXamlAstNode namescopeRoot, string name) public static IXamlType GetTargetType(IXamlAstNode namescopeRoot, string name)
{ {
// If we start from the nested scope - skip it.
if (namescopeRoot is NestedScopeMetadataNode scope)
{
namescopeRoot = scope.Value;
}
var finder = new ScopeRegistrationFinder(name); var finder = new ScopeRegistrationFinder(name);
namescopeRoot.Visit(finder); namescopeRoot.Visit(finder);
return finder.TargetType; return finder.TargetType;
@ -399,6 +410,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
{ {
// Ignore name registrations, if we are inside of the nested namescope.
if (_childScopesStack.Count == 0 && node is AvaloniaNameScopeRegistrationXamlIlNode registration) if (_childScopesStack.Count == 0 && node is AvaloniaNameScopeRegistrationXamlIlNode registration)
{ {
if (registration.Name is XamlAstTextNode text && text.Text == Name) if (registration.Name is XamlAstTextNode text && text.Text == Name)

2
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs

@ -74,6 +74,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
return pathRoot ?? new EmptyExpressionNode(); return pathRoot ?? new EmptyExpressionNode();
} }
internal IEnumerable<ICompiledBindingPathElement> Elements => _elements;
internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data;
internal object RawSource { get; } internal object RawSource { get; }

0
src/Avalonia.SourceGenerator/IsExternalInit.cs → src/Shared/IsExternalInit.cs

5
tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs

@ -142,14 +142,13 @@ namespace Avalonia.Base.UnitTests.Styling
var border = (Border)target.Object.VisualChildren.Single(); var border = (Border)target.Object.VisualChildren.Single();
var selector = default(Selector).OfType(templatedControl.Object.GetType()).Class("foo").Template().OfType<Border>(); var selector = default(Selector).OfType(templatedControl.Object.GetType()).Class("foo").Template().OfType<Border>();
var activator = selector.Match(border).Activator; var activator = selector.Match(border).Activator;
var inccDebug = (INotifyCollectionChangedDebug)styleable.Object.Classes;
using (activator.Subscribe(_ => { })) using (activator.Subscribe(_ => { }))
{ {
Assert.Single(inccDebug.GetCollectionChangedSubscribers()); Assert.Equal(1, ((Classes)styleable.Object.Classes).ListenerCount);
} }
Assert.Null(inccDebug.GetCollectionChangedSubscribers()); Assert.Equal(0, ((Classes)styleable.Object.Classes).ListenerCount);
} }
private void BuildVisualTree<T>(Mock<T> templatedControl) where T : class, IVisual private void BuildVisualTree<T>(Mock<T> templatedControl) where T : class, IVisual

85
tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs

@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.Styling;
using BenchmarkDotNet.Attributes;
#nullable enable
namespace Avalonia.Benchmarks.Styling
{
[MemoryDiagnoser]
public class Style_ClassSelector
{
private Style _style = null!;
public Style_ClassSelector()
{
RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
}
[GlobalSetup]
public void Setup()
{
_style = new Style(x => x.OfType<TestClass>().Class("foo"))
{
Setters = { new Setter(TestClass.StringProperty, "foo") }
};
}
[Benchmark(OperationsPerInvoke = 50)]
public void Apply()
{
var target = new TestClass();
target.BeginBatchUpdate();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
target.EndBatchUpdate();
}
[Benchmark(OperationsPerInvoke = 50)]
public void Apply_Toggle()
{
var target = new TestClass();
target.BeginBatchUpdate();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
target.EndBatchUpdate();
target.Classes.Add("foo");
target.Classes.Remove("foo");
}
[Benchmark(OperationsPerInvoke = 50)]
public void Apply_Detach()
{
var target = new TestClass();
target.BeginBatchUpdate();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
target.EndBatchUpdate();
target.DetachStyles();
}
private class TestClass : Control
{
public static readonly StyledProperty<string?> StringProperty =
AvaloniaProperty.Register<TestClass, string?>("String");
public void DetachStyles() => InvalidateStyles();
}
private class TestClass2 : Control
{
}
}
}

44
tests/Avalonia.Controls.UnitTests/MenuItemTests.cs

@ -8,6 +8,7 @@ using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq; using Moq;
using Xunit; using Xunit;
@ -301,6 +302,49 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(3, canExecuteCallCount); Assert.Equal(3, canExecuteCallCount);
} }
} }
[Fact]
public void TemplatedParent_Should_Not_Be_Applied_To_Submenus()
{
using (Application())
{
MenuItem topLevelMenu;
MenuItem childMenu1;
MenuItem childMenu2;
var menu = new Menu
{
Items = new[]
{
(topLevelMenu = new MenuItem
{
Header = "Foo",
Items = new[]
{
(childMenu1 = new MenuItem { Header = "Bar" }),
(childMenu2 = new MenuItem { Header = "Baz" }),
}
}),
}
};
var window = new Window { Content = menu };
window.LayoutManager.ExecuteInitialLayoutPass();
topLevelMenu.IsSubMenuOpen = true;
Assert.True(((IVisual)childMenu1).IsAttachedToVisualTree);
Assert.Null(childMenu1.TemplatedParent);
Assert.Null(childMenu2.TemplatedParent);
topLevelMenu.IsSubMenuOpen = false;
topLevelMenu.IsSubMenuOpen = true;
Assert.Null(childMenu1.TemplatedParent);
Assert.Null(childMenu2.TemplatedParent);
}
}
private IDisposable Application() private IDisposable Application()
{ {
var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100)); var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100));

147
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -295,7 +295,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
} }
[Fact] [Fact]
public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent() public void ContentControl_With_Popup_In_Template_Should_Set_TemplatedParent()
{ {
// Test uses OverlayPopupHost default template // Test uses OverlayPopupHost default template
using (CreateServices()) using (CreateServices())
@ -384,6 +384,134 @@ namespace Avalonia.Controls.UnitTests.Primitives
} }
} }
[Fact]
public void ItemsControl_With_Popup_In_Template_Should_Set_TemplatedParent()
{
// Test uses OverlayPopupHost default template
using (CreateServices())
{
PopupItemsControl target;
var item = new Border();
var root = PreparedWindow(target = new PopupItemsControl
{
Items = new[] { item },
Template = new FuncControlTemplate<PopupItemsControl>(PopupItemsControlTemplate),
}); ;
root.Show();
target.ApplyTemplate();
var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup");
popup.Open();
var popupRoot = (Control)popup.Host;
popupRoot.Measure(Size.Infinity);
popupRoot.Arrange(new Rect(popupRoot.DesiredSize));
var children = popupRoot.GetVisualDescendants().ToList();
var types = children.Select(x => x.GetType().Name).ToList();
if (UsePopupHost)
{
Assert.Equal(
new[]
{
"LayoutTransformControl",
"VisualLayerManager",
"ContentPresenter",
"ItemsPresenter",
"StackPanel",
"Border",
},
types);
}
else
{
Assert.Equal(
new[]
{
"LayoutTransformControl",
"Panel",
"Border",
"VisualLayerManager",
"ContentPresenter",
"ItemsPresenter",
"StackPanel",
"Border",
},
types);
}
var templatedParents = children
.OfType<IControl>()
.Select(x => x.TemplatedParent).ToList();
if (UsePopupHost)
{
Assert.Equal(
new object[]
{
popupRoot,
popupRoot,
popupRoot,
target,
target,
null,
},
templatedParents);
}
else
{
Assert.Equal(
new object[]
{
popupRoot,
popupRoot,
popupRoot,
popupRoot,
popupRoot,
target,
target,
null,
},
templatedParents);
}
}
}
[Fact]
public void Should_Not_Overwrite_TemplatedParent_Of_Item_In_ItemsControl_With_Popup_On_Second_Open()
{
// Test uses OverlayPopupHost default template
using (CreateServices())
{
PopupItemsControl target;
var item = new Border();
var root = PreparedWindow(target = new PopupItemsControl
{
Items = new[] { item },
Template = new FuncControlTemplate<PopupItemsControl>(PopupItemsControlTemplate),
});
root.Show();
target.ApplyTemplate();
var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup");
popup.Open();
var popupRoot = (Control)popup.Host;
popupRoot.Measure(Size.Infinity);
popupRoot.Arrange(new Rect(popupRoot.DesiredSize));
Assert.Null(item.TemplatedParent);
popup.Close();
popup.Open();
Assert.Null(item.TemplatedParent);
}
}
[Fact] [Fact]
public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit() public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit()
{ {
@ -979,10 +1107,27 @@ namespace Avalonia.Controls.UnitTests.Primitives
}.RegisterInNameScope(scope); }.RegisterInNameScope(scope);
} }
private static IControl PopupItemsControlTemplate(PopupItemsControl control, INameScope scope)
{
return new Popup
{
Name = "popup",
PlacementTarget = control,
Child = new ItemsPresenter
{
[~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
}
}.RegisterInNameScope(scope);
}
private class PopupContentControl : ContentControl private class PopupContentControl : ContentControl
{ {
} }
private class PopupItemsControl : ItemsControl
{
}
private class TestControl : Decorator private class TestControl : Decorator
{ {
public event EventHandler DataContextBeginUpdate; public event EventHandler DataContextBeginUpdate;

2
tests/Avalonia.LeakTests/ControlTests.cs

@ -314,7 +314,7 @@ namespace Avalonia.LeakTests
// The TextBox should have subscriptions to its Classes collection from the // The TextBox should have subscriptions to its Classes collection from the
// default theme. // default theme.
Assert.NotEmpty(((INotifyCollectionChangedDebug)textBox.Classes).GetCollectionChangedSubscribers()); Assert.NotEqual(0, textBox.Classes.ListenerCount);
// Clear the content and ensure the TextBox is removed. // Clear the content and ensure the TextBox is removed.
window.Content = null; window.Content = null;

11
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@ -637,6 +637,17 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal("baz", source["Foo"]); Assert.Equal("baz", source["Foo"]);
} }
[Fact]
public void Binding_To_Types_Should_Work()
{
var type = typeof(string);
var textBlock = new TextBlock() { DataContext = type };
using (textBlock.Bind(TextBlock.TextProperty, new Binding("Name")))
{
Assert.Equal("String", textBlock.Text);
};
}
private class StyledPropertyClass : AvaloniaObject private class StyledPropertyClass : AvaloniaObject
{ {
public static readonly StyledProperty<double> DoubleValueProperty = public static readonly StyledProperty<double> DoubleValueProperty =

225
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@ -3,22 +3,23 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Markup.Data; using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
using Avalonia.Markup.Xaml.Templates; using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using JetBrains.Annotations;
using XamlX;
using Xunit; using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
@ -102,6 +103,91 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal(dataContext.StringProperty, textBlock.Text); Assert.Equal(dataContext.StringProperty, textBlock.Text);
} }
} }
[Fact]
public void ResolvesPathPassedByPropertyWithInnerItemTemplate()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'>
<ItemsControl Name='itemsControl' Items='{CompiledBinding Path=ListProperty}'>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<ItemsControl>("itemsControl");
var dataContext = new TestDataContext
{
ListProperty =
{
"Hello"
}
};
window.DataContext = dataContext;
Assert.Equal(dataContext.ListProperty, textBlock.Items);
}
}
[Fact]
public void ResolvesDataTypeFromBindingProperty()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'>
<TextBlock Text='{CompiledBinding StringProperty, DataType=local:TestDataContext}' Name='textBlock' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
var dataContext = new TestDataContext
{
StringProperty = "foobar"
};
window.DataContext = dataContext;
Assert.Equal(dataContext.StringProperty, textBlock.Text);
}
}
[Fact]
public void ResolvesDataTypeFromBindingProperty_TypeExtension()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'>
<TextBlock Text='{CompiledBinding StringProperty, DataType={x:Type local:TestDataContext}}' Name='textBlock' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
var dataContext = new TestDataContext
{
StringProperty = "foobar"
};
window.DataContext = dataContext;
Assert.Equal(dataContext.StringProperty, textBlock.Text);
}
}
[Fact] [Fact]
public void ResolvesStreamTaskBindingCorrectly() public void ResolvesStreamTaskBindingCorrectly()
@ -641,6 +727,64 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
} }
} }
[Fact]
public void ResolvesRelativeSourceBindingFromTemplate()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<ContentControl xmlns='https://github.com/avaloniaui'
Content='Hello'>
<ContentControl.Styles>
<Style Selector='ContentControl'>
<Setter Property='Template'>
<ControlTemplate>
<ContentPresenter Content='{CompiledBinding Content, RelativeSource={RelativeSource TemplatedParent}}' />
</ControlTemplate>
</Setter>
</Style>
</ContentControl.Styles>
</ContentControl>";
var contentControl = AvaloniaRuntimeXamlLoader.Parse<ContentControl>(xaml);
contentControl.Measure(new Size(10, 10));
var result = contentControl.GetTemplateChildren().OfType<ContentPresenter>().First();
Assert.Equal("Hello", result.Content);
}
}
[Fact]
public void ResolvesElementNameInTemplate()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<ContentControl xmlns='https://github.com/avaloniaui'
Content='Hello'>
<ContentControl.Styles>
<Style Selector='ContentControl'>
<Setter Property='Template'>
<ControlTemplate>
<Panel>
<TextBox Name='InnerTextBox' Text='Hello' />
<ContentPresenter Content='{CompiledBinding Text, ElementName=InnerTextBox}' />
</Panel>
</ControlTemplate>
</Setter>
</Style>
</ContentControl.Styles>
</ContentControl>";
var contentControl = AvaloniaRuntimeXamlLoader.Parse<ContentControl>(xaml);
contentControl.Measure(new Size(10, 10));
var result = contentControl.GetTemplateChildren().OfType<ContentPresenter>().First();
Assert.Equal("Hello", result.Content);
}
}
[Fact] [Fact]
public void Binds_To_Source() public void Binds_To_Source()
{ {
@ -1145,6 +1289,28 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal("bar-" + typeof(TestDataContext).FullName, textBlock.Text); Assert.Equal("bar-" + typeof(TestDataContext).FullName, textBlock.Text);
} }
} }
[Fact]
public void SupportCastToTypeInExpressionWithProperty_ExplicitPropertyCast()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'>
<ContentControl Content='{CompiledBinding $parent.((local:IHasExplicitProperty)DataContext).ExplicitProperty}' Name='contentControl' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var contentControl = window.GetControl<ContentControl>("contentControl");
var dataContext = new TestDataContext();
window.DataContext = dataContext;
Assert.Equal(((IHasExplicitProperty)dataContext).ExplicitProperty, contentControl.Content);
}
}
[Fact] [Fact]
public void Binds_To_Self_Without_DataType() public void Binds_To_Self_Without_DataType()
@ -1366,6 +1532,43 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
} }
} }
[Fact]
public void ResolvesDataTypeForAssignBinding()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<local:AssignBindingControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'
X='{CompiledBinding StringProperty}' />";
var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var compiledPath = ((CompiledBindingExtension)control.X).Path;
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath.Elements));
Assert.Equal(typeof(string), node.Property.PropertyType);
}
}
[Fact]
public void ResolvesDataTypeForAssignBinding_FromBindingProperty()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<local:AssignBindingControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
X='{CompiledBinding StringProperty, DataType=local:TestDataContext}' />";
var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var compiledPath = ((CompiledBindingExtension)control.X).Path;
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath.Elements));
Assert.Equal(typeof(string), node.Property.PropertyType);
}
}
void Throws(string type, Action cb) void Throws(string type, Action cb)
{ {
try try
@ -1410,6 +1613,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public interface IHasPropertyDerived : IHasProperty public interface IHasPropertyDerived : IHasProperty
{ } { }
public interface IHasExplicitProperty
{
string ExplicitProperty { get; }
}
public class AppendConverter : IValueConverter public class AppendConverter : IValueConverter
{ {
public static IValueConverter Instance { get; } = new AppendConverter(); public static IValueConverter Instance { get; } = new AppendConverter();
@ -1429,7 +1637,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public class TestDataContextBaseClass {} public class TestDataContextBaseClass {}
public class TestDataContext : TestDataContextBaseClass, IHasPropertyDerived public class TestDataContext : TestDataContextBaseClass, IHasPropertyDerived, IHasExplicitProperty
{ {
public string StringProperty { get; set; } public string StringProperty { get; set; }
@ -1449,6 +1657,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public INonIntegerIndexerDerived NonIntegerIndexerInterfaceProperty => NonIntegerIndexerProperty; public INonIntegerIndexerDerived NonIntegerIndexerInterfaceProperty => NonIntegerIndexerProperty;
string IHasExplicitProperty.ExplicitProperty => "Hello";
public string ExplicitProperty => "Bye";
public class NonIntegerIndexer : NotifyingBase, INonIntegerIndexerDerived public class NonIntegerIndexer : NotifyingBase, INonIntegerIndexerDerived
{ {
private readonly Dictionary<string, string> _storage = new Dictionary<string, string>(); private readonly Dictionary<string, string> _storage = new Dictionary<string, string>();
@ -1534,4 +1746,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
} }
public class CustomDataTemplateInherit : CustomDataTemplate { } public class CustomDataTemplateInherit : CustomDataTemplate { }
public class AssignBindingControl : Control
{
[AssignBinding] public IBinding X { get; set; }
}
} }

Loading…
Cancel
Save