Browse Source

Merge branch 'master' into robloo-SplitButton

pull/7422/head
Max Katz 5 years ago
committed by GitHub
parent
commit
07578273e7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      build/System.Drawing.Common.props
  2. 23
      readme.md
  3. 160
      samples/ControlCatalog/Pages/ComboBoxPage.xaml
  4. 2
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  5. 4
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  6. 21
      samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs
  7. 7
      samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
  8. 37
      src/Avalonia.Controls/ComboBox.cs
  9. 25
      src/Avalonia.Controls/ItemsControl.cs
  10. 6
      src/Avalonia.Controls/MenuBase.cs
  11. 6
      src/Avalonia.Controls/MenuItem.cs
  12. 2
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  13. 15
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  14. 199
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  15. 22
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  16. 5
      src/Avalonia.Controls/SystemDialog.cs
  17. 2
      src/Avalonia.Controls/TabControl.cs
  18. 7
      src/Avalonia.Controls/TextBlock.cs
  19. 25
      src/Avalonia.Controls/TextBox.cs
  20. 2
      src/Avalonia.Controls/TreeView.cs
  21. 2
      src/Avalonia.Controls/TreeViewItem.cs
  22. 9
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  23. 10149
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json
  24. 51
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package.json
  25. 17
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js
  26. 14
      src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs
  27. 70
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  28. 2
      src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs
  29. 32
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  30. 11
      src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
  31. 5
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  32. 8
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  33. 16
      src/Avalonia.X11/SMLib.cs
  34. 21
      src/Avalonia.X11/X11PlatformLifetimeEvents.cs
  35. 6
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  36. 10
      src/Windows/Avalonia.Win32/SystemDialogImpl.cs
  37. 10
      src/Windows/Avalonia.Win32/WindowImpl.cs
  38. 49
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  39. 21
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

3
build/System.Drawing.Common.props

@ -1,5 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="4.5.0" /> <PackageReference Condition="'$(TargetFramework)'!='netstandard2.0'" Include="System.Drawing.Common" Version="6.0.0" />
<PackageReference Condition="'$(TargetFramework)'=='netstandard2.0'" Include="System.Drawing.Common" Version="4.5.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

23
readme.md

@ -3,13 +3,11 @@
<br /> <br />
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg) [![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg)
## 📖 About AvaloniaUI ## 📖 About
Avalonia is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows via .NET Framework and .NET Core, Linux via Xorg, macOS. Avalonia is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. 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.
<img src="https://user-images.githubusercontent.com/6759207/84751662-7c79da00-afc5-11ea-8780-dda28db70b76.png" width="700" /> ![image](https://user-images.githubusercontent.com/4672627/152126443-932966cf-57e7-4e77-9be6-62463a66b9f8.png)
([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery))
To see the status of some of our features, please see our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. [Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia! To see the status of some of our features, please see our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. [Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia!
@ -28,18 +26,15 @@ Install-Package Avalonia.Desktop
## Showcase ## Showcase
Examples of UIs built with Avalonia Examples of UIs built with Avalonia
![image](https://user-images.githubusercontent.com/4672627/84707589-5b69a880-af35-11ea-87a6-7ad57a31d314.png) <video src="https://user-images.githubusercontent.com/4672627/152325602-28df36ec-6444-44a6-aebe-90ad52c8f27a.mp4"></video>
([Lunacy](https://icons8.com/lunacy))
([Synfonia](https://github.com/jmacato/Synfonia))
![image](https://user-images.githubusercontent.com/4672627/85069644-d8419000-b18a-11ea-8732-be9055bb61fd.PNG) ![image](https://user-images.githubusercontent.com/4672627/152325740-261c27a3-e6f0-4662-bff7-4796d4940e04.png)
([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery)) ([PlasticSCM](https://www.plasticscm.com/))
![image](https://user-images.githubusercontent.com/4672627/85069659-dc6dad80-b18a-11ea-8375-39ef95315b5c.PNG) ![image](https://user-images.githubusercontent.com/4672627/152326453-14944c4d-33da-4d50-a268-b87f80927adb.png)
([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery)) ([WasabiWallet](https://www.wasabiwallet.io/))
![image](https://user-images.githubusercontent.com/4672627/84708947-c3b98980-af37-11ea-8c9d-503334615bbf.png)
([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery))
## JetBrains Rider ## JetBrains Rider

160
samples/ControlCatalog/Pages/ComboBoxPage.xaml

@ -1,77 +1,95 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.ComboBoxPage"
x:Class="ControlCatalog.Pages.ComboBoxPage" xmlns="https://github.com/avaloniaui"
xmlns:sys="using:System" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:col="using:System.Collections"> xmlns:col="using:System.Collections"
<StackPanel Orientation="Vertical" Spacing="4"> xmlns:sys="using:System">
<TextBlock Classes="h2">A drop-down list.</TextBlock> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h2">A drop-down list.</TextBlock>
<WrapPanel HorizontalAlignment="Center" Margin="0 16 0 0" <StackPanel
MaxWidth="660"> Margin="0,16,0,0"
<WrapPanel.Styles> HorizontalAlignment="Center"
<Style Selector="ComboBox"> Orientation="Horizontal"
<Setter Property="Width" Value="250" /> Spacing="8">
<Setter Property="Margin" Value="10" /> <WrapPanel
</Style> MaxWidth="660"
</WrapPanel.Styles> Margin="0,16,0,0"
<ComboBox PlaceholderText="Pick an Item"> HorizontalAlignment="Center">
<ComboBoxItem>Inline Items</ComboBoxItem> <WrapPanel.Styles>
<ComboBoxItem>Inline Item 2</ComboBoxItem> <Style Selector="ComboBox">
<ComboBoxItem>Inline Item 3</ComboBoxItem> <Setter Property="Width" Value="250" />
<ComboBoxItem>Inline Item 4</ComboBoxItem> <Setter Property="Margin" Value="10" />
</ComboBox> </Style>
</WrapPanel.Styles>
<ComboBox> <ComboBox PlaceholderText="Pick an Item" WrapSelection="{Binding WrapSelection}">
<ComboBox.Items> <ComboBoxItem>Inline Items</ComboBoxItem>
<col:ArrayList> <ComboBoxItem>Inline Item 2</ComboBoxItem>
<x:Null /> <ComboBoxItem>Inline Item 3</ComboBoxItem>
<sys:String>Hello</sys:String> <ComboBoxItem>Inline Item 4</ComboBoxItem>
<sys:String>World</sys:String> </ComboBox>
</col:ArrayList>
</ComboBox.Items>
<ComboBox.ItemTemplate>
<DataTemplate>
<Panel>
<TextBlock Text="{Binding}" />
<TextBlock Text="Null object" IsVisible="{Binding Converter={x:Static ObjectConverters.IsNull}}" />
</Panel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox SelectedIndex="0"> <ComboBox WrapSelection="{Binding WrapSelection}">
<ComboBoxItem> <ComboBox.Items>
<Panel> <col:ArrayList>
<Rectangle Fill="{DynamicResource SystemAccentColor}"/> <x:Null />
<TextBlock Margin="8">Control Items</TextBlock> <sys:String>Hello</sys:String>
</Panel> <sys:String>World</sys:String>
</ComboBoxItem> </col:ArrayList>
<ComboBoxItem> </ComboBox.Items>
<Ellipse Width="50" Height="50" Fill="Yellow"/> <ComboBox.ItemTemplate>
</ComboBoxItem> <DataTemplate>
<ComboBoxItem> <Panel>
<TextBox Text="TextBox"/> <TextBlock Text="{Binding}" />
</ComboBoxItem> <TextBlock IsVisible="{Binding Converter={x:Static ObjectConverters.IsNull}}" Text="Null object" />
</ComboBox> </Panel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox x:Name="fontComboBox" SelectedIndex="0"> <ComboBox SelectedIndex="0" WrapSelection="{Binding WrapSelection}">
<ComboBox.ItemTemplate> <ComboBoxItem>
<DataTemplate> <Panel>
<TextBlock Text="{Binding Name}" FontFamily="{Binding}" /> <Rectangle Fill="{DynamicResource SystemAccentColor}" />
</DataTemplate> <TextBlock Margin="8">Control Items</TextBlock>
</ComboBox.ItemTemplate> </Panel>
</ComboBox> </ComboBoxItem>
<ComboBoxItem>
<ComboBox PlaceholderText="Pick an Item"> <Ellipse
<ComboBoxItem>Inline Items</ComboBoxItem> Width="50"
<ComboBoxItem>Inline Item 2</ComboBoxItem> Height="50"
<ComboBoxItem>Inline Item 3</ComboBoxItem> Fill="Yellow" />
<ComboBoxItem>Inline Item 4</ComboBoxItem> </ComboBoxItem>
<DataValidationErrors.Error> <ComboBoxItem>
<sys:Exception /> <TextBox Text="TextBox" />
</DataValidationErrors.Error> </ComboBoxItem>
</ComboBox> </ComboBox>
</WrapPanel>
</StackPanel> <ComboBox
x:Name="fontComboBox"
SelectedIndex="0"
WrapSelection="{Binding WrapSelection}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock FontFamily="{Binding}" Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox PlaceholderText="Pick an Item" WrapSelection="{Binding WrapSelection}">
<ComboBoxItem>Inline Items</ComboBoxItem>
<ComboBoxItem>Inline Item 2</ComboBoxItem>
<ComboBoxItem>Inline Item 3</ComboBoxItem>
<ComboBoxItem>Inline Item 4</ComboBoxItem>
<DataValidationErrors.Error>
<sys:Exception />
</DataValidationErrors.Error>
</ComboBox>
</WrapPanel>
<CheckBox IsChecked="{Binding WrapSelection}">WrapSelection</CheckBox>
</StackPanel>
</StackPanel>
</UserControl> </UserControl>

2
samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs

@ -2,6 +2,7 @@ using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages
{ {
@ -10,6 +11,7 @@ namespace ControlCatalog.Pages
public ComboBoxPage() public ComboBoxPage()
{ {
this.InitializeComponent(); this.InitializeComponent();
DataContext = new ComboBoxPageViewModel();
} }
private void InitializeComponent() private void InitializeComponent()

4
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -21,6 +21,7 @@
<CheckBox IsChecked="{Binding Toggle}">Toggle</CheckBox> <CheckBox IsChecked="{Binding Toggle}">Toggle</CheckBox>
<CheckBox IsChecked="{Binding AlwaysSelected}">AlwaysSelected</CheckBox> <CheckBox IsChecked="{Binding AlwaysSelected}">AlwaysSelected</CheckBox>
<CheckBox IsChecked="{Binding AutoScrollToSelectedItem}">AutoScrollToSelectedItem</CheckBox> <CheckBox IsChecked="{Binding AutoScrollToSelectedItem}">AutoScrollToSelectedItem</CheckBox>
<CheckBox IsChecked="{Binding WrapSelection}">WrapSelection</CheckBox>
</StackPanel> </StackPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="4"> <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="4">
<Button Command="{Binding AddItemCommand}">Add</Button> <Button Command="{Binding AddItemCommand}">Add</Button>
@ -30,6 +31,7 @@
<ListBox Items="{Binding Items}" <ListBox Items="{Binding Items}"
Selection="{Binding Selection}" Selection="{Binding Selection}"
AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}" AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}"
SelectionMode="{Binding SelectionMode^}"/> SelectionMode="{Binding SelectionMode^}"
WrapSelection="{Binding WrapSelection}"/>
</DockPanel> </DockPanel>
</UserControl> </UserControl>

21
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);
}
}
}

7
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@ -14,6 +14,7 @@ namespace ControlCatalog.ViewModels
private bool _toggle; private bool _toggle;
private bool _alwaysSelected; private bool _alwaysSelected;
private bool _autoScrollToSelectedItem = true; private bool _autoScrollToSelectedItem = true;
private bool _wrapSelection;
private int _counter; private int _counter;
private IObservable<SelectionMode> _selectionMode; private IObservable<SelectionMode> _selectionMode;
@ -85,6 +86,12 @@ namespace ControlCatalog.ViewModels
set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value); set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value);
} }
public bool WrapSelection
{
get => _wrapSelection;
set => this.RaiseAndSetIfChanged(ref _wrapSelection, value);
}
public MiniCommand AddItemCommand { get; } public MiniCommand AddItemCommand { get; }
public MiniCommand RemoveItemCommand { get; } public MiniCommand RemoveItemCommand { get; }
public MiniCommand SelectRandomItemCommand { get; } public MiniCommand SelectRandomItemCommand { get; }

37
src/Avalonia.Controls/ComboBox.cs

@ -10,9 +10,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -91,7 +89,7 @@ namespace Avalonia.Controls
{ {
ItemsPanelProperty.OverrideDefaultValue<ComboBox>(DefaultPanel); ItemsPanelProperty.OverrideDefaultValue<ComboBox>(DefaultPanel);
FocusableProperty.OverrideDefaultValue<ComboBox>(true); FocusableProperty.OverrideDefaultValue<ComboBox>(true);
SelectedItemProperty.Changed.AddClassHandler<ComboBox>((x,e) => x.SelectedItemChanged(e)); SelectedItemProperty.Changed.AddClassHandler<ComboBox>((x, e) => x.SelectedItemChanged(e));
KeyDownEvent.AddClassHandler<ComboBox>((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel); KeyDownEvent.AddClassHandler<ComboBox>((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel);
IsTextSearchEnabledProperty.OverrideDefaultValue<ComboBox>(true); IsTextSearchEnabledProperty.OverrideDefaultValue<ComboBox>(true);
} }
@ -221,8 +219,9 @@ namespace Avalonia.Controls
e.Handled = true; 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 && 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)); var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c));
if (firstChild != null) if (firstChild != null)
@ -360,12 +359,12 @@ namespace Avalonia.Controls
var selectedIndex = SelectedIndex; var selectedIndex = SelectedIndex;
if (IsDropDownOpen && selectedIndex != -1) if (IsDropDownOpen && selectedIndex != -1)
{ {
var container = ItemContainerGenerator!.ContainerFromIndex(selectedIndex); var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
if (container == null && SelectedIndex != -1) if (container == null && SelectedIndex != -1)
{ {
ScrollIntoView(Selection.SelectedIndex); ScrollIntoView(Selection.SelectedIndex);
container = ItemContainerGenerator!.ContainerFromIndex(selectedIndex); container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
} }
if (container != null && CanFocus(container)) if (container != null && CanFocus(container))
@ -415,7 +414,7 @@ namespace Avalonia.Controls
private void SelectFocusedItem() private void SelectFocusedItem()
{ {
foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator!.Containers) foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers)
{ {
if (dropdownItem.ContainerControl.IsFocused) if (dropdownItem.ContainerControl.IsFocused)
{ {
@ -430,7 +429,18 @@ namespace Avalonia.Controls
int next = SelectedIndex + 1; int next = SelectedIndex + 1;
if (next >= ItemCount) if (next >= ItemCount)
next = 0; {
if (WrapSelection == true)
{
next = 0;
}
else
{
return;
}
}
SelectedIndex = next; SelectedIndex = next;
} }
@ -440,7 +450,16 @@ namespace Avalonia.Controls
int prev = SelectedIndex - 1; int prev = SelectedIndex - 1;
if (prev < 0) if (prev < 0)
prev = ItemCount - 1; {
if (WrapSelection == true)
{
prev = ItemCount - 1;
}
else
{
return;
}
}
SelectedIndex = prev; SelectedIndex = prev;
} }

25
src/Avalonia.Controls/ItemsControl.cs

@ -79,7 +79,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets the <see cref="IItemContainerGenerator"/> for the control. /// Gets the <see cref="IItemContainerGenerator"/> for the control.
/// </summary> /// </summary>
public IItemContainerGenerator? ItemContainerGenerator public IItemContainerGenerator ItemContainerGenerator
{ {
get get
{ {
@ -87,13 +87,10 @@ namespace Avalonia.Controls
{ {
_itemContainerGenerator = CreateItemContainerGenerator(); _itemContainerGenerator = CreateItemContainerGenerator();
if (_itemContainerGenerator != null) _itemContainerGenerator.ItemTemplate = ItemTemplate;
{ _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
_itemContainerGenerator.ItemTemplate = ItemTemplate; _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
_itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e);
_itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
_itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e);
}
} }
return _itemContainerGenerator; return _itemContainerGenerator;
@ -146,6 +143,8 @@ namespace Avalonia.Controls
protected set; protected set;
} }
private protected bool WrapFocus { get; set; }
event EventHandler<ChildIndexChangedEventArgs>? IChildIndexProvider.ChildIndexChanged event EventHandler<ChildIndexChangedEventArgs>? IChildIndexProvider.ChildIndexChanged
{ {
add => _childIndexChanged += value; add => _childIndexChanged += value;
@ -240,14 +239,8 @@ namespace Avalonia.Controls
/// Creates the <see cref="ItemContainerGenerator"/> for the control. /// Creates the <see cref="ItemContainerGenerator"/> for the control.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// An <see cref="IItemContainerGenerator"/> or null. /// An <see cref="IItemContainerGenerator"/>.
/// </returns> /// </returns>
/// <remarks>
/// Certain controls such as <see cref="TabControl"/> 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.
/// </remarks>
protected virtual IItemContainerGenerator CreateItemContainerGenerator() protected virtual IItemContainerGenerator CreateItemContainerGenerator()
{ {
return new ItemContainerGenerator(this); return new ItemContainerGenerator(this);
@ -324,7 +317,7 @@ namespace Avalonia.Controls
{ {
if (current.VisualParent == container && current is IInputElement inputElement) 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) if (next != null)
{ {

6
src/Avalonia.Controls/MenuBase.cs

@ -81,13 +81,13 @@ namespace Avalonia.Controls
{ {
var index = SelectedIndex; var index = SelectedIndex;
return (index != -1) ? return (index != -1) ?
(IMenuItem?)ItemContainerGenerator!.ContainerFromIndex(index) : (IMenuItem?)ItemContainerGenerator.ContainerFromIndex(index) :
null; null;
} }
set set
{ {
SelectedIndex = value is not null ? SelectedIndex = value is not null ?
ItemContainerGenerator!.IndexFromContainer(value) : -1; ItemContainerGenerator.IndexFromContainer(value) : -1;
} }
} }
@ -96,7 +96,7 @@ namespace Avalonia.Controls
{ {
get get
{ {
return ItemContainerGenerator!.Containers return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl) .Select(x => x.ContainerControl)
.OfType<IMenuItem>(); .OfType<IMenuItem>();
} }

6
src/Avalonia.Controls/MenuItem.cs

@ -308,12 +308,12 @@ namespace Avalonia.Controls
{ {
var index = SelectedIndex; var index = SelectedIndex;
return (index != -1) ? return (index != -1) ?
(IMenuItem?)ItemContainerGenerator!.ContainerFromIndex(index) : (IMenuItem?)ItemContainerGenerator.ContainerFromIndex(index) :
null; null;
} }
set 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 get
{ {
return ItemContainerGenerator!.Containers return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl) .Select(x => x.ContainerControl)
.OfType<IMenuItem>(); .OfType<IMenuItem>();
} }

2
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@ -170,7 +170,7 @@ namespace Avalonia.Controls.Presenters
{ {
if (fromIndex != toIndex) if (fromIndex != toIndex)
{ {
var generator = ItemContainerGenerator!; var generator = ItemContainerGenerator;
IControl? from = null; IControl? from = null;
IControl? to = null; IControl? to = null;

15
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -233,11 +233,6 @@ namespace Avalonia.Controls.Presenters
var itemIndex = generator.IndexFromContainer(from); var itemIndex = generator.IndexFromContainer(from);
var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical; var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical;
if (itemIndex == -1)
{
return null;
}
var newItemIndex = -1; var newItemIndex = -1;
switch (direction) switch (direction)
@ -250,6 +245,16 @@ namespace Avalonia.Controls.Presenters
newItemIndex = ItemCount - 1; newItemIndex = ItemCount - 1;
break; break;
default:
if (itemIndex == -1)
{
return null;
}
break;
}
switch (direction)
{
case NavigationDirection.Up: case NavigationDirection.Up:
if (vertical) if (vertical)
{ {

199
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -1,7 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Reactive.Linq;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -26,14 +24,14 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<char> PasswordCharProperty = public static readonly StyledProperty<char> PasswordCharProperty =
AvaloniaProperty.Register<TextPresenter, char>(nameof(PasswordChar)); AvaloniaProperty.Register<TextPresenter, char>(nameof(PasswordChar));
public static readonly StyledProperty<IBrush> SelectionBrushProperty = public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush>(nameof(SelectionBrushProperty)); AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(SelectionBrushProperty));
public static readonly StyledProperty<IBrush> SelectionForegroundBrushProperty = public static readonly StyledProperty<IBrush?> SelectionForegroundBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush>(nameof(SelectionForegroundBrushProperty)); AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(SelectionForegroundBrushProperty));
public static readonly StyledProperty<IBrush> CaretBrushProperty = public static readonly StyledProperty<IBrush?> CaretBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush>(nameof(CaretBrushProperty)); AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(CaretBrushProperty));
public static readonly DirectProperty<TextPresenter, int> SelectionStartProperty = public static readonly DirectProperty<TextPresenter, int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<TextPresenter>( TextBox.SelectionStartProperty.AddOwner<TextPresenter>(
@ -48,8 +46,8 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Defines the <see cref="Text"/> property. /// Defines the <see cref="Text"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<TextPresenter, string> TextProperty = public static readonly DirectProperty<TextPresenter, string?> TextProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, string>( AvaloniaProperty.RegisterDirect<TextPresenter, string?>(
nameof(Text), nameof(Text),
o => o.Text, o => o.Text,
(o, v) => o.Text = v); (o, v) => o.Text = v);
@ -77,16 +75,14 @@ namespace Avalonia.Controls.Presenters
private int _selectionStart; private int _selectionStart;
private int _selectionEnd; private int _selectionEnd;
private bool _caretBlink; private bool _caretBlink;
private string _text; private string? _text;
private TextLayout? _textLayout; private TextLayout? _textLayout;
private Size _constraint = Size.Infinity; private Size _constraint;
private CharacterHit _lastCharacterHit; private CharacterHit _lastCharacterHit;
private Rect _caretBounds; private Rect _caretBounds;
private Point _navigationPosition; private Point _navigationPosition;
private ScrollViewer? _scrollViewer;
static TextPresenter() static TextPresenter()
{ {
AffectsRender<TextPresenter>(CaretBrushProperty, SelectionBrushProperty); AffectsRender<TextPresenter>(CaretBrushProperty, SelectionBrushProperty);
@ -114,7 +110,7 @@ namespace Avalonia.Controls.Presenters
/// Gets or sets the text. /// Gets or sets the text.
/// </summary> /// </summary>
[Content] [Content]
public string Text public string? Text
{ {
get => _text; get => _text;
set => SetAndRaise(TextProperty, ref _text, value); set => SetAndRaise(TextProperty, ref _text, value);
@ -186,7 +182,7 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text. /// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
public TextLayout? TextLayout public TextLayout TextLayout
{ {
get get
{ {
@ -230,19 +226,19 @@ namespace Avalonia.Controls.Presenters
set => SetValue(RevealPasswordProperty, value); set => SetValue(RevealPasswordProperty, value);
} }
public IBrush SelectionBrush public IBrush? SelectionBrush
{ {
get => GetValue(SelectionBrushProperty); get => GetValue(SelectionBrushProperty);
set => SetValue(SelectionBrushProperty, value); set => SetValue(SelectionBrushProperty, value);
} }
public IBrush SelectionForegroundBrush public IBrush? SelectionForegroundBrush
{ {
get => GetValue(SelectionForegroundBrushProperty); get => GetValue(SelectionForegroundBrushProperty);
set => SetValue(SelectionForegroundBrushProperty, value); set => SetValue(SelectionForegroundBrushProperty, value);
} }
public IBrush CaretBrush public IBrush? CaretBrush
{ {
get => GetValue(CaretBrushProperty); get => GetValue(CaretBrushProperty);
set => SetValue(CaretBrushProperty, value); set => SetValue(CaretBrushProperty, value);
@ -284,20 +280,14 @@ namespace Avalonia.Controls.Presenters
/// <param name="typeface"></param> /// <param name="typeface"></param>
/// <param name="textStyleOverrides"></param> /// <param name="textStyleOverrides"></param>
/// <returns>A <see cref="TextLayout"/> object.</returns> /// <returns>A <see cref="TextLayout"/> object.</returns>
private TextLayout? CreateTextLayoutInternal(Size constraint, string text, Typeface typeface, private TextLayout CreateTextLayoutInternal(Size constraint, string? text, Typeface typeface,
IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides) IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides)
{ {
var foreground = Foreground; var foreground = Foreground;
if (foreground == null)
{
return null;
}
var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width; var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width;
var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height; 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, TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
flowDirection: FlowDirection); flowDirection: FlowDirection);
@ -317,11 +307,6 @@ namespace Avalonia.Controls.Presenters
context.FillRectangle(background, new Rect(Bounds.Size)); context.FillRectangle(background, new Rect(Bounds.Size));
} }
if (TextLayout == null)
{
return;
}
var top = 0d; var top = 0d;
var left = 0.0; var left = 0.0;
@ -346,17 +331,11 @@ namespace Avalonia.Controls.Presenters
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
if (double.IsPositiveInfinity (_constraint.Width))
{
_constraint = _scrollViewer?.Viewport ?? Size.Infinity;
InvalidateTextLayout();
}
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
var selectionBrush = SelectionBrush;
if (selectionStart != selectionEnd && TextLayout != null) if (selectionStart != selectionEnd && selectionBrush != null)
{ {
var start = Math.Min(selectionStart, selectionEnd); var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start; var length = Math.Max(selectionStart, selectionEnd) - start;
@ -365,7 +344,7 @@ namespace Avalonia.Controls.Presenters
foreach (var rect in rects) foreach (var rect in rects)
{ {
context.FillRectangle(SelectionBrush, rect); context.FillRectangle(selectionBrush, rect);
} }
} }
@ -431,38 +410,40 @@ namespace Avalonia.Controls.Presenters
internal void CaretChanged() internal void CaretChanged()
{ {
if (this.GetVisualParent() != null) if (this.GetVisualParent() == null)
{ {
if (_caretTimer.IsEnabled) return;
{ }
_caretBlink = true;
_caretTimer.Stop();
_caretTimer.Start();
InvalidateVisual();
}
else
{
_caretTimer.Start();
InvalidateVisual();
_caretTimer.Stop();
}
if (IsMeasureValid) if (_caretTimer.IsEnabled)
{ {
this.BringIntoView(_caretBounds); _caretBlink = true;
} _caretTimer.Stop();
else _caretTimer.Start();
{ InvalidateVisual();
// 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 else
// viewer extents may not be up-to-date. {
Dispatcher.UIThread.Post( _caretTimer.Start();
() => InvalidateVisual();
{ _caretTimer.Stop();
this.BringIntoView(_caretBounds); }
},
DispatcherPriority.Render); 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 <see cref="TextLayout"/> used to render the text. /// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
/// <returns>A <see cref="TextLayout"/> object.</returns> /// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual TextLayout? CreateTextLayout() protected virtual TextLayout CreateTextLayout()
{ {
TextLayout? result; TextLayout result;
var text = Text; var text = Text;
@ -517,14 +498,25 @@ namespace Avalonia.Controls.Presenters
protected override Size MeasureOverride(Size availableSize) 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; return base.ArrangeOverride(finalSize);
InvalidateTextLayout();
} }
_constraint = finalSize;
_textLayout = null;
return TextLayout?.Size ?? default; return base.ArrangeOverride(finalSize);
} }
private int CoerceCaretIndex(int value) private int CoerceCaretIndex(int value)
@ -537,16 +529,12 @@ namespace Avalonia.Controls.Presenters
private void CaretTimerTick(object? sender, EventArgs e) private void CaretTimerTick(object? sender, EventArgs e)
{ {
_caretBlink = !_caretBlink; _caretBlink = !_caretBlink;
InvalidateVisual(); InvalidateVisual();
} }
public void MoveCaretToTextPosition(int textPosition, bool trailingEdge = false) public void MoveCaretToTextPosition(int textPosition, bool trailingEdge = false)
{ {
if (TextLayout == null)
{
return;
}
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, trailingEdge); var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, trailingEdge);
var textLine = TextLayout.TextLines[lineIndex]; var textLine = TextLayout.TextLines[lineIndex];
@ -569,29 +557,23 @@ namespace Avalonia.Controls.Presenters
} }
_navigationPosition = _caretBounds.Position; _navigationPosition = _caretBounds.Position;
CaretChanged();
} }
public void MoveCaretToPoint(Point point) public void MoveCaretToPoint(Point point)
{ {
if (TextLayout == null)
{
return;
}
var hit = TextLayout.HitTestPoint(point); var hit = TextLayout.HitTestPoint(point);
UpdateCaret(hit.CharacterHit); UpdateCaret(hit.CharacterHit);
_navigationPosition = _caretBounds.Position; _navigationPosition = _caretBounds.Position;
CaretChanged();
} }
public void MoveCaretVertical(LogicalDirection direction = LogicalDirection.Forward) public void MoveCaretVertical(LogicalDirection direction = LogicalDirection.Forward)
{ {
if (TextLayout == null)
{
return;
}
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(CaretIndex, _lastCharacterHit.TrailingLength > 0); var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(CaretIndex, _lastCharacterHit.TrailingLength > 0);
if (lineIndex < 0) if (lineIndex < 0)
@ -629,14 +611,23 @@ namespace Avalonia.Controls.Presenters
MoveCaretToPoint(new Point(currentX, currentY)); MoveCaretToPoint(new Point(currentX, currentY));
_navigationPosition = navigationPosition.WithY(_caretBounds.Y); _navigationPosition = navigationPosition.WithY(_caretBounds.Y);
CaretChanged();
} }
public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward) public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward)
{ {
if (TextLayout == null) if (Text is null)
{ {
return; return;
} }
if (FlowDirection == FlowDirection.RightToLeft)
{
direction = direction == LogicalDirection.Forward ?
LogicalDirection.Backward :
LogicalDirection.Forward;
}
var characterHit = _lastCharacterHit; var characterHit = _lastCharacterHit;
var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
@ -709,15 +700,12 @@ namespace Avalonia.Controls.Presenters
UpdateCaret(characterHit); UpdateCaret(characterHit);
_navigationPosition = _caretBounds.Position; _navigationPosition = _caretBounds.Position;
CaretChanged();
} }
private void UpdateCaret(CharacterHit characterHit) private void UpdateCaret(CharacterHit characterHit)
{ {
if (TextLayout == null)
{
return;
}
_lastCharacterHit = characterHit; _lastCharacterHit = characterHit;
var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
@ -743,9 +731,7 @@ namespace Avalonia.Controls.Presenters
CaretBoundsChanged?.Invoke(this, EventArgs.Empty); CaretBoundsChanged?.Invoke(this, EventArgs.Empty);
} }
CaretChanged();
SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex);
} }
@ -754,19 +740,10 @@ namespace Avalonia.Controls.Presenters
return _caretBounds; return _caretBounds;
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_scrollViewer = this.FindAncestorOfType<ScrollViewer>();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnDetachedFromVisualTree(e); base.OnDetachedFromVisualTree(e);
_scrollViewer = null;
_caretTimer.Stop(); _caretTimer.Stop();
_caretTimer.Tick -= CaretTimerTick; _caretTimer.Tick -= CaretTimerTick;

22
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -114,6 +114,12 @@ namespace Avalonia.Controls.Primitives
"SelectionChanged", "SelectionChanged",
RoutingStrategies.Bubble); RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="WrapSelection"/> property.
/// </summary>
public static readonly StyledProperty<bool> WrapSelectionProperty =
AvaloniaProperty.Register<ItemsControl, bool>(nameof(WrapSelection), defaultValue: false);
private static readonly IList Empty = Array.Empty<object>(); private static readonly IList Empty = Array.Empty<object>();
private string _textSearchTerm = string.Empty; private string _textSearchTerm = string.Empty;
private DispatcherTimer? _textSearchTimer; private DispatcherTimer? _textSearchTimer;
@ -321,6 +327,16 @@ namespace Avalonia.Controls.Primitives
set { SetValue(IsTextSearchEnabledProperty, value); } set { SetValue(IsTextSearchEnabledProperty, value); }
} }
/// <summary>
/// Gets or sets a value which indicates whether to wrap around when the first
/// or last item is reached.
/// </summary>
public bool WrapSelection
{
get { return GetValue(WrapSelectionProperty); }
set { SetValue(WrapSelectionProperty, value); }
}
/// <summary> /// <summary>
/// Gets or sets the selection mode. /// Gets or sets the selection mode.
/// </summary> /// </summary>
@ -580,6 +596,10 @@ namespace Avalonia.Controls.Primitives
var newValue = change.NewValue.GetValueOrDefault<SelectionMode>(); var newValue = change.NewValue.GetValueOrDefault<SelectionMode>();
_selection.SingleSelect = !newValue.HasAllFlags(SelectionMode.Multiple); _selection.SingleSelect = !newValue.HasAllFlags(SelectionMode.Multiple);
} }
else if (change.Property == WrapSelectionProperty)
{
WrapFocus = WrapSelection;
}
} }
/// <summary> /// <summary>
@ -908,7 +928,7 @@ namespace Avalonia.Controls.Primitives
{ {
MarkContainerSelected( MarkContainerSelected(
container, container,
Selection.IsSelected(ItemContainerGenerator!.IndexFromContainer(container))); Selection.IsSelected(ItemContainerGenerator.IndexFromContainer(container)));
} }
} }
} }

5
src/Avalonia.Controls/SystemDialog.cs

@ -52,6 +52,11 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public string? DefaultExtension { get; set; } public string? DefaultExtension { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to display a warning if the user specifies the name of a file that already exists.
/// </summary>
public bool? ShowOverwritePrompt { get; set; }
/// <summary> /// <summary>
/// Shows the save file dialog. /// Shows the save file dialog.
/// </summary> /// </summary>

2
src/Avalonia.Controls/TabControl.cs

@ -164,7 +164,7 @@ namespace Avalonia.Controls
else else
{ {
var container = SelectedItem as IContentControl ?? var container = SelectedItem as IContentControl ??
ItemContainerGenerator!.ContainerFromIndex(SelectedIndex) as IContentControl; ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as IContentControl;
SelectedContentTemplate = container?.ContentTemplate; SelectedContentTemplate = container?.ContentTemplate;
SelectedContent = container?.Content; SelectedContent = container?.Content;
} }

7
src/Avalonia.Controls/TextBlock.cs

@ -492,13 +492,6 @@ namespace Avalonia.Controls
return measuredSize.Inflate(padding); 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 IsValidMaxLines(int maxLines) => maxLines >= 0;
private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0; private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;

25
src/Avalonia.Controls/TextBox.cs

@ -540,14 +540,19 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter"); _presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
_imClient.SetPresenter(_presenter, this);
if (IsFocused)
{
_presenter?.ShowCaret();
}
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnAttachedToVisualTree(e); base.OnAttachedToVisualTree(e);
_imClient.SetPresenter(_presenter, this);
if (IsFocused) if (IsFocused)
{ {
_presenter?.ShowCaret(); _presenter?.ShowCaret();
@ -607,6 +612,8 @@ namespace Avalonia.Controls
} }
UpdateCommandStates(); UpdateCommandStates();
_imClient.SetPresenter(_presenter, this);
_presenter?.ShowCaret(); _presenter?.ShowCaret();
} }
@ -625,6 +632,8 @@ namespace Avalonia.Controls
UpdateCommandStates(); UpdateCommandStates();
_presenter?.HideCaret(); _presenter?.HideCaret();
_imClient.SetPresenter(null, null);
} }
protected override void OnTextInput(TextInputEventArgs e) protected override void OnTextInput(TextInputEventArgs e)
@ -930,6 +939,10 @@ namespace Avalonia.Controls
{ {
SelectionEnd = _presenter.CaretIndex; SelectionEnd = _presenter.CaretIndex;
} }
else
{
SelectionStart = SelectionEnd = _presenter.CaretIndex;
}
break; break;
} }
@ -948,6 +961,10 @@ namespace Avalonia.Controls
{ {
SelectionEnd = _presenter.CaretIndex; SelectionEnd = _presenter.CaretIndex;
} }
else
{
SelectionStart = SelectionEnd = _presenter.CaretIndex;
}
break; break;
} }
@ -1273,7 +1290,7 @@ namespace Avalonia.Controls
{ {
caretIndex = 0; caretIndex = 0;
} }
else if (_presenter.TextLayout is not null) else
{ {
var lines = _presenter.TextLayout.TextLines; var lines = _presenter.TextLayout.TextLines;
var pos = 0; var pos = 0;
@ -1308,7 +1325,7 @@ namespace Avalonia.Controls
{ {
caretIndex = text.Length; caretIndex = text.Length;
} }
else if (_presenter.TextLayout is not null) else
{ {
var lines = _presenter.TextLayout.TextLines; var lines = _presenter.TextLayout.TextLines;
var pos = 0; var pos = 0;

2
src/Avalonia.Controls/TreeView.cs

@ -79,7 +79,7 @@ namespace Avalonia.Controls
/// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view. /// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view.
/// </summary> /// </summary>
public new ITreeItemContainerGenerator ItemContainerGenerator => public new ITreeItemContainerGenerator ItemContainerGenerator =>
(ITreeItemContainerGenerator)base.ItemContainerGenerator!; (ITreeItemContainerGenerator)base.ItemContainerGenerator;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether to automatically scroll to newly selected items. /// Gets or sets a value indicating whether to automatically scroll to newly selected items.

2
src/Avalonia.Controls/TreeViewItem.cs

@ -89,7 +89,7 @@ namespace Avalonia.Controls
/// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view. /// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view.
/// </summary> /// </summary>
public new ITreeItemContainerGenerator ItemContainerGenerator => public new ITreeItemContainerGenerator ItemContainerGenerator =>
(ITreeItemContainerGenerator)base.ItemContainerGenerator!; (ITreeItemContainerGenerator)base.ItemContainerGenerator;
/// <inheritdoc/> /// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator<TreeViewItem>(); protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator<TreeViewItem>();

9
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -36,7 +36,7 @@ namespace Avalonia.Controls
{ {
get get
{ {
var bounds = Orientation == Orientation.Horizontal ? var bounds = Orientation == Orientation.Horizontal ?
_availableSpace.Width : _availableSpace.Height; _availableSpace.Width : _availableSpace.Height;
return Math.Max(0, _takenSpace - bounds); return Math.Max(0, _takenSpace - bounds);
} }
@ -129,14 +129,11 @@ namespace Avalonia.Controls
protected override IInputElement? GetControlInDirection(NavigationDirection direction, IControl? from) protected override IInputElement? GetControlInDirection(NavigationDirection direction, IControl? from)
{ {
if (from == null)
return null;
var logicalScrollable = Parent as ILogicalScrollable; var logicalScrollable = Parent as ILogicalScrollable;
if (logicalScrollable?.IsLogicalScrollEnabled == true) if (logicalScrollable?.IsLogicalScrollEnabled == true)
{ {
return logicalScrollable.GetControlInDirection(direction, from); return logicalScrollable.GetControlInDirection(direction, from!);
} }
else else
{ {
@ -145,7 +142,7 @@ namespace Avalonia.Controls
} }
internal override void ArrangeChild( internal override void ArrangeChild(
IControl child, IControl child,
Rect rect, Rect rect,
Size panelSize, Size panelSize,
Orientation orientation) Orientation orientation)

10149
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json

File diff suppressed because it is too large

51
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package.json

@ -5,37 +5,36 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"webpack-ver": "cross-env NODE_ENV=production webpack --version", "webpack-ver": "cross-env NODE_ENV=production webpack --version",
"dist": "cross-env NODE_ENV=production webpack --display-error-details", "dist": "cross-env NODE_ENV=production webpack",
"watch": "cross-env NODE_ENV=development webpack --watch --display-error-details" "watch": "cross-env NODE_ENV=development webpack --watch"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"awesome-typescript-loader": "^5.0.0", "clean-webpack-plugin": "^4.0.0",
"clean-webpack-plugin": "^0.1.19", "compression-webpack-plugin": "^9.2.0",
"compression-webpack-plugin": "^2.0.0", "cross-env": "^7.0.3",
"copy-webpack-plugin": "^4.6.0", "css-loader": "^6.5.1",
"cross-env": "^5.1.6", "file-loader": "^6.2.0",
"css-loader": "^1.0.0", "html-webpack-plugin": "^5.5.0",
"file-loader": "^1.1.11", "mini-css-extract-plugin": "^2.5.3",
"html-webpack-plugin": "^3.2.0", "source-map-loader": "^3.0.1",
"mini-css-extract-plugin": "^0.4.1", "style-loader": "^3.3.1",
"source-map-loader": "^0.2.3", "to-string-loader": "^1.2.0",
"style-loader": "^0.21.0", "ts-loader": "^9.2.6",
"to-string-loader": "^1.1.5", "tsconfig-paths-webpack-plugin": "^3.5.2",
"tsconfig-paths-webpack-plugin": "^3.2.0", "typescript": "^4.5.5",
"typescript": "^2.9.2", "url-loader": "^4.1.1",
"url-loader": "^1.0.1", "webpack": "^5.68.0",
"webpack": "~4.16.3", "webpack-cli": "^4.9.2",
"webpack-cli": "~2.1.3", "webpack-livereload-plugin": "^3.0.2"
"webpack-livereload-plugin": "~2.1.1"
}, },
"dependencies": { "dependencies": {
"@types/react": "^16.3.14", "@types/react": "^17.0.38",
"@types/react-dom": "^16.0.5", "@types/react-dom": "^17.0.11",
"mobx": "4.3.0", "mobx": "^6.3.13",
"mobx-react": "^5.1.2", "mobx-react": "^7.2.1",
"react": "^16.3.2", "react": "^17.0.2",
"react-dom": "^16.3.2" "react-dom": "^17.0.2"
} }
} }

17
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js

@ -3,10 +3,9 @@ const path = require('path');
const LiveReloadPlugin = require('webpack-livereload-plugin'); const LiveReloadPlugin = require('webpack-livereload-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const prod = process.env.NODE_ENV == 'production'; const prod = process.env.NODE_ENV == 'production';
class Printer { class Printer {
@ -17,6 +16,9 @@ class Printer {
} }
const config = { const config = {
stats: {
errorDetails: true
},
entry: { entry: {
bundle: './src/index.tsx' bundle: './src/index.tsx'
}, },
@ -42,7 +44,7 @@ const config = {
{ {
test: /\.(ts|tsx)$/, test: /\.(ts|tsx)$/,
exclude: /node_modules/, exclude: /node_modules/,
use: 'awesome-typescript-loader' use: 'ts-loader'
}, },
{ {
test: /\.css$/, test: /\.css$/,
@ -93,7 +95,9 @@ const config = {
plugins: plugins:
[ [
new Printer(), new Printer(),
new CleanWebpackPlugin([path.resolve(__dirname, 'build')], { verbose: false }), new CleanWebpackPlugin({
cleanAfterEveryBuildPatterns: ['dist']
}),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: "[name].[chunkhash]h" + filename: "[name].[chunkhash]h" +
".css", ".css",
@ -104,11 +108,6 @@ const config = {
template: path.resolve(__dirname, './src/index.html'), template: path.resolve(__dirname, './src/index.html'),
filename: 'index.html' //relative to root of the application filename: 'index.html' //relative to root of the application
}), }),
new CopyWebpackPlugin([
// relative path from src
//{ from: './src/favicon.ico' },
//{ from: './src/assets' }
]),
new CompressionPlugin({ new CompressionPlugin({
test: /(\?.*)?$/i test: /(\?.*)?$/i
}) })

14
src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs

@ -17,6 +17,7 @@ namespace Avalonia.Dialogs
private readonly ManagedFileDialogOptions _options; private readonly ManagedFileDialogOptions _options;
public event Action CancelRequested; public event Action CancelRequested;
public event Action<string[]> CompleteRequested; public event Action<string[]> CompleteRequested;
public event Action<string> OverwritePrompt;
public AvaloniaList<ManagedFileChooserItemViewModel> QuickLinks { get; } = public AvaloniaList<ManagedFileChooserItemViewModel> QuickLinks { get; } =
new AvaloniaList<ManagedFileChooserItemViewModel>(); new AvaloniaList<ManagedFileChooserItemViewModel>();
@ -39,6 +40,7 @@ namespace Avalonia.Dialogs
private bool _scheduledSelectionValidation; private bool _scheduledSelectionValidation;
private bool _alreadyCancelled = false; private bool _alreadyCancelled = false;
private string _defaultExtension; private string _defaultExtension;
private bool _overwritePrompt;
private CompositeDisposable _disposables; private CompositeDisposable _disposables;
public string Location public string Location
@ -167,6 +169,7 @@ namespace Avalonia.Dialogs
{ {
_savingFile = true; _savingFile = true;
_defaultExtension = sfd.DefaultExtension; _defaultExtension = sfd.DefaultExtension;
_overwritePrompt = sfd.ShowOverwritePrompt ?? true;
FileName = sfd.InitialFileName; FileName = sfd.InitialFileName;
} }
@ -360,7 +363,16 @@ namespace Avalonia.Dialogs
FileName = Path.ChangeExtension(FileName, _defaultExtension); 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 else

70
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -1,3 +1,4 @@
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
@ -31,6 +32,75 @@ namespace Avalonia.Dialogs
dialog.Close(); 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; model.CancelRequested += dialog.Close;
await dialog.ShowDialog<object>(parent); await dialog.ShowDialog<object>(parent);

2
src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs

@ -89,7 +89,9 @@ namespace Avalonia.OpenGL.Controls
gl.BindTexture(GL_TEXTURE_2D, 0); gl.BindTexture(GL_TEXTURE_2D, 0);
gl.BindFramebuffer(GL_FRAMEBUFFER, 0); gl.BindFramebuffer(GL_FRAMEBUFFER, 0);
gl.DeleteFramebuffers(1, new[] { _fb }); gl.DeleteFramebuffers(1, new[] { _fb });
_fb = 0;
gl.DeleteRenderbuffers(1, new[] { _depthBuffer }); gl.DeleteRenderbuffers(1, new[] { _depthBuffer });
_depthBuffer = 0;
_attachment?.Dispose(); _attachment?.Dispose();
_attachment = null; _attachment = null;
_bitmap?.Dispose(); _bitmap?.Dispose();

32
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@ -35,10 +35,10 @@ namespace Avalonia.Media.TextFormatting
/// <param name="maxLines">The maximum number of text lines.</param> /// <param name="maxLines">The maximum number of text lines.</param>
/// <param name="textStyleOverrides">The text style overrides.</param> /// <param name="textStyleOverrides">The text style overrides.</param>
public TextLayout( public TextLayout(
string text, string? text,
Typeface typeface, Typeface typeface,
double fontSize, double fontSize,
IBrush foreground, IBrush? foreground,
TextAlignment textAlignment = TextAlignment.Left, TextAlignment textAlignment = TextAlignment.Left,
TextWrapping textWrapping = TextWrapping.NoWrap, TextWrapping textWrapping = TextWrapping.NoWrap,
TextTrimming textTrimming = TextTrimming.None, TextTrimming textTrimming = TextTrimming.None,
@ -194,6 +194,7 @@ namespace Avalonia.Media.TextFormatting
var currentY = 0d; var currentY = 0d;
var currentPosition = 0; var currentPosition = 0;
var currentRect = Rect.Empty;
foreach (var textLine in TextLines) foreach (var textLine in TextLines)
{ {
@ -209,7 +210,10 @@ namespace Avalonia.Media.TextFormatting
//The whole line is covered. //The whole line is covered.
if (currentPosition >= start && start + length > currentPosition + textLine.TextRange.Length) 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; currentY += textLine.Height;
currentPosition += textLine.TextRange.Length; currentPosition += textLine.TextRange.Length;
@ -317,13 +321,23 @@ namespace Avalonia.Media.TextFormatting
var width = endX - startX; 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 (currentRun.ShapedBuffer.IsLeftToRight)
{ {
if (nextRun != null) if (nextRun != null)
{ {
if (nextRun.Text.Start > currentRun.Text.Start) if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= start + length)
{ {
break; break;
} }
@ -397,7 +411,7 @@ namespace Avalonia.Media.TextFormatting
return new TextHitTestResult(); return new TextHitTestResult();
} }
characterHit = currentLine.GetNextCaretCharacterHit(new CharacterHit(currentLine.TextRange.End)); characterHit = currentLine.GetCharacterHitFromDistance(point.X);
return GetHitTestResult(currentLine, characterHit, point); return GetHitTestResult(currentLine, characterHit, point);
} }
@ -424,7 +438,7 @@ namespace Avalonia.Media.TextFormatting
continue; 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; return index;
} }
@ -478,7 +492,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="lineHeight">The height of each line of text.</param> /// <param name="lineHeight">The height of each line of text.</param>
/// <returns></returns> /// <returns></returns>
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, 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) TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight)
{ {
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground); var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);

11
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 namespace Avalonia.Media.TextFormatting
{ {
@ -210,10 +211,10 @@ namespace Avalonia.Media.TextFormatting
switch (textAlignment) switch (textAlignment)
{ {
case TextAlignment.Center: case TextAlignment.Center:
return (paragraphWidth - width) / 2; return Math.Max(0, (paragraphWidth - width) / 2);
case TextAlignment.Right: case TextAlignment.Right:
return paragraphWidth - widthIncludingTrailingWhitespace; return Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace);
default: default:
return 0; return 0;
@ -223,13 +224,13 @@ namespace Avalonia.Media.TextFormatting
switch (textAlignment) switch (textAlignment)
{ {
case TextAlignment.Center: case TextAlignment.Center:
return (paragraphWidth - width) / 2; return Math.Max(0, (paragraphWidth - width) / 2);
case TextAlignment.Right: case TextAlignment.Right:
return 0; return 0;
default: default:
return paragraphWidth - widthIncludingTrailingWhitespace; return Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace);
} }
} }
} }

5
src/Avalonia.X11/NativeDialogs/Gtk.cs

@ -63,7 +63,7 @@ namespace Avalonia.X11.NativeDialogs
public static IDisposable ConnectSignal<T>(IntPtr obj, string name, T handler) public static IDisposable ConnectSignal<T>(IntPtr obj, string name, T handler)
{ {
var handle = GCHandle.Alloc(handler); var handle = GCHandle.Alloc(handler);
var ptr = Marshal.GetFunctionPointerForDelegate((Delegate)(object)handler); var ptr = Marshal.GetFunctionPointerForDelegate<T>(handler);
using (var utf = new Utf8Buffer(name)) using (var utf = new Utf8Buffer(name))
{ {
var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0); var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0);
@ -179,6 +179,9 @@ namespace Avalonia.X11.NativeDialogs
[DllImport(GtkName)] [DllImport(GtkName)]
public static extern void gtk_file_chooser_set_select_multiple(IntPtr chooser, bool allow); 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)] [DllImport(GtkName)]
public static extern void public static extern void

8
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -16,7 +16,7 @@ namespace Avalonia.X11.NativeDialogs
{ {
private Task<bool> _initialized; private Task<bool> _initialized;
private unsafe Task<string[]> ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action, private unsafe Task<string[]> ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action,
bool multiSelect, string initialFileName, IEnumerable<FileDialogFilter> filters, string defaultExtension) bool multiSelect, string initialFileName, IEnumerable<FileDialogFilter> filters, string defaultExtension, bool overwritePrompt)
{ {
IntPtr dlg; IntPtr dlg;
using (var name = new Utf8Buffer(title)) 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_filename(dlg, fn);
} }
gtk_file_chooser_set_do_overwrite_confirmation(dlg, overwritePrompt);
gtk_window_present(dlg); gtk_window_present(dlg);
return tcs.Task; return tcs.Task;
} }
@ -148,7 +150,7 @@ namespace Avalonia.X11.NativeDialogs
(dialog as OpenFileDialog)?.AllowMultiple ?? false, (dialog as OpenFileDialog)?.AllowMultiple ?? false,
Path.Combine(string.IsNullOrEmpty(dialog.Directory) ? "" : dialog.Directory, Path.Combine(string.IsNullOrEmpty(dialog.Directory) ? "" : dialog.Directory,
string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters, string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters,
(dialog as SaveFileDialog)?.DefaultExtension)); (dialog as SaveFileDialog)?.DefaultExtension, (dialog as SaveFileDialog)?.ShowOverwritePrompt ?? false));
} }
public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
@ -160,7 +162,7 @@ namespace Avalonia.X11.NativeDialogs
return await await RunOnGlibThread(async () => return await await RunOnGlibThread(async () =>
{ {
var res = await ShowDialog(dialog.Title, platformImpl, 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(); return res?.FirstOrDefault();
}); });
} }

16
src/Avalonia.X11/SMLib.cs

@ -1,3 +1,4 @@
#nullable enable
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -7,20 +8,19 @@ namespace Avalonia.X11
{ {
private const string LibSm = "libSM.so.6"; 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( public static extern IntPtr SmcOpenConnection(
[MarshalAs(UnmanagedType.LPWStr)] string networkId, [MarshalAs(UnmanagedType.LPStr)] string? networkId,
IntPtr content, IntPtr content,
int xsmpMajorRev, int xsmpMajorRev,
int xsmpMinorRev, int xsmpMinorRev,
ulong mask, nuint mask,
ref SmcCallbacks callbacks, ref SmcCallbacks callbacks,
[MarshalAs(UnmanagedType.LPWStr)] [Out] [MarshalAs(UnmanagedType.LPStr)] string? previousId,
out string previousId, ref IntPtr clientIdRet,
[MarshalAs(UnmanagedType.LPWStr)] [Out]
out string clientIdRet,
int errorLength, int errorLength,
[Out] char[] errorStringRet); [Out] byte[] errorStringRet
);
[DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)]
public static extern int SmcCloseConnection( public static extern int SmcCloseConnection(

21
src/Avalonia.X11/X11PlatformLifetimeEvents.cs

@ -1,5 +1,6 @@
#nullable enable #nullable enable
using System; using System;
using System.Text;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
@ -14,10 +15,10 @@ namespace Avalonia.X11
internal unsafe class X11PlatformLifetimeEvents : IDisposable, IPlatformLifetimeEventsImpl internal unsafe class X11PlatformLifetimeEvents : IDisposable, IPlatformLifetimeEventsImpl
{ {
private readonly AvaloniaX11Platform _platform; private readonly AvaloniaX11Platform _platform;
private const ulong SmcSaveYourselfProcMask = 1L; private const nuint SmcSaveYourselfProcMask = 1;
private const ulong SmcDieProcMask = 2L; private const nuint SmcDieProcMask = 2;
private const ulong SmcSaveCompleteProcMask = 4L; private const nuint SmcSaveCompleteProcMask = 4;
private const ulong SmcShutdownCancelledProcMask = 8L; private const nuint SmcShutdownCancelledProcMask = 8;
private static readonly ConcurrentDictionary<IntPtr, X11PlatformLifetimeEvents> s_nativeToManagedMapper = private static readonly ConcurrentDictionary<IntPtr, X11PlatformLifetimeEvents> s_nativeToManagedMapper =
new ConcurrentDictionary<IntPtr, X11PlatformLifetimeEvents>(); new ConcurrentDictionary<IntPtr, X11PlatformLifetimeEvents>();
@ -62,24 +63,24 @@ namespace Avalonia.X11
return; return;
} }
var errorBuf = new char[255]; byte[] errorBuf = new byte[255];
IntPtr clientIdRet = IntPtr.Zero;
var smcConn = SMLib.SmcOpenConnection(null!, var smcConn = SMLib.SmcOpenConnection(null,
IntPtr.Zero, 1, 0, IntPtr.Zero, 1, 0,
SmcSaveYourselfProcMask | SmcSaveYourselfProcMask |
SmcSaveCompleteProcMask | SmcSaveCompleteProcMask |
SmcShutdownCancelledProcMask | SmcShutdownCancelledProcMask |
SmcDieProcMask, SmcDieProcMask,
ref s_callbacks, ref s_callbacks,
out _, null,
out _, ref clientIdRet,
errorBuf.Length, errorBuf.Length,
errorBuf); errorBuf);
if (smcConn == IntPtr.Zero) if (smcConn == IntPtr.Zero)
{ {
Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, 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; return;
} }

6
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -862,7 +862,7 @@ namespace Avalonia.Win32.Interop
public void Init() public void Init()
{ {
biSize = (uint)Marshal.SizeOf(this); biSize = (uint)sizeof(BITMAPINFOHEADER);
} }
} }
@ -1521,7 +1521,7 @@ namespace Avalonia.Win32.Interop
internal static Version RtlGetVersion() internal static Version RtlGetVersion()
{ {
RTL_OSVERSIONINFOEX v = new RTL_OSVERSIONINFOEX(); RTL_OSVERSIONINFOEX v = new RTL_OSVERSIONINFOEX();
v.dwOSVersionInfoSize = (uint)Marshal.SizeOf(v); v.dwOSVersionInfoSize = (uint)Marshal.SizeOf<RTL_OSVERSIONINFOEX>();
if (RtlGetVersion(ref v) == 0) if (RtlGetVersion(ref v) == 0)
{ {
return new Version((int)v.dwMajorVersion, (int)v.dwMinorVersion, (int)v.dwBuildNumber); return new Version((int)v.dwMajorVersion, (int)v.dwMinorVersion, (int)v.dwBuildNumber);
@ -1914,7 +1914,7 @@ namespace Avalonia.Win32.Interop
get get
{ {
WINDOWPLACEMENT result = new WINDOWPLACEMENT(); WINDOWPLACEMENT result = new WINDOWPLACEMENT();
result.Length = Marshal.SizeOf(result); result.Length = Marshal.SizeOf<WINDOWPLACEMENT>();
return result; return result;
} }
} }

10
src/Windows/Avalonia.Win32/SystemDialogImpl.cs

@ -40,6 +40,16 @@ namespace Avalonia.Win32
{ {
options |= FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT; options |= FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT;
} }
if (dialog is SaveFileDialog saveFileDialog)
{
var overwritePrompt = saveFileDialog.ShowOverwritePrompt ?? true;
if (!overwritePrompt)
{
options &= ~FILEOPENDIALOGOPTIONS.FOS_OVERWRITEPROMPT;
}
}
frm.SetOptions(options); frm.SetOptions(options);
var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? ""; var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? "";

10
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -228,7 +228,7 @@ namespace Avalonia.Win32
return new Size(rcWindow.Width, rcWindow.Height) / RenderScaling; 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<RECT>());
return new Size(rect.Width, rect.Height) / RenderScaling; return new Size(rect.Width, rect.Height) / RenderScaling;
} }
} }
@ -265,7 +265,7 @@ namespace Avalonia.Win32
{ {
if (IsWindowVisible(_hwnd)) if (IsWindowVisible(_hwnd))
{ {
ShowWindow(value, true); ShowWindow(value, value != WindowState.Minimized); // If the window is minimized, it shouldn't be activated
} }
_showWindowState = value; _showWindowState = value;
@ -337,7 +337,7 @@ namespace Avalonia.Win32
private WindowTransparencyLevel Win8xEnableBlur(WindowTransparencyLevel transparencyLevel) private WindowTransparencyLevel Win8xEnableBlur(WindowTransparencyLevel transparencyLevel)
{ {
var accent = new AccentPolicy(); var accent = new AccentPolicy();
var accentStructSize = Marshal.SizeOf(accent); var accentStructSize = Marshal.SizeOf<AccentPolicy>();
if (transparencyLevel == WindowTransparencyLevel.AcrylicBlur) if (transparencyLevel == WindowTransparencyLevel.AcrylicBlur)
{ {
@ -392,7 +392,7 @@ namespace Avalonia.Win32
bool canUseAcrylic = Win32Platform.WindowsVersion.Major > 10 || Win32Platform.WindowsVersion.Build >= 19628; bool canUseAcrylic = Win32Platform.WindowsVersion.Major > 10 || Win32Platform.WindowsVersion.Build >= 19628;
var accent = new AccentPolicy(); var accent = new AccentPolicy();
var accentStructSize = Marshal.SizeOf(accent); var accentStructSize = Marshal.SizeOf<AccentPolicy>();
if (transparencyLevel == WindowTransparencyLevel.AcrylicBlur && !canUseAcrylic) if (transparencyLevel == WindowTransparencyLevel.AcrylicBlur && !canUseAcrylic)
{ {
@ -973,7 +973,7 @@ namespace Avalonia.Win32
{ {
case WindowState.Minimized: case WindowState.Minimized:
newWindowProperties.IsFullScreen = false; newWindowProperties.IsFullScreen = false;
command = activate ? ShowWindowCommand.Minimize : ShowWindowCommand.ShowMinNoActive; command = ShowWindowCommand.Minimize;
break; break;
case WindowState.Maximized: case WindowState.Maximized:
newWindowProperties.IsFullScreen = false; newWindowProperties.IsFullScreen = false;

49
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -613,5 +613,54 @@ namespace Avalonia.Controls.UnitTests
Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetHasErrors(target));
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); 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<string>((x, _) => new TextBlock { Height = 10 }),
WrapSelection = true
};
Prepare(target);
var lbItems = target.GetLogicalChildren().OfType<ListBoxItem>().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);
}
}
} }
} }

21
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@ -735,27 +735,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine = layout.TextLines[0]; var textLine = layout.TextLines[0];
var firstStart = textLine.GetDistanceFromCharacterHit(new CharacterHit(5, 1)); var start = textLine.GetDistanceFromCharacterHit(new CharacterHit(5, 1));
var firstEnd = textLine.GetDistanceFromCharacterHit(new CharacterHit(0));
var secondStart = textLine.GetDistanceFromCharacterHit(new CharacterHit(6));
var secondEnd = textLine.GetDistanceFromCharacterHit(new CharacterHit(6, 1)); var end = textLine.GetDistanceFromCharacterHit(new CharacterHit(6, 1));
var rects = layout.HitTestTextRange(0, 7).ToArray(); var rects = layout.HitTestTextRange(0, 7).ToArray();
Assert.Equal(2, rects.Length); Assert.Equal(1, rects.Length);
var firstExpected = rects[0];
Assert.Equal(firstExpected.Left, firstStart);
Assert.Equal(firstExpected.Right, firstEnd);
var secondExpected = rects[1]; var expected = rects[0];
Assert.Equal(secondExpected.Left, secondStart); Assert.Equal(expected.Left, start);
Assert.Equal(secondExpected.Right, secondEnd); Assert.Equal(expected.Right, end);
} }
} }

Loading…
Cancel
Save