From b040ac5414276c7dbd2c036b6d9b32c3e9b29844 Mon Sep 17 00:00:00 2001 From: Dominik Matijaca Date: Fri, 4 Feb 2022 17:11:40 +0100 Subject: [PATCH 1/5] ItemsControl: WrapSelection (#6286) Add support for selection wrapping for ListBox, ItemsControl, ComboBox. Co-authored-by: Steven Kirk Co-authored-by: Takoooooo Co-authored-by: Steven Kirk --- .../ControlCatalog/Pages/ComboBoxPage.xaml | 160 ++++++++++-------- .../ControlCatalog/Pages/ComboBoxPage.xaml.cs | 2 + samples/ControlCatalog/Pages/ListBoxPage.xaml | 4 +- .../ViewModels/ComboBoxPageViewModel.cs | 21 +++ .../ViewModels/ListBoxPageViewModel.cs | 7 + src/Avalonia.Controls/ComboBox.cs | 31 +++- src/Avalonia.Controls/ItemsControl.cs | 4 +- .../Presenters/ItemVirtualizerSimple.cs | 15 +- .../Primitives/SelectingItemsControl.cs | 20 +++ .../VirtualizingStackPanel.cs | 9 +- .../ListBoxTests.cs | 49 ++++++ 11 files changed, 232 insertions(+), 90 deletions(-) create mode 100644 samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml index decbd763a1..64e80a8e11 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml @@ -1,77 +1,95 @@ - - - A drop-down list. + + + A drop-down list. - - - - - - Inline Items - Inline Item 2 - Inline Item 3 - Inline Item 4 - + + + + + - - - - - Hello - World - - - - - - - - - - - + + Inline Items + Inline Item 2 + Inline Item 3 + Inline Item 4 + - - - - - Control Items - - - - - - - - - + + + + + Hello + World + + + + + + + + + + + - - - - - - - - - - Inline Items - Inline Item 2 - Inline Item 3 - Inline Item 4 - - - - - + + + + + Control Items + + + + + + + + + - + + + + + + + + + + Inline Items + Inline Item 2 + Inline Item 3 + Inline Item 4 + + + + + + + WrapSelection + + + diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index d50b051d9f..d304bf227d 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -2,6 +2,7 @@ using System.Linq; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Media; +using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { @@ -10,6 +11,7 @@ namespace ControlCatalog.Pages public ComboBoxPage() { this.InitializeComponent(); + DataContext = new ComboBoxPageViewModel(); } private void InitializeComponent() diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 41658329df..433592345a 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -21,6 +21,7 @@ Toggle AlwaysSelected AutoScrollToSelectedItem + WrapSelection @@ -30,6 +31,7 @@ + SelectionMode="{Binding SelectionMode^}" + WrapSelection="{Binding WrapSelection}"/> diff --git a/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs new file mode 100644 index 0000000000..bbe970afd6 --- /dev/null +++ b/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using Avalonia.Controls; +using Avalonia.Controls.Selection; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ComboBoxPageViewModel : ViewModelBase + { + private bool _wrapSelection; + + public bool WrapSelection + { + get => _wrapSelection; + set => this.RaiseAndSetIfChanged(ref _wrapSelection, value); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs index 7f2d6e9572..59489ebcc0 100644 --- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -14,6 +14,7 @@ namespace ControlCatalog.ViewModels private bool _toggle; private bool _alwaysSelected; private bool _autoScrollToSelectedItem = true; + private bool _wrapSelection; private int _counter; private IObservable _selectionMode; @@ -85,6 +86,12 @@ namespace ControlCatalog.ViewModels set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value); } + public bool WrapSelection + { + get => _wrapSelection; + set => this.RaiseAndSetIfChanged(ref _wrapSelection, value); + } + public MiniCommand AddItemCommand { get; } public MiniCommand RemoveItemCommand { get; } public MiniCommand SelectRandomItemCommand { get; } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index e9eca97e13..72b09b7a3c 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -10,9 +10,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; -using Avalonia.LogicalTree; using Avalonia.Media; -using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -91,7 +89,7 @@ namespace Avalonia.Controls { ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); FocusableProperty.OverrideDefaultValue(true); - SelectedItemProperty.Changed.AddClassHandler((x,e) => x.SelectedItemChanged(e)); + SelectedItemProperty.Changed.AddClassHandler((x, e) => x.SelectedItemChanged(e)); KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel); IsTextSearchEnabledProperty.OverrideDefaultValue(true); } @@ -221,8 +219,9 @@ namespace Avalonia.Controls e.Handled = true; } } + // This part of code is needed just to acquire initial focus, subsequent focus navigation will be done by ItemsControl. else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 && - (e.Key == Key.Up || e.Key == Key.Down)) + (e.Key == Key.Up || e.Key == Key.Down) && IsFocused == true) { var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c)); if (firstChild != null) @@ -430,7 +429,18 @@ namespace Avalonia.Controls int next = SelectedIndex + 1; if (next >= ItemCount) - next = 0; + { + if (WrapSelection == true) + { + next = 0; + } + else + { + return; + } + } + + SelectedIndex = next; } @@ -440,7 +450,16 @@ namespace Avalonia.Controls int prev = SelectedIndex - 1; if (prev < 0) - prev = ItemCount - 1; + { + if (WrapSelection == true) + { + prev = ItemCount - 1; + } + else + { + return; + } + } SelectedIndex = prev; } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 10e12a1ae0..ed8f9efb2e 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -143,6 +143,8 @@ namespace Avalonia.Controls protected set; } + private protected bool WrapFocus { get; set; } + event EventHandler? IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -315,7 +317,7 @@ namespace Avalonia.Controls { if (current.VisualParent == container && current is IInputElement inputElement) { - var next = GetNextControl(container, direction.Value, inputElement, false); + var next = GetNextControl(container, direction.Value, inputElement, WrapFocus); if (next != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index a34e5d6438..361febf305 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -233,11 +233,6 @@ namespace Avalonia.Controls.Presenters var itemIndex = generator.IndexFromContainer(from); var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical; - if (itemIndex == -1) - { - return null; - } - var newItemIndex = -1; switch (direction) @@ -250,6 +245,16 @@ namespace Avalonia.Controls.Presenters newItemIndex = ItemCount - 1; break; + default: + if (itemIndex == -1) + { + return null; + } + break; + } + + switch (direction) + { case NavigationDirection.Up: if (vertical) { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 840a5ac1dc..b4cfd9404c 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -114,6 +114,12 @@ namespace Avalonia.Controls.Primitives "SelectionChanged", RoutingStrategies.Bubble); + /// + /// Defines the property. + /// + public static readonly StyledProperty WrapSelectionProperty = + AvaloniaProperty.Register(nameof(WrapSelection), defaultValue: false); + private static readonly IList Empty = Array.Empty(); private string _textSearchTerm = string.Empty; private DispatcherTimer? _textSearchTimer; @@ -321,6 +327,16 @@ namespace Avalonia.Controls.Primitives set { SetValue(IsTextSearchEnabledProperty, value); } } + /// + /// Gets or sets a value which indicates whether to wrap around when the first + /// or last item is reached. + /// + public bool WrapSelection + { + get { return GetValue(WrapSelectionProperty); } + set { SetValue(WrapSelectionProperty, value); } + } + /// /// Gets or sets the selection mode. /// @@ -580,6 +596,10 @@ namespace Avalonia.Controls.Primitives var newValue = change.NewValue.GetValueOrDefault(); _selection.SingleSelect = !newValue.HasAllFlags(SelectionMode.Multiple); } + else if (change.Property == WrapSelectionProperty) + { + WrapFocus = WrapSelection; + } } /// diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 34b774e23f..f27568694d 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -36,7 +36,7 @@ namespace Avalonia.Controls { get { - var bounds = Orientation == Orientation.Horizontal ? + var bounds = Orientation == Orientation.Horizontal ? _availableSpace.Width : _availableSpace.Height; return Math.Max(0, _takenSpace - bounds); } @@ -129,14 +129,11 @@ namespace Avalonia.Controls protected override IInputElement? GetControlInDirection(NavigationDirection direction, IControl? from) { - if (from == null) - return null; - var logicalScrollable = Parent as ILogicalScrollable; if (logicalScrollable?.IsLogicalScrollEnabled == true) { - return logicalScrollable.GetControlInDirection(direction, from); + return logicalScrollable.GetControlInDirection(direction, from!); } else { @@ -145,7 +142,7 @@ namespace Avalonia.Controls } internal override void ArrangeChild( - IControl child, + IControl child, Rect rect, Size panelSize, Orientation orientation) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 7c57e22933..aa63e18691 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -613,5 +613,54 @@ namespace Avalonia.Controls.UnitTests Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); } + + private void RaiseKeyEvent(ListBox listBox, Key key, KeyModifiers inputModifiers = 0) + { + listBox.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + KeyModifiers = inputModifiers, + Key = key + }); + } + + [Fact] + public void WrapSelection_Should_Wrap() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray(); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), + WrapSelection = true + }; + + Prepare(target); + + var lbItems = target.GetLogicalChildren().OfType().ToArray(); + + var first = lbItems.First(); + var last = lbItems.Last(); + + first.Focus(); + + RaisePressedEvent(target, first, MouseButton.Left); + Assert.Equal(true, first.IsSelected); + + RaiseKeyEvent(target, Key.Up); + Assert.Equal(true, last.IsSelected); + + RaiseKeyEvent(target, Key.Down); + Assert.Equal(true, first.IsSelected); + + target.WrapSelection = false; + RaiseKeyEvent(target, Key.Up); + + Assert.Equal(true, first.IsSelected); + } + } } } From e26d9d796c4fb3e747a8500083944c8ead57e1fa Mon Sep 17 00:00:00 2001 From: Olivier DALET Date: Fri, 4 Feb 2022 17:55:12 +0100 Subject: [PATCH 2/5] Fix #7519 - Reset fb and depth buffer Ids once they are deleted --- src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index 33773ed8e2..b3469c212b 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -89,7 +89,9 @@ namespace Avalonia.OpenGL.Controls gl.BindTexture(GL_TEXTURE_2D, 0); gl.BindFramebuffer(GL_FRAMEBUFFER, 0); gl.DeleteFramebuffers(1, new[] { _fb }); + _fb = 0; gl.DeleteRenderbuffers(1, new[] { _depthBuffer }); + _depthBuffer = 0; _attachment?.Dispose(); _attachment = null; _bitmap?.Dispose(); From 5c4179b510c5066e306b826f3c0c83bbdfef6974 Mon Sep 17 00:00:00 2001 From: Imran H Date: Fri, 4 Feb 2022 22:55:32 +0600 Subject: [PATCH 3/5] Fix typo in README --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index f8bbc02d07..96c7937559 100644 --- a/readme.md +++ b/readme.md @@ -5,7 +5,7 @@ ## 📖 About -Avalonia is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, MacOs. Avalonia is mature and production ready. We also have in beta release support for iOS, Andriod and in early stages support for browser via WASM. +Avalonia is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, MacOs. Avalonia is mature and production ready. We also have in beta release support for iOS, Android and in early stages support for browser via WASM. ![image](https://user-images.githubusercontent.com/4672627/152126443-932966cf-57e7-4e77-9be6-62463a66b9f8.png) From 0dca90310568c3e7c0bc36b1790c65700e2c42c7 Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Sat, 5 Feb 2022 17:51:15 +0600 Subject: [PATCH 4/5] Fix AOT incompatible code (#7534) * Fix AOT incompatible code Use code patterns which are AOT-friendly. That improves R2R and Native AOT scenarios --- src/Avalonia.X11/NativeDialogs/Gtk.cs | 2 +- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 6 +++--- src/Windows/Avalonia.Win32/WindowImpl.cs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index 77e410162f..872c824f74 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -63,7 +63,7 @@ namespace Avalonia.X11.NativeDialogs public static IDisposable ConnectSignal(IntPtr obj, string name, T handler) { var handle = GCHandle.Alloc(handler); - var ptr = Marshal.GetFunctionPointerForDelegate((Delegate)(object)handler); + var ptr = Marshal.GetFunctionPointerForDelegate(handler); using (var utf = new Utf8Buffer(name)) { var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0); diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index c74c5fbc01..1809fcf98b 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -862,7 +862,7 @@ namespace Avalonia.Win32.Interop public void Init() { - biSize = (uint)Marshal.SizeOf(this); + biSize = (uint)sizeof(BITMAPINFOHEADER); } } @@ -1521,7 +1521,7 @@ namespace Avalonia.Win32.Interop internal static Version RtlGetVersion() { RTL_OSVERSIONINFOEX v = new RTL_OSVERSIONINFOEX(); - v.dwOSVersionInfoSize = (uint)Marshal.SizeOf(v); + v.dwOSVersionInfoSize = (uint)Marshal.SizeOf(); if (RtlGetVersion(ref v) == 0) { return new Version((int)v.dwMajorVersion, (int)v.dwMinorVersion, (int)v.dwBuildNumber); @@ -1914,7 +1914,7 @@ namespace Avalonia.Win32.Interop get { WINDOWPLACEMENT result = new WINDOWPLACEMENT(); - result.Length = Marshal.SizeOf(result); + result.Length = Marshal.SizeOf(); return result; } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e4f5268285..1e1cd482f4 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -228,7 +228,7 @@ namespace Avalonia.Win32 return new Size(rcWindow.Width, rcWindow.Height) / RenderScaling; } - DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, Marshal.SizeOf(typeof(RECT))); + DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, Marshal.SizeOf()); return new Size(rect.Width, rect.Height) / RenderScaling; } } @@ -337,7 +337,7 @@ namespace Avalonia.Win32 private WindowTransparencyLevel Win8xEnableBlur(WindowTransparencyLevel transparencyLevel) { var accent = new AccentPolicy(); - var accentStructSize = Marshal.SizeOf(accent); + var accentStructSize = Marshal.SizeOf(); if (transparencyLevel == WindowTransparencyLevel.AcrylicBlur) { @@ -392,7 +392,7 @@ namespace Avalonia.Win32 bool canUseAcrylic = Win32Platform.WindowsVersion.Major > 10 || Win32Platform.WindowsVersion.Build >= 19628; var accent = new AccentPolicy(); - var accentStructSize = Marshal.SizeOf(accent); + var accentStructSize = Marshal.SizeOf(); if (transparencyLevel == WindowTransparencyLevel.AcrylicBlur && !canUseAcrylic) { From ecb0de2c32819b2ad158ba86ed1ccef8e6d6cbde Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sat, 5 Feb 2022 16:30:59 +0000 Subject: [PATCH 5/5] Add Support for Overwrite Prompt option in save dialog (#7531) Add support for overwrite prompt option --- src/Avalonia.Controls/SystemDialog.cs | 5 ++ .../ManagedFileChooserViewModel.cs | 14 +++- .../ManagedFileDialogExtensions.cs | 70 +++++++++++++++++++ src/Avalonia.X11/NativeDialogs/Gtk.cs | 3 + .../NativeDialogs/GtkNativeFileDialogs.cs | 8 ++- .../Avalonia.Win32/SystemDialogImpl.cs | 10 +++ 6 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index 4d217d7459..4a9e745e30 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -52,6 +52,11 @@ namespace Avalonia.Controls /// public string? DefaultExtension { get; set; } + /// + /// Gets or sets a value indicating whether to display a warning if the user specifies the name of a file that already exists. + /// + public bool? ShowOverwritePrompt { get; set; } + /// /// Shows the save file dialog. /// diff --git a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs index 28d40f13b9..405a248caf 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs @@ -17,6 +17,7 @@ namespace Avalonia.Dialogs private readonly ManagedFileDialogOptions _options; public event Action CancelRequested; public event Action CompleteRequested; + public event Action OverwritePrompt; public AvaloniaList QuickLinks { get; } = new AvaloniaList(); @@ -39,6 +40,7 @@ namespace Avalonia.Dialogs private bool _scheduledSelectionValidation; private bool _alreadyCancelled = false; private string _defaultExtension; + private bool _overwritePrompt; private CompositeDisposable _disposables; public string Location @@ -167,6 +169,7 @@ namespace Avalonia.Dialogs { _savingFile = true; _defaultExtension = sfd.DefaultExtension; + _overwritePrompt = sfd.ShowOverwritePrompt ?? true; FileName = sfd.InitialFileName; } @@ -360,7 +363,16 @@ namespace Avalonia.Dialogs FileName = Path.ChangeExtension(FileName, _defaultExtension); } - CompleteRequested?.Invoke(new[] { Path.Combine(Location, FileName) }); + var fullName = Path.Combine(Location, FileName); + + if (_overwritePrompt && File.Exists(fullName)) + { + OverwritePrompt?.Invoke(fullName); + } + else + { + CompleteRequested?.Invoke(new[] { fullName }); + } } } else diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index f9e62d905b..1970c5557d 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; @@ -31,6 +32,75 @@ namespace Avalonia.Dialogs dialog.Close(); }; + model.OverwritePrompt += async (filename) => + { + Window overwritePromptDialog = new Window() + { + Title = "Confirm Save As", + SizeToContent = SizeToContent.WidthAndHeight, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Padding = new Thickness(10), + MinWidth = 270 + }; + + string name = Path.GetFileName(filename); + + var panel = new DockPanel() + { + HorizontalAlignment = Layout.HorizontalAlignment.Stretch + }; + + var label = new Label() + { + Content = $"{name} already exists.\nDo you want to replace it?" + }; + + panel.Children.Add(label); + DockPanel.SetDock(label, Dock.Top); + + var buttonPanel = new StackPanel() + { + HorizontalAlignment = Layout.HorizontalAlignment.Right, + Orientation = Layout.Orientation.Horizontal, + Spacing = 10 + }; + + var button = new Button() + { + Content = "Yes", + HorizontalAlignment = Layout.HorizontalAlignment.Right + }; + + button.Click += (sender, args) => + { + result = new string[1] { filename }; + overwritePromptDialog.Close(); + dialog.Close(); + }; + + buttonPanel.Children.Add(button); + + button = new Button() + { + Content = "No", + HorizontalAlignment = Layout.HorizontalAlignment.Right + }; + + button.Click += (sender, args) => + { + overwritePromptDialog.Close(); + }; + + buttonPanel.Children.Add(button); + + panel.Children.Add(buttonPanel); + DockPanel.SetDock(buttonPanel, Dock.Bottom); + + overwritePromptDialog.Content = panel; + + await overwritePromptDialog.ShowDialog(dialog); + }; + model.CancelRequested += dialog.Close; await dialog.ShowDialog(parent); diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index 872c824f74..a0f146afa8 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -179,6 +179,9 @@ namespace Avalonia.X11.NativeDialogs [DllImport(GtkName)] public static extern void gtk_file_chooser_set_select_multiple(IntPtr chooser, bool allow); + + [DllImport(GtkName)] + public static extern void gtk_file_chooser_set_do_overwrite_confirmation(IntPtr chooser, bool do_overwrite_confirmation); [DllImport(GtkName)] public static extern void diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index 6cf6c6f35f..1a6514eb03 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -16,7 +16,7 @@ namespace Avalonia.X11.NativeDialogs { private Task _initialized; private unsafe Task ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action, - bool multiSelect, string initialFileName, IEnumerable filters, string defaultExtension) + bool multiSelect, string initialFileName, IEnumerable filters, string defaultExtension, bool overwritePrompt) { IntPtr dlg; using (var name = new Utf8Buffer(title)) @@ -109,6 +109,8 @@ namespace Avalonia.X11.NativeDialogs gtk_file_chooser_set_filename(dlg, fn); } + gtk_file_chooser_set_do_overwrite_confirmation(dlg, overwritePrompt); + gtk_window_present(dlg); return tcs.Task; } @@ -148,7 +150,7 @@ namespace Avalonia.X11.NativeDialogs (dialog as OpenFileDialog)?.AllowMultiple ?? false, Path.Combine(string.IsNullOrEmpty(dialog.Directory) ? "" : dialog.Directory, string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters, - (dialog as SaveFileDialog)?.DefaultExtension)); + (dialog as SaveFileDialog)?.DefaultExtension, (dialog as SaveFileDialog)?.ShowOverwritePrompt ?? false)); } public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) @@ -160,7 +162,7 @@ namespace Avalonia.X11.NativeDialogs return await await RunOnGlibThread(async () => { var res = await ShowDialog(dialog.Title, platformImpl, - GtkFileChooserAction.SelectFolder, false, dialog.Directory, null, null); + GtkFileChooserAction.SelectFolder, false, dialog.Directory, null, null, false); return res?.FirstOrDefault(); }); } diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index 29844368db..531020698a 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -40,6 +40,16 @@ namespace Avalonia.Win32 { options |= FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT; } + + if (dialog is SaveFileDialog saveFileDialog) + { + var overwritePrompt = saveFileDialog.ShowOverwritePrompt ?? true; + + if (!overwritePrompt) + { + options &= ~FILEOPENDIALOGOPTIONS.FOS_OVERWRITEPROMPT; + } + } frm.SetOptions(options); var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? "";