Browse Source

Merge branch 'master' into refactor/merge-core-assemblies

feature/ilogicalvisualchildren
Steven Kirk 4 years ago
parent
commit
0a01ea9bf2
  1. 6
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  2. 1
      native/Avalonia.Native/src/OSX/common.h
  3. 11
      native/Avalonia.Native/src/OSX/main.mm
  4. 33
      native/Avalonia.Native/src/OSX/trayicon.h
  5. 85
      native/Avalonia.Native/src/OSX/trayicon.mm
  6. 1
      native/Avalonia.Native/src/OSX/window.h
  7. 54
      native/Avalonia.Native/src/OSX/window.mm
  8. 1
      packages/Avalonia/AvaloniaBuildTasks.targets
  9. 8
      readme.md
  10. 25
      samples/ControlCatalog/App.xaml
  11. 9
      samples/ControlCatalog/App.xaml.cs
  12. 1
      samples/ControlCatalog/MainView.xaml
  13. 9
      samples/ControlCatalog/MainWindow.xaml
  14. 2
      samples/ControlCatalog/MainWindow.xaml.cs
  15. 2
      samples/ControlCatalog/Pages/ButtonPage.xaml
  16. 6
      samples/ControlCatalog/Pages/CheckBoxPage.xaml
  17. 20
      samples/ControlCatalog/Pages/DialogsPage.xaml
  18. 24
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  19. 11
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  20. 6
      samples/ControlCatalog/Pages/RadioButtonPage.xaml
  21. 10
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  22. 6
      samples/ControlCatalog/Pages/ToggleSwitchPage.xaml
  23. 1
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  24. 26
      samples/ControlCatalog/ViewModels/ApplicationViewModel.cs
  25. 5
      src/Avalonia.Base/Logging/LogArea.cs
  26. 26
      src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs
  27. 32
      src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs
  28. 8
      src/Avalonia.Base/Media/DrawingImage.cs
  29. 1
      src/Avalonia.Base/Metadata/TemplateContent.cs
  30. 4
      src/Avalonia.Base/Rendering/DeferredRenderer.cs
  31. 20
      src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs
  32. 12
      src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  33. 14
      src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs
  34. 45
      src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
  35. 56
      src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
  36. 145
      src/Avalonia.Base/Styling/NthChildSelector.cs
  37. 23
      src/Avalonia.Base/Styling/NthLastChildSelector.cs
  38. 16
      src/Avalonia.Base/Styling/Selectors.cs
  39. 18
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  40. 2
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  41. 29
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  42. 56
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  43. 4
      src/Avalonia.Controls/ApiCompatBaseline.txt
  44. 2
      src/Avalonia.Controls/Border.cs
  45. 10
      src/Avalonia.Controls/Button.cs
  46. 15
      src/Avalonia.Controls/ContextMenu.cs
  47. 43
      src/Avalonia.Controls/ItemsControl.cs
  48. 13
      src/Avalonia.Controls/NativeMenu.Export.cs
  49. 3
      src/Avalonia.Controls/NativeMenuItem.cs
  50. 2
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  51. 24
      src/Avalonia.Controls/Panel.cs
  52. 17
      src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs
  53. 35
      src/Avalonia.Controls/Platform/ITrayIconImpl.cs
  54. 5
      src/Avalonia.Controls/Platform/IWindowingPlatform.cs
  55. 16
      src/Avalonia.Controls/Platform/PlatformManager.cs
  56. 41
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  57. 19
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  58. 2
      src/Avalonia.Controls/Primitives/AccessText.cs
  59. 4
      src/Avalonia.Controls/Primitives/Popup.cs
  60. 4
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  61. 3
      src/Avalonia.Controls/ProgressBar.cs
  62. 2
      src/Avalonia.Controls/Remote/RemoteWidget.cs
  63. 10
      src/Avalonia.Controls/RepeatButton.cs
  64. 37
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  65. 9
      src/Avalonia.Controls/Templates/IControlTemplate.cs
  66. 20
      src/Avalonia.Controls/Templates/TemplateResult.cs
  67. 2
      src/Avalonia.Controls/TextBox.cs
  68. 234
      src/Avalonia.Controls/TrayIcon.cs
  69. 27
      src/Avalonia.Controls/Utils/IEnumerableUtils.cs
  70. 7
      src/Avalonia.Controls/WindowTransparencyLevel.cs
  71. 4
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  72. 2
      src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
  73. 26
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
  74. 5
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
  75. 4
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
  76. 9
      src/Avalonia.FreeDesktop/DBusHelper.cs
  77. 75
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  78. 457
      src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs
  79. 2
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  80. 63
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  81. 5
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  82. 63
      src/Avalonia.Native/TrayIconImpl.cs
  83. 9
      src/Avalonia.Native/avn.idl
  84. 3
      src/Avalonia.Themes.Default/AutoCompleteBox.xaml
  85. 1
      src/Avalonia.Themes.Default/Button.xaml
  86. 1
      src/Avalonia.Themes.Default/CheckBox.xaml
  87. 3
      src/Avalonia.Themes.Default/ComboBox.xaml
  88. 1
      src/Avalonia.Themes.Default/ContextMenu.xaml
  89. 2
      src/Avalonia.Themes.Default/FlyoutPresenter.xaml
  90. 2
      src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml
  91. 4
      src/Avalonia.Themes.Default/MenuItem.xaml
  92. 28
      src/Avalonia.Themes.Default/OverlayPopupHost.xaml
  93. 12
      src/Avalonia.Themes.Default/PopupRoot.xaml
  94. 1
      src/Avalonia.Themes.Default/RadioButton.xaml
  95. 1
      src/Avalonia.Themes.Default/ToggleButton.xaml
  96. 1
      src/Avalonia.Themes.Default/ToggleSwitch.xaml
  97. 1
      src/Avalonia.Themes.Fluent/Controls/Button.xaml
  98. 1
      src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
  99. 23
      src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml
  100. 7
      src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml

6
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@ -22,6 +22,7 @@
37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; };
520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; };
522D5959258159C1006F7F7A /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 522D5958258159C1006F7F7A /* Carbon.framework */; };
523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */ = {isa = PBXBuildFile; fileRef = 523484C926EA688F00EA0C2C /* trayicon.mm */; };
5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; };
5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; };
AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; };
@ -51,6 +52,8 @@
37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = "<group>"; };
520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = "<group>"; };
522D5958258159C1006F7F7A /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; };
523484C926EA688F00EA0C2C /* trayicon.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = trayicon.mm; sourceTree = "<group>"; };
523484CB26EA68AA00EA0C2C /* trayicon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = trayicon.h; sourceTree = "<group>"; };
5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = "<group>"; };
5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = "<group>"; };
5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
@ -114,6 +117,8 @@
AB00E4F62147CA920032A60A /* main.mm */,
37155CE3233C00EB0034DCE9 /* menu.h */,
520624B222973F4100C4DCEF /* menu.mm */,
523484C926EA688F00EA0C2C /* trayicon.mm */,
523484CB26EA68AA00EA0C2C /* trayicon.h */,
1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */,
37A517B22159597E00FBA241 /* Screens.mm */,
37C09D8721580FE4006A6758 /* SystemDialogs.mm */,
@ -204,6 +209,7 @@
1A1852DC23E05814008F0DED /* deadlock.mm in Sources */,
5B21A982216530F500CEE36E /* cursor.mm in Sources */,
37DDA9B0219330F8002E132B /* AvnString.mm in Sources */,
523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */,
AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */,
1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */,
1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */,

1
native/Avalonia.Native/src/OSX/common.h

@ -22,6 +22,7 @@ extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop);
extern IAvnCursorFactory* CreateCursorFactory();
extern IAvnGlDisplay* GetGlDisplay();
extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);
extern IAvnTrayIcon* CreateTrayIcon();
extern IAvnMenuItem* CreateAppMenuItem();
extern IAvnMenuItem* CreateAppMenuItemSeparator();
extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);

11
native/Avalonia.Native/src/OSX/main.mm

@ -303,6 +303,17 @@ public:
}
}
virtual HRESULT CreateTrayIcon (IAvnTrayIcon** ppv) override
{
START_COM_CALL;
@autoreleasepool
{
*ppv = ::CreateTrayIcon();
return S_OK;
}
}
virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override
{
START_COM_CALL;

33
native/Avalonia.Native/src/OSX/trayicon.h

@ -0,0 +1,33 @@
//
// trayicon.h
// Avalonia.Native.OSX
//
// Created by Dan Walmsley on 09/09/2021.
// Copyright © 2021 Avalonia. All rights reserved.
//
#ifndef trayicon_h
#define trayicon_h
#include "common.h"
class AvnTrayIcon : public ComSingleObject<IAvnTrayIcon, &IID_IAvnTrayIcon>
{
private:
NSStatusItem* _native;
public:
FORWARD_IUNKNOWN()
AvnTrayIcon();
~AvnTrayIcon ();
virtual HRESULT SetIcon (void* data, size_t length) override;
virtual HRESULT SetMenu (IAvnMenu* menu) override;
virtual HRESULT SetIsVisible (bool isVisible) override;
};
#endif /* trayicon_h */

85
native/Avalonia.Native/src/OSX/trayicon.mm

@ -0,0 +1,85 @@
#include "common.h"
#include "trayicon.h"
#include "menu.h"
extern IAvnTrayIcon* CreateTrayIcon()
{
@autoreleasepool
{
return new AvnTrayIcon();
}
}
AvnTrayIcon::AvnTrayIcon()
{
_native = [[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength];
}
AvnTrayIcon::~AvnTrayIcon()
{
if(_native != nullptr)
{
[[_native statusBar] removeStatusItem:_native];
_native = nullptr;
}
}
HRESULT AvnTrayIcon::SetIcon (void* data, size_t length)
{
START_COM_CALL;
@autoreleasepool
{
if(data != nullptr)
{
NSData *imageData = [NSData dataWithBytes:data length:length];
NSImage *image = [[NSImage alloc] initWithData:imageData];
NSSize originalSize = [image size];
NSSize size;
size.height = [[NSFont menuFontOfSize:0] pointSize] * 1.333333;
auto scaleFactor = size.height / originalSize.height;
size.width = originalSize.width * scaleFactor;
[image setSize: size];
[_native setImage:image];
}
else
{
[_native setImage:nullptr];
}
return S_OK;
}
}
HRESULT AvnTrayIcon::SetMenu (IAvnMenu* menu)
{
START_COM_CALL;
@autoreleasepool
{
auto appMenu = dynamic_cast<AvnAppMenu*>(menu);
if(appMenu != nullptr)
{
[_native setMenu:appMenu->GetNative()];
}
}
return S_OK;
}
HRESULT AvnTrayIcon::SetIsVisible(bool isVisible)
{
START_COM_CALL;
@autoreleasepool
{
[_native setVisible:isVisible];
}
return S_OK;
}

1
native/Avalonia.Native/src/OSX/window.h

@ -12,6 +12,7 @@ class WindowBaseImpl;
-(AvnPixelSize) getPixelSize;
-(AvnPlatformResizeReason) getResizeReason;
-(void) setResizeReason:(AvnPlatformResizeReason)reason;
+ (AvnPoint)toAvnPoint:(CGPoint)p;
@end
@interface AutoFitContentView : NSView

54
native/Avalonia.Native/src/OSX/window.mm

@ -52,7 +52,6 @@ public:
[Window setBackingType:NSBackingStoreBuffered];
[Window setOpaque:false];
[Window setContentView: StandardContainer];
}
virtual HRESULT ObtainNSWindowHandle(void** ret) override
@ -125,6 +124,8 @@ public:
SetPosition(lastPositionSet);
UpdateStyle();
[Window setContentView: StandardContainer];
[Window setTitle:_lastTitle];
if(ShouldTakeFocusOnShow() && activate)
@ -231,6 +232,8 @@ public:
virtual HRESULT GetFrameSize(AvnSize* ret) override
{
START_COM_CALL;
@autoreleasepool
{
if(ret == nullptr)
@ -321,6 +324,7 @@ public:
BaseEvents->Resized(AvnSize{x,y}, reason);
}
[StandardContainer setFrameSize:NSSize{x,y}];
[Window setContentSize:NSSize{x, y}];
}
@finally
@ -1541,7 +1545,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return pt;
}
- (AvnPoint)toAvnPoint:(CGPoint)p
+ (AvnPoint)toAvnPoint:(CGPoint)p
{
AvnPoint result;
@ -1598,7 +1602,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
}
auto localPoint = [self convertPoint:[event locationInWindow] toView:self];
auto avnPoint = [self toAvnPoint:localPoint];
auto avnPoint = [AvnView toAvnPoint:localPoint];
auto point = [self translateLocalPoint:avnPoint];
AvnVector delta;
@ -1943,7 +1947,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id <NSDraggingInfo>)info
{
auto localPoint = [self convertPoint:[info draggingLocation] toView:self];
auto avnPoint = [self toAvnPoint:localPoint];
auto avnPoint = [AvnView toAvnPoint:localPoint];
auto point = [self translateLocalPoint:avnPoint];
auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]];
NSDragOperation nsop = [info draggingSourceOperationMask];
@ -2376,6 +2380,48 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_parent->BaseEvents->PositionChanged(position);
}
}
- (AvnPoint) translateLocalPoint:(AvnPoint)pt
{
pt.Y = [self frame].size.height - pt.Y;
return pt;
}
- (void)sendEvent:(NSEvent *)event
{
if(_parent != nullptr)
{
switch(event.type)
{
case NSEventTypeLeftMouseDown:
{
auto avnPoint = [AvnView toAvnPoint:[event locationInWindow]];
auto point = [self translateLocalPoint:avnPoint];
AvnVector delta;
_parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta);
}
break;
case NSEventTypeMouseEntered:
{
_parent->UpdateCursor();
}
break;
case NSEventTypeMouseExited:
{
[[NSCursor arrowCursor] set];
}
break;
default:
break;
}
}
[super sendEvent:event];
}
@end
class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup

1
packages/Avalonia/AvaloniaBuildTasks.targets

@ -54,6 +54,7 @@
<Output TaskParameter="HashResult" PropertyName="AvaloniaResourcesDependencyHash" />
</Hash>
<MakeDir Directories="$(IntermediateOutputPath)/Avalonia" />
<WriteLinesToFile Overwrite="true" File="$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache" Lines="$(AvaloniaResourcesDependencyHash)" WriteOnlyWhenDifferent="True" />
</Target>

8
readme.md

@ -60,6 +60,9 @@ See the [build instructions here](Documentation/build.md).
## Contributing
This project exists thanks to all the people who contribute.
<a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
Please read the [contribution guidelines](CONTRIBUTING.md) before submitting a pull request.
## Code of Conduct
@ -71,11 +74,6 @@ For more information see the [.NET Foundation Code of Conduct](https://dotnetfou
Avalonia is licenced under the [MIT licence](licence.md).
## Contributors
This project exists thanks to all the people who contribute. [[Contribute](https://avaloniaui.net/contributing)].
<a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
### Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)]

25
samples/ControlCatalog/App.xaml

@ -1,5 +1,8 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ControlCatalog.ViewModels"
x:DataType="vm:ApplicationViewModel"
x:CompileBindings="True"
x:Class="ControlCatalog.App">
<Application.Styles>
<Style Selector="TextBlock.h1">
@ -22,6 +25,26 @@
<Style Selector="Label.h3">
<Setter Property="FontSize" Value="12" />
</Style>
<StyleInclude Source="/SideBar.xaml"/>
<StyleInclude Source="/SideBar.xaml" />
</Application.Styles>
<TrayIcon.Icons>
<TrayIcons>
<TrayIcon Icon="/Assets/test_icon.ico" ToolTipText="Avalonia Tray Icon ToolTip">
<TrayIcon.Menu>
<NativeMenu>
<NativeMenuItem Header="Settings">
<NativeMenu>
<NativeMenuItem Header="Option 1" ToggleType="Radio" IsChecked="True" Command="{Binding ToggleCommand}" />
<NativeMenuItem Header="Option 2" ToggleType="Radio" IsChecked="True" Command="{Binding ToggleCommand}" />
<NativeMenuItemSeparator />
<NativeMenuItem Header="Option 3" ToggleType="CheckBox" IsChecked="True" Command="{Binding ToggleCommand}" />
<NativeMenuItem Icon="/Assets/test_icon.ico" Header="Restore Defaults" Command="{Binding ToggleCommand}" />
</NativeMenu>
</NativeMenuItem>
<NativeMenuItem Header="Exit" Command="{Binding ExitCommand}" />
</NativeMenu>
</TrayIcon.Menu>
</TrayIcon>
</TrayIcons>
</TrayIcon.Icons>
</Application>

9
samples/ControlCatalog/App.xaml.cs

@ -1,14 +1,21 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Styling;
using ControlCatalog.ViewModels;
namespace ControlCatalog
{
public class App : Application
{
public App()
{
DataContext = new ApplicationViewModel();
}
private static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml")
@ -97,7 +104,9 @@ namespace ControlCatalog
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
desktopLifetime.MainWindow = new MainWindow();
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
singleViewLifetime.MainView = new MainView();

1
samples/ControlCatalog/MainView.xaml

@ -98,6 +98,7 @@
<ComboBoxItem>Transparent</ComboBoxItem>
<ComboBoxItem>Blur</ComboBoxItem>
<ComboBoxItem>AcrylicBlur</ComboBoxItem>
<ComboBoxItem>Mica</ComboBoxItem>
</ComboBox>
<ComboBox Items="{Binding WindowStates}" SelectedItem="{Binding WindowState}" />
</StackPanel>

9
samples/ControlCatalog/MainWindow.xaml

@ -12,6 +12,7 @@
ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}"
TransparencyLevelHint="{Binding TransparencyLevel}"
x:Name="MainWindow"
Background="Transparent"
x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}">
<NativeMenu.Menu>
<NativeMenu>
@ -62,11 +63,11 @@
<Panel Margin="{Binding #MainWindow.OffScreenMargin}">
<DockPanel LastChildFill="True" Margin="{Binding #MainWindow.WindowDecorationMargin}">
<Menu Name="MainMenu" DockPanel.Dock="Top">
<MenuItem Header="File">
<MenuItem Header="Exit" Command="{Binding ExitCommand}" />
<MenuItem Header="_File">
<MenuItem Header="E_xit" Command="{Binding ExitCommand}" />
</MenuItem>
<MenuItem Header="Help">
<MenuItem Header="About" Command="{Binding AboutCommand}" />
<MenuItem Header="_Help">
<MenuItem Header="_About" Command="{Binding AboutCommand}" />
</MenuItem>
</Menu>
<local:MainView />

2
samples/ControlCatalog/MainWindow.xaml.cs

@ -35,6 +35,8 @@ namespace ControlCatalog
var mainMenu = this.FindControl<Menu>("MainMenu");
mainMenu.AttachedToVisualTree += MenuAttached;
ExtendClientAreaChromeHints = Avalonia.Platform.ExtendClientAreaChromeHints.OSXThickTitleBar;
}
public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit";

2
samples/ControlCatalog/Pages/ButtonPage.xaml

@ -10,7 +10,7 @@
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8" Width="200">
<Button>Standard XAML Button</Button>
<Button>Standard _XAML Button</Button>
<Button Foreground="White">Foreground</Button>
<Button Background="{DynamicResource SystemAccentColor}">Background</Button>
<Button IsEnabled="False">Disabled</Button>

6
samples/ControlCatalog/Pages/CheckBoxPage.xaml

@ -11,9 +11,9 @@
Spacing="16">
<StackPanel Orientation="Vertical"
Spacing="16">
<CheckBox>Unchecked</CheckBox>
<CheckBox IsChecked="True">Checked</CheckBox>
<CheckBox IsChecked="{x:Null}">Indeterminate</CheckBox>
<CheckBox>_Unchecked</CheckBox>
<CheckBox IsChecked="True">_Checked</CheckBox>
<CheckBox IsChecked="{x:Null}">_Indeterminate</CheckBox>
<CheckBox IsChecked="True" IsEnabled="False">Disabled</CheckBox>
</StackPanel>
<StackPanel Orientation="Vertical"

20
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -3,15 +3,15 @@
x:Class="ControlCatalog.Pages.DialogsPage">
<StackPanel Orientation="Vertical" Spacing="4" Margin="4">
<CheckBox Name="UseFilters">Use filters</CheckBox>
<Button Name="OpenFile">Open File</Button>
<Button Name="SaveFile">Save File</Button>
<Button Name="SelectFolder">Select Folder</Button>
<Button Name="OpenBoth">Select Both</Button>
<Button Name="DecoratedWindow">Decorated window</Button>
<Button Name="DecoratedWindowDialog">Decorated window (dialog)</Button>
<Button Name="Dialog">Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (No taskbar icon)</Button>
<Button Name="OwnedWindow">Owned window</Button>
<Button Name="OwnedWindowNoTaskbar">Owned window (No taskbar icon)</Button>
<Button Name="OpenFile">_Open File</Button>
<Button Name="SaveFile">_Save File</Button>
<Button Name="SelectFolder">Select Fo_lder</Button>
<Button Name="OpenBoth">Select _Both</Button>
<Button Name="DecoratedWindow">Decorated _window</Button>
<Button Name="DecoratedWindowDialog">Decorated w_indow (dialog)</Button>
<Button Name="Dialog">_Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (_No taskbar icon)</Button>
<Button Name="OwnedWindow">Own_ed window</Button>
<Button Name="OwnedWindowNoTaskbar">Owned window (No tas_kbar icon)</Button>
</StackPanel>
</UserControl>

24
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -1,17 +1,33 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ItemsRepeaterPage">
<UserControl.Styles>
<Style Selector="ItemsRepeater TextBlock.oddTemplate">
<Setter Property="Background" Value="Yellow" />
<Setter Property="Foreground" Value="Black" />
</Style>
<Style Selector="ItemsRepeater TextBlock.evenTemplate">
<Setter Property="Background" Value="Wheat" />
<Setter Property="Foreground" Value="Black" />
</Style>
<Style Selector="ItemsRepeater TextBlock:nth-child(5n+3)">
<Setter Property="Foreground" Value="Red" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="ItemsRepeater TextBlock:nth-last-child(5n+4)">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</UserControl.Styles>
<UserControl.Resources>
<RecyclePool x:Key="RecyclePool" />
<DataTemplate x:Key="odd">
<TextBlock Background="Yellow"
Foreground="Black"
<TextBlock Classes="oddTemplate"
Height="{Binding Height}"
Text="{Binding Text}"/>
</DataTemplate>
<DataTemplate x:Key="even">
<TextBlock Background="Wheat"
Foreground="Black"
<TextBlock Classes="evenTemplate"
Height="{Binding Height}"
Text="{Binding Text}"/>
</DataTemplate>

11
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -2,9 +2,20 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ListBoxPage">
<DockPanel>
<DockPanel.Styles>
<Style Selector="ListBox ListBoxItem:nth-child(5n+3)">
<Setter Property="Foreground" Value="Red" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="ListBox ListBoxItem:nth-last-child(5n+4)">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</DockPanel.Styles>
<StackPanel DockPanel.Dock="Top" Margin="4">
<TextBlock Classes="h1">ListBox</TextBlock>
<TextBlock Classes="h2">Hosts a collection of ListBoxItem.</TextBlock>
<TextBlock Classes="h2">Each 5th item is highlighted with nth-child(5n+3) and nth-last-child(5n+4) rules.</TextBlock>
</StackPanel>
<StackPanel DockPanel.Dock="Right" Margin="4">
<CheckBox IsChecked="{Binding Multiple}">Multiple</CheckBox>

6
samples/ControlCatalog/Pages/RadioButtonPage.xaml

@ -11,9 +11,9 @@
Spacing="16">
<StackPanel Orientation="Vertical"
Spacing="16">
<RadioButton IsChecked="True">Option 1</RadioButton>
<RadioButton>Option 2</RadioButton>
<RadioButton IsChecked="{x:Null}">Option 3</RadioButton>
<RadioButton IsChecked="True">_Option 1</RadioButton>
<RadioButton>O_ption 2</RadioButton>
<RadioButton IsChecked="{x:Null}">Op_tion 3</RadioButton>
<RadioButton IsEnabled="False">Disabled</RadioButton>
</StackPanel>
<StackPanel Orientation="Vertical"

10
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -11,7 +11,15 @@
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200" />
<TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200"
FontFamily="Comic Sans MS"
Foreground="Blue">
<TextBox.ContextFlyout>
<Flyout>
<TextBlock>Custom context flyout</TextBlock>
</Flyout>
</TextBox.ContextFlyout>
</TextBox>
<TextBox Width="200" Watermark="ReadOnly" IsReadOnly="True" Text="This is read only"/>
<TextBox Width="200" Watermark="Numeric Watermark" x:Name="numericWatermark"/>
<TextBox Width="200"

6
samples/ControlCatalog/Pages/ToggleSwitchPage.xaml

@ -14,7 +14,7 @@
<Border Classes="Thin">
<StackPanel>
<ToggleSwitch Content="headered" IsChecked="true" Margin="10"/>
<ToggleSwitch Content="h_eadered" IsChecked="true" Margin="10"/>
<TextBox Classes="CodeBox"
Text="&lt;ToggleSwitch&gt;headered&lt;/ToggleSwitch&gt;"/>
</StackPanel>
@ -24,7 +24,7 @@
<Border Classes="Thin">
<StackPanel>
<ToggleSwitch Content="Custom"
<ToggleSwitch Content="_Custom"
OnContent="On"
OffContent="Off"
Margin="10"/>
@ -40,7 +40,7 @@ ContentOff=&quot;Off&quot; /&gt;"
<Border Classes="Thin">
<StackPanel>
<ToggleSwitch Content="Just Click!" Margin="10">
<ToggleSwitch Content="_Just Click!" Margin="10">
<ToggleSwitch.OnContent>
<Image Source="/Assets/hirsch-899118_640.jpg" Height="32"/>
</ToggleSwitch.OnContent>

1
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@ -14,6 +14,7 @@
<ComboBoxItem>Transparent</ComboBoxItem>
<ComboBoxItem>Blur</ComboBoxItem>
<ComboBoxItem>AcrylicBlur</ComboBoxItem>
<ComboBoxItem>Mica</ComboBoxItem>
</ComboBox>
</StackPanel>
</UserControl>

26
samples/ControlCatalog/ViewModels/ApplicationViewModel.cs

@ -0,0 +1,26 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using MiniMvvm;
namespace ControlCatalog.ViewModels
{
public class ApplicationViewModel : ViewModelBase
{
public ApplicationViewModel()
{
ExitCommand = MiniCommand.Create(() =>
{
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
lifetime.Shutdown();
}
});
ToggleCommand = MiniCommand.Create(() => { });
}
public MiniCommand ExitCommand { get; }
public MiniCommand ToggleCommand { get; }
}
}

5
src/Avalonia.Base/Logging/LogArea.cs

@ -39,5 +39,10 @@ namespace Avalonia.Logging
/// The log event comes from Win32Platform.
/// </summary>
public const string Win32Platform = nameof(Win32Platform);
/// <summary>
/// The log event comes from X11Platform.
/// </summary>
public const string X11Platform = nameof(X11Platform);
}
}

26
src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs

@ -0,0 +1,26 @@
#nullable enable
using System;
namespace Avalonia.LogicalTree
{
/// <summary>
/// Event args for <see cref="IChildIndexProvider.ChildIndexChanged"/> event.
/// </summary>
public class ChildIndexChangedEventArgs : EventArgs
{
public ChildIndexChangedEventArgs()
{
}
public ChildIndexChangedEventArgs(ILogical child)
{
Child = child;
}
/// <summary>
/// Logical child which index was changed.
/// If null, all children should be reset.
/// </summary>
public ILogical? Child { get; }
}
}

32
src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs

@ -0,0 +1,32 @@
#nullable enable
using System;
namespace Avalonia.LogicalTree
{
/// <summary>
/// Child's index and total count information provider used by list-controls (ListBox, StackPanel, etc.)
/// </summary>
/// <remarks>
/// Used by nth-child and nth-last-child selectors.
/// </remarks>
public interface IChildIndexProvider
{
/// <summary>
/// Gets child's actual index in order of the original source.
/// </summary>
/// <param name="child">Logical child.</param>
/// <returns>Index or -1 if child was not found.</returns>
int GetChildIndex(ILogical child);
/// <summary>
/// Total children count or null if source is infinite.
/// Some Avalonia features might not work if <see cref="TryGetTotalCount"/> returns false, for instance: nth-last-child selector.
/// </summary>
bool TryGetTotalCount(out int count);
/// <summary>
/// Notifies subscriber when child's index or total count was changed.
/// </summary>
event EventHandler<ChildIndexChangedEventArgs>? ChildIndexChanged;
}
}

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

@ -9,6 +9,14 @@ namespace Avalonia.Media
/// </summary>
public class DrawingImage : AvaloniaObject, IImage, IAffectsRender
{
public DrawingImage()
{
}
public DrawingImage(Drawing drawing)
{
Drawing = drawing;
}
/// <summary>
/// Defines the <see cref="Drawing"/> property.
/// </summary>

1
src/Avalonia.Base/Metadata/TemplateContent.cs

@ -8,5 +8,6 @@ namespace Avalonia.Metadata
[AttributeUsage(AttributeTargets.Property)]
public class TemplateContentAttribute : Attribute
{
public Type TemplateResultType { get; set; }
}
}

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

@ -279,13 +279,13 @@ namespace Avalonia.Rendering
/// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{
return (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty;
return (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty;
}
/// <inheritdoc/>
void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
{
var childScene = (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual];
var childScene = (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual];
if (childScene != null)
{

20
src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs

@ -11,19 +11,23 @@ namespace Avalonia.Rendering.SceneGraph
/// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
/// clip push.
/// </summary>
/// <param name="transform">The current transform.</param>
/// <param name="clip">The clip to push.</param>
public ClipNode(Rect clip)
public ClipNode(Matrix transform, Rect clip)
{
Transform = transform;
Clip = clip;
}
/// <summary>
/// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
/// clip push.
/// </summary>
/// <param name="transform">The current transform.</param>
/// <param name="clip">The clip to push.</param>
public ClipNode(RoundedRect clip)
public ClipNode(Matrix transform, RoundedRect clip)
{
Transform = transform;
Clip = clip;
}
@ -43,23 +47,31 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
public RoundedRect? Clip { get; }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <inheritdoc/>
public bool HitTest(Point p) => false;
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="transform">The transform of the other draw operation.</param>
/// <param name="clip">The clip 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(RoundedRect? clip) => Clip == clip;
public bool Equals(Matrix transform, RoundedRect? clip) => Transform == transform && Clip == clip;
/// <inheritdoc/>
public void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
if (Clip.HasValue)
{
context.PushClip(Clip.Value);

12
src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -303,9 +303,9 @@ namespace Avalonia.Rendering.SceneGraph
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Item.Equals(clip))
if (next == null || !next.Item.Equals(Transform, clip))
{
Add(new ClipNode(clip));
Add(new ClipNode(Transform, clip));
}
else
{
@ -318,9 +318,9 @@ namespace Avalonia.Rendering.SceneGraph
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Item.Equals(clip))
if (next == null || !next.Item.Equals(Transform, clip))
{
Add(new ClipNode(clip));
Add(new ClipNode(Transform, clip));
}
else
{
@ -333,9 +333,9 @@ namespace Avalonia.Rendering.SceneGraph
{
var next = NextDrawAs<GeometryClipNode>();
if (next == null || !next.Item.Equals(clip))
if (next == null || !next.Item.Equals(Transform, clip))
{
Add(new GeometryClipNode(clip));
Add(new GeometryClipNode(Transform, clip));
}
else
{

14
src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs

@ -11,9 +11,11 @@ namespace Avalonia.Rendering.SceneGraph
/// Initializes a new instance of the <see cref="GeometryClipNode"/> class that represents a
/// geometry clip push.
/// </summary>
/// <param name="transform">The current transform.</param>
/// <param name="clip">The clip to push.</param>
public GeometryClipNode(IGeometryImpl clip)
public GeometryClipNode(Matrix transform, IGeometryImpl clip)
{
Transform = transform;
Clip = clip;
}
@ -33,23 +35,31 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
public IGeometryImpl Clip { get; }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <inheritdoc/>
public bool HitTest(Point p) => false;
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="transform">The transform of the other draw operation.</param>
/// <param name="clip">The clip 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(IGeometryImpl clip) => Clip == clip;
public bool Equals(Matrix transform, IGeometryImpl clip) => Transform == transform && Clip == clip;
/// <inheritdoc/>
public void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
if (Clip != null)
{
context.PushGeometryClip(Clip);

45
src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
@ -82,8 +83,46 @@ namespace Avalonia.Rendering.SceneGraph
public override bool HitTest(Point p)
{
// TODO: Implement line hit testing.
return false;
if (!Transform.HasInverse)
return false;
p *= Transform.Invert();
var halfThickness = Pen.Thickness / 2;
var minX = Math.Min(P1.X, P2.X) - halfThickness;
var maxX = Math.Max(P1.X, P2.X) + halfThickness;
var minY = Math.Min(P1.Y, P2.Y) - halfThickness;
var maxY = Math.Max(P1.Y, P2.Y) + halfThickness;
if (p.X < minX || p.X > maxX || p.Y < minY || p.Y > maxY)
return false;
var a = P1;
var b = P2;
//If dot1 or dot2 is negative, then the angle between the perpendicular and the segment is obtuse.
//The distance from a point to a straight line is defined as the
//length of the vector formed by the point and the closest point of the segment
Vector ap = p - a;
var dot1 = Vector.Dot(b - a, ap);
if (dot1 < 0)
return ap.Length <= Pen.Thickness / 2;
Vector bp = p - b;
var dot2 = Vector.Dot(a - b, bp);
if (dot2 < 0)
return bp.Length <= halfThickness;
var bXaX = b.X - a.X;
var bYaY = b.Y - a.Y;
var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) /
(Math.Sqrt(bXaX * bXaX + bYaY * bYaY));
return Math.Abs(distance) <= halfThickness;
}
}
}

56
src/Avalonia.Base/Styling/Activators/NthChildActivator.cs

@ -0,0 +1,56 @@
#nullable enable
using Avalonia.LogicalTree;
namespace Avalonia.Styling.Activators
{
/// <summary>
/// An <see cref="IStyleActivator"/> which is active when control's index was changed.
/// </summary>
internal sealed class NthChildActivator : StyleActivatorBase
{
private readonly ILogical _control;
private readonly IChildIndexProvider _provider;
private readonly int _step;
private readonly int _offset;
private readonly bool _reversed;
public NthChildActivator(
ILogical control,
IChildIndexProvider provider,
int step, int offset, bool reversed)
{
_control = control;
_provider = provider;
_step = step;
_offset = offset;
_reversed = reversed;
}
protected override void Initialize()
{
PublishNext(IsMatching());
_provider.ChildIndexChanged += ChildIndexChanged;
}
protected override void Deinitialize()
{
_provider.ChildIndexChanged -= ChildIndexChanged;
}
private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e)
{
// Run matching again if:
// 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index.
// 2. e.Child is null, when all children indeces were changed.
// 3. Subscribed child index was changed.
if (_reversed
|| e.Child is null
|| e.Child == _control)
{
PublishNext(IsMatching());
}
}
private bool IsMatching() => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
}
}

145
src/Avalonia.Base/Styling/NthChildSelector.cs

@ -0,0 +1,145 @@
#nullable enable
using System;
using System.Text;
using Avalonia.LogicalTree;
using Avalonia.Styling.Activators;
namespace Avalonia.Styling
{
/// <summary>
/// The :nth-child() pseudo-class matches elements based on their position in a group of siblings.
/// </summary>
/// <remarks>
/// Element indices are 1-based.
/// </remarks>
public class NthChildSelector : Selector
{
private const string NthChildSelectorName = "nth-child";
private const string NthLastChildSelectorName = "nth-last-child";
private readonly Selector? _previous;
private readonly bool _reversed;
internal protected NthChildSelector(Selector? previous, int step, int offset, bool reversed)
{
_previous = previous;
Step = step;
Offset = offset;
_reversed = reversed;
}
/// <summary>
/// Creates an instance of <see cref="NthChildSelector"/>
/// </summary>
/// <param name="previous">Previous selector.</param>
/// <param name="step">Position step.</param>
/// <param name="offset">Initial index offset.</param>
public NthChildSelector(Selector? previous, int step, int offset)
: this(previous, step, offset, false)
{
}
public override bool InTemplate => _previous?.InTemplate ?? false;
public override bool IsCombinator => false;
public override Type? TargetType => _previous?.TargetType;
public int Step { get; }
public int Offset { get; }
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
{
if (!(control is ILogical logical))
{
return SelectorMatch.NeverThisType;
}
var controlParent = logical.LogicalParent;
if (controlParent is IChildIndexProvider childIndexProvider)
{
return subscribe
? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed))
: Evaluate(logical, childIndexProvider, Step, Offset, _reversed);
}
else
{
return SelectorMatch.NeverThisInstance;
}
}
internal static SelectorMatch Evaluate(
ILogical logical, IChildIndexProvider childIndexProvider,
int step, int offset, bool reversed)
{
var index = childIndexProvider.GetChildIndex(logical);
if (index < 0)
{
return SelectorMatch.NeverThisInstance;
}
if (reversed)
{
if (childIndexProvider.TryGetTotalCount(out var totalCountValue))
{
index = totalCountValue - index;
}
else
{
return SelectorMatch.NeverThisInstance;
}
}
else
{
// nth child index is 1-based
index += 1;
}
var n = Math.Sign(step);
var diff = index - offset;
var match = diff == 0 || (Math.Sign(diff) == n && diff % step == 0);
return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
}
protected override Selector? MovePrevious() => _previous;
public override string ToString()
{
var expectedCapacity = NthLastChildSelectorName.Length + 8;
var stringBuilder = new StringBuilder(_previous?.ToString(), expectedCapacity);
stringBuilder.Append(':');
stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName);
stringBuilder.Append('(');
var hasStep = false;
if (Step != 0)
{
hasStep = true;
stringBuilder.Append(Step);
stringBuilder.Append('n');
}
if (Offset > 0)
{
if (hasStep)
{
stringBuilder.Append('+');
}
stringBuilder.Append(Offset);
}
else if (Offset < 0)
{
stringBuilder.Append('-');
stringBuilder.Append(-Offset);
}
stringBuilder.Append(')');
return stringBuilder.ToString();
}
}
}

23
src/Avalonia.Base/Styling/NthLastChildSelector.cs

@ -0,0 +1,23 @@
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
/// The :nth-child() pseudo-class matches elements based on their position among a group of siblings, counting from the end.
/// </summary>
/// <remarks>
/// Element indices are 1-based.
/// </remarks>
public class NthLastChildSelector : NthChildSelector
{
/// <summary>
/// Creates an instance of <see cref="NthLastChildSelector"/>
/// </summary>
/// <param name="previous">Previous selector.</param>
/// <param name="step">Position step.</param>
/// <param name="offset">Initial index offset, counting from the end.</param>
public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true)
{
}
}
}

16
src/Avalonia.Base/Styling/Selectors.cs

@ -123,6 +123,22 @@ namespace Avalonia.Styling
return new NotSelector(previous, argument);
}
/// <inheritdoc cref="NthChildSelector"/>
/// <inheritdoc cref="NthChildSelector(Selector?, int, int)"/>
/// <returns>The selector.</returns>
public static Selector NthChild(this Selector previous, int step, int offset)
{
return new NthChildSelector(previous, step, offset);
}
/// <inheritdoc cref="NthLastChildSelector"/>
/// <inheritdoc cref="NthLastChildSelector(Selector?, int, int)"/>
/// <returns>The selector.</returns>
public static Selector NthLastChild(this Selector previous, int step, int offset)
{
return new NthLastChildSelector(previous, step, offset);
}
/// <summary>
/// Returns a selector which matches a type.
/// </summary>

18
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -2223,6 +2223,7 @@ namespace Avalonia.Controls
if (IsEnabled && DisplayData.NumDisplayedScrollingElements > 0)
{
var handled = false;
var ignoreInvalidate = false;
var scrollHeight = 0d;
// Vertical scroll handling
@ -2252,8 +2253,7 @@ namespace Avalonia.Controls
// Horizontal scroll handling
if (delta.X != 0)
{
var originalHorizontalOffset = HorizontalOffset;
var horizontalOffset = originalHorizontalOffset - delta.X;
var horizontalOffset = HorizontalOffset - delta.X;
var widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth);
if (horizontalOffset < 0)
@ -2265,16 +2265,20 @@ namespace Avalonia.Controls
horizontalOffset = widthNotVisible;
}
if (horizontalOffset != originalHorizontalOffset)
if (UpdateHorizontalOffset(horizontalOffset))
{
HorizontalOffset = horizontalOffset;
// We don't need to invalidate once again after UpdateHorizontalOffset.
ignoreInvalidate = true;
handled = true;
}
}
if (handled)
{
InvalidateRowsMeasure(invalidateIndividualElements: false);
if (!ignoreInvalidate)
{
InvalidateRowsMeasure(invalidateIndividualElements: false);
}
return true;
}
}
@ -2932,7 +2936,7 @@ namespace Avalonia.Controls
return SetCurrentCellCore(columnIndex, slot, commitEdit: true, endRowEdit: true);
}
internal void UpdateHorizontalOffset(double newValue)
internal bool UpdateHorizontalOffset(double newValue)
{
if (HorizontalOffset != newValue)
{
@ -2940,7 +2944,9 @@ namespace Avalonia.Controls
InvalidateColumnHeadersMeasure();
InvalidateRowsMeasure(true);
return true;
}
return false;
}
internal bool UpdateSelectionAndCurrency(int columnIndex, int slot, DataGridSelectionAction action, bool scrollIntoView)

2
src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs

@ -133,7 +133,7 @@ namespace Avalonia.Controls
protected abstract IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem);
internal AvaloniaProperty BindingTarget { get; set; }
protected AvaloniaProperty BindingTarget { get; set; }
internal void SetHeaderFromBinding()
{

29
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -9,6 +9,7 @@ using Avalonia.VisualTree;
using Avalonia.Collections;
using Avalonia.Utilities;
using System;
using System.ComponentModel;
using System.Linq;
using System.Diagnostics;
using Avalonia.Controls.Utils;
@ -653,6 +654,34 @@ namespace Avalonia.Controls
return null;
}
/// <summary>
/// Clears the current sort direction
/// </summary>
public void ClearSort()
{
//InvokeProcessSort is already validating if sorting is possible
_headerCell?.InvokeProcessSort(Input.KeyModifiers.Control);
}
/// <summary>
/// Switches the current state of sort direction
/// </summary>
public void Sort()
{
//InvokeProcessSort is already validating if sorting is possible
_headerCell?.InvokeProcessSort(Input.KeyModifiers.None);
}
/// <summary>
/// Changes the sort direction of this column
/// </summary>
/// <param name="direction">New sort direction</param>
public void Sort(ListSortDirection direction)
{
//InvokeProcessSort is already validating if sorting is possible
_headerCell?.InvokeProcessSort(Input.KeyModifiers.None, direction);
}
/// <summary>
/// When overridden in a derived class, causes the column cell being edited to revert to the unedited value.
/// </summary>

56
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -35,6 +35,7 @@ namespace Avalonia.Controls
private const int DATAGRIDCOLUMNHEADER_resizeRegionWidth = 5;
private const double DATAGRIDCOLUMNHEADER_separatorThickness = 1;
private const int DATAGRIDCOLUMNHEADER_columnsDragTreshold = 5;
private bool _areHandlersSuspended;
private static DragMode _dragMode;
@ -201,21 +202,21 @@ namespace Avalonia.Controls
handled = true;
}
internal void InvokeProcessSort(KeyModifiers keyModifiers)
internal void InvokeProcessSort(KeyModifiers keyModifiers, ListSortDirection? forcedDirection = null)
{
Debug.Assert(OwningGrid != null);
if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(keyModifiers)))
if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(keyModifiers, forcedDirection)))
{
return;
}
if (OwningGrid.CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true))
{
Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers));
Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers, forcedDirection));
}
}
//TODO GroupSorting
internal void ProcessSort(KeyModifiers keyModifiers)
internal void ProcessSort(KeyModifiers keyModifiers, ListSortDirection? forcedDirection = null)
{
// if we can sort:
// - AllowUserToSortColumns and CanSort are true, and
@ -259,7 +260,14 @@ namespace Avalonia.Controls
{
if (sort != null)
{
newSort = sort.SwitchSortDirection();
if (forcedDirection == null || sort.Direction != forcedDirection)
{
newSort = sort.SwitchSortDirection();
}
else
{
newSort = sort;
}
// changing direction should not affect sort order, so we replace this column's
// sort description instead of just adding it to the end of the collection
@ -276,7 +284,10 @@ namespace Avalonia.Controls
}
else if (OwningColumn.CustomSortComparer != null)
{
newSort = DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer);
newSort = forcedDirection != null ?
DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer, forcedDirection.Value) :
DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer);
owningGrid.DataConnection.SortDescriptions.Add(newSort);
}
@ -290,6 +301,10 @@ namespace Avalonia.Controls
}
newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
if (forcedDirection != null && newSort.Direction != forcedDirection)
{
newSort = newSort.SwitchSortDirection();
}
owningGrid.DataConnection.SortDescriptions.Add(newSort);
}
@ -434,19 +449,6 @@ namespace Avalonia.Controls
OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders, distanceFromLeft, distanceFromRight);
// if we still haven't done anything about moving the mouse while
// the button is down, we remember that we're dragging, but we don't
// claim to have actually handled the event
if (_dragMode == DragMode.MouseDown)
{
_dragMode = DragMode.Drag;
}
_lastMousePositionHeaders = mousePositionHeaders;
if (args.Pointer.Captured != this && _dragMode == DragMode.Drag)
args.Pointer.Capture(this);
SetDragCursor(mousePosition);
}
@ -718,15 +720,19 @@ namespace Avalonia.Controls
{
return;
}
//handle entry into reorder mode
if (_dragMode == DragMode.MouseDown && _dragColumn == null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth))
if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth))
{
handled = CanReorderColumn(OwningColumn);
if (handled)
var distanceFromInitial = (Vector)(mousePositionHeaders - _lastMousePositionHeaders);
if (distanceFromInitial.Length > DATAGRIDCOLUMNHEADER_columnsDragTreshold)
{
OnMouseMove_BeginReorder(mousePosition);
handled = CanReorderColumn(OwningColumn);
if (handled)
{
OnMouseMove_BeginReorder(mousePosition);
}
}
}

4
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -37,6 +37,7 @@ MembersMustExist : Member 'public System.Action<Avalonia.Size> Avalonia.Controls
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action<Avalonia.Size>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation.
EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract.
@ -55,4 +56,5 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract.
Total Issues: 56
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract.
Total Issues: 58

2
src/Avalonia.Controls/Border.cs

@ -8,7 +8,9 @@ namespace Avalonia.Controls
/// <summary>
/// A control which decorates a child with a border and background.
/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
public partial class Border : Decorator, IVisualWithRoundRectClip
#pragma warning restore CS0618 // Type or member is obsolete
{
/// <summary>
/// Defines the <see cref="Background"/> property.

10
src/Avalonia.Controls/Button.cs

@ -99,6 +99,7 @@ namespace Avalonia.Controls
CommandParameterProperty.Changed.Subscribe(CommandParameterChanged);
IsDefaultProperty.Changed.Subscribe(IsDefaultChanged);
IsCancelProperty.Changed.Subscribe(IsCancelChanged);
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Button>((lbl, args) => lbl.OnAccessKey(args));
}
public Button()
@ -256,6 +257,8 @@ namespace Avalonia.Controls
}
}
protected virtual void OnAccessKey(RoutedEventArgs e) => OnClick();
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
@ -358,6 +361,13 @@ namespace Avalonia.Controls
IsPressed = false;
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
IsPressed = false;
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);

15
src/Avalonia.Controls/ContextMenu.cs

@ -329,16 +329,8 @@ namespace Avalonia.Controls
{
_popup = new Popup
{
HorizontalOffset = HorizontalOffset,
VerticalOffset = VerticalOffset,
PlacementAnchor = PlacementAnchor,
PlacementConstraintAdjustment = PlacementConstraintAdjustment,
PlacementGravity = PlacementGravity,
PlacementMode = PlacementMode,
PlacementRect = PlacementRect,
IsLightDismissEnabled = true,
OverlayDismissEventPassThrough = true,
WindowManagerAddShadowHint = WindowManagerAddShadowHint,
};
_popup.Opened += PopupOpened;
@ -358,6 +350,13 @@ namespace Avalonia.Controls
: PlacementMode;
_popup.PlacementTarget = placementTarget;
_popup.HorizontalOffset = HorizontalOffset;
_popup.VerticalOffset = VerticalOffset;
_popup.PlacementAnchor = PlacementAnchor;
_popup.PlacementConstraintAdjustment = PlacementConstraintAdjustment;
_popup.PlacementGravity = PlacementGravity;
_popup.PlacementRect = PlacementRect;
_popup.WindowManagerAddShadowHint = WindowManagerAddShadowHint;
_popup.Child = this;
IsOpen = true;
_popup.IsOpen = true;

43
src/Avalonia.Controls/ItemsControl.cs

@ -21,7 +21,7 @@ namespace Avalonia.Controls
/// Displays a collection of items.
/// </summary>
[PseudoClasses(":empty", ":singleitem")]
public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener
public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener, IChildIndexProvider
{
/// <summary>
/// The default value for the <see cref="ItemsPanel"/> property.
@ -56,6 +56,7 @@ namespace Avalonia.Controls
private IEnumerable _items = new AvaloniaList<object>();
private int _itemCount;
private IItemContainerGenerator _itemContainerGenerator;
private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
/// <summary>
/// Initializes static members of the <see cref="ItemsControl"/> class.
@ -145,11 +146,28 @@ namespace Avalonia.Controls
protected set;
}
event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
/// <inheritdoc/>
void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter)
{
if (Presenter is IChildIndexProvider oldInnerProvider)
{
oldInnerProvider.ChildIndexChanged -= PresenterChildIndexChanged;
}
Presenter = presenter;
ItemContainerGenerator.Clear();
if (Presenter is IChildIndexProvider innerProvider)
{
innerProvider.ChildIndexChanged += PresenterChildIndexChanged;
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs());
}
}
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
@ -506,5 +524,28 @@ namespace Avalonia.Controls
return null;
}
private void PresenterChildIndexChanged(object sender, ChildIndexChangedEventArgs e)
{
_childIndexChanged?.Invoke(this, e);
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
return Presenter is IChildIndexProvider innerProvider
? innerProvider.GetChildIndex(child) : -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
if (Presenter is IChildIndexProvider presenter
&& presenter.TryGetTotalCount(out count))
{
return true;
}
count = ItemCount;
return true;
}
}
}

13
src/Avalonia.Controls/NativeMenu.Export.cs

@ -52,15 +52,10 @@ namespace Avalonia.Controls
}
public static readonly AttachedProperty<NativeMenu> MenuProperty
= AvaloniaProperty.RegisterAttached<NativeMenu, AvaloniaObject, NativeMenu>("Menu"/*, validate:
(o, v) =>
{
if(!(o is Application || o is TopLevel))
throw new InvalidOperationException("NativeMenu.Menu property isn't valid on "+o.GetType());
return v;
}*/);
= AvaloniaProperty.RegisterAttached<NativeMenu, AvaloniaObject, NativeMenu>("Menu");
public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu);
public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty);
static NativeMenu()
@ -79,6 +74,10 @@ namespace Avalonia.Controls
{
GetInfo(tl).Exporter?.SetNativeMenu(args.NewValue.GetValueOrDefault());
}
else if(args.Sender is INativeMenuExporterProvider provider)
{
provider.NativeMenuExporter?.SetNativeMenu(args.NewValue.GetValueOrDefault());
}
});
}
}

3
src/Avalonia.Controls/NativeMenuItem.cs

@ -16,6 +16,7 @@ namespace Avalonia.Controls
private bool _isChecked = false;
private NativeMenuItemToggleType _toggleType;
private IBitmap _icon;
private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber;
private NativeMenu _menu;
@ -47,8 +48,6 @@ namespace Avalonia.Controls
}
}
private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber;
public NativeMenuItem()
{

2
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -332,7 +332,9 @@ namespace Avalonia.Controls
/// </summary>
static NumericUpDown()
{
#pragma warning disable CS0612 // Type or member is obsolete
CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged);
#pragma warning restore CS0612 // Type or member is obsolete
NumberFormatProperty.Changed.Subscribe(OnNumberFormatChanged);
FormatStringProperty.Changed.Subscribe(FormatStringChanged);
IncrementProperty.Changed.Subscribe(IncrementChanged);

24
src/Avalonia.Controls/Panel.cs

@ -2,8 +2,10 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Controls
{
@ -14,7 +16,7 @@ namespace Avalonia.Controls
/// Controls can be added to a <see cref="Panel"/> by adding them to its <see cref="Children"/>
/// collection. All children are layed out to fill the panel.
/// </remarks>
public class Panel : Control, IPanel
public class Panel : Control, IPanel, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -30,6 +32,8 @@ namespace Avalonia.Controls
AffectsRender<Panel>(BackgroundProperty);
}
private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
/// <summary>
/// Initializes a new instance of the <see cref="Panel"/> class.
/// </summary>
@ -53,6 +57,12 @@ namespace Avalonia.Controls
set { SetValue(BackgroundProperty, value); }
}
event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
/// <summary>
/// Renders the visual to a <see cref="DrawingContext"/>.
/// </summary>
@ -137,6 +147,7 @@ namespace Avalonia.Controls
throw new NotSupportedException();
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs());
InvalidateMeasureOnChildrenChanged();
}
@ -160,5 +171,16 @@ namespace Avalonia.Controls
var panel = control?.VisualParent as TPanel;
panel?.InvalidateMeasure();
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
return child is IControl control ? Children.IndexOf(control) : -1;
}
public bool TryGetTotalCount(out int count)
{
count = Children.Count;
return true;
}
}
}

17
src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs

@ -1,14 +1,25 @@
using System;
using System.Collections.Generic;
using Avalonia.Platform;
#nullable enable
namespace Avalonia.Controls.Platform
{
public interface ITopLevelNativeMenuExporter
public interface INativeMenuExporter
{
void SetNativeMenu(NativeMenu? menu);
}
public interface ITopLevelNativeMenuExporter : INativeMenuExporter
{
bool IsNativeMenuExported { get; }
event EventHandler OnIsNativeMenuExportedChanged;
void SetNativeMenu(NativeMenu menu);
}
public interface INativeMenuExporterProvider
{
INativeMenuExporter? NativeMenuExporter { get; }
}
public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl

35
src/Avalonia.Controls/Platform/ITrayIconImpl.cs

@ -0,0 +1,35 @@
using System;
using Avalonia.Controls.Platform;
#nullable enable
namespace Avalonia.Platform
{
public interface ITrayIconImpl : IDisposable
{
/// <summary>
/// Sets the icon of this tray icon.
/// </summary>
void SetIcon(IWindowIconImpl? icon);
/// <summary>
/// Sets the icon of this tray icon.
/// </summary>
void SetToolTipText(string? text);
/// <summary>
/// Sets if the tray icon is visible or not.
/// </summary>
void SetIsVisible(bool visible);
/// <summary>
/// Gets the MenuExporter to allow native menus to be exported to the TrayIcon.
/// </summary>
INativeMenuExporter? MenuExporter { get; }
/// <summary>
/// Gets or Sets the Action that is called when the TrayIcon is clicked.
/// </summary>
Action? OnClicked { get; set; }
}
}

5
src/Avalonia.Controls/Platform/IWindowingPlatform.cs

@ -1,8 +1,13 @@
#nullable enable
namespace Avalonia.Platform
{
public interface IWindowingPlatform
{
IWindowImpl CreateWindow();
IWindowImpl CreateEmbeddableWindow();
ITrayIconImpl? CreateTrayIcon();
}
}

16
src/Avalonia.Controls/Platform/PlatformManager.cs

@ -1,8 +1,9 @@
using System;
using System.Reactive.Disposables;
using Avalonia.Media;
using Avalonia.Platform;
#nullable enable
namespace Avalonia.Controls.Platform
{
public static partial class PlatformManager
@ -22,6 +23,19 @@ namespace Avalonia.Controls.Platform
{
}
public static ITrayIconImpl? CreateTrayIcon()
{
var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();
if (platform == null)
{
throw new Exception("Could not CreateWindow(): IWindowingPlatform is not registered.");
}
return s_designerMode ? null : platform.CreateTrayIcon();
}
public static IWindowImpl CreateWindow()
{
var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();

41
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -5,6 +5,7 @@ using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls.Presenters
@ -12,7 +13,7 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Base class for controls that present items inside an <see cref="ItemsControl"/>.
/// </summary>
public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl
public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="Items"/> property.
@ -36,6 +37,7 @@ namespace Avalonia.Controls.Presenters
private IDisposable _itemsSubscription;
private bool _createdPanel;
private IItemContainerGenerator _generator;
private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
/// <summary>
/// Initializes static members of the <see cref="ItemsPresenter"/> class.
@ -129,6 +131,12 @@ namespace Avalonia.Controls.Presenters
protected bool IsHosted => TemplatedParent is IItemsPresenterHost;
event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
/// <inheritdoc/>
public override sealed void ApplyTemplate()
{
@ -149,6 +157,8 @@ namespace Avalonia.Controls.Presenters
if (Panel != null)
{
ItemsChanged(e);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs());
}
}
@ -169,9 +179,21 @@ namespace Avalonia.Controls.Presenters
result.ItemTemplate = ItemTemplate;
}
result.Materialized += ContainerActionHandler;
result.Dematerialized += ContainerActionHandler;
result.Recycled += ContainerActionHandler;
return result;
}
private void ContainerActionHandler(object sender, ItemContainerEventArgs e)
{
for (var i = 0; i < e.Containers.Count; i++)
{
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl));
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
@ -248,5 +270,22 @@ namespace Avalonia.Controls.Presenters
{
(e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this);
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
if (child is IControl control && ItemContainerGenerator is { } generator)
{
var index = ItemContainerGenerator.IndexFromContainer(control);
return index;
}
return -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
return Items.TryGetCountFast(out count);
}
}
}

19
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -60,9 +60,6 @@ namespace Avalonia.Controls.Presenters
o => o.Viewport,
(o, v) => o.Viewport = v);
// Arbitrary chosen value, probably need to ask ILogicalScrollable
private const int LogicalScrollItemSize = 50;
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private bool _arranging;
@ -351,7 +348,8 @@ namespace Avalonia.Controls.Presenters
if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width)
{
var scrollable = Child as ILogicalScrollable;
bool isLogical = scrollable?.IsLogicalScrollEnabled == true;
var isLogical = scrollable?.IsLogicalScrollEnabled == true;
var logicalScrollItemSize = new Vector(1, 1);
double x = Offset.X;
double y = Offset.Y;
@ -361,13 +359,18 @@ namespace Avalonia.Controls.Presenters
_activeLogicalGestureScrolls?.TryGetValue(e.Id, out delta);
delta += e.Delta;
if (isLogical && scrollable is object)
{
logicalScrollItemSize = Bounds.Size / scrollable.Viewport;
}
if (Extent.Height > Viewport.Height)
{
double dy;
if (isLogical)
{
var logicalUnits = delta.Y / LogicalScrollItemSize;
delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize);
var logicalUnits = delta.Y / logicalScrollItemSize.Y;
delta = delta.WithY(delta.Y - logicalUnits * logicalScrollItemSize.Y);
dy = logicalUnits * scrollable!.ScrollSize.Height;
}
else
@ -384,8 +387,8 @@ namespace Avalonia.Controls.Presenters
double dx;
if (isLogical)
{
var logicalUnits = delta.X / LogicalScrollItemSize;
delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize);
var logicalUnits = delta.X / logicalScrollItemSize.X;
delta = delta.WithX(delta.X - logicalUnits * logicalScrollItemSize.X);
dx = logicalUnits * scrollable!.ScrollSize.Width;
}
else

2
src/Avalonia.Controls/Primitives/AccessText.cs

@ -68,7 +68,7 @@ namespace Avalonia.Controls.Primitives
if (underscore != -1 && ShowAccessKey)
{
var rect = TextLayout.HitTestTextPosition(underscore);
var offset = new Vector(0, -0.5);
var offset = new Vector(0, -1.5);
context.DrawLine(
new Pen(Foreground, 1),
rect.BottomLeft + offset,

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

@ -20,10 +20,12 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Displays a popup window.
/// </summary>
#pragma warning disable CS0612 // Type or member is obsolete
public class Popup : Control, IVisualTreeHost, IPopupHostProvider
#pragma warning restore CS0612 // Type or member is obsolete
{
public static readonly StyledProperty<bool> WindowManagerAddShadowHintProperty =
AvaloniaProperty.Register<PopupRoot, bool>(nameof(WindowManagerAddShadowHint), true);
AvaloniaProperty.Register<PopupRoot, bool>(nameof(WindowManagerAddShadowHint), false);
/// <summary>
/// Defines the <see cref="Child"/> property.

4
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@ -447,8 +447,10 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect)
{
// We need a better way for tracking the last pointer position
#pragma warning disable CS0618 // Type or member is obsolete
var pointer = topLevel.PointToClient(topLevel.PlatformImpl.MouseDevice.Position);
#pragma warning restore CS0618 // Type or member is obsolete
positionerParameters.Offset = offset;
positionerParameters.ConstraintAdjustment = constraintAdjustment;
if (placement == PlacementMode.Pointer)

3
src/Avalonia.Controls/ProgressBar.cs

@ -218,9 +218,12 @@ namespace Avalonia.Controls
TemplateProperties.Container2AnimationStartPosition = barIndicatorWidth2 * -1.5; // Position at -150%
TemplateProperties.Container2AnimationEndPosition = barIndicatorWidth2 * 1.66; // Position at 166%
#pragma warning disable CS0618 // Type or member is obsolete
// Remove these properties when we switch to fluent as default and removed the old one.
IndeterminateStartingOffset = -dim;
IndeterminateEndingOffset = dim;
#pragma warning restore CS0618 // Type or member is obsolete
var padding = Padding;
var rectangle = new RectangleGeometry(

2
src/Avalonia.Controls/Remote/RemoteWidget.cs

@ -77,8 +77,10 @@ namespace Avalonia.Controls.Remote
_bitmap.PixelSize.Height != _lastFrame.Height)
{
_bitmap?.Dispose();
#pragma warning disable CS0618 // Type or member is obsolete
_bitmap = new WriteableBitmap(new PixelSize(_lastFrame.Width, _lastFrame.Height),
new Vector(96, 96), fmt);
#pragma warning restore CS0618 // Type or member is obsolete
}
using (var l = _bitmap.Lock())
{

10
src/Avalonia.Controls/RepeatButton.cs

@ -70,6 +70,16 @@ namespace Avalonia.Controls
_repeatTimer?.Stop();
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == IsPressedProperty && change.NewValue.GetValueOrDefault<bool>() == false)
{
StopTimer();
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);

37
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -10,6 +10,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Utilities;
using Avalonia.VisualTree;
@ -19,7 +20,7 @@ namespace Avalonia.Controls
/// Represents a data-driven collection control that incorporates a flexible layout system,
/// custom views, and virtualization.
/// </summary>
public class ItemsRepeater : Panel
public class ItemsRepeater : Panel, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="HorizontalCacheLength"/> property.
@ -61,8 +62,9 @@ namespace Avalonia.Controls
private readonly ViewportManager _viewportManager;
private IEnumerable _items;
private VirtualizingLayoutContext _layoutContext;
private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
private bool _isLayoutInProgress;
private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs;
private ItemsRepeaterElementClearingEventArgs _elementClearingArgs;
private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs;
@ -163,6 +165,25 @@ namespace Avalonia.Controls
}
}
event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
return child is IControl control
? GetElementIndex(control)
: -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
count = ItemsSourceView.Count;
return true;
}
/// <summary>
/// Occurs each time an element is cleared and made available to be re-used.
/// </summary>
@ -545,6 +566,8 @@ namespace Avalonia.Controls
ElementPrepared(this, _elementPreparedArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
}
internal void OnElementClearing(IControl element)
@ -562,6 +585,8 @@ namespace Avalonia.Controls
ElementClearing(this, _elementClearingArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
}
internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex)
@ -579,6 +604,8 @@ namespace Avalonia.Controls
ElementIndexChanged(this, _elementIndexChangedArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
}
private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue)
@ -588,14 +615,14 @@ namespace Avalonia.Controls
throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout.");
}
ItemsSourceView?.Dispose();
ItemsSourceView = newValue;
if (oldValue != null)
{
oldValue.CollectionChanged -= OnItemsSourceViewChanged;
}
ItemsSourceView?.Dispose();
ItemsSourceView = newValue;
if (newValue != null)
{
newValue.CollectionChanged += OnItemsSourceViewChanged;

9
src/Avalonia.Controls/Templates/IControlTemplate.cs

@ -1,3 +1,4 @@
using System;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
@ -10,18 +11,16 @@ namespace Avalonia.Controls.Templates
{
}
public class ControlTemplateResult
public class ControlTemplateResult : TemplateResult<IControl>
{
public IControl Control { get; }
public INameScope NameScope { get; }
public ControlTemplateResult(IControl control, INameScope nameScope)
public ControlTemplateResult(IControl control, INameScope nameScope) : base(control, nameScope)
{
Control = control;
NameScope = nameScope;
}
public void Deconstruct(out IControl control, out INameScope scope)
public new void Deconstruct(out IControl control, out INameScope scope)
{
control = Control;
scope = NameScope;

20
src/Avalonia.Controls/Templates/TemplateResult.cs

@ -0,0 +1,20 @@
namespace Avalonia.Controls.Templates
{
public class TemplateResult<T>
{
public T Result { get; }
public INameScope NameScope { get; }
public TemplateResult(T result, INameScope nameScope)
{
Result = result;
NameScope = nameScope;
}
public void Deconstruct(out T result, out INameScope scope)
{
result = Result;
scope = NameScope;
}
}
}

2
src/Avalonia.Controls/TextBox.cs

@ -991,7 +991,9 @@ namespace Avalonia.Controls
{
var point = e.GetPosition(_presenter);
var index = CaretIndex = _presenter.GetCaretIndex(point);
#pragma warning disable CS0618 // Type or member is obsolete
switch (e.ClickCount)
#pragma warning restore CS0618 // Type or member is obsolete
{
case 1:
SelectionStart = SelectionEnd = index;

234
src/Avalonia.Controls/TrayIcon.cs

@ -0,0 +1,234 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using Avalonia.Collections;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Utilities;
#nullable enable
namespace Avalonia.Controls
{
public sealed class TrayIcons : AvaloniaList<TrayIcon>
{
}
public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable
{
private readonly ITrayIconImpl? _impl;
private ICommand? _command;
private TrayIcon(ITrayIconImpl? impl)
{
if (impl != null)
{
_impl = impl;
_impl.SetIsVisible(IsVisible);
_impl.OnClicked = () =>
{
Clicked?.Invoke(this, EventArgs.Empty);
if (Command?.CanExecute(CommandParameter) == true)
{
Command.Execute(CommandParameter);
}
};
}
}
public TrayIcon() : this(PlatformManager.CreateTrayIcon())
{
}
static TrayIcon()
{
IconsProperty.Changed.Subscribe(args =>
{
if (args.Sender is Application)
{
if (args.OldValue.Value != null)
{
RemoveIcons(args.OldValue.Value);
}
if (args.NewValue.Value != null)
{
args.NewValue.Value.CollectionChanged += Icons_CollectionChanged;
}
}
});
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
lifetime.Exit += Lifetime_Exit;
}
}
/// <summary>
/// Raised when the TrayIcon is clicked.
/// Note, this is only supported on Win32 and some Linux DEs,
/// on OSX this event is not raised.
/// </summary>
public event EventHandler? Clicked;
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
public static readonly DirectProperty<TrayIcon, ICommand?> CommandProperty =
Button.CommandProperty.AddOwner<TrayIcon>(
trayIcon => trayIcon.Command,
(trayIcon, command) => trayIcon.Command = command,
enableDataValidation: true);
/// <summary>
/// Defines the <see cref="CommandParameter"/> property.
/// </summary>
public static readonly StyledProperty<object?> CommandParameterProperty =
Button.CommandParameterProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="TrayIcons"/> attached property.
/// </summary>
public static readonly AttachedProperty<TrayIcons> IconsProperty
= AvaloniaProperty.RegisterAttached<TrayIcon, Application, TrayIcons>("Icons");
/// <summary>
/// Defines the <see cref="Menu"/> property.
/// </summary>
public static readonly StyledProperty<NativeMenu?> MenuProperty
= AvaloniaProperty.Register<TrayIcon, NativeMenu?>(nameof(Menu));
/// <summary>
/// Defines the <see cref="Icon"/> property.
/// </summary>
public static readonly StyledProperty<WindowIcon> IconProperty =
Window.IconProperty.AddOwner<TrayIcon>();
/// <summary>
/// Defines the <see cref="ToolTipText"/> property.
/// </summary>
public static readonly StyledProperty<string?> ToolTipTextProperty =
AvaloniaProperty.Register<TrayIcon, string?>(nameof(ToolTipText));
/// <summary>
/// Defines the <see cref="IsVisible"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsVisibleProperty =
Visual.IsVisibleProperty.AddOwner<TrayIcon>();
public static void SetIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons);
public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty);
/// <summary>
/// Gets or sets the <see cref="Command"/> property of a TrayIcon.
/// </summary>
public ICommand? Command
{
get => _command;
set => SetAndRaise(CommandProperty, ref _command, value);
}
/// <summary>
/// Gets or sets the parameter to pass to the <see cref="Command"/> property of a
/// <see cref="TrayIcon"/>.
/// </summary>
public object CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
/// <summary>
/// Gets or sets the Menu of the TrayIcon.
/// </summary>
public NativeMenu? Menu
{
get => GetValue(MenuProperty);
set => SetValue(MenuProperty, value);
}
/// <summary>
/// Gets or sets the icon of the TrayIcon.
/// </summary>
public WindowIcon Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
/// <summary>
/// Gets or sets the tooltip text of the TrayIcon.
/// </summary>
public string? ToolTipText
{
get => GetValue(ToolTipTextProperty);
set => SetValue(ToolTipTextProperty, value);
}
/// <summary>
/// Gets or sets the visibility of the TrayIcon.
/// </summary>
public bool IsVisible
{
get => GetValue(IsVisibleProperty);
set => SetValue(IsVisibleProperty, value);
}
public INativeMenuExporter? NativeMenuExporter => _impl?.MenuExporter;
private static void Lifetime_Exit(object sender, ControlledApplicationLifetimeExitEventArgs e)
{
var trayIcons = GetIcons(Application.Current);
RemoveIcons(trayIcons);
}
private static void Icons_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
RemoveIcons(e.OldItems.Cast<TrayIcon>());
}
private static void RemoveIcons(IEnumerable<TrayIcon> icons)
{
foreach (var icon in icons)
{
icon.Dispose();
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == IconProperty)
{
_impl?.SetIcon(Icon.PlatformImpl);
}
else if (change.Property == IsVisibleProperty)
{
_impl?.SetIsVisible(change.NewValue.GetValueOrDefault<bool>());
}
else if (change.Property == ToolTipTextProperty)
{
_impl?.SetToolTipText(change.NewValue.GetValueOrDefault<string?>());
}
else if (change.Property == MenuProperty)
{
_impl?.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault<NativeMenu>());
}
}
/// <summary>
/// Disposes the tray icon (removing it from the tray area).
/// </summary>
public void Dispose() => _impl?.Dispose();
}
}

27
src/Avalonia.Controls/Utils/IEnumerableUtils.cs

@ -12,23 +12,36 @@ namespace Avalonia.Controls.Utils
return items.IndexOf(item) != -1;
}
public static int Count(this IEnumerable items)
public static bool TryGetCountFast(this IEnumerable items, out int count)
{
if (items != null)
{
if (items is ICollection collection)
{
return collection.Count;
count = collection.Count;
return true;
}
else if (items is IReadOnlyCollection<object> readOnly)
{
return readOnly.Count;
}
else
{
return Enumerable.Count(items.Cast<object>());
count = readOnly.Count;
return true;
}
}
count = 0;
return false;
}
public static int Count(this IEnumerable items)
{
if (TryGetCountFast(items, out var count))
{
return count;
}
else if (items != null)
{
return Enumerable.Count(items.Cast<object>());
}
else
{
return 0;

7
src/Avalonia.Controls/WindowTransparencyLevel.cs

@ -20,6 +20,11 @@
/// <summary>
/// The window background is a blur-behind with a high blur radius. This level may fallback to Blur.
/// </summary>
AcrylicBlur
AcrylicBlur,
/// <summary>
/// The window background is based on desktop wallpaper tint with a blur. This will only work on Windows 11
/// </summary>
Mica
}
}

4
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -16,7 +16,9 @@ namespace Avalonia.DesignerSupport.Remote
private static DetachableTransportConnection s_lastWindowTransport;
private static PreviewerWindowImpl s_lastWindow;
public static List<object> PreFlightMessages = new List<object>();
public ITrayIconImpl CreateTrayIcon() => null;
public IWindowImpl CreateWindow() => new WindowStub();
public IWindowImpl CreateEmbeddableWindow()

2
src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs

@ -29,7 +29,7 @@ namespace Avalonia.Diagnostics.Models
}
}
public bool Handled { get; }
public bool Handled { get; set; }
public RoutingStrategies Route { get; }
}

26
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs

@ -55,6 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels
// FIXME: This leaks event handlers.
Event.AddClassHandler(typeof(object), HandleEvent, allRoutes, handledEventsToo: true);
Event.RouteFinished.Subscribe(HandleRouteFinished);
_isRegistered = true;
}
}
@ -92,6 +94,30 @@ namespace Avalonia.Diagnostics.ViewModels
else
handler();
}
private void HandleRouteFinished(RoutedEventArgs e)
{
if (!_isRegistered || IsEnabled == false)
return;
if (e.Source is IVisual v && BelongsToDevTool(v))
return;
var s = e.Source;
var handled = e.Handled;
var route = e.Route;
void handler()
{
if (_currentEvent != null && handled)
{
var linkIndex = _currentEvent.EventChain.Count - 1;
var link = _currentEvent.EventChain[linkIndex];
link.Handled = true;
_currentEvent.HandledBy = link;
}
}
}
private static bool BelongsToDevTool(IVisual v)
{

5
src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs

@ -63,8 +63,8 @@ namespace Avalonia.Diagnostics.ViewModels
{
if (EventChain.Count > 0)
{
var prevLink = EventChain[EventChain.Count-1];
var prevLink = EventChain[EventChain.Count - 1];
if (prevLink.Route != link.Route)
{
link.BeginsNewRoute = true;
@ -72,6 +72,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
EventChain.Add(link);
if (HandledBy == null && link.Handled)
HandledBy = link;
}

4
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs

@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
@ -42,7 +43,8 @@ namespace Avalonia.Dialogs
private static void ShellExec(string cmd, bool waitForExit = true)
{
var escapedArgs = cmd.Replace("\"", "\\\"");
var escapedArgs = Regex.Replace(cmd, "(?=[`~!#&*()|;'<>])", "\\")
.Replace("\"", "\\\\\\\"");
using (var process = Process.Start(
new ProcessStartInfo

9
src/Avalonia.FreeDesktop/DBusHelper.cs

@ -12,7 +12,7 @@ namespace Avalonia.FreeDesktop
/// This class uses synchronous execution at DBus connection establishment stage
/// then switches to using AvaloniaSynchronizationContext
/// </summary>
class DBusSyncContext : SynchronizationContext
private class DBusSyncContext : SynchronizationContext
{
private SynchronizationContext _ctx;
private object _lock = new object();
@ -51,8 +51,11 @@ namespace Avalonia.FreeDesktop
public static Connection TryInitialize(string dbusAddress = null)
{
if (Connection != null)
return Connection;
return Connection ?? TryCreateNewConnection(dbusAddress);
}
public static Connection TryCreateNewConnection(string dbusAddress = null)
{
var oldContext = SynchronizationContext.Current;
try
{

75
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -8,6 +8,7 @@ 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
@ -16,51 +17,78 @@ namespace Avalonia.FreeDesktop
{
public class DBusMenuExporter
{
public static ITopLevelNativeMenuExporter TryCreate(IntPtr xid)
public static ITopLevelNativeMenuExporter TryCreateTopLevelNativeMenu(IntPtr xid)
{
if (DBusHelper.Connection == null)
return null;
return new DBusMenuExporterImpl(DBusHelper.Connection, xid);
}
public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConection)
{
return new DBusMenuExporterImpl(currentConection, path);
}
public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/"
+ Guid.NewGuid().ToString("N");
class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable
private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable
{
private readonly Connection _dbus;
private readonly uint _xid;
private IRegistrar _registar;
private IRegistrar _registrar;
private bool _disposed;
private uint _revision = 1;
private NativeMenu _menu;
private Dictionary<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>();
private Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>();
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)
{
_dbus = dbus;
_xid = (uint)xid.ToInt32();
ObjectPath = new ObjectPath("/net/avaloniaui/dbusmenu/"
+ Guid.NewGuid().ToString().Replace("-", ""));
ObjectPath = GenerateDBusMenuObjPath;
SetNativeMenu(new NativeMenu());
Init();
}
public DBusMenuExporterImpl(Connection dbus, ObjectPath path)
{
_dbus = dbus;
_appMenu = false;
ObjectPath = path;
SetNativeMenu(new NativeMenu());
Init();
}
async void Init()
{
try
{
await _dbus.RegisterObjectAsync(this);
_registar = DBusHelper.Connection.CreateProxy<IRegistrar>(
"com.canonical.AppMenu.Registrar",
"/com/canonical/AppMenu/Registrar");
if (!_disposed)
await _registar.RegisterWindowAsync(_xid, ObjectPath);
if (_appMenu)
{
await _dbus.RegisterObjectAsync(this);
_registrar = DBusHelper.Connection.CreateProxy<IRegistrar>(
"com.canonical.AppMenu.Registrar",
"/com/canonical/AppMenu/Registrar");
if (!_disposed)
await _registrar.RegisterWindowAsync(_xid, ObjectPath);
}
else
{
await _dbus.RegisterObjectAsync(this);
}
}
catch (Exception e)
{
Console.Error.WriteLine(e);
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
@ -75,7 +103,7 @@ namespace Avalonia.FreeDesktop
_disposed = true;
_dbus.UnregisterObject(this);
// Fire and forget
_registar?.UnregisterWindowAsync(_xid);
_registrar?.UnregisterWindowAsync(_xid);
}
@ -248,17 +276,24 @@ namespace Avalonia.FreeDesktop
if (item.ToggleType != NativeMenuItemToggleType.None)
return item.IsChecked ? 1 : 0;
}
if (name == "icon-data")
{
if (item.Icon != null)
{
var ms = new MemoryStream();
item.Icon.Save(ms);
return ms.ToArray();
var loader = AvaloniaLocator.Current.GetService<IPlatformIconLoader>();
if (loader != null)
{
var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item);
using var ms = new MemoryStream();
icon.Save(ms);
return ms.ToArray();
}
}
}
if (name == "children-display")
return menu != null ? "submenu" : null;
}

457
src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs

@ -0,0 +1,457 @@
#nullable enable
using System;
using System.Diagnostics;
using System.Reactive.Disposables;
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)]
[assembly:
InternalsVisibleTo(
"Avalonia.X11, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
namespace Avalonia.FreeDesktop
{
internal class DBusTrayIconImpl : ITrayIconImpl
{
private static int s_trayIconInstanceId;
private readonly ObjectPath _dbusMenuPath;
private readonly Connection? _connection;
private IDisposable? _serviceWatchDisposable;
private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj;
private IStatusNotifierWatcher? _statusNotifierWatcher;
private DbusPixmap _icon;
private string? _sysTrayServiceName;
private string? _tooltipText;
private bool _isDisposed;
private bool _serviceConnected;
private bool _isVisible = true;
public bool IsActive { get; private set; }
public INativeMenuExporter? MenuExporter { get; }
public Action? OnClicked { get; set; }
public Func<IWindowIconImpl?, uint[]>? IconConverterDelegate { get; set; }
public DBusTrayIconImpl()
{
_connection = DBusHelper.TryCreateNewConnection();
if (_connection is null)
{
Logger.TryGet(LogEventLevel.Error, "DBUS")
?.Log(this, "Unable to get a dbus connection for system tray icons.");
return;
}
IsActive = true;
_dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath;
MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection);
WatchAsync();
}
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.");
return;
}
_serviceConnected = true;
}
private async void WatchAsync()
{
try
{
_serviceWatchDisposable =
await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!;
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, "DBUS")
?.Log(this,
$"Unable to hook watcher method on org.kde.StatusNotifierWatcher: {e}");
}
}
private void OnNameChange(ServiceOwnerChangedEventArgs obj)
{
if (_isDisposed)
return;
if (!_serviceConnected & obj.NewOwner != null)
{
_serviceConnected = true;
InitializeSNWService();
DestroyTrayIcon();
if (_isVisible)
{
CreateTrayIcon();
}
}
else if (_serviceConnected & obj.NewOwner is null)
{
DestroyTrayIcon();
_serviceConnected = false;
}
}
private void CreateTrayIcon()
{
if (_connection is null || !_serviceConnected || _isDisposed)
return;
var pid = Process.GetCurrentProcess().Id;
var tid = s_trayIconInstanceId++;
_sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}";
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_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;
}
_statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText);
_statusNotifierItemDbusObj.SetIcon(_icon);
_statusNotifierItemDbusObj.ActivationDelegate += OnClicked;
}
private void DestroyTrayIcon()
{
if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null)
return;
_connection.UnregisterObject(_statusNotifierItemDbusObj);
_connection.UnregisterServiceAsync(_sysTrayServiceName);
}
public void Dispose()
{
IsActive = false;
_isDisposed = true;
DestroyTrayIcon();
_connection?.Dispose();
_serviceWatchDisposable?.Dispose();
}
public void SetIcon(IWindowIconImpl? icon)
{
if (_isDisposed || IconConverterDelegate is null)
return;
if (icon is null)
{
_statusNotifierItemDbusObj?.SetIcon(DbusPixmap.EmptyPixmap);
return;
}
var x11iconData = IconConverterDelegate(icon);
if (x11iconData.Length == 0) return;
var w = (int)x11iconData[0];
var h = (int)x11iconData[1];
var pixLength = w * h;
var pixByteArrayCounter = 0;
var pixByteArray = new byte[w * h * 4];
for (var i = 0; i < pixLength; i++)
{
var rawPixel = x11iconData[i + 2];
pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24);
pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16);
pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8);
pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF);
}
_icon = new DbusPixmap(w, h, pixByteArray);
_statusNotifierItemDbusObj?.SetIcon(_icon);
}
public void SetIsVisible(bool visible)
{
if (_isDisposed)
return;
switch (visible)
{
case true when !_isVisible:
DestroyTrayIcon();
CreateTrayIcon();
break;
case false when _isVisible:
DestroyTrayIcon();
break;
}
_isVisible = visible;
}
public void SetToolTipText(string? text)
{
if (_isDisposed || text is null)
return;
_tooltipText = text;
_statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText);
}
}
/// <summary>
/// DBus Object used for setting system tray icons.
/// </summary>
/// <remarks>
/// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html
/// </remarks>
internal class StatusNotifierItemDbusObj : IStatusNotifierItem
{
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)
{
ObjectPath = new ObjectPath($"/StatusNotifierItem");
_backingProperties = new StatusNotifierItemProperties
{
Menu = dbusmenuPath, // Needs a dbus menu somehow
ToolTip = new ToolTip("")
};
InvalidateAll();
}
public Task ContextMenuAsync(int x, int y) => Task.CompletedTask;
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 void InvalidateAll()
{
OnTitleChanged?.Invoke();
OnIconChanged?.Invoke();
OnOverlayIconChanged?.Invoke();
OnAttentionIconChanged?.Invoke();
OnTooltipChanged?.Invoke();
}
public Task<IDisposable> WatchNewTitleAsync(Action handler, Action<Exception> onError)
{
OnTitleChanged += handler;
return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler));
}
public Task<IDisposable> WatchNewIconAsync(Action handler, Action<Exception> onError)
{
OnIconChanged += handler;
return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler));
}
public Task<IDisposable> WatchNewAttentionIconAsync(Action handler, Action<Exception> onError)
{
OnAttentionIconChanged += handler;
return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler));
}
public Task<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError)
{
OnOverlayIconChanged += handler;
return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler));
}
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)
{
NewStatusAsync += handler;
return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler));
}
public Task<object?> GetAsync(string prop)
{
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 };
InvalidateAll();
}
public void SetTitleAndTooltip(string? text)
{
if (text is null)
return;
_backingProperties.Id = text;
_backingProperties.Category = "ApplicationStatus";
_backingProperties.Status = text;
_backingProperties.Title = text;
_backingProperties.ToolTip = new ToolTip(text);
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 });
}
}

2
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -51,6 +51,8 @@ namespace Avalonia.Headless
public IWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException();
public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true);
public ITrayIconImpl CreateTrayIcon() => null;
}
internal static void Initialize()

63
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@ -9,14 +9,15 @@ using Avalonia.Threading;
namespace Avalonia.Native
{
class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter
internal class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter
{
private IAvaloniaNativeFactory _factory;
private readonly IAvaloniaNativeFactory _factory;
private bool _resetQueued = true;
private bool _exported = false;
private IAvnWindow _nativeWindow;
private bool _exported;
private readonly IAvnWindow _nativeWindow;
private NativeMenu _menu;
private __MicroComIAvnMenuProxy _nativeMenu;
private readonly IAvnTrayIcon _trayIcon;
public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory)
{
@ -33,13 +34,21 @@ namespace Avalonia.Native
DoLayoutReset();
}
public AvaloniaNativeMenuExporter(IAvnTrayIcon trayIcon, IAvaloniaNativeFactory factory)
{
_factory = factory;
_trayIcon = trayIcon;
DoLayoutReset();
}
public bool IsNativeMenuExported => _exported;
public event EventHandler OnIsNativeMenuExportedChanged;
public void SetNativeMenu(NativeMenu menu)
{
_menu = menu == null ? new NativeMenu() : menu;
_menu = menu ?? new NativeMenu();
DoLayoutReset(true);
}
@ -82,15 +91,22 @@ namespace Avalonia.Native
if (_nativeWindow is null)
{
var appMenu = NativeMenu.GetMenu(Application.Current);
if (_trayIcon is null)
{
var appMenu = NativeMenu.GetMenu(Application.Current);
if (appMenu == null)
if (appMenu == null)
{
appMenu = CreateDefaultAppMenu();
NativeMenu.SetMenu(Application.Current, appMenu);
}
SetMenu(appMenu);
}
else if (_menu != null)
{
appMenu = CreateDefaultAppMenu();
NativeMenu.SetMenu(Application.Current, appMenu);
SetMenu(_trayIcon, _menu);
}
SetMenu(appMenu);
}
else
{
@ -118,7 +134,7 @@ namespace Avalonia.Native
var appMenuHolder = menuItem?.Parent;
if (menu.Parent is null)
if (menuItem is null)
{
menuItem = new NativeMenuItem();
}
@ -136,7 +152,7 @@ namespace Avalonia.Native
if (_nativeMenu is null)
{
_nativeMenu = (__MicroComIAvnMenuProxy)__MicroComIAvnMenuProxy.Create(_factory);
_nativeMenu = __MicroComIAvnMenuProxy.Create(_factory);
_nativeMenu.Initialize(this, appMenuHolder, "");
@ -171,5 +187,26 @@ namespace Avalonia.Native
avnWindow.SetMainMenu(_nativeMenu);
}
}
private void SetMenu(IAvnTrayIcon trayIcon, NativeMenu menu)
{
var setMenu = false;
if (_nativeMenu is null)
{
_nativeMenu = __MicroComIAvnMenuProxy.Create(_factory);
_nativeMenu.Initialize(this, menu, "");
setMenu = true;
}
_nativeMenu.Update(_factory, menu);
if(setMenu)
{
trayIcon.SetMenu(_nativeMenu);
}
}
}
}

5
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -134,6 +134,11 @@ namespace Avalonia.Native
}
}
public ITrayIconImpl CreateTrayIcon ()
{
return new TrayIconImpl(_factory);
}
public IWindowImpl CreateWindow()
{
return new WindowImpl(_factory, _options, _platformGl);

63
src/Avalonia.Native/TrayIconImpl.cs

@ -0,0 +1,63 @@
using System;
using System.IO;
using Avalonia.Controls.Platform;
using Avalonia.Native.Interop;
using Avalonia.Platform;
#nullable enable
namespace Avalonia.Native
{
internal class TrayIconImpl : ITrayIconImpl
{
private readonly IAvnTrayIcon _native;
public TrayIconImpl(IAvaloniaNativeFactory factory)
{
_native = factory.CreateTrayIcon();
MenuExporter = new AvaloniaNativeMenuExporter(_native, factory);
}
public Action? OnClicked { get; set; }
public void Dispose()
{
_native.Dispose();
}
public unsafe void SetIcon(IWindowIconImpl? icon)
{
if (icon is null)
{
_native.SetIcon(null, IntPtr.Zero);
}
else
{
using (var ms = new MemoryStream())
{
icon.Save(ms);
var imageData = ms.ToArray();
fixed (void* ptr = imageData)
{
_native.SetIcon(ptr, new IntPtr(imageData.Length));
}
}
}
}
public void SetToolTipText(string? text)
{
// NOP
}
public void SetIsVisible(bool visible)
{
_native.SetIsVisible(visible.AsComBool());
}
public INativeMenuExporter? MenuExporter { get; }
}
}

9
src/Avalonia.Native/avn.idl

@ -427,6 +427,7 @@ interface IAvaloniaNativeFactory : IUnknown
HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv);
HRESULT CreateMenuItem(IAvnMenuItem** ppv);
HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv);
HRESULT CreateTrayIcon(IAvnTrayIcon** ppv);
}
[uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)]
@ -665,6 +666,14 @@ interface IAvnGlSurfaceRenderingSession : IUnknown
HRESULT GetScaling(double* ret);
}
[uuid(60992d19-38f0-4141-a0a9-76ac303801f3)]
interface IAvnTrayIcon : IUnknown
{
HRESULT SetIcon(void* data, size_t length);
HRESULT SetMenu(IAvnMenu* menu);
HRESULT SetIsVisible(bool isVisible);
}
[uuid(a7724dc1-cf6b-4fa8-9d23-228bf2593edc)]
interface IAvnMenu : IUnknown
{

3
src/Avalonia.Themes.Default/AutoCompleteBox.xaml

@ -21,7 +21,8 @@
MaxHeight="{TemplateBinding MaxDropDownHeight}"
PlacementTarget="{TemplateBinding}"
IsLightDismissEnabled="True">
<Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
<Border Background="{DynamicResource ThemeBackgroundBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1">
<ListBox Name="PART_SelectingItemsControl"
BorderThickness="0"

1
src/Avalonia.Themes.Default/Button.xaml

@ -17,6 +17,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
RecognizesAccessKey="True"
TextBlock.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>

1
src/Avalonia.Themes.Default/CheckBox.xaml

@ -41,6 +41,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
IsVisible="{TemplateBinding Content, Converter={x:Static ObjectConverters.IsNotNull}}"

3
src/Avalonia.Themes.Default/ComboBox.xaml

@ -69,7 +69,8 @@
MaxHeight="{TemplateBinding MaxDropDownHeight}"
PlacementTarget="{TemplateBinding}"
IsLightDismissEnabled="True">
<Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
<Border Background="{DynamicResource ThemeBackgroundBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1">
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">

1
src/Avalonia.Themes.Default/ContextMenu.xaml

@ -1,4 +1,5 @@
<Style xmlns="https://github.com/avaloniaui" Selector="ContextMenu">
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="4,2"/>

2
src/Avalonia.Themes.Default/FlyoutPresenter.xaml

@ -2,7 +2,7 @@
<Style Selector="FlyoutPresenter">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="4" />

2
src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml

@ -1,6 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector="MenuFlyoutPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />

4
src/Avalonia.Themes.Default/MenuItem.xaml

@ -62,7 +62,7 @@
PlacementMode="Right"
IsLightDismissEnabled="False"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Background="{TemplateBinding Background}"
<Border Background="{DynamicResource ThemeBackgroundBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer Classes="menuscroller">
@ -113,7 +113,7 @@
IsLightDismissEnabled="True"
OverlayInputPassThroughElement="{Binding $parent[Menu]}"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Background="{TemplateBinding Background}"
<Border Background="{DynamicResource ThemeBackgroundBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer Classes="menuscroller">

28
src/Avalonia.Themes.Default/OverlayPopupHost.xaml

@ -1,17 +1,21 @@
<Style xmlns="https://github.com/avaloniaui" Selector="OverlayPopupHost">
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Style xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Selector="OverlayPopupHost">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}" />
<Setter Property="FontFamily" Value="{x:Static FontFamily.Default}" />
<Setter Property="FontWeight" Value="400" />
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</Panel>
<!-- Do not forget to update Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent test -->
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</ControlTemplate>
</Setter>
</Style>

12
src/Avalonia.Themes.Default/PopupRoot.xaml

@ -1,5 +1,13 @@
<Style xmlns="https://github.com/avaloniaui" Selector="PopupRoot">
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Style xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Selector="PopupRoot">
<Setter Property="Background" Value="{x:Null}"/>
<Setter Property="TransparencyLevelHint" Value="Transparent" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}"/>
<Setter Property="FontFamily" Value="{x:Static FontFamily.Default}" />
<Setter Property="FontWeight" Value="400" />
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="Template">
<ControlTemplate>
<Panel>

1
src/Avalonia.Themes.Default/RadioButton.xaml

@ -31,6 +31,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="4,0,0,0"
RecognizesAccessKey="True"
VerticalAlignment="Center"
Grid.Column="1"/>
</Grid>

1
src/Avalonia.Themes.Default/ToggleButton.xaml

@ -17,6 +17,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
RecognizesAccessKey="True"
TextBlock.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>

1
src/Avalonia.Themes.Default/ToggleSwitch.xaml

@ -87,6 +87,7 @@
Grid.Row="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
RecognizesAccessKey="True"
VerticalAlignment="Top"/>
<Grid Grid.Row="1"

1
src/Avalonia.Themes.Fluent/Controls/Button.xaml

@ -34,6 +34,7 @@
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Padding="{TemplateBinding Padding}"
RecognizesAccessKey="True"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
</ControlTemplate>

1
src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml

@ -44,6 +44,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Grid.Column="1" />

23
src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml

@ -1,17 +1,18 @@
<Style xmlns="https://github.com/avaloniaui" Selector="OverlayPopupHost">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAltHighBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}"/>
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}"/>
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="FontWeight" Value="400" />
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</Panel>
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</ControlTemplate>
</Setter>
</Style>

7
src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml

@ -1,8 +1,13 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="PopupRoot">
<Setter Property="Background" Value="{x:Null}"/>
<Setter Property="TransparencyLevelHint" Value="Transparent" />
<Setter Property="Background" Value="{x:Null}" />
<Setter Property="Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}"/>
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}"/>
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="FontWeight" Value="400" />
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="Template">
<ControlTemplate>
<Panel>

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

Loading…
Cancel
Save