From 95af64a8d055531126724c4da55d669191ce6b38 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 27 Dec 2018 20:16:50 +0300 Subject: [PATCH 0001/1791] [WIP] Managed file dialog --- Avalonia.sln | 25 ++ build/CoreLibraries.props | 1 + .../ControlCatalog.NetCore.csproj | 1 + samples/ControlCatalog.NetCore/Program.cs | 41 ++-- src/Avalonia.Controls/AppBuilderBase.cs | 11 + src/Avalonia.Controls/SystemDialog.cs | 16 +- src/Avalonia.Dialogs/Avalonia.Dialogs.csproj | 20 ++ src/Avalonia.Dialogs/Internal/ChildFitter.cs | 20 ++ .../Internal/InternalViewModelBase.cs | 31 +++ .../Internal/ManagedFileChooser.xaml | 74 ++++++ .../Internal/ManagedFileChooser.xaml.cs | 65 +++++ .../ManagedFileChooserFilterViewModel.cs | 38 +++ .../ManagedFileChooserItemViewModel.cs | 45 ++++ .../Internal/ManagedFileChooserSources.cs | 75 ++++++ .../Internal/ManagedFileChooserViewModel.cs | 223 ++++++++++++++++++ .../Internal/ManagedFileDialog.xaml | 7 + .../Internal/ManagedFileDialog.xaml.cs | 18 ++ .../Internal/ResourceSelectorConverter.cs | 21 ++ .../ManagedFileDialogExtensions.cs | 54 +++++ 19 files changed, 765 insertions(+), 21 deletions(-) create mode 100644 src/Avalonia.Dialogs/Avalonia.Dialogs.csproj create mode 100644 src/Avalonia.Dialogs/Internal/ChildFitter.cs create mode 100644 src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs create mode 100644 src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs create mode 100644 src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs diff --git a/Avalonia.sln b/Avalonia.sln index df60ff4a75..14ecd76517 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -190,6 +190,7 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Desktop", "src\Avalonia.Desktop\Avalonia.Desktop.csproj", "{3C471044-3640-45E3-B1B2-16D2FF8399EE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Build.Tasks", "src\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj", "{BF28998D-072C-439A-AFBB-2FE5021241E0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -1714,6 +1715,30 @@ Global {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|iPhone.Build.0 = Release|Any CPU {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhone.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhone.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.Build.0 = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhone.ActiveCfg = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhone.Build.0 = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index d989e643b8..3923bdeeda 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -13,6 +13,7 @@ + diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 7e2c707e91..f3dce7fc0e 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -6,6 +6,7 @@ + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 1f53dedc14..2a455cb0b2 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -1,8 +1,13 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Avalonia; +using Avalonia.Controls; +using Avalonia.Dialogs; +using Avalonia.Dialogs.Internal; using Avalonia.Skia; namespace ControlCatalog.NetCore @@ -13,31 +18,31 @@ namespace ControlCatalog.NetCore static void Main(string[] args) { Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA); - if (args.Contains("--wait-for-attach")) + var b = BuildAvaloniaApp(); + b.SetupWithoutStarting(); + var window = new Window(); + window.Show(); + new OpenFileDialog() { - Console.WriteLine("Attach debugger and use 'Set next statement'"); - while (true) + Filters = new List { - Thread.Sleep(100); - if (Debugger.IsAttached) - break; - } - } - if (args.Contains("--fbdev")) - AppBuilder.Configure().InitializeWithLinuxFramebuffer(tl => - { - tl.Content = new MainView(); - System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer()); - }); - else - BuildAvaloniaApp().Start(); + new FileDialogFilter {Name = "All files", Extensions = {"*"}}, + new FileDialogFilter {Name = "Image files", Extensions = {"jpg", "png", "gif"}} + }, + Directory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + Title = "My dialog", + InitialFileName = "config.local.json", + AllowMultiple = true + }.ShowAsync(window).ContinueWith(_ => { window.Close(); }, TaskContinuationOptions.ExecuteSynchronously); + + b.Instance.Run(window); } /// /// This method is needed for IDE previewer infrastructure /// public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure().UsePlatformDetect().UseSkia().UseReactiveUI(); + => AppBuilder.Configure().UsePlatformDetect().UseSkia().UseReactiveUI().UseManagedSystemDialogs(); static void ConsoleSilencer() { @@ -46,4 +51,4 @@ namespace ControlCatalog.NetCore Console.ReadKey(true); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 376714b20b..9ee9a2aea5 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Reflection; using System.Linq; using Avalonia.Platform; @@ -61,6 +62,8 @@ namespace Avalonia.Controls /// public Action BeforeStartCallback { get; private set; } = builder => { }; + public Action AfterPlatformServicesSetupCallback { get; private set; } = builder => { }; + protected AppBuilderBase(IRuntimePlatform platform, Action platformServices) { RuntimePlatform = platform; @@ -110,6 +113,13 @@ namespace Avalonia.Controls AfterSetupCallback = (Action)Delegate.Combine(AfterSetupCallback, callback); return Self; } + + + public TAppBuilder AfterPlatformServicesSetup(Action callback) + { + AfterPlatformServicesSetupCallback = (Action)Delegate.Combine(AfterPlatformServicesSetupCallback, callback); + return Self; + } /// /// Starts the application with an instance of . @@ -275,6 +285,7 @@ namespace Avalonia.Controls RuntimePlatformServicesInitializer(); WindowingSubsystemInitializer(); RenderingSubsystemInitializer(); + AfterPlatformServicesSetupCallback(Self); Instance.RegisterServices(); Instance.Initialize(); AfterSetupCallback(Self); diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index e7cb4763ed..2f03db5aee 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -14,7 +14,13 @@ namespace Avalonia.Controls public abstract class FileSystemDialog : SystemDialog { - public string InitialDirectory { get; set; } + [Obsolete("Use Directory")] + public string InitialDirectory + { + get => Directory; + set => Directory = value; + } + public string Directory { get; set; } } public class SaveFileDialog : FileDialog @@ -45,8 +51,12 @@ namespace Avalonia.Controls public class OpenFolderDialog : FileSystemDialog { - public string DefaultDirectory { get; set; } - + [Obsolete("Use Directory")] + public string DefaultDirectory + { + get => Directory; + set => Directory = value; + } public Task ShowAsync(Window parent) { if(parent == null) diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj new file mode 100644 index 0000000000..d5c6b1d920 --- /dev/null +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -0,0 +1,20 @@ + + + netstandard2.0 + false + + + + + Designer + + + Designer + + + + + + + + diff --git a/src/Avalonia.Dialogs/Internal/ChildFitter.cs b/src/Avalonia.Dialogs/Internal/ChildFitter.cs new file mode 100644 index 0000000000..d81dafcd99 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ChildFitter.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.Layout; + +namespace Avalonia.Dialogs.Internal +{ + class ChildFitter : Decorator + { + protected override Size MeasureOverride(Size availableSize) + { + return new Size(0, 0); + } + + protected override Size ArrangeOverride(Size finalSize) + { + Child.Measure(finalSize); + base.ArrangeOverride(finalSize); + return finalSize; + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs b/src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs new file mode 100644 index 0000000000..35e37eb810 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace Avalonia.Dialogs.Internal +{ + class InternalViewModelBase : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (!EqualityComparer.Default.Equals(field, value)) + { + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + return true; + } + + return false; + } + + [NotifyPropertyChangedInvocator] + protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml new file mode 100644 index 0000000000..51154f1161 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Show hidden files + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs new file mode 100644 index 0000000000..dd23a90922 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Dialogs.Internal +{ + class ManagedFileChooser : UserControl + { + private Control _quickLinksRoot; + private ListBox _filesView; + + public ManagedFileChooser() + { + AvaloniaXamlLoader.Load(this); + AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel); + _quickLinksRoot = this.FindControl("QuickLinks"); + _filesView = this.FindControl("Files"); + } + + ManagedFileChooserViewModel Model => DataContext as ManagedFileChooserViewModel; + + private void OnPointerPressed(object sender, PointerPressedEventArgs e) + { + var model = (e.Source as StyledElement)?.DataContext as ManagedFileChooserItemViewModel; + if(model == null) + return; + + var isQuickLink = _quickLinksRoot.IsLogicalParentOf(e.Source as Control); + if (e.ClickCount == 2 || isQuickLink) + { + if (model.IsDirectory) + Model?.Navigate(model.Path); + else + Model?.SelectSingleFile(model); + e.Handled = true; + } + } + + protected override async void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + var model = (DataContext as ManagedFileChooserViewModel); + if (model == null) + return; + var preselected = model.SelectedItems.FirstOrDefault(); + if(preselected == null) + return; + + //Let everything to settle down and scroll to selected item + await Task.Delay(100); + if (preselected != model.SelectedItems.FirstOrDefault()) + return; + + // Workaround for ListBox bug, scroll to the previous file + var indexOfPreselected = model.Items.IndexOf(preselected); + if (indexOfPreselected > 1) + _filesView.ScrollIntoView(model.Items[indexOfPreselected - 1]); + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs new file mode 100644 index 0000000000..1f561d0cd2 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; + +namespace Avalonia.Dialogs.Internal +{ + class ManagedFileChooserFilterViewModel : InternalViewModelBase + { + private readonly string[] _extensions; + public string Name { get; } + + public ManagedFileChooserFilterViewModel(FileDialogFilter filter) + { + Name = filter.Name; + if (filter.Extensions.Contains("*")) + return; + _extensions = filter.Extensions?.Select(e => "." + e.ToLowerInvariant()).ToArray(); + } + + public ManagedFileChooserFilterViewModel() + { + Name = "All files"; + } + + public bool Match(string filename) + { + if (_extensions == null) + return true; + foreach(var ext in _extensions) + if (filename.EndsWith(ext, StringComparison.InvariantCultureIgnoreCase)) + return true; + return false; + } + + public override string ToString() => Name; + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs new file mode 100644 index 0000000000..d5f72cdbac --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs @@ -0,0 +1,45 @@ +namespace Avalonia.Dialogs.Internal +{ + class ManagedFileChooserItemViewModel : InternalViewModelBase + { + private string _displayName; + private string _path; + private bool _isDirectory; + + public string DisplayName + { + get => _displayName; + set => RaiseAndSetIfChanged(ref _displayName, value); + } + + public string Path + { + get => _path; + set => RaiseAndSetIfChanged(ref _path, value); + } + + public string IconKey => IsDirectory ? "Icon_Folder" : "Icon_File"; + + public bool IsDirectory + { + get => _isDirectory; + set + { + if (RaiseAndSetIfChanged(ref _isDirectory, value)) + RaisePropertyChanged(nameof(IconKey)); + } + } + + public ManagedFileChooserItemViewModel() + { + + } + + public ManagedFileChooserItemViewModel(ManagedFileChooserNavigationItem item) + { + IsDirectory = true; + Path = item.Path; + DisplayName = item.DisplayName; + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs new file mode 100644 index 0000000000..587b32fe03 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Avalonia.Dialogs.Internal +{ + public class ManagedFileChooserSources + { + public Func GetUserDirectories { get; set; } + = DefaultGetUserDirectories; + + public Func GetFileSystemRoots { get; set; } + = DefaultGetFileSystemRoots; + + public Func GetAllItemsDelegate { get; set; } + = DefaultGetAllItems; + + public ManagedFileChooserNavigationItem[] GetAllItems() => GetAllItemsDelegate(this); + + public static ManagedFileChooserNavigationItem[] DefaultGetAllItems(ManagedFileChooserSources sources) + { + return sources.GetUserDirectories().Concat(sources.GetFileSystemRoots()).ToArray(); + } + + private static Environment.SpecialFolder[] s_folders = new[] + { + Environment.SpecialFolder.Desktop, + Environment.SpecialFolder.UserProfile, + Environment.SpecialFolder.MyDocuments, + Environment.SpecialFolder.MyMusic, + Environment.SpecialFolder.MyPictures, + Environment.SpecialFolder.MyVideos + }; + + public static ManagedFileChooserNavigationItem[] DefaultGetUserDirectories() + { + return s_folders.Select(Environment.GetFolderPath).Distinct() + .Where(d => !string.IsNullOrWhiteSpace(d)) + .Where(Directory.Exists) + .Select(d => new ManagedFileChooserNavigationItem + { + Path = d, + DisplayName = Path.GetFileName(d) + }).ToArray(); + } + + public static ManagedFileChooserNavigationItem[] DefaultGetFileSystemRoots() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return DriveInfo.GetDrives().Select(d => new ManagedFileChooserNavigationItem + { + DisplayName = d.Name, + Path = d.RootDirectory.FullName + }).ToArray(); + } + + return new[] + { + new ManagedFileChooserNavigationItem + { + DisplayName = "File System", + Path = "/" + } + }; + } + } + + public class ManagedFileChooserNavigationItem + { + public string DisplayName { get; set; } + public string Path { get; set; } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs new file mode 100644 index 0000000000..72bff302bd --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Threading; + +namespace Avalonia.Dialogs.Internal +{ + class ManagedFileChooserViewModel : InternalViewModelBase + { + public event Action CancelRequested; + public event Action CompleteRequested; + + public AvaloniaList QuickLinks { get; } = + new AvaloniaList(); + + public AvaloniaList Items { get; } = + new AvaloniaList(); + + public AvaloniaList Filters { get; } = + new AvaloniaList(); + + public AvaloniaList SelectedItems { get; } = + new AvaloniaList(); + + string _location; + private bool _showHiddenFiles; + private ManagedFileChooserFilterViewModel _selectedFilter; + private bool _selectingDirectory; + private bool _scheduledSelectionValidation; + + public string Location + { + get => _location; + private set => RaiseAndSetIfChanged(ref _location, value); + } + + public bool ShowFilters { get; } + public SelectionMode SelectionMode { get; } + public string Title { get; } + + public int QuickLinksSelectedIndex + { + get + { + for (var index = 0; index < QuickLinks.Count; index++) + { + var i = QuickLinks[index]; + if (i.Path == Location) + return index; + } + + return -1; + } + set => RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); + } + + public ManagedFileChooserFilterViewModel SelectedFilter + { + get => _selectedFilter; + set + { + RaiseAndSetIfChanged(ref _selectedFilter, value); + Refresh(); + } + } + + public bool ShowHiddenFiles + { + get => _showHiddenFiles; + set + { + RaiseAndSetIfChanged(ref _showHiddenFiles, value); + Refresh(); + } + } + + public ManagedFileChooserViewModel(FileSystemDialog dialog) + { + var quickSources = AvaloniaLocator.Current.GetService() + ?? new ManagedFileChooserSources(); + QuickLinks.Clear(); + + QuickLinks.AddRange(quickSources.GetAllItems().Select(i => new ManagedFileChooserItemViewModel(i))); + Title = dialog.Title ?? ( + dialog is OpenFileDialog ? "Open file" + : dialog is SaveFileDialog ? "Save file" + : dialog is OpenFolderDialog ? "Select directory" + : throw new ArgumentException(nameof(dialog))); + + var directory = dialog.Directory; + if (directory == null || !Directory.Exists(directory)) + directory = Directory.GetCurrentDirectory(); + + if (dialog is FileDialog fd) + { + if (fd.Filters?.Count > 0) + { + Filters.AddRange(fd.Filters.Select(f => new ManagedFileChooserFilterViewModel(f))); + _selectedFilter = Filters[0]; + ShowFilters = true; + } + + if (dialog is OpenFileDialog ofd) + { + if (ofd.AllowMultiple) + SelectionMode = SelectionMode.Multiple; + } + } + + _selectingDirectory = dialog is OpenFolderDialog; + + Navigate(directory, (dialog as FileDialog)?.InitialFileName); + SelectedItems.CollectionChanged += OnSelectionChanged; + } + + private async void OnSelectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if(_scheduledSelectionValidation) + return; + _scheduledSelectionValidation = true; + await Dispatcher.UIThread.InvokeAsync(() => + { + try + { + if(_selectingDirectory) + SelectedItems.Clear(); + else + { + var invalidItems = SelectedItems.Where(i => i.IsDirectory).ToList(); + foreach (var item in invalidItems) + SelectedItems.Remove(item); + } + } + finally + { + _scheduledSelectionValidation = false; + } + }); + } + + void NavigateRoot(string initialSelectionName) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + Navigate(Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.System)), initialSelectionName); + else + Navigate("/", initialSelectionName); + } + + public void Refresh() => Navigate(Location); + + public void Navigate(string path, string initialSelectionName = null) + { + if (!Directory.Exists(path)) + NavigateRoot(initialSelectionName); + else + { + Location = path; + Items.Clear(); + SelectedItems.Clear(); + var infos = new DirectoryInfo(path).EnumerateFileSystemInfos(); + if (!ShowHiddenFiles) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + infos = infos.Where(i => (i.Attributes & (FileAttributes.Hidden | FileAttributes.System)) != 0); + else + infos = infos.Where(i => !i.Name.StartsWith(".")); + } + + if (SelectedFilter != null) + infos = infos.Where(i => i is DirectoryInfo || SelectedFilter.Match(i.Name)); + + Items.AddRange(infos.Select(info => new ManagedFileChooserItemViewModel + { + DisplayName = info.Name, + Path = info.FullName, + IsDirectory = info is DirectoryInfo + }).OrderByDescending(x => x.IsDirectory) + .ThenBy(x => x.DisplayName, StringComparer.InvariantCultureIgnoreCase)); + + if (initialSelectionName != null) + { + var sel = Items.FirstOrDefault(i => !i.IsDirectory && i.DisplayName == initialSelectionName); + if (sel != null) + SelectedItems.Add(sel); + } + + RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); + } + + + } + + public void GoUp() + { + var parent = Path.GetDirectoryName(Location); + if (string.IsNullOrWhiteSpace(parent)) + return; + Navigate(parent); + } + + public void Cancel() + { + CancelRequested?.Invoke(); + } + + public void Ok() + { + if (_selectingDirectory) + CompleteRequested?.Invoke(new[] {Location}); + else + CompleteRequested?.Invoke(SelectedItems.Select(i => i.Path).ToArray()); + } + + public void SelectSingleFile(ManagedFileChooserItemViewModel item) + { + CompleteRequested?.Invoke(new[] {item.Path}); + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml new file mode 100644 index 0000000000..34c8dba363 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml @@ -0,0 +1,7 @@ + + + diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs new file mode 100644 index 0000000000..43276f5e90 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs @@ -0,0 +1,18 @@ +using System; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Dialogs.Internal +{ + class ManagedFileDialog : Window + { + private ManagedFileChooserViewModel _model; + public ManagedFileDialog() + { + AvaloniaXamlLoader.Load(this); + #if DEBUG + this.AttachDevTools(); + #endif + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs new file mode 100644 index 0000000000..a492dfed3a --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data.Converters; + +namespace Avalonia.Dialogs.Internal +{ + public class ResourceSelectorConverter : ResourceDictionary, IValueConverter + { + public object Convert(object key, Type targetType, object parameter, CultureInfo culture) + { + TryGetResource((string)key, out var value); + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs new file mode 100644 index 0000000000..26a11fc4ff --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -0,0 +1,54 @@ +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Dialogs.Internal; +using Avalonia.Platform; + +namespace Avalonia.Dialogs +{ + public static class ManagedFileDialogExtensions + { + class ManagedSystemDialogImpl : ISystemDialogImpl + { + async Task Show(SystemDialog d, IWindowImpl parent) + { + var model = new ManagedFileChooserViewModel((FileSystemDialog)d); + + var dialog = new ManagedFileDialog + { + DataContext = model + }; + + string[] result = null; + model.CompleteRequested += items => + { + result = items; + dialog.Close(); + }; + model.CancelRequested += dialog.Close; + + await dialog.ShowDialog(parent); + return result; + } + + public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + { + return await Show(dialog, parent); + } + + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + return (await Show(dialog, parent))?.FirstOrDefault(); + } + } + + public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) + where TAppBuilder : AppBuilderBase, new() + { + builder.AfterPlatformServicesSetup(_ => + AvaloniaLocator.CurrentMutable.Bind().ToSingleton()); + return builder; + } + } +} From ce92286112282e5cead7fd5f9fc40590c4bb46cc Mon Sep 17 00:00:00 2001 From: JaggerJo Date: Mon, 22 Jul 2019 00:21:43 +0200 Subject: [PATCH 0002/1791] add basic operations as static methods and use them in the declared operators. - Dot - Cross - Normalize - Divide - Multiply - Add - Subtract - Negate --- src/Avalonia.Visuals/Vector.cs | 115 +++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 2f1690184d..f185682dc0 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -65,9 +65,7 @@ namespace Avalonia /// Second vector /// The dot product public static double operator *(Vector a, Vector b) - { - return a.X * b.X + a.Y * b.Y; - } + => Dot(a, b); /// /// Scales a vector. @@ -76,9 +74,7 @@ namespace Avalonia /// The scaling factor. /// The scaled vector. public static Vector operator *(Vector vector, double scale) - { - return new Vector(vector._x * scale, vector._y * scale); - } + => Multiply(vector, scale); /// /// Scales a vector. @@ -87,9 +83,7 @@ namespace Avalonia /// The divisor. /// The scaled vector. public static Vector operator /(Vector vector, double scale) - { - return new Vector(vector._x / scale, vector._y / scale); - } + => Divide(vector, scale); /// /// Length of the vector @@ -102,9 +96,7 @@ namespace Avalonia /// The vector. /// The negated vector. public static Vector operator -(Vector a) - { - return new Vector(-a._x, -a._y); - } + => Negate(a); /// /// Adds two vectors. @@ -113,9 +105,7 @@ namespace Avalonia /// The second vector. /// A vector that is the result of the addition. public static Vector operator +(Vector a, Vector b) - { - return new Vector(a._x + b._x, a._y + b._y); - } + => Add(a, b); /// /// Subtracts two vectors. @@ -124,9 +114,7 @@ namespace Avalonia /// The second vector. /// A vector that is the result of the subtraction. public static Vector operator -(Vector a, Vector b) - { - return new Vector(a._x - b._x, a._y - b._y); - } + => Subtract(a, b); /// /// Check if two vectors are equal (bitwise). @@ -155,7 +143,8 @@ namespace Avalonia public override bool Equals(object obj) { - if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(null, obj)) + return false; return obj is Vector vector && Equals(vector); } @@ -206,5 +195,93 @@ namespace Avalonia { return new Vector(_x, y); } + + /// + /// Returns the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product. + public static double Dot(Vector a, Vector b) + => a._x * b._x + a._y * b._y; + + /// + /// Returns the cross product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The cross product. + public static double Cross(Vector a, Vector b) + => a._x * b._y - a._y * b._x; + + /// + /// Normalizes the given vector. + /// + /// The vector + /// The normalized vector. + public static Vector Normalize(Vector vector) + => Divide(vector, vector.Length); + + /// + /// Divides the first vector by the second. + /// + /// The first vector. + /// The second vector. + /// The scaled vector. + public static Vector Divide(Vector a, Vector b) + => new Vector(a._x / b._x, a._y / b._y); + + /// + /// Divides the vector by the given scalar. + /// + /// The vector + /// The scalar value + /// The scaled vector. + public static Vector Divide(Vector vector, double scalar) + => new Vector(vector._x / scalar, vector._y / scalar); + + /// + /// Multiplies the first vector by the second. + /// + /// The first vector. + /// The second vector. + /// The scaled vector. + public static Vector Multiply(Vector a, Vector b) + => new Vector(a._x * b._x, a._y * b._y); + + /// + /// Multiplies the vector by the given scalar. + /// + /// The vector + /// The scalar value + /// The scaled vector. + public static Vector Multiply(Vector vector, double scalar) + => new Vector(vector._x * scalar, vector._y * scalar); + + /// + /// Adds the second to the first vector + /// + /// The first vector. + /// The second vector. + /// The summed vector. + public static Vector Add(Vector a, Vector b) + => new Vector(a._x + b._x, a._y + b._y); + + /// + /// Subtracts the second from the first vector + /// + /// The first vector. + /// The second vector. + /// The difference vector. + public static Vector Subtract(Vector a, Vector b) + => new Vector(a._x - b._x, a._y - b._y); + + /// + /// Negates the vector + /// + /// The vector to negate. + /// The scaled vector. + public static Vector Negate(Vector vector) + => new Vector(-vector._x, -vector._y); } } From 44803ef9dd17e19fd6b676a08ab3c0b3feabdb9f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 25 Jul 2019 21:41:33 +0100 Subject: [PATCH 0003/1791] back port managed dialog work from wasabi wallet. --- Avalonia.sln | 10 +- build/ReactiveUI.props | 2 +- build/Rx.props | 2 +- src/Avalonia.Dialogs/Avalonia.Dialogs.csproj | 5 +- .../Internal/ByteSizeHelper.cs | 29 + src/Avalonia.Dialogs/Internal/ChildFitter.cs | 27 +- .../Internal/FileSizeStringConverter.cs | 26 + .../Internal/ManagedFileChooser.xaml | 170 ++++-- .../Internal/ManagedFileChooser.xaml.cs | 129 +++-- .../ManagedFileChooserFilterViewModel.cs | 72 ++- .../ManagedFileChooserItemViewModel.cs | 108 ++-- .../ManagedFileChooserNavigationItem.cs | 8 + .../Internal/ManagedFileChooserSources.cs | 134 +++-- .../Internal/ManagedFileChooserViewModel.cs | 532 +++++++++++------- .../Internal/ManagedFileDialog.xaml | 7 +- .../Internal/ManagedFileDialog.xaml.cs | 24 +- .../Internal/ResourceSelectorConverter.cs | 24 +- .../ManagedFileDialogExtensions.cs | 81 +-- 18 files changed, 852 insertions(+), 538 deletions(-) create mode 100644 src/Avalonia.Dialogs/Internal/ByteSizeHelper.cs create mode 100644 src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs diff --git a/Avalonia.sln b/Avalonia.sln index cb9669fe6b..bf7710d7e8 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2027 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29102.190 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" EndProject @@ -197,9 +197,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Build.Tasks", "src\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj", "{BF28998D-072C-439A-AFBB-2FE5021241E0}" -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props index 1208be34b8..3baf83f6b5 100644 --- a/build/ReactiveUI.props +++ b/build/ReactiveUI.props @@ -1,5 +1,5 @@ - + diff --git a/build/Rx.props b/build/Rx.props index 359ce53a92..d40849686f 100644 --- a/build/Rx.props +++ b/build/Rx.props @@ -1,5 +1,5 @@  - + diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index d5c6b1d920..b94b1db8bc 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 false @@ -13,6 +13,9 @@ + + + diff --git a/src/Avalonia.Dialogs/Internal/ByteSizeHelper.cs b/src/Avalonia.Dialogs/Internal/ByteSizeHelper.cs new file mode 100644 index 0000000000..3517f760b4 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ByteSizeHelper.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Dialogs.Internal +{ + public static class ByteSizeHelper + { + private static readonly string[] Prefixes = + { + "B", + "KB", + "MB", + "GB", + "TB" + }; + + public static string ToString(long bytes) + { + var index = 0; + while (bytes >= 1000) + { + bytes /= 1000; + ++index; + } + return $"{bytes:N} {Prefixes[index]}"; + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ChildFitter.cs b/src/Avalonia.Dialogs/Internal/ChildFitter.cs index d81dafcd99..6f9baff82f 100644 --- a/src/Avalonia.Dialogs/Internal/ChildFitter.cs +++ b/src/Avalonia.Dialogs/Internal/ChildFitter.cs @@ -1,20 +1,21 @@ +using Avalonia; using Avalonia.Controls; using Avalonia.Layout; namespace Avalonia.Dialogs.Internal { - class ChildFitter : Decorator - { - protected override Size MeasureOverride(Size availableSize) - { - return new Size(0, 0); - } + class ChildFitter : Decorator + { + protected override Size MeasureOverride(Size availableSize) + { + return new Size(0, 0); + } - protected override Size ArrangeOverride(Size finalSize) - { - Child.Measure(finalSize); - base.ArrangeOverride(finalSize); - return finalSize; - } - } + protected override Size ArrangeOverride(Size finalSize) + { + Child.Measure(finalSize); + base.ArrangeOverride(finalSize); + return finalSize; + } + } } diff --git a/src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs b/src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs new file mode 100644 index 0000000000..6de931c89d --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs @@ -0,0 +1,26 @@ +using Avalonia.Data.Converters; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace Avalonia.Dialogs.Internal +{ + public class FileSizeStringConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if(value is long size && size > 0) + { + return ByteSizeHelper.ToString(size); + } + + return ""; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml index 51154f1161..cb7f2bef34 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml @@ -4,71 +4,127 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:dialogs="clr-namespace:Avalonia.Dialogs.Internal" xmlns:internal="clr-namespace:Avalonia.Dialogs.Internal"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Show hidden files - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Show hidden files + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SelectedItems="{Binding SelectedItems}" + ScrollViewer.HorizontalScrollBarVisibility="Disabled"> + + + + + + + + + + + + + + + + + + + + + + + - + + diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs index dd23a90922..7aba87115d 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -9,57 +10,79 @@ using Avalonia.Markup.Xaml; namespace Avalonia.Dialogs.Internal { - class ManagedFileChooser : UserControl - { - private Control _quickLinksRoot; - private ListBox _filesView; - - public ManagedFileChooser() - { - AvaloniaXamlLoader.Load(this); - AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel); - _quickLinksRoot = this.FindControl("QuickLinks"); - _filesView = this.FindControl("Files"); - } - - ManagedFileChooserViewModel Model => DataContext as ManagedFileChooserViewModel; - - private void OnPointerPressed(object sender, PointerPressedEventArgs e) - { - var model = (e.Source as StyledElement)?.DataContext as ManagedFileChooserItemViewModel; - if(model == null) - return; - - var isQuickLink = _quickLinksRoot.IsLogicalParentOf(e.Source as Control); - if (e.ClickCount == 2 || isQuickLink) - { - if (model.IsDirectory) - Model?.Navigate(model.Path); - else - Model?.SelectSingleFile(model); - e.Handled = true; - } - } - - protected override async void OnDataContextChanged(EventArgs e) - { - base.OnDataContextChanged(e); - var model = (DataContext as ManagedFileChooserViewModel); - if (model == null) - return; - var preselected = model.SelectedItems.FirstOrDefault(); - if(preselected == null) - return; - - //Let everything to settle down and scroll to selected item - await Task.Delay(100); - if (preselected != model.SelectedItems.FirstOrDefault()) - return; - - // Workaround for ListBox bug, scroll to the previous file - var indexOfPreselected = model.Items.IndexOf(preselected); - if (indexOfPreselected > 1) - _filesView.ScrollIntoView(model.Items[indexOfPreselected - 1]); - } - } + class ManagedFileChooser : UserControl + { + private Control _quickLinksRoot; + private ListBox _filesView; + + public ManagedFileChooser() + { + AvaloniaXamlLoader.Load(this); + AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel); + _quickLinksRoot = this.FindControl("QuickLinks"); + _filesView = this.FindControl("Files"); + } + + ManagedFileChooserViewModel Model => DataContext as ManagedFileChooserViewModel; + + private void OnPointerPressed(object sender, PointerPressedEventArgs e) + { + var model = (e.Source as StyledElement)?.DataContext as ManagedFileChooserItemViewModel; + + if (model == null) + { + return; + } + + var isQuickLink = _quickLinksRoot.IsLogicalParentOf(e.Source as Control); + if (e.ClickCount == 2 || isQuickLink) + { + if (model.IsDirectory) + { + Model?.Navigate(model.Path); + } + else + { + Model?.SelectSingleFile(model); + } + + e.Handled = true; + } + } + + protected override async void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + var model = (DataContext as ManagedFileChooserViewModel); + + if (model == null) + { + return; + } + + var preselected = model.SelectedItems.FirstOrDefault(); + + if (preselected == null) + { + return; + } + + //Let everything to settle down and scroll to selected item + await Task.Delay(100); + + if (preselected != model.SelectedItems.FirstOrDefault()) + { + return; + } + + // Workaround for ListBox bug, scroll to the previous file + var indexOfPreselected = model.Items.IndexOf(preselected); + + if (indexOfPreselected > 1) + { + _filesView.ScrollIntoView(model.Items[indexOfPreselected - 1]); + } + } + } } diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs index 1f561d0cd2..d54945ac60 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs @@ -5,34 +5,46 @@ using Avalonia.Controls; namespace Avalonia.Dialogs.Internal { - class ManagedFileChooserFilterViewModel : InternalViewModelBase - { - private readonly string[] _extensions; - public string Name { get; } - - public ManagedFileChooserFilterViewModel(FileDialogFilter filter) - { - Name = filter.Name; - if (filter.Extensions.Contains("*")) - return; - _extensions = filter.Extensions?.Select(e => "." + e.ToLowerInvariant()).ToArray(); - } - - public ManagedFileChooserFilterViewModel() - { - Name = "All files"; - } - - public bool Match(string filename) - { - if (_extensions == null) - return true; - foreach(var ext in _extensions) - if (filename.EndsWith(ext, StringComparison.InvariantCultureIgnoreCase)) - return true; - return false; - } - - public override string ToString() => Name; - } + class ManagedFileChooserFilterViewModel : InternalViewModelBase + { + private readonly string[] _extensions; + public string Name { get; } + + public ManagedFileChooserFilterViewModel(FileDialogFilter filter) + { + Name = filter.Name; + + if (filter.Extensions.Contains("*")) + { + return; + } + + _extensions = filter.Extensions?.Select(e => "." + e.ToLowerInvariant()).ToArray(); + } + + public ManagedFileChooserFilterViewModel() + { + Name = "All files"; + } + + public bool Match(string filename) + { + if (_extensions == null) + { + return true; + } + + foreach (var ext in _extensions) + { + if (filename.EndsWith(ext, StringComparison.InvariantCultureIgnoreCase)) + { + return true; + } + } + + return false; + } + + public override string ToString() => Name; + } } diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs index d5f72cdbac..b6535a93d0 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs @@ -1,45 +1,69 @@ +using System; + namespace Avalonia.Dialogs.Internal { - class ManagedFileChooserItemViewModel : InternalViewModelBase - { - private string _displayName; - private string _path; - private bool _isDirectory; - - public string DisplayName - { - get => _displayName; - set => RaiseAndSetIfChanged(ref _displayName, value); - } - - public string Path - { - get => _path; - set => RaiseAndSetIfChanged(ref _path, value); - } - - public string IconKey => IsDirectory ? "Icon_Folder" : "Icon_File"; - - public bool IsDirectory - { - get => _isDirectory; - set - { - if (RaiseAndSetIfChanged(ref _isDirectory, value)) - RaisePropertyChanged(nameof(IconKey)); - } - } - - public ManagedFileChooserItemViewModel() - { - - } - - public ManagedFileChooserItemViewModel(ManagedFileChooserNavigationItem item) - { - IsDirectory = true; - Path = item.Path; - DisplayName = item.DisplayName; - } - } + class ManagedFileChooserItemViewModel : InternalViewModelBase + { + private string _displayName; + private string _path; + private bool _isDirectory; + private DateTime _modified; + private string _type; + private long _size; + + public string DisplayName + { + get => _displayName; + set => this.RaiseAndSetIfChanged(ref _displayName, value); + } + + public string Path + { + get => _path; + set => this.RaiseAndSetIfChanged(ref _path, value); + } + + public DateTime Modified + { + get => _modified; + set => this.RaiseAndSetIfChanged(ref _modified, value); + } + + public string Type + { + get => _type; + set => this.RaiseAndSetIfChanged(ref _type, value); + } + + public long Size + { + get => _size; + set => this.RaiseAndSetIfChanged(ref _size, value); + } + + public string IconKey => IsDirectory ? "Icon_Folder" : "Icon_File"; + + public bool IsDirectory + { + get => _isDirectory; + set + { + if (this.RaiseAndSetIfChanged(ref _isDirectory, value)) + { + this.RaisePropertyChanged(nameof(IconKey)); + } + } + } + + public ManagedFileChooserItemViewModel() + { + } + + public ManagedFileChooserItemViewModel(ManagedFileChooserNavigationItem item) + { + IsDirectory = true; + Path = item.Path; + DisplayName = item.DisplayName; + } + } } diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs new file mode 100644 index 0000000000..b04a36692c --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs @@ -0,0 +1,8 @@ +namespace Avalonia.Dialogs.Internal +{ + public class ManagedFileChooserNavigationItem + { + public string DisplayName { get; set; } + public string Path { get; set; } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs index 587b32fe03..84a3173eac 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs @@ -5,71 +5,85 @@ using System.Runtime.InteropServices; namespace Avalonia.Dialogs.Internal { - public class ManagedFileChooserSources - { - public Func GetUserDirectories { get; set; } - = DefaultGetUserDirectories; + public class ManagedFileChooserSources + { + public Func GetUserDirectories { get; set; } + = DefaultGetUserDirectories; - public Func GetFileSystemRoots { get; set; } - = DefaultGetFileSystemRoots; + public Func GetFileSystemRoots { get; set; } + = DefaultGetFileSystemRoots; - public Func GetAllItemsDelegate { get; set; } - = DefaultGetAllItems; + public Func GetAllItemsDelegate { get; set; } + = DefaultGetAllItems; - public ManagedFileChooserNavigationItem[] GetAllItems() => GetAllItemsDelegate(this); + public ManagedFileChooserNavigationItem[] GetAllItems() => GetAllItemsDelegate(this); - public static ManagedFileChooserNavigationItem[] DefaultGetAllItems(ManagedFileChooserSources sources) - { - return sources.GetUserDirectories().Concat(sources.GetFileSystemRoots()).ToArray(); - } - - private static Environment.SpecialFolder[] s_folders = new[] - { - Environment.SpecialFolder.Desktop, - Environment.SpecialFolder.UserProfile, - Environment.SpecialFolder.MyDocuments, - Environment.SpecialFolder.MyMusic, - Environment.SpecialFolder.MyPictures, - Environment.SpecialFolder.MyVideos - }; - - public static ManagedFileChooserNavigationItem[] DefaultGetUserDirectories() - { - return s_folders.Select(Environment.GetFolderPath).Distinct() - .Where(d => !string.IsNullOrWhiteSpace(d)) - .Where(Directory.Exists) - .Select(d => new ManagedFileChooserNavigationItem - { - Path = d, - DisplayName = Path.GetFileName(d) - }).ToArray(); - } + public static ManagedFileChooserNavigationItem[] DefaultGetAllItems(ManagedFileChooserSources sources) + { + return sources.GetUserDirectories().Concat(sources.GetFileSystemRoots()).ToArray(); + } - public static ManagedFileChooserNavigationItem[] DefaultGetFileSystemRoots() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return DriveInfo.GetDrives().Select(d => new ManagedFileChooserNavigationItem - { - DisplayName = d.Name, - Path = d.RootDirectory.FullName - }).ToArray(); - } + private static Environment.SpecialFolder[] s_folders = new[] + { + Environment.SpecialFolder.Desktop, + Environment.SpecialFolder.UserProfile, + Environment.SpecialFolder.MyDocuments, + Environment.SpecialFolder.MyMusic, + Environment.SpecialFolder.MyPictures, + Environment.SpecialFolder.MyVideos + }; - return new[] - { - new ManagedFileChooserNavigationItem - { - DisplayName = "File System", - Path = "/" - } - }; - } - } + public static ManagedFileChooserNavigationItem[] DefaultGetUserDirectories() + { + return s_folders.Select(Environment.GetFolderPath).Distinct() + .Where(d => !string.IsNullOrWhiteSpace(d)) + .Where(Directory.Exists) + .Select(d => new ManagedFileChooserNavigationItem + { + Path = d, + DisplayName = Path.GetFileName(d) + }).ToArray(); + } - public class ManagedFileChooserNavigationItem - { - public string DisplayName { get; set; } - public string Path { get; set; } - } + public static ManagedFileChooserNavigationItem[] DefaultGetFileSystemRoots() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return DriveInfo.GetDrives().Select(d => new ManagedFileChooserNavigationItem + { + DisplayName = d.Name, + Path = d.RootDirectory.FullName + }).ToArray(); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var paths = Directory.GetDirectories("/Volumes"); + + return paths.Select(x => new ManagedFileChooserNavigationItem + { + DisplayName = Path.GetFileName(x), + Path = x + }).ToArray(); + } + else + { + var paths = Directory.GetDirectories("/media/"); + + var drives = new ManagedFileChooserNavigationItem[] + { + new ManagedFileChooserNavigationItem + { + DisplayName = "File System", + Path = "/" + } + }.Concat(paths.Select(x => new ManagedFileChooserNavigationItem + { + DisplayName = Path.GetFileName(x), + Path = x + })).ToArray(); + + return drives; + } + } + } } diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs index 72bff302bd..8f557a212d 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs @@ -2,222 +2,334 @@ using System; using System.Collections.Specialized; using System.IO; using System.Linq; +using System.Reactive; using System.Runtime.InteropServices; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Threading; +using ReactiveUI; namespace Avalonia.Dialogs.Internal { class ManagedFileChooserViewModel : InternalViewModelBase - { - public event Action CancelRequested; - public event Action CompleteRequested; - - public AvaloniaList QuickLinks { get; } = - new AvaloniaList(); - - public AvaloniaList Items { get; } = - new AvaloniaList(); - - public AvaloniaList Filters { get; } = - new AvaloniaList(); - - public AvaloniaList SelectedItems { get; } = - new AvaloniaList(); - - string _location; - private bool _showHiddenFiles; - private ManagedFileChooserFilterViewModel _selectedFilter; - private bool _selectingDirectory; - private bool _scheduledSelectionValidation; - - public string Location - { - get => _location; - private set => RaiseAndSetIfChanged(ref _location, value); - } - - public bool ShowFilters { get; } - public SelectionMode SelectionMode { get; } - public string Title { get; } - - public int QuickLinksSelectedIndex - { - get - { - for (var index = 0; index < QuickLinks.Count; index++) - { - var i = QuickLinks[index]; - if (i.Path == Location) - return index; - } - - return -1; - } - set => RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); - } - - public ManagedFileChooserFilterViewModel SelectedFilter - { - get => _selectedFilter; - set - { - RaiseAndSetIfChanged(ref _selectedFilter, value); - Refresh(); - } - } - - public bool ShowHiddenFiles - { - get => _showHiddenFiles; - set - { - RaiseAndSetIfChanged(ref _showHiddenFiles, value); - Refresh(); - } - } - - public ManagedFileChooserViewModel(FileSystemDialog dialog) - { - var quickSources = AvaloniaLocator.Current.GetService() - ?? new ManagedFileChooserSources(); - QuickLinks.Clear(); - - QuickLinks.AddRange(quickSources.GetAllItems().Select(i => new ManagedFileChooserItemViewModel(i))); - Title = dialog.Title ?? ( - dialog is OpenFileDialog ? "Open file" - : dialog is SaveFileDialog ? "Save file" - : dialog is OpenFolderDialog ? "Select directory" - : throw new ArgumentException(nameof(dialog))); - - var directory = dialog.Directory; - if (directory == null || !Directory.Exists(directory)) - directory = Directory.GetCurrentDirectory(); - - if (dialog is FileDialog fd) - { - if (fd.Filters?.Count > 0) - { - Filters.AddRange(fd.Filters.Select(f => new ManagedFileChooserFilterViewModel(f))); - _selectedFilter = Filters[0]; - ShowFilters = true; - } - - if (dialog is OpenFileDialog ofd) - { - if (ofd.AllowMultiple) - SelectionMode = SelectionMode.Multiple; - } - } - - _selectingDirectory = dialog is OpenFolderDialog; - - Navigate(directory, (dialog as FileDialog)?.InitialFileName); - SelectedItems.CollectionChanged += OnSelectionChanged; - } - - private async void OnSelectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if(_scheduledSelectionValidation) - return; - _scheduledSelectionValidation = true; - await Dispatcher.UIThread.InvokeAsync(() => - { - try - { - if(_selectingDirectory) - SelectedItems.Clear(); - else - { - var invalidItems = SelectedItems.Where(i => i.IsDirectory).ToList(); - foreach (var item in invalidItems) - SelectedItems.Remove(item); - } - } - finally - { - _scheduledSelectionValidation = false; - } - }); - } - - void NavigateRoot(string initialSelectionName) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - Navigate(Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.System)), initialSelectionName); - else - Navigate("/", initialSelectionName); - } - - public void Refresh() => Navigate(Location); - - public void Navigate(string path, string initialSelectionName = null) - { - if (!Directory.Exists(path)) - NavigateRoot(initialSelectionName); - else - { - Location = path; - Items.Clear(); - SelectedItems.Clear(); - var infos = new DirectoryInfo(path).EnumerateFileSystemInfos(); - if (!ShowHiddenFiles) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - infos = infos.Where(i => (i.Attributes & (FileAttributes.Hidden | FileAttributes.System)) != 0); - else - infos = infos.Where(i => !i.Name.StartsWith(".")); - } - - if (SelectedFilter != null) - infos = infos.Where(i => i is DirectoryInfo || SelectedFilter.Match(i.Name)); - - Items.AddRange(infos.Select(info => new ManagedFileChooserItemViewModel - { - DisplayName = info.Name, - Path = info.FullName, - IsDirectory = info is DirectoryInfo - }).OrderByDescending(x => x.IsDirectory) - .ThenBy(x => x.DisplayName, StringComparer.InvariantCultureIgnoreCase)); - - if (initialSelectionName != null) - { - var sel = Items.FirstOrDefault(i => !i.IsDirectory && i.DisplayName == initialSelectionName); - if (sel != null) - SelectedItems.Add(sel); - } - - RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); - } - - - } - - public void GoUp() - { - var parent = Path.GetDirectoryName(Location); - if (string.IsNullOrWhiteSpace(parent)) - return; - Navigate(parent); - } - - public void Cancel() - { - CancelRequested?.Invoke(); - } - - public void Ok() - { - if (_selectingDirectory) - CompleteRequested?.Invoke(new[] {Location}); - else - CompleteRequested?.Invoke(SelectedItems.Select(i => i.Path).ToArray()); - } - - public void SelectSingleFile(ManagedFileChooserItemViewModel item) - { - CompleteRequested?.Invoke(new[] {item.Path}); - } - } + { + public event Action CancelRequested; + public event Action CompleteRequested; + + public AvaloniaList QuickLinks { get; } = + new AvaloniaList(); + + public AvaloniaList Items { get; } = + new AvaloniaList(); + + public AvaloniaList Filters { get; } = + new AvaloniaList(); + + public AvaloniaList SelectedItems { get; } = + new AvaloniaList(); + + string _location; + string _fileName; + private bool _showHiddenFiles; + private ManagedFileChooserFilterViewModel _selectedFilter; + private bool _selectingDirectory; + private bool _savingFile; + private bool _scheduledSelectionValidation; + private string _defaultExtension; + + public string Location + { + get => _location; + private set => this.RaiseAndSetIfChanged(ref _location, value); + } + + public string FileName + { + get => _fileName; + private set => this.RaiseAndSetIfChanged(ref _fileName, value); + } + + public bool SelectingFolder => _selectingDirectory; + + public bool ShowFilters { get; } + public SelectionMode SelectionMode { get; } + public string Title { get; } + + public int QuickLinksSelectedIndex + { + get + { + for (var index = 0; index < QuickLinks.Count; index++) + { + var i = QuickLinks[index]; + + if (i.Path == Location) + { + return index; + } + } + + return -1; + } + set => this.RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); + } + + public ManagedFileChooserFilterViewModel SelectedFilter + { + get => _selectedFilter; + set + { + this.RaiseAndSetIfChanged(ref _selectedFilter, value); + Refresh(); + } + } + + public bool ShowHiddenFiles + { + get => _showHiddenFiles; + set + { + this.RaiseAndSetIfChanged(ref _showHiddenFiles, value); + Refresh(); + } + } + + public ManagedFileChooserViewModel(FileSystemDialog dialog) + { + var quickSources = AvaloniaLocator.Current.GetService() + ?? new ManagedFileChooserSources(); + + QuickLinks.Clear(); + + QuickLinks.AddRange(quickSources.GetAllItems().Select(i => new ManagedFileChooserItemViewModel(i))); + + Title = dialog.Title ?? ( + dialog is OpenFileDialog ? "Open file" + : dialog is SaveFileDialog ? "Save file" + : dialog is OpenFolderDialog ? "Select directory" + : throw new ArgumentException(nameof(dialog))); + + var directory = dialog.InitialDirectory; + + if (directory == null || !Directory.Exists(directory)) + { + directory = Directory.GetCurrentDirectory(); + } + + if (dialog is FileDialog fd) + { + if (fd.Filters?.Count > 0) + { + Filters.AddRange(fd.Filters.Select(f => new ManagedFileChooserFilterViewModel(f))); + _selectedFilter = Filters[0]; + ShowFilters = true; + } + + if (dialog is OpenFileDialog ofd) + { + if (ofd.AllowMultiple) + { + SelectionMode = SelectionMode.Multiple; + } + } + } + + _selectingDirectory = dialog is OpenFolderDialog; + + if(dialog is SaveFileDialog sfd) + { + _savingFile = true; + _defaultExtension = sfd.DefaultExtension; + FileName = sfd.InitialFileName; + } + + Navigate(directory, (dialog as FileDialog)?.InitialFileName); + SelectedItems.CollectionChanged += OnSelectionChangedAsync; + + EnterLocationCommand = ReactiveCommand.Create(() => + { + if (Directory.Exists(Location)) + { + Navigate(Location); + } + else if (File.Exists(Location)) + { + CompleteRequested?.Invoke(new[] { Location }); + } + }); + } + + private async void OnSelectionChangedAsync(object sender, NotifyCollectionChangedEventArgs e) + { + if (_scheduledSelectionValidation) + { + return; + } + + _scheduledSelectionValidation = true; + await Dispatcher.UIThread.InvokeAsync(() => + { + try + { + if (_selectingDirectory) + { + SelectedItems.Clear(); + } + else + { + var invalidItems = SelectedItems.Where(i => i.IsDirectory).ToList(); + foreach (var item in invalidItems) + { + SelectedItems.Remove(item); + } + + if(!_selectingDirectory) + { + FileName = SelectedItems.FirstOrDefault()?.DisplayName; + } + } + } + finally + { + _scheduledSelectionValidation = false; + } + }); + } + + void NavigateRoot(string initialSelectionName) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Navigate(Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.System)), initialSelectionName); + } + else + { + Navigate("/", initialSelectionName); + } + } + + public void Refresh() => Navigate(Location); + + public void Navigate(string path, string initialSelectionName = null) + { + if (!Directory.Exists(path)) + { + NavigateRoot(initialSelectionName); + } + else + { + Location = path; + Items.Clear(); + SelectedItems.Clear(); + + try + { + var infos = new DirectoryInfo(path).EnumerateFileSystemInfos(); + + if (!ShowHiddenFiles) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + infos = infos.Where(i => (i.Attributes & (FileAttributes.Hidden | FileAttributes.System)) != 0); + } + else + { + infos = infos.Where(i => !i.Name.StartsWith(".")); + } + } + + if (SelectedFilter != null) + { + infos = infos.Where(i => i is DirectoryInfo || SelectedFilter.Match(i.Name)); + } + + Items.AddRange(infos.Where(x => + { + if (_selectingDirectory) + { + if (!(x is DirectoryInfo)) + { + return false; + } + } + + return true; + }).Select(info => new ManagedFileChooserItemViewModel + { + DisplayName = info.Name, + Path = info.FullName, + IsDirectory = info is DirectoryInfo, + Type = info is FileInfo ? info.Extension : "File Folder", + Size = info is FileInfo f ? f.Length : 0, + Modified = info.LastWriteTime + }) + .OrderByDescending(x => x.IsDirectory) + .ThenBy(x => x.DisplayName, StringComparer.InvariantCultureIgnoreCase)); + + if (initialSelectionName != null) + { + var sel = Items.FirstOrDefault(i => !i.IsDirectory && i.DisplayName == initialSelectionName); + + if (sel != null) + { + SelectedItems.Add(sel); + } + } + + this.RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); + } + catch (System.UnauthorizedAccessException) + { + } + } + } + + public void GoUp() + { + var parent = Path.GetDirectoryName(Location); + + if (string.IsNullOrWhiteSpace(parent)) + { + return; + } + + Navigate(parent); + } + + public void Cancel() + { + CancelRequested?.Invoke(); + } + + public void Ok() + { + if (_selectingDirectory) + { + CompleteRequested?.Invoke(new[] { Location }); + } + else if(_savingFile) + { + if (!string.IsNullOrWhiteSpace(FileName)) + { + if (!Path.HasExtension(FileName) && !string.IsNullOrWhiteSpace(_defaultExtension)) + { + FileName = Path.ChangeExtension(FileName, _defaultExtension); + } + + CompleteRequested?.Invoke(new[] { Path.Combine(Location, FileName) }); + } + } + else + { + CompleteRequested?.Invoke(SelectedItems.Select(i => i.Path).ToArray()); + } + } + + public void SelectSingleFile(ManagedFileChooserItemViewModel item) + { + CompleteRequested?.Invoke(new[] { item.Path }); + } + + public ReactiveCommand EnterLocationCommand { get; } + } } diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml index 34c8dba363..002f35ae24 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml +++ b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml @@ -2,6 +2,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:dialogs="clr-namespace:Avalonia.Dialogs.Internal" xmlns:internal="clr-namespace:Avalonia.Dialogs.Internal" - Title="{Binding Title}" MinWidth="700" MinHeight="500"> - + Icon="resm:WalletWasabi.Gui.Assets.WasabiLogo256.png?assembly=WalletWasabi.Gui" + Title="{Binding Title}" Width="1100" Height="500" MinWidth="1100" MinHeight="500" + FontFamily="{DynamicResource UiFont}" FontSize="14" + Foreground="{DynamicResource ThemeForegroundBrush}"> + diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs index 43276f5e90..34b0163808 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs @@ -1,18 +1,20 @@ using System; +using System.Runtime.InteropServices; +using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Media; namespace Avalonia.Dialogs.Internal { - class ManagedFileDialog : Window - { - private ManagedFileChooserViewModel _model; - public ManagedFileDialog() - { - AvaloniaXamlLoader.Load(this); - #if DEBUG - this.AttachDevTools(); - #endif - } - } + class ManagedFileDialog : Window + { + public ManagedFileDialog() + { + AvaloniaXamlLoader.Load(this); +#if DEBUG + this.AttachDevTools(); +#endif + } + } } diff --git a/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs index a492dfed3a..8346ba952c 100644 --- a/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs +++ b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs @@ -5,17 +5,17 @@ using Avalonia.Data.Converters; namespace Avalonia.Dialogs.Internal { - public class ResourceSelectorConverter : ResourceDictionary, IValueConverter - { - public object Convert(object key, Type targetType, object parameter, CultureInfo culture) - { - TryGetResource((string)key, out var value); - return value; - } + public class ResourceSelectorConverter : ResourceDictionary, IValueConverter + { + public object Convert(object key, Type targetType, object parameter, CultureInfo culture) + { + TryGetResource((string)key, out var value); + return value; + } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } - } + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } } diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index 26a11fc4ff..8c0aba5942 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Dialogs.Internal; @@ -7,48 +8,48 @@ using Avalonia.Platform; namespace Avalonia.Dialogs { - public static class ManagedFileDialogExtensions - { - class ManagedSystemDialogImpl : ISystemDialogImpl - { - async Task Show(SystemDialog d, IWindowImpl parent) - { - var model = new ManagedFileChooserViewModel((FileSystemDialog)d); - - var dialog = new ManagedFileDialog - { - DataContext = model - }; + public static class ManagedFileDialogExtensions + { + class ManagedSystemDialogImpl : ISystemDialogImpl + { + async Task Show(SystemDialog d, IWindowImpl parent) + { + var model = new ManagedFileChooserViewModel((FileSystemDialog)d); - string[] result = null; - model.CompleteRequested += items => - { - result = items; - dialog.Close(); - }; - model.CancelRequested += dialog.Close; + var dialog = new ManagedFileDialog + { + DataContext = model + }; - await dialog.ShowDialog(parent); - return result; - } - - public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) - { - return await Show(dialog, parent); - } + string[] result = null; + model.CompleteRequested += items => + { + result = items; + dialog.Close(); + }; + model.CancelRequested += dialog.Close; - public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) - { - return (await Show(dialog, parent))?.FirstOrDefault(); - } - } + await dialog.ShowDialog(parent); + return result; + } - public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() - { - builder.AfterPlatformServicesSetup(_ => - AvaloniaLocator.CurrentMutable.Bind().ToSingleton()); - return builder; - } - } + public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + { + return await Show(dialog, parent); + } + + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + return (await Show(dialog, parent))?.FirstOrDefault(); + } + } + + public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) + where TAppBuilder : AppBuilderBase, new() + { + builder.AfterSetup(_ => + AvaloniaLocator.CurrentMutable.Bind().ToSingleton()); + return builder; + } + } } From 5cd0a43cbcae67130193ae5e70e1e5fdf055c6ee Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 25 Jul 2019 21:59:27 +0100 Subject: [PATCH 0004/1791] fix managed file dialog demo --- samples/ControlCatalog.NetCore/Program.cs | 18 +++++++++--------- samples/ControlCatalog/MainWindow.xaml.cs | 17 +++++++++++++++++ src/Avalonia.Dialogs/Avalonia.Dialogs.csproj | 7 ++----- .../Internal/ManagedFileChooser.xaml | 3 ++- .../Internal/ManagedFileDialog.xaml | 2 +- 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 0475e2b660..15ecfa5811 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -1,14 +1,15 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.LinuxFramebuffer.Output; using Avalonia.Skia; using Avalonia.ReactiveUI; +using Avalonia.Dialogs; +using System.Collections.Generic; +using System.Threading.Tasks; namespace ControlCatalog.NetCore { @@ -18,13 +19,10 @@ namespace ControlCatalog.NetCore static int Main(string[] args) { Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA); - var b = BuildAvaloniaApp(); - b.SetupWithoutStarting(); - var window = new Window(); - window.Show(); - new OpenFileDialog() + if (args.Contains("--wait-for-attach")) { - Filters = new List + Console.WriteLine("Attach debugger and use 'Set next statement'"); + while (true) { Thread.Sleep(100); if (Debugger.IsAttached) @@ -33,6 +31,7 @@ namespace ControlCatalog.NetCore } var builder = BuildAvaloniaApp(); + if (args.Contains("--fbdev")) { SilenceConsole(); @@ -60,7 +59,8 @@ namespace ControlCatalog.NetCore AllowEglInitialization = true }) .UseSkia() - .UseReactiveUI(); + .UseReactiveUI() + .UseManagedSystemDialogs(); static void SilenceConsole() { diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 91d9f034a5..ade5e4c2fc 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -6,6 +6,7 @@ using Avalonia.Markup.Xaml; using Avalonia.Threading; using ControlCatalog.ViewModels; using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace ControlCatalog @@ -28,6 +29,22 @@ namespace ControlCatalog }; DataContext = new MainWindowViewModel(_notificationArea); + + Dispatcher.UIThread.Post(() => + { + new OpenFileDialog() + { + Filters = new List + { + new FileDialogFilter {Name = "All files", Extensions = {"*"}}, + new FileDialogFilter {Name = "Image files", Extensions = {"jpg", "png", "gif"}} + }, + Directory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + Title = "My dialog", + InitialFileName = "config.local.json", + AllowMultiple = true + }.ShowAsync(this); + }); } private void InitializeComponent() diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index b94b1db8bc..5b3e27eebd 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -5,12 +5,9 @@ - + Designer - - - Designer - + diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml index cb7f2bef34..d048d45dbd 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml @@ -3,7 +3,8 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:dialogs="clr-namespace:Avalonia.Dialogs.Internal" - xmlns:internal="clr-namespace:Avalonia.Dialogs.Internal"> + xmlns:internal="clr-namespace:Avalonia.Dialogs.Internal" + x:Class="Avalonia.Dialogs.Internal.ManagedFileChooser"> diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml index 002f35ae24..15fd8fcddb 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml +++ b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:dialogs="clr-namespace:Avalonia.Dialogs.Internal" xmlns:internal="clr-namespace:Avalonia.Dialogs.Internal" - Icon="resm:WalletWasabi.Gui.Assets.WasabiLogo256.png?assembly=WalletWasabi.Gui" + x:Class="Avalonia.Dialogs.Internal.ManagedFileDialog" Title="{Binding Title}" Width="1100" Height="500" MinWidth="1100" MinHeight="500" FontFamily="{DynamicResource UiFont}" FontSize="14" Foreground="{DynamicResource ThemeForegroundBrush}"> From fc1e033cc424276b17c1036935291f34ab247864 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 25 Jul 2019 22:14:09 +0100 Subject: [PATCH 0005/1791] remove need for reactiveCommand. --- src/Avalonia.Dialogs/Avalonia.Dialogs.csproj | 1 + .../Internal/ManagedFileChooser.xaml | 2 +- .../Internal/ManagedFileChooserViewModel.cs | 26 +++++++++---------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index 5b3e27eebd..afe0d48f6d 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml index d048d45dbd..24222600ef 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml @@ -39,7 +39,7 @@ - + diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs index 8f557a212d..aa81507fc1 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs @@ -145,20 +145,20 @@ namespace Avalonia.Dialogs.Internal Navigate(directory, (dialog as FileDialog)?.InitialFileName); SelectedItems.CollectionChanged += OnSelectionChangedAsync; - - EnterLocationCommand = ReactiveCommand.Create(() => - { - if (Directory.Exists(Location)) - { - Navigate(Location); - } - else if (File.Exists(Location)) - { - CompleteRequested?.Invoke(new[] { Location }); - } - }); } + public void EnterPressed () + { + if (Directory.Exists(Location)) + { + Navigate(Location); + } + else if (File.Exists(Location)) + { + CompleteRequested?.Invoke(new[] { Location }); + } + } + private async void OnSelectionChangedAsync(object sender, NotifyCollectionChangedEventArgs e) { if (_scheduledSelectionValidation) @@ -329,7 +329,5 @@ namespace Avalonia.Dialogs.Internal { CompleteRequested?.Invoke(new[] { item.Path }); } - - public ReactiveCommand EnterLocationCommand { get; } } } From d149a62dd33455dd8fc2bbb618a1b6a37afa9e26 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 25 Jul 2019 22:29:35 +0100 Subject: [PATCH 0006/1791] add grid splitter for resize of columns --- .../Internal/ManagedFileChooser.xaml | 220 +++++++++--------- 1 file changed, 112 insertions(+), 108 deletions(-) diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml index 24222600ef..5e02836173 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml @@ -5,127 +5,131 @@ xmlns:dialogs="clr-namespace:Avalonia.Dialogs.Internal" xmlns:internal="clr-namespace:Avalonia.Dialogs.Internal" x:Class="Avalonia.Dialogs.Internal.ManagedFileChooser"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Show hidden files - - - - - - - - - - - + + + + + + + + + + + + Show hidden files + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - From c685a9e3479e450f77252439dad85b9a3f9e00eb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 27 Jul 2019 10:38:39 +0100 Subject: [PATCH 0007/1791] fix for file dialog crashing with strange file name encoded strings. --- src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs index aa81507fc1..3977874e93 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs @@ -254,7 +254,9 @@ namespace Avalonia.Dialogs.Internal } return true; - }).Select(info => new ManagedFileChooserItemViewModel + }) + .Where(x => x.Exists) + .Select(info => new ManagedFileChooserItemViewModel { DisplayName = info.Name, Path = info.FullName, From f5c85674591f4e89a6a29da9e433b680b517006f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 27 Jul 2019 10:58:27 +0100 Subject: [PATCH 0008/1791] allow use of custom window for hosting file dialogs --- .../Internal/ManagedFileChooser.xaml | 2 +- .../Internal/ManagedFileDialog.xaml | 10 --- .../Internal/ManagedFileDialog.xaml.cs | 20 ----- .../ManagedFileDialogExtensions.cs | 82 +++++++++++-------- 4 files changed, 47 insertions(+), 67 deletions(-) delete mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml delete mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml index 5e02836173..2e694e726d 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml @@ -4,7 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:dialogs="clr-namespace:Avalonia.Dialogs.Internal" xmlns:internal="clr-namespace:Avalonia.Dialogs.Internal" - x:Class="Avalonia.Dialogs.Internal.ManagedFileChooser"> + x:Class="Avalonia.Dialogs.Internal.ManagedFileChooser" Margin="12 0 12 6"> diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml deleted file mode 100644 index 15fd8fcddb..0000000000 --- a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs deleted file mode 100644 index 34b0163808..0000000000 --- a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using Avalonia.Media; - -namespace Avalonia.Dialogs.Internal -{ - class ManagedFileDialog : Window - { - public ManagedFileDialog() - { - AvaloniaXamlLoader.Load(this); -#if DEBUG - this.AttachDevTools(); -#endif - } - } -} diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index 8c0aba5942..864de4b3f6 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Threading.Tasks; using Avalonia; @@ -10,46 +11,55 @@ namespace Avalonia.Dialogs { public static class ManagedFileDialogExtensions { - class ManagedSystemDialogImpl : ISystemDialogImpl - { - async Task Show(SystemDialog d, IWindowImpl parent) - { - var model = new ManagedFileChooserViewModel((FileSystemDialog)d); - - var dialog = new ManagedFileDialog - { - DataContext = model - }; - - string[] result = null; - model.CompleteRequested += items => - { - result = items; - dialog.Close(); - }; - model.CancelRequested += dialog.Close; - - await dialog.ShowDialog(parent); - return result; - } - - public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) - { - return await Show(dialog, parent); - } - - public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) - { - return (await Show(dialog, parent))?.FirstOrDefault(); - } - } + class CustomWindowManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new() + { + async Task Show(SystemDialog d, IWindowImpl parent) + { + var model = new ManagedFileChooserViewModel((FileSystemDialog)d); + + var dialog = new T + { + Content = new ManagedFileChooser(), + DataContext = model + }; + + string[] result = null; + model.CompleteRequested += items => + { + result = items; + dialog.Close(); + }; + model.CancelRequested += dialog.Close; - public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) + await dialog.ShowDialog(parent); + return result; + } + + public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + { + return await Show(dialog, parent); + } + + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + return (await Show(dialog, parent))?.FirstOrDefault(); + } + } + + public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) where TAppBuilder : AppBuilderBase, new() { builder.AfterSetup(_ => - AvaloniaLocator.CurrentMutable.Bind().ToSingleton()); + AvaloniaLocator.CurrentMutable.Bind().ToSingleton>()); return builder; } - } + + public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) + where TAppBuilder : AppBuilderBase, new() where TWindow : Window, new() + { + builder.AfterSetup(_ => + AvaloniaLocator.CurrentMutable.Bind().ToSingleton>()); + return builder; + } + } } From 24ae6ce1ad8bac1de2e2dbf16727262ba7fe313c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 27 Jul 2019 10:59:54 +0100 Subject: [PATCH 0009/1791] [Managed Dialogs] make classes internal --- src/Avalonia.Dialogs/Internal/ByteSizeHelper.cs | 6 +----- src/Avalonia.Dialogs/Internal/ChildFitter.cs | 2 +- src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs | 2 +- src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs | 2 +- src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs | 2 +- .../Internal/ManagedFileChooserFilterViewModel.cs | 2 +- .../Internal/ManagedFileChooserItemViewModel.cs | 2 +- .../Internal/ManagedFileChooserNavigationItem.cs | 2 +- src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs | 2 +- .../Internal/ManagedFileChooserViewModel.cs | 4 +--- src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs | 2 +- 11 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Dialogs/Internal/ByteSizeHelper.cs b/src/Avalonia.Dialogs/Internal/ByteSizeHelper.cs index 3517f760b4..d8035daf40 100644 --- a/src/Avalonia.Dialogs/Internal/ByteSizeHelper.cs +++ b/src/Avalonia.Dialogs/Internal/ByteSizeHelper.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Text; - namespace Avalonia.Dialogs.Internal { - public static class ByteSizeHelper + internal static class ByteSizeHelper { private static readonly string[] Prefixes = { diff --git a/src/Avalonia.Dialogs/Internal/ChildFitter.cs b/src/Avalonia.Dialogs/Internal/ChildFitter.cs index 6f9baff82f..5d0f89f3ac 100644 --- a/src/Avalonia.Dialogs/Internal/ChildFitter.cs +++ b/src/Avalonia.Dialogs/Internal/ChildFitter.cs @@ -4,7 +4,7 @@ using Avalonia.Layout; namespace Avalonia.Dialogs.Internal { - class ChildFitter : Decorator + internal class ChildFitter : Decorator { protected override Size MeasureOverride(Size availableSize) { diff --git a/src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs b/src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs index 6de931c89d..e526932829 100644 --- a/src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs +++ b/src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs @@ -6,7 +6,7 @@ using System.Text; namespace Avalonia.Dialogs.Internal { - public class FileSizeStringConverter : IValueConverter + internal class FileSizeStringConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { diff --git a/src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs b/src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs index 35e37eb810..2a8ae67ce1 100644 --- a/src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs +++ b/src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs @@ -5,7 +5,7 @@ using JetBrains.Annotations; namespace Avalonia.Dialogs.Internal { - class InternalViewModelBase : INotifyPropertyChanged + internal class InternalViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs index 7aba87115d..d476d226b0 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs @@ -10,7 +10,7 @@ using Avalonia.Markup.Xaml; namespace Avalonia.Dialogs.Internal { - class ManagedFileChooser : UserControl + internal class ManagedFileChooser : UserControl { private Control _quickLinksRoot; private ListBox _filesView; diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs index d54945ac60..b86b7a3f72 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs @@ -5,7 +5,7 @@ using Avalonia.Controls; namespace Avalonia.Dialogs.Internal { - class ManagedFileChooserFilterViewModel : InternalViewModelBase + internal class ManagedFileChooserFilterViewModel : InternalViewModelBase { private readonly string[] _extensions; public string Name { get; } diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs index b6535a93d0..306643b018 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs @@ -2,7 +2,7 @@ using System; namespace Avalonia.Dialogs.Internal { - class ManagedFileChooserItemViewModel : InternalViewModelBase + internal class ManagedFileChooserItemViewModel : InternalViewModelBase { private string _displayName; private string _path; diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs index b04a36692c..d3d5e2292b 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs @@ -1,6 +1,6 @@ namespace Avalonia.Dialogs.Internal { - public class ManagedFileChooserNavigationItem + internal class ManagedFileChooserNavigationItem { public string DisplayName { get; set; } public string Path { get; set; } diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs index 84a3173eac..ad99206789 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; namespace Avalonia.Dialogs.Internal { - public class ManagedFileChooserSources + internal class ManagedFileChooserSources { public Func GetUserDirectories { get; set; } = DefaultGetUserDirectories; diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs index 3977874e93..b6e61bd18e 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs @@ -2,16 +2,14 @@ using System; using System.Collections.Specialized; using System.IO; using System.Linq; -using System.Reactive; using System.Runtime.InteropServices; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Threading; -using ReactiveUI; namespace Avalonia.Dialogs.Internal { - class ManagedFileChooserViewModel : InternalViewModelBase + internal class ManagedFileChooserViewModel : InternalViewModelBase { public event Action CancelRequested; public event Action CompleteRequested; diff --git a/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs index 8346ba952c..11ef6c0f5b 100644 --- a/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs +++ b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs @@ -5,7 +5,7 @@ using Avalonia.Data.Converters; namespace Avalonia.Dialogs.Internal { - public class ResourceSelectorConverter : ResourceDictionary, IValueConverter + internal class ResourceSelectorConverter : ResourceDictionary, IValueConverter { public object Convert(object key, Type targetType, object parameter, CultureInfo culture) { From 91d570bbf6ccfe9f7514725294f73d142e806bf4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 27 Jul 2019 11:00:15 +0100 Subject: [PATCH 0010/1791] [Managed Dialogs] Dont depend on rx and rxui --- src/Avalonia.Dialogs/Avalonia.Dialogs.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index afe0d48f6d..8b48b4a92c 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -10,8 +10,6 @@ - - From 09dd7c67c195fabaf9d4ab33c9c4d51759e431bd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 27 Jul 2019 11:09:33 +0100 Subject: [PATCH 0011/1791] some polishing of file dialog --- .../Internal/ManagedFileChooser.xaml | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml index 2e694e726d..7fedf2ec30 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml @@ -4,7 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:dialogs="clr-namespace:Avalonia.Dialogs.Internal" xmlns:internal="clr-namespace:Avalonia.Dialogs.Internal" - x:Class="Avalonia.Dialogs.Internal.ManagedFileChooser" Margin="12 0 12 6"> + x:Class="Avalonia.Dialogs.Internal.ManagedFileChooser" Margin="10"> @@ -30,7 +30,7 @@ - + + - + + + + + + + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index 1a607342f3..decd849adc 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -74,7 +74,7 @@ namespace ControlCatalog.Pages private void RepeaterClick(object sender, PointerPressedEventArgs e) { - var item = (e.Source as TextBlock)?.DataContext as string; + var item = (e.Source as TextBlock)?.DataContext as ItemsRepeaterPageViewModel.Item; ((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item; } } diff --git a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs index 25fd550520..bc2ce80714 100644 --- a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs @@ -1,4 +1,5 @@ -using System.Collections.ObjectModel; +using System; +using System.Collections.ObjectModel; using System.Linq; using ReactiveUI; @@ -10,18 +11,44 @@ namespace ControlCatalog.ViewModels public ItemsRepeaterPageViewModel() { - Items = new ObservableCollection( - Enumerable.Range(1, 100000).Select(i => $"Item {i.ToString()}")); + Items = new ObservableCollection( + Enumerable.Range(1, 100000).Select(i => new Item + { + Text = $"Item {i.ToString()}", + })); } - public ObservableCollection Items { get; } + public ObservableCollection Items { get; } - public string SelectedItem { get; set; } + public Item SelectedItem { get; set; } public void AddItem() { var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1; - Items.Insert(index + 1, $"New Item {newItemIndex++}"); + Items.Insert(index + 1, new Item { Text = $"New Item {newItemIndex++}" }); + } + + public void RandomizeHeights() + { + var random = new Random(); + + foreach (var i in Items) + { + i.Height = random.Next(240) + 10; + } + } + + public class Item : ReactiveObject + { + private double _height = double.NaN; + + public string Text { get; set; } + + public double Height + { + get => _height; + set => this.RaiseAndSetIfChanged(ref _height, value); + } } } } From b317df99e779e81d3dccee25682742daabb65c6f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 4 Oct 2019 01:20:25 +0200 Subject: [PATCH 0302/1791] Added failing test for #3041. --- .../LayoutManagerTests.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index 4c288b2702..3f20c9a76a 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -341,5 +341,38 @@ namespace Avalonia.Layout.UnitTests //layoutmanager should process properly other visuals Assert.All(targets, c => Assert.True(c.Arranged)); } + + + [Fact] + public void LayoutManager_Should_Recover_From_Infinite_Loop_On_Measure() + { + // Test for issue #3041. + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + root.LayoutManager.ExecuteInitialLayoutPass(root); + control.Measured = false; + + control.DoMeasureOverride = (l, s) => + { + control.InvalidateMeasure(); + return new Size(100, 100); + }; + + control.InvalidateMeasure(); + root.LayoutManager.ExecuteLayoutPass(); + + // This is the important part: running a second layout pass in which we exceed the maximum + // retries causes LayoutQueue.Info.Count to exceed _maxEnqueueCountPerLoop. + root.LayoutManager.ExecuteLayoutPass(); + + control.Measured = false; + control.DoMeasureOverride = null; + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.True(control.Measured); + Assert.True(control.IsMeasureValid); + } } } From 9fd8fdb53473372a72c3c705b674596838605176 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 4 Oct 2019 01:22:57 +0200 Subject: [PATCH 0303/1791] Fix comparison in LayoutQueue. --- src/Avalonia.Layout/LayoutQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/LayoutQueue.cs b/src/Avalonia.Layout/LayoutQueue.cs index ce40fdde49..eb0e4bd9f3 100644 --- a/src/Avalonia.Layout/LayoutQueue.cs +++ b/src/Avalonia.Layout/LayoutQueue.cs @@ -60,7 +60,7 @@ namespace Avalonia.Layout public void EndLoop() { - var notfinalized = _loopQueueInfo.Where(v => v.Value.Count == _maxEnqueueCountPerLoop).ToArray(); + var notfinalized = _loopQueueInfo.Where(v => v.Value.Count >= _maxEnqueueCountPerLoop).ToArray(); _loopQueueInfo.Clear(); From 393bbcf882bb7818937b4b474b421f20e20bb89a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 4 Oct 2019 01:27:00 +0200 Subject: [PATCH 0304/1791] Disable horizontal scrollbar on ItemsRepeaterPage. --- samples/ControlCatalog/MainView.xaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index c99a6b117b..874560a294 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -34,7 +34,8 @@ + ScrollViewer.VerticalScrollBarVisibility="Disabled" + ScrollViewer.HorizontalScrollBarVisibility="Disabled"> From 76840f463e5d4f5da6e3f1bb5ca30414347e7b7c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 4 Oct 2019 02:04:17 +0200 Subject: [PATCH 0305/1791] Don't hide Track during measure. It causes it to stop being laid out. Instead hide the child controls. --- src/Avalonia.Controls/Primitives/Track.cs | 24 +++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 0af8ac3415..a569808b35 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -342,7 +342,7 @@ namespace Avalonia.Controls.Primitives // hide the track and don't arrange the pieces if (notEnoughContentToScroll || thumbLongerThanTrack) { - IsVisible = false; + ShowChildren(false); ThumbCenterOffset = Double.NaN; Density = Double.NaN; decreaseButtonLength = 0.0; @@ -351,7 +351,7 @@ namespace Avalonia.Controls.Primitives } else { - IsVisible = true; + ShowChildren(true); } // Compute lengths of increase and decrease button @@ -413,5 +413,25 @@ namespace Avalonia.Controls.Primitives Minimum, Maximum); } + + private void ShowChildren(bool visible) + { + // WPF sets Visible = Hidden here but we don't have that, and setting IsVisible = false + // will cause us to stop being laid out. Instead show/hide the child controls. + if (Thumb != null) + { + Thumb.IsVisible = visible; + } + + if (IncreaseButton != null) + { + IncreaseButton.IsVisible = visible; + } + + if (DecreaseButton != null) + { + DecreaseButton.IsVisible = visible; + } + } } } From 01a404836ee80d7dc43113f97da25139391c50d9 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 1 Oct 2019 22:57:44 +0200 Subject: [PATCH 0306/1791] Reduce allocations caused by logging. --- src/Avalonia.Base/AvaloniaObject.cs | 68 ++++++++---- src/Avalonia.Base/Logging/ILogSink.cs | 71 ++++++++++++ src/Avalonia.Base/Logging/Logger.cs | 90 +++++++++++++++ .../Primitives/TemplatedControl.cs | 2 +- src/Avalonia.Layout/LayoutManager.cs | 32 ++++-- src/Avalonia.Layout/Layoutable.cs | 8 +- src/Avalonia.Logging.Serilog/SerilogLogger.cs | 104 +++++++++++++++++- src/Avalonia.Visuals/Visual.cs | 4 +- tests/Avalonia.UnitTests/TestLogSink.cs | 32 +++++- 9 files changed, 365 insertions(+), 46 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 8cc512d132..22a85663c7 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -326,8 +326,6 @@ namespace Avalonia VerifyAccess(); - var description = GetDescription(source); - if (property.IsDirect) { if (property.IsReadOnly) @@ -335,12 +333,18 @@ namespace Avalonia throw new ArgumentException($"The property {property.Name} is readonly."); } - Logger.Verbose( - LogArea.Property, - this, - "Bound {Property} to {Binding} with priority LocalValue", - property, - description); + if (Logger.IsEnabled(LogEventLevel.Verbose)) + { + var description = GetDescription(source); + + Logger.Log( + LogEventLevel.Verbose, + LogArea.Property, + this, + "Bound {Property} to {Binding} with priority LocalValue", + property, + description); + } if (_directBindings == null) { @@ -351,13 +355,19 @@ namespace Avalonia } else { - Logger.Verbose( - LogArea.Property, - this, - "Bound {Property} to {Binding} with priority {Priority}", - property, - description, - priority); + if (Logger.IsEnabled(LogEventLevel.Verbose)) + { + var description = GetDescription(source); + + Logger.Log( + LogEventLevel.Verbose, + LogArea.Property, + this, + "Bound {Property} to {Binding} with priority {Priority}", + property, + description, + priority); + } return Values.AddBinding(property, source, priority); } @@ -406,14 +416,18 @@ namespace Avalonia { RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority); - Logger.Verbose( - LogArea.Property, - this, - "{Property} changed from {$Old} to {$Value} with priority {Priority}", - property, - oldValue, - newValue, - (BindingPriority)priority); + if (Logger.IsEnabled(LogEventLevel.Verbose)) + { + Logger.Log( + LogEventLevel.Verbose, + LogArea.Property, + this, + "{Property} changed from {$Old} to {$Value} with priority {Priority}", + property, + oldValue, + newValue, + (BindingPriority)priority); + } } } @@ -812,7 +826,13 @@ namespace Avalonia /// The priority. private void LogPropertySet(AvaloniaProperty property, object value, BindingPriority priority) { - Logger.Verbose( + if (!Logger.IsEnabled(LogEventLevel.Verbose)) + { + return; + } + + Logger.Log( + LogEventLevel.Verbose, LogArea.Property, this, "Set {Property} to {$Value} with priority {Priority}", diff --git a/src/Avalonia.Base/Logging/ILogSink.cs b/src/Avalonia.Base/Logging/ILogSink.cs index 0ed4eede8f..8b5751b0af 100644 --- a/src/Avalonia.Base/Logging/ILogSink.cs +++ b/src/Avalonia.Base/Logging/ILogSink.cs @@ -8,6 +8,77 @@ namespace Avalonia.Logging /// public interface ILogSink { + /// + /// Checks if given log level is enabled. + /// + /// The log event level. + /// if given log level is enabled. + bool IsEnabled(LogEventLevel level); + + /// + /// Logs an event. + /// + /// The log event level. + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate); + + /// + /// Logs an event. + /// + /// The log event level. + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate, + T0 propertyValue0); + + /// + /// Logs an event. + /// + /// The log event level. + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1); + + /// + /// Logs an event. + /// + /// The log event level. + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + /// Message property value. + void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1, + T2 propertyValue2); + /// /// Logs a new event. /// diff --git a/src/Avalonia.Base/Logging/Logger.cs b/src/Avalonia.Base/Logging/Logger.cs index b1132ff4a9..c46edd4b4b 100644 --- a/src/Avalonia.Base/Logging/Logger.cs +++ b/src/Avalonia.Base/Logging/Logger.cs @@ -15,6 +15,96 @@ namespace Avalonia.Logging /// public static ILogSink Sink { get; set; } + /// + /// Checks if given log level is enabled. + /// + /// The log event level. + /// if given log level is enabled. + public static bool IsEnabled(LogEventLevel level) + { + return Sink?.IsEnabled(level) == true; + } + + /// + /// Logs an event. + /// + /// The log event level. + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate) + { + Sink?.Log(level, area, source, messageTemplate); + } + + /// + /// Logs an event. + /// + /// The log event level. + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate, + T0 propertyValue0) + { + Sink?.Log(level, area, source, messageTemplate, propertyValue0); + } + + /// + /// Logs an event. + /// + /// The log event level. + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1) + { + Sink?.Log(level, area, source, messageTemplate, propertyValue0, propertyValue1); + } + + /// + /// Logs an event. + /// + /// The log event level. + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1, + T2 propertyValue2) + { + Sink?.Log(level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + /// /// Logs an event. /// diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 47c3240374..daf79b3558 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -255,7 +255,7 @@ namespace Avalonia.Controls.Primitives if (template != null) { - Logger.Verbose(LogArea.Control, this, "Creating control template"); + Logger.Log(LogEventLevel.Verbose, LogArea.Control, this, "Creating control template"); var (child, nameScope) = template.Build(this); ApplyTemplatedParent(child); diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 45efccc1fa..ef5464c4ae 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Diagnostics; using Avalonia.Logging; using Avalonia.Threading; @@ -69,15 +70,22 @@ namespace Avalonia.Layout { _running = true; - Logger.Information( - LogArea.Layout, - this, - "Started layout pass. To measure: {Measure} To arrange: {Arrange}", - _toMeasure.Count, - _toArrange.Count); + Stopwatch stopwatch = null; - var stopwatch = new System.Diagnostics.Stopwatch(); - stopwatch.Start(); + bool captureTiming = Logger.IsEnabled(LogEventLevel.Information); + + if (captureTiming) + { + Logger.Log(LogEventLevel.Information, + LogArea.Layout, + this, + "Started layout pass. To measure: {Measure} To arrange: {Arrange}", + _toMeasure.Count, + _toArrange.Count); + + stopwatch = new Stopwatch(); + stopwatch.Start(); + } _toMeasure.BeginLoop(MaxPasses); _toArrange.BeginLoop(MaxPasses); @@ -103,8 +111,12 @@ namespace Avalonia.Layout _toMeasure.EndLoop(); _toArrange.EndLoop(); - stopwatch.Stop(); - Logger.Information(LogArea.Layout, this, "Layout pass finished in {Time}", stopwatch.Elapsed); + if (captureTiming) + { + stopwatch.Stop(); + + Logger.Information(LogArea.Layout, this, "Layout pass finished in {Time}", stopwatch.Elapsed); + } } _queued = false; diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index bd248d6d44..9b5f66b19d 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -329,7 +329,7 @@ namespace Avalonia.Layout DesiredSize = desiredSize; _previousMeasure = availableSize; - Logger.Verbose(LogArea.Layout, this, "Measure requested {DesiredSize}", DesiredSize); + Logger.Log(LogEventLevel.Verbose, LogArea.Layout, this, "Measure requested {DesiredSize}", DesiredSize); if (DesiredSize != previousDesiredSize) { @@ -356,7 +356,7 @@ namespace Avalonia.Layout if (!IsArrangeValid || _previousArrange != rect) { - Logger.Verbose(LogArea.Layout, this, "Arrange to {Rect} ", rect); + Logger.Log(LogEventLevel.Verbose, LogArea.Layout, this, "Arrange to {Rect} ", rect); IsArrangeValid = true; ArrangeCore(rect); @@ -381,7 +381,7 @@ namespace Avalonia.Layout { if (IsMeasureValid) { - Logger.Verbose(LogArea.Layout, this, "Invalidated measure"); + Logger.Log(LogEventLevel.Verbose, LogArea.Layout, this, "Invalidated measure"); IsMeasureValid = false; IsArrangeValid = false; @@ -402,7 +402,7 @@ namespace Avalonia.Layout { if (IsArrangeValid) { - Logger.Verbose(LogArea.Layout, this, "Invalidated arrange"); + Logger.Log(LogEventLevel.Verbose, LogArea.Layout, this, "Invalidated arrange"); IsArrangeValid = false; (VisualRoot as ILayoutRoot)?.LayoutManager?.InvalidateArrange(this); diff --git a/src/Avalonia.Logging.Serilog/SerilogLogger.cs b/src/Avalonia.Logging.Serilog/SerilogLogger.cs index 0534fe3012..895ee268d2 100644 --- a/src/Avalonia.Logging.Serilog/SerilogLogger.cs +++ b/src/Avalonia.Logging.Serilog/SerilogLogger.cs @@ -34,6 +34,76 @@ namespace Avalonia.Logging.Serilog Logger.Sink = new SerilogLogger(output); } + public bool IsEnabled(LogEventLevel level) + { + return _output.IsEnabled((SerilogLogEventLevel)level); + } + + public void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate) + { + Contract.Requires(area != null); + Contract.Requires(messageTemplate != null); + + using (PushLogContextProperties(area, source)) + { + _output.Write((SerilogLogEventLevel)level, messageTemplate); + } + } + + public void Log( + LogEventLevel level, + string area, object source, + string messageTemplate, + T0 propertyValue0) + { + Contract.Requires(area != null); + Contract.Requires(messageTemplate != null); + + using (PushLogContextProperties(area, source)) + { + _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0); + } + } + + public void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1) + { + Contract.Requires(area != null); + Contract.Requires(messageTemplate != null); + + using (PushLogContextProperties(area, source)) + { + _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0, propertyValue1); + } + } + + public void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1, + T2 propertyValue2) + { + Contract.Requires(area != null); + Contract.Requires(messageTemplate != null); + + using (PushLogContextProperties(area, source)) + { + _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + } + /// public void Log( AvaloniaLogEventLevel level, @@ -45,12 +115,40 @@ namespace Avalonia.Logging.Serilog Contract.Requires(area != null); Contract.Requires(messageTemplate != null); - using (LogContext.PushProperty("Area", area)) - using (LogContext.PushProperty("SourceType", source?.GetType())) - using (LogContext.PushProperty("SourceHash", source?.GetHashCode())) + using (PushLogContextProperties(area, source)) { _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValues); } } + + private static LogContextDisposable PushLogContextProperties(string area, object source) + { + return new LogContextDisposable( + LogContext.PushProperty("Area", area), + LogContext.PushProperty("SourceType", source?.GetType()), + LogContext.PushProperty("SourceHash", source?.GetHashCode()) + ); + } + + private readonly struct LogContextDisposable : IDisposable + { + private readonly IDisposable _areaDisposable; + private readonly IDisposable _sourceTypeDisposable; + private readonly IDisposable _sourceHashDisposable; + + public LogContextDisposable(IDisposable areaDisposable, IDisposable sourceTypeDisposable, IDisposable sourceHashDisposable) + { + _areaDisposable = areaDisposable; + _sourceTypeDisposable = sourceTypeDisposable; + _sourceHashDisposable = sourceHashDisposable; + } + + public void Dispose() + { + _areaDisposable.Dispose(); + _sourceTypeDisposable.Dispose(); + _sourceHashDisposable.Dispose(); + } + } } } diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 1f2d67b69e..8d32371c6d 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -359,7 +359,7 @@ namespace Avalonia /// The event args. protected virtual void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) { - Logger.Verbose(LogArea.Visual, this, "Attached to visual tree"); + Logger.Log(LogEventLevel.Verbose, LogArea.Visual, this, "Attached to visual tree"); _visualRoot = e.Root; @@ -388,7 +388,7 @@ namespace Avalonia /// The event args. protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { - Logger.Verbose(LogArea.Visual, this, "Detached from visual tree"); + Logger.Log(LogEventLevel.Verbose, LogArea.Visual, this, "Detached from visual tree"); _visualRoot = null; diff --git a/tests/Avalonia.UnitTests/TestLogSink.cs b/tests/Avalonia.UnitTests/TestLogSink.cs index 8e4dd7164f..a2b188c273 100644 --- a/tests/Avalonia.UnitTests/TestLogSink.cs +++ b/tests/Avalonia.UnitTests/TestLogSink.cs @@ -16,7 +16,7 @@ namespace Avalonia.UnitTests public class TestLogSink : ILogSink { - private LogCallback _callback; + private readonly LogCallback _callback; public TestLogSink(LogCallback callback) { @@ -30,7 +30,35 @@ namespace Avalonia.UnitTests return Disposable.Create(() => Logger.Sink = null); } - public void Log(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues) + public bool IsEnabled(LogEventLevel level) + { + return true; + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate) + { + _callback(level, area, source, messageTemplate); + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0) + { + _callback(level, area, source, messageTemplate, propertyValue0); + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, + T0 propertyValue0, T1 propertyValue1) + { + _callback(level, area, source, messageTemplate, propertyValue0, propertyValue1); + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, + T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + _callback(level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, + params object[] propertyValues) { _callback(level, area, source, messageTemplate, propertyValues); } From 068b73750b0f7bb5adc67c5470e30589eb36b79f Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 4 Oct 2019 00:01:44 +0200 Subject: [PATCH 0307/1791] Cleanup conditional logging API. --- src/Avalonia.Base/AvaloniaObject.cs | 49 +++----- src/Avalonia.Base/Logging/Logger.cs | 30 +++++ .../Logging/ParametrizedLogger.cs | 116 ++++++++++++++++++ 3 files changed, 166 insertions(+), 29 deletions(-) create mode 100644 src/Avalonia.Base/Logging/ParametrizedLogger.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 22a85663c7..ddcf16e11a 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -333,12 +333,11 @@ namespace Avalonia throw new ArgumentException($"The property {property.Name} is readonly."); } - if (Logger.IsEnabled(LogEventLevel.Verbose)) + if (Logger.TryGetLogger(LogEventLevel.Verbose, out var logger)) { var description = GetDescription(source); - Logger.Log( - LogEventLevel.Verbose, + logger.Log( LogArea.Property, this, "Bound {Property} to {Binding} with priority LocalValue", @@ -355,12 +354,11 @@ namespace Avalonia } else { - if (Logger.IsEnabled(LogEventLevel.Verbose)) + if (Logger.TryGetLogger(LogEventLevel.Verbose, out var logger)) { var description = GetDescription(source); - Logger.Log( - LogEventLevel.Verbose, + logger.Log( LogArea.Property, this, "Bound {Property} to {Binding} with priority {Priority}", @@ -416,18 +414,14 @@ namespace Avalonia { RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority); - if (Logger.IsEnabled(LogEventLevel.Verbose)) - { - Logger.Log( - LogEventLevel.Verbose, - LogArea.Property, - this, - "{Property} changed from {$Old} to {$Value} with priority {Priority}", - property, - oldValue, - newValue, - (BindingPriority)priority); - } + Logger.TryGetLogger(LogEventLevel.Verbose)?.Log( + LogArea.Property, + this, + "{Property} changed from {$Old} to {$Value} with priority {Priority}", + property, + oldValue, + newValue, + (BindingPriority)priority); } } @@ -826,19 +820,16 @@ namespace Avalonia /// The priority. private void LogPropertySet(AvaloniaProperty property, object value, BindingPriority priority) { - if (!Logger.IsEnabled(LogEventLevel.Verbose)) + if (Logger.TryGetLogger(LogEventLevel.Verbose, out var logger)) { - return; + logger.Log( + LogArea.Property, + this, + "Set {Property} to {$Value} with priority {Priority}", + property, + value, + priority); } - - Logger.Log( - LogEventLevel.Verbose, - LogArea.Property, - this, - "Set {Property} to {$Value} with priority {Priority}", - property, - value, - priority); } private class DirectBindingSubscription : IObserver, IDisposable diff --git a/src/Avalonia.Base/Logging/Logger.cs b/src/Avalonia.Base/Logging/Logger.cs index c46edd4b4b..ef4c29b9f3 100644 --- a/src/Avalonia.Base/Logging/Logger.cs +++ b/src/Avalonia.Base/Logging/Logger.cs @@ -25,6 +25,36 @@ namespace Avalonia.Logging return Sink?.IsEnabled(level) == true; } + /// + /// Returns parametrized logging sink if given log level is enabled. + /// + /// The log event level. + /// Log sink or if log level is not enabled. + public static ParametrizedLogger? TryGetLogger(LogEventLevel level) + { + if (!IsEnabled(level)) + { + return null; + } + + return new ParametrizedLogger(Sink, level); + } + + /// + /// Returns parametrized logging sink if given log level is enabled. + /// + /// The log event level. + /// Log sink that is valid only if method returns . + /// if logger was obtained successfully. + public static bool TryGetLogger(LogEventLevel level, out ParametrizedLogger outLogger) + { + ParametrizedLogger? logger = TryGetLogger(level); + + outLogger = logger.GetValueOrDefault(); + + return logger.HasValue; + } + /// /// Logs an event. /// diff --git a/src/Avalonia.Base/Logging/ParametrizedLogger.cs b/src/Avalonia.Base/Logging/ParametrizedLogger.cs new file mode 100644 index 0000000000..1a0678c79a --- /dev/null +++ b/src/Avalonia.Base/Logging/ParametrizedLogger.cs @@ -0,0 +1,116 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Runtime.CompilerServices; + +namespace Avalonia.Logging +{ + /// + /// Logger sink parametrized for given logging level. + /// + public readonly struct ParametrizedLogger + { + private readonly ILogSink _sink; + private readonly LogEventLevel _level; + + public ParametrizedLogger(ILogSink sink, LogEventLevel level) + { + _sink = sink; + _level = level; + } + + /// + /// Checks if this logger can be used. + /// + public bool IsValid => _sink != null; + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( + string area, + object source, + string messageTemplate) + { + _sink.Log(_level, area, source, messageTemplate); + } + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( + string area, + object source, + string messageTemplate, + T0 propertyValue0) + { + _sink.Log(_level, area, source, messageTemplate, propertyValue0); + } + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1) + { + _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1); + } + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1, + T2 propertyValue2) + { + _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// The message property values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( + string area, + object source, + string messageTemplate, + params object[] propertyValues) + { + _sink.Log(_level, area, source, messageTemplate, propertyValues); + } + } +} From 1f693404526da965fad8f6d30aef138125b9e9f6 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 6 Oct 2019 00:26:10 +0200 Subject: [PATCH 0308/1791] Cleanup. --- src/Avalonia.Base/AvaloniaObject.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index ddcf16e11a..8d9a98d087 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -820,16 +820,13 @@ namespace Avalonia /// The priority. private void LogPropertySet(AvaloniaProperty property, object value, BindingPriority priority) { - if (Logger.TryGetLogger(LogEventLevel.Verbose, out var logger)) - { - logger.Log( - LogArea.Property, - this, - "Set {Property} to {$Value} with priority {Priority}", - property, - value, - priority); - } + Logger.TryGetLogger(LogEventLevel.Verbose)?.Log( + LogArea.Property, + this, + "Set {Property} to {$Value} with priority {Priority}", + property, + value, + priority); } private class DirectBindingSubscription : IObserver, IDisposable From 2116439c727d8e9001975e3cdf4691e10b9b39d4 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 6 Oct 2019 15:53:39 +0200 Subject: [PATCH 0309/1791] Optimize TypeNameAndClassSelector. --- .../Styling/TypeNameAndClassSelector.cs | 90 +++++++++++-------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs index 362ac86e50..f1fd2f6c7f 100644 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs @@ -18,8 +18,9 @@ namespace Avalonia.Styling internal class TypeNameAndClassSelector : Selector { private readonly Selector _previous; + private readonly Lazy> _classes = new Lazy>(() => new List()); private Type _targetType; - private Lazy> _classes = new Lazy>(() => new List()); + private string _selectorString; public static TypeNameAndClassSelector OfType(Selector previous, Type targetType) @@ -27,6 +28,7 @@ namespace Avalonia.Styling var result = new TypeNameAndClassSelector(previous); result._targetType = targetType; result.IsConcreteType = true; + return result; } @@ -35,6 +37,7 @@ namespace Avalonia.Styling var result = new TypeNameAndClassSelector(previous); result._targetType = targetType; result.IsConcreteType = false; + return result; } @@ -42,6 +45,7 @@ namespace Avalonia.Styling { var result = new TypeNameAndClassSelector(previous); result.Name = name; + return result; } @@ -49,6 +53,7 @@ namespace Avalonia.Styling { var result = new TypeNameAndClassSelector(previous); result.Classes.Add(className); + return result; } @@ -126,9 +131,11 @@ namespace Avalonia.Styling if (subscribe) { var observable = new ClassObserver(control.Classes, _classes.Value); + return new SelectorMatch(observable); } - else if (!Matches(control.Classes)) + + if (!AreClassesMatching(control.Classes, Classes)) { return SelectorMatch.NeverThisInstance; } @@ -139,21 +146,6 @@ namespace Avalonia.Styling protected override Selector MovePrevious() => _previous; - private bool Matches(IEnumerable classes) - { - int remaining = Classes.Count; - - foreach (var c in classes) - { - if (Classes.Contains(c)) - { - --remaining; - } - } - - return remaining == 0; - } - private string BuildSelectorString() { var builder = new StringBuilder(); @@ -199,11 +191,41 @@ namespace Avalonia.Styling return builder.ToString(); } - private class ClassObserver : LightweightObservableBase + private static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) { - readonly IList _match; - IAvaloniaReadOnlyList _classes; - bool _value; + int remainingMatches = toMatch.Count; + int classesCount = classes.Count; + + // Early bail out - we can't match if control does not have enough classes. + if (classesCount < remainingMatches) + { + return false; + } + + for (var i = 0; i < classesCount; i++) + { + var c = classes[i]; + + if (toMatch.Contains(c)) + { + --remainingMatches; + + // Already matched so we can skip checking other classes. + if (remainingMatches == 0) + { + break; + } + } + } + + return remainingMatches == 0; + } + + private sealed class ClassObserver : LightweightObservableBase + { + private readonly IList _match; + private readonly IAvaloniaReadOnlyList _classes; + private bool _hasMatch; public ClassObserver(IAvaloniaReadOnlyList classes, IList match) { @@ -215,42 +237,32 @@ namespace Avalonia.Styling protected override void Initialize() { - _value = GetResult(); + _hasMatch = IsMatching(); _classes.CollectionChanged += ClassesChanged; } protected override void Subscribed(IObserver observer, bool first) { - observer.OnNext(_value); + observer.OnNext(_hasMatch); } private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action != NotifyCollectionChangedAction.Move) { - var value = GetResult(); + var hasMatch = IsMatching(); - if (value != _value) + if (hasMatch != _hasMatch) { - PublishNext(GetResult()); - _value = value; + PublishNext(hasMatch); + _hasMatch = hasMatch; } } } - private bool GetResult() + private bool IsMatching() { - int remaining = _match.Count; - - foreach (var c in _classes) - { - if (_match.Contains(c)) - { - --remaining; - } - } - - return remaining == 0; + return AreClassesMatching(_classes, _match); } } } From a95273902b2f975787f2b7d3d48516d305a31989 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 6 Oct 2019 20:45:17 +0200 Subject: [PATCH 0310/1791] Queue caret update with correct priority. When trying to scroll the caret into view, if we need to wait for a layout first make sure that we queue the update with a lower priority then `Layout`. `Render` is the next lowest priority so use this. Fixes #3070 --- src/Avalonia.Controls/Presenters/TextPresenter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index debbb81264..5931fec350 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -244,7 +244,7 @@ namespace Avalonia.Controls.Presenters var rect = FormattedText.HitTestTextPosition(caretIndex); this.BringIntoView(rect); }, - DispatcherPriority.Normal); + DispatcherPriority.Render); } } } From 5814bd7163193c614dbffd8364b04bece257afa3 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 6 Oct 2019 22:16:07 +0200 Subject: [PATCH 0311/1791] Remove direct logging functions. --- src/Avalonia.Base/AvaloniaObject.cs | 11 +- .../Data/Core/BindingExpression.cs | 2 +- src/Avalonia.Base/Logging/Logger.cs | 209 +----------------- .../Logging/ParametrizedLogger.cs | 66 +++++- src/Avalonia.Base/PriorityValue.cs | 2 +- src/Avalonia.Controls/DropDown.cs | 4 +- .../Primitives/SelectingItemsControl.cs | 2 +- .../Primitives/TemplatedControl.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 2 +- src/Avalonia.Layout/LayoutManager.cs | 7 +- src/Avalonia.Layout/Layoutable.cs | 8 +- src/Avalonia.OpenGL/EglGlPlatformFeature.cs | 2 +- src/Avalonia.Styling/StyledElement.cs | 4 +- .../Animation/Animators/TransformAnimator.cs | 4 +- .../Rendering/DeferredRenderer.cs | 3 +- .../Rendering/ImmediateRenderer.cs | 3 +- src/Avalonia.Visuals/Rendering/RenderLoop.cs | 4 +- src/Avalonia.Visuals/Visual.cs | 7 +- src/Avalonia.X11/Glx/GlxPlatformFeature.cs | 2 +- .../AvaloniaPropertyTypeConverter.cs | 2 +- .../Markup/Data/DelayedBinding.cs | 2 +- .../Media/StreamGeometryContextImpl.cs | 2 +- 22 files changed, 103 insertions(+), 247 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 8d9a98d087..1d49401d99 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -333,7 +333,7 @@ namespace Avalonia throw new ArgumentException($"The property {property.Name} is readonly."); } - if (Logger.TryGetLogger(LogEventLevel.Verbose, out var logger)) + if (Logger.TryGet(LogEventLevel.Verbose, out var logger)) { var description = GetDescription(source); @@ -354,7 +354,7 @@ namespace Avalonia } else { - if (Logger.TryGetLogger(LogEventLevel.Verbose, out var logger)) + if (Logger.TryGet(LogEventLevel.Verbose, out var logger)) { var description = GetDescription(source); @@ -414,7 +414,7 @@ namespace Avalonia { RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority); - Logger.TryGetLogger(LogEventLevel.Verbose)?.Log( + Logger.TryGet(LogEventLevel.Verbose)?.Log( LogArea.Property, this, "{Property} changed from {$Old} to {$Value} with priority {Priority}", @@ -466,8 +466,7 @@ namespace Avalonia /// The binding error. protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e) { - Logger.Log( - LogEventLevel.Warning, + Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Binding, this, "Error in binding to {Target}.{Property}: {Message}", @@ -820,7 +819,7 @@ namespace Avalonia /// The priority. private void LogPropertySet(AvaloniaProperty property, object value, BindingPriority priority) { - Logger.TryGetLogger(LogEventLevel.Verbose)?.Log( + Logger.TryGet(LogEventLevel.Verbose)?.Log( LogArea.Property, this, "Set {Property} to {$Value} with priority {Priority}", diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 7f8396cdfa..986e2cf012 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -165,7 +165,7 @@ namespace Avalonia.Data.Core } else { - Logger.Error( + Logger.TryGet(LogEventLevel.Error)?.Log( LogArea.Binding, this, "Could not convert FallbackValue {FallbackValue} to {Type}", diff --git a/src/Avalonia.Base/Logging/Logger.cs b/src/Avalonia.Base/Logging/Logger.cs index ef4c29b9f3..c895c70094 100644 --- a/src/Avalonia.Base/Logging/Logger.cs +++ b/src/Avalonia.Base/Logging/Logger.cs @@ -1,8 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Runtime.CompilerServices; - namespace Avalonia.Logging { /// @@ -30,7 +28,7 @@ namespace Avalonia.Logging /// /// The log event level. /// Log sink or if log level is not enabled. - public static ParametrizedLogger? TryGetLogger(LogEventLevel level) + public static ParametrizedLogger? TryGet(LogEventLevel level) { if (!IsEnabled(level)) { @@ -46,214 +44,13 @@ namespace Avalonia.Logging /// The log event level. /// Log sink that is valid only if method returns . /// if logger was obtained successfully. - public static bool TryGetLogger(LogEventLevel level, out ParametrizedLogger outLogger) + public static bool TryGet(LogEventLevel level, out ParametrizedLogger outLogger) { - ParametrizedLogger? logger = TryGetLogger(level); + ParametrizedLogger? logger = TryGet(level); outLogger = logger.GetValueOrDefault(); return logger.HasValue; } - - /// - /// Logs an event. - /// - /// The log event level. - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Log( - LogEventLevel level, - string area, - object source, - string messageTemplate) - { - Sink?.Log(level, area, source, messageTemplate); - } - - /// - /// Logs an event. - /// - /// The log event level. - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// Message property value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Log( - LogEventLevel level, - string area, - object source, - string messageTemplate, - T0 propertyValue0) - { - Sink?.Log(level, area, source, messageTemplate, propertyValue0); - } - - /// - /// Logs an event. - /// - /// The log event level. - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// Message property value. - /// Message property value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Log( - LogEventLevel level, - string area, - object source, - string messageTemplate, - T0 propertyValue0, - T1 propertyValue1) - { - Sink?.Log(level, area, source, messageTemplate, propertyValue0, propertyValue1); - } - - /// - /// Logs an event. - /// - /// The log event level. - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// Message property value. - /// Message property value. - /// Message property value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Log( - LogEventLevel level, - string area, - object source, - string messageTemplate, - T0 propertyValue0, - T1 propertyValue1, - T2 propertyValue2) - { - Sink?.Log(level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2); - } - - /// - /// Logs an event. - /// - /// The log event level. - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Log( - LogEventLevel level, - string area, - object source, - string messageTemplate, - params object[] propertyValues) - { - Sink?.Log(level, area, source, messageTemplate, propertyValues); - } - - /// - /// Logs an event with the level. - /// - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Verbose( - string area, - object source, - string messageTemplate, - params object[] propertyValues) - { - Log(LogEventLevel.Verbose, area, source, messageTemplate, propertyValues); - } - - /// - /// Logs an event with the level. - /// - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Debug( - string area, - object source, - string messageTemplate, - params object[] propertyValues) - { - Log(LogEventLevel.Debug, area, source, messageTemplate, propertyValues); - } - - /// - /// Logs an event with the level. - /// - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Information( - string area, - object source, - string messageTemplate, - params object[] propertyValues) - { - Log(LogEventLevel.Information, area, source, messageTemplate, propertyValues); - } - - /// - /// Logs an event with the level. - /// - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Warning( - string area, - object source, - string messageTemplate, - params object[] propertyValues) - { - Log(LogEventLevel.Warning, area, source, messageTemplate, propertyValues); - } - - /// - /// Logs an event with the level. - /// - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Error( - string area, - object source, - string messageTemplate, - params object[] propertyValues) - { - Log(LogEventLevel.Error, area, source, messageTemplate, propertyValues); - } - - /// - /// Logs an event with the level. - /// - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Fatal( - string area, - object source, - string messageTemplate, - params object[] propertyValues) - { - Log(LogEventLevel.Fatal, area, source, messageTemplate, propertyValues); - } } } diff --git a/src/Avalonia.Base/Logging/ParametrizedLogger.cs b/src/Avalonia.Base/Logging/ParametrizedLogger.cs index 1a0678c79a..1550cc1b40 100644 --- a/src/Avalonia.Base/Logging/ParametrizedLogger.cs +++ b/src/Avalonia.Base/Logging/ParametrizedLogger.cs @@ -102,15 +102,73 @@ namespace Avalonia.Logging /// The area that the event originates. /// The object from which the event originates. /// The message template. - /// The message property values. + /// Message property value. + /// Message property value. + /// Message property value. + /// Message property value. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Log( + public void Log( + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1, + T2 propertyValue2, + T3 propertyValue3) + { + _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3); + } + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + /// Message property value. + /// Message property value. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( string area, object source, string messageTemplate, - params object[] propertyValues) + T0 propertyValue0, + T1 propertyValue1, + T2 propertyValue2, + T3 propertyValue3, + T4 propertyValue4) + { + _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3, propertyValue4); + } + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + /// Message property value. + /// Message property value. + /// Message property value. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1, + T2 propertyValue2, + T3 propertyValue3, + T4 propertyValue4, + T5 propertyValue5) { - _sink.Log(_level, area, source, messageTemplate, propertyValues); + _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3, propertyValue4, propertyValue5); } } } diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 2871271062..61184ef7b1 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -301,7 +301,7 @@ namespace Avalonia } else { - Logger.Error( + Logger.TryGet(LogEventLevel.Error)?.Log( LogArea.Binding, Owner, "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index b67d9ef89a..9da803d16d 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -9,7 +9,7 @@ namespace Avalonia.Controls { public DropDown() { - Logger.Warning(LogArea.Control, this, "DropDown is deprecated: Use ComboBox"); + Logger.TryGet(LogEventLevel.Warning)?.Log(LogArea.Control, this, "DropDown is deprecated: Use ComboBox"); } Type IStyleable.StyleKey => typeof(ComboBox); @@ -20,7 +20,7 @@ namespace Avalonia.Controls { public DropDownItem() { - Logger.Warning(LogArea.Control, this, "DropDownItem is deprecated: Use ComboBoxItem"); + Logger.TryGet(LogEventLevel.Warning)?.Log(LogArea.Control, this, "DropDownItem is deprecated: Use ComboBoxItem"); } Type IStyleable.StyleKey => typeof(ComboBoxItem); diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 6869ea0822..c6172c0f36 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1062,7 +1062,7 @@ namespace Avalonia.Controls.Primitives } catch (Exception ex) { - Logger.Error( + Logger.TryGet(LogEventLevel.Error)?.Log( LogArea.Property, this, "Error thrown updating SelectedItems: {Error}", diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index daf79b3558..7d0f306db8 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -255,7 +255,7 @@ namespace Avalonia.Controls.Primitives if (template != null) { - Logger.Log(LogEventLevel.Verbose, LogArea.Control, this, "Creating control template"); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Control, this, "Creating control template"); var (child, nameScope) = template.Build(this); ApplyTemplatedParent(child); diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index e0acab1133..c54ebd5360 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -330,7 +330,7 @@ namespace Avalonia.Controls if (result == null) { - Logger.Warning( + Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Control, this, "Could not create {Service} : maybe Application.RegisterServices() wasn't called?", diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index ef5464c4ae..855f123748 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -72,11 +72,12 @@ namespace Avalonia.Layout Stopwatch stopwatch = null; - bool captureTiming = Logger.IsEnabled(LogEventLevel.Information); + const LogEventLevel timingLogLevel = LogEventLevel.Information; + bool captureTiming = Logger.IsEnabled(timingLogLevel); if (captureTiming) { - Logger.Log(LogEventLevel.Information, + Logger.TryGet(timingLogLevel)?.Log( LogArea.Layout, this, "Started layout pass. To measure: {Measure} To arrange: {Arrange}", @@ -115,7 +116,7 @@ namespace Avalonia.Layout { stopwatch.Stop(); - Logger.Information(LogArea.Layout, this, "Layout pass finished in {Time}", stopwatch.Elapsed); + Logger.TryGet(timingLogLevel)?.Log(LogArea.Layout, this, "Layout pass finished in {Time}", stopwatch.Elapsed); } } diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 9b5f66b19d..b0757a823d 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -329,7 +329,7 @@ namespace Avalonia.Layout DesiredSize = desiredSize; _previousMeasure = availableSize; - Logger.Log(LogEventLevel.Verbose, LogArea.Layout, this, "Measure requested {DesiredSize}", DesiredSize); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Measure requested {DesiredSize}", DesiredSize); if (DesiredSize != previousDesiredSize) { @@ -356,7 +356,7 @@ namespace Avalonia.Layout if (!IsArrangeValid || _previousArrange != rect) { - Logger.Log(LogEventLevel.Verbose, LogArea.Layout, this, "Arrange to {Rect} ", rect); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Arrange to {Rect} ", rect); IsArrangeValid = true; ArrangeCore(rect); @@ -381,7 +381,7 @@ namespace Avalonia.Layout { if (IsMeasureValid) { - Logger.Log(LogEventLevel.Verbose, LogArea.Layout, this, "Invalidated measure"); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Invalidated measure"); IsMeasureValid = false; IsArrangeValid = false; @@ -402,7 +402,7 @@ namespace Avalonia.Layout { if (IsArrangeValid) { - Logger.Log(LogEventLevel.Verbose, LogArea.Layout, this, "Invalidated arrange"); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Invalidated arrange"); IsArrangeValid = false; (VisualRoot as ILayoutRoot)?.LayoutManager?.InvalidateArrange(this); diff --git a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs index 86411b89da..5f5064fba5 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs +++ b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs @@ -31,7 +31,7 @@ namespace Avalonia.OpenGL } catch(Exception e) { - Logger.Error("OpenGL", null, "Unable to initialize EGL-based rendering: {0}", e); + Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", null, "Unable to initialize EGL-based rendering: {0}", e); return null; } } diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 38c29289b6..1465b9eb85 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -743,11 +743,11 @@ namespace Avalonia #if DEBUG if (((INotifyCollectionChangedDebug)_classes).GetCollectionChangedSubscribers()?.Length > 0) { - Logger.Warning( + Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Control, this, "{Type} detached from logical tree but still has class listeners", - this.GetType()); + GetType()); } #endif } diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs index e7be272f13..1f1590bdcd 100644 --- a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs @@ -65,14 +65,14 @@ namespace Avalonia.Animation.Animators } } - Logger.Warning( + Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Animations, control, $"Cannot find the appropriate transform: \"{Property.OwnerType}\" in {control}."); } else { - Logger.Error( + Logger.TryGet(LogEventLevel.Error)?.Log( LogArea.Animations, control, $"Cannot apply animation: Target property owner {Property.OwnerType} is not a Transform object."); diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index efcc555159..d9a68b236a 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Avalonia.Logging; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -269,7 +270,7 @@ namespace Avalonia.Rendering } catch (RenderTargetCorruptedException ex) { - Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); + Logger.TryGet(LogEventLevel.Information)?.Log("Renderer", this, "Render target was corrupted. Exception: {0}", ex); RenderTarget?.Dispose(); RenderTarget = null; } diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index b2d242d4af..68d56eeedd 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Logging; using Avalonia.Media; using Avalonia.Platform; using Avalonia.VisualTree; @@ -80,7 +81,7 @@ namespace Avalonia.Rendering } catch (RenderTargetCorruptedException ex) { - Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); + Logger.TryGet(LogEventLevel.Information)?.Log("Renderer", this, "Render target was corrupted. Exception: {0}", ex); _renderTarget.Dispose(); _renderTarget = null; } diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs index 140688f8bc..c2594658b9 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -120,7 +120,7 @@ namespace Avalonia.Rendering } catch (Exception ex) { - Logger.Error(LogArea.Visual, this, "Exception in render update: {Error}", ex); + Logger.TryGet(LogEventLevel.Error)?.Log(LogArea.Visual, this, "Exception in render update: {Error}", ex); } } } @@ -136,7 +136,7 @@ namespace Avalonia.Rendering } catch (Exception ex) { - Logger.Error(LogArea.Visual, this, "Exception in render loop: {Error}", ex); + Logger.TryGet(LogEventLevel.Error)?.Log(LogArea.Visual, this, "Exception in render loop: {Error}", ex); } finally { diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 8d32371c6d..f4306d3929 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -359,7 +359,7 @@ namespace Avalonia /// The event args. protected virtual void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) { - Logger.Log(LogEventLevel.Verbose, LogArea.Visual, this, "Attached to visual tree"); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Visual, this, "Attached to visual tree"); _visualRoot = e.Root; @@ -388,7 +388,7 @@ namespace Avalonia /// The event args. protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { - Logger.Log(LogEventLevel.Verbose, LogArea.Visual, this, "Detached from visual tree"); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Visual, this, "Detached from visual tree"); _visualRoot = null; @@ -453,8 +453,7 @@ namespace Avalonia return; } - Logger.Log( - LogEventLevel.Warning, + Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Binding, this, "Error in binding to {Target}.{Property}: {Message}", diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index d15b5fe4b8..3dc2e8e41f 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -36,7 +36,7 @@ namespace Avalonia.X11.Glx } catch(Exception e) { - Logger.Error("OpenGL", null, "Unable to initialize GLX-based rendering: {0}", e); + Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", null, "Unable to initialize GLX-based rendering: {0}", e); return null; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index b42bd53619..6000b71f9d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -42,7 +42,7 @@ namespace Avalonia.Markup.Xaml.Converters !property.IsAttached && !registry.IsRegistered(targetType, property)) { - Logger.Warning( + Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Property, this, "Property '{Owner}.{Name}' is not registered on '{Type}'.", diff --git a/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs b/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs index e03427c161..f7d228609d 100644 --- a/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs +++ b/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs @@ -150,7 +150,7 @@ namespace Avalonia.Markup.Data } catch (Exception e) { - Logger.Error( + Logger.TryGet(LogEventLevel.Error)?.Log( LogArea.Property, control, "Error setting {Property} on {Target}: {Exception}", diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs index 10b89d79b8..fd2f14fc58 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs @@ -85,7 +85,7 @@ namespace Avalonia.Direct2D1.Media } catch (Exception ex) { - Logger.Error( + Logger.TryGet(LogEventLevel.Error)?.Log( LogArea.Visual, this, "GeometrySink.Close exception: {Exception}", From 55e9037383df98654870186db13031ab50f27bca Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 7 Oct 2019 10:39:50 +0200 Subject: [PATCH 0312/1791] Added failing test for #2980. --- .../TreeViewTests.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 6c9f0f4f57..a91b7a0701 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -14,6 +14,7 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.Styling; using Avalonia.UnitTests; using Xunit; @@ -892,6 +893,46 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2, GetItem(target, 0, 1, 0).Level); } + [Fact] + public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() + { + /// Issue #2980. + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var target = new DerivedTreeView + { + Template = CreateTreeViewTemplate(), + SelectionMode = SelectionMode.Multiple, + Items = new List + { + new Node { Value = "Root1", }, + new Node { Value = "Root2", }, + }, + }; + + var visualRoot = new TestRoot + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(TreeViewItem.IsExpandedProperty, true), + }, + }, + }, + Child = target, + }; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + _mouse.Click(GetItem(target, 0)); + _mouse.Click(GetItem(target, 1), modifiers: InputModifiers.Shift); + } + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); From 75fdd0d3432a033c0b8c93e8d583754c58a6a2e3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 7 Oct 2019 10:41:44 +0200 Subject: [PATCH 0313/1791] Check for empty items in GetContainerInDirection. Don't try to navigate into `TreeViewItem`s with no children: this causes a out-of-range exception. Fixes #2980 --- src/Avalonia.Controls/TreeView.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index cc47365214..8907137ecb 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -1,3 +1,4 @@ + // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. @@ -470,7 +471,7 @@ namespace Avalonia.Controls if (index > 0) { var previous = (TreeViewItem)parentGenerator.ContainerFromIndex(index - 1); - result = previous.IsExpanded ? + result = previous.IsExpanded && previous.ItemCount > 0 ? (TreeViewItem)previous.ItemContainerGenerator.ContainerFromIndex(previous.ItemCount - 1) : previous; } @@ -482,7 +483,7 @@ namespace Avalonia.Controls break; case NavigationDirection.Down: - if (from.IsExpanded && intoChildren) + if (from.IsExpanded && intoChildren && from.ItemCount > 0) { result = (TreeViewItem)from.ItemContainerGenerator.ContainerFromIndex(0); } From b7efe2037c5ac3cb9976e95d36b2285ec95d0306 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 8 Oct 2019 00:35:20 +0200 Subject: [PATCH 0314/1791] Only set SizeToContent for non-set width/height. Always setting `SizeToContext = WidthAndHeight` was causing `Grid` to default to `Auto` sizing for `Star` rows/columns at design-time. Fixes #2862 --- src/Avalonia.DesignerSupport/DesignWindowLoader.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index a7d4b96974..f3bb0edce5 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -69,7 +69,17 @@ namespace Avalonia.DesignerSupport } if (!window.IsSet(Window.SizeToContentProperty)) - window.SizeToContent = SizeToContent.WidthAndHeight; + { + if (double.IsNaN(window.Width)) + { + window.SizeToContent |= SizeToContent.Width; + } + + if (double.IsNaN(window.Height)) + { + window.SizeToContent |= SizeToContent.Height; + } + } } window.Show(); Design.ApplyDesignModeProperties(window, control); From d2dbc12ee8540aeb80d6e914617bfe99c14d2ce1 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 8 Oct 2019 00:49:23 +0200 Subject: [PATCH 0315/1791] Final review fixes. --- src/Avalonia.Base/AvaloniaObject.cs | 36 +++++++++++------------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 1d49401d99..2450f1a3a1 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -333,17 +333,12 @@ namespace Avalonia throw new ArgumentException($"The property {property.Name} is readonly."); } - if (Logger.TryGet(LogEventLevel.Verbose, out var logger)) - { - var description = GetDescription(source); - - logger.Log( - LogArea.Property, - this, - "Bound {Property} to {Binding} with priority LocalValue", - property, - description); - } + Logger.TryGet(LogEventLevel.Verbose)?.Log( + LogArea.Property, + this, + "Bound {Property} to {Binding} with priority LocalValue", + property, + GetDescription(source)); if (_directBindings == null) { @@ -354,18 +349,13 @@ namespace Avalonia } else { - if (Logger.TryGet(LogEventLevel.Verbose, out var logger)) - { - var description = GetDescription(source); - - logger.Log( - LogArea.Property, - this, - "Bound {Property} to {Binding} with priority {Priority}", - property, - description, - priority); - } + Logger.TryGet(LogEventLevel.Verbose)?.Log( + LogArea.Property, + this, + "Bound {Property} to {Binding} with priority {Priority}", + property, + GetDescription(source), + priority); return Values.AddBinding(property, source, priority); } From cb663f98b18f638ec63abaa5226cc79a6c085718 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 6 Oct 2019 19:03:05 +0200 Subject: [PATCH 0316/1791] Add missing IEquatable interfaces to structs. Standardize parsing error messages. Fix two instances of boxing in hash code. --- src/Avalonia.Visuals/CornerRadius.cs | 68 +++++++++++++++---- src/Avalonia.Visuals/Matrix.cs | 6 +- src/Avalonia.Visuals/Media/PixelPoint.cs | 20 ++++-- src/Avalonia.Visuals/Media/PixelRect.cs | 18 +++-- src/Avalonia.Visuals/Media/PixelSize.cs | 18 +++-- src/Avalonia.Visuals/Point.cs | 27 ++++++-- src/Avalonia.Visuals/Rect.cs | 38 ++++++++--- src/Avalonia.Visuals/RelativePoint.cs | 7 +- src/Avalonia.Visuals/RelativeRect.cs | 9 +-- src/Avalonia.Visuals/Size.cs | 26 +++++-- src/Avalonia.Visuals/Thickness.cs | 34 +++++++--- src/Avalonia.Visuals/Vector.cs | 2 +- .../VisualTree/TransformedBounds.cs | 3 +- 13 files changed, 200 insertions(+), 76 deletions(-) diff --git a/src/Avalonia.Visuals/CornerRadius.cs b/src/Avalonia.Visuals/CornerRadius.cs index 09163a2dac..5445c5a200 100644 --- a/src/Avalonia.Visuals/CornerRadius.cs +++ b/src/Avalonia.Visuals/CornerRadius.cs @@ -8,7 +8,10 @@ using Avalonia.Utilities; namespace Avalonia { - public struct CornerRadius + /// + /// Represents the radii of a rectangle's corners. + /// + public readonly struct CornerRadius : IEquatable { static CornerRadius() { @@ -33,20 +36,60 @@ namespace Avalonia BottomLeft = bottomLeft; } + /// + /// Radius of the top left corner. + /// public double TopLeft { get; } + + /// + /// Radius of the top right corner. + /// public double TopRight { get; } + + /// + /// Radius of the bottom right corner. + /// public double BottomRight { get; } + + /// + /// Radius of the bottom left corner. + /// public double BottomLeft { get; } + + /// + /// Gets a value indicating whether all corner radii are set to 0. + /// public bool IsEmpty => TopLeft.Equals(0) && IsUniform; + + /// + /// Gets a value indicating whether all corner radii are equal. + /// public bool IsUniform => TopLeft.Equals(TopRight) && BottomLeft.Equals(BottomRight) && TopRight.Equals(BottomRight); + /// + /// Returns a boolean indicating whether the corner radius is equal to the other given corner radius. + /// + /// The other corner radius to test equality against. + /// True if this corner radius is equal to other; False otherwise. + public bool Equals(CornerRadius other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return TopLeft == other.TopLeft && + + TopRight == other.TopRight && + BottomRight == other.BottomRight && + BottomLeft == other.BottomLeft; + // ReSharper restore CompareOfFloatsByEqualityOperator + } + public override bool Equals(object obj) { - if (obj is CornerRadius) + if (!(obj is CornerRadius)) { - return this == (CornerRadius)obj; + return false; } - return false; + + return Equals((CornerRadius)obj); } public override int GetHashCode() @@ -61,7 +104,9 @@ namespace Avalonia public static CornerRadius Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Thickness")) + const string exceptionMessage = "Invalid CornerRadius."; + + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage)) { if (tokenizer.TryReadDouble(out var a)) { @@ -78,21 +123,18 @@ namespace Avalonia return new CornerRadius(a); } - throw new FormatException("Invalid CornerRadius."); + throw new FormatException(exceptionMessage); } } - public static bool operator ==(CornerRadius cr1, CornerRadius cr2) + public static bool operator ==(CornerRadius left, CornerRadius right) { - return cr1.TopLeft.Equals(cr2.TopLeft) - && cr1.TopRight.Equals(cr2.TopRight) - && cr1.BottomRight.Equals(cr2.BottomRight) - && cr1.BottomLeft.Equals(cr2.BottomLeft); + return left.Equals(right); } - public static bool operator !=(CornerRadius cr1, CornerRadius cr2) + public static bool operator !=(CornerRadius left, CornerRadius right) { - return !(cr1 == cr2); + return !(left == right); } } } diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index d083a2aaf8..6f9839b6a1 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -10,7 +10,7 @@ namespace Avalonia /// /// A 2x3 matrix. /// - public readonly struct Matrix + public readonly struct Matrix : IEquatable { private readonly double _m11; private readonly double _m12; @@ -235,12 +235,14 @@ namespace Avalonia /// True if this matrix is equal to other; False otherwise. public bool Equals(Matrix other) { + // ReSharper disable CompareOfFloatsByEqualityOperator return _m11 == other.M11 && _m12 == other.M12 && _m21 == other.M21 && _m22 == other.M22 && _m31 == other.M31 && _m32 == other.M32; + // ReSharper restore CompareOfFloatsByEqualityOperator } /// @@ -316,7 +318,7 @@ namespace Avalonia /// The . public static Matrix Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix.")) { return new Matrix( tokenizer.ReadDouble(), diff --git a/src/Avalonia.Visuals/Media/PixelPoint.cs b/src/Avalonia.Visuals/Media/PixelPoint.cs index d62c2a2e55..5a329d0238 100644 --- a/src/Avalonia.Visuals/Media/PixelPoint.cs +++ b/src/Avalonia.Visuals/Media/PixelPoint.cs @@ -10,7 +10,7 @@ namespace Avalonia /// /// Represents a point in device pixels. /// - public readonly struct PixelPoint + public readonly struct PixelPoint : IEquatable { /// /// A point representing 0,0. @@ -46,7 +46,7 @@ namespace Avalonia /// True if the points are equal; otherwise false. public static bool operator ==(PixelPoint left, PixelPoint right) { - return left.X == right.X && left.Y == right.Y; + return left.Equals(right); } /// @@ -120,7 +120,7 @@ namespace Avalonia /// The . public static PixelPoint Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelPoint")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelPoint.")) { return new PixelPoint( tokenizer.ReadInt32(), @@ -128,6 +128,18 @@ namespace Avalonia } } + /// + /// Returns a boolean indicating whether the point is equal to the other given point. + /// + /// The other point to test equality against. + /// True if this point is equal to other; False otherwise. + public bool Equals(PixelPoint other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return X == other.X && Y == other.Y; + // ReSharper restore CompareOfFloatsByEqualityOperator + } + /// /// Checks for equality between a point and an object. /// @@ -139,7 +151,7 @@ namespace Avalonia { if (obj is PixelPoint other) { - return this == other; + return Equals(other); } return false; diff --git a/src/Avalonia.Visuals/Media/PixelRect.cs b/src/Avalonia.Visuals/Media/PixelRect.cs index 0e2094da07..b830f4b4b4 100644 --- a/src/Avalonia.Visuals/Media/PixelRect.cs +++ b/src/Avalonia.Visuals/Media/PixelRect.cs @@ -10,7 +10,7 @@ namespace Avalonia /// /// Represents a rectangle in device pixels. /// - public readonly struct PixelRect + public readonly struct PixelRect : IEquatable { /// /// An empty rectangle. @@ -148,7 +148,7 @@ namespace Avalonia /// True if the rects are equal; otherwise false. public static bool operator ==(PixelRect left, PixelRect right) { - return left.Position == right.Position && left.Size == right.Size; + return left.Equals(right); } /// @@ -196,6 +196,16 @@ namespace Avalonia rect.Height); } + /// + /// Returns a boolean indicating whether the rect is equal to the other given rect. + /// + /// The other rect to test equality against. + /// True if this rect is equal to other; False otherwise. + public bool Equals(PixelRect other) + { + return Position == other.Position && Size == other.Size; + } + /// /// Returns a boolean indicating whether the given object is equal to this rectangle. /// @@ -205,7 +215,7 @@ namespace Avalonia { if (obj is PixelRect other) { - return this == other; + return Equals(other); } return false; @@ -432,7 +442,7 @@ namespace Avalonia /// The parsed . public static PixelRect Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelRect")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelRect.")) { return new PixelRect( tokenizer.ReadInt32(), diff --git a/src/Avalonia.Visuals/Media/PixelSize.cs b/src/Avalonia.Visuals/Media/PixelSize.cs index b903b804f9..e2d6b46225 100644 --- a/src/Avalonia.Visuals/Media/PixelSize.cs +++ b/src/Avalonia.Visuals/Media/PixelSize.cs @@ -10,7 +10,7 @@ namespace Avalonia /// /// Represents a size in device pixels. /// - public readonly struct PixelSize + public readonly struct PixelSize : IEquatable { /// /// A size representing zero @@ -51,7 +51,7 @@ namespace Avalonia /// True if the sizes are equal; otherwise false. public static bool operator ==(PixelSize left, PixelSize right) { - return left.Width == right.Width && left.Height == right.Height; + return left.Equals(right); } /// @@ -72,7 +72,7 @@ namespace Avalonia /// The . public static PixelSize Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelSize")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelSize.")) { return new PixelSize( tokenizer.ReadInt32(), @@ -80,6 +80,16 @@ namespace Avalonia } } + /// + /// Returns a boolean indicating whether the size is equal to the other given size. + /// + /// The other size to test equality against. + /// True if this size is equal to other; False otherwise. + public bool Equals(PixelSize other) + { + return Width == other.Width && Height == other.Height; + } + /// /// Checks for equality between a size and an object. /// @@ -91,7 +101,7 @@ namespace Avalonia { if (obj is PixelSize other) { - return this == other; + return Equals(other); } return false; diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index 0d3e354615..0d4dcb7c73 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.Globalization; using Avalonia.Animation.Animators; using Avalonia.Utilities; @@ -10,7 +11,7 @@ namespace Avalonia /// /// Defines a point. /// - public readonly struct Point + public readonly struct Point : IEquatable { static Point() { @@ -75,7 +76,7 @@ namespace Avalonia /// True if the points are equal; otherwise false. public static bool operator ==(Point left, Point right) { - return left.X == right.X && left.Y == right.Y; + return left.Equals(right); } /// @@ -177,7 +178,7 @@ namespace Avalonia /// The . public static Point Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point.")) { return new Point( tokenizer.ReadDouble(), @@ -186,6 +187,19 @@ namespace Avalonia } } + /// + /// Returns a boolean indicating whether the point is equal to the other given point. + /// + /// The other point to test equality against. + /// True if this point is equal to other; False otherwise. + public bool Equals(Point other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return _x == other._x && + _y == other._y; + // ReSharper enable CompareOfFloatsByEqualityOperator + } + /// /// Checks for equality between a point and an object. /// @@ -195,13 +209,12 @@ namespace Avalonia /// public override bool Equals(object obj) { - if (obj is Point) + if (!(obj is Point)) { - var other = (Point)obj; - return X == other.X && Y == other.Y; + return false; } - return false; + return Equals((Point)obj); } /// diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index 8f08f7f51f..94fd2afb3d 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -11,7 +11,7 @@ namespace Avalonia /// /// Defines a rectangle. /// - public readonly struct Rect + public readonly struct Rect : IEquatable { static Rect() { @@ -164,7 +164,9 @@ namespace Avalonia /// /// Gets a value that indicates whether the rectangle is empty. /// + // ReSharper disable CompareOfFloatsByEqualityOperator public bool IsEmpty => _width == 0 && _height == 0; + // ReSharper restore CompareOfFloatsByEqualityOperator /// /// Checks for equality between two s. @@ -174,7 +176,7 @@ namespace Avalonia /// True if the rects are equal; otherwise false. public static bool operator ==(Rect left, Rect right) { - return left.Position == right.Position && left.Size == right.Size; + return left.Equals(right); } /// @@ -297,6 +299,21 @@ namespace Avalonia Size.Deflate(thickness)); } + /// + /// Returns a boolean indicating whether the rect is equal to the other given rect. + /// + /// The other rect to test equality against. + /// True if this rect is equal to other; False otherwise. + public bool Equals(Rect other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return _x == other._x && + _y == other._y && + _width == other._width && + _height == other._height; + // ReSharper enable CompareOfFloatsByEqualityOperator + } + /// /// Returns a boolean indicating whether the given object is equal to this rectangle. /// @@ -304,13 +321,12 @@ namespace Avalonia /// True if the object is equal to this rectangle; false otherwise. public override bool Equals(object obj) { - if (obj is Rect) + if (!(obj is Rect)) { - var other = (Rect)obj; - return Position == other.Position && Size == other.Size; + return false; } - return false; + return Equals((Rect)obj); } /// @@ -422,10 +438,10 @@ namespace Avalonia } else { - var x1 = Math.Min(this.X, rect.X); - var x2 = Math.Max(this.Right, rect.Right); - var y1 = Math.Min(this.Y, rect.Y); - var y2 = Math.Max(this.Bottom, rect.Bottom); + var x1 = Math.Min(X, rect.X); + var x2 = Math.Max(Right, rect.Right); + var y1 = Math.Min(Y, rect.Y); + var y2 = Math.Max(Bottom, rect.Bottom); return new Rect(new Point(x1, y1), new Point(x2, y2)); } @@ -493,7 +509,7 @@ namespace Avalonia /// The parsed . public static Rect Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect.")) { return new Rect( tokenizer.ReadDouble(), diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs index d38bb1d496..c822486767 100644 --- a/src/Avalonia.Visuals/RelativePoint.cs +++ b/src/Avalonia.Visuals/RelativePoint.cs @@ -130,10 +130,7 @@ namespace Avalonia { unchecked { - int hash = 17; - hash = (hash * 23) + Unit.GetHashCode(); - hash = (hash * 23) + Point.GetHashCode(); - return hash; + return (_point.GetHashCode() * 397) ^ (int)_unit; } } @@ -156,7 +153,7 @@ namespace Avalonia /// The parsed . public static RelativePoint Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint.")) { var x = tokenizer.ReadString(); var y = tokenizer.ReadString(); diff --git a/src/Avalonia.Visuals/RelativeRect.cs b/src/Avalonia.Visuals/RelativeRect.cs index 927ec3ef75..c69dab3c42 100644 --- a/src/Avalonia.Visuals/RelativeRect.cs +++ b/src/Avalonia.Visuals/RelativeRect.cs @@ -139,10 +139,7 @@ namespace Avalonia { unchecked { - int hash = 17; - hash = (hash * 23) + Unit.GetHashCode(); - hash = (hash * 23) + Rect.GetHashCode(); - return hash; + return ((int)Unit * 397) ^ Rect.GetHashCode(); } } @@ -161,7 +158,7 @@ namespace Avalonia Rect.Width * size.Width, Rect.Height * size.Height); } - + /// /// Parses a string. /// @@ -169,7 +166,7 @@ namespace Avalonia /// The parsed . public static RelativeRect Parse(string s) { - using (var tokenizer = new StringTokenizer(s, exceptionMessage: "Invalid RelativeRect")) + using (var tokenizer = new StringTokenizer(s, exceptionMessage: "Invalid RelativeRect.")) { var x = tokenizer.ReadString(); var y = tokenizer.ReadString(); diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs index 782c5ea67b..9d524d6fa7 100644 --- a/src/Avalonia.Visuals/Size.cs +++ b/src/Avalonia.Visuals/Size.cs @@ -11,7 +11,7 @@ namespace Avalonia /// /// Defines a size. /// - public readonly struct Size + public readonly struct Size : IEquatable { static Size() { @@ -72,7 +72,7 @@ namespace Avalonia /// True if the sizes are equal; otherwise false. public static bool operator ==(Size left, Size right) { - return left._width == right._width && left._height == right._height; + return left.Equals(right); } /// @@ -158,7 +158,7 @@ namespace Avalonia /// The . public static Size Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Size")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Size.")) { return new Size( tokenizer.ReadDouble(), @@ -191,6 +191,19 @@ namespace Avalonia Math.Max(0, _height - thickness.Top - thickness.Bottom)); } + /// + /// Returns a boolean indicating whether the size is equal to the other given size. + /// + /// The other size to test equality against. + /// True if this size is equal to other; False otherwise. + public bool Equals(Size other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return _width == other._width && + _height == other._height; + // ReSharper enable CompareOfFloatsByEqualityOperator + } + /// /// Checks for equality between a size and an object. /// @@ -200,13 +213,12 @@ namespace Avalonia /// public override bool Equals(object obj) { - if (obj is Size) + if (!(obj is Size)) { - var other = (Size)obj; - return Width == other.Width && Height == other.Height; + return false; } - return false; + return Equals((Size)obj); } /// diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs index 830ee4666e..d0fc63e254 100644 --- a/src/Avalonia.Visuals/Thickness.cs +++ b/src/Avalonia.Visuals/Thickness.cs @@ -3,7 +3,6 @@ using System; using System.Globalization; -using Avalonia.Animation; using Avalonia.Animation.Animators; using Avalonia.Utilities; @@ -12,7 +11,7 @@ namespace Avalonia /// /// Describes the thickness of a frame around a rectangle. /// - public readonly struct Thickness + public readonly struct Thickness : IEquatable { static Thickness() { @@ -204,7 +203,9 @@ namespace Avalonia /// The . public static Thickness Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Thickness")) + const string exceptionMessage = "Invalid Thickness."; + + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage)) { if (tokenizer.TryReadDouble(out var a)) { @@ -221,10 +222,25 @@ namespace Avalonia return new Thickness(a); } - throw new FormatException("Invalid Thickness."); + throw new FormatException(exceptionMessage); } } + /// + /// Returns a boolean indicating whether the thickness is equal to the other given point. + /// + /// The other thickness to test equality against. + /// True if this thickness is equal to other; False otherwise. + public bool Equals(Thickness other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return _left == other._left && + _top == other._top && + _right == other._right && + _bottom == other._bottom; + // ReSharper restore CompareOfFloatsByEqualityOperator + } + /// /// Checks for equality between a thickness and an object. /// @@ -234,16 +250,12 @@ namespace Avalonia /// public override bool Equals(object obj) { - if (obj is Thickness) + if (!(obj is Thickness)) { - Thickness other = (Thickness)obj; - return Left == other.Left && - Top == other.Top && - Right == other.Right && - Bottom == other.Bottom; + return false; } - return false; + return Equals((Thickness)obj); } /// diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 11bda8b00e..bd2dfdc828 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -11,7 +11,7 @@ namespace Avalonia /// /// Defines a vector. /// - public readonly struct Vector + public readonly struct Vector : IEquatable { static Vector() { diff --git a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs index 39b328adc2..5fb22680dc 100644 --- a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs @@ -1,13 +1,14 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; namespace Avalonia.VisualTree { /// /// Holds information about the bounds of a control, together with a transform and a clip. /// - public readonly struct TransformedBounds + public readonly struct TransformedBounds : IEquatable { /// /// Initializes a new instance of the struct. From 58af05644088e9590dc03d40f27abf6aa207d4fd Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 8 Oct 2019 01:01:41 +0200 Subject: [PATCH 0317/1791] Streamline object.Equals implementation. --- src/Avalonia.Visuals/CornerRadius.cs | 15 ++++++--------- src/Avalonia.Visuals/Matrix.cs | 10 +--------- src/Avalonia.Visuals/Media/PixelPoint.cs | 10 +--------- src/Avalonia.Visuals/Media/PixelRect.cs | 10 +--------- src/Avalonia.Visuals/Media/PixelSize.cs | 10 +--------- src/Avalonia.Visuals/Point.cs | 10 +--------- src/Avalonia.Visuals/Rect.cs | 10 +--------- src/Avalonia.Visuals/RelativePoint.cs | 5 +---- src/Avalonia.Visuals/RelativeRect.cs | 5 +---- src/Avalonia.Visuals/Size.cs | 10 +--------- src/Avalonia.Visuals/Thickness.cs | 10 +--------- src/Avalonia.Visuals/Vector.cs | 9 +-------- .../VisualTree/TransformedBounds.cs | 10 +--------- 13 files changed, 18 insertions(+), 106 deletions(-) diff --git a/src/Avalonia.Visuals/CornerRadius.cs b/src/Avalonia.Visuals/CornerRadius.cs index 5445c5a200..ecb9e75d82 100644 --- a/src/Avalonia.Visuals/CornerRadius.cs +++ b/src/Avalonia.Visuals/CornerRadius.cs @@ -82,15 +82,12 @@ namespace Avalonia // ReSharper restore CompareOfFloatsByEqualityOperator } - public override bool Equals(object obj) - { - if (!(obj is CornerRadius)) - { - return false; - } - - return Equals((CornerRadius)obj); - } + /// + /// Returns a boolean indicating whether the given Object is equal to this corner radius instance. + /// + /// The Object to compare against. + /// True if the Object is equal to this corner radius; False otherwise. + public override bool Equals(object obj) => obj is CornerRadius other && Equals(other); public override int GetHashCode() { diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index 6f9839b6a1..92b7dae904 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -250,15 +250,7 @@ namespace Avalonia /// /// The Object to compare against. /// True if the Object is equal to this matrix; False otherwise. - public override bool Equals(object obj) - { - if (!(obj is Matrix)) - { - return false; - } - - return Equals((Matrix)obj); - } + public override bool Equals(object obj) => obj is Matrix other && Equals(other); /// /// Returns the hash code for this instance. diff --git a/src/Avalonia.Visuals/Media/PixelPoint.cs b/src/Avalonia.Visuals/Media/PixelPoint.cs index 5a329d0238..5384a40be9 100644 --- a/src/Avalonia.Visuals/Media/PixelPoint.cs +++ b/src/Avalonia.Visuals/Media/PixelPoint.cs @@ -147,15 +147,7 @@ namespace Avalonia /// /// True if is a point that equals the current point. /// - public override bool Equals(object obj) - { - if (obj is PixelPoint other) - { - return Equals(other); - } - - return false; - } + public override bool Equals(object obj) => obj is PixelPoint other && Equals(other); /// /// Returns a hash code for a . diff --git a/src/Avalonia.Visuals/Media/PixelRect.cs b/src/Avalonia.Visuals/Media/PixelRect.cs index b830f4b4b4..a024e1af11 100644 --- a/src/Avalonia.Visuals/Media/PixelRect.cs +++ b/src/Avalonia.Visuals/Media/PixelRect.cs @@ -211,15 +211,7 @@ namespace Avalonia /// /// The object to compare against. /// True if the object is equal to this rectangle; false otherwise. - public override bool Equals(object obj) - { - if (obj is PixelRect other) - { - return Equals(other); - } - - return false; - } + public override bool Equals(object obj) => obj is PixelRect other && Equals(other); /// /// Returns the hash code for this instance. diff --git a/src/Avalonia.Visuals/Media/PixelSize.cs b/src/Avalonia.Visuals/Media/PixelSize.cs index e2d6b46225..a49aa82c0d 100644 --- a/src/Avalonia.Visuals/Media/PixelSize.cs +++ b/src/Avalonia.Visuals/Media/PixelSize.cs @@ -97,15 +97,7 @@ namespace Avalonia /// /// True if is a size that equals the current size. /// - public override bool Equals(object obj) - { - if (obj is PixelSize other) - { - return Equals(other); - } - - return false; - } + public override bool Equals(object obj) => obj is PixelSize other && Equals(other); /// /// Returns a hash code for a . diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index 0d4dcb7c73..d92f8b0fc4 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -207,15 +207,7 @@ namespace Avalonia /// /// True if is a point that equals the current point. /// - public override bool Equals(object obj) - { - if (!(obj is Point)) - { - return false; - } - - return Equals((Point)obj); - } + public override bool Equals(object obj) => obj is Point other && Equals(other); /// /// Returns a hash code for a . diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index 94fd2afb3d..4dfd641525 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -319,15 +319,7 @@ namespace Avalonia /// /// The object to compare against. /// True if the object is equal to this rectangle; false otherwise. - public override bool Equals(object obj) - { - if (!(obj is Rect)) - { - return false; - } - - return Equals((Rect)obj); - } + public override bool Equals(object obj) => obj is Rect other && Equals(other); /// /// Returns the hash code for this instance. diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs index c822486767..2e8fb16bc1 100644 --- a/src/Avalonia.Visuals/RelativePoint.cs +++ b/src/Avalonia.Visuals/RelativePoint.cs @@ -107,10 +107,7 @@ namespace Avalonia /// /// The other object. /// True if the objects are equal, otherwise false. - public override bool Equals(object obj) - { - return (obj is RelativePoint) && Equals((RelativePoint)obj); - } + public override bool Equals(object obj) => obj is RelativePoint other && Equals(other); /// /// Checks if the equals another point. diff --git a/src/Avalonia.Visuals/RelativeRect.cs b/src/Avalonia.Visuals/RelativeRect.cs index c69dab3c42..d2e4b2dc26 100644 --- a/src/Avalonia.Visuals/RelativeRect.cs +++ b/src/Avalonia.Visuals/RelativeRect.cs @@ -116,10 +116,7 @@ namespace Avalonia /// /// The other object. /// True if the objects are equal, otherwise false. - public override bool Equals(object obj) - { - return (obj is RelativeRect) && Equals((RelativeRect)obj); - } + public override bool Equals(object obj) => obj is RelativeRect other && Equals(other); /// /// Checks if the equals another rectangle. diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs index 9d524d6fa7..aba2ed8d62 100644 --- a/src/Avalonia.Visuals/Size.cs +++ b/src/Avalonia.Visuals/Size.cs @@ -211,15 +211,7 @@ namespace Avalonia /// /// True if is a size that equals the current size. /// - public override bool Equals(object obj) - { - if (!(obj is Size)) - { - return false; - } - - return Equals((Size)obj); - } + public override bool Equals(object obj) => obj is Size other && Equals(other); /// /// Returns a hash code for a . diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs index d0fc63e254..44ff66069f 100644 --- a/src/Avalonia.Visuals/Thickness.cs +++ b/src/Avalonia.Visuals/Thickness.cs @@ -248,15 +248,7 @@ namespace Avalonia /// /// True if is a size that equals the current size. /// - public override bool Equals(object obj) - { - if (!(obj is Thickness)) - { - return false; - } - - return Equals((Thickness)obj); - } + public override bool Equals(object obj) => obj is Thickness other && Equals(other); /// /// Returns a hash code for a . diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index bd2dfdc828..576d2daaaa 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -138,7 +138,6 @@ namespace Avalonia /// /// The other vector. /// True if vectors are nearly equal. - [Pure] public bool NearlyEquals(Vector other) { const float tolerance = float.Epsilon; @@ -146,13 +145,7 @@ namespace Avalonia return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance; } - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - return false; - - return obj is Vector vector && Equals(vector); - } + public override bool Equals(object obj) => obj is Vector other && Equals(other); public override int GetHashCode() { diff --git a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs index 5fb22680dc..b2121aa8da 100644 --- a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs @@ -57,15 +57,7 @@ namespace Avalonia.VisualTree return Bounds == other.Bounds && Clip == other.Clip && Transform == other.Transform; } - public override bool Equals(object obj) - { - if (obj is null) - { - return false; - } - - return obj is TransformedBounds other && Equals(other); - } + public override bool Equals(object obj) => obj is TransformedBounds other && Equals(other); public override int GetHashCode() { From d600ffcb7e3f6565770e1716c104067382c680c6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 8 Oct 2019 14:33:06 +0200 Subject: [PATCH 0318/1791] Added Window.OnClosed and sealed HandleClosed. --- src/Avalonia.Controls/TopLevel.cs | 8 +++++++- src/Avalonia.Controls/Window.cs | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index c54ebd5360..293809bf51 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -271,7 +271,7 @@ namespace Avalonia.Controls { (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; - Closed?.Invoke(this, EventArgs.Empty); + OnClosed(EventArgs.Empty); Renderer?.Dispose(); Renderer = null; } @@ -317,6 +317,12 @@ namespace Avalonia.Controls /// The event args. protected virtual void OnOpened(EventArgs e) => Opened?.Invoke(this, e); + /// + /// Raises the event. + /// + /// The event args. + protected virtual void OnClosed(EventArgs e) => Closed?.Invoke(this, e); + /// /// Tries to get a service from an , logging a /// warning if not found. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ef43746665..ec2fca0db5 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -557,7 +557,7 @@ namespace Avalonia.Controls return result; } - protected override void HandleClosed() + protected sealed override void HandleClosed() { RaiseEvent(new RoutedEventArgs(WindowClosedEvent)); @@ -565,7 +565,7 @@ namespace Avalonia.Controls } /// - protected override void HandleResized(Size clientSize) + protected sealed override void HandleResized(Size clientSize) { if (!AutoSizing) { From 918af64ea4ad430b146451d7e34ad54c5bfd54f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Tue, 8 Oct 2019 21:10:48 +0100 Subject: [PATCH 0319/1791] Added missing AffectsMeasure to UniformGrid. --- src/Avalonia.Controls/Primitives/UniformGrid.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/UniformGrid.cs b/src/Avalonia.Controls/Primitives/UniformGrid.cs index f3580eee10..09554412db 100644 --- a/src/Avalonia.Controls/Primitives/UniformGrid.cs +++ b/src/Avalonia.Controls/Primitives/UniformGrid.cs @@ -28,6 +28,11 @@ namespace Avalonia.Controls.Primitives private int _rows; private int _columns; + static UniformGrid() + { + AffectsMeasure(RowsProperty, ColumnsProperty, FirstColumnProperty); + } + /// /// Specifies the row count. If set to 0, row count will be calculated automatically. /// From a2c6bc1b7cb052d2cd2b746d32c290e6c2e6232d Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Wed, 9 Oct 2019 00:06:39 +0200 Subject: [PATCH 0320/1791] Remove usage of AddClassHandler that returns new lambda per invocation. --- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 9 ++-- src/Avalonia.Controls.DataGrid/DataGrid.cs | 46 +++++++++---------- .../DataGridCell.cs | 4 +- .../DataGridColumnHeader.cs | 2 +- src/Avalonia.Controls.DataGrid/DataGridRow.cs | 8 ++-- .../DataGridRowGroupHeader.cs | 4 +- src/Avalonia.Controls/AutoCompleteBox.cs | 18 ++++---- src/Avalonia.Controls/Calendar/Calendar.cs | 23 +++++----- src/Avalonia.Controls/Calendar/DatePicker.cs | 16 +++---- src/Avalonia.Controls/ComboBox.cs | 4 +- src/Avalonia.Controls/ContentControl.cs | 2 +- src/Avalonia.Controls/DataValidationErrors.cs | 2 +- src/Avalonia.Controls/Decorator.cs | 2 +- src/Avalonia.Controls/Expander.cs | 2 +- src/Avalonia.Controls/ItemsControl.cs | 4 +- .../LayoutTransformControl.cs | 8 ++-- src/Avalonia.Controls/MenuBase.cs | 2 +- src/Avalonia.Controls/MenuItem.cs | 12 ++--- .../Presenters/CarouselPresenter.cs | 4 +- .../Presenters/ContentPresenter.cs | 6 +-- .../Presenters/ItemsPresenter.cs | 2 +- .../Presenters/ItemsPresenterBase.cs | 2 +- .../Presenters/ScrollContentPresenter.cs | 2 +- .../Primitives/HeaderedContentControl.cs | 2 +- .../Primitives/HeaderedItemsControl.cs | 2 +- .../HeaderedSelectingItemsControl.cs | 2 +- src/Avalonia.Controls/Primitives/Popup.cs | 4 +- src/Avalonia.Controls/Primitives/ScrollBar.cs | 4 +- .../Primitives/SelectingItemsControl.cs | 2 +- .../Primitives/TemplatedControl.cs | 2 +- src/Avalonia.Controls/Primitives/Thumb.cs | 6 +-- src/Avalonia.Controls/Primitives/Track.cs | 6 +-- src/Avalonia.Controls/ProgressBar.cs | 5 +- src/Avalonia.Controls/ScrollViewer.cs | 4 +- src/Avalonia.Controls/Slider.cs | 6 +-- src/Avalonia.Controls/TabItem.cs | 4 +- src/Avalonia.Controls/TreeViewItem.cs | 2 +- src/Avalonia.Controls/WindowBase.cs | 2 +- src/Avalonia.Input/InputElement.cs | 24 +++++----- src/Avalonia.Interactivity/RoutedEvent.cs | 30 ++++++++---- src/Avalonia.Styling/StyledElement.cs | 2 +- src/Avalonia.Visuals/Media/Geometry.cs | 2 +- .../InteractiveTests.cs | 2 +- 43 files changed, 156 insertions(+), 141 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 393482cccf..ad1cefd4ea 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -192,9 +192,9 @@ namespace Avalonia { return observable.Subscribe(e => { - if (e.Sender is TTarget) + if (e.Sender is TTarget target) { - action((TTarget)e.Sender, e); + action(target, e); } }); } @@ -207,6 +207,7 @@ namespace Avalonia /// The property changed observable. /// Given a TTarget, returns the handler. /// A disposable that can be used to terminate the subscription. + [Obsolete("Use overload taking Action.")] public static IDisposable AddClassHandler( this IObservable observable, Func> handler) @@ -238,9 +239,7 @@ namespace Avalonia Func> handler) where TTarget : class { - var target = e.Sender as TTarget; - - if (target != null) + if (e.Sender is TTarget target) { handler(target)(e); } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 490a724eda..a6aaed1e80 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -723,29 +723,29 @@ namespace Avalonia.Controls PseudoClass(IsValidProperty, x => !x, ":invalid"); - ItemsProperty.Changed.AddClassHandler(x => x.OnItemsPropertyChanged); - CanUserResizeColumnsProperty.Changed.AddClassHandler(x => x.OnCanUserResizeColumnsChanged); - ColumnWidthProperty.Changed.AddClassHandler(x => x.OnColumnWidthChanged); - RowBackgroundProperty.Changed.AddClassHandler(x => x.OnRowBackgroundChanged); - AlternatingRowBackgroundProperty.Changed.AddClassHandler(x => x.OnRowBackgroundChanged); - FrozenColumnCountProperty.Changed.AddClassHandler(x => x.OnFrozenColumnCountChanged); - GridLinesVisibilityProperty.Changed.AddClassHandler(x => x.OnGridLinesVisibilityChanged); - HeadersVisibilityProperty.Changed.AddClassHandler(x => x.OnHeadersVisibilityChanged); - HorizontalGridLinesBrushProperty.Changed.AddClassHandler(x => x.OnHorizontalGridLinesBrushChanged); - IsReadOnlyProperty.Changed.AddClassHandler(x => x.OnIsReadOnlyChanged); - MaxColumnWidthProperty.Changed.AddClassHandler(x => x.OnMaxColumnWidthChanged); - MinColumnWidthProperty.Changed.AddClassHandler(x => x.OnMinColumnWidthChanged); - RowHeightProperty.Changed.AddClassHandler(x => x.OnRowHeightChanged); - RowHeaderWidthProperty.Changed.AddClassHandler(x => x.OnRowHeaderWidthChanged); - SelectionModeProperty.Changed.AddClassHandler(x => x.OnSelectionModeChanged); - VerticalGridLinesBrushProperty.Changed.AddClassHandler(x => x.OnVerticalGridLinesBrushChanged); - SelectedIndexProperty.Changed.AddClassHandler(x => x.OnSelectedIndexChanged); - SelectedItemProperty.Changed.AddClassHandler(x => x.OnSelectedItemChanged); - IsEnabledProperty.Changed.AddClassHandler(x => x.DataGrid_IsEnabledChanged); - AreRowGroupHeadersFrozenProperty.Changed.AddClassHandler(x => x.OnAreRowGroupHeadersFrozenChanged); - RowDetailsTemplateProperty.Changed.AddClassHandler(x => x.OnRowDetailsTemplateChanged); - RowDetailsVisibilityModeProperty.Changed.AddClassHandler(x => x.OnRowDetailsVisibilityModeChanged); - AutoGenerateColumnsProperty.Changed.AddClassHandler(x => x.OnAutoGenerateColumnsChanged); + ItemsProperty.Changed.AddClassHandler((x,e) => x.OnItemsPropertyChanged(e)); + CanUserResizeColumnsProperty.Changed.AddClassHandler((x,e) => x.OnCanUserResizeColumnsChanged(e)); + ColumnWidthProperty.Changed.AddClassHandler((x,e) => x.OnColumnWidthChanged(e)); + RowBackgroundProperty.Changed.AddClassHandler((x,e) => x.OnRowBackgroundChanged(e)); + AlternatingRowBackgroundProperty.Changed.AddClassHandler((x,e) => x.OnRowBackgroundChanged(e)); + FrozenColumnCountProperty.Changed.AddClassHandler((x,e) => x.OnFrozenColumnCountChanged(e)); + GridLinesVisibilityProperty.Changed.AddClassHandler((x,e) => x.OnGridLinesVisibilityChanged(e)); + HeadersVisibilityProperty.Changed.AddClassHandler((x,e) => x.OnHeadersVisibilityChanged(e)); + HorizontalGridLinesBrushProperty.Changed.AddClassHandler((x,e) => x.OnHorizontalGridLinesBrushChanged(e)); + IsReadOnlyProperty.Changed.AddClassHandler((x,e) => x.OnIsReadOnlyChanged(e)); + MaxColumnWidthProperty.Changed.AddClassHandler((x,e) => x.OnMaxColumnWidthChanged(e)); + MinColumnWidthProperty.Changed.AddClassHandler((x,e) => x.OnMinColumnWidthChanged(e)); + RowHeightProperty.Changed.AddClassHandler((x,e) => x.OnRowHeightChanged(e)); + RowHeaderWidthProperty.Changed.AddClassHandler((x,e) => x.OnRowHeaderWidthChanged(e)); + SelectionModeProperty.Changed.AddClassHandler((x,e) => x.OnSelectionModeChanged(e)); + VerticalGridLinesBrushProperty.Changed.AddClassHandler((x,e) => x.OnVerticalGridLinesBrushChanged(e)); + SelectedIndexProperty.Changed.AddClassHandler((x,e) => x.OnSelectedIndexChanged(e)); + SelectedItemProperty.Changed.AddClassHandler((x,e) => x.OnSelectedItemChanged(e)); + IsEnabledProperty.Changed.AddClassHandler((x,e) => x.DataGrid_IsEnabledChanged(e)); + AreRowGroupHeadersFrozenProperty.Changed.AddClassHandler((x,e) => x.OnAreRowGroupHeadersFrozenChanged(e)); + RowDetailsTemplateProperty.Changed.AddClassHandler((x,e) => x.OnRowDetailsTemplateChanged(e)); + RowDetailsVisibilityModeProperty.Changed.AddClassHandler((x,e) => x.OnRowDetailsVisibilityModeChanged(e)); + AutoGenerateColumnsProperty.Changed.AddClassHandler((x,e) => x.OnAutoGenerateColumnsChanged(e)); } /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index a21583b38e..e56c534f50 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls static DataGridCell() { PointerPressedEvent.AddClassHandler( - x => x.DataGridCell_PointerPressed, handledEventsToo: true); + (x,e) => x.DataGridCell_PointerPressed(e), handledEventsToo: true); } public DataGridCell() { } @@ -219,4 +219,4 @@ namespace Avalonia.Controls } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 6cb0807e29..4c77c8b5ae 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls static DataGridColumnHeader() { - AreSeparatorsVisibleProperty.Changed.AddClassHandler(x => x.OnAreSeparatorsVisibleChanged); + AreSeparatorsVisibleProperty.Changed.AddClassHandler((x,e) => x.OnAreSeparatorsVisibleChanged(e)); } /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index 04a1575486..c9924660be 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -116,10 +116,10 @@ namespace Avalonia.Controls static DataGridRow() { - HeaderProperty.Changed.AddClassHandler(x => x.OnHeaderChanged); - DetailsTemplateProperty.Changed.AddClassHandler(x => x.OnDetailsTemplateChanged); - AreDetailsVisibleProperty.Changed.AddClassHandler(x => x.OnAreDetailsVisibleChanged); - PointerPressedEvent.AddClassHandler(x => x.DataGridRow_PointerPressed, handledEventsToo: true); + HeaderProperty.Changed.AddClassHandler((x, e) => x.OnHeaderChanged(e)); + DetailsTemplateProperty.Changed.AddClassHandler((x, e) => x.OnDetailsTemplateChanged(e)); + AreDetailsVisibleProperty.Changed.AddClassHandler((x, e) => x.OnAreDetailsVisibleChanged(e)); + PointerPressedEvent.AddClassHandler((x, e) => x.DataGridRow_PointerPressed(e), handledEventsToo: true); } /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 716997f62c..7dafef9d8b 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -109,7 +109,7 @@ namespace Avalonia.Controls static DataGridRowGroupHeader() { - SublevelIndentProperty.Changed.AddClassHandler(x => x.OnSublevelIndentChanged); + SublevelIndentProperty.Changed.AddClassHandler((x,e) => x.OnSublevelIndentChanged(e)); } /// @@ -446,4 +446,4 @@ namespace Avalonia.Controls } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 1e2fc9f9d0..ce4358648b 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -805,15 +805,15 @@ namespace Avalonia.Controls { FocusableProperty.OverrideDefaultValue(true); - MinimumPopulateDelayProperty.Changed.AddClassHandler(x => x.OnMinimumPopulateDelayChanged); - IsDropDownOpenProperty.Changed.AddClassHandler(x => x.OnIsDropDownOpenChanged); - SelectedItemProperty.Changed.AddClassHandler(x => x.OnSelectedItemPropertyChanged); - TextProperty.Changed.AddClassHandler(x => x.OnTextPropertyChanged); - SearchTextProperty.Changed.AddClassHandler(x => x.OnSearchTextPropertyChanged); - FilterModeProperty.Changed.AddClassHandler(x => x.OnFilterModePropertyChanged); - ItemFilterProperty.Changed.AddClassHandler(x => x.OnItemFilterPropertyChanged); - ItemsProperty.Changed.AddClassHandler(x => x.OnItemsPropertyChanged); - IsEnabledProperty.Changed.AddClassHandler(x => x.OnControlIsEnabledChanged); + MinimumPopulateDelayProperty.Changed.AddClassHandler((x,e) => x.OnMinimumPopulateDelayChanged(e)); + IsDropDownOpenProperty.Changed.AddClassHandler((x,e) => x.OnIsDropDownOpenChanged(e)); + SelectedItemProperty.Changed.AddClassHandler((x,e) => x.OnSelectedItemPropertyChanged(e)); + TextProperty.Changed.AddClassHandler((x,e) => x.OnTextPropertyChanged(e)); + SearchTextProperty.Changed.AddClassHandler((x,e) => x.OnSearchTextPropertyChanged(e)); + FilterModeProperty.Changed.AddClassHandler((x,e) => x.OnFilterModePropertyChanged(e)); + ItemFilterProperty.Changed.AddClassHandler((x,e) => x.OnItemFilterPropertyChanged(e)); + ItemsProperty.Changed.AddClassHandler((x,e) => x.OnItemsPropertyChanged(e)); + IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnControlIsEnabledChanged(e)); } /// diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 56805e1d6a..89b375996b 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -2057,18 +2057,17 @@ namespace Avalonia.Controls static Calendar() { - IsEnabledProperty.Changed.AddClassHandler(x => x.OnIsEnabledChanged); - FirstDayOfWeekProperty.Changed.AddClassHandler(x => x.OnFirstDayOfWeekChanged); - IsTodayHighlightedProperty.Changed.AddClassHandler(x => x.OnIsTodayHighlightedChanged); - DisplayModeProperty.Changed.AddClassHandler(x => x.OnDisplayModePropertyChanged); - SelectionModeProperty.Changed.AddClassHandler(x => x.OnSelectionModeChanged); - SelectedDateProperty.Changed.AddClassHandler(x => x.OnSelectedDateChanged); - DisplayDateProperty.Changed.AddClassHandler(x => x.OnDisplayDateChanged); - DisplayDateStartProperty.Changed.AddClassHandler(x => x.OnDisplayDateStartChanged); - DisplayDateEndProperty.Changed.AddClassHandler(x => x.OnDisplayDateEndChanged); - KeyDownEvent.AddClassHandler(x => x.Calendar_KeyDown); - KeyUpEvent.AddClassHandler(x => x.Calendar_KeyUp); - + IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnIsEnabledChanged(e)); + FirstDayOfWeekProperty.Changed.AddClassHandler((x,e) => x.OnFirstDayOfWeekChanged(e)); + IsTodayHighlightedProperty.Changed.AddClassHandler((x,e) => x.OnIsTodayHighlightedChanged(e)); + DisplayModeProperty.Changed.AddClassHandler((x,e) => x.OnDisplayModePropertyChanged(e)); + SelectionModeProperty.Changed.AddClassHandler((x,e) => x.OnSelectionModeChanged(e)); + SelectedDateProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateChanged(e)); + DisplayDateProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateChanged(e)); + DisplayDateStartProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateStartChanged(e)); + DisplayDateEndProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateEndChanged(e)); + KeyDownEvent.AddClassHandler((x,e) => x.Calendar_KeyDown(e)); + KeyUpEvent.AddClassHandler((x,e) => x.Calendar_KeyUp(e)); } /// diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index 55797ae1e3..841b73cd92 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -393,14 +393,14 @@ namespace Avalonia.Controls { FocusableProperty.OverrideDefaultValue(true); - DisplayDateProperty.Changed.AddClassHandler(x => x.OnDisplayDateChanged); - DisplayDateStartProperty.Changed.AddClassHandler(x => x.OnDisplayDateStartChanged); - DisplayDateEndProperty.Changed.AddClassHandler(x => x.OnDisplayDateEndChanged); - IsDropDownOpenProperty.Changed.AddClassHandler(x => x.OnIsDropDownOpenChanged); - SelectedDateProperty.Changed.AddClassHandler(x => x.OnSelectedDateChanged); - SelectedDateFormatProperty.Changed.AddClassHandler(x => x.OnSelectedDateFormatChanged); - CustomDateFormatStringProperty.Changed.AddClassHandler(x => x.OnCustomDateFormatStringChanged); - TextProperty.Changed.AddClassHandler(x => x.OnTextChanged); + DisplayDateProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateChanged(e)); + DisplayDateStartProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateStartChanged(e)); + DisplayDateEndProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateEndChanged(e)); + IsDropDownOpenProperty.Changed.AddClassHandler((x,e) => x.OnIsDropDownOpenChanged(e)); + SelectedDateProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateChanged(e)); + SelectedDateFormatProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateFormatChanged(e)); + CustomDateFormatStringProperty.Changed.AddClassHandler((x,e) => x.OnCustomDateFormatStringChanged(e)); + TextProperty.Changed.AddClassHandler((x,e) => x.OnTextChanged(e)); } /// /// Initializes a new instance of the diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index a70d26624c..c2cf20b32d 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -65,8 +65,8 @@ namespace Avalonia.Controls { ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); FocusableProperty.OverrideDefaultValue(true); - SelectedItemProperty.Changed.AddClassHandler(x => x.SelectedItemChanged); - KeyDownEvent.AddClassHandler(x => x.OnKeyDown, Interactivity.RoutingStrategies.Tunnel); + SelectedItemProperty.Changed.AddClassHandler((x,e) => x.SelectedItemChanged(e)); + KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel); } /// diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 02d7890404..bb3cc4585b 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -43,7 +43,7 @@ namespace Avalonia.Controls static ContentControl() { - ContentProperty.Changed.AddClassHandler(x => x.ContentChanged); + ContentProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e)); } /// diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs index 50b387e636..2588b7cc11 100644 --- a/src/Avalonia.Controls/DataValidationErrors.cs +++ b/src/Avalonia.Controls/DataValidationErrors.cs @@ -56,7 +56,7 @@ namespace Avalonia.Controls { ErrorsProperty.Changed.Subscribe(ErrorsChanged); HasErrorsProperty.Changed.Subscribe(HasErrorsChanged); - TemplatedParentProperty.Changed.AddClassHandler(x => x.OnTemplatedParentChange); + TemplatedParentProperty.Changed.AddClassHandler((x, e) => x.OnTemplatedParentChange(e)); } private void OnTemplatedParentChange(AvaloniaPropertyChangedEventArgs e) diff --git a/src/Avalonia.Controls/Decorator.cs b/src/Avalonia.Controls/Decorator.cs index 15651b918e..6f16870b9f 100644 --- a/src/Avalonia.Controls/Decorator.cs +++ b/src/Avalonia.Controls/Decorator.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls static Decorator() { AffectsMeasure(ChildProperty, PaddingProperty); - ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); + ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); } /// diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 1fa9798784..b2a442b6cc 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls PseudoClass(IsExpandedProperty, ":expanded"); - IsExpandedProperty.Changed.AddClassHandler(x => x.OnIsExpandedChanged); + IsExpandedProperty.Changed.AddClassHandler((x, e) => x.OnIsExpandedChanged(e)); } public IPageTransition ContentTransition diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 0fe7291835..558f496ede 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -64,8 +64,8 @@ namespace Avalonia.Controls /// static ItemsControl() { - ItemsProperty.Changed.AddClassHandler(x => x.ItemsChanged); - ItemTemplateProperty.Changed.AddClassHandler(x => x.ItemTemplateChanged); + ItemsProperty.Changed.AddClassHandler((x, e) => x.ItemsChanged(e)); + ItemTemplateProperty.Changed.AddClassHandler((x, e) => x.ItemTemplateChanged(e)); } /// diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 1430c39c76..db67a24159 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -28,11 +28,13 @@ namespace Avalonia.Controls ClipToBoundsProperty.OverrideDefaultValue(true); LayoutTransformProperty.Changed - .AddClassHandler(x => x.OnLayoutTransformChanged); + .AddClassHandler((x, e) => x.OnLayoutTransformChanged(e)); ChildProperty.Changed - .AddClassHandler(x => x.OnChildChanged); - UseRenderTransformProperty.Changed.AddClassHandler(x => x.OnUseRenderTransformPropertyChanged); + .AddClassHandler((x, e) => x.OnChildChanged(e)); + + UseRenderTransformProperty.Changed + .AddClassHandler((x, e) => x.OnUseRenderTransformPropertyChanged(e)); } /// diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index 8eed58bb4d..be677b5479 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls /// static MenuBase() { - MenuItem.SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); + MenuItem.SubmenuOpenedEvent.AddClassHandler((x, e) => x.OnSubmenuOpened(e)); } /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 33a708b6a5..8c82fed58d 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -102,13 +102,13 @@ namespace Avalonia.Controls SelectableMixin.Attach(IsSelectedProperty); CommandProperty.Changed.Subscribe(CommandChanged); FocusableProperty.OverrideDefaultValue(true); - HeaderProperty.Changed.AddClassHandler(x => x.HeaderChanged); - IconProperty.Changed.AddClassHandler(x => x.IconChanged); - IsSelectedProperty.Changed.AddClassHandler(x => x.IsSelectedChanged); + HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); + IconProperty.Changed.AddClassHandler((x, e) => x.IconChanged(e)); + IsSelectedProperty.Changed.AddClassHandler((x, e) => x.IsSelectedChanged(e)); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); - ClickEvent.AddClassHandler(x => x.OnClick); - SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); - IsSubMenuOpenProperty.Changed.AddClassHandler(x => x.SubMenuOpenChanged); + ClickEvent.AddClassHandler((x, e) => x.OnClick(e)); + SubmenuOpenedEvent.AddClassHandler((x, e) => x.OnSubmenuOpened(e)); + IsSubMenuOpenProperty.Changed.AddClassHandler((x, e) => x.SubMenuOpenChanged(e)); } public MenuItem() diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index dedab3e43e..89de24a81a 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -46,8 +46,8 @@ namespace Avalonia.Controls.Presenters /// static CarouselPresenter() { - IsVirtualizedProperty.Changed.AddClassHandler(x => x.IsVirtualizedChanged); - SelectedIndexProperty.Changed.AddClassHandler(x => x.SelectedIndexChanged); + IsVirtualizedProperty.Changed.AddClassHandler((x, e) => x.IsVirtualizedChanged(e)); + SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.SelectedIndexChanged(e)); } /// diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index a5374e7c5a..5e1a844720 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -94,9 +94,9 @@ namespace Avalonia.Controls.Presenters { AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty); AffectsMeasure(BorderThicknessProperty, PaddingProperty); - ContentProperty.Changed.AddClassHandler(x => x.ContentChanged); - ContentTemplateProperty.Changed.AddClassHandler(x => x.ContentChanged); - TemplatedParentProperty.Changed.AddClassHandler(x => x.TemplatedParentChanged); + ContentProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e)); + ContentTemplateProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e)); + TemplatedParentProperty.Changed.AddClassHandler((x, e) => x.TemplatedParentChanged(e)); } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 500c7aa187..7a5451821e 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls.Presenters KeyboardNavigationMode.Once); VirtualizationModeProperty.Changed - .AddClassHandler(x => x.VirtualizationModeChanged); + .AddClassHandler((x,e) => x.VirtualizationModeChanged(e)); } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index ea56a0c6fc..0f0cdc37cf 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -45,7 +45,7 @@ namespace Avalonia.Controls.Presenters /// static ItemsPresenterBase() { - TemplatedParentProperty.Changed.AddClassHandler(x => x.TemplatedParentChanged); + TemplatedParentProperty.Changed.AddClassHandler((x,e) => x.TemplatedParentChanged(e)); } /// diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index ec6a228421..6fffc3741a 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -73,7 +73,7 @@ namespace Avalonia.Controls.Presenters static ScrollContentPresenter() { ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); - ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); + ChildProperty.Changed.AddClassHandler((x,e) => x.ChildChanged(e)); AffectsArrange(OffsetProperty); } diff --git a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs index 3cf50a7b80..d431420a8f 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs @@ -30,7 +30,7 @@ namespace Avalonia.Controls.Primitives /// static HeaderedContentControl() { - ContentProperty.Changed.AddClassHandler(x => x.HeaderChanged); + ContentProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); } /// diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index e0eb0b005f..f4af694f28 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -24,7 +24,7 @@ namespace Avalonia.Controls.Primitives /// static HeaderedItemsControl() { - HeaderProperty.Changed.AddClassHandler(x => x.HeaderChanged); + HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); } /// diff --git a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs index 533b643ea6..5e053ed9b4 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs @@ -24,7 +24,7 @@ namespace Avalonia.Controls.Primitives /// static HeaderedSelectingItemsControl() { - HeaderProperty.Changed.AddClassHandler(x => x.HeaderChanged); + HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); } /// diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index b3f86e8a76..77febf9384 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -93,8 +93,8 @@ namespace Avalonia.Controls.Primitives static Popup() { IsHitTestVisibleProperty.OverrideDefaultValue(false); - ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); - IsOpenProperty.Changed.AddClassHandler(x => x.IsOpenChanged); + ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); + IsOpenProperty.Changed.AddClassHandler((x, e) => x.IsOpenChanged(e)); } public Popup() diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index c6119e89dc..9251ca273f 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -58,8 +58,8 @@ namespace Avalonia.Controls.Primitives PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); - Thumb.DragDeltaEvent.AddClassHandler(o => o.OnThumbDragDelta, RoutingStrategies.Bubble); - Thumb.DragCompletedEvent.AddClassHandler(o => o.OnThumbDragComplete, RoutingStrategies.Bubble); + Thumb.DragDeltaEvent.AddClassHandler((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble); + Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragComplete(e), RoutingStrategies.Bubble); } /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 6869ea0822..b752b3f7a8 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -119,7 +119,7 @@ namespace Avalonia.Controls.Primitives /// static SelectingItemsControl() { - IsSelectedChangedEvent.AddClassHandler(x => x.ContainerSelectionChanged); + IsSelectedChangedEvent.AddClassHandler((x,e) => x.ContainerSelectionChanged(e)); } /// diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 47c3240374..ba4c6830d1 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -99,7 +99,7 @@ namespace Avalonia.Controls.Primitives static TemplatedControl() { ClipToBoundsProperty.OverrideDefaultValue(true); - TemplateProperty.Changed.AddClassHandler(x => x.OnTemplateChanged); + TemplateProperty.Changed.AddClassHandler((x, e) => x.OnTemplateChanged(e)); } /// diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index b01ddd5dba..7e9680dc9f 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -22,9 +22,9 @@ namespace Avalonia.Controls.Primitives static Thumb() { - DragStartedEvent.AddClassHandler(x => x.OnDragStarted, RoutingStrategies.Bubble); - DragDeltaEvent.AddClassHandler(x => x.OnDragDelta, RoutingStrategies.Bubble); - DragCompletedEvent.AddClassHandler(x => x.OnDragCompleted, RoutingStrategies.Bubble); + DragStartedEvent.AddClassHandler((x,e) => x.OnDragStarted(e), RoutingStrategies.Bubble); + DragDeltaEvent.AddClassHandler((x, e) => x.OnDragDelta(e), RoutingStrategies.Bubble); + DragCompletedEvent.AddClassHandler((x, e) => x.OnDragCompleted(e), RoutingStrategies.Bubble); } public event EventHandler DragStarted diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index a569808b35..292c65aa06 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -48,9 +48,9 @@ namespace Avalonia.Controls.Primitives { PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); - ThumbProperty.Changed.AddClassHandler(x => x.ThumbChanged); - IncreaseButtonProperty.Changed.AddClassHandler(x => x.ButtonChanged); - DecreaseButtonProperty.Changed.AddClassHandler(x => x.ButtonChanged); + ThumbProperty.Changed.AddClassHandler((x,e) => x.ThumbChanged(e)); + IncreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); + DecreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); AffectsArrange(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); } diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 29e3a17f74..8963ceeddf 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using Avalonia.Controls.Primitives; using Avalonia.Layout; @@ -38,8 +39,8 @@ namespace Avalonia.Controls PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); PseudoClass(IsIndeterminateProperty, ":indeterminate"); - ValueProperty.Changed.AddClassHandler(x => x.UpdateIndicatorWhenPropChanged); - IsIndeterminateProperty.Changed.AddClassHandler(x => x.UpdateIndicatorWhenPropChanged); + ValueProperty.Changed.AddClassHandler((x,e) => x.UpdateIndicatorWhenPropChanged(e)); + IsIndeterminateProperty.Changed.AddClassHandler((x,e) => x.UpdateIndicatorWhenPropChanged(e)); } public bool IsIndeterminate diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index c9b5cbb75b..cdf5010920 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -163,8 +163,8 @@ namespace Avalonia.Controls { AffectsValidation(ExtentProperty, OffsetProperty); AffectsValidation(ViewportProperty, OffsetProperty); - HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler(x => x.ScrollBarVisibilityChanged); - VerticalScrollBarVisibilityProperty.Changed.AddClassHandler(x => x.ScrollBarVisibilityChanged); + HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); + VerticalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); } /// diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 9eaa246434..f71be8d836 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -45,9 +45,9 @@ namespace Avalonia.Controls OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal); PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); - Thumb.DragStartedEvent.AddClassHandler(x => x.OnThumbDragStarted, RoutingStrategies.Bubble); - Thumb.DragDeltaEvent.AddClassHandler(x => x.OnThumbDragDelta, RoutingStrategies.Bubble); - Thumb.DragCompletedEvent.AddClassHandler(x => x.OnThumbDragCompleted, RoutingStrategies.Bubble); + Thumb.DragStartedEvent.AddClassHandler((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble); + Thumb.DragDeltaEvent.AddClassHandler((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble); + Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble); } /// diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 47a2348d59..fca1e022aa 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -30,8 +30,8 @@ namespace Avalonia.Controls { SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); - IsSelectedProperty.Changed.AddClassHandler(x => x.UpdateSelectedContent); - DataContextProperty.Changed.AddClassHandler(x => x.UpdateHeader); + IsSelectedProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent(e)); + DataContextProperty.Changed.AddClassHandler((x, e) => x.UpdateHeader(e)); } /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index c7fd96b68a..07d5497c14 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -54,7 +54,7 @@ namespace Avalonia.Controls SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(true); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); - RequestBringIntoViewEvent.AddClassHandler(x => x.OnRequestBringIntoView); + RequestBringIntoViewEvent.AddClassHandler((x, e) => x.OnRequestBringIntoView(e)); } /// diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index a47c55f87c..196110edf7 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls static WindowBase() { IsVisibleProperty.OverrideDefaultValue(false); - IsVisibleProperty.Changed.AddClassHandler(x => x.IsVisibleChanged); + IsVisibleProperty.Changed.AddClassHandler((x,e) => x.IsVisibleChanged(e)); TopmostProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetTopmost((bool)e.NewValue)); diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 47e85416cf..535b930f8b 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -169,18 +169,18 @@ namespace Avalonia.Input { IsEnabledProperty.Changed.Subscribe(IsEnabledChanged); - GotFocusEvent.AddClassHandler(x => x.OnGotFocus); - LostFocusEvent.AddClassHandler(x => x.OnLostFocus); - KeyDownEvent.AddClassHandler(x => x.OnKeyDown); - KeyUpEvent.AddClassHandler(x => x.OnKeyUp); - TextInputEvent.AddClassHandler(x => x.OnTextInput); - PointerEnterEvent.AddClassHandler(x => x.OnPointerEnterCore); - PointerLeaveEvent.AddClassHandler(x => x.OnPointerLeaveCore); - PointerMovedEvent.AddClassHandler(x => x.OnPointerMoved); - PointerPressedEvent.AddClassHandler(x => x.OnPointerPressed); - PointerReleasedEvent.AddClassHandler(x => x.OnPointerReleased); - PointerCaptureLostEvent.AddClassHandler(x => x.OnPointerCaptureLost); - PointerWheelChangedEvent.AddClassHandler(x => x.OnPointerWheelChanged); + GotFocusEvent.AddClassHandler((x, e) => x.OnGotFocus(e)); + LostFocusEvent.AddClassHandler((x, e) => x.OnLostFocus(e)); + KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e)); + KeyUpEvent.AddClassHandler((x, e) => x.OnKeyUp(e)); + TextInputEvent.AddClassHandler((x, e) => x.OnTextInput(e)); + PointerEnterEvent.AddClassHandler((x, e) => x.OnPointerEnterCore(e)); + PointerLeaveEvent.AddClassHandler((x, e) => x.OnPointerLeaveCore(e)); + PointerMovedEvent.AddClassHandler((x, e) => x.OnPointerMoved(e)); + PointerPressedEvent.AddClassHandler((x, e) => x.OnPointerPressed(e)); + PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); + PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); + PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); PseudoClass(IsEffectivelyEnabledProperty, x => !x, ":disabled"); PseudoClass(IsFocusedProperty, ":focus"); diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index cfbaddb327..bc5dec9a90 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -113,24 +113,38 @@ namespace Avalonia.Interactivity Contract.Requires(ownerType != null); } + [Obsolete("Use overload taking Action.")] public IDisposable AddClassHandler( Func> handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) - where TTarget : class, IInteractive + where TTarget : class, IInteractive { - EventHandler adapter = (sender, e) => + void Adapter(object sender, RoutedEventArgs e) { - var target = sender as TTarget; - var args = e as TEventArgs; - - if (target != null && args != null) + if (sender is TTarget target && e is TEventArgs args) { handler(target)(args); } - }; + } + + return AddClassHandler(typeof(TTarget), Adapter, routes, handledEventsToo); + } + + public IDisposable AddClassHandler( + Action handler, + RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, + bool handledEventsToo = false) where TTarget : class, IInteractive + { + void Adapter(object sender, RoutedEventArgs e) + { + if (sender is TTarget target && e is TEventArgs args) + { + handler(target, args); + } + } - return AddClassHandler(typeof(TTarget), adapter, routes, handledEventsToo); + return AddClassHandler(typeof(TTarget), Adapter, routes, handledEventsToo); } } } diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 38c29289b6..910846ae63 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -73,7 +73,7 @@ namespace Avalonia /// static StyledElement() { - DataContextProperty.Changed.AddClassHandler(x => x.OnDataContextChangedCore); + DataContextProperty.Changed.AddClassHandler((x,e) => x.OnDataContextChangedCore(e)); } /// diff --git a/src/Avalonia.Visuals/Media/Geometry.cs b/src/Avalonia.Visuals/Media/Geometry.cs index 748d2526af..f9bcea85af 100644 --- a/src/Avalonia.Visuals/Media/Geometry.cs +++ b/src/Avalonia.Visuals/Media/Geometry.cs @@ -22,7 +22,7 @@ namespace Avalonia.Media static Geometry() { - TransformProperty.Changed.AddClassHandler(x => x.TransformChanged); + TransformProperty.Changed.AddClassHandler((x,e) => x.TransformChanged(e)); } /// diff --git a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs index 58ee63cea4..414e67bb94 100644 --- a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs @@ -331,7 +331,7 @@ namespace Avalonia.Interactivity.UnitTests var target = CreateTree(ev, null, 0); - ev.AddClassHandler(x => x.ClassHandler, RoutingStrategies.Bubble); + ev.AddClassHandler((x, e) => x.ClassHandler(e), RoutingStrategies.Bubble); var args = new RoutedEventArgs(ev, target); target.RaiseEvent(args); From 5041f0a6df4349d1f5e501fcd188252e4f7476c4 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 9 Oct 2019 11:57:46 +0300 Subject: [PATCH 0321/1791] Fixed DBusMenuExporter --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 27 +++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 0ab949ee69..911dc82e96 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -33,6 +33,7 @@ namespace Avalonia.FreeDesktop private NativeMenu _menu; private Dictionary _idsToItems = new Dictionary(); private Dictionary _itemsToIds = new Dictionary(); + private readonly HashSet _menus = new HashSet(); private bool _resetQueued; private int _nextId = 1; public DBusMenuExporterImpl(Connection dbus, IntPtr xid) @@ -104,12 +105,11 @@ namespace Avalonia.FreeDesktop void DoLayoutReset() { _resetQueued = false; - foreach (var i in _idsToItems.Values) - { + foreach (var i in _idsToItems.Values) i.PropertyChanged -= OnItemPropertyChanged; - if (i is NativeMenuItem nmi) - ((INotifyCollectionChanged)nmi.Menu.Items).CollectionChanged -= OnMenuItemsChanged; - } + foreach(var menu in _menus) + ((INotifyCollectionChanged)menu.Items).CollectionChanged -= OnMenuItemsChanged; + _menus.Clear(); _idsToItems.Clear(); _itemsToIds.Clear(); _revision++; @@ -132,6 +132,12 @@ namespace Avalonia.FreeDesktop return (item, (item as NativeMenuItem)?.Menu); } + private void EnsureSubscribed(NativeMenu menu) + { + if(menu!=null && _menus.Add(menu)) + ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged; + } + private int GetId(NativeMenuItemBase item) { if (_itemsToIds.TryGetValue(item, out var id)) @@ -141,7 +147,7 @@ namespace Avalonia.FreeDesktop _itemsToIds[item] = id; item.PropertyChanged += OnItemPropertyChanged; if (item is NativeMenuItem nmi) - ((INotifyCollectionChanged)nmi.Menu.Items).CollectionChanged += OnMenuItemsChanged; + EnsureSubscribed(nmi.Menu); return id; } @@ -190,12 +196,15 @@ namespace Avalonia.FreeDesktop { var (it, menu) = i; - if (it is NativeMenuItem item) + if (it is NativeMenuItemSeperator) + { + if (name == "type") + return "separator"; + } + else if (it is NativeMenuItem item) { if (name == "type") { - if (item != null && item.Header == null) - return "separator"; return null; } if (name == "label") From 465273c883125861d0dcb692e3d12d8f8d70d34e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 9 Oct 2019 11:58:08 +0300 Subject: [PATCH 0322/1791] Implemented managed menu bar presenter for native menus --- samples/ControlCatalog/MainWindow.xaml | 2 ++ samples/ControlCatalog/MainWindow.xaml.cs | 2 +- samples/ControlCatalog/Pages/MenuPage.xaml | 5 ++- src/Avalonia.Controls/NativeMenu.Export.cs | 5 ++- src/Avalonia.Controls/NativeMenu.cs | 2 +- src/Avalonia.Controls/NativeMenuBar.cs | 36 +++++++++++++++++++ .../NativeMenuItemSeperator.cs | 7 ++-- src/Avalonia.Themes.Default/DefaultTheme.xaml | 1 + .../InverseBooleanValueConverter.cs | 20 +++++++++++ .../NativeMenuBar.xaml | 25 +++++++++++++ 10 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 src/Avalonia.Controls/NativeMenuBar.cs create mode 100644 src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs create mode 100644 src/Avalonia.Themes.Default/NativeMenuBar.xaml diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 1b01fa56a8..a8fc6bb07d 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -15,11 +15,13 @@ + + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 819ab0655a..7b0ee897c4 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -30,7 +30,7 @@ namespace ControlCatalog }; DataContext = new MainWindowViewModel(_notificationArea); - _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[1] as NativeMenuItem).Menu; + _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu; } public void OnOpenClicked(object sender, EventArgs args) diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index e1a5cf2c5a..868f0df6ad 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -3,8 +3,11 @@ x:Class="ControlCatalog.Pages.MenuPage"> Menu + Exported menu fallback + (Should be only visible on platforms without desktop-global menu bar) + A window menu - + IsNativeMenuExportedProperty = - AvaloniaProperty.RegisterAttached("IsNativeMenuExported", - defaultBindingMode: BindingMode.OneWayToSource); + AvaloniaProperty.RegisterAttached("IsNativeMenuExported"); public static bool GetIsNativeMenuExported(TopLevel tl) => tl.GetValue(IsNativeMenuExportedProperty); @@ -53,7 +52,7 @@ namespace Avalonia.Controls } public static readonly AttachedProperty MenuProperty - = AvaloniaProperty.RegisterAttached("NativeMenuItems", validate: + = AvaloniaProperty.RegisterAttached("Menu", validate: (o, v) => { if(!(o is Application || o is TopLevel)) diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 58cff581e1..54aa2b5e3d 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -11,7 +11,7 @@ namespace Avalonia.Controls { public partial class NativeMenu : AvaloniaObject, IEnumerable { - private AvaloniaList _items = + private readonly AvaloniaList _items = new AvaloniaList { ResetBehavior = ResetBehavior.Remove }; private NativeMenuItem _parent; [Content] diff --git a/src/Avalonia.Controls/NativeMenuBar.cs b/src/Avalonia.Controls/NativeMenuBar.cs new file mode 100644 index 0000000000..9b96ab9c8c --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuBar.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Styling; + +namespace Avalonia.Controls +{ + public class NativeMenuBar : TemplatedControl + { + public static readonly AttachedProperty EnableMenuItemClickForwardingProperty = + AvaloniaProperty.RegisterAttached( + "EnableMenuItemClickForwarding"); + + static NativeMenuBar() + { + EnableMenuItemClickForwardingProperty.Changed.Subscribe(args => + { + var item = (MenuItem)args.Sender; + if (args.NewValue.Equals(true)) + item.Click += OnMenuItemClick; + else + item.Click -= OnMenuItemClick; + }); + } + + public static void SetEnableMenuItemClickForwarding(MenuItem menuItem, bool enable) + { + menuItem.SetValue(EnableMenuItemClickForwardingProperty, enable); + } + + private static void OnMenuItemClick(object sender, RoutedEventArgs e) + { + (((MenuItem)sender).DataContext as NativeMenuItem)?.RaiseClick(); + } + } +} diff --git a/src/Avalonia.Controls/NativeMenuItemSeperator.cs b/src/Avalonia.Controls/NativeMenuItemSeperator.cs index 85d62023d4..e743483dab 100644 --- a/src/Avalonia.Controls/NativeMenuItemSeperator.cs +++ b/src/Avalonia.Controls/NativeMenuItemSeperator.cs @@ -1,7 +1,10 @@ -namespace Avalonia.Controls +using System; + +namespace Avalonia.Controls { public class NativeMenuItemSeperator : NativeMenuItemBase { - + [Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)] + public string Header => "-"; } } diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 114979fba2..67279fca99 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -51,4 +51,5 @@ + diff --git a/src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs b/src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs new file mode 100644 index 0000000000..7befc81b8e --- /dev/null +++ b/src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Themes.Default +{ + class InverseBooleanValueConverter : IValueConverter + { + public bool Default { get; set; } + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool b ? !b : Default; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool b ? !b : !Default; + } + } +} diff --git a/src/Avalonia.Themes.Default/NativeMenuBar.xaml b/src/Avalonia.Themes.Default/NativeMenuBar.xaml new file mode 100644 index 0000000000..2832bab226 --- /dev/null +++ b/src/Avalonia.Themes.Default/NativeMenuBar.xaml @@ -0,0 +1,25 @@ + + + + + + From dbcc58fa074e028a2158907f071d14a5d90e3291 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 9 Oct 2019 12:00:15 +0300 Subject: [PATCH 0323/1791] Remove NativeMenu.PrependApplicationMenu for now --- src/Avalonia.Controls/NativeMenu.Export.cs | 14 -------------- .../Platform/ITopLevelNativeMenuExporter.cs | 1 - src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 5 ----- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 5 ----- 4 files changed, 25 deletions(-) diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index e3e134d316..5d3a4526cc 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -62,15 +62,6 @@ namespace Avalonia.Controls public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu); public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty); - - - public static readonly AttachedProperty PrependApplicationMenuProperty - = AvaloniaProperty.RegisterAttached("PrependApplicationMenu"); - - public static void SetPrependApplicationMenu(TopLevel tl, bool value) => - tl.SetValue(PrependApplicationMenuProperty, value); - - public static bool GetPrependApplicationMenu(TopLevel tl) => tl.GetValue(PrependApplicationMenuProperty); static NativeMenu() { @@ -89,11 +80,6 @@ namespace Avalonia.Controls GetInfo(tl).Exporter?.SetNativeMenu((NativeMenu)args.NewValue); } }); - - PrependApplicationMenuProperty.Changed.Subscribe(args => - { - GetInfo((TopLevel)args.Sender).Exporter?.SetPrependApplicationMenu((bool)args.NewValue); - }); } } } diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs index 5112424c3c..3ac5f28956 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs @@ -9,7 +9,6 @@ namespace Avalonia.Controls.Platform bool IsNativeMenuExported { get; } event EventHandler OnIsNativeMenuExportedChanged; void SetNativeMenu(NativeMenu menu); - void SetPrependApplicationMenu(bool prepend); } public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 911dc82e96..90239b5a49 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -161,11 +161,6 @@ namespace Avalonia.FreeDesktop QueueReset(); } - public void SetPrependApplicationMenu(bool prepend) - { - // Not implemented yet :( - } - public ObjectPath ObjectPath { get; } diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index d7635ebe78..1a22b95409 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -83,11 +83,6 @@ namespace Avalonia.Native DoLayoutReset(); } - public void SetPrependApplicationMenu(bool prepend) - { - // OSX always exports the app menu. - } - private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) { QueueReset(); From f02bc61bfee4871c45b8868d4f511aba53bfadf1 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 9 Oct 2019 12:21:23 +0300 Subject: [PATCH 0324/1791] I've said remove NativeMenu.PrependApplicationMenu for now --- samples/ControlCatalog/MainWindow.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index a8fc6bb07d..6088f2ec57 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,7 +7,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" - x:Class="ControlCatalog.MainWindow" NativeMenu.PrependApplicationMenu="True"> + x:Class="ControlCatalog.MainWindow"> From 10618f9beb62f5dbb1209ac086393922e2d39a5c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 10 Oct 2019 21:42:16 +0200 Subject: [PATCH 0325/1791] Seal PopupRoot as suggested by @kekekeks. --- src/Avalonia.Controls/Primitives/PopupRoot.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index b7f0c8f47d..74a6d288f4 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -17,7 +17,7 @@ namespace Avalonia.Controls.Primitives /// /// The root window of a . /// - public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost + public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost { private readonly TopLevel _parent; private PopupPositionerParameters _positionerParameters; From e1bfdf03246a8fe2df2fb443eb6226e17ef1940d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 10 Oct 2019 23:01:30 +0200 Subject: [PATCH 0326/1791] Added failing test for #3094. --- .../Primitives/SelectingItemsControlTests.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index d5237e2aca..17f0e609a5 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1000,6 +1000,26 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "Bar" }, selectedItems); } + [Fact] + public void MoveSelection_Wrap_Does_Not_Hang_With_No_Focusable_Controls() + { + // Issue #3094. + var target = new TestSelector + { + Template = Template(), + Items = new[] + { + new ListBoxItem { Focusable = false }, + new ListBoxItem { Focusable = false }, + }, + SelectedIndex = 0, + }; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + target.MoveSelection(NavigationDirection.Next, true); + } + private FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) => @@ -1044,5 +1064,13 @@ namespace Avalonia.Controls.UnitTests.Primitives public List Items { get; set; } = new List() { "a", "b", "c", "d", "e" }; public string Selected { get; set; } = "b"; } + + private class TestSelector : SelectingItemsControl + { + public new bool MoveSelection(NavigationDirection direction, bool wrap) + { + return base.MoveSelection(direction, wrap); + } + } } } From 60c9015a7fd42a5cc13d156fd70c0c3f5444ceda Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 10 Oct 2019 23:03:12 +0200 Subject: [PATCH 0327/1791] Prevent hang in ItemsControl with no focusable controls. --- src/Avalonia.Controls/ItemsControl.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 0fe7291835..6b81237e6f 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -489,18 +489,20 @@ namespace Avalonia.Controls bool wrap) { IInputElement result; + var c = from; do { - result = container.GetControl(direction, from, wrap); + result = container.GetControl(direction, c, wrap); + from ??= result; if (result?.Focusable == true) { return result; } - from = result; - } while (from != null); + c = result; + } while (c != null && c != from); return null; } From 400e64dc15ffd9c140b48918853660ccc2c6c470 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 11 Oct 2019 00:05:11 +0200 Subject: [PATCH 0328/1791] Fix formatting. --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 50 +++++++++---------- .../Presenters/ItemsPresenter.cs | 2 +- .../Presenters/ScrollContentPresenter.cs | 6 +-- .../Primitives/SelectingItemsControl.cs | 6 +-- src/Avalonia.Controls/ProgressBar.cs | 4 +- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index a6aaed1e80..b65fd2a8b7 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -723,29 +723,29 @@ namespace Avalonia.Controls PseudoClass(IsValidProperty, x => !x, ":invalid"); - ItemsProperty.Changed.AddClassHandler((x,e) => x.OnItemsPropertyChanged(e)); - CanUserResizeColumnsProperty.Changed.AddClassHandler((x,e) => x.OnCanUserResizeColumnsChanged(e)); - ColumnWidthProperty.Changed.AddClassHandler((x,e) => x.OnColumnWidthChanged(e)); - RowBackgroundProperty.Changed.AddClassHandler((x,e) => x.OnRowBackgroundChanged(e)); - AlternatingRowBackgroundProperty.Changed.AddClassHandler((x,e) => x.OnRowBackgroundChanged(e)); - FrozenColumnCountProperty.Changed.AddClassHandler((x,e) => x.OnFrozenColumnCountChanged(e)); - GridLinesVisibilityProperty.Changed.AddClassHandler((x,e) => x.OnGridLinesVisibilityChanged(e)); - HeadersVisibilityProperty.Changed.AddClassHandler((x,e) => x.OnHeadersVisibilityChanged(e)); - HorizontalGridLinesBrushProperty.Changed.AddClassHandler((x,e) => x.OnHorizontalGridLinesBrushChanged(e)); - IsReadOnlyProperty.Changed.AddClassHandler((x,e) => x.OnIsReadOnlyChanged(e)); - MaxColumnWidthProperty.Changed.AddClassHandler((x,e) => x.OnMaxColumnWidthChanged(e)); - MinColumnWidthProperty.Changed.AddClassHandler((x,e) => x.OnMinColumnWidthChanged(e)); - RowHeightProperty.Changed.AddClassHandler((x,e) => x.OnRowHeightChanged(e)); - RowHeaderWidthProperty.Changed.AddClassHandler((x,e) => x.OnRowHeaderWidthChanged(e)); - SelectionModeProperty.Changed.AddClassHandler((x,e) => x.OnSelectionModeChanged(e)); - VerticalGridLinesBrushProperty.Changed.AddClassHandler((x,e) => x.OnVerticalGridLinesBrushChanged(e)); - SelectedIndexProperty.Changed.AddClassHandler((x,e) => x.OnSelectedIndexChanged(e)); - SelectedItemProperty.Changed.AddClassHandler((x,e) => x.OnSelectedItemChanged(e)); - IsEnabledProperty.Changed.AddClassHandler((x,e) => x.DataGrid_IsEnabledChanged(e)); - AreRowGroupHeadersFrozenProperty.Changed.AddClassHandler((x,e) => x.OnAreRowGroupHeadersFrozenChanged(e)); - RowDetailsTemplateProperty.Changed.AddClassHandler((x,e) => x.OnRowDetailsTemplateChanged(e)); - RowDetailsVisibilityModeProperty.Changed.AddClassHandler((x,e) => x.OnRowDetailsVisibilityModeChanged(e)); - AutoGenerateColumnsProperty.Changed.AddClassHandler((x,e) => x.OnAutoGenerateColumnsChanged(e)); + ItemsProperty.Changed.AddClassHandler((x, e) => x.OnItemsPropertyChanged(e)); + CanUserResizeColumnsProperty.Changed.AddClassHandler((x, e) => x.OnCanUserResizeColumnsChanged(e)); + ColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnColumnWidthChanged(e)); + RowBackgroundProperty.Changed.AddClassHandler((x, e) => x.OnRowBackgroundChanged(e)); + AlternatingRowBackgroundProperty.Changed.AddClassHandler((x, e) => x.OnRowBackgroundChanged(e)); + FrozenColumnCountProperty.Changed.AddClassHandler((x, e) => x.OnFrozenColumnCountChanged(e)); + GridLinesVisibilityProperty.Changed.AddClassHandler((x, e) => x.OnGridLinesVisibilityChanged(e)); + HeadersVisibilityProperty.Changed.AddClassHandler((x, e) => x.OnHeadersVisibilityChanged(e)); + HorizontalGridLinesBrushProperty.Changed.AddClassHandler((x, e) => x.OnHorizontalGridLinesBrushChanged(e)); + IsReadOnlyProperty.Changed.AddClassHandler((x, e) => x.OnIsReadOnlyChanged(e)); + MaxColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnMaxColumnWidthChanged(e)); + MinColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnMinColumnWidthChanged(e)); + RowHeightProperty.Changed.AddClassHandler((x, e) => x.OnRowHeightChanged(e)); + RowHeaderWidthProperty.Changed.AddClassHandler((x, e) => x.OnRowHeaderWidthChanged(e)); + SelectionModeProperty.Changed.AddClassHandler((x, e) => x.OnSelectionModeChanged(e)); + VerticalGridLinesBrushProperty.Changed.AddClassHandler((x, e) => x.OnVerticalGridLinesBrushChanged(e)); + SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.OnSelectedIndexChanged(e)); + SelectedItemProperty.Changed.AddClassHandler((x, e) => x.OnSelectedItemChanged(e)); + IsEnabledProperty.Changed.AddClassHandler((x, e) => x.DataGrid_IsEnabledChanged(e)); + AreRowGroupHeadersFrozenProperty.Changed.AddClassHandler((x, e) => x.OnAreRowGroupHeadersFrozenChanged(e)); + RowDetailsTemplateProperty.Changed.AddClassHandler((x, e) => x.OnRowDetailsTemplateChanged(e)); + RowDetailsVisibilityModeProperty.Changed.AddClassHandler((x, e) => x.OnRowDetailsVisibilityModeChanged(e)); + AutoGenerateColumnsProperty.Changed.AddClassHandler((x, e) => x.OnAutoGenerateColumnsChanged(e)); } /// @@ -3533,7 +3533,7 @@ namespace Avalonia.Controls if (AreColumnHeadersVisible && _vScrollBar != null && _vScrollBar.IsVisible) { - _topRightCornerHeader.IsVisible = true; ; + _topRightCornerHeader.IsVisible = true; } else { @@ -5594,7 +5594,7 @@ namespace Avalonia.Controls { // This will trigger a call to this method via Cells_SizeChanged for // which no processing is needed. - _vScrollBar.IsVisible = true; ; + _vScrollBar.IsVisible = true; if (_vScrollBar.DesiredSize.Width == 0) { // We need to know the width for the rest of layout to work correctly so measure it now diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 7a5451821e..ab40fbd53b 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls.Presenters KeyboardNavigationMode.Once); VirtualizationModeProperty.Changed - .AddClassHandler((x,e) => x.VirtualizationModeChanged(e)); + .AddClassHandler((x, e) => x.VirtualizationModeChanged(e)); } /// diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 6fffc3741a..48d0aff551 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -73,7 +73,7 @@ namespace Avalonia.Controls.Presenters static ScrollContentPresenter() { ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); - ChildProperty.Changed.AddClassHandler((x,e) => x.ChildChanged(e)); + ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); AffectsArrange(OffsetProperty); } @@ -246,7 +246,7 @@ namespace Avalonia.Controls.Presenters if (isLogical) _activeLogicalGestureScrolls?.TryGetValue(e.Id, out delta); delta += e.Delta; - + if (Extent.Height > Viewport.Height) { double dy; @@ -293,7 +293,7 @@ namespace Avalonia.Controls.Presenters } } - private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e) + private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e) => _activeLogicalGestureScrolls?.Remove(e.Id); /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index d2d82f5ca1..7fddee1012 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -119,7 +119,7 @@ namespace Avalonia.Controls.Primitives /// static SelectingItemsControl() { - IsSelectedChangedEvent.AddClassHandler((x,e) => x.ContainerSelectionChanged(e)); + IsSelectedChangedEvent.AddClassHandler((x, e) => x.ContainerSelectionChanged(e)); } /// @@ -1088,8 +1088,8 @@ namespace Avalonia.Controls.Primitives } else { - SelectedIndex = _updateSelectedIndex != int.MinValue ? - _updateSelectedIndex : + SelectedIndex = _updateSelectedIndex != int.MinValue ? + _updateSelectedIndex : AlwaysSelected ? 0 : -1; } } diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 8963ceeddf..94898951a9 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -39,8 +39,8 @@ namespace Avalonia.Controls PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); PseudoClass(IsIndeterminateProperty, ":indeterminate"); - ValueProperty.Changed.AddClassHandler((x,e) => x.UpdateIndicatorWhenPropChanged(e)); - IsIndeterminateProperty.Changed.AddClassHandler((x,e) => x.UpdateIndicatorWhenPropChanged(e)); + ValueProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); + IsIndeterminateProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); } public bool IsIndeterminate From adb65f14b1e0fdb86889d2038c5f4dcdf18a312c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 11 Oct 2019 15:13:48 +0200 Subject: [PATCH 0329/1791] Don't use C#8 features yet. --- src/Avalonia.Controls/ItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 6b81237e6f..11fdd0457d 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -494,7 +494,7 @@ namespace Avalonia.Controls do { result = container.GetControl(direction, c, wrap); - from ??= result; + from = from ?? result; if (result?.Focusable == true) { From 4ab0131f90d5facc62518417f00dae61e561587e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 11 Oct 2019 15:46:17 +0200 Subject: [PATCH 0330/1791] Don't override key/pointer events in Carousel. Fixes #3098. --- src/Avalonia.Controls/Carousel.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Avalonia.Controls/Carousel.cs b/src/Avalonia.Controls/Carousel.cs index 069bf40820..ebc8890721 100644 --- a/src/Avalonia.Controls/Carousel.cs +++ b/src/Avalonia.Controls/Carousel.cs @@ -84,17 +84,5 @@ namespace Avalonia.Controls --SelectedIndex; } } - - /// - protected override void OnKeyDown(KeyEventArgs e) - { - // Ignore key presses. - } - - /// - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - // Ignore pointer presses. - } } } From e7b39c1672ecefa99fb5095a17ee56998e0b0681 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 11 Oct 2019 19:34:21 +0100 Subject: [PATCH 0331/1791] convert osx special key codes. --- .../AvaloniaNativeMenuExporter.cs | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 1a22b95409..a8a1121405 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -5,12 +5,89 @@ using System.Linq; using System.Text; using Avalonia.Controls; using Avalonia.Controls.Platform; +using Avalonia.Input; using Avalonia.Native.Interop; using Avalonia.Platform.Interop; using Avalonia.Threading; namespace Avalonia.Native { + enum OsxUnicodeSpecialKey + { + NSUpArrowFunctionKey = 0xF700, + NSDownArrowFunctionKey = 0xF701, + NSLeftArrowFunctionKey = 0xF702, + NSRightArrowFunctionKey = 0xF703, + NSF1FunctionKey = 0xF704, + NSF2FunctionKey = 0xF705, + NSF3FunctionKey = 0xF706, + NSF4FunctionKey = 0xF707, + NSF5FunctionKey = 0xF708, + NSF6FunctionKey = 0xF709, + NSF7FunctionKey = 0xF70A, + NSF8FunctionKey = 0xF70B, + NSF9FunctionKey = 0xF70C, + NSF10FunctionKey = 0xF70D, + NSF11FunctionKey = 0xF70E, + NSF12FunctionKey = 0xF70F, + NSF13FunctionKey = 0xF710, + NSF14FunctionKey = 0xF711, + NSF15FunctionKey = 0xF712, + NSF16FunctionKey = 0xF713, + NSF17FunctionKey = 0xF714, + NSF18FunctionKey = 0xF715, + NSF19FunctionKey = 0xF716, + NSF20FunctionKey = 0xF717, + NSF21FunctionKey = 0xF718, + NSF22FunctionKey = 0xF719, + NSF23FunctionKey = 0xF71A, + NSF24FunctionKey = 0xF71B, + NSF25FunctionKey = 0xF71C, + NSF26FunctionKey = 0xF71D, + NSF27FunctionKey = 0xF71E, + NSF28FunctionKey = 0xF71F, + NSF29FunctionKey = 0xF720, + NSF30FunctionKey = 0xF721, + NSF31FunctionKey = 0xF722, + NSF32FunctionKey = 0xF723, + NSF33FunctionKey = 0xF724, + NSF34FunctionKey = 0xF725, + NSF35FunctionKey = 0xF726, + NSInsertFunctionKey = 0xF727, + NSDeleteFunctionKey = 0xF728, + NSHomeFunctionKey = 0xF729, + NSBeginFunctionKey = 0xF72A, + NSEndFunctionKey = 0xF72B, + NSPageUpFunctionKey = 0xF72C, + NSPageDownFunctionKey = 0xF72D, + NSPrintScreenFunctionKey = 0xF72E, + NSScrollLockFunctionKey = 0xF72F, + NSPauseFunctionKey = 0xF730, + NSSysReqFunctionKey = 0xF731, + NSBreakFunctionKey = 0xF732, + NSResetFunctionKey = 0xF733, + NSStopFunctionKey = 0xF734, + NSMenuFunctionKey = 0xF735, + NSUserFunctionKey = 0xF736, + NSSystemFunctionKey = 0xF737, + NSPrintFunctionKey = 0xF738, + NSClearLineFunctionKey = 0xF739, + NSClearDisplayFunctionKey = 0xF73A, + NSInsertLineFunctionKey = 0xF73B, + NSDeleteLineFunctionKey = 0xF73C, + NSInsertCharFunctionKey = 0xF73D, + NSDeleteCharFunctionKey = 0xF73E, + NSPrevFunctionKey = 0xF73F, + NSNextFunctionKey = 0xF740, + NSSelectFunctionKey = 0xF741, + NSExecuteFunctionKey = 0xF742, + NSUndoFunctionKey = 0xF743, + NSRedoFunctionKey = 0xF744, + NSFindFunctionKey = 0xF745, + NSHelpFunctionKey = 0xF746, + NSModeSwitchFunctionKey = 0xF747 + } + public class MenuActionCallback : CallbackBase, IAvnActionCallback { private Action _action; @@ -50,6 +127,11 @@ namespace Avalonia.Native private IAvnWindow _nativeWindow; private List _menuItems = new List(); + private static Dictionary osxKeys = new Dictionary + { + { Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey }, + }; + public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { _factory = factory; @@ -147,6 +229,18 @@ namespace Avalonia.Native } } + private static string ConvertOSXSpecialKeyCodes(Key key) + { + if (osxKeys.ContainsKey(key)) + { + return Encoding.UTF8.GetString(BitConverter.GetBytes((ushort)osxKeys[key])); + } + else + { + return key.ToString().ToLower(); + } + } + private void SetChildren(IAvnAppMenu menu, ICollection children) { foreach (var i in children) @@ -164,7 +258,7 @@ namespace Avalonia.Native if (item.Gesture != null) { - using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower())) + using (var buffer = new Utf8Buffer(ConvertOSXSpecialKeyCodes(item.Gesture.Key))) { menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); } From 0327d8e56bcf0cffa17f67b12872b0ad6b2bc8e8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 11 Oct 2019 19:51:21 +0100 Subject: [PATCH 0332/1791] implement osx keycodes. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index a8a1121405..3b0c4d94a6 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -130,6 +130,7 @@ namespace Avalonia.Native private static Dictionary osxKeys = new Dictionary { { Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey }, + { Key.F4, OsxUnicodeSpecialKey.NSF4FunctionKey }, }; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) @@ -233,7 +234,7 @@ namespace Avalonia.Native { if (osxKeys.ContainsKey(key)) { - return Encoding.UTF8.GetString(BitConverter.GetBytes((ushort)osxKeys[key])); + return ((char)osxKeys[key]).ToString(); } else { From 1474409a5ee6f80135c3234444a5b403dd0738bd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 11 Oct 2019 20:07:11 +0100 Subject: [PATCH 0333/1791] add missing keycodes for osx gestures. --- .../AvaloniaNativeMenuExporter.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 3b0c4d94a6..950943d54a 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -129,8 +129,53 @@ namespace Avalonia.Native private static Dictionary osxKeys = new Dictionary { + {Key.Up, OsxUnicodeSpecialKey.NSUpArrowFunctionKey }, + {Key.Down, OsxUnicodeSpecialKey.NSDownArrowFunctionKey }, + {Key.Left, OsxUnicodeSpecialKey.NSLeftArrowFunctionKey }, + {Key.Right, OsxUnicodeSpecialKey.NSRightArrowFunctionKey }, { Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey }, + { Key.F2, OsxUnicodeSpecialKey.NSF2FunctionKey }, + { Key.F3, OsxUnicodeSpecialKey.NSF3FunctionKey }, { Key.F4, OsxUnicodeSpecialKey.NSF4FunctionKey }, + { Key.F5, OsxUnicodeSpecialKey.NSF5FunctionKey }, + { Key.F6, OsxUnicodeSpecialKey.NSF6FunctionKey }, + { Key.F7, OsxUnicodeSpecialKey.NSF7FunctionKey }, + { Key.F8, OsxUnicodeSpecialKey.NSF8FunctionKey }, + { Key.F9, OsxUnicodeSpecialKey.NSF9FunctionKey }, + { Key.F10, OsxUnicodeSpecialKey.NSF10FunctionKey }, + { Key.F11, OsxUnicodeSpecialKey.NSF11FunctionKey }, + { Key.F12, OsxUnicodeSpecialKey.NSF12FunctionKey }, + { Key.F13, OsxUnicodeSpecialKey.NSF13FunctionKey }, + { Key.F14, OsxUnicodeSpecialKey.NSF14FunctionKey }, + { Key.F15, OsxUnicodeSpecialKey.NSF15FunctionKey }, + { Key.F16, OsxUnicodeSpecialKey.NSF16FunctionKey }, + { Key.F17, OsxUnicodeSpecialKey.NSF17FunctionKey }, + { Key.F18, OsxUnicodeSpecialKey.NSF18FunctionKey }, + { Key.F19, OsxUnicodeSpecialKey.NSF19FunctionKey }, + { Key.F20, OsxUnicodeSpecialKey.NSF20FunctionKey }, + { Key.F21, OsxUnicodeSpecialKey.NSF21FunctionKey }, + { Key.F22, OsxUnicodeSpecialKey.NSF22FunctionKey }, + { Key.F23, OsxUnicodeSpecialKey.NSF23FunctionKey }, + { Key.F24, OsxUnicodeSpecialKey.NSF24FunctionKey }, + { Key.Insert, OsxUnicodeSpecialKey.NSInsertFunctionKey }, + { Key.Delete, OsxUnicodeSpecialKey.NSDeleteFunctionKey }, + { Key.Home, OsxUnicodeSpecialKey.NSHomeFunctionKey }, + //{ Key.Begin, OsxUnicodeSpecialKey.NSBeginFunctionKey }, + { Key.End, OsxUnicodeSpecialKey.NSEndFunctionKey }, + { Key.PageUp, OsxUnicodeSpecialKey.NSPageUpFunctionKey }, + { Key.PageDown, OsxUnicodeSpecialKey.NSPageDownFunctionKey }, + { Key.PrintScreen, OsxUnicodeSpecialKey.NSPrintScreenFunctionKey }, + { Key.Scroll, OsxUnicodeSpecialKey.NSScrollLockFunctionKey }, + //{ Key.SysReq, OsxUnicodeSpecialKey.NSSysReqFunctionKey }, + //{ Key.Break, OsxUnicodeSpecialKey.NSBreakFunctionKey }, + //{ Key.Reset, OsxUnicodeSpecialKey.NSResetFunctionKey }, + //{ Key.Stop, OsxUnicodeSpecialKey.NSStopFunctionKey }, + //{ Key.Menu, OsxUnicodeSpecialKey.NSMenuFunctionKey }, + //{ Key.UserFunction, OsxUnicodeSpecialKey.NSUserFunctionKey }, + //{ Key.SystemFunction, OsxUnicodeSpecialKey.NSSystemFunctionKey }, + { Key.Print, OsxUnicodeSpecialKey.NSPrintFunctionKey }, + //{ Key.ClearLine, OsxUnicodeSpecialKey.NSClearLineFunctionKey }, + //{ Key.ClearDisplay, OsxUnicodeSpecialKey.NSClearDisplayFunctionKey }, }; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) From a16e3bff7fd44f3581c4b5df1e0eb9d616c20846 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 12 Oct 2019 10:21:17 +0300 Subject: [PATCH 0334/1791] Added InitialPressMouseButton to PointerReleasedEventArgs that returns the button that caused the PointerPressed event MouseButton is now deprecated with error, since people need to decide which behavior they want --- .../DataGridColumnHeader.cs | 2 +- src/Avalonia.Controls/Button.cs | 2 +- src/Avalonia.Controls/Calendar/Calendar.cs | 2 +- .../Calendar/CalendarButton.cs | 2 +- .../Calendar/CalendarDayButton.cs | 2 +- src/Avalonia.Controls/ContextMenu.cs | 2 +- .../Platform/DefaultMenuInteractionHandler.cs | 2 +- src/Avalonia.Controls/RepeatButton.cs | 4 +-- src/Avalonia.Controls/TabControl.cs | 2 +- .../SelectingItemsControlSelectionAdapter.cs | 2 +- src/Avalonia.Input/Gestures.cs | 2 +- src/Avalonia.Input/MouseDevice.cs | 5 ++-- src/Avalonia.Input/PointerEventArgs.cs | 16 ++++++++---- src/Avalonia.Input/PointerPoint.cs | 25 +++++++++++-------- src/Avalonia.Input/TouchDevice.cs | 2 +- .../DefaultMenuInteractionHandlerTests.cs | 4 ++- tests/Avalonia.UnitTests/MouseTestHelper.cs | 3 +-- 17 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 4c77c8b5ae..d1651b2d09 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -468,7 +468,7 @@ namespace Avalonia.Controls private void DataGridColumnHeader_PointerReleased(object sender, PointerReleasedEventArgs e) { - if (OwningColumn == null || e.Handled || !IsEnabled || e.MouseButton != MouseButton.Left) + if (OwningColumn == null || e.Handled || !IsEnabled || e.InitialPressMouseButton != MouseButton.Left) { return; } diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index d39ea73828..78d02e200f 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -294,7 +294,7 @@ namespace Avalonia.Controls { base.OnPointerReleased(e); - if (IsPressed && e.MouseButton == MouseButton.Left) + if (IsPressed && e.InitialPressMouseButton == MouseButton.Left) { IsPressed = false; e.Handled = true; diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 89b375996b..beafab3edf 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -1565,7 +1565,7 @@ namespace Avalonia.Controls protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); - if (!HasFocusInternal && e.MouseButton == MouseButton.Left) + if (!HasFocusInternal && e.InitialPressMouseButton == MouseButton.Left) { FocusManager.Instance.Focus(this); } diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index 53852defb3..a273e68d56 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -173,7 +173,7 @@ namespace Avalonia.Controls.Primitives protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); - if (e.MouseButton == MouseButton.Left) + if (e.InitialPressMouseButton == MouseButton.Left) CalendarLeftMouseButtonUp?.Invoke(this, e); } } diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index cb2a98e5ca..e62a1ce1f4 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -231,7 +231,7 @@ namespace Avalonia.Controls.Primitives { base.OnPointerReleased(e); - if (e.MouseButton == MouseButton.Left) + if (e.InitialPressMouseButton == MouseButton.Left) CalendarDayButtonMouseUp?.Invoke(this, e); } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index a5025df82d..5dfa5863f5 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -192,7 +192,7 @@ namespace Avalonia.Controls e.Handled = true; } - if (e.MouseButton == MouseButton.Right) + if (e.InitialPressMouseButton == MouseButton.Right) { if (contextMenu.CancelOpening()) return; diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 98f925cd0c..b5dbd1e668 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -356,7 +356,7 @@ namespace Avalonia.Controls.Platform { var item = GetMenuItem(e.Source as IControl); - if (e.MouseButton == MouseButton.Left && item?.HasSubMenu == false) + if (e.InitialPressMouseButton == MouseButton.Left && item?.HasSubMenu == false) { Click(item); e.Handled = true; diff --git a/src/Avalonia.Controls/RepeatButton.cs b/src/Avalonia.Controls/RepeatButton.cs index 07a1e82638..a982a0970c 100644 --- a/src/Avalonia.Controls/RepeatButton.cs +++ b/src/Avalonia.Controls/RepeatButton.cs @@ -98,10 +98,10 @@ namespace Avalonia.Controls { base.OnPointerReleased(e); - if (e.MouseButton == MouseButton.Left) + if (e.InitialPressMouseButton == MouseButton.Left) { StopTimer(); } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 50bcb034ac..61ac0822b0 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -196,7 +196,7 @@ namespace Avalonia.Controls protected override void OnPointerReleased(PointerReleasedEventArgs e) { - if (e.MouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) + if (e.InitialPressMouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) { var container = GetContainerFromEventSource(e.Source); if (container != null diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs index 4d814170c6..78361fcc8f 100644 --- a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs +++ b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs @@ -178,7 +178,7 @@ namespace Avalonia.Controls.Utils /// The event data. private void OnSelectorPointerReleased(object sender, PointerReleasedEventArgs e) { - if (e.MouseButton == MouseButton.Left) + if (e.InitialPressMouseButton == MouseButton.Left) { OnCommit(); } diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 6b06151773..a5bd4feb64 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -97,7 +97,7 @@ namespace Avalonia.Input if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { - var et = e.MouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent; + var et = e.InitialPressMouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent; e.Source.RaiseEvent(new RoutedEventArgs(et)); } } diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 0d5471f790..c84596b913 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -221,7 +221,7 @@ namespace Avalonia.Input _lastClickTime = timestamp; _lastClickRect = new Rect(p, new Size()) .Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2)); - _lastMouseDownButton = properties.GetObsoleteMouseButton(); + _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); source.RaiseEvent(e); return e.Handled; @@ -267,7 +267,8 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers); + var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, + _lastMouseDownButton); source?.RaiseEvent(e); _pointer.Capture(null); diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 5b3c43e4df..f22d5c29fb 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -124,7 +124,7 @@ namespace Avalonia.Input public int ClickCount => _obsoleteClickCount; [Obsolete("Use PointerUpdateKind")] - public MouseButton MouseButton => Properties.GetObsoleteMouseButton(); + public MouseButton MouseButton => Properties.PointerUpdateKind.GetMouseButton(); } public class PointerReleasedEventArgs : PointerEventArgs @@ -132,15 +132,21 @@ namespace Avalonia.Input public PointerReleasedEventArgs( IInteractive source, IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp, - PointerPointProperties properties, KeyModifiers modifiers) + PointerPointProperties properties, KeyModifiers modifiers, + MouseButton initialPressMouseButton) : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) { - + InitialPressMouseButton = initialPressMouseButton; } - [Obsolete("Use PointerUpdateKind")] - public MouseButton MouseButton => Properties.GetObsoleteMouseButton(); + /// + /// Gets the mouse button that triggered the corresponding PointerPressed event + /// + public MouseButton InitialPressMouseButton { get; } + + [Obsolete("Either use GetCurrentPoint(this).Properties.PointerUpdateKind or InitialPressMouseButton, see ", true)] + public MouseButton MouseButton => InitialPressMouseButton; } public class PointerCaptureLostEventArgs : RoutedEventArgs diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index d823a78090..e9f3c02b7f 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -49,17 +49,6 @@ namespace Avalonia.Input } public static PointerPointProperties None { get; } = new PointerPointProperties(); - - public MouseButton GetObsoleteMouseButton() - { - if (PointerUpdateKind == PointerUpdateKind.LeftButtonPressed || PointerUpdateKind == PointerUpdateKind.LeftButtonReleased) - return MouseButton.Left; - if (PointerUpdateKind == PointerUpdateKind.MiddleButtonPressed || PointerUpdateKind == PointerUpdateKind.MiddleButtonReleased) - return MouseButton.Middle; - if (PointerUpdateKind == PointerUpdateKind.RightButtonPressed || PointerUpdateKind == PointerUpdateKind.RightButtonReleased) - return MouseButton.Right; - return MouseButton.None; - } } public enum PointerUpdateKind @@ -72,4 +61,18 @@ namespace Avalonia.Input RightButtonReleased, Other } + + public static class PointerUpdateKindExtensions + { + public static MouseButton GetMouseButton(this PointerUpdateKind kind) + { + if (kind == PointerUpdateKind.LeftButtonPressed || kind == PointerUpdateKind.LeftButtonReleased) + return MouseButton.Left; + if (kind == PointerUpdateKind.MiddleButtonPressed || kind == PointerUpdateKind.MiddleButtonReleased) + return MouseButton.Middle; + if (kind == PointerUpdateKind.RightButtonPressed || kind == PointerUpdateKind.RightButtonReleased) + return MouseButton.Right; + return MouseButton.None; + } + } } diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index 765e02848f..b231c9fff4 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -60,7 +60,7 @@ namespace Avalonia.Input args.Root, args.Position, ev.Timestamp, new PointerPointProperties(GetModifiers(args.InputModifiers, false), PointerUpdateKind.LeftButtonReleased), - GetKeyModifiers(args.InputModifiers))); + GetKeyModifiers(args.InputModifiers), MouseButton.Left)); } } diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index ff11bc513d..989bd744a6 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -18,7 +18,9 @@ namespace Avalonia.Controls.UnitTests.Platform default); static PointerReleasedEventArgs CreateReleased(IInteractive source) => new PointerReleasedEventArgs(source, - new FakePointer(), (IVisual)source, default,0, new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonReleased), default); + new FakePointer(), (IVisual)source, default,0, + new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonReleased), + default, MouseButton.Left); public class TopLevel { diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index d6e64936c7..48c4d73471 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -86,8 +86,7 @@ namespace Avalonia.UnitTests { _pointer.Capture(null); target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, - Timestamp(), props, - GetModifiers(modifiers))); + Timestamp(), props, GetModifiers(modifiers), _pressedButton)); } else Move(target, source, position); From 1b5f3b6cc8ea613af0521c1df080f4584ab0e5ea Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 12 Oct 2019 10:31:18 +0300 Subject: [PATCH 0335/1791] Renamed GetPointerPoint to GetCurrentPoint to match UWP --- src/Avalonia.Controls/MenuItem.cs | 4 ++-- src/Avalonia.Input/PointerEventArgs.cs | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 8c82fed58d..3ba0007f6b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -337,7 +337,7 @@ namespace Avalonia.Controls { base.OnPointerEnter(e); - var point = e.GetPointerPoint(null); + var point = e.GetCurrentPoint(null); RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position, e.Timestamp, point.Properties, e.KeyModifiers)); } @@ -347,7 +347,7 @@ namespace Avalonia.Controls { base.OnPointerLeave(e); - var point = e.GetPointerPoint(null); + var point = e.GetCurrentPoint(null); RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position, e.Timestamp, point.Properties, e.KeyModifiers)); } diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index f22d5c29fb..ca3bbc518c 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -88,9 +88,20 @@ namespace Avalonia.Input return _rootVisualPosition * _rootVisual.TransformToVisual(relativeTo) ?? default; } - public PointerPoint GetPointerPoint(IVisual relativeTo) + [Obsolete("Use GetCurrentPoint")] + public PointerPoint GetPointerPoint(IVisual relativeTo) => GetCurrentPoint(relativeTo); + + /// + /// Returns the PointerPoint associated with the current event + /// + /// The visual which coordinate system to use. Pass null for toplevel coordinate system + /// + public PointerPoint GetCurrentPoint(IVisual relativeTo) => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); + /// + /// Returns the current pointer point properties + /// protected PointerPointProperties Properties => _properties; } From 9cb36f2dc4708cc7620f0ec919f320661166bf4e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 12 Oct 2019 23:28:30 +0300 Subject: [PATCH 0336/1791] Fixed obsolete comment --- src/Avalonia.Input/PointerEventArgs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index ca3bbc518c..e12a20f7a2 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -156,7 +156,7 @@ namespace Avalonia.Input /// public MouseButton InitialPressMouseButton { get; } - [Obsolete("Either use GetCurrentPoint(this).Properties.PointerUpdateKind or InitialPressMouseButton, see ", true)] + [Obsolete("Either use GetCurrentPoint(this).Properties.PointerUpdateKind or InitialPressMouseButton, see https://github.com/AvaloniaUI/Avalonia/wiki/Pointer-events-in-0.9 for more details", true)] public MouseButton MouseButton => InitialPressMouseButton; } From 88446b1e9ead00ad4a8178981e3670d7100eeebc Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 14 Oct 2019 17:33:14 +0800 Subject: [PATCH 0337/1791] Pass PointerPressed event to BeginModeDrag and BeginResizeDrag. --- samples/ControlCatalog/DecoratedWindow.xaml.cs | 11 ++++++----- src/Avalonia.Controls/Platform/IWindowImpl.cs | 13 +++++++------ src/Avalonia.Controls/Window.cs | 4 ++-- .../Remote/PreviewerWindowImpl.cs | 5 +++-- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 14 +++++++------- src/Avalonia.Native/WindowImplBase.cs | 4 ++-- src/Avalonia.X11/X11Window.cs | 12 +++++++----- src/Windows/Avalonia.Win32/WindowImpl.cs | 4 ++-- 8 files changed, 36 insertions(+), 31 deletions(-) diff --git a/samples/ControlCatalog/DecoratedWindow.xaml.cs b/samples/ControlCatalog/DecoratedWindow.xaml.cs index 2e7218b956..d76ef0a7bf 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml.cs +++ b/samples/ControlCatalog/DecoratedWindow.xaml.cs @@ -18,18 +18,18 @@ namespace ControlCatalog { var ctl = this.FindControl(name); ctl.Cursor = new Cursor(cursor); - ctl.PointerPressed += delegate + ctl.PointerPressed += (i, e) => { - PlatformImpl?.BeginResizeDrag(edge); + PlatformImpl?.BeginResizeDrag(edge, e); }; } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - this.FindControl("TitleBar").PointerPressed += delegate + this.FindControl("TitleBar").PointerPressed += (i, e) => { - PlatformImpl?.BeginMoveDrag(); + PlatformImpl?.BeginMoveDrag(e); }; SetupSide("Left", StandardCursorType.LeftSide, WindowEdge.West); SetupSide("Right", StandardCursorType.RightSide, WindowEdge.East); @@ -39,7 +39,8 @@ namespace ControlCatalog SetupSide("TopRight", StandardCursorType.TopRightCorner, WindowEdge.NorthEast); SetupSide("BottomLeft", StandardCursorType.BottomLeftCorner, WindowEdge.SouthWest); SetupSide("BottomRight", StandardCursorType.BottomRightCorner, WindowEdge.SouthEast); - this.FindControl Func Closing { get; set; } - + /// /// Starts moving a window with left button being held. Should be called from left mouse button press event handler. /// - void BeginMoveDrag(); + void BeginMoveDrag(PointerPressedEventArgs e); /// /// Starts resizing a window. This function is used if an application has window resizing controls. /// Should be called from left mouse button press event handler /// - void BeginResizeDrag(WindowEdge edge); - + void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e); + /// /// Sets the client size of the top level. /// void Resize(Size clientSize); - + /// /// Sets the client size of the top level. /// void Move(PixelPoint point); - + /// /// Minimum width of the window. /// diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ec2fca0db5..1816a6c81d 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -261,13 +261,13 @@ namespace Avalonia.Controls /// /// Starts moving a window with left button being held. Should be called from left mouse button press event handler /// - public void BeginMoveDrag() => PlatformImpl?.BeginMoveDrag(); + public void BeginMoveDrag(PointerPressedEventArgs e) => PlatformImpl?.BeginMoveDrag(e); /// /// Starts resizing a window. This function is used if an application has window resizing controls. /// Should be called from left mouse button press event handler /// - public void BeginResizeDrag(WindowEdge edge) => PlatformImpl?.BeginResizeDrag(edge); + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e); /// /// Carries out the arrange pass of the window. diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 40524ad4b7..86e34ca6d4 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -2,6 +2,7 @@ using System.Reactive.Disposables; using Avalonia.Controls; using Avalonia.Controls.Remote.Server; +using Avalonia.Input; using Avalonia.Platform; using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Viewport; @@ -27,11 +28,11 @@ namespace Avalonia.DesignerSupport.Remote { } - public void BeginMoveDrag() + public void BeginMoveDrag(PointerPressedEventArgs e) { } - public void BeginResizeDrag(WindowEdge edge) + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 16d434b614..4bba5ef41b 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -46,7 +46,7 @@ namespace Avalonia.DesignerSupport.Remote Resize(size); })); } - + public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer(root); public void Dispose() { @@ -75,11 +75,11 @@ namespace Avalonia.DesignerSupport.Remote { } - public void BeginMoveDrag() + public void BeginMoveDrag(PointerPressedEventArgs e) { } - public void BeginResizeDrag(WindowEdge edge) + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { } @@ -93,7 +93,7 @@ namespace Avalonia.DesignerSupport.Remote public void Move(PixelPoint point) { - + } public IScreenImpl Screen { get; } = new ScreenStub(); @@ -153,7 +153,7 @@ namespace Avalonia.DesignerSupport.Remote { public void Save(Stream outputStream) { - + } } @@ -167,10 +167,10 @@ namespace Avalonia.DesignerSupport.Remote class SystemDialogsStub : ISystemDialogImpl { public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) => - Task.FromResult((string[]) null); + Task.FromResult((string[])null); public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) => - Task.FromResult((string) null); + Task.FromResult((string)null); } class ScreenStub : IScreenImpl diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index d8ff370c45..209e1bc7ea 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -301,7 +301,7 @@ namespace Avalonia.Native _native.Hide(); } - public void BeginMoveDrag() + public void BeginMoveDrag(PointerPressedEventArgs e) { _native.BeginMoveDrag(); } @@ -343,7 +343,7 @@ namespace Avalonia.Native _native.SetMinMaxSize(minSize.ToAvnSize(), maxSize.ToAvnSize()); } - public void BeginResizeDrag(WindowEdge edge) + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 860456a838..2630f9cf96 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -878,21 +878,23 @@ namespace Avalonia.X11 } - void BeginMoveResize(NetWmMoveResize side) + void BeginMoveResize(NetWmMoveResize side, PointerPressedEventArgs e) { var pos = GetCursorPos(_x11); XUngrabPointer(_x11.Display, new IntPtr(0)); SendNetWMMessage (_x11.Atoms._NET_WM_MOVERESIZE, (IntPtr) pos.x, (IntPtr) pos.y, (IntPtr) side, (IntPtr) 1, (IntPtr)1); // left button + + e.Pointer.Capture(null); } - public void BeginMoveDrag() + public void BeginMoveDrag(PointerPressedEventArgs e) { - BeginMoveResize(NetWmMoveResize._NET_WM_MOVERESIZE_MOVE); + BeginMoveResize(NetWmMoveResize._NET_WM_MOVERESIZE_MOVE, e); } - public void BeginResizeDrag(WindowEdge edge) + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { var side = NetWmMoveResize._NET_WM_MOVERESIZE_CANCEL; if (edge == WindowEdge.East) @@ -911,7 +913,7 @@ namespace Avalonia.X11 side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT; if (edge == WindowEdge.SouthWest) side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT; - BeginMoveResize(side); + BeginMoveResize(side, e); } public void SetTitle(string title) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e8c3177ec5..e8117f5533 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -331,7 +331,7 @@ namespace Avalonia.Win32 ShowWindow(_showWindowState); } - public void BeginMoveDrag() + public void BeginMoveDrag(PointerPressedEventArgs e) { WindowsMouseDevice.Instance.Capture(null); UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN, @@ -350,7 +350,7 @@ namespace Avalonia.Win32 {WindowEdge.West, UnmanagedMethods.HitTestValues.HTLEFT} }; - public void BeginResizeDrag(WindowEdge edge) + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { #if USE_MANAGED_DRAG _managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position)); From a351e7c523638367d4b361298646f1d21783a68c Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 14 Oct 2019 20:54:26 +0800 Subject: [PATCH 0338/1791] Address review. --- samples/ControlCatalog/DecoratedWindow.xaml.cs | 3 +-- src/Windows/Avalonia.Win32/WindowImpl.cs | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog/DecoratedWindow.xaml.cs b/samples/ControlCatalog/DecoratedWindow.xaml.cs index d76ef0a7bf..bdf5b8fbee 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml.cs +++ b/samples/ControlCatalog/DecoratedWindow.xaml.cs @@ -39,8 +39,7 @@ namespace ControlCatalog SetupSide("TopRight", StandardCursorType.TopRightCorner, WindowEdge.NorthEast); SetupSide("BottomLeft", StandardCursorType.BottomLeftCorner, WindowEdge.SouthWest); SetupSide("BottomRight", StandardCursorType.BottomRightCorner, WindowEdge.SouthEast); - this.FindControl /// The associated device. /// The event timestamp. - public RawInputEventArgs(IInputDevice device, ulong timestamp) + /// The root from which the event originates. + public RawInputEventArgs(IInputDevice device, ulong timestamp, IInputRoot root) { Contract.Requires(device != null); Device = device; Timestamp = timestamp; + Root = root; } /// @@ -34,6 +36,11 @@ namespace Avalonia.Input.Raw /// public IInputDevice Device { get; } + /// + /// Gets the root from which the event originates. + /// + public IInputRoot Root { get; } + /// /// Gets or sets a value indicating whether the event was handled. /// diff --git a/src/Avalonia.Input/Raw/RawKeyEventArgs.cs b/src/Avalonia.Input/Raw/RawKeyEventArgs.cs index cd8b2eacf7..c720cf11f8 100644 --- a/src/Avalonia.Input/Raw/RawKeyEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawKeyEventArgs.cs @@ -14,9 +14,10 @@ namespace Avalonia.Input.Raw public RawKeyEventArgs( IKeyboardDevice device, ulong timestamp, + IInputRoot root, RawKeyEventType type, Key key, RawInputModifiers modifiers) - : base(device, timestamp) + : base(device, timestamp, root) { Key = key; Type = type; diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 88f6daf11f..56854c7d29 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -44,22 +44,16 @@ namespace Avalonia.Input.Raw RawPointerEventType type, Point position, RawInputModifiers inputModifiers) - : base(device, timestamp) + : base(device, timestamp, root) { Contract.Requires(device != null); Contract.Requires(root != null); - Root = root; Position = position; Type = type; InputModifiers = inputModifiers; } - /// - /// Gets the root from which the event originates. - /// - public IInputRoot Root { get; } - /// /// Gets the mouse position, in client DIPs. /// diff --git a/src/Avalonia.Input/Raw/RawTextInputEventArgs.cs b/src/Avalonia.Input/Raw/RawTextInputEventArgs.cs index 0d1e5d2422..010342da15 100644 --- a/src/Avalonia.Input/Raw/RawTextInputEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawTextInputEventArgs.cs @@ -5,11 +5,16 @@ namespace Avalonia.Input.Raw { public class RawTextInputEventArgs : RawInputEventArgs { - public string Text { get; set; } - - public RawTextInputEventArgs(IKeyboardDevice device, ulong timestamp, string text) : base(device, timestamp) + public RawTextInputEventArgs( + IKeyboardDevice device, + ulong timestamp, + IInputRoot root, + string text) + : base(device, timestamp, root) { Text = text; } + + public string Text { get; set; } } } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 209e1bc7ea..fe7458d583 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -204,7 +204,7 @@ namespace Avalonia.Native { Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); - var args = new RawTextInputEventArgs(_keyboard, timeStamp, text); + var args = new RawTextInputEventArgs(_keyboard, timeStamp, _inputRoot, text); Input?.Invoke(args); @@ -215,7 +215,7 @@ namespace Avalonia.Native { Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); - var args = new RawKeyEventArgs(_keyboard, timeStamp, (RawKeyEventType)type, (Key)key, (RawInputModifiers)modifiers); + var args = new RawKeyEventArgs(_keyboard, timeStamp, _inputRoot, (RawKeyEventType)type, (Key)key, (RawInputModifiers)modifiers); Input?.Invoke(args); diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 2630f9cf96..17471fad10 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -455,7 +455,7 @@ namespace Avalonia.X11 key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32(); - ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), + ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev); @@ -470,7 +470,7 @@ namespace Avalonia.X11 if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL return; } - ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), text), + ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text), ref ev); } } diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 71c398481b..2887468046 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -212,17 +212,17 @@ namespace Avalonia.Win32.Interop.Wpf protected override void OnMouseLeave(MouseEventArgs e) => MouseEvent(RawPointerEventType.LeaveWindow, e); protected override void OnKeyDown(KeyEventArgs e) - => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint) e.Timestamp, RawKeyEventType.KeyDown, + => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint) e.Timestamp, _inputRoot, RawKeyEventType.KeyDown, (Key) e.Key, GetModifiers(null))); protected override void OnKeyUp(KeyEventArgs e) - => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint)e.Timestamp, RawKeyEventType.KeyUp, + => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint)e.Timestamp, _inputRoot, RawKeyEventType.KeyUp, (Key)e.Key, GetModifiers(null))); protected override void OnTextInput(TextCompositionEventArgs e) - => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, e.Text)); + => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, _inputRoot, e.Text)); void ITopLevelImpl.SetCursor(IPlatformHandle cursor) { diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index 8ac0f25598..b17e0d6c09 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -8,13 +8,13 @@ namespace Avalonia.Win32 { class OleDropTarget : IDropTarget { - private readonly IInputElement _target; + private readonly IInputRoot _target; private readonly ITopLevelImpl _tl; private readonly IDragDropDevice _dragDevice; private IDataObject _currentDrag = null; - public OleDropTarget(ITopLevelImpl tl, IInputElement target) + public OleDropTarget(ITopLevelImpl tl, IInputRoot target) { _dragDevice = AvaloniaLocator.Current.GetService(); _tl = tl; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 4f4c9852e8..04a9303d53 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -508,6 +508,7 @@ namespace Avalonia.Win32 e = new RawKeyEventArgs( WindowsKeyboardDevice.Instance, timestamp, + _owner, RawKeyEventType.KeyDown, KeyInterop.KeyFromVirtualKey(ToInt32(wParam)), WindowsKeyboardDevice.Instance.Modifiers); break; @@ -521,6 +522,7 @@ namespace Avalonia.Win32 e = new RawKeyEventArgs( WindowsKeyboardDevice.Instance, timestamp, + _owner, RawKeyEventType.KeyUp, KeyInterop.KeyFromVirtualKey(ToInt32(wParam)), WindowsKeyboardDevice.Instance.Modifiers); break; @@ -528,7 +530,7 @@ namespace Avalonia.Win32 // Ignore control chars if (ToInt32(wParam) >= 32) { - e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, + e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, _owner, new string((char)ToInt32(wParam), 1)); } diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 645f87163a..72c81659c3 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -182,6 +182,7 @@ namespace Avalonia.Controls.UnitTests var input = new RawKeyEventArgs( new Mock().Object, 0, + target, RawKeyEventType.KeyDown, Key.A, RawInputModifiers.None); impl.Object.Input(input); From 91662d7020cbb8ab3a370c7f67a02faf4da44d31 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 17 Oct 2019 23:29:00 +0200 Subject: [PATCH 0353/1791] Fix Grid not reacting to cell structure changes during runtime. --- src/Avalonia.Controls/Grid.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 4f41b0bf1e..8ecfe349f8 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -24,13 +24,14 @@ namespace Avalonia.Controls { static Grid() { - IsSharedSizeScopeProperty.Changed.AddClassHandler(DefinitionBase.OnIsSharedSizeScopePropertyChanged); ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); - ColumnProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); - ColumnSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); - RowProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); - RowSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + IsSharedSizeScopeProperty.Changed.AddClassHandler(DefinitionBase.OnIsSharedSizeScopePropertyChanged); + ColumnProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + ColumnSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + RowProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + RowSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); } From b7e509f04637c9fccec17f38caadd5e68196c055 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 18 Oct 2019 15:35:08 +0100 Subject: [PATCH 0354/1791] thumb adds :pressed pseudo class. --- src/Avalonia.Controls/Primitives/Thumb.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 7e9680dc9f..9f5ddb666c 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -83,6 +83,8 @@ namespace Avalonia.Controls.Primitives Vector = (Vector)_lastPoint, }; + PseudoClasses.Add(":pressed"); + RaiseEvent(ev); } @@ -102,6 +104,8 @@ namespace Avalonia.Controls.Primitives RaiseEvent(ev); } + + PseudoClasses.Remove(":pressed"); } } } From 5b1a552127e240b16fb8d34fe9dc0698c79f35ab Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 18 Oct 2019 15:35:51 +0100 Subject: [PATCH 0355/1791] fill in the bottom right corner of scrollviewer area where the horizontal and vertical scrollbars meet. --- src/Avalonia.Themes.Default/ScrollViewer.xaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Themes.Default/ScrollViewer.xaml b/src/Avalonia.Themes.Default/ScrollViewer.xaml index 3e130cad67..38f4eef964 100644 --- a/src/Avalonia.Themes.Default/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Default/ScrollViewer.xaml @@ -36,6 +36,7 @@ Visibility="{TemplateBinding VerticalScrollBarVisibility}" Grid.Column="1" Focusable="False"/> + From 1bc56dc777cd1e41eaa72306877add569dd88023 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 18 Oct 2019 15:36:29 +0100 Subject: [PATCH 0356/1791] overhaul scrollbar templates, to support pointer over, pointer pressed, and nice arrows. basically matches visual studio now. --- src/Avalonia.Themes.Default/RepeatButton.xaml | 6 +- src/Avalonia.Themes.Default/ScrollBar.xaml | 264 +++++++++--------- 2 files changed, 139 insertions(+), 131 deletions(-) diff --git a/src/Avalonia.Themes.Default/RepeatButton.xaml b/src/Avalonia.Themes.Default/RepeatButton.xaml index f555209471..702e4e6ebd 100644 --- a/src/Avalonia.Themes.Default/RepeatButton.xaml +++ b/src/Avalonia.Themes.Default/RepeatButton.xaml @@ -33,12 +33,8 @@ - - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ScrollBar.xaml b/src/Avalonia.Themes.Default/ScrollBar.xaml index c9552a607c..05689def0a 100644 --- a/src/Avalonia.Themes.Default/ScrollBar.xaml +++ b/src/Avalonia.Themes.Default/ScrollBar.xaml @@ -1,128 +1,140 @@ - - - - - - - - + + + + + + + + + + + + + + From 45028cd2eaeb4d56e3d4348fcaead360739438a7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 18 Oct 2019 15:37:06 +0100 Subject: [PATCH 0357/1791] add 2 extra control colour levels like UWP, will need to rename these in 0.10, right now will be a breaking change. --- .../Accents/BaseDark.xaml | 7 ++++++- .../Accents/BaseLight.xaml | 18 +++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index 0ed17fae76..fbe74df1b2 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -14,7 +14,9 @@ #FFA0A0A0 #FF282828 #FF505050 + #FF686868 #FF808080 + #FFEFEBEF #FFA8A8A8 #FF828282 #FF505050 @@ -32,7 +34,9 @@ + + @@ -61,6 +65,7 @@ 12 16 - 10 + 20 + 9 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 3a8a8ec446..2643aae7b2 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -12,10 +12,15 @@ #FFAAAAAA #FF888888 #FF333333 - #FFFFFFFF - #FFAAAAAA - #FF888888 - #FFF0F0F0 + + + #FF868999 + #FFF5F5F5 + #FFC2C3C9 + #FF686868 + #FF5B5B5B + + #FFF0F0F0 #FFD0D0D0 #FF808080 #FF000000 @@ -32,7 +37,9 @@ + + @@ -61,6 +68,7 @@ 12 16 - 10 + 20 + 9 From 7658791ab9e49c416b3934c60047862a3d1d5b9a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 18 Oct 2019 16:52:46 +0100 Subject: [PATCH 0358/1791] fix centralization of buttons. --- src/Avalonia.Themes.Default/ScrollBar.xaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Themes.Default/ScrollBar.xaml b/src/Avalonia.Themes.Default/ScrollBar.xaml index 05689def0a..856fba00c0 100644 --- a/src/Avalonia.Themes.Default/ScrollBar.xaml +++ b/src/Avalonia.Themes.Default/ScrollBar.xaml @@ -5,12 +5,12 @@ - - + - - + From ab54a9ced42c7c40c5aa9556e201b4071ccde0b1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 18 Oct 2019 16:57:48 +0100 Subject: [PATCH 0359/1791] fix indentation --- src/Avalonia.Themes.Default/Accents/BaseDark.xaml | 8 ++++---- src/Avalonia.Themes.Default/Accents/BaseLight.xaml | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index fbe74df1b2..e040ed233e 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -34,9 +34,9 @@ - + - + @@ -65,7 +65,7 @@ 12 16 - 20 - 9 + 20 + 9 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 2643aae7b2..b56bd3eaf6 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -20,7 +20,7 @@ #FF686868 #FF5B5B5B - #FFF0F0F0 + #FFF0F0F0 #FFD0D0D0 #FF808080 #FF000000 @@ -37,9 +37,9 @@ - + - + @@ -68,7 +68,7 @@ 12 16 - 20 - 9 + 20 + 9 From 31e56bc155ed5fb9fb27b5ba2c56ec059248925f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 18 Oct 2019 17:11:57 +0100 Subject: [PATCH 0360/1791] fix centralization of scroll buttons --- src/Avalonia.Themes.Default/Accents/BaseDark.xaml | 4 ++-- src/Avalonia.Themes.Default/Accents/BaseLight.xaml | 4 ++-- src/Avalonia.Themes.Default/ScrollBar.xaml | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index e040ed233e..ffe3e92202 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -65,7 +65,7 @@ 12 16 - 20 - 9 + 18 + 8 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index b56bd3eaf6..c0e5f47eed 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -68,7 +68,7 @@ 12 16 - 20 - 9 + 18 + 8 diff --git a/src/Avalonia.Themes.Default/ScrollBar.xaml b/src/Avalonia.Themes.Default/ScrollBar.xaml index 856fba00c0..64a4399d16 100644 --- a/src/Avalonia.Themes.Default/ScrollBar.xaml +++ b/src/Avalonia.Themes.Default/ScrollBar.xaml @@ -10,7 +10,7 @@ Grid.Row="0" Focusable="False" MinHeight="{DynamicResource ScrollBarThickness}"> - + - + @@ -57,7 +57,7 @@ Grid.Column="0" Focusable="False" MinWidth="{DynamicResource ScrollBarThickness}"> - + - + From c1ce0d6b99745e6c21d3767149bfbf2cad99e4df Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 18 Oct 2019 17:37:21 +0100 Subject: [PATCH 0361/1791] [OSX] fix scroll wheel speed. --- native/Avalonia.Native/src/OSX/window.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 9c7e9323e0..2a5acaea91 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -855,8 +855,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent if(type == Wheel) { - delta.X = [event scrollingDeltaX] / 5; - delta.Y = [event scrollingDeltaY] / 5; + delta.X = [event scrollingDeltaX] / 50; + delta.Y = [event scrollingDeltaY] / 50; if(delta.X == 0 && delta.Y == 0) { From 8f736db8fc71aa8d1545192559d603dd68245365 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 19 Oct 2019 18:32:45 +0300 Subject: [PATCH 0362/1791] Updated XamlIl --- src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index c7155c5f6c..6a6a28513e 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit c7155c5f6c1a5153ee2d8cd78e5d1524dd6744cf +Subproject commit 6a6a28513e5abddacaf52e088635ff52d98f079d From 61340acf4b86e1e9dd5e7e769c6b7438cb9104dd Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 19 Oct 2019 19:11:21 +0300 Subject: [PATCH 0363/1791] Disable IL verification for nuget version of compiler by default --- build/BuildTargets.targets | 1 + packages/Avalonia/AvaloniaBuildTasks.targets | 2 ++ src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs | 4 +++- src/Avalonia.Build.Tasks/Program.cs | 3 ++- src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs | 4 ++-- .../XamlIl/AvaloniaXamlIlRuntimeCompiler.cs | 6 +++--- src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- 7 files changed, 14 insertions(+), 8 deletions(-) diff --git a/build/BuildTargets.targets b/build/BuildTargets.targets index 08ec039ec7..a5543cd050 100644 --- a/build/BuildTargets.targets +++ b/build/BuildTargets.targets @@ -2,6 +2,7 @@ $(MSBuildThisFileDirectory)\..\src\Avalonia.Build.Tasks\bin\$(Configuration)\netstandard2.0\Avalonia.Build.Tasks.dll true + true diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index a455e2b3fc..552713f94b 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -53,6 +53,7 @@ $(IntermediateOutputPath)/Avalonia/references $(IntermediateOutputPath)/Avalonia/original.dll + false !string.IsNullOrWhiteSpace(l)).ToArray(), - ProjectDirectory, OutputPath); + ProjectDirectory, OutputPath, VerifyIl); if (!res.Success) return false; if (!res.WrittenFile) @@ -66,6 +66,8 @@ namespace Avalonia.Build.Tasks public string ProjectDirectory { get; set; } public string OutputPath { get; set; } + + public bool VerifyIl { get; set; } public IBuildEngine BuildEngine { get; set; } public ITaskHost HostObject { get; set; } diff --git a/src/Avalonia.Build.Tasks/Program.cs b/src/Avalonia.Build.Tasks/Program.cs index d356b15408..1909c4c6ec 100644 --- a/src/Avalonia.Build.Tasks/Program.cs +++ b/src/Avalonia.Build.Tasks/Program.cs @@ -28,7 +28,8 @@ namespace Avalonia.Build.Tasks ReferencesFilePath = args[1], OutputPath = args[2], BuildEngine = new ConsoleBuildEngine(), - ProjectDirectory = Directory.GetCurrentDirectory() + ProjectDirectory = Directory.GetCurrentDirectory(), + VerifyIl = true }.Execute() ? 0 : 2; diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 0dd52c9b48..e348eb0fbc 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -40,7 +40,7 @@ namespace Avalonia.Build.Tasks } public static CompileResult Compile(IBuildEngine engine, string input, string[] references, string projectDirectory, - string output) + string output, bool verifyIl) { var typeSystem = new CecilTypeSystem(references.Concat(new[] {input}), input); var asm = typeSystem.TargetAssemblyDefinition; @@ -65,7 +65,7 @@ namespace Avalonia.Build.Tasks var contextClass = XamlIlContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem, xamlLanguage); - var compiler = new AvaloniaXamlIlCompiler(compilerConfig, contextClass); + var compiler = new AvaloniaXamlIlCompiler(compilerConfig, contextClass) { EnableIlVerification = verifyIl }; var editorBrowsableAttribute = typeSystem .GetTypeReference(typeSystem.FindType("System.ComponentModel.EditorBrowsableAttribute")) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs index b91d679fba..5a5da518d0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs @@ -114,10 +114,10 @@ namespace Avalonia.Markup.Xaml.XamlIl InitializeSre(); var asm = localAssembly == null ? null : _sreTypeSystem.GetAssembly(localAssembly); - + var compiler = new AvaloniaXamlIlCompiler(new XamlIlTransformerConfiguration(_sreTypeSystem, asm, - _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter), - _sreContextType); + _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter), + _sreContextType) { EnableIlVerification = true }; var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri); IXamlIlType overrideType = null; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 6a6a28513e..88468ebe90 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 6a6a28513e5abddacaf52e088635ff52d98f079d +Subproject commit 88468ebe90112dc23b04147deba0d9a0dcc99157 From 842958e551aca5e616caf7f70dc3b691eb9c2e90 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 19 Oct 2019 21:26:19 +0300 Subject: [PATCH 0364/1791] Updated XamlIl --- src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 88468ebe90..ad9915e193 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 88468ebe90112dc23b04147deba0d9a0dcc99157 +Subproject commit ad9915e19398a49c5a11b66000c361659ca692b3 From 389d58f550cc5f5d4ba8e8883bd96921b0dddf23 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 20 Oct 2019 14:23:28 +0300 Subject: [PATCH 0365/1791] issue #3089 add failing tests --- .../Primitives/SelectingItemsControlTests.cs | 128 +++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 17f0e609a5..ce333f66d6 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.UnitTests.Primitives public class SelectingItemsControlTests { private MouseTestHelper _helper = new MouseTestHelper(); - + [Fact] public void SelectedIndex_Should_Initially_Be_Minus_1() { @@ -168,6 +168,130 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal("B", listBox.SelectedItem); } + [Fact] + public void Setting_SelectedIndex_Before_Initialize_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single, + Items = new[] { "foo", "bar", "baz" }, + SelectedIndex = 1 + }; + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + + [Fact] + public void Setting_SelectedIndex_During_Initialize_Should_Take_Priority_Over_Previous_Value() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single, + Items = new[] { "foo", "bar", "baz" }, + SelectedIndex = 2 + }; + + listBox.BeginInit(); + + listBox.SelectedIndex = 1; + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + + [Fact] + public void Setting_SelectedItem_Before_Initialize_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single, + Items = new[] { "foo", "bar", "baz" }, + SelectedItem = "bar" + }; + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + + + [Fact] + public void Setting_SelectedItems_Before_Initialize_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Multiple, + Items = new[] { "foo", "bar", "baz" }, + }; + + var selected = new[] { "foo", "bar" }; + + foreach (var v in selected) + { + listBox.SelectedItems.Add(v); + } + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(selected, listBox.SelectedItems); + } + + [Fact] + public void Setting_SelectedItems_During_Initialize_Should_Take_Priority_Over_Previous_Value() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Multiple, + Items = new[] { "foo", "bar", "baz" }, + }; + + var selected = new[] { "foo", "bar" }; + + foreach (var v in new[] { "bar", "baz" }) + { + listBox.SelectedItems.Add(v); + } + + listBox.BeginInit(); + + listBox.SelectedItems = new AvaloniaList(selected); + + listBox.EndInit(); + + Assert.Equal(selected, listBox.SelectedItems); + } + + [Fact] + public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single | SelectionMode.AlwaysSelected, + + Items = new[] { "foo", "bar", "baz" }, + SelectedIndex = 1 + }; + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + [Fact] public void Setting_SelectedIndex_Before_ApplyTemplate_Should_Set_Item_IsSelected_True() { @@ -849,7 +973,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var target = new ListBox { Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz"}, + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, }; target.ApplyTemplate(); From 2e70be023b4a5236d080be36b11572866acf0271 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 20 Oct 2019 14:36:23 +0300 Subject: [PATCH 0366/1791] fixed #3089 now set selecteditem/s before initialization should work properly --- .../Primitives/SelectingItemsControl.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7fddee1012..4b33b18475 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1088,9 +1088,15 @@ namespace Avalonia.Controls.Primitives } else { - SelectedIndex = _updateSelectedIndex != int.MinValue ? - _updateSelectedIndex : - AlwaysSelected ? 0 : -1; + if (_updateSelectedIndex != int.MinValue) + { + SelectedIndex = _updateSelectedIndex; + } + + if (AlwaysSelected && SelectedIndex == -1) + { + SelectedIndex = 0; + } } } } From 919f6fc5186c31c52c95b4b76c07239d421786b9 Mon Sep 17 00:00:00 2001 From: nishanth2143 Date: Mon, 21 Oct 2019 20:07:33 +0530 Subject: [PATCH 0367/1791] Updated README.md file Corrected grammatical errors --- readme.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index ee44a0cc3f..512b35a454 100644 --- a/readme.md +++ b/readme.md @@ -8,9 +8,9 @@ ## About -**Avalonia** is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), MacOS and with experimental support for Android and iOS. +**Avalonia** is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS and with experimental support for Android and iOS. -**Avalonia** is ready for **General-Purpose Desktop App Development**. However there may be some bugs and breaking changes as we continue along into this project's development. To see the status for some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). +**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). | Control catalog | Desktop platforms | Mobile platforms | |---|---|---| @@ -20,11 +20,11 @@ Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (screenshot). Now you can write code and markup that will work on multiple platforms! -For those without Visual Studio, starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). +For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). Avalonia is delivered via NuGet package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed)) -Use these commands in Package Manager console to install Avalonia manually: +Use these commands in the Package Manager console to install Avalonia manually: ``` Install-Package Avalonia Install-Package Avalonia.Desktop From 47c9f53e9a4aadf9d626541082326259e5684b6d Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 22 Oct 2019 00:38:39 +0800 Subject: [PATCH 0368/1791] Don't reopen an already active submenu on hover. --- .../Platform/DefaultMenuInteractionHandler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index b5dbd1e668..97aeead608 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -273,7 +273,8 @@ namespace Avalonia.Controls.Platform if (item.IsTopLevel) { - if (item.Parent.SelectedItem?.IsSubMenuOpen == true) + if (item != item.Parent.SelectedItem && + item.Parent.SelectedItem?.IsSubMenuOpen == true) { item.Parent.SelectedItem.Close(); SelectItemAndAncestors(item); From 1f2a37bcb77a98c66cdbf566dc71f24a38b94daf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 21 Oct 2019 22:17:26 +0100 Subject: [PATCH 0369/1791] remove vertical seperator on menus between icon and menu header, polished spacing, now closely matches Windows and Visual Studio menus --- src/Avalonia.Themes.Default/ContextMenu.xaml | 8 ------ src/Avalonia.Themes.Default/MenuItem.xaml | 29 +++++--------------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Themes.Default/ContextMenu.xaml b/src/Avalonia.Themes.Default/ContextMenu.xaml index 53d7c5abb4..75f8f7c23d 100644 --- a/src/Avalonia.Themes.Default/ContextMenu.xaml +++ b/src/Avalonia.Themes.Default/ContextMenu.xaml @@ -11,19 +11,11 @@ BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}"> - - - diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index a794d15577..93989d3782 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -4,14 +4,14 @@ - - - - - - - + + - - - - - - - + - + diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index a790d2fca1..15d62e9140 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -2,9 +2,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Platform; using Avalonia.UnitTests; - using Moq; - using Xunit; namespace Avalonia.Controls.UnitTests @@ -21,183 +19,246 @@ namespace Avalonia.Controls.UnitTests public void Detects_Horizontal_Orientation() { GridSplitter splitter; - var grid = new Grid() - { - RowDefinitions = new RowDefinitions("*,Auto,*"), - ColumnDefinitions = new ColumnDefinitions("*,*"), - Children = - { - new Border { [Grid.RowProperty] = 0 }, - (splitter = new GridSplitter { [Grid.RowProperty] = 1 }), - new Border { [Grid.RowProperty] = 2 } - } - }; + + var grid = new Grid + { + RowDefinitions = new RowDefinitions("*,Auto,*"), + ColumnDefinitions = new ColumnDefinitions("*,*"), + Children = + { + new Border { [Grid.RowProperty] = 0 }, + (splitter = new GridSplitter { [Grid.RowProperty] = 1 }), + new Border { [Grid.RowProperty] = 2 } + } + }; var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(splitter.Classes, ":horizontal".Equals); + Assert.Equal(GridResizeDirection.Rows, splitter.GetEffectiveResizeDirection()); } [Fact] public void Detects_Vertical_Orientation() { GridSplitter splitter; - var grid = new Grid() - { - ColumnDefinitions = new ColumnDefinitions("*,Auto,*"), - RowDefinitions = new RowDefinitions("*,*"), - Children = - { - new Border { [Grid.ColumnProperty] = 0 }, - (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), - new Border { [Grid.ColumnProperty] = 2 }, - } - }; + + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("*,Auto,*"), + RowDefinitions = new RowDefinitions("*,*"), + Children = + { + new Border { [Grid.ColumnProperty] = 0 }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }), + new Border { [Grid.ColumnProperty] = 2 }, + } + }; var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(splitter.Classes, ":vertical".Equals); + Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection()); } [Fact] public void Detects_With_Both_Auto() { GridSplitter splitter; - var grid = new Grid() - { - ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), - RowDefinitions = new RowDefinitions("Auto,Auto"), - Children = - { - new Border { [Grid.ColumnProperty] = 0 }, - (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), - new Border { [Grid.ColumnProperty] = 2 }, - } - }; + + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), + RowDefinitions = new RowDefinitions("Auto,Auto"), + Children = + { + new Border { [Grid.ColumnProperty] = 0 }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }), + new Border { [Grid.ColumnProperty] = 2 }, + } + }; var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(splitter.Classes, ":vertical".Equals); + Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection()); } [Fact] - public void Horizontal_Stays_Within_Constraints() + public void In_First_Position_Doesnt_Throw_Exception() + { + GridSplitter splitter; + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,*,*"), + RowDefinitions = new RowDefinitions("*,*"), + Children = + { + (splitter = new GridSplitter { [Grid.ColumnProperty] = 0 }), + new Border { [Grid.ColumnProperty] = 1 }, + new Border { [Grid.ColumnProperty] = 2 }, + } + }; + + var root = new TestRoot { Child = grid }; + root.Measure(new Size(100, 300)); + root.Arrange(new Rect(0, 0, 100, 300)); + + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, Vector = new Vector(100, 1000) + }); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Horizontal_Stays_Within_Constraints(bool showsPreview) { var control1 = new Border { [Grid.RowProperty] = 0 }; - var splitter = new GridSplitter - { - [Grid.RowProperty] = 1, - }; + var splitter = new GridSplitter { [Grid.RowProperty] = 1, ShowsPreview = showsPreview}; var control2 = new Border { [Grid.RowProperty] = 2 }; - var rowDefinitions = new RowDefinitions() - { - new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 }, - new RowDefinition(GridLength.Auto), - new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 }, - }; - - var grid = new Grid() - { - RowDefinitions = rowDefinitions, - Children = - { - control1, splitter, control2 - } - }; + var rowDefinitions = new RowDefinitions + { + new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 }, + new RowDefinition(GridLength.Auto), + new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 }, + }; + + var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = new VisualLayerManager + { + Child = grid + } + }; - var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 200)); root.Arrange(new Rect(0, 0, 100, 200)); + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(0, -100) - }); - Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star)); - Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star)); + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(0, -100) + }); + + if (showsPreview) + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star)); + } + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(0, 100) - }); - Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star)); - Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star)); - } + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(0, 100) + }); - [Fact] - public void In_First_Position_Doesnt_Throw_Exception() - { - GridSplitter splitter; - var grid = new Grid() - { - ColumnDefinitions = new ColumnDefinitions("Auto,*,*"), - RowDefinitions = new RowDefinitions("*,*"), - Children = - { - (splitter = new GridSplitter { [Grid.ColumnProperty] = 0} ), - new Border { [Grid.ColumnProperty] = 1 }, - new Border { [Grid.ColumnProperty] = 2 }, - } - }; + if (showsPreview) + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star)); + } - var root = new TestRoot { Child = grid }; - root.Measure(new Size(100, 300)); - root.Arrange(new Rect(0, 0, 100, 300)); splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(100, 1000) - }); + { + RoutedEvent = Thumb.DragCompletedEvent + }); + + Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star)); } - [Fact] - public void Vertical_Stays_Within_Constraints() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Vertical_Stays_Within_Constraints(bool showsPreview) { var control1 = new Border { [Grid.ColumnProperty] = 0 }; - var splitter = new GridSplitter - { - [Grid.ColumnProperty] = 1, - }; + var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, ShowsPreview = showsPreview}; var control2 = new Border { [Grid.ColumnProperty] = 2 }; - var columnDefinitions = new ColumnDefinitions() - { - new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 }, - new ColumnDefinition(GridLength.Auto), - new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 }, - }; - - var grid = new Grid() - { - ColumnDefinitions = columnDefinitions, - Children = - { - control1, splitter, control2 - } - }; + var columnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 }, + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 }, + }; - var root = new TestRoot { Child = grid }; + var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = new VisualLayerManager + { + Child = grid + } + }; root.Measure(new Size(200, 100)); root.Arrange(new Rect(0, 0, 200, 100)); + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(-100, 0) - }); - Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star)); - Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star)); + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(-100, 0) + }); + + if (showsPreview) + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star)); + } + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(100, 0) - }); + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(100, 0) + }); + + if (showsPreview) + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); + } + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragCompletedEvent + }); + Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star)); Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); } From ab26c18a8f882b1665cbd62dfdbc5a877c5c534e Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sat, 26 Oct 2019 21:01:43 +0300 Subject: [PATCH 0393/1791] typo fix --- .../Avalonia.Controls.UnitTests/GridTests.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index b74dd641df..a2d6f14b26 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1383,7 +1383,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { if (setUsingAvaloniaProperty) grid.ColumnDefinitions[0][ColumnDefinition.WidthProperty] = new GridLength(5); @@ -1399,7 +1399,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { if (setUsingAvaloniaProperty) grid.ColumnDefinitions[0][ColumnDefinition.MinWidthProperty] = 5; @@ -1415,7 +1415,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { if (setUsingAvaloniaProperty) grid.ColumnDefinitions[0][ColumnDefinition.MaxWidthProperty] = 5; @@ -1431,7 +1431,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { if (setUsingAvaloniaProperty) grid.RowDefinitions[0][RowDefinition.HeightProperty] = new GridLength(5); @@ -1447,7 +1447,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { if (setUsingAvaloniaProperty) grid.RowDefinitions[0][RowDefinition.MinHeightProperty] = 5; @@ -1463,7 +1463,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { if (setUsingAvaloniaProperty) grid.RowDefinitions[0][RowDefinition.MaxHeightProperty] = 5; @@ -1477,7 +1477,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { grid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(5))); }); @@ -1488,7 +1488,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { grid.RowDefinitions.Add(new RowDefinition(new GridLength(5))); }); @@ -1499,7 +1499,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { grid.ColumnDefinitions = ColumnDefinitions.Parse("2*,1*"); }); @@ -1510,7 +1510,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { grid.RowDefinitions = RowDefinitions.Parse("2*,1*"); }); @@ -1521,7 +1521,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { grid.ColumnDefinitions.RemoveAt(0); }); @@ -1532,7 +1532,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { grid.RowDefinitions.RemoveAt(0); }); @@ -1601,7 +1601,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(10, grid.Children[1].Bounds.Width); } - private static void Change_Propery_And_Verify_Measure_Requested(Grid grid, Action change) + private static void Change_Property_And_Verify_Measure_Requested(Grid grid, Action change) { grid.Measure(new Size(100, 100)); grid.Arrange(new Rect(grid.DesiredSize)); From bf04d22856d36ffeded87fedd211de21c36a66f3 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 19:49:20 +0100 Subject: [PATCH 0394/1791] Comment fixes. --- src/Avalonia.Controls/GridSplitter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index e5749c8ed9..3fc1ed3ea5 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -16,7 +16,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls { /// - /// Enum to indicate whether GridSplitter resizes Columns or Rows. + /// Enum to indicate whether resizes Columns or Rows. /// public enum GridResizeDirection { @@ -38,7 +38,7 @@ namespace Avalonia.Controls } /// - /// Enum to indicate what Columns or Rows the GridSplitter resizes. + /// Enum to indicate what Columns or Rows the resizes. /// public enum GridResizeBehavior { From 14b3683127db6661c127a98b79b7842d2d44d894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sat, 26 Oct 2019 20:47:10 +0100 Subject: [PATCH 0395/1791] Replaced obsolete API usages. --- src/Avalonia.Controls/ListBox.cs | 20 ++++++++++++------- .../NumericUpDown/NumericUpDown.cs | 4 ++-- src/Avalonia.Controls/Primitives/TabStrip.cs | 10 ++++++++-- src/Avalonia.Controls/Primitives/Thumb.cs | 2 -- src/Avalonia.Controls/TextBox.cs | 8 ++++---- src/Avalonia.Controls/TreeView.cs | 19 +++++++++++------- src/Avalonia.Input/AccessKeyHandler.cs | 2 +- src/Avalonia.Input/FocusManager.cs | 2 +- 8 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 449ca18465..b15a6ce668 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -132,14 +133,19 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) + if (e.Source is IVisual source) { - e.Handled = UpdateSelectionFromEventSource( - e.Source, - true, - (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0, - e.MouseButton == MouseButton.Right); + var point = e.GetCurrentPoint(source); + + if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed) + { + e.Handled = UpdateSelectionFromEventSource( + e.Source, + true, + (e.KeyModifiers & KeyModifiers.Shift) != 0, + (e.KeyModifiers & KeyModifiers.Control) != 0, + point.Properties.IsRightButtonPressed); + } } } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index f8ae5c9690..6d450a0155 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -804,9 +804,9 @@ namespace Avalonia.Controls private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e) { - if (e.Device.Captured != Spinner) + if (e.Pointer.Captured != Spinner) { - Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input); + Dispatcher.UIThread.InvokeAsync(() => { e.Pointer.Capture(Spinner); }, DispatcherPriority.Input); } } diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index ec0dbd124c..e1a6cf79bb 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -44,9 +45,14 @@ namespace Avalonia.Controls.Primitives { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left) + if (e.Source is IVisual source) { - e.Handled = UpdateSelectionFromEventSource(e.Source); + var point = e.GetCurrentPoint(source); + + if (point.Properties.IsLeftButtonPressed) + { + e.Handled = UpdateSelectionFromEventSource(e.Source); + } } } } diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 9f5ddb666c..d88018cf32 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -73,7 +73,6 @@ namespace Avalonia.Controls.Primitives protected override void OnPointerPressed(PointerPressedEventArgs e) { - e.Device.Capture(this); e.Handled = true; _lastPoint = e.GetPosition(this); @@ -92,7 +91,6 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { - e.Device.Capture(null); e.Handled = true; _lastPoint = null; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index fdc9d153e2..3d472fca18 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -677,13 +677,13 @@ namespace Avalonia.Controls } } - e.Device.Capture(_presenter); + e.Pointer.Capture(_presenter); e.Handled = true; } protected override void OnPointerMoved(PointerEventArgs e) { - if (_presenter != null && e.Device.Captured == _presenter) + if (_presenter != null && e.Pointer.Captured == _presenter) { var point = e.GetPosition(_presenter); @@ -694,9 +694,9 @@ namespace Avalonia.Controls protected override void OnPointerReleased(PointerReleasedEventArgs e) { - if (_presenter != null && e.Device.Captured == _presenter) + if (_presenter != null && e.Pointer.Captured == _presenter) { - e.Device.Capture(null); + e.Pointer.Capture(null); } } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 8907137ecb..59844be8a6 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -507,14 +507,19 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) + if (e.Source is IVisual source) { - e.Handled = UpdateSelectionFromEventSource( - e.Source, - true, - (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0, - e.MouseButton == MouseButton.Right); + var point = e.GetCurrentPoint(source); + + if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed) + { + e.Handled = UpdateSelectionFromEventSource( + e.Source, + true, + (e.KeyModifiers & KeyModifiers.Shift) != 0, + (e.KeyModifiers & KeyModifiers.Control) != 0, + point.Properties.IsRightButtonPressed); + } } } diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 9e4b2b84e0..aa009770f6 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -182,7 +182,7 @@ namespace Avalonia.Input { bool menuIsOpen = MainMenu?.IsOpen == true; - if ((e.Modifiers & InputModifiers.Alt) != 0 || menuIsOpen) + if ((e.KeyModifiers & KeyModifiers.Alt) != 0 || menuIsOpen) { // If any other key is pressed with the Alt key held down, or the main menu is open, // find all controls who have registered that access key. diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 80e18cb7bf..104ac9cb61 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -180,7 +180,7 @@ namespace Avalonia.Input if (sender == e.Source && ev.MouseButton == MouseButton.Left) { - var element = (ev.Device?.Captured as IInputElement) ?? (e.Source as IInputElement); + var element = (ev.Pointer?.Captured as IInputElement) ?? (e.Source as IInputElement); if (element == null || !CanFocus(element)) { From b315d5024f28d82f8eb51b0a46a86085d13d24d9 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 21:42:59 +0100 Subject: [PATCH 0396/1791] Move enums lower. Add workaround for thumb raising multiple started events. --- src/Avalonia.Controls/GridSplitter.cs | 106 ++++++++++++++------------ 1 file changed, 56 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 3fc1ed3ea5..56a28e15c2 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -16,55 +16,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls { /// - /// Enum to indicate whether resizes Columns or Rows. - /// - public enum GridResizeDirection - { - /// - /// Determines whether to resize rows or columns based on its Alignment and - /// width compared to height. - /// - Auto, - - /// - /// Resize columns when dragging Splitter. - /// - Columns, - - /// - /// Resize rows when dragging Splitter. - /// - Rows - } - - /// - /// Enum to indicate what Columns or Rows the resizes. - /// - public enum GridResizeBehavior - { - /// - /// Determine which columns or rows to resize based on its Alignment. - /// - BasedOnAlignment, - - /// - /// Resize the current and next Columns or Rows. - /// - CurrentAndNext, - - /// - /// Resize the previous and current Columns or Rows. - /// - PreviousAndCurrent, - - /// - /// Resize the previous and next Columns or Rows. - /// - PreviousAndNext - } - - /// - /// Represents the control that redistributes space between columns or rows of a Grid control. + /// Represents the control that redistributes space between columns or rows of a control. /// public class GridSplitter : Thumb { @@ -427,7 +379,13 @@ namespace Avalonia.Controls { base.OnDragStarted(e); - Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called"); + // TODO: Looks like that sometimes thumb will raise multiple drag started events. + // Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called"); + + if (_resizeData != null) + { + return; + } InitializeData(ShowsPreview); } @@ -845,4 +803,52 @@ namespace Avalonia.Controls public double SplitterLength; } } + + /// + /// Enum to indicate whether resizes Columns or Rows. + /// + public enum GridResizeDirection + { + /// + /// Determines whether to resize rows or columns based on its Alignment and + /// width compared to height. + /// + Auto, + + /// + /// Resize columns when dragging Splitter. + /// + Columns, + + /// + /// Resize rows when dragging Splitter. + /// + Rows + } + + /// + /// Enum to indicate what Columns or Rows the resizes. + /// + public enum GridResizeBehavior + { + /// + /// Determine which columns or rows to resize based on its Alignment. + /// + BasedOnAlignment, + + /// + /// Resize the current and next Columns or Rows. + /// + CurrentAndNext, + + /// + /// Resize the previous and current Columns or Rows. + /// + PreviousAndCurrent, + + /// + /// Resize the previous and next Columns or Rows. + /// + PreviousAndNext + } } From 9c5e688958c2f2590644d8e54f1951d92d8551c6 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 22:13:49 +0100 Subject: [PATCH 0397/1791] Fix Grid shared scope not being updated when resizing row/column definitions. --- src/Avalonia.Controls/ColumnDefinition.cs | 4 ++- src/Avalonia.Controls/DefinitionBase.cs | 33 +++++++++++++++-- src/Avalonia.Controls/RowDefinition.cs | 4 ++- .../Avalonia.Controls.UnitTests/GridTests.cs | 35 +++++++++++++++++++ 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs index 6cad357e93..293b6326d6 100644 --- a/src/Avalonia.Controls/ColumnDefinition.cs +++ b/src/Avalonia.Controls/ColumnDefinition.cs @@ -31,7 +31,9 @@ namespace Avalonia.Controls /// static ColumnDefinition() { - AffectsParentMeasure(WidthProperty, MinWidthProperty, MaxWidthProperty); + AffectsParentMeasure(MinWidthProperty, MaxWidthProperty); + + WidthProperty.Changed.AddClassHandler(OnUserSizePropertyChanged); } /// diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 6ad32f080a..0b45d7e16a 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -7,9 +7,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; - -using Avalonia; -using Avalonia.Collections; using Avalonia.Utilities; namespace Avalonia.Controls @@ -118,6 +115,36 @@ namespace Avalonia.Controls } } + /// + /// This method needs to be internal to be accessable from derived classes. + /// + internal static void OnUserSizePropertyChanged(DefinitionBase definition, AvaloniaPropertyChangedEventArgs e) + { + if (definition.Parent == null) + { + return; + } + + if (definition._sharedState != null) + { + definition._sharedState.Invalidate(); + } + else + { + GridUnitType oldUnitType = ((GridLength)e.OldValue).GridUnitType; + GridUnitType newUnitType = ((GridLength)e.NewValue).GridUnitType; + + if (oldUnitType != newUnitType) + { + definition.Parent.Invalidate(); + } + else + { + definition.Parent.InvalidateMeasure(); + } + } + } + /// /// Returns true if this definition is a part of shared group. /// diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/RowDefinition.cs index 1a1a7e770b..85e7ed6519 100644 --- a/src/Avalonia.Controls/RowDefinition.cs +++ b/src/Avalonia.Controls/RowDefinition.cs @@ -31,7 +31,9 @@ namespace Avalonia.Controls /// static RowDefinition() { - AffectsParentMeasure(HeightProperty, MaxHeightProperty, MinHeightProperty); + AffectsParentMeasure(MaxHeightProperty, MinHeightProperty); + + HeightProperty.Changed.AddClassHandler(OnUserSizePropertyChanged); } /// diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index a2d6f14b26..353bb9c98d 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1172,6 +1172,41 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); } + [Fact] + public void Size_Group_Definition_Resizes_Are_Tracked() + { + var grids = new[] { + CreateGrid(("A", new GridLength(5, GridUnitType.Pixel)), (null, new GridLength())), + CreateGrid(("A", new GridLength(5, GridUnitType.Pixel)), (null, new GridLength())) }; + var scope = new Grid(); + foreach (var xgrids in grids) + scope.Children.Add(xgrids); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + root.Measure(new Size(50, 50)); + root.Arrange(new Rect(new Point(), new Point(50, 50))); + + PrintColumnDefinitions(grids[0]); + Assert.Equal(5, grids[0].ColumnDefinitions[0].ActualWidth); + Assert.Equal(5, grids[1].ColumnDefinitions[0].ActualWidth); + + grids[0].ColumnDefinitions[0].Width = new GridLength(10, GridUnitType.Pixel); + + foreach (Grid grid in grids) + { + grid.Measure(new Size(50, 50)); + grid.Arrange(new Rect(new Point(), new Point(50, 50))); + } + + PrintColumnDefinitions(grids[0]); + Assert.Equal(10, grids[0].ColumnDefinitions[0].ActualWidth); + Assert.Equal(10, grids[1].ColumnDefinitions[0].ActualWidth); + } + [Fact] public void Collection_Changes_Are_Tracked() { From 969166422eff8622f2f48963cf169f9f39b40c7d Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 22:23:38 +0100 Subject: [PATCH 0398/1791] Comment fix. --- src/Avalonia.Controls/DefinitionBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 0b45d7e16a..e4ae777453 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -116,7 +116,7 @@ namespace Avalonia.Controls } /// - /// This method needs to be internal to be accessable from derived classes. + /// Notifies parent or size scope that definition size has been changed. /// internal static void OnUserSizePropertyChanged(DefinitionBase definition, AvaloniaPropertyChangedEventArgs e) { From 1d7feade1b6b8dffbb83487a0f0c588988200467 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 23:32:20 +0100 Subject: [PATCH 0399/1791] Add keyboard input tests. --- .../GridSplitterTests.cs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index 15d62e9140..f2b6b0db4b 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -262,5 +262,123 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star)); Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); } + + [Theory] + [InlineData(Key.Up, 90, 110)] + [InlineData(Key.Down, 110, 90)] + public void Vertical_Keyboard_Input_Can_Move_Splitter(Key key, double expectedHeightFirst, double expectedHeightSecond) + { + var control1 = new Border { [Grid.RowProperty] = 0 }; + var splitter = new GridSplitter { [Grid.RowProperty] = 1, KeyboardIncrement = 10d }; + var control2 = new Border { [Grid.RowProperty] = 2 }; + + var rowDefinitions = new RowDefinitions + { + new RowDefinition(1, GridUnitType.Star), + new RowDefinition(GridLength.Auto), + new RowDefinition(1, GridUnitType.Star) + }; + + var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = grid + }; + + root.Measure(new Size(200, 200)); + root.Arrange(new Rect(0, 0, 200, 200)); + + splitter.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = key + }); + + Assert.Equal(rowDefinitions[0].Height, new GridLength(expectedHeightFirst, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(expectedHeightSecond, GridUnitType.Star)); + } + + [Theory] + [InlineData(Key.Left, 90, 110)] + [InlineData(Key.Right, 110, 90)] + public void Horizontal_Keyboard_Input_Can_Move_Splitter(Key key, double expectedWidthFirst, double expectedWidthSecond) + { + var control1 = new Border { [Grid.ColumnProperty] = 0 }; + var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, KeyboardIncrement = 10d }; + var control2 = new Border { [Grid.ColumnProperty] = 2 }; + + var columnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(1, GridUnitType.Star) + }; + + var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = grid + }; + + root.Measure(new Size(200, 200)); + root.Arrange(new Rect(0, 0, 200, 200)); + + splitter.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = key + }); + + Assert.Equal(columnDefinitions[0].Width, new GridLength(expectedWidthFirst, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(expectedWidthSecond, GridUnitType.Star)); + } + + [Fact] + public void Pressing_Escape_Key_Cancels_Resizing() + { + var control1 = new Border { [Grid.ColumnProperty] = 0 }; + var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, KeyboardIncrement = 10d }; + var control2 = new Border { [Grid.ColumnProperty] = 2 }; + + var columnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(1, GridUnitType.Star) + }; + + var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = grid + }; + + root.Measure(new Size(200, 200)); + root.Arrange(new Rect(0, 0, 200, 200)); + + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(-100, 0) + }); + + Assert.Equal(columnDefinitions[0].Width, new GridLength(0, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(200, GridUnitType.Star)); + + splitter.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Escape + }); + + Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star)); + } } } From 6dd6f336da5509d517c3e0c1935ea322ca2e7f0e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 23:59:17 +0100 Subject: [PATCH 0400/1791] Restore original delta constraints algorithm. --- src/Avalonia.Controls/GridSplitter.cs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 56a28e15c2..a2fefa0548 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -554,22 +554,9 @@ namespace Avalonia.Controls definition2Min = Math.Max(definition2Min, _resizeData.SplitterLength); } - if (_resizeData.SplitBehavior == SplitBehavior.Split) - { - // Determine the minimum and maximum the columns can be resized. - minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len); - maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min); - } - else if (_resizeData.SplitBehavior == SplitBehavior.Resize1) - { - minDelta = definition1Min - definition1Len; - maxDelta = definition1Max - definition1Len; - } - else - { - minDelta = definition2Len - definition2Max; - maxDelta = definition2Len - definition2Min; - } + // Determine the minimum and maximum the columns can be resized. + minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len); + maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min); } /// From 58b79a7724f1d492758a60bc7723abbc65b3be3b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Oct 2019 18:47:18 +0100 Subject: [PATCH 0401/1791] Validate min/max in MathUtilities.Clamp. --- src/Avalonia.Base/Utilities/MathUtilities.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 41b57b6e70..027028480c 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -159,6 +159,11 @@ namespace Avalonia.Utilities /// The clamped value. public static int Clamp(int val, int min, int max) { + if (min > max) + { + throw new ArgumentException($"{min} cannot be greater than {max}."); + } + if (val < min) { return min; From 0a8915b1cc9ffb2868a77e287527cb492dc272d6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Oct 2019 19:55:23 +0100 Subject: [PATCH 0402/1791] Added failing test for #3148 --- .../Primitives/SelectingItemsControlTests.cs | 61 +++++++++++++++++++ tests/Avalonia.UnitTests/TestRoot.cs | 11 ++++ 2 files changed, 72 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 17f0e609a5..e5bbde2eba 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -14,6 +15,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Data; +using Avalonia.Styling; using Avalonia.UnitTests; using Moq; using Xunit; @@ -980,6 +982,45 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.True(raised); } + [Fact] + public void AutoScrollToSelectedItem_On_Reset_Works() + { + // Issue #3148 + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = new ResettingCollection(100); + + var target = new ListBox + { + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => + new TextBlock + { + Text = x, + Width = 100, + Height = 10 + }), + AutoScrollToSelectedItem = true, + VirtualizationMode = ItemVirtualizationMode.Simple, + }; + + var root = new TestRoot(true, target); + root.Measure(new Size(100, 100)); + root.Arrange(new Rect(0, 0, 100, 100)); + + Assert.True(target.Presenter.Panel.Children.Count > 0); + Assert.True(target.Presenter.Panel.Children.Count < 100); + + target.SelectedItem = "Item99"; + + // #3148 triggered here. + items.Reset(new[] { "Item99" }); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(1, target.Presenter.Panel.Children.Count); + } + } + [Fact] public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization() { @@ -1028,6 +1069,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Name = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], + [~ItemsPresenter.VirtualizationModeProperty] = control[~ListBox.VirtualizationModeProperty], }.RegisterInNameScope(scope)); } @@ -1072,5 +1114,24 @@ namespace Avalonia.Controls.UnitTests.Primitives return base.MoveSelection(direction, wrap); } } + + private class ResettingCollection : List, INotifyCollectionChanged + { + public ResettingCollection(int itemCount) + { + AddRange(Enumerable.Range(0, itemCount).Select(x => $"Item{x}")); + } + + public void Reset(IEnumerable items) + { + Clear(); + AddRange(items); + CollectionChanged?.Invoke( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public event NotifyCollectionChangedEventHandler CollectionChanged; + } } } diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 969d7bc821..56d7f028f2 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -24,8 +24,19 @@ namespace Avalonia.UnitTests } public TestRoot(IControl child) + : this(false, child) + { + Child = child; + } + + public TestRoot(bool useGlobalStyles, IControl child) : this() { + if (useGlobalStyles) + { + StylingParent = UnitTestApplication.Current; + } + Child = child; } From 4f41a70455516f9ee73c87212ea7913a038deb97 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Oct 2019 21:45:06 +0100 Subject: [PATCH 0403/1791] Make sure ItemsPresenter is updated before selection change. Having a separate subscription to `Items.CollectionChanged` in both `ItemsControl` and `ItemsPresenter` meant that the `ItemsPresenter` sometimes doesn't update itself in time for a selection change, resulting in #3148. Make `ItemsControl` trigger `ItemsPresenter.ItemsChanged` rather than both controls listening to the event separately. --- src/Avalonia.Controls/ItemsControl.cs | 6 ++++-- .../Presenters/IItemsPresenter.cs | 4 ++++ .../Presenters/ItemsPresenterBase.cs | 15 +++++++++++++-- .../Primitives/SelectingItemsControl.cs | 17 +++++++++++++---- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index b027da6d0c..b93346792d 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -356,6 +356,7 @@ namespace Avalonia.Controls var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; + Presenter?.ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); UpdateItemCount(); RemoveControlItemsFromLogicalChildren(oldValue); AddControlItemsToLogicalChildren(newValue); @@ -370,6 +371,9 @@ namespace Avalonia.Controls /// The event args. protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { + UpdateItemCount(); + Presenter?.ItemsChanged(e); + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -381,8 +385,6 @@ namespace Avalonia.Controls break; } - UpdateItemCount(); - var collection = sender as ICollection; PseudoClasses.Set(":empty", collection == null || collection.Count == 0); PseudoClasses.Set(":singleitem", collection != null && collection.Count == 1); diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 42311dc781..21a03402a0 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -1,12 +1,16 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections.Specialized; + namespace Avalonia.Controls.Presenters { public interface IItemsPresenter : IPresenter { IPanel Panel { get; } + void ItemsChanged(NotifyCollectionChangedEventArgs e); + void ScrollIntoView(object item); } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 0f0cdc37cf..ef1f277162 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -63,7 +63,7 @@ namespace Avalonia.Controls.Presenters _itemsSubscription?.Dispose(); _itemsSubscription = null; - if (_createdPanel && value is INotifyCollectionChanged incc) + if (!IsHosted && _createdPanel && value is INotifyCollectionChanged incc) { _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } @@ -130,6 +130,8 @@ namespace Avalonia.Controls.Presenters private set; } + protected bool IsHosted => TemplatedParent is IItemsPresenterHost; + /// public override sealed void ApplyTemplate() { @@ -144,6 +146,15 @@ namespace Avalonia.Controls.Presenters { } + /// + void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e) + { + if (Panel != null) + { + ItemsChanged(e); + } + } + /// /// Creates the for the control. /// @@ -215,7 +226,7 @@ namespace Avalonia.Controls.Presenters _createdPanel = true; - if (_itemsSubscription == null && Items is INotifyCollectionChanged incc) + if (!IsHosted && _itemsSubscription == null && Items is INotifyCollectionChanged incc) { _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7fddee1012..abad53f0d6 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -302,13 +302,24 @@ namespace Avalonia.Controls.Primitives /// protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - base.ItemsCollectionChanged(sender, e); - if (_updateCount > 0) { + base.ItemsCollectionChanged(sender, e); return; } + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); + break; + case NotifyCollectionChangedAction.Remove: + _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); + break; + } + + base.ItemsCollectionChanged(sender, e); + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -318,14 +329,12 @@ namespace Avalonia.Controls.Primitives } else { - _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); UpdateSelectedItem(_selection.First(), false); } break; case NotifyCollectionChangedAction.Remove: - _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); UpdateSelectedItem(_selection.First(), false); ResetSelectedItems(); break; From 5fea821c94e210f72c4f8c0c1aa812a6ce8a4c14 Mon Sep 17 00:00:00 2001 From: mstr2 Date: Mon, 28 Oct 2019 22:48:12 +0100 Subject: [PATCH 0404/1791] Failing test for bug #3170 --- .../AvaloniaObjectTests_Validation.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index 2cf1eb1a97..f0e93dbb3a 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -78,6 +78,18 @@ namespace Avalonia.Base.UnitTests Assert.Equal(10, target.GetValue(Class1.AttachedProperty)); } + [Fact] + public void PropertyChanged_Event_Uses_Coerced_Value() + { + var inst = new Class1(); + inst.PropertyChanged += (sender, e) => + { + Assert.Equal(10, e.NewValue); + }; + + inst.SetValue(Class1.QuxProperty, 15); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty QuxProperty = From e46cb674234d60cae9da83d11733c98617722c3e Mon Sep 17 00:00:00 2001 From: mstr2 Date: Mon, 28 Oct 2019 22:50:01 +0100 Subject: [PATCH 0405/1791] Fixed a bug where ValueStore.Changed could be called with pre-validation value --- src/Avalonia.Base/ValueStore.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 1bdbd4ca7c..e06c5996c9 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -57,7 +57,8 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - _propertyValues.SetValue(property, Validate(property, value)); + Validate(property, ref value); + _propertyValues.SetValue(property, value); Changed(property, priority, v, value); return; } @@ -78,7 +79,8 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - _propertyValues.AddValue(property, Validate(property, value)); + Validate(property, ref value); + _propertyValues.AddValue(property, value); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } @@ -166,16 +168,14 @@ namespace Avalonia validate2); } - private object Validate(AvaloniaProperty property, object value) + private void Validate(AvaloniaProperty property, ref object value) { var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); if (validate != null && value != AvaloniaProperty.UnsetValue) { - return validate(_owner, value); + value = validate(_owner, value); } - - return value; } private DeferredSetter GetDeferredSetter(AvaloniaProperty property) From e27c7c0fff35b04663cff08bdb75e848e8501990 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 29 Oct 2019 10:21:14 +0300 Subject: [PATCH 0406/1791] Fixed parameter order in OnSelectionChanged in AutoCompleteBox Apparently Silverlight has different constructor argument order --- src/Avalonia.Controls/AutoCompleteBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index ce4358648b..64db832a81 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -704,7 +704,7 @@ namespace Avalonia.Controls added.Add(e.NewValue); } - OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added)); + OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, added, removed)); } /// From 96619e44408aea5319a80b40327d67cee94c4615 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Oct 2019 14:49:19 +0100 Subject: [PATCH 0407/1791] Added failing test. `ItemsPresenter.Items` isn't correctly in sync with `ItemsControl.Items` when assigning new `Items`. --- .../ItemsControlTests.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index b2839360ee..c338d29e96 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -522,6 +522,36 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Presenter_Items_Should_Be_In_Sync() + { + var target = new ItemsControl + { + Template = GetTemplate(), + Items = new object[] + { + new Button(), + new Button(), + }, + }; + + var root = new TestRoot { Child = target }; + var otherPanel = new StackPanel(); + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.ItemContainerGenerator.Materialized += (s, e) => + { + Assert.IsType(e.Containers[0].Item); + }; + + target.Items = new[] + { + new Canvas() + }; + } + private class Item { public Item(string value) From 496fae20e55995017faf8f6f5672c507dc45fd15 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Oct 2019 14:59:47 +0100 Subject: [PATCH 0408/1791] Ensure Items are synchronised with Presenter. Ensure that the `ItemsControl`'s presenter is notified of an `Items` change at the appropriate time. --- src/Avalonia.Controls/ItemsControl.cs | 7 ++++++- src/Avalonia.Controls/Presenters/IItemsPresenter.cs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index b93346792d..1203792559 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -356,10 +356,15 @@ namespace Avalonia.Controls var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; - Presenter?.ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); UpdateItemCount(); RemoveControlItemsFromLogicalChildren(oldValue); AddControlItemsToLogicalChildren(newValue); + + if (Presenter != null) + { + Presenter.Items = newValue; + } + SubscribeToItems(newValue); } diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 21a03402a0..c4acf1ebef 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -1,12 +1,15 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections; using System.Collections.Specialized; namespace Avalonia.Controls.Presenters { public interface IItemsPresenter : IPresenter { + IEnumerable Items { get; set; } + IPanel Panel { get; } void ItemsChanged(NotifyCollectionChangedEventArgs e); From 9107c0e96eea42756f7d6ea26013979986e7adfe Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Oct 2019 23:01:18 +0100 Subject: [PATCH 0409/1791] Added another failing test. Adapted from failing test described by @MarchingCube in PR feedback: https://github.com/AvaloniaUI/Avalonia/pull/3177#issuecomment-547515972 --- .../ItemsControlTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index c338d29e96..227d783874 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -12,6 +12,7 @@ using Xunit; using System.Collections.ObjectModel; using Avalonia.UnitTests; using Avalonia.Input; +using System.Collections.Generic; namespace Avalonia.Controls.UnitTests { @@ -104,6 +105,28 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new[] { child }, target.GetLogicalChildren()); } + [Fact] + public void Added_Container_Should_Have_LogicalParent_Set_To_ItemsControl() + { + var item = new Border(); + var items = new ObservableCollection(); + + var target = new ItemsControl + { + Template = GetTemplate(), + Items = items, + }; + + var root = new TestRoot(true, target); + + root.Measure(new Size(100, 100)); + root.Arrange(new Rect(0, 0, 100, 100)); + + items.Add(item); + + Assert.Equal(target, item.Parent); + } + [Fact] public void Control_Item_Should_Be_Removed_From_Logical_Children_Before_ApplyTemplate() { From 68b655b5177175fc1b168b2130064992575c1fb6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Oct 2019 07:32:38 +0100 Subject: [PATCH 0410/1791] Add to logical children before notifying presenter. This makes sure that newly added control items have the correct logical parent. --- src/Avalonia.Controls/ItemsControl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1203792559..bf22f0a08a 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -377,7 +377,6 @@ namespace Avalonia.Controls protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { UpdateItemCount(); - Presenter?.ItemsChanged(e); switch (e.Action) { @@ -390,6 +389,8 @@ namespace Avalonia.Controls break; } + Presenter?.ItemsChanged(e); + var collection = sender as ICollection; PseudoClasses.Set(":empty", collection == null || collection.Count == 0); PseudoClasses.Set(":singleitem", collection != null && collection.Count == 1); From 4d14218a653d2cdd0655d38d47b4af84092fc4f1 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 30 Oct 2019 06:18:22 +0100 Subject: [PATCH 0411/1791] Failing unit tests #3180 --- .../TabControlTests.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index ddf7e7a0fa..1e2c2084bd 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -44,6 +45,31 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(selected, target.SelectedItem); } + [Fact] + public void Pre_Selecting_TabItem_Should_Set_SelectedContent_After_It_Was_Added() + { + var target = new TabControl + { + Template = TabControlTemplate(), + }; + + const string secondContent = "Second"; + + var items = new AvaloniaList + { + new TabItem { Header = "First"}, + new TabItem { Header = "Second", Content = secondContent, IsSelected = true } + }; + + target.Items = items; + + target.ApplyTemplate(); + + target.Measure(Size.Infinity); + + Assert.Equal(secondContent, target.SelectedContent); + } + [Fact] public void Logical_Children_Should_Be_TabItems() { From b20f3f4cdfa0f2de0a63e8caf9891a0e9af1e215 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 30 Oct 2019 09:20:50 +0100 Subject: [PATCH 0412/1791] Update selected context when selected index changes --- .../Primitives/SelectingItemsControl.cs | 2 +- src/Avalonia.Controls/TabControl.cs | 59 +++++++++++++++++++ src/Avalonia.Controls/TabItem.cs | 19 ------ 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7fddee1012..a030e2cd9a 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -358,7 +358,7 @@ namespace Avalonia.Controls.Primitives { if ((container.ContainerControl as ISelectable)?.IsSelected == true) { - if (SelectedIndex == -1) + if (SelectionMode.HasFlag(SelectionMode.Single)) { SelectedIndex = container.Index; } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 61ac0822b0..a2227d66e8 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -70,6 +70,7 @@ namespace Avalonia.Controls SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); + SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent(e)); } /// @@ -145,6 +146,64 @@ namespace Avalonia.Controls return RegisterContentPresenter(presenter); } + private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) + { + var index = (int)e.NewValue; + + if (index == -1) + { + SelectedContentTemplate = null; + + SelectedContent = null; + + return; + } + + var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index); + + if (container == null) + { + if (Items is AvaloniaList items) + { + container = items[index] as TabItem; + } + } + + if (container == null) + { + return; + } + + UpdateSelectedContent(container); + } + + private void UpdateSelectedContent(TabItem item) + { + if (SelectedContentTemplate != item.ContentTemplate) + { + SelectedContentTemplate = item.ContentTemplate; + } + + if (SelectedContent != item.Content) + { + SelectedContent = item.Content; + } + } + + protected override Size MeasureOverride(Size availableSize) + { + var size = base.MeasureOverride(availableSize); + + if (SelectedIndex != -1 && SelectedContent == null) + { + var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex); + + UpdateSelectedContent(container); + } + + return size; + } + /// /// Called when an is registered with the control. /// diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index fca1e022aa..0160bfcec1 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -30,7 +30,6 @@ namespace Avalonia.Controls { SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); - IsSelectedProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent(e)); DataContextProperty.Changed.AddClassHandler((x, e) => x.UpdateHeader(e)); } @@ -83,23 +82,5 @@ namespace Avalonia.Controls } } } - - private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) - { - if (!IsSelected) - { - return; - } - - if (ParentTabControl.SelectedContentTemplate != ContentTemplate) - { - ParentTabControl.SelectedContentTemplate = ContentTemplate; - } - - if (ParentTabControl.SelectedContent != Content) - { - ParentTabControl.SelectedContent = Content; - } - } } } From 16f2bcdb9bc95b8e16b502c398d540dbf038603f Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 30 Oct 2019 09:27:10 +0100 Subject: [PATCH 0413/1791] Remove redundant ParentTabControl property --- src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs | 2 -- src/Avalonia.Controls/TabItem.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs index a6a64e570b..1c5419735b 100644 --- a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs @@ -19,8 +19,6 @@ namespace Avalonia.Controls.Generators { var tabItem = (TabItem)base.CreateContainer(item); - tabItem.ParentTabControl = Owner; - tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty]; if (tabItem.HeaderTemplate == null) diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 0160bfcec1..e27977bf3d 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -53,8 +53,6 @@ namespace Avalonia.Controls set { SetValue(IsSelectedProperty, value); } } - internal TabControl ParentTabControl { get; set; } - private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) { if (Header == null) From b7a3747da66b5d0feae56dc10636c0f10b695ed2 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Wed, 30 Oct 2019 23:26:34 +0100 Subject: [PATCH 0414/1791] Add failing test for https://github.com/AvaloniaUI/Avalonia/issues/3179. --- .../Rendering/DeferredRendererTests.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index b4743e900d..6063a382a0 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -369,6 +369,81 @@ namespace Avalonia.Visuals.UnitTests.Rendering } + [Fact] + public void Should_Update_VisualNodes_When_Child_Moved_To_New_Parent_And_New_Root() + { + var dispatcher = new ImmediateDispatcher(); + var loop = new Mock(); + + Decorator moveFrom; + Decorator moveTo; + Canvas moveMe; + + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + (moveFrom = new Decorator + { + Child = moveMe = new Canvas(), + }) + } + } + }; + + var otherRoot = new TestRoot + { + Child = new StackPanel + { + Children = + { + (moveTo = new Decorator()) + } + } + }; + + var sceneBuilder = new SceneBuilder(); + var target = new DeferredRenderer( + root, + loop.Object, + sceneBuilder: sceneBuilder, + dispatcher: dispatcher); + + var otherSceneBuilder = new SceneBuilder(); + var otherTarget = new DeferredRenderer( + otherRoot, + loop.Object, + sceneBuilder: otherSceneBuilder, + dispatcher: dispatcher); + + root.Renderer = target; + otherRoot.Renderer = otherTarget; + + target.Start(); + otherTarget.Start(); + + RunFrame(target); + RunFrame(otherTarget); + + moveFrom.Child = null; + moveTo.Child = moveMe; + + RunFrame(target); + RunFrame(otherTarget); + + var scene = target.UnitTestScene(); + var otherScene = otherTarget.UnitTestScene(); + + var moveFromNode = (VisualNode)scene.FindNode(moveFrom); + var moveToNode = (VisualNode)otherScene.FindNode(moveTo); + + Assert.Empty(moveFromNode.Children); + Assert.Equal(1, moveToNode.Children.Count); + Assert.Same(moveMe, moveToNode.Children[0].Visual); + } + [Fact] public void Should_Push_Opacity_For_Controls_With_Less_Than_1_Opacity() { From 5df55e5c9f42721a4917fb6228bead4349e9d450 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 31 Oct 2019 13:21:46 +0100 Subject: [PATCH 0415/1791] Fix multiselection --- .../Primitives/SelectingItemsControl.cs | 10 +++--- src/Avalonia.Controls/TabControl.cs | 33 +++++++++++-------- .../SelectingItemsControlTests_Multiple.cs | 1 + .../TabControlTests.cs | 4 +-- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a030e2cd9a..5a176d733d 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -358,17 +358,17 @@ namespace Avalonia.Controls.Primitives { if ((container.ContainerControl as ISelectable)?.IsSelected == true) { - if (SelectionMode.HasFlag(SelectionMode.Single)) - { - SelectedIndex = container.Index; - } - else + if (SelectionMode.HasFlag(SelectionMode.Multiple)) { if (_selection.Add(container.Index)) { resetSelectedItems = true; } } + else + { + SelectedIndex = container.Index; + } MarkContainerSelected(container.ContainerControl, true); } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index a2227d66e8..87ba282db0 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -146,6 +146,25 @@ namespace Avalonia.Controls return RegisterContentPresenter(presenter); } + protected override void OnContainersMaterialized(ItemContainerEventArgs e) + { + base.OnContainersMaterialized(e); + + if (SelectedContent != null || SelectedIndex == -1) + { + return; + } + + var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex); + + if (container == null) + { + return; + } + + UpdateSelectedContent(container); + } + private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) { var index = (int)e.NewValue; @@ -190,20 +209,6 @@ namespace Avalonia.Controls } } - protected override Size MeasureOverride(Size availableSize) - { - var size = base.MeasureOverride(availableSize); - - if (SelectedIndex != -1 && SelectedContent == null) - { - var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex); - - UpdateSelectedContent(container); - } - - return size; - } - /// /// Called when an is registered with the control. /// diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index be0f4272a5..952a00a14e 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1080,6 +1080,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var target = new TestSelector { Items = items, + SelectionMode = SelectionMode.Multiple, Template = Template(), }; diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 1e2c2084bd..2f8c974802 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -63,9 +63,7 @@ namespace Avalonia.Controls.UnitTests target.Items = items; - target.ApplyTemplate(); - - target.Measure(Size.Infinity); + ApplyTemplate(target); Assert.Equal(secondContent, target.SelectedContent); } From d59d0904f43997a8cc4dcbb6064d141543045091 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 31 Oct 2019 11:20:33 +0200 Subject: [PATCH 0416/1791] add test for tooltip --- .../ToolTipTests.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/ToolTipTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs new file mode 100644 index 0000000000..2a1bb78c2e --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Disposables; +using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class TolTipTests + { + private MouseTestHelper _mouseHelper = new MouseTestHelper(); + + [Fact] + public void Should_Open_On_Pointer_Enter() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + + var target = new Decorator() + { + [ToolTip.TipProperty] = "Tip", + [ToolTip.ShowDelayProperty] = 0 + }; + + window.Content = target; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + Assert.True((target as IVisual).IsAttachedToVisualTree); + + _mouseHelper.Enter(target); + + Assert.True(ToolTip.GetIsOpen(target)); + } + } + } +} From 60a596bd82ebb387da5047f647e1388d3afb0b85 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 31 Oct 2019 14:52:08 +0200 Subject: [PATCH 0417/1791] add another test for tooltip --- .../ToolTipTests.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index 2a1bb78c2e..eba0d7cb3e 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -41,5 +41,51 @@ namespace Avalonia.Controls.UnitTests Assert.True(ToolTip.GetIsOpen(target)); } } + + [Fact] + public void Should_Open_On_Pointer_Enter_With_Delay() + { + Action timercallback = null; + var delay = TimeSpan.Zero; + + var pti = Mock.Of(x => x.CurrentThreadIsLoopThread == true); + + Mock.Get(pti) + .Setup(v => v.StartTimer(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((priority, interval, tick) => + { + delay = interval; + timercallback = tick; + }) + .Returns(Disposable.Empty); + + using (UnitTestApplication.Start(TestServices.StyledWindow.With(threadingInterface: pti))) + { + var window = new Window(); + + var target = new Decorator() + { + [ToolTip.TipProperty] = "Tip", + [ToolTip.ShowDelayProperty] = 1 + }; + + window.Content = target; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + Assert.True((target as IVisual).IsAttachedToVisualTree); + + _mouseHelper.Enter(target); + + Assert.Equal(TimeSpan.FromMilliseconds(1), delay); + Assert.NotNull(timercallback); + Assert.False(ToolTip.GetIsOpen(target)); + + timercallback(); + + Assert.True(ToolTip.GetIsOpen(target)); + } + } } } From 2288b7ab1d0463f84032c8907eb4b1fae671840b Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 31 Oct 2019 14:52:59 +0200 Subject: [PATCH 0418/1791] add failing test for tooltip for issue #3188 --- .../ToolTipTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index eba0d7cb3e..362dd6d111 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -16,6 +16,24 @@ namespace Avalonia.Controls.UnitTests { private MouseTestHelper _mouseHelper = new MouseTestHelper(); + [Fact] + public void Should_Not_Open_On_Detached_Control() + { + //issue #3188 + var control = new Decorator() + { + [ToolTip.TipProperty] = "Tip", + [ToolTip.ShowDelayProperty] = 0 + }; + + Assert.False((control as IVisual).IsAttachedToVisualTree); + + //here in issue #3188 exception is raised + _mouseHelper.Enter(control); + + Assert.False(ToolTip.GetIsOpen(control)); + } + [Fact] public void Should_Open_On_Pointer_Enter() { From a3dbf72947d4e971325cd2870b19e99cd4ae65c5 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 31 Oct 2019 15:03:33 +0200 Subject: [PATCH 0419/1791] don't try open tooltip on detached control --- src/Avalonia.Controls/ToolTipService.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index 384a9db0cf..d90729e8a5 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Input; using Avalonia.Threading; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -79,7 +80,10 @@ namespace Avalonia.Controls { StopTimer(); - ToolTip.SetIsOpen(control, true); + if ((control as IVisual).IsAttachedToVisualTree) + { + ToolTip.SetIsOpen(control, true); + } } private void Close(Control control) From f933fe53cfd8d5e029e930b81c8a7f36c5640e9a Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 31 Oct 2019 14:13:58 +0100 Subject: [PATCH 0420/1791] Add failing test #2699 --- .../TabControlTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index ddf7e7a0fa..b65a4d67d4 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -287,6 +288,25 @@ namespace Avalonia.Controls.UnitTests Assert.Single(target.GetLogicalChildren(), content); } + [Fact] + public void Should_Not_Propagate_DataContext_To_TabItem_Content() + { + var dataContext = "DataContext"; + + var tabItem = new TabItem(); + + var target = new TabControl + { + Template = TabControlTemplate(), + DataContext = dataContext, + Items = new AvaloniaList { tabItem } + }; + + ApplyTemplate(target); + + Assert.NotEqual(dataContext, tabItem.Content); + } + private IControlTemplate TabControlTemplate() { return new FuncControlTemplate((parent, scope) => From e9335d6a2c4755bd120e7da06e2dec0029f93f54 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 31 Oct 2019 14:15:11 +0100 Subject: [PATCH 0421/1791] Fix #2699 --- .../Generators/TabItemContainerGenerator.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs index a6a64e570b..d99648a158 100644 --- a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs @@ -48,11 +48,6 @@ namespace Avalonia.Controls.Generators tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty]; } - if (tabItem.Content == null) - { - tabItem[~ContentControl.ContentProperty] = tabItem[~StyledElement.DataContextProperty]; - } - return tabItem; } } From 83bfb8ec70837d260527b3724c68f4da0eef9243 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 1 Nov 2019 11:07:08 +0100 Subject: [PATCH 0422/1791] Initial implementation of a font manager --- Avalonia.sln | 3 +- build/HarfBuzzSharp.props | 6 + build/SkiaSharp.props | 4 +- .../Presenters/TextPresenter.cs | 3 +- src/Avalonia.Controls/TextBlock.cs | 5 +- src/Avalonia.Visuals/Media/FontFamily.cs | 23 ++- src/Avalonia.Visuals/Media/FontManager.cs | 100 ++++++++++ .../Media/Fonts/FamilyNameCollection.cs | 6 +- src/Avalonia.Visuals/Media/FormattedText.cs | 47 ++++- src/Avalonia.Visuals/Media/GlyphTypeface.cs | 108 ++++++++++ src/Avalonia.Visuals/Media/Typeface.cs | 99 ++++++--- .../Platform/IFontManagerImpl.cs | 57 ++++++ .../Platform/IGlyphTypefaceImpl.cs | 89 +++++++++ .../Platform/IPlatformRenderInterface.cs | 7 +- .../Rendering/RendererBase.cs | 6 +- src/Skia/Avalonia.Skia/Avalonia.Skia.csproj | 1 + src/Skia/Avalonia.Skia/FontKey.cs | 40 ++++ src/Skia/Avalonia.Skia/FontManagerImpl.cs | 91 +++++++++ src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 48 ++--- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 179 +++++++++++++++++ .../Avalonia.Skia/PlatformRenderInterface.cs | 5 +- .../Avalonia.Skia/SKTypefaceCollection.cs | 91 ++------- .../SKTypefaceCollectionCache.cs | 8 +- src/Skia/Avalonia.Skia/SkiaPlatform.cs | 5 + src/Skia/Avalonia.Skia/TypefaceCache.cs | 86 ++------ .../Avalonia.Skia/TypefaceCollectionEntry.cs | 19 ++ .../Avalonia.Direct2D1.csproj | 1 + .../Avalonia.Direct2D1/Direct2D1Platform.cs | 17 +- .../Media/Direct2D1FontCollectionCache.cs | 49 +++-- .../Media/FontManagerImpl.cs | 80 ++++++++ .../Media/FormattedTextImpl.cs | 19 +- .../Media/GlyphTypefaceImpl.cs | 188 ++++++++++++++++++ .../FullLayoutTests.cs | 1 + .../Media/FormattedTextImplTests.cs | 3 +- .../MockPlatformRenderInterface.cs | 3 +- tests/Avalonia.UnitTests/TestServices.cs | 1 + .../Media/FontFamilyTests.cs | 44 +++- .../Media/TypefaceTests.cs | 14 +- .../VisualTree/MockRenderInterface.cs | 1 + 39 files changed, 1262 insertions(+), 295 deletions(-) create mode 100644 build/HarfBuzzSharp.props create mode 100644 src/Avalonia.Visuals/Media/FontManager.cs create mode 100644 src/Avalonia.Visuals/Media/GlyphTypeface.cs create mode 100644 src/Avalonia.Visuals/Platform/IFontManagerImpl.cs create mode 100644 src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs create mode 100644 src/Skia/Avalonia.Skia/FontKey.cs create mode 100644 src/Skia/Avalonia.Skia/FontManagerImpl.cs create mode 100644 src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs create mode 100644 src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs create mode 100644 src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs create mode 100644 src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs diff --git a/Avalonia.sln b/Avalonia.sln index 568a16ce0e..e40ebae4d6 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -128,6 +128,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Base.props = build\Base.props build\Binding.props = build\Binding.props build\BuildTargets.targets = build\BuildTargets.targets + build\HarfBuzzSharp.props = build\HarfBuzzSharp.props build\JetBrains.Annotations.props = build\JetBrains.Annotations.props build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props @@ -201,7 +202,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props new file mode 100644 index 0000000000..f8767c7599 --- /dev/null +++ b/build/HarfBuzzSharp.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index c03ad0fefd..796bd8e596 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 5931fec350..e0cc9aa128 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -297,7 +297,8 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Text = "X", - Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), + Typeface = new Typeface(FontFamily, FontWeight, FontStyle), + FontSize = FontSize, TextAlignment = TextAlignment, Constraint = availableSize, }.Bounds.Size; diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index b9603b91ed..c7855ddfd1 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -352,10 +352,11 @@ namespace Avalonia.Controls return new FormattedText { Constraint = constraint, - Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), + Typeface = new Typeface(FontFamily, FontWeight, FontStyle), + FontSize = FontSize, Text = text ?? string.Empty, TextAlignment = TextAlignment, - Wrapping = TextWrapping, + TextWrapping = TextWrapping, }; } diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index a486723d86..665dfc1129 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -5,12 +5,16 @@ using System; using System.Collections.Generic; using System.Linq; using Avalonia.Media.Fonts; -using Avalonia.Platform; namespace Avalonia.Media { public class FontFamily { + static FontFamily() + { + Default = new FontFamily(FontManager.Default.DefaultFontFamilyName); + } + /// /// /// Initializes a new instance of the class. @@ -30,9 +34,7 @@ namespace Avalonia.Media { if (string.IsNullOrEmpty(name)) { - FamilyNames = new FamilyNameCollection(string.Empty); - - return; + throw new ArgumentNullException(nameof(name)); } var fontFamilySegment = GetFontFamilyIdentifier(name); @@ -53,13 +55,13 @@ namespace Avalonia.Media /// /// Represents the default font family /// - public static FontFamily Default => new FontFamily(string.Empty); + public static FontFamily Default { get; } /// /// Represents all font families in the system. This can be an expensive call depending on platform implementation. /// public static IEnumerable SystemFontFamilies => - AvaloniaLocator.Current.GetService().InstalledFontNames.Select(name => new FontFamily(name)); + FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); /// /// Gets the primary family name of the font family. @@ -181,7 +183,14 @@ namespace Avalonia.Media { var hash = (int)2186146271; - hash = (hash * 15768619) ^ FamilyNames.GetHashCode(); + if (Key != null) + { + hash = (hash * 15768619) ^ Key.GetHashCode(); + } + else + { + hash = (hash * 15768619) ^ FamilyNames.GetHashCode(); + } if (Key != null) { diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs new file mode 100644 index 0000000000..e89471ede8 --- /dev/null +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -0,0 +1,100 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Platform; + +namespace Avalonia.Media +{ + public abstract class FontManager : IFontManagerImpl + { + public static readonly FontManager Default = CreateDefaultFontManger(); + + /// + public string DefaultFontFamilyName { get; protected set; } + + private static FontManager CreateDefaultFontManger() + { + var platformImpl = AvaloniaLocator.Current.GetService(); + + if(platformImpl == null) + { + return new EmptyFontManager(); + } + + return new PlatformFontManger(platformImpl); + } + + /// + public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + + /// + public abstract IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); + + /// + public abstract Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); + + /// + public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, + FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null); + + private class PlatformFontManger : FontManager + { + private readonly IFontManagerImpl _platformImpl; + + public PlatformFontManger(IFontManagerImpl platformImpl) + { + _platformImpl = platformImpl; + + DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName; + } + + public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => + _platformImpl.GetInstalledFontFamilyNames(checkForUpdates); + + public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) => _platformImpl.CreateGlyphTypeface(typeface); + + public override Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => + _platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle); + + public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, + FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null) => + _platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture); + } + + private class EmptyFontManager : FontManager + { + private readonly string[] _defaultFontFamilies = { "Arial" }; + + public EmptyFontManager() + { + DefaultFontFamilyName = "Arial"; + } + + public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + { + return _defaultFontFamilies; + } + + public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + throw new NotSupportedException(); + } + + public override Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + { + throw new NotSupportedException(); + } + + public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null) + { + throw new NotSupportedException(); + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index acf0bbdb11..0c161131dc 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs @@ -9,7 +9,7 @@ using System.Text; namespace Avalonia.Media.Fonts { - public class FamilyNameCollection : IEnumerable + public class FamilyNameCollection : IReadOnlyList { /// /// Initializes a new instance of the class. @@ -130,5 +130,9 @@ namespace Avalonia.Media.Fonts return other.ToString().Equals(ToString()); } + + public int Count => Names.Count; + + public string this[int index] => Names[index]; } } diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index e20e03e296..5013f925b3 100644 --- a/src/Avalonia.Visuals/Media/FormattedText.cs +++ b/src/Avalonia.Visuals/Media/FormattedText.cs @@ -16,9 +16,10 @@ namespace Avalonia.Media private IFormattedTextImpl _platformImpl; private IReadOnlyList _spans; private Typeface _typeface; + private double _fontSize; private string _text; private TextAlignment _textAlignment; - private TextWrapping _wrapping; + private TextWrapping _textWrapping; /// /// Initializes a new instance of the class. @@ -37,6 +38,31 @@ namespace Avalonia.Media _platform = platform; } + /// + /// + /// + /// + /// + /// + /// + /// + /// + public FormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment, + TextWrapping textWrapping, Size constraint) + { + _text = text; + + _typeface = typeface; + + _fontSize = fontSize; + + _textAlignment = textAlignment; + + _textWrapping = textWrapping; + + _constraint = constraint; + } + /// /// Gets the bounds of the text within the . /// @@ -61,6 +87,16 @@ namespace Avalonia.Media set => Set(ref _typeface, value); } + + /// + /// Gets or sets the font size. + /// + public double FontSize + { + get => _fontSize; + set => Set(ref _fontSize, value); + } + /// /// Gets or sets a collection of spans that describe the formatting of subsections of the /// text. @@ -92,10 +128,10 @@ namespace Avalonia.Media /// /// Gets or sets the text wrapping. /// - public TextWrapping Wrapping + public TextWrapping TextWrapping { - get => _wrapping; - set => Set(ref _wrapping, value); + get => _textWrapping; + set => Set(ref _textWrapping, value); } /// @@ -110,8 +146,9 @@ namespace Avalonia.Media _platformImpl = _platform.CreateFormattedText( _text, _typeface, + _fontSize, _textAlignment, - _wrapping, + _textWrapping, _constraint, _spans); } diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs new file mode 100644 index 0000000000..1c959a86c5 --- /dev/null +++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs @@ -0,0 +1,108 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +using Avalonia.Platform; + +namespace Avalonia.Media +{ + public class GlyphTypeface : IDisposable + { + public GlyphTypeface(Typeface typeface) : this(FontManager.Default.CreateGlyphTypeface(typeface)) + { + } + + public GlyphTypeface(IGlyphTypefaceImpl platformImpl) + { + PlatformImpl = platformImpl; + } + + public IGlyphTypefaceImpl PlatformImpl { get; } + + /// + /// Gets the font design units per em. + /// + public short DesignEmHeight => PlatformImpl.DesignEmHeight; + + /// + /// Gets the recommended distance above the baseline in design em size. + /// + public int Ascent => PlatformImpl.Ascent; + + /// + /// Gets the recommended distance under the baseline in design em size. + /// + public int Descent => PlatformImpl.Descent; + + /// + /// Gets the recommended additional space between two lines of text in design em size. + /// + public int LineGap => PlatformImpl.LineGap; + + /// + /// Gets the recommended line height. + /// + public int LineHeight => Descent - Ascent + LineGap; + + /// + /// Gets a value that indicates the distance of the underline from the baseline in design em size. + /// + public int UnderlinePosition => PlatformImpl.UnderlinePosition; + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + public int UnderlineThickness => PlatformImpl.UnderlineThickness; + + /// + /// Gets a value that indicates the distance of the strikethrough from the baseline in design em size. + /// + public int StrikethroughPosition => PlatformImpl.StrikethroughPosition; + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + public int StrikethroughThickness => PlatformImpl.StrikethroughThickness; + + /// + /// Returns an glyph index for the specified codepoint. + /// + /// + /// Returns 0 if a glyph isn't found. + /// + /// The codepoint. + /// + /// A glyph index. + /// + public ushort GetGlyph(uint codepoint) => PlatformImpl.GetGlyph(codepoint); + + /// + /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0. + /// + /// The codepoints to map. + /// + public ushort[] GetGlyphs(ReadOnlySpan codepoints) => PlatformImpl.GetGlyphs(codepoints); + + /// + /// Returns the glyph advance for the specified glyph. + /// + /// The glyph. + /// + /// The advance. + /// + public int GetGlyphAdvance(ushort glyph) => PlatformImpl.GetGlyphAdvance(glyph); + + /// + /// Returns an array of glyph advances in design em size. + /// + /// The glyph indices. + /// + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) => PlatformImpl.GetGlyphAdvances(glyphs); + + void IDisposable.Dispose() + { + PlatformImpl?.Dispose(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 37ac0953bf..2d3c7e6ffa 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -1,39 +1,38 @@ -using System; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Diagnostics; +using JetBrains.Annotations; namespace Avalonia.Media { /// /// Represents a typeface. /// - public class Typeface + [DebuggerDisplay("Name = {FontFamily.Name}, Style = {Style}, Weight = {Weight}")] + public class Typeface : IEquatable { public static readonly Typeface Default = new Typeface(FontFamily.Default); + private GlyphTypeface _glyphTypeface; + /// /// Initializes a new instance of the class. /// /// The font family. - /// The font size, in DIPs. - /// The font style. /// The font weight. - public Typeface( - FontFamily fontFamily, - double fontSize = 12, - FontStyle style = FontStyle.Normal, - FontWeight weight = FontWeight.Normal) + /// The font style. + public Typeface([NotNull]FontFamily fontFamily, + FontWeight weight = FontWeight.Normal, + FontStyle style = FontStyle.Normal) { - if (fontSize <= 0) - { - throw new ArgumentException("Font size must be > 0."); - } - if (weight <= 0) { throw new ArgumentException("Font weight must be > 0."); } FontFamily = fontFamily; - FontSize = fontSize; Style = style; Weight = weight; } @@ -42,15 +41,12 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The name of the font family. - /// The font size, in DIPs. /// The font style. /// The font weight. - public Typeface( - string fontFamilyName, - double fontSize = 12, - FontStyle style = FontStyle.Normal, - FontWeight weight = FontWeight.Normal) - : this(new FontFamily(fontFamilyName), fontSize, style, weight) + public Typeface(string fontFamilyName, + FontWeight weight = FontWeight.Normal, + FontStyle style = FontStyle.Normal) + : this(new FontFamily(fontFamilyName), weight, style) { } @@ -59,11 +55,6 @@ namespace Avalonia.Media /// public FontFamily FontFamily { get; } - /// - /// Gets the size of the font in DIPs. - /// - public double FontSize { get; } - /// /// Gets the font style. /// @@ -73,5 +64,59 @@ namespace Avalonia.Media /// Gets the font weight. /// public FontWeight Weight { get; } + + /// + /// Gets the glyph typeface. + /// + /// + /// The glyph typeface. + /// + public GlyphTypeface GlyphTypeface => _glyphTypeface ?? (_glyphTypeface = new GlyphTypeface(this)); + + public static bool operator !=(Typeface a, Typeface b) + { + return !(a == b); + } + + public static bool operator ==(Typeface a, Typeface b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + return !(a is null) && a.Equals(b); + } + + public override bool Equals(object obj) + { + if (obj is Typeface typeface) + { + return Equals(typeface); + } + + return false; + } + + public bool Equals(Typeface other) + { + if (other is null) + { + return false; + } + + return FontFamily.Equals(other.FontFamily) && Style == other.Style && Weight == other.Weight; + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (FontFamily != null ? FontFamily.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (int)Style; + hashCode = (hashCode * 397) ^ (int)Weight; + return hashCode; + } + } } } diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs new file mode 100644 index 0000000000..236631edde --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -0,0 +1,57 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Media; + +namespace Avalonia.Platform +{ + public interface IFontManagerImpl + { + /// + /// Gets the system's default font family's name. + /// + string DefaultFontFamilyName { get; } + + /// + /// Get all installed fonts in the system. + /// If true the font collection is updated. + /// + IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + + /// + /// Creates a glyph typeface for specified typeface. + /// + /// The typeface. + /// + /// The glyph typeface implementation. + /// + IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); + + /// + /// Get a typeface from specified parameters. + /// + /// The font family. + /// The font weight. + /// The font style. + /// + /// The typeface. + /// + Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); + + /// + /// Tries to match a specified character to a typeface that supports specified font properties. + /// + /// The codepoint to match against. + /// The font weight. + /// The font style. + /// The font family. This is optional and used for fallback lookup. + /// The culture. + /// + /// The typeface. + /// + Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null); + } +} diff --git a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs new file mode 100644 index 0000000000..8c043a5129 --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs @@ -0,0 +1,89 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Avalonia.Platform +{ + public interface IGlyphTypefaceImpl : IDisposable + { + /// + /// Gets the font design units per em. + /// + short DesignEmHeight { get; } + + /// + /// Gets the recommended distance above the baseline in design em size. + /// + int Ascent { get; } + + /// + /// Gets the recommended distance under the baseline in design em size. + /// + int Descent { get; } + + /// + /// Gets the recommended additional space between two lines of text in design em size. + /// + int LineGap { get; } + + /// + /// Gets a value that indicates the distance of the underline from the baseline in design em size. + /// + int UnderlinePosition { get; } + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + int UnderlineThickness { get; } + + /// + /// Gets a value that indicates the distance of the strikethrough from the baseline in design em size. + /// + int StrikethroughPosition { get; } + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + int StrikethroughThickness { get; } + + /// + /// Returns an glyph index for the specified codepoint. + /// + /// + /// Returns 0 if a glyph isn't found. + /// + /// The codepoint. + /// + /// A glyph index. + /// + ushort GetGlyph(uint codepoint); + + /// + /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0. + /// + /// The codepoints to map. + /// + /// An array of glyph indices. + /// + ushort[] GetGlyphs(ReadOnlySpan codepoints); + + /// + /// Returns the glyph advance for the specified glyph. + /// + /// The glyph. + /// + /// The advance. + /// + int GetGlyphAdvance(ushort glyph); + + /// + /// Returns an array of glyph advances in design em size. + /// + /// The glyph indices. + /// + /// An array of glyph advances. + /// + int[] GetGlyphAdvances(ReadOnlySpan glyphs); + } +} diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 87db9251e1..619d3088b4 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -13,16 +13,12 @@ namespace Avalonia.Platform /// public interface IPlatformRenderInterface { - /// - /// Get all installed fonts in the system - /// - IEnumerable InstalledFontNames { get; } - /// /// Creates a formatted text implementation. /// /// The text. /// The base typeface. + /// The font size. /// The text alignment. /// The text wrapping mode. /// The text layout constraints. @@ -31,6 +27,7 @@ namespace Avalonia.Platform IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index 7b10fc1212..e341f02901 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -7,7 +7,8 @@ namespace Avalonia.Rendering { public class RendererBase { - private static readonly Typeface s_fpsTypeface = new Typeface("Arial", 18); + private static readonly Typeface s_fpsTypeface = new Typeface("Arial"); + private static int s_fontSize = 18; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private int _framesThisSecond; private int _fps; @@ -18,7 +19,8 @@ namespace Avalonia.Rendering { _fpsText = new FormattedText { - Typeface = s_fpsTypeface + Typeface = s_fpsTypeface, + FontSize = s_fontSize }; } diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index 4f884cdf33..68da513528 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -12,5 +12,6 @@ + diff --git a/src/Skia/Avalonia.Skia/FontKey.cs b/src/Skia/Avalonia.Skia/FontKey.cs new file mode 100644 index 0000000000..bb3fe230c1 --- /dev/null +++ b/src/Skia/Avalonia.Skia/FontKey.cs @@ -0,0 +1,40 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; + +namespace Avalonia.Skia +{ + internal readonly struct FontKey : IEquatable + { + public readonly FontStyle Style; + public readonly FontWeight Weight; + + public FontKey(FontWeight weight, FontStyle style) + { + Style = style; + Weight = weight; + } + + public override int GetHashCode() + { + var hash = 17; + hash = hash * 31 + (int)Style; + hash = hash * 31 + (int)Weight; + + return hash; + } + + public override bool Equals(object other) + { + return other is FontKey key && Equals(key); + } + + public bool Equals(FontKey other) + { + return Style == other.Style && + Weight == other.Weight; + } + } +} diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs new file mode 100644 index 0000000000..6c67438533 --- /dev/null +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -0,0 +1,91 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia +{ + internal class FontManagerImpl : IFontManagerImpl + { + private SKFontManager _skFontManager = SKFontManager.Default; + + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); + + public FontManagerImpl() + { + DefaultFontFamilyName = SKTypeface.Default.FamilyName; + } + + public string DefaultFontFamilyName { get; } + + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + { + if (checkForUpdates) + { + _skFontManager = SKFontManager.CreateDefault(); + } + + return _skFontManager.FontFamilies; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + } + + public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + { + return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface; + } + + public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null) + { + var fontFamilyName = FontFamily.Default.Name; + + if (culture == null) + { + culture = CultureInfo.CurrentUICulture; + } + + if (fontFamily != null) + { + foreach (var familyName in fontFamily.FamilyNames) + { + var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight, + SKFontStyleWidth.Normal, + (SKFontStyleSlant)fontStyle, + new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint); + + if (skTypeface == null) + { + continue; + } + + fontFamilyName = familyName; + + break; + } + } + else + { + var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, + (SKFontStyleSlant)fontStyle, + new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint); + + if (skTypeface != null) + { + fontFamilyName = skTypeface.FamilyName; + } + } + + return GetTypeface(fontFamilyName, fontWeight, fontStyle); + } + } +} diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index eb7b65cdce..78ff785bdc 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -18,6 +18,7 @@ namespace Avalonia.Skia public FormattedTextImpl( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -28,47 +29,22 @@ namespace Avalonia.Skia // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); - SKTypeface skiaTypeface = null; + var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style); - if (typeface.FontFamily.Key != null) + _paint = new SKPaint { - var typefaces = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); - skiaTypeface = typefaces.GetTypeFace(typeface); - } - else - { - if (typeface.FontFamily.FamilyNames.HasFallbacks) - { - foreach (var familyName in typeface.FontFamily.FamilyNames) - { - skiaTypeface = TypefaceCache.GetTypeface( - familyName, - typeface.Style, - typeface.Weight); - if (skiaTypeface.FamilyName != TypefaceCache.DefaultFamilyName) break; - } - } - else - { - skiaTypeface = TypefaceCache.GetTypeface( - typeface.FontFamily.Name, - typeface.Style, - typeface.Weight); - } - } - - _paint = new SKPaint(); + TextEncoding = SKTextEncoding.Utf16, + IsStroke = false, + IsAntialias = true, + LcdRenderText = true, + SubpixelText = true, + Typeface = entry.SKTypeface, + TextSize = (float)fontSize, + TextAlign = textAlignment.ToSKTextAlign() + }; //currently Skia does not measure properly with Utf8 !!! //Paint.TextEncoding = SKTextEncoding.Utf8; - _paint.TextEncoding = SKTextEncoding.Utf16; - _paint.IsStroke = false; - _paint.IsAntialias = true; - _paint.LcdRenderText = true; - _paint.SubpixelText = true; - _paint.Typeface = skiaTypeface; - _paint.TextSize = (float)typeface.FontSize; - _paint.TextAlign = textAlignment.ToSKTextAlign(); _wrapping = wrapping; _constraint = constraint; diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs new file mode 100644 index 0000000000..b9b5b07d7d --- /dev/null +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -0,0 +1,179 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using Avalonia.Media; +using Avalonia.Platform; +using HarfBuzzSharp; +using SkiaSharp; + +namespace Avalonia.Skia +{ + internal class GlyphTypefaceImpl : IGlyphTypefaceImpl + { + private bool _isDisposed; + + public GlyphTypefaceImpl(Typeface typeface) + { + Typeface = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style).SKTypeface; + + Face = new Face(GetTable) + { + UnitsPerEm = Typeface.UnitsPerEm + }; + + Font = new Font(Face); + + Font.SetFunctionsOpenType(); + + Font.GetScale(out var xScale, out _); + + DesignEmHeight = (short)xScale; + + if (!Font.TryGetHorizontalFontExtents(out var fontExtents)) + { + Font.TryGetVerticalFontExtents(out fontExtents); + } + + Ascent = -fontExtents.Ascender; + + Descent = -fontExtents.Descender; + + LineGap = fontExtents.LineGap; + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition)) + { + UnderlinePosition = underlinePosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness)) + { + UnderlineThickness = underlineThickness; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition)) + { + StrikethroughPosition = strikethroughPosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness)) + { + StrikethroughThickness = strikethroughThickness; + } + } + + public Face Face { get; } + + public Font Font { get; } + + public SKTypeface Typeface { get; } + + /// + public short DesignEmHeight { get; } + + /// + public int Ascent { get; } + + /// + public int Descent { get; } + + /// + public int LineGap { get; } + + //ToDo: Get these values from HarfBuzz + /// + public int UnderlinePosition { get; } + + /// + public int UnderlineThickness { get; } + + /// + public int StrikethroughPosition { get; } + + /// + public int StrikethroughThickness { get; } + + /// + public ushort GetGlyph(uint codepoint) + { + if (Font.TryGetGlyph(codepoint, out var glyph)) + { + return (ushort)glyph; + } + + return 0; + } + + /// + public ushort[] GetGlyphs(ReadOnlySpan codepoints) + { + var glyphs = new ushort[codepoints.Length]; + + for (var i = 0; i < codepoints.Length; i++) + { + if (Font.TryGetGlyph(codepoints[i], out var glyph)) + { + glyphs[i] = (ushort)glyph; + } + } + + return glyphs; + } + + /// + public int GetGlyphAdvance(ushort glyph) + { + return Font.GetHorizontalGlyphAdvance(glyph); + } + + /// + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) + { + var glyphIndices = new uint[glyphs.Length]; + + for (var i = 0; i < glyphs.Length; i++) + { + glyphIndices[i] = glyphs[i]; + } + + return Font.GetHorizontalGlyphAdvances(glyphIndices); + } + + private Blob GetTable(Face face, Tag tag) + { + var size = Typeface.GetTableSize(tag); + + var data = Marshal.AllocCoTaskMem(size); + + var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data)); + + return Typeface.TryGetTableData(tag, 0, size, data) ? + new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null; + } + + private void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!disposing) + { + return; + } + + Font?.Dispose(); + Face?.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 15f38b1c4f..e4ad0c1b24 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -21,8 +21,6 @@ namespace Avalonia.Skia private GRContext GrContext { get; } - public IEnumerable InstalledFontNames => SKFontManager.Default.FontFamilies; - public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu) { if (customSkiaGpu != null) @@ -52,12 +50,13 @@ namespace Avalonia.Skia public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList spans) { - return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); + return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans); } public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect); diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 17448127b0..8ec2a9c3f8 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -4,114 +4,59 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; - using Avalonia.Media; -using SkiaSharp; - namespace Avalonia.Skia { internal class SKTypefaceCollection { - private readonly ConcurrentDictionary> _fontFamilies = - new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> _fontFamilies = + new ConcurrentDictionary>(); - public void AddTypeFace(SKTypeface typeface) + public void AddEntry(string familyName, FontKey key, TypefaceCollectionEntry entry) { - var key = new FontKey((SKFontStyleWeight)typeface.FontWeight, typeface.FontSlant); - - if (!_fontFamilies.TryGetValue(typeface.FamilyName, out var fontFamily)) + if (!_fontFamilies.TryGetValue(familyName, out var fontFamily)) { - fontFamily = new ConcurrentDictionary(); + fontFamily = new ConcurrentDictionary(); - _fontFamilies.TryAdd(typeface.FamilyName, fontFamily); + _fontFamilies.TryAdd(familyName, fontFamily); } - fontFamily.TryAdd(key, typeface); + fontFamily.TryAdd(key, entry); } - public SKTypeface GetTypeFace(Typeface typeface) + public TypefaceCollectionEntry Get(string familyName, FontWeight fontWeight, FontStyle fontStyle) { - var styleSlant = SKFontStyleSlant.Upright; - - switch (typeface.Style) - { - case FontStyle.Italic: - styleSlant = SKFontStyleSlant.Italic; - break; - - case FontStyle.Oblique: - styleSlant = SKFontStyleSlant.Oblique; - break; - } + var key = new FontKey(fontWeight, fontStyle); - if (!_fontFamilies.TryGetValue(typeface.FontFamily.Name, out var fontFamily)) - { - return TypefaceCache.GetTypeface(TypefaceCache.DefaultFamilyName, typeface.Style, typeface.Weight); - } - - var weight = (SKFontStyleWeight)typeface.Weight; - - var key = new FontKey(weight, styleSlant); - - return fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)); + return _fontFamilies.TryGetValue(familyName, out var fontFamily) ? + fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) : + null; } - private static SKTypeface GetFallback(IDictionary fontFamily, FontKey key) + private static TypefaceCollectionEntry GetFallback(IDictionary fontFamily, FontKey key) { var keys = fontFamily.Keys.Where( - x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Slant == key.Slant).ToArray(); + x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray(); if (!keys.Any()) { keys = fontFamily.Keys.Where( - x => x.Weight == key.Weight && (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); + x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray(); if (!keys.Any()) { keys = fontFamily.Keys.Where( x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && - (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); + (x.Style >= key.Style || x.Style < key.Style)).ToArray(); } } key = keys.FirstOrDefault(); - fontFamily.TryGetValue(key, out var typeface); - - return typeface; - } - - private struct FontKey - { - public readonly SKFontStyleSlant Slant; - public readonly SKFontStyleWeight Weight; - - public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant) - { - Slant = slant; - Weight = weight; - } - - public override int GetHashCode() - { - var hash = 17; - hash = (hash * 31) + (int)Slant; - hash = (hash * 31) + (int)Weight; - - return hash; - } + fontFamily.TryGetValue(key, out var entry); - public override bool Equals(object other) - { - return other is FontKey key && this.Equals(key); - } - - private bool Equals(FontKey other) - { - return Slant == other.Slant && - Weight == other.Weight; - } + return entry; } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index ab8ee85a54..4bb42c7118 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -45,9 +45,13 @@ namespace Avalonia.Skia { var assetStream = assetLoader.Open(asset); - var typeface = SKTypeface.FromStream(assetStream); + var skTypeface = SKTypeface.FromStream(assetStream); - typeFaceCollection.AddTypeFace(typeface); + var typeface = new Typeface(fontFamily, (FontWeight)skTypeface.FontWeight, (FontStyle)skTypeface.FontSlant); + + var entry = new TypefaceCollectionEntry(typeface, skTypeface); + + typeFaceCollection.AddEntry(skTypeface.FamilyName, new FontKey(typeface.Weight, typeface.Style), entry); } return typeFaceCollection; diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index f16e967f42..ce3aef755b 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -25,6 +25,11 @@ namespace Avalonia.Skia AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface); + + var fontManager = new FontManagerImpl(); + + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(fontManager); } /// diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs index 9e270114d2..1c2b855032 100644 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ b/src/Skia/Avalonia.Skia/TypefaceCache.cs @@ -1,7 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Collections.Generic; +using System.Collections.Concurrent; using Avalonia.Media; using SkiaSharp; @@ -12,88 +12,36 @@ namespace Avalonia.Skia /// internal static class TypefaceCache { - public static readonly string DefaultFamilyName = CreateDefaultFamilyName(); + private static readonly ConcurrentDictionary> s_cache = + new ConcurrentDictionary>(); - private static readonly Dictionary> s_cache = - new Dictionary>(); - - struct FontKey + public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { - public readonly SKFontStyleSlant Slant; - public readonly SKFontStyleWeight Weight; - - public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant) + if (fontFamily.Key != null) { - Slant = slant; - Weight = weight; + return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily) + .Get(fontFamily.Name, fontWeight, fontStyle); } - public override int GetHashCode() - { - int hash = 17; - hash = hash * 31 + (int)Slant; - hash = hash * 31 + (int)Weight; - - return hash; - } - - public override bool Equals(object other) - { - return other is FontKey ? Equals((FontKey)other) : false; - } - - public bool Equals(FontKey other) - { - return Slant == other.Slant && - Weight == other.Weight; - } - - // Equals and GetHashCode ommitted - } - - private static string CreateDefaultFamilyName() - { - var defaultTypeface = SKTypeface.CreateDefault(); + var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary()); - return defaultTypeface.FamilyName; - } + var key = new FontKey(fontWeight, fontStyle); - private static SKTypeface GetTypeface(string name, FontKey key) - { - var familyKey = name; - - if (!s_cache.TryGetValue(familyKey, out var entry)) + if (typefaceCollection.TryGetValue(key, out var entry)) { - s_cache[familyKey] = entry = new Dictionary(); + return entry; } - if (!entry.TryGetValue(key, out var typeface)) - { - typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant) ?? - GetTypeface(DefaultFamilyName, key); + var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight, + SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default; - entry[key] = typeface; - } + var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle); - return typeface; - } - - public static SKTypeface GetTypeface(string name, FontStyle style, FontWeight weight) - { - var skStyle = SKFontStyleSlant.Upright; + entry = new TypefaceCollectionEntry(typeface, skTypeface); - switch (style) - { - case FontStyle.Italic: - skStyle = SKFontStyleSlant.Italic; - break; - - case FontStyle.Oblique: - skStyle = SKFontStyleSlant.Oblique; - break; - } + typefaceCollection[key] = entry; - return GetTypeface(name, new FontKey((SKFontStyleWeight)weight, skStyle)); + return entry; } } } diff --git a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs b/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs new file mode 100644 index 0000000000..ef9f889819 --- /dev/null +++ b/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs @@ -0,0 +1,19 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Media; +using SkiaSharp; + +namespace Avalonia.Skia +{ + internal class TypefaceCollectionEntry + { + public TypefaceCollectionEntry(Typeface typeface, SKTypeface skTypeface) + { + Typeface = typeface; + SKTypeface = skTypeface; + } + public Typeface Typeface { get; } + public SKTypeface SKTypeface { get; } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index 458d8f9cbb..7d47b95ede 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 5ab9a8f74d..e76596e925 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -41,20 +41,6 @@ namespace Avalonia.Direct2D1 public static SharpDX.DXGI.Device1 DxgiDevice { get; private set; } - public IEnumerable InstalledFontNames - { - get - { - var cache = Direct2D1FontCollectionCache.s_installedFontCollection; - var length = cache.FontFamilyCount; - for (int i = 0; i < length; i++) - { - var names = cache.GetFontFamily(i).FamilyNames; - yield return names.GetString(0); - } - } - } - private static readonly object s_initLock = new object(); private static bool s_initialized = false; @@ -120,6 +106,7 @@ namespace Avalonia.Direct2D1 { InitializeDirect2D(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new FontManagerImpl()); SharpDX.Configuration.EnableReleaseOnFinalizer = true; } @@ -131,6 +118,7 @@ namespace Avalonia.Direct2D1 public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -139,6 +127,7 @@ namespace Avalonia.Direct2D1 return new FormattedTextImpl( text, typeface, + fontSize, textAlignment, wrapping, constraint, diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index d93a59d384..b455c4fbee 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs @@ -1,62 +1,61 @@ using System.Collections.Concurrent; using Avalonia.Media; using Avalonia.Media.Fonts; +using SharpDX.DirectWrite; +using FontFamily = Avalonia.Media.FontFamily; +using FontStyle = SharpDX.DirectWrite.FontStyle; +using FontWeight = SharpDX.DirectWrite.FontWeight; namespace Avalonia.Direct2D1.Media { internal static class Direct2D1FontCollectionCache { - private static readonly ConcurrentDictionary s_cachedCollections; - internal static readonly SharpDX.DirectWrite.FontCollection s_installedFontCollection; + private static readonly ConcurrentDictionary s_cachedCollections; + internal static readonly FontCollection InstalledFontCollection; static Direct2D1FontCollectionCache() { - s_cachedCollections = new ConcurrentDictionary(); + s_cachedCollections = new ConcurrentDictionary(); - s_installedFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false); + InstalledFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false); } - public static SharpDX.DirectWrite.TextFormat GetTextFormat(Typeface typeface) + public static Font GetFont(Typeface typeface) { var fontFamily = typeface.FontFamily; var fontCollection = GetOrAddFontCollection(fontFamily); - var fontFamilyName = FontFamily.Default.Name; - // Should this be cached? foreach (var familyName in fontFamily.FamilyNames) { - if (!fontCollection.FindFamilyName(familyName, out _)) + if (fontCollection.FindFamilyName(familyName, out var index)) { - continue; + return fontCollection.GetFontFamily(index).GetFirstMatchingFont( + (FontWeight)typeface.Weight, + FontStretch.Normal, + (FontStyle)typeface.Style); } - - fontFamilyName = familyName; - - break; } - return new SharpDX.DirectWrite.TextFormat( - Direct2D1Platform.DirectWriteFactory, - fontFamilyName, - fontCollection, - (SharpDX.DirectWrite.FontWeight)typeface.Weight, - (SharpDX.DirectWrite.FontStyle)typeface.Style, - SharpDX.DirectWrite.FontStretch.Normal, - (float)typeface.FontSize); + InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i); + + return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont( + (FontWeight)typeface.Weight, + FontStretch.Normal, + (FontStyle)typeface.Style); } - private static SharpDX.DirectWrite.FontCollection GetOrAddFontCollection(FontFamily fontFamily) + private static FontCollection GetOrAddFontCollection(FontFamily fontFamily) { - return fontFamily.Key == null ? s_installedFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection); + return fontFamily.Key == null ? InstalledFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection); } - private static SharpDX.DirectWrite.FontCollection CreateFontCollection(FontFamilyKey key) + private static FontCollection CreateFontCollection(FontFamilyKey key) { var assets = FontFamilyLoader.LoadFontAssets(key); var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets); - return new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); + return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs new file mode 100644 index 0000000000..de1a4cf2d1 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -0,0 +1,80 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Platform; +using SharpDX.DirectWrite; +using FontFamily = Avalonia.Media.FontFamily; +using FontStyle = Avalonia.Media.FontStyle; +using FontWeight = Avalonia.Media.FontWeight; + +namespace Avalonia.Direct2D1.Media +{ + internal class FontManagerImpl : IFontManagerImpl + { + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); + + public FontManagerImpl() + { + //ToDo: Implement a real lookup of the system's default font. + DefaultFontFamilyName = "segoe ui"; + } + + public string DefaultFontFamilyName { get; } + + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + { + var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; + + var fontFamilies = new string[familyCount]; + + for (var i = 0; i < familyCount; i++) + { + fontFamilies[i] = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i).FamilyNames.GetString(0); + } + + return fontFamilies; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + } + + public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + { + //ToDo: Implement caching. + return new Typeface(fontFamily, fontWeight, fontStyle); + } + + public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null) + { + var fontFamilyName = FontFamily.Default.Name; + + var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; + + for (var i = 0; i < familyCount; i++) + { + var font = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i) + .GetMatchingFonts((SharpDX.DirectWrite.FontWeight)fontWeight, FontStretch.Normal, + (SharpDX.DirectWrite.FontStyle)fontStyle).GetFont(0); + + if (!font.HasCharacter(codepoint)) + { + continue; + } + + fontFamilyName = font.FontFamily.FamilyNames.GetString(0); + + break; + } + + return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index b73deb1f0a..b1a177ad24 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -14,6 +14,7 @@ namespace Avalonia.Direct2D1.Media public FormattedTextImpl( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -21,20 +22,20 @@ namespace Avalonia.Direct2D1.Media { Text = text; - using (var textFormat = Direct2D1FontCollectionCache.GetTextFormat(typeface)) + using (var font = Direct2D1FontCollectionCache.GetFont(typeface)) + using (var textFormat = new DWrite.TextFormat(Direct2D1Platform.DirectWriteFactory, + typeface.FontFamily.Name, font.FontFamily.FontCollection, (DWrite.FontWeight)typeface.Weight, + (DWrite.FontStyle)typeface.Style, DWrite.FontStretch.Normal, (float)fontSize)) { textFormat.WordWrapping = wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap; TextLayout = new DWrite.TextLayout( - Direct2D1Platform.DirectWriteFactory, - Text ?? string.Empty, - textFormat, - (float)constraint.Width, - (float)constraint.Height) - { - TextAlignment = textAlignment.ToDirect2D() - }; + Direct2D1Platform.DirectWriteFactory, + Text ?? string.Empty, + textFormat, + (float)constraint.Width, + (float)constraint.Height) { TextAlignment = textAlignment.ToDirect2D() }; } if (spans != null) diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs new file mode 100644 index 0000000000..66cf397110 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs @@ -0,0 +1,188 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; +using Avalonia.Platform; +using HarfBuzzSharp; +using SharpDX.DirectWrite; + +namespace Avalonia.Direct2D1.Media +{ + internal class GlyphTypefaceImpl : IGlyphTypefaceImpl + { + private bool _isDisposed; + + public GlyphTypefaceImpl(Typeface typeface) + { + DWFont = Direct2D1FontCollectionCache.GetFont(typeface); + + FontFace = new FontFace(DWFont); + + Face = new Face(GetTable); + + Font = new HarfBuzzSharp.Font(Face); + + Font.SetFunctionsOpenType(); + + Font.GetScale(out var xScale, out _); + + DesignEmHeight = (short)xScale; + + if (!Font.TryGetHorizontalFontExtents(out var fontExtents)) + { + Font.TryGetVerticalFontExtents(out fontExtents); + } + + Ascent = -fontExtents.Ascender; + + Descent = -fontExtents.Descender; + + LineGap = fontExtents.LineGap; + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition)) + { + UnderlinePosition = underlinePosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness)) + { + UnderlineThickness = underlineThickness; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition)) + { + StrikethroughPosition = strikethroughPosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness)) + { + StrikethroughThickness = strikethroughThickness; + } + } + + private Blob GetTable(Face face, Tag tag) + { + var dwTag = (int)SwapBytes(tag); + + if (FontFace.TryGetFontTable(dwTag, out var tableData, out _)) + { + return new Blob(tableData.Pointer, tableData.Size, MemoryMode.ReadOnly, () => { }); + } + + return null; + } + + private static uint SwapBytes(uint x) + { + x = (x >> 16) | (x << 16); + + return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8); + } + + public SharpDX.DirectWrite.Font DWFont { get; } + + public FontFace FontFace { get; } + + public Face Face { get; } + + public HarfBuzzSharp.Font Font { get; } + + /// + public short DesignEmHeight { get; } + + /// + public int Ascent { get; } + + /// + public int Descent { get; } + + /// + public int LineGap { get; } + + //ToDo: Read font table for these values + /// + public int UnderlinePosition { get; } + + /// + public int UnderlineThickness { get; } + + /// + public int StrikethroughPosition { get; } + + /// + public int StrikethroughThickness { get; } + + /// + public ushort GetGlyph(uint codepoint) + { + if (Font.TryGetGlyph(codepoint, out var glyph)) + { + return (ushort)glyph; + } + + return 0; + } + + /// + public ushort[] GetGlyphs(ReadOnlySpan codepoints) + { + var glyphs = new ushort[codepoints.Length]; + + for (var i = 0; i < codepoints.Length; i++) + { + if (Font.TryGetGlyph(codepoints[i], out var glyph)) + { + glyphs[i] = (ushort)glyph; + } + } + + return glyphs; + } + + /// + public int GetGlyphAdvance(ushort glyph) + { + return Font.GetHorizontalGlyphAdvance(glyph); + } + + /// + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) + { + var glyphIndices = new uint[glyphs.Length]; + + for (var i = 0; i < glyphs.Length; i++) + { + glyphIndices[i] = glyphs[i]; + } + + return Font.GetHorizontalGlyphAdvances(glyphIndices); + } + + private void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!disposing) + { + return; + } + + Font?.Dispose(); + Face?.Dispose(); + FontFace?.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} + diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index 6cf38b6121..a683e5cfca 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -175,6 +175,7 @@ namespace Avalonia.Layout.UnitTests x.CreateFormattedText( It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index 353123ab2a..bca34dd69d 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -53,7 +53,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var r = AvaloniaLocator.Current.GetService(); return r.CreateFormattedText(text, - new Typeface(fontFamily, fontSize, fontStyle, fontWeight), + new Typeface(fontFamily, fontWeight, fontStyle), + fontSize, textAlignment, wrapping, widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity), diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index a3cc3dec17..b3f0af55f4 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -9,11 +9,10 @@ namespace Avalonia.UnitTests { public class MockPlatformRenderInterface : IPlatformRenderInterface { - public IEnumerable InstalledFontNames => new string[0]; - public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index f7a878feba..d189aa3165 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -169,6 +169,7 @@ namespace Avalonia.UnitTests x.CreateFormattedText( It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs index 75ae43a1fa..5d47333d51 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs @@ -19,12 +19,48 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(new FontFamily("Arial"), fontFamily); } - [Fact] - public void Should_Be_Equal() + [InlineData("Font A")] + [InlineData("Font A, Font B")] + [InlineData("resm: Avalonia.Visuals.UnitTests#MyFont")] + [InlineData("avares://Avalonia.Visuals.UnitTests/Assets/Fonts#MyFont")] + [Theory] + public void Should_Have_Equal_Hash(string s) { - var fontFamily = new FontFamily("Arial"); + var fontFamily = new FontFamily(s); - Assert.Equal(new FontFamily("Arial"), fontFamily); + Assert.Equal(new FontFamily(s).GetHashCode(), fontFamily.GetHashCode()); + } + + [InlineData("Font A, Font B", "Font B, Font A")] + [InlineData("Font A, Font B", "Font A, Font C")] + [Theory] + public void Should_Not_Have_Equal_Hash(string a, string b) + { + var fontFamily = new FontFamily(b); + + Assert.NotEqual(new FontFamily(a).GetHashCode(), fontFamily.GetHashCode()); + } + + [InlineData("Font A")] + [InlineData("Font A, Font B")] + [InlineData("resm: Avalonia.Visuals.UnitTests#MyFont")] + [InlineData("avares://Avalonia.Visuals.UnitTests/Assets/Fonts#MyFont")] + [Theory] + public void Should_Be_Equal(string s) + { + var fontFamily = new FontFamily(s); + + Assert.Equal(new FontFamily(s), fontFamily); + } + + [InlineData("Font A, Font B", "Font B, Font A")] + [InlineData("Font A, Font B", "Font A, Font C")] + [Theory] + public void Should_Not_Be_Equal(string a, string b) + { + var fontFamily = new FontFamily(b); + + Assert.NotEqual(new FontFamily(a), fontFamily); } [Fact] diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs index 2c8f8eb9b2..0e43c76da1 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs @@ -7,15 +7,21 @@ namespace Avalonia.Visuals.UnitTests.Media public class TypefaceTests { [Fact] - public void Exception_Should_Be_Thrown_If_FontSize_LessThanEqualTo_0() + public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_Zero() { - Assert.Throws(() => new Typeface("foo", 0)); + Assert.Throws(() => new Typeface("foo", 0, (FontStyle)12)); } [Fact] - public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_0() + public void Should_Be_Equal() { - Assert.Throws(() => new Typeface("foo", 12, weight: 0)); + Assert.Equal(new Typeface("Font A"), new Typeface("Font A")); + } + + [Fact] + public void Should_Have_Equal_Hash() + { + Assert.Equal(new Typeface("Font A").GetHashCode(), new Typeface("Font A").GetHashCode()); } } } diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index d31210bc71..335cdf4597 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -13,6 +13,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, From 9d105c7dbbb026cb7d3d362f96aa24cb70e3e9d2 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 1 Nov 2019 12:01:02 +0100 Subject: [PATCH 0423/1791] Introduce a static FontManager that uses a platform implementation under the hood. --- src/Avalonia.Visuals/Media/FontFamily.cs | 7 +- src/Avalonia.Visuals/Media/FontManager.cs | 92 +++++-------------- src/Avalonia.Visuals/Media/GlyphTypeface.cs | 5 +- .../Platform/IFontManagerImpl.cs | 9 -- .../Platform/IPlatformRenderInterface.cs | 9 ++ src/Skia/Avalonia.Skia/FontManagerImpl.cs | 9 -- .../Avalonia.Skia/PlatformRenderInterface.cs | 9 ++ .../Avalonia.Direct2D1/Direct2D1Platform.cs | 8 ++ .../Media/FontManagerImpl.cs | 9 -- 9 files changed, 59 insertions(+), 98 deletions(-) diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index 665dfc1129..d263097e6a 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -12,7 +12,7 @@ namespace Avalonia.Media { static FontFamily() { - Default = new FontFamily(FontManager.Default.DefaultFontFamilyName); + Default = new FontFamily(FontManager.DefaultFontFamilyName); } /// @@ -60,8 +60,11 @@ namespace Avalonia.Media /// /// Represents all font families in the system. This can be an expensive call depending on platform implementation. /// + /// + /// Consider using the new instead. + /// public static IEnumerable SystemFontFamilies => - FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); + FontManager.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); /// /// Gets the primary family name of the font family. diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index e89471ede8..95d91e7df1 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -1,99 +1,55 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; using System.Collections.Generic; using System.Globalization; using Avalonia.Platform; namespace Avalonia.Media { - public abstract class FontManager : IFontManagerImpl + public static class FontManager { - public static readonly FontManager Default = CreateDefaultFontManger(); + private static readonly IFontManagerImpl s_platformImpl = GetPlatformImpl(); - /// - public string DefaultFontFamilyName { get; protected set; } + /// + public static string DefaultFontFamilyName => s_platformImpl.DefaultFontFamilyName; - private static FontManager CreateDefaultFontManger() - { - var platformImpl = AvaloniaLocator.Current.GetService(); - - if(platformImpl == null) - { - return new EmptyFontManager(); - } - - return new PlatformFontManger(platformImpl); - } + /// + public static IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => + s_platformImpl.GetInstalledFontFamilyNames(checkForUpdates); - /// - public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + /// + public static Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => + s_platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle); - /// - public abstract IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); - - /// - public abstract Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); - - /// - public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, + /// + public static Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null); + FontFamily fontFamily = null, CultureInfo culture = null) => + s_platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture); - private class PlatformFontManger : FontManager + private static IFontManagerImpl GetPlatformImpl() { - private readonly IFontManagerImpl _platformImpl; - - public PlatformFontManger(IFontManagerImpl platformImpl) - { - _platformImpl = platformImpl; - - DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName; - } - - public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - _platformImpl.GetInstalledFontFamilyNames(checkForUpdates); - - public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) => _platformImpl.CreateGlyphTypeface(typeface); - - public override Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => - _platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle); + var platformImpl = AvaloniaLocator.Current.GetService(); - public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, - FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) => - _platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture); + return platformImpl ?? new EmptyFontManagerImpl(); } - private class EmptyFontManager : FontManager + private class EmptyFontManagerImpl : IFontManagerImpl { - private readonly string[] _defaultFontFamilies = { "Arial" }; - - public EmptyFontManager() - { - DefaultFontFamilyName = "Arial"; - } - - public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) - { - return _defaultFontFamilies; - } + public string DefaultFontFamilyName => "Arial"; - public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) - { - throw new NotSupportedException(); - } + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => new[] { "Arial" }; - public override Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { - throw new NotSupportedException(); + return new Typeface(fontFamily, fontWeight, fontStyle); } - public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, + public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, FontFamily fontFamily = null, CultureInfo culture = null) { - throw new NotSupportedException(); + return null; } } } diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs index 1c959a86c5..cba7c8c795 100644 --- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs +++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs @@ -9,7 +9,10 @@ namespace Avalonia.Media { public class GlyphTypeface : IDisposable { - public GlyphTypeface(Typeface typeface) : this(FontManager.Default.CreateGlyphTypeface(typeface)) + private static readonly IPlatformRenderInterface s_platformRenderInterface = + AvaloniaLocator.Current.GetService(); + + public GlyphTypeface(Typeface typeface) : this(s_platformRenderInterface.CreateGlyphTypeface(typeface)) { } diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 236631edde..254b5d07d1 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -20,15 +20,6 @@ namespace Avalonia.Platform /// IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); - /// - /// Creates a glyph typeface for specified typeface. - /// - /// The typeface. - /// - /// The glyph typeface implementation. - /// - IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); - /// /// Get a typeface from specified parameters. /// diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 619d3088b4..5a0a7b2f19 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -111,5 +111,14 @@ namespace Avalonia.Platform /// The number of bytes per row. /// An . IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride); + + /// + /// Creates a glyph typeface for specified typeface. + /// + /// The typeface. + /// + /// The glyph typeface implementation. + /// + IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); } } diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 6c67438533..03de82178a 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using Avalonia.Media; @@ -14,9 +13,6 @@ namespace Avalonia.Skia { private SKFontManager _skFontManager = SKFontManager.Default; - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - public FontManagerImpl() { DefaultFontFamilyName = SKTypeface.Default.FamilyName; @@ -34,11 +30,6 @@ namespace Avalonia.Skia return _skFontManager.FontFamilies; } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) - { - return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); - } - public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface; diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index e4ad0c1b24..ee0cfb2f06 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using Avalonia.Controls.Platform.Surfaces; @@ -17,6 +18,9 @@ namespace Avalonia.Skia /// internal class PlatformRenderInterface : IPlatformRenderInterface { + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); + private readonly ICustomSkiaGpu _customSkiaGpu; private GRContext GrContext { get; } @@ -150,5 +154,10 @@ namespace Avalonia.Skia { return new WriteableBitmapImpl(size, dpi, format); } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index e76596e925..1bda5157a5 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using Avalonia.Controls; @@ -27,6 +28,8 @@ namespace Avalonia.Direct2D1 { public class Direct2D1Platform : IPlatformRenderInterface { + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); private static readonly Direct2D1Platform s_instance = new Direct2D1Platform(); public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; } @@ -190,5 +193,10 @@ namespace Avalonia.Direct2D1 { return new WicBitmapImpl(format, data, size, dpi, stride); } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index de1a4cf2d1..94de397652 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using Avalonia.Media; @@ -15,9 +14,6 @@ namespace Avalonia.Direct2D1.Media { internal class FontManagerImpl : IFontManagerImpl { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - public FontManagerImpl() { //ToDo: Implement a real lookup of the system's default font. @@ -40,11 +36,6 @@ namespace Avalonia.Direct2D1.Media return fontFamilies; } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) - { - return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); - } - public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { //ToDo: Implement caching. From 46d3a916527e791c0c16ba1ec99b39d828ffbd1a Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 1 Nov 2019 12:08:54 +0100 Subject: [PATCH 0424/1791] Add missing mocks --- tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs | 5 +++++ .../VisualTree/MockRenderInterface.cs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index b3f0af55f4..187853283f 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -78,5 +78,10 @@ namespace Avalonia.UnitTests { throw new NotImplementedException(); } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return Mock.Of(); + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 335cdf4597..032b6582a9 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -52,6 +52,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + throw new NotImplementedException(); + } + public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt) { throw new NotImplementedException(); From 38ae0140c253475d9695474e75587382f56969f3 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 1 Nov 2019 17:03:12 +0100 Subject: [PATCH 0425/1791] Remove special case that checks for AvaloniaList --- src/Avalonia.Controls/TabControl.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 87ba282db0..8b1cd48379 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -4,7 +4,6 @@ using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; -using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -180,14 +179,6 @@ namespace Avalonia.Controls var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index); - if (container == null) - { - if (Items is AvaloniaList items) - { - container = items[index] as TabItem; - } - } - if (container == null) { return; @@ -196,7 +187,7 @@ namespace Avalonia.Controls UpdateSelectedContent(container); } - private void UpdateSelectedContent(TabItem item) + private void UpdateSelectedContent(IContentControl item) { if (SelectedContentTemplate != item.ContentTemplate) { From a612ee5a3eb1f5d80dfd540e5c64f5d998139784 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 1 Nov 2019 18:04:12 +0200 Subject: [PATCH 0426/1791] fix WrapPanel issue #3165 item width/height should trigger new measure --- src/Avalonia.Controls/WrapPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 7c88401615..d06a71a9f8 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls /// static WrapPanel() { - AffectsMeasure(OrientationProperty); + AffectsMeasure(OrientationProperty, ItemWidthProperty, ItemHeightProperty); } /// From e1df65c94b4b829b87403e472da0d8931ce5cddd Mon Sep 17 00:00:00 2001 From: mstr2 Date: Fri, 1 Nov 2019 18:36:25 +0100 Subject: [PATCH 0427/1791] Failing unit test for #3197 --- .../Properties/AssemblyInfo.cs | 1 + .../TransitionsTests.cs | 28 ++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Animation/Properties/AssemblyInfo.cs b/src/Avalonia.Animation/Properties/AssemblyInfo.cs index eb38a66a84..8523b9537d 100644 --- a/src/Avalonia.Animation/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Animation/Properties/AssemblyInfo.cs @@ -10,3 +10,4 @@ using System.Runtime.CompilerServices; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")] [assembly: InternalsVisibleTo("Avalonia.LeakTests")] +[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests")] diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs index f1b4b0d071..22f3b4f501 100644 --- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs +++ b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs @@ -1,14 +1,7 @@ using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Animation; using Avalonia.Controls; -using Avalonia.Styling; using Avalonia.UnitTests; -using Avalonia.Data; using Xunit; -using Avalonia.Animation.Easings; namespace Avalonia.Animation.UnitTests { @@ -69,5 +62,26 @@ namespace Avalonia.Animation.UnitTests Assert.Equal(0, border.Opacity); } } + + [Fact] + public void TransitionInstance_With_Zero_Duration_Is_Completed_On_First_Tick() + { + var clock = new MockGlobalClock(); + + using (UnitTestApplication.Start(new TestServices(globalClock: clock))) + { + int i = 0; + var inst = new TransitionInstance(clock, TimeSpan.Zero).Subscribe(nextValue => + { + switch (i++) + { + case 0: Assert.Equal(0, nextValue); break; + case 1: Assert.Equal(1d, nextValue); break; + } + }); + + clock.Pulse(TimeSpan.FromMilliseconds(10)); + } + } } } From 533c17a1816b8eb7b8cc38670634e3f9690d264a Mon Sep 17 00:00:00 2001 From: mstr2 Date: Fri, 1 Nov 2019 18:37:18 +0100 Subject: [PATCH 0428/1791] TransitionInstance with zero duration is now completed on first tick --- src/Avalonia.Animation/TransitionInstance.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index 10ea6bf523..a69ad50a4b 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -28,7 +28,7 @@ namespace Avalonia.Animation private void TimerTick(TimeSpan t) { - var interpVal = (double)t.Ticks / _duration.Ticks; + var interpVal = _duration.Ticks == 0 ? 1d : (double)t.Ticks / _duration.Ticks; // Clamp interpolation value. if (interpVal >= 1d | interpVal < 0d) From 75142ce5fa71fc89991eeb4753352b50a6648eec Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 2 Nov 2019 21:43:01 +0100 Subject: [PATCH 0429/1791] Set grid splitter min width and height to avoid invisible splitters. Make splitter background lighter. --- src/Avalonia.Diagnostics/Views/TreePageView.xaml | 4 ++-- src/Avalonia.Themes.Default/GridSplitter.xaml | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Views/TreePageView.xaml index ca7314264a..2619fd744a 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Views/TreePageView.xaml @@ -2,7 +2,7 @@ xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Avalonia.Diagnostics.Views.TreePageView"> - + - + diff --git a/src/Avalonia.Themes.Default/GridSplitter.xaml b/src/Avalonia.Themes.Default/GridSplitter.xaml index cfab5dab56..dc5cd002dc 100644 --- a/src/Avalonia.Themes.Default/GridSplitter.xaml +++ b/src/Avalonia.Themes.Default/GridSplitter.xaml @@ -2,7 +2,9 @@ - - - - - - - - - - - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 4fc63ea054..7adceddacf 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -13,15 +13,6 @@ namespace ControlCatalog public override void Initialize() { AvaloniaXamlLoader.Load(this); - - Name = "Avalonia"; - - _recentMenu = (NativeMenu.GetMenu(this).Items[1] as NativeMenuItem).Menu; - } - - public void OnOpenClicked(object sender, EventArgs args) - { - _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); } public override void OnFrameworkInitializationCompleted() From 3e8138cfe4782e5ca007f96abb5db41083a43d76 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Nov 2019 11:38:21 +0000 Subject: [PATCH 0461/1791] Set a default Avalonia application name. --- src/Avalonia.Controls/Application.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index ce60a0f0b9..59c6c47ed9 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -48,6 +48,14 @@ namespace Avalonia /// public event EventHandler ResourcesChanged; + /// + /// Creates an instance of the class. + /// + public Application() + { + Name = "Avalonia Application"; + } + /// /// Gets the current instance of the class. /// From 48234ad3ab8aea1c8ff5801ff20c4ea565f2e615 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Wed, 6 Nov 2019 20:21:15 +0800 Subject: [PATCH 0462/1791] Add a Avalonia about window default implementation. --- src/Avalonia.Dialogs/DefaultAboutWindow.xaml | 109 ++++++++++++++++++ .../DefaultAboutWindow.xaml.cs | 16 +++ 2 files changed, 125 insertions(+) create mode 100644 src/Avalonia.Dialogs/DefaultAboutWindow.xaml create mode 100644 src/Avalonia.Dialogs/DefaultAboutWindow.xaml.cs diff --git a/src/Avalonia.Dialogs/DefaultAboutWindow.xaml b/src/Avalonia.Dialogs/DefaultAboutWindow.xaml new file mode 100644 index 0000000000..8d666f7e54 --- /dev/null +++ b/src/Avalonia.Dialogs/DefaultAboutWindow.xaml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Dialogs/DefaultAboutWindow.xaml.cs b/src/Avalonia.Dialogs/DefaultAboutWindow.xaml.cs new file mode 100644 index 0000000000..b5adab5b7c --- /dev/null +++ b/src/Avalonia.Dialogs/DefaultAboutWindow.xaml.cs @@ -0,0 +1,16 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Dialogs +{ + public class DefaultAboutWindow : Window + { + public DefaultAboutWindow() + { + AvaloniaXamlLoader.Load(this); + } + } +} From 5da9bcd72304537e9f4eb6978775f6a1bfde729f Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Wed, 6 Nov 2019 22:51:47 +0800 Subject: [PATCH 0463/1791] Change to AboutAvaloniaDialog --- .../{DefaultAboutWindow.xaml => AboutAvaloniaDialog.xaml} | 2 +- ...DefaultAboutWindow.xaml.cs => AboutAvaloniaDialog.xaml.cs} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Avalonia.Dialogs/{DefaultAboutWindow.xaml => AboutAvaloniaDialog.xaml} (98%) rename src/Avalonia.Dialogs/{DefaultAboutWindow.xaml.cs => AboutAvaloniaDialog.xaml.cs} (79%) diff --git a/src/Avalonia.Dialogs/DefaultAboutWindow.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml similarity index 98% rename from src/Avalonia.Dialogs/DefaultAboutWindow.xaml rename to src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml index 8d666f7e54..fd62bddd3c 100644 --- a/src/Avalonia.Dialogs/DefaultAboutWindow.xaml +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml @@ -6,7 +6,7 @@ MinHeight="220" Title="About Avalonia" Background="Purple" - x:Class="Avalonia.Dialogs.DefaultAboutWindow"> + x:Class="Avalonia.Dialogs.AboutAvaloniaDialog"> - + + + + + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - + - + \ No newline at end of file From 08bb2399f6944c6fbcc444b1582a932a289641b1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 7 Nov 2019 13:41:38 +0000 Subject: [PATCH 0472/1791] implement File -> Exit and Help -> About in control catalog. --- samples/ControlCatalog/MainWindow.xaml | 4 ++-- .../ViewModels/MainWindowViewModel.cs | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index c45548e439..248f94082d 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -45,10 +45,10 @@ - + - + diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index adf0345a70..89e7653618 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,7 @@ using System.Reactive; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; +using Avalonia.Dialogs; using ReactiveUI; namespace ControlCatalog.ViewModels @@ -26,6 +28,20 @@ namespace ControlCatalog.ViewModels { NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", "Native Notifications are not quite ready. Coming soon.", NotificationType.Error)); }); + + AboutCommand = ReactiveCommand.CreateFromTask(async () => + { + var dialog = new AboutAvaloniaDialog(); + + var mainWindow = (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow; + + await dialog.ShowDialog(mainWindow); + }); + + ExitCommand = ReactiveCommand.Create(() => + { + (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown(); + }); } public IManagedNotificationManager NotificationManager @@ -39,5 +55,9 @@ namespace ControlCatalog.ViewModels public ReactiveCommand ShowManagedNotificationCommand { get; } public ReactiveCommand ShowNativeNotificationCommand { get; } + + public ReactiveCommand AboutCommand { get; } + + public ReactiveCommand ExitCommand { get; } } } From 868b125848f2380f80adb659cd6ac697d41581c7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 7 Nov 2019 14:13:14 +0000 Subject: [PATCH 0473/1791] add hyper links. --- src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml | 12 +++- .../AboutAvaloniaDialog.xaml.cs | 55 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml index d7fcb22b95..ffc23cc5b0 100644 --- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml @@ -73,6 +73,14 @@ - + + + protected virtual void HandleClosed() { + { + var e = new LogicalTreeAttachmentEventArgs(this); + + ((ILogical)this).NotifyDetachedFromLogicalTree(e); + } + + { + var e = new VisualTreeAttachmentEventArgs(this, this); + + OnDetachedFromVisualTreeCore(e); + } + (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; OnClosed(EventArgs.Empty); From 1212e2444769f76c60c75b8b06e4988fcdae7ff3 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sun, 24 Nov 2019 15:45:31 +0800 Subject: [PATCH 0536/1791] Hackfix for #3284 --- src/Avalonia.Controls/Primitives/RangeBase.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index f1ee7c0e1a..39d00b6953 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -75,8 +75,11 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Minimum"); - + if (!ValidateDouble(value)) + { + value = _minimum; + } + if (IsInitialized) { SetAndRaise(MinimumProperty, ref _minimum, value); @@ -102,7 +105,10 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Maximum"); + if (!ValidateDouble(value)) + { + value = _maximum; + } if (IsInitialized) { @@ -129,7 +135,10 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Value"); + if (!ValidateDouble(value)) + { + value = default; + } if (IsInitialized) { @@ -167,13 +176,9 @@ namespace Avalonia.Controls.Primitives /// Throws an exception if the double value is NaN or Inf. /// /// The value. - /// The name of the property being set. - private static void ValidateDouble(double value, string property) + private static bool ValidateDouble(double value) { - if (double.IsInfinity(value) || double.IsNaN(value)) - { - throw new ArgumentException($"{value} is not a valid value for {property}."); - } + return (!double.IsInfinity(value) || !double.IsNaN(value)); } /// From 7c7f6022cba12ad59c69fe2b614da27e2786c725 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 24 Nov 2019 15:46:00 +0100 Subject: [PATCH 0537/1791] Cleanup event args locals. --- src/Avalonia.Controls/TopLevel.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 81f132660d..a0df186eb7 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -269,17 +269,11 @@ namespace Avalonia.Controls /// protected virtual void HandleClosed() { - { - var e = new LogicalTreeAttachmentEventArgs(this); - - ((ILogical)this).NotifyDetachedFromLogicalTree(e); - } + var logicalArgs = new LogicalTreeAttachmentEventArgs(this); + ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); - { - var e = new VisualTreeAttachmentEventArgs(this, this); - - OnDetachedFromVisualTreeCore(e); - } + var visualArgs = new VisualTreeAttachmentEventArgs(this, this); + OnDetachedFromVisualTreeCore(visualArgs); (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; From aa81db75a0912ee4e95f4547d1218acc17f02cc6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 22 Nov 2019 11:09:39 +0100 Subject: [PATCH 0538/1791] Added WPF-style validation for AvaloniaProperty. --- src/Avalonia.Base/AvaloniaProperty.cs | 7 +- src/Avalonia.Base/BoxedValue.cs | 28 ---- .../PropertyStore/BindingEntry.cs | 5 + src/Avalonia.Base/StyledProperty.cs | 4 +- src/Avalonia.Base/StyledPropertyBase.cs | 28 +++- src/Avalonia.Base/StyledPropertyMetadata`1.cs | 14 +- src/Avalonia.Base/ValueStore.cs | 5 + src/Avalonia.Controls.DataGrid/DataGrid.cs | 121 ++++-------------- .../DataGridRowGroupHeader.cs | 22 +--- src/Avalonia.Controls/AutoCompleteBox.cs | 43 ++----- src/Avalonia.Controls/Calendar/Calendar.cs | 16 +-- src/Avalonia.Controls/Calendar/DatePicker.cs | 30 +---- src/Avalonia.Controls/DefinitionBase.cs | 10 +- src/Avalonia.Controls/Grid.cs | 32 ++--- .../AvaloniaObjectTests_Validation.cs | 82 ++++++++++++ 15 files changed, 199 insertions(+), 248 deletions(-) delete mode 100644 src/Avalonia.Base/BoxedValue.cs create mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index a39b8e6e1d..31a7f21a1b 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -257,6 +257,7 @@ namespace Avalonia /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. + /// A value validation callback. /// /// A method that gets called before and after the property starts being notified on an /// object; the bool argument will be true before and false afterwards. This callback is @@ -268,6 +269,7 @@ namespace Avalonia TValue defaultValue = default(TValue), bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, + Func validate = null, Action notifying = null) where TOwner : IAvaloniaObject { @@ -282,6 +284,7 @@ namespace Avalonia typeof(TOwner), metadata, inherits, + validate, notifying); AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; @@ -297,12 +300,14 @@ namespace Avalonia /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. + /// A value validation callback. /// A public static AttachedProperty RegisterAttached( string name, TValue defaultValue = default(TValue), bool inherits = false, - BindingMode defaultBindingMode = BindingMode.OneWay) + BindingMode defaultBindingMode = BindingMode.OneWay, + Func validate = null) where THost : IAvaloniaObject { Contract.Requires(name != null); diff --git a/src/Avalonia.Base/BoxedValue.cs b/src/Avalonia.Base/BoxedValue.cs deleted file mode 100644 index 5fc515f299..0000000000 --- a/src/Avalonia.Base/BoxedValue.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -namespace Avalonia -{ - /// - /// Represents boxed value of type . - /// - /// Type of stored value. - internal readonly struct BoxedValue - { - public BoxedValue(T value) - { - Boxed = value; - Typed = value; - } - - /// - /// Boxed value. - /// - public object Boxed { get; } - - /// - /// Typed value. - /// - public T Typed { get; } - } -} diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 2a7cf098a5..a53dda2aef 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -77,6 +77,11 @@ namespace Avalonia.PropertyStore private void UpdateValue(BindingValue value) { + if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false) + { + value = Property.GetDefaultValue(_owner.GetType()); + } + if (value.Type == BindingValueType.DoNothing) { return; diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 4eb85a046e..62443b424c 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -17,14 +17,16 @@ namespace Avalonia /// The type of the class that registers the property. /// The property metadata. /// Whether the property inherits its value. + /// A value validation callback. /// A callback. public StyledProperty( string name, Type ownerType, StyledPropertyMetadata metadata, bool inherits = false, + Func validate = null, Action notifying = null) - : base(name, ownerType, metadata, inherits, notifying) + : base(name, ownerType, metadata, inherits, validate, notifying) { } diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 129b1f3c12..8e7bfd6467 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; using System.Diagnostics; using Avalonia.Data; using Avalonia.Reactive; @@ -23,12 +22,14 @@ namespace Avalonia /// The type of the class that registers the property. /// The property metadata. /// Whether the property inherits its value. + /// A value validation callback. /// A callback. protected StyledPropertyBase( string name, Type ownerType, StyledPropertyMetadata metadata, bool inherits = false, + Func validate = null, Action notifying = null) : base(name, ownerType, metadata, notifying) { @@ -41,6 +42,13 @@ namespace Avalonia } _inherits = inherits; + ValidateValue = validate; + + if (validate?.Invoke(metadata.DefaultValue) == false) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{name}'."); + } } /// @@ -62,6 +70,11 @@ namespace Avalonia /// public override bool Inherits => _inherits; + /// + /// Gets the value validation callback for the property. + /// + public Func ValidateValue { get; } + /// /// Gets the default value for the property on the specified type. /// @@ -71,7 +84,7 @@ namespace Avalonia { Contract.Requires(type != null); - return GetMetadata(type).DefaultValue.Typed; + return GetMetadata(type).DefaultValue; } /// @@ -123,6 +136,15 @@ namespace Avalonia /// The metadata. public void OverrideMetadata(Type type, StyledPropertyMetadata metadata) { + if (ValidateValue != null) + { + if (!ValidateValue(metadata.DefaultValue)) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{Name}'."); + } + } + base.OverrideMetadata(type, metadata); } @@ -209,7 +231,7 @@ namespace Avalonia { Contract.Requires(type != null); - return GetMetadata(type).DefaultValue.Boxed; + return GetMetadata(type).DefaultValue; } [DebuggerHidden] diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index d4ce137e0a..50236e2c7c 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -12,25 +12,27 @@ namespace Avalonia /// public class StyledPropertyMetadata : PropertyMetadata, IStyledPropertyMetadata { + private Optional _defaultValue; + /// /// Initializes a new instance of the class. /// /// The default value of the property. /// The default binding mode. public StyledPropertyMetadata( - TValue defaultValue = default, + Optional defaultValue = default, BindingMode defaultBindingMode = BindingMode.Default) : base(defaultBindingMode) { - DefaultValue = new BoxedValue(defaultValue); + _defaultValue = defaultValue; } /// /// Gets the default value for the property. /// - internal BoxedValue DefaultValue { get; private set; } + internal TValue DefaultValue => _defaultValue.ValueOrDefault(); - object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed; + object IStyledPropertyMetadata.DefaultValue => DefaultValue; /// public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property) @@ -39,9 +41,9 @@ namespace Avalonia if (baseMetadata is StyledPropertyMetadata src) { - if (DefaultValue.Boxed == null) + if (!_defaultValue.HasValue) { - DefaultValue = src.DefaultValue; + _defaultValue = src.DefaultValue; } } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 44ee1c4c8f..09961b399b 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -66,6 +66,11 @@ namespace Avalonia public void SetValue(StyledPropertyBase property, T value, BindingPriority priority) { + if (property.ValidateValue?.Invoke(value) == false) + { + throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); + } + if (_values.TryGetValue(property, out var slot)) { SetExisting(slot, property, value, priority); diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index a217d02ecd..86133d5fdb 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -201,21 +201,13 @@ namespace Avalonia.Controls public static readonly StyledProperty ColumnHeaderHeightProperty = AvaloniaProperty.Register( nameof(ColumnHeaderHeight), - defaultValue: double.NaN/*, - validate: ValidateColumnHeaderHeight*/); + defaultValue: double.NaN, + validate: IsValidColumnHeaderHeight); - private static double ValidateColumnHeaderHeight(DataGrid grid, double value) + private static bool IsValidColumnHeaderHeight(double value) { - if (value < DATAGRID_minimumColumnHeaderHeight) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_minimumColumnHeaderHeight); - } - if (value > DATAGRID_maxHeadersThickness) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_maxHeadersThickness); - } - - return value; + return double.IsNaN(value) || + (value >= DATAGRID_minimumColumnHeaderHeight && value <= DATAGRID_maxHeadersThickness); } /// @@ -261,8 +253,8 @@ namespace Avalonia.Controls public static readonly StyledProperty FrozenColumnCountProperty = AvaloniaProperty.Register( - nameof(FrozenColumnCount)/*, - validate: ValidateFrozenColumnCount*/); + nameof(FrozenColumnCount), + validate: ValidateFrozenColumnCount); /// /// Gets or sets the number of columns that the user cannot scroll horizontally. @@ -273,15 +265,7 @@ namespace Avalonia.Controls set { SetValue(FrozenColumnCountProperty, value); } } - private static int ValidateFrozenColumnCount(DataGrid grid, int value) - { - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0); - } - - return value; - } + private static bool ValidateFrozenColumnCount(int value) => value >= 0; public static readonly StyledProperty GridLinesVisibilityProperty = AvaloniaProperty.Register(nameof(GridLinesVisibility)); @@ -395,30 +379,12 @@ namespace Avalonia.Controls public static readonly StyledProperty MaxColumnWidthProperty = AvaloniaProperty.Register( nameof(MaxColumnWidth), - defaultValue: DATAGRID_defaultMaxColumnWidth/*, - validate: ValidateMaxColumnWidth*/); + defaultValue: DATAGRID_defaultMaxColumnWidth, + validate: IsValidColumnWidth); - private static double ValidateMaxColumnWidth(DataGrid grid, double value) + private static bool IsValidColumnWidth(double value) { - if (double.IsNaN(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MaxColumnWidth)); - } - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), 0); - } - if (grid.MinColumnWidth > value) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), nameof(MinColumnWidth)); - } - - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0); - } - - return value; + return !double.IsNaN(value) && value > 0; } /// @@ -433,29 +399,12 @@ namespace Avalonia.Controls public static readonly StyledProperty MinColumnWidthProperty = AvaloniaProperty.Register( nameof(MinColumnWidth), - defaultValue: DATAGRID_defaultMinColumnWidth/*, - validate: ValidateMinColumnWidth*/); + defaultValue: DATAGRID_defaultMinColumnWidth, + validate: IsValidMinColumnWidth); - private static double ValidateMinColumnWidth(DataGrid grid, double value) + private static bool IsValidMinColumnWidth(double value) { - if (double.IsNaN(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MinColumnWidth)); - } - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MinColumnWidth), 0); - } - if (double.IsPositiveInfinity(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(MinColumnWidth)); - } - if (grid.MaxColumnWidth < value) - { - throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(MinColumnWidth), nameof(MaxColumnWidth)); - } - - return value; + return !double.IsNaN(value) && !double.IsPositiveInfinity(value) && value >= 0; } /// @@ -482,20 +431,13 @@ namespace Avalonia.Controls public static readonly StyledProperty RowHeightProperty = AvaloniaProperty.Register( nameof(RowHeight), - defaultValue: double.NaN/*, - validate: ValidateRowHeight*/); - private static double ValidateRowHeight(DataGrid grid, double value) + defaultValue: double.NaN, + validate: IsValidRowHeight); + private static bool IsValidRowHeight(double value) { - if (value < DataGridRow.DATAGRIDROW_minimumHeight) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeight), 0); - } - if (value > DataGridRow.DATAGRIDROW_maximumHeight) - { - throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeight), DataGridRow.DATAGRIDROW_maximumHeight); - } - - return value; + return double.IsNaN(value) || + (value >= DataGridRow.DATAGRIDROW_minimumHeight && + value <= DataGridRow.DATAGRIDROW_maximumHeight); } /// @@ -510,20 +452,13 @@ namespace Avalonia.Controls public static readonly StyledProperty RowHeaderWidthProperty = AvaloniaProperty.Register( nameof(RowHeaderWidth), - defaultValue: double.NaN/*, - validate: ValidateRowHeaderWidth*/); - private static double ValidateRowHeaderWidth(DataGrid grid, double value) + defaultValue: double.NaN, + validate: IsValidRowHeaderWidth); + private static bool IsValidRowHeaderWidth(double value) { - if (value < DATAGRID_minimumRowHeaderWidth) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_minimumRowHeaderWidth); - } - if (value > DATAGRID_maxHeadersThickness) - { - throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_maxHeadersThickness); - } - - return value; + return double.IsNaN(value) || + (value >= DATAGRID_minimumRowHeaderWidth && + value <= DATAGRID_maxHeadersThickness); } /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index bede7f481e..69dfed761f 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -67,26 +67,12 @@ namespace Avalonia.Controls public static readonly StyledProperty SublevelIndentProperty = AvaloniaProperty.Register( nameof(SublevelIndent), - defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent/*, - validate: ValidateSublevelIndent*/); + defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent, + validate: IsValidSublevelIndent); - private static double ValidateSublevelIndent(DataGridRowGroupHeader header, double value) + private static bool IsValidSublevelIndent(double value) { - // We don't need to revert to the old value if our input is bad because we never read this property value - if (double.IsNaN(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(SublevelIndent)); - } - else if (double.IsInfinity(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(SublevelIndent)); - } - else if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(SublevelIndent), 0); - } - - return value; + return !double.IsNaN(value) && !double.IsInfinity(value) && value >= 0; } /// diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 1e1a62f4a4..6deddef0d0 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -377,8 +377,8 @@ namespace Avalonia.Controls /// dependency property. public static readonly StyledProperty MinimumPrefixLengthProperty = AvaloniaProperty.Register( - nameof(MinimumPrefixLength), 1/*, - validate: ValidateMinimumPrefixLength*/); + nameof(MinimumPrefixLength), 1, + validate: IsValidMinimumPrefixLength); /// /// Identifies the @@ -391,8 +391,8 @@ namespace Avalonia.Controls public static readonly StyledProperty MinimumPopulateDelayProperty = AvaloniaProperty.Register( nameof(MinimumPopulateDelay), - TimeSpan.Zero/*, - validate: ValidateMinimumPopulateDelay*/); + TimeSpan.Zero, + validate: IsValidMinimumPopulateDelay); /// /// Identifies the @@ -405,8 +405,8 @@ namespace Avalonia.Controls public static readonly StyledProperty MaxDropDownHeightProperty = AvaloniaProperty.Register( nameof(MaxDropDownHeight), - double.PositiveInfinity/*, - validate: ValidateMaxDropDownHeight*/); + double.PositiveInfinity, + validate: IsValidMaxDropDownHeight); /// /// Identifies the @@ -494,8 +494,8 @@ namespace Avalonia.Controls public static readonly StyledProperty FilterModeProperty = AvaloniaProperty.Register( nameof(FilterMode), - defaultValue: AutoCompleteFilterMode.StartsWith/*, - validate: ValidateFilterMode*/); + defaultValue: AutoCompleteFilterMode.StartsWith, + validate: IsValidFilterMode); /// /// Identifies the @@ -546,26 +546,11 @@ namespace Avalonia.Controls o => o.AsyncPopulator, (o, v) => o.AsyncPopulator = v); - private static int ValidateMinimumPrefixLength(AutoCompleteBox control, int value) - { - Contract.Requires(value >= -1); - - return value; - } - - private static TimeSpan ValidateMinimumPopulateDelay(AutoCompleteBox control, TimeSpan value) - { - Contract.Requires(value.TotalMilliseconds >= 0.0); - - return value; - } + private static bool IsValidMinimumPrefixLength(int value) => value >= -1; - private static double ValidateMaxDropDownHeight(AutoCompleteBox control, double value) - { - Contract.Requires(value >= 0.0); + private static bool IsValidMinimumPopulateDelay(TimeSpan value) => value.TotalMilliseconds >= 0.0; - return value; - } + private static bool IsValidMaxDropDownHeight(double value) => value >= 0.0; private static bool IsValidFilterMode(AutoCompleteFilterMode mode) { @@ -590,12 +575,6 @@ namespace Avalonia.Controls return false; } } - private static AutoCompleteFilterMode ValidateFilterMode(AutoCompleteBox control, AutoCompleteFilterMode value) - { - Contract.Requires(IsValidFilterMode(value)); - - return value; - } /// /// Handle the change of the IsEnabled property. diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 58b7d7cb47..94f8ad41a0 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -351,8 +351,9 @@ namespace Avalonia.Controls public static readonly StyledProperty DisplayModeProperty = AvaloniaProperty.Register( - nameof(DisplayMode)/*, - validate: ValidateDisplayMode*/); + nameof(DisplayMode), + validate: IsValidDisplayMode); + /// /// Gets or sets a value indicating whether the calendar is displayed in /// months, years, or decades. @@ -417,17 +418,6 @@ namespace Avalonia.Controls } OnDisplayModeChanged(new CalendarModeChangedEventArgs((CalendarMode)e.OldValue, mode)); } - private static CalendarMode ValidateDisplayMode(Calendar o, CalendarMode mode) - { - if(IsValidDisplayMode(mode)) - { - return mode; - } - else - { - throw new ArgumentOutOfRangeException(nameof(mode), "Invalid DisplayMode"); - } - } private static bool IsValidDisplayMode(CalendarMode mode) { return mode == CalendarMode.Month diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index aa3a8fae3d..b4d4fed9fc 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -189,14 +189,14 @@ namespace Avalonia.Controls public static readonly StyledProperty SelectedDateFormatProperty = AvaloniaProperty.Register( nameof(SelectedDateFormat), - defaultValue: DatePickerFormat.Short/*, - validate: ValidateSelectedDateFormat*/); + defaultValue: DatePickerFormat.Short, + validate: IsValidSelectedDateFormat); public static readonly StyledProperty CustomDateFormatStringProperty = AvaloniaProperty.Register( nameof(CustomDateFormatString), - defaultValue: "d"/*, - validate: ValidateDateFormatString*/); + defaultValue: "d", + validate: IsValidDateFormatString); public static readonly DirectProperty TextProperty = AvaloniaProperty.RegisterDirect( @@ -1146,27 +1146,9 @@ namespace Avalonia.Controls || value == DatePickerFormat.Short || value == DatePickerFormat.Custom; } - private static DatePickerFormat ValidateSelectedDateFormat(DatePicker dp, DatePickerFormat format) + private static bool IsValidDateFormatString(string formatString) { - if(IsValidSelectedDateFormat(format)) - { - return format; - } - else - { - throw new ArgumentOutOfRangeException(nameof(format), "DatePickerFormat value is not valid."); - } - } - private static string ValidateDateFormatString(DatePicker dp, string formatString) - { - if(string.IsNullOrWhiteSpace(formatString)) - { - throw new ArgumentException("DateFormatString value is not valid.", nameof(formatString)); - } - else - { - return formatString; - } + return !string.IsNullOrWhiteSpace(formatString); } private static DateTime DiscardDayTime(DateTime d) { diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index eae6cf5e30..6f121afbef 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -356,7 +356,7 @@ namespace Avalonia.Controls /// b) contains only letters, digits and underscore ('_'). /// c) does not start with a digit. /// - private static string SharedSizeGroupPropertyValueValid(Control _, string value) + private static bool SharedSizeGroupPropertyValueValid(string value) { Contract.Requires(value != null); @@ -380,11 +380,11 @@ namespace Avalonia.Controls if (i == id.Length) { - return value; + return true; } } - throw new ArgumentException("Invalid SharedSizeGroup string."); + return false; } /// @@ -750,8 +750,8 @@ namespace Avalonia.Controls /// public static readonly AttachedProperty SharedSizeGroupProperty = AvaloniaProperty.RegisterAttached( - "SharedSizeGroup"/*, - validate: SharedSizeGroupPropertyValueValid*/); + "SharedSizeGroup", + validate: SharedSizeGroupPropertyValueValid); /// /// Static ctor. Used for static registration of properties. diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 7b57288e76..1781067abb 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -2740,12 +2740,8 @@ namespace Avalonia.Controls public static readonly AttachedProperty ColumnProperty = AvaloniaProperty.RegisterAttached( "Column", - defaultValue: 0/*, - validate: (_, v) => - { - if (v >= 0) return v; - else throw new ArgumentException("Invalid Grid.Column value."); - }*/); + defaultValue: 0, + validate: v => v >= 0); /// /// Row property. This is an attached property. @@ -2761,12 +2757,8 @@ namespace Avalonia.Controls public static readonly AttachedProperty RowProperty = AvaloniaProperty.RegisterAttached( "Row", - defaultValue: 0/*, - validate: (_, v) => - { - if (v >= 0) return v; - else throw new ArgumentException("Invalid Grid.Row value."); - }*/); + defaultValue: 0, + validate: v => v >= 0); /// /// ColumnSpan property. This is an attached property. @@ -2781,12 +2773,8 @@ namespace Avalonia.Controls public static readonly AttachedProperty ColumnSpanProperty = AvaloniaProperty.RegisterAttached( "ColumnSpan", - defaultValue: 1/*, - validate: (_, v) => - { - if (v >= 1) return v; - else throw new ArgumentException("Invalid Grid.ColumnSpan value."); - }*/); + defaultValue: 1, + validate: v => v >= 0); /// /// RowSpan property. This is an attached property. @@ -2801,12 +2789,8 @@ namespace Avalonia.Controls public static readonly AttachedProperty RowSpanProperty = AvaloniaProperty.RegisterAttached( "RowSpan", - defaultValue: 1/*, - validate: (_, v) => - { - if (v >= 1) return v; - else throw new ArgumentException("Invalid Grid.RowSpan value."); - }*/); + defaultValue: 1, + validate: v => v >= 0); /// /// IsSharedSizeScope property marks scoping element for shared size. diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs new file mode 100644 index 0000000000..70de7b449f --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -0,0 +1,82 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Subjects; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_Validation + { + [Fact] + public void Registration_Throws_If_DefaultValue_Fails_Validation() + { + Assert.Throws(() => + new StyledProperty( + "BadDefault", + typeof(Class1), + new StyledPropertyMetadata(101), + validate: Class1.ValidateFoo)); + } + + [Fact] + public void Metadata_Override_Throws_If_DefaultValue_Fails_Validation() + { + Assert.Throws(() => Class1.FooProperty.OverrideDefaultValue(101)); + } + + [Fact] + public void SetValue_Throws_If_Fails_Validation() + { + var target = new Class1(); + + Assert.Throws(() => target.SetValue(Class1.FooProperty, 101)); + } + + [Fact] + public void Reverts_To_DefaultValue_If_Binding_Fails_Validation() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.FooProperty, source); + source.OnNext(150); + + Assert.Equal(11, target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Reverts_To_DefaultValue_Even_In_Presence_Of_Other_Bindings() + { + var target = new Class1(); + var source1 = new Subject(); + var source2 = new Subject(); + + target.Bind(Class1.FooProperty, source1); + target.Bind(Class1.FooProperty, source2); + source1.OnNext(42); + source2.OnNext(150); + + Assert.Equal(11, target.GetValue(Class1.FooProperty)); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register( + "Qux", + defaultValue: 11, + validate: ValidateFoo); + + public static bool ValidateFoo(int value) + { + return value < 100; + } + } + + private class Class2 : AvaloniaObject + { + } + } +} From 0f04f4d01afc489d30d7bbbb71a095951e8c7095 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 24 Nov 2019 11:03:49 +0100 Subject: [PATCH 0539/1791] Add WPF-style property coercion. --- src/Avalonia.Base/AvaloniaObject.cs | 10 ++ src/Avalonia.Base/AvaloniaProperty.cs | 7 +- src/Avalonia.Base/IAvaloniaObject.cs | 7 + .../PropertyStore/PriorityValue.cs | 14 ++ src/Avalonia.Base/StyledPropertyBase.cs | 21 +++ src/Avalonia.Base/StyledPropertyMetadata`1.cs | 17 ++- src/Avalonia.Base/ValueStore.cs | 27 ++++ .../AvaloniaObjectTests_Coercion.cs | 139 ++++++++++++++++++ 8 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index e6d54727b3..5bddf95616 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -504,6 +504,16 @@ namespace Avalonia return new DirectBindingSubscription(this, property, source); } + /// + /// Coerces the specified . + /// + /// The type of the property. + /// The property. + public void CoerceValue(StyledPropertyBase property) + { + _values?.CoerceValue(property); + } + /// void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child) { diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 31a7f21a1b..8e5716a5bf 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -257,7 +257,8 @@ namespace Avalonia /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. - /// A value validation callback. + /// A value validation callback. + /// A value coercion callback. /// /// A method that gets called before and after the property starts being notified on an /// object; the bool argument will be true before and false afterwards. This callback is @@ -270,6 +271,7 @@ namespace Avalonia bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, Func validate = null, + Func coerce = null, Action notifying = null) where TOwner : IAvaloniaObject { @@ -277,7 +279,8 @@ namespace Avalonia var metadata = new StyledPropertyMetadata( defaultValue, - defaultBindingMode: defaultBindingMode); + defaultBindingMode: defaultBindingMode, + coerce: coerce); var result = new StyledProperty( name, diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index 4fec3d71af..1ccdaa8f0b 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -106,6 +106,13 @@ namespace Avalonia DirectPropertyBase property, IObservable> source); + /// + /// Coerces the specified . + /// + /// The type of the property. + /// The property. + void CoerceValue(StyledPropertyBase property); + /// /// Registers an object as an inheritance child. /// diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 1a4b616bd9..60baf9d405 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -11,6 +11,7 @@ namespace Avalonia.PropertyStore private readonly IAvaloniaObject _owner; private readonly IValueSink _sink; private readonly List> _entries = new List>(); + private readonly Func? _coerceValue; private Optional _localValue; public PriorityValue( @@ -21,6 +22,12 @@ namespace Avalonia.PropertyStore _owner = owner; Property = property; _sink = sink; + + if (property.HasCoercion) + { + var metadata = property.GetMetadata(owner.GetType()); + _coerceValue = metadata.CoerceValue; + } } public PriorityValue( @@ -83,6 +90,8 @@ namespace Avalonia.PropertyStore return binding; } + public void CoerceValue() => UpdateEffectiveValue(); + void IValueSink.ValueChanged( StyledPropertyBase property, BindingPriority priority, @@ -156,6 +165,11 @@ namespace Avalonia.PropertyStore ValuePriority = BindingPriority.LocalValue; } + if (value.HasValue && _coerceValue != null) + { + value = _coerceValue(_owner, value.Value); + } + if (value != Value) { var old = Value; diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 8e7bfd6467..8c4d683ae0 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -43,6 +43,7 @@ namespace Avalonia _inherits = inherits; ValidateValue = validate; + HasCoercion |= metadata.CoerceValue != null; if (validate?.Invoke(metadata.DefaultValue) == false) { @@ -75,6 +76,24 @@ namespace Avalonia /// public Func ValidateValue { get; } + /// + /// Gets a value indicating whether this property has any value coercion callbacks defined + /// in its metadata. + /// + internal bool HasCoercion { get; private set; } + + public TValue CoerceValue(IAvaloniaObject instance, TValue baseValue) + { + var metadata = GetMetadata(instance.GetType()); + + if (metadata.CoerceValue != null) + { + return metadata.CoerceValue.Invoke(instance, baseValue); + } + + return baseValue; + } + /// /// Gets the default value for the property on the specified type. /// @@ -145,6 +164,8 @@ namespace Avalonia } } + HasCoercion |= metadata.CoerceValue != null; + base.OverrideMetadata(type, metadata); } diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index 50236e2c7c..bd6c125776 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -19,18 +19,26 @@ namespace Avalonia /// /// The default value of the property. /// The default binding mode. + /// A value coercion callback. public StyledPropertyMetadata( Optional defaultValue = default, - BindingMode defaultBindingMode = BindingMode.Default) + BindingMode defaultBindingMode = BindingMode.Default, + Func coerce = null) : base(defaultBindingMode) { _defaultValue = defaultValue; + CoerceValue = coerce; } /// /// Gets the default value for the property. /// - internal TValue DefaultValue => _defaultValue.ValueOrDefault(); + public TValue DefaultValue => _defaultValue.ValueOrDefault(); + + /// + /// Gets the value coercion callback, if any. + /// + public Func? CoerceValue { get; private set; } object IStyledPropertyMetadata.DefaultValue => DefaultValue; @@ -45,6 +53,11 @@ namespace Avalonia { _defaultValue = src.DefaultValue; } + + if (CoerceValue == null) + { + CoerceValue = src.CoerceValue; + } } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 09961b399b..dec1f62386 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -75,6 +75,13 @@ namespace Avalonia { SetExisting(slot, property, value, priority); } + else if (property.HasCoercion) + { + // If the property has any coercion callbacks then always create a PriorityValue. + var entry = new PriorityValue(_owner, property, this); + _values.AddValue(property, entry); + entry.SetValue(value, priority); + } else if (priority == BindingPriority.LocalValue) { _values.AddValue(property, new LocalValueEntry(value)); @@ -97,6 +104,15 @@ namespace Avalonia { return BindExisting(slot, property, source, priority); } + else if (property.HasCoercion) + { + // If the property has any coercion callbacks then always create a PriorityValue. + var entry = new PriorityValue(_owner, property, this); + var binding = entry.AddBinding(source, priority); + _values.AddValue(property, entry); + binding.Start(); + return binding; + } else { var entry = new BindingEntry(_owner, property, source, priority, this); @@ -134,6 +150,17 @@ namespace Avalonia } } + public void CoerceValue(StyledPropertyBase property) + { + if (_values.TryGetValue(property, out var slot)) + { + if (slot is PriorityValue p) + { + p.CoerceValue(); + } + } + } + public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property) { if (_values.TryGetValue(property, out var slot)) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs new file mode 100644 index 0000000000..8d8dbb03a2 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -0,0 +1,139 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Subjects; +using Avalonia.Data; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_Coercion + { + [Fact] + public void Coerces_Set_Value() + { + var target = new Class1(); + + target.Foo = 150; + + Assert.Equal(100, target.Foo); + } + + [Fact] + public void Coerces_Bound_Value() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext(150); + + Assert.Equal(100, target.Foo); + } + + [Fact] + public void CoerceValue_Updates_Value() + { + var target = new Class1 { Foo = 99 }; + + Assert.Equal(99, target.Foo); + + target.MaxFoo = 50; + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(50, target.Foo); + } + + [Fact] + public void Coerced_Value_Can_Be_Restored_If_Limit_Changed() + { + var target = new Class1(); + + target.Foo = 150; + Assert.Equal(100, target.Foo); + + target.MaxFoo = 200; + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(150, target.Foo); + } + + [Fact] + public void Coerced_Value_Can_Be_Restored_From_Previously_Active_Binding() + { + var target = new Class1(); + var source1 = new Subject>(); + var source2 = new Subject>(); + + target.Bind(Class1.FooProperty, source1); + source1.OnNext(150); + + target.Bind(Class1.FooProperty, source2); + source2.OnNext(160); + + Assert.Equal(100, target.Foo); + + target.MaxFoo = 200; + source2.OnCompleted(); + + Assert.Equal(150, target.Foo); + } + + [Fact] + public void Coercion_Can_Be_Overridden() + { + var target = new Class2(); + + target.Foo = 150; + + Assert.Equal(-150, target.Foo); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register( + "Qux", + defaultValue: 11, + coerce: CoerceFoo); + + public int Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + public int MaxFoo { get; set; } = 100; + + public static int CoerceFoo(IAvaloniaObject instance, int value) + { + return Math.Min(((Class1)instance).MaxFoo, value); + } + } + + private class Class2 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + Class1.FooProperty.AddOwner(); + + static Class2() + { + FooProperty.OverrideMetadata( + new StyledPropertyMetadata( + coerce: CoerceFoo)); + } + + public int Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + public static int CoerceFoo(IAvaloniaObject instance, int value) + { + return -value; + } + } + } +} From 1616e76735444fc5a186a419d4794a1e9ef161cf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 24 Nov 2019 11:28:56 +0100 Subject: [PATCH 0540/1791] Added benchmarks for property validation/coercion. --- .../Base/StyledPropertyBenchmark.cs | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs index 4b57776759..b70ae19275 100644 --- a/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs @@ -84,18 +84,64 @@ namespace Avalonia.Benchmarks.Base } } - class StyledClass : AvaloniaObject + [Benchmark] + public void Set_Validated_Int_Property_LocalValue() + { + var obj = new StyledClass(); + + for (var i = 0; i < 100; ++i) + { + obj.ValidatedIntValue += 1; + } + } + + [Benchmark] + public void Set_Coerced_Int_Property_LocalValue() { - private int _intValue; + var obj = new StyledClass(); + for (var i = 0; i < 100; ++i) + { + obj.CoercedIntValue += 1; + } + } + + class StyledClass : AvaloniaObject + { public static readonly StyledProperty IntValueProperty = AvaloniaProperty.Register(nameof(IntValue)); + public static readonly StyledProperty ValidatedIntValueProperty = + AvaloniaProperty.Register(nameof(ValidatedIntValue), validate: ValidateIntValue); + public static readonly StyledProperty CoercedIntValueProperty = + AvaloniaProperty.Register(nameof(CoercedIntValue), coerce: CoerceIntValue); public int IntValue { get => GetValue(IntValueProperty); set => SetValue(IntValueProperty, value); } + + public int ValidatedIntValue + { + get => GetValue(ValidatedIntValueProperty); + set => SetValue(ValidatedIntValueProperty, value); + } + + public int CoercedIntValue + { + get => GetValue(CoercedIntValueProperty); + set => SetValue(CoercedIntValueProperty, value); + } + + private static bool ValidateIntValue(int arg) + { + return arg < 1000; + } + + private static int CoerceIntValue(IAvaloniaObject arg1, int arg2) + { + return Math.Min(1000, arg2); + } } } } From 40657425c139723bbb3226ae2d29cfb2286350f2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 24 Nov 2019 21:58:18 +0100 Subject: [PATCH 0541/1791] Reintroduce coercion to NumericUpDown. --- .../NumericUpDown/NumericUpDown.cs | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 9bc97e3758..cbb5b667e7 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -58,7 +58,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IncrementProperty = - AvaloniaProperty.Register(nameof(Increment), 1.0d/*, validate: OnCoerceIncrement*/); + AvaloniaProperty.Register(nameof(Increment), 1.0d, coerce: OnCoerceIncrement); /// /// Defines the property. @@ -70,13 +70,13 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty MaximumProperty = - AvaloniaProperty.Register(nameof(Maximum), double.MaxValue/*, validate: OnCoerceMaximum*/); + AvaloniaProperty.Register(nameof(Maximum), double.MaxValue, coerce: OnCoerceMaximum); /// /// Defines the property. /// public static readonly StyledProperty MinimumProperty = - AvaloniaProperty.Register(nameof(Minimum), double.MinValue/*, validate: OnCoerceMinimum*/); + AvaloniaProperty.Register(nameof(Minimum), double.MinValue, coerce: OnCoerceMinimum); /// /// Defines the property. @@ -738,19 +738,34 @@ namespace Avalonia.Controls } } - private static double OnCoerceMaximum(NumericUpDown upDown, double value) + private static double OnCoerceMaximum(IAvaloniaObject instance, double value) { - return upDown.OnCoerceMaximum(value); + if (instance is NumericUpDown upDown) + { + return upDown.OnCoerceMaximum(value); + } + + return value; } - private static double OnCoerceMinimum(NumericUpDown upDown, double value) + private static double OnCoerceMinimum(IAvaloniaObject instance, double value) { - return upDown.OnCoerceMinimum(value); + if (instance is NumericUpDown upDown) + { + return upDown.OnCoerceMinimum(value); + } + + return value; } - private static double OnCoerceIncrement(NumericUpDown upDown, double value) + private static double OnCoerceIncrement(IAvaloniaObject instance, double value) { - return upDown.OnCoerceIncrement(value); + if (instance is NumericUpDown upDown) + { + return upDown.OnCoerceIncrement(value); + } + + return value; } private void TextBoxOnTextChanged() From 6a74ab560c96078aa15e268900df342c47d6f50d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 25 Nov 2019 10:05:45 +0100 Subject: [PATCH 0542/1791] Added IDataContextProvider. So that non-`IStyledElement`s can take part in binding. Apply the interface to `Application`. --- src/Avalonia.Controls/Application.cs | 22 +++++++++++++++++++- src/Avalonia.Styling/IDataContextProvider.cs | 13 ++++++++++++ src/Avalonia.Styling/IStyledElement.cs | 8 ++----- src/Avalonia.Styling/StyledElement.cs | 2 +- src/Markup/Avalonia.Markup/Data/Binding.cs | 4 ++-- 5 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 src/Avalonia.Styling/IDataContextProvider.cs diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 59c6c47ed9..9df92a49bc 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -32,7 +32,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode { /// /// The application-global data templates. @@ -45,6 +45,13 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; + /// + /// Defines the property. + /// + public static readonly StyledProperty DataContextProperty = + AvaloniaProperty.Register( + nameof(DataContext)); + /// public event EventHandler ResourcesChanged; @@ -56,6 +63,19 @@ namespace Avalonia Name = "Avalonia Application"; } + /// + /// Gets or sets the Applications's data context. + /// + /// + /// The data context property specifies the default object that will + /// be used for data binding. + /// + public object DataContext + { + get { return GetValue(DataContextProperty); } + set { SetValue(DataContextProperty, value); } + } + /// /// Gets the current instance of the class. /// diff --git a/src/Avalonia.Styling/IDataContextProvider.cs b/src/Avalonia.Styling/IDataContextProvider.cs new file mode 100644 index 0000000000..31639c5784 --- /dev/null +++ b/src/Avalonia.Styling/IDataContextProvider.cs @@ -0,0 +1,13 @@ +namespace Avalonia +{ + /// + /// Defines an element with a data context that can be used for binding. + /// + public interface IDataContextProvider : IAvaloniaObject + { + /// + /// Gets or sets the element's data context. + /// + object DataContext { get; set; } + } +} diff --git a/src/Avalonia.Styling/IStyledElement.cs b/src/Avalonia.Styling/IStyledElement.cs index bcf1898c4c..d4d0f179c3 100644 --- a/src/Avalonia.Styling/IStyledElement.cs +++ b/src/Avalonia.Styling/IStyledElement.cs @@ -10,7 +10,8 @@ namespace Avalonia IStyleHost, ILogical, IResourceProvider, - IResourceNode + IResourceNode, + IDataContextProvider { /// /// Occurs when the control has finished initialization. @@ -27,11 +28,6 @@ namespace Avalonia /// new Classes Classes { get; set; } - /// - /// Gets or sets the control's data context. - /// - object DataContext { get; set; } - /// /// Gets the control's logical parent. /// diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index de8093c048..cf3c4dc855 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -24,7 +24,7 @@ namespace Avalonia /// - Implements to form part of a logical tree. /// - A collection of class strings for custom styling. /// - public class StyledElement : Animatable, IStyledElement, ISetLogicalParent, ISetInheritanceParent + public class StyledElement : Animatable, IDataContextProvider, IStyledElement, ISetLogicalParent, ISetInheritanceParent { /// /// Defines the property. diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 61d0f7c83b..b4545f792e 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -231,9 +231,9 @@ namespace Avalonia.Data { Contract.Requires(target != null); - if (!(target is IStyledElement)) + if (!(target is IDataContextProvider)) { - target = anchor as IStyledElement; + target = anchor as IDataContextProvider; if (target == null) { From 7b11d7674aecbd31e8c15bee1c2715ffd798105e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 09:28:42 +0000 Subject: [PATCH 0543/1791] implement IDataContextProvider on NativeMenuItem --- src/Avalonia.Controls/NativeMenuItemBase.cs | 24 ++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/NativeMenuItemBase.cs b/src/Avalonia.Controls/NativeMenuItemBase.cs index 47eb86cdc3..fa2ecd609a 100644 --- a/src/Avalonia.Controls/NativeMenuItemBase.cs +++ b/src/Avalonia.Controls/NativeMenuItemBase.cs @@ -2,7 +2,7 @@ namespace Avalonia.Controls { - public class NativeMenuItemBase : AvaloniaObject + public class NativeMenuItemBase : AvaloniaObject, IDataContextProvider { private NativeMenu _parent; @@ -11,6 +11,7 @@ namespace Avalonia.Controls } + public static readonly DirectProperty ParentProperty = AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); @@ -19,5 +20,26 @@ namespace Avalonia.Controls get => _parent; set => SetAndRaise(ParentProperty, ref _parent, value); } + + /// + /// Defines the property. + /// + public static readonly StyledProperty DataContextProperty = + AvaloniaProperty.Register( + nameof(DataContext)); + + /// + /// Gets or sets the controls's data context. + /// + /// + /// The data context property + /// specifies the default object that will + /// be used for data binding. + /// + public object DataContext + { + get { return GetValue(DataContextProperty); } + set { SetValue(DataContextProperty, value); } + } } } From 7eea7f65c65fca0b7f4e35b3b8a518e31afc687b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 10:07:18 +0000 Subject: [PATCH 0544/1791] dont implement datacontext on menuitems --- src/Avalonia.Controls/NativeMenuItemBase.cs | 24 +-------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/NativeMenuItemBase.cs b/src/Avalonia.Controls/NativeMenuItemBase.cs index fa2ecd609a..47eb86cdc3 100644 --- a/src/Avalonia.Controls/NativeMenuItemBase.cs +++ b/src/Avalonia.Controls/NativeMenuItemBase.cs @@ -2,7 +2,7 @@ namespace Avalonia.Controls { - public class NativeMenuItemBase : AvaloniaObject, IDataContextProvider + public class NativeMenuItemBase : AvaloniaObject { private NativeMenu _parent; @@ -11,7 +11,6 @@ namespace Avalonia.Controls } - public static readonly DirectProperty ParentProperty = AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); @@ -20,26 +19,5 @@ namespace Avalonia.Controls get => _parent; set => SetAndRaise(ParentProperty, ref _parent, value); } - - /// - /// Defines the property. - /// - public static readonly StyledProperty DataContextProperty = - AvaloniaProperty.Register( - nameof(DataContext)); - - /// - /// Gets or sets the controls's data context. - /// - /// - /// The data context property - /// specifies the default object that will - /// be used for data binding. - /// - public object DataContext - { - get { return GetValue(DataContextProperty); } - set { SetValue(DataContextProperty, value); } - } } } From eb6f3e620d802b495f0120204cc4b9c3590a9a4a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 10:07:45 +0000 Subject: [PATCH 0545/1791] GetDefaultAnchor will looks for IDataContextProviders --- .../MarkupExtensions/BindingExtension.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index a466714136..7532e01f55 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -52,6 +52,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions // the context. object anchor = context.GetFirstParent(); + if(anchor is null) + { + anchor = context.GetFirstParent(); + } + // If a control was not found, then try to find the highest-level style as the XAML // file could be a XAML file containing only styles. return anchor ?? From 2ffb1af3bc0fbbcd6f666e4cf62d63111e9f03ef Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 10:08:55 +0000 Subject: [PATCH 0546/1791] add comment --- .../Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 7532e01f55..20f68df820 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -54,6 +54,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if(anchor is null) { + // Try to find IDataContextProvider, this was added to allow us to find + // a datacontext for Application class when using NativeMenuItems. anchor = context.GetFirstParent(); } From 055115415c4aaad19f755aa525e778014444ec67 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 12:11:29 +0000 Subject: [PATCH 0547/1791] Application class uses AddOwner instead of making its own version of datacontext property. --- src/Avalonia.Controls/Application.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 9df92a49bc..9158ac7038 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -49,8 +49,7 @@ namespace Avalonia /// Defines the property. /// public static readonly StyledProperty DataContextProperty = - AvaloniaProperty.Register( - nameof(DataContext)); + StyledElement.DataContextProperty.AddOwner(); /// public event EventHandler ResourcesChanged; From c37562971734e5bbad55055ec47410235c48e3ad Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 12:31:12 +0000 Subject: [PATCH 0548/1791] restore app.xaml /cs --- samples/ControlCatalog/App.xaml | 5 ----- samples/ControlCatalog/App.xaml.cs | 6 ------ 2 files changed, 11 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 813a043b01..e40509dfda 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -17,9 +17,4 @@ - - - - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index b713c3dfd6..ea5ae0a75b 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -12,12 +12,6 @@ namespace ControlCatalog { public App() { - DataContext = this; - - AboutCommand = ReactiveCommand.Create(() => - { - - }); } public override void Initialize() From 79527e6ab9e195437fb533e1e12f2ab435682e45 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 25 Nov 2019 12:35:15 +0000 Subject: [PATCH 0549/1791] restore app.xaml.cs --- samples/ControlCatalog/App.xaml.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index ea5ae0a75b..52a9591c94 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -1,33 +1,23 @@ -using System; -using System.Reactive; using Avalonia; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; -using ReactiveUI; namespace ControlCatalog { public class App : Application { - public App() - { - } - public override void Initialize() { AvaloniaXamlLoader.Load(this); } - public ReactiveCommand AboutCommand { get; } - public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) desktopLifetime.MainWindow = new MainWindow(); else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) singleViewLifetime.MainView = new MainView(); - + base.OnFrameworkInitializationCompleted(); } } From 73e1224ac33de0e100bbbd3be8333569b4553777 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 25 Nov 2019 21:55:26 +0800 Subject: [PATCH 0550/1791] Ignore changes if isnt a valid double value. --- src/Avalonia.Controls/Primitives/RangeBase.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index 39d00b6953..7155cceb8b 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -77,9 +77,9 @@ namespace Avalonia.Controls.Primitives { if (!ValidateDouble(value)) { - value = _minimum; + return; } - + if (IsInitialized) { SetAndRaise(MinimumProperty, ref _minimum, value); @@ -107,7 +107,7 @@ namespace Avalonia.Controls.Primitives { if (!ValidateDouble(value)) { - value = _maximum; + return; } if (IsInitialized) @@ -137,7 +137,7 @@ namespace Avalonia.Controls.Primitives { if (!ValidateDouble(value)) { - value = default; + return; } if (IsInitialized) @@ -178,7 +178,7 @@ namespace Avalonia.Controls.Primitives /// The value. private static bool ValidateDouble(double value) { - return (!double.IsInfinity(value) || !double.IsNaN(value)); + return !double.IsInfinity(value) || !double.IsNaN(value); } /// From f569287998ee37b90f5604683c11dd5704765e0e Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 25 Nov 2019 21:56:19 +0800 Subject: [PATCH 0551/1791] Update xml comment. --- src/Avalonia.Controls/Primitives/RangeBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index 7155cceb8b..baa51f92ec 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -173,7 +173,7 @@ namespace Avalonia.Controls.Primitives } /// - /// Throws an exception if the double value is NaN or Inf. + /// Checks if the double value is not inifinity nor NaN. /// /// The value. private static bool ValidateDouble(double value) From e23da2873111e7fabdf12c2e56abddca6d180aa0 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 25 Nov 2019 23:13:01 +0800 Subject: [PATCH 0552/1791] Remove obsolete unit test. --- .../Primitives/RangeBaseTests.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs index e2eb628512..34e6b228d0 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs @@ -82,22 +82,6 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(50, target.Value); } - [Fact] - public void Properties_Should_Not_Accept_Nan_And_Inifinity() - { - var target = new TestRange(); - - Assert.Throws(() => target.Minimum = double.NaN); - Assert.Throws(() => target.Minimum = double.PositiveInfinity); - Assert.Throws(() => target.Minimum = double.NegativeInfinity); - Assert.Throws(() => target.Maximum = double.NaN); - Assert.Throws(() => target.Maximum = double.PositiveInfinity); - Assert.Throws(() => target.Maximum = double.NegativeInfinity); - Assert.Throws(() => target.Value = double.NaN); - Assert.Throws(() => target.Value = double.PositiveInfinity); - Assert.Throws(() => target.Value = double.NegativeInfinity); - } - [Theory] [InlineData(true)] [InlineData(false)] From 1d4a7923f1799934df70175d252881cf316a637f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 25 Nov 2019 22:31:22 +0100 Subject: [PATCH 0553/1791] Remove OperationsPerInvoke here. It was incorrect: invoking the method doesn't create 1000 buttons. --- .../Base/AvaloniaObjectInitializationBenchmark.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs index 06716f7102..d2e38850f2 100644 --- a/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs @@ -6,7 +6,7 @@ namespace Avalonia.Benchmarks.Base [MemoryDiagnoser] public class AvaloniaObjectInitializationBenchmark { - [Benchmark(OperationsPerInvoke = 1000)] + [Benchmark] public Button InitializeButton() { return new Button(); From 35bd69f23c24878f7534160b1bd6b6b7d4a12612 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 25 Nov 2019 23:01:27 +0100 Subject: [PATCH 0554/1791] Remove AvaloniaProperty.Initialized. It was only needed for pseudoclasses. Move pseudoclass registration to an engine that doesn't require `AvaloniaProperty.Initialized`, implemented in `PseudoclassEngine`. --- src/Avalonia.Base/AvaloniaObject.cs | 1 - src/Avalonia.Base/AvaloniaProperty.cs | 39 ------ src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 45 ------- src/Avalonia.Base/DirectPropertyBase.cs | 15 --- src/Avalonia.Base/StyledPropertyBase.cs | 15 --- src/Avalonia.Styling/PseudoclassEngine.cs | 120 ++++++++++++++++++ src/Avalonia.Styling/StyledElement.cs | 19 +-- .../AvaloniaObjectTests_Attached.cs | 17 --- .../AvaloniaObjectTests_Direct.cs | 16 --- .../AvaloniaPropertyTests.cs | 23 ---- .../DirectPropertyTests.cs | 22 ---- 11 files changed, 126 insertions(+), 206 deletions(-) create mode 100644 src/Avalonia.Styling/PseudoclassEngine.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 5bddf95616..e427c64945 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -34,7 +34,6 @@ namespace Avalonia public AvaloniaObject() { VerifyAccess(); - AvaloniaPropertyRegistry.Instance.NotifyInitialized(this); } /// diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 8e5716a5bf..cd9b40f94a 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -22,7 +22,6 @@ namespace Avalonia public static readonly object UnsetValue = new UnsetValueType(); private static int s_nextId; - private readonly Subject _initialized; private readonly Subject _changed; private readonly PropertyMetadata _defaultMetadata; private readonly Dictionary _metadata; @@ -55,7 +54,6 @@ namespace Avalonia throw new ArgumentException("'name' may not contain periods."); } - _initialized = new Subject(); _changed = new Subject(); _metadata = new Dictionary(); @@ -83,7 +81,6 @@ namespace Avalonia Contract.Requires(source != null); Contract.Requires(ownerType != null); - _initialized = source._initialized; _changed = source._changed; _metadata = new Dictionary(); @@ -138,22 +135,6 @@ namespace Avalonia /// public virtual bool IsReadOnly => false; - /// - /// Gets an observable that is fired when this property is initialized on a - /// new instance. - /// - /// - /// This observable is fired each time a new is constructed - /// for all properties registered on the object's type. The default value of the property - /// for the object is passed in the args' NewValue (OldValue will always be - /// . - /// - /// - /// An observable that is fired when this property is initialized on a new - /// instance. - /// - public IObservable Initialized => _initialized; - /// /// Gets an observable that is fired when this property changes on any /// instance. @@ -482,26 +463,6 @@ namespace Avalonia return Name; } - /// - /// True if has any observers. - /// - internal bool HasNotifyInitializedObservers => _initialized.HasObservers; - - /// - /// Notifies the observable. - /// - /// The object being initialized. - internal abstract void NotifyInitialized(IAvaloniaObject o); - - /// - /// Notifies the observable. - /// - /// The observable arguments. - internal void NotifyInitialized(AvaloniaPropertyChangedEventArgs e) - { - _initialized.OnNext(e); - } - /// /// Notifies the observable. /// diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 0734b64721..50ffb51550 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -407,51 +407,6 @@ namespace Avalonia _inheritedCache.Clear(); } - internal void NotifyInitialized(AvaloniaObject o) - { - Contract.Requires(o != null); - - var type = o.GetType(); - - if (!_initializedCache.TryGetValue(type, out var initializationData)) - { - var visited = new HashSet(); - - initializationData = new List(); - - foreach (AvaloniaProperty property in GetRegistered(type)) - { - if (property.IsDirect) - { - initializationData.Add(new PropertyInitializationData(property, (IDirectPropertyAccessor)property)); - } - else - { - initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type)); - } - - visited.Add(property); - } - - foreach (AvaloniaProperty property in GetRegisteredAttached(type)) - { - if (!visited.Contains(property)) - { - initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type)); - - visited.Add(property); - } - } - - _initializedCache.Add(type, initializationData); - } - - foreach (PropertyInitializationData data in initializationData) - { - data.Property.NotifyInitialized(o); - } - } - private readonly struct PropertyInitializationData { public AvaloniaProperty Property { get; } diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 7a0be065eb..39ed3b084f 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -101,21 +101,6 @@ namespace Avalonia return (DirectPropertyMetadata)base.GetMetadata(type); } - /// - internal override void NotifyInitialized(IAvaloniaObject o) - { - if (HasNotifyInitializedObservers) - { - var e = new AvaloniaPropertyChangedEventArgs( - o, - this, - default, - InvokeGetter(o), - BindingPriority.Unset); - NotifyInitialized(e); - } - } - /// internal override void RouteClearValue(IAvaloniaObject o) { diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 8c4d683ae0..d1f961a567 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -181,21 +181,6 @@ namespace Avalonia /// object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); - /// - internal override void NotifyInitialized(IAvaloniaObject o) - { - if (HasNotifyInitializedObservers) - { - var e = new AvaloniaPropertyChangedEventArgs( - o, - this, - default, - o.GetValue(this), - BindingPriority.Unset); - NotifyInitialized(e); - } - } - /// internal override void RouteClearValue(IAvaloniaObject o) { diff --git a/src/Avalonia.Styling/PseudoclassEngine.cs b/src/Avalonia.Styling/PseudoclassEngine.cs new file mode 100644 index 0000000000..6c8f8a832a --- /dev/null +++ b/src/Avalonia.Styling/PseudoclassEngine.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; + +namespace Avalonia +{ + internal static class PseudoclassEngine + { + private static Dictionary> _entries = new Dictionary>(); + private static Dictionary> _properties = new Dictionary>(); + private static Dictionary> _cache = new Dictionary>(); + + public static void Created(StyledElement element) + { + var t = element.GetType(); + + if (!_cache.TryGetValue(t, out var list)) + { + list = new List(); + + foreach (var i in _entries) + { + if (i.Key.IsAssignableFrom(t)) + { + list.AddRange(i.Value); + } + } + + _cache.Add(t, list); + } + + foreach (var i in list) + { + i.Update(element); + } + } + + public static void Register( + Type type, + AvaloniaProperty property, + Func selector, + string className) + { + if (!_entries.TryGetValue(type, out var list)) + { + list = new List(); + _entries.Add(type, list); + } + + var entry = new Entry(type, property, selector, className); + list.Add(entry); + + if (!_properties.TryGetValue(property, out list)) + { + list = new List(); + _properties.Add(property, list); + property.Changed.Subscribe(ValueChanged); + } + + list.Add(entry); + } + + private static void ValueChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is StyledElement element && + _properties.TryGetValue(e.Property, out var list)) + { + var t = e.Sender.GetType(); + + foreach (var i in list) + { + if (i.Type.IsAssignableFrom(t)) + { + i.Update(element); + } + } + } + } + + private interface IEntry + { + Type Type { get; } + void Update(StyledElement element); + } + + private class Entry : IEntry + { + public Entry( + Type type, + AvaloniaProperty property, + Func selector, + string className) + { + Type = type; + Property = property; + Selector = selector; + ClassName = className; + } + + public Type Type { get; } + public AvaloniaProperty Property { get; } + public Func Selector { get; } + public string ClassName { get; } + + public void Update(StyledElement element) + { + var value = element.GetValue(Property); + + if (Selector(value)) + { + ((IPseudoClasses)element.Classes).Add(ClassName); + } + else + { + ((IPseudoClasses)element.Classes).Remove(ClassName); + } + } + } + } +} diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index cc7da2b436..90e1259ce6 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -82,6 +82,7 @@ namespace Avalonia public StyledElement() { _isAttachedToLogicalTree = this is IStyleRoot; + PseudoclassEngine.Created(this); } /// @@ -557,19 +558,11 @@ namespace Avalonia throw new ArgumentException("Cannot supply an empty className."); } - property.Changed.Merge(property.Initialized) - .Where(e => e.Sender is TOwner) - .Subscribe(e => - { - if (selector((TProperty)e.NewValue)) - { - ((StyledElement)e.Sender).PseudoClasses.Add(className); - } - else - { - ((StyledElement)e.Sender).PseudoClasses.Remove(className); - } - }); + PseudoclassEngine.Register( + typeof(TOwner), + property, + selector, + className); } protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs index 44e2976e03..474ef9abec 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs @@ -16,19 +16,6 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foodefault", target.GetValue(Class2.FooProperty)); } - [Fact] - public void AvaloniaProperty_Initialized_Is_Called_For_Attached_Property() - { - bool raised = false; - - using (Class1.FooProperty.Initialized.Subscribe(x => raised = true)) - { - new Class3(); - } - - Assert.True(raised); - } - private class Base : AvaloniaObject { } @@ -46,9 +33,5 @@ namespace Avalonia.Base.UnitTests public static readonly AttachedProperty FooProperty = Class1.FooProperty.AddOwner(); } - - private class Class3 : Base - { - } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 4110c3771f..b1a5b5ae92 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -424,22 +424,6 @@ namespace Avalonia.Base.UnitTests Assert.Equal("second", target.Foo); } - [Fact] - public void Property_Notifies_Initialized() - { - bool raised = false; - - Class1.FooProperty.Initialized.Subscribe(e => - raised = e.Property == Class1.FooProperty && - e.OldValue == AvaloniaProperty.UnsetValue && - (string)e.NewValue == "initial" && - e.Priority == BindingPriority.Unset); - - var target = new Class1(); - - Assert.True(raised); - } - [Fact] public void Binding_Error_Reverts_To_Default_Value() { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 90b8bcff63..6bb8dfe1f5 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -78,24 +78,6 @@ namespace Avalonia.Base.UnitTests Assert.Equal(BindingMode.TwoWay, result.DefaultBindingMode); } - [Fact] - public void Initialized_Observable_Fired() - { - bool invoked = false; - - Class1.FooProperty.Initialized.Subscribe(x => - { - Assert.Equal(AvaloniaProperty.UnsetValue, x.OldValue); - Assert.Equal("default", x.NewValue); - Assert.Equal(BindingPriority.Unset, x.Priority); - invoked = true; - }); - - var target = new Class1(); - - Assert.True(invoked); - } - [Fact] public void Changed_Observable_Fired() { @@ -141,11 +123,6 @@ namespace Avalonia.Base.UnitTests OverrideMetadata(typeof(T), metadata); } - internal override void NotifyInitialized(IAvaloniaObject o) - { - throw new NotImplementedException(); - } - internal override IDisposable RouteBind( IAvaloniaObject o, IObservable> source, diff --git a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs index c2a8b03f15..8f62dc9136 100644 --- a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs @@ -1,33 +1,12 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Reactive.Subjects; -using Avalonia.Data; using Xunit; namespace Avalonia.Base.UnitTests { public class DirectPropertyTests { - [Fact] - public void Initialized_Observable_Fired() - { - bool invoked = false; - - Class1.FooProperty.Initialized.Subscribe(x => - { - Assert.Equal(AvaloniaProperty.UnsetValue, x.OldValue); - Assert.Equal("foo", x.NewValue); - Assert.Equal(BindingPriority.Unset, x.Priority); - invoked = true; - }); - - var target = new Class1(); - - Assert.True(invoked); - } - [Fact] public void IsDirect_Property_Returns_True() { @@ -69,7 +48,6 @@ namespace Avalonia.Base.UnitTests var p2 = p1.AddOwner(o => null, (o, v) => { }); Assert.Same(p1.Changed, p2.Changed); - Assert.Same(p1.Initialized, p2.Initialized); } private class Class1 : AvaloniaObject From b89305d708ac67a9c31270bfda8e6028d6527e3f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 Nov 2019 01:11:10 +0100 Subject: [PATCH 0555/1791] Set pseudoclasses manually. It makes creating new controls a _lot_ faster. --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 8 +- src/Avalonia.Controls/Button.cs | 25 +++- src/Avalonia.Controls/ButtonSpinner.cs | 28 +++- src/Avalonia.Controls/ControlExtensions.cs | 20 --- src/Avalonia.Controls/Expander.cs | 41 ++++-- .../WindowNotificationManager.cs | 30 ++++- src/Avalonia.Controls/Primitives/ScrollBar.cs | 24 +++- .../Primitives/ToggleButton.cs | 21 ++- src/Avalonia.Controls/Primitives/Track.cs | 28 +++- src/Avalonia.Controls/ProgressBar.cs | 44 ++++++- src/Avalonia.Controls/Slider.cs | 24 +++- src/Avalonia.Input/InputElement.cs | 42 +++++- .../Controls/PseudoClassesExtensions.cs | 28 ++++ src/Avalonia.Styling/PseudoclassEngine.cs | 120 ------------------ src/Avalonia.Styling/StyledElement.cs | 70 ---------- 15 files changed, 302 insertions(+), 251 deletions(-) create mode 100644 src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs delete mode 100644 src/Avalonia.Styling/PseudoclassEngine.cs diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 86133d5fdb..a2b8544787 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -373,7 +373,11 @@ namespace Avalonia.Controls public bool IsValid { get { return _isValid; } - internal set { SetAndRaise(IsValidProperty, ref _isValid, value); } + internal set + { + SetAndRaise(IsValidProperty, ref _isValid, value); + PseudoClasses.Set(":invalid", !value); + } } public static readonly StyledProperty MaxColumnWidthProperty = @@ -656,8 +660,6 @@ namespace Avalonia.Controls HorizontalScrollBarVisibilityProperty, VerticalScrollBarVisibilityProperty); - PseudoClass(IsValidProperty, x => !x, ":invalid"); - ItemsProperty.Changed.AddClassHandler((x, e) => x.OnItemsPropertyChanged(e)); CanUserResizeColumnsProperty.Changed.AddClassHandler((x, e) => x.OnCanUserResizeColumnsChanged(e)); ColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnColumnWidthChanged(e)); diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 2e115463ac..1e0fa01cc3 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -91,7 +91,11 @@ namespace Avalonia.Controls CommandProperty.Changed.Subscribe(CommandChanged); IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); IsCancelProperty.Changed.Subscribe(IsCancelChanged); - PseudoClass