From d9ce64e1b03da867fdf7981796ad9b5d93f8e672 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 14 Jul 2022 14:29:43 +0200 Subject: [PATCH 01/11] Skip disabled controls when moving to first/last item --- src/Avalonia.Controls/ItemsControl.cs | 29 ++++++++++++------- .../Platform/DefaultMenuInteractionHandler.cs | 4 +-- .../Primitives/SelectingItemsControlTests.cs | 24 +++++++-------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 56b0014c05..c7348f8609 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -502,25 +502,34 @@ namespace Avalonia.Controls IInputElement? from, bool wrap) { - IInputElement? result; - var c = from; - - do + for(;;) { - result = container.GetControl(direction, c, wrap); + var result = container.GetControl(direction, from, wrap); + + if (result is null || result == from) + { + return null; + } - if (result != null && - result.Focusable && + if (result.Focusable && result.IsEffectivelyEnabled && result.IsEffectivelyVisible) { return result; } - c = result; - } while (c != null && c != from && direction != NavigationDirection.First && direction != NavigationDirection.Last); + direction = direction switch + { + //We did not find an enabled first item. Move downwards until we find one. + NavigationDirection.First => NavigationDirection.Down, + + //We did not find an enabled last item. Move upwards until we find one. + NavigationDirection.Last => NavigationDirection.Up, + _ => direction + }; - return null; + from = result; + } } private void PresenterChildIndexChanged(object? sender, ChildIndexChangedEventArgs e) diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 868cce879a..ce1cddc8cd 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -53,7 +53,7 @@ namespace Avalonia.Controls.Platform Menu.PointerPressed += PointerPressed; Menu.PointerReleased += PointerReleased; Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed); - Menu.AddHandler(Avalonia.Controls.Menu.MenuOpenedEvent, MenuOpened); + Menu.AddHandler(MenuBase.MenuOpenedEvent, MenuOpened); Menu.AddHandler(MenuItem.PointerEnteredItemEvent, PointerEntered); Menu.AddHandler(MenuItem.PointerExitedItemEvent, PointerExited); Menu.AddHandler(InputElement.PointerMovedEvent, PointerMoved); @@ -89,7 +89,7 @@ namespace Avalonia.Controls.Platform Menu.PointerPressed -= PointerPressed; Menu.PointerReleased -= PointerReleased; Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed); - Menu.RemoveHandler(Avalonia.Controls.Menu.MenuOpenedEvent, MenuOpened); + Menu.RemoveHandler(MenuBase.MenuOpenedEvent, MenuOpened); Menu.RemoveHandler(MenuItem.PointerEnteredItemEvent, PointerEntered); Menu.RemoveHandler(MenuItem.PointerExitedItemEvent, PointerExited); Menu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 3d36395c3a..e861f4a5db 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1615,48 +1615,44 @@ namespace Avalonia.Controls.UnitTests.Primitives target.MoveSelection(NavigationDirection.Next, true); } - [Fact(Timeout = 2000)] - public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_First_Item() + [Fact] + public void MoveSelection_Skips_Non_Focusable_Controls_When_Moving_To_Last_Item() { var target = new TestSelector { Template = Template(), Items = new[] { - new ListBoxItem { Focusable = false }, new ListBoxItem(), + new ListBoxItem { Focusable = false }, } }; target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); + target.MoveSelection(NavigationDirection.Last, true); - // Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below. - // https://github.com/xunit/xunit/issues/2222 - await Task.Run(() => target.MoveSelection(NavigationDirection.First, true)); - Assert.Equal(-1, target.SelectedIndex); + Assert.Equal(0, target.SelectedIndex); } - [Fact(Timeout = 2000)] - public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_Last_Item() + [Fact] + public void MoveSelection_Skips_Non_Focusable_Controls_When_Moving_To_First_Item() { var target = new TestSelector { Template = Template(), Items = new[] { - new ListBoxItem(), new ListBoxItem { Focusable = false }, + new ListBoxItem(), } }; target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); + target.MoveSelection(NavigationDirection.Last, true); - // Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below. - // https://github.com/xunit/xunit/issues/2222 - await Task.Run(() => target.MoveSelection(NavigationDirection.Last, true)); - Assert.Equal(-1, target.SelectedIndex); + Assert.Equal(1, target.SelectedIndex); } [Fact] From 107fc7162e0a7082d2e612316362229973f86532 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 15 Jul 2022 13:41:14 +0200 Subject: [PATCH 02/11] Fix infinite loop when all items are disabled --- src/Avalonia.Controls/ItemsControl.cs | 16 +++++--- .../Primitives/SelectingItemsControlTests.cs | 40 +++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index c7348f8609..9f52371d8c 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -518,15 +518,21 @@ namespace Avalonia.Controls return result; } - direction = direction switch + switch (direction) { //We did not find an enabled first item. Move downwards until we find one. - NavigationDirection.First => NavigationDirection.Down, + case NavigationDirection.First: + direction = NavigationDirection.Down; + wrap = false; + break; //We did not find an enabled last item. Move upwards until we find one. - NavigationDirection.Last => NavigationDirection.Up, - _ => direction - }; + case NavigationDirection.Last: + direction = NavigationDirection.Up; + wrap = false; + break; + + } from = result; } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index e861f4a5db..76729d8e41 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1655,6 +1655,46 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(1, target.SelectedIndex); } + [Fact(Timeout = 2000)] + public void MoveSelection_Does_Not_Hang_When_All_Items_Are_Non_Focusable_And_We_Move_To_First_Item() + { + var target = new TestSelector + { + Template = Template(), + Items = new[] + { + new ListBoxItem { Focusable = false }, + new ListBoxItem { Focusable = false }, + } + }; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + target.MoveSelection(NavigationDirection.First, true); + + Assert.Equal(-1, target.SelectedIndex); + } + + [Fact(Timeout = 2000)] + public void MoveSelection_Does_Not_Hang_When_All_Items_Are_Non_Focusable_And_We_Move_To_Last_Item() + { + var target = new TestSelector + { + Template = Template(), + Items = new[] + { + new ListBoxItem { Focusable = false }, + new ListBoxItem { Focusable = false }, + } + }; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + target.MoveSelection(NavigationDirection.Last, true); + + Assert.Equal(-1, target.SelectedIndex); + } + [Fact] public void MoveSelection_Does_Select_Disabled_Controls() { From e64a4430a06c00ad588823fcf2bed4a090f727ef Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 15 Jul 2022 13:58:01 +0200 Subject: [PATCH 03/11] Apply async hack to make timeout work --- .../Primitives/SelectingItemsControlTests.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 76729d8e41..19fdf0c569 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1656,7 +1656,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact(Timeout = 2000)] - public void MoveSelection_Does_Not_Hang_When_All_Items_Are_Non_Focusable_And_We_Move_To_First_Item() + public async Task MoveSelection_Does_Not_Hang_When_All_Items_Are_Non_Focusable_And_We_Move_To_First_Item() { var target = new TestSelector { @@ -1670,13 +1670,16 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); - target.MoveSelection(NavigationDirection.First, true); + + // Timeout in xUnit doesn't work with synchronous methods so we need to apply hack below. + // https://github.com/xunit/xunit/issues/2222 + await Task.Run(() => target.MoveSelection(NavigationDirection.First, true)); Assert.Equal(-1, target.SelectedIndex); } [Fact(Timeout = 2000)] - public void MoveSelection_Does_Not_Hang_When_All_Items_Are_Non_Focusable_And_We_Move_To_Last_Item() + public async Task MoveSelection_Does_Not_Hang_When_All_Items_Are_Non_Focusable_And_We_Move_To_Last_Item() { var target = new TestSelector { @@ -1690,7 +1693,10 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); - target.MoveSelection(NavigationDirection.Last, true); + + // Timeout in xUnit doesn't work with synchronous methods so we need to apply hack below. + // https://github.com/xunit/xunit/issues/2222 + await Task.Run(() => target.MoveSelection(NavigationDirection.Last, true)); Assert.Equal(-1, target.SelectedIndex); } From ace7787526c412e112e60f03aa82b2b5e20ddd09 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 15 Jul 2022 14:16:25 +0200 Subject: [PATCH 04/11] Fix arrow left on all-disabled submenu closing menu --- .../Platform/DefaultMenuInteractionHandler.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index ce1cddc8cd..16aeb2f559 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -175,7 +175,11 @@ namespace Avalonia.Controls.Platform case Key.Left: { - if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) + if (item is { IsSubMenuOpen: true, SelectedItem: null }) + { + item.Close(); + } + else if (item?.Parent is IMenuItem { IsTopLevel: false, IsSubMenuOpen: true } parent) { parent.Close(); parent.Focus(); From 25b19931e830c51ba0a895a69995c227a86edc0f Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 15 Jul 2022 16:13:03 +0200 Subject: [PATCH 05/11] Add timeout to "hang" UT --- .../Primitives/SelectingItemsControlTests.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 19fdf0c569..4b6b6a1182 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1595,8 +1595,8 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "Bar" }, selectedItems); } - [Fact] - public void MoveSelection_Wrap_Does_Not_Hang_With_No_Focusable_Controls() + [Fact(Timeout = 2000)] + public async Task MoveSelection_Wrap_Does_Not_Hang_With_No_Focusable_Controls() { // Issue #3094. var target = new TestSelector @@ -1612,7 +1612,10 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); - target.MoveSelection(NavigationDirection.Next, true); + + // Timeout in xUnit doesn't work with synchronous methods so we need to apply hack below. + // https://github.com/xunit/xunit/issues/2222 + await Task.Run(() => target.MoveSelection(NavigationDirection.Next, true)); } [Fact] From 16d75632fe362ddd79d5ed0e9dfe33357e0c6952 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 15 Jul 2022 16:29:49 +0200 Subject: [PATCH 06/11] Properly terminate when reaching "from" element again --- src/Avalonia.Controls/ItemsControl.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 9f52371d8c..6aa4006ddb 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -502,11 +502,13 @@ namespace Avalonia.Controls IInputElement? from, bool wrap) { - for(;;) + var current = from; + + for (;;) { - var result = container.GetControl(direction, from, wrap); + var result = container.GetControl(direction, current, wrap); - if (result is null || result == from) + if (result is null || current == from) { return null; } @@ -534,7 +536,7 @@ namespace Avalonia.Controls } - from = result; + current = result; } } From 0973e66d04505264f2e34c9fba0769be8f35c15f Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 15 Jul 2022 16:31:50 +0200 Subject: [PATCH 07/11] Remove wrong fix --- src/Avalonia.Controls/ItemsControl.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 6aa4006ddb..c6b572abba 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -525,13 +525,11 @@ namespace Avalonia.Controls //We did not find an enabled first item. Move downwards until we find one. case NavigationDirection.First: direction = NavigationDirection.Down; - wrap = false; break; //We did not find an enabled last item. Move upwards until we find one. case NavigationDirection.Last: direction = NavigationDirection.Up; - wrap = false; break; } From 235713823fcaf2014240da78721a5358327304f3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 20 Jul 2022 00:48:59 -0400 Subject: [PATCH 08/11] Abstract linux DBus and GTK storage providers, use async initialization --- Avalonia.sln | 1 + src/Avalonia.FreeDesktop/DBusHelper.cs | 3 +- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 33 +++++---- src/Avalonia.X11/NativeDialogs/Gtk.cs | 2 - .../NativeDialogs/GtkNativeFileDialogs.cs | 30 +++----- .../NativeDialogs/LinuxStorageProvider.cs | 72 +++++++++++++++++++ src/Avalonia.X11/X11Platform.cs | 8 +-- src/Avalonia.X11/X11Window.cs | 4 +- 8 files changed, 104 insertions(+), 49 deletions(-) create mode 100644 src/Avalonia.X11/NativeDialogs/LinuxStorageProvider.cs diff --git a/Avalonia.sln b/Avalonia.sln index 071d0457b8..4999719676 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -559,6 +559,7 @@ Global {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index 9f9d75b411..ef99838208 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -24,8 +24,7 @@ namespace Avalonia.FreeDesktop if (_ctx is not null) _ctx?.Post(d, state); else - lock (_lock) - d(state); + d(state); } } diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index c17d5b993c..e3fc7526d8 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -15,27 +15,26 @@ namespace Avalonia.FreeDesktop { internal class DBusSystemDialog : BclStorageProvider { - private static readonly Lazy s_fileChooser = new(() => + private static readonly Lazy s_fileChooser = new(() => DBusHelper.Connection? + .CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop")); + + internal static async Task TryCreate(IPlatformHandle handle) { - var fileChooser = DBusHelper.Connection?.CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); - if (fileChooser is null) - return null; - try + if (handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser) { - _ = fileChooser.GetVersionAsync(); - return fileChooser; - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}"); - return null; + try + { + await fileChooser.GetVersionAsync(); + return new DBusSystemDialog(fileChooser, handle); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}"); + return null; + } } - }); - internal static DBusSystemDialog? TryCreate(IPlatformHandle handle) - { - return handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser - ? new DBusSystemDialog(fileChooser, handle) : null; + return null; } private readonly IFileChooser _fileChooser; diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index c9e482db86..ae04c072a5 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -264,8 +264,6 @@ namespace Avalonia.X11.NativeDialogs public static Task StartGtk() { return StartGtkCore(); - lock (s_startGtkLock) - return s_startGtkTask ??= StartGtkCore(); } private static void GtkThread(TaskCompletionSource tcs) diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index 89d08a3974..ca3e0cd33d 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -17,10 +17,10 @@ namespace Avalonia.X11.NativeDialogs { internal class GtkSystemDialog : BclStorageProvider { - private Task? _initialized; + private static Task? _initialized; private readonly X11Window _window; - public GtkSystemDialog(X11Window window) + private GtkSystemDialog(X11Window window) { _window = window; } @@ -31,10 +31,15 @@ namespace Avalonia.X11.NativeDialogs public override bool CanPickFolder => true; - public override async Task> OpenFilePickerAsync(FilePickerOpenOptions options) + internal static async Task TryCreate(X11Window window) { - await EnsureInitialized(); + _initialized ??= StartGtk(); + + return await _initialized ? new GtkSystemDialog(window) : null; + } + public override async Task> OpenFilePickerAsync(FilePickerOpenOptions options) + { return await await RunOnGlibThread(async () => { var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.Open, @@ -46,8 +51,6 @@ namespace Avalonia.X11.NativeDialogs public override async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { - await EnsureInitialized(); - return await await RunOnGlibThread(async () => { var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.SelectFolder, @@ -59,8 +62,6 @@ namespace Avalonia.X11.NativeDialogs public override async Task SaveFilePickerAsync(FilePickerSaveOptions options) { - await EnsureInitialized(); - return await await RunOnGlibThread(async () => { var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.Save, @@ -225,19 +226,6 @@ namespace Avalonia.X11.NativeDialogs return tcs.Task; } - private async Task EnsureInitialized() - { - if (_initialized == null) - { - _initialized = StartGtk(); - } - - if (!(await _initialized)) - { - throw new Exception("Unable to initialize GTK on separate thread"); - } - } - private static void UpdateParent(IntPtr chooser, IWindowImpl parentWindow) { var xid = parentWindow.Handle.Handle; diff --git a/src/Avalonia.X11/NativeDialogs/LinuxStorageProvider.cs b/src/Avalonia.X11/NativeDialogs/LinuxStorageProvider.cs new file mode 100644 index 0000000000..75293e12fb --- /dev/null +++ b/src/Avalonia.X11/NativeDialogs/LinuxStorageProvider.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.FreeDesktop; +using Avalonia.Platform.Storage; + +namespace Avalonia.X11.NativeDialogs; + +internal class LinuxStorageProvider : IStorageProvider +{ + private readonly X11Window _window; + public LinuxStorageProvider(X11Window window) + { + _window = window; + } + + public bool CanOpen => true; + public bool CanSave => true; + public bool CanPickFolder => true; + + private async Task EnsureStorageProvider() + { + var options = AvaloniaLocator.Current.GetService() ?? new X11PlatformOptions(); + + if (options.UseDBusFilePicker) + { + var dBusDialog = await DBusSystemDialog.TryCreate(_window.Handle); + if (dBusDialog is not null) + { + return dBusDialog; + } + } + + var gtkDialog = await GtkSystemDialog.TryCreate(_window); + if (gtkDialog is not null) + { + return gtkDialog; + } + + throw new InvalidOperationException("Neither DBus nor GTK are available on the system"); + } + + public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) + { + var provider = await EnsureStorageProvider().ConfigureAwait(false); + return await provider.OpenFilePickerAsync(options).ConfigureAwait(false); + } + + public async Task SaveFilePickerAsync(FilePickerSaveOptions options) + { + var provider = await EnsureStorageProvider().ConfigureAwait(false); + return await provider.SaveFilePickerAsync(options).ConfigureAwait(false); + } + + public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) + { + var provider = await EnsureStorageProvider().ConfigureAwait(false); + return await provider.OpenFolderPickerAsync(options).ConfigureAwait(false); + } + + public async Task OpenFileBookmarkAsync(string bookmark) + { + var provider = await EnsureStorageProvider().ConfigureAwait(false); + return await provider.OpenFileBookmarkAsync(bookmark).ConfigureAwait(false); + } + + public async Task OpenFolderBookmarkAsync(string bookmark) + { + var provider = await EnsureStorageProvider().ConfigureAwait(false); + return await provider.OpenFolderBookmarkAsync(bookmark).ConfigureAwait(false); + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index edb320d4f0..7043c60ae7 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -216,16 +216,16 @@ namespace Avalonia public bool OverlayPopups { get; set; } /// - /// Enables native file dialogs as well as global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc). + /// Enables global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc). /// The default value is true. /// public bool UseDBusMenu { get; set; } = true; /// - /// Enables GTK file picker instead of default FreeDesktop. - /// The default value is true. And FreeDesktop file picker is used instead if available. + /// Enables DBus file picker instead of GTK. + /// The default value is true. /// - public bool UseGtkFilePicker { get; set; } = false; + public bool UseDBusFilePicker { get; set; } = true; /// /// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true. diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 2f92448f4b..ef8ae2f70f 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -215,9 +215,7 @@ namespace Avalonia.X11 _x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref _xSyncCounter, 1); } - var canUseFreeDekstopPicker = !platform.Options.UseGtkFilePicker && platform.Options.UseDBusMenu; - StorageProvider = canUseFreeDekstopPicker && DBusSystemDialog.TryCreate(Handle) is {} dBusStorage - ? dBusStorage : new NativeDialogs.GtkSystemDialog(this); + StorageProvider = new NativeDialogs.LinuxStorageProvider(this); } class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo From c58e43b373d033d1d2ff59719f5e4344a0c8ef56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Jul 2022 18:08:13 +0000 Subject: [PATCH 09/11] Bump terser in /src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp Bumps [terser](https://github.com/terser/terser) from 5.10.0 to 5.14.2. - [Release notes](https://github.com/terser/terser/releases) - [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md) - [Commits](https://github.com/terser/terser/commits) --- updated-dependencies: - dependency-name: terser dependency-type: indirect ... Signed-off-by: dependabot[bot] --- .../HtmlTransport/webapp/package-lock.json | 98 +++++++++++++------ 1 file changed, 70 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json index 403bb5a59a..2fbcbfdb6a 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json @@ -10,6 +10,55 @@ "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", "dev": true }, + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "@types/eslint": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", @@ -2136,6 +2185,12 @@ "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", "dev": true }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, "source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -2153,6 +2208,16 @@ "source-map-js": "^1.0.1" } }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "string-template": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", @@ -2208,13 +2273,14 @@ "dev": true }, "terser": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", - "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dev": true, "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" }, "dependencies": { @@ -2223,30 +2289,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } } } }, From 24effcf2ec6a23ea87fcea3e46464c7a6545ea7f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 20 Jul 2022 23:54:31 -0400 Subject: [PATCH 10/11] Change pattern to CompositeStorageProvider --- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 2 +- ...rovider.cs => CompositeStorageProvider.cs} | 34 +++++++------------ .../NativeDialogs/GtkNativeFileDialogs.cs | 2 +- src/Avalonia.X11/X11Window.cs | 7 +++- 4 files changed, 21 insertions(+), 24 deletions(-) rename src/Avalonia.X11/NativeDialogs/{LinuxStorageProvider.cs => CompositeStorageProvider.cs} (62%) diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index e3fc7526d8..7974069184 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -18,7 +18,7 @@ namespace Avalonia.FreeDesktop private static readonly Lazy s_fileChooser = new(() => DBusHelper.Connection? .CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop")); - internal static async Task TryCreate(IPlatformHandle handle) + internal static async Task TryCreate(IPlatformHandle handle) { if (handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser) { diff --git a/src/Avalonia.X11/NativeDialogs/LinuxStorageProvider.cs b/src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs similarity index 62% rename from src/Avalonia.X11/NativeDialogs/LinuxStorageProvider.cs rename to src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs index 75293e12fb..5fe0f46b14 100644 --- a/src/Avalonia.X11/NativeDialogs/LinuxStorageProvider.cs +++ b/src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs @@ -1,17 +1,17 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Threading.Tasks; -using Avalonia.FreeDesktop; using Avalonia.Platform.Storage; namespace Avalonia.X11.NativeDialogs; -internal class LinuxStorageProvider : IStorageProvider +internal class CompositeStorageProvider : IStorageProvider { - private readonly X11Window _window; - public LinuxStorageProvider(X11Window window) + private readonly IEnumerable>> _factories; + public CompositeStorageProvider(IEnumerable>> factories) { - _window = window; + _factories = factories; } public bool CanOpen => true; @@ -20,22 +20,14 @@ internal class LinuxStorageProvider : IStorageProvider private async Task EnsureStorageProvider() { - var options = AvaloniaLocator.Current.GetService() ?? new X11PlatformOptions(); - - if (options.UseDBusFilePicker) + foreach (var factory in _factories) { - var dBusDialog = await DBusSystemDialog.TryCreate(_window.Handle); - if (dBusDialog is not null) + var provider = await factory(); + if (provider is not null) { - return dBusDialog; + return provider; } } - - var gtkDialog = await GtkSystemDialog.TryCreate(_window); - if (gtkDialog is not null) - { - return gtkDialog; - } throw new InvalidOperationException("Neither DBus nor GTK are available on the system"); } @@ -46,7 +38,7 @@ internal class LinuxStorageProvider : IStorageProvider return await provider.OpenFilePickerAsync(options).ConfigureAwait(false); } - public async Task SaveFilePickerAsync(FilePickerSaveOptions options) + public async Task SaveFilePickerAsync(FilePickerSaveOptions options) { var provider = await EnsureStorageProvider().ConfigureAwait(false); return await provider.SaveFilePickerAsync(options).ConfigureAwait(false); @@ -58,13 +50,13 @@ internal class LinuxStorageProvider : IStorageProvider return await provider.OpenFolderPickerAsync(options).ConfigureAwait(false); } - public async Task OpenFileBookmarkAsync(string bookmark) + public async Task OpenFileBookmarkAsync(string bookmark) { var provider = await EnsureStorageProvider().ConfigureAwait(false); return await provider.OpenFileBookmarkAsync(bookmark).ConfigureAwait(false); } - public async Task OpenFolderBookmarkAsync(string bookmark) + public async Task OpenFolderBookmarkAsync(string bookmark) { var provider = await EnsureStorageProvider().ConfigureAwait(false); return await provider.OpenFolderBookmarkAsync(bookmark).ConfigureAwait(false); diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index ca3e0cd33d..89aa0340b5 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -31,7 +31,7 @@ namespace Avalonia.X11.NativeDialogs public override bool CanPickFolder => true; - internal static async Task TryCreate(X11Window window) + internal static async Task TryCreate(X11Window window) { _initialized ??= StartGtk(); diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index ef8ae2f70f..009ccb6159 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -22,6 +22,7 @@ using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.X11.Glx; +using Avalonia.X11.NativeDialogs; using static Avalonia.X11.XLib; // ReSharper disable IdentifierTypo // ReSharper disable StringLiteralTypo @@ -215,7 +216,11 @@ namespace Avalonia.X11 _x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref _xSyncCounter, 1); } - StorageProvider = new NativeDialogs.LinuxStorageProvider(this); + StorageProvider = new CompositeStorageProvider(new Func>[] + { + () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreate(Handle) : Task.FromResult(null), + () => GtkSystemDialog.TryCreate(this), + }); } class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo From 080fdee57dcaec1860fa9384079d4514e9680205 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 21 Jul 2022 10:55:52 +0200 Subject: [PATCH 11/11] Fix UTS --- src/Avalonia.Controls/ItemsControl.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index c6b572abba..1ac642c22b 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -508,7 +508,7 @@ namespace Avalonia.Controls { var result = container.GetControl(direction, current, wrap); - if (result is null || current == from) + if (result is null) { return null; } @@ -520,21 +520,28 @@ namespace Avalonia.Controls return result; } + current = result; + + if (current == from) + { + return null; + } + switch (direction) { //We did not find an enabled first item. Move downwards until we find one. case NavigationDirection.First: direction = NavigationDirection.Down; + from = result; break; //We did not find an enabled last item. Move upwards until we find one. case NavigationDirection.Last: direction = NavigationDirection.Up; + from = result; break; - - } - current = result; + } } }