Browse Source

Merge branch 'master' into fix/SplitViewOverlay

pull/10697/head
amwx 4 years ago
committed by GitHub
parent
commit
1207673145
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject
  2. 5
      .ncrunch/Generators.Sandbox.v3.ncrunchproject
  3. 10
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  4. 2
      samples/ControlCatalog/MainView.xaml
  5. 60
      samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
  6. 2
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  7. 2
      samples/ControlCatalog/Pages/CompositionPage.axaml.cs
  8. 2
      samples/ControlCatalog/Pages/ContextFlyoutPage.xaml
  9. 4
      samples/ControlCatalog/Pages/ContextMenuPage.xaml
  10. 2
      samples/ControlCatalog/Pages/CursorPage.xaml
  11. 8
      samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs
  12. 12
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  13. 2
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  14. 6
      samples/ControlCatalog/Pages/MenuPage.xaml
  15. 8
      samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs
  16. 6
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  17. 2
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  18. 4
      samples/ControlCatalog/Pages/ScrollSnapPage.xaml
  19. 4
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml
  20. 2
      samples/ControlCatalog/Pages/TabStripPage.xaml
  21. 2
      samples/ControlCatalog/Pages/ThemePage.axaml.cs
  22. 2
      samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
  23. 2
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  24. 8
      samples/IntegrationTestApp/MainWindow.axaml
  25. 6
      samples/IntegrationTestApp/MainWindow.axaml.cs
  26. 24
      samples/IntegrationTestApp/ShowWindowTest.axaml
  27. 2
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  28. 2
      samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
  29. 9
      src/Avalonia.Base/CornerRadius.cs
  30. 2
      src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs
  31. 11
      src/Avalonia.Base/Media/BoxShadow.cs
  32. 4
      src/Avalonia.Base/Media/BoxShadows.cs
  33. 2
      src/Avalonia.Base/Media/DrawingGroup.cs
  34. 5
      src/Avalonia.Base/Media/FontFamily.cs
  35. 5
      src/Avalonia.Base/Media/FormattedText.cs
  36. 2
      src/Avalonia.Base/Media/ImageDrawing.cs
  37. 2
      src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs
  38. 2
      src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
  39. 19
      src/Avalonia.Base/PixelRect.cs
  40. 17
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  41. 16
      src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
  42. 8
      src/Avalonia.Base/Point.cs
  43. 2
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  44. 34
      src/Avalonia.Base/Rect.cs
  45. 12
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  46. 55
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  47. 2
      src/Avalonia.Base/Rendering/DirtyRects.cs
  48. 4
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  49. 11
      src/Avalonia.Base/Size.cs
  50. 14
      src/Avalonia.Base/StyledProperty.cs
  51. 10
      src/Avalonia.Base/Thickness.cs
  52. 8
      src/Avalonia.Base/Vector.cs
  53. 2
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  54. 2
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  55. 2
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
  56. 2
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  57. 8
      src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
  58. 5
      src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
  59. 4
      src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs
  60. 12
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs
  61. 3
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs
  62. 2
      src/Avalonia.Controls/BorderVisual.cs
  63. 20
      src/Avalonia.Controls/ContainerClearingEventArgs.cs
  64. 32
      src/Avalonia.Controls/ContainerIndexChangedEventArgs.cs
  65. 26
      src/Avalonia.Controls/ContainerPreparedEventArgs.cs
  66. 103
      src/Avalonia.Controls/ContextMenu.cs
  67. 2
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  68. 2
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  69. 8
      src/Avalonia.Controls/Documents/InlineCollection.cs
  70. 4
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  71. 16
      src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs
  72. 165
      src/Avalonia.Controls/ItemCollection.cs
  73. 243
      src/Avalonia.Controls/ItemsControl.cs
  74. 155
      src/Avalonia.Controls/ItemsSourceView.cs
  75. 4
      src/Avalonia.Controls/LayoutTransformControl.cs
  76. 135
      src/Avalonia.Controls/MaskedTextBox.cs
  77. 2
      src/Avalonia.Controls/NativeControlHost.cs
  78. 18
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  79. 109
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  80. 80
      src/Avalonia.Controls/Primitives/Popup.cs
  81. 3
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  82. 55
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  83. 107
      src/Avalonia.Controls/SelectableTextBlock.cs
  84. 27
      src/Avalonia.Controls/Selection/InternalSelectionModel.cs
  85. 4
      src/Avalonia.Controls/Selection/SelectionModel.cs
  86. 2
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  87. 66
      src/Avalonia.Controls/TextBlock.cs
  88. 433
      src/Avalonia.Controls/TextBox.cs
  89. 19
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  90. 1
      src/Avalonia.Controls/TopLevel.cs
  91. 4
      src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs
  92. 4
      src/Avalonia.Controls/Utils/UndoRedoHelper.cs
  93. 20
      src/Avalonia.Controls/VirtualizingPanel.cs
  94. 5
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  95. 29
      src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs
  96. 2
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml
  97. 6
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  98. 6
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
  99. 2
      src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs
  100. 2
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml

5
.ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Generators.Sandbox.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

10
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -281,10 +281,13 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) {
case SystemDecorationsFull: case SystemDecorationsFull:
[Window setHasShadow:YES]; [Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle]; [Window setTitle:_lastTitle];
if (!_isClientAreaExtended) {
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
}
if (currentWindowState == Maximized) { if (currentWindowState == Maximized) {
auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
@ -611,7 +614,8 @@ void WindowImpl::UpdateStyle() {
} }
bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; bool hasTrafficLights = (_decorations == SystemDecorationsFull) &&
(_isClientAreaExtended ? wantsChrome : true);
NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton]; NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton];
NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton]; NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton];

2
samples/ControlCatalog/MainView.xaml

@ -241,7 +241,7 @@
</ComboBox.Items> </ComboBox.Items>
</ComboBox> </ComboBox>
<ComboBox HorizontalAlignment="Stretch" <ComboBox HorizontalAlignment="Stretch"
Items="{Binding WindowStates}" ItemsSource="{Binding WindowStates}"
SelectedItem="{Binding WindowState}" /> SelectedItem="{Binding WindowState}" />
</StackPanel> </StackPanel>
</Flyout> </Flyout>

60
samples/ControlCatalog/Pages/ClipboardPage.xaml.cs

@ -1,16 +1,23 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages
{ {
public partial class ClipboardPage : UserControl public partial class ClipboardPage : UserControl
{ {
private INotificationManager? _notificationManager;
private INotificationManager NotificationManager => _notificationManager
??= new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
public ClipboardPage() public ClipboardPage()
{ {
InitializeComponent(); InitializeComponent();
@ -31,7 +38,7 @@ namespace ControlCatalog.Pages
private async void PasteText(object? sender, RoutedEventArgs args) private async void PasteText(object? sender, RoutedEventArgs args)
{ {
if(Application.Current!.Clipboard is { } clipboard) if (Application.Current!.Clipboard is { } clipboard)
{ {
ClipboardContent.Text = await clipboard.GetTextAsync(); ClipboardContent.Text = await clipboard.GetTextAsync();
} }
@ -59,15 +66,45 @@ namespace ControlCatalog.Pages
{ {
if (Application.Current!.Clipboard is { } clipboard) if (Application.Current!.Clipboard is { } clipboard)
{ {
var files = (ClipboardContent.Text ?? String.Empty) var storageProvider = TopLevel.GetTopLevel(this)!.StorageProvider;
.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); var filesPath = (ClipboardContent.Text ?? string.Empty)
if (files.Length == 0) .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
if (filesPath.Length == 0)
{ {
return; return;
} }
var dataObject = new DataObject(); List<string> invalidFile = new(filesPath.Length);
dataObject.Set(DataFormats.FileNames, files); List<IStorageFile> files = new(filesPath.Length);
await clipboard.SetDataObjectAsync(dataObject);
for (int i = 0; i < filesPath.Length; i++)
{
var file = await storageProvider.TryGetFileFromPathAsync(filesPath[i]);
if (file is null)
{
invalidFile.Add(filesPath[i]);
}
else
{
files.Add(file);
}
}
if (invalidFile.Count > 0)
{
NotificationManager.Show(new Notification("Warning", "There is one o more invalid path.", NotificationType.Warning));
}
if (files.Count > 0)
{
var dataObject = new DataObject();
dataObject.Set(DataFormats.Files, files);
await clipboard.SetDataObjectAsync(dataObject);
NotificationManager.Show(new Notification("Success", "Copy completated.", NotificationType.Success));
}
else
{
NotificationManager.Show(new Notification("Warning", "Any files to copy in Clipboard.", NotificationType.Warning));
}
} }
} }
@ -75,8 +112,9 @@ namespace ControlCatalog.Pages
{ {
if (Application.Current!.Clipboard is { } clipboard) if (Application.Current!.Clipboard is { } clipboard)
{ {
var fiels = await clipboard.GetDataAsync(DataFormats.FileNames) as IEnumerable<string>; var files = await clipboard.GetDataAsync(DataFormats.Files) as IEnumerable<Avalonia.Platform.Storage.IStorageItem>;
ClipboardContent.Text = fiels != null ? string.Join(Environment.NewLine, fiels) : string.Empty;
ClipboardContent.Text = files != null ? string.Join(Environment.NewLine, files.Select(f => f.TryGetLocalPath() ?? f.Name)) : string.Empty;
} }
} }
@ -95,7 +133,7 @@ namespace ControlCatalog.Pages
{ {
await clipboard.ClearAsync(); await clipboard.ClearAsync();
} }
} }
} }
} }

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

@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
var fontComboBox = this.Get<ComboBox>("fontComboBox"); var fontComboBox = this.Get<ComboBox>("fontComboBox");
fontComboBox.Items = FontManager.Current.SystemFonts; fontComboBox.ItemsSource = FontManager.Current.SystemFonts;
fontComboBox.SelectedIndex = 0; fontComboBox.SelectedIndex = 0;
} }
} }

2
samples/ControlCatalog/Pages/CompositionPage.axaml.cs

@ -32,7 +32,7 @@ public partial class CompositionPage : UserControl
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnAttachedToVisualTree(e); base.OnAttachedToVisualTree(e);
this.Get<ItemsControl>("Items").Items = CreateColorItems(); this.Get<ItemsControl>("Items").ItemsSource = CreateColorItems();
} }

2
samples/ControlCatalog/Pages/ContextFlyoutPage.xaml

@ -61,7 +61,7 @@
<Border.Styles> <Border.Styles>
<Style Selector="MenuFlyoutPresenter MenuItem" x:DataType="viewModels:MenuItemViewModel"> <Style Selector="MenuFlyoutPresenter MenuItem" x:DataType="viewModels:MenuItemViewModel">
<Setter Property="Header" Value="{Binding Header}"/> <Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/> <Setter Property="ItemsSource" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/> <Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/> <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style> </Style>

4
samples/ControlCatalog/Pages/ContextMenuPage.xaml

@ -51,13 +51,13 @@
<Border.Styles> <Border.Styles>
<Style Selector="ContextMenu MenuItem" x:DataType="viewModels:MenuItemViewModel"> <Style Selector="ContextMenu MenuItem" x:DataType="viewModels:MenuItemViewModel">
<Setter Property="Header" Value="{Binding Header}"/> <Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/> <Setter Property="ItemsSource" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/> <Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/> <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style> </Style>
</Border.Styles> </Border.Styles>
<Border.ContextMenu> <Border.ContextMenu>
<ContextMenu Items="{Binding MenuItems}" /> <ContextMenu ItemsSource="{Binding MenuItems}" />
</Border.ContextMenu> </Border.ContextMenu>
<TextBlock Text="Dynamically Generated"/> <TextBlock Text="Dynamically Generated"/>
</Border> </Border>

2
samples/ControlCatalog/Pages/CursorPage.xaml

@ -8,7 +8,7 @@
<TextBlock Classes="h2">Defines a cursor (mouse pointer)</TextBlock> <TextBlock Classes="h2">Defines a cursor (mouse pointer)</TextBlock>
</StackPanel> </StackPanel>
<ListBox Grid.Row="1" Items="{Binding StandardCursors}" Margin="0 8 8 8"> <ListBox Grid.Row="1" ItemsSource="{Binding StandardCursors}" Margin="0 8 8 8">
<ListBox.Styles> <ListBox.Styles>
<Style Selector="ListBoxItem"> <Style Selector="ListBoxItem">
<Setter Property="Cursor" Value="{Binding Cursor}" x:DataType="viewModels:StandardCursorModel"/> <Setter Property="Cursor" Value="{Binding Cursor}" x:DataType="viewModels:StandardCursorModel"/>

8
samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs

@ -133,17 +133,17 @@ namespace ControlCatalog.Pages
// 0,0 refers to the top-left of the control now. It is not prime time to draw gui stuff because it'll be under the world // 0,0 refers to the top-left of the control now. It is not prime time to draw gui stuff because it'll be under the world
var translateModifier = context.PushPreTransform(Avalonia.Matrix.CreateTranslation(new Avalonia.Vector(halfWidth, halfHeight))); var translateModifier = context.PushTransform(Avalonia.Matrix.CreateTranslation(new Avalonia.Vector(halfWidth, halfHeight)));
// now 0,0 refers to the ViewportCenter(X,Y). // now 0,0 refers to the ViewportCenter(X,Y).
var rotationMatrix = Avalonia.Matrix.CreateRotation(Rotation); var rotationMatrix = Avalonia.Matrix.CreateRotation(Rotation);
var rotationModifier = context.PushPreTransform(rotationMatrix); var rotationModifier = context.PushTransform(rotationMatrix);
// everything is rotated but not scaled // everything is rotated but not scaled
var scaleModifier = context.PushPreTransform(Avalonia.Matrix.CreateScale(Scale, -Scale)); var scaleModifier = context.PushTransform(Avalonia.Matrix.CreateScale(Scale, -Scale));
var mapPositionModifier = context.PushPreTransform(Matrix.CreateTranslation(new Vector(-ViewportCenterX, -ViewportCenterY))); var mapPositionModifier = context.PushTransform(Matrix.CreateTranslation(new Vector(-ViewportCenterX, -ViewportCenterY)));
// now everything is rotated and scaled, and at the right position, now we're drawing strictly in world coordinates // now everything is rotated and scaled, and at the right position, now we're drawing strictly in world coordinates

12
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -106,7 +106,7 @@ namespace ControlCatalog.Pages
Directory = initialDirectory, Directory = initialDirectory,
InitialFileName = initialFileName InitialFileName = initialFileName
}.ShowAsync(GetWindow()); }.ShowAsync(GetWindow());
results.Items = result; results.ItemsSource = result;
resultsVisible.IsVisible = result?.Any() == true; resultsVisible.IsVisible = result?.Any() == true;
}; };
this.Get<Button>("OpenMultipleFiles").Click += async delegate this.Get<Button>("OpenMultipleFiles").Click += async delegate
@ -118,7 +118,7 @@ namespace ControlCatalog.Pages
Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null, Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
AllowMultiple = true AllowMultiple = true
}.ShowAsync(GetWindow()); }.ShowAsync(GetWindow());
results.Items = result; results.ItemsSource = result;
resultsVisible.IsVisible = result?.Any() == true; resultsVisible.IsVisible = result?.Any() == true;
}; };
this.Get<Button>("SaveFile").Click += async delegate this.Get<Button>("SaveFile").Click += async delegate
@ -132,7 +132,7 @@ namespace ControlCatalog.Pages
DefaultExtension = filters?.Any() == true ? "txt" : null, DefaultExtension = filters?.Any() == true ? "txt" : null,
InitialFileName = "test.txt" InitialFileName = "test.txt"
}.ShowAsync(GetWindow()); }.ShowAsync(GetWindow());
results.Items = new[] { result }; results.ItemsSource = new[] { result };
resultsVisible.IsVisible = result != null; resultsVisible.IsVisible = result != null;
}; };
this.Get<Button>("SelectFolder").Click += async delegate this.Get<Button>("SelectFolder").Click += async delegate
@ -149,7 +149,7 @@ namespace ControlCatalog.Pages
else else
{ {
SetFolder(await GetStorageProvider().TryGetFolderFromPathAsync(result)); SetFolder(await GetStorageProvider().TryGetFolderFromPathAsync(result));
results.Items = new[] { result }; results.ItemsSource = new[] { result };
resultsVisible.IsVisible = true; resultsVisible.IsVisible = true;
} }
}; };
@ -164,7 +164,7 @@ namespace ControlCatalog.Pages
{ {
AllowDirectorySelection = true AllowDirectorySelection = true
}); });
results.Items = result; results.ItemsSource = result;
resultsVisible.IsVisible = result?.Any() == true; resultsVisible.IsVisible = result?.Any() == true;
}; };
this.Get<Button>("DecoratedWindow").Click += delegate this.Get<Button>("DecoratedWindow").Click += delegate
@ -332,7 +332,7 @@ namespace ControlCatalog.Pages
} }
} }
results.Items = mappedResults; results.ItemsSource = mappedResults;
resultsVisible.IsVisible = mappedResults.Any(); resultsVisible.IsVisible = mappedResults.Any();
} }
} }

2
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -30,7 +30,7 @@
<Button Command="{Binding RemoveItemCommand}">Remove</Button> <Button Command="{Binding RemoveItemCommand}">Remove</Button>
<Button Command="{Binding SelectRandomItemCommand}">Select Random Item</Button> <Button Command="{Binding SelectRandomItemCommand}">Select Random Item</Button>
</StackPanel> </StackPanel>
<ListBox Items="{Binding Items}" <ListBox ItemsSource="{Binding Items}"
Selection="{Binding Selection}" Selection="{Binding Selection}"
DisplayMemberBinding="{Binding (viewModels:ItemModel).ID, StringFormat='{}Item {0:N0}'}" DisplayMemberBinding="{Binding (viewModels:ItemModel).ID, StringFormat='{}Item {0:N0}'}"
AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}" AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}"

6
samples/ControlCatalog/Pages/MenuPage.xaml

@ -45,11 +45,11 @@
<StackPanel> <StackPanel>
<TextBlock Classes="h3" Margin="4 8">Dyanamically generated</TextBlock> <TextBlock Classes="h3" Margin="4 8">Dyanamically generated</TextBlock>
<Menu Items="{Binding MenuItems}"> <Menu ItemsSource="{Binding MenuItems}">
<Menu.Styles> <Menu.Styles>
<Style Selector="MenuItem" x:DataType="viewModels:MenuItemViewModel"> <Style Selector="MenuItem" x:DataType="viewModels:MenuItemViewModel">
<Setter Property="Header" Value="{Binding Header}"/> <Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/> <Setter Property="ItemsSource" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/> <Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/> <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style> </Style>
@ -68,7 +68,7 @@
<Separator/> <Separator/>
<MenuItem Header="Execu_te Script..." /> <MenuItem Header="Execu_te Script..." />
<Separator/> <Separator/>
<MenuItem Header="_Recent" Items="{Binding RecentItems}"> <MenuItem Header="_Recent" ItemsSource="{Binding RecentItems}">
<MenuItem.Styles> <MenuItem.Styles>
<Style Selector="MenuItem" x:DataType="viewModels:MenuItemViewModel"> <Style Selector="MenuItem" x:DataType="viewModels:MenuItemViewModel">
<Setter Property="Header" Value="{Binding Header}"/> <Setter Property="Header" Value="{Binding Header}"/>

8
samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs

@ -33,10 +33,10 @@ namespace ControlCatalog.Pages
{ {
new ContextMenu() new ContextMenu()
{ {
Items = new List<MenuItem> Items =
{ {
new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" } new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" }
} }
}.Open((Control)sender); }.Open((Control)sender);
} }

6
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -27,7 +27,7 @@
</Grid> </Grid>
<Grid Grid.Row="0" Grid.Column="1" Margin="8" ColumnDefinitions="Auto, 120" RowDefinitions="Auto,Auto,Auto,Auto,Auto"> <Grid Grid.Row="0" Grid.Column="1" Margin="8" ColumnDefinitions="Auto, 120" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock> <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock>
<ComboBox Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}" <ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
VerticalAlignment="Center" Margin="2"> VerticalAlignment="Center" Margin="2">
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>
<DataTemplate> <DataTemplate>
@ -41,11 +41,11 @@
</ComboBox> </ComboBox>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">ButtonSpinnerLocation:</TextBlock> <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">ButtonSpinnerLocation:</TextBlock>
<ComboBox Grid.Row="1" Grid.Column="1" Items="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}" <ComboBox Grid.Row="1" Grid.Column="1" ItemsSource="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}"
VerticalAlignment="Center" Margin="2"/> VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">CultureInfo:</TextBlock> <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">CultureInfo:</TextBlock>
<ComboBox x:Name="CultureSelector" Grid.Row="2" Grid.Column="1" Items="{Binding Cultures}" <ComboBox x:Name="CultureSelector" Grid.Row="2" Grid.Column="1" ItemsSource="{Binding Cultures}"
VerticalAlignment="Center" Margin="2"/> VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock> <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock>

2
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

@ -21,7 +21,7 @@
Margin="5"> Margin="5">
<ListBox HorizontalAlignment="Stretch" <ListBox HorizontalAlignment="Stretch"
VerticalAlignment="Top" VerticalAlignment="Top"
Items="{Binding Items}"/> ItemsSource="{Binding Items}"/>
</RefreshContainer> </RefreshContainer>
</DockPanel> </DockPanel>
</UserControl> </UserControl>

4
samples/ControlCatalog/Pages/ScrollSnapPage.xaml

@ -16,14 +16,14 @@
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Spacing="4"> Spacing="4">
<TextBlock Text="Snap Point Type" /> <TextBlock Text="Snap Point Type" />
<ComboBox Items="{Binding AvailableSnapPointsType}" <ComboBox ItemsSource="{Binding AvailableSnapPointsType}"
SelectedItem="{Binding SnapPointsType}" /> SelectedItem="{Binding SnapPointsType}" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Spacing="4"> Spacing="4">
<TextBlock Text="Snap Point Alignment" /> <TextBlock Text="Snap Point Alignment" />
<ComboBox Items="{Binding AvailableSnapPointsAlignment}" <ComboBox ItemsSource="{Binding AvailableSnapPointsAlignment}"
SelectedItem="{Binding SnapPointsAlignment}" /> SelectedItem="{Binding SnapPointsAlignment}" />
</StackPanel> </StackPanel>

4
samples/ControlCatalog/Pages/ScrollViewerPage.xaml

@ -13,12 +13,12 @@
<StackPanel Orientation="Vertical" Spacing="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Text="Horizontal Scroll" /> <TextBlock Text="Horizontal Scroll" />
<ComboBox Items="{Binding AvailableVisibility}" SelectedItem="{Binding HorizontalScrollVisibility}" /> <ComboBox ItemsSource="{Binding AvailableVisibility}" SelectedItem="{Binding HorizontalScrollVisibility}" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Vertical" Spacing="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Text="Vertical Scroll" /> <TextBlock Text="Vertical Scroll" />
<ComboBox Items="{Binding AvailableVisibility}" SelectedItem="{Binding VerticalScrollVisibility}" /> <ComboBox ItemsSource="{Binding AvailableVisibility}" SelectedItem="{Binding VerticalScrollVisibility}" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

2
samples/ControlCatalog/Pages/TabStripPage.xaml

@ -18,7 +18,7 @@
<Separator Margin="0 16"/> <Separator Margin="0 16"/>
<TextBlock Classes="h1">Dynamically generated</TextBlock> <TextBlock Classes="h1">Dynamically generated</TextBlock>
<TabStrip Items="{Binding Tabs}"> <TabStrip ItemsSource="{Binding Tabs}">
<TabStrip.Styles> <TabStrip.Styles>
<Style Selector="TabStripItem" x:DataType="viewModels:TabControlPageViewModelItem"> <Style Selector="TabStripItem" x:DataType="viewModels:TabControlPageViewModelItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/> <Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>

2
samples/ControlCatalog/Pages/ThemePage.axaml.cs

@ -12,7 +12,7 @@ namespace ControlCatalog.Pages
{ {
InitializeComponent(); InitializeComponent();
Selector.Items = new[] Selector.ItemsSource = new[]
{ {
ThemeVariant.Default, ThemeVariant.Default,
ThemeVariant.Dark, ThemeVariant.Dark,

2
samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml

@ -53,7 +53,7 @@
<StackPanel Margin="5" Spacing="5" Grid.IsSharedSizeScope="True"> <StackPanel Margin="5" Spacing="5" Grid.IsSharedSizeScope="True">
<HeaderedContentControl Header="Select a transition"> <HeaderedContentControl Header="Select a transition">
<ComboBox Items="{Binding PageTransitions}" SelectedItem="{Binding SelectedTransition}" /> <ComboBox ItemsSource="{Binding PageTransitions}" SelectedItem="{Binding SelectedTransition}" />
</HeaderedContentControl> </HeaderedContentControl>
<HeaderedContentControl Header="Duration"> <HeaderedContentControl Header="Duration">
<NumericUpDown Value="{Binding Duration}" Increment="250" Minimum="100" /> <NumericUpDown Value="{Binding Duration}" Increment="250" Minimum="100" />

2
samples/ControlCatalog/Pages/TreeViewPage.xaml

@ -11,7 +11,7 @@
HorizontalAlignment="Center" HorizontalAlignment="Center"
Spacing="16"> Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8"> <StackPanel Orientation="Vertical" Spacing="8">
<TreeView Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"> <TreeView ItemsSource="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
<TreeView.ItemTemplate> <TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}"> <TreeDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}"/> <TextBlock Text="{Binding Header}"/>

8
samples/IntegrationTestApp/MainWindow.axaml

@ -109,7 +109,7 @@
<StackPanel DockPanel.Dock="Bottom"> <StackPanel DockPanel.Dock="Bottom">
<Button Name="ListBoxSelectionClear">Clear Selection</Button> <Button Name="ListBoxSelectionClear">Clear Selection</Button>
</StackPanel> </StackPanel>
<ListBox Name="BasicListBox" Items="{Binding ListBoxItems}" SelectionMode="Multiple"/> <ListBox Name="BasicListBox" ItemsSource="{Binding ListBoxItems}" SelectionMode="Multiple"/>
</DockPanel> </DockPanel>
</TabItem> </TabItem>
@ -151,6 +151,12 @@
<ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem> <ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem> <ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox> </ComboBox>
<ComboBox Name="ShowWindowSystemDecorations" SelectedIndex="2">
<ComboBoxItem Name="ShowWindowSystemDecorationsNone">None</ComboBoxItem>
<ComboBoxItem Name="ShowWindowSystemDecorationsBorderOnly">BorderOnly</ComboBoxItem>
<ComboBoxItem Name="ShowWindowSystemDecorationsFull">Full</ComboBoxItem>
</ComboBox>
<CheckBox Name="ShowWindowExtendClientAreaToDecorationsHint">ExtendClientAreaToDecorationsHint</CheckBox>
<CheckBox Name="ShowWindowCanResize" IsChecked="True">Can Resize</CheckBox> <CheckBox Name="ShowWindowCanResize" IsChecked="True">Can Resize</CheckBox>
<Button Name="ShowWindow">Show Window</Button> <Button Name="ShowWindow">Show Window</Button>
<Button Name="SendToBack">Send to Back</Button> <Button Name="SendToBack">Send to Back</Button>

6
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -68,6 +68,8 @@ namespace IntegrationTestApp
var locationComboBox = this.GetControl<ComboBox>("ShowWindowLocation"); var locationComboBox = this.GetControl<ComboBox>("ShowWindowLocation");
var stateComboBox = this.GetControl<ComboBox>("ShowWindowState"); var stateComboBox = this.GetControl<ComboBox>("ShowWindowState");
var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null; var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null;
var systemDecorations = this.GetControl<ComboBox>("ShowWindowSystemDecorations");
var extendClientArea = this.GetControl<CheckBox>("ShowWindowExtendClientAreaToDecorationsHint");
var canResizeCheckBox = this.GetControl<CheckBox>("ShowWindowCanResize"); var canResizeCheckBox = this.GetControl<CheckBox>("ShowWindowCanResize");
var owner = (Window)this.GetVisualRoot()!; var owner = (Window)this.GetVisualRoot()!;
@ -95,6 +97,8 @@ namespace IntegrationTestApp
} }
sizeTextBox.Text = string.Empty; sizeTextBox.Text = string.Empty;
window.ExtendClientAreaToDecorationsHint = extendClientArea.IsChecked ?? false;
window.SystemDecorations = (SystemDecorations)systemDecorations.SelectedIndex;
window.WindowState = (WindowState)stateComboBox.SelectedIndex; window.WindowState = (WindowState)stateComboBox.SelectedIndex;
switch (modeComboBox.SelectedIndex) switch (modeComboBox.SelectedIndex)
@ -158,7 +162,7 @@ namespace IntegrationTestApp
var popup = new Popup var popup = new Popup
{ {
WindowManagerAddShadowHint = false, WindowManagerAddShadowHint = false,
PlacementMode = PlacementMode.AnchorAndGravity, Placement = PlacementMode.AnchorAndGravity,
PlacementAnchor = PopupAnchor.Top, PlacementAnchor = PopupAnchor.Top,
PlacementGravity = PopupGravity.Bottom, PlacementGravity = PopupGravity.Bottom,
Width= 200, Width= 200,

24
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -6,7 +6,7 @@
x:DataType="Window" x:DataType="Window"
Title="Show Window Test"> Title="Show Window Test">
<integrationTestApp:MeasureBorder Name="MyBorder"> <integrationTestApp:MeasureBorder Name="MyBorder">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label> <Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True" <TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}" /> Text="{Binding ClientSize, Mode=OneWay}" />
@ -35,13 +35,25 @@
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem> <ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox> </ComboBox>
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label> <Label Grid.Column="0" Grid.Row="8">SystemDecorations</Label>
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True" /> <ComboBox Name="CurrentSystemDecorations" Grid.Column="1" Grid.Row="8" SelectedIndex="{Binding SystemDecorations}">
<ComboBoxItem Name="SystemDecorationsNone">None</ComboBoxItem>
<ComboBoxItem Name="SystemDecorationsBorderOnly">BorderOnly</ComboBoxItem>
<ComboBoxItem Name="SystemDecorationsFull">Full</ComboBoxItem>
</ComboBox>
<CheckBox Name="CurrentExtendClientAreaToDecorationsHint" Grid.ColumnSpan="2" Grid.Row="9"
IsChecked="{Binding ExtendClientAreaToDecorationsHint}">
ExtendClientAreaToDecorationsHint
</CheckBox>
<Label Grid.Column="0" Grid.Row="10">Order (mac)</Label>
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="10" IsReadOnly="True" />
<Label Grid.Row="9" Content="MeasuredWith:" /> <Label Grid.Row="11" Content="MeasuredWith:" />
<TextBlock Grid.Column="1" Grid.Row="9" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" /> <TextBlock Grid.Column="1" Grid.Row="11" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
<Button Name="HideButton" Grid.Row="10" Command="{Binding $parent[Window].Hide}">Hide</Button> <Button Name="HideButton" Grid.Row="12" Command="{Binding $parent[Window].Hide}">Hide</Button>
</Grid> </Grid>
</integrationTestApp:MeasureBorder> </integrationTestApp:MeasureBorder>

2
samples/RenderDemo/Pages/RenderTargetBitmapPage.cs

@ -29,7 +29,7 @@ namespace RenderDemo.Pages
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
using (var ctx = _bitmap.CreateDrawingContext()) using (var ctx = _bitmap.CreateDrawingContext())
using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100) using (ctx.PushTransform(Matrix.CreateTranslation(-100, -100)
* Matrix.CreateRotation(_st.Elapsed.TotalSeconds) * Matrix.CreateRotation(_st.Elapsed.TotalSeconds)
* Matrix.CreateTranslation(100, 100))) * Matrix.CreateTranslation(100, 100)))
{ {

2
samples/SampleControls/HamburgerMenu/HamburgerMenu.cs

@ -57,7 +57,7 @@ namespace ControlSamples
{ {
if (_splitView is not null && _splitView.DisplayMode == SplitViewDisplayMode.Overlay) if (_splitView is not null && _splitView.DisplayMode == SplitViewDisplayMode.Overlay)
{ {
_splitView.SetValue(SplitView.IsPaneOpenProperty, false, Avalonia.Data.BindingPriority.Animation); _splitView.SetCurrentValue(SplitView.IsPaneOpenProperty, false);
} }
} }
} }

9
src/Avalonia.Base/CornerRadius.cs

@ -60,15 +60,6 @@ namespace Avalonia
/// </summary> /// </summary>
public double BottomLeft { get; } public double BottomLeft { get; }
/// <summary>
/// Gets a value indicating whether the instance has default values (all corner radii are set to 0).
/// </summary>
public bool IsDefault => TopLeft == 0 && TopRight == 0 && BottomLeft == 0 && BottomRight == 0;
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
/// <summary> /// <summary>
/// Gets a value indicating whether all corner radii are equal. /// Gets a value indicating whether all corner radii are equal.
/// </summary> /// </summary>

2
src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs

@ -180,7 +180,7 @@ namespace Avalonia.Input.GestureRecognizers
internal Velocity GetVelocity() internal Velocity GetVelocity()
{ {
var estimate = GetVelocityEstimate(); var estimate = GetVelocityEstimate();
if (estimate == null || estimate.PixelsPerSecond.IsDefault) if (estimate == null || estimate.PixelsPerSecond == default(Vector))
{ {
return new Velocity(Vector.Zero); return new Velocity(Vector.Zero);
} }

11
src/Avalonia.Base/Media/BoxShadow.cs

@ -45,15 +45,6 @@ namespace Avalonia.Media
} }
} }
/// <summary>
/// Gets a value indicating whether the instance has default values.
/// </summary>
public bool IsDefault => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0;
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
private readonly static char[] s_Separator = new char[] { ' ', '\t' }; private readonly static char[] s_Separator = new char[] { ' ', '\t' };
struct ArrayReader struct ArrayReader
@ -89,7 +80,7 @@ namespace Avalonia.Media
{ {
var sb = StringBuilderCache.Acquire(); var sb = StringBuilderCache.Acquire();
if (IsDefault) if (this == default)
{ {
return "none"; return "none";
} }

4
src/Avalonia.Base/Media/BoxShadows.cs

@ -21,7 +21,7 @@ namespace Avalonia.Media
{ {
_first = shadow; _first = shadow;
_list = null; _list = null;
Count = _first.IsDefault ? 0 : 1; Count = _first == default ? 0 : 1;
} }
public BoxShadows(BoxShadow first, BoxShadow[] rest) public BoxShadows(BoxShadow first, BoxShadow[] rest)
@ -120,7 +120,7 @@ namespace Avalonia.Media
get get
{ {
foreach(var boxShadow in this) foreach(var boxShadow in this)
if (!boxShadow.IsDefault && boxShadow.IsInset) if (boxShadow != default && boxShadow.IsInset)
return true; return true;
return false; return false;
} }

2
src/Avalonia.Base/Media/DrawingGroup.cs

@ -73,7 +73,7 @@ namespace Avalonia.Media
{ {
var bounds = GetBounds(); var bounds = GetBounds();
using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity)) using (context.PushTransform(Transform?.Value ?? Matrix.Identity))
using (context.PushOpacity(Opacity, bounds)) using (context.PushOpacity(Opacity, bounds))
using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default) using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default) using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default)

5
src/Avalonia.Base/Media/FontFamily.cs

@ -79,11 +79,6 @@ namespace Avalonia.Media
/// <remarks>Key is only used for custom fonts.</remarks> /// <remarks>Key is only used for custom fonts.</remarks>
public FontFamilyKey? Key { get; } public FontFamilyKey? Key { get; }
/// <summary>
/// Returns <c>True</c> if this instance is the system's default.
/// </summary>
public bool IsDefault => Name.Equals(DefaultFontFamilyName);
/// <summary> /// <summary>
/// Implicit conversion of string to FontFamily /// Implicit conversion of string to FontFamily
/// </summary> /// </summary>

5
src/Avalonia.Base/Media/FormattedText.cs

@ -1393,10 +1393,11 @@ namespace Avalonia.Media
} }
} }
if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsDefault) if (accumulatedBounds?.PlatformImpl == null ||
(accumulatedBounds.PlatformImpl.Bounds.Width == 0 && accumulatedBounds.PlatformImpl.Bounds.Height == 0))
{ {
return null; return null;
} }
return accumulatedBounds; return accumulatedBounds;
} }

2
src/Avalonia.Base/Media/ImageDrawing.cs

@ -42,7 +42,7 @@ namespace Avalonia.Media
var imageSource = ImageSource; var imageSource = ImageSource;
var rect = Rect; var rect = Rect;
if (imageSource is object && !rect.IsDefault) if (imageSource is object && (rect.Width != 0 || rect.Height != 0))
{ {
context.DrawImage(imageSource, rect); context.DrawImage(imageSource, rect);
} }

2
src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs

@ -77,7 +77,7 @@ namespace Avalonia.Media.Imaging
{ {
if (Source is not IBitmap bmp) if (Source is not IBitmap bmp)
return default; return default;
if (SourceRect.IsDefault) if (SourceRect.Width == 0 && SourceRect.Height == 0)
return Source.Size; return Source.Size;
return SourceRect.Size.ToSizeWithDpi(bmp.Dpi); return SourceRect.Size.ToSizeWithDpi(bmp.Dpi);
} }

2
src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs

@ -9,7 +9,7 @@ namespace Avalonia.Metadata;
/// A typical usage example is a ListBox control, where <see cref="InheritDataTypeFromItemsAttribute"/> is defined on the ItemTemplate property, /// A typical usage example is a ListBox control, where <see cref="InheritDataTypeFromItemsAttribute"/> is defined on the ItemTemplate property,
/// allowing the template to inherit the data type from the Items collection binding. /// allowing the template to inherit the data type from the Items collection binding.
/// </remarks> /// </remarks>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public sealed class InheritDataTypeFromItemsAttribute : Attribute public sealed class InheritDataTypeFromItemsAttribute : Attribute
{ {
/// <summary> /// <summary>

19
src/Avalonia.Base/PixelRect.cs

@ -9,12 +9,6 @@ namespace Avalonia
/// </summary> /// </summary>
public readonly struct PixelRect : IEquatable<PixelRect> public readonly struct PixelRect : IEquatable<PixelRect>
{ {
/// <summary>
/// An empty rectangle.
/// </summary>
[Obsolete("Use the default keyword instead.")]
public static readonly PixelRect Empty = default;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PixelRect"/> structure. /// Initializes a new instance of the <see cref="PixelRect"/> structure.
/// </summary> /// </summary>
@ -133,15 +127,6 @@ namespace Avalonia
/// </summary> /// </summary>
public PixelPoint Center => new PixelPoint(X + (Width / 2), Y + (Height / 2)); public PixelPoint Center => new PixelPoint(X + (Width / 2), Y + (Height / 2));
/// <summary>
/// Gets a value indicating whether the instance has default values (the rectangle is empty).
/// </summary>
public bool IsDefault => Width == 0 && Height == 0;
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
/// <summary> /// <summary>
/// Checks for equality between two <see cref="PixelRect"/>s. /// Checks for equality between two <see cref="PixelRect"/>s.
/// </summary> /// </summary>
@ -295,11 +280,11 @@ namespace Avalonia
/// <returns>The union.</returns> /// <returns>The union.</returns>
public PixelRect Union(PixelRect rect) public PixelRect Union(PixelRect rect)
{ {
if (IsDefault) if (Width == 0 && Height == 0)
{ {
return rect; return rect;
} }
else if (rect.IsDefault) else if (rect.Width == 0 && rect.Height == 0)
{ {
return this; return this;
} }

17
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -23,7 +24,7 @@ internal static class StorageProviderHelpers
return null; return null;
} }
public static Uri FilePathToUri(string path) public static Uri FilePathToUri(string path)
{ {
var uriPath = new StringBuilder(path) var uriPath = new StringBuilder(path)
@ -35,6 +36,20 @@ internal static class StorageProviderHelpers
return new UriBuilder("file", string.Empty) { Path = uriPath }.Uri; return new UriBuilder("file", string.Empty) { Path = uriPath }.Uri;
} }
public static bool TryFilePathToUri(string path, [NotNullWhen(true)] out Uri? uri)
{
try
{
uri = FilePathToUri(path);
return true;
}
catch
{
uri = null;
return false;
}
}
public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter) public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
{ {
var name = Path.GetFileName(path); var name = Path.GetFileName(path);

16
src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs

@ -16,8 +16,13 @@ public static class StorageProviderExtensions
{ {
return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile); return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile);
} }
return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath)); if (StorageProviderHelpers.TryFilePathToUri(filePath, out var uri))
{
return provider.TryGetFileFromPathAsync(uri);
}
return Task.FromResult<IStorageFile?>(null);
} }
/// <inheritdoc cref="IStorageProvider.TryGetFolderFromPathAsync"/> /// <inheritdoc cref="IStorageProvider.TryGetFolderFromPathAsync"/>
@ -29,7 +34,12 @@ public static class StorageProviderExtensions
return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder); return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder);
} }
return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath)); if (StorageProviderHelpers.TryFilePathToUri(folderPath, out var uri))
{
return provider.TryGetFolderFromPathAsync(uri);
}
return Task.FromResult<IStorageFolder?>(null);
} }
/// <summary> /// <summary>

8
src/Avalonia.Base/Point.cs

@ -288,13 +288,5 @@ namespace Avalonia
x = this._x; x = this._x;
y = this._y; y = this._y;
} }
/// <summary>
/// Gets a value indicating whether the X and Y coordinates are zero.
/// </summary>
public bool IsDefault
{
get { return (_x == 0) && (_y == 0); }
}
} }
} }

2
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@ -31,7 +31,7 @@ namespace Avalonia.PropertyStore
var value = inherited is null ? _metadata.DefaultValue : inherited.Value; var value = inherited is null ? _metadata.DefaultValue : inherited.Value;
if (property.HasCoercion && _metadata.CoerceValue is { } coerce) if (_metadata.CoerceValue is { } coerce)
{ {
_uncommon = new() _uncommon = new()
{ {

34
src/Avalonia.Base/Rect.cs

@ -16,12 +16,6 @@ namespace Avalonia
Animation.Animation.RegisterAnimator<RectAnimator>(prop => typeof(Rect).IsAssignableFrom(prop.PropertyType)); Animation.Animation.RegisterAnimator<RectAnimator>(prop => typeof(Rect).IsAssignableFrom(prop.PropertyType));
} }
/// <summary>
/// An empty rectangle.
/// </summary>
[Obsolete("Use the default keyword instead.")]
public static readonly Rect Empty = default;
/// <summary> /// <summary>
/// The X position. /// The X position.
/// </summary> /// </summary>
@ -170,17 +164,6 @@ namespace Avalonia
/// </summary> /// </summary>
public Point Center => new Point(_x + (_width / 2), _y + (_height / 2)); public Point Center => new Point(_x + (_width / 2), _y + (_height / 2));
/// <summary>
/// Gets a value indicating whether the instance has default values (the rectangle is empty).
/// </summary>
// ReSharper disable CompareOfFloatsByEqualityOperator
public bool IsDefault => _width == 0 && _height == 0;
// ReSharper restore CompareOfFloatsByEqualityOperator
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
/// <summary> /// <summary>
/// Checks for equality between two <see cref="Rect"/>s. /// Checks for equality between two <see cref="Rect"/>s.
/// </summary> /// </summary>
@ -517,19 +500,18 @@ namespace Avalonia
return rect; return rect;
} }
/// <summary>
/// <summary> /// Gets the union of two rectangles.
/// Gets the union of two rectangles. /// </summary>
/// </summary> /// <param name="rect">The other rectangle.</param>
/// <param name="rect">The other rectangle.</param> /// <returns>The union.</returns>
/// <returns>The union.</returns> public Rect Union(Rect rect)
public Rect Union(Rect rect)
{ {
if (IsDefault) if (Width == 0 && Height == 0)
{ {
return rect; return rect;
} }
else if (rect.IsDefault) else if (rect.Width == 0 && rect.Height == 0)
{ {
return this; return this;
} }

12
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -130,8 +130,8 @@ namespace Avalonia.Rendering.Composition.Server
} }
_renderTarget ??= _compositor.CreateRenderTarget(_surfaces()); _renderTarget ??= _compositor.CreateRenderTarget(_surfaces());
if(_dirtyRect.IsDefault && !_redrawRequested) if ((_dirtyRect.Width == 0 && _dirtyRect.Height == 0) && !_redrawRequested)
return; return;
Revision++; Revision++;
@ -163,7 +163,7 @@ namespace Avalonia.Rendering.Composition.Server
_dirtyRect = new Rect(0, 0, layerSize.Width, layerSize.Height); _dirtyRect = new Rect(0, 0, layerSize.Width, layerSize.Height);
} }
if (!_dirtyRect.IsDefault) if (_dirtyRect.Width != 0 || _dirtyRect.Height != 0)
{ {
using (var context = _layer.CreateDrawingContext()) using (var context = _layer.CreateDrawingContext())
{ {
@ -260,7 +260,7 @@ namespace Avalonia.Rendering.Composition.Server
public void AddDirtyRect(Rect rect) public void AddDirtyRect(Rect rect)
{ {
if(rect.IsDefault) if (rect.Width == 0 && rect.Height == 0)
return; return;
var snapped = SnapToDevicePixels(rect, Scaling); var snapped = SnapToDevicePixels(rect, Scaling);
DebugEvents?.RectInvalidated(rect); DebugEvents?.RectInvalidated(rect);
@ -275,7 +275,7 @@ namespace Avalonia.Rendering.Composition.Server
public void Dispose() public void Dispose()
{ {
if(_disposed) if (_disposed)
return; return;
_disposed = true; _disposed = true;
using (_compositor.RenderInterface.EnsureCurrent()) using (_compositor.RenderInterface.EnsureCurrent())
@ -302,7 +302,7 @@ namespace Avalonia.Rendering.Composition.Server
{ {
if (_attachedVisuals.Remove(visual) && IsEnabled) if (_attachedVisuals.Remove(visual) && IsEnabled)
visual.Deactivate(); visual.Deactivate();
if(visual.IsVisibleInFrame) if (visual.IsVisibleInFrame)
AddDirtyRect(visual.TransformedOwnContentBounds); AddDirtyRect(visual.TransformedOwnContentBounds);
} }

55
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -23,21 +23,20 @@ namespace Avalonia.Rendering.Composition.Server
private bool _isBackface; private bool _isBackface;
private Rect? _transformedClipBounds; private Rect? _transformedClipBounds;
private Rect _combinedTransformedClipBounds; private Rect _combinedTransformedClipBounds;
protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{ {
} }
public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{ {
if(Visible == false || IsVisibleInFrame == false) if (Visible == false || IsVisibleInFrame == false)
return; return;
if(Opacity == 0) if (Opacity == 0)
return; return;
currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds); currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds);
if(currentTransformedClip.IsDefault) if (currentTransformedClip.Width == 0 && currentTransformedClip.Height == 0)
return; return;
Root!.RenderedVisuals++; Root!.RenderedVisuals++;
@ -61,7 +60,7 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PushClip(Root!.SnapToDevicePixels(boundsRect)); canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
if (Clip != null) if (Clip != null)
canvas.PushGeometryClip(Clip); canvas.PushGeometryClip(Clip);
if(OpacityMaskBrush != null) if (OpacityMaskBrush != null)
canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); canvas.PushOpacityMask(OpacityMaskBrush, boundsRect);
RenderCore(canvas, currentTransformedClip); RenderCore(canvas, currentTransformedClip);
@ -78,12 +77,12 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PopClip(); canvas.PopClip();
if (AdornedVisual != null && AdornerIsClipped) if (AdornedVisual != null && AdornerIsClipped)
canvas.PopClip(); canvas.PopClip();
if(Opacity != 1) if (Opacity != 1)
canvas.PopOpacity(); canvas.PopOpacity();
} }
protected virtual bool HandlesClipToBounds => false; protected virtual bool HandlesClipToBounds => false;
private ReadbackData _readback0, _readback1, _readback2; private ReadbackData _readback0, _readback1, _readback2;
/// <summary> /// <summary>
@ -98,17 +97,17 @@ namespace Avalonia.Rendering.Composition.Server
return ref _readback1; return ref _readback1;
return ref _readback2; return ref _readback2;
} }
public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity; public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity;
public Matrix4x4 GlobalTransformMatrix { get; private set; } public Matrix4x4 GlobalTransformMatrix { get; private set; }
public virtual void Update(ServerCompositionTarget root) public virtual void Update(ServerCompositionTarget root)
{ {
if(Parent == null && Root == null) if (Parent == null && Root == null)
return; return;
var wasVisible = IsVisibleInFrame; var wasVisible = IsVisibleInFrame;
// Calculate new parent-relative transform // Calculate new parent-relative transform
if (_combinedTransformDirty) if (_combinedTransformDirty)
{ {
@ -122,7 +121,7 @@ namespace Avalonia.Rendering.Composition.Server
var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix4x4.Identity; var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix4x4.Identity;
var newTransform = CombinedTransformMatrix * parentTransform; var newTransform = CombinedTransformMatrix * parentTransform;
// Check if visual was moved and recalculate face orientation // Check if visual was moved and recalculate face orientation
var positionChanged = false; var positionChanged = false;
if (GlobalTransformMatrix != newTransform) if (GlobalTransformMatrix != newTransform)
@ -134,23 +133,23 @@ namespace Avalonia.Rendering.Composition.Server
var oldTransformedContentBounds = TransformedOwnContentBounds; var oldTransformedContentBounds = TransformedOwnContentBounds;
var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds; var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds;
if (_parent?.IsDirtyComposition == true) if (_parent?.IsDirtyComposition == true)
{ {
IsDirtyComposition = true; IsDirtyComposition = true;
_isDirtyForUpdate = true; _isDirtyForUpdate = true;
} }
var invalidateOldBounds = _isDirtyForUpdate; var invalidateOldBounds = _isDirtyForUpdate;
var invalidateNewBounds = _isDirtyForUpdate; var invalidateNewBounds = _isDirtyForUpdate;
GlobalTransformMatrix = newTransform; GlobalTransformMatrix = newTransform;
var ownBounds = OwnContentBounds; var ownBounds = OwnContentBounds;
if (ownBounds != _oldOwnContentBounds || positionChanged) if (ownBounds != _oldOwnContentBounds || positionChanged)
{ {
_oldOwnContentBounds = ownBounds; _oldOwnContentBounds = ownBounds;
if (ownBounds.IsDefault) if (ownBounds.Width == 0 && ownBounds.Height == 0)
TransformedOwnContentBounds = default; TransformedOwnContentBounds = default;
else else
TransformedOwnContentBounds = TransformedOwnContentBounds =
@ -171,16 +170,16 @@ namespace Avalonia.Rendering.Composition.Server
AdornedVisual?._combinedTransformedClipBounds AdornedVisual?._combinedTransformedClipBounds
?? Parent?._combinedTransformedClipBounds ?? Parent?._combinedTransformedClipBounds
?? new Rect(Root!.Size); ?? new Rect(Root!.Size);
if (_transformedClipBounds != null) if (_transformedClipBounds != null)
_combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value); _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value);
EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1); EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1);
IsHitTestVisibleInFrame = _parent?.IsHitTestVisibleInFrame != false IsHitTestVisibleInFrame = _parent?.IsHitTestVisibleInFrame != false
&& Visible && Visible
&& !_isBackface && !_isBackface
&& !_combinedTransformedClipBounds.IsDefault; && (_combinedTransformedClipBounds.Width != 0 || _combinedTransformedClipBounds.Height != 0);
IsVisibleInFrame = IsHitTestVisibleInFrame IsVisibleInFrame = IsHitTestVisibleInFrame
&& _parent?.IsVisibleInFrame != false && _parent?.IsVisibleInFrame != false
@ -213,11 +212,11 @@ namespace Avalonia.Rendering.Composition.Server
void AddDirtyRect(Rect rc) void AddDirtyRect(Rect rc)
{ {
if(rc == default) if (rc == default)
return; return;
Root?.AddDirtyRect(rc); Root?.AddDirtyRect(rc);
} }
/// <summary> /// <summary>
/// Data that can be read from the UI thread /// Data that can be read from the UI thread
/// </summary> /// </summary>
@ -228,7 +227,7 @@ namespace Avalonia.Rendering.Composition.Server
public long TargetId; public long TargetId;
public bool Visible; public bool Visible;
} }
partial void DeserializeChangesExtra(BatchStreamReader c) partial void DeserializeChangesExtra(BatchStreamReader c)
{ {
ValuesInvalidated(); ValuesInvalidated();
@ -245,9 +244,8 @@ namespace Avalonia.Rendering.Composition.Server
protected virtual void OnDetachedFromRoot(ServerCompositionTarget target) protected virtual void OnDetachedFromRoot(ServerCompositionTarget target)
{ {
} }
partial void OnRootChanged() partial void OnRootChanged()
{ {
if (Root != null) if (Root != null)
@ -256,12 +254,11 @@ namespace Avalonia.Rendering.Composition.Server
OnAttachedToRoot(Root); OnAttachedToRoot(Root);
} }
} }
protected virtual void OnAttachedToRoot(ServerCompositionTarget target) protected virtual void OnAttachedToRoot(ServerCompositionTarget target)
{ {
} }
protected override void ValuesInvalidated() protected override void ValuesInvalidated()
{ {
_isDirtyForUpdate = true; _isDirtyForUpdate = true;
@ -274,6 +271,4 @@ namespace Avalonia.Rendering.Composition.Server
public Rect TransformedOwnContentBounds { get; set; } public Rect TransformedOwnContentBounds { get; set; }
public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y); public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y);
} }
} }

2
src/Avalonia.Base/Rendering/DirtyRects.cs

@ -30,7 +30,7 @@ namespace Avalonia.Rendering
/// </remarks> /// </remarks>
public void Add(Rect rect) public void Add(Rect rect)
{ {
if (!rect.IsDefault) if (rect.Width != 0 || rect.Height != 0)
{ {
for (var i = 0; i < _rects.Count; ++i) for (var i = 0; i < _rects.Count; ++i)
{ {

4
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@ -83,7 +83,7 @@ namespace Avalonia.Rendering
} }
} }
using (context.PushPostTransform(m)) using (context.PushTransform(m))
using (context.PushOpacity(opacity, bounds)) using (context.PushOpacity(opacity, bounds))
using (clipToBounds using (clipToBounds
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete
@ -95,7 +95,7 @@ namespace Avalonia.Rendering
using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default) using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default)
using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default) using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default)
using (context.PushTransformContainer()) using (context.PushTransform(Matrix.Identity))
{ {
visual.Render(context); visual.Render(context);

11
src/Avalonia.Base/Size.cs

@ -27,12 +27,6 @@ namespace Avalonia
/// </summary> /// </summary>
public static readonly Size Infinity = new Size(double.PositiveInfinity, double.PositiveInfinity); public static readonly Size Infinity = new Size(double.PositiveInfinity, double.PositiveInfinity);
/// <summary>
/// A size representing zero.
/// </summary>
[Obsolete("Use the default keyword instead.")]
public static readonly Size Empty = new Size(0, 0);
/// <summary> /// <summary>
/// The width. /// The width.
/// </summary> /// </summary>
@ -306,10 +300,5 @@ namespace Avalonia
width = this._width; width = this._width;
height = this._height; height = this._height;
} }
/// <summary>
/// Gets a value indicating whether the Width and Height values are zero.
/// </summary>
public bool IsDefault => (_width == 0) && (_height == 0);
} }
} }

14
src/Avalonia.Base/StyledProperty.cs

@ -34,7 +34,6 @@ namespace Avalonia
{ {
Inherits = inherits; Inherits = inherits;
ValidateValue = validate; ValidateValue = validate;
HasCoercion |= metadata.CoerceValue != null;
if (validate?.Invoke(metadata.DefaultValue) == false) if (validate?.Invoke(metadata.DefaultValue) == false)
{ {
@ -48,12 +47,6 @@ namespace Avalonia
/// </summary> /// </summary>
public Func<TValue, bool>? ValidateValue { get; } public Func<TValue, bool>? ValidateValue { get; }
/// <summary>
/// Gets a value indicating whether this property has any value coercion callbacks defined
/// in its metadata.
/// </summary>
internal bool HasCoercion { get; private set; }
/// <summary> /// <summary>
/// Registers the property on another type. /// Registers the property on another type.
/// </summary> /// </summary>
@ -130,10 +123,7 @@ namespace Avalonia
/// </summary> /// </summary>
/// <typeparam name="T">The type.</typeparam> /// <typeparam name="T">The type.</typeparam>
/// <param name="metadata">The metadata.</param> /// <param name="metadata">The metadata.</param>
public void OverrideMetadata<T>(StyledPropertyMetadata<TValue> metadata) where T : AvaloniaObject public void OverrideMetadata<T>(StyledPropertyMetadata<TValue> metadata) where T : AvaloniaObject => OverrideMetadata(typeof(T), metadata);
{
base.OverrideMetadata(typeof(T), metadata);
}
/// <summary> /// <summary>
/// Overrides the metadata for the property on the specified type. /// Overrides the metadata for the property on the specified type.
@ -151,8 +141,6 @@ namespace Avalonia
} }
} }
HasCoercion |= metadata.CoerceValue != null;
base.OverrideMetadata(type, metadata); base.OverrideMetadata(type, metadata);
} }

10
src/Avalonia.Base/Thickness.cs

@ -97,10 +97,6 @@ namespace Avalonia
/// </summary> /// </summary>
public double Bottom => _bottom; public double Bottom => _bottom;
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
/// <summary> /// <summary>
/// Gets a value indicating whether all sides are equal. /// Gets a value indicating whether all sides are equal.
/// </summary> /// </summary>
@ -293,11 +289,5 @@ namespace Avalonia
right = this._right; right = this._right;
bottom = this._bottom; bottom = this._bottom;
} }
/// <summary>
/// Gets a value indicating whether the instance has default values
/// (the left, top, right and bottom values are zero).
/// </summary>
public bool IsDefault => (_left == 0) && (_top == 0) && (_right == 0) && (_bottom == 0);
} }
} }

8
src/Avalonia.Base/Vector.cs

@ -360,13 +360,5 @@ namespace Avalonia
x = this._x; x = this._x;
y = this._y; y = this._y;
} }
/// <summary>
/// Gets a value indicating whether the X and Y components are zero.
/// </summary>
public bool IsDefault
{
get { return (_x == 0) && (_y == 0); }
}
} }
} }

2
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

@ -165,7 +165,7 @@
</TabItem.Header> </TabItem.Header>
<ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}" <ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}" ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
Items="{TemplateBinding PaletteColors}" ItemsSource="{TemplateBinding PaletteColors}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}" SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
UseLayoutRounding="False" UseLayoutRounding="False"
Margin="12"> Margin="12">

2
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@ -414,7 +414,7 @@
</TabItem.Header> </TabItem.Header>
<ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}" <ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}" ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
Items="{TemplateBinding PaletteColors}" ItemsSource="{TemplateBinding PaletteColors}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}" SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
UseLayoutRounding="False" UseLayoutRounding="False"
Margin="12"> Margin="12">

2
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml

@ -165,7 +165,7 @@
</TabItem.Header> </TabItem.Header>
<ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}" <ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}" ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
Items="{TemplateBinding PaletteColors}" ItemsSource="{TemplateBinding PaletteColors}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}" SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
UseLayoutRounding="False" UseLayoutRounding="False"
Margin="12"> Margin="12">

2
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml

@ -376,7 +376,7 @@
</TabItem.Header> </TabItem.Header>
<ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}" <ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}" ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
Items="{TemplateBinding PaletteColors}" ItemsSource="{TemplateBinding PaletteColors}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}" SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
UseLayoutRounding="False" UseLayoutRounding="False"
Margin="12"> Margin="12">

8
src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs

@ -177,14 +177,14 @@ namespace Avalonia.Controls
} }
bool? uneditedValue = editingCheckBox.IsChecked; bool? uneditedValue = editingCheckBox.IsChecked;
if(editingEventArgs is PointerPressedEventArgs args) if (editingEventArgs is PointerPressedEventArgs args)
{ {
void ProcessPointerArgs() void ProcessPointerArgs()
{ {
// Editing was triggered by a mouse click // Editing was triggered by a mouse click
Point position = args.GetPosition(editingCheckBox); Point position = args.GetPosition(editingCheckBox);
Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height); Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
if(rect.Contains(position)) if (rect.Contains(position))
{ {
EditValue(); EditValue();
} }
@ -192,14 +192,14 @@ namespace Avalonia.Controls
void OnLayoutUpdated(object sender, EventArgs e) void OnLayoutUpdated(object sender, EventArgs e)
{ {
if(!editingCheckBox.Bounds.IsDefault) if (editingCheckBox.Bounds.Width != 0 || editingCheckBox.Bounds.Height != 0)
{ {
editingCheckBox.LayoutUpdated -= OnLayoutUpdated; editingCheckBox.LayoutUpdated -= OnLayoutUpdated;
ProcessPointerArgs(); ProcessPointerArgs();
} }
} }
if(editingCheckBox.Bounds.IsDefault) if (editingCheckBox.Bounds.Width == 0 && editingCheckBox.Bounds.Height == 0)
{ {
editingCheckBox.LayoutUpdated += OnLayoutUpdated; editingCheckBox.LayoutUpdated += OnLayoutUpdated;
} }

5
src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs

@ -39,7 +39,10 @@ namespace Avalonia.Controls
/// Defines the <see cref="Items"/> property. /// Defines the <see cref="Items"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ItemsRepeater, IEnumerable?> ItemsProperty = public static readonly DirectProperty<ItemsRepeater, IEnumerable?> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<ItemsRepeater>(o => o.Items, (o, v) => o.Items = v); AvaloniaProperty.RegisterDirect<ItemsRepeater, IEnumerable?>(
nameof(Items),
o => o.Items,
(o, v) => o.Items = v);
/// <summary> /// <summary>
/// Defines the <see cref="Layout"/> property. /// Defines the <see cref="Layout"/> property.

4
src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs

@ -441,7 +441,7 @@ namespace Avalonia.Controls
_pendingViewportShift = default; _pendingViewportShift = default;
_unshiftableShift = default; _unshiftableShift = default;
if (_visibleWindow.IsDefault) if (_visibleWindow.Width == 0 && _visibleWindow.Height == 0)
{ {
// We got cleared. // We got cleared.
_layoutExtent = default; _layoutExtent = default;
@ -527,7 +527,7 @@ namespace Avalonia.Controls
private void TryInvalidateMeasure() private void TryInvalidateMeasure()
{ {
// Don't invalidate measure if we have an invalid window. // Don't invalidate measure if we have an invalid window.
if (!_visibleWindow.IsDefault) if (_visibleWindow.Width != 0 || _visibleWindow.Height != 0)
{ {
// We invalidate measure instead of just invalidating arrange because // We invalidate measure instead of just invalidating arrange because
// we don't invalidate measure in UpdateViewport if the view is changing to // we don't invalidate measure in UpdateViewport if the view is changing to

12
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs

@ -87,12 +87,10 @@ namespace Avalonia.Controls
/// Identifies the <see cref="Text" /> property. /// Identifies the <see cref="Text" /> property.
/// </summary> /// </summary>
/// <value>The identifier for the <see cref="Text" /> property.</value> /// <value>The identifier for the <see cref="Text" /> property.</value>
public static readonly DirectProperty<AutoCompleteBox, string?> TextProperty = public static readonly StyledProperty<string?> TextProperty =
TextBlock.TextProperty.AddOwnerWithDataValidation<AutoCompleteBox>( TextBlock.TextProperty.AddOwner<AutoCompleteBox>(new(string.Empty,
o => o.Text,
(o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay, defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true); enableDataValidation: true));
/// <summary> /// <summary>
/// Identifies the <see cref="SearchText" /> property. /// Identifies the <see cref="SearchText" /> property.
@ -317,8 +315,8 @@ namespace Avalonia.Controls
/// <see cref="AutoCompleteBox" /> control.</value> /// <see cref="AutoCompleteBox" /> control.</value>
public string? Text public string? Text
{ {
get => _text; get => GetValue(TextProperty);
set => SetAndRaise(TextProperty, ref _text, value); set => SetValue(TextProperty, value);
} }
/// <summary> /// <summary>

3
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs

@ -198,7 +198,6 @@ namespace Avalonia.Controls
private bool _isDropDownOpen; private bool _isDropDownOpen;
private bool _isFocused = false; private bool _isFocused = false;
private string? _text = string.Empty;
private string? _searchText = string.Empty; private string? _searchText = string.Empty;
private AutoCompleteFilterPredicate<object?>? _itemFilter; private AutoCompleteFilterPredicate<object?>? _itemFilter;
@ -1275,7 +1274,7 @@ namespace Avalonia.Controls
if ((userInitiated ?? true) && Text != value) if ((userInitiated ?? true) && Text != value)
{ {
_ignoreTextPropertyChange++; _ignoreTextPropertyChange++;
Text = value; SetCurrentValue(TextProperty, value);
callTextChanged = true; callTextChanged = true;
} }

2
src/Avalonia.Controls/BorderVisual.cs

@ -50,7 +50,7 @@ class CompositionBorderVisual : CompositionDrawListVisual
if (ClipToBounds) if (ClipToBounds)
{ {
var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y))); var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y)));
if (_cornerRadius.IsDefault) if (_cornerRadius == default)
canvas.PushClip(clipRect); canvas.PushClip(clipRect);
else else
canvas.PushClip(new RoundedRect(clipRect, _cornerRadius)); canvas.PushClip(new RoundedRect(clipRect, _cornerRadius));

20
src/Avalonia.Controls/ContainerClearingEventArgs.cs

@ -0,0 +1,20 @@
using System;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsControl.ContainerClearing"/> event.
/// </summary>
public class ContainerClearingEventArgs : EventArgs
{
public ContainerClearingEventArgs(Control container)
{
Container = container;
}
/// <summary>
/// Gets the prepared container.
/// </summary>
public Control Container { get; }
}
}

32
src/Avalonia.Controls/ContainerIndexChangedEventArgs.cs

@ -0,0 +1,32 @@
using System;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsControl.ContainerIndexChanged"/> event.
/// </summary>
public class ContainerIndexChangedEventArgs : EventArgs
{
public ContainerIndexChangedEventArgs(Control container, int oldIndex, int newIndex)
{
Container = container;
OldIndex = oldIndex;
NewIndex = newIndex;
}
/// <summary>
/// Get the container for which the index changed.
/// </summary>
public Control Container { get; }
/// <summary>
/// Gets the index of the container after the change.
/// </summary>
public int NewIndex { get; }
/// <summary>
/// Gets the index of the container before the change.
/// </summary>
public int OldIndex { get; }
}
}

26
src/Avalonia.Controls/ContainerPreparedEventArgs.cs

@ -0,0 +1,26 @@
using System;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsControl.ContainerPrepared"/> event.
/// </summary>
public class ContainerPreparedEventArgs : EventArgs
{
public ContainerPreparedEventArgs(Control container, int index)
{
Container = container;
Index = index;
}
/// <summary>
/// Gets the prepared container.
/// </summary>
public Control Container { get; }
/// <summary>
/// Gets the index of the item the container was prepared for.
/// </summary>
public int Index { get; }
}
}

103
src/Avalonia.Controls/ContextMenu.cs

@ -54,11 +54,17 @@ namespace Avalonia.Controls
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty = public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
Popup.PlacementGravityProperty.AddOwner<ContextMenu>(); Popup.PlacementGravityProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="Placement"/> property.
/// </summary>
public static readonly StyledProperty<PlacementMode> PlacementProperty =
Popup.PlacementProperty.AddOwner<ContextMenu>();
/// <summary> /// <summary>
/// Defines the <see cref="PlacementMode"/> property. /// Defines the <see cref="PlacementMode"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<PlacementMode> PlacementModeProperty = [Obsolete("Use the Placement property instead.")]
Popup.PlacementModeProperty.AddOwner<ContextMenu>(); public static readonly StyledProperty<PlacementMode> PlacementModeProperty = PlacementProperty;
/// <summary> /// <summary>
/// Defines the <see cref="PlacementRect"/> property. /// Defines the <see cref="PlacementRect"/> property.
@ -108,99 +114,80 @@ namespace Avalonia.Controls
static ContextMenu() static ContextMenu()
{ {
ItemsPanelProperty.OverrideDefaultValue<ContextMenu>(DefaultPanel); ItemsPanelProperty.OverrideDefaultValue<ContextMenu>(DefaultPanel);
PlacementModeProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer); PlacementProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<ContextMenu>(AccessibilityView.Control); AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<ContextMenu>(AccessibilityView.Control);
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<ContextMenu>(AutomationControlType.Menu); AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<ContextMenu>(AutomationControlType.Menu);
} }
/// <summary> /// <inheritdoc cref="Popup.HorizontalOffset"/>
/// Gets or sets the Horizontal offset of the context menu in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double HorizontalOffset public double HorizontalOffset
{ {
get { return GetValue(HorizontalOffsetProperty); } get => GetValue(HorizontalOffsetProperty);
set { SetValue(HorizontalOffsetProperty, value); } set => SetValue(HorizontalOffsetProperty, value);
} }
/// <summary> /// <inheritdoc cref="Popup.VerticalOffset"/>
/// Gets or sets the Vertical offset of the context menu in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double VerticalOffset public double VerticalOffset
{ {
get { return GetValue(VerticalOffsetProperty); } get => GetValue(VerticalOffsetProperty);
set { SetValue(VerticalOffsetProperty, value); } set => SetValue(VerticalOffsetProperty, value);
} }
/// <summary> /// <inheritdoc cref="Popup.PlacementAnchor"/>
/// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="PlacementMode"/>
/// is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupAnchor PlacementAnchor public PopupAnchor PlacementAnchor
{ {
get { return GetValue(PlacementAnchorProperty); } get => GetValue(PlacementAnchorProperty);
set { SetValue(PlacementAnchorProperty, value); } set => SetValue(PlacementAnchorProperty, value);
} }
/// <summary> /// <inheritdoc cref="Popup.PlacementConstraintAdjustment"/>
/// Gets or sets a value describing how the context menu position will be adjusted if the
/// unadjusted position would result in the context menu being partly constrained.
/// </summary>
public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
{ {
get { return GetValue(PlacementConstraintAdjustmentProperty); } get => GetValue(PlacementConstraintAdjustmentProperty);
set { SetValue(PlacementConstraintAdjustmentProperty, value); } set => SetValue(PlacementConstraintAdjustmentProperty, value);
} }
/// <summary> /// <inheritdoc cref="Popup.PlacementGravity"/>
/// Gets or sets a value which defines in what direction the context menu should open
/// when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupGravity PlacementGravity public PopupGravity PlacementGravity
{ {
get { return GetValue(PlacementGravityProperty); } get => GetValue(PlacementGravityProperty);
set { SetValue(PlacementGravityProperty, value); } set => SetValue(PlacementGravityProperty, value);
} }
/// <summary> /// <inheritdoc cref="Placement"/>
/// Gets or sets the placement mode of the context menu in relation to the<see cref="PlacementTarget"/>. [Obsolete("Use the Placement property instead.")]
/// </summary>
public PlacementMode PlacementMode public PlacementMode PlacementMode
{ {
get { return GetValue(PlacementModeProperty); } get => GetValue(PlacementProperty);
set { SetValue(PlacementModeProperty, value); } set => SetValue(PlacementProperty, value);
}
/// <inheritdoc cref="Popup.Placement"/>
public PlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
} }
public bool WindowManagerAddShadowHint public bool WindowManagerAddShadowHint
{ {
get { return GetValue(WindowManagerAddShadowHintProperty); } get => GetValue(WindowManagerAddShadowHintProperty);
set { SetValue(WindowManagerAddShadowHintProperty, value); } set => SetValue(WindowManagerAddShadowHintProperty, value);
} }
/// <summary> /// <inheritdoc cref="Popup.PlacementRect"/>
/// Gets or sets the the anchor rectangle within the parent that the context menu will be placed
/// relative to when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
/// <remarks>
/// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
/// which the popup will be opened, with <see cref="PlacementAnchor"/> determining which edge
/// of the placement target is used.
///
/// If unset, the anchor rectangle will be the bounds of the <see cref="PlacementTarget"/>.
/// </remarks>
public Rect? PlacementRect public Rect? PlacementRect
{ {
get { return GetValue(PlacementRectProperty); } get => GetValue(PlacementRectProperty);
set { SetValue(PlacementRectProperty, value); } set => SetValue(PlacementRectProperty, value);
} }
/// <summary> /// <inheritdoc cref="Popup.PlacementTarget"/>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
public Control? PlacementTarget public Control? PlacementTarget
{ {
get { return GetValue(PlacementTargetProperty); } get => GetValue(PlacementTargetProperty);
set { SetValue(PlacementTargetProperty, value); } set => SetValue(PlacementTargetProperty, value);
} }
/// <summary> /// <summary>
@ -343,9 +330,9 @@ namespace Avalonia.Controls
((ISetLogicalParent)_popup).SetParent(control); ((ISetLogicalParent)_popup).SetParent(control);
} }
_popup.PlacementMode = !requestedByPointer && PlacementMode == PlacementMode.Pointer _popup.Placement = !requestedByPointer && Placement == PlacementMode.Pointer
? PlacementMode.Bottom ? PlacementMode.Bottom
: PlacementMode; : Placement;
//Position of the line below is really important. //Position of the line below is really important.
//All styles are being applied only when control has logical parent. //All styles are being applied only when control has logical parent.

2
src/Avalonia.Controls/DateTimePickers/DatePicker.cs

@ -389,7 +389,7 @@ namespace Avalonia.Controls
_presenter.Date = SelectedDate ?? DateTimeOffset.Now; _presenter.Date = SelectedDate ?? DateTimeOffset.Now;
_popup.PlacementMode = PlacementMode.AnchorAndGravity; _popup.Placement = PlacementMode.AnchorAndGravity;
_popup.PlacementAnchor = Primitives.PopupPositioning.PopupAnchor.Bottom; _popup.PlacementAnchor = Primitives.PopupPositioning.PopupAnchor.Bottom;
_popup.PlacementGravity = Primitives.PopupPositioning.PopupGravity.Bottom; _popup.PlacementGravity = Primitives.PopupPositioning.PopupGravity.Bottom;
_popup.PlacementConstraintAdjustment = Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; _popup.PlacementConstraintAdjustment = Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;

2
src/Avalonia.Controls/DateTimePickers/TimePicker.cs

@ -255,7 +255,7 @@ namespace Avalonia.Controls
_presenter.Time = SelectedTime ?? DateTime.Now.TimeOfDay; _presenter.Time = SelectedTime ?? DateTime.Now.TimeOfDay;
_popup.PlacementMode = PlacementMode.AnchorAndGravity; _popup.Placement = PlacementMode.AnchorAndGravity;
_popup.PlacementAnchor = Primitives.PopupPositioning.PopupAnchor.Bottom; _popup.PlacementAnchor = Primitives.PopupPositioning.PopupAnchor.Bottom;
_popup.PlacementGravity = Primitives.PopupPositioning.PopupGravity.Bottom; _popup.PlacementGravity = Primitives.PopupPositioning.PopupGravity.Bottom;
_popup.PlacementConstraintAdjustment = Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; _popup.PlacementConstraintAdjustment = Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;

8
src/Avalonia.Controls/Documents/InlineCollection.cs

@ -91,11 +91,11 @@ namespace Avalonia.Controls.Documents
public override void Add(Inline inline) public override void Add(Inline inline)
{ {
if (InlineHost is TextBlock textBlock && !string.IsNullOrEmpty(textBlock._text)) if (InlineHost is TextBlock textBlock && !string.IsNullOrEmpty(textBlock.Text))
{ {
base.Add(new Run(textBlock._text)); base.Add(new Run(textBlock.Text));
textBlock._text = null; textBlock.ClearTextInternal();
} }
base.Add(inline); base.Add(inline);
@ -113,7 +113,7 @@ namespace Avalonia.Controls.Documents
{ {
if (InlineHost is TextBlock textBlock && !textBlock.HasComplexContent) if (InlineHost is TextBlock textBlock && !textBlock.HasComplexContent)
{ {
textBlock._text += text; textBlock.Text += text;
} }
else else
{ {

4
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@ -19,7 +19,9 @@ namespace Avalonia.Controls
/// Defines the <see cref="Items"/> property /// Defines the <see cref="Items"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<MenuFlyout, IEnumerable?> ItemsProperty = public static readonly DirectProperty<MenuFlyout, IEnumerable?> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<MenuFlyout>(x => x.Items, AvaloniaProperty.RegisterDirect<MenuFlyout, IEnumerable?>(
nameof(Items),
x => x.Items,
(x, v) => x.Items = v); (x, v) => x.Items = v);
/// <summary> /// <summary>

16
src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs

@ -14,9 +14,9 @@ namespace Avalonia.Controls.Primitives
{ {
public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
{ {
/// <inheritdoc cref="Popup.PlacementModeProperty"/> /// <inheritdoc cref="Popup.PlacementProperty"/>
public static readonly StyledProperty<PlacementMode> PlacementProperty = public static readonly StyledProperty<PlacementMode> PlacementProperty =
Popup.PlacementModeProperty.AddOwner<PopupFlyoutBase>(); Popup.PlacementProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.HorizontalOffsetProperty"/> /// <inheritdoc cref="Popup.HorizontalOffsetProperty"/>
public static readonly StyledProperty<double> HorizontalOffsetProperty = public static readonly StyledProperty<double> HorizontalOffsetProperty =
@ -64,15 +64,13 @@ namespace Avalonia.Controls.Primitives
protected Popup Popup => _popupLazy.Value; protected Popup Popup => _popupLazy.Value;
/// <summary> /// <inheritdoc cref="Popup.Placement"/>
/// Gets or sets the desired placement.
/// </summary>
public PlacementMode Placement public PlacementMode Placement
{ {
get => GetValue(PlacementProperty); get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value); set => SetValue(PlacementProperty, value);
} }
/// <inheritdoc cref="Popup.PlacementGravity"/> /// <inheritdoc cref="Popup.PlacementGravity"/>
public PopupGravity PlacementGravity public PopupGravity PlacementGravity
{ {
@ -407,7 +405,7 @@ namespace Avalonia.Controls.Primitives
{ {
Size sz; Size sz;
// Popup.Child can't be null here, it was set in ShowAtCore. // Popup.Child can't be null here, it was set in ShowAtCore.
if (Popup.Child!.DesiredSize.IsDefault) if (Popup.Child!.DesiredSize == default)
{ {
// Popup may not have been shown yet. Measure content // Popup may not have been shown yet. Measure content
sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness()); sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
@ -423,11 +421,11 @@ namespace Avalonia.Controls.Primitives
Popup.PlacementGravity = PlacementGravity; Popup.PlacementGravity = PlacementGravity;
if (showAtPointer) if (showAtPointer)
{ {
Popup.PlacementMode = PlacementMode.Pointer; Popup.Placement = PlacementMode.Pointer;
} }
else else
{ {
Popup.PlacementMode = Placement; Popup.Placement = Placement;
Popup.PlacementConstraintAdjustment = Popup.PlacementConstraintAdjustment =
PopupPositioning.PopupPositionerConstraintAdjustment.SlideX | PopupPositioning.PopupPositionerConstraintAdjustment.SlideX |
PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;

165
src/Avalonia.Controls/ItemCollection.cs

@ -0,0 +1,165 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Collections;
namespace Avalonia.Controls
{
/// <summary>
/// Holds the list of items that constitute the content of an <see cref="ItemsControl"/>.
/// </summary>
public class ItemCollection : ItemsSourceView, IList
{
// Suppress "Avoid zero-length array allocations": This is a sentinel value and must be unique.
#pragma warning disable CA1825
private static readonly object?[] s_uninitialized = new object?[0];
#pragma warning restore CA1825
private Mode _mode;
internal ItemCollection()
: base(s_uninitialized)
{
}
public new object? this[int index]
{
get => base[index];
set => WritableSource[index] = value;
}
public bool IsReadOnly => _mode == Mode.ItemsSource;
internal event EventHandler? SourceChanged;
/// <summary>
/// Adds an item to the <see cref="ItemsControl"/>.
/// </summary>
/// <param name="value">The item to add to the collection.</param>
/// <returns>
/// The position into which the new element was inserted, or -1 to indicate that
/// the item was not inserted into the collection.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public int Add(object? value) => WritableSource.Add(value);
/// <summary>
/// Clears the collection and releases the references on all items currently in the
/// collection.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public void Clear() => WritableSource.Clear();
/// <summary>
/// Inserts an element into the collection at the specified index.
/// </summary>
/// <param name="index">The zero-based index at which to insert the item.</param>
/// <param name="value">The item to insert.</param>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public void Insert(int index, object? value) => WritableSource.Insert(index, value);
/// <summary>
/// Removes the item at the specified index of the collection or view.
/// </summary>
/// <param name="index">The zero-based index of the item to remove.</param>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public void RemoveAt(int index) => WritableSource.RemoveAt(index);
/// <summary>
/// Removes the specified item reference from the collection or view.
/// </summary>
/// <param name="value">The object to remove.</param>
/// <returns>True if the item was removed; otherwise false.</returns>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public bool Remove(object? value)
{
var c = Count;
WritableSource.Remove(value);
return Count < c;
}
int IList.Add(object? value) => Add(value);
void IList.Clear() => Clear();
void IList.Insert(int index, object? value) => Insert(index, value);
void IList.RemoveAt(int index) => RemoveAt(index);
private IList WritableSource
{
get
{
if (IsReadOnly)
ThrowIsItemsSource();
if (Source == s_uninitialized)
SetSource(CreateDefaultCollection());
return Source;
}
}
internal IList? GetItemsPropertyValue()
{
if (_mode == Mode.ObsoleteItemsSetter)
return Source == s_uninitialized ? null : Source;
return this;
}
internal void SetItems(IList? items)
{
_mode = Mode.ObsoleteItemsSetter;
SetSource(items ?? s_uninitialized);
}
internal void SetItemsSource(IEnumerable? value)
{
if (_mode != Mode.ItemsSource && Count > 0)
throw new InvalidOperationException(
"Items collection must be empty before using ItemsSource.");
_mode = value is not null ? Mode.ItemsSource : Mode.Items;
SetSource(value ?? CreateDefaultCollection());
}
private new void SetSource(IEnumerable source)
{
var oldSource = Source;
base.SetSource(source);
if (oldSource.Count > 0)
RaiseCollectionChanged(new(NotifyCollectionChangedAction.Remove, oldSource, 0));
if (Source.Count > 0)
RaiseCollectionChanged(new(NotifyCollectionChangedAction.Add, Source, 0));
SourceChanged?.Invoke(this, EventArgs.Empty);
}
private static AvaloniaList<object?> CreateDefaultCollection()
{
return new() { ResetBehavior = ResetBehavior.Remove };
}
[DoesNotReturn]
private static void ThrowIsItemsSource()
{
throw new InvalidOperationException(
"Operation is not valid while ItemsSource is in use." +
"Access and modify elements with ItemsControl.ItemsSource instead.");
}
private enum Mode
{
Items,
ItemsSource,
ObsoleteItemsSetter,
}
}
}

243
src/Avalonia.Controls/ItemsControl.cs

@ -2,6 +2,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
@ -34,8 +35,13 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="Items"/> property. /// Defines the <see cref="Items"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ItemsControl, IEnumerable?> ItemsProperty = public static readonly DirectProperty<ItemsControl, IList?> ItemsProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, IEnumerable?>(nameof(Items), o => o.Items, (o, v) => o.Items = v); AvaloniaProperty.RegisterDirect<ItemsControl, IList?>(
nameof(Items),
o => o.Items,
#pragma warning disable CS0618 // Type or member is obsolete
(o, v) => o.Items = v);
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary> /// <summary>
/// Defines the <see cref="ItemContainerTheme"/> property. /// Defines the <see cref="ItemContainerTheme"/> property.
@ -56,23 +62,23 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<ItemsControl, ITemplate<Panel>>(nameof(ItemsPanel), DefaultPanel); AvaloniaProperty.Register<ItemsControl, ITemplate<Panel>>(nameof(ItemsPanel), DefaultPanel);
/// <summary> /// <summary>
/// Defines the <see cref="ItemTemplate"/> property. /// Defines the <see cref="ItemsSource"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty = public static readonly StyledProperty<IEnumerable?> ItemsSourceProperty =
AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate)); AvaloniaProperty.Register<ItemsControl, IEnumerable?>(nameof(ItemsSource));
/// <summary> /// <summary>
/// Defines the <see cref="ItemsView"/> property. /// Defines the <see cref="ItemTemplate"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ItemsControl, ItemsSourceView> ItemsViewProperty = public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, ItemsSourceView>(nameof(ItemsView), o => o.ItemsView); AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
/// <summary> /// <summary>
/// Defines the <see cref="DisplayMemberBinding" /> property /// Defines the <see cref="DisplayMemberBinding" /> property
/// </summary> /// </summary>
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty = public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding)); AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding));
/// <summary> /// <summary>
/// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property. /// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
/// </summary> /// </summary>
@ -89,15 +95,15 @@ namespace Avalonia.Controls
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item. /// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
/// </summary> /// </summary>
[AssignBinding] [AssignBinding]
[InheritDataTypeFromItems(nameof(ItemsSource))]
[InheritDataTypeFromItems(nameof(Items))] [InheritDataTypeFromItems(nameof(Items))]
public IBinding? DisplayMemberBinding public IBinding? DisplayMemberBinding
{ {
get => GetValue(DisplayMemberBindingProperty); get => GetValue(DisplayMemberBindingProperty);
set => SetValue(DisplayMemberBindingProperty, value); set => SetValue(DisplayMemberBindingProperty, value);
} }
private IEnumerable? _items = new AvaloniaList<object>(); private readonly ItemCollection _items = new();
private ItemsSourceView _itemsView;
private int _itemCount; private int _itemCount;
private ItemContainerGenerator? _itemContainerGenerator; private ItemContainerGenerator? _itemContainerGenerator;
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged; private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
@ -110,9 +116,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public ItemsControl() public ItemsControl()
{ {
_itemsView = ItemsSourceView.GetOrCreate(_items); UpdatePseudoClasses();
_itemsView.PostCollectionChanged += ItemsCollectionChanged; _items.CollectionChanged += OnItemsViewCollectionChanged;
UpdatePseudoClasses(0);
} }
/// <summary> /// <summary>
@ -128,11 +133,45 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets or sets the items to display. /// Gets or sets the items to display.
/// </summary> /// </summary>
/// <remarks>
/// Since Avalonia 11, <see cref="ItemsControl"/> has both an <see cref="Items"/> property
/// and an <see cref="ItemsSource"/> property. The properties have the following differences:
///
/// <list type="bullet">
/// <item><see cref="Items"/> is initialized with an empty collection and is a direct property,
/// meaning that it cannot be styled </item>
/// <item><see cref="ItemsSource"/> is by default null, and is a styled property. This property
/// is marked as the content property and will be used for items added via inline XAML.</item>
/// </list>
///
/// In Avalonia 11 the two properties can be used almost interchangeably but this will change
/// in a later version. In order to be ready for this change, follow the following guidance:
///
/// <list type="bullet">
/// <item>You should use the <see cref="Items"/> property when you're assigning a collection of
/// item containers directly, for example adding a collection of <see cref="ListBoxItem"/>s
/// directly to a <see cref="ListBox"/>. Add the containers to the pre-existing list, do not
/// reassign the <see cref="Items"/> property via the setter or with a binding.</item>
/// <item>You should use the <see cref="ItemsSource"/> property when you're assigning or
/// binding a collection of models which will be transformed by a data template.</item>
/// </list>
/// </remarks>
[Content] [Content]
public IEnumerable? Items public IList? Items
{ {
get => _items; get => _items.GetItemsPropertyValue();
set => SetAndRaise(ItemsProperty, ref _items, value);
[Obsolete("Use ItemsSource to set or bind items.")]
set
{
var oldItems = _items.GetItemsPropertyValue();
if (value != oldItems)
{
_items.SetItems(value);
RaisePropertyChanged(ItemsProperty, oldItems, value);
}
}
} }
/// <summary> /// <summary>
@ -140,17 +179,24 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public ControlTheme? ItemContainerTheme public ControlTheme? ItemContainerTheme
{ {
get => GetValue(ItemContainerThemeProperty); get => GetValue(ItemContainerThemeProperty);
set => SetValue(ItemContainerThemeProperty, value); set => SetValue(ItemContainerThemeProperty, value);
} }
/// <summary> /// <summary>
/// Gets the number of items in <see cref="Items"/>. /// Gets the number of items being displayed by the <see cref="ItemsControl"/>.
/// </summary> /// </summary>
public int ItemCount public int ItemCount
{ {
get => _itemCount; get => _itemCount;
private set => SetAndRaise(ItemCountProperty, ref _itemCount, value); private set
{
if (SetAndRaise(ItemCountProperty, ref _itemCount, value))
{
UpdatePseudoClasses();
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
}
}
} }
/// <summary> /// <summary>
@ -162,13 +208,46 @@ namespace Avalonia.Controls
set => SetValue(ItemsPanelProperty, value); set => SetValue(ItemsPanelProperty, value);
} }
/// <summary>
/// Gets or sets a collection used to generate the content of the <see cref="ItemsControl"/>.
/// </summary>
/// <remarks>
/// Since Avalonia 11, <see cref="ItemsControl"/> has both an <see cref="Items"/> property
/// and an <see cref="ItemsSource"/> property. The properties have the following differences:
///
/// <list type="bullet">
/// <item><see cref="Items"/> is initialized with an empty collection and is a direct property,
/// meaning that it cannot be styled </item>
/// <item><see cref="ItemsSource"/> is by default null, and is a styled property. This property
/// is marked as the content property and will be used for items added via inline XAML.</item>
/// </list>
///
/// In Avalonia 11 the two properties can be used almost interchangeably but this will change
/// in a later version. In order to be ready for this change, follow the following guidance:
///
/// <list type="bullet">
/// <item>You should use the <see cref="Items"/> property when you're assigning a collection of
/// item containers directly, for example adding a collection of <see cref="ListBoxItem"/>s
/// directly to a <see cref="ListBox"/>. Add the containers to the pre-existing list, do not
/// reassign the <see cref="Items"/> property via the setter or with a binding.</item>
/// <item>You should use the <see cref="ItemsSource"/> property when you're assigning or
/// binding a collection of models which will be transformed by a data template.</item>
/// </list>
/// </remarks>
public IEnumerable? ItemsSource
{
get => GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the data template used to display the items in the control. /// Gets or sets the data template used to display the items in the control.
/// </summary> /// </summary>
[InheritDataTypeFromItems(nameof(ItemsSource))]
[InheritDataTypeFromItems(nameof(Items))] [InheritDataTypeFromItems(nameof(Items))]
public IDataTemplate? ItemTemplate public IDataTemplate? ItemTemplate
{ {
get => GetValue(ItemTemplateProperty); get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value); set => SetValue(ItemTemplateProperty, value);
} }
@ -183,31 +262,9 @@ namespace Avalonia.Controls
public Panel? ItemsPanelRoot => Presenter?.Panel; public Panel? ItemsPanelRoot => Presenter?.Panel;
/// <summary> /// <summary>
/// Gets a standardized view over <see cref="Items"/>. /// Gets a read-only view of the items in the <see cref="ItemsControl"/>.
/// </summary> /// </summary>
/// <remarks> public ItemsSourceView ItemsView => _items;
/// The <see cref="Items"/> property may be an enumerable which does not implement
/// <see cref="IList"/> or may be null. This view can be used to provide a standardized
/// view of the current items regardless of the type of the concrete collection, and
/// without having to deal with null values.
/// </remarks>
public ItemsSourceView ItemsView
{
get => _itemsView;
private set
{
if (ReferenceEquals(_itemsView, value))
return;
var oldValue = _itemsView;
RemoveControlItemsFromLogicalChildren(_itemsView);
_itemsView.PostCollectionChanged -= ItemsCollectionChanged;
_itemsView = value;
_itemsView.PostCollectionChanged += ItemsCollectionChanged;
AddControlItemsToLogicalChildren(_itemsView);
RaisePropertyChanged(ItemsViewProperty, oldValue, _itemsView);
}
}
private protected bool WrapFocus { get; set; } private protected bool WrapFocus { get; set; }
@ -217,6 +274,34 @@ namespace Avalonia.Controls
remove => _childIndexChanged -= value; remove => _childIndexChanged -= value;
} }
/// <summary>
/// Occurs each time a container is prepared for use.
/// </summary>
/// <remarks>
/// The prepared element might be newly created or an existing container that is being re-
/// used.
/// </remarks>
public event EventHandler<ContainerPreparedEventArgs>? ContainerPrepared;
/// <summary>
/// Occurs for each realized container when the index for the item it represents has changed.
/// </summary>
/// <remarks>
/// This event is raised for each realized container where the index for the item it
/// represents has changed. For example, when another item is added or removed in the data
/// source, the index for items that come after in the ordering will be impacted.
/// </remarks>
public event EventHandler<ContainerIndexChangedEventArgs>? ContainerIndexChanged;
/// <summary>
/// Occurs each time a container is cleared.
/// </summary>
/// <remarks>
/// This event is raised immediately each time an container is cleared, such as when it
/// falls outside the range of realized items or the corresponding item is removed.
/// </remarks>
public event EventHandler<ContainerClearingEventArgs>? ContainerClearing;
/// <inheritdoc /> /// <inheritdoc />
public event EventHandler<RoutedEventArgs> HorizontalSnapPointsChanged public event EventHandler<RoutedEventArgs> HorizontalSnapPointsChanged
{ {
@ -262,7 +347,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public bool AreHorizontalSnapPointsRegular public bool AreHorizontalSnapPointsRegular
{ {
get => GetValue(AreHorizontalSnapPointsRegularProperty); get => GetValue(AreHorizontalSnapPointsRegularProperty);
set => SetValue(AreHorizontalSnapPointsRegularProperty, value); set => SetValue(AreHorizontalSnapPointsRegularProperty, value);
} }
@ -271,7 +356,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public bool AreVerticalSnapPointsRegular public bool AreVerticalSnapPointsRegular
{ {
get => GetValue(AreVerticalSnapPointsRegularProperty); get => GetValue(AreVerticalSnapPointsRegularProperty);
set => SetValue(AreVerticalSnapPointsRegularProperty, value); set => SetValue(AreVerticalSnapPointsRegularProperty, value);
} }
@ -295,7 +380,7 @@ namespace Avalonia.Controls
/// </returns> /// </returns>
public Control? ContainerFromItem(object item) public Control? ContainerFromItem(object item)
{ {
var index = ItemsView.IndexOf(item); var index = _items.IndexOf(item);
return index >= 0 ? ContainerFromIndex(index) : null; return index >= 0 ? ContainerFromIndex(index) : null;
} }
@ -319,7 +404,7 @@ namespace Avalonia.Controls
public object? ItemFromContainer(Control container) public object? ItemFromContainer(Control container)
{ {
var index = IndexFromContainer(container); var index = IndexFromContainer(container);
return index >= 0 && index < ItemsView.Count ? ItemsView[index] : null; return index >= 0 && index < _items.Count ? _items[index] : null;
} }
/// <summary> /// <summary>
@ -478,19 +563,13 @@ namespace Avalonia.Controls
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == ItemsProperty) if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
{
ItemsView = ItemsSourceView.GetOrCreate(change.GetNewValue<IEnumerable?>());
ItemCount = ItemsView.Count;
}
else if (change.Property == ItemCountProperty)
{ {
UpdatePseudoClasses(change.GetNewValue<int>()); RefreshContainers();
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
} }
else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null) else if (change.Property == ItemsSourceProperty)
{ {
RefreshContainers(); _items.SetItemsSource(change.GetNewValue<IEnumerable?>());
} }
else if (change.Property == ItemTemplateProperty) else if (change.Property == ItemTemplateProperty)
{ {
@ -517,24 +596,27 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Called when the <see cref="INotifyCollectionChanged.CollectionChanged"/> event is /// Called when the <see cref="INotifyCollectionChanged.CollectionChanged"/> event is
/// raised on <see cref="Items"/>. /// raised on <see cref="ItemsView"/>.
/// </summary> /// </summary>
/// <param name="sender">The event sender.</param> /// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
protected virtual void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) private protected virtual void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
ItemCount = _itemsView.Count; if (!_items.IsReadOnly)
switch (e.Action)
{ {
case NotifyCollectionChangedAction.Add: switch (e.Action)
AddControlItemsToLogicalChildren(e.NewItems); {
break; case NotifyCollectionChangedAction.Add:
AddControlItemsToLogicalChildren(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
RemoveControlItemsFromLogicalChildren(e.OldItems); RemoveControlItemsFromLogicalChildren(e.OldItems);
break; break;
}
} }
ItemCount = ItemsView.Count;
} }
/// <summary> /// <summary>
@ -578,7 +660,7 @@ namespace Avalonia.Controls
{ {
var itemContainerTheme = ItemContainerTheme; var itemContainerTheme = ItemContainerTheme;
if (itemContainerTheme is not null && if (itemContainerTheme is not null &&
!container.IsSet(ThemeProperty) && !container.IsSet(ThemeProperty) &&
((IStyleable)container).StyleKey == itemContainerTheme.TargetType) ((IStyleable)container).StyleKey == itemContainerTheme.TargetType)
{ {
@ -595,24 +677,23 @@ namespace Avalonia.Controls
{ {
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index)); _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index));
_scrollViewer?.RegisterAnchorCandidate(container); _scrollViewer?.RegisterAnchorCandidate(container);
ContainerPrepared?.Invoke(this, new(container, index));
} }
internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex) internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex)
{ {
ContainerIndexChangedOverride(container, oldIndex, newIndex); ContainerIndexChangedOverride(container, oldIndex, newIndex);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, newIndex)); _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, newIndex));
ContainerIndexChanged?.Invoke(this, new(container, oldIndex, newIndex));
} }
internal void ClearItemContainer(Control container) internal void ClearItemContainer(Control container)
{ {
_scrollViewer?.UnregisterAnchorCandidate(container); _scrollViewer?.UnregisterAnchorCandidate(container);
ClearContainerForItemOverride(container); ClearContainerForItemOverride(container);
ContainerClearing?.Invoke(this, new(container));
} }
/// <summary>
/// Given a collection of items, adds those that are controls to the logical children.
/// </summary>
/// <param name="items">The items.</param>
private void AddControlItemsToLogicalChildren(IEnumerable? items) private void AddControlItemsToLogicalChildren(IEnumerable? items)
{ {
if (items is null) if (items is null)
@ -633,10 +714,6 @@ namespace Avalonia.Controls
LogicalChildren.AddRange(toAdd); LogicalChildren.AddRange(toAdd);
} }
/// <summary>
/// Given a collection of items, removes those that are controls to from logical children.
/// </summary>
/// <param name="items">The items.</param>
private void RemoveControlItemsFromLogicalChildren(IEnumerable? items) private void RemoveControlItemsFromLogicalChildren(IEnumerable? items)
{ {
if (items is null) if (items is null)
@ -674,10 +751,10 @@ namespace Avalonia.Controls
return _displayMemberItemTemplate; return _displayMemberItemTemplate;
} }
private void UpdatePseudoClasses(int itemCount) private void UpdatePseudoClasses()
{ {
PseudoClasses.Set(":empty", itemCount == 0); PseudoClasses.Set(":empty", ItemCount == 0);
PseudoClasses.Set(":singleitem", itemCount == 1); PseudoClasses.Set(":singleitem", ItemCount == 1);
} }
protected static IInputElement? GetNextControl( protected static IInputElement? GetNextControl(

155
src/Avalonia.Controls/ItemsSourceView.cs

@ -7,6 +7,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
@ -17,15 +18,16 @@ namespace Avalonia.Controls
/// and an items control. /// and an items control.
/// </summary> /// </summary>
public class ItemsSourceView : IReadOnlyList<object?>, public class ItemsSourceView : IReadOnlyList<object?>,
IList,
INotifyCollectionChanged, INotifyCollectionChanged,
ICollectionChangedListener ICollectionChangedListener
{ {
/// <summary> /// <summary>
/// Gets an empty <see cref="ItemsSourceView"/> /// Gets an empty <see cref="ItemsSourceView"/>
/// </summary> /// </summary>
public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>()); public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object?>());
private readonly IList _inner; private IList _source;
private NotifyCollectionChangedEventHandler? _collectionChanged; private NotifyCollectionChangedEventHandler? _collectionChanged;
private NotifyCollectionChangedEventHandler? _preCollectionChanged; private NotifyCollectionChangedEventHandler? _preCollectionChanged;
private NotifyCollectionChangedEventHandler? _postCollectionChanged; private NotifyCollectionChangedEventHandler? _postCollectionChanged;
@ -35,30 +37,17 @@ namespace Avalonia.Controls
/// Initializes a new instance of the ItemsSourceView class for the specified data source. /// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary> /// </summary>
/// <param name="source">The data source.</param> /// <param name="source">The data source.</param>
private protected ItemsSourceView(IEnumerable source) private protected ItemsSourceView(IEnumerable source) => SetSource(source);
{
_inner = source switch
{
ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
IList list => list,
INotifyCollectionChanged => throw new ArgumentException(
"Collection implements INotifyCollectionChanged but not IList.",
nameof(source)),
IEnumerable<object> iObj => new List<object>(iObj),
null => throw new ArgumentNullException(nameof(source)),
_ => new List<object>(source.Cast<object>())
};
}
/// <summary> /// <summary>
/// Gets the number of items in the collection. /// Gets the number of items in the collection.
/// </summary> /// </summary>
public int Count => Inner.Count; public int Count => Source.Count;
/// <summary> /// <summary>
/// Gets the inner collection. /// Gets the source collection.
/// </summary> /// </summary>
public IList Inner => _inner; public IList Source => _source;
/// <summary> /// <summary>
/// Retrieves the item at the specified index. /// Retrieves the item at the specified index.
@ -67,12 +56,20 @@ namespace Avalonia.Controls
/// <returns>The item.</returns> /// <returns>The item.</returns>
public object? this[int index] => GetAt(index); public object? this[int index] => GetAt(index);
bool IList.IsFixedSize => false;
bool IList.IsReadOnly => true;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => this;
object? IList.this[int index]
{
get => GetAt(index);
set => ThrowReadOnly();
}
/// <summary> /// <summary>
/// Gets a value that indicates whether the items source can provide a unique key for each item.
/// </summary>
/// <remarks>
/// Not implemented in Avalonia, preserved here for ItemsRepeater's usage. /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
/// </remarks> /// </summary>
internal bool HasKeyIndexMapping => false; internal bool HasKeyIndexMapping => false;
/// <summary> /// <summary>
@ -131,39 +128,14 @@ namespace Avalonia.Controls
} }
} }
private void AddListenerIfNecessary()
{
if (!_listening)
{
if (_inner is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.AddListener(incc, this);
_listening = true;
}
}
private void RemoveListenerIfNecessary()
{
if (_listening && _collectionChanged is null && _postCollectionChanged is null)
{
if (_inner is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.RemoveListener(incc, this);
_listening = false;
}
}
/// <summary> /// <summary>
/// Retrieves the item at the specified index. /// Retrieves the item at the specified index.
/// </summary> /// </summary>
/// <param name="index">The index.</param> /// <param name="index">The index.</param>
/// <returns>The item.</returns> /// <returns>The item.</returns>
public object? GetAt(int index) => Inner[index]; public object? GetAt(int index) => Source[index];
public bool Contains(object? item) => Source.Contains(item);
/// <summary> public int IndexOf(object? item) => Source.IndexOf(item);
/// Determines the index of a specific item in the collection.
/// </summary>
/// <param name="item">The object to locate in the collection.</param>
/// <returns>The index of value if found in the list; otherwise, -1.</returns>
public int IndexOf(object? item) => Inner.IndexOf(item);
/// <summary> /// <summary>
/// Gets or creates an <see cref="ItemsSourceView"/> for the specified enumerable. /// Gets or creates an <see cref="ItemsSourceView"/> for the specified enumerable.
@ -201,7 +173,8 @@ namespace Avalonia.Controls
{ {
return items switch return items switch
{ {
ItemsSourceView<T> isv => isv, ItemsSourceView<T> isvt => isvt,
ItemsSourceView isv => new ItemsSourceView<T>(isv.Source),
null => ItemsSourceView<T>.Empty, null => ItemsSourceView<T>.Empty,
_ => new ItemsSourceView<T>(items) _ => new ItemsSourceView<T>(items)
}; };
@ -236,7 +209,7 @@ namespace Avalonia.Controls
yield return o; yield return o;
} }
var inner = Inner; var inner = Source;
return inner switch return inner switch
{ {
@ -245,7 +218,7 @@ namespace Avalonia.Controls
}; };
} }
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator();
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{ {
@ -262,15 +235,69 @@ namespace Avalonia.Controls
_postCollectionChanged?.Invoke(this, e); _postCollectionChanged?.Invoke(this, e);
} }
int IList.Add(object? value) => ThrowReadOnly();
void IList.Clear() => ThrowReadOnly();
void IList.Insert(int index, object? value) => ThrowReadOnly();
void IList.Remove(object? value) => ThrowReadOnly();
void IList.RemoveAt(int index) => ThrowReadOnly();
void ICollection.CopyTo(Array array, int index) => Source.CopyTo(array, index);
/// <summary> /// <summary>
/// Retrieves the index of the item that has the specified unique identifier (key). /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
/// </summary> /// </summary>
/// <param name="index">The index.</param>
/// <returns>The key</returns>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
internal string KeyFromIndex(int index) => throw new NotImplementedException(); internal string KeyFromIndex(int index) => throw new NotImplementedException();
private protected void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e)
{
_preCollectionChanged?.Invoke(this, e);
_collectionChanged?.Invoke(this, e);
_postCollectionChanged?.Invoke(this, e);
}
[MemberNotNull(nameof(_source))]
private protected void SetSource(IEnumerable source)
{
if (_listening && _source is INotifyCollectionChanged inccOld)
CollectionChangedEventManager.Instance.RemoveListener(inccOld, this);
_source = source switch
{
ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
IList list => list,
INotifyCollectionChanged => throw new ArgumentException(
"Collection implements INotifyCollectionChanged but not IList.",
nameof(source)),
IEnumerable<object> iObj => new List<object>(iObj),
null => throw new ArgumentNullException(nameof(source)),
_ => new List<object>(source.Cast<object>())
};
if (_listening && _source is INotifyCollectionChanged inccNew)
CollectionChangedEventManager.Instance.AddListener(inccNew, this);
}
private void AddListenerIfNecessary()
{
if (!_listening)
{
if (_source is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.AddListener(incc, this);
_listening = true;
}
}
private void RemoveListenerIfNecessary()
{
if (_listening && _collectionChanged is null && _postCollectionChanged is null)
{
if (_source is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.RemoveListener(incc, this);
_listening = false;
}
}
[DoesNotReturn]
private static int ThrowReadOnly() => throw new NotSupportedException("Collection is read-only.");
} }
public sealed class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T> public sealed class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
@ -306,7 +333,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
/// <param name="index">The index.</param> /// <param name="index">The index.</param>
/// <returns>The item.</returns> /// <returns>The item.</returns>
public new T GetAt(int index) => (T)Inner[index]!; public new T GetAt(int index) => (T)Source[index]!;
public new IEnumerator<T> GetEnumerator() public new IEnumerator<T> GetEnumerator()
{ {
@ -316,7 +343,7 @@ namespace Avalonia.Controls
yield return (T)o; yield return (T)o;
} }
var inner = Inner; var inner = Source;
return inner switch return inner switch
{ {
@ -325,6 +352,6 @@ namespace Avalonia.Controls
}; };
} }
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator();
} }
} }

4
src/Avalonia.Controls/LayoutTransformControl.cs

@ -91,7 +91,7 @@ namespace Avalonia.Controls
arrangedsize = TransformRoot.Bounds.Size; arrangedsize = TransformRoot.Bounds.Size;
// This is the first opportunity under Silverlight to find out the Child's true DesiredSize // This is the first opportunity under Silverlight to find out the Child's true DesiredSize
if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && _childActualSize.IsDefault) if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && _childActualSize == default)
{ {
//// Unfortunately, all the work so far is invalid because the wrong DesiredSize was used //// Unfortunately, all the work so far is invalid because the wrong DesiredSize was used
//// Make a note of the actual DesiredSize //// Make a note of the actual DesiredSize
@ -122,7 +122,7 @@ namespace Avalonia.Controls
} }
Size measureSize; Size measureSize;
if (_childActualSize.IsDefault) if (_childActualSize == default)
{ {
// Determine the largest size after the transformation // Determine the largest size after the transformation
measureSize = ComputeLargestTransformedSize(availableSize); measureSize = ComputeLargestTransformedSize(availableSize);

135
src/Avalonia.Controls/MaskedTextBox.cs

@ -15,9 +15,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> AsciiOnlyProperty = public static readonly StyledProperty<bool> AsciiOnlyProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(AsciiOnly)); AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(AsciiOnly));
public static readonly DirectProperty<MaskedTextBox, CultureInfo?> CultureProperty = public static readonly StyledProperty<CultureInfo?> CultureProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, CultureInfo?>(nameof(Culture), o => o.Culture, AvaloniaProperty.Register<MaskedTextBox, CultureInfo?>(nameof(Culture), CultureInfo.CurrentCulture);
(o, v) => o.Culture = v, CultureInfo.CurrentCulture);
public static readonly StyledProperty<bool> HidePromptOnLeaveProperty = public static readonly StyledProperty<bool> HidePromptOnLeaveProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(HidePromptOnLeave)); AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(HidePromptOnLeave));
@ -32,26 +31,49 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<MaskedTextBox, string?>(nameof(Mask), string.Empty); AvaloniaProperty.Register<MaskedTextBox, string?>(nameof(Mask), string.Empty);
public static readonly StyledProperty<char> PromptCharProperty = public static readonly StyledProperty<char> PromptCharProperty =
AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_'); AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_', coerce: CoercePromptChar);
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnPromptProperty = public static readonly StyledProperty<bool> ResetOnPromptProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnPrompt), o => o.ResetOnPrompt, (o, v) => o.ResetOnPrompt = v); AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(ResetOnPrompt), true);
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnSpaceProperty = public static readonly StyledProperty<bool> ResetOnSpaceProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnSpace), o => o.ResetOnSpace, (o, v) => o.ResetOnSpace = v); AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(ResetOnSpace), true);
private CultureInfo? _culture; private bool _ignoreTextChanges;
private bool _resetOnPrompt = true; static MaskedTextBox()
{
PasswordCharProperty.OverrideMetadata<MaskedTextBox>(new('\0', coerce: CoercePasswordChar));
}
private bool _ignoreTextChanges; private static char CoercePasswordChar(AvaloniaObject sender, char baseValue)
{
if (!MaskedTextProvider.IsValidPasswordChar(baseValue))
{
throw new ArgumentException($"'{baseValue}' is not a valid value for PasswordChar.");
}
var textbox = (MaskedTextBox)sender;
if (textbox.MaskProvider is { } maskProvider && baseValue == maskProvider.PromptChar)
{
// Prompt and password chars must be different.
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
}
private bool _resetOnSpace = true; return baseValue;
}
static MaskedTextBox() private static char CoercePromptChar(AvaloniaObject sender, char baseValue)
{ {
PasswordCharProperty if (!MaskedTextProvider.IsValidInputChar(baseValue))
.OverrideDefaultValue<MaskedTextBox>('\0'); {
throw new ArgumentException($"'{baseValue}' is not a valid value for PromptChar.");
}
if (baseValue == sender.GetValue(PasswordCharProperty))
{
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
}
return baseValue;
} }
public MaskedTextBox() { } public MaskedTextBox() { }
@ -59,6 +81,9 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Constructs the MaskedTextBox with the specified MaskedTextProvider object. /// Constructs the MaskedTextBox with the specified MaskedTextProvider object.
/// </summary> /// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty",
"AVP1012:An AvaloniaObject should use SetCurrentValue when assigning its own StyledProperty or AttachedProperty values",
Justification = "These values are being explicitly provided by a constructor parameter.")]
public MaskedTextBox(MaskedTextProvider maskedTextProvider) public MaskedTextBox(MaskedTextProvider maskedTextProvider)
{ {
if (maskedTextProvider == null) if (maskedTextProvider == null)
@ -87,8 +112,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public CultureInfo? Culture public CultureInfo? Culture
{ {
get => _culture; get => GetValue(CultureProperty);
set => SetAndRaise(CultureProperty, ref _culture, value); set => SetValue(CultureProperty, value);
} }
/// <summary> /// <summary>
@ -131,15 +156,6 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public MaskedTextProvider? MaskProvider { get; private set; } public MaskedTextProvider? MaskProvider { get; private set; }
/// <summary>
/// Gets or sets the character to be displayed in substitute for user input.
/// </summary>
public new char PasswordChar
{
get => GetValue(PasswordCharProperty);
set => SetValue(PasswordCharProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the character used to represent the absence of user input in MaskedTextBox. /// Gets or sets the character used to represent the absence of user input in MaskedTextBox.
/// </summary> /// </summary>
@ -154,16 +170,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public bool ResetOnPrompt public bool ResetOnPrompt
{ {
get => _resetOnPrompt; get => GetValue(ResetOnPromptProperty);
set set => SetValue(ResetOnPromptProperty, value);
{
SetAndRaise(ResetOnPromptProperty, ref _resetOnPrompt, value);
if (MaskProvider != null)
{
MaskProvider.ResetOnPrompt = value;
}
}
} }
/// <summary> /// <summary>
@ -171,16 +179,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public bool ResetOnSpace public bool ResetOnSpace
{ {
get => _resetOnSpace; get => GetValue(ResetOnSpaceProperty);
set set => SetValue(ResetOnSpaceProperty, value);
{
SetAndRaise(ResetOnSpaceProperty, ref _resetOnSpace, value);
if (MaskProvider != null)
{
MaskProvider.ResetOnSpace = value;
}
}
} }
Type IStyleable.StyleKey => typeof(TextBox); Type IStyleable.StyleKey => typeof(TextBox);
@ -190,7 +190,7 @@ namespace Avalonia.Controls
{ {
if (HidePromptOnLeave == true && MaskProvider != null) if (HidePromptOnLeave == true && MaskProvider != null)
{ {
Text = MaskProvider.ToDisplayString(); SetCurrentValue(TextProperty, MaskProvider.ToDisplayString());
} }
base.OnGotFocus(e); base.OnGotFocus(e);
} }
@ -225,11 +225,11 @@ namespace Avalonia.Controls
var index = GetNextCharacterPosition(CaretIndex); var index = GetNextCharacterPosition(CaretIndex);
if (MaskProvider.InsertAt(item, index)) if (MaskProvider.InsertAt(item, index))
{ {
CaretIndex = ++index; SetCurrentValue(CaretIndexProperty, ++index);
} }
} }
Text = MaskProvider.ToDisplayString(); SetCurrentValue(TextProperty, MaskProvider.ToDisplayString());
e.Handled = true; e.Handled = true;
return; return;
} }
@ -279,7 +279,7 @@ namespace Avalonia.Controls
{ {
if (HidePromptOnLeave && MaskProvider != null) if (HidePromptOnLeave && MaskProvider != null)
{ {
Text = MaskProvider.ToString(!HidePromptOnLeave, true); SetCurrentValue(TextProperty, MaskProvider.ToString(!HidePromptOnLeave, true));
} }
base.OnLostFocus(e); base.OnLostFocus(e);
} }
@ -326,15 +326,6 @@ namespace Avalonia.Controls
} }
else if (change.Property == PasswordCharProperty) else if (change.Property == PasswordCharProperty)
{ {
if (!MaskedTextProvider.IsValidPasswordChar(PasswordChar))
{
throw new ArgumentException("Specified character value is not allowed for this property.", nameof(PasswordChar));
}
if (MaskProvider != null && PasswordChar == MaskProvider.PromptChar)
{
// Prompt and password chars must be different.
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
}
if (MaskProvider != null && MaskProvider.PasswordChar != PasswordChar) if (MaskProvider != null && MaskProvider.PasswordChar != PasswordChar)
{ {
UpdateMaskProvider(); UpdateMaskProvider();
@ -342,17 +333,23 @@ namespace Avalonia.Controls
} }
else if (change.Property == PromptCharProperty) else if (change.Property == PromptCharProperty)
{ {
if (!MaskedTextProvider.IsValidInputChar(PromptChar)) if (MaskProvider != null && MaskProvider.PromptChar != PromptChar)
{ {
throw new ArgumentException("Specified character value is not allowed for this property."); UpdateMaskProvider();
} }
if (PromptChar == PasswordChar) }
else if (change.Property == ResetOnPromptProperty)
{
if (MaskProvider != null && change.GetNewValue<bool>() is { } newValue)
{ {
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same."); MaskProvider.ResetOnPrompt = newValue;
} }
if (MaskProvider != null && MaskProvider.PromptChar != PromptChar) }
else if (change.Property == ResetOnSpaceProperty)
{
if (MaskProvider != null && change.GetNewValue<bool>() is { } newValue)
{ {
UpdateMaskProvider(); MaskProvider.ResetOnSpace = newValue;
} }
} }
else if (change.Property == AsciiOnlyProperty && MaskProvider != null && MaskProvider.AsciiOnly != AsciiOnly else if (change.Property == AsciiOnlyProperty && MaskProvider != null && MaskProvider.AsciiOnly != AsciiOnly
@ -390,7 +387,7 @@ namespace Avalonia.Controls
if (CaretIndex < Text?.Length) if (CaretIndex < Text?.Length)
{ {
CaretIndex = GetNextCharacterPosition(CaretIndex); SetCurrentValue(CaretIndexProperty, GetNextCharacterPosition(CaretIndex));
if (MaskProvider.InsertAt(e.Text!, CaretIndex)) if (MaskProvider.InsertAt(e.Text!, CaretIndex))
{ {
@ -399,7 +396,7 @@ namespace Avalonia.Controls
var nextPos = GetNextCharacterPosition(CaretIndex); var nextPos = GetNextCharacterPosition(CaretIndex);
if (nextPos != 0 && CaretIndex != Text.Length) if (nextPos != 0 && CaretIndex != Text.Length)
{ {
CaretIndex = nextPos; SetCurrentValue(CaretIndexProperty, nextPos);
} }
} }
@ -434,8 +431,8 @@ namespace Avalonia.Controls
{ {
if (provider != null) if (provider != null)
{ {
Text = provider.ToDisplayString(); SetCurrentValue(TextProperty, provider.ToDisplayString());
CaretIndex = position; SetCurrentValue(CaretIndexProperty, position);
} }
} }

2
src/Avalonia.Controls/NativeControlHost.cs

@ -141,7 +141,7 @@ namespace Avalonia.Controls
if (IsEffectivelyVisible && bounds.HasValue) if (IsEffectivelyVisible && bounds.HasValue)
{ {
if (bounds.Value.IsDefault) if (bounds.Value.Width == 0 && bounds.Value.Height == 0)
return false; return false;
_attachment?.ShowInBounds(bounds.Value); _attachment?.ShowInBounds(bounds.Value);
} }

18
src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@ -22,7 +22,6 @@ namespace Avalonia.Controls.Presenters
Debug.Assert(presenter.Panel is not null or VirtualizingPanel); Debug.Assert(presenter.Panel is not null or VirtualizingPanel);
_presenter = presenter; _presenter = presenter;
_presenter.ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
_presenter.ItemsControl.ItemsView.PostCollectionChanged += OnItemsChanged; _presenter.ItemsControl.ItemsView.PostCollectionChanged += OnItemsChanged;
OnItemsChanged(null, CollectionUtils.ResetEventArgs); OnItemsChanged(null, CollectionUtils.ResetEventArgs);
@ -32,9 +31,7 @@ namespace Avalonia.Controls.Presenters
{ {
if (_presenter.ItemsControl is { } itemsControl) if (_presenter.ItemsControl is { } itemsControl)
{ {
itemsControl.PropertyChanged -= OnItemsControlPropertyChanged;
itemsControl.ItemsView.PostCollectionChanged -= OnItemsChanged; itemsControl.ItemsView.PostCollectionChanged -= OnItemsChanged;
ClearItemsControlLogicalChildren(); ClearItemsControlLogicalChildren();
} }
@ -43,18 +40,6 @@ namespace Avalonia.Controls.Presenters
internal void Refresh() => OnItemsChanged(null, CollectionUtils.ResetEventArgs); internal void Refresh() => OnItemsChanged(null, CollectionUtils.ResetEventArgs);
private void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ItemsControl.ItemsProperty)
{
if (e.OldValue is INotifyCollectionChanged inccOld)
inccOld.CollectionChanged -= OnItemsChanged;
OnItemsChanged(null, CollectionUtils.ResetEventArgs);
if (e.NewValue is INotifyCollectionChanged inccNew)
inccNew.CollectionChanged += OnItemsChanged;
}
}
private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
if (_presenter.Panel is null || _presenter.ItemsControl is null) if (_presenter.Panel is null || _presenter.ItemsControl is null)
@ -84,8 +69,7 @@ namespace Avalonia.Controls.Presenters
var c = children[index + i]; var c = children[index + i];
if (!c.IsSet(ItemIsOwnContainerProperty)) if (!c.IsSet(ItemIsOwnContainerProperty))
itemsControl.RemoveLogicalChild(children[i + index]); itemsControl.RemoveLogicalChild(children[i + index]);
else generator.ClearItemContainer(c);
generator.ClearItemContainer(c);
} }
children.RemoveRange(index, count); children.RemoveRange(index, count);

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

@ -9,17 +9,13 @@ using Avalonia.VisualTree;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Input.TextInput;
using Avalonia.Data;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
public class TextPresenter : Control public class TextPresenter : Control
{ {
public static readonly DirectProperty<TextPresenter, int> CaretIndexProperty = public static readonly StyledProperty<int> CaretIndexProperty =
TextBox.CaretIndexProperty.AddOwner<TextPresenter>( TextBox.CaretIndexProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
o => o.CaretIndex,
(o, v) => o.CaretIndex = v);
public static readonly StyledProperty<bool> RevealPasswordProperty = public static readonly StyledProperty<bool> RevealPasswordProperty =
AvaloniaProperty.Register<TextPresenter, bool>(nameof(RevealPassword)); AvaloniaProperty.Register<TextPresenter, bool>(nameof(RevealPassword));
@ -36,33 +32,23 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<IBrush?> CaretBrushProperty = public static readonly StyledProperty<IBrush?> CaretBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(CaretBrush)); AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(CaretBrush));
public static readonly DirectProperty<TextPresenter, int> SelectionStartProperty = public static readonly StyledProperty<int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<TextPresenter>( TextBox.SelectionStartProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
public static readonly DirectProperty<TextPresenter, int> SelectionEndProperty = public static readonly StyledProperty<int> SelectionEndProperty =
TextBox.SelectionEndProperty.AddOwner<TextPresenter>( TextBox.SelectionEndProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
/// <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 StyledProperty<string?> TextProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, string?>( TextBlock.TextProperty.AddOwner<TextPresenter>(new(string.Empty));
nameof(Text),
o => o.Text,
(o, v) => o.Text = v, defaultBindingMode: BindingMode.OneWay);
/// <summary> /// <summary>
/// Defines the <see cref="PreeditText"/> property. /// Defines the <see cref="PreeditText"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<TextPresenter, string?> PreeditTextProperty = public static readonly StyledProperty<string?> PreeditTextProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, string?>( AvaloniaProperty.Register<TextPresenter, string?>(nameof(PreeditText));
nameof(PreeditText),
o => o.PreeditText,
(o, v) => o.PreeditText = v);
/// <summary> /// <summary>
/// Defines the <see cref="CompositionRegion"/> property. /// Defines the <see cref="CompositionRegion"/> property.
@ -104,18 +90,13 @@ namespace Avalonia.Controls.Presenters
Border.BackgroundProperty.AddOwner<TextPresenter>(); Border.BackgroundProperty.AddOwner<TextPresenter>();
private readonly DispatcherTimer _caretTimer; private readonly DispatcherTimer _caretTimer;
private int _caretIndex;
private int _selectionStart;
private int _selectionEnd;
private bool _caretBlink; private bool _caretBlink;
internal string? _text;
private TextLayout? _textLayout; private TextLayout? _textLayout;
private Size _constraint; private Size _constraint;
private CharacterHit _lastCharacterHit; private CharacterHit _lastCharacterHit;
private Rect _caretBounds; private Rect _caretBounds;
private Point _navigationPosition; private Point _navigationPosition;
private string? _preeditText;
private TextRange? _compositionRegion; private TextRange? _compositionRegion;
static TextPresenter() static TextPresenter()
@ -125,7 +106,6 @@ namespace Avalonia.Controls.Presenters
public TextPresenter() public TextPresenter()
{ {
_text = string.Empty;
_caretTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; _caretTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
_caretTimer.Tick += CaretTimerTick; _caretTimer.Tick += CaretTimerTick;
} }
@ -147,14 +127,14 @@ namespace Avalonia.Controls.Presenters
[Content] [Content]
public string? Text public string? Text
{ {
get => _text; get => GetValue(TextProperty);
set => SetAndRaise(TextProperty, ref _text, value); set => SetValue(TextProperty, value);
} }
public string? PreeditText public string? PreeditText
{ {
get => _preeditText; get => GetValue(PreeditTextProperty);
set => SetAndRaise(PreeditTextProperty, ref _preeditText, value); set => SetValue(PreeditTextProperty, value);
} }
public TextRange? CompositionRegion public TextRange? CompositionRegion
@ -275,17 +255,8 @@ namespace Avalonia.Controls.Presenters
public int CaretIndex public int CaretIndex
{ {
get get => GetValue(CaretIndexProperty);
{ set => SetValue(CaretIndexProperty, value);
return _caretIndex;
}
set
{
if (value != _caretIndex)
{
MoveCaretToTextPosition(value);
}
}
} }
public char PasswordChar public char PasswordChar
@ -320,30 +291,14 @@ namespace Avalonia.Controls.Presenters
public int SelectionStart public int SelectionStart
{ {
get get => GetValue(SelectionStartProperty);
{ set => SetValue(SelectionStartProperty, value);
return _selectionStart;
}
set
{
value = CoerceCaretIndex(value);
SetAndRaise(SelectionStartProperty, ref _selectionStart, value);
}
} }
public int SelectionEnd public int SelectionEnd
{ {
get get => GetValue(SelectionEndProperty);
{ set => SetValue(SelectionEndProperty, value);
return _selectionEnd;
}
set
{
value = CoerceCaretIndex(value);
SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
}
} }
protected override bool BypassFlowDirectionPolicies => true; protected override bool BypassFlowDirectionPolicies => true;
@ -535,12 +490,12 @@ namespace Avalonia.Controls.Presenters
{ {
TextLayout result; TextLayout result;
var text = _text; var text = Text;
var typeface = new Typeface(FontFamily, FontStyle, FontWeight); var typeface = new Typeface(FontFamily, FontStyle, FontWeight);
var selectionStart = CoerceCaretIndex(SelectionStart); var selectionStart = SelectionStart;
var selectionEnd = CoerceCaretIndex(SelectionEnd); var selectionEnd = SelectionEnd;
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;
@ -561,9 +516,9 @@ namespace Avalonia.Controls.Presenters
}; };
} }
else if (!string.IsNullOrEmpty(_preeditText)) else if (!string.IsNullOrEmpty(PreeditText))
{ {
var preeditHighlight = new ValueSpan<TextRunProperties>(_caretIndex, _preeditText.Length, var preeditHighlight = new ValueSpan<TextRunProperties>(CaretIndex, PreeditText.Length,
new GenericTextRunProperties(typeface, FontSize, new GenericTextRunProperties(typeface, FontSize,
foregroundBrush: foreground, foregroundBrush: foreground,
textDecorations: TextDecorations.Underline)); textDecorations: TextDecorations.Underline));
@ -643,13 +598,6 @@ namespace Avalonia.Controls.Presenters
return finalSize; return finalSize;
} }
private int CoerceCaretIndex(int value)
{
var text = Text;
var length = text?.Length ?? 0;
return Math.Max(0, Math.Min(length, value));
}
private void CaretTimerTick(object? sender, EventArgs e) private void CaretTimerTick(object? sender, EventArgs e)
{ {
_caretBlink = !_caretBlink; _caretBlink = !_caretBlink;
@ -865,7 +813,7 @@ namespace Avalonia.Controls.Presenters
if (notify) if (notify)
{ {
SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); SetCurrentValue(CaretIndexProperty, caretIndex);
} }
} }
@ -887,6 +835,11 @@ namespace Avalonia.Controls.Presenters
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == CaretIndexProperty)
{
MoveCaretToTextPosition(change.GetNewValue<int>());
}
switch (change.Property.Name) switch (change.Property.Name)
{ {
case nameof(PreeditText): case nameof(PreeditText):

80
src/Avalonia.Controls/Primitives/Popup.cs

@ -65,11 +65,17 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty = public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
AvaloniaProperty.Register<Popup, PopupGravity>(nameof(PlacementGravity)); AvaloniaProperty.Register<Popup, PopupGravity>(nameof(PlacementGravity));
/// <summary>
/// Defines the <see cref="Placement"/> property.
/// </summary>
public static readonly StyledProperty<PlacementMode> PlacementProperty =
AvaloniaProperty.Register<Popup, PlacementMode>(nameof(Placement), defaultValue: PlacementMode.Bottom);
/// <summary> /// <summary>
/// Defines the <see cref="PlacementMode"/> property. /// Defines the <see cref="PlacementMode"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<PlacementMode> PlacementModeProperty = [Obsolete("Use the Placement property instead.")]
AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom); public static readonly StyledProperty<PlacementMode> PlacementModeProperty = PlacementProperty;
/// <summary> /// <summary>
/// Defines the <see cref="PlacementRect"/> property. /// Defines the <see cref="PlacementRect"/> property.
@ -146,8 +152,8 @@ namespace Avalonia.Controls.Primitives
public bool WindowManagerAddShadowHint public bool WindowManagerAddShadowHint
{ {
get { return GetValue(WindowManagerAddShadowHintProperty); } get => GetValue(WindowManagerAddShadowHintProperty);
set { SetValue(WindowManagerAddShadowHintProperty, value); } set => SetValue(WindowManagerAddShadowHintProperty, value);
} }
/// <summary> /// <summary>
@ -156,8 +162,8 @@ namespace Avalonia.Controls.Primitives
[Content] [Content]
public Control? Child public Control? Child
{ {
get { return GetValue(ChildProperty); } get => GetValue(ChildProperty);
set { SetValue(ChildProperty, value); } set => SetValue(ChildProperty, value);
} }
/// <summary> /// <summary>
@ -205,13 +211,13 @@ namespace Avalonia.Controls.Primitives
} }
/// <summary> /// <summary>
/// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="PlacementMode"/> /// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="Placement"/>
/// is <see cref="PlacementMode.AnchorAndGravity"/>. /// is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary> /// </summary>
public PopupAnchor PlacementAnchor public PopupAnchor PlacementAnchor
{ {
get { return GetValue(PlacementAnchorProperty); } get => GetValue(PlacementAnchorProperty);
set { SetValue(PlacementAnchorProperty, value); } set => SetValue(PlacementAnchorProperty, value);
} }
/// <summary> /// <summary>
@ -220,32 +226,40 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
{ {
get { return GetValue(PlacementConstraintAdjustmentProperty); } get => GetValue(PlacementConstraintAdjustmentProperty);
set { SetValue(PlacementConstraintAdjustmentProperty, value); } set => SetValue(PlacementConstraintAdjustmentProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets a value which defines in what direction the popup should open /// Gets or sets a value which defines in what direction the popup should open
/// when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>. /// when <see cref="Placement"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary> /// </summary>
public PopupGravity PlacementGravity public PopupGravity PlacementGravity
{ {
get { return GetValue(PlacementGravityProperty); } get => GetValue(PlacementGravityProperty);
set { SetValue(PlacementGravityProperty, value); } set => SetValue(PlacementGravityProperty, value);
}
/// <inheritdoc cref="Placement"/>
[Obsolete("Use the Placement property instead.")]
public PlacementMode PlacementMode
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the placement mode of the popup in relation to the <see cref="PlacementTarget"/>. /// Gets or sets the desired placement of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary> /// </summary>
public PlacementMode PlacementMode public PlacementMode Placement
{ {
get { return GetValue(PlacementModeProperty); } get => GetValue(PlacementProperty);
set { SetValue(PlacementModeProperty, value); } set => SetValue(PlacementProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the the anchor rectangle within the parent that the popup will be placed /// Gets or sets the the anchor rectangle within the parent that the popup will be placed
/// relative to when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>. /// relative to when <see cref="Placement"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around /// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
@ -256,8 +270,8 @@ namespace Avalonia.Controls.Primitives
/// </remarks> /// </remarks>
public Rect? PlacementRect public Rect? PlacementRect
{ {
get { return GetValue(PlacementRectProperty); } get => GetValue(PlacementRectProperty);
set { SetValue(PlacementRectProperty, value); } set => SetValue(PlacementRectProperty, value);
} }
/// <summary> /// <summary>
@ -266,8 +280,8 @@ namespace Avalonia.Controls.Primitives
[ResolveByName] [ResolveByName]
public Control? PlacementTarget public Control? PlacementTarget
{ {
get { return GetValue(PlacementTargetProperty); } get => GetValue(PlacementTargetProperty);
set { SetValue(PlacementTargetProperty, value); } set => SetValue(PlacementTargetProperty, value);
} }
/// <summary> /// <summary>
@ -301,8 +315,8 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public double HorizontalOffset public double HorizontalOffset
{ {
get { return GetValue(HorizontalOffsetProperty); } get => GetValue(HorizontalOffsetProperty);
set { SetValue(HorizontalOffsetProperty, value); } set => SetValue(HorizontalOffsetProperty, value);
} }
/// <summary> /// <summary>
@ -310,8 +324,8 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public double VerticalOffset public double VerticalOffset
{ {
get { return GetValue(VerticalOffsetProperty); } get => GetValue(VerticalOffsetProperty);
set { SetValue(VerticalOffsetProperty, value); } set => SetValue(VerticalOffsetProperty, value);
} }
/// <summary> /// <summary>
@ -319,8 +333,8 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public bool Topmost public bool Topmost
{ {
get { return GetValue(TopmostProperty); } get => GetValue(TopmostProperty);
set { SetValue(TopmostProperty, value); } set => SetValue(TopmostProperty, value);
} }
IPopupHost? IPopupHostProvider.PopupHost => Host; IPopupHost? IPopupHostProvider.PopupHost => Host;
@ -404,7 +418,7 @@ namespace Avalonia.Controls.Primitives
(x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup); (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup);
// Recalculate popup position on parent moved/resized, but not if placement was on pointer // Recalculate popup position on parent moved/resized, but not if placement was on pointer
if (PlacementMode != PlacementMode.Pointer) if (Placement != PlacementMode.Pointer)
{ {
SubscribeToEventHandler<IWindowImpl, Action<PixelPoint>>(window.PlatformImpl, WindowPositionChanged, SubscribeToEventHandler<IWindowImpl, Action<PixelPoint>>(window.PlatformImpl, WindowPositionChanged,
(x, handler) => x.PositionChanged += handler, (x, handler) => x.PositionChanged += handler,
@ -534,7 +548,7 @@ namespace Avalonia.Controls.Primitives
UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget); UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget);
} }
else if (change.Property == PlacementTargetProperty || else if (change.Property == PlacementTargetProperty ||
change.Property == PlacementModeProperty || change.Property == PlacementProperty ||
change.Property == HorizontalOffsetProperty || change.Property == HorizontalOffsetProperty ||
change.Property == VerticalOffsetProperty || change.Property == VerticalOffsetProperty ||
change.Property == PlacementAnchorProperty || change.Property == PlacementAnchorProperty ||
@ -567,7 +581,7 @@ namespace Avalonia.Controls.Primitives
{ {
popupHost.ConfigurePosition( popupHost.ConfigurePosition(
placementTarget, placementTarget,
PlacementMode, Placement,
new Point(HorizontalOffset, VerticalOffset), new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor, PlacementAnchor,
PlacementGravity, PlacementGravity,
@ -615,7 +629,7 @@ namespace Avalonia.Controls.Primitives
return; return;
_openState.PopupHost.ConfigurePosition( _openState.PopupHost.ConfigurePosition(
placementTarget, placementTarget,
PlacementMode, Placement,
new Point(HorizontalOffset, VerticalOffset), new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor, PlacementAnchor,
PlacementGravity, PlacementGravity,

3
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@ -112,7 +112,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry)) ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry))
?? screens.FirstOrDefault(); ?? screens.FirstOrDefault();
if (targetScreen != null && targetScreen.WorkingArea.IsDefault) if (targetScreen != null &&
(targetScreen.WorkingArea.Width == 0 && targetScreen.WorkingArea.Height == 0))
{ {
return targetScreen.Bounds; return targetScreen.Bounds;
} }

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

@ -145,6 +145,11 @@ namespace Avalonia.Controls.Primitives
private BindingHelper? _bindingHelper; private BindingHelper? _bindingHelper;
private bool _isSelectionChangeActive; private bool _isSelectionChangeActive;
public SelectingItemsControl()
{
((ItemCollection)ItemsView).SourceChanged += OnItemsViewSourceChanged;
}
/// <summary> /// <summary>
/// Initializes static members of the <see cref="SelectingItemsControl"/> class. /// Initializes static members of the <see cref="SelectingItemsControl"/> class.
/// </summary> /// </summary>
@ -229,6 +234,7 @@ namespace Avalonia.Controls.Primitives
/// <see cref="SelectedValue"/> property /// <see cref="SelectedValue"/> property
/// </summary> /// </summary>
[AssignBinding] [AssignBinding]
[InheritDataTypeFromItems(nameof(ItemsSource))]
[InheritDataTypeFromItems(nameof(Items))] [InheritDataTypeFromItems(nameof(Items))]
public IBinding? SelectedValueBinding public IBinding? SelectedValueBinding
{ {
@ -322,7 +328,7 @@ namespace Avalonia.Controls.Primitives
} }
else if (_selection != value) else if (_selection != value)
{ {
if (value.Source != null && value.Source != Items) if (value.Source != null && value.Source != ItemsView.Source)
{ {
throw new ArgumentException( throw new ArgumentException(
"The supplied ISelectionModel already has an assigned Source but this " + "The supplied ISelectionModel already has an assigned Source but this " +
@ -434,10 +440,9 @@ namespace Avalonia.Controls.Primitives
return null; return null;
} }
/// <inheritdoc /> private protected override void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
protected override void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
base.ItemsCollectionChanged(sender!, e); base.OnItemsViewCollectionChanged(sender!, e);
if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
{ {
@ -547,7 +552,7 @@ namespace Avalonia.Controls.Primitives
if (_selection is object) if (_selection is object)
{ {
_selection.Source = Items; _selection.Source = ItemsView.Source;
} }
} }
@ -635,16 +640,6 @@ namespace Avalonia.Controls.Primitives
{ {
AutoScrollToSelectedItemIfNecessary(); AutoScrollToSelectedItemIfNecessary();
} }
if (change.Property == ItemsProperty && _updateState is null && _selection is object)
{
var newValue = change.GetNewValue<IEnumerable?>();
_selection.Source = newValue;
if (newValue is null)
{
_selection.Clear();
}
}
else if (change.Property == SelectionModeProperty && _selection is object) else if (change.Property == SelectionModeProperty && _selection is object)
{ {
var newValue = change.GetNewValue<SelectionMode>(); var newValue = change.GetNewValue<SelectionMode>();
@ -880,6 +875,12 @@ namespace Avalonia.Controls.Primitives
return false; return false;
} }
private void OnItemsViewSourceChanged(object? sender, EventArgs e)
{
if (_selection is not null && _updateState is null)
_selection.Source = ItemsView.Source;
}
/// <summary> /// <summary>
/// Called when <see cref="INotifyPropertyChanged.PropertyChanged"/> is raised on /// Called when <see cref="INotifyPropertyChanged.PropertyChanged"/> is raised on
/// <see cref="Selection"/>. /// <see cref="Selection"/>.
@ -968,7 +969,7 @@ namespace Avalonia.Controls.Primitives
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
private void OnSelectionModelLostSelection(object? sender, EventArgs e) private void OnSelectionModelLostSelection(object? sender, EventArgs e)
{ {
if (AlwaysSelected && Items is object) if (AlwaysSelected && ItemsView.Count > 0)
{ {
SelectedIndex = 0; SelectedIndex = 0;
} }
@ -998,14 +999,14 @@ namespace Avalonia.Controls.Primitives
} }
} }
private object FindItemWithValue(object? value) private object? FindItemWithValue(object? value)
{ {
if (ItemCount == 0 || value is null) if (ItemCount == 0 || value is null)
{ {
return AvaloniaProperty.UnsetValue; return AvaloniaProperty.UnsetValue;
} }
var items = Items; var items = ItemsView;
var binding = SelectedValueBinding; var binding = SelectedValueBinding;
if (binding is null) if (binding is null)
@ -1169,7 +1170,7 @@ namespace Avalonia.Controls.Primitives
{ {
if (_updateState is null) if (_updateState is null)
{ {
model.Source = Items; model.Source = ItemsView.Source;
} }
model.PropertyChanged += OnSelectionModelPropertyChanged; model.PropertyChanged += OnSelectionModelPropertyChanged;
@ -1231,16 +1232,18 @@ namespace Avalonia.Controls.Primitives
Selection = state.Selection.Value; Selection = state.Selection.Value;
} }
if (state.SelectedItems.HasValue) if (_selection is InternalSelectionModel s)
{ {
SelectedItems = state.SelectedItems.Value; s.Update(ItemsView.Source, state.SelectedItems);
} }
else
Selection.Source = Items;
if (Items is null)
{ {
Selection.Clear(); if (state.SelectedItems.HasValue)
{
SelectedItems = state.SelectedItems.Value;
}
Selection.Source = ItemsView.Source;
} }
if (state.SelectedValue.HasValue) if (state.SelectedValue.HasValue)

107
src/Avalonia.Controls/SelectableTextBlock.cs

@ -17,17 +17,11 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public class SelectableTextBlock : TextBlock, IInlineHost public class SelectableTextBlock : TextBlock, IInlineHost
{ {
public static readonly DirectProperty<SelectableTextBlock, int> SelectionStartProperty = public static readonly StyledProperty<int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>( TextBox.SelectionStartProperty.AddOwner<SelectableTextBlock>(new(coerce: TextBox.CoerceCaretIndex));
nameof(SelectionStart),
o => o.SelectionStart, public static readonly StyledProperty<int> SelectionEndProperty =
(o, v) => o.SelectionStart = v); TextBox.SelectionEndProperty.AddOwner<SelectableTextBlock>(new(coerce: TextBox.CoerceCaretIndex));
public static readonly DirectProperty<SelectableTextBlock, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
nameof(SelectionEnd),
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
public static readonly DirectProperty<SelectableTextBlock, string> SelectedTextProperty = public static readonly DirectProperty<SelectableTextBlock, string> SelectedTextProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, string>( AvaloniaProperty.RegisterDirect<SelectableTextBlock, string>(
@ -35,21 +29,16 @@ namespace Avalonia.Controls
o => o.SelectedText); o => o.SelectedText);
public static readonly StyledProperty<IBrush?> SelectionBrushProperty = public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<SelectableTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue); TextBox.SelectionBrushProperty.AddOwner<SelectableTextBlock>(new(new Data.Optional<IBrush?>(Brushes.Blue)));
public static readonly DirectProperty<SelectableTextBlock, bool> CanCopyProperty = public static readonly DirectProperty<SelectableTextBlock, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, bool>( TextBox.CanCopyProperty.AddOwner<SelectableTextBlock>(o => o.CanCopy);
nameof(CanCopy),
o => o.CanCopy);
public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent = public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
RoutedEvent.Register<SelectableTextBlock, RoutedEventArgs>( RoutedEvent.Register<SelectableTextBlock, RoutedEventArgs>(
nameof(CopyingToClipboard), RoutingStrategies.Bubble); nameof(CopyingToClipboard), RoutingStrategies.Bubble);
private bool _canCopy; private bool _canCopy;
private int _selectionStart;
private int _selectionEnd;
private int _wordSelectionStart = -1; private int _wordSelectionStart = -1;
static SelectableTextBlock() static SelectableTextBlock()
@ -78,16 +67,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public int SelectionStart public int SelectionStart
{ {
get => _selectionStart; get => GetValue(SelectionStartProperty);
set set => SetValue(SelectionStartProperty, value);
{
if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
}
}
} }
/// <summary> /// <summary>
@ -95,16 +76,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public int SelectionEnd public int SelectionEnd
{ {
get => _selectionEnd; get => GetValue(SelectionEndProperty);
set set => SetValue(SelectionEndProperty, value);
{
if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
}
}
} }
/// <summary> /// <summary>
@ -150,7 +123,7 @@ namespace Avalonia.Controls
await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard))) await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
.SetTextAsync(text); .SetTextAsync(text);
} }
} }
/// <summary> /// <summary>
/// Select all text in the TextBox /// Select all text in the TextBox
@ -159,8 +132,8 @@ namespace Avalonia.Controls
{ {
var text = Text; var text = Text;
SelectionStart = 0; SetCurrentValue(SelectionStartProperty, 0);
SelectionEnd = text?.Length ?? 0; SetCurrentValue(SelectionEndProperty, text?.Length ?? 0);
} }
/// <summary> /// <summary>
@ -168,7 +141,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public void ClearSelection() public void ClearSelection()
{ {
SelectionEnd = SelectionStart; SetCurrentValue(SelectionEndProperty, SelectionStart);
} }
protected override void OnGotFocus(GotFocusEventArgs e) protected override void OnGotFocus(GotFocusEventArgs e)
@ -204,7 +177,7 @@ namespace Avalonia.Controls
var rects = TextLayout.HitTestTextRange(start, length); var rects = TextLayout.HitTestTextRange(start, length);
using (context.PushPostTransform(Matrix.CreateTranslation(origin))) using (context.PushTransform(Matrix.CreateTranslation(origin)))
{ {
foreach (var rect in rects) foreach (var rect in rects)
{ {
@ -240,6 +213,17 @@ namespace Avalonia.Controls
e.Handled = handled; e.Handled = handled;
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == SelectionStartProperty || change.Property == SelectionEndProperty)
{
RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e) protected override void OnPointerPressed(PointerPressedEventArgs e)
{ {
base.OnPointerPressed(e); base.OnPointerPressed(e);
@ -271,25 +255,26 @@ namespace Avalonia.Controls
if (index > _wordSelectionStart) if (index > _wordSelectionStart)
{ {
SelectionEnd = StringUtils.NextWord(text, index); SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
} }
if (index < _wordSelectionStart || previousWord == _wordSelectionStart) if (index < _wordSelectionStart || previousWord == _wordSelectionStart)
{ {
SelectionStart = previousWord; SetCurrentValue(SelectionStartProperty, previousWord);
} }
} }
else else
{ {
SelectionStart = Math.Min(oldIndex, index); SetCurrentValue(SelectionStartProperty, Math.Min(oldIndex, index));
SelectionEnd = Math.Max(oldIndex, index); SetCurrentValue(SelectionEndProperty, Math.Max(oldIndex, index));
} }
} }
else else
{ {
if (_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd) if (_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd)
{ {
SelectionStart = SelectionEnd = index; SetCurrentValue(SelectionStartProperty, index);
SetCurrentValue(SelectionEndProperty, index);
_wordSelectionStart = -1; _wordSelectionStart = -1;
} }
@ -299,16 +284,16 @@ namespace Avalonia.Controls
case 2: case 2:
if (!StringUtils.IsStartOfWord(text, index)) if (!StringUtils.IsStartOfWord(text, index))
{ {
SelectionStart = StringUtils.PreviousWord(text, index); SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, index));
} }
_wordSelectionStart = SelectionStart; _wordSelectionStart = SelectionStart;
if (!StringUtils.IsEndOfWord(text, index)) if (!StringUtils.IsEndOfWord(text, index))
{ {
SelectionEnd = StringUtils.NextWord(text, index); SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
} }
break; break;
case 3: case 3:
_wordSelectionStart = -1; _wordSelectionStart = -1;
@ -347,22 +332,22 @@ namespace Avalonia.Controls
if (distance <= 0) if (distance <= 0)
{ {
SelectionStart = StringUtils.PreviousWord(text, textPosition); SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, textPosition));
} }
if (distance >= 0) if (distance >= 0)
{ {
if (SelectionStart != _wordSelectionStart) if (SelectionStart != _wordSelectionStart)
{ {
SelectionStart = _wordSelectionStart; SetCurrentValue(SelectionStartProperty, _wordSelectionStart);
} }
SelectionEnd = StringUtils.NextWord(text, textPosition); SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, textPosition));
} }
} }
else else
{ {
SelectionEnd = textPosition; SetCurrentValue(SelectionEndProperty, textPosition);
} }
} }
@ -395,7 +380,8 @@ namespace Avalonia.Controls
caretIndex >= firstSelection && caretIndex <= lastSelection; caretIndex >= firstSelection && caretIndex <= lastSelection;
if (!didClickInSelection) if (!didClickInSelection)
{ {
SelectionStart = SelectionEnd = caretIndex; SetCurrentValue(SelectionStartProperty, caretIndex);
SetCurrentValue(SelectionEndProperty, caretIndex);
} }
} }
@ -411,9 +397,8 @@ namespace Avalonia.Controls
private string GetSelection() private string GetSelection()
{ {
var text = GetText(); var textLength = Text?.Length ?? 0;
if (textLength == 0)
if (string.IsNullOrEmpty(text))
{ {
return ""; return "";
} }
@ -423,14 +408,14 @@ namespace Avalonia.Controls
var start = Math.Min(selectionStart, selectionEnd); var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd); var end = Math.Max(selectionStart, selectionEnd);
if (start == end || text.Length < end) if (start == end || textLength < end)
{ {
return ""; return "";
} }
var length = Math.Max(0, end - start); var length = Math.Max(0, end - start);
var selectedText = text.Substring(start, length); var selectedText = Text!.Substring(start, length);
return selectedText; return selectedText;
} }

27
src/Avalonia.Controls/Selection/InternalSelectionModel.cs

@ -5,6 +5,7 @@ using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Data;
namespace Avalonia.Controls.Selection namespace Avalonia.Controls.Selection
{ {
@ -13,6 +14,7 @@ namespace Avalonia.Controls.Selection
private IList? _writableSelectedItems; private IList? _writableSelectedItems;
private int _ignoreModelChanges; private int _ignoreModelChanges;
private bool _ignoreSelectedItemsChanges; private bool _ignoreSelectedItemsChanges;
private bool _skipSyncFromSelectedItems;
private bool _isResetting; private bool _isResetting;
public InternalSelectionModel() public InternalSelectionModel()
@ -60,6 +62,29 @@ namespace Avalonia.Controls.Selection
} }
} }
internal void Update(IEnumerable? source, Optional<IList?> selectedItems)
{
var previousSource = Source;
var previousWritableSelectedItems = _writableSelectedItems;
try
{
_skipSyncFromSelectedItems = true;
SetSource(source);
if (selectedItems.HasValue)
WritableSelectedItems = selectedItems.Value;
}
finally
{
_skipSyncFromSelectedItems = false;
}
// We skipped the sync from WritableSelectedItems before; do it now that both
// the source and WritableSelectedItems are updated.
if (previousSource != Source || previousWritableSelectedItems != _writableSelectedItems)
SyncFromSelectedItems();
}
private protected override void SetSource(IEnumerable? value) private protected override void SetSource(IEnumerable? value)
{ {
if (Source == value) if (Source == value)
@ -121,7 +146,7 @@ namespace Avalonia.Controls.Selection
private void SyncFromSelectedItems() private void SyncFromSelectedItems()
{ {
if (Source is null || _writableSelectedItems is null) if (_skipSyncFromSelectedItems || Source is null || _writableSelectedItems is null)
{ {
return; return;
} }

4
src/Avalonia.Controls/Selection/SelectionModel.cs

@ -30,9 +30,9 @@ namespace Avalonia.Controls.Selection
Source = source; Source = source;
} }
public new IEnumerable<T>? Source public new IEnumerable? Source
{ {
get => base.Source as IEnumerable<T>; get => base.Source;
set => SetSource(value); set => SetSource(value);
} }

2
src/Avalonia.Controls/SplitButton/SplitButton.cs

@ -172,7 +172,7 @@ namespace Avalonia.Controls
flyout.Opened += Flyout_Opened; flyout.Opened += Flyout_Opened;
flyout.Closed += Flyout_Closed; flyout.Closed += Flyout_Closed;
_flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(Popup.PlacementModeProperty).Subscribe(Flyout_PlacementPropertyChanged); _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(Popup.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged);
} }
} }

66
src/Avalonia.Controls/TextBlock.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Layout; using Avalonia.Layout;
@ -13,6 +14,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control that displays a block of text. /// A control that displays a block of text.
/// </summary> /// </summary>
[DebuggerDisplay("Text = {" + nameof(DebugText) + "}")]
public class TextBlock : Control, IInlineHost public class TextBlock : Control, IInlineHost
{ {
/// <summary> /// <summary>
@ -103,11 +105,8 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="Text"/> property. /// Defines the <see cref="Text"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<TextBlock, string?> TextProperty = public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.RegisterDirect<TextBlock, string?>( AvaloniaProperty.Register<TextBlock, string?>(nameof(Text));
nameof(Text),
o => o.GetText(),
(o, v) => o.SetText(v));
/// <summary> /// <summary>
/// Defines the <see cref="TextAlignment"/> property. /// Defines the <see cref="TextAlignment"/> property.
@ -142,14 +141,14 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="Inlines"/> property. /// Defines the <see cref="Inlines"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<InlineCollection?> InlinesProperty = public static readonly DirectProperty<TextBlock, InlineCollection?> InlinesProperty =
AvaloniaProperty.Register<TextBlock, InlineCollection?>( AvaloniaProperty.RegisterDirect<TextBlock, InlineCollection?>(
nameof(Inlines)); nameof(Inlines), t => t.Inlines, (t, v) => t.Inlines = v);
internal string? _text;
protected TextLayout? _textLayout; protected TextLayout? _textLayout;
protected Size _constraint; protected Size _constraint;
private IReadOnlyList<TextRun>? _textRuns; private IReadOnlyList<TextRun>? _textRuns;
private InlineCollection? _inlines;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="TextBlock"/> class. /// Initializes static members of the <see cref="TextBlock"/> class.
@ -173,7 +172,7 @@ namespace Avalonia.Controls
/// <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 => _textLayout ??= CreateTextLayout(_text); public TextLayout TextLayout => _textLayout ??= CreateTextLayout(Text);
/// <summary> /// <summary>
/// Gets or sets the padding to place around the <see cref="Text"/>. /// Gets or sets the padding to place around the <see cref="Text"/>.
@ -198,10 +197,12 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public string? Text public string? Text
{ {
get => GetText(); get => GetValue(TextProperty);
set => SetText(value); set => SetValue(TextProperty, value);
} }
private string? DebugText => Text ?? Inlines?.Text;
/// <summary> /// <summary>
/// Gets or sets the font family used to draw the control's text. /// Gets or sets the font family used to draw the control's text.
/// </summary> /// </summary>
@ -325,8 +326,8 @@ namespace Avalonia.Controls
[Content] [Content]
public InlineCollection? Inlines public InlineCollection? Inlines
{ {
get => GetValue(InlinesProperty); get => _inlines;
set => SetValue(InlinesProperty, value); set => SetAndRaise(InlinesProperty, ref _inlines, value);
} }
protected override bool BypassFlowDirectionPolicies => true; protected override bool BypassFlowDirectionPolicies => true;
@ -590,19 +591,18 @@ namespace Avalonia.Controls
TextLayout.Draw(context, origin); TextLayout.Draw(context, origin);
} }
protected virtual string? GetText() private bool _clearTextInternal;
{ internal void ClearTextInternal()
return _text ?? Inlines?.Text;
}
protected virtual void SetText(string? text)
{ {
if (HasComplexContent) _clearTextInternal = true;
try
{
SetCurrentValue(TextProperty, null);
}
finally
{ {
Inlines?.Clear(); _clearTextInternal = false;
} }
SetAndRaise(TextProperty, ref _text, text);
} }
/// <summary> /// <summary>
@ -780,6 +780,14 @@ namespace Avalonia.Controls
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == TextProperty)
{
if (HasComplexContent && !_clearTextInternal)
{
Inlines?.Clear();
}
}
switch (change.Property.Name) switch (change.Property.Name)
{ {
case nameof(FontSize): case nameof(FontSize):
@ -794,10 +802,10 @@ namespace Avalonia.Controls
case nameof(FlowDirection): case nameof(FlowDirection):
case nameof (Padding): case nameof(Padding):
case nameof (LineHeight): case nameof(LineHeight):
case nameof (LetterSpacing): case nameof(LetterSpacing):
case nameof (MaxLines): case nameof(MaxLines):
case nameof(Text): case nameof(Text):
case nameof(TextDecorations): case nameof(TextDecorations):
@ -899,7 +907,7 @@ namespace Avalonia.Controls
continue; continue;
} }
if (textRun is TextCharacters) if (textRun is TextCharacters)
{ {
var skip = Math.Max(0, textSourceIndex - currentPosition); var skip = Math.Max(0, textSourceIndex - currentPosition);

433
src/Avalonia.Controls/TextBox.cs

@ -61,11 +61,9 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="CaretIndex"/> property /// Defines the <see cref="CaretIndex"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<TextBox, int> CaretIndexProperty = public static readonly StyledProperty<int> CaretIndexProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>( AvaloniaProperty.Register<TextBox, int>(nameof(CaretIndex),
nameof(CaretIndex), coerce: CoerceCaretIndex);
o => o.CaretIndex,
(o, v) => o.CaretIndex = v);
/// <summary> /// <summary>
/// Defines the <see cref="IsReadOnly"/> property /// Defines the <see cref="IsReadOnly"/> property
@ -100,42 +98,37 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="SelectionStart"/> property /// Defines the <see cref="SelectionStart"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<TextBox, int> SelectionStartProperty = public static readonly StyledProperty<int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>( AvaloniaProperty.Register<TextBox, int>(nameof(SelectionStart),
nameof(SelectionStart), coerce: CoerceCaretIndex);
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
/// <summary> /// <summary>
/// Defines the <see cref="SelectionEnd"/> property /// Defines the <see cref="SelectionEnd"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<TextBox, int> SelectionEndProperty = public static readonly StyledProperty<int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>( AvaloniaProperty.Register<TextBox, int>(nameof(SelectionEnd),
nameof(SelectionEnd), coerce: CoerceCaretIndex);
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
/// <summary> /// <summary>
/// Defines the <see cref="MaxLength"/> property /// Defines the <see cref="MaxLength"/> property
/// </summary> /// </summary>
public static readonly StyledProperty<int> MaxLengthProperty = public static readonly StyledProperty<int> MaxLengthProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength), defaultValue: 0); AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength));
/// <summary> /// <summary>
/// Defines the <see cref="MaxLines"/> property /// Defines the <see cref="MaxLines"/> property
/// </summary> /// </summary>
public static readonly StyledProperty<int> MaxLinesProperty = public static readonly StyledProperty<int> MaxLinesProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLines), defaultValue: 0); AvaloniaProperty.Register<TextBox, int>(nameof(MaxLines));
/// <summary> /// <summary>
/// Defines the <see cref="Text"/> property /// Defines the <see cref="Text"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<TextBox, string?> TextProperty = public static readonly StyledProperty<string?> TextProperty =
TextBlock.TextProperty.AddOwnerWithDataValidation<TextBox>( TextBlock.TextProperty.AddOwner<TextBox>(new(
o => o.Text, coerce: CoerceText,
(o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay, defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true); enableDataValidation: true));
/// <summary> /// <summary>
/// Defines the <see cref="TextAlignment"/> property /// Defines the <see cref="TextAlignment"/> property
@ -185,9 +178,8 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="NewLine"/> property /// Defines the <see cref="NewLine"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<TextBox, string> NewLineProperty = public static readonly StyledProperty<string> NewLineProperty =
AvaloniaProperty.RegisterDirect<TextBox, string>(nameof(NewLine), AvaloniaProperty.Register<TextBox, string>(nameof(NewLine), Environment.NewLine);
textbox => textbox.NewLine, (textbox, newline) => textbox.NewLine = newline);
/// <summary> /// <summary>
/// Defines the <see cref="InnerLeftContent"/> property /// Defines the <see cref="InnerLeftContent"/> property
@ -242,12 +234,8 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="UndoLimit"/> property /// Defines the <see cref="UndoLimit"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<TextBox, int> UndoLimitProperty = public static readonly StyledProperty<int> UndoLimitProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>( AvaloniaProperty.Register<TextBox, int>(nameof(UndoLimit), UndoRedoHelper<UndoRedoState>.DefaultUndoLimit);
nameof(UndoLimit),
o => o.UndoLimit,
(o, v) => o.UndoLimit = v,
unsetValue: -1);
/// <summary> /// <summary>
/// Defines the <see cref="CanUndo"/> property /// Defines the <see cref="CanUndo"/> property
@ -318,18 +306,13 @@ namespace Avalonia.Controls
public override int GetHashCode() => Text?.GetHashCode() ?? 0; public override int GetHashCode() => Text?.GetHashCode() ?? 0;
} }
private string? _text;
private int _caretIndex;
private int _selectionStart;
private int _selectionEnd;
private TextPresenter? _presenter; private TextPresenter? _presenter;
private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient(); private readonly TextBoxTextInputMethodClient _imClient = new();
private UndoRedoHelper<UndoRedoState> _undoRedoHelper; private readonly UndoRedoHelper<UndoRedoState> _undoRedoHelper;
private bool _isUndoingRedoing; private bool _isUndoingRedoing;
private bool _canCut; private bool _canCut;
private bool _canCopy; private bool _canCopy;
private bool _canPaste; private bool _canPaste;
private string _newLine = Environment.NewLine;
private static readonly string[] invalidCharacters = new String[1] { "\u007f" }; private static readonly string[] invalidCharacters = new String[1] { "\u007f" };
private bool _canUndo; private bool _canUndo;
private bool _canRedo; private bool _canRedo;
@ -399,18 +382,19 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public int CaretIndex public int CaretIndex
{ {
get => _caretIndex; get => GetValue(CaretIndexProperty);
set set => SetValue(CaretIndexProperty, value);
{ }
value = CoerceCaretIndex(value);
SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
UndoRedoState state; private void OnCaretIndexChanged(AvaloniaPropertyChangedEventArgs e)
if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text) {
_undoRedoHelper.UpdateLastState(); UndoRedoState state;
if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
_undoRedoHelper.UpdateLastState();
SelectionStart = SelectionEnd = value; var newValue = e.GetNewValue<int>();
} SetCurrentValue(SelectionStartProperty, newValue);
SetCurrentValue(SelectionEndProperty, newValue);
} }
/// <summary> /// <summary>
@ -463,21 +447,18 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public int SelectionStart public int SelectionStart
{ {
get => _selectionStart; get => GetValue(SelectionStartProperty);
set set => SetValue(SelectionStartProperty, value);
{ }
value = CoerceCaretIndex(value);
var changed = SetAndRaise(SelectionStartProperty, ref _selectionStart, value);
if (changed) private void OnSelectionStartChanged(AvaloniaPropertyChangedEventArgs e)
{ {
UpdateCommandStates(); UpdateCommandStates();
}
if (SelectionEnd == value && CaretIndex != value) var value = e.GetNewValue<int>();
{ if (SelectionEnd == value && CaretIndex != value)
CaretIndex = value; {
} SetCurrentValue(CaretIndexProperty, value);
} }
} }
@ -490,21 +471,18 @@ namespace Avalonia.Controls
/// </remarks> /// </remarks>
public int SelectionEnd public int SelectionEnd
{ {
get => _selectionEnd; get => GetValue(SelectionEndProperty);
set set => SetValue(SelectionEndProperty, value);
{ }
value = CoerceCaretIndex(value);
var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); private void OnSelectionEndChanged(AvaloniaPropertyChangedEventArgs e)
{
if (changed) UpdateCommandStates();
{
UpdateCommandStates();
}
if (SelectionStart == value && CaretIndex != value) var value = e.GetNewValue<int>();
{ if (SelectionStart == value && CaretIndex != value)
CaretIndex = value; {
} SetCurrentValue(CaretIndexProperty, value);
} }
} }
@ -550,36 +528,27 @@ namespace Avalonia.Controls
[Content] [Content]
public string? Text public string? Text
{ {
get => _text; get => GetValue(TextProperty);
set set => SetValue(TextProperty, value);
{ }
var caretIndex = CaretIndex;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
CaretIndex = CoerceCaretIndex(caretIndex, value);
SelectionStart = CoerceCaretIndex(selectionStart, value);
SelectionEnd = CoerceCaretIndex(selectionEnd, value);
// Before #9490, snapshot here was done AFTER text change - this doesn't make sense
// since intial state would never be no text and you'd always have to make a text
// change before undo would be available
// The undo/redo stacks were also cleared at this point, which also doesn't make sense
// as it is still valid to want to undo a programmatic text set
// So we snapshot text now BEFORE the change so we can always revert
// Also don't need to check IsUndoEnabled here, that's done in SnapshotUndoRedo
if (!_isUndoingRedoing)
{
SnapshotUndoRedo();
}
var textChanged = SetAndRaise(TextProperty, ref _text, value);
if (textChanged) private static string? CoerceText(AvaloniaObject sender, string? value)
{ {
RaiseTextChangeEvents(); var textBox = (TextBox)sender;
}
// Before #9490, snapshot here was done AFTER text change - this doesn't make sense
// since intial state would never be no text and you'd always have to make a text
// change before undo would be available
// The undo/redo stacks were also cleared at this point, which also doesn't make sense
// as it is still valid to want to undo a programmatic text set
// So we snapshot text now BEFORE the change so we can always revert
// Also don't need to check IsUndoEnabled here, that's done in SnapshotUndoRedo
if (!textBox._isUndoingRedoing)
{
textBox.SnapshotUndoRedo();
} }
return value;
} }
/// <summary> /// <summary>
@ -691,8 +660,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public string NewLine public string NewLine
{ {
get => _newLine; get => GetValue(NewLineProperty);
set => SetAndRaise(NewLineProperty, ref _newLine, value); set => SetValue(NewLineProperty, value);
} }
/// <summary> /// <summary>
@ -700,7 +669,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public void ClearSelection() public void ClearSelection()
{ {
CaretIndex = SelectionStart; SetCurrentValue(CaretIndexProperty, SelectionStart);
SetCurrentValue(SelectionEndProperty, SelectionStart);
} }
/// <summary> /// <summary>
@ -744,25 +714,20 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public int UndoLimit public int UndoLimit
{ {
get => _undoRedoHelper.Limit; get => GetValue(UndoLimitProperty);
set set => SetValue(UndoLimitProperty, value);
{ }
if (_undoRedoHelper.Limit != value)
{ private void OnUndoLimitChanged(int newValue)
// can't use SetAndRaise due to using _undoRedoHelper.Limit {
// (can't send a ref of a property to SetAndRaise), _undoRedoHelper.Limit = newValue;
// so use RaisePropertyChanged instead.
var oldValue = _undoRedoHelper.Limit; // from docs at
_undoRedoHelper.Limit = value; // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
RaisePropertyChanged(UndoLimitProperty, oldValue, value); // "Setting UndoLimit clears the undo queue."
} _undoRedoHelper.Clear();
// from docs at _selectedTextChangesMadeSinceLastUndoSnapshot = 0;
// https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: _hasDoneSnapshotOnce = false;
// "Setting UndoLimit clears the undo queue."
_undoRedoHelper.Clear();
_selectedTextChangesMadeSinceLastUndoSnapshot = 0;
_hasDoneSnapshotOnce = false;
}
} }
/// <summary> /// <summary>
@ -866,9 +831,31 @@ namespace Avalonia.Controls
if (change.Property == TextProperty) if (change.Property == TextProperty)
{ {
CoerceValue(CaretIndexProperty);
CoerceValue(SelectionStartProperty);
CoerceValue(SelectionEndProperty);
RaiseTextChangeEvents();
UpdatePseudoclasses(); UpdatePseudoclasses();
UpdateCommandStates(); UpdateCommandStates();
} }
else if (change.Property == CaretIndexProperty)
{
OnCaretIndexChanged(change);
}
else if (change.Property == SelectionStartProperty)
{
OnSelectionStartChanged(change);
}
else if (change.Property == SelectionEndProperty)
{
OnSelectionEndChanged(change);
}
else if (change.Property == UndoLimitProperty)
{
OnUndoLimitChanged(change.GetNewValue<int>());
}
else if (change.Property == IsUndoEnabledProperty && change.GetNewValue<bool>() == false) else if (change.Property == IsUndoEnabledProperty && change.GetNewValue<bool>() == false)
{ {
// from docs at // from docs at
@ -920,7 +907,7 @@ namespace Avalonia.Controls
(ContextMenu == null || !ContextMenu.IsOpen)) (ContextMenu == null || !ContextMenu.IsOpen))
{ {
ClearSelection(); ClearSelection();
RevealPassword = false; SetCurrentValue(RevealPasswordProperty, false);
} }
UpdateCommandStates(); UpdateCommandStates();
@ -986,35 +973,44 @@ namespace Avalonia.Controls
} }
} }
var text = Text ?? string.Empty; var currentText = Text ?? string.Empty;
var newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); var selectionLength = Math.Abs(SelectionStart - SelectionEnd);
var newLength = input.Length + currentText.Length - selectionLength;
if (MaxLength > 0 && newLength > MaxLength) if (MaxLength > 0 && newLength > MaxLength)
{ {
input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength))); input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength)));
newLength = MaxLength;
} }
if (!string.IsNullOrEmpty(input)) if (!string.IsNullOrEmpty(input))
{ {
var oldText = _text; var textBuilder = StringBuilderCache.Acquire(Math.Max(currentText.Length, newLength));
textBuilder.Append(currentText);
DeleteSelection(false);
var caretIndex = CaretIndex; var caretIndex = CaretIndex;
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
ClearSelection();
if (IsUndoEnabled) if (selectionLength != 0)
{ {
_undoRedoHelper.DiscardRedo(); var (start, _) = GetSelectionRange();
textBuilder.Remove(start, selectionLength);
caretIndex = start;
} }
if (_text != oldText) textBuilder.Insert(caretIndex, input);
SetCurrentValue(TextProperty, StringBuilderCache.GetStringAndRelease(textBuilder));
ClearSelection();
if (IsUndoEnabled)
{ {
RaisePropertyChanged(TextProperty, oldText, _text); _undoRedoHelper.DiscardRedo();
} }
CaretIndex = caretIndex + input.Length; SetCurrentValue(CaretIndexProperty, caretIndex + input.Length);
} }
} }
@ -1168,7 +1164,7 @@ namespace Avalonia.Controls
movement = true; movement = true;
selection = false; selection = false;
handled = true; handled = true;
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
else if (Match(keymap.MoveCursorToTheEndOfDocument)) else if (Match(keymap.MoveCursorToTheEndOfDocument))
{ {
@ -1176,7 +1172,7 @@ namespace Avalonia.Controls
movement = true; movement = true;
selection = false; selection = false;
handled = true; handled = true;
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
else if (Match(keymap.MoveCursorToTheStartOfLine)) else if (Match(keymap.MoveCursorToTheStartOfLine))
{ {
@ -1184,7 +1180,7 @@ namespace Avalonia.Controls
movement = true; movement = true;
selection = false; selection = false;
handled = true; handled = true;
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
else if (Match(keymap.MoveCursorToTheEndOfLine)) else if (Match(keymap.MoveCursorToTheEndOfLine))
{ {
@ -1192,31 +1188,31 @@ namespace Avalonia.Controls
movement = true; movement = true;
selection = false; selection = false;
handled = true; handled = true;
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
else if (Match(keymap.MoveCursorToTheStartOfDocumentWithSelection)) else if (Match(keymap.MoveCursorToTheStartOfDocumentWithSelection))
{ {
SelectionStart = caretIndex; SetCurrentValue(SelectionStartProperty, caretIndex);
MoveHome(true); MoveHome(true);
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
movement = true; movement = true;
selection = true; selection = true;
handled = true; handled = true;
} }
else if (Match(keymap.MoveCursorToTheEndOfDocumentWithSelection)) else if (Match(keymap.MoveCursorToTheEndOfDocumentWithSelection))
{ {
SelectionStart = caretIndex; SetCurrentValue(SelectionStartProperty, caretIndex);
MoveEnd(true); MoveEnd(true);
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
movement = true; movement = true;
selection = true; selection = true;
handled = true; handled = true;
} }
else if (Match(keymap.MoveCursorToTheStartOfLineWithSelection)) else if (Match(keymap.MoveCursorToTheStartOfLineWithSelection))
{ {
SelectionStart = caretIndex; SetCurrentValue(SelectionStartProperty, caretIndex);
MoveHome(false); MoveHome(false);
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
movement = true; movement = true;
selection = true; selection = true;
handled = true; handled = true;
@ -1224,9 +1220,9 @@ namespace Avalonia.Controls
} }
else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection)) else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection))
{ {
SelectionStart = caretIndex; SetCurrentValue(SelectionStartProperty, caretIndex);
MoveEnd(false); MoveEnd(false);
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
movement = true; movement = true;
selection = true; selection = true;
handled = true; handled = true;
@ -1261,11 +1257,11 @@ namespace Avalonia.Controls
if (selection) if (selection)
{ {
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
} }
else else
{ {
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
break; break;
@ -1283,11 +1279,11 @@ namespace Avalonia.Controls
if (selection) if (selection)
{ {
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
} }
else else
{ {
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
break; break;
@ -1314,11 +1310,13 @@ namespace Avalonia.Controls
var length = end - start; var length = end - start;
var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); var sb = StringBuilderCache.Acquire(text.Length);
sb.Append(text);
sb.Remove(start, end - start);
SetTextInternal(editedText); SetCurrentValue(TextProperty, StringBuilderCache.GetStringAndRelease(sb));
CaretIndex = start; SetCurrentValue(CaretIndexProperty, start);
} }
} }
@ -1346,9 +1344,11 @@ namespace Avalonia.Controls
var start = Math.Min(nextPosition, caretIndex); var start = Math.Min(nextPosition, caretIndex);
var end = Math.Max(nextPosition, caretIndex); var end = Math.Max(nextPosition, caretIndex);
var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); var sb = StringBuilderCache.Acquire(text.Length);
sb.Append(text);
sb.Remove(start, end - start);
SetTextInternal(editedText); SetCurrentValue(TextProperty, StringBuilderCache.GetStringAndRelease(sb));
} }
} }
@ -1425,7 +1425,7 @@ namespace Avalonia.Controls
var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift); var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
SetAndRaise(CaretIndexProperty, ref _caretIndex, index); SetCurrentValue(CaretIndexProperty, index);
switch (e.ClickCount) switch (e.ClickCount)
{ {
@ -1438,25 +1438,26 @@ namespace Avalonia.Controls
if (index > _wordSelectionStart) if (index > _wordSelectionStart)
{ {
SelectionEnd = StringUtils.NextWord(text, index); SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
} }
if (index < _wordSelectionStart || previousWord == _wordSelectionStart) if (index < _wordSelectionStart || previousWord == _wordSelectionStart)
{ {
SelectionStart = previousWord; SetCurrentValue(SelectionStartProperty, previousWord);
} }
} }
else else
{ {
SelectionStart = Math.Min(oldIndex, index); SetCurrentValue(SelectionStartProperty, Math.Min(oldIndex, index));
SelectionEnd = Math.Max(oldIndex, index); SetCurrentValue(SelectionEndProperty, Math.Max(oldIndex, index));
} }
} }
else else
{ {
if(_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd) if(_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd)
{ {
SelectionStart = SelectionEnd = index; SetCurrentValue(SelectionStartProperty, index);
SetCurrentValue(SelectionEndProperty, index);
_wordSelectionStart = -1; _wordSelectionStart = -1;
} }
} }
@ -1466,14 +1467,14 @@ namespace Avalonia.Controls
if (!StringUtils.IsStartOfWord(text, index)) if (!StringUtils.IsStartOfWord(text, index))
{ {
SelectionStart = StringUtils.PreviousWord(text, index); SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, index));
} }
_wordSelectionStart = SelectionStart; _wordSelectionStart = SelectionStart;
if (!StringUtils.IsEndOfWord(text, index)) if (!StringUtils.IsEndOfWord(text, index))
{ {
SelectionEnd = StringUtils.NextWord(text, index); SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
} }
break; break;
@ -1517,22 +1518,22 @@ namespace Avalonia.Controls
if (distance <= 0) if (distance <= 0)
{ {
SelectionStart = StringUtils.PreviousWord(text, caretIndex); SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, caretIndex));
} }
if (distance >= 0) if (distance >= 0)
{ {
if(SelectionStart != _wordSelectionStart) if(SelectionStart != _wordSelectionStart)
{ {
SelectionStart = _wordSelectionStart; SetCurrentValue(SelectionStartProperty, _wordSelectionStart);
} }
SelectionEnd = StringUtils.NextWord(text, caretIndex); SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, caretIndex));
} }
} }
else else
{ {
SelectionEnd = caretIndex; SetCurrentValue(SelectionEndProperty, caretIndex);
} }
} }
} }
@ -1565,7 +1566,9 @@ namespace Avalonia.Controls
caretIndex >= firstSelection && caretIndex <= lastSelection; caretIndex >= firstSelection && caretIndex <= lastSelection;
if (!didClickInSelection) if (!didClickInSelection)
{ {
CaretIndex = SelectionEnd = SelectionStart = caretIndex; SetCurrentValue(CaretIndexProperty, caretIndex);
SetCurrentValue(SelectionEndProperty, caretIndex);
SetCurrentValue(SelectionStartProperty, caretIndex);
} }
} }
@ -1588,10 +1591,10 @@ namespace Avalonia.Controls
} }
} }
private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text); internal static int CoerceCaretIndex(AvaloniaObject sender, int value)
private static int CoerceCaretIndex(int value, string? text)
{ {
var text = sender.GetValue(TextProperty); // method also used by TextPresenter and SelectableTextBlock
if (text == null) if (text == null)
{ {
return 0; return 0;
@ -1619,10 +1622,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Clears the text in the TextBox /// Clears the text in the TextBox
/// </summary> /// </summary>
public void Clear() public void Clear() => SetCurrentValue(TextProperty, string.Empty);
{
Text = string.Empty;
}
private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting) private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting)
{ {
@ -1645,7 +1645,7 @@ namespace Avalonia.Controls
LogicalDirection.Forward : LogicalDirection.Forward :
LogicalDirection.Backward); LogicalDirection.Backward);
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
} }
else else
{ {
@ -1662,7 +1662,7 @@ namespace Avalonia.Controls
LogicalDirection.Backward); LogicalDirection.Backward);
} }
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
} }
else else
@ -1678,17 +1678,17 @@ namespace Avalonia.Controls
offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd; offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd;
} }
SelectionEnd += offset; SetCurrentValue(SelectionEndProperty, SelectionEnd + offset);
_presenter.MoveCaretToTextPosition(SelectionEnd); _presenter.MoveCaretToTextPosition(SelectionEnd);
if (!isSelecting) if (!isSelecting)
{ {
CaretIndex = SelectionEnd; SetCurrentValue(CaretIndexProperty, SelectionEnd);
} }
else else
{ {
SelectionStart = selectionStart; SetCurrentValue(SelectionStartProperty, selectionStart);
} }
} }
} }
@ -1747,36 +1747,45 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public void SelectAll() public void SelectAll()
{ {
SelectionStart = 0; SetCurrentValue(SelectionStartProperty, 0);
SelectionEnd = Text?.Length ?? 0; SetCurrentValue(SelectionEndProperty, Text?.Length ?? 0);
} }
internal bool DeleteSelection(bool raiseTextChanged = true) private (int start, int end) GetSelectionRange()
{
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
return (Math.Min(selectionStart, selectionEnd), Math.Max(selectionStart, selectionEnd));
}
internal bool DeleteSelection()
{ {
if (IsReadOnly) if (IsReadOnly)
return true; return true;
var selectionStart = SelectionStart; var (start, end) = GetSelectionRange();
var selectionEnd = SelectionEnd;
if (selectionStart != selectionEnd) if (start != end)
{ {
var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd);
var text = Text!; var text = Text!;
var textBuilder = StringBuilderCache.Acquire(text.Length);
SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged); textBuilder.Append(text);
textBuilder.Remove(start, end - start);
SetCurrentValue(TextProperty, textBuilder.ToString());
_presenter?.MoveCaretToTextPosition(start); _presenter?.MoveCaretToTextPosition(start);
CaretIndex = start; SetCurrentValue(CaretIndexProperty, start);
ClearSelection(); ClearSelection();
return true; return true;
} }
CaretIndex = SelectionStart; SetCurrentValue(CaretIndexProperty, SelectionStart);
return false; return false;
} }
@ -1826,46 +1835,30 @@ namespace Avalonia.Controls
}, DispatcherPriority.Normal); }, DispatcherPriority.Normal);
} }
private void SetTextInternal(string value, bool raiseTextChanged = true)
{
if (raiseTextChanged)
{
bool textChanged = SetAndRaise(TextProperty, ref _text, value);
if (textChanged)
{
RaiseTextChangeEvents();
}
}
else
{
_text = value;
}
}
private void SetSelectionForControlBackspace() private void SetSelectionForControlBackspace()
{ {
var selectionStart = CaretIndex; var selectionStart = CaretIndex;
MoveHorizontal(-1, true, false); MoveHorizontal(-1, true, false);
SelectionStart = selectionStart; SetCurrentValue(SelectionStartProperty, selectionStart);
} }
private void SetSelectionForControlDelete() private void SetSelectionForControlDelete()
{ {
if (_text == null || _presenter == null) var textLength = Text?.Length ?? 0;
if (_presenter == null || textLength == 0)
{ {
return; return;
} }
SelectionStart = CaretIndex; SetCurrentValue(SelectionStartProperty, CaretIndex);
MoveHorizontal(1, true, true); MoveHorizontal(1, true, true);
if (SelectionEnd < _text.Length && _text[SelectionEnd] == ' ') if (SelectionEnd < textLength && Text![SelectionEnd] == ' ')
{ {
SelectionEnd++; SetCurrentValue(SelectionEndProperty, SelectionEnd + 1);
} }
} }
@ -1881,8 +1874,8 @@ namespace Avalonia.Controls
get => new UndoRedoState(Text, CaretIndex); get => new UndoRedoState(Text, CaretIndex);
set set
{ {
Text = value.Text; SetCurrentValue(TextProperty, value.Text);
CaretIndex = value.CaretPosition; SetCurrentValue(CaretIndexProperty, value.CaretPosition);
ClearSelection(); ClearSelection();
} }
} }

19
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -106,7 +106,7 @@ namespace Avalonia.Controls
{ {
if (_presenter != null && _textEditable != null) if (_presenter != null && _textEditable != null)
{ {
_presenter.CompositionRegion = new TextRange(_textEditable.CompositionStart, _textEditable.CompositionEnd); _presenter.SetCurrentValue(TextPresenter.CompositionRegionProperty, new TextRange(_textEditable.CompositionStart, _textEditable.CompositionEnd));
} }
} }
@ -177,9 +177,9 @@ namespace Avalonia.Controls
var text = GetText(preeditText); var text = GetText(preeditText);
_presenter._text = text; _presenter.SetCurrentValue(TextPresenter.TextProperty, text);
_presenter.PreeditText = preeditText; _presenter.SetCurrentValue(TextPresenter.PreeditTextProperty, preeditText);
_presenter.UpdateCaret(new CharacterHit(_compositionStart + (preeditText != null ? preeditText.Length : 0)), false); _presenter.UpdateCaret(new CharacterHit(_compositionStart + (preeditText != null ? preeditText.Length : 0)), false);
@ -201,9 +201,12 @@ namespace Avalonia.Controls
return preeditText; return preeditText;
} }
var text = _presenterText.Substring(0, _compositionStart) + preeditText + _presenterText.Substring(_compositionStart); var sb = StringBuilderCache.Acquire(_presenterText.Length + preeditText.Length);
return text; sb.Append(_presenterText);
sb.Insert(_compositionStart, preeditText);
return StringBuilderCache.GetStringAndRelease(sb);
} }
public void SetComposingRegion(TextRange? region) public void SetComposingRegion(TextRange? region)
@ -213,7 +216,7 @@ namespace Avalonia.Controls
return; return;
} }
_presenter.CompositionRegion = region; _presenter.SetCurrentValue(TextPresenter.CompositionRegionProperty, region);
} }
public void SelectInSurroundingText(int start, int end) public void SelectInSurroundingText(int start, int end)
@ -252,9 +255,9 @@ namespace Avalonia.Controls
if (_presenter != null) if (_presenter != null)
{ {
_presenter.PreeditText = null; _presenter.ClearValue(TextPresenter.PreeditTextProperty);
_presenter.CompositionRegion = null; _presenter.ClearValue(TextPresenter.CompositionRegionProperty);
_presenter.CaretBoundsChanged -= OnCaretBoundsChanged; _presenter.CaretBoundsChanged -= OnCaretBoundsChanged;
} }

1
src/Avalonia.Controls/TopLevel.cs

@ -15,7 +15,6 @@ using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Reactive;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Utilities; using Avalonia.Utilities;

4
src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs

@ -144,13 +144,13 @@ namespace Avalonia.Controls.Utils
{ {
get get
{ {
return SelectorControl?.Items; return SelectorControl?.ItemsSource;
} }
set set
{ {
if (SelectorControl != null) if (SelectorControl != null)
{ {
SelectorControl.Items = value; SelectorControl.ItemsSource = value;
} }
} }
} }

4
src/Avalonia.Controls/Utils/UndoRedoHelper.cs

@ -4,6 +4,8 @@ namespace Avalonia.Controls.Utils
{ {
class UndoRedoHelper<TState> class UndoRedoHelper<TState>
{ {
public const int DefaultUndoLimit = 10;
private readonly IUndoRedoHost _host; private readonly IUndoRedoHost _host;
public interface IUndoRedoHost public interface IUndoRedoHost
@ -23,7 +25,7 @@ namespace Avalonia.Controls.Utils
/// Maximum number of states this helper can store for undo/redo. /// Maximum number of states this helper can store for undo/redo.
/// If -1, no limit is imposed. /// If -1, no limit is imposed.
/// </summary> /// </summary>
public int Limit { get; set; } = 10; public int Limit { get; set; } = DefaultUndoLimit;
public bool CanUndo => _currentNode?.Previous != null; public bool CanUndo => _currentNode?.Previous != null;

20
src/Avalonia.Controls/VirtualizingPanel.cs

@ -34,7 +34,8 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets the items to display. /// Gets the items to display.
/// </summary> /// </summary>
protected IReadOnlyList<object?> Items => ItemsControl?.ItemsView ?? ItemsSourceView.Empty; protected IReadOnlyList<object?> Items => (IReadOnlyList<object?>?)ItemsControl?.ItemsView ??
Array.Empty<object?>();
/// <summary> /// <summary>
/// Gets the <see cref="ItemsControl"/> that the panel is displaying items for. /// Gets the <see cref="ItemsControl"/> that the panel is displaying items for.
@ -192,17 +193,13 @@ namespace Avalonia.Controls
throw new InvalidOperationException("The VirtualizingPanel is already attached to an ItemsControl"); throw new InvalidOperationException("The VirtualizingPanel is already attached to an ItemsControl");
ItemsControl = itemsControl; ItemsControl = itemsControl;
ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
ItemsControl.ItemsView.PostCollectionChanged += OnItemsControlItemsChanged; ItemsControl.ItemsView.PostCollectionChanged += OnItemsControlItemsChanged;
} }
internal void Detach() internal void Detach()
{ {
var itemsControl = EnsureItemsControl(); var itemsControl = EnsureItemsControl();
itemsControl.PropertyChanged -= OnItemsControlPropertyChanged;
itemsControl.ItemsView.PostCollectionChanged -= OnItemsControlItemsChanged; itemsControl.ItemsView.PostCollectionChanged -= OnItemsControlItemsChanged;
ItemsControl = null; ItemsControl = null;
Children.Clear(); Children.Clear();
} }
@ -216,20 +213,9 @@ namespace Avalonia.Controls
return ItemsControl; return ItemsControl;
} }
private protected virtual void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ItemsControl.ItemsViewProperty)
{
var (oldValue, newValue) = e.GetOldAndNewValue<ItemsSourceView>();
oldValue.PostCollectionChanged -= OnItemsControlItemsChanged;
Refresh();
newValue.PostCollectionChanged += OnItemsControlItemsChanged;
}
}
private void OnItemsControlItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) private void OnItemsControlItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
OnItemsChanged(_itemsControl?.ItemsView ?? ItemsSourceView.Empty, e); OnItemsChanged(Items, e);
} }
[DoesNotReturn] [DoesNotReturn]

5
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -459,7 +459,8 @@ namespace Avalonia.Controls
while (c is not null) while (c is not null)
{ {
if (!c.Bounds.IsDefault && c.TransformToVisual(this) is Matrix transform) if ((c.Bounds.Width != 0 || c.Bounds.Height != 0) &&
c.TransformToVisual(this) is Matrix transform)
{ {
viewport = new Rect(0, 0, c.Bounds.Width, c.Bounds.Height) viewport = new Rect(0, 0, c.Bounds.Width, c.Bounds.Height)
.TransformToAABB(transform); .TransformToAABB(transform);
@ -1078,7 +1079,7 @@ namespace Avalonia.Controls
// elements after the insertion point. // elements after the insertion point.
var elementCount = _elements.Count; var elementCount = _elements.Count;
var start = Math.Max(realizedIndex, 0); var start = Math.Max(realizedIndex, 0);
var newIndex = first + count; var newIndex = realizedIndex + count;
for (var i = start; i < elementCount; ++i) for (var i = start; i < elementCount; ++i)
{ {

29
src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs

@ -7,23 +7,18 @@ namespace Avalonia.Diagnostics.Controls
{ {
internal class FilterTextBox : TextBox, IStyleable internal class FilterTextBox : TextBox, IStyleable
{ {
public static readonly DirectProperty<FilterTextBox, bool> UseRegexFilterProperty = public static readonly StyledProperty<bool> UseRegexFilterProperty =
AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseRegexFilter), AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseRegexFilter),
o => o.UseRegexFilter, (o, v) => o.UseRegexFilter = v,
defaultBindingMode: BindingMode.TwoWay); defaultBindingMode: BindingMode.TwoWay);
public static readonly DirectProperty<FilterTextBox, bool> UseCaseSensitiveFilterProperty = public static readonly StyledProperty<bool> UseCaseSensitiveFilterProperty =
AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseCaseSensitiveFilter), AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseCaseSensitiveFilter),
o => o.UseCaseSensitiveFilter, (o, v) => o.UseCaseSensitiveFilter = v,
defaultBindingMode: BindingMode.TwoWay); defaultBindingMode: BindingMode.TwoWay);
public static readonly DirectProperty<FilterTextBox, bool> UseWholeWordFilterProperty = public static readonly StyledProperty<bool> UseWholeWordFilterProperty =
AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseWholeWordFilter), AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseWholeWordFilter),
o => o.UseWholeWordFilter, (o, v) => o.UseWholeWordFilter = v,
defaultBindingMode: BindingMode.TwoWay); defaultBindingMode: BindingMode.TwoWay);
private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter;
public FilterTextBox() public FilterTextBox()
{ {
Classes.Add("filter-text-box"); Classes.Add("filter-text-box");
@ -31,20 +26,20 @@ namespace Avalonia.Diagnostics.Controls
public bool UseRegexFilter public bool UseRegexFilter
{ {
get => _useRegexFilter; get => GetValue(UseRegexFilterProperty);
set => SetAndRaise(UseRegexFilterProperty, ref _useRegexFilter, value); set => SetValue(UseRegexFilterProperty, value);
} }
public bool UseCaseSensitiveFilter public bool UseCaseSensitiveFilter
{ {
get => _useCaseSensitiveFilter; get => GetValue(UseCaseSensitiveFilterProperty);
set => SetAndRaise(UseCaseSensitiveFilterProperty, ref _useCaseSensitiveFilter, value); set => SetValue(UseCaseSensitiveFilterProperty,value);
} }
public bool UseWholeWordFilter public bool UseWholeWordFilter
{ {
get => _useWholeWordFilter; get => GetValue(UseWholeWordFilterProperty);
set => SetAndRaise(UseWholeWordFilterProperty, ref _useWholeWordFilter, value); set => SetValue(UseWholeWordFilterProperty, value);
} }
Type IStyleable.StyleKey => typeof(TextBox); Type IStyleable.StyleKey => typeof(TextBox);

2
src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml

@ -40,7 +40,7 @@
BorderBrush="{DynamicResource ThemeControlMidBrush}" BorderBrush="{DynamicResource ThemeControlMidBrush}"
BorderThickness="0,0,0,1" BorderThickness="0,0,0,1"
FontFamily="/Assets/Fonts/SourceSansPro-Regular.ttf" FontFamily="/Assets/Fonts/SourceSansPro-Regular.ttf"
Items="{Binding History}"> ItemsSource="{Binding History}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">

6
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -124,7 +124,7 @@
</Grid> </Grid>
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled"> <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled">
<ItemsControl Items="{Binding AppliedStyles}" > <ItemsControl ItemsSource="{Binding AppliedStyles}" >
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border BorderThickness="0,0,0,1" BorderBrush="#6C6C6C" Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}"> <Border BorderThickness="0,0,0,1" BorderBrush="#6C6C6C" Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}">
@ -142,7 +142,7 @@
<TextBlock Grid.Row="0" Text="{Binding Name}" /> <TextBlock Grid.Row="0" Text="{Binding Name}" />
</Expander.Header> </Expander.Header>
<ItemsControl Margin="20,0,0,0" Grid.Row="1" Items="{Binding Setters}"> <ItemsControl Margin="20,0,0,0" Grid.Row="1" ItemsSource="{Binding Setters}">
<ItemsControl.Styles> <ItemsControl.Styles>
<Style Selector="TextBlock.property-name"> <Style Selector="TextBlock.property-name">
@ -253,7 +253,7 @@
</ScrollViewer> </ScrollViewer>
<Expander Header="Pseudo Classes" Grid.Row="2"> <Expander Header="Pseudo Classes" Grid.Row="2">
<ItemsControl Items="{Binding PseudoClasses}"> <ItemsControl ItemsSource="{Binding PseudoClasses}">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<WrapPanel /> <WrapPanel />

6
src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml

@ -45,7 +45,7 @@
UseWholeWordFilter="{Binding UseWholeWordFilter}" UseWholeWordFilter="{Binding UseWholeWordFilter}"
UseRegexFilter="{Binding UseRegexFilter}" /> UseRegexFilter="{Binding UseRegexFilter}" />
<TreeView Grid.Row="1" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" > <TreeView Grid.Row="1" ItemsSource="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" >
<TreeView.DataTemplates> <TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:EventTreeNodeBase" <TreeDataTemplate DataType="vm:EventTreeNodeBase"
ItemsSource="{Binding Children}"> ItemsSource="{Binding Children}">
@ -71,7 +71,7 @@
<Grid RowDefinitions="*,4,2*,Auto" Grid.Column="2"> <Grid RowDefinitions="*,4,2*,Auto" Grid.Column="2">
<ListBox Name="EventsList" Items="{Binding RecordedEvents}" <ListBox Name="EventsList" ItemsSource="{Binding RecordedEvents}"
SelectedItem="{Binding SelectedEvent, Mode=TwoWay}"> SelectedItem="{Binding SelectedEvent, Mode=TwoWay}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
@ -108,7 +108,7 @@
<DockPanel Grid.Row="2" LastChildFill="True"> <DockPanel Grid.Row="2" LastChildFill="True">
<TextBlock DockPanel.Dock="Top" FontSize="16" Text="Event chain:" /> <TextBlock DockPanel.Dock="Top" FontSize="16" Text="Event chain:" />
<ListBox Items="{Binding SelectedEvent.EventChain}"> <ListBox ItemsSource="{Binding SelectedEvent.EventChain}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<ListBoxItem Classes.handled="{Binding Handled}"> <ListBoxItem Classes.handled="{Binding Handled}">

2
src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs

@ -221,7 +221,7 @@ namespace Avalonia.Diagnostics.Views
return CreateControl<ComboBox>( return CreateControl<ComboBox>(
SelectingItemsControl.SelectedItemProperty, init: c => SelectingItemsControl.SelectedItemProperty, init: c =>
{ {
c.Items = Enum.GetValues(propertyType); c.ItemsSource = Enum.GetValues(propertyType);
}); });
var tb = CreateControl<CommitTextBox>( var tb = CreateControl<CommitTextBox>(

2
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml

@ -6,7 +6,7 @@
<Grid ColumnDefinitions="0.35*,4,0.65*"> <Grid ColumnDefinitions="0.35*,4,0.65*">
<TreeView Name="tree" <TreeView Name="tree"
BorderThickness="0" BorderThickness="0"
Items="{Binding Nodes}" ItemsSource="{Binding Nodes}"
SelectedItem="{Binding SelectedNode, Mode=TwoWay}"> SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
<TreeView.DataTemplates> <TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:TreeNode" <TreeDataTemplate DataType="vm:TreeNode"

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save