Browse Source

Merge branch 'master' into move-name-generator

pull/10407/head
Max Katz 4 years ago
committed by GitHub
parent
commit
4c2939ead5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      Avalonia.Desktop.slnf
  2. 1
      Avalonia.sln
  3. 41
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  4. 3
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  5. 48
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  6. 39
      src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
  7. 10
      src/Avalonia.Base/Input/DataFormats.cs
  8. 25
      src/Avalonia.Base/Input/DataObject.cs
  9. 50
      src/Avalonia.Base/Input/DataObjectExtensions.cs
  10. 17
      src/Avalonia.Base/Input/IDataObject.cs
  11. 6
      src/Avalonia.Base/Layout/Layoutable.cs
  12. 5
      src/Avalonia.Base/Media/DrawingContext.cs
  13. 8
      src/Avalonia.Base/Media/DrawingGroup.cs
  14. 2
      src/Avalonia.Base/Media/FormattedText.cs
  15. 5
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  16. 4
      src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs
  17. 13
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  18. 21
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  19. 9
      src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
  20. 191
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  21. 25
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  22. 7
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  23. 47
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  24. 7
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  25. 7
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  26. 2
      src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs
  27. 4
      src/Avalonia.Base/Media/TextTrailingTrimming.cs
  28. 2
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  29. 5
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  30. 9
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  31. 17
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  32. 2
      src/Avalonia.Base/Platform/Storage/PickerOptions.cs
  33. 12
      src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
  34. 6
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
  35. 4
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  36. 13
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  37. 2
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  38. 11
      src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs
  39. 48
      src/Avalonia.Base/StyledElement.cs
  40. 22
      src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs
  41. 26
      src/Avalonia.Base/Styling/IThemeVariantHost.cs
  42. 17
      src/Avalonia.Base/Styling/ThemeVariant.cs
  43. 71
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  44. 19
      src/Avalonia.Controls/Application.cs
  45. 2
      src/Avalonia.Controls/Control.cs
  46. 9
      src/Avalonia.Controls/Primitives/Popup.cs
  47. 8
      src/Avalonia.Controls/ThemeVariantScope.cs
  48. 64
      src/Avalonia.Controls/TopLevel.cs
  49. 2
      src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs
  50. 26
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  51. 13
      src/Avalonia.FreeDesktop/DBusCallQueue.cs
  52. 32
      src/Avalonia.FreeDesktop/DBusFileChooser.cs
  53. 54
      src/Avalonia.FreeDesktop/DBusHelper.cs
  54. 68
      src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
  55. 69
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
  56. 64
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs
  57. 33
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
  58. 52
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
  59. 52
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs
  60. 4
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs
  61. 61
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
  62. 25
      src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
  63. 56
      src/Avalonia.FreeDesktop/DBusMenu.cs
  64. 373
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  65. 85
      src/Avalonia.FreeDesktop/DBusPlatformSettings.cs
  66. 16
      src/Avalonia.FreeDesktop/DBusRequest.cs
  67. 16
      src/Avalonia.FreeDesktop/DBusSettings.cs
  68. 171
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  69. 316
      src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs
  70. 89
      src/Avalonia.FreeDesktop/DBusXml/DBus.xml
  71. 437
      src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml
  72. 96
      src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml
  73. 42
      src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml
  74. 56
      src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml
  75. 64
      src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml
  76. 64
      src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml
  77. 16
      src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml
  78. 12
      src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml
  79. 139
      src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml
  80. 377
      src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml
  81. 86
      src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml
  82. 99
      src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml
  83. 8
      src/Avalonia.FreeDesktop/NativeMethods.cs
  84. 2
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  85. 41
      src/Avalonia.Native/ClipboardImpl.cs
  86. 104
      src/Avalonia.Themes.Simple/Controls/SplitButton.xaml
  87. 6
      src/Avalonia.Themes.Simple/Controls/SplitView.xaml
  88. 91
      src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml
  89. 6
      src/Avalonia.X11/X11Window.cs
  90. 55
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  91. 91
      src/Browser/Avalonia.Browser/BrowserDataObject.cs
  92. 9
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  93. 2
      src/Browser/Avalonia.Browser/ClipboardImpl.cs
  94. 21
      src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs
  95. 22
      src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs
  96. 5
      src/Browser/Avalonia.Browser/Interop/InputHelper.cs
  97. 3
      src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
  98. 16
      src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
  99. 4
      src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
  100. 19
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts

4
Avalonia.Desktop.slnf

@ -8,9 +8,9 @@
"samples\\GpuInterop\\GpuInterop.csproj",
"samples\\IntegrationTestApp\\IntegrationTestApp.csproj",
"samples\\MiniMvvm\\MiniMvvm.csproj",
"samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"samples\\SampleControls\\ControlSamples.csproj",
"samples\\Sandbox\\Sandbox.csproj",
"samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"src\\Avalonia.Base\\Avalonia.Base.csproj",
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",
@ -62,4 +62,4 @@
"tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj"
]
}
}
}

1
Avalonia.sln

@ -559,7 +559,6 @@ Global
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU

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;

39
src/Avalonia.Base/Controls/ResourceNodeExtensions.cs

@ -138,18 +138,18 @@ namespace Avalonia.Controls
protected override void Initialize()
{
_target.ResourcesChanged += ResourcesChanged;
if (_target is StyledElement themeStyleable)
if (_target is IThemeVariantHost themeVariantHost)
{
themeStyleable.PropertyChanged += PropertyChanged;
themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged;
}
}
protected override void Deinitialize()
{
_target.ResourcesChanged -= ResourcesChanged;
if (_target is StyledElement themeStyleable)
if (_target is IThemeVariantHost themeVariantHost)
{
themeStyleable.PropertyChanged -= PropertyChanged;
themeVariantHost.ActualThemeVariantChanged -= ActualThemeVariantChanged;
}
}
@ -163,18 +163,15 @@ namespace Avalonia.Controls
PublishNext(GetValue());
}
private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
private void ActualThemeVariantChanged(object? sender, EventArgs e)
{
if (e.Property == StyledElement.ActualThemeVariantProperty)
{
PublishNext(GetValue());
}
PublishNext(GetValue());
}
private object? GetValue()
{
if (_target is not StyledElement themeStyleable
|| !_target.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value))
if (_target is not IThemeVariantHost themeVariantHost
|| !_target.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value))
{
value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue;
}
@ -236,9 +233,9 @@ namespace Avalonia.Controls
{
_owner.ResourcesChanged -= ResourcesChanged;
}
if (_owner is StyledElement styleable)
if (_owner is IThemeVariantHost themeVariantHost)
{
styleable.PropertyChanged += PropertyChanged;
themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged;
}
_owner = _target.Owner;
@ -247,20 +244,18 @@ namespace Avalonia.Controls
{
_owner.ResourcesChanged += ResourcesChanged;
}
if (_owner is StyledElement styleable2)
if (_owner is IThemeVariantHost themeVariantHost2)
{
styleable2.PropertyChanged += PropertyChanged;
themeVariantHost2.ActualThemeVariantChanged -= ActualThemeVariantChanged;
}
PublishNext();
}
private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
private void ActualThemeVariantChanged(object? sender, EventArgs e)
{
if (e.Property == StyledElement.ActualThemeVariantProperty)
{
PublishNext();
}
PublishNext();
}
private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e)
@ -270,8 +265,8 @@ namespace Avalonia.Controls
private object? GetValue()
{
if (!(_target.Owner is StyledElement themeStyleable)
|| !_target.Owner.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value))
if (!(_target.Owner is IThemeVariantHost themeVariantHost)
|| !_target.Owner.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value))
{
value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue;
}

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>

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

@ -361,11 +361,12 @@ namespace Avalonia.Media
/// Pushes an opacity value.
/// </summary>
/// <param name="opacity">The opacity.</param>
/// <param name="bounds">The bounds.</param>
/// <returns>A disposable used to undo the opacity.</returns>
public PushedState PushOpacity(double opacity)
public PushedState PushOpacity(double opacity, Rect bounds)
//TODO: Eliminate platform-specific push opacity call
{
PlatformImpl.PushOpacity(opacity);
PlatformImpl.PushOpacity(opacity, bounds);
return new PushedState(this, PushedState.PushedStateType.Opacity);
}

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

@ -74,10 +74,12 @@ namespace Avalonia.Media
public override void Draw(DrawingContext context)
{
var bounds = GetBounds();
using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity))
using (context.PushOpacity(Opacity))
using (context.PushOpacity(Opacity, bounds))
using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default)
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default)
{
foreach (var drawing in Children)
{
@ -284,7 +286,7 @@ namespace Avalonia.Media
throw new NotImplementedException();
}
public void PushOpacity(double opacity)
public void PushOpacity(double opacity, Rect bounds)
{
throw new NotImplementedException();
}

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

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

@ -281,11 +281,12 @@ namespace Avalonia.Media
/// Pushes an opacity value.
/// </summary>
/// <param name="opacity">The opacity.</param>
/// <param name="bounds">The bounds.</param>
/// <returns>A disposable used to undo the opacity.</returns>
public PushedState PushOpacity(double opacity)
public PushedState PushOpacity(double opacity, Rect bounds)
//TODO: Eliminate platform-specific push opacity call
{
PlatformImpl.PushOpacity(opacity);
PlatformImpl.PushOpacity(opacity, bounds);
return new PushedState(this, PushedState.PushedStateType.Opacity);
}

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

2
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -128,7 +128,7 @@ namespace Avalonia.Platform
/// Pushes an opacity value.
/// </summary>
/// <param name="opacity">The opacity.</param>
void PushOpacity(double opacity);
void PushOpacity(double opacity, Rect bounds);
/// <summary>
/// Pops the latest pushed opacity value.

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

6
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs

@ -313,13 +313,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
/// <inheritdoc/>
public void PushOpacity(double opacity)
public void PushOpacity(double opacity, Rect bounds)
{
var next = NextDrawAs<OpacityNode>();
if (next == null || !next.Item.Equals(opacity))
if (next == null || !next.Item.Equals(opacity, bounds))
{
Add(new OpacityNode(opacity));
Add(new OpacityNode(opacity, bounds));
}
else
{

4
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@ -111,9 +111,9 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont
_impl.PopClip();
}
public void PushOpacity(double opacity)
public void PushOpacity(double opacity, Rect bounds)
{
_impl.PushOpacity(opacity);
_impl.PushOpacity(opacity, bounds);
}
public void PopOpacity()

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

@ -41,9 +41,9 @@ namespace Avalonia.Rendering.Composition.Server
return;
Root!.RenderedVisuals++;
if (Opacity != 1)
canvas.PushOpacity(Opacity);
var boundsRect = new Rect(new Size(Size.X, Size.Y));
if (AdornedVisual != null)
{
canvas.PostTransform = Matrix.Identity;
@ -54,15 +54,16 @@ namespace Avalonia.Rendering.Composition.Server
var transform = GlobalTransformMatrix;
canvas.PostTransform = MatrixUtils.ToMatrix(transform);
canvas.Transform = Matrix.Identity;
var boundsRect = new Rect(new Size(Size.X, Size.Y));
if (Opacity != 1)
canvas.PushOpacity(Opacity, boundsRect);
if (ClipToBounds && !HandlesClipToBounds)
canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
if (Clip != null)
canvas.PushGeometryClip(Clip);
if(OpacityMaskBrush != null)
canvas.PushOpacityMask(OpacityMaskBrush, boundsRect);
RenderCore(canvas, currentTransformedClip);
// Hack to force invalidation of SKMatrix

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

@ -117,7 +117,7 @@ namespace Avalonia.Rendering
}
using (context.PushPostTransform(m))
using (context.PushOpacity(opacity))
using (context.PushOpacity(opacity, bounds))
using (clipToBounds
#pragma warning disable CS0618 // Type or member is obsolete
? visual is IVisualWithRoundRectClip roundClipVisual

11
src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs

@ -12,9 +12,11 @@ namespace Avalonia.Rendering.SceneGraph
/// opacity push.
/// </summary>
/// <param name="opacity">The opacity to push.</param>
public OpacityNode(double opacity)
/// <param name="bounds">The bounds.</param>
public OpacityNode(double opacity, Rect bounds)
{
Opacity = opacity;
Bounds = bounds;
}
/// <summary>
@ -26,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public Rect Bounds => default;
public Rect Bounds { get; }
/// <summary>
/// Gets the opacity to be pushed or null if the operation represents a pop.
@ -40,19 +42,20 @@ namespace Avalonia.Rendering.SceneGraph
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="opacity">The opacity of the other draw operation.</param>
/// <param name="bounds">The bounds of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(double? opacity) => Opacity == opacity;
public bool Equals(double? opacity, Rect bounds) => Opacity == opacity && Bounds == bounds;
/// <inheritdoc/>
public void Render(IDrawingContextImpl context)
{
if (Opacity.HasValue)
{
context.PushOpacity(Opacity.Value);
context.PushOpacity(Opacity.Value, Bounds);
}
else
{

48
src/Avalonia.Base/StyledElement.cs

@ -28,7 +28,7 @@ namespace Avalonia
public class StyledElement : Animatable,
IDataContextProvider,
ILogical,
IResourceHost,
IThemeVariantHost,
IStyleHost,
IStyleable,
ISetLogicalParent,
@ -75,23 +75,6 @@ namespace Avalonia
public static readonly StyledProperty<ControlTheme?> ThemeProperty =
AvaloniaProperty.Register<StyledElement, ControlTheme?>(nameof(Theme));
/// <summary>
/// Defines the <see cref="ActualThemeVariant"/> property.
/// </summary>
public static readonly StyledProperty<ThemeVariant> ActualThemeVariantProperty =
AvaloniaProperty.Register<StyledElement, ThemeVariant>(
nameof(ThemeVariant),
inherits: true,
defaultValue: ThemeVariant.Light);
/// <summary>
/// Defines the RequestedThemeVariant property.
/// </summary>
public static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty =
AvaloniaProperty.Register<StyledElement, ThemeVariant?>(
nameof(ThemeVariant),
defaultValue: ThemeVariant.Default);
private static readonly ControlTheme s_invalidTheme = new ControlTheme();
private int _initCount;
private string? _name;
@ -160,6 +143,9 @@ namespace Avalonia
/// </summary>
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
/// <inheritdoc />
public event EventHandler? ActualThemeVariantChanged;
/// <summary>
/// Gets or sets the name of the styled element.
/// </summary>
@ -278,15 +264,6 @@ namespace Avalonia
set => SetValue(ThemeProperty, value);
}
/// <summary>
/// Gets the UI theme that is currently used by the element, which might be different than the <see cref="RequestedThemeVariantProperty"/>.
/// </summary>
/// <returns>
/// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned.
/// Otherwise, current OS theme variant is returned.
/// </returns>
public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty);
/// <summary>
/// Gets the styled element's logical children.
/// </summary>
@ -325,6 +302,9 @@ namespace Avalonia
/// </summary>
public StyledElement? Parent { get; private set; }
/// <inheritdoc />
public ThemeVariant ActualThemeVariant => GetValue(ThemeVariant.ActualThemeVariantProperty);
/// <summary>
/// Gets the styled element's logical parent.
/// </summary>
@ -394,7 +374,7 @@ namespace Avalonia
/// </returns>
public bool ApplyStyling()
{
if (_initCount == 0 && (!_stylesApplied || !_themeApplied))
if (_initCount == 0 && (!_stylesApplied || !_themeApplied || !_templatedParentThemeApplied))
{
GetValueStore().BeginStyling();
@ -644,13 +624,19 @@ namespace Avalonia
base.OnPropertyChanged(change);
if (change.Property == ThemeProperty)
{
OnControlThemeChanged();
else if (change.Property == RequestedThemeVariantProperty)
}
else if (change.Property == ThemeVariant.RequestedThemeVariantProperty)
{
if (change.GetNewValue<ThemeVariant>() is {} themeVariant && themeVariant != ThemeVariant.Default)
SetValue(ActualThemeVariantProperty, themeVariant);
SetValue(ThemeVariant.ActualThemeVariantProperty, themeVariant);
else
ClearValue(ActualThemeVariantProperty);
ClearValue(ThemeVariant.ActualThemeVariantProperty);
}
else if (change.Property == ThemeVariant.ActualThemeVariantProperty)
{
ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty);
}
}

22
src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs

@ -1,22 +0,0 @@
using System;
using Avalonia.Controls;
using Avalonia.Metadata;
namespace Avalonia.Styling;
/// <summary>
/// Interface for an application host element with a root theme variant.
/// </summary>
[Unstable]
public interface IGlobalThemeVariantProvider : IResourceHost
{
/// <summary>
/// Gets the UI theme variant that is used by the control (and its child elements) for resource determination.
/// </summary>
ThemeVariant ActualThemeVariant { get; }
/// <summary>
/// Raised when the theme variant is changed on the element or an ancestor of the element.
/// </summary>
event EventHandler? ActualThemeVariantChanged;
}

26
src/Avalonia.Base/Styling/IThemeVariantHost.cs

@ -0,0 +1,26 @@
using System;
using Avalonia.Controls;
using Avalonia.Metadata;
namespace Avalonia.Styling;
/// <summary>
/// Interface for the host element with a theme variant.
/// </summary>
[Unstable]
public interface IThemeVariantHost : IResourceHost
{
/// <summary>
/// Gets the UI theme that is currently used by the element, which might be different than the RequestedThemeVariantProperty.
/// </summary>
/// <returns>
/// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned.
/// Otherwise, current OS theme variant is returned.
/// </returns>
ThemeVariant ActualThemeVariant { get; }
/// <summary>
/// Raised when the theme variant is changed on the element or an ancestor of the element.
/// </summary>
event EventHandler? ActualThemeVariantChanged;
}

17
src/Avalonia.Base/Styling/ThemeVariant.cs

@ -6,11 +6,26 @@ using Avalonia.Platform;
namespace Avalonia.Styling;
/// <summary>
/// Specifies a UI theme variant that should be used for the
/// Specifies a UI theme variant that should be used for the Control and Application types.
/// </summary>
[TypeConverter(typeof(ThemeVariantTypeConverter))]
public sealed record ThemeVariant
{
/// <summary>
/// Defines the ActualThemeVariant property.
/// </summary>
internal static readonly StyledProperty<ThemeVariant> ActualThemeVariantProperty =
AvaloniaProperty.Register<StyledElement, ThemeVariant>(
"ActualThemeVariant",
inherits: true);
/// <summary>
/// Defines the RequestedThemeVariant property.
/// </summary>
internal static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty =
AvaloniaProperty.Register<StyledElement, ThemeVariant?>(
"RequestedThemeVariant", defaultValue: Default);
/// <summary>
/// Creates a new instance of the <see cref="ThemeVariant"/>
/// </summary>

71
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -1,6 +1,48 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:collections="using:Avalonia.Collections">
<Styles.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="DataGridGridLinesBrush" Opacity="0.4" Color="{DynamicResource SystemBaseMediumLowColor}" />
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Default">
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="DataGridGridLinesBrush" Opacity="0.4" Color="{DynamicResource SystemBaseMediumLowColor}" />
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<x:Double x:Key="ListAccentLowOpacity">0.6</x:Double>
<x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double>
@ -9,19 +51,6 @@
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z</StreamGeometry>
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush"
Color="{DynamicResource SystemChromeMediumLowColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush"
Color="{DynamicResource SystemListMediumColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<StaticResource x:Key="DataGridRowBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
<SolidColorBrush x:Key="DataGridRowSelectedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
@ -29,23 +58,10 @@
<StaticResource x:Key="DataGridRowSelectedHoveredBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
<SolidColorBrush x:Key="DataGridRowSelectedUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
<SolidColorBrush x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundBrush"
Color="{DynamicResource SystemAccentColor}" />
<SolidColorBrush x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<StaticResource x:Key="DataGridCellBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="DataGridGridLinesBrush"
Opacity="0.4"
Color="{DynamicResource SystemBaseMediumLowColor}" />
<StaticResource x:Key="DataGridCurrencyVisualPrimaryBrush" ResourceKey="SystemControlTransparentBrush" />
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush"
Color="{DynamicResource SystemChromeMediumLowColor}" />
<StaticResource x:Key="DataGridFillerColumnGridLinesBrush" ResourceKey="SystemControlTransparentBrush" />
<ControlTheme x:Key="DataGridCellTextBlockTheme" TargetType="TextBlock">
@ -565,5 +581,6 @@
</Style>
</Style>
</ControlTheme>
</ResourceDictionary>
</Styles.Resources>
</Styles>

19
src/Avalonia.Controls/Application.cs

@ -29,7 +29,7 @@ namespace Avalonia
/// method.
/// - Tracks the lifetime of the application.
/// </remarks>
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IGlobalThemeVariantProvider, IApplicationPlatformEvents
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IThemeVariantHost, IApplicationPlatformEvents
{
/// <summary>
/// The application-global data templates.
@ -50,13 +50,13 @@ namespace Avalonia
public static readonly StyledProperty<object?> DataContextProperty =
StyledElement.DataContextProperty.AddOwner<Application>();
/// <inheritdoc cref="StyledElement.ActualThemeVariantProperty" />
/// <inheritdoc cref="ThemeVariantScope.ActualThemeVariantProperty" />
public static readonly StyledProperty<ThemeVariant> ActualThemeVariantProperty =
StyledElement.ActualThemeVariantProperty.AddOwner<Application>();
ThemeVariantScope.ActualThemeVariantProperty.AddOwner<Application>();
/// <inheritdoc cref="StyledElement.RequestedThemeVariantProperty" />
/// <inheritdoc cref="ThemeVariantScope.RequestedThemeVariantProperty" />
public static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty =
StyledElement.RequestedThemeVariantProperty.AddOwner<Application>();
ThemeVariantScope.RequestedThemeVariantProperty.AddOwner<Application>();
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
@ -95,11 +95,8 @@ namespace Avalonia
set => SetValue(RequestedThemeVariantProperty, value);
}
/// <inheritdoc cref="StyledElement.ActualThemeVariant"/>
public ThemeVariant ActualThemeVariant
{
get => GetValue(ActualThemeVariantProperty);
}
/// <inheritdoc />
public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty);
/// <summary>
/// Gets the current instance of the <see cref="Application"/> class.
@ -256,7 +253,7 @@ namespace Avalonia
.Bind<IAccessKeyHandler>().ToTransient<AccessKeyHandler>()
.Bind<IGlobalDataTemplates>().ToConstant(this)
.Bind<IGlobalStyles>().ToConstant(this)
.Bind<IGlobalThemeVariantProvider>().ToConstant(this)
.Bind<IThemeVariantHost>().ToConstant(this)
.Bind<IFocusManager>().ToConstant(FocusManager)
.Bind<IInputManager>().ToConstant(InputManager)
.Bind<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>()

2
src/Avalonia.Controls/Control.cs

@ -161,7 +161,7 @@ namespace Avalonia.Controls
get => GetValue(TagProperty);
set => SetValue(TagProperty, value);
}
/// <summary>
/// Occurs when the user has completed a context input gesture, such as a right-click.
/// </summary>

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

@ -387,6 +387,15 @@ namespace Avalonia.Controls.Primitives
popupHost.Transform = null;
}
if (popupHost is PopupRoot topLevelPopup)
{
topLevelPopup
.Bind(
ThemeVariantScope.ActualThemeVariantProperty,
this.GetBindingObservable(ThemeVariantScope.ActualThemeVariantProperty))
.DisposeWith(handlerCleanup);
}
UpdateHostPosition(popupHost, placementTarget);
SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,

8
src/Avalonia.Controls/ThemeVariantScope.cs

@ -7,6 +7,14 @@ namespace Avalonia.Controls
/// </summary>
public class ThemeVariantScope : Decorator
{
/// <inheritdoc cref="ThemeVariant.ActualThemeVariantProperty" />
public static readonly StyledProperty<ThemeVariant> ActualThemeVariantProperty =
ThemeVariant.ActualThemeVariantProperty.AddOwner<ThemeVariantScope>();
/// <inheritdoc cref="ThemeVariant.RequestedThemeVariantProperty" />
public static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty =
ThemeVariant.RequestedThemeVariantProperty.AddOwner<ThemeVariantScope>();
/// <summary>
/// Gets or sets the UI theme variant that is used by the control (and its child elements) for resource determination.
/// The UI theme you specify with ThemeVariant can override the app-level ThemeVariant.

64
src/Avalonia.Controls/TopLevel.cs

@ -80,6 +80,14 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty =
AvaloniaProperty.Register<TopLevel, IBrush>(nameof(TransparencyBackgroundFallback), Brushes.White);
/// <inheritdoc cref="ThemeVariantScope.ActualThemeVariantProperty" />
public static readonly StyledProperty<ThemeVariant> ActualThemeVariantProperty =
ThemeVariantScope.ActualThemeVariantProperty.AddOwner<Application>();
/// <inheritdoc cref="ThemeVariantScope.RequestedThemeVariantProperty" />
public static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty =
ThemeVariantScope.RequestedThemeVariantProperty.AddOwner<Application>();
/// <summary>
/// Defines the <see cref="BackRequested"/> event.
/// </summary>
@ -96,7 +104,7 @@ namespace Avalonia.Controls
private readonly IAccessKeyHandler? _accessKeyHandler;
private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler;
private readonly IGlobalStyles? _globalStyles;
private readonly IGlobalThemeVariantProvider? _applicationThemeHost;
private readonly IThemeVariantHost? _applicationThemeHost;
private readonly PointerOverPreProcessor? _pointerOverPreProcessor;
private readonly IDisposable? _pointerOverPreProcessorSubscription;
private readonly IDisposable? _backGestureSubscription;
@ -147,7 +155,7 @@ namespace Avalonia.Controls
_inputManager = TryGetService<IInputManager>(dependencyResolver);
_keyboardNavigationHandler = TryGetService<IKeyboardNavigationHandler>(dependencyResolver);
_globalStyles = TryGetService<IGlobalStyles>(dependencyResolver);
_applicationThemeHost = TryGetService<IGlobalThemeVariantProvider>(dependencyResolver);
_applicationThemeHost = TryGetService<IThemeVariantHost>(dependencyResolver);
Renderer = impl.CreateRenderer(this);
Renderer.SceneInvalidated += SceneInvalidated;
@ -405,7 +413,31 @@ namespace Avalonia.Controls
{
return visual?.VisualRoot as TopLevel;
}
/// <summary>
/// Requests a <see cref="PlatformInhibitionType"/> to be inhibited.
/// The behavior remains inhibited until the return value is disposed.
/// The available set of <see cref="PlatformInhibitionType"/>s depends on the platform.
/// If a behavior is inhibited on a platform where this type is not supported the request will have no effect.
/// </summary>
public async Task<IDisposable> RequestPlatformInhibition(PlatformInhibitionType type, string reason)
{
var platformBehaviorInhibition = PlatformImpl?.TryGetFeature<IPlatformBehaviorInhibition>();
if (platformBehaviorInhibition == null)
{
return Disposable.Create(() => { });
}
switch (type)
{
case PlatformInhibitionType.AppSleep:
await platformBehaviorInhibition.SetInhibitAppSleep(true, reason);
return Disposable.Create(() => platformBehaviorInhibition.SetInhibitAppSleep(false, reason).Wait());
default:
return Disposable.Create(() => { });
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
@ -571,30 +603,6 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
protected virtual void OnClosed(EventArgs e) => Closed?.Invoke(this, e);
/// <summary>
/// Requests a <see cref="PlatformInhibitionType"/> to be inhibited.
/// The behavior remains inhibited until the return value is disposed.
/// The available set of <see cref="PlatformInhibitionType"/>s depends on the platform.
/// If a behavior is inhibited on a platform where this type is not supported the request will have no effect.
/// </summary>
protected async Task<IDisposable> RequestPlatformInhibition(PlatformInhibitionType type, string reason)
{
var platformBehaviorInhibition = PlatformImpl?.TryGetFeature<IPlatformBehaviorInhibition>();
if (platformBehaviorInhibition == null)
{
return Disposable.Create(() => { });
}
switch (type)
{
case PlatformInhibitionType.AppSleep:
await platformBehaviorInhibition.SetInhibitAppSleep(true, reason);
return Disposable.Create(() => platformBehaviorInhibition.SetInhibitAppSleep(false, reason).Wait());
default:
return Disposable.Create(() => { });
}
}
/// <summary>
/// Tries to get a service from an <see cref="IAvaloniaDependencyResolver"/>, logging a
/// warning if not found.
@ -642,7 +650,7 @@ namespace Avalonia.Controls
private void GlobalActualThemeVariantChanged(object? sender, EventArgs e)
{
SetValue(ActualThemeVariantProperty, ((IGlobalThemeVariantProvider)sender!).ActualThemeVariant, BindingPriority.Template);
SetValue(ActualThemeVariantProperty, ((IThemeVariantHost)sender!).ActualThemeVariant, BindingPriority.Template);
}
private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e)

2
src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs

@ -14,7 +14,7 @@ namespace Avalonia.Diagnostics.Controls
public event EventHandler? Closed;
public static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty =
StyledElement.RequestedThemeVariantProperty.AddOwner<Application>();
ThemeVariantScope.RequestedThemeVariantProperty.AddOwner<Application>();
public Application(Avalonia.Application application)
{

26
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -5,16 +5,40 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<Import Project="..\..\build\TrimmingEnable.props" />
<ItemGroup>
<Compile Include="..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Tmds.DBus.Protocol" Version="0.13.0" />
<PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<PackageReference Include="Tmds.DBus" Version="0.9.0" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="DBusXml/DBus.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/StatusNotifierWatcher.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/com.canonical.AppMenu.Registrar.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputContext.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputMethod.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputContext1.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputMethod1.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.IBus.Portal.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.portal.FileChooser.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.portal.Request.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.portal.Settings.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/DBusMenu.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/StatusNotifierItem.xml" DBusGeneratorMode="Handler" />
</ItemGroup>
</Project>

13
src/Avalonia.FreeDesktop/DBusCallQueue.cs

@ -7,19 +7,20 @@ namespace Avalonia.FreeDesktop
class DBusCallQueue
{
private readonly Func<Exception, Task> _errorHandler;
private readonly Queue<Item> _q = new();
record Item(Func<Task> Callback)
private bool _processing;
private record Item(Func<Task> Callback)
{
public Action<Exception?>? OnFinish;
}
private Queue<Item> _q = new Queue<Item>();
private bool _processing;
public DBusCallQueue(Func<Exception, Task> errorHandler)
{
_errorHandler = errorHandler;
}
public void Enqueue(Func<Task> cb)
{
_q.Enqueue(new Item(cb));
@ -42,7 +43,7 @@ namespace Avalonia.FreeDesktop
Process();
return tcs.Task;
}
public Task<T> EnqueueAsync<T>(Func<Task<T>> cb)
{
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
@ -62,7 +63,7 @@ namespace Avalonia.FreeDesktop
return tcs.Task;
}
async void Process()
private async void Process()
{
if(_processing)
return;

32
src/Avalonia.FreeDesktop/DBusFileChooser.cs

@ -1,32 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
namespace Avalonia.FreeDesktop
{
[DBusInterface("org.freedesktop.portal.FileChooser")]
internal interface IFileChooser : IDBusObject
{
Task<ObjectPath> OpenFileAsync(string ParentWindow, string Title, IDictionary<string, object> Options);
Task<ObjectPath> SaveFileAsync(string ParentWindow, string Title, IDictionary<string, object> Options);
Task<ObjectPath> SaveFilesAsync(string ParentWindow, string Title, IDictionary<string, object> Options);
Task<T> GetAsync<T>(string prop);
Task<FileChooserProperties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
[Dictionary]
internal class FileChooserProperties
{
public uint Version { get; set; }
}
internal static class FileChooserExtensions
{
public static Task<uint> GetVersionAsync(this IFileChooser o) => o.GetAsync<uint>("version");
}
}

54
src/Avalonia.FreeDesktop/DBusHelper.cs

@ -1,51 +1,12 @@
using System;
using System.Threading;
using Avalonia.Logging;
using Avalonia.Threading;
using Tmds.DBus;
using Tmds.DBus.Protocol;
namespace Avalonia.FreeDesktop
{
internal static class DBusHelper
{
/// <summary>
/// This class uses synchronous execution at DBus connection establishment stage
/// then switches to using AvaloniaSynchronizationContext
/// </summary>
private class DBusSyncContext : SynchronizationContext
{
private readonly object _lock = new();
private SynchronizationContext? _ctx;
public override void Post(SendOrPostCallback d, object? state)
{
lock (_lock)
{
if (_ctx is not null)
_ctx?.Post(d, state);
else
d(state);
}
}
public override void Send(SendOrPostCallback d, object? state)
{
lock (_lock)
{
if (_ctx is not null)
_ctx?.Send(d, state);
else
d(state);
}
}
public void Initialized()
{
lock (_lock)
_ctx = new AvaloniaSynchronizationContext();
}
}
public static Connection? Connection { get; private set; }
public static Connection? TryInitialize(string? dbusAddress = null)
@ -56,19 +17,14 @@ namespace Avalonia.FreeDesktop
var oldContext = SynchronizationContext.Current;
try
{
var dbusContext = new DBusSyncContext();
SynchronizationContext.SetSynchronizationContext(dbusContext);
var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session)
var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session!)
{
AutoConnect = false,
SynchronizationContext = dbusContext
AutoConnect = false
});
// Connect synchronously
conn.ConnectAsync().Wait();
conn.ConnectAsync().GetAwaiter().GetResult();
// Initialize a brand new sync-context
dbusContext.Initialized();
Connection = conn;
}
catch (Exception e)

68
src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs

@ -5,7 +5,8 @@ using System.Threading.Tasks;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Logging;
using Tmds.DBus;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
namespace Avalonia.FreeDesktop.DBusIme
{
@ -46,20 +47,25 @@ namespace Avalonia.FreeDesktop.DBusIme
public DBusTextInputMethodBase(Connection connection, params string[] knownNames)
{
_queue = new DBusCallQueue(QueueOnError);
_queue = new DBusCallQueue(QueueOnErrorAsync);
Connection = connection;
_knownNames = knownNames;
Watch();
_ = WatchAsync();
}
public ITextInputMethodClient Client => _client;
public bool IsActive => _client != null;
public bool IsActive => _client is not null;
async void Watch()
private async Task WatchAsync()
{
foreach (var name in _knownNames)
_disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange));
{
var dbus = new OrgFreedesktopDBus(Connection, "org.freedesktop.DBus", "/org/freedesktop/DBus");
_disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange));
var nameOwner = await dbus.GetNameOwnerAsync(name);
OnNameChange(null, (name, null, nameOwner));
}
}
protected abstract Task<bool> Connect(string name);
@ -67,9 +73,12 @@ namespace Avalonia.FreeDesktop.DBusIme
protected string GetAppName() =>
Application.Current?.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia";
private async void OnNameChange(ServiceOwnerChangedEventArgs args)
private async void OnNameChange(Exception? e, (string ServiceName, string? OldOwner, string? NewOwner) args)
{
if (args.NewOwner != null && _currentName == null)
if (e is not null)
return;
if (args.NewOwner is not null && _currentName is null)
{
_onlineNamesQueue.Enqueue(args.ServiceName);
if (!_connecting)
@ -89,10 +98,10 @@ namespace Avalonia.FreeDesktop.DBusIme
return;
}
}
catch (Exception e)
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Error, "IME")
?.Log(this, "Unable to create IME input context:\n" + e);
?.Log(this, "Unable to create IME input context:\n" + ex);
}
}
}
@ -105,7 +114,7 @@ namespace Avalonia.FreeDesktop.DBusIme
}
// IME has crashed
if (args.NewOwner == null && args.ServiceName == _currentName)
if (args.NewOwner is null && args.ServiceName == _currentName)
{
_currentName = null;
foreach (var s in _disposables)
@ -116,11 +125,11 @@ namespace Avalonia.FreeDesktop.DBusIme
Reset();
// Watch again
Watch();
_ = WatchAsync();
}
}
protected virtual Task Disconnect()
protected virtual Task DisconnectAsync()
{
return Task.CompletedTask;
}
@ -136,13 +145,13 @@ namespace Avalonia.FreeDesktop.DBusIme
_imeActive = null;
}
async Task QueueOnError(Exception e)
private async Task QueueOnErrorAsync(Exception e)
{
Logger.TryGet(LogEventLevel.Error, "IME")
?.Log(this, "Error:\n" + e);
try
{
await Disconnect();
await DisconnectAsync();
}
catch (Exception ex)
{
@ -157,23 +166,16 @@ namespace Avalonia.FreeDesktop.DBusIme
protected void AddDisposable(IDisposable? d)
{
if(d is { })
if (d is { })
_disposables.Add(d);
}
public void Dispose()
{
foreach(var d in _disposables)
d.Dispose();
_disposables.Clear();
try
{
Disconnect().ContinueWith(_ => { });
}
catch
{
// fire and forget
}
_ = DisconnectAsync();
_currentName = null;
}
@ -182,13 +184,13 @@ namespace Avalonia.FreeDesktop.DBusIme
protected abstract Task ResetContextCore();
protected abstract Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode);
void UpdateActive()
private void UpdateActive()
{
_queue.Enqueue(async () =>
{
if(!IsConnected)
return;
var active = _windowActive && IsActive;
if (active != _imeActive)
{
@ -204,7 +206,7 @@ namespace Avalonia.FreeDesktop.DBusIme
_windowActive = active;
UpdateActive();
}
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
{
_client = client;
@ -227,7 +229,7 @@ namespace Avalonia.FreeDesktop.DBusIme
// Error, disconnect
catch (Exception e)
{
await QueueOnError(e);
await QueueOnErrorAsync(e);
return false;
}
}
@ -240,7 +242,7 @@ namespace Avalonia.FreeDesktop.DBusIme
}
protected void FireCommit(string s) => _onCommit?.Invoke(s);
private Action<X11InputMethodForwardedKey>? _onForward;
event Action<X11InputMethodForwardedKey> IX11InputMethodControl.ForwardKey
{
@ -249,8 +251,8 @@ namespace Avalonia.FreeDesktop.DBusIme
}
protected void FireForward(X11InputMethodForwardedKey k) => _onForward?.Invoke(k);
void UpdateCursorRect()
private void UpdateCursorRect()
{
_queue.Enqueue(async () =>
{
@ -265,7 +267,7 @@ namespace Avalonia.FreeDesktop.DBusIme
}
});
}
void IX11InputMethodControl.UpdateWindowInfo(PixelPoint position, double scaling)
{
_windowPosition = position;

69
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs

@ -1,69 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)]
namespace Avalonia.FreeDesktop.DBusIme.Fcitx
{
[DBusInterface("org.fcitx.Fcitx.InputMethod")]
interface IFcitxInputMethod : IDBusObject
{
Task<(int icid, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv3Async(
string Appname, int Pid);
}
[DBusInterface("org.fcitx.Fcitx.InputContext")]
interface IFcitxInputContext : IDBusObject
{
Task EnableICAsync();
Task CloseICAsync();
Task FocusInAsync();
Task FocusOutAsync();
Task ResetAsync();
Task MouseEventAsync(int X);
Task SetCursorLocationAsync(int X, int Y);
Task SetCursorRectAsync(int X, int Y, int W, int H);
Task SetCapacityAsync(uint Caps);
Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor);
Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
Task DestroyICAsync();
Task<int> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time);
Task<IDisposable> WatchEnableIMAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCloseIMAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable?> WatchCommitStringAsync(Action<string> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable?> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception>? onError = null);
}
[DBusInterface("org.fcitx.Fcitx.InputContext1")]
interface IFcitxInputContext1 : IDBusObject
{
Task FocusInAsync();
Task FocusOutAsync();
Task ResetAsync();
Task SetCursorRectAsync(int X, int Y, int W, int H);
Task SetCapabilityAsync(ulong Caps);
Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor);
Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
Task DestroyICAsync();
Task<bool> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time);
Task<IDisposable?> WatchCommitStringAsync(Action<string> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable?> WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception>? onError = null);
}
[DBusInterface("org.fcitx.Fcitx.InputMethod1")]
interface IFcitxInputMethod1 : IDBusObject
{
Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0);
}
}

64
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs

@ -2,45 +2,45 @@ using System;
namespace Avalonia.FreeDesktop.DBusIme.Fcitx
{
enum FcitxKeyEventType
internal enum FcitxKeyEventType
{
FCITX_PRESS_KEY,
FCITX_RELEASE_KEY
};
}
[Flags]
enum FcitxCapabilityFlags
internal enum FcitxCapabilityFlags
{
CAPACITY_NONE = 0,
CAPACITY_CLIENT_SIDE_UI = (1 << 0),
CAPACITY_PREEDIT = (1 << 1),
CAPACITY_CLIENT_SIDE_CONTROL_STATE = (1 << 2),
CAPACITY_PASSWORD = (1 << 3),
CAPACITY_FORMATTED_PREEDIT = (1 << 4),
CAPACITY_CLIENT_UNFOCUS_COMMIT = (1 << 5),
CAPACITY_SURROUNDING_TEXT = (1 << 6),
CAPACITY_EMAIL = (1 << 7),
CAPACITY_DIGIT = (1 << 8),
CAPACITY_UPPERCASE = (1 << 9),
CAPACITY_LOWERCASE = (1 << 10),
CAPACITY_NOAUTOUPPERCASE = (1 << 11),
CAPACITY_URL = (1 << 12),
CAPACITY_DIALABLE = (1 << 13),
CAPACITY_NUMBER = (1 << 14),
CAPACITY_NO_ON_SCREEN_KEYBOARD = (1 << 15),
CAPACITY_SPELLCHECK = (1 << 16),
CAPACITY_NO_SPELLCHECK = (1 << 17),
CAPACITY_WORD_COMPLETION = (1 << 18),
CAPACITY_UPPERCASE_WORDS = (1 << 19),
CAPACITY_UPPERCASE_SENTENCES = (1 << 20),
CAPACITY_ALPHA = (1 << 21),
CAPACITY_NAME = (1 << 22),
CAPACITY_GET_IM_INFO_ON_FOCUS = (1 << 23),
CAPACITY_RELATIVE_CURSOR_RECT = (1 << 24),
};
CAPACITY_CLIENT_SIDE_UI = 1 << 0,
CAPACITY_PREEDIT = 1 << 1,
CAPACITY_CLIENT_SIDE_CONTROL_STATE = 1 << 2,
CAPACITY_PASSWORD = 1 << 3,
CAPACITY_FORMATTED_PREEDIT = 1 << 4,
CAPACITY_CLIENT_UNFOCUS_COMMIT = 1 << 5,
CAPACITY_SURROUNDING_TEXT = 1 << 6,
CAPACITY_EMAIL = 1 << 7,
CAPACITY_DIGIT = 1 << 8,
CAPACITY_UPPERCASE = 1 << 9,
CAPACITY_LOWERCASE = 1 << 10,
CAPACITY_NOAUTOUPPERCASE = 1 << 11,
CAPACITY_URL = 1 << 12,
CAPACITY_DIALABLE = 1 << 13,
CAPACITY_NUMBER = 1 << 14,
CAPACITY_NO_ON_SCREEN_KEYBOARD = 1 << 15,
CAPACITY_SPELLCHECK = 1 << 16,
CAPACITY_NO_SPELLCHECK = 1 << 17,
CAPACITY_WORD_COMPLETION = 1 << 18,
CAPACITY_UPPERCASE_WORDS = 1 << 19,
CAPACITY_UPPERCASE_SENTENCES = 1 << 20,
CAPACITY_ALPHA = 1 << 21,
CAPACITY_NAME = 1 << 22,
CAPACITY_GET_IM_INFO_ON_FOCUS = 1 << 23,
CAPACITY_RELATIVE_CURSOR_RECT = 1 << 24
}
[Flags]
enum FcitxKeyState
internal enum FcitxKeyState
{
FcitxKeyState_None = 0,
FcitxKeyState_Shift = 1 << 0,
@ -63,5 +63,5 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
FcitxKeyState_Hyper = 1 << 27,
FcitxKeyState_Meta = 1 << 28,
FcitxKeyState_UsedMask = 0x5c001fff
};
}
}

33
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs

@ -1,19 +1,20 @@
using System;
using System.Threading.Tasks;
using Tmds.DBus.SourceGenerator;
namespace Avalonia.FreeDesktop.DBusIme.Fcitx
{
internal class FcitxICWrapper
{
private readonly IFcitxInputContext1? _modern;
private readonly IFcitxInputContext? _old;
private readonly OrgFcitxFcitxInputContext1? _modern;
private readonly OrgFcitxFcitxInputContext? _old;
public FcitxICWrapper(IFcitxInputContext old)
public FcitxICWrapper(OrgFcitxFcitxInputContext old)
{
_old = old;
}
public FcitxICWrapper(IFcitxInputContext1 modern)
public FcitxICWrapper(OrgFcitxFcitxInputContext1 modern)
{
_modern = modern;
}
@ -21,32 +22,30 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern?.FocusInAsync() ?? Task.CompletedTask;
public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern?.FocusOutAsync() ?? Task.CompletedTask;
public Task ResetAsync() => _old?.ResetAsync() ?? _modern?.ResetAsync() ?? Task.CompletedTask;
public Task SetCursorRectAsync(int x, int y, int w, int h) =>
_old?.SetCursorRectAsync(x, y, w, h) ?? _modern?.SetCursorRectAsync(x, y, w, h) ?? Task.CompletedTask;
public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern?.DestroyICAsync() ?? Task.CompletedTask;
public async Task<bool> ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time)
{
if(_old!=null)
if (_old is not null)
return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0;
return await (_modern?.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time) ?? Task.FromResult(false));
}
public Task<IDisposable?> WatchCommitStringAsync(Action<string> handler) =>
_old?.WatchCommitStringAsync(handler)
?? _modern?.WatchCommitStringAsync(handler)
?? Task.FromResult(default(IDisposable?));
public ValueTask<IDisposable?> WatchCommitStringAsync(Action<Exception?, string> handler) =>
_old?.WatchCommitStringAsync(handler)
?? _modern?.WatchCommitStringAsync(handler)
?? new ValueTask<IDisposable?>(default(IDisposable?));
public Task<IDisposable?> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler)
{
return _old?.WatchForwardKeyAsync(handler)
?? _modern?.WatchForwardKeyAsync(ev =>
handler((ev.keyval, ev.state, ev.type ? 1 : 0)))
?? Task.FromResult(default(IDisposable?));
}
public ValueTask<IDisposable?> WatchForwardKeyAsync(Action<Exception?, (uint keyval, uint state, int type)> handler) =>
_old?.WatchForwardKeyAsync(handler)
?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.keyval, ev.state, ev.type ? 1 : 0)))
?? new ValueTask<IDisposable?>(default(IDisposable?));
public Task SetCapacityAsync(uint flags) =>
_old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask;

52
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs

@ -1,11 +1,11 @@
using System;
using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Tmds.DBus;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
namespace Avalonia.FreeDesktop.DBusIme.Fcitx
{
@ -14,32 +14,24 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
private FcitxICWrapper? _context;
private FcitxCapabilityFlags? _lastReportedFlags;
public FcitxX11TextInputMethod(Connection connection) : base(connection,
"org.fcitx.Fcitx",
"org.freedesktop.portal.Fcitx"
)
{
}
public FcitxX11TextInputMethod(Connection connection) : base(connection, "org.fcitx.Fcitx", "org.freedesktop.portal.Fcitx") { }
protected override async Task<bool> Connect(string name)
{
if (name == "org.fcitx.Fcitx")
{
var method = Connection.CreateProxy<IFcitxInputMethod>(name, "/inputmethod");
var method = new OrgFcitxFcitxInputMethod(Connection, name, "/inputmethod");
var resp = await method.CreateICv3Async(GetAppName(),
Process.GetCurrentProcess().Id);
var proxy = Connection.CreateProxy<IFcitxInputContext>(name,
"/inputcontext_" + resp.icid);
var proxy = new OrgFcitxFcitxInputContext(Connection, name, $"/inputcontext_{resp.icid}");
_context = new FcitxICWrapper(proxy);
}
else
{
var method = Connection.CreateProxy<IFcitxInputMethod1>(name, "/inputmethod");
var method = new OrgFcitxFcitxInputMethod1(Connection, name, "/inputmethod");
var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) });
var proxy = Connection.CreateProxy<IFcitxInputContext1>(name, resp.path);
var proxy = new OrgFcitxFcitxInputContext1(Connection, name, resp.Item1);
_context = new FcitxICWrapper(proxy);
}
@ -48,7 +40,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
return true;
}
protected override Task Disconnect() => _context?.DestroyICAsync() ?? Task.CompletedTask;
protected override Task DisconnectAsync() => _context?.DestroyICAsync() ?? Task.CompletedTask;
protected override void OnDisconnected() => _context = null;
@ -63,14 +55,12 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
Math.Max(1, cursorRect.Height))
?? Task.CompletedTask;
protected override Task SetActiveCore(bool active)=> (active
protected override Task SetActiveCore(bool active)=> (active
? _context?.FocusInAsync()
: _context?.FocusOutAsync())
?? Task.CompletedTask;
protected override Task ResetContextCore() => _context?.ResetAsync()
?? Task.CompletedTask;
protected override Task ResetContextCore() => _context?.ResetAsync() ?? Task.CompletedTask;
protected override async Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
{
@ -87,17 +77,13 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
var type = args.Type == RawKeyEventType.KeyDown ?
FcitxKeyEventType.FCITX_PRESS_KEY :
FcitxKeyEventType.FCITX_RELEASE_KEY;
if (_context is { })
{
if (_context is not null)
return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type,
(uint)args.Timestamp).ConfigureAwait(false);
}
else
{
return false;
}
return false;
}
public override void SetOptions(TextInputOptions options) =>
Enqueue(async () =>
{
@ -127,7 +113,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
}
});
private void OnForward((uint keyval, uint state, int type) ev)
private void OnForward(Exception? e, (uint keyval, uint state, int type) ev)
{
var state = (FcitxKeyState)ev.state;
KeyModifiers mods = default;
@ -149,6 +135,12 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
});
}
private void OnCommitString(string s) => FireCommit(s);
private void OnCommitString(Exception? e, string s)
{
if (e is not null)
return;
FireCommit(s);
}
}
}

52
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs

@ -1,52 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
namespace Avalonia.FreeDesktop.DBusIme.IBus
{
[DBusInterface("org.freedesktop.IBus.InputContext")]
interface IIBusInputContext : IDBusObject
{
Task<bool> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State);
Task SetCursorLocationAsync(int X, int Y, int W, int H);
Task FocusInAsync();
Task FocusOutAsync();
Task ResetAsync();
Task SetCapabilitiesAsync(uint Caps);
Task PropertyActivateAsync(string Name, int State);
Task SetEngineAsync(string Name);
Task<object> GetEngineAsync();
Task DestroyAsync();
Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos);
Task<IDisposable> WatchCommitTextAsync(Action<object> cb, Action<Exception>? onError = null);
Task<IDisposable> WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchRequireSurroundingTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchShowPreeditTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchHidePreeditTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchShowAuxiliaryTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchHideAuxiliaryTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchShowLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchHideLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchPageUpLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchPageDownLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCursorUpLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCursorDownLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchRegisterPropertiesAsync(Action<object> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdatePropertyAsync(Action<object> handler, Action<Exception>? onError = null);
}
[DBusInterface("org.freedesktop.IBus.Portal")]
interface IIBusPortal : IDBusObject
{
Task<ObjectPath> CreateInputContextAsync(string Name);
}
}

4
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs

@ -18,7 +18,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
Button3Mask = 1 << 10,
Button4Mask = 1 << 11,
Button5Mask = 1 << 12,
HandledMask = 1 << 24,
ForwardMask = 1 << 25,
IgnoredMask = ForwardMask,
@ -40,6 +40,6 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
CapLookupTable = 1 << 2,
CapFocus = 1 << 3,
CapProperty = 1 << 4,
CapSurroundingText = 1 << 5,
CapSurroundingText = 1 << 5
}
}

61
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs

@ -1,36 +1,38 @@
using System.Collections.Generic;
using System;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Tmds.DBus;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
namespace Avalonia.FreeDesktop.DBusIme.IBus
{
internal class IBusX11TextInputMethod : DBusTextInputMethodBase
{
private IIBusInputContext? _context;
private OrgFreedesktopIBusService? _service;
private OrgFreedesktopIBusInputContext? _context;
public IBusX11TextInputMethod(Connection connection) : base(connection,
"org.freedesktop.portal.IBus")
{
}
public IBusX11TextInputMethod(Connection connection) : base(connection, "org.freedesktop.portal.IBus") { }
protected override async Task<bool> Connect(string name)
{
var path =
await Connection.CreateProxy<IIBusPortal>(name, "/org/freedesktop/IBus")
.CreateInputContextAsync(GetAppName());
_context = Connection.CreateProxy<IIBusInputContext>(name, path);
var portal = new OrgFreedesktopIBusPortal(Connection, name, "/org/freedesktop/IBus");
var path = await portal.CreateInputContextAsync(GetAppName());
_service = new OrgFreedesktopIBusService(Connection, name, path);
_context = new OrgFreedesktopIBusInputContext(Connection, name, path);
AddDisposable(await _context.WatchCommitTextAsync(OnCommitText));
AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey));
Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus));
return true;
}
private void OnForwardKey((uint keyval, uint keycode, uint state) k)
private void OnForwardKey(Exception? e, (uint keyval, uint keycode, uint state) k)
{
if (e is not null)
return;
var state = (IBusModifierMask)k.state;
KeyModifiers mods = default;
if (state.HasAllFlags(IBusModifierMask.ControlMask))
@ -49,28 +51,25 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
});
}
private void OnCommitText(object wtf)
private void OnCommitText(Exception? e, DBusVariantItem variantItem)
{
// Hello darkness, my old friend
if (wtf.GetType().GetField("Item3") is { } prop)
{
var text = prop.GetValue(wtf) as string;
if (!string.IsNullOrEmpty(text))
FireCommit(text!);
}
if (e is not null)
return;
if (variantItem.Value is DBusStructItem { Count: >= 3 } structItem && structItem[2] is DBusStringItem stringItem)
FireCommit(stringItem.Value);
}
protected override Task Disconnect() => _context?.DestroyAsync()
?? Task.CompletedTask;
protected override Task DisconnectAsync() => _service?.DestroyAsync() ?? Task.CompletedTask;
protected override void OnDisconnected()
{
_service = null;
_context = null;
base.OnDisconnected();
}
protected override Task SetCursorRectCore(PixelRect rect)
protected override Task SetCursorRectCore(PixelRect rect)
=> _context?.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height)
?? Task.CompletedTask;
@ -96,20 +95,12 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
if (args.Type == RawKeyEventType.KeyUp)
state |= IBusModifierMask.ReleaseMask;
if(_context is { })
{
return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state);
}
else
{
return Task.FromResult(false);
}
return _context is not null ? _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state) : Task.FromResult(false);
}
public override void SetOptions(TextInputOptions options)
{
// No-op, because ibus
// No-op, because ibus
}
}
}

25
src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs

@ -2,44 +2,43 @@ using System;
using System.Collections.Generic;
using Avalonia.FreeDesktop.DBusIme.Fcitx;
using Avalonia.FreeDesktop.DBusIme.IBus;
using Tmds.DBus;
using Tmds.DBus.Protocol;
namespace Avalonia.FreeDesktop.DBusIme
{
internal class X11DBusImeHelper
{
private static readonly Dictionary<string, Func<Connection, IX11InputMethodFactory>> KnownMethods =
new Dictionary<string, Func<Connection, IX11InputMethodFactory>>
private static readonly Dictionary<string, Func<Connection, IX11InputMethodFactory>> KnownMethods = new()
{
["fcitx"] = conn =>
["fcitx"] = static conn =>
new DBusInputMethodFactory<FcitxX11TextInputMethod>(_ => new FcitxX11TextInputMethod(conn)),
["ibus"] = conn =>
["ibus"] = static conn =>
new DBusInputMethodFactory<IBusX11TextInputMethod>(_ => new IBusX11TextInputMethod(conn))
};
static Func<Connection, IX11InputMethodFactory>? DetectInputMethod()
private static Func<Connection, IX11InputMethodFactory>? DetectInputMethod()
{
foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" })
{
var value = Environment.GetEnvironmentVariable(name);
if (value == "none")
return null;
if (value != null && KnownMethods.TryGetValue(value, out var factory))
if (value is not null && KnownMethods.TryGetValue(value, out var factory))
return factory;
}
return null;
}
public static bool DetectAndRegister()
{
var factory = DetectInputMethod();
if (factory != null)
if (factory is not null)
{
var conn = DBusHelper.TryInitialize();
if (conn != null)
if (conn is not null)
{
AvaloniaLocator.CurrentMutable.Bind<IX11InputMethodFactory>().ToConstant(factory(conn));
return true;

56
src/Avalonia.FreeDesktop/DBusMenu.cs

@ -1,56 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)]
namespace Avalonia.FreeDesktop.DBusMenu
{
[DBusInterface("org.freedesktop.DBus.Properties")]
interface IFreeDesktopDBusProperties : IDBusObject
{
Task<object> GetAsync(string prop);
Task<DBusMenuProperties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
[DBusInterface("com.canonical.dbusmenu")]
interface IDBusMenu : IFreeDesktopDBusProperties
{
Task<(uint revision, (int, KeyValuePair<string, object>[], object[]) layout)> GetLayoutAsync(int ParentId, int RecursionDepth, string[] PropertyNames);
Task<(int, KeyValuePair<string, object>[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames);
Task<object> GetPropertyAsync(int Id, string Name);
Task EventAsync(int Id, string EventId, object Data, uint Timestamp);
Task<int[]> EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events);
Task<bool> AboutToShowAsync(int Id);
Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids);
Task<IDisposable> WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception>? onError = null);
}
[Dictionary]
class DBusMenuProperties
{
public uint Version { get; set; } = default;
public string? TextDirection { get; set; } = default;
public string? Status { get; set; } = default;
public string[]? IconThemePath { get; set; } = default;
}
[DBusInterface("com.canonical.AppMenu.Registrar")]
interface IRegistrar : IDBusObject
{
Task RegisterWindowAsync(uint WindowId, ObjectPath MenuObjectPath);
Task UnregisterWindowAsync(uint WindowId);
Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId);
Task<(uint, string, ObjectPath)[]> GetMenusAsync();
Task<IDisposable> WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchWindowUnregisteredAsync(Action<uint> handler, Action<Exception>? onError = null);
}
}

373
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -2,97 +2,116 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using Avalonia.Reactive;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.FreeDesktop.DBusMenu;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.Threading;
using Tmds.DBus;
#pragma warning disable 1998
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
namespace Avalonia.FreeDesktop
{
internal class DBusMenuExporter
{
public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid)
{
if (DBusHelper.Connection == null)
return null;
public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) =>
DBusHelper.Connection is null ? null : new DBusMenuExporterImpl(DBusHelper.Connection, xid);
return new DBusMenuExporterImpl(DBusHelper.Connection, xid);
}
public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConnection)
{
return new DBusMenuExporterImpl(currentConnection, path);
}
public static INativeMenuExporter TryCreateDetachedNativeMenu(string path, Connection currentConnection) =>
new DBusMenuExporterImpl(currentConnection, path);
public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/"
+ Guid.NewGuid().ToString("N");
public static string GenerateDBusMenuObjPath => $"/net/avaloniaui/dbusmenu/{Guid.NewGuid():N}";
private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable
private class DBusMenuExporterImpl : ComCanonicalDbusmenu, ITopLevelNativeMenuExporter, IDisposable
{
private readonly Connection _dbus;
private readonly Dictionary<int, NativeMenuItemBase> _idsToItems = new();
private readonly Dictionary<NativeMenuItemBase, int> _itemsToIds = new();
private readonly HashSet<NativeMenu> _menus = new();
private readonly uint _xid;
private IRegistrar? _registrar;
private readonly bool _appMenu = true;
private ComCanonicalAppMenuRegistrar? _registrar;
private NativeMenu? _menu;
private bool _disposed;
private uint _revision = 1;
private NativeMenu? _menu;
private readonly Dictionary<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>();
private readonly Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>();
private readonly HashSet<NativeMenu> _menus = new HashSet<NativeMenu>();
private bool _resetQueued;
private int _nextId = 1;
private bool _appMenu = true;
public DBusMenuExporterImpl(Connection dbus, IntPtr xid)
public DBusMenuExporterImpl(Connection connection, IntPtr xid)
{
_dbus = dbus;
Connection = connection;
_xid = (uint)xid.ToInt32();
ObjectPath = GenerateDBusMenuObjPath;
Path = GenerateDBusMenuObjPath;
SetNativeMenu(new NativeMenu());
Init();
_ = InitializeAsync();
}
public DBusMenuExporterImpl(Connection dbus, ObjectPath path)
public DBusMenuExporterImpl(Connection connection, string path)
{
_dbus = dbus;
Connection = connection;
_appMenu = false;
ObjectPath = path;
Path = path;
SetNativeMenu(new NativeMenu());
Init();
_ = InitializeAsync();
}
async void Init()
protected override Connection Connection { get; }
public override string Path { get; }
protected override (uint revision, (int, Dictionary<string, DBusVariantItem>, DBusVariantItem[]) layout) OnGetLayout(int parentId, int recursionDepth, string[] propertyNames)
{
var menu = GetMenu(parentId);
var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames);
if (!IsNativeMenuExported)
{
IsNativeMenuExported = true;
Dispatcher.UIThread.Post(() => OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty));
}
return (_revision, layout);
}
protected override (int, Dictionary<string, DBusVariantItem>)[] OnGetGroupProperties(int[] ids, string[] propertyNames) =>
ids.Select(id => (id, GetProperties(GetMenu(id), propertyNames))).ToArray();
protected override DBusVariantItem OnGetProperty(int id, string name) => GetProperty(GetMenu(id), name) ?? new DBusVariantItem("i", new DBusInt32Item(0));
protected override void OnEvent(int id, string eventId, DBusVariantItem data, uint timestamp) =>
Dispatcher.UIThread.Post(() => HandleEvent(id, eventId));
protected override int[] OnEventGroup((int, string, DBusVariantItem, uint)[] events)
{
foreach (var e in events)
Dispatcher.UIThread.Post(() => HandleEvent(e.Item1, e.Item2));
return Array.Empty<int>();
}
protected override bool OnAboutToShow(int id) => false;
protected override (int[] updatesNeeded, int[] idErrors) OnAboutToShowGroup(int[] ids) =>
(Array.Empty<int>(), Array.Empty<int>());
private async Task InitializeAsync()
{
Connection.AddMethodHandler(this);
if (!_appMenu)
return;
_registrar = new ComCanonicalAppMenuRegistrar(Connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar");
try
{
if (_appMenu)
{
await _dbus.RegisterObjectAsync(this);
_registrar = DBusHelper.Connection?.CreateProxy<IRegistrar>(
"com.canonical.AppMenu.Registrar",
"/com/canonical/AppMenu/Registrar");
if (!_disposed && _registrar is { })
await _registrar.RegisterWindowAsync(_xid, ObjectPath);
}
else
{
await _dbus.RegisterObjectAsync(this);
}
if (!_disposed)
await _registrar.RegisterWindowAsync(_xid, Path);
}
catch (Exception e)
catch
{
Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.X11Platform)
?.Log(this, e.Message);
// It's not really important if this code succeeds,
// and it's not important to know if it succeeds
// since even if we register the window it's not guaranteed that
// menu will be actually exported
_registrar = null;
}
}
@ -101,29 +120,28 @@ namespace Avalonia.FreeDesktop
if (_disposed)
return;
_disposed = true;
_dbus.UnregisterObject(this);
// Fire and forget
_registrar?.UnregisterWindowAsync(_xid);
_ = _registrar?.UnregisterWindowAsync(_xid);
}
public bool IsNativeMenuExported { get; private set; }
public event EventHandler? OnIsNativeMenuExportedChanged;
public void SetNativeMenu(NativeMenu? menu)
{
if (menu == null)
menu = new NativeMenu();
menu ??= new NativeMenu();
if (_menu != null)
if (_menu is not null)
((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged;
_menu = menu;
((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged;
DoLayoutReset();
}
/*
This is basic initial implementation, so we don't actually track anything and
just reset the whole layout on *ANY* change
@ -131,10 +149,10 @@ namespace Avalonia.FreeDesktop
This is not how it should work and will prevent us from implementing various features,
but that's the fastest way to get things working, so...
*/
void DoLayoutReset()
private void DoLayoutReset()
{
_resetQueued = false;
foreach (var i in _idsToItems.Values)
foreach (var i in _idsToItems.Values)
i.PropertyChanged -= OnItemPropertyChanged;
foreach(var menu in _menus)
((INotifyCollectionChanged)menu.Items).CollectionChanged -= OnMenuItemsChanged;
@ -142,10 +160,10 @@ namespace Avalonia.FreeDesktop
_idsToItems.Clear();
_itemsToIds.Clear();
_revision++;
LayoutUpdated?.Invoke((_revision, 0));
EmitLayoutUpdated(_revision, 0);
}
void QueueReset()
private void QueueReset()
{
if(_resetQueued)
return;
@ -163,10 +181,10 @@ namespace Avalonia.FreeDesktop
private void EnsureSubscribed(NativeMenu? menu)
{
if(menu!=null && _menus.Add(menu))
if (menu is not null && _menus.Add(menu))
((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged;
}
private int GetId(NativeMenuItemBase item)
{
if (_itemsToIds.TryGetValue(item, out var id))
@ -190,258 +208,137 @@ namespace Avalonia.FreeDesktop
QueueReset();
}
public ObjectPath ObjectPath { get; }
async Task<object> IFreeDesktopDBusProperties.GetAsync(string prop)
{
if (prop == "Version")
return 2;
if (prop == "Status")
return "normal";
return 0;
}
async Task<DBusMenuProperties> IFreeDesktopDBusProperties.GetAllAsync()
{
return new DBusMenuProperties
{
Version = 2,
Status = "normal",
};
}
private static string[] AllProperties = new[]
{
private static readonly string[] s_allProperties = {
"type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data"
};
object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name)
private static DBusVariantItem? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name)
{
var (it, menu) = i;
if (it is NativeMenuItemSeparator)
{
if (name == "type")
return "separator";
return new DBusVariantItem("s", new DBusStringItem("separator"));
}
else if (it is NativeMenuItem item)
{
if (name == "type")
{
return null;
}
if (name == "label")
return item?.Header ?? "<null>";
return new DBusVariantItem("s", new DBusStringItem(item.Header ?? "<null>"));
if (name == "enabled")
{
if (item == null)
return null;
if (item.Menu != null && item.Menu.Items.Count == 0)
return false;
if (item.IsEnabled == false)
return false;
if (item.Menu is not null && item.Menu.Items.Count == 0)
return new DBusVariantItem("b", new DBusBoolItem(false));
if (!item.IsEnabled)
return new DBusVariantItem("b", new DBusBoolItem(false));
return null;
}
if (name == "shortcut")
{
if (item?.Gesture == null)
if (item.Gesture is null)
return null;
if (item.Gesture.KeyModifiers == 0)
return null;
var lst = new List<string>();
var lst = new List<DBusItem>();
var mod = item.Gesture;
if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Control))
lst.Add("Control");
lst.Add(new DBusStringItem("Control"));
if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Alt))
lst.Add("Alt");
lst.Add(new DBusStringItem("Alt"));
if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Shift))
lst.Add("Shift");
lst.Add(new DBusStringItem("Shift"));
if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Meta))
lst.Add("Super");
lst.Add(item.Gesture.Key.ToString());
return new[] { lst.ToArray() };
lst.Add(new DBusStringItem("Super"));
lst.Add(new DBusStringItem(item.Gesture.Key.ToString()));
return new DBusVariantItem("aas", new DBusArrayItem(DBusType.Array, new[] { new DBusArrayItem(DBusType.String, lst) }));
}
if (name == "toggle-type")
{
if (item.ToggleType == NativeMenuItemToggleType.CheckBox)
return "checkmark";
return new DBusVariantItem("s", new DBusStringItem("checkmark"));
if (item.ToggleType == NativeMenuItemToggleType.Radio)
return "radio";
return new DBusVariantItem("s", new DBusStringItem("radio"));
}
if (name == "toggle-state")
{
if (item.ToggleType != NativeMenuItemToggleType.None)
return item.IsChecked ? 1 : 0;
}
if (name == "toggle-state" && item.ToggleType != NativeMenuItemToggleType.None)
return new DBusVariantItem("i", new DBusInt32Item(item.IsChecked ? 1 : 0));
if (name == "icon-data")
{
if (item.Icon != null)
if (item.Icon is not null)
{
var loader = AvaloniaLocator.Current.GetService<IPlatformIconLoader>();
if (loader != null)
if (loader is not null)
{
var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item);
using var ms = new MemoryStream();
icon.Save(ms);
return ms.ToArray();
return new DBusVariantItem("ay",
new DBusArrayItem(DBusType.Byte, ms.ToArray().Select(static x => new DBusByteItem(x))));
}
}
}
if (name == "children-display")
return menu != null ? "submenu" : null;
return menu is not null ? new DBusVariantItem("s", new DBusStringItem("submenu")) : null;
}
return null;
}
private List<KeyValuePair<string, object>> _reusablePropertyList = new List<KeyValuePair<string, object>>();
KeyValuePair<string, object>[] GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names)
private static Dictionary<string, DBusVariantItem> GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names)
{
if (names?.Length > 0 != true)
names = AllProperties;
_reusablePropertyList.Clear();
if (names.Length == 0)
names = s_allProperties;
var properties = new Dictionary<string, DBusVariantItem>();
foreach (var n in names)
{
var v = GetProperty(i, n);
if (v != null)
_reusablePropertyList.Add(new KeyValuePair<string, object>(n, v));
if (v is not null)
properties.Add(n, v);
}
return _reusablePropertyList.ToArray();
}
public Task SetAsync(string prop, object val) => Task.CompletedTask;
public Task<(uint revision, (int, KeyValuePair<string, object>[], object[]) layout)> GetLayoutAsync(
int ParentId, int RecursionDepth, string[] PropertyNames)
{
var menu = GetMenu(ParentId);
var rv = (_revision, GetLayout(menu.item, menu.menu, RecursionDepth, PropertyNames));
if (!IsNativeMenuExported)
{
IsNativeMenuExported = true;
Dispatcher.UIThread.Post(() =>
{
OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty);
});
}
return Task.FromResult(rv);
return properties;
}
(int, KeyValuePair<string, object>[], object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames)
private (int, Dictionary<string, DBusVariantItem>, DBusVariantItem[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames)
{
var id = item == null ? 0 : GetId(item);
var id = item is null ? 0 : GetId(item);
var props = GetProperties((item, menu), propertyNames);
var children = (depth == 0 || menu == null) ? Array.Empty<object>() : new object[menu.Items.Count];
if(menu != null)
var children = depth == 0 || menu is null ? Array.Empty<DBusVariantItem>() : new DBusVariantItem[menu.Items.Count];
if (menu is not null)
{
for (var c = 0; c < children.Length; c++)
{
var ch = menu.Items[c];
children[c] = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames);
var layout = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames);
children[c] = new DBusVariantItem("(ia{sv}av)", new DBusStructItem(new DBusItem[]
{
new DBusInt32Item(layout.Item1),
new DBusArrayItem(DBusType.DictEntry, layout.Item2.Select(static x => new DBusDictEntryItem(new DBusStringItem(x.Key), x.Value))),
new DBusArrayItem(DBusType.Variant, layout.Item3)
}));
}
return (id, props, children);
}
public Task<(int, KeyValuePair<string, object>[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames)
{
var arr = new (int, KeyValuePair<string, object>[])[Ids.Length];
for (var c = 0; c < Ids.Length; c++)
{
var id = Ids[c];
var item = GetMenu(id);
var props = GetProperties(item, PropertyNames);
arr[c] = (id, props);
}
return Task.FromResult(arr);
}
public async Task<object> GetPropertyAsync(int Id, string Name)
{
return GetProperty(GetMenu(Id), Name) ?? 0;
return (id, props, children);
}
public void HandleEvent(int id, string eventId, object data, uint timestamp)
private void HandleEvent(int id, string eventId)
{
if (eventId == "clicked")
{
var item = GetMenu(id).item;
if (item is NativeMenuItem menuItem && item is INativeMenuItemExporterEventsImplBridge bridge)
{
if (menuItem?.IsEnabled == true)
bridge?.RaiseClicked();
}
if (item is NativeMenuItem { IsEnabled: true } and INativeMenuItemExporterEventsImplBridge bridge)
bridge.RaiseClicked();
}
}
public Task EventAsync(int Id, string EventId, object Data, uint Timestamp)
{
HandleEvent(Id, EventId, Data, Timestamp);
return Task.CompletedTask;
}
public Task<int[]> EventGroupAsync((int id, string eventId, object data, uint timestamp)[] Events)
{
foreach (var e in Events)
HandleEvent(e.id, e.eventId, e.data, e.timestamp);
return Task.FromResult(Array.Empty<int>());
}
public async Task<bool> AboutToShowAsync(int Id)
{
return false;
}
public async Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids)
{
return (Array.Empty<int>(), Array.Empty<int>());
}
#region Events
private event Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)>
ItemsPropertiesUpdated { add { } remove { } }
private event Action<(uint revision, int parent)>? LayoutUpdated;
private event Action<(int id, uint timestamp)> ItemActivationRequested { add { } remove { } }
private event Action<PropertyChanges> PropertiesChanged { add { } remove { } }
async Task<IDisposable> IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception>? onError)
{
ItemsPropertiesUpdated += handler;
return Disposable.Create(() => ItemsPropertiesUpdated -= handler);
}
async Task<IDisposable> IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception>? onError)
{
LayoutUpdated += handler;
return Disposable.Create(() => LayoutUpdated -= handler);
}
async Task<IDisposable> IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception>? onError)
{
ItemActivationRequested+= handler;
return Disposable.Create(() => ItemActivationRequested -= handler);
}
async Task<IDisposable> IFreeDesktopDBusProperties.WatchPropertiesAsync(Action<PropertyChanges> handler)
{
PropertiesChanged += handler;
return Disposable.Create(() => PropertiesChanged -= handler);
}
#endregion
}
}
}

85
src/Avalonia.FreeDesktop/DBusPlatformSettings.cs

@ -2,44 +2,35 @@
using System.Threading.Tasks;
using Avalonia.Logging;
using Avalonia.Platform;
using Tmds.DBus.SourceGenerator;
namespace Avalonia.FreeDesktop;
internal class DBusPlatformSettings : DefaultPlatformSettings
namespace Avalonia.FreeDesktop
{
private readonly IDBusSettings? _settings;
private PlatformColorValues? _lastColorValues;
public DBusPlatformSettings()
internal class DBusPlatformSettings : DefaultPlatformSettings
{
_settings = DBusHelper.TryInitialize()?
.CreateProxy<IDBusSettings>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
private readonly OrgFreedesktopPortalSettings? _settings;
private PlatformColorValues? _lastColorValues;
if (_settings is not null)
public DBusPlatformSettings()
{
_ = _settings.WatchSettingChangedAsync(SettingsChangedHandler);
if (DBusHelper.Connection is null)
return;
_ = TryGetInitialValue();
_settings = new OrgFreedesktopPortalSettings(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
_ = _settings.WatchSettingChangedAsync(SettingsChangedHandler);
_ = TryGetInitialValueAsync();
}
}
public override PlatformColorValues GetColorValues()
{
return _lastColorValues ?? base.GetColorValues();
}
private async Task TryGetInitialValue()
{
var colorSchemeTask = _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme");
if (colorSchemeTask.Status == TaskStatus.RanToCompletion)
public override PlatformColorValues GetColorValues()
{
_lastColorValues = GetColorValuesFromSetting(colorSchemeTask.Result);
return _lastColorValues ?? base.GetColorValues();
}
else
private async Task TryGetInitialValueAsync()
{
try
{
var value = await colorSchemeTask;
var value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme");
_lastColorValues = GetColorValuesFromSetting(value);
OnColorValuesChanged(_lastColorValues);
}
@ -49,29 +40,31 @@ internal class DBusPlatformSettings : DefaultPlatformSettings
Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get setting value", ex);
}
}
}
private void SettingsChangedHandler((string @namespace, string key, object value) tuple)
{
if (tuple.@namespace == "org.freedesktop.appearance"
&& tuple.key == "color-scheme")
private void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple)
{
/*
<member>0: No preference</member>
<member>1: Prefer dark appearance</member>
<member>2: Prefer light appearance</member>
*/
_lastColorValues = GetColorValuesFromSetting(tuple.value);
OnColorValuesChanged(_lastColorValues);
if (exception is not null)
return;
if (valueTuple is ("org.freedesktop.appearance", "color-scheme", { } value))
{
/*
<member>0: No preference</member>
<member>1: Prefer dark appearance</member>
<member>2: Prefer light appearance</member>
*/
_lastColorValues = GetColorValuesFromSetting(value);
OnColorValuesChanged(_lastColorValues);
}
}
}
private static PlatformColorValues GetColorValuesFromSetting(object value)
{
var isDark = value?.ToString() == "1";
return new PlatformColorValues
private static PlatformColorValues GetColorValuesFromSetting(DBusVariantItem value)
{
ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light
};
var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1;
return new PlatformColorValues
{
ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light
};
}
}
}

16
src/Avalonia.FreeDesktop/DBusRequest.cs

@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
namespace Avalonia.FreeDesktop
{
[DBusInterface("org.freedesktop.portal.Request")]
internal interface IRequest : IDBusObject
{
Task CloseAsync();
Task<IDisposable> WatchResponseAsync(Action<(uint response, IDictionary<string, object> results)> handler, Action<Exception>? onError = null);
}
}

16
src/Avalonia.FreeDesktop/DBusSettings.cs

@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Tmds.DBus;
namespace Avalonia.FreeDesktop;
[DBusInterface("org.freedesktop.portal.Settings")]
internal interface IDBusSettings : IDBusObject
{
Task<(string @namespace, IDictionary<string, object>)> ReadAllAsync(string[] namespaces);
Task<object> ReadAsync(string @namespace, string key);
Task<IDisposable> WatchSettingChangedAsync(Action<(string @namespace, string key, object value)> handler, Action<Exception>? onError = null);
}

171
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -2,46 +2,42 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Logging;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Tmds.DBus;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
namespace Avalonia.FreeDesktop
{
internal class DBusSystemDialog : BclStorageProvider
{
private static readonly Lazy<IFileChooser?> s_fileChooser = new(() => DBusHelper.Connection?
.CreateProxy<IFileChooser>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"));
internal static async Task<IStorageProvider?> TryCreate(IPlatformHandle handle)
internal static async Task<IStorageProvider?> TryCreateAsync(IPlatformHandle handle)
{
if (handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser)
if (DBusHelper.Connection is null)
return null;
var dbusFileChooser = new OrgFreedesktopPortalFileChooser(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
try
{
try
{
await fileChooser.GetVersionAsync();
return new DBusSystemDialog(fileChooser, handle);
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}");
return null;
}
await dbusFileChooser.GetVersionAsync();
}
catch
{
return null;
}
return null;
return new DBusSystemDialog(DBusHelper.Connection, handle, dbusFileChooser);
}
private readonly IFileChooser _fileChooser;
private readonly Connection _connection;
private readonly OrgFreedesktopPortalFileChooser _fileChooser;
private readonly IPlatformHandle _handle;
private DBusSystemDialog(IFileChooser fileChooser, IPlatformHandle handle)
private DBusSystemDialog(Connection connection, IPlatformHandle handle, OrgFreedesktopPortalFileChooser fileChooser)
{
_connection = connection;
_fileChooser = fileChooser;
_handle = handle;
}
@ -56,115 +52,124 @@ namespace Avalonia.FreeDesktop
{
var parentWindow = $"x11:{_handle.Handle:X}";
ObjectPath objectPath;
var chooserOptions = new Dictionary<string, object>();
var chooserOptions = new Dictionary<string, DBusVariantItem>();
var filters = ParseFilters(options.FileTypeFilter);
if (filters.Any())
{
if (filters is not null)
chooserOptions.Add("filters", filters);
}
chooserOptions.Add("multiple", options.AllowMultiple);
chooserOptions.Add("multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple)));
objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath);
var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath);
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
var uris = await tsc.Task ?? Array.Empty<string>();
using var disposable = await request.WatchResponseAsync((e, x) =>
{
if (e is not null)
return;
tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray());
});
return uris.Select(path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList();
var uris = await tsc.Task ?? Array.Empty<string>();
return uris.Select(static path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList();
}
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var parentWindow = $"x11:{_handle.Handle:X}";
ObjectPath objectPath;
var chooserOptions = new Dictionary<string, object>();
var chooserOptions = new Dictionary<string, DBusVariantItem>();
var filters = ParseFilters(options.FileTypeChoices);
if (filters.Any())
{
if (filters is not null)
chooserOptions.Add("filters", filters);
}
if (options.SuggestedFileName is { } currentName)
chooserOptions.Add("current_name", currentName);
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(folderPath));
objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
chooserOptions.Add("current_name", new DBusVariantItem("s", new DBusStringItem(currentName)));
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", new DBusVariantItem("s", new DBusStringItem(folderPath)));
var request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath);
objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath);
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
using var disposable = await request.WatchResponseAsync((e, x) =>
{
if (e is not null)
return;
tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray());
});
var uris = await tsc.Task;
var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).LocalPath : null;
if (path is null)
{
return null;
}
else
{
// WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually.
path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, null);
return new BclStorageFile(new FileInfo(path));
}
// WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually.
path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, null);
return new BclStorageFile(new FileInfo(path));
}
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
var parentWindow = $"x11:{_handle.Handle:X}";
var chooserOptions = new Dictionary<string, object>
var chooserOptions = new Dictionary<string, DBusVariantItem>
{
{ "directory", true },
{ "multiple", options.AllowMultiple }
{ "directory", new DBusVariantItem("b", new DBusBoolItem(true)) },
{ "multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple)) }
};
var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath);
var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath);
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
var uris = await tsc.Task ?? Array.Empty<string>();
using var disposable = await request.WatchResponseAsync((e, x) =>
{
if (e is not null)
return;
tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray());
});
var uris = await tsc.Task ?? Array.Empty<string>();
return uris
.Select(path => new Uri(path).LocalPath)
.Select(static path => new Uri(path).LocalPath)
// WSL2 freedesktop allows to select files as well in directory picker, filter it out.
.Where(Directory.Exists)
.Select(path => new BclStorageFolder(new DirectoryInfo(path))).ToList();
.Select(static path => new BclStorageFolder(new DirectoryInfo(path))).ToList();
}
private static (string name, (uint style, string extension)[])[] ParseFilters(IReadOnlyList<FilePickerFileType>? fileTypes)
private static DBusVariantItem? ParseFilters(IReadOnlyList<FilePickerFileType>? fileTypes)
{
// Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]
const uint GlobStyle = 0u;
const uint MimeStyle = 1u;
// Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]
if (fileTypes is null)
{
return Array.Empty<(string name, (uint style, string extension)[])>();
}
return null;
var filters = new DBusArrayItem(DBusType.Struct, new List<DBusItem>());
var filters = new List<(string name, (uint style, string extension)[])>();
foreach (var fileType in fileTypes)
{
const uint globStyle = 0u;
const uint mimeStyle = 1u;
var extensions = Enumerable.Empty<(uint, string)>();
if (fileType.Patterns is { } patterns)
{
extensions = extensions.Concat(patterns.Select(static x => (globStyle, x)));
}
else if (fileType.MimeTypes is { } mimeTypes)
{
extensions = extensions.Concat(mimeTypes.Select(static x => (mimeStyle, x)));
}
if (extensions.Any())
{
filters.Add((fileType.Name, extensions.ToArray()));
}
var extensions = new List<DBusItem>();
if (fileType.Patterns?.Count > 0)
extensions.AddRange(
fileType.Patterns.Select(static pattern =>
new DBusStructItem(new DBusItem[] { new DBusUInt32Item(GlobStyle), new DBusStringItem(pattern) })));
else if (fileType.MimeTypes?.Count > 0)
extensions.AddRange(
fileType.MimeTypes.Select(static mimeType =>
new DBusStructItem(new DBusItem[] { new DBusUInt32Item(MimeStyle), new DBusStringItem(mimeType) })));
else
continue;
filters.Add(new DBusStructItem(
new DBusItem[]
{
new DBusStringItem(fileType.Name),
new DBusArrayItem(DBusType.Struct, extensions)
}));
}
return filters.ToArray();
return filters.Count > 0 ? new DBusVariantItem("a(sa(us))", filters) : null;
}
}
}

316
src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs

@ -1,30 +1,26 @@
#nullable enable
using System;
using System;
using System.Diagnostics;
using Avalonia.Reactive;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Logging;
using Avalonia.Platform;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
namespace Avalonia.FreeDesktop
{
internal class DBusTrayIconImpl : ITrayIconImpl
{
private static int s_trayIconInstanceId;
public static readonly (int, int, byte[]) EmptyPixmap = (1, 1, new byte[] { 255, 0, 0, 0 });
private readonly ObjectPath _dbusMenuPath;
private readonly Connection? _connection;
private IDisposable? _serviceWatchDisposable;
private readonly OrgFreedesktopDBus? _dBus;
private IDisposable? _serviceWatchDisposable;
private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj;
private IStatusNotifierWatcher? _statusNotifierWatcher;
private DbusPixmap _icon;
private OrgKdeStatusNotifierWatcher? _statusNotifierWatcher;
private (int, int, byte[]) _icon;
private string? _sysTrayServiceName;
private string? _tooltipText;
@ -51,6 +47,7 @@ namespace Avalonia.FreeDesktop
IsActive = true;
_dBus = new OrgFreedesktopDBus(_connection, "org.freedesktop.DBus", "/org/freedesktop/DBus");
_dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath;
MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection);
@ -60,23 +57,10 @@ namespace Avalonia.FreeDesktop
private void InitializeSNWService()
{
if (_connection is null || _isDisposed) return;
try
{
_statusNotifierWatcher = _connection.CreateProxy<IStatusNotifierWatcher>(
"org.kde.StatusNotifierWatcher",
"/StatusNotifierWatcher");
}
catch
{
Logger.TryGet(LogEventLevel.Error, "DBUS")
?.Log(this,
"org.kde.StatusNotifierWatcher service is not available on this system. Tray Icons will not work without it.");
if (_connection is null || _isDisposed)
return;
}
_statusNotifierWatcher = new OrgKdeStatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher");
_serviceConnected = true;
}
@ -84,23 +68,24 @@ namespace Avalonia.FreeDesktop
{
try
{
_serviceWatchDisposable =
await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!;
_serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.Item2));
var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher");
OnNameChange(nameOwner);
}
catch (Exception e)
catch
{
_serviceWatchDisposable = null;
Logger.TryGet(LogEventLevel.Error, "DBUS")
?.Log(this,
$"Unable to hook watcher method on org.kde.StatusNotifierWatcher: {e}");
?.Log(this, "Interface 'org.kde.StatusNotifierWatcher' is unavailable.");
}
}
private void OnNameChange(ServiceOwnerChangedEventArgs obj)
private void OnNameChange(string? newOwner)
{
if (_isDisposed)
return;
if (!_serviceConnected & obj.NewOwner != null)
if (!_serviceConnected & newOwner is not null)
{
_serviceConnected = true;
InitializeSNWService();
@ -108,55 +93,45 @@ namespace Avalonia.FreeDesktop
DestroyTrayIcon();
if (_isVisible)
{
CreateTrayIcon();
}
}
else if (_serviceConnected & obj.NewOwner is null)
else if (_serviceConnected & newOwner is null)
{
DestroyTrayIcon();
_serviceConnected = false;
}
}
private void CreateTrayIcon()
private async void CreateTrayIcon()
{
if (_connection is null || !_serviceConnected || _isDisposed)
if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierWatcher is null)
return;
#if NET5_0_OR_GREATER
var pid = Environment.ProcessId;
#else
var pid = Process.GetCurrentProcess().Id;
#endif
var tid = s_trayIconInstanceId++;
_sysTrayServiceName = FormattableString.Invariant($"org.kde.StatusNotifierItem-{pid}-{tid}");
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath);
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, _dbusMenuPath);
try
{
_connection.RegisterObjectAsync(_statusNotifierItemDbusObj);
_connection.RegisterServiceAsync(_sysTrayServiceName);
_statusNotifierWatcher?.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, "DBUS")
?.Log(this, $"Error creating a DBus tray icon: {e}.");
_serviceConnected = false;
}
_connection.AddMethodHandler(_statusNotifierItemDbusObj);
await _dBus!.RequestNameAsync(_sysTrayServiceName, 0);
await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
_statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText);
_statusNotifierItemDbusObj.SetIcon(_icon);
_statusNotifierItemDbusObj.ActivationDelegate += OnClicked;
}
private void DestroyTrayIcon()
{
if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null)
if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null || _sysTrayServiceName is null)
return;
_connection.UnregisterObject(_statusNotifierItemDbusObj);
_connection.UnregisterServiceAsync(_sysTrayServiceName);
_dBus!.ReleaseNameAsync(_sysTrayServiceName);
}
public void Dispose()
@ -164,7 +139,6 @@ namespace Avalonia.FreeDesktop
IsActive = false;
_isDisposed = true;
DestroyTrayIcon();
_connection?.Dispose();
_serviceWatchDisposable?.Dispose();
}
@ -175,13 +149,14 @@ namespace Avalonia.FreeDesktop
if (icon is null)
{
_statusNotifierItemDbusObj?.SetIcon(DbusPixmap.EmptyPixmap);
_statusNotifierItemDbusObj?.SetIcon(EmptyPixmap);
return;
}
var x11iconData = IconConverterDelegate(icon);
if (x11iconData.Length == 0) return;
if (x11iconData.Length == 0)
return;
var w = (int)x11iconData[0];
var h = (int)x11iconData[1];
@ -199,7 +174,7 @@ namespace Avalonia.FreeDesktop
pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF);
}
_icon = new DbusPixmap(w, h, pixByteArray);
_icon = (w, h, pixByteArray);
_statusNotifierItemDbusObj?.SetIcon(_icon);
}
@ -237,113 +212,50 @@ namespace Avalonia.FreeDesktop
/// <remarks>
/// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html
/// </remarks>
internal class StatusNotifierItemDbusObj : IStatusNotifierItem
internal class StatusNotifierItemDbusObj : OrgKdeStatusNotifierItem
{
private readonly StatusNotifierItemProperties _backingProperties;
public event Action? OnTitleChanged;
public event Action? OnIconChanged;
public event Action? OnAttentionIconChanged;
public event Action? OnOverlayIconChanged;
public event Action? OnTooltipChanged;
public Action<string>? NewStatusAsync { get; set; }
public Action? ActivationDelegate { get; set; }
public ObjectPath ObjectPath { get; }
public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath)
public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath)
{
ObjectPath = new ObjectPath($"/StatusNotifierItem");
_backingProperties = new StatusNotifierItemProperties
{
Menu = dbusmenuPath, // Needs a dbus menu somehow
ToolTip = new ToolTip("")
};
Connection = connection;
BackingProperties.Menu = dbusMenuPath;
BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty);
BackingProperties.IconName = string.Empty;
BackingProperties.AttentionIconName = string.Empty;
BackingProperties.AttentionIconPixmap = new []{ DBusTrayIconImpl.EmptyPixmap };
BackingProperties.AttentionMovieName = string.Empty;
BackingProperties.IconThemePath = string.Empty;
BackingProperties.OverlayIconName = string.Empty;
BackingProperties.OverlayIconPixmap = new []{ DBusTrayIconImpl.EmptyPixmap };
InvalidateAll();
}
public Task ContextMenuAsync(int x, int y) => Task.CompletedTask;
protected override Connection Connection { get; }
public Task ActivateAsync(int x, int y)
{
ActivationDelegate?.Invoke();
return Task.CompletedTask;
}
public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask;
public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask;
public override string Path => "/StatusNotifierItem";
public void InvalidateAll()
{
OnTitleChanged?.Invoke();
OnIconChanged?.Invoke();
OnOverlayIconChanged?.Invoke();
OnAttentionIconChanged?.Invoke();
OnTooltipChanged?.Invoke();
}
public event Action? ActivationDelegate;
public Task<IDisposable> WatchNewTitleAsync(Action handler, Action<Exception> onError)
{
OnTitleChanged += handler;
return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler));
}
protected override void OnContextMenu(int x, int y) { }
public Task<IDisposable> WatchNewIconAsync(Action handler, Action<Exception> onError)
{
OnIconChanged += handler;
return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler));
}
protected override void OnActivate(int x, int y) => ActivationDelegate?.Invoke();
public Task<IDisposable> WatchNewAttentionIconAsync(Action handler, Action<Exception> onError)
{
OnAttentionIconChanged += handler;
return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler));
}
protected override void OnSecondaryActivate(int x, int y) { }
public Task<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError)
{
OnOverlayIconChanged += handler;
return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler));
}
protected override void OnScroll(int delta, string orientation) { }
public Task<IDisposable> WatchNewToolTipAsync(Action handler, Action<Exception> onError)
{
OnTooltipChanged += handler;
return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler));
}
public Task<IDisposable> WatchNewStatusAsync(Action<string> handler, Action<Exception> onError)
public void InvalidateAll()
{
NewStatusAsync += handler;
return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler));
EmitNewTitle();
EmitNewIcon();
EmitNewAttentionIcon();
EmitNewOverlayIcon();
EmitNewToolTip();
EmitNewStatus(BackingProperties.Status);
}
public Task<object?> GetAsync(string prop)
public void SetIcon((int, int, byte[]) dbusPixmap)
{
return Task.FromResult<object?>(prop switch
{
nameof(_backingProperties.Category) => _backingProperties.Category,
nameof(_backingProperties.Id) => _backingProperties.Id,
nameof(_backingProperties.Menu) => _backingProperties.Menu,
nameof(_backingProperties.IconPixmap) => _backingProperties.IconPixmap,
nameof(_backingProperties.Status) => _backingProperties.Status,
nameof(_backingProperties.Title) => _backingProperties.Title,
nameof(_backingProperties.ToolTip) => _backingProperties.ToolTip,
_ => null
});
}
public Task<StatusNotifierItemProperties> GetAllAsync() => Task.FromResult(_backingProperties);
public Task SetAsync(string prop, object val) => Task.CompletedTask;
public Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler) =>
Task.FromResult(Disposable.Empty);
public void SetIcon(DbusPixmap dbusPixmap)
{
_backingProperties.IconPixmap = new[] { dbusPixmap };
BackingProperties.IconPixmap = new[] { dbusPixmap };
InvalidateAll();
}
@ -352,102 +264,12 @@ namespace Avalonia.FreeDesktop
if (text is null)
return;
_backingProperties.Id = text;
_backingProperties.Category = "ApplicationStatus";
_backingProperties.Status = text;
_backingProperties.Title = text;
_backingProperties.ToolTip = new ToolTip(text);
BackingProperties.Id = text;
BackingProperties.Category = "ApplicationStatus";
BackingProperties.Status = text;
BackingProperties.Title = text;
BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty);
InvalidateAll();
}
}
[DBusInterface("org.kde.StatusNotifierWatcher")]
internal interface IStatusNotifierWatcher : IDBusObject
{
Task RegisterStatusNotifierItemAsync(string Service);
Task RegisterStatusNotifierHostAsync(string Service);
}
[DBusInterface("org.kde.StatusNotifierItem")]
internal interface IStatusNotifierItem : IDBusObject
{
Task ContextMenuAsync(int x, int y);
Task ActivateAsync(int x, int y);
Task SecondaryActivateAsync(int x, int y);
Task ScrollAsync(int delta, string orientation);
Task<IDisposable> WatchNewTitleAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewIconAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewAttentionIconAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewToolTipAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewStatusAsync(Action<string> handler, Action<Exception> onError);
Task<object?> GetAsync(string prop);
Task<StatusNotifierItemProperties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
// This class is used by Tmds.Dbus to ferry properties
// from the SNI spec.
// Don't change this to actual C# properties since
// Tmds.Dbus will get confused.
[Dictionary]
internal class StatusNotifierItemProperties
{
public string? Category;
public string? Id;
public string? Title;
public string? Status;
public ObjectPath Menu;
public DbusPixmap[]? IconPixmap;
public ToolTip ToolTip;
}
internal struct ToolTip
{
public readonly string First;
public readonly DbusPixmap[] Second;
public readonly string Third;
public readonly string Fourth;
private static readonly DbusPixmap[] s_blank =
{
new DbusPixmap(0, 0, Array.Empty<byte>()), new DbusPixmap(0, 0, Array.Empty<byte>())
};
public ToolTip(string message) : this("", s_blank, message, "")
{
}
public ToolTip(string first, DbusPixmap[] second, string third, string fourth)
{
First = first;
Second = second;
Third = third;
Fourth = fourth;
}
}
internal readonly struct DbusPixmap
{
public readonly int Width;
public readonly int Height;
public readonly byte[] Data;
public DbusPixmap(int width, int height, byte[] data)
{
Width = width;
Height = height;
Data = data;
}
public static DbusPixmap EmptyPixmap = new DbusPixmap(1, 1, new byte[] { 255, 0, 0, 0 });
}
}

89
src/Avalonia.FreeDesktop/DBusXml/DBus.xml

@ -0,0 +1,89 @@
<node>
<interface name="org.freedesktop.DBus">
<method name="Hello">
<arg type="s" direction="out"></arg>
</method>
<method name="RequestName">
<arg type="s" direction="in"></arg>
<arg type="u" direction="in"></arg>
<arg type="u" direction="out"></arg>
</method>
<method name="ReleaseName">
<arg type="s" direction="in"></arg>
<arg type="u" direction="out"></arg>
</method>
<method name="StartServiceByName">
<arg type="s" direction="in"></arg>
<arg type="u" direction="in"></arg>
<arg type="u" direction="out"></arg>
</method>
<method name="UpdateActivationEnvironment">
<arg type="a{ss}" direction="in"></arg>
</method>
<method name="NameHasOwner">
<arg type="s" direction="in"></arg>
<arg type="b" direction="out"></arg>
</method>
<method name="ListNames">
<arg type="as" direction="out"></arg>
</method>
<method name="ListActivatableNames">
<arg type="as" direction="out"></arg>
</method>
<method name="AddMatch">
<arg type="s" direction="in"></arg>
</method>
<method name="RemoveMatch">
<arg type="s" direction="in"></arg>
</method>
<method name="GetNameOwner">
<arg type="s" direction="in"></arg>
<arg type="s" direction="out"></arg>
</method>
<method name="ListQueuedOwners">
<arg type="s" direction="in"></arg>
<arg type="as" direction="out"></arg>
</method>
<method name="GetConnectionUnixUser">
<arg type="s" direction="in"></arg>
<arg type="u" direction="out"></arg>
</method>
<method name="GetConnectionUnixProcessID">
<arg type="s" direction="in"></arg>
<arg type="u" direction="out"></arg>
</method>
<method name="GetAdtAuditSessionData">
<arg type="s" direction="in"></arg>
<arg type="ay" direction="out"></arg>
</method>
<method name="GetConnectionSELinuxSecurityContext">
<arg type="s" direction="in"></arg>
<arg type="ay" direction="out"></arg>
</method>
<method name="ReloadConfig"></method>
<method name="GetId">
<arg type="s" direction="out"></arg>
</method>
<method name="GetConnectionCredentials">
<arg type="s" direction="in"></arg>
<arg type="a{sv}" direction="out"></arg>
</method>
<signal name="NameOwnerChanged">
<arg type="s"></arg>
<arg type="s"></arg>
<arg type="s"></arg>
</signal>
<signal name="NameLost">
<arg type="s"></arg>
</signal>
<signal name="NameAcquired">
<arg type="s"></arg>
</signal>
<property name="Features" type="as" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"></annotation>
</property>
<property name="Interfaces" type="as" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"></annotation>
</property>
</interface>
</node>

437
src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml

@ -0,0 +1,437 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
A library to allow applications to provide simple indications of
information to be displayed to users of the application through the
interface shell.
Copyright 2009 Canonical Ltd.
Authors:
Ted Gould <ted@canonical.com>
Aurélien Gâteau <aurelien.gateau@canonical.com>
This program is free software: you can redistribute it and/or modify it
under the terms of either or both of the following licenses:
1) the GNU Lesser General Public License version 3, as published by the
Free Software Foundation; and/or
2) the GNU Lesser General Public License version 2.1, as published by
the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR
PURPOSE. See the applicable version of the GNU Lesser General Public
License for more details.
You should have received a copy of both the GNU Lesser General Public
License version 3 and version 2.1 along with this program. If not, see
<http://www.gnu.org/licenses/>
-->
<node name="/" xmlns:dox="http://www.canonical.com/dbus/dox.dtd">
<dox:d><![CDATA[
@mainpage
The goal of DBusMenu is to expose menus on DBus.
Main interface is documented here: @ref com::canonical::dbusmenu
]]></dox:d>
<interface name="com.canonical.dbusmenu">
<dox:d><![CDATA[
A DBus interface to expose menus on DBus.
Menu items are represented with a unique numeric id and a dictionary of
properties.
To reduce the amount of DBus traffic, a property should only be returned
if its value is not the default value.
Available properties are:
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default Value</th>
</tr>
<tr>
<td>type</td>
<td>String</td>
<td>Can be one of:
- "standard": an item which can be clicked to trigger an action or
show another menu
- "separator": a separator
Vendor specific types can be added by prefixing them with
"x-<vendor>-".
</td>
<td>"standard"</td>
</tr>
<tr>
<td>label</td>
<td>string</td>
<td>Text of the item, except that:
-# two consecutive underscore characters "__" are displayed as a
single underscore,
-# any remaining underscore characters are not displayed at all,
-# the first of those remaining underscore characters (unless it is
the last character in the string) indicates that the following
character is the access key.
</td>
<td>""</td>
</tr>
<tr>
<td>enabled</td>
<td>boolean</td>
<td>Whether the item can be activated or not.</td>
<td>true</td>
</tr>
<tr>
<td>visible</td>
<td>boolean</td>
<td>True if the item is visible in the menu.</td>
<td>true</td>
</tr>
<tr>
<td>icon-name</td>
<td>string</td>
<td>Icon name of the item, following the freedesktop.org icon spec.</td>
<td>""</td>
</tr>
<tr>
<td>icon-data</td>
<td>binary</td>
<td>PNG data of the icon.</td>
<td>Empty</td>
</tr>
<tr>
<td>shortcut</td>
<td>array of arrays of strings</td>
<td>The shortcut of the item. Each array represents the key press
in the list of keypresses. Each list of strings contains a list of
modifiers and then the key that is used. The modifier strings
allowed are: "Control", "Alt", "Shift" and "Super".
- A simple shortcut like Ctrl+S is represented as:
[["Control", "S"]]
- A complex shortcut like Ctrl+Q, Alt+X is represented as:
[["Control", "Q"], ["Alt", "X"]]</td>
<td>Empty</td>
</tr>
<tr>
<td>toggle-type</td>
<td>string</td>
<td>
If the item can be toggled, this property should be set to:
- "checkmark": Item is an independent togglable item
- "radio": Item is part of a group where only one item can be
toggled at a time
- "": Item cannot be toggled
</td>
<td>""</td>
</tr>
<tr>
<td>toggle-state</td>
<td>int</td>
<td>
Describe the current state of a "togglable" item. Can be one of:
- 0 = off
- 1 = on
- anything else = indeterminate
Note:
The implementation does not itself handle ensuring that only one
item in a radio group is set to "on", or that a group does not have
"on" and "indeterminate" items simultaneously; maintaining this
policy is up to the toolkit wrappers.
</td>
<td>-1</td>
</tr>
<tr>
<td>children-display</td>
<td>string</td>
<td>
If the menu item has children this property should be set to
"submenu".
</td>
<td>""</td>
</tr>
<tr>
<td>disposition</td>
<td>string</td>
<td>
How the menuitem feels the information it's displaying to the
user should be presented.
- "normal" a standard menu item
- "informative" providing additional information to the user
- "warning" looking at potentially harmful results
- "alert" something bad could potentially happen
</td>
<td>"normal"</td>
</tr>
</table>
Vendor specific properties can be added by prefixing them with
"x-<vendor>-".
]]></dox:d>
<!-- Properties -->
<property name="Version" type="u" access="read">
<dox:d>
Provides the version of the DBusmenu API that this API is
implementing.
</dox:d>
</property>
<property name="TextDirection" type="s" access="read">
<dox:d>
Represents the way the text direction of the application. This
allows the server to handle mismatches intelligently. For left-
to-right the string is "ltr" for right-to-left it is "rtl".
</dox:d>
</property>
<property name="Status" type="s" access="read">
<dox:d>
Tells if the menus are in a normal state or they believe that they
could use some attention. Cases for showing them would be if help
were referring to them or they accessors were being highlighted.
This property can have two values: "normal" in almost all cases and
"notice" when they should have a higher priority to be shown.
</dox:d>
</property>
<property name="IconThemePath" type="as" access="read">
<dox:d>
A list of directories that should be used for finding icons using
the icon naming spec. Idealy there should only be one for the icon
theme, but additional ones are often added by applications for
app specific icons.
</dox:d>
</property>
<!-- Functions -->
<method name="GetLayout">
<dox:d>
Provides the layout and propertiers that are attached to the entries
that are in the layout. It only gives the items that are children
of the item that is specified in @a parentId. It will return all of the
properties or specific ones depending of the value in @a propertyNames.
The format is recursive, where the second 'v' is in the same format
as the original 'a(ia{sv}av)'. Its content depends on the value
of @a recursionDepth.
</dox:d>
<arg type="i" name="parentId" direction="in">
<dox:d>The ID of the parent node for the layout. For
grabbing the layout from the root node use zero.</dox:d>
</arg>
<arg type="i" name="recursionDepth" direction="in">
<dox:d>
The amount of levels of recursion to use. This affects the
content of the second variant array.
- -1: deliver all the items under the @a parentId.
- 0: no recursion, the array will be empty.
- n: array will contains items up to 'n' level depth.
</dox:d>
</arg>
<arg type="as" name="propertyNames" direction="in" >
<dox:d>
The list of item properties we are
interested in. If there are no entries in the list all of
the properties will be sent.
</dox:d>
</arg>
<arg type="u" name="revision" direction="out">
<dox:d>The revision number of the layout. For matching
with layoutUpdated signals.</dox:d>
</arg>
<arg type="(ia{sv}av)" name="layout" direction="out">
<dox:d>The layout, as a recursive structure.</dox:d>
</arg>
</method>
<method name="GetGroupProperties">
<dox:d>
Returns the list of items which are children of @a parentId.
</dox:d>
<arg type="ai" name="ids" direction="in" >
<dox:d>
A list of ids that we should be finding the properties
on. If the list is empty, all menu items should be sent.
</dox:d>
</arg>
<arg type="as" name="propertyNames" direction="in" >
<dox:d>
The list of item properties we are
interested in. If there are no entries in the list all of
the properties will be sent.
</dox:d>
</arg>
<arg type="a(ia{sv})" name="properties" direction="out" >
<dox:d>
An array of property values.
An item in this area is represented as a struct following
this format:
@li id unsigned the item id
@li properties map(string => variant) the requested item properties
</dox:d>
</arg>
</method>
<method name="GetProperty">
<dox:d>
Get a signal property on a single item. This is not useful if you're
going to implement this interface, it should only be used if you're
debugging via a commandline tool.
</dox:d>
<arg type="i" name="id" direction="in">
<dox:d>the id of the item which received the event</dox:d>
</arg>
<arg type="s" name="name" direction="in">
<dox:d>the name of the property to get</dox:d>
</arg>
<arg type="v" name="value" direction="out">
<dox:d>the value of the property</dox:d>
</arg>
</method>
<method name="Event">
<dox:d><![CDATA[
This is called by the applet to notify the application an event happened on a
menu item.
@a type can be one of the following:
@li "clicked"
@li "hovered"
@li "opened"
@li "closed"
Vendor specific events can be added by prefixing them with "x-<vendor>-"
]]></dox:d>
<arg type="i" name="id" direction="in" >
<dox:d>the id of the item which received the event</dox:d>
</arg>
<arg type="s" name="eventId" direction="in" >
<dox:d>the type of event</dox:d>
</arg>
<arg type="v" name="data" direction="in" >
<dox:d>event-specific data</dox:d>
</arg>
<arg type="u" name="timestamp" direction="in" >
<dox:d>The time that the event occured if available or the time the message was sent if not</dox:d>
</arg>
</method>
<method name="EventGroup">
<dox:d>
Used to pass a set of events as a single message for possibily several
different menuitems. This is done to optimize DBus traffic.
</dox:d>
<arg type="a(isvu)" name="events" direction="in">
<dox:d>
An array of all the events that should be passed. This tuple should
match the parameters of the 'Event' signal. Which is roughly:
id, eventID, data and timestamp.
</dox:d>
</arg>
<arg type="ai" name="idErrors" direction="out">
<dox:d>
I list of menuitem IDs that couldn't be found. If none of the ones
in the list can be found, a DBus error is returned.
</dox:d>
</arg>
</method>
<method name="AboutToShow">
<dox:d>
This is called by the applet to notify the application that it is about
to show the menu under the specified item.
</dox:d>
<arg type="i" name="id" direction="in">
<dox:d>
Which menu item represents the parent of the item about to be shown.
</dox:d>
</arg>
<arg type="b" name="needUpdate" direction="out">
<dox:d>
Whether this AboutToShow event should result in the menu being updated.
</dox:d>
</arg>
</method>
<method name="AboutToShowGroup">
<dox:d>
A function to tell several menus being shown that they are about to
be shown to the user. This is likely only useful for programitc purposes
so while the return values are returned, in general, the singular function
should be used in most user interacation scenarios.
</dox:d>
<arg type="ai" name="ids" direction="in">
<dox:d>
The IDs of the menu items who's submenus are being shown.
</dox:d>
</arg>
<arg type="ai" name="updatesNeeded" direction="out">
<dox:d>
The IDs of the menus that need updates. Note: if no update information
is needed the DBus message should set the no reply flag.
</dox:d>
</arg>
<arg type="ai" name="idErrors" direction="out">
<dox:d>
I list of menuitem IDs that couldn't be found. If none of the ones
in the list can be found, a DBus error is returned.
</dox:d>
</arg>
</method>
<!-- Signals -->
<signal name="ItemsPropertiesUpdated">
<dox:d>
Triggered when there are lots of property updates across many items
so they all get grouped into a single dbus message. The format is
the ID of the item with a hashtable of names and values for those
properties.
</dox:d>
<arg type="a(ia{sv})" name="updatedProps" direction="out" />
<arg type="a(ias)" name="removedProps" direction="out" />
</signal>
<signal name="LayoutUpdated">
<dox:d>
Triggered by the application to notify display of a layout update, up to
revision
</dox:d>
<arg type="u" name="revision" direction="out" >
<dox:d>The revision of the layout that we're currently on</dox:d>
</arg>
<arg type="i" name="parent" direction="out" >
<dox:d>
If the layout update is only of a subtree, this is the
parent item for the entries that have changed. It is zero if
the whole layout should be considered invalid.
</dox:d>
</arg>
</signal>
<signal name="ItemActivationRequested">
<dox:d>
The server is requesting that all clients displaying this
menu open it to the user. This would be for things like
hotkeys that when the user presses them the menu should
open and display itself to the user.
</dox:d>
<arg type="i" name="id" direction="out" >
<dox:d>ID of the menu that should be activated</dox:d>
</arg>
<arg type="u" name="timestamp" direction="out" >
<dox:d>The time that the event occured</dox:d>
</arg>
</signal>
<!-- End of interesting stuff -->
</interface>
</node>

96
src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml

@ -0,0 +1,96 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.StatusNotifierItem">
<property name="Category" type="s" access="read"/>
<property name="Id" type="s" access="read"/>
<property name="Title" type="s" access="read"/>
<property name="Status" type="s" access="read"/>
<property name="WindowId" type="i" access="read"/>
<!-- An additional path to add to the theme search path to find the icons specified above. -->
<property name="IconThemePath" type="s" access="read"/>
<property name="Menu" type="o" access="read"/>
<property name="ItemIsMenu" type="b" access="read"/>
<!-- main icon -->
<!-- names are preferred over pixmaps -->
<property name="IconName" type="s" access="read"/>
<!--struct containing width, height and image data-->
<property name="IconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
</property>
<property name="OverlayIconName" type="s" access="read"/>
<property name="OverlayIconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
</property>
<!-- Requesting attention icon -->
<property name="AttentionIconName" type="s" access="read"/>
<!--same definition as image-->
<property name="AttentionIconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
</property>
<property name="AttentionMovieName" type="s" access="read"/>
<!-- tooltip data -->
<!--(iiay) is an image-->
<property name="ToolTip" type="(sa(iiay)ss)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusToolTipStruct"/>
</property>
<!-- interaction: the systemtray wants the application to do something -->
<method name="ContextMenu">
<!-- we're passing the coordinates of the icon, so the app knows where to put the popup window -->
<arg name="x" type="i" direction="in"/>
<arg name="y" type="i" direction="in"/>
</method>
<method name="Activate">
<arg name="x" type="i" direction="in"/>
<arg name="y" type="i" direction="in"/>
</method>
<method name="SecondaryActivate">
<arg name="x" type="i" direction="in"/>
<arg name="y" type="i" direction="in"/>
</method>
<method name="Scroll">
<arg name="delta" type="i" direction="in"/>
<arg name="orientation" type="s" direction="in"/>
</method>
<!-- Signals: the client wants to change something in the status-->
<signal name="NewTitle">
</signal>
<signal name="NewIcon">
</signal>
<signal name="NewAttentionIcon">
</signal>
<signal name="NewOverlayIcon">
</signal>
<signal name="NewToolTip">
</signal>
<signal name="NewStatus">
<arg name="status" type="s"/>
</signal>
</interface>
</node>

42
src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml

@ -0,0 +1,42 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.StatusNotifierWatcher">
<!-- methods -->
<method name="RegisterStatusNotifierItem">
<arg name="service" type="s" direction="in"/>
</method>
<method name="RegisterStatusNotifierHost">
<arg name="service" type="s" direction="in"/>
</method>
<!-- properties -->
<property name="RegisteredStatusNotifierItems" type="as" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QStringList"/>
</property>
<property name="IsStatusNotifierHostRegistered" type="b" access="read"/>
<property name="ProtocolVersion" type="i" access="read"/>
<!-- signals -->
<signal name="StatusNotifierItemRegistered">
<arg type="s"/>
</signal>
<signal name="StatusNotifierItemUnregistered">
<arg type="s"/>
</signal>
<signal name="StatusNotifierHostRegistered">
</signal>
<signal name="StatusNotifierHostUnregistered">
</signal>
</interface>
</node>

56
src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml

@ -0,0 +1,56 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node xmlns:dox="http://www.ayatana.org/dbus/dox.dtd">
<dox:d><![CDATA[
@mainpage
 
An interface to register menus that are associated with a window in an application.  The
main interface is documented here: @ref com::canonical::AppMenu::Registrar.
    
The actual menus are transported using the dbusmenu protocol which is available
here: @ref com::canonical::dbusmenu.
]]></dox:d>
<interface name="com.canonical.AppMenu.Registrar" xmlns:dox="http://www.ayatana.org/dbus/dox.dtd">
<dox:d>
An interface to register a menu from an application's window to be displayed in another
window.  This manages that association between XWindow Window IDs and the dbus
address and object that provides the menu using the dbusmenu dbus interface.
</dox:d>
<method name="RegisterWindow">
<dox:d><![CDATA[
Associates a dbusmenu with a window
     
/note this method assumes that the connection from the caller is the DBus connection
to use for the object.  Applications that use multiple DBus connections will need to
ensure this method is called with the same connection that implmenets the object.
]]></dox:d>
<arg name="windowId" type="u" direction="in">
<dox:d>The XWindow ID of the window</dox:d>
</arg>
<arg name="menuObjectPath" type="o" direction="in">
<dox:d>The object on the dbus interface implementing the dbusmenu interface</dox:d>
</arg>
</method>
<method name="UnregisterWindow">
<dox:d>
A method to allow removing a window from the database. Windows will also be removed
when the client drops off DBus so this is not required. It is polite though. And
important for testing.
</dox:d>
<arg name="windowId" type="u" direction="in">
<dox:d>The XWindow ID of the window</dox:d>
</arg>
</method>
<method name="GetMenuForWindow">
<dox:d>Gets the registered menu for a given window ID.</dox:d>
<arg name="windowId" type="u" direction="in">
<dox:d>The XWindow ID of the window to get</dox:d>
</arg>
<arg name="service" type="s" direction="out">
<dox:d>The address of the connection on DBus (e.g. :1.23 or org.example.service)</dox:d>
</arg>
<arg name="menuObjectPath" type="o" direction="out">
<dox:d>The path to the object which implements the com.canonical.dbusmenu interface.</dox:d>
</arg>
</method>
</interface>
</node>

64
src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml

@ -0,0 +1,64 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.fcitx.Fcitx.InputContext">
<method name="FocusIn">
</method>
<method name="FocusOut">
</method>
<method name="Reset">
</method>
<method name="SetCursorRect">
<arg name="x" direction="in" type="i"/>
<arg name="y" direction="in" type="i"/>
<arg name="w" direction="in" type="i"/>
<arg name="h" direction="in" type="i"/>
</method>
<method name="SetCapacity">
<arg name="caps" direction="in" type="u"/>
</method>
<method name="SetSurroundingText">
<arg name="text" direction="in" type="s"/>
<arg name="cursor" direction="in" type="u"/>
<arg name="anchor" direction="in" type="u"/>
</method>
<method name="SetSurroundingTextPosition">
<arg name="cursor" direction="in" type="u"/>
<arg name="anchor" direction="in" type="u"/>
</method>
<method name="DestroyIC">
</method>
<method name="ProcessKeyEvent">
<arg name="keyval" direction="in" type="u"/>
<arg name="keycode" direction="in" type="u"/>
<arg name="state" direction="in" type="u"/>
<arg name="type" direction="in" type="i"/>
<arg name="time" direction="in" type="u"/>
<arg name="ret" direction="out" type="i"/>
</method>
<signal name="CommitString">
<arg name="str" type="s"/>
</signal>
<signal name="CurrentIM">
<arg name="name" type="s"/>
<arg name="uniqueName" type="s"/>
<arg name="langCode" type="s"/>
</signal>
<signal name="UpdateFormattedPreedit">
<arg name="str" type="a(si)" />
<arg name="cursorpos" type="i"/>
<!-- qt4 / 5 seems use in/out differently -->
<annotation name="com.trolltech.QtDBus.QtTypeName.In0" value="FcitxFormattedPreeditList" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="FcitxFormattedPreeditList" />
</signal>
<signal name="ForwardKey">
<arg name="keyval" type="u"/>
<arg name="state" type="u"/>
<arg name="type" type="i"/>
</signal>
<signal name="DeleteSurroundingText">
<arg name="offset" type="i"/>
<arg name="nchar" type="u"/>
</signal>
</interface>
</node>

64
src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml

@ -0,0 +1,64 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.fcitx.Fcitx.InputContext1">
<method name="FocusIn">
</method>
<method name="FocusOut">
</method>
<method name="Reset">
</method>
<method name="SetCursorRect">
<arg name="x" direction="in" type="i"/>
<arg name="y" direction="in" type="i"/>
<arg name="w" direction="in" type="i"/>
<arg name="h" direction="in" type="i"/>
</method>
<method name="SetCapability">
<arg name="caps" direction="in" type="t"/>
</method>
<method name="SetSurroundingText">
<arg name="text" direction="in" type="s"/>
<arg name="cursor" direction="in" type="u"/>
<arg name="anchor" direction="in" type="u"/>
</method>
<method name="SetSurroundingTextPosition">
<arg name="cursor" direction="in" type="u"/>
<arg name="anchor" direction="in" type="u"/>
</method>
<method name="DestroyIC">
</method>
<method name="ProcessKeyEvent">
<arg name="keyval" direction="in" type="u"/>
<arg name="keycode" direction="in" type="u"/>
<arg name="state" direction="in" type="u"/>
<arg name="type" direction="in" type="b"/>
<arg name="time" direction="in" type="u"/>
<arg name="ret" direction="out" type="b"/>
</method>
<signal name="CommitString">
<arg name="str" type="s"/>
</signal>
<signal name="CurrentIM">
<arg name="name" type="s"/>
<arg name="uniqueName" type="s"/>
<arg name="langCode" type="s"/>
</signal>
<signal name="UpdateFormattedPreedit">
<arg name="str" type="a(si)" />
<arg name="cursorpos" type="i"/>
<!-- qt4 / 5 seems use in/out differently -->
<annotation name="com.trolltech.QtDBus.QtTypeName.In0" value="FcitxFormattedPreeditList" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="FcitxFormattedPreeditList" />
</signal>
<signal name="ForwardKey">
<arg name="keyval" type="u"/>
<arg name="state" type="u"/>
<arg name="type" type="b"/>
</signal>
<signal name="DeleteSurroundingText">
<arg name="offset" type="i"/>
<arg name="nchar" type="u"/>
</signal>
</interface>
</node>

16
src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml

@ -0,0 +1,16 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.fcitx.Fcitx.InputMethod">
<method name="CreateICv3">
<arg name="appname" direction="in" type="s"/>
<arg name="pid" direction="in" type="i"/>
<arg name="icid" direction="out" type="i"/>
<arg name="enable" direction="out" type="b"/>
<arg name="keyval1" direction="out" type="u"/>
<arg name="state1" direction="out" type="u"/>
<arg name="keyval2" direction="out" type="u"/>
<arg name="state2" direction="out" type="u"/>
</method>
</interface>
</node>

12
src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml

@ -0,0 +1,12 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.fcitx.Fcitx.InputMethod1">
<method name="CreateInputContext">
<arg type="a(ss)" direction="in"/>
<arg type="o" direction="out"/>
<arg type="ay" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="FcitxInputContextArgumentList" />
</method>
</interface>
</node>

139
src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml

@ -0,0 +1,139 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2017-2019 Red Hat, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
Author: Alexander Larsson <alexl@redhat.com>
-->
<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
<!--
org.freedesktop.IBus.Portal:
@short_description: Portal for ibus client access
This interface is a minimal interface to IBus that is safe to expose to
clients.
-->
<interface name="org.freedesktop.IBus.Portal">
<method name='CreateInputContext'>
<arg direction='in' type='s' name='client_name' />
<arg direction='out' type='o' name='object_path' />
</method>
</interface>
<!-- This is a copy of the interface in inputcontext.c, they should be shared.
We want this for the code generator so that we can be sure we verify all
caller types, etc.
-->
<interface name='org.freedesktop.IBus.InputContext'>
<method name='ProcessKeyEvent'>
<arg direction='in' type='u' name='keyval' />
<arg direction='in' type='u' name='keycode' />
<arg direction='in' type='u' name='state' />
<arg direction='out' type='b' name='handled' />
</method>
<method name='SetCursorLocation'>
<arg direction='in' type='i' name='x' />
<arg direction='in' type='i' name='y' />
<arg direction='in' type='i' name='w' />
<arg direction='in' type='i' name='h' />
</method>
<method name='SetCursorLocationRelative'>
<arg direction='in' type='i' name='x' />
<arg direction='in' type='i' name='y' />
<arg direction='in' type='i' name='w' />
<arg direction='in' type='i' name='h' />
</method>
<method name='ProcessHandWritingEvent'>
<arg direction='in' type='ad' name='coordinates' />
</method>
<method name='CancelHandWriting'>
<arg direction='in' type='u' name='n_strokes' />
</method>
<method name='FocusIn' />
<method name='FocusOut' />
<method name='Reset' />
<method name='SetCapabilities'>
<arg direction='in' type='u' name='caps' />
</method>
<method name='PropertyActivate'>
<arg direction='in' type='s' name='name' />
<arg direction='in' type='u' name='state' />
</method>
<method name='SetEngine'>
<arg direction='in' type='s' name='name' />
</method>
<method name='GetEngine'>
<arg direction='out' type='v' name='desc' />
</method>
<method name='SetSurroundingText'>
<arg direction='in' type='v' name='text' />
<arg direction='in' type='u' name='cursor_pos' />
<arg direction='in' type='u' name='anchor_pos' />
</method>
<signal name='CommitText'>
<arg type='v' name='text' />
</signal>
<signal name='ForwardKeyEvent'>
<arg type='u' name='keyval' />
<arg type='u' name='keycode' />
<arg type='u' name='state' />
</signal>
<signal name='UpdatePreeditText'>
<arg type='v' name='text' />
<arg type='u' name='cursor_pos' />
<arg type='b' name='visible' />
</signal>
<signal name='UpdatePreeditTextWithMode'>
<arg type='v' name='text' />
<arg type='u' name='cursor_pos' />
<arg type='b' name='visible' />
<arg type='u' name='mode' />
</signal>
<signal name='ShowPreeditText'/>
<signal name='HidePreeditText'/>
<signal name='UpdateAuxiliaryText'>
<arg type='v' name='text' />
<arg type='b' name='visible' />
</signal>
<signal name='ShowAuxiliaryText'/>
<signal name='HideAuxiliaryText'/>
<signal name='UpdateLookupTable'>
<arg type='v' name='table' />
<arg type='b' name='visible' />
</signal>
<signal name='ShowLookupTable'/>
<signal name='HideLookupTable'/>
<signal name='PageUpLookupTable'/>
<signal name='PageDownLookupTable'/>
<signal name='CursorUpLookupTable'/>
<signal name='CursorDownLookupTable'/>
<signal name='RegisterProperties'>
<arg type='v' name='props' />
</signal>
<signal name='UpdateProperty'>
<arg type='v' name='prop' />
</signal>
<property name='ContentType' type='(uu)' access='write' />
<property name='ClientCommitPreedit' type='(b)' access='write' />
</interface>
<interface name='org.freedesktop.IBus.Service'>
<method name='Destroy' />
</interface>
</node>

377
src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml

@ -0,0 +1,377 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2015 Red Hat, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
Author: Alexander Larsson <alexl@redhat.com>
-->
<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
<!--
org.freedesktop.portal.FileChooser:
@short_description: File chooser portal
The FileChooser portal allows sandboxed applications to ask
the user for access to files outside the sandbox. The portal
backend will present the user with a file chooser dialog.
The selected files will be made accessible to the application
via the document portal, and the returned URI will point
into the document portal fuse filesystem in /run/user/$UID/doc/.
This documentation describes version 3 of this interface.
-->
<interface name="org.freedesktop.portal.FileChooser">
<!--
OpenFile:
@parent_window: Identifier for the application window, see <link linkend="parent_window">Common Conventions</link>
@title: Title for the file chooser dialog
@options: Vardict with optional further information
@handle: Object path for the #org.freedesktop.portal.Request object representing this call
Asks to open one or more files.
Supported keys in the @options vardict include:
<variablelist>
<varlistentry>
<term>handle_token s</term>
<listitem><para>
A string that will be used as the last element of the @handle. Must be a valid
object path element. See the #org.freedesktop.portal.Request documentation for
more information about the @handle.
</para></listitem>
</varlistentry>
<varlistentry>
<term>accept_label s</term>
<listitem><para>
Label for the accept button. Mnemonic underlines are allowed.
</para></listitem>
</varlistentry>
<varlistentry>
<term>modal b</term>
<listitem><para>
Whether the dialog should be modal. Default is yes.
</para></listitem>
</varlistentry>
<varlistentry>
<term>multiple b</term>
<listitem><para>
Whether multiple files can be selected or not. Default is single-selection.
</para></listitem>
</varlistentry>
<varlistentry>
<term>directory b</term>
<listitem><para>
Whether to select for folders instead of files. Default is to select files. This option was added in version 3.
</para></listitem>
</varlistentry>
<varlistentry>
<term>filters a(sa(us))</term>
<listitem>
<para>
List of serialized file filters.
</para>
<para>
Each item in the array specifies a single filter to offer to the user.
The first string is a user-visible name for the filter. The a(us)
specifies a list of filter strings, which can be either a glob-style pattern
(indicated by 0) or a mimetype (indicated by 1). Patterns are case-sensitive.
To match different capitalizations of, e.g. '*.ico', use a pattern like
'*.[iI][cC][oO]'.
</para>
<para>
Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]
</para>
<para>
Note that filters are purely there to aid the user in making a useful selection.
The portal may still allow the user to select files that don't match any filter
criteria, and applications must be prepared to handle that.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>current_filter (sa(us))</term>
<listitem><para>
Request that this filter be set by default at dialog creation. If
the filters list is nonempty, it should match a filter in the
list to set the default filter from the list. Alternatively, it
may be specified when the list is empty to apply the filter
unconditionally.
</para></listitem>
</varlistentry>
<varlistentry>
<term>choices a(ssa(ss)s)</term>
<listitem>
<para>
List of serialized combo boxes to add to the file chooser.
</para>
<para>
For each element, the first string is an ID that will be returned
with the response, the second string is a user-visible label. The
a(ss) is the list of choices, each being an ID and a
user-visible label. The final string is the initial selection,
or "", to let the portal decide which choice will be initially selected.
None of the strings, except for the initial selection, should be empty.
</para>
<para>
As a special case, passing an empty array for the list of choices
indicates a boolean choice that is typically displayed as a check
button, using "true" and "false" as the choices.
</para>
<para>
Example: [('encoding', 'Encoding', [('utf8', 'Unicode (UTF-8)'), ('latin15', 'Western')], 'latin15'), ('reencode', 'Reencode', [], 'false')]
</para>
</listitem>
</varlistentry>
</variablelist>
The following results get returned via the #org.freedesktop.portal.Request::Response signal:
<variablelist>
<varlistentry>
<term>uris as</term>
<listitem><para>
An array of strings containing the uris of the selected files.
</para></listitem>
</varlistentry>
<varlistentry>
<term>choices a(ss)</term>
<listitem>
<para>
An array of pairs of strings, the first string being the ID of a
combobox that was passed into this call, the second string being
the selected option.
</para>
<para>
Example: [('encoding', 'utf8'), ('reencode', 'true')]
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>current_filter (sa(us))</term>
<listitem>
<para>
The filter that was selected. This may match a filter in the
filter list or another filter that was applied unconditionally.
</para>
</listitem>
</varlistentry>
</variablelist>
-->
<method name="OpenFile">
<arg type="s" name="parent_window" direction="in"/>
<arg type="s" name="title" direction="in"/>
<arg type="a{sv}" name="options" direction="in"/>
<arg type="o" name="handle" direction="out"/>
</method>
<!--
SaveFile:
@parent_window: Identifier for the application window, see <link linkend="parent_window">Common Conventions</link>
@title: Title for the file chooser dialog
@options: Vardict with optional further information
@handle: Object path for the #org.freedesktop.portal.Request object representing this call
Asks for a location to save a file.
Supported keys in the @options vardict include:
<variablelist>
<varlistentry>
<term>handle_token s</term>
<listitem><para>
A string that will be used as the last element of the @handle. Must be a valid
object path element. See the #org.freedesktop.portal.Request documentation for
more information about the @handle.
</para></listitem>
</varlistentry>
<varlistentry>
<term>accept_label s</term>
<listitem><para>
Label for the accept button. Mnemonic underlines are allowed.
</para></listitem>
</varlistentry>
<varlistentry>
<term>modal b</term>
<listitem><para>
Whether the dialog should be modal. Default is yes.
</para></listitem>
</varlistentry>
<varlistentry>
<term>filters a(sa(us))</term>
<listitem><para>
List of serialized file filters.
See org.freedesktop.portal.FileChooser.OpenFile() for details.
</para></listitem>
</varlistentry>
<varlistentry>
<term>current_filter (sa(us))</term>
<listitem><para>
Request that this filter be set by default at dialog creation.
See org.freedesktop.portal.FileChooser.OpenFile() for details.
</para></listitem>
</varlistentry>
<varlistentry>
<term>choices a(ssa(ss)s)</term>
<listitem><para>
List of serialized combo boxes.
See org.freedesktop.portal.FileChooser.OpenFile() for details.
</para></listitem>
</varlistentry>
<varlistentry>
<term>current_name s</term>
<listitem><para>Suggested filename.</para></listitem>
</varlistentry>
<varlistentry>
<term>current_folder ay</term>
<listitem>
<para>
Suggested folder to save the file in. The byte array is expected to be null-terminated.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>current_file ay</term>
<listitem>
<para>
The current file (when saving an existing file). The byte array is expected to be null-terminated.
</para>
</listitem>
</varlistentry>
</variablelist>
The following results get returned via the #org.freedesktop.portal.Request::Response signal:
<variablelist>
<varlistentry>
<term>uris as</term>
<listitem><para>
An array of strings containing the uri of the selected file.
</para></listitem>
</varlistentry>
<varlistentry>
<term>choices a(ss)</term>
<listitem><para>
An array of pairs of strings, corresponding to the passed-in choices.
See org.freedesktop.portal.FileChooser.OpenFile() for details.
</para></listitem>
</varlistentry>
<varlistentry>
<term>current_filter (sa(us))</term>
<listitem>
<para>
The filter that was selected.
See org.freedesktop.portal.FileChooser.OpenFile() for details.
</para>
</listitem>
</varlistentry>
</variablelist>
-->
<method name="SaveFile">
<arg type="s" name="parent_window" direction="in"/>
<arg type="s" name="title" direction="in"/>
<arg type="a{sv}" name="options" direction="in"/>
<arg type="o" name="handle" direction="out"/>
</method>
<!--
SaveFiles:
@parent_window: Identifier for the application window, see <link linkend="parent_window">Common Conventions</link>
@title: Title for the file chooser dialog
@options: Vardict with optional further information
@handle: Object path for the #org.freedesktop.portal.Request object representing this call
Asks for a folder as a location to save one or more files. The
names of the files will be used as-is and appended to the
selected folder's path in the list of returned files. If the
selected folder already contains a file with one of the given
names, the portal may prompt or take some other action to
construct a unique file name and return that instead.
Supported keys in the @options vardict include:
<variablelist>
<varlistentry>
<term>handle_token s</term>
<listitem><para>
A string that will be used as the last element of the
@handle. Must be a valid object path element. See the
#org.freedesktop.portal.Request documentation for more
information about the @handle.
</para></listitem>
</varlistentry>
<varlistentry>
<term>accept_label s</term>
<listitem><para>
Label for the accept button. Mnemonic underlines are allowed.
</para></listitem>
</varlistentry>
<varlistentry>
<term>modal b</term>
<listitem><para>
Whether the dialog should be modal. Default is yes.
</para></listitem>
</varlistentry>
<varlistentry>
<term>choices a(ssa(ss)s)</term>
<listitem><para>
List of serialized combo boxes.
See org.freedesktop.portal.FileChooser.OpenFile() for details.
</para></listitem>
</varlistentry>
<varlistentry>
<term>current_folder ay</term>
<listitem>
<para>
Suggested folder to save the files in. The byte array is
expected to be null-terminated.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>files aay</term>
<listitem>
<para>
An array of file names to be saved. The byte arrays are
expected to be null-terminated.
</para>
</listitem>
</varlistentry>
</variablelist>
The following results get returned via the
#org.freedesktop.portal.Request::Response signal:
<variablelist>
<varlistentry>
<term>uris as</term>
<listitem><para>
An array of strings containing the uri corresponding to
each file given by @options, in the same order. Note that
the file names may have changed, for example if a file
with the same name in the selected folder already exists.
</para></listitem>
</varlistentry>
<varlistentry>
<term>choices a(ss)</term>
<listitem><para>
An array of pairs of strings, corresponding to the passed-in choices.
See org.freedesktop.portal.FileChooser.OpenFile() for details.
</para></listitem>
</varlistentry>
</variablelist>
-->
<method name="SaveFiles">
<arg type="s" name="parent_window" direction="in"/>
<arg type="s" name="title" direction="in"/>
<arg type="a{sv}" name="options" direction="in"/>
<arg type="o" name="handle" direction="out"/>
</method>
<property name="version" type="u" access="read"/>
</interface>
</node>

86
src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml

@ -0,0 +1,86 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2015 Red Hat, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
Author: Alexander Larsson <alexl@redhat.com>
-->
<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
<!--
org.freedesktop.portal.Request:
@short_description: Shared request interface
The Request interface is shared by all portal interfaces. When a
portal method is called, the reply includes a handle (i.e. object path)
for a Request object, which will stay alive for the duration of the
user interaction related to the method call.
The portal indicates that a portal request interaction is over by
emitting the #org.freedesktop.portal.Request::Response signal on the
Request object.
The application can abort the interaction calling
org.freedesktop.portal.Request.Close() on the Request object.
Since version 0.9 of xdg-desktop-portal, the handle will be of the form
/org/freedesktop/portal/desktop/request/SENDER/TOKEN,
where SENDER is the callers unique name, with the initial ':' removed and
all '.' replaced by '_', and TOKEN is a unique token that the caller provided
with the handle_token key in the options vardict.
This change was made to let applications subscribe to the Response signal before
making the initial portal call, thereby avoiding a race condition. It is recommended
that the caller should verify that the returned handle is what it expected, and update
its signal subscription if it isn't. This ensures that applications will work with both
old and new versions of xdg-desktop-portal.
The token that the caller provides should be unique and not guessable. To avoid clashes
with calls made from unrelated libraries, it is a good idea to use a per-library prefix
combined with a random number.
-->
<interface name="org.freedesktop.portal.Request">
<!--
Close:
Closes the portal request to which this object refers and ends all
related user interaction (dialogs, etc).
A Response signal will not be emitted in this case.
-->
<method name="Close">
</method>
<!--
Response:
@response: Numeric response
@results: Vardict with results. The keys and values in the vardict depend on the request.
Emitted when the user interaction for a portal request is over.
The @response indicates how the user interaction ended:
<simplelist>
<member>0: Success, the request is carried out</member>
<member>1: The user cancelled the interaction</member>
<member>2: The user interaction was ended in some other way</member>
</simplelist>
-->
<signal name="Response">
<arg type="u" name="response"/>
<arg type="a{sv}" name="results"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
</signal>
</interface>
</node>

99
src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml

@ -0,0 +1,99 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2018 Igalia S.L.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
Author: Patrick Griffis <pgriffis@igalia.com>
-->
<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
<!--
org.freedesktop.portal.Settings:
@short_description: Settings interface
This interface provides read-only access to a small number
of host settings required for toolkits similar to XSettings.
It is not for general purpose settings.
Currently the interface provides the following keys:
<variablelist>
<varlistentry>
<term>org.freedesktop.appearance color-scheme u</term>
<listitem><para>
Indicates the system's preferred color scheme.
Supported values are:
<simplelist>
<member>0: No preference</member>
<member>1: Prefer dark appearance</member>
<member>2: Prefer light appearance</member>
</simplelist>
Unknown values should be treated as 0 (no preference).
</para></listitem>
</varlistentry>
</variablelist>
Implementations can provide other keys; they are entirely
implementation details that are undocumented. If you are a
toolkit and want to use this please open an issue.
This documentation describes version 1 of this interface.
-->
<interface name="org.freedesktop.portal.Settings">
<!--
ReadAll:
@namespaces: List of namespaces to filter results by, supports simple globbing explained below.
@value: Dictionary of namespaces to its keys and values.
If @namespaces is an empty array or contains an empty string it matches all. Globbing is supported but only for
trailing sections, e.g. "org.example.*".
-->
<method name='ReadAll'>
<arg name='namespaces' type='as'/>
<arg name='value' direction='out' type='a{sa{sv}}'/>
</method>
<!--
Read:
@namespace: Namespace to look up @key in.
@key: The key to get.
@value: The value @key is set to.
Reads a single value. Returns an error on any unknown namespace or key.
-->
<method name='Read'>
<arg name='namespace' type='s'/>
<arg name='key' type='s'/>
<arg name='value' direction='out' type='v'/>
</method>
<!--
SettingChanged:
@namespace: Namespace of changed setting.
@key: The key of changed setting.
@value: The new value.
Emitted when a setting changes.
-->
<signal name='SettingChanged'>
<arg name='namespace' direction='out' type='s'/>
<arg name='key' direction='out' type='s'/>
<arg name='value' direction='out' type='v'/>
</signal>
<property name="version" type="u" access="read"/>
</interface>
</node>

8
src/Avalonia.FreeDesktop/NativeMethods.cs

@ -15,18 +15,18 @@ namespace Avalonia.FreeDesktop
public static string ReadLink(string path)
{
var symlinkSize = Encoding.UTF8.GetByteCount(path);
var bufferSize = 4097; // PATH_MAX is (usually?) 4096, but we need to know if the result was truncated
const int BufferSize = 4097; // PATH_MAX is (usually?) 4096, but we need to know if the result was truncated
var symlink = ArrayPool<byte>.Shared.Rent(symlinkSize + 1);
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
var buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
try
{
Encoding.UTF8.GetBytes(path, 0, path.Length, symlink, 0);
symlink[symlinkSize] = 0;
var size = readlink(symlink, buffer, bufferSize);
Debug.Assert(size < bufferSize); // if this fails, we need to increase the buffer size (dynamically?)
var size = readlink(symlink, buffer, BufferSize);
Debug.Assert(size < BufferSize); // if this fails, we need to increase the buffer size (dynamically?)
return Encoding.UTF8.GetString(buffer, 0, (int)size);
}

2
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -392,7 +392,7 @@ namespace Avalonia.Headless
}
public void PushOpacity(double opacity)
public void PushOpacity(double opacity, Rect rect)
{
}

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

104
src/Avalonia.Themes.Simple/Controls/SplitButton.xaml

@ -2,8 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:Avalonia.Controls.Converters">
<!-- This style is heavily based on the Fluent v1 style.
The base resources are switched out along with a few minor changes such as Padding, CornerRadius. -->
<!-- This style is based on the Fluent v1 style. -->
<Design.PreviewWith>
<Border Padding="20">
@ -26,37 +25,7 @@
<converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight" />
<converters:CornerRadiusFilterConverter x:Key="BottomCornerRadiusFilterConverter" Filter="BottomLeft, BottomRight" />
<converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft" />
<SolidColorBrush x:Key="SplitButtonBackground" Color="{DynamicResource ThemeControlMidColor}" />
<SolidColorBrush x:Key="SplitButtonBackgroundPointerOver" Color="{DynamicResource ThemeControlMidColor}" />
<SolidColorBrush x:Key="SplitButtonBackgroundPressed" Color="{DynamicResource ThemeBorderHighColor}" />
<SolidColorBrush x:Key="SplitButtonBackgroundDisabled" Color="{DynamicResource ThemeControlMidColor}"
Opacity="{DynamicResource ThemeDisabledOpacity}" />
<SolidColorBrush x:Key="SplitButtonBackgroundChecked" Color="{DynamicResource ThemeBorderHighColor}" />
<SolidColorBrush x:Key="SplitButtonBackgroundCheckedPointerOver" Color="{DynamicResource ThemeBorderHighColor}" />
<SolidColorBrush x:Key="SplitButtonBackgroundCheckedPressed" Color="{DynamicResource ThemeBorderHighColor}" />
<SolidColorBrush x:Key="SplitButtonBackgroundCheckedDisabled" Color="{DynamicResource ThemeControlHighColor}"
Opacity="{DynamicResource ThemeDisabledOpacity}" />
<SolidColorBrush x:Key="SplitButtonForeground" Color="{DynamicResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="SplitButtonForegroundPointerOver" Color="{DynamicResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="SplitButtonForegroundPressed" Color="{DynamicResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="SplitButtonForegroundDisabled" Color="{DynamicResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="SplitButtonForegroundChecked" Color="{DynamicResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="SplitButtonForegroundCheckedPointerOver" Color="{DynamicResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="SplitButtonForegroundCheckedPressed" Color="{DynamicResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="SplitButtonForegroundCheckedDisabled" Color="{DynamicResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="SplitButtonBorderBrush" Color="{DynamicResource ThemeBorderLowColor}" />
<SolidColorBrush x:Key="SplitButtonBorderBrushPointerOver" Color="{DynamicResource ThemeBorderMidColor}" />
<SolidColorBrush x:Key="SplitButtonBorderBrushPressed" Color="{DynamicResource ThemeBorderLowColor}" />
<SolidColorBrush x:Key="SplitButtonBorderBrushDisabled" Color="{DynamicResource ThemeBorderLowColor}" />
<SolidColorBrush x:Key="SplitButtonBorderBrushChecked" Color="{DynamicResource ThemeBorderMidColor}" />
<SolidColorBrush x:Key="SplitButtonBorderBrushCheckedPointerOver" Color="{DynamicResource ThemeBorderMidColor}" />
<SolidColorBrush x:Key="SplitButtonBorderBrushCheckedPressed" Color="{DynamicResource ThemeControlHighColor}" />
<SolidColorBrush x:Key="SplitButtonBorderBrushCheckedDisabled" Color="{DynamicResource ThemeControlMidColor}"
Opacity="{DynamicResource ThemeDisabledOpacity}" />
<ControlTheme x:Key="SimpleSplitButtonComponent" TargetType="Button">
<Setter Property="Template">
<ControlTemplate>
@ -75,66 +44,75 @@
</Setter>
<Style Selector="^:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SplitButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource SplitButtonBorderBrushPointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundPointerOver}" />
<Setter Property="Background" Value="{DynamicResource ThemeControlMidColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidColor}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SplitButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource SplitButtonBorderBrushPressed}" />
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundPressed}" />
<Setter Property="Background" Value="{DynamicResource ThemeBorderHighColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderLowColor}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
</Style>
<Style Selector="^:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SplitButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource SplitButtonBorderBrushDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundDisabled}" />
<Setter Property="Background">
<SolidColorBrush Color="{DynamicResource ThemeControlMidColor}"
Opacity="{DynamicResource ThemeDisabledOpacity}" />
</Setter>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderLowColor}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
</Style>
<Style Selector="^[Tag=flyout-open] /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SplitButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource SplitButtonBorderBrushPressed}" />
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundPressed}" />
<Setter Property="Background" Value="{DynamicResource ThemeBorderHighColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderLowColor}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
</Style>
<Style Selector="^[Tag=checked]">
<Style Selector="^ /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SplitButtonBackgroundChecked}" />
<Setter Property="BorderBrush" Value="{DynamicResource SplitButtonBorderBrushChecked}" />
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundChecked}" />
<Setter Property="Background" Value="{DynamicResource ThemeBorderHighColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidColor}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
</Style>
<Style Selector="^:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SplitButtonBackgroundCheckedPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource SplitButtonBorderBrushCheckedPointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundCheckedPointerOver}" />
<Setter Property="Background" Value="{DynamicResource ThemeBorderHighColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidColor}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SplitButtonBackgroundCheckedPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource SplitButtonBorderBrushCheckedPressed}" />
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundCheckedPressed}" />
<Setter Property="Background" Value="{DynamicResource ThemeBorderHighColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeControlHighColor}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
</Style>
<Style Selector="^:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SplitButtonBackgroundCheckedDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource SplitButtonBorderBrushCheckedDisabled}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource SplitButtonForegroundCheckedDisabled}" />
<Setter Property="Background">
<SolidColorBrush Color="{DynamicResource ThemeControlHighColor}"
Opacity="{DynamicResource ThemeDisabledOpacity}" />
</Setter>
<Setter Property="BorderBrush">
<SolidColorBrush Color="{DynamicResource ThemeControlMidColor}"
Opacity="{DynamicResource ThemeDisabledOpacity}" />
</Setter>
<Setter Property="TextElement.Foreground" Value="{DynamicResource ThemeForegroundColor}" />
</Style>
</Style>
<Style Selector="^[Tag=checked-flyout-open] /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SplitButtonBackgroundCheckedPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource SplitButtonBorderBrushCheckedPressed}" />
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundCheckedPressed}" />
<Setter Property="Background" Value="{DynamicResource ThemeBorderHighColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeControlHighColor}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type SplitButton}" TargetType="SplitButton">
<Setter Property="Background" Value="{DynamicResource SplitButtonBackground}" />
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForeground}" />
<Setter Property="BorderBrush" Value="{DynamicResource SplitButtonBorderBrush}" />
<Setter Property="Background" Value="{DynamicResource ThemeControlMidColor}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderLowColor}" />
<Setter Property="BorderThickness" Value="{DynamicResource SplitButtonBorderThemeThickness}" />
<Setter Property="MinHeight" Value="{DynamicResource SplitButtonMinHeight}" />
<Setter Property="HorizontalAlignment" Value="Left" />

6
src/Avalonia.Themes.Simple/Controls/SplitView.xaml

@ -17,8 +17,6 @@
<x:Double x:Key="SplitViewOpenPaneThemeLength">320</x:Double>
<x:Double x:Key="SplitViewCompactPaneThemeLength">48</x:Double>
<SolidColorBrush x:Key="SplitViewLightDismissOverlayBackground" Color="{DynamicResource ThemeControlLowColor}"
Opacity="0.6" />
<sys:TimeSpan x:Key="SplitViewPaneAnimationOpenDuration">00:00:00.2</sys:TimeSpan>
<sys:TimeSpan x:Key="SplitViewPaneAnimationCloseDuration">00:00:00.1</sys:TimeSpan>
<Easing x:Key="SplitViewPaneAnimationEasing">0.1,0.9,0.2,1.0</Easing>
@ -240,7 +238,9 @@
<Setter Property="Fill" Value="Transparent" />
</Style>
<Style Selector="^:lightdismiss /template/ Rectangle#LightDismissLayer">
<Setter Property="Fill" Value="{DynamicResource SplitViewLightDismissOverlayBackground}" />
<Setter Property="Fill">
<SolidColorBrush Color="{DynamicResource ThemeControlLowColor}" Opacity="0.6" />
</Setter>
</Style>
<Style Selector="^:overlay:open /template/ Rectangle#LightDismissLayer">

91
src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml

@ -36,43 +36,10 @@
<x:Double x:Key="ToggleSwitchThemeMinWidth">0</x:Double>
<Thickness x:Key="ToggleSwitchOnStrokeThickness">0</Thickness>
<Thickness x:Key="ToggleSwitchOuterBorderStrokeThickness">1</Thickness>
<SolidColorBrush x:Key="ToggleSwitchContentForeground" Color="{DynamicResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="ToggleSwitchContentForegroundDisabled" Color="{DynamicResource ThemeForegroundLowColor}" />
<SolidColorBrush x:Key="ToggleSwitchHeaderForeground" Color="{DynamicResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="ToggleSwitchHeaderForegroundDisabled" Color="{DynamicResource ThemeForegroundLowColor}" />
<SolidColorBrush x:Key="ToggleSwitchContainerBackground" Color="Transparent" />
<SolidColorBrush x:Key="ToggleSwitchContainerBackgroundPointerOver" Color="Transparent" />
<SolidColorBrush x:Key="ToggleSwitchContainerBackgroundPressed" Color="Transparent" />
<SolidColorBrush x:Key="ToggleSwitchContainerBackgroundDisabled" Color="Transparent" />
<SolidColorBrush x:Key="ToggleSwitchFillOff" Color="Transparent" />
<SolidColorBrush x:Key="ToggleSwitchFillOffPointerOver" Color="Transparent" />
<SolidColorBrush x:Key="ToggleSwitchFillOffPressed" Color="{DynamicResource ThemeControlMidHighColor}" />
<SolidColorBrush x:Key="ToggleSwitchFillOffDisabled" Color="Transparent" />
<SolidColorBrush x:Key="ToggleSwitchStrokeOff" Color="{DynamicResource ThemeBorderMidColor}" />
<SolidColorBrush x:Key="ToggleSwitchStrokeOffPointerOver" Color="{DynamicResource ThemeBorderHighColor}" />
<SolidColorBrush x:Key="ToggleSwitchStrokeOffPressed" Color="{DynamicResource ThemeBorderHighColor}" />
<SolidColorBrush x:Key="ToggleSwitchStrokeOffDisabled" Color="{DynamicResource ThemeForegroundLowColor}" />
<SolidColorBrush x:Key="ToggleSwitchFillOn" Color="{DynamicResource ThemeAccentColor}" />
<SolidColorBrush x:Key="ToggleSwitchFillOnPointerOver" Color="{DynamicResource ThemeAccentColor2}" />
<SolidColorBrush x:Key="ToggleSwitchFillOnPressed" Color="{DynamicResource ThemeAccentColor3}" />
<SolidColorBrush x:Key="ToggleSwitchFillOnDisabled" Color="{DynamicResource ThemeForegroundLowColor}" />
<SolidColorBrush x:Key="ToggleSwitchStrokeOn" Color="{DynamicResource ThemeAccentColor}" />
<SolidColorBrush x:Key="ToggleSwitchStrokeOnPointerOver" Color="{DynamicResource ThemeAccentColor2}" />
<SolidColorBrush x:Key="ToggleSwitchStrokeOnPressed" Color="{DynamicResource ThemeAccentColor3}" />
<SolidColorBrush x:Key="ToggleSwitchStrokeOnDisabled" Color="{DynamicResource ThemeForegroundLowColor}" />
<SolidColorBrush x:Key="ToggleSwitchKnobFillOff" Color="{DynamicResource ThemeBorderMidColor}" />
<SolidColorBrush x:Key="ToggleSwitchKnobFillOffPointerOver" Color="{DynamicResource ThemeBorderHighColor}" />
<SolidColorBrush x:Key="ToggleSwitchKnobFillOffPressed" Color="{DynamicResource ThemeBorderHighColor}" />
<SolidColorBrush x:Key="ToggleSwitchKnobFillOffDisabled" Color="{DynamicResource ThemeForegroundLowColor}" />
<SolidColorBrush x:Key="ToggleSwitchKnobFillOn" Color="{DynamicResource HighlightForegroundColor}" />
<SolidColorBrush x:Key="ToggleSwitchKnobFillOnPointerOver" Color="{DynamicResource HighlightForegroundColor}" />
<SolidColorBrush x:Key="ToggleSwitchKnobFillOnPressed" Color="{DynamicResource HighlightForegroundColor}" />
<SolidColorBrush x:Key="ToggleSwitchKnobFillOnDisabled" Color="{DynamicResource HighlightForegroundColor}" />
<ControlTheme x:Key="{x:Type ToggleSwitch}"
TargetType="ToggleSwitch">
<Setter Property="Foreground" Value="{DynamicResource ToggleSwitchContentForeground}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
@ -111,7 +78,7 @@
<Grid x:Name="SwitchAreaGrid"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
Background="{DynamicResource ToggleSwitchContainerBackground}"
Background="Transparent"
TemplatedControl.IsTemplateFocusTarget="True"
Margin="0,5" />
@ -136,8 +103,8 @@
Height="20"
Width="40"
CornerRadius="10"
Background="{DynamicResource ToggleSwitchFillOff}"
BorderBrush="{DynamicResource ToggleSwitchStrokeOff}"
Background="Transparent"
BorderBrush="{DynamicResource ThemeBorderMidColor}"
BorderThickness="{DynamicResource ToggleSwitchOuterBorderStrokeThickness}" />
<Border x:Name="SwitchKnobBounds"
@ -145,8 +112,8 @@
Height="20"
Width="40"
CornerRadius="10"
Background="{DynamicResource ToggleSwitchFillOn}"
BorderBrush="{DynamicResource ToggleSwitchStrokeOn}"
Background="{DynamicResource ThemeAccentColor}"
BorderBrush="{DynamicResource ThemeAccentColor}"
BorderThickness="{DynamicResource ToggleSwitchOnStrokeThickness}" />
<Canvas x:Name="PART_SwitchKnob" Grid.Row="1"
@ -162,11 +129,11 @@
</Grid.Transitions>
<Ellipse x:Name="SwitchKnobOn"
Fill="{DynamicResource ToggleSwitchKnobFillOn}"
Fill="{DynamicResource HighlightForegroundColor}"
Width="10" Height="10" />
<Ellipse x:Name="SwitchKnobOff"
Fill="{DynamicResource ToggleSwitchKnobFillOff}"
Fill="{DynamicResource ThemeBorderMidColor}"
Width="10" Height="10" />
</Grid>
</Canvas>
@ -181,71 +148,71 @@
<!-- PointerOverState -->
<Style Selector="^:pointerover /template/ Border#OuterBorder">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOffPointerOver}" />
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOffPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighColor}" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="^:pointerover /template/ Ellipse#SwitchKnobOff">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOffPointerOver}" />
<Setter Property="Fill" Value="{DynamicResource ThemeBorderHighColor}" />
</Style>
<Style Selector="^:pointerover /template/ Ellipse#SwitchKnobOn">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOnPointerOver}" />
<Setter Property="Fill" Value="{DynamicResource HighlightForegroundColor}" />
</Style>
<Style Selector="^:pointerover /template/ Border#SwitchKnobBounds">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOnPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOnPointerOver}" />
<Setter Property="Background" Value="{DynamicResource ThemeAccentColor2}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentColor2}" />
</Style>
<Style Selector="^:pointerover /template/ Grid#SwitchAreaGrid">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchContainerBackgroundPointerOver}" />
<Setter Property="Background" Value="Transparent" />
</Style>
<!-- PressedState -->
<Style Selector="^:pressed /template/ Border#OuterBorder">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOffPressed}" />
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOffPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighColor}" />
<Setter Property="Background" Value="{DynamicResource ThemeControlMidHighColor}" />
</Style>
<Style Selector="^:pressed /template/ Border#SwitchKnobBounds">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOnPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOnPressed}" />
<Setter Property="Background" Value="{DynamicResource ThemeAccentColor3}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentColor3}" />
</Style>
<Style Selector="^:pressed /template/ Ellipse#SwitchKnobOff">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOffPressed}" />
<Setter Property="Fill" Value="{DynamicResource ThemeBorderHighColor}" />
</Style>
<Style Selector="^:pressed /template/ Ellipse#SwitchKnobOn">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOnPressed}" />
<Setter Property="Fill" Value="{DynamicResource HighlightForegroundColor}" />
</Style>
<Style Selector="^:pressed /template/ Grid#SwitchAreaGrid">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchContainerBackgroundPressed}" />
<Setter Property="Background" Value="Transparent" />
</Style>
<!-- DisabledState -->
<Style Selector="^:disabled">
<Setter Property="Foreground" Value="{DynamicResource ToggleSwitchHeaderForegroundDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundLowColor}" />
</Style>
<Style Selector="^:disabled /template/ Border#OuterBorder">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOffDisabled}" />
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOffDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundLowColor}" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="^:disabled /template/ Ellipse#SwitchKnobOff">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOffDisabled}" />
<Setter Property="Fill" Value="{DynamicResource ThemeForegroundLowColor}" />
</Style>
<Style Selector="^:disabled /template/ Ellipse#SwitchKnobOn">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOnDisabled}" />
<Setter Property="Fill" Value="{DynamicResource HighlightForegroundColor}" />
</Style>
<Style Selector="^:disabled /template/ Border#SwitchKnobBounds">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOnDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOnDisabled}" />
<Setter Property="Background" Value="{DynamicResource ThemeForegroundLowColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundLowColor}" />
</Style>
<!-- CheckedState -->

6
src/Avalonia.X11/X11Window.cs

@ -212,10 +212,10 @@ namespace Avalonia.X11
_x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref _xSyncCounter, 1);
}
_storageProvider = new CompositeStorageProvider(new Func<Task<IStorageProvider>>[]
_storageProvider = new CompositeStorageProvider(new[]
{
() => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreate(Handle) : Task.FromResult<IStorageProvider>(null),
() => GtkSystemDialog.TryCreate(this),
() => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult<IStorageProvider>(null),
() => GtkSystemDialog.TryCreate(this)
});
}

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

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

Loading…
Cancel
Save