Browse Source

Merge branch 'master' into fixes/9997-nth-last-child-itemscontrol

pull/10055/head
Max Katz 3 years ago
committed by GitHub
parent
commit
5849167fff
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  2. 41
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  3. 3
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  4. 48
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  5. 10
      src/Avalonia.Base/Input/DataFormats.cs
  6. 25
      src/Avalonia.Base/Input/DataObject.cs
  7. 50
      src/Avalonia.Base/Input/DataObjectExtensions.cs
  8. 17
      src/Avalonia.Base/Input/IDataObject.cs
  9. 6
      src/Avalonia.Base/Layout/Layoutable.cs
  10. 2
      src/Avalonia.Base/Media/FormattedText.cs
  11. 4
      src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs
  12. 13
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  13. 21
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  14. 9
      src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
  15. 191
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  16. 25
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  17. 7
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  18. 47
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  19. 7
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  20. 7
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  21. 2
      src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs
  22. 4
      src/Avalonia.Base/Media/TextTrailingTrimming.cs
  23. 5
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  24. 9
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  25. 17
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  26. 2
      src/Avalonia.Base/Platform/Storage/PickerOptions.cs
  27. 12
      src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
  28. 2
      src/Avalonia.Base/StyledElement.cs
  29. 4
      src/Avalonia.Controls/Expander.cs
  30. 389
      src/Avalonia.Controls/SplitView/SplitView.cs
  31. 29
      src/Avalonia.Controls/SplitView/SplitViewDisplayMode.cs
  32. 18
      src/Avalonia.Controls/SplitView/SplitViewPanePlacement.cs
  33. 32
      src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs
  34. 14
      src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs
  35. 41
      src/Avalonia.Native/ClipboardImpl.cs
  36. 55
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  37. 91
      src/Browser/Avalonia.Browser/BrowserDataObject.cs
  38. 9
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  39. 2
      src/Browser/Avalonia.Browser/ClipboardImpl.cs
  40. 21
      src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs
  41. 22
      src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs
  42. 5
      src/Browser/Avalonia.Browser/Interop/InputHelper.cs
  43. 3
      src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
  44. 16
      src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
  45. 4
      src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
  46. 19
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts
  47. 22
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts
  48. 42
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts
  49. 8
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts
  50. 3
      src/Windows/Avalonia.Win32/ClipboardFormats.cs
  51. 15
      src/Windows/Avalonia.Win32/DataObject.cs
  52. 18
      src/Windows/Avalonia.Win32/OleDataObject.cs
  53. 40
      tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs
  54. 12
      tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs
  55. 2
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  56. 2
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

8
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -238,7 +238,7 @@
-(BOOL)canBecomeKeyWindow
{
if(_canBecomeKeyWindow)
if(_canBecomeKeyWindow && !_closed)
{
// If the window has a child window being shown as a dialog then don't allow it to become the key window.
auto parent = dynamic_cast<WindowImpl*>(_parent.getRaw());
@ -292,12 +292,14 @@
{
if (_parent == nullptr)
return;
_parent->BringToFront();
dispatch_async(dispatch_get_main_queue(), ^{
@try {
[self invalidateShadow];
[self invalidateShadow];
if (self->_parent != nullptr)
self->_parent->BringToFront();
}
@finally{
}

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

@ -306,25 +306,8 @@ namespace ControlCatalog.Pages
resultText += @$"
Content:
";
#if NET6_0_OR_GREATER
await using var stream = await file.OpenReadAsync();
#else
using var stream = await file.OpenReadAsync();
#endif
using var reader = new System.IO.StreamReader(stream);
// 4GB file test, shouldn't load more than 10000 chars into a memory.
const int length = 10000;
var buffer = ArrayPool<char>.Shared.Rent(length);
try
{
var charsRead = await reader.ReadAsync(buffer, 0, length);
resultText += new string(buffer, 0, charsRead);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
resultText += await ReadTextFromFile(file, 10000);
}
openedFileContent.Text = resultText;
@ -354,6 +337,28 @@ namespace ControlCatalog.Pages
}
}
public static async Task<string> ReadTextFromFile(IStorageFile file, int length)
{
#if NET6_0_OR_GREATER
await using var stream = await file.OpenReadAsync();
#else
using var stream = await file.OpenReadAsync();
#endif
using var reader = new System.IO.StreamReader(stream);
// 4GB file test, shouldn't load more than 10000 chars into a memory.
var buffer = ArrayPool<char>.Shared.Rent(length);
try
{
var charsRead = await reader.ReadAsync(buffer, 0, length);
return new string(buffer, 0, charsRead);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);

3
samples/ControlCatalog/Pages/DragAndDropPage.xaml

@ -25,7 +25,6 @@
BorderThickness="2">
<TextBlock Name="DragStateCustom" TextWrapping="Wrap">Drag Me (custom)</TextBlock>
</Border>
<TextBlock Name="DropState" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Margin="8"
@ -47,5 +46,7 @@
</Border>
</StackPanel>
</WrapPanel>
<TextBlock x:Name="DropState" TextWrapping="Wrap" />
</StackPanel>
</UserControl>

48
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@ -1,27 +1,29 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
namespace ControlCatalog.Pages
{
public class DragAndDropPage : UserControl
{
TextBlock _DropState;
private readonly TextBlock _dropState;
private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom";
public DragAndDropPage()
{
this.InitializeComponent();
_DropState = this.Get<TextBlock>("DropState");
_dropState = this.Get<TextBlock>("DropState");
int textCount = 0;
SetupDnd("Text", d => d.Set(DataFormats.Text,
$"Text was dragged {++textCount} times"), DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link);
SetupDnd("Custom", d => d.Set(CustomFormat, "Test123"), DragDropEffects.Move);
SetupDnd("Files", d => d.Set(DataFormats.FileNames, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy);
SetupDnd("Files", d => d.Set(DataFormats.Files, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy);
}
void SetupDnd(string suffix, Action<DataObject> factory, DragDropEffects effects)
@ -68,12 +70,12 @@ namespace ControlCatalog.Pages
// Only allow if the dragged data contains text or filenames.
if (!e.Data.Contains(DataFormats.Text)
&& !e.Data.Contains(DataFormats.FileNames)
&& !e.Data.Contains(DataFormats.Files)
&& !e.Data.Contains(CustomFormat))
e.DragEffects = DragDropEffects.None;
}
void Drop(object? sender, DragEventArgs e)
async void Drop(object? sender, DragEventArgs e)
{
if (e.Source is Control c && c.Name == "MoveTarget")
{
@ -85,11 +87,41 @@ namespace ControlCatalog.Pages
}
if (e.Data.Contains(DataFormats.Text))
_DropState.Text = e.Data.GetText();
{
_dropState.Text = e.Data.GetText();
}
else if (e.Data.Contains(DataFormats.Files))
{
var files = e.Data.GetFiles() ?? Array.Empty<IStorageItem>();
var contentStr = "";
foreach (var item in files)
{
if (item is IStorageFile file)
{
var content = await DialogsPage.ReadTextFromFile(file, 1000);
contentStr += $"File {item.Name}:{Environment.NewLine}{content}{Environment.NewLine}{Environment.NewLine}";
}
else if (item is IStorageFolder folder)
{
var items = await folder.GetItemsAsync();
contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}";
}
}
_dropState.Text = contentStr;
}
#pragma warning disable CS0618 // Type or member is obsolete
else if (e.Data.Contains(DataFormats.FileNames))
_DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames() ?? Array.Empty<string>());
{
var files = e.Data.GetFileNames();
_dropState.Text = string.Join(Environment.NewLine, files ?? Array.Empty<string>());
}
#pragma warning restore CS0618 // Type or member is obsolete
else if (e.Data.Contains(CustomFormat))
_DropState.Text = "Custom: " + e.Data.Get(CustomFormat);
{
_dropState.Text = "Custom: " + e.Data.Get(CustomFormat);
}
}
dragMe.PointerPressed += DoDrag;

10
src/Avalonia.Base/Input/DataFormats.cs

@ -1,4 +1,6 @@
namespace Avalonia.Input
using System;
namespace Avalonia.Input
{
public static class DataFormats
{
@ -7,9 +9,15 @@
/// </summary>
public static readonly string Text = nameof(Text);
/// <summary>
/// Dataformat for one or more files.
/// </summary>
public static readonly string Files = nameof(Files);
/// <summary>
/// Dataformat for one or more filenames
/// </summary>
[Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms.")]
public static readonly string FileNames = nameof(FileNames);
}
}

25
src/Avalonia.Base/Input/DataObject.cs

@ -2,37 +2,34 @@
namespace Avalonia.Input
{
/// <summary>
/// Specific and mutable implementation of the IDataObject interface.
/// </summary>
public class DataObject : IDataObject
{
private readonly Dictionary<string, object> _items = new Dictionary<string, object>();
private readonly Dictionary<string, object> _items = new();
/// <inheritdoc />
public bool Contains(string dataFormat)
{
return _items.ContainsKey(dataFormat);
}
/// <inheritdoc />
public object? Get(string dataFormat)
{
if (_items.ContainsKey(dataFormat))
return _items[dataFormat];
return null;
return _items.TryGetValue(dataFormat, out var item) ? item : null;
}
/// <inheritdoc />
public IEnumerable<string> GetDataFormats()
{
return _items.Keys;
}
public IEnumerable<string>? GetFileNames()
{
return Get(DataFormats.FileNames) as IEnumerable<string>;
}
public string? GetText()
{
return Get(DataFormats.Text) as string;
}
/// <summary>
/// Sets a value to the internal store of the data object with <see cref="DataFormats"/> as a key.
/// </summary>
public void Set(string dataFormat, object value)
{
_items[dataFormat] = value;

50
src/Avalonia.Base/Input/DataObjectExtensions.cs

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform.Storage;
namespace Avalonia.Input
{
public static class DataObjectExtensions
{
/// <summary>
/// Returns a list of files if the DataObject contains files or filenames.
/// <seealso cref="DataFormats.Files"/>.
/// </summary>
/// <returns>
/// Collection of storage items - files or folders. If format isn't available, returns null.
/// </returns>
public static IEnumerable<IStorageItem>? GetFiles(this IDataObject dataObject)
{
return dataObject.Get(DataFormats.Files) as IEnumerable<IStorageItem>;
}
/// <summary>
/// Returns a list of filenames if the DataObject contains filenames.
/// <seealso cref="DataFormats.FileNames"/>
/// </summary>
/// <returns>
/// Collection of file names. If format isn't available, returns null.
/// </returns>
[System.Obsolete("Use GetFiles, this method is supported only on desktop platforms.")]
public static IEnumerable<string>? GetFileNames(this IDataObject dataObject)
{
return (dataObject.Get(DataFormats.FileNames) as IEnumerable<string>)
?? dataObject.GetFiles()?
.Select(f => f.TryGetLocalPath())
.Where(p => !string.IsNullOrEmpty(p))
.OfType<string>();
}
/// <summary>
/// Returns the dragged text if the DataObject contains any text.
/// <seealso cref="DataFormats.Text"/>
/// </summary>
/// <returns>
/// A text string. If format isn't available, returns null.
/// </returns>
public static string? GetText(this IDataObject dataObject)
{
return dataObject.Get(DataFormats.Text) as string;
}
}
}

17
src/Avalonia.Base/Input/IDataObject.cs

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform.Storage;
namespace Avalonia.Input
{
@ -19,21 +21,12 @@ namespace Avalonia.Input
/// </summary>
bool Contains(string dataFormat);
/// <summary>
/// Returns the dragged text if the DataObject contains any text.
/// <seealso cref="DataFormats.Text"/>
/// </summary>
string? GetText();
/// <summary>
/// Returns a list of filenames if the DataObject contains filenames.
/// <seealso cref="DataFormats.FileNames"/>
/// </summary>
IEnumerable<string>? GetFileNames();
/// <summary>
/// Tries to get the data of the given DataFormat.
/// </summary>
/// <returns>
/// Object data. If format isn't available, returns null.
/// </returns>
object? Get(string dataFormat);
}
}

6
src/Avalonia.Base/Layout/Layoutable.cs

@ -798,6 +798,12 @@ namespace Avalonia.Layout
InvalidateMeasure();
}
internal override void OnTemplatedParentControlThemeChanged()
{
base.OnTemplatedParentControlThemeChanged();
InvalidateMeasure();
}
/// <summary>
/// Called when the layout manager raises a LayoutUpdated event.
/// </summary>

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

@ -877,7 +877,7 @@ namespace Avalonia.Media
var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps));
TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps, paraProps.FlowDirection));
var collapsedLine = line.Collapse(collapsingProperties);

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

@ -6,11 +6,13 @@ namespace Avalonia.Media
{
public readonly double Width;
public readonly TextRunProperties TextRunProperties;
public readonly FlowDirection FlowDirection;
public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties)
public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties, FlowDirection flowDirection)
{
Width = width;
TextRunProperties = textRunProperties;
FlowDirection = flowDirection;
}
}
}

13
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@ -27,16 +27,6 @@ namespace Avalonia.Media.TextFormatting
return;
}
if (lineImpl.NewLineLength > 0)
{
return;
}
if (lineImpl.TextLineBreak is { TextEndOfLine: not null, IsSplit: false })
{
return;
}
var breakOportunities = new Queue<int>();
var currentPosition = textLine.FirstTextSourceIndex;
@ -97,7 +87,8 @@ namespace Avalonia.Media.TextFormatting
continue;
}
var glyphIndex = glyphRun.FindGlyphIndex(characterIndex);
var offset = Math.Max(0, currentPosition - glyphRun.Metrics.FirstCluster);
var glyphIndex = glyphRun.FindGlyphIndex(characterIndex - offset);
var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex];
shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex,

21
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@ -148,33 +148,38 @@ namespace Avalonia.Media.TextFormatting
internal SplitResult<ShapedTextRun> Split(int length)
{
if (IsReversed)
var isReversed = IsReversed;
if (isReversed)
{
Reverse();
}
length = Length - length;
}
#if DEBUG
if(length == 0)
if (length == 0)
{
throw new ArgumentOutOfRangeException(nameof(length), "length must be greater than zero.");
}
#endif
#endif
var splitBuffer = ShapedBuffer.Split(length);
var first = new ShapedTextRun(splitBuffer.First, Properties);
#if DEBUG
#if DEBUG
if (first.Length != length)
{
throw new InvalidOperationException("Split length mismatch.");
}
#endif
var second = new ShapedTextRun(splitBuffer.Second!, Properties);
if (isReversed)
{
return new SplitResult<ShapedTextRun>(second, first);
}
return new SplitResult<ShapedTextRun>(first, second);
}

9
src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs

@ -1,4 +1,6 @@
namespace Avalonia.Media.TextFormatting
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Properties of text collapsing.
@ -15,6 +17,11 @@
/// </summary>
public abstract TextRun Symbol { get; }
/// <summary>
/// Gets the flow direction that is used for collapsing.
/// </summary>
public abstract FlowDirection FlowDirection { get; }
/// <summary>
/// Collapses given text line.
/// </summary>

191
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Media.TextFormatting
@ -28,97 +27,191 @@ namespace Avalonia.Media.TextFormatting
var availableWidth = properties.Width - shapedSymbol.Size.Width;
while (runIndex < textRuns.Count)
if(properties.FlowDirection== FlowDirection.LeftToRight)
{
var currentRun = textRuns[runIndex];
switch (currentRun)
while (runIndex < textRuns.Count)
{
case ShapedTextRun shapedRun:
{
currentWidth += shapedRun.Size.Width;
var currentRun = textRuns[runIndex];
if (currentWidth > availableWidth)
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
currentWidth += shapedRun.Size.Width;
if (currentWidth > availableWidth)
{
if (isWordEllipsis && measuredLength < textLine.Length)
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
if (isWordEllipsis && measuredLength < textLine.Length)
{
var nextBreakPosition = lineBreak.PositionMeasure;
var currentBreakPosition = 0;
if (nextBreakPosition == 0)
{
break;
}
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
if (nextBreakPosition >= measuredLength)
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
{
break;
var nextBreakPosition = lineBreak.PositionMeasure;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition >= measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
currentBreakPosition = nextBreakPosition;
measuredLength = currentBreakPosition;
}
measuredLength = currentBreakPosition;
}
collapsedLength += measuredLength;
return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol);
}
collapsedLength += measuredLength;
availableWidth -= shapedRun.Size.Width;
return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
break;
}
availableWidth -= shapedRun.Size.Width;
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol);
}
break;
}
availableWidth -= drawableRun.Size.Width;
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
break;
}
}
availableWidth -= drawableRun.Size.Width;
collapsedLength += currentRun.Length;
break;
}
runIndex++;
}
}
else
{
runIndex = textRuns.Count - 1;
while (runIndex >= 0)
{
var currentRun = textRuns[runIndex];
collapsedLength += currentRun.Length;
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
currentWidth += shapedRun.Size.Width;
runIndex++;
}
if (currentWidth > availableWidth)
{
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
if (isWordEllipsis && measuredLength < textLine.Length)
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
{
var nextBreakPosition = lineBreak.PositionMeasure;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition >= measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
measuredLength = currentBreakPosition;
}
}
collapsedLength += measuredLength;
return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol);
}
availableWidth -= shapedRun.Size.Width;
break;
}
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol);
}
availableWidth -= drawableRun.Size.Width;
break;
}
}
collapsedLength += currentRun.Length;
runIndex--;
}
}
return null;
}
private static TextRun[] CreateCollapsedRuns(IReadOnlyList<TextRun> textRuns, int collapsedLength,
TextRun shapedSymbol)
private static TextRun[] CreateCollapsedRuns(TextLine textLine, int collapsedLength,
FlowDirection flowDirection, TextRun shapedSymbol)
{
var textRuns = textLine.TextRuns;
if (collapsedLength <= 0)
{
return new[] { shapedSymbol };
}
if(flowDirection == FlowDirection.RightToLeft)
{
collapsedLength = textLine.Length - collapsedLength;
}
var objectPool = FormattingObjectPool.Instance;
var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool);
try
{
var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
return collapsedRuns;
if (flowDirection == FlowDirection.RightToLeft)
{
var collapsedRuns = new TextRun[postSplitRuns!.Count + 1];
postSplitRuns.CopyTo(collapsedRuns, 1);
collapsedRuns[0] = shapedSymbol;
return collapsedRuns;
}
else
{
var collapsedRuns = new TextRun[preSplitRuns!.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
return collapsedRuns;
}
}
finally
{

25
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -352,7 +352,7 @@ namespace Avalonia.Media.TextFormatting
var lastTrailingIndex = 0;
if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight)
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{
lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
@ -377,7 +377,7 @@ namespace Avalonia.Media.TextFormatting
{
lastTrailingIndex += textEndOfLine.Length;
}
}
}
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
@ -553,26 +553,18 @@ namespace Avalonia.Media.TextFormatting
if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{
var whitespaceWidth = 0d;
var justificationWidth = MaxWidth;
for (var i = 0; i < textLines.Count; i++)
if (_paragraphProperties.TextWrapping != TextWrapping.NoWrap)
{
var line = textLines[i];
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
if (lineWhitespaceWidth > whitespaceWidth)
{
whitespaceWidth = lineWhitespaceWidth;
}
justificationWidth = width;
}
var justificationWidth = width - whitespaceWidth;
if (justificationWidth > 0)
{
var justificationProperties = new InterWordJustification(justificationWidth);
for (var i = 0; i < textLines.Count - 1; i++)
for (var i = 0; i < textLines.Count; i++)
{
var line = textLines[i];
@ -597,12 +589,13 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The <see cref="TextCollapsingProperties"/>.</returns>
private TextCollapsingProperties? GetCollapsingProperties(double width)
{
if(_textTrimming == TextTrimming.None)
if (_textTrimming == TextTrimming.None)
{
return null;
}
return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties));
return _textTrimming.CreateCollapsingProperties(
new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties, _paragraphProperties.FlowDirection));
}
public void Dispose()

7
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -19,11 +19,13 @@ namespace Avalonia.Media.TextFormatting
/// <param name="prefixLength">Length of leading prefix.</param>
/// <param name="width">width in which collapsing is constrained to</param>
/// <param name="textRunProperties">text run properties of ellipsis symbol</param>
/// <param name="flowDirection">the flow direction of the collapes line.</param>
public TextLeadingPrefixCharacterEllipsis(
string ellipsis,
int prefixLength,
double width,
TextRunProperties textRunProperties)
TextRunProperties textRunProperties,
FlowDirection flowDirection)
{
if (_prefixLength < 0)
{
@ -33,6 +35,7 @@ namespace Avalonia.Media.TextFormatting
_prefixLength = prefixLength;
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);
FlowDirection = flowDirection;
}
/// <inheritdoc/>
@ -41,6 +44,8 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override FlowDirection FlowDirection { get; }
/// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{

47
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -658,7 +658,7 @@ namespace Avalonia.Media.TextFormatting
currentX += drawableTextRun.Size.Width;
}
if(lastRunIndex - 1 < 0)
if (lastRunIndex - 1 < 0)
{
break;
}
@ -685,7 +685,7 @@ namespace Avalonia.Media.TextFormatting
directionalWidth -= drawableTextRun.Size.Width;
}
if(firstRunIndex + 1 == _textRuns.Length)
if (firstRunIndex + 1 == _textRuns.Length)
{
break;
}
@ -1097,7 +1097,7 @@ namespace Avalonia.Media.TextFormatting
var runWidth = endX - startX;
return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
}
public override void Dispose()
@ -1439,13 +1439,6 @@ namespace Avalonia.Media.TextFormatting
}
}
if (index == lastRunIndex)
{
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength += textRun.GlyphRun.Metrics.NewLineLength;
}
widthIncludingWhitespace += textRun.Size.Width;
break;
@ -1455,12 +1448,6 @@ namespace Avalonia.Media.TextFormatting
{
widthIncludingWhitespace += drawableTextRun.Size.Width;
if (index == lastRunIndex)
{
width = widthIncludingWhitespace;
trailingWhitespaceLength = 0;
}
if (drawableTextRun.Size.Height > height)
{
height = drawableTextRun.Size.Height;
@ -1476,6 +1463,32 @@ namespace Avalonia.Media.TextFormatting
}
}
width = widthIncludingWhitespace;
for (var i = _textRuns.Length - 1; i >= 0; i--)
{
var currentRun = _textRuns[i];
if(currentRun is ShapedTextRun shapedText)
{
var glyphRun = shapedText.GlyphRun;
var glyphRunMetrics = glyphRun.Metrics;
newLineLength += glyphRunMetrics.NewLineLength;
if (glyphRunMetrics.TrailingWhitespaceLength == 0)
{
break;
}
trailingWhitespaceLength += glyphRunMetrics.TrailingWhitespaceLength;
var whitespaceWidth = glyphRun.Size.Width - glyphRunMetrics.Width;
width -= whitespaceWidth;
}
}
var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
@ -1543,7 +1556,7 @@ namespace Avalonia.Media.TextFormatting
return Math.Max(0, start);
case TextAlignment.Right:
return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);
return Math.Max(0, _paragraphWidth - width);
default:
return 0;

7
src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@ -12,10 +12,13 @@
/// <param name="ellipsis">Text used as collapsing symbol.</param>
/// <param name="width">Width in which collapsing is constrained to.</param>
/// <param name="textRunProperties">Text run properties of ellipsis symbol.</param>
public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties)
/// <param name="flowDirection">The flow direction of the collapsed line.</param>
public TextTrailingCharacterEllipsis(string ellipsis, double width,
TextRunProperties textRunProperties, FlowDirection flowDirection)
{
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);
FlowDirection = flowDirection;
}
/// <inheritdoc/>
@ -24,6 +27,8 @@
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override FlowDirection FlowDirection { get; }
/// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{

7
src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs

@ -12,14 +12,17 @@
/// <param name="ellipsis">Text used as collapsing symbol.</param>
/// <param name="width">width in which collapsing is constrained to.</param>
/// <param name="textRunProperties">text run properties of ellipsis symbol.</param>
/// <param name="flowDirection">flow direction of the collapsed line.</param>
public TextTrailingWordEllipsis(
string ellipsis,
double width,
TextRunProperties textRunProperties
TextRunProperties textRunProperties,
FlowDirection flowDirection
)
{
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);
FlowDirection = flowDirection;
}
/// <inheritdoc/>
@ -28,6 +31,8 @@
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override FlowDirection FlowDirection { get; }
/// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{

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

@ -15,7 +15,7 @@ namespace Avalonia.Media
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)
{
return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties);
return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection);
}
public override string ToString()

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

@ -17,10 +17,10 @@ namespace Avalonia.Media
{
if (_isWordBased)
{
return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties);
return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection);
}
return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties);
return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection);
}
public override string ToString()

5
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs

@ -7,11 +7,6 @@ namespace Avalonia.Platform.Storage.FileIO;
internal class BclStorageFile : IStorageBookmarkFile
{
public BclStorageFile(string fileName)
{
FileInfo = new FileInfo(fileName);
}
public BclStorageFile(FileInfo fileInfo)
{
FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo));

9
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs

@ -9,15 +9,6 @@ namespace Avalonia.Platform.Storage.FileIO;
internal class BclStorageFolder : IStorageBookmarkFolder
{
public BclStorageFolder(string path)
{
DirectoryInfo = new DirectoryInfo(path);
if (!DirectoryInfo.Exists)
{
throw new ArgumentException("Directory must exist");
}
}
public BclStorageFolder(DirectoryInfo directoryInfo)
{
DirectoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo));

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

@ -7,6 +7,23 @@ namespace Avalonia.Platform.Storage.FileIO;
internal static class StorageProviderHelpers
{
public static IStorageItem? TryCreateBclStorageItem(string path)
{
var directory = new DirectoryInfo(path);
if (directory.Exists)
{
return new BclStorageFolder(directory);
}
var file = new FileInfo(path);
if (file.Exists)
{
return new BclStorageFile(file);
}
return null;
}
public static Uri FilePathToUri(string path)
{
var uriPath = new StringBuilder(path)

2
src/Avalonia.Base/Platform/Storage/PickerOptions.cs

@ -12,6 +12,8 @@ public class PickerOptions
/// <summary>
/// Gets or sets the initial location where the file open picker looks for files to present to the user.
/// Can be obtained from previously picked folder or using <see cref="IStorageProvider.TryGetFolderFromPathAsync"/>
/// or <see cref="IStorageProvider.TryGetWellKnownFolderAsync"/>.
/// </summary>
public IStorageFolder? SuggestedStartLocation { get; set; }
}

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

@ -11,12 +11,24 @@ public static class StorageProviderExtensions
/// <inheritdoc cref="IStorageProvider.TryGetFileFromPathAsync"/>
public static Task<IStorageFile?> TryGetFileFromPathAsync(this IStorageProvider provider, string filePath)
{
// We can avoid double escaping of the path by checking for BclStorageProvider.
if (provider is BclStorageProvider)
{
return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile);
}
return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath));
}
/// <inheritdoc cref="IStorageProvider.TryGetFolderFromPathAsync"/>
public static Task<IStorageFolder?> TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath)
{
// We can avoid double escaping of the path by checking for BclStorageProvider.
if (provider is BclStorageProvider)
{
return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder);
}
return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath));
}

2
src/Avalonia.Base/StyledElement.cs

@ -374,7 +374,7 @@ namespace Avalonia
/// </returns>
public bool ApplyStyling()
{
if (_initCount == 0 && (!_stylesApplied || !_themeApplied))
if (_initCount == 0 && (!_stylesApplied || !_themeApplied || !_templatedParentThemeApplied))
{
GetValueStore().BeginStyling();

4
src/Avalonia.Controls/Expander.cs

@ -191,7 +191,7 @@ namespace Avalonia.Controls
/// <summary>
/// Invoked just before the <see cref="Collapsing"/> event.
/// </summary>
protected virtual void OnCollapsing(RoutedEventArgs eventArgs)
protected virtual void OnCollapsing(CancelRoutedEventArgs eventArgs)
{
RaiseEvent(eventArgs);
}
@ -207,7 +207,7 @@ namespace Avalonia.Controls
/// <summary>
/// Invoked just before the <see cref="Expanding"/> event.
/// </summary>
protected virtual void OnExpanding(RoutedEventArgs eventArgs)
protected virtual void OnExpanding(CancelRoutedEventArgs eventArgs)
{
RaiseEvent(eventArgs);
}

389
src/Avalonia.Controls/SplitView.cs → src/Avalonia.Controls/SplitView/SplitView.cs

@ -1,77 +1,16 @@
using Avalonia.Controls.Metadata;
using System;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.VisualTree;
using System;
using Avalonia.Reactive;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
namespace Avalonia.Controls
{
/// <summary>
/// Defines constants for how the SplitView Pane should display
/// </summary>
public enum SplitViewDisplayMode
{
/// <summary>
/// Pane is displayed next to content, and does not auto collapse
/// when tapped outside
/// </summary>
Inline,
/// <summary>
/// Pane is displayed next to content. When collapsed, pane is still
/// visible according to CompactPaneLength. Pane does not auto collapse
/// when tapped outside
/// </summary>
CompactInline,
/// <summary>
/// Pane is displayed above content. Pane collapses when tapped outside
/// </summary>
Overlay,
/// <summary>
/// Pane is displayed above content. When collapsed, pane is still
/// visible according to CompactPaneLength. Pane collapses when tapped outside
/// </summary>
CompactOverlay
}
/// <summary>
/// Defines constants for where the Pane should appear
/// </summary>
public enum SplitViewPanePlacement
{
Left,
Right
}
public class SplitViewTemplateSettings : AvaloniaObject
{
internal SplitViewTemplateSettings() { }
public static readonly StyledProperty<double> ClosedPaneWidthProperty =
AvaloniaProperty.Register<SplitViewTemplateSettings, double>(nameof(ClosedPaneWidth), 0d);
public static readonly StyledProperty<GridLength> PaneColumnGridLengthProperty =
AvaloniaProperty.Register<SplitViewTemplateSettings, GridLength>(nameof(PaneColumnGridLength));
public double ClosedPaneWidth
{
get => GetValue(ClosedPaneWidthProperty);
internal set => SetValue(ClosedPaneWidthProperty, value);
}
public GridLength PaneColumnGridLength
{
get => GetValue(PaneColumnGridLengthProperty);
internal set => SetValue(PaneColumnGridLengthProperty, value);
}
}
/// <summary>
/// A control with two views: A collapsible pane and an area for content
/// </summary>
@ -93,26 +32,34 @@ namespace Avalonia.Controls
/// Defines the <see cref="CompactPaneLength"/> property
/// </summary>
public static readonly StyledProperty<double> CompactPaneLengthProperty =
AvaloniaProperty.Register<SplitView, double>(nameof(CompactPaneLength), defaultValue: 48);
AvaloniaProperty.Register<SplitView, double>(
nameof(CompactPaneLength),
defaultValue: 48);
/// <summary>
/// Defines the <see cref="DisplayMode"/> property
/// </summary>
public static readonly StyledProperty<SplitViewDisplayMode> DisplayModeProperty =
AvaloniaProperty.Register<SplitView, SplitViewDisplayMode>(nameof(DisplayMode), defaultValue: SplitViewDisplayMode.Overlay);
AvaloniaProperty.Register<SplitView, SplitViewDisplayMode>(
nameof(DisplayMode),
defaultValue: SplitViewDisplayMode.Overlay);
/// <summary>
/// Defines the <see cref="IsPaneOpen"/> property
/// </summary>
public static readonly DirectProperty<SplitView, bool> IsPaneOpenProperty =
AvaloniaProperty.RegisterDirect<SplitView, bool>(nameof(IsPaneOpen),
x => x.IsPaneOpen, (x, v) => x.IsPaneOpen = v);
public static readonly StyledProperty<bool> IsPaneOpenProperty =
AvaloniaProperty.Register<SplitView, bool>(
nameof(IsPaneOpen),
defaultValue: false,
coerce: CoerceIsPaneOpen);
/// <summary>
/// Defines the <see cref="OpenPaneLength"/> property
/// </summary>
public static readonly StyledProperty<double> OpenPaneLengthProperty =
AvaloniaProperty.Register<SplitView, double>(nameof(OpenPaneLength), defaultValue: 320);
AvaloniaProperty.Register<SplitView, double>(
nameof(OpenPaneLength),
defaultValue: 320);
/// <summary>
/// Defines the <see cref="PaneBackground"/> property
@ -150,7 +97,38 @@ namespace Avalonia.Controls
public static readonly StyledProperty<SplitViewTemplateSettings> TemplateSettingsProperty =
AvaloniaProperty.Register<SplitView, SplitViewTemplateSettings>(nameof(TemplateSettings));
private bool _isPaneOpen;
/// <summary>
/// Defines the <see cref="PaneClosed"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> PaneClosedEvent =
RoutedEvent.Register<SplitView, RoutedEventArgs>(
nameof(PaneClosed),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PaneClosing"/> event.
/// </summary>
public static readonly RoutedEvent<CancelRoutedEventArgs> PaneClosingEvent =
RoutedEvent.Register<SplitView, CancelRoutedEventArgs>(
nameof(PaneClosing),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PaneOpened"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> PaneOpenedEvent =
RoutedEvent.Register<SplitView, RoutedEventArgs>(
nameof(PaneOpened),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PaneOpening"/> event.
/// </summary>
public static readonly RoutedEvent<CancelRoutedEventArgs> PaneOpeningEvent =
RoutedEvent.Register<SplitView, CancelRoutedEventArgs>(
nameof(PaneOpening),
RoutingStrategies.Bubble);
private Panel? _pane;
private IDisposable? _pointerDisposable;
@ -164,12 +142,6 @@ namespace Avalonia.Controls
static SplitView()
{
UseLightDismissOverlayModeProperty.Changed.AddClassHandler<SplitView>((x, v) => x.OnUseLightDismissChanged(v));
CompactPaneLengthProperty.Changed.AddClassHandler<SplitView>((x, v) => x.OnCompactPaneLengthChanged(v));
PanePlacementProperty.Changed.AddClassHandler<SplitView>((x, v) => x.OnPanePlacementChanged(v));
DisplayModeProperty.Changed.AddClassHandler<SplitView>((x, v) => x.OnDisplayModeChanged(v));
PaneProperty.Changed.AddClassHandler<SplitView>((x, e) => x.PaneChanged(e));
}
/// <summary>
@ -196,37 +168,8 @@ namespace Avalonia.Controls
/// </summary>
public bool IsPaneOpen
{
get => _isPaneOpen;
set
{
if (value == _isPaneOpen)
{
return;
}
if (value)
{
OnPaneOpening(this, EventArgs.Empty);
SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value);
PseudoClasses.Add(":open");
PseudoClasses.Remove(":closed");
OnPaneOpened(this, EventArgs.Empty);
}
else
{
SplitViewPaneClosingEventArgs args = new SplitViewPaneClosingEventArgs(false);
OnPaneClosing(this, args);
if (!args.Cancel)
{
SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value);
PseudoClasses.Add(":closed");
PseudoClasses.Remove(":open");
OnPaneClosed(this, EventArgs.Empty);
}
}
}
get => GetValue(IsPaneOpenProperty);
set => SetValue(IsPaneOpenProperty, value);
}
/// <summary>
@ -297,24 +240,48 @@ namespace Avalonia.Controls
}
/// <summary>
/// Fired when the pane is closed
/// Fired when the pane is closed.
/// </summary>
public event EventHandler<EventArgs>? PaneClosed;
public event EventHandler<RoutedEventArgs>? PaneClosed
{
add => AddHandler(PaneClosedEvent, value);
remove => RemoveHandler(PaneClosedEvent, value);
}
/// <summary>
/// Fired when the pane is closing
/// Fired when the pane is closing.
/// </summary>
public event EventHandler<SplitViewPaneClosingEventArgs>? PaneClosing;
/// <remarks>
/// The event args <see cref="CancelRoutedEventArgs.Cancel"/> property may be set to true to cancel the event
/// and keep the pane open.
/// </remarks>
public event EventHandler<CancelRoutedEventArgs>? PaneClosing
{
add => AddHandler(PaneClosingEvent, value);
remove => RemoveHandler(PaneClosingEvent, value);
}
/// <summary>
/// Fired when the pane is opened
/// Fired when the pane is opened.
/// </summary>
public event EventHandler<EventArgs>? PaneOpened;
public event EventHandler<RoutedEventArgs>? PaneOpened
{
add => AddHandler(PaneOpenedEvent, value);
remove => RemoveHandler(PaneOpenedEvent, value);
}
/// <summary>
/// Fired when the pane is opening
/// Fired when the pane is opening.
/// </summary>
public event EventHandler<EventArgs>? PaneOpening;
/// <remarks>
/// The event args <see cref="CancelRoutedEventArgs.Cancel"/> property may be set to true to cancel the event
/// and keep the pane closed.
/// </remarks>
public event EventHandler<CancelRoutedEventArgs>? PaneOpening
{
add => AddHandler(PaneOpeningEvent, value);
remove => RemoveHandler(PaneOpeningEvent, value);
}
protected override bool RegisterContentPresenter(IContentPresenter presenter)
{
@ -351,6 +318,89 @@ namespace Avalonia.Controls
_pointerDisposable?.Dispose();
}
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == CompactPaneLengthProperty)
{
var newLen = change.GetNewValue<double>();
var displayMode = DisplayMode;
if (displayMode == SplitViewDisplayMode.CompactInline)
{
TemplateSettings.ClosedPaneWidth = newLen;
}
else if (displayMode == SplitViewDisplayMode.CompactOverlay)
{
TemplateSettings.ClosedPaneWidth = newLen;
TemplateSettings.PaneColumnGridLength = new GridLength(newLen, GridUnitType.Pixel);
}
}
else if (change.Property == DisplayModeProperty)
{
var oldState = GetPseudoClass(change.GetOldValue<SplitViewDisplayMode>());
var newState = GetPseudoClass(change.GetNewValue<SplitViewDisplayMode>());
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
var (closedPaneWidth, paneColumnGridLength) = change.GetNewValue<SplitViewDisplayMode>() switch
{
SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)),
SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)),
SplitViewDisplayMode.Inline => (0, new GridLength(0, GridUnitType.Auto)),
SplitViewDisplayMode.CompactInline => (CompactPaneLength, new GridLength(0, GridUnitType.Auto)),
_ => throw new NotImplementedException(),
};
TemplateSettings.ClosedPaneWidth = closedPaneWidth;
TemplateSettings.PaneColumnGridLength = paneColumnGridLength;
}
else if (change.Property == IsPaneOpenProperty)
{
bool isPaneOpen = change.GetNewValue<bool>();
if (isPaneOpen)
{
PseudoClasses.Add(":open");
PseudoClasses.Remove(":closed");
OnPaneOpened(new RoutedEventArgs(PaneOpenedEvent, this));
}
else
{
PseudoClasses.Add(":closed");
PseudoClasses.Remove(":open");
OnPaneClosed(new RoutedEventArgs(PaneClosedEvent, this));
}
}
else if (change.Property == PaneProperty)
{
if (change.OldValue is ILogical oldChild)
{
LogicalChildren.Remove(oldChild);
}
if (change.NewValue is ILogical newChild)
{
LogicalChildren.Add(newChild);
}
}
else if (change.Property == PanePlacementProperty)
{
var oldState = GetPseudoClass(change.GetOldValue<SplitViewPanePlacement>());
var newState = GetPseudoClass(change.GetNewValue<SplitViewPanePlacement>());
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
}
else if (change.Property == UseLightDismissOverlayModeProperty)
{
var mode = change.GetNewValue<bool>();
PseudoClasses.Set(":lightdismiss", mode);
}
}
private void PointerPressedOutside(object? sender, PointerPressedEventArgs e)
{
if (!IsPaneOpen)
@ -384,7 +434,7 @@ namespace Avalonia.Controls
}
if (closePane)
{
IsPaneOpen = false;
SetCurrentValue(IsPaneOpenProperty, false);
e.Handled = true;
}
}
@ -394,41 +444,29 @@ namespace Avalonia.Controls
return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay);
}
protected virtual void OnPaneOpening(SplitView sender, EventArgs args)
protected virtual void OnPaneOpening(CancelRoutedEventArgs args)
{
PaneOpening?.Invoke(sender, args);
RaiseEvent(args);
}
protected virtual void OnPaneOpened(SplitView sender, EventArgs args)
protected virtual void OnPaneOpened(RoutedEventArgs args)
{
PaneOpened?.Invoke(sender, args);
RaiseEvent(args);
}
protected virtual void OnPaneClosing(SplitView sender, SplitViewPaneClosingEventArgs args)
protected virtual void OnPaneClosing(CancelRoutedEventArgs args)
{
PaneClosing?.Invoke(sender, args);
RaiseEvent(args);
}
protected virtual void OnPaneClosed(SplitView sender, EventArgs args)
protected virtual void OnPaneClosed(RoutedEventArgs args)
{
PaneClosed?.Invoke(sender, args);
}
private void OnCompactPaneLengthChanged(AvaloniaPropertyChangedEventArgs e)
{
var newLen = (double)e.NewValue!;
var displayMode = DisplayMode;
if (displayMode == SplitViewDisplayMode.CompactInline)
{
TemplateSettings.ClosedPaneWidth = newLen;
}
else if (displayMode == SplitViewDisplayMode.CompactOverlay)
{
TemplateSettings.ClosedPaneWidth = newLen;
TemplateSettings.PaneColumnGridLength = new GridLength(newLen, GridUnitType.Pixel);
}
RaiseEvent(args);
}
/// <summary>
/// Gets the appropriate PseudoClass for the given <see cref="SplitViewDisplayMode"/>.
/// </summary>
private static string GetPseudoClass(SplitViewDisplayMode mode)
{
return mode switch
@ -441,6 +479,9 @@ namespace Avalonia.Controls
};
}
/// <summary>
/// Gets the appropriate PseudoClass for the given <see cref="SplitViewPanePlacement"/>.
/// </summary>
private static string GetPseudoClass(SplitViewPanePlacement placement)
{
return placement switch
@ -451,51 +492,47 @@ namespace Avalonia.Controls
};
}
private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldState = GetPseudoClass(e.GetOldValue<SplitViewPanePlacement>());
var newState = GetPseudoClass(e.GetNewValue<SplitViewPanePlacement>());
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
}
private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e)
/// <summary>
/// Called when the <see cref="IsPaneOpen"/> property has to be coerced.
/// </summary>
/// <param name="value">The value to coerce.</param>
protected virtual bool OnCoerceIsPaneOpen(bool value)
{
var oldState = GetPseudoClass(e.GetOldValue<SplitViewDisplayMode>());
var newState = GetPseudoClass(e.GetNewValue<SplitViewDisplayMode>());
CancelRoutedEventArgs eventArgs;
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
if (value)
{
eventArgs = new CancelRoutedEventArgs(PaneOpeningEvent, this);
OnPaneOpening(eventArgs);
}
else
{
eventArgs = new CancelRoutedEventArgs(PaneClosingEvent, this);
OnPaneClosing(eventArgs);
}
var (closedPaneWidth, paneColumnGridLength) = e.GetNewValue<SplitViewDisplayMode>() switch
if (eventArgs.Cancel)
{
SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)),
SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)),
SplitViewDisplayMode.Inline => (0, new GridLength(0, GridUnitType.Auto)),
SplitViewDisplayMode.CompactInline => (CompactPaneLength, new GridLength(0, GridUnitType.Auto)),
_ => throw new NotImplementedException(),
};
TemplateSettings.ClosedPaneWidth = closedPaneWidth;
TemplateSettings.PaneColumnGridLength = paneColumnGridLength;
}
return !value;
}
private void OnUseLightDismissChanged(AvaloniaPropertyChangedEventArgs e)
{
var mode = (bool)e.NewValue!;
PseudoClasses.Set(":lightdismiss", mode);
return value;
}
private void PaneChanged(AvaloniaPropertyChangedEventArgs e)
/// <summary>
/// Coerces/validates the <see cref="IsPaneOpen"/> property value.
/// </summary>
/// <param name="instance">The <see cref="SplitView"/> instance.</param>
/// <param name="value">The value to coerce.</param>
/// <returns>The coerced/validated value.</returns>
private static bool CoerceIsPaneOpen(AvaloniaObject instance, bool value)
{
if (e.OldValue is ILogical oldChild)
if (instance is SplitView splitView)
{
LogicalChildren.Remove(oldChild);
return splitView.OnCoerceIsPaneOpen(value);
}
if (e.NewValue is ILogical newChild)
{
LogicalChildren.Add(newChild);
}
return value;
}
}
}

29
src/Avalonia.Controls/SplitView/SplitViewDisplayMode.cs

@ -0,0 +1,29 @@
namespace Avalonia.Controls
{
/// <summary>
/// Defines constants for how the SplitView Pane should display
/// </summary>
public enum SplitViewDisplayMode
{
/// <summary>
/// Pane is displayed next to content, and does not auto collapse
/// when tapped outside
/// </summary>
Inline,
/// <summary>
/// Pane is displayed next to content. When collapsed, pane is still
/// visible according to CompactPaneLength. Pane does not auto collapse
/// when tapped outside
/// </summary>
CompactInline,
/// <summary>
/// Pane is displayed above content. Pane collapses when tapped outside
/// </summary>
Overlay,
/// <summary>
/// Pane is displayed above content. When collapsed, pane is still
/// visible according to CompactPaneLength. Pane collapses when tapped outside
/// </summary>
CompactOverlay
}
}

18
src/Avalonia.Controls/SplitView/SplitViewPanePlacement.cs

@ -0,0 +1,18 @@
namespace Avalonia.Controls
{
/// <summary>
/// Defines constants for where the Pane should appear
/// </summary>
public enum SplitViewPanePlacement
{
/// <summary>
/// The pane is shown to the left of content.
/// </summary>
Left,
/// <summary>
/// The pane is shown to the right of content.
/// </summary>
Right
}
}

32
src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs

@ -0,0 +1,32 @@
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Provides calculated values for use with the <see cref="SplitView"/>'s control theme or template.
/// This class is NOT intended for general use.
/// </summary>
public class SplitViewTemplateSettings : AvaloniaObject
{
internal SplitViewTemplateSettings() { }
public static readonly StyledProperty<double> ClosedPaneWidthProperty =
AvaloniaProperty.Register<SplitViewTemplateSettings,
double>(nameof(ClosedPaneWidth),
0d);
public static readonly StyledProperty<GridLength> PaneColumnGridLengthProperty =
AvaloniaProperty.Register<SplitViewTemplateSettings, GridLength>(
nameof(PaneColumnGridLength));
public double ClosedPaneWidth
{
get => GetValue(ClosedPaneWidthProperty);
internal set => SetValue(ClosedPaneWidthProperty, value);
}
public GridLength PaneColumnGridLength
{
get => GetValue(PaneColumnGridLengthProperty);
internal set => SetValue(PaneColumnGridLengthProperty, value);
}
}
}

14
src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs

@ -1,14 +0,0 @@
using System;
namespace Avalonia.Controls
{
public class SplitViewPaneClosingEventArgs : EventArgs
{
public bool Cancel { get; set; }
public SplitViewPaneClosingEventArgs(bool cancel)
{
Cancel = cancel;
}
}
}

41
src/Avalonia.Native/ClipboardImpl.cs

@ -2,11 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Native.Interop;
using Avalonia.Platform.Interop;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Native
{
@ -56,8 +56,13 @@ namespace Avalonia.Native
{
if(fmt.String == NSPasteboardTypeString)
rv.Add(DataFormats.Text);
if(fmt.String == NSFilenamesPboardType)
rv.Add(DataFormats.FileNames);
if (fmt.String == NSFilenamesPboardType)
{
#pragma warning disable CS0618 // Type or member is obsolete
rv.Add(DataFormats.FileNames);
#pragma warning restore CS0618 // Type or member is obsolete
rv.Add(DataFormats.Files);
}
}
}
}
@ -74,7 +79,13 @@ namespace Avalonia.Native
public IEnumerable<string> GetFileNames()
{
using (var strings = _native.GetStrings(NSFilenamesPboardType))
return strings.ToStringArray();
return strings?.ToStringArray();
}
public IEnumerable<IStorageItem> GetFiles()
{
return GetFileNames()?.Select(f => StorageProviderHelpers.TryCreateBclStorageItem(f)!)
.Where(f => f is not null);
}
public unsafe Task SetDataObjectAsync(IDataObject data)
@ -102,8 +113,12 @@ namespace Avalonia.Native
{
if (format == DataFormats.Text)
return await GetTextAsync();
#pragma warning disable CS0618 // Type or member is obsolete
if (format == DataFormats.FileNames)
return GetFileNames();
#pragma warning restore CS0618 // Type or member is obsolete
if (format == DataFormats.Files)
return GetFiles();
using (var n = _native.GetBytes(format))
return n.Bytes;
}
@ -131,20 +146,16 @@ namespace Avalonia.Native
public bool Contains(string dataFormat) => Formats.Contains(dataFormat);
public string GetText()
{
// bad idea in general, but API is synchronous anyway
return _clipboard.GetTextAsync().Result;
}
public IEnumerable<string> GetFileNames() => _clipboard.GetFileNames();
public object Get(string dataFormat)
{
if (dataFormat == DataFormats.Text)
return GetText();
return _clipboard.GetTextAsync().Result;
if (dataFormat == DataFormats.Files)
return _clipboard.GetFiles();
#pragma warning disable CS0618
if (dataFormat == DataFormats.FileNames)
return GetFileNames();
#pragma warning restore CS0618
return _clipboard.GetFileNames();
return null;
}
}

55
src/Browser/Avalonia.Browser/AvaloniaView.cs

@ -106,6 +106,8 @@ namespace Avalonia.Browser
InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp,
OnPointerCancel, OnWheel);
InputHelper.SubscribeDropEvents(_containerElement, OnDragEvent);
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>();
_dpi = DomHelper.ObserveDpi(OnDpiChanged);
@ -293,6 +295,59 @@ namespace Avalonia.Browser
return modifiers;
}
public bool OnDragEvent(JSObject args)
{
var eventType = args?.GetPropertyAsString("type") switch
{
"dragenter" => RawDragEventType.DragEnter,
"dragover" => RawDragEventType.DragOver,
"dragleave" => RawDragEventType.DragLeave,
"drop" => RawDragEventType.Drop,
_ => (RawDragEventType)(int)-1
};
var dataObject = args?.GetPropertyAsJSObject("dataTransfer");
if (args is null || eventType < 0 || dataObject is null)
{
return false;
}
// If file is dropped, we need storage js to be referenced.
// TODO: restructure JS files, so it's not needed.
_ = AvaloniaModule.ImportStorage();
var position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY"));
var modifiers = GetModifiers(args);
var effectAllowedStr = dataObject.GetPropertyAsString("effectAllowed") ?? "none";
var effectAllowed = DragDropEffects.None;
if (effectAllowedStr.Contains("copy", StringComparison.OrdinalIgnoreCase))
{
effectAllowed |= DragDropEffects.Copy;
}
if (effectAllowedStr.Contains("link", StringComparison.OrdinalIgnoreCase))
{
effectAllowed |= DragDropEffects.Link;
}
if (effectAllowedStr.Contains("move", StringComparison.OrdinalIgnoreCase))
{
effectAllowed |= DragDropEffects.Move;
}
if (effectAllowedStr.Equals("all", StringComparison.OrdinalIgnoreCase))
{
effectAllowed |= DragDropEffects.Move | DragDropEffects.Copy | DragDropEffects.Link;
}
if (effectAllowed == DragDropEffects.None)
{
return false;
}
var dropEffect = _topLevelImpl.RawDragEvent(eventType, position, modifiers, new BrowserDataObject(dataObject), effectAllowed);
dataObject.SetProperty("dropEffect", dropEffect.ToString().ToLowerInvariant());
return eventType is RawDragEventType.Drop or RawDragEventType.DragOver
&& dropEffect != DragDropEffects.None;
}
private bool OnKeyDown (string code, string key, int modifier)
{
var handled = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier);

91
src/Browser/Avalonia.Browser/BrowserDataObject.cs

@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Browser.Interop;
using Avalonia.Browser.Storage;
using Avalonia.Input;
using Avalonia.Platform.Storage;
namespace Avalonia.Browser;
internal class BrowserDataObject : IDataObject
{
private readonly JSObject _dataObject;
public BrowserDataObject(JSObject dataObject)
{
_dataObject = dataObject;
}
public IEnumerable<string> GetDataFormats()
{
var types = new HashSet<string>(_dataObject.GetPropertyAsStringArray("types"));
var dataFormats = new HashSet<string>(types.Count);
foreach (var type in types)
{
if (type.StartsWith("text/", StringComparison.Ordinal))
{
dataFormats.Add(DataFormats.Text);
}
else if (type.Equals("Files", StringComparison.Ordinal))
{
dataFormats.Add(DataFormats.Files);
}
dataFormats.Add(type);
}
// If drag'n'drop an image from the another web page, if won't add "Files" to the supported types, but only a "text/uri-list".
// With "text/uri-list" browser can add actual file as well.
var filesCount = _dataObject.GetPropertyAsJSObject("files")?.GetPropertyAsInt32("count");
if (filesCount > 0)
{
dataFormats.Add(DataFormats.Files);
}
return dataFormats;
}
public bool Contains(string dataFormat)
{
return GetDataFormats().Contains(dataFormat);
}
public object? Get(string dataFormat)
{
if (dataFormat == DataFormats.Files)
{
var files = _dataObject.GetPropertyAsJSObject("files");
if (files is not null)
{
return StorageHelper.FilesToItemsArray(files)
.Select(reference => reference.GetPropertyAsString("kind") switch
{
"directory" => (IStorageItem)new JSStorageFolder(reference),
"file" => new JSStorageFile(reference),
_ => null
})
.Where(i => i is not null)
.ToArray()!;
}
return null;
}
if (dataFormat == DataFormats.Text)
{
if (_dataObject.CallMethodString("getData", "text/plain") is { Length :> 0 } textData)
{
return textData;
}
}
if (_dataObject.CallMethodString("getData", dataFormat) is { Length: > 0 } data)
{
return data;
}
return null;
}
}

9
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@ -164,6 +164,15 @@ namespace Avalonia.Browser
return false;
}
public DragDropEffects RawDragEvent(RawDragEventType eventType, Point position, RawInputModifiers modifiers, BrowserDataObject dataObject, DragDropEffects dropEffect)
{
var device = AvaloniaLocator.Current.GetRequiredService<IDragDropDevice>();
var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataObject, dropEffect, modifiers);
Console.WriteLine($"{eventArgs.Location} {eventArgs.Effects} {eventArgs.Type} {eventArgs.KeyModifiers}");
Input?.Invoke(eventArgs);
return eventArgs.Effects;
}
public void Dispose()
{

2
src/Browser/Avalonia.Browser/ClipboardImpl.cs

@ -24,6 +24,6 @@ namespace Avalonia.Browser
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
public Task<object?> GetDataAsync(string format) => Task.FromResult<object?>(new());
public Task<object?> GetDataAsync(string format) => Task.FromResult<object?>(null);
}
}

21
src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs

@ -1,24 +1,29 @@
using System.Runtime.InteropServices.JavaScript;
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
namespace Avalonia.Browser.Interop;
internal static partial class AvaloniaModule
{
public const string MainModuleName = "avalonia";
public const string StorageModuleName = "storage";
public static Task ImportMain()
private static readonly Lazy<Task> s_importMain = new(() =>
{
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver!("avalonia.js"));
}
});
public static Task ImportStorage()
private static readonly Lazy<Task> s_importStorage = new(() =>
{
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver!("storage.js"));
}
});
public const string MainModuleName = "avalonia";
public const string StorageModuleName = "storage";
public static Task ImportMain() => s_importMain.Value;
public static Task ImportStorage() => s_importStorage.Value;
[JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)]
public static partial bool IsMobile();

22
src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs

@ -0,0 +1,22 @@
using System.Runtime.InteropServices.JavaScript;
namespace Avalonia.Browser.Interop;
internal static partial class GeneralHelpers
{
[JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)]
public static partial JSObject[] ItemsArrayAt(JSObject jsObject, string key);
public static JSObject[] GetPropertyAsJSObjectArray(this JSObject jsObject, string key) => ItemsArrayAt(jsObject, key);
[JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)]
public static partial string[] ItemsArrayAtAsStrings(JSObject jsObject, string key);
public static string[] GetPropertyAsStringArray(this JSObject jsObject, string key) => ItemsArrayAtAsStrings(jsObject, key);
[JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)]
public static partial string IntCallMethodString(JSObject jsObject, string name);
[JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)]
public static partial string IntCallMethodStringString(JSObject jsObject, string name, string arg1);
public static string CallMethodString(this JSObject jsObject, string name) => IntCallMethodString(jsObject, name);
public static string CallMethodString(this JSObject jsObject, string name, string arg1) => IntCallMethodStringString(jsObject, name, arg1);
}

5
src/Browser/Avalonia.Browser/Interop/InputHelper.cs

@ -43,13 +43,16 @@ internal static partial class InputHelper
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> wheel);
[JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)]
public static partial void SubscribeInputEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.String, JSType.Boolean>>]
Func<string, bool> input);
[JSImport("InputHelper.subscribeDropEvents", AvaloniaModule.MainModuleName)]
public static partial void SubscribeDropEvents(JSObject containerElement,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] Func<JSObject, bool> dragEvent);
[JSImport("InputHelper.getCoalescedEvents", AvaloniaModule.MainModuleName)]
[return: JSMarshalAs<JSType.Array<JSType.Object>>]
public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent);

3
src/Browser/Avalonia.Browser/Interop/StorageHelper.cs

@ -46,6 +46,9 @@ internal static partial class StorageHelper
[JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)]
public static partial JSObject[] ItemsArray(JSObject item);
[JSImport("StorageItems.filesToItemsArray", AvaloniaModule.StorageModuleName)]
public static partial JSObject[] FilesToItemsArray(JSObject item);
[JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)]
public static partial JSObject CreateAcceptType(string description, string[] mimeTypes, string[]? extensions);

16
src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs

@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
using Avalonia.Platform.Storage;
@ -18,15 +16,13 @@ internal class BrowserStorageProvider : IStorageProvider
internal const string PickerCancelMessage = "The user aborted a request";
internal const string NoPermissionsMessage = "Permissions denied";
private readonly Lazy<Task> _lazyModule = new(() => AvaloniaModule.ImportStorage());
public bool CanOpen => true;
public bool CanSave => StorageHelper.HasNativeFilePicker();
public bool CanPickFolder => true;
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
await _lazyModule.Value;
await AvaloniaModule.ImportStorage();
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
var (types, excludeAll) = ConvertFileTypes(options.FileTypeFilter);
@ -60,7 +56,7 @@ internal class BrowserStorageProvider : IStorageProvider
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
await _lazyModule.Value;
await AvaloniaModule.ImportStorage();
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
var (types, excludeAll) = ConvertFileTypes(options.FileTypeChoices);
@ -88,7 +84,7 @@ internal class BrowserStorageProvider : IStorageProvider
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
await _lazyModule.Value;
await AvaloniaModule.ImportStorage();
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
try
@ -104,14 +100,14 @@ internal class BrowserStorageProvider : IStorageProvider
public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
{
await _lazyModule.Value;
await AvaloniaModule.ImportStorage();
var item = await StorageHelper.OpenBookmark(bookmark);
return item is not null ? new JSStorageFile(item) : null;
}
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{
await _lazyModule.Value;
await AvaloniaModule.ImportStorage();
var item = await StorageHelper.OpenBookmark(bookmark);
return item is not null ? new JSStorageFolder(item) : null;
}
@ -128,7 +124,7 @@ internal class BrowserStorageProvider : IStorageProvider
public async Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{
await _lazyModule.Value;
await AvaloniaModule.ImportStorage();
var directory = StorageHelper.CreateWellKnownDirectory(wellKnownFolder switch
{
WellKnownFolder.Desktop => "desktop",

4
src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts

@ -5,6 +5,7 @@ import { Caniuse } from "./avalonia/caniuse";
import { StreamHelper } from "./avalonia/stream";
import { NativeControlHost } from "./avalonia/nativeControlHost";
import { NavigationHelper } from "./avalonia/navigationHelper";
import { GeneralHelpers } from "./avalonia/generalHelpers";
export {
Caniuse,
@ -15,5 +16,6 @@ export {
AvaloniaDOM,
StreamHelper,
NativeControlHost,
NavigationHelper
NavigationHelper,
GeneralHelpers
};

19
src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts

@ -0,0 +1,19 @@
export class GeneralHelpers {
public static itemsArrayAt(instance: any, key: string): any[] {
const items = instance[key];
if (!items) {
return [];
}
const retItems = [];
for (let i = 0; i < items.length; i++) {
retItems[i] = items[i];
}
return retItems;
}
public static callMethod(instance: any, name: string /*, args */): any {
const args = Array.prototype.slice.call(arguments, 2);
return instance[name].apply(instance, args);
}
}

22
src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts

@ -174,6 +174,28 @@ export class InputHelper {
};
}
public static subscribeDropEvents(
element: HTMLInputElement,
dragEvent: (args: any) => boolean
) {
const dragHandler = (args: Event) => {
if (dragEvent(args as any)) {
args.preventDefault();
}
};
element.addEventListener("dragover", dragHandler);
element.addEventListener("dragenter", dragHandler);
element.addEventListener("dragleave", dragHandler);
element.addEventListener("drop", dragHandler);
return () => {
element.removeEventListener("dragover", dragHandler);
element.removeEventListener("dragenter", dragHandler);
element.removeEventListener("dragleave", dragHandler);
element.removeEventListener("drop", dragHandler);
};
}
public static getCoalescedEvents(pointerEvent: PointerEvent): PointerEvent[] {
return pointerEvent.getCoalescedEvents();
}

42
src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts

@ -3,8 +3,9 @@ import { FileSystemFileHandle, FileSystemDirectoryHandle, FileSystemWritableFile
import { Caniuse } from "../avalonia";
export class StorageItem {
constructor(
private constructor(
public handle?: FileSystemFileHandle | FileSystemDirectoryHandle,
private readonly file?: File,
private readonly bookmarkId?: string,
public wellKnownType?: WellKnownDirectory
) {
@ -14,6 +15,9 @@ export class StorageItem {
if (this.handle) {
return this.handle.name;
}
if (this.file) {
return this.file.name;
}
return this.wellKnownType ?? "";
}
@ -21,14 +25,29 @@ export class StorageItem {
if (this.handle) {
return this.handle.kind;
}
if (this.file) {
return "file";
}
return "directory";
}
public static createFromHandle(handle: FileSystemFileHandle | FileSystemDirectoryHandle, bookmarkId?: string) {
return new StorageItem(handle, undefined, bookmarkId, undefined);
}
public static createFromFile(file: File) {
return new StorageItem(undefined, file, undefined, undefined);
}
public static createWellKnownDirectory(type: WellKnownDirectory) {
return new StorageItem(undefined, undefined, type);
return new StorageItem(undefined, undefined, undefined, type);
}
public static async openRead(item: StorageItem): Promise<Blob> {
if (item.file) {
return item.file;
}
if (!item.handle || item.kind !== "file") {
throw new Error("StorageItem is not a file");
}
@ -41,7 +60,7 @@ export class StorageItem {
public static async openWrite(item: StorageItem): Promise<FileSystemWritableFileStream> {
if (!item.handle || item.kind !== "file") {
throw new Error("StorageItem is not a file");
throw new Error("StorageItem is not a writeable file");
}
await item.verityPermissions("readwrite");
@ -52,8 +71,9 @@ export class StorageItem {
public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> {
// getFile can fail with an exception depending if we use polyfill with a save file dialog or not.
try {
const file = item.handle instanceof FileSystemFileHandle &&
await item.handle.getFile();
const file = item.handle && "getFile" in item.handle
? await item.handle.getFile()
: item.file;
if (!file) {
return null;
@ -144,4 +164,16 @@ export class StorageItems {
public static itemsArray(instance: StorageItems): StorageItem[] {
return instance.items;
}
public static filesToItemsArray(files: File[]): StorageItem[] {
if (!files) {
return [];
}
const retItems = [];
for (let i = 0; i < files.length; i++) {
retItems[i] = StorageItem.createFromFile(files[i]);
}
return retItems;
}
}

8
src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts

@ -19,7 +19,7 @@ export class StorageProvider {
};
const handle = await showDirectoryPicker(options as any);
return new StorageItem(handle);
return StorageItem.createFromHandle(handle);
}
public static async openFileDialog(
@ -33,7 +33,7 @@ export class StorageProvider {
};
const handles = await showOpenFilePicker(options);
return new StorageItems(handles.map((handle: FileSystemFileHandle) => new StorageItem(handle)));
return new StorageItems(handles.map((handle: FileSystemFileHandle) => StorageItem.createFromHandle(handle)));
}
public static async saveFileDialog(
@ -48,14 +48,14 @@ export class StorageProvider {
// Always prefer native save file picker, as polyfill solutions are not reliable.
const handle = await (globalThis as any).showSaveFilePicker(options);
return new StorageItem(handle);
return StorageItem.createFromHandle(handle);
}
public static async openBookmark(key: string): Promise<StorageItem | null> {
const connection = await avaloniaDb.connect();
try {
const handle = await connection.get(fileBookmarksStore, key);
return handle && new StorageItem(handle, key);
return handle && StorageItem.createFromHandle(handle, key);
} finally {
connection.close();
}

3
src/Windows/Avalonia.Win32/ClipboardFormats.cs

@ -29,7 +29,10 @@ namespace Avalonia.Win32
private static readonly List<ClipboardFormat> s_formatList = new()
{
new ClipboardFormat(DataFormats.Text, (ushort)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (ushort)UnmanagedMethods.ClipboardFormat.CF_TEXT),
new ClipboardFormat(DataFormats.Files, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP),
#pragma warning disable CS0618 // Type or member is obsolete
new ClipboardFormat(DataFormats.FileNames, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP),
#pragma warning restore CS0618 // Type or member is obsolete
};

15
src/Windows/Avalonia.Win32/DataObject.cs

@ -10,6 +10,7 @@ using System.Runtime.InteropServices.ComTypes;
using System.Runtime.Serialization.Formatters.Binary;
using Avalonia.Input;
using Avalonia.MicroCom;
using Avalonia.Platform.Storage;
using Avalonia.Win32.Interop;
using FORMATETC = Avalonia.Win32.Interop.FORMATETC;
@ -124,16 +125,6 @@ namespace Avalonia.Win32
return _wrapped.GetDataFormats();
}
IEnumerable<string>? IDataObject.GetFileNames()
{
return _wrapped.GetFileNames();
}
string? IDataObject.GetText()
{
return _wrapped.GetText();
}
object? IDataObject.Get(string dataFormat)
{
return _wrapped.Get(dataFormat);
@ -260,8 +251,12 @@ namespace Avalonia.Win32
object data = _wrapped.Get(dataFormat)!;
if (dataFormat == DataFormats.Text || data is string)
return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data) ?? string.Empty);
#pragma warning disable CS0618 // Type or member is obsolete
if (dataFormat == DataFormats.FileNames && data is IEnumerable<string> files)
return WriteFileListToHGlobal(ref hGlobal, files);
#pragma warning restore CS0618 // Type or member is obsolete
if (dataFormat == DataFormats.Files && data is IEnumerable<IStorageItem> items)
return WriteFileListToHGlobal(ref hGlobal, items.Select(f => f.TryGetLocalPath()).Where(f => f is not null)!);
if (data is Stream stream)
{
var length = (int)(stream.Length - stream.Position);

18
src/Windows/Avalonia.Win32/OleDataObject.cs

@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Runtime.Serialization.Formatters.Binary;
using Avalonia.Input;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Utilities;
using Avalonia.Win32.Interop;
using MicroCom.Runtime;
@ -34,16 +35,6 @@ namespace Avalonia.Win32
return GetDataFormatsCore().Distinct();
}
public string? GetText()
{
return (string?)GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT);
}
public IEnumerable<string>? GetFileNames()
{
return (IEnumerable<string>?)GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT);
}
public object? Get(string dataFormat)
{
return GetDataFromOleHGLOBAL(dataFormat, DVASPECT.DVASPECT_CONTENT);
@ -67,8 +58,15 @@ namespace Avalonia.Win32
{
if (format == DataFormats.Text)
return ReadStringFromHGlobal(medium.unionmember);
#pragma warning disable CS0618
if (format == DataFormats.FileNames)
#pragma warning restore CS0618
return ReadFileNamesFromHGlobal(medium.unionmember);
if (format == DataFormats.Files)
return ReadFileNamesFromHGlobal(medium.unionmember)
.Select(f => StorageProviderHelpers.TryCreateBclStorageItem(f)!)
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
.Where(f => f is not null);
byte[] data = ReadBytesFromHGlobal(medium.unionmember);

40
tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs

@ -104,7 +104,7 @@ public class StyledElementTests_Theming
target.Theme = null;
Assert.Equal("style", target.Tag);
}
[Fact]
public void TemplatedParent_Theme_Is_Detached_From_Template_Controls_When_Theme_Property_Cleared()
{
@ -539,12 +539,42 @@ public class StyledElementTests_Theming
Assert.Same(target.Theme, theme3);
}
[Fact]
public void TemplatedParent_Theme_Change_Applies_To_Children()
{
var theme = CreateDerivedTheme();
var target = CreateTarget();
Assert.Null(target.Theme);
Assert.Null(target.Template);
var root = CreateRoot(target, theme.BasedOn);
Assert.NotNull(target.Theme);
Assert.NotNull(target.Template);
root.Styles.Add(new Style(x => x.OfType<ThemedControl>().Class("foo"))
{
Setters = { new Setter(StyledElement.ThemeProperty, theme) }
});
root.LayoutManager.ExecuteLayoutPass();
var border = Assert.IsType<Border>(target.VisualChild);
Assert.Equal(Brushes.Red, border.Background);
target.Classes.Add("foo");
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(Brushes.Green, border.Background);
}
private static ThemedControl CreateTarget()
{
return new ThemedControl();
}
private static TestRoot CreateRoot(Control child)
private static TestRoot CreateRoot(Control child, ControlTheme? theme = null)
{
var result = new TestRoot()
{
@ -552,7 +582,7 @@ public class StyledElementTests_Theming
{
new Style(x => x.OfType<ThemedControl>())
{
Setters = { new Setter(StyledElement.ThemeProperty, CreateTheme()) }
Setters = { new Setter(StyledElement.ThemeProperty, theme ?? CreateTheme()) }
}
}
};
@ -580,8 +610,8 @@ public class StyledElementTests_Theming
{
new Style(x => x.Nesting().Template().OfType<Border>())
{
Setters =
{
Setters =
{
new Setter(Border.BackgroundProperty, Brushes.Red),
new Setter(Control.TagProperty, tag),
}

12
tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs

@ -150,6 +150,18 @@ namespace Avalonia.IntegrationTests.Appium
windowState = mainWindow.FindElementByAccessibilityId("MainWindowState");
Assert.Equal("Normal", windowState.Text);
}
[PlatformFact(TestPlatforms.MacOS)]
public void WindowOrder_Owned_Dialog_Stays_InFront_Of_Parent_After_Modal_Closed()
{
using (OpenWindow(new PixelSize(200, 300), ShowWindowMode.Owned, WindowStartupLocation.Manual))
{
OpenWindow(null, ShowWindowMode.Modal, WindowStartupLocation.Manual).Dispose();
var secondaryWindowIndex = GetWindowOrder("SecondaryWindow");
Assert.Equal(1, secondaryWindowIndex);
}
}
[PlatformFact(TestPlatforms.MacOS)]
public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked()

2
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -457,7 +457,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
{
textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties));
textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties, FlowDirection.LeftToRight));
}
currentHeight += textLine.Height;

2
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -407,7 +407,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.False(textLine.HasCollapsed);
TextCollapsingProperties collapsingProperties = trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, defaultProperties));
TextCollapsingProperties collapsingProperties = trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, defaultProperties, FlowDirection.LeftToRight));
var collapsedLine = textLine.Collapse(collapsingProperties);

Loading…
Cancel
Save