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">
<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>
</Project>

23
readme.md

@ -3,13 +3,11 @@
<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)
## 📖 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" />
([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery))
![image](https://user-images.githubusercontent.com/4672627/152126443-932966cf-57e7-4e77-9be6-62463a66b9f8.png)
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
Examples of UIs built with Avalonia
![image](https://user-images.githubusercontent.com/4672627/84707589-5b69a880-af35-11ea-87a6-7ad57a31d314.png)
([Synfonia](https://github.com/jmacato/Synfonia))
<video src="https://user-images.githubusercontent.com/4672627/152325602-28df36ec-6444-44a6-aebe-90ad52c8f27a.mp4"></video>
([Lunacy](https://icons8.com/lunacy))
![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/152325740-261c27a3-e6f0-4662-bff7-4796d4940e04.png)
([PlasticSCM](https://www.plasticscm.com/))
![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/152326453-14944c4d-33da-4d50-a268-b87f80927adb.png)
([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

160
samples/ControlCatalog/Pages/ComboBoxPage.xaml

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

2
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()

4
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -21,6 +21,7 @@
<CheckBox IsChecked="{Binding Toggle}">Toggle</CheckBox>
<CheckBox IsChecked="{Binding AlwaysSelected}">AlwaysSelected</CheckBox>
<CheckBox IsChecked="{Binding AutoScrollToSelectedItem}">AutoScrollToSelectedItem</CheckBox>
<CheckBox IsChecked="{Binding WrapSelection}">WrapSelection</CheckBox>
</StackPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="4">
<Button Command="{Binding AddItemCommand}">Add</Button>
@ -30,6 +31,7 @@
<ListBox Items="{Binding Items}"
Selection="{Binding Selection}"
AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}"
SelectionMode="{Binding SelectionMode^}"/>
SelectionMode="{Binding SelectionMode^}"
WrapSelection="{Binding WrapSelection}"/>
</DockPanel>
</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 _alwaysSelected;
private bool _autoScrollToSelectedItem = true;
private bool _wrapSelection;
private int _counter;
private IObservable<SelectionMode> _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; }

37
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<ComboBox>(DefaultPanel);
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);
IsTextSearchEnabledProperty.OverrideDefaultValue<ComboBox>(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;
}

25
src/Avalonia.Controls/ItemsControl.cs

@ -79,7 +79,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets the <see cref="IItemContainerGenerator"/> for the control.
/// </summary>
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<ChildIndexChangedEventArgs>? IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
@ -240,14 +239,8 @@ namespace Avalonia.Controls
/// Creates the <see cref="ItemContainerGenerator"/> for the control.
/// </summary>
/// <returns>
/// An <see cref="IItemContainerGenerator"/> or null.
/// An <see cref="IItemContainerGenerator"/>.
/// </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()
{
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)
{

6
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<IMenuItem>();
}

6
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<IMenuItem>();
}

2
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;

15
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)
{

199
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<char> PasswordCharProperty =
AvaloniaProperty.Register<TextPresenter, char>(nameof(PasswordChar));
public static readonly StyledProperty<IBrush> SelectionBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush>(nameof(SelectionBrushProperty));
public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(SelectionBrushProperty));
public static readonly StyledProperty<IBrush> SelectionForegroundBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush>(nameof(SelectionForegroundBrushProperty));
public static readonly StyledProperty<IBrush?> SelectionForegroundBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(SelectionForegroundBrushProperty));
public static readonly StyledProperty<IBrush> CaretBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush>(nameof(CaretBrushProperty));
public static readonly StyledProperty<IBrush?> CaretBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(CaretBrushProperty));
public static readonly DirectProperty<TextPresenter, int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<TextPresenter>(
@ -48,8 +46,8 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Defines the <see cref="Text"/> property.
/// </summary>
public static readonly DirectProperty<TextPresenter, string> TextProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, string>(
public static readonly DirectProperty<TextPresenter, string?> TextProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, string?>(
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<TextPresenter>(CaretBrushProperty, SelectionBrushProperty);
@ -114,7 +110,7 @@ namespace Avalonia.Controls.Presenters
/// Gets or sets the text.
/// </summary>
[Content]
public string Text
public string? Text
{
get => _text;
set => SetAndRaise(TextProperty, ref _text, value);
@ -186,7 +182,7 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary>
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
/// <param name="typeface"></param>
/// <param name="textStyleOverrides"></param>
/// <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)
{
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 <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <returns>A <see cref="TextLayout"/> object.</returns>
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<ScrollViewer>();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_scrollViewer = null;
_caretTimer.Stop();
_caretTimer.Tick -= CaretTimerTick;

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

@ -114,6 +114,12 @@ namespace Avalonia.Controls.Primitives
"SelectionChanged",
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 string _textSearchTerm = string.Empty;
private DispatcherTimer? _textSearchTimer;
@ -321,6 +327,16 @@ namespace Avalonia.Controls.Primitives
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>
/// Gets or sets the selection mode.
/// </summary>
@ -580,6 +596,10 @@ namespace Avalonia.Controls.Primitives
var newValue = change.NewValue.GetValueOrDefault<SelectionMode>();
_selection.SingleSelect = !newValue.HasAllFlags(SelectionMode.Multiple);
}
else if (change.Property == WrapSelectionProperty)
{
WrapFocus = WrapSelection;
}
}
/// <summary>
@ -908,7 +928,7 @@ namespace Avalonia.Controls.Primitives
{
MarkContainerSelected(
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>
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>
/// Shows the save file dialog.
/// </summary>

2
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;
}

7
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;

25
src/Avalonia.Controls/TextBox.cs

@ -540,14 +540,19 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get<TextPresenter>("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;

2
src/Avalonia.Controls/TreeView.cs

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

9
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)

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",
"scripts": {
"webpack-ver": "cross-env NODE_ENV=production webpack --version",
"dist": "cross-env NODE_ENV=production webpack --display-error-details",
"watch": "cross-env NODE_ENV=development webpack --watch --display-error-details"
"dist": "cross-env NODE_ENV=production webpack",
"watch": "cross-env NODE_ENV=development webpack --watch"
},
"author": "",
"license": "ISC",
"devDependencies": {
"awesome-typescript-loader": "^5.0.0",
"clean-webpack-plugin": "^0.1.19",
"compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^4.6.0",
"cross-env": "^5.1.6",
"css-loader": "^1.0.0",
"file-loader": "^1.1.11",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.4.1",
"source-map-loader": "^0.2.3",
"style-loader": "^0.21.0",
"to-string-loader": "^1.1.5",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"typescript": "^2.9.2",
"url-loader": "^1.0.1",
"webpack": "~4.16.3",
"webpack-cli": "~2.1.3",
"webpack-livereload-plugin": "~2.1.1"
"clean-webpack-plugin": "^4.0.0",
"compression-webpack-plugin": "^9.2.0",
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.5.3",
"source-map-loader": "^3.0.1",
"style-loader": "^3.3.1",
"to-string-loader": "^1.2.0",
"ts-loader": "^9.2.6",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.5.5",
"url-loader": "^4.1.1",
"webpack": "^5.68.0",
"webpack-cli": "^4.9.2",
"webpack-livereload-plugin": "^3.0.2"
},
"dependencies": {
"@types/react": "^16.3.14",
"@types/react-dom": "^16.0.5",
"mobx": "4.3.0",
"mobx-react": "^5.1.2",
"react": "^16.3.2",
"react-dom": "^16.3.2"
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11",
"mobx": "^6.3.13",
"mobx-react": "^7.2.1",
"react": "^17.0.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 HtmlWebpackPlugin = require('html-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 TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const prod = process.env.NODE_ENV == 'production';
class Printer {
@ -17,6 +16,9 @@ class Printer {
}
const config = {
stats: {
errorDetails: true
},
entry: {
bundle: './src/index.tsx'
},
@ -42,7 +44,7 @@ const config = {
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: 'awesome-typescript-loader'
use: 'ts-loader'
},
{
test: /\.css$/,
@ -93,7 +95,9 @@ const config = {
plugins:
[
new Printer(),
new CleanWebpackPlugin([path.resolve(__dirname, 'build')], { verbose: false }),
new CleanWebpackPlugin({
cleanAfterEveryBuildPatterns: ['dist']
}),
new MiniCssExtractPlugin({
filename: "[name].[chunkhash]h" +
".css",
@ -104,11 +108,6 @@ const config = {
template: path.resolve(__dirname, './src/index.html'),
filename: 'index.html' //relative to root of the application
}),
new CopyWebpackPlugin([
// relative path from src
//{ from: './src/favicon.ico' },
//{ from: './src/assets' }
]),
new CompressionPlugin({
test: /(\?.*)?$/i
})

14
src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs

@ -17,6 +17,7 @@ namespace Avalonia.Dialogs
private readonly ManagedFileDialogOptions _options;
public event Action CancelRequested;
public event Action<string[]> CompleteRequested;
public event Action<string> OverwritePrompt;
public AvaloniaList<ManagedFileChooserItemViewModel> QuickLinks { get; } =
new AvaloniaList<ManagedFileChooserItemViewModel>();
@ -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

70
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<object>(parent);

2
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();

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="textStyleOverrides">The text style overrides.</param>
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
/// <param name="lineHeight">The height of each line of text.</param>
/// <returns></returns>
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);

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
{
@ -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);
}
}
}

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)
{
var handle = GCHandle.Alloc(handler);
var ptr = Marshal.GetFunctionPointerForDelegate((Delegate)(object)handler);
var ptr = Marshal.GetFunctionPointerForDelegate<T>(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

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

@ -16,7 +16,7 @@ namespace Avalonia.X11.NativeDialogs
{
private Task<bool> _initialized;
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;
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<string> 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();
});
}

16
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(

21
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<IntPtr, X11PlatformLifetimeEvents> s_nativeToManagedMapper =
new ConcurrentDictionary<IntPtr, X11PlatformLifetimeEvents>();
@ -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;
}

6
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<RTL_OSVERSIONINFOEX>();
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<WINDOWPLACEMENT>();
return result;
}
}

10
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 ?? "";

10
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<RECT>());
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<AccentPolicy>();
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<AccentPolicy>();
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;

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

Loading…
Cancel
Save