From 95af64a8d055531126724c4da55d669191ce6b38 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 27 Dec 2018 20:16:50 +0300 Subject: [PATCH 001/129] [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 1707496b3f6adb5fa1c28f7edc89315b4ec3fdb7 Mon Sep 17 00:00:00 2001 From: Ivan Kochurkin Date: Sat, 29 Jun 2019 16:49:05 +0300 Subject: [PATCH 002/129] Add `Add`, `Remove` buttons and `SelectionMode` combobox to ListBoxPage.xaml --- samples/ControlCatalog/Pages/ListBoxPage.xaml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 4783c8cfb8..e7c81a28d4 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -9,7 +9,20 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - + + + + + + + + + Single + Multiple + Toggle + AlwaysSelected + + From c3f0841142cd253c6b97ae31f9fbf965b472b9e7 Mon Sep 17 00:00:00 2001 From: Ivan Kochurkin Date: Sat, 29 Jun 2019 16:50:14 +0300 Subject: [PATCH 003/129] Implement functionality for added controls in ListBoxPage.xaml.cs --- .../ControlCatalog/Pages/ListBoxPage.xaml.cs | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs index dbe6c74800..e1a615af4b 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ReactiveUI; namespace ControlCatalog.Pages { @@ -11,9 +11,8 @@ namespace ControlCatalog.Pages { public ListBoxPage() { - this.InitializeComponent(); - DataContext = Enumerable.Range(1, 10).Select(i => $"Item {i}" ) - .ToArray(); + InitializeComponent(); + DataContext = new PageViewModel(this.Find("listBox")); } private void InitializeComponent() @@ -21,5 +20,46 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } + private class PageViewModel : ReactiveObject + { + private readonly ListBox _listBox; + private int _counter; + private SelectionMode _selectionMode; + + public PageViewModel(ListBox listBox) + { + _listBox = listBox; + + Items = new ObservableCollection(Enumerable.Range(1, 10).Select(i => GenerateItem())); + + AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); + + RemoveItemCommand = ReactiveCommand.Create(() => + { + foreach (string selectedItem in listBox.SelectedItems) + { + Items.Remove(selectedItem); + } + }); + } + + public ObservableCollection Items { get; } + + public ReactiveCommand AddItemCommand { get; } + + public ReactiveCommand RemoveItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + _listBox.SelectedItems.Clear(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + + private string GenerateItem() => $"Item {_counter++}"; + } } } From 9e874529c6ef747105b76f1ed66f50b3579efcce Mon Sep 17 00:00:00 2001 From: Ivan Kochurkin Date: Sat, 29 Jun 2019 18:40:51 +0300 Subject: [PATCH 004/129] Add `Add`, `Remove` buttons and `SelectionMode` combobox to TreeViewPage.xaml --- .../ControlCatalog/Pages/TreeViewPage.xaml | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index c03edb8b03..1b01a38c60 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -6,16 +6,29 @@ Displays a hierachical tree of data. - - - - - - - + Margin="0,16,0,0" + HorizontalAlignment="Center" + Spacing="16"> + + + + + + + + + + + + + + + Single + Multiple + Toggle + AlwaysSelected + + From 6f58bb0e392081739453ac2fef67c93552b5fa55 Mon Sep 17 00:00:00 2001 From: Ivan Kochurkin Date: Sat, 29 Jun 2019 18:41:29 +0300 Subject: [PATCH 005/129] Implement functionality for added controls in TreeViewPage.xaml.cs --- .../ControlCatalog/Pages/ListBoxPage.xaml.cs | 2 +- .../ControlCatalog/Pages/TreeViewPage.xaml.cs | 93 +++++++++++++++++-- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs index e1a615af4b..d6d48cd030 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs @@ -36,7 +36,7 @@ namespace ControlCatalog.Pages RemoveItemCommand = ReactiveCommand.Create(() => { - foreach (string selectedItem in listBox.SelectedItems) + foreach (string selectedItem in _listBox.SelectedItems) { Items.Remove(selectedItem); } diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs index a83f9cf43f..cf6aa50db7 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs @@ -1,8 +1,9 @@ -using System.Collections; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ReactiveUI; namespace ControlCatalog.Pages { @@ -10,8 +11,8 @@ namespace ControlCatalog.Pages { public TreeViewPage() { - this.InitializeComponent(); - DataContext = new Node().Children; + InitializeComponent(); + DataContext = new PageViewModel(this.Find("treeView")); } private void InitializeComponent() @@ -19,22 +20,96 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } - public class Node + private class PageViewModel : ReactiveObject { - private IList _children; + private readonly TreeView _treeView; + private SelectionMode _selectionMode; + + public PageViewModel(TreeView treeView) + { + _treeView = treeView; + + Node root = new Node(); + Items = root.Children; + + AddItemCommand = ReactiveCommand.Create(() => + { + Node selectedItem = _treeView.SelectedItems.Count > 0 ? (Node)_treeView.SelectedItems[0] : root; + selectedItem.AddNewItem(); + }); + + RemoveItemCommand = ReactiveCommand.Create(() => + { + foreach (Node selectedItem in _treeView.SelectedItems) + { + RecursiveRemove(Items, selectedItem); + } + + _treeView.SelectedItems.Clear(); + + bool RecursiveRemove(ObservableCollection items, Node selectedItem) + { + if (items.Remove(selectedItem)) + { + return true; + } + + foreach (Node item in items) + { + if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) + { + return true; + } + } + + return false; + } + }); + } + + public ObservableCollection Items { get; } + + public ReactiveCommand AddItemCommand { get; } + + public ReactiveCommand RemoveItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + _treeView.SelectedItems.Clear(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + } + + private class Node + { + private int _counter; + private ObservableCollection _children; + public string Header { get; private set; } - public IList Children + + public bool AreChildrenInitialized => _children != null; + + public ObservableCollection Children { get { if (_children == null) { - _children = Enumerable.Range(1, 10).Select(i => new Node() {Header = $"Item {i}"}) - .ToArray(); + _children = new ObservableCollection(Enumerable.Range(1, 10).Select(i => CreateNewNode())); } return _children; } } + + public void AddNewItem() => Children.Add(CreateNewNode()); + + public override string ToString() => Header; + + private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"}; } } } From df6068604b69270f14c785465a7a703e9da3ed73 Mon Sep 17 00:00:00 2001 From: Ivan Kochurkin Date: Sat, 29 Jun 2019 23:10:13 +0300 Subject: [PATCH 006/129] Fix @Gillibald review notes: get rid of passing control to viewmodel, use binding instead --- samples/ControlCatalog/Pages/ListBoxPage.xaml | 2 +- .../ControlCatalog/Pages/ListBoxPage.xaml.cs | 16 ++++++------- .../ControlCatalog/Pages/TreeViewPage.xaml | 2 +- .../ControlCatalog/Pages/TreeViewPage.xaml.cs | 24 +++++++++---------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index e7c81a28d4..49e9aafc4a 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -10,7 +10,7 @@ HorizontalAlignment="Center" Spacing="16"> - + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs index d6d48cd030..8a67766c76 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs @@ -12,7 +12,7 @@ namespace ControlCatalog.Pages public ListBoxPage() { InitializeComponent(); - DataContext = new PageViewModel(this.Find("listBox")); + DataContext = new PageViewModel(); } private void InitializeComponent() @@ -22,29 +22,29 @@ namespace ControlCatalog.Pages private class PageViewModel : ReactiveObject { - private readonly ListBox _listBox; private int _counter; private SelectionMode _selectionMode; - public PageViewModel(ListBox listBox) + public PageViewModel() { - _listBox = listBox; - Items = new ObservableCollection(Enumerable.Range(1, 10).Select(i => GenerateItem())); + SelectedItems = new ObservableCollection(); AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); RemoveItemCommand = ReactiveCommand.Create(() => { - foreach (string selectedItem in _listBox.SelectedItems) + while (SelectedItems.Count > 0) { - Items.Remove(selectedItem); + Items.Remove(SelectedItems[0]); } }); } public ObservableCollection Items { get; } + public ObservableCollection SelectedItems { get; } + public ReactiveCommand AddItemCommand { get; } public ReactiveCommand RemoveItemCommand { get; } @@ -54,7 +54,7 @@ namespace ControlCatalog.Pages get => _selectionMode; set { - _listBox.SelectedItems.Clear(); + SelectedItems.Clear(); this.RaiseAndSetIfChanged(ref _selectionMode, value); } } diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index 1b01a38c60..3a81e2ed02 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -10,7 +10,7 @@ HorizontalAlignment="Center" Spacing="16"> - + diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs index cf6aa50db7..1f35f05f1d 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs @@ -12,7 +12,7 @@ namespace ControlCatalog.Pages public TreeViewPage() { InitializeComponent(); - DataContext = new PageViewModel(this.Find("treeView")); + DataContext = new PageViewModel(); } private void InitializeComponent() @@ -22,31 +22,29 @@ namespace ControlCatalog.Pages private class PageViewModel : ReactiveObject { - private readonly TreeView _treeView; private SelectionMode _selectionMode; - public PageViewModel(TreeView treeView) + public PageViewModel() { - _treeView = treeView; - Node root = new Node(); Items = root.Children; + SelectedItems = new ObservableCollection(); AddItemCommand = ReactiveCommand.Create(() => { - Node selectedItem = _treeView.SelectedItems.Count > 0 ? (Node)_treeView.SelectedItems[0] : root; - selectedItem.AddNewItem(); + Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root; + parentItem.AddNewItem(); }); RemoveItemCommand = ReactiveCommand.Create(() => { - foreach (Node selectedItem in _treeView.SelectedItems) + while (SelectedItems.Count > 0) { - RecursiveRemove(Items, selectedItem); + Node lastItem = SelectedItems[0]; + RecursiveRemove(Items, lastItem); + SelectedItems.Remove(lastItem); } - _treeView.SelectedItems.Clear(); - bool RecursiveRemove(ObservableCollection items, Node selectedItem) { if (items.Remove(selectedItem)) @@ -69,6 +67,8 @@ namespace ControlCatalog.Pages public ObservableCollection Items { get; } + public ObservableCollection SelectedItems { get; } + public ReactiveCommand AddItemCommand { get; } public ReactiveCommand RemoveItemCommand { get; } @@ -78,7 +78,7 @@ namespace ControlCatalog.Pages get => _selectionMode; set { - _treeView.SelectedItems.Clear(); + SelectedItems.Clear(); this.RaiseAndSetIfChanged(ref _selectionMode, value); } } From d52d3417aea2fb281c91ad3d2907f0fa7c2475a7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 6 Jul 2019 21:09:51 +0300 Subject: [PATCH 007/129] Backported eb07c4b - jitter fix --- .../ControlCatalog.NetCore.csproj | 1 + samples/ControlCatalog.NetCore/Program.cs | 7 ++++ src/Avalonia.OpenGL/EglDisplay.cs | 1 + src/Avalonia.OpenGL/EglGlPlatformSurface.cs | 33 +++++++++++++++---- src/Avalonia.OpenGL/EglInterface.cs | 12 +++++++ .../IGlPlatformSurfaceRenderTarget.cs | 7 +++- .../Platform/IRenderTarget.cs | 5 +++ .../Rendering/DeferredRenderer.cs | 5 +++ .../Rendering/ManagedDeferredRendererLock.cs | 14 ++++++++ .../Rendering/UiThreadRenderTimer.cs | 32 ++++++++++++++++++ src/Skia/Avalonia.Skia/GlRenderTarget.cs | 2 ++ src/Windows/Avalonia.Win32/WindowImpl.cs | 28 +++++++++++----- 12 files changed, 132 insertions(+), 15 deletions(-) create mode 100644 src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 589f41c06b..a25cbfc696 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -10,6 +10,7 @@ + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index d13a5b5ef3..a364d191db 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using Avalonia; using Avalonia.Skia; +using Avalonia.ReactiveUI; namespace ControlCatalog.NetCore { @@ -45,6 +46,12 @@ namespace ControlCatalog.NetCore public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() + .With(new X11PlatformOptions {EnableMultiTouch = true}) + .With(new Win32PlatformOptions + { + EnableMultitouch = true, + AllowEglInitialization = true + }) .UseSkia() .UseReactiveUI() .UseDataGrid(); diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index b14932acfe..ec445a4605 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -97,6 +97,7 @@ namespace Avalonia.OpenGL public GlDisplayType Type { get; } public GlInterface GlInterface { get; } + public EglInterface EglInterface => _egl; public IGlContext CreateContext(IGlContext share) { var shareCtx = (EglContext)share; diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs index f5dd413b0f..d2e4543af3 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs @@ -26,31 +26,44 @@ namespace Avalonia.OpenGL public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() { var glSurface = _display.CreateWindowSurface(_info.Handle); - return new RenderTarget(_context, glSurface, _info); + return new RenderTarget(_display, _context, glSurface, _info); } - class RenderTarget : IGlPlatformSurfaceRenderTarget + class RenderTarget : IGlPlatformSurfaceRenderTargetWithCorruptionInfo { + private readonly EglDisplay _display; private readonly EglContext _context; private readonly EglSurface _glSurface; private readonly IEglWindowGlPlatformSurfaceInfo _info; + private PixelSize _initialSize; - public RenderTarget(EglContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) + public RenderTarget(EglDisplay display, EglContext context, + EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) { + _display = display; _context = context; _glSurface = glSurface; _info = info; + _initialSize = info.Size; } public void Dispose() => _glSurface.Dispose(); + public bool IsCorrupted => _initialSize != _info.Size; + public IGlPlatformSurfaceRenderingSession BeginDraw() { var l = _context.Lock(); try { + if (IsCorrupted) + throw new RenderTargetCorruptedException(); _context.MakeCurrent(_glSurface); - return new Session(_context, _glSurface, _info, l); + _display.EglInterface.WaitClient(); + _display.EglInterface.WaitGL(); + _display.EglInterface.WaitNative(); + + return new Session(_display, _context, _glSurface, _info, l); } catch { @@ -61,15 +74,19 @@ namespace Avalonia.OpenGL class Session : IGlPlatformSurfaceRenderingSession { - private readonly IGlContext _context; + private readonly EglContext _context; private readonly EglSurface _glSurface; private readonly IEglWindowGlPlatformSurfaceInfo _info; + private readonly EglDisplay _display; private IDisposable _lock; + - public Session(IGlContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info, + public Session(EglDisplay display, EglContext context, + EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info, IDisposable @lock) { _context = context; + _display = display; _glSurface = glSurface; _info = info; _lock = @lock; @@ -78,7 +95,11 @@ namespace Avalonia.OpenGL public void Dispose() { _context.Display.GlInterface.Flush(); + _display.EglInterface.WaitGL(); _glSurface.SwapBuffers(); + _display.EglInterface.WaitClient(); + _display.EglInterface.WaitGL(); + _display.EglInterface.WaitNative(); _context.Display.ClearContext(); _lock.Dispose(); } diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index 00fcd97af0..2838e41461 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -91,6 +91,18 @@ namespace Avalonia.OpenGL [GlEntryPoint("eglGetConfigAttrib")] public EglGetConfigAttrib GetConfigAttrib { get; } + public delegate bool EglWaitGL(); + [GlEntryPoint("eglWaitGL")] + public EglWaitGL WaitGL { get; } + + public delegate bool EglWaitClient(); + [GlEntryPoint("eglWaitClient")] + public EglWaitGL WaitClient { get; } + + public delegate bool EglWaitNative(); + [GlEntryPoint("eglWaitNative")] + public EglWaitGL WaitNative { get; } + // ReSharper restore UnassignedGetOnlyAutoProperty } } diff --git a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs index 53da93315c..d198d46e5c 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs +++ b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs @@ -6,4 +6,9 @@ namespace Avalonia.OpenGL { IGlPlatformSurfaceRenderingSession BeginDraw(); } -} \ No newline at end of file + + public interface IGlPlatformSurfaceRenderTargetWithCorruptionInfo : IGlPlatformSurfaceRenderTarget + { + bool IsCorrupted { get; } + } +} diff --git a/src/Avalonia.Visuals/Platform/IRenderTarget.cs b/src/Avalonia.Visuals/Platform/IRenderTarget.cs index 522de64ec7..516bea782e 100644 --- a/src/Avalonia.Visuals/Platform/IRenderTarget.cs +++ b/src/Avalonia.Visuals/Platform/IRenderTarget.cs @@ -23,4 +23,9 @@ namespace Avalonia.Platform /// IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer); } + + public interface IRenderTargetWithCorruptionInfo : IRenderTarget + { + bool IsCorrupted { get; } + } } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 5293e1b978..6f2ffe916d 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -248,6 +248,11 @@ namespace Avalonia.Rendering { if (context != null) return context; + if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) + { + RenderTarget.Dispose(); + RenderTarget = null; + } if (RenderTarget == null) RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); return context = RenderTarget.CreateDrawingContext(this); diff --git a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs index 75d8f036d6..2d4a39e026 100644 --- a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs +++ b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs @@ -7,11 +7,25 @@ namespace Avalonia.Rendering public class ManagedDeferredRendererLock : IDeferredRendererLock { private readonly object _lock = new object(); + + /// + /// Tries to lock the target surface or window + /// + /// IDisposable if succeeded to obtain the lock public IDisposable TryLock() { if (Monitor.TryEnter(_lock)) return Disposable.Create(() => Monitor.Exit(_lock)); return null; } + + /// + /// Enters a waiting lock, only use from platform code, not from the renderer + /// + public IDisposable Lock() + { + Monitor.Enter(_lock); + return Disposable.Create(() => Monitor.Exit(_lock)); + } } } diff --git a/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs b/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs new file mode 100644 index 0000000000..dd6cf7ad15 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs @@ -0,0 +1,32 @@ +using System; +using System.Diagnostics; +using System.Reactive.Disposables; +using Avalonia.Threading; + +namespace Avalonia.Rendering +{ + /// + /// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms + /// + + public class UiThreadRenderTimer : DefaultRenderTimer + { + public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond) + { + } + + protected override IDisposable StartCore(Action tick) + { + bool cancelled = false; + var st = Stopwatch.StartNew(); + DispatcherTimer.Run(() => + { + if (cancelled) + return false; + tick(st.Elapsed); + return !cancelled; + }, TimeSpan.FromSeconds(1.0 / FramesPerSecond), DispatcherPriority.Render); + return Disposable.Create(() => cancelled = true); + } + } +} diff --git a/src/Skia/Avalonia.Skia/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/GlRenderTarget.cs index cd8c334b53..e9e1727dfe 100644 --- a/src/Skia/Avalonia.Skia/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/GlRenderTarget.cs @@ -21,6 +21,8 @@ namespace Avalonia.Skia public void Dispose() => _surface.Dispose(); + public bool IsCorrupted => (_surface as IGlPlatformSurfaceRenderTargetWithCorruptionInfo)?.IsCorrupted == true; + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) { var session = _surface.BeginDraw(); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 081a713e95..11d6c467de 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -31,6 +31,7 @@ namespace Avalonia.Win32 private string _className; private IntPtr _hwnd; private IInputRoot _owner; + private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock(); private bool _trackingMouse; private bool _decorated = true; private bool _resizable = true; @@ -148,7 +149,9 @@ namespace Avalonia.Win32 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); - return Win32Platform.UseDeferredRendering ? (IRenderer)new DeferredRenderer(root, loop) : new ImmediateRenderer(root); + return Win32Platform.UseDeferredRendering ? + (IRenderer)new DeferredRenderer(root, loop, rendererLock: _rendererLock) : + new ImmediateRenderer(root); } public void Resize(Size value) @@ -627,18 +630,26 @@ namespace Avalonia.Win32 break; case UnmanagedMethods.WindowsMessage.WM_PAINT: - UnmanagedMethods.PAINTSTRUCT ps; - if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) + using (_rendererLock.Lock()) { - var f = Scaling; - var r = ps.rcPaint; - Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, (r.bottom - r.top) / f)); - UnmanagedMethods.EndPaint(_hwnd, ref ps); + UnmanagedMethods.PAINTSTRUCT ps; + if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) + { + var f = Scaling; + var r = ps.rcPaint; + Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, + (r.bottom - r.top) / f)); + UnmanagedMethods.EndPaint(_hwnd, ref ps); + } } return IntPtr.Zero; case UnmanagedMethods.WindowsMessage.WM_SIZE: + using (_rendererLock.Lock()) + { + // Do nothing here, just block until the pending frame render is completed on the render thread + } var size = (UnmanagedMethods.SizeCommand)wParam; if (Resized != null && @@ -704,7 +715,8 @@ namespace Avalonia.Win32 } } - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + using (_rendererLock.Lock()) + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); } static InputModifiers GetMouseModifiers(IntPtr wParam) From ce84902ec65f1793f7a811d1eefd941a3579fa9c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 4 Jun 2019 13:17:23 +0300 Subject: [PATCH 008/129] Use Direct3D9 as ANGLE platform type by default --- src/Avalonia.OpenGL/AngleOptions.cs | 18 ++++++++ src/Avalonia.OpenGL/EglDisplay.cs | 70 ++++++++++++++++++++--------- src/Avalonia.OpenGL/EglInterface.cs | 13 ++++++ 3 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 src/Avalonia.OpenGL/AngleOptions.cs diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs new file mode 100644 index 0000000000..4b9c04f4e6 --- /dev/null +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Avalonia.OpenGL +{ + public class AngleOptions + { + public enum PlatformApi + { + DirectX9, + DirectX11 + } + + public List AllowedPlatformApis = new List + { + PlatformApi.DirectX9 + }; + } +} diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index ec445a4605..b2b5a1a646 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using Avalonia.Platform.Interop; using static Avalonia.OpenGL.EglConsts; @@ -13,21 +14,42 @@ namespace Avalonia.OpenGL private readonly int[] _contextAttributes; public IntPtr Handle => _display; + private AngleOptions.PlatformApi? _angleApi; public EglDisplay(EglInterface egl) { _egl = egl; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _egl.GetPlatformDisplayEXT != null) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - foreach (var dapi in new[] {EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE}) + if (_egl.GetPlatformDisplayEXT == null) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); + + var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis + ?? new List {AngleOptions.PlatformApi.DirectX9}; + + foreach (var platformApi in allowedApis) { + int dapi; + if (platformApi == AngleOptions.PlatformApi.DirectX9) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; + else if (platformApi == AngleOptions.PlatformApi.DirectX11) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; + else + continue; + _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE }); - if(_display != IntPtr.Zero) + if (_display != IntPtr.Zero) + { + _angleApi = platformApi; break; + } } + + if (_display == IntPtr.Zero) + throw new OpenGlException("Unable to create ANGLE display"); } if (_display == IntPtr.Zero) @@ -64,29 +86,35 @@ namespace Avalonia.OpenGL if (!_egl.BindApi(cfg.Api)) continue; - var attribs = new[] + foreach(var stencilSize in new[]{8, 1, 0}) + foreach (var depthSize in new []{8, 1, 0}) { - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, - EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_ALPHA_SIZE, 8, - EGL_STENCIL_SIZE, 8, - EGL_DEPTH_SIZE, 8, - EGL_NONE - }; - if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs)) - continue; - if (numConfigs == 0) - continue; - _contextAttributes = cfg.Attributes; - Type = cfg.Type; + var attribs = new[] + { + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + + EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, + + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_STENCIL_SIZE, stencilSize, + EGL_DEPTH_SIZE, depthSize, + EGL_NONE + }; + if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs)) + continue; + if (numConfigs == 0) + continue; + _contextAttributes = cfg.Attributes; + Type = cfg.Type; + } } if (_contextAttributes == null) throw new OpenGlException("No suitable EGL config was found"); - + GlInterface = GlInterface.FromNativeUtf8GetProcAddress(b => _egl.GetProcAddress(b)); } diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index 2838e41461..0e4b012dbe 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -102,6 +102,19 @@ namespace Avalonia.OpenGL public delegate bool EglWaitNative(); [GlEntryPoint("eglWaitNative")] public EglWaitGL WaitNative { get; } + + public delegate IntPtr EglQueryString(IntPtr display, int i); + + [GlEntryPoint("eglQueryString")] + public EglQueryString QueryStringNative { get; } + + public string QueryString(IntPtr display, int i) + { + var rv = QueryStringNative(display, i); + if (rv == IntPtr.Zero) + return null; + return Marshal.PtrToStringAnsi(rv); + } // ReSharper restore UnassignedGetOnlyAutoProperty } From 9b606ab513f4c8eb941c5d108c7ad76a96079f8d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 28 Jun 2019 15:21:05 +0300 Subject: [PATCH 009/129] [X11] Fixed NumLock --- src/Avalonia.X11/X11KeyTransform.cs | 10 +++------- src/Avalonia.X11/X11Window.cs | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.X11/X11KeyTransform.cs b/src/Avalonia.X11/X11KeyTransform.cs index 26495111d1..c68cb04733 100644 --- a/src/Avalonia.X11/X11KeyTransform.cs +++ b/src/Avalonia.X11/X11KeyTransform.cs @@ -221,12 +221,8 @@ namespace Avalonia.X11 //{ X11Key.?, Key.DeadCharProcessed } }; - public static Key ConvertKey(IntPtr key) - { - var ikey = key.ToInt32(); - Key result; - return KeyDic.TryGetValue((X11Key)ikey, out result) ? result : Key.None; - } -} + public static Key ConvertKey(X11Key key) + => KeyDic.TryGetValue(key, out var result) ? result : Key.None; + } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index c5e77fe352..878ed12b66 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -419,10 +419,21 @@ namespace Avalonia.X11 return; var buffer = stackalloc byte[40]; - var latinKeysym = XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, 0); + var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask); + + // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway + var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32(); + + // Manually switch the Shift index for the keypad, + // there should be a proper way to do this + if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask) + && key > X11Key.Num_Lock && key <= X11Key.KP_9) + key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32(); + + ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, - X11KeyTransform.ConvertKey(latinKeysym), TranslateModifiers(ev.KeyEvent.state)), ref ev); + X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev); if (ev.type == XEventName.KeyPress) { From f65fcb6fa413199e02258f086eb098d532364e6d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 6 Jul 2019 21:25:45 +0300 Subject: [PATCH 010/129] [X11] Blacklist llvmpipe --- src/Avalonia.OpenGL/GlInterface.cs | 18 ++++++++++++++---- src/Avalonia.X11/Glx/GlxDisplay.cs | 13 +++++++++++++ src/Avalonia.X11/X11Platform.cs | 8 ++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index 718afc4a94..f556949cfa 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -9,12 +9,14 @@ namespace Avalonia.OpenGL public class GlInterface : GlInterfaceBase { public string Version { get; } + public string Vendor { get; } + public string Renderer { get; } public GlInterface(Func getProcAddress) : base(getProcAddress) { - var versionPtr = GetString(GlConsts.GL_VERSION); - if (versionPtr != IntPtr.Zero) - Version = Marshal.PtrToStringAnsi(versionPtr); + Version = GetString(GlConsts.GL_VERSION); + Renderer = GetString(GlConsts.GL_RENDERER); + Vendor = GetString(GlConsts.GL_VENDOR); } public GlInterface(Func n) : this(ConvertNative(n)) @@ -54,7 +56,15 @@ namespace Avalonia.OpenGL public delegate IntPtr GlGetString(int v); [GlEntryPoint("glGetString")] - public GlGetString GetString { get; } + public GlGetString GetStringNative { get; } + + public string GetString(int v) + { + var ptr = GetStringNative(v); + if (ptr != IntPtr.Zero) + return Marshal.PtrToStringAnsi(ptr); + return null; + } public delegate void GlGetIntegerv(int name, out int rv); [GlEntryPoint("glGetIntegerv")] diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 5602b33280..04f2a7137c 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -90,6 +90,19 @@ namespace Avalonia.X11.Glx GlInterface = new GlInterface(GlxInterface.GlxGetProcAddress); if (GlInterface.Version == null) throw new OpenGlException("GL version string is null, aborting"); + if (GlInterface.Renderer == null) + throw new OpenGlException("GL renderer string is null, aborting"); + + if (Environment.GetEnvironmentVariable("AVALONIA_GLX_IGNORE_RENDERER_BLACKLIST") != "1") + { + var blacklist = AvaloniaLocator.Current.GetService() + ?.GlxRendererBlacklist; + if (blacklist != null) + foreach(var item in blacklist) + if (GlInterface.Renderer.Contains(item)) + throw new OpenGlException($"Renderer '{GlInterface.Renderer}' is blacklisted by '{item}'"); + } + } public void ClearContext() => Glx.MakeContextCurrent(_x11.Display, diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index ce03113169..47d5276fbc 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -95,6 +95,14 @@ namespace Avalonia { public bool UseEGL { get; set; } public bool UseGpu { get; set; } = true; + + public List GlxRendererBlacklist { get; set; } = new List + { + // llvmpipe is a software GL rasterizer. If it's returned by glGetString, + // that usually means that something in the system is horribly misconfigured + // and sometimes attempts to use GLX might cause a segfault + "llvmpipe" + }; public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication"; } public static class AvaloniaX11PlatformExtensions From f3e504ca152922ea1492e28a0be12949c1729316 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 6 Jul 2019 21:33:03 +0300 Subject: [PATCH 011/129] Bump version for 0.8.1 --- build/SharedVersion.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 7ea1dd0c65..16a4b828f4 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> Avalonia - 0.8.0 + 0.8.1 Copyright 2018 © The AvaloniaUI Project https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md https://github.com/AvaloniaUI/Avalonia/ From bc2cc9928c71d7be9fa8e143002a3c5ea164eff0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 6 Jul 2019 21:34:58 +0300 Subject: [PATCH 012/129] Compilation --- samples/ControlCatalog.NetCore/Program.cs | 4 ---- src/Avalonia.Native/Avalonia.Native.csproj | 2 -- src/Avalonia.OpenGL/EglInterface.cs | 1 + 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index a364d191db..c58d071ac8 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -3,8 +3,6 @@ using System.Diagnostics; using System.Linq; using System.Threading; using Avalonia; -using Avalonia.Skia; -using Avalonia.ReactiveUI; namespace ControlCatalog.NetCore { @@ -46,10 +44,8 @@ namespace ControlCatalog.NetCore public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() - .With(new X11PlatformOptions {EnableMultiTouch = true}) .With(new Win32PlatformOptions { - EnableMultitouch = true, AllowEglInitialization = true }) .UseSkia() diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index c8ee73ad5d..9d6a82a2fb 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -7,8 +7,6 @@ /usr/bin/castxml /usr/local/bin/castxml true - - $(MSBuildThisFileDirectory)/Generated diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index 0e4b012dbe..fd1374f3fe 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using Avalonia.Platform; using Avalonia.Platform.Interop; From 80a3d0471a52f0fe1de4ddc92d8f2c159bc432c2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 6 Jul 2019 22:00:08 +0300 Subject: [PATCH 013/129] Azure pipelines update --- azure-pipelines.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 39333f37ba..981b17b0de 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -32,7 +32,7 @@ jobs: - job: macOS pool: - vmImage: 'xcode9-macos10.13' + vmImage: 'macOS-10.14' steps: - task: DotNetCoreInstaller@0 inputs: @@ -49,7 +49,7 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx10.13' + sdk: 'macosx10.14' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' xcodeVersion: 'default' # Options: 8, 9, default, specifyPath @@ -134,3 +134,4 @@ jobs: pathToPublish: '$(Build.SourcesDirectory)/artifacts/zip' artifactName: 'Samples' condition: succeeded() + From 94e2cbbd43a2bb246b0a68c726bd68d8d627cba7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 6 Jul 2019 22:02:46 +0300 Subject: [PATCH 014/129] Updated Build.cs to version from master --- nukebuild/Build.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index bb31034299..84092d52eb 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -122,6 +122,14 @@ partial class Build : NukeBuild foreach(var fw in frameworks) { + if (fw.StartsWith("net4") + && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") + { + Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969"); + continue; + } + Information("Running for " + fw); DotNetTest(c => { From 44803ef9dd17e19fd6b676a08ab3c0b3feabdb9f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 25 Jul 2019 21:41:33 +0100 Subject: [PATCH 015/129] 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 016/129] 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 017/129] 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 018/129] 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 35f64af761bc80e7efbeffd0633c61afb39e95cb Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 27 Jul 2019 10:37:24 +0300 Subject: [PATCH 019/129] Make toplevels responsible of creating popups --- .../Avalonia.Android/AndroidPlatform.cs | 5 --- .../Platform/SkiaPlatform/TopLevelImpl.cs | 2 + .../Offscreen/OffscreenTopLevelImpl.cs | 1 + .../Platform/ITopLevelImpl.cs | 2 + .../Platform/IWindowingPlatform.cs | 1 - .../Platform/PlatformManager.cs | 5 --- src/Avalonia.Controls/Primitives/Popup.cs | 37 +++++++++---------- src/Avalonia.Controls/Primitives/PopupRoot.cs | 8 ++-- src/Avalonia.Controls/ToolTip.cs | 3 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 + src/Avalonia.Native/AvaloniaNativePlatform.cs | 5 --- src/Avalonia.Native/PopupImpl.cs | 6 +++ src/Avalonia.Native/WindowImpl.cs | 5 +++ src/Avalonia.Native/WindowImplBase.cs | 3 +- src/Avalonia.X11/X11Platform.cs | 7 +--- src/Avalonia.X11/X11Window.cs | 10 +++-- .../FramebufferToplevelImpl.cs | 2 + .../Wpf/WpfTopLevelImpl.cs | 2 + src/Windows/Avalonia.Win32/Win32Platform.cs | 5 --- src/iOS/Avalonia.iOS/TopLevelImpl.cs | 2 + .../Primitives/PopupRootTests.cs | 12 +++--- 21 files changed, 63 insertions(+), 62 deletions(-) diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 4e48811c35..c91b58311b 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -71,10 +71,5 @@ namespace Avalonia.Android { throw new NotSupportedException(); } - - public IPopupImpl CreatePopup() - { - return new PopupImpl(); - } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index f42faeaa63..0d0d9db252 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -191,6 +191,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform } } + public IPopupImpl CreatePopup() => null; + ILockedFramebuffer IFramebufferPlatformSurface.Lock()=>new AndroidFramebuffer(_view.Holder.Surface); } } diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 9c53dc0c10..29f0374301 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -61,5 +61,6 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action Closed { get; set; } public abstract IMouseDevice MouseDevice { get; } + public IPopupImpl CreatePopup() => null; } } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 8d8ce35c38..cfbc0b1c4b 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -107,5 +107,7 @@ namespace Avalonia.Platform /// [CanBeNull] IMouseDevice MouseDevice { get; } + + IPopupImpl CreatePopup(); } } diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index 5c2c1a8da3..a55bd63c6a 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -4,6 +4,5 @@ namespace Avalonia.Platform { IWindowImpl CreateWindow(); IEmbeddableWindowImpl CreateEmbeddableWindow(); - IPopupImpl CreatePopup(); } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index fa01b9e839..ef453274b8 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -41,10 +41,5 @@ namespace Avalonia.Controls.Platform throw new Exception("Could not CreateEmbeddableWindow(): IWindowingPlatform is not registered."); return platform.CreateEmbeddableWindow(); } - - public static IPopupImpl CreatePopup() - { - return AvaloniaLocator.Current.GetService().CreatePopup(); - } } } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 058658357f..895094eded 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -218,9 +218,16 @@ namespace Avalonia.Controls.Primitives /// public void Open() { + if (PlacementTarget == null) + throw new InvalidOperationException("It's not valid to show a popup without a PlacementTarget"); + + + if (_topLevel == null && PlacementTarget != null) + _topLevel = PlacementTarget.GetSelfAndLogicalAncestors().First(x => x is TopLevel) as TopLevel; + if (_popupRoot == null) { - _popupRoot = new PopupRoot(DependencyResolver) + _popupRoot = new PopupRoot(_topLevel, DependencyResolver) { [~ContentControl.ContentProperty] = this[~ChildProperty], [~WidthProperty] = this[~WidthProperty], @@ -236,30 +243,22 @@ namespace Avalonia.Controls.Primitives _popupRoot.Position = GetPosition(); - if (_topLevel == null && PlacementTarget != null) + var window = _topLevel as Window; + if (window != null) { - _topLevel = PlacementTarget.GetSelfAndLogicalAncestors().First(x => x is TopLevel) as TopLevel; + window.Deactivated += WindowDeactivated; } - - if (_topLevel != null) + else { - var window = _topLevel as Window; - if (window != null) + var parentPopuproot = _topLevel as PopupRoot; + if (parentPopuproot?.Parent is Popup popup) { - window.Deactivated += WindowDeactivated; + popup.Closed += ParentClosed; } - else - { - var parentPopuproot = _topLevel as PopupRoot; - if (parentPopuproot?.Parent is Popup popup) - { - popup.Closed += ParentClosed; - } - } - _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); - _nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick); } - + _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); + _nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick); + PopupRootCreated?.Invoke(this, EventArgs.Empty); _popupRoot.Show(); diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index d2e8f1ab92..47863932d1 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -31,8 +31,8 @@ namespace Avalonia.Controls.Primitives /// /// Initializes a new instance of the class. /// - public PopupRoot() - : this(null) + public PopupRoot(TopLevel parent) + : this(parent, null) { } @@ -42,8 +42,8 @@ namespace Avalonia.Controls.Primitives /// /// The dependency resolver to use. If null the default dependency resolver will be used. /// - public PopupRoot(IAvaloniaDependencyResolver dependencyResolver) - : base(PlatformManager.CreatePopup(), dependencyResolver) + public PopupRoot(TopLevel parent, IAvaloniaDependencyResolver dependencyResolver) + : base(parent.PlatformImpl.CreatePopup(), dependencyResolver) { } diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 28d1ba5e0f..8c23f4abdc 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -4,6 +4,7 @@ using System; using System.Reactive.Linq; using Avalonia.Controls.Primitives; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -234,7 +235,7 @@ namespace Avalonia.Controls { Close(); - _popup = new PopupRoot { Content = this, }; + _popup = new PopupRoot((TopLevel)control.GetVisualRoot()) {Content = this}; ((ISetLogicalParent)_popup).SetParent(control); _popup.Position = Popup.GetPosition(control, GetPlacement(control), _popup, GetHorizontalOffset(control), GetVerticalOffset(control)); diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 9c547279d6..ddb8b62b6a 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -29,6 +29,8 @@ namespace Avalonia.DesignerSupport.Remote public Func Closing { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice { get; } = new MouseDevice(); + public IPopupImpl CreatePopup() => null; + public PixelPoint Position { get; set; } public Action PositionChanged { get; set; } public WindowState WindowState { get; set; } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index adb27d348d..edde2176bd 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -97,11 +97,6 @@ namespace Avalonia.Native { throw new NotImplementedException(); } - - public IPopupImpl CreatePopup() - { - return new PopupImpl(_factory, _options); - } } public class AvaloniaNativeMacOptions diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index a470caa80e..976208b058 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -9,8 +9,12 @@ namespace Avalonia.Native { public class PopupImpl : WindowBaseImpl, IPopupImpl { + private readonly IAvaloniaNativeFactory _factory; + private readonly AvaloniaNativePlatformOptions _opts; public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts) { + _factory = factory; + _opts = opts; using (var e = new PopupEvents(this)) { Init(factory.CreatePopup(e), factory.CreateScreens()); @@ -35,5 +39,7 @@ namespace Avalonia.Native { } } + + public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts); } } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 076fe9ccae..c7857898d2 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -11,9 +11,13 @@ namespace Avalonia.Native { public class WindowImpl : WindowBaseImpl, IWindowImpl { + private readonly IAvaloniaNativeFactory _factory; + private readonly AvaloniaNativePlatformOptions _opts; IAvnWindow _native; public WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts) { + _factory = factory; + _opts = opts; using (var e = new WindowEvents(this)) { Init(_native = factory.CreateWindow(e), factory.CreateScreens()); @@ -100,5 +104,6 @@ namespace Avalonia.Native } public Func Closing { get; set; } + public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts); } } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 638879ba14..ae0a2f535b 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -15,7 +15,7 @@ using Avalonia.Threading; namespace Avalonia.Native { - public class WindowBaseImpl : IWindowBaseImpl, + public abstract class WindowBaseImpl : IWindowBaseImpl, IFramebufferPlatformSurface { IInputRoot _inputRoot; @@ -91,6 +91,7 @@ namespace Avalonia.Native public Action Resized { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice => AvaloniaNativePlatform.MouseDevice; + public abstract IPopupImpl CreatePopup(); class FramebufferWrapper : ILockedFramebuffer diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 7bdc61eb28..9bdcaab82b 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -74,18 +74,13 @@ namespace Avalonia.X11 public IntPtr Display { get; set; } public IWindowImpl CreateWindow() { - return new X11Window(this, false); + return new X11Window(this, null); } public IEmbeddableWindowImpl CreateEmbeddableWindow() { throw new NotSupportedException(); } - - public IPopupImpl CreatePopup() - { - return new X11Window(this, true); - } } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 18c23aa31e..a1e386892b 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -21,6 +21,7 @@ namespace Avalonia.X11 unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client { private readonly AvaloniaX11Platform _platform; + private readonly IWindowImpl _popupParent; private readonly bool _popup; private readonly X11Info _x11; private bool _invalidated; @@ -47,10 +48,10 @@ namespace Avalonia.X11 private readonly Queue _inputQueue = new Queue(); private InputEventContainer _lastEvent; private bool _useRenderWindow = false; - public X11Window(AvaloniaX11Platform platform, bool popup) + public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent) { _platform = platform; - _popup = popup; + _popup = popupParent != null; _x11 = platform.Info; _mouse = platform.MouseDevice; _keyboard = platform.KeyboardDevice; @@ -66,7 +67,7 @@ namespace Avalonia.X11 | SetWindowValuemask.BackPixmap | SetWindowValuemask.BackingStore | SetWindowValuemask.BitGravity | SetWindowValuemask.WinGravity; - if (popup) + if (_popup) { attr.override_redirect = true; valueMask |= SetWindowValuemask.OverrideRedirect; @@ -793,7 +794,8 @@ namespace Avalonia.X11 } public IMouseDevice MouseDevice => _mouse; - + public IPopupImpl CreatePopup() => new X11Window(_platform, this); + public void Activate() { if (_x11.Atoms._NET_ACTIVE_WINDOW != IntPtr.Zero) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 5e2ba51caf..ebaad81fa1 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -59,6 +59,8 @@ namespace Avalonia.LinuxFramebuffer public Size ClientSize => ScaledSize; public IMouseDevice MouseDevice => new MouseDevice(); + public IPopupImpl CreatePopup() => null; + public double Scaling => 1; public IEnumerable Surfaces => new object[] {_outputBackend}; public Action Input { get; set; } diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index c89d0a15cf..7798452f10 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -240,5 +240,7 @@ namespace Avalonia.Win32.Interop.Wpf return new Vector(1, 1); return new Vector(src.TransformToDevice.M11, src.TransformToDevice.M22); } + + IPopupImpl CreatePopup() => null; } } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index c45bf6389e..56a7e356b6 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -210,11 +210,6 @@ namespace Avalonia.Win32 return embedded; } - public IPopupImpl CreatePopup() - { - return new PopupImpl(); - } - public IWindowIconImpl LoadIcon(string fileName) { using (var stream = File.OpenRead(fileName)) diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs index 15e8b35056..d5f456409f 100644 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs @@ -134,5 +134,7 @@ namespace Avalonia.iOS } public ILockedFramebuffer Lock() => new EmulatedFramebuffer(this); + + public IPopupImpl CreatePopup() => null; } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 059146f17d..44bb7cb69b 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -21,7 +21,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var target = CreateTarget(); + var target = CreateTarget(new Window()); Assert.True(((ILogical)target).IsAttachedToLogicalTree); } @@ -32,7 +32,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var target = CreateTarget(); + var target = CreateTarget(new Window()); Assert.True(target.Presenter.IsAttachedToLogicalTree); } @@ -63,8 +63,8 @@ namespace Avalonia.Controls.UnitTests.Primitives using (UnitTestApplication.Start(TestServices.StyledWindow)) { var child = new Decorator(); - var target = CreateTarget(); var window = new Window(); + var target = CreateTarget(window); var detachedCount = 0; var attachedCount = 0; @@ -88,8 +88,8 @@ namespace Avalonia.Controls.UnitTests.Primitives using (UnitTestApplication.Start(TestServices.StyledWindow)) { var child = new Decorator(); - var target = CreateTarget(); var window = new Window(); + var target = CreateTarget(window); var detachedCount = 0; var attachedCount = 0; @@ -130,9 +130,9 @@ namespace Avalonia.Controls.UnitTests.Primitives } } - private PopupRoot CreateTarget() + private PopupRoot CreateTarget(TopLevel popupParent) { - var result = new PopupRoot + var result = new PopupRoot(popupParent) { Template = new FuncControlTemplate((parent, scope) => new ContentPresenter From 3e786dbadb4df265eb958dba8be06f7e48ad0664 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 27 Jul 2019 11:04:04 +0300 Subject: [PATCH 020/129] PixelVector and stuff --- src/Avalonia.Visuals/Media/PixelPoint.cs | 55 ++++++ src/Avalonia.Visuals/Media/PixelRect.cs | 11 ++ src/Avalonia.Visuals/Media/PixelVector.cs | 205 ++++++++++++++++++++++ 3 files changed, 271 insertions(+) create mode 100644 src/Avalonia.Visuals/Media/PixelVector.cs diff --git a/src/Avalonia.Visuals/Media/PixelPoint.cs b/src/Avalonia.Visuals/Media/PixelPoint.cs index 995781ee9f..1fc102045e 100644 --- a/src/Avalonia.Visuals/Media/PixelPoint.cs +++ b/src/Avalonia.Visuals/Media/PixelPoint.cs @@ -59,6 +59,59 @@ namespace Avalonia { return !(left == right); } + + /// + /// Converts the to a . + /// + /// The point. + public static implicit operator PixelVector(PixelPoint p) + { + return new PixelVector(p.X, p.Y); + } + + /// + /// Adds two points. + /// + /// The first point. + /// The second point. + /// A point that is the result of the addition. + public static PixelPoint operator +(PixelPoint a, PixelPoint b) + { + return new PixelPoint(a.X + b.X, a.Y + b.Y); + } + + /// + /// Adds a vector to a point. + /// + /// The point. + /// The vector. + /// A point that is the result of the addition. + public static PixelPoint operator +(PixelPoint a, PixelVector b) + { + return new PixelPoint(a.X + b.X, a.Y + b.Y); + } + + /// + /// Subtracts two points. + /// + /// The first point. + /// The second point. + /// A point that is the result of the subtraction. + public static PixelPoint operator -(PixelPoint a, PixelPoint b) + { + return new PixelPoint(a.X - b.X, a.Y - b.Y); + } + + /// + /// Subtracts a vector from a point. + /// + /// The point. + /// The vector. + /// A point that is the result of the subtraction. + public static PixelPoint operator -(PixelPoint a, PixelVector b) + { + return new PixelPoint(a.X - b.X, a.Y - b.Y); + } /// /// Parses a string. @@ -106,6 +159,8 @@ namespace Avalonia return hash; } } + + /// /// Returns a new with the same Y co-ordinate and the specified X co-ordinate. diff --git a/src/Avalonia.Visuals/Media/PixelRect.cs b/src/Avalonia.Visuals/Media/PixelRect.cs index 9c8e5ad1c4..75987681ff 100644 --- a/src/Avalonia.Visuals/Media/PixelRect.cs +++ b/src/Avalonia.Visuals/Media/PixelRect.cs @@ -261,6 +261,17 @@ namespace Avalonia { return (rect.X < Right) && (X < rect.Right) && (rect.Y < Bottom) && (Y < rect.Bottom); } + + /// + /// Translates the rectangle by an offset. + /// + /// The offset. + /// The translated rectangle. + public PixelRect Translate(PixelVector offset) + { + return new PixelRect(Position + offset, Size); + } + /// /// Gets the union of two rectangles. diff --git a/src/Avalonia.Visuals/Media/PixelVector.cs b/src/Avalonia.Visuals/Media/PixelVector.cs new file mode 100644 index 0000000000..b959b462c2 --- /dev/null +++ b/src/Avalonia.Visuals/Media/PixelVector.cs @@ -0,0 +1,205 @@ +// 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 JetBrains.Annotations; + +namespace Avalonia +{ + /// + /// Defines a vector. + /// + public readonly struct PixelVector + { + /// + /// The X vector. + /// + private readonly int _x; + + /// + /// The Y vector. + /// + private readonly int _y; + + /// + /// Initializes a new instance of the structure. + /// + /// The X vector. + /// The Y vector. + public PixelVector(int x, int y) + { + _x = x; + _y = y; + } + + /// + /// Gets the X vector. + /// + public int X => _x; + + /// + /// Gets the Y vector. + /// + public int Y => _y; + + /// + /// Converts the to a . + /// + /// The vector. + public static explicit operator PixelPoint(PixelVector a) + { + return new PixelPoint(a._x, a._y); + } + + /// + /// Calculates the dot product of two vectors + /// + /// First vector + /// Second vector + /// The dot product + public static int operator *(PixelVector a, PixelVector b) + { + return a.X * b.X + a.Y * b.Y; + } + + /// + /// Scales a vector. + /// + /// The vector + /// The scaling factor. + /// The scaled vector. + public static PixelVector operator *(PixelVector vector, int scale) + { + return new PixelVector(vector._x * scale, vector._y * scale); + } + + /// + /// Scales a vector. + /// + /// The vector + /// The divisor. + /// The scaled vector. + public static PixelVector operator /(PixelVector vector, int scale) + { + return new PixelVector(vector._x / scale, vector._y / scale); + } + + /// + /// Length of the vector + /// + public double Length => Math.Sqrt(X * X + Y * Y); + + /// + /// Negates a vector. + /// + /// The vector. + /// The negated vector. + public static PixelVector operator -(PixelVector a) + { + return new PixelVector(-a._x, -a._y); + } + + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// A vector that is the result of the addition. + public static PixelVector operator +(PixelVector a, PixelVector b) + { + return new PixelVector(a._x + b._x, a._y + b._y); + } + + /// + /// Subtracts two vectors. + /// + /// The first vector. + /// The second vector. + /// A vector that is the result of the subtraction. + public static PixelVector operator -(PixelVector a, PixelVector b) + { + return new PixelVector(a._x - b._x, a._y - b._y); + } + + /// + /// Check if two vectors are equal (bitwise). + /// + /// + /// + public bool Equals(PixelVector other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return _x == other._x && _y == other._y; + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + /// + /// Check if two vectors are nearly equal (numerically). + /// + /// The other vector. + /// True if vectors are nearly equal. + [Pure] + public bool NearlyEquals(PixelVector other) + { + const float tolerance = float.Epsilon; + + 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 PixelVector vector && Equals(vector); + } + + public override int GetHashCode() + { + unchecked + { + return (_x.GetHashCode() * 397) ^ _y.GetHashCode(); + } + } + + public static bool operator ==(PixelVector left, PixelVector right) + { + return left.Equals(right); + } + + public static bool operator !=(PixelVector left, PixelVector right) + { + return !left.Equals(right); + } + + /// + /// Returns the string representation of the point. + /// + /// The string representation of the point. + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y); + } + + /// + /// Returns a new vector with the specified X coordinate. + /// + /// The X coordinate. + /// The new vector. + public PixelVector WithX(int x) + { + return new PixelVector(x, _y); + } + + /// + /// Returns a new vector with the specified Y coordinate. + /// + /// The Y coordinate. + /// The new vector. + public PixelVector WithY(int y) + { + return new PixelVector(_x, y); + } + } +} From c685a9e3479e450f77252439dad85b9a3f9e00eb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 27 Jul 2019 10:38:39 +0100 Subject: [PATCH 021/129] 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 880a2269fd36df27a42265c796ad7fd62c273b14 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 27 Jul 2019 12:56:29 +0300 Subject: [PATCH 022/129] IPopupPositioner + managed implementation --- .../PopupPositioning/IPopupPositioner.cs | 294 ++++++++++++++++++ .../ManagedPopupPositioner.cs | 174 +++++++++++ .../ManagedPopupPositionerPopupImplHelper.cs | 50 +++ 3 files changed, 518 insertions(+) create mode 100644 src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs create mode 100644 src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs create mode 100644 src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs new file mode 100644 index 0000000000..af78483b7f --- /dev/null +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -0,0 +1,294 @@ +// The documentation and flag names in this file are initially taken from +// xdg_shell wayland protocol this API is designed after +// therefore, I'm including the license from wayland-protocols repo + +/* +Copyright © 2008-2013 Kristian Høgsberg +Copyright © 2010-2013 Intel Corporation +Copyright © 2013 Rafael Antognolli +Copyright © 2013 Jasper St. Pierre +Copyright © 2014 Jonas Ådahl +Copyright © 2014 Jason Ekstrand +Copyright © 2014-2015 Collabora, Ltd. +Copyright © 2015 Red Hat Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +--- + +The above is the version of the MIT "Expat" License used by X.org: + + http://cgit.freedesktop.org/xorg/xserver/tree/COPYING + + +Adjustments for Avalonia needs: +Copyright © 2019 Nikita Tsukanov + + +*/ + +using System; + +namespace Avalonia.Controls.Primitives.PopupPositioning +{ + /// + /// + /// The IPopupPositioner provides a collection of rules for the placement of a + /// a popup relative to its parent. Rules can be defined to ensure + /// the popup remains within the visible area's borders, and to + /// specify how the popup changes its position, such as sliding along + /// an axis, or flipping around a rectangle. These positioner-created rules are + /// constrained by the requirement that a popup must intersect with or + /// be at least partially adjacent to its parent surface. + /// + public struct PopupPositionerParameters + { + private PopupPositioningEdge _gravity; + private PopupPositioningEdge _anchor; + + /// + /// Set the size of the popup that is to be positioned with the positioner + /// object. The size is in scaled coordinates. + /// + public Size Size { get; set; } + + /// + /// Specify the anchor rectangle within the parent that the popup + /// will be placed relative to. The rectangle is relative to the + /// parent geometry + /// + /// The anchor rectangle may not extend outside the window geometry of the + /// popup's parent. The anchor rectangle is in scaled coordinates + /// + public Rect AnchorRectangle { get; set; } + + + /// + /// Defines the anchor point for the anchor rectangle. The specified anchor + /// is used derive an anchor point that the popup will be + /// positioned relative to. If a corner anchor is set (e.g. 'TopLeft' or + /// 'BottomRight'), the anchor point will be at the specified corner; + /// otherwise, the derived anchor point will be centered on the specified + /// edge, or in the center of the anchor rectangle if no edge is specified. + /// + public PopupPositioningEdge Anchor + { + get => _anchor; + set + { + PopupPositioningEdgeHelper.ValidateEdge(value); + _anchor = value; + } + } + + /// + /// Defines in what direction a popup should be positioned, relative to + /// the anchor point of the parent. If a corner gravity is + /// specified (e.g. 'BottomRight' or 'TopLeft'), then the popup + /// will be placed towards the specified gravity; otherwise, the popup + /// will be centered over the anchor point on any axis that had no + /// gravity specified. + /// + public PopupPositioningEdge Gravity + { + get => _gravity; + set + { + PopupPositioningEdgeHelper.ValidateEdge(value); + _gravity = value; + } + } + + /// + /// Specify how the popup should be positioned if the originally intended + /// position caused the popup to be constrained, meaning at least + /// partially outside positioning boundaries set by the positioner. The + /// adjustment is set by constructing a bitmask describing the adjustment to + /// be made when the popup is constrained on that axis. + /// + /// If no bit for one axis is set, the positioner will assume that the child + /// surface should not change its position on that axis when constrained. + /// + /// If more than one bit for one axis is set, the order of how adjustments + /// are applied is specified in the corresponding adjustment descriptions. + /// + /// The default adjustment is none. + /// + public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; } + + /// + /// Specify the popup position offset relative to the position of the + /// anchor on the anchor rectangle and the anchor on the popup. For + /// example if the anchor of the anchor rectangle is at (x, y), the popup + /// has the gravity bottom|right, and the offset is (ox, oy), the calculated + /// surface position will be (x + ox, y + oy). The offset position of the + /// surface is the one used for constraint testing. See + /// set_constraint_adjustment. + /// + /// An example use case is placing a popup menu on top of a user interface + /// element, while aligning the user interface element of the parent surface + /// with some user interface element placed somewhere in the popup. + /// + public Point Offset { get; set; } + } + + /// + /// The constraint adjustment value define ways how popup position will + /// be adjusted if the unadjusted position would result in the popup + /// being partly constrained. + /// + /// Whether a popup is considered 'constrained' is left to the positioner + /// to determine. For example, the popup may be partly outside the + /// target platform defined 'work area', thus necessitating the popup's + /// position be adjusted until it is entirely inside the work area. + /// + [Flags] + public enum PopupPositionerConstraintAdjustment + { + /// + /// Don't alter the surface position even if it is constrained on some + /// axis, for example partially outside the edge of an output. + /// + None = 0, + + /// + /// Slide the surface along the x axis until it is no longer constrained. + /// First try to slide towards the direction of the gravity on the x axis + /// until either the edge in the opposite direction of the gravity is + /// unconstrained or the edge in the direction of the gravity is + /// constrained. + /// + /// Then try to slide towards the opposite direction of the gravity on the + /// x axis until either the edge in the direction of the gravity is + /// unconstrained or the edge in the opposite direction of the gravity is + /// constrained. + /// + SlideX = 1, + + + /// + /// Slide the surface along the y axis until it is no longer constrained. + /// + /// First try to slide towards the direction of the gravity on the y axis + /// until either the edge in the opposite direction of the gravity is + /// unconstrained or the edge in the direction of the gravity is + /// constrained. + /// + /// Then try to slide towards the opposite direction of the gravity on the + /// y axis until either the edge in the direction of the gravity is + /// unconstrained or the edge in the opposite direction of the gravity is + /// constrained. + /// */ + /// + SlideY = 2, + + /// + /// Invert the anchor and gravity on the x axis if the surface is + /// constrained on the x axis. For example, if the left edge of the + /// surface is constrained, the gravity is 'left' and the anchor is + /// 'left', change the gravity to 'right' and the anchor to 'right'. + /// + /// If the adjusted position also ends up being constrained, the resulting + /// position of the flip_x adjustment will be the one before the + /// adjustment. + /// + FlipX = 4, + + /// + /// Invert the anchor and gravity on the y axis if the surface is + /// constrained on the y axis. For example, if the bottom edge of the + /// surface is constrained, the gravity is 'bottom' and the anchor is + /// 'bottom', change the gravity to 'top' and the anchor to 'top'. + /// + /// The adjusted position is calculated given the original anchor + /// rectangle and offset, but with the new flipped anchor and gravity + /// values. + /// + /// If the adjusted position also ends up being constrained, the resulting + /// position of the flip_y adjustment will be the one before the + /// adjustment. + /// + FlipY = 8, + All = SlideX|SlideY|FlipX|FlipY + } + + static class PopupPositioningEdgeHelper + { + public static void ValidateEdge(this PopupPositioningEdge edge) + { + if (((edge & PopupPositioningEdge.Left) != 0 && (edge & PopupPositioningEdge.Right) != 0) + || + ((edge & PopupPositioningEdge.Top) != 0 && (edge & PopupPositioningEdge.Bottom) != 0)) + throw new ArgumentException("Opposite edges specified"); + } + + public static PopupPositioningEdge Flip(this PopupPositioningEdge edge) + { + var hmask = PopupPositioningEdge.Left | PopupPositioningEdge.Right; + var vmask = PopupPositioningEdge.Top | PopupPositioningEdge.Bottom; + if ((edge & hmask) != 0) + edge ^= hmask; + if ((edge & vmask) != 0) + edge ^= vmask; + return edge; + } + + public static PopupPositioningEdge FlipX(this PopupPositioningEdge edge) + { + if ((edge & PopupPositioningEdge.HorizontalMask) != 0) + edge ^= PopupPositioningEdge.HorizontalMask; + return edge; + } + + public static PopupPositioningEdge FlipY(this PopupPositioningEdge edge) + { + if ((edge & PopupPositioningEdge.VerticalMask) != 0) + edge ^= PopupPositioningEdge.VerticalMask; + return edge; + } + + } + + [Flags] + public enum PopupPositioningEdge + { + None, + Top = 1, + Bottom = 2, + Left = 4, + Right = 8, + TopLeft = Top | Left, + TopRight = Top | Right, + BottomLeft = Bottom | Left, + BottomRight = Bottom | Right, + + + VerticalMask = Top | Bottom, + HorizontalMask = Left | Right, + AllMask = VerticalMask|HorizontalMask + } + + public interface IPopupPositioner + { + void Update(PopupPositionerParameters parameters); + } + + +} diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs new file mode 100644 index 0000000000..d9d3c5a61b --- /dev/null +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Controls.Primitives.PopupPositioning +{ + public interface IManagedPopupPositionerPopup + { + IReadOnlyList Screens { get; } + Rect ParentClientAreaScreenGeometry { get; } + void MoveAndResize(Point devicePoint, Size virtualSize); + Point TranslatePoint(Point pt); + Size TranslateSize(Size size); + } + + public class ManagedPopupPositionerScreenInfo + { + public Rect Bounds { get; } + public Rect WorkingArea { get; } + + public ManagedPopupPositionerScreenInfo(Rect bounds, Rect workingArea) + { + Bounds = bounds; + WorkingArea = workingArea; + } + } + + public class ManagedPopupPositioner : IPopupPositioner + { + private readonly IManagedPopupPositionerPopup _popup; + + public ManagedPopupPositioner(IManagedPopupPositionerPopup popup) + { + _popup = popup; + } + + + static Point GetAnchorPoint(Rect anchorRect, PopupPositioningEdge edge) + { + double x, y; + if ((edge & PopupPositioningEdge.Left) != 0) + x = anchorRect.X; + else if ((edge & PopupPositioningEdge.Right) != 0) + x = anchorRect.Right; + else + x = anchorRect.X + anchorRect.Width / 2; + + if ((edge & PopupPositioningEdge.Top) != 0) + y = anchorRect.Y; + else if ((edge & PopupPositioningEdge.Bottom) != 0) + y = anchorRect.Bottom; + else + y = anchorRect.Y + anchorRect.Height / 2; + return new Point(x, y); + } + + static Point Gravitate(Point anchorPoint, Size size, PopupPositioningEdge gravity) + { + double x, y; + if ((gravity & PopupPositioningEdge.Left) != 0) + x = -size.Width; + else if ((gravity & PopupPositioningEdge.Right) != 0) + x = 0; + else + x = -size.Width / 2; + + if ((gravity & PopupPositioningEdge.Top) != 0) + y = -size.Height; + else if ((gravity & PopupPositioningEdge.Bottom) != 0) + y = 0; + else + y = -size.Height / 2; + return anchorPoint + new Point(x, y); + } + + public void Update(PopupPositionerParameters parameters) + { + + Update(_popup.TranslateSize(parameters.Size), + new Rect(_popup.TranslatePoint(parameters.AnchorRectangle.TopLeft), + _popup.TranslateSize(parameters.AnchorRectangle.Size)), + parameters.Anchor, parameters.Gravity, parameters.ConstraintAdjustment, + _popup.TranslatePoint(parameters.Offset)); + } + + + void Update(Size size, Rect anchorRect, PopupPositioningEdge anchor, PopupPositioningEdge gravity, + PopupPositionerConstraintAdjustment constraintAdjustment, Point offset) + { + var parentGeometry = _popup.ParentClientAreaScreenGeometry; + anchorRect = anchorRect.Translate(parentGeometry.TopLeft); + + Rect GetBounds() + { + var screens = _popup.Screens; + + var targetScreen = screens.FirstOrDefault(s => s.Bounds.Contains(anchorRect.TopLeft)) + ?? screens.FirstOrDefault(s => s.Bounds.Intersects(anchorRect)) + ?? screens.FirstOrDefault(s => s.Bounds.Contains(parentGeometry.TopLeft)) + ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry)) + ?? screens.FirstOrDefault(); + return targetScreen?.WorkingArea + ?? new Rect(0, 0, double.MaxValue, double.MaxValue); + } + + var bounds = GetBounds(); + + bool FitsInBounds(Rect rc, PopupPositioningEdge edge = PopupPositioningEdge.AllMask) + { + if ((edge & PopupPositioningEdge.Left) != 0 + && rc.X < bounds.X) + return false; + + if ((edge & PopupPositioningEdge.Top) != 0 + && rc.Y < bounds.Y) + return false; + + if ((edge & PopupPositioningEdge.Right) != 0 + && rc.Right > bounds.Right) + return false; + + if ((edge & PopupPositioningEdge.Bottom) != 0 + && rc.Bottom > bounds.Bottom) + return false; + + return true; + } + + Rect GetUnconstrained(PopupPositioningEdge a, PopupPositioningEdge g) => + new Rect(Gravitate(GetAnchorPoint(anchorRect, a), size, g) + offset, size); + + + var geo = GetUnconstrained(anchor, gravity); + + // If flipping geometry and anchor is allowed and helps, use the flipped one, + // otherwise leave it as is + if (!FitsInBounds(geo, PopupPositioningEdge.HorizontalMask) + && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0) + { + var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX()); + if (FitsInBounds(flipped, PopupPositioningEdge.HorizontalMask)) + geo = geo.WithX(flipped.X); + } + + // If sliding is allowed, try moving the rect into the bounds + if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideX) != 0) + { + geo = geo.WithX(Math.Max(geo.X, bounds.X)); + if (geo.Right > bounds.Right) + geo = geo.WithX(bounds.Right - geo.Width); + } + + // If flipping geometry and anchor is allowed and helps, use the flipped one, + // otherwise leave it as is + if (!FitsInBounds(geo, PopupPositioningEdge.VerticalMask) + && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0) + { + var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY()); + if (FitsInBounds(flipped, PopupPositioningEdge.VerticalMask)) + geo = geo.WithY(flipped.Y); + } + + // If sliding is allowed, try moving the rect into the bounds + if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideY) != 0) + { + geo = geo.WithY(Math.Max(geo.Y, bounds.Y)); + if (geo.Bottom > bounds.Bottom) + geo = geo.WithY(bounds.Bottom - geo.Height); + } + + _popup.MoveAndResize(geo.TopLeft, size); + } + } +} diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs new file mode 100644 index 0000000000..ed1551bba5 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Platform; + +namespace Avalonia.Controls.Primitives.PopupPositioning +{ + /// + /// This class is used to simplify integration of IPopupImpl implementations with popup positioner + /// + public class ManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup + { + private readonly IWindowBaseImpl _parent; + + public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); + private readonly MoveResizeDelegate _moveResize; + + public ManagedPopupPositionerPopupImplHelper(IWindowBaseImpl parent, MoveResizeDelegate moveResize) + { + _parent = parent; + _moveResize = moveResize; + } + + public IReadOnlyList Screens => + + _parent.Screen.AllScreens.Select(s => new ManagedPopupPositionerScreenInfo( + s.Bounds.ToRect(_parent.Scaling), s.WorkingArea.ToRect(_parent.Scaling))).ToList(); + + public Rect ParentClientAreaScreenGeometry + { + get + { + // Popup positioner operates with abstract coordinates, but in our case they are pixel ones + var point = _parent.PointToScreen(default); + var size = PixelSize.FromSize(_parent.ClientSize, _parent.Scaling); + return new Rect(point.X, point.Y, size.Width, size.Height); + + } + } + + public void MoveAndResize(Point devicePoint, Size virtualSize) + { + _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.Scaling); + } + + public Point TranslatePoint(Point pt) => pt * _parent.Scaling; + + public Size TranslateSize(Size size) => size * _parent.Scaling; + } +} From f5c85674591f4e89a6a29da9e433b680b517006f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 27 Jul 2019 10:58:27 +0100 Subject: [PATCH 023/129] 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 024/129] [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 025/129] [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 9343ba4c23f993b023106d7513c2e80d20afda66 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 27 Jul 2019 13:05:19 +0300 Subject: [PATCH 026/129] Wired up the popup positioner Tests are failing because they are trying create popups out of a thin air --- src/Avalonia.Controls/PlacementMode.cs | 19 ++- src/Avalonia.Controls/Platform/IPopupImpl.cs | 4 +- .../Platform/IWindowBaseImpl.cs | 24 +--- src/Avalonia.Controls/Platform/IWindowImpl.cs | 27 +++++ src/Avalonia.Controls/Primitives/Popup.cs | 60 +--------- src/Avalonia.Controls/Primitives/PopupRoot.cs | 111 +++++++++++++----- src/Avalonia.Controls/ToolTip.cs | 6 +- src/Avalonia.Controls/Window.cs | 45 +++++++ src/Avalonia.Controls/WindowBase.cs | 49 +------- .../Remote/PreviewerWindowImpl.cs | 5 + .../Remote/PreviewerWindowingPlatform.cs | 2 - src/Avalonia.DesignerSupport/Remote/Stubs.cs | 23 +++- src/Avalonia.Native/PopupImpl.cs | 16 ++- src/Avalonia.Native/WindowImpl.cs | 4 +- src/Avalonia.X11/X11Window.cs | 34 +++++- src/Windows/Avalonia.Win32/PopupImpl.cs | 15 +++ src/Windows/Avalonia.Win32/WindowImpl.cs | 4 +- .../WindowBaseTests.cs | 27 ----- .../WindowTests.cs | 25 ++++ 19 files changed, 304 insertions(+), 196 deletions(-) diff --git a/src/Avalonia.Controls/PlacementMode.cs b/src/Avalonia.Controls/PlacementMode.cs index db77b6a365..99958c4c9e 100644 --- a/src/Avalonia.Controls/PlacementMode.cs +++ b/src/Avalonia.Controls/PlacementMode.cs @@ -23,6 +23,21 @@ namespace Avalonia.Controls /// /// The popup is placed at the top right of its target. /// - Right + Right, + + /// + /// The popup is placed at the top left of its target. + /// + Left, + + /// + /// The popup is placed at the top left of its target. + /// + Top, + + /// + /// The popup is placed according to anchor and gravity rules + /// + AnchorAndGravity } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Platform/IPopupImpl.cs b/src/Avalonia.Controls/Platform/IPopupImpl.cs index 1b606f550b..2978016519 100644 --- a/src/Avalonia.Controls/Platform/IPopupImpl.cs +++ b/src/Avalonia.Controls/Platform/IPopupImpl.cs @@ -1,6 +1,8 @@ // 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.Primitives.PopupPositioning; + namespace Avalonia.Platform { /// @@ -8,6 +10,6 @@ namespace Avalonia.Platform /// public interface IPopupImpl : IWindowBaseImpl { - + IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index b37521de30..8c99dffc28 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -15,21 +15,10 @@ namespace Avalonia.Platform /// void Hide(); - /// - /// Starts moving a window with left button being held. Should be called from left mouse button press event handler. - /// - void BeginMoveDrag(); - - /// - /// 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); - /// /// Gets the position of the window in device pixels. /// - PixelPoint Position { get; set; } + PixelPoint Position { get; } /// /// Gets or sets a method called when the window's position changes. @@ -61,17 +50,6 @@ namespace Avalonia.Platform /// Size MaxClientSize { get; } - /// - /// Sets the client size of the top level. - /// - void Resize(Size clientSize); - - /// - /// Minimum width of the window. - /// - /// - void SetMinMaxSize(Size minSize, Size maxSize); - /// /// Sets whether this window appears on top of all other windows /// diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 2ddc5a5c85..bc5d38c845 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -57,5 +57,32 @@ namespace Avalonia.Platform /// Return true to prevent the underlying implementation from closing. /// 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(); + + /// + /// 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); + + /// + /// 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. + /// + /// + void SetMinMaxSize(Size minSize, Size maxSize); } } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 895094eded..f9ec9796fb 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty ObeyScreenEdgesProperty = - AvaloniaProperty.Register(nameof(ObeyScreenEdges)); + AvaloniaProperty.Register(nameof(ObeyScreenEdges), true); /// /// Defines the property. @@ -147,10 +147,7 @@ namespace Avalonia.Controls.Primitives set { SetValue(PlacementModeProperty, value); } } - /// - /// Gets or sets a value indicating whether the popup positions itself within the nearest screen boundary - /// when its opened at a position where it would otherwise overlap the screen edge. - /// + [Obsolete("This property has no effect")] public bool ObeyScreenEdges { get => GetValue(ObeyScreenEdgesProperty); @@ -241,8 +238,9 @@ namespace Avalonia.Controls.Primitives ((ISetLogicalParent)_popupRoot).SetParent(this); } - _popupRoot.Position = GetPosition(); - + _popupRoot.ConfigurePosition(PlacementTarget ?? this.GetVisualParent(), + PlacementMode, new Point(HorizontalOffset, VerticalOffset)); + var window = _topLevel as Window; if (window != null) { @@ -263,11 +261,6 @@ namespace Avalonia.Controls.Primitives _popupRoot.Show(); - if (ObeyScreenEdges) - { - _popupRoot.SnapInsideScreenEdges(); - } - using (BeginIgnoringIsOpen()) { IsOpen = true; @@ -379,49 +372,6 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Gets the position for the popup based on the placement properties. - /// - /// The popup's position in screen coordinates. - protected virtual PixelPoint GetPosition() - { - var result = GetPosition(PlacementTarget ?? this.GetVisualParent(), PlacementMode, PopupRoot, - HorizontalOffset, VerticalOffset); - - return result; - } - - internal static PixelPoint GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset) - { - var root = target?.GetVisualRoot(); - var mode = root != null ? placement : PlacementMode.Pointer; - var scaling = root?.RenderScaling ?? 1; - - switch (mode) - { - case PlacementMode.Pointer: - if (popupRoot != null) - { - var screenOffset = PixelPoint.FromPoint(new Point(horizontalOffset, verticalOffset), scaling); - var mouseOffset = ((IInputRoot)popupRoot)?.MouseDevice?.Position ?? default; - return new PixelPoint( - screenOffset.X + mouseOffset.X, - screenOffset.Y + mouseOffset.Y); - } - - return default; - - case PlacementMode.Bottom: - return target?.PointToScreen(new Point(0 + horizontalOffset, target.Bounds.Height + verticalOffset)) ?? default; - - case PlacementMode.Right: - return target?.PointToScreen(new Point(target.Bounds.Width + horizontalOffset, 0 + verticalOffset)) ?? default; - - default: - throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); - } - } - private void ListenForNonClientClick(RawInputEventArgs e) { var mouse = e as RawPointerEventArgs; diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 47863932d1..efe4d09b3d 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -4,6 +4,7 @@ using System; using Avalonia.Controls.Platform; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Platform; @@ -18,7 +19,9 @@ namespace Avalonia.Controls.Primitives /// public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost { + private readonly TopLevel _parent; private IDisposable _presenterSubscription; + private PopupPositionerParameters _positionerParameters; /// /// Initializes static members of the class. @@ -45,6 +48,7 @@ namespace Avalonia.Controls.Primitives public PopupRoot(TopLevel parent, IAvaloniaDependencyResolver dependencyResolver) : base(parent.PlatformImpl.CreatePopup(), dependencyResolver) { + _parent = parent; } /// @@ -74,33 +78,6 @@ namespace Avalonia.Controls.Primitives /// public void Dispose() => PlatformImpl?.Dispose(); - /// - /// Moves the Popups position so that it doesnt overlap screen edges. - /// This method can be called immediately after Show has been called. - /// - public void SnapInsideScreenEdges() - { - var screen = (VisualRoot as WindowBase)?.Screens?.ScreenFromPoint(Position); - - if (screen != null) - { - var scaling = VisualRoot.RenderScaling; - var bounds = PixelRect.FromRect(Bounds, scaling); - var screenX = Position.X + bounds.Width - screen.Bounds.X; - var screenY = Position.Y + bounds.Height - screen.Bounds.Y; - - if (screenX > screen.Bounds.Width) - { - Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width)); - } - - if (screenY > screen.Bounds.Height) - { - Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height)); - } - } - } - /// protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { @@ -142,5 +119,85 @@ namespace Avalonia.Controls.Primitives } } } + + void UpdatePosition() + { + PlatformImpl?.PopupPositioner.Update(_positionerParameters); + } + + public void ConfigurePosition(Control target, PlacementMode placement, Point offset, + PopupPositioningEdge anchor = PopupPositioningEdge.None, + PopupPositioningEdge gravity = PopupPositioningEdge.None) + { + // We need a better way for tracking the last pointer position + var pointer = _parent.PointToClient(_parent.PlatformImpl.MouseDevice.Position); + + _positionerParameters.Offset = offset; + _positionerParameters.ConstraintAdjustment = PopupPositionerConstraintAdjustment.All; + if (placement == PlacementMode.Pointer) + { + _positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1)); + _positionerParameters.Anchor = PopupPositioningEdge.BottomRight; + _positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + } + else + { + if (target == null) + throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); + var matrix = target.TransformToVisual(_parent); + if (matrix == null) + throw new InvalidCastException("Target control is not in the same tree as the popup parent"); + + _positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size) + .TransformToAABB(matrix.Value); + + if (placement == PlacementMode.Right) + { + _positionerParameters.Anchor = PopupPositioningEdge.TopRight; + _positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + } + else if (placement == PlacementMode.Bottom) + { + _positionerParameters.Anchor = PopupPositioningEdge.BottomLeft; + _positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + } + else if (placement == PlacementMode.Left) + { + _positionerParameters.Anchor = PopupPositioningEdge.TopLeft; + _positionerParameters.Gravity = PopupPositioningEdge.BottomLeft; + } + else if (placement == PlacementMode.Top) + { + _positionerParameters.Anchor = PopupPositioningEdge.TopLeft; + _positionerParameters.Gravity = PopupPositioningEdge.TopRight; + } + else if (placement == PlacementMode.AnchorAndGravity) + { + _positionerParameters.Anchor = anchor; + _positionerParameters.Gravity = gravity; + } + else + throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); + } + + if (_positionerParameters.Size != default) + UpdatePosition(); + } + + /// + /// Carries out the arrange pass of the window. + /// + /// The final window size. + /// The parameter unchanged. + protected override Size ArrangeOverride(Size finalSize) + { + using (BeginAutoSizing()) + { + _positionerParameters.Size = finalSize; + UpdatePosition(); + } + + return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size)); + } } } diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 8c23f4abdc..da537a2e65 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -237,10 +237,10 @@ namespace Avalonia.Controls _popup = new PopupRoot((TopLevel)control.GetVisualRoot()) {Content = this}; ((ISetLogicalParent)_popup).SetParent(control); - _popup.Position = Popup.GetPosition(control, GetPlacement(control), _popup, - GetHorizontalOffset(control), GetVerticalOffset(control)); + + _popup.ConfigurePosition(control, GetPlacement(control), + new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); _popup.Show(); - _popup.SnapInsideScreenEdges(); } private void Close() diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index d2793fe0dd..ef43746665 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -135,6 +135,12 @@ namespace Avalonia.Controls WindowStateProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); + + MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); + MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); + MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); + MaxHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); + } /// @@ -155,6 +161,7 @@ namespace Avalonia.Controls impl.Closing = HandleClosing; impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); + this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x)); } /// @@ -239,6 +246,44 @@ namespace Avalonia.Controls set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLocation, value); } } + /// + /// Gets or sets the window position in screen coordinates. + /// + public PixelPoint Position + { + get { return PlatformImpl?.Position ?? PixelPoint.Origin; } + set + { + PlatformImpl?.Move(value); + } + } + + /// + /// Starts moving a window with left button being held. Should be called from left mouse button press event handler + /// + public void BeginMoveDrag() => PlatformImpl?.BeginMoveDrag(); + + /// + /// 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); + + /// + /// Carries out the arrange pass of the window. + /// + /// The final window size. + /// The parameter unchanged. + protected override Size ArrangeOverride(Size finalSize) + { + using (BeginAutoSizing()) + { + PlatformImpl?.Resize(finalSize); + } + + return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size)); + } + /// Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize; diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 40c9fc94d2..53e43e4ec4 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -49,10 +49,6 @@ namespace Avalonia.Controls IsVisibleProperty.OverrideDefaultValue(false); IsVisibleProperty.Changed.AddClassHandler(x => x.IsVisibleChanged); - MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); - MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); - MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); - MaxHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); TopmostProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetTopmost((bool)e.NewValue)); } @@ -67,7 +63,6 @@ namespace Avalonia.Controls impl.Activated = HandleActivated; impl.Deactivated = HandleDeactivated; impl.PositionChanged = HandlePositionChanged; - this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x)); } /// @@ -96,19 +91,6 @@ namespace Avalonia.Controls get { return _isActive; } private set { SetAndRaise(IsActiveProperty, ref _isActive, value); } } - - /// - /// Gets or sets the window position in screen coordinates. - /// - public PixelPoint Position - { - get { return PlatformImpl?.Position ?? PixelPoint.Origin; } - set - { - if (PlatformImpl is IWindowBaseImpl impl) - impl.Position = value; - } - } public Screens Screens { get; private set; } @@ -193,6 +175,11 @@ namespace Avalonia.Controls } } + protected internal virtual void OnBeforeShow() + { + + } + /// /// Begins an auto-resize operation. /// @@ -208,21 +195,6 @@ namespace Avalonia.Controls return Disposable.Create(() => AutoSizing = false); } - /// - /// Carries out the arrange pass of the window. - /// - /// The final window size. - /// The parameter unchanged. - protected override Size ArrangeOverride(Size finalSize) - { - using (BeginAutoSizing()) - { - PlatformImpl?.Resize(finalSize); - } - - return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size)); - } - /// /// Ensures that the window is initialized. /// @@ -318,16 +290,5 @@ 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(); - - /// - /// 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); } } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index dc01bcb07e..40524ad4b7 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -72,6 +72,11 @@ namespace Avalonia.DesignerSupport.Remote RenderIfNeeded(); } + public void Move(PixelPoint point) + { + + } + public void SetMinMaxSize(Size minSize, Size maxSize) { } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index a7a94130ea..dcfcd42c04 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -40,8 +40,6 @@ namespace Avalonia.DesignerSupport.Remote return s_lastWindow; } - public IPopupImpl CreatePopup() => new WindowStub(); - public static void Initialize(IAvaloniaRemoteTransportConnection transport) { s_transport = transport; diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index ddb8b62b6a..4ce0da60a2 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -5,6 +5,7 @@ using System.Reactive.Disposables; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Input.Raw; @@ -13,7 +14,7 @@ using Avalonia.Rendering; namespace Avalonia.DesignerSupport.Remote { - class WindowStub : IPopupImpl, IWindowImpl + class WindowStub : IWindowImpl, IPopupImpl { public Action Deactivated { get; set; } public Action Activated { get; set; } @@ -29,12 +30,23 @@ namespace Avalonia.DesignerSupport.Remote public Func Closing { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice { get; } = new MouseDevice(); - public IPopupImpl CreatePopup() => null; + public IPopupImpl CreatePopup() => new WindowStub(this); public PixelPoint Position { get; set; } public Action PositionChanged { get; set; } public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } + + public WindowStub(IWindowImpl parent = null) + { + if (parent != null) + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, + (_, size, __) => + { + Resize(size); + })); + } + public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer(root); public void Dispose() { @@ -79,6 +91,11 @@ namespace Avalonia.DesignerSupport.Remote { } + public void Move(PixelPoint point) + { + + } + public IScreenImpl Screen { get; } = new ScreenStub(); public void SetMinMaxSize(Size minSize, Size maxSize) @@ -112,6 +129,8 @@ namespace Avalonia.DesignerSupport.Remote public void SetTopmost(bool value) { } + + public IPopupPositioner PopupPositioner { get; } } class ClipboardStub : IClipboard diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 976208b058..f776ee0132 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.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.PopupPositioning; using Avalonia.Native.Interop; using Avalonia.Platform; @@ -11,7 +12,9 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; - public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts) + public PopupImpl(IAvaloniaNativeFactory factory, + AvaloniaNativePlatformOptions opts, + IWindowBaseImpl parent) : base(opts) { _factory = factory; _opts = opts; @@ -19,6 +22,14 @@ namespace Avalonia.Native { Init(factory.CreatePopup(e), factory.CreateScreens()); } + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); + } + + private void MoveResize(PixelPoint position, Size size, double scaling) + { + Position = position; + Resize(size); + //TODO: We ignore the scaling override for now } class PopupEvents : WindowBaseEvents, IAvnWindowEvents @@ -40,6 +51,7 @@ namespace Avalonia.Native } } - public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts); + public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, this); + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index c7857898d2..e4c158eeb3 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -104,6 +104,8 @@ namespace Avalonia.Native } public Func Closing { get; set; } - public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts); + public void Move(PixelPoint point) => Position = point; + + public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, this); } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index a1e386892b..7ca2672d2b 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reactive.Disposables; using System.Text; using Avalonia.Controls; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.OpenGL; @@ -39,6 +40,7 @@ namespace Avalonia.X11 private bool _mapped; private HashSet _transientChildren = new HashSet(); private X11Window _transientParent; + private double? _scalingOverride; public object SyncRoot { get; } = new object(); class InputEventContainer @@ -151,6 +153,8 @@ namespace Avalonia.X11 _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing, XNames.XNClientWindow, _handle, IntPtr.Zero); XFlush(_x11.Display); + if(_popup) + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); } class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo @@ -454,13 +458,20 @@ namespace Avalonia.X11 } } - private bool UpdateScaling() + private bool UpdateScaling(bool skipResize = false) { lock (SyncRoot) { - var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity) - .FirstOrDefault(m => m.Bounds.Contains(Position)); - var newScaling = monitor?.PixelDensity ?? Scaling; + double newScaling; + if (_scalingOverride.HasValue) + newScaling = _scalingOverride.Value; + else + { + var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity) + .FirstOrDefault(m => m.Bounds.Contains(Position)); + newScaling = monitor?.PixelDensity ?? Scaling; + } + if (Scaling != newScaling) { Console.WriteLine( @@ -469,7 +480,8 @@ namespace Avalonia.X11 Scaling = newScaling; ScalingChanged?.Invoke(Scaling); SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize); - Resize(oldScaledSize, true); + if(!skipResize) + Resize(oldScaledSize, true); return true; } @@ -731,6 +743,14 @@ namespace Avalonia.X11 public void Resize(Size clientSize) => Resize(clientSize, false); + public void Move(PixelPoint point) => Position = point; + private void MoveResize(PixelPoint position, Size size, double scaling) + { + Move(position); + _scalingOverride = scaling; + UpdateScaling(true); + Resize(size, true); + } PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * Scaling), (int)(size.Height * Scaling)); @@ -939,6 +959,8 @@ namespace Avalonia.X11 { SendNetWMMessage(_x11.Atoms._NET_WM_STATE, (IntPtr)(value ? 0 : 1), _x11.Atoms._NET_WM_STATE_SKIP_TASKBAR, IntPtr.Zero); - } + } + + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 39f1a95466..c9aa1ce4e7 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.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.PopupPositioning; using Avalonia.Platform; using Avalonia.Win32.Interop; @@ -57,5 +58,19 @@ namespace Avalonia.Win32 return base.WndProc(hWnd, msg, wParam, lParam); } } + + public PopupImpl(IWindowBaseImpl parent) + { + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); + } + + private void MoveResize(PixelPoint position, Size size, double scaling) + { + Move(position); + Resize(size); + //TODO: We ignore the scaling override for now + } + + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 2f7805884d..21625af84a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -131,6 +131,8 @@ namespace Avalonia.Win32 } } + public void Move(PixelPoint point) => Position = point; + public void SetMinMaxSize(Size minSize, Size maxSize) { _minSize = minSize; @@ -250,7 +252,7 @@ namespace Avalonia.Win32 public IPopupImpl CreatePopup() { - return new PopupImpl(); + return new PopupImpl(this); } public void Dispose() diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 3ee6a50e69..55e8ae0115 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -20,33 +20,6 @@ namespace Avalonia.Controls.UnitTests { public class WindowBaseTests { - [Fact] - public void Impl_ClientSize_Should_Be_Set_After_Layout_Pass() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var impl = Mock.Of(x => x.Scaling == 1); - - Mock.Get(impl).Setup(x => x.Resize(It.IsAny())).Callback(() => { }); - - var target = new TestWindowBase(impl) - { - Template = CreateTemplate(), - Content = new TextBlock - { - Width = 321, - Height = 432, - }, - IsVisible = true, - }; - - target.LayoutManager.ExecuteInitialLayoutPass(target); - - Mock.Get(impl).Verify(x => x.Resize(new Size(321, 432))); - } - } - - [Fact] public void Activate_Should_Call_Impl_Activate() { diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index f4d9a91d0c..cbcf08049e 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -17,6 +17,31 @@ namespace Avalonia.Controls.UnitTests { public class WindowTests { + [Fact] + public void Impl_ClientSize_Should_Be_Set_After_Layout_Pass() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = Mock.Of(x => x.Scaling == 1); + + Mock.Get(impl).Setup(x => x.Resize(It.IsAny())).Callback(() => { }); + + var target = new Window(impl) + { + Content = new TextBlock + { + Width = 321, + Height = 432, + }, + IsVisible = true, + }; + + target.LayoutManager.ExecuteInitialLayoutPass(target); + + Mock.Get(impl).Verify(x => x.Resize(new Size(321, 432))); + } + } + [Fact] public void Setting_Title_Should_Set_Impl_Title() { From 09dd7c67c195fabaf9d4ab33c9c4d51759e431bd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 27 Jul 2019 11:09:33 +0100 Subject: [PATCH 027/129] 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 @@ - +