diff --git a/build/System.Drawing.Common.props b/build/System.Drawing.Common.props index 2b3707d38a..108a0f41e0 100644 --- a/build/System.Drawing.Common.props +++ b/build/System.Drawing.Common.props @@ -1,5 +1,6 @@  - + + diff --git a/readme.md b/readme.md index 5fbc5daaab..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) @@ -26,18 +26,15 @@ Install-Package Avalonia.Desktop ## Showcase Examples of UIs built with Avalonia -![image](https://user-images.githubusercontent.com/4672627/84707589-5b69a880-af35-11ea-87a6-7ad57a31d314.png) + +([Lunacy](https://icons8.com/lunacy)) -([Synfonia](https://github.com/jmacato/Synfonia)) +![image](https://user-images.githubusercontent.com/4672627/152325740-261c27a3-e6f0-4662-bff7-4796d4940e04.png) +([PlasticSCM](https://www.plasticscm.com/)) -![image](https://user-images.githubusercontent.com/4672627/85069644-d8419000-b18a-11ea-8732-be9055bb61fd.PNG) -([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery)) +![image](https://user-images.githubusercontent.com/4672627/152326453-14944c4d-33da-4d50-a268-b87f80927adb.png) +([WasabiWallet](https://www.wasabiwallet.io/)) -![image](https://user-images.githubusercontent.com/4672627/85069659-dc6dad80-b18a-11ea-8375-39ef95315b5c.PNG) -([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery)) - -![image](https://user-images.githubusercontent.com/4672627/84708947-c3b98980-af37-11ea-8c9d-503334615bbf.png) -([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery)) ## JetBrains Rider diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 3d869bb723..7beb496ab5 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -22,8 +22,8 @@ - - + + diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml deleted file mode 100644 index 2ce1469118..0000000000 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml +++ /dev/null @@ -1,45 +0,0 @@ - - - A button control - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog/Pages/ButtonsPage.xaml b/samples/ControlCatalog/Pages/ButtonsPage.xaml new file mode 100644 index 0000000000..7de0872eae --- /dev/null +++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A standard button control + + + + + + + + + + + + + + + + + + + + + + + + + + + + A button control with multiple states: checked, unchecked or indeterminate. + + + + + + + + + + + + + + A button control that raises its Click event repeatedly when it is pressed and held. + + + + + + + + + + + + + + + + A button with primary and secondary parts that can each be pressed separately. The primary part behaves like a Button and the secondary part opens a flyout. + + + + + + + + + + + + + + + + + + + + + + + + A button with primary and secondary parts that can each be pressed separately. The primary part behaves like a ToggleButton with two states and the secondary part opens a flyout. + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs similarity index 90% rename from samples/ControlCatalog/Pages/ButtonPage.xaml.cs rename to samples/ControlCatalog/Pages/ButtonsPage.xaml.cs index 5e555c8c91..5594bab176 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs @@ -3,11 +3,11 @@ using Avalonia.Markup.Xaml; namespace ControlCatalog.Pages { - public class ButtonPage : UserControl + public class ButtonsPage : UserControl { private int repeatButtonClickCount = 0; - public ButtonPage() + public ButtonsPage() { InitializeComponent(); 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/Pages/ToggleSwitchPage.xaml b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml index 4db755b6fc..6afe6dd135 100644 --- a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml +++ b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml @@ -11,7 +11,7 @@ - + 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/Button.cs b/src/Avalonia.Controls/Button.cs index a2efc7fba0..a7a4759182 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -85,8 +85,8 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly StyledProperty FlyoutProperty = - AvaloniaProperty.Register(nameof(Flyout)); + public static readonly StyledProperty FlyoutProperty = + AvaloniaProperty.Register(nameof(Flyout)); private ICommand? _command; private bool _commandCanExecute = true; @@ -186,7 +186,7 @@ namespace Avalonia.Controls /// /// Gets or sets the Flyout that should be shown with this button. /// - public FlyoutBase Flyout + public FlyoutBase? Flyout { get => GetValue(FlyoutProperty); set => SetValue(FlyoutProperty, value); diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index d38cd3a0fd..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) @@ -360,12 +359,12 @@ namespace Avalonia.Controls var selectedIndex = SelectedIndex; if (IsDropDownOpen && selectedIndex != -1) { - var container = ItemContainerGenerator!.ContainerFromIndex(selectedIndex); + var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); if (container == null && SelectedIndex != -1) { ScrollIntoView(Selection.SelectedIndex); - container = ItemContainerGenerator!.ContainerFromIndex(selectedIndex); + container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); } if (container != null && CanFocus(container)) @@ -415,7 +414,7 @@ namespace Avalonia.Controls private void SelectFocusedItem() { - foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator!.Containers) + foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers) { if (dropdownItem.ContainerControl.IsFocused) { @@ -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 70f771ca00..ed8f9efb2e 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -79,7 +79,7 @@ namespace Avalonia.Controls /// /// Gets the for the control. /// - public IItemContainerGenerator? ItemContainerGenerator + public IItemContainerGenerator ItemContainerGenerator { get { @@ -87,13 +87,10 @@ namespace Avalonia.Controls { _itemContainerGenerator = CreateItemContainerGenerator(); - if (_itemContainerGenerator != null) - { - _itemContainerGenerator.ItemTemplate = ItemTemplate; - _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); - _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); - _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e); - } + _itemContainerGenerator.ItemTemplate = ItemTemplate; + _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); + _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); + _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e); } return _itemContainerGenerator; @@ -146,6 +143,8 @@ namespace Avalonia.Controls protected set; } + private protected bool WrapFocus { get; set; } + event EventHandler? IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -240,14 +239,8 @@ namespace Avalonia.Controls /// Creates the for the control. /// /// - /// An or null. + /// An . /// - /// - /// Certain controls such as don't actually create item - /// containers; however they want it to be ItemsControls so that they have an Items - /// property etc. In this case, a derived class can override this method to return null - /// in order to disable the creation of item containers. - /// protected virtual IItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator(this); @@ -324,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/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index c36fbcc51f..bdcff5cd09 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -81,13 +81,13 @@ namespace Avalonia.Controls { var index = SelectedIndex; return (index != -1) ? - (IMenuItem?)ItemContainerGenerator!.ContainerFromIndex(index) : + (IMenuItem?)ItemContainerGenerator.ContainerFromIndex(index) : null; } set { SelectedIndex = value is not null ? - ItemContainerGenerator!.IndexFromContainer(value) : -1; + ItemContainerGenerator.IndexFromContainer(value) : -1; } } @@ -96,7 +96,7 @@ namespace Avalonia.Controls { get { - return ItemContainerGenerator!.Containers + return ItemContainerGenerator.Containers .Select(x => x.ContainerControl) .OfType(); } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 9540ab05f9..185b834052 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -308,12 +308,12 @@ namespace Avalonia.Controls { var index = SelectedIndex; return (index != -1) ? - (IMenuItem?)ItemContainerGenerator!.ContainerFromIndex(index) : + (IMenuItem?)ItemContainerGenerator.ContainerFromIndex(index) : null; } set { - SelectedIndex = value is not null ? ItemContainerGenerator!.IndexFromContainer(value) : -1; + SelectedIndex = value is not null ? ItemContainerGenerator.IndexFromContainer(value) : -1; } } @@ -322,7 +322,7 @@ namespace Avalonia.Controls { get { - return ItemContainerGenerator!.Containers + return ItemContainerGenerator.Containers .Select(x => x.ContainerControl) .OfType(); } diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 8883c9e8ce..3d1e7eb5a8 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -170,7 +170,7 @@ namespace Avalonia.Controls.Presenters { if (fromIndex != toIndex) { - var generator = ItemContainerGenerator!; + var generator = ItemContainerGenerator; IControl? from = null; IControl? to = 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/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 1c60c0d906..8629af5243 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Reactive.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; @@ -26,14 +24,14 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); - public static readonly StyledProperty SelectionBrushProperty = - AvaloniaProperty.Register(nameof(SelectionBrushProperty)); + public static readonly StyledProperty SelectionBrushProperty = + AvaloniaProperty.Register(nameof(SelectionBrushProperty)); - public static readonly StyledProperty SelectionForegroundBrushProperty = - AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); + public static readonly StyledProperty SelectionForegroundBrushProperty = + AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); - public static readonly StyledProperty CaretBrushProperty = - AvaloniaProperty.Register(nameof(CaretBrushProperty)); + public static readonly StyledProperty CaretBrushProperty = + AvaloniaProperty.Register(nameof(CaretBrushProperty)); public static readonly DirectProperty SelectionStartProperty = TextBox.SelectionStartProperty.AddOwner( @@ -48,8 +46,8 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty TextProperty = + AvaloniaProperty.RegisterDirect( nameof(Text), o => o.Text, (o, v) => o.Text = v); @@ -77,16 +75,14 @@ namespace Avalonia.Controls.Presenters private int _selectionStart; private int _selectionEnd; private bool _caretBlink; - private string _text; + private string? _text; private TextLayout? _textLayout; - private Size _constraint = Size.Infinity; + private Size _constraint; private CharacterHit _lastCharacterHit; private Rect _caretBounds; private Point _navigationPosition; - private ScrollViewer? _scrollViewer; - static TextPresenter() { AffectsRender(CaretBrushProperty, SelectionBrushProperty); @@ -114,7 +110,7 @@ namespace Avalonia.Controls.Presenters /// Gets or sets the text. /// [Content] - public string Text + public string? Text { get => _text; set => SetAndRaise(TextProperty, ref _text, value); @@ -186,7 +182,7 @@ namespace Avalonia.Controls.Presenters /// /// Gets the used to render the text. /// - public TextLayout? TextLayout + public TextLayout TextLayout { get { @@ -230,19 +226,19 @@ namespace Avalonia.Controls.Presenters set => SetValue(RevealPasswordProperty, value); } - public IBrush SelectionBrush + public IBrush? SelectionBrush { get => GetValue(SelectionBrushProperty); set => SetValue(SelectionBrushProperty, value); } - public IBrush SelectionForegroundBrush + public IBrush? SelectionForegroundBrush { get => GetValue(SelectionForegroundBrushProperty); set => SetValue(SelectionForegroundBrushProperty, value); } - public IBrush CaretBrush + public IBrush? CaretBrush { get => GetValue(CaretBrushProperty); set => SetValue(CaretBrushProperty, value); @@ -284,20 +280,14 @@ namespace Avalonia.Controls.Presenters /// /// /// A object. - private TextLayout? CreateTextLayoutInternal(Size constraint, string text, Typeface typeface, + private TextLayout CreateTextLayoutInternal(Size constraint, string? text, Typeface typeface, IReadOnlyList>? textStyleOverrides) { var foreground = Foreground; - - if (foreground == null) - { - return null; - } - var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width; var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height; - var textLayout = new TextLayout(text ?? string.Empty, typeface, FontSize, foreground, TextAlignment, + var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment, TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides, flowDirection: FlowDirection); @@ -317,11 +307,6 @@ namespace Avalonia.Controls.Presenters context.FillRectangle(background, new Rect(Bounds.Size)); } - if (TextLayout == null) - { - return; - } - var top = 0d; var left = 0.0; @@ -346,17 +331,11 @@ namespace Avalonia.Controls.Presenters public override void Render(DrawingContext context) { - if (double.IsPositiveInfinity (_constraint.Width)) - { - _constraint = _scrollViewer?.Viewport ?? Size.Infinity; - - InvalidateTextLayout(); - } - var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; + var selectionBrush = SelectionBrush; - if (selectionStart != selectionEnd && TextLayout != null) + if (selectionStart != selectionEnd && selectionBrush != null) { var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; @@ -365,7 +344,7 @@ namespace Avalonia.Controls.Presenters foreach (var rect in rects) { - context.FillRectangle(SelectionBrush, rect); + context.FillRectangle(selectionBrush, rect); } } @@ -431,38 +410,40 @@ namespace Avalonia.Controls.Presenters internal void CaretChanged() { - if (this.GetVisualParent() != null) + if (this.GetVisualParent() == null) { - if (_caretTimer.IsEnabled) - { - _caretBlink = true; - _caretTimer.Stop(); - _caretTimer.Start(); - InvalidateVisual(); - } - else - { - _caretTimer.Start(); - InvalidateVisual(); - _caretTimer.Stop(); - } + return; + } - if (IsMeasureValid) - { - this.BringIntoView(_caretBounds); - } - else - { - // The measure is currently invalid so there's no point trying to bring the - // current char into view until a measure has been carried out as the scroll - // viewer extents may not be up-to-date. - Dispatcher.UIThread.Post( - () => - { - this.BringIntoView(_caretBounds); - }, - DispatcherPriority.Render); - } + if (_caretTimer.IsEnabled) + { + _caretBlink = true; + _caretTimer.Stop(); + _caretTimer.Start(); + InvalidateVisual(); + } + else + { + _caretTimer.Start(); + InvalidateVisual(); + _caretTimer.Stop(); + } + + if (IsMeasureValid) + { + this.BringIntoView(_caretBounds); + } + else + { + // The measure is currently invalid so there's no point trying to bring the + // current char into view until a measure has been carried out as the scroll + // viewer extents may not be up-to-date. + Dispatcher.UIThread.Post( + () => + { + this.BringIntoView(_caretBounds); + }, + DispatcherPriority.Render); } } @@ -470,9 +451,9 @@ namespace Avalonia.Controls.Presenters /// Creates the used to render the text. /// /// A object. - protected virtual TextLayout? CreateTextLayout() + protected virtual TextLayout CreateTextLayout() { - TextLayout? result; + TextLayout result; var text = Text; @@ -517,14 +498,25 @@ namespace Avalonia.Controls.Presenters protected override Size MeasureOverride(Size availableSize) { - if (!double.IsInfinity(availableSize.Width) && availableSize != _constraint) + _textLayout = null; + + _constraint = availableSize; + + return TextLayout.Size; + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (!double.IsInfinity(_constraint.Width)) { - _constraint = availableSize; - - InvalidateTextLayout(); + return base.ArrangeOverride(finalSize); } + + _constraint = finalSize; + + _textLayout = null; - return TextLayout?.Size ?? default; + return base.ArrangeOverride(finalSize); } private int CoerceCaretIndex(int value) @@ -537,16 +529,12 @@ namespace Avalonia.Controls.Presenters private void CaretTimerTick(object? sender, EventArgs e) { _caretBlink = !_caretBlink; + InvalidateVisual(); } public void MoveCaretToTextPosition(int textPosition, bool trailingEdge = false) { - if (TextLayout == null) - { - return; - } - var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, trailingEdge); var textLine = TextLayout.TextLines[lineIndex]; @@ -569,29 +557,23 @@ namespace Avalonia.Controls.Presenters } _navigationPosition = _caretBounds.Position; + + CaretChanged(); } public void MoveCaretToPoint(Point point) { - if (TextLayout == null) - { - return; - } - var hit = TextLayout.HitTestPoint(point); UpdateCaret(hit.CharacterHit); _navigationPosition = _caretBounds.Position; + + CaretChanged(); } public void MoveCaretVertical(LogicalDirection direction = LogicalDirection.Forward) { - if (TextLayout == null) - { - return; - } - var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(CaretIndex, _lastCharacterHit.TrailingLength > 0); if (lineIndex < 0) @@ -629,14 +611,23 @@ namespace Avalonia.Controls.Presenters MoveCaretToPoint(new Point(currentX, currentY)); _navigationPosition = navigationPosition.WithY(_caretBounds.Y); + + CaretChanged(); } public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward) { - if (TextLayout == null) + if (Text is null) { return; } + + if (FlowDirection == FlowDirection.RightToLeft) + { + direction = direction == LogicalDirection.Forward ? + LogicalDirection.Backward : + LogicalDirection.Forward; + } var characterHit = _lastCharacterHit; var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -709,15 +700,12 @@ namespace Avalonia.Controls.Presenters UpdateCaret(characterHit); _navigationPosition = _caretBounds.Position; + + CaretChanged(); } private void UpdateCaret(CharacterHit characterHit) { - if (TextLayout == null) - { - return; - } - _lastCharacterHit = characterHit; var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -743,9 +731,7 @@ namespace Avalonia.Controls.Presenters CaretBoundsChanged?.Invoke(this, EventArgs.Empty); } - - CaretChanged(); - + SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); } @@ -754,19 +740,10 @@ namespace Avalonia.Controls.Presenters return _caretBounds; } - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - - _scrollViewer = this.FindAncestorOfType(); - } - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - _scrollViewer = null; - _caretTimer.Stop(); _caretTimer.Tick -= CaretTimerTick; diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 31de6f6398..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; + } } /// @@ -908,7 +928,7 @@ namespace Avalonia.Controls.Primitives { MarkContainerSelected( container, - Selection.IsSelected(ItemContainerGenerator!.IndexFromContainer(container))); + Selection.IsSelected(ItemContainerGenerator.IndexFromContainer(container))); } } } diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs new file mode 100644 index 0000000000..f1d07b2679 --- /dev/null +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -0,0 +1,485 @@ +using System; +using System.Windows.Input; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; + +namespace Avalonia.Controls +{ + /// + /// A button with primary and secondary parts that can each be pressed separately. + /// The primary part behaves like a and the secondary part opens a flyout. + /// + [PseudoClasses(pcFlyoutOpen, pcPressed)] + public class SplitButton : ContentControl, ICommandSource + { + protected const string pcChecked = ":checked"; + protected const string pcPressed = ":pressed"; + protected const string pcFlyoutOpen = ":flyout-open"; + + /// + /// Raised when the user presses the primary part of the . + /// + public event EventHandler Click + { + add => AddHandler(ClickEvent, value); + remove => RemoveHandler(ClickEvent, value); + } + + /// + /// Defines the event. + /// + public static readonly RoutedEvent ClickEvent = + RoutedEvent.Register(nameof(Click), RoutingStrategies.Bubble); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CommandProperty = + Button.CommandProperty.AddOwner( + splitButton => splitButton.Command, + (splitButton, command) => splitButton.Command = command); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CommandParameterProperty = + Button.CommandParameterProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly StyledProperty FlyoutProperty = + Button.FlyoutProperty.AddOwner(); + + private ICommand? _Command; + + private Button? _primaryButton = null; + private Button? _secondaryButton = null; + + private bool _commandCanExecute = true; + private bool _isAttachedToLogicalTree = false; + private bool _isFlyoutOpen = false; + private bool _isKeyboardPressed = false; + + private IDisposable? _flyoutPropertyChangedDisposable; + + //////////////////////////////////////////////////////////////////////// + // Constructor / Destructors + //////////////////////////////////////////////////////////////////////// + + /// + /// Initializes a new instance of the class. + /// + public SplitButton() + { + } + + //////////////////////////////////////////////////////////////////////// + // Properties + //////////////////////////////////////////////////////////////////////// + + /// + /// Gets or sets the invoked when the primary part is pressed. + /// + public ICommand? Command + { + get => _Command; + set => SetAndRaise(CommandProperty, ref _Command, value); + } + + /// + /// Gets or sets a parameter to be passed to the . + /// + public object? CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } + + /// + /// Gets or sets the that is shown when the secondary part is pressed. + /// + public FlyoutBase? Flyout + { + get => GetValue(FlyoutProperty); + set => SetValue(FlyoutProperty, value); + } + + /// + /// Gets a value indicating whether the button is currently checked. + /// + /// + /// This property exists only for the derived and is + /// unused (set to false) within . Doing this allows the + /// two controls to share a default style. + /// + internal virtual bool InternalIsChecked => false; + + /// + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + + //////////////////////////////////////////////////////////////////////// + // Methods + //////////////////////////////////////////////////////////////////////// + + /// + void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); + + /// + private void CanExecuteChanged(object? sender, EventArgs e) + { + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } + } + + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// + protected void UpdatePseudoClasses() + { + PseudoClasses.Set(pcFlyoutOpen, _isFlyoutOpen); + PseudoClasses.Set(pcPressed, _isKeyboardPressed); + PseudoClasses.Set(pcChecked, InternalIsChecked); + } + + /// + /// Opens the secondary button's flyout. + /// + protected void OpenFlyout() + { + if (Flyout != null) + { + Flyout.ShowAt(this); + } + } + + /// + /// Closes the secondary button's flyout. + /// + protected void CloseFlyout() + { + if (Flyout != null) + { + Flyout.Hide(); + } + } + + /// + /// Registers all flyout events. + /// + /// The flyout to connect events to. + private void RegisterFlyoutEvents(FlyoutBase? flyout) + { + if (flyout != null) + { + flyout.Opened += Flyout_Opened; + flyout.Closed += Flyout_Closed; + + _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(FlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged); + } + } + + /// + /// Explicitly unregisters all flyout events. + /// + /// The flyout to disconnect events from. + private void UnregisterFlyoutEvents(FlyoutBase? flyout) + { + if (flyout != null) + { + flyout.Opened -= Flyout_Opened; + flyout.Closed -= Flyout_Closed; + + _flyoutPropertyChangedDisposable?.Dispose(); + _flyoutPropertyChangedDisposable = null; + } + } + + /// + /// Explicitly unregisters all events related to the two buttons in OnApplyTemplate(). + /// + private void UnregisterEvents() + { + if (_primaryButton != null) + { + _primaryButton.Click -= PrimaryButton_Click; + } + + if (_secondaryButton != null) + { + _secondaryButton.Click -= SecondaryButton_Click; + } + } + + //////////////////////////////////////////////////////////////////////// + // OnEvent Overridable Methods + //////////////////////////////////////////////////////////////////////// + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + UnregisterEvents(); + UnregisterFlyoutEvents(Flyout); + + _primaryButton = e.NameScope.Find 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.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 019bdfb98a..e0ec23b31f 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -164,7 +164,7 @@ namespace Avalonia.Controls else { var container = SelectedItem as IContentControl ?? - ItemContainerGenerator!.ContainerFromIndex(SelectedIndex) as IContentControl; + ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as IContentControl; SelectedContentTemplate = container?.ContentTemplate; SelectedContent = container?.Content; } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c0f5980321..ed012bd8b1 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -492,13 +492,6 @@ namespace Avalonia.Controls return measuredSize.Inflate(padding); } - protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - base.OnAttachedToLogicalTree(e); - - InvalidateTextLayout(); - } - private static bool IsValidMaxLines(int maxLines) => maxLines >= 0; private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index dc6b8ffa4c..4ec0c4c5e1 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -540,14 +540,19 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); + + _imClient.SetPresenter(_presenter, this); + + if (IsFocused) + { + _presenter?.ShowCaret(); + } } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - _imClient.SetPresenter(_presenter, this); - if (IsFocused) { _presenter?.ShowCaret(); @@ -607,6 +612,8 @@ namespace Avalonia.Controls } UpdateCommandStates(); + + _imClient.SetPresenter(_presenter, this); _presenter?.ShowCaret(); } @@ -625,6 +632,8 @@ namespace Avalonia.Controls UpdateCommandStates(); _presenter?.HideCaret(); + + _imClient.SetPresenter(null, null); } protected override void OnTextInput(TextInputEventArgs e) @@ -930,6 +939,10 @@ namespace Avalonia.Controls { SelectionEnd = _presenter.CaretIndex; } + else + { + SelectionStart = SelectionEnd = _presenter.CaretIndex; + } break; } @@ -948,6 +961,10 @@ namespace Avalonia.Controls { SelectionEnd = _presenter.CaretIndex; } + else + { + SelectionStart = SelectionEnd = _presenter.CaretIndex; + } break; } @@ -1273,7 +1290,7 @@ namespace Avalonia.Controls { caretIndex = 0; } - else if (_presenter.TextLayout is not null) + else { var lines = _presenter.TextLayout.TextLines; var pos = 0; @@ -1308,7 +1325,7 @@ namespace Avalonia.Controls { caretIndex = text.Length; } - else if (_presenter.TextLayout is not null) + else { var lines = _presenter.TextLayout.TextLines; var pos = 0; diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 9e0a31ad64..9a276e74d2 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -79,7 +79,7 @@ namespace Avalonia.Controls /// Gets the for the tree view. /// public new ITreeItemContainerGenerator ItemContainerGenerator => - (ITreeItemContainerGenerator)base.ItemContainerGenerator!; + (ITreeItemContainerGenerator)base.ItemContainerGenerator; /// /// Gets or sets a value indicating whether to automatically scroll to newly selected items. diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 79e8a199f9..20c0ed386d 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -89,7 +89,7 @@ namespace Avalonia.Controls /// Gets the for the tree view. /// public new ITreeItemContainerGenerator ItemContainerGenerator => - (ITreeItemContainerGenerator)base.ItemContainerGenerator!; + (ITreeItemContainerGenerator)base.ItemContainerGenerator; /// protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator(); 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/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 819a203d62..4a5cb0fd3f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -84,7 +84,8 @@ namespace Avalonia.Diagnostics.ViewModels c.GetObservable(Control.ContextMenuProperty), c.GetObservable(FlyoutBase.AttachedFlyoutProperty), c.GetObservable(ToolTipDiagnostics.ToolTipProperty), - (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip) => + c.GetObservable(Button.FlyoutProperty), + (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip, ButtonFlyout) => { if (ContextMenu != null) //Note: ContextMenus are special since all the items are added as visual children. @@ -100,6 +101,9 @@ namespace Avalonia.Diagnostics.ViewModels if (ToolTip != null) return GetPopupHostObservable(ToolTip, "ToolTip"); + if (ButtonFlyout != null) + return GetPopupHostObservable(ButtonFlyout, "Flyout"); + return Observable.Return(null); }) .Switch(), diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 7a894d96fb..a3cff7f3d3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -147,6 +147,7 @@ namespace Avalonia.Diagnostics.Views ProcessProperty(control, ContextMenuProperty); ProcessProperty(control, FlyoutBase.AttachedFlyoutProperty); ProcessProperty(control, ToolTipDiagnostics.ToolTipProperty); + ProcessProperty(control, Button.FlyoutProperty); } return popupRoots; 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.Input/ICommandSource.cs b/src/Avalonia.Input/ICommandSource.cs index 410b3a2e47..a6b5189440 100644 --- a/src/Avalonia.Input/ICommandSource.cs +++ b/src/Avalonia.Input/ICommandSource.cs @@ -20,12 +20,11 @@ namespace Avalonia.Input /// object? CommandParameter { get; } - /// - /// Bor the behavior CanExecuteChanged + /// Called for the CanExecuteChanged event when changes are detected. /// - /// - /// + /// The event sender. + /// The event args. void CanExecuteChanged(object sender, System.EventArgs e); /// diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 34d2038d66..3749947da9 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -273,7 +273,7 @@ namespace Avalonia.Input } private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers, IReadOnlyList? intermediatePoints) + KeyModifiers inputModifiers, Lazy?>? intermediatePoints) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 40495a2f0a..0604d09dc4 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -11,7 +11,7 @@ namespace Avalonia.Input private readonly IVisual? _rootVisual; private readonly Point _rootVisualPosition; private readonly PointerPointProperties _properties; - private readonly IReadOnlyList? _previousPoints; + private Lazy?>? _previousPoints; public PointerEventArgs(RoutedEvent routedEvent, IInteractive? source, @@ -38,7 +38,7 @@ namespace Avalonia.Input ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, - IReadOnlyList? previousPoints) + Lazy?>? previousPoints) : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) { _previousPoints = previousPoints; @@ -121,13 +121,14 @@ namespace Avalonia.Input /// public IReadOnlyList GetIntermediatePoints(IVisual? relativeTo) { - if (_previousPoints == null || _previousPoints.Count == 0) + var previousPoints = _previousPoints?.Value; + if (previousPoints == null || previousPoints.Count == 0) return new[] { GetCurrentPoint(relativeTo) }; - var points = new PointerPoint[_previousPoints.Count + 1]; - for (var c = 0; c < _previousPoints.Count; c++) + var points = new PointerPoint[previousPoints.Count + 1]; + for (var c = 0; c < previousPoints.Count; c++) { - var pt = _previousPoints[c]; - points[c] = new PointerPoint(Pointer, GetPosition(pt, relativeTo), _properties); + var pt = previousPoints[c]; + points[c] = new PointerPoint(Pointer, GetPosition(pt.Position, relativeTo), _properties); } points[points.Length - 1] = GetCurrentPoint(relativeTo); diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 1fe19d9c55..c157fa059c 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -33,6 +33,8 @@ namespace Avalonia.Input.Raw /// public class RawPointerEventArgs : RawInputEventArgs { + private RawPointerPoint _point; + /// /// Initializes a new instance of the class. /// @@ -58,11 +60,50 @@ namespace Avalonia.Input.Raw Type = type; InputModifiers = inputModifiers; } + + /// + /// Initializes a new instance of the class. + /// + /// The associated device. + /// The event timestamp. + /// The root from which the event originates. + /// The type of the event. + /// The point properties and position, in client DIPs. + /// The input modifiers. + public RawPointerEventArgs( + IInputDevice device, + ulong timestamp, + IInputRoot root, + RawPointerEventType type, + RawPointerPoint point, + RawInputModifiers inputModifiers) + : base(device, timestamp, root) + { + Contract.Requires(device != null); + Contract.Requires(root != null); + + Point = point; + Type = type; + InputModifiers = inputModifiers; + } + + /// + /// Gets the pointer properties and position, in client DIPs. + /// + public RawPointerPoint Point + { + get => _point; + set => _point = value; + } /// /// Gets the mouse position, in client DIPs. /// - public Point Position { get; set; } + public Point Position + { + get => _point.Position; + set => _point.Position = value; + } /// /// Gets the type of the event. @@ -78,6 +119,19 @@ namespace Avalonia.Input.Raw /// Points that were traversed by a pointer since the previous relevant event, /// only valid for Move and TouchUpdate /// - public IReadOnlyList? IntermediatePoints { get; set; } + public Lazy?>? IntermediatePoints { get; set; } + } + + public struct RawPointerPoint + { + /// + /// Pointer position, in client DIPs. + /// + public Point Position { get; set; } + + public RawPointerPoint() + { + Position = default; + } } } 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(); diff --git a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml new file mode 100644 index 0000000000..ce20a1a165 --- /dev/null +++ b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml @@ -0,0 +1,271 @@ + + + + + + + + + + + + 24 + 24 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 702fcc7c7a..009b56852a 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -57,6 +57,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index a1f423c059..5b86de02d5 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -556,6 +556,33 @@ 1 32 + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index f75af76144..eb68270354 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -550,6 +550,33 @@ 1 32 + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index f07fc6f2e0..857d70084c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -150,7 +150,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 51836255ef..065278521e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -56,6 +56,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml new file mode 100644 index 0000000000..e0dd0180b3 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml @@ -0,0 +1,247 @@ + + + + + + + + Hello + + + + + Hello + + + + + + + + 32 + 32 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index f01ef886f7..64f0eaab53 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -35,10 +35,10 @@ namespace Avalonia.Media.TextFormatting /// The maximum number of text lines. /// The text style overrides. public TextLayout( - string text, + string? text, Typeface typeface, double fontSize, - IBrush foreground, + IBrush? foreground, TextAlignment textAlignment = TextAlignment.Left, TextWrapping textWrapping = TextWrapping.NoWrap, TextTrimming textTrimming = TextTrimming.None, @@ -194,6 +194,7 @@ namespace Avalonia.Media.TextFormatting var currentY = 0d; var currentPosition = 0; + var currentRect = Rect.Empty; foreach (var textLine in TextLines) { @@ -209,7 +210,10 @@ namespace Avalonia.Media.TextFormatting //The whole line is covered. if (currentPosition >= start && start + length > currentPosition + textLine.TextRange.Length) { - result.Add(new Rect(textLine.Start, currentY, textLine.WidthIncludingTrailingWhitespace, textLine.Height)); + currentRect = new Rect(textLine.Start, currentY, textLine.WidthIncludingTrailingWhitespace, + textLine.Height); + + result.Add(currentRect); currentY += textLine.Height; currentPosition += textLine.TextRange.Length; @@ -317,13 +321,23 @@ namespace Avalonia.Media.TextFormatting var width = endX - startX; - result.Add(new Rect(startX, currentY, width, textLine.Height)); - + if (result.Count > 0 && MathUtilities.AreClose(currentRect.Top, currentY) && + MathUtilities.AreClose(currentRect.Right, startX)) + { + result[result.Count - 1] = currentRect.WithWidth(currentRect.Width + width); + } + else + { + currentRect = new Rect(startX, currentY, width, textLine.Height); + + result.Add(currentRect); + } + if (currentRun.ShapedBuffer.IsLeftToRight) { if (nextRun != null) { - if (nextRun.Text.Start > currentRun.Text.Start) + if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= start + length) { break; } @@ -397,7 +411,7 @@ namespace Avalonia.Media.TextFormatting return new TextHitTestResult(); } - characterHit = currentLine.GetNextCaretCharacterHit(new CharacterHit(currentLine.TextRange.End)); + characterHit = currentLine.GetCharacterHitFromDistance(point.X); return GetHitTestResult(currentLine, characterHit, point); } @@ -424,7 +438,7 @@ namespace Avalonia.Media.TextFormatting continue; } - if (charIndex >= textLine.Start && charIndex <= textLine.TextRange.End + (trailingEdge ? 1 : 0)) + if (charIndex >= textLine.TextRange.Start && charIndex <= textLine.TextRange.End + (trailingEdge ? 1 : 0)) { return index; } @@ -478,7 +492,7 @@ namespace Avalonia.Media.TextFormatting /// The height of each line of text. /// private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, - IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, + IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight) { var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs index 9bbc4a8a9d..130d0e9c39 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Avalonia.Media.TextFormatting { @@ -210,10 +211,10 @@ namespace Avalonia.Media.TextFormatting switch (textAlignment) { case TextAlignment.Center: - return (paragraphWidth - width) / 2; + return Math.Max(0, (paragraphWidth - width) / 2); case TextAlignment.Right: - return paragraphWidth - widthIncludingTrailingWhitespace; + return Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace); default: return 0; @@ -223,13 +224,13 @@ namespace Avalonia.Media.TextFormatting switch (textAlignment) { case TextAlignment.Center: - return (paragraphWidth - width) / 2; + return Math.Max(0, (paragraphWidth - width) / 2); case TextAlignment.Right: return 0; default: - return paragraphWidth - widthIncludingTrailingWhitespace; + return Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace); } } } diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index 77e410162f..a0f146afa8 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); @@ -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/Avalonia.X11/SMLib.cs b/src/Avalonia.X11/SMLib.cs index 6ffcad60bf..e2b39cfcff 100644 --- a/src/Avalonia.X11/SMLib.cs +++ b/src/Avalonia.X11/SMLib.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Runtime.InteropServices; @@ -7,20 +8,19 @@ namespace Avalonia.X11 { private const string LibSm = "libSM.so.6"; - [DllImport(LibSm, CharSet = CharSet.Ansi)] + [DllImport(LibSm, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] public static extern IntPtr SmcOpenConnection( - [MarshalAs(UnmanagedType.LPWStr)] string networkId, + [MarshalAs(UnmanagedType.LPStr)] string? networkId, IntPtr content, int xsmpMajorRev, int xsmpMinorRev, - ulong mask, + nuint mask, ref SmcCallbacks callbacks, - [MarshalAs(UnmanagedType.LPWStr)] [Out] - out string previousId, - [MarshalAs(UnmanagedType.LPWStr)] [Out] - out string clientIdRet, + [MarshalAs(UnmanagedType.LPStr)] string? previousId, + ref IntPtr clientIdRet, int errorLength, - [Out] char[] errorStringRet); + [Out] byte[] errorStringRet + ); [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] public static extern int SmcCloseConnection( diff --git a/src/Avalonia.X11/X11PlatformLifetimeEvents.cs b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs index 06e1d8879f..1a17a018e8 100644 --- a/src/Avalonia.X11/X11PlatformLifetimeEvents.cs +++ b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Text; using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Threading; @@ -14,10 +15,10 @@ namespace Avalonia.X11 internal unsafe class X11PlatformLifetimeEvents : IDisposable, IPlatformLifetimeEventsImpl { private readonly AvaloniaX11Platform _platform; - private const ulong SmcSaveYourselfProcMask = 1L; - private const ulong SmcDieProcMask = 2L; - private const ulong SmcSaveCompleteProcMask = 4L; - private const ulong SmcShutdownCancelledProcMask = 8L; + private const nuint SmcSaveYourselfProcMask = 1; + private const nuint SmcDieProcMask = 2; + private const nuint SmcSaveCompleteProcMask = 4; + private const nuint SmcShutdownCancelledProcMask = 8; private static readonly ConcurrentDictionary s_nativeToManagedMapper = new ConcurrentDictionary(); @@ -62,24 +63,24 @@ namespace Avalonia.X11 return; } - var errorBuf = new char[255]; - - var smcConn = SMLib.SmcOpenConnection(null!, + byte[] errorBuf = new byte[255]; + IntPtr clientIdRet = IntPtr.Zero; + var smcConn = SMLib.SmcOpenConnection(null, IntPtr.Zero, 1, 0, SmcSaveYourselfProcMask | SmcSaveCompleteProcMask | SmcShutdownCancelledProcMask | SmcDieProcMask, ref s_callbacks, - out _, - out _, + null, + ref clientIdRet, errorBuf.Length, errorBuf); if (smcConn == IntPtr.Zero) { Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, - $"SMLib/ICELib reported a new error: {new string(errorBuf)}"); + $"SMLib/ICELib reported a new error: {Encoding.ASCII.GetString(errorBuf)}"); return; } diff --git a/src/Shared/RawEventGrouping.cs b/src/Shared/RawEventGrouping.cs index 25b4b41e56..084593ffc6 100644 --- a/src/Shared/RawEventGrouping.cs +++ b/src/Shared/RawEventGrouping.cs @@ -53,7 +53,7 @@ internal class RawEventGrouper : IDisposable _eventCallback?.Invoke(ev); - if (ev is RawPointerEventArgs { IntermediatePoints: PooledList list }) + if (ev is RawPointerEventArgs { IntermediatePoints.Value: PooledList list }) list.Dispose(); if (Dispatcher.UIThread.HasJobsWithPriority(DispatcherPriority.Input + 1)) @@ -110,10 +110,14 @@ internal class RawEventGrouper : IDisposable AddToQueue(args); } + private static IReadOnlyList GetPooledList() => new PooledList(); + private static readonly Func> s_getPooledListDelegate = GetPooledList; + private static void MergeEvents(RawPointerEventArgs last, RawPointerEventArgs current) { - last.IntermediatePoints ??= new PooledList(); - ((PooledList)last.IntermediatePoints).Add(last.Position); + + last.IntermediatePoints ??= new Lazy?>(s_getPooledListDelegate); + ((PooledList)last.IntermediatePoints.Value!).Add(new RawPointerPoint { Position = last.Position }); last.Position = current.Position; last.Timestamp = current.Timestamp; last.InputModifiers = current.InputModifiers; 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/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index 29844368db..410ba55627 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 ?? ""; @@ -79,7 +89,16 @@ namespace Avalonia.Win32 } } - frm.Show(hWnd); + var showResult = frm.Show(hWnd); + + if ((uint)showResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED) + { + return result; + } + else if ((uint)showResult != (uint)UnmanagedMethods.HRESULT.S_OK) + { + throw new Win32Exception(showResult); + } if (openDialog?.AllowMultiple == true) { @@ -108,10 +127,6 @@ namespace Avalonia.Win32 } catch (COMException ex) { - if ((uint)ex.HResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED) - { - return result; - } throw new Win32Exception(ex.HResult); } })!; @@ -151,7 +166,17 @@ namespace Avalonia.Win32 } } - frm.Show(hWnd); + var showResult = frm.Show(hWnd); + + if ((uint)showResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED) + { + return result; + } + else if ((uint)showResult != (uint)UnmanagedMethods.HRESULT.S_OK) + { + throw new Win32Exception(showResult); + } + if (frm.Result is not null) { result = GetAbsoluteFilePath(frm.Result); @@ -161,10 +186,6 @@ namespace Avalonia.Win32 } catch (COMException ex) { - if ((uint)ex.HResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED) - { - return result; - } throw new Win32Exception(ex.HResult); } }); diff --git a/src/Windows/Avalonia.Win32/Win32Com/win32.idl b/src/Windows/Avalonia.Win32/Win32Com/win32.idl index 904ac41ff2..54e9aa583f 100644 --- a/src/Windows/Avalonia.Win32/Win32Com/win32.idl +++ b/src/Windows/Avalonia.Win32/Win32Com/win32.idl @@ -112,7 +112,7 @@ interface IShellItemArray : IUnknown interface IModalWindow : IUnknown { [local] - HRESULT Show( + int Show( [in, unique] HWND hwndOwner); } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e4f5268285..d1945e6c85 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; } } @@ -265,7 +265,7 @@ namespace Avalonia.Win32 { if (IsWindowVisible(_hwnd)) { - ShowWindow(value, true); + ShowWindow(value, value != WindowState.Minimized); // If the window is minimized, it shouldn't be activated } _showWindowState = value; @@ -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) { @@ -973,7 +973,7 @@ namespace Avalonia.Win32 { case WindowState.Minimized: newWindowProperties.IsFullScreen = false; - command = activate ? ShowWindowCommand.Minimize : ShowWindowCommand.ShowMinNoActive; + command = ShowWindowCommand.Minimize; break; case WindowState.Maximized: newWindowProperties.IsFullScreen = false; 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); + } + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index b525931061..595bcd4009 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -735,27 +735,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = layout.TextLines[0]; - var firstStart = textLine.GetDistanceFromCharacterHit(new CharacterHit(5, 1)); - - var firstEnd = textLine.GetDistanceFromCharacterHit(new CharacterHit(0)); - - var secondStart = textLine.GetDistanceFromCharacterHit(new CharacterHit(6)); + var start = textLine.GetDistanceFromCharacterHit(new CharacterHit(5, 1)); - var secondEnd = textLine.GetDistanceFromCharacterHit(new CharacterHit(6, 1)); + var end = textLine.GetDistanceFromCharacterHit(new CharacterHit(6, 1)); var rects = layout.HitTestTextRange(0, 7).ToArray(); - Assert.Equal(2, rects.Length); - - var firstExpected = rects[0]; - - Assert.Equal(firstExpected.Left, firstStart); - Assert.Equal(firstExpected.Right, firstEnd); + Assert.Equal(1, rects.Length); - var secondExpected = rects[1]; + var expected = rects[0]; - Assert.Equal(secondExpected.Left, secondStart); - Assert.Equal(secondExpected.Right, secondEnd); + Assert.Equal(expected.Left, start); + Assert.Equal(expected.Right, end); } }