diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings index 2c0a6b9dc8..b0692905e7 100644 --- a/Avalonia.sln.DotSettings +++ b/Avalonia.sln.DotSettings @@ -1,5 +1,4 @@  - True ExplicitlyExcluded ExplicitlyExcluded ExplicitlyExcluded @@ -39,4 +38,4 @@ <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> True True - True \ No newline at end of file + True diff --git a/Documentation/build.md b/Documentation/build.md index a7d68eb599..9f5436e68e 100644 --- a/Documentation/build.md +++ b/Documentation/build.md @@ -6,6 +6,7 @@ Avalonia requires at least Visual Studio 2019 and .NET Core SDK 3.1 to build on ``` git clone https://github.com/AvaloniaUI/Avalonia.git +cd Avalonia git submodule update --init ``` diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fbd8507193..bc8d065137 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,21 +1,25 @@ jobs: - job: Linux pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-20.04' steps: - - task: CmdLine@2 - displayName: 'Install Nuke' + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 3.1.414' inputs: - script: | - dotnet tool install --global Nuke.GlobalTool --version 0.24.0 + version: 3.1.414 + + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 5.0.402' + inputs: + version: 5.0.402 + - task: CmdLine@2 - displayName: 'Run Nuke' + displayName: 'Run Build' inputs: script: | - export PATH="$PATH:$HOME/.dotnet/tools" dotnet --info printenv - nuke --target CiAzureLinux --configuration=Release + ./build.sh --target CiAzureLinux --configuration=Release - task: PublishTestResults@2 inputs: @@ -23,6 +27,7 @@ jobs: testResultsFiles: '$(Build.SourcesDirectory)/artifacts/test-results/*.trx' condition: not(canceled()) + - job: macOS variables: SolutionDir: '$(Build.SourcesDirectory)' @@ -30,10 +35,15 @@ jobs: vmImage: 'macOS-10.15' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.401' + displayName: 'Use .NET Core SDK 3.1.414' inputs: - version: 3.1.401 + version: 3.1.414 + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 5.0.402' + inputs: + version: 5.0.402 + - task: CmdLine@2 displayName: 'Install Mono 5.18' inputs: @@ -45,6 +55,7 @@ jobs: displayName: 'Generate avalonia-native' inputs: script: | + export PATH="`pwd`/sdk:$PATH" cd src/tools/MicroComGenerator; dotnet run -i ../../Avalonia.Native/avn.idl --cpp ../../../native/Avalonia.Native/inc/avalonia-native.h - task: Xcode@5 @@ -58,13 +69,7 @@ jobs: args: '-derivedDataPath ./' - task: CmdLine@2 - displayName: 'Install Nuke' - inputs: - script: | - dotnet tool install --global Nuke.GlobalTool --version 0.24.0 - - - task: CmdLine@2 - displayName: 'Run Nuke' + displayName: 'Run Build' inputs: script: | export COREHOST_TRACE=0 @@ -72,10 +77,8 @@ jobs: export DOTNET_CLI_TELEMETRY_OPTOUT=1 which dotnet dotnet --info - export PATH="$PATH:$HOME/.dotnet/tools" - dotnet --info printenv - nuke --target CiAzureOSX --configuration Release --skip-previewer + ./build.sh --target CiAzureOSX --configuration Release --skip-previewer - task: PublishTestResults@2 inputs: @@ -102,9 +105,14 @@ jobs: SolutionDir: '$(Build.SourcesDirectory)' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.401' + displayName: 'Use .NET Core SDK 3.1.414' + inputs: + version: 3.1.414 + + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 5.0.402' inputs: - version: 3.1.401 + version: 5.0.402 - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/build.sh b/build.sh index bd162fab9b..9532b4fbe0 100755 --- a/build.sh +++ b/build.sh @@ -20,55 +20,11 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) ########################################################################### BUILD_PROJECT_FILE="$SCRIPT_DIR/nukebuild/_build.csproj" -TEMP_DIRECTORY="$SCRIPT_DIR//.tmp" - -DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" -DOTNET_INSTALL_URL="https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.sh" -DOTNET_CHANNEL="Current" export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 export NUGET_XMLDOC_MODE="skip" -########################################################################### -# EXECUTION -########################################################################### - -function FirstJsonValue { - perl -nle 'print $1 if m{"'$1'": "([^"\-]+)",?}' <<< ${@:2} -} - -# If global.json exists, load expected version -if [ -f "$DOTNET_GLOBAL_FILE" ]; then - DOTNET_VERSION=$(FirstJsonValue "version" $(cat "$DOTNET_GLOBAL_FILE")) - if [ "$DOTNET_VERSION" == "" ]; then - unset DOTNET_VERSION - fi -fi - -# If dotnet is installed locally, and expected version is not set or installation matches the expected version -if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") || "$SKIP_DOTNET_DOWNLOAD" == "1" ]]; then - export DOTNET_EXE="$(command -v dotnet)" -else - DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" - export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" - - # Download install script - DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" - mkdir -p "$TEMP_DIRECTORY" - curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" - chmod +x "$DOTNET_INSTALL_FILE" - - # Install by channel or version - if [ -z ${DOTNET_VERSION+x} ]; then - "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path - else - "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path - fi -fi - -export PATH=$DOTNET_DIRECTORY:$PATH - -echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" +dotnet --info -"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} +dotnet run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 75bada4bfc..7d75901288 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -17,7 +17,6 @@ git $(MSBuildThisFileDirectory)\avalonia.snk true - $(DefineConstants);SIGNED_BUILD diff --git a/global.json b/global.json index b2b2da7c4f..9f83c1ea1e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.401" + "version": "5.0.402" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index dba3ee6d31..85fcf20034 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/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 = ""; }; 520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = ""; }; 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 = ""; }; + 523484CB26EA68AA00EA0C2C /* trayicon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = trayicon.h; sourceTree = ""; }; 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; @@ -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 */, diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index c082003ccf..8896fbe88b 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/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); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 3e152a6125..eeaaecfdbd 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/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; diff --git a/native/Avalonia.Native/src/OSX/trayicon.h b/native/Avalonia.Native/src/OSX/trayicon.h new file mode 100644 index 0000000000..f94f9a871b --- /dev/null +++ b/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 +{ +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 */ diff --git a/native/Avalonia.Native/src/OSX/trayicon.mm b/native/Avalonia.Native/src/OSX/trayicon.mm new file mode 100644 index 0000000000..151990cfb1 --- /dev/null +++ b/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(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; +} diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 3a54bd4b79..1dc091a48d 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/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 diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 14fe60ab0b..094c1f7c93 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/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 @@ -641,6 +645,7 @@ private: [Window setCanBecomeKeyAndMain]; [Window disableCursorRects]; [Window setTabbingMode:NSWindowTabbingModeDisallowed]; + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; } void HideOrShowTrafficLights () @@ -1091,14 +1096,7 @@ private: { _fullScreenActive = true; - [Window setHasShadow:YES]; - [Window setTitleVisibility:NSWindowTitleVisible]; - [Window setTitlebarAppearsTransparent:NO]; [Window setTitle:_lastTitle]; - - Window.styleMask = Window.styleMask | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable; - Window.styleMask = Window.styleMask & ~NSWindowStyleMaskFullSizeContentView; - [Window toggleFullScreen:nullptr]; } @@ -1547,7 +1545,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return pt; } -- (AvnPoint)toAvnPoint:(CGPoint)p ++ (AvnPoint)toAvnPoint:(CGPoint)p { AvnPoint result; @@ -1604,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; @@ -1672,6 +1670,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent switch(event.buttonNumber) { + case 2: case 3: _isMiddlePressed = true; [self mouseEvent:event withType:MiddleButtonDown]; @@ -1704,6 +1703,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { switch(event.buttonNumber) { + case 2: case 3: _isMiddlePressed = false; [self mouseEvent:event withType:MiddleButtonUp]; @@ -1947,7 +1947,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )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]; @@ -2380,6 +2380,49 @@ 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 +{ + [super sendEvent:event]; + + /// This is to detect non-client clicks. This can only be done on Windows... not popups, hence the dynamic_cast. + if(_parent != nullptr && dynamic_cast(_parent.getRaw()) != 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; + } + } +} @end class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 45a7f1aa44..3f9ccb04eb 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -42,12 +42,25 @@ - $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences + $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences;_GenerateAvaloniaResourcesDependencyCache + + + + + + + + + + + + + diff --git a/readme.md b/readme.md index 7959bc5540..ef8c116728 100644 --- a/readme.md +++ b/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. + + 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)]. - - ### Backers Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)] diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 6aad44c0d5..6e57686e00 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -1,5 +1,8 @@ - + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index f3ec7b48aa..36b6fc2dcd 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/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(); diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 6537c470d5..f61b59e6cd 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -98,6 +98,7 @@ Transparent Blur AcrylicBlur + Mica diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index a107ee2163..375345f64e 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/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}"> @@ -62,11 +63,11 @@ - - + + - - + + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 2446c0e1c9..a9900471c5 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -35,6 +35,8 @@ namespace ControlCatalog var mainMenu = this.FindControl("MainMenu"); mainMenu.AttachedToVisualTree += MenuAttached; + + ExtendClientAreaChromeHints = Avalonia.Platform.ExtendClientAreaChromeHints.OSXThickTitleBar; } public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit"; diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml index be114bbbc9..b35c112a68 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml @@ -10,7 +10,7 @@ HorizontalAlignment="Center" Spacing="16"> - + diff --git a/samples/ControlCatalog/Pages/CheckBoxPage.xaml b/samples/ControlCatalog/Pages/CheckBoxPage.xaml index 1359cfa2ef..769ef26699 100644 --- a/samples/ControlCatalog/Pages/CheckBoxPage.xaml +++ b/samples/ControlCatalog/Pages/CheckBoxPage.xaml @@ -11,9 +11,9 @@ Spacing="16"> - Unchecked - Checked - Indeterminate + _Unchecked + _Checked + _Indeterminate Disabled Use filters - - - - - - - - - - + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 392ccb57c3..4d0bd663df 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -1,17 +1,33 @@ + + + + + + - - diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index f515db84d4..b36629fb2a 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -2,9 +2,20 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.ListBoxPage"> + + + + ListBox Hosts a collection of ListBoxItem. + Each 5th item is highlighted with nth-child(5n+3) and nth-last-child(5n+4) rules. Multiple diff --git a/samples/ControlCatalog/Pages/RadioButtonPage.xaml b/samples/ControlCatalog/Pages/RadioButtonPage.xaml index bf31c40e2a..408f9c2411 100644 --- a/samples/ControlCatalog/Pages/RadioButtonPage.xaml +++ b/samples/ControlCatalog/Pages/RadioButtonPage.xaml @@ -11,9 +11,9 @@ Spacing="16"> - Option 1 - Option 2 - Option 3 + _Option 1 + O_ption 2 + Op_tion 3 Disabled - + + + + Custom context flyout + + + + diff --git a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml index 161ee2ee16..7a65275a8e 100644 --- a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml +++ b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml @@ -14,7 +14,7 @@ - + @@ -24,7 +24,7 @@ - @@ -40,7 +40,7 @@ ContentOff="Off" />" - + diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml index b90f43c3b6..caab42e98c 100644 --- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -14,6 +14,7 @@ Transparent Blur AcrylicBlur + Mica diff --git a/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs new file mode 100644 index 0000000000..7eea7b0657 --- /dev/null +++ b/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; } + } +} diff --git a/src/Avalonia.Animation/Properties/AssemblyInfo.cs b/src/Avalonia.Animation/Properties/AssemblyInfo.cs index 221b51e95a..6b539b075b 100644 --- a/src/Avalonia.Animation/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Animation/Properties/AssemblyInfo.cs @@ -1,15 +1,9 @@ using Avalonia.Metadata; -using System.Reflection; using System.Runtime.CompilerServices; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Easings")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")] -#if SIGNED_BUILD [assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -#else -[assembly: InternalsVisibleTo("Avalonia.LeakTests")] -[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests")] -#endif diff --git a/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs b/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs index 9ec600d2bc..2385d4981c 100644 --- a/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs @@ -26,7 +26,7 @@ namespace Avalonia.Data.Converters /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value is TIn || (value == null && TypeUtilities.AcceptsNull(typeof(TIn)))) + if (TypeUtilities.CanCast(value)) { return _convert((TIn)value); } diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index 2ad220dddd..c049f9e763 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -39,5 +39,10 @@ namespace Avalonia.Logging /// The log event comes from Win32Platform. /// public const string Win32Platform = nameof(Win32Platform); + + /// + /// The log event comes from X11Platform. + /// + public const string X11Platform = nameof(X11Platform); } } diff --git a/src/Avalonia.Base/Metadata/TemplateContent.cs b/src/Avalonia.Base/Metadata/TemplateContent.cs index fcd7d69e7b..7f9e878419 100644 --- a/src/Avalonia.Base/Metadata/TemplateContent.cs +++ b/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; } } } diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index b054c186ae..053c7a7547 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -5,18 +5,10 @@ using System.Runtime.CompilerServices; using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")] -#if SIGNED_BUILD [assembly: InternalsVisibleTo("Avalonia.Base.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] [assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Visuals, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -#else -[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")] -[assembly: InternalsVisibleTo("Avalonia.UnitTests")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] -[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")] -[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests")] -[assembly: InternalsVisibleTo("Avalonia.Visuals")] -#endif + diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index 1a78792173..326d1a3f53 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -39,7 +39,7 @@ namespace Avalonia.Threading if (Dispatcher.UIThread.CheckAccess()) d(state); else - Dispatcher.UIThread.InvokeAsync(() => d(state), DispatcherPriority.Send).Wait(); + Dispatcher.UIThread.InvokeAsync(() => d(state), DispatcherPriority.Send).GetAwaiter().GetResult(); } diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 179ded3549..0978308ef6 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; namespace Avalonia.Utilities { @@ -93,6 +94,17 @@ namespace Avalonia.Utilities return !type.IsValueType || IsNullableType(type); } + /// + /// Returns a value indicating whether null can be assigned to the specified type. + /// + /// The type + /// True if the type accepts null values; otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AcceptsNull() + { + return default(T) is null; + } + /// /// Returns a value indicating whether value can be casted to the specified type. /// If value is null, checks if instances of that type can be null. @@ -102,7 +114,7 @@ namespace Avalonia.Utilities /// True if the cast is possible, otherwise false. public static bool CanCast(object value) { - return value is T || (value is null && AcceptsNull(typeof(T))); + return value is T || (value is null && AcceptsNull()); } /// diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index ab1aff9220..10c7c16488 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/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) @@ -3039,6 +3045,12 @@ namespace Avalonia.Controls } } + //TODO: Ensure right button is checked for + internal bool UpdateStateOnMouseRightButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit) + { + KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.KeyModifiers, out bool ctrl, out bool shift); + return UpdateStateOnMouseRightButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl); + } //TODO: Ensure left button is checked for internal bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit) { @@ -4489,17 +4501,27 @@ namespace Avalonia.Controls element = dataGridColumn.GenerateEditingElementInternal(dataGridCell, dataGridRow.DataContext); if (element != null) { - // Subscribe to the new element's events - element.Initialized += EditingElement_Initialized; + + dataGridCell.Content = element; + if (element.IsInitialized) + { + PreparingCellForEditPrivate(element as Control); + } + else + { + // Subscribe to the new element's events + element.Initialized += EditingElement_Initialized; + } } } else { // Generate Element and apply column style if available element = dataGridColumn.GenerateElementInternal(dataGridCell, dataGridRow.DataContext); + dataGridCell.Content = element; } - dataGridCell.Content = element; + } private void PreparingCellForEditPrivate(Control editingElement) @@ -5711,6 +5733,35 @@ namespace Avalonia.Controls VerticalScroll?.Invoke(sender, e); } + //TODO: Ensure right button is checked for + private bool UpdateStateOnMouseRightButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit, bool shift, bool ctrl) + { + Debug.Assert(slot >= 0); + + if (shift || ctrl) + { + return true; + } + if (IsSlotOutOfBounds(slot)) + { + return true; + } + if (GetRowSelection(slot)) + { + return true; + } + // Unselect everything except the row that was clicked on + try + { + UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false); + } + finally + { + NoSelectionChangeCount--; + } + return true; + } + //TODO: Ensure left button is checked for private bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit, bool shift, bool ctrl) { diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 90401a00a2..97e247bdc6 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/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() { diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index 7dda936317..e3f150f5c4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -161,29 +161,42 @@ namespace Avalonia.Controls private void DataGridCell_PointerPressed(PointerPressedEventArgs e) { // OwningGrid is null for TopLeftHeaderCell and TopRightHeaderCell because they have no OwningRow - if (OwningGrid != null) + if (OwningGrid == null) { - OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e)); - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + } + OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e)); + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + { + if (!e.Handled) + //if (!e.Handled && OwningGrid.IsTabStop) + { + OwningGrid.Focus(); + } + if (OwningRow != null) { - if (!e.Handled) - //if (!e.Handled && OwningGrid.IsTabStop) + var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); + + // Do not handle PointerPressed with touch, + // so we can start scroll gesture on the same event. + if (e.Pointer.Type != PointerType.Touch) { - OwningGrid.Focus(); + e.Handled = handled; } - if (OwningRow != null) - { - var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); - - // Do not handle PointerPressed with touch, - // so we can start scroll gesture on the same event. - if (e.Pointer.Type != PointerType.Touch) - { - e.Handled = handled; - } - OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; - } + OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; + } + } + else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed) + { + if (!e.Handled) + //if (!e.Handled && OwningGrid.IsTabStop) + { + OwningGrid.Focus(); + } + if (OwningRow != null) + { + e.Handled = OwningGrid.UpdateStateOnMouseRightButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); } } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 4ab2869138..5499257171 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/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; } + /// + /// Clears the current sort direction + /// + public void ClearSort() + { + //InvokeProcessSort is already validating if sorting is possible + _headerCell?.InvokeProcessSort(Input.KeyModifiers.Control); + } + + /// + /// Switches the current state of sort direction + /// + public void Sort() + { + //InvokeProcessSort is already validating if sorting is possible + _headerCell?.InvokeProcessSort(Input.KeyModifiers.None); + } + + /// + /// Changes the sort direction of this column + /// + /// New sort direction + public void Sort(ListSortDirection direction) + { + //InvokeProcessSort is already validating if sorting is possible + _headerCell?.InvokeProcessSort(Input.KeyModifiers.None, direction); + } + /// /// When overridden in a derived class, causes the column cell being edited to revert to the unedited value. /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 6f957497cb..915b36687c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/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); + } } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs index a94acdec57..fade597ca1 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs @@ -233,7 +233,7 @@ namespace Avalonia.Controls else { editableCollectionView.EditItem(dataItem); - return editableCollectionView.IsEditingItem; + return editableCollectionView.IsEditingItem || editableCollectionView.IsAddingNew; } } @@ -314,7 +314,14 @@ namespace Avalonia.Controls CommittingEdit = true; try { - editableCollectionView.CommitEdit(); + if (editableCollectionView.IsAddingNew) + { + editableCollectionView.CommitNew(); + } + else + { + editableCollectionView.CommitEdit(); + } } finally { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index 7546970498..1efce7c0b8 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -378,13 +378,13 @@ namespace Avalonia.Controls } } } - } + } internal Panel RootElement { get; private set; - } + } internal int Slot { @@ -638,7 +638,7 @@ namespace Avalonia.Controls PseudoClasses.Set(":editing", IsEditing); PseudoClasses.Set(":invalid", !IsValid); ApplyHeaderStatus(); - } + } } //TODO Animation @@ -896,7 +896,7 @@ namespace Avalonia.Controls _detailsElement.ContentHeight = _detailsDesiredHeight; } } - } + } // Makes sure the _detailsDesiredHeight is initialized. We need to measure it to know what // height we want to animate to. Subsequently, we just update that height in response to SizeChanged @@ -919,7 +919,7 @@ namespace Avalonia.Controls //TODO Cleanup double? _previousDetailsHeight = null; - + //TODO Animation private void DetailsContent_HeightChanged(double newValue) { @@ -1022,7 +1022,7 @@ namespace Avalonia.Controls } } } - + internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight) { if (_detailsElement != null && AreDetailsVisible) @@ -1066,7 +1066,7 @@ namespace Avalonia.Controls .Subscribe(DetailsContent_MarginChanged); } - + _detailsElement.Children.Add(_detailsContent); } } @@ -1090,6 +1090,28 @@ namespace Avalonia.Controls } } } + + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == DataContextProperty) + { + var owner = OwningGrid; + if (owner != null && this.IsRecycled) + { + var columns = owner.ColumnsItemsInternal; + var nc = columns.Count; + for (int ci = 0; ci < nc; ci++) + { + if (columns[ci] is DataGridTemplateColumn column) + { + column.RefreshCellContent((Control)this.Cells[column.Index].Content, nameof(DataGridTemplateColumn.CellTemplate)); + } + } + } + } + base.OnPropertyChanged(change); + } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 1e03b134b1..49ca23d34c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -283,7 +283,11 @@ namespace Avalonia.Controls //TODO TabStop private void DataGridRowGroupHeader_PointerPressed(PointerPressedEventArgs e) { - if (OwningGrid != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + if (OwningGrid == null) + { + return; + } + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { if (OwningGrid.IsDoubleClickRecordsClickOnCall(this) && !e.Handled) { @@ -300,6 +304,15 @@ namespace Avalonia.Controls e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, OwningGrid.CurrentColumnIndex, RowGroupInfo.Slot, allowEdit: false); } } + else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed) + { + if (!e.Handled) + { + OwningGrid.Focus(); + } + e.Handled = OwningGrid.UpdateStateOnMouseRightButtonDown(e, OwningGrid.CurrentColumnIndex, RowGroupInfo.Slot, allowEdit: false); + } + } private void EnsureChildClip(Visual child, double frozenLeftEdge) diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index 0cd3589a57..510072174f 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -179,12 +179,12 @@ namespace Avalonia.Controls.Primitives //TODO TabStop private void DataGridRowHeader_PointerPressed(object sender, PointerPressedEventArgs e) { - if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + if (OwningGrid == null) { return; } - if (OwningGrid != null) + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { if (!e.Handled) //if (!e.Handled && OwningGrid.IsTabStop) @@ -199,6 +199,19 @@ namespace Avalonia.Controls.Primitives OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; } } + else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed) + { + if (!e.Handled) + { + OwningGrid.Focus(); + } + if (OwningRow != null) + { + Debug.Assert(sender is DataGridRowHeader); + Debug.Assert(sender == this); + e.Handled = OwningGrid.UpdateStateOnMouseRightButtonDown(e, -1, Slot, false); + } + } } } diff --git a/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs b/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs index 1b122996d2..d61c05ab6e 100644 --- a/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs @@ -1,13 +1,9 @@ -using System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Metadata; -#if SIGNED_BUILD + [assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -#else -[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests")] -[assembly: InternalsVisibleTo("Avalonia.DesignerSupport")] -#endif + [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Collections")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")] diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index fac5923db5..dd41c30e85 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -37,6 +37,7 @@ MembersMustExist : Member 'public System.Action Avalonia.Controls MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' 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.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable 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 diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 5a6e78f441..0e946126ea 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -2094,7 +2094,21 @@ namespace Avalonia.Controls bool inResults = !(stringFiltering || objectFiltering); if (!inResults) { - inResults = stringFiltering ? TextFilter(text, FormatValue(item)) : ItemFilter(text, item); + if (stringFiltering) + { + inResults = TextFilter(text, FormatValue(item)); + } + else + { + if (ItemFilter is null) + { + throw new Exception("ItemFilter property can not be null when FilterMode has value AutoCompleteFilterMode.Custom"); + } + else + { + inResults = ItemFilter(text, item); + } + } } if (view_count > view_index && inResults && _view[view_index] == item) diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index c8337df99c..0c6949465b 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -8,7 +8,9 @@ namespace Avalonia.Controls /// /// A control which decorates a child with a border and background. /// +#pragma warning disable CS0618 // Type or member is obsolete public partial class Border : Decorator, IVisualWithRoundRectClip +#pragma warning restore CS0618 // Type or member is obsolete { /// /// Defines the property. diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 6912b2db63..8b22cdd4ec 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/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 [PseudoClasses(":empty", ":singleitem")] - public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener + public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener, IChildIndexProvider { /// /// The default value for the property. @@ -56,6 +56,7 @@ namespace Avalonia.Controls private IEnumerable _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; + private EventHandler _childIndexChanged; /// /// Initializes static members of the class. @@ -145,11 +146,28 @@ namespace Avalonia.Controls protected set; } + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// 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; + } } } diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index b2663f3213..c2d20495ef 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -32,8 +32,8 @@ namespace Avalonia.Controls /// public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); - private protected readonly IList _inner; - private INotifyCollectionChanged? _notifyCollectionChanged; + private IList? _inner; + private NotifyCollectionChangedEventHandler? _collectionChanged; /// /// Initializes a new instance of the ItemsSourceView class for the specified data source. @@ -42,27 +42,22 @@ namespace Avalonia.Controls public ItemsSourceView(IEnumerable source) { source = source ?? throw new ArgumentNullException(nameof(source)); - - if (source is IList list) - { - _inner = list; - } - else if (source is IEnumerable objectEnumerable) + _inner = source switch { - _inner = new List(objectEnumerable); - } - else - { - _inner = new List(source.Cast()); - } - - ListenToCollectionChanges(); + ItemsSourceView _ => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)), + IList list => list, + INotifyCollectionChanged _ => throw new ArgumentException( + "Collection implements INotifyCollectionChanged by not IList.", + nameof(source)), + IEnumerable iObj => new List(iObj), + _ => new List(source.Cast()) + }; } /// /// Gets the number of items in the collection. /// - public int Count => _inner.Count; + public int Count => Inner.Count; /// /// Gets a value that indicates whether the items source can provide a unique key for each item. @@ -72,6 +67,19 @@ namespace Avalonia.Controls /// public bool HasKeyIndexMapping => false; + /// + /// Gets the inner collection. + /// + public IList Inner + { + get + { + if (_inner is null) + ThrowDisposed(); + return _inner!; + } + } + /// /// Retrieves the item at the specified index. /// @@ -82,15 +90,38 @@ namespace Avalonia.Controls /// /// Occurs when the collection has changed to indicate the reason for the change and which items changed. /// - public event NotifyCollectionChangedEventHandler? CollectionChanged; + public event NotifyCollectionChangedEventHandler? CollectionChanged + { + add + { + if (_collectionChanged is null && Inner is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnCollectionChanged; + } + + _collectionChanged += value; + } + + remove + { + _collectionChanged -= value; + + if (_collectionChanged is null && Inner is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= OnCollectionChanged; + } + } + } /// public void Dispose() { - if (_notifyCollectionChanged != null) + if (_inner is INotifyCollectionChanged incc) { - _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; + incc.CollectionChanged -= OnCollectionChanged; } + + _inner = null; } /// @@ -98,9 +129,9 @@ namespace Avalonia.Controls /// /// The index. /// The item. - public object? GetAt(int index) => _inner[index]; + public object? GetAt(int index) => Inner[index]; - public int IndexOf(object? item) => _inner.IndexOf(item); + public int IndexOf(object? item) => Inner.IndexOf(item); public static ItemsSourceView GetOrCreate(IEnumerable? items) { @@ -146,7 +177,7 @@ namespace Avalonia.Controls internal void AddListener(ICollectionChangedListener listener) { - if (_inner is INotifyCollectionChanged incc) + if (Inner is INotifyCollectionChanged incc) { CollectionChangedEventManager.Instance.AddListener(incc, listener); } @@ -154,7 +185,7 @@ namespace Avalonia.Controls internal void RemoveListener(ICollectionChangedListener listener) { - if (_inner is INotifyCollectionChanged incc) + if (Inner is INotifyCollectionChanged incc) { CollectionChangedEventManager.Instance.RemoveListener(incc, listener); } @@ -162,22 +193,15 @@ namespace Avalonia.Controls protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args) { - CollectionChanged?.Invoke(this, args); - } - - private void ListenToCollectionChanges() - { - if (_inner is INotifyCollectionChanged incc) - { - incc.CollectionChanged += OnCollectionChanged; - _notifyCollectionChanged = incc; - } + _collectionChanged?.Invoke(this, args); } private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { OnItemsSourceChanged(e); } + + private void ThrowDisposed() => throw new ObjectDisposedException(nameof(ItemsSourceView)); } public class ItemsSourceView : ItemsSourceView, IReadOnlyList @@ -216,10 +240,10 @@ namespace Avalonia.Controls /// The index. /// The item. [return: MaybeNull] - public new T GetAt(int index) => (T)_inner[index]; + public new T GetAt(int index) => (T)Inner[index]; - public IEnumerator GetEnumerator() => _inner.Cast().GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); + public IEnumerator GetEnumerator() => Inner.Cast().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator(); public static new ItemsSourceView GetOrCreate(IEnumerable? items) { diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs new file mode 100644 index 0000000000..a72c617f05 --- /dev/null +++ b/src/Avalonia.Controls/MaskedTextBox.cs @@ -0,0 +1,433 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Interactivity; +using Avalonia.Styling; + +#nullable enable + +namespace Avalonia.Controls +{ + public class MaskedTextBox : TextBox, IStyleable + { + public static readonly StyledProperty AsciiOnlyProperty = + AvaloniaProperty.Register(nameof(AsciiOnly)); + + public static readonly DirectProperty CultureProperty = + AvaloniaProperty.RegisterDirect(nameof(Culture), o => o.Culture, + (o, v) => o.Culture = v, CultureInfo.CurrentCulture); + + public static readonly StyledProperty HidePromptOnLeaveProperty = + AvaloniaProperty.Register(nameof(HidePromptOnLeave)); + + public static readonly DirectProperty MaskCompletedProperty = + AvaloniaProperty.RegisterDirect(nameof(MaskCompleted), o => o.MaskCompleted); + + public static readonly DirectProperty MaskFullProperty = + AvaloniaProperty.RegisterDirect(nameof(MaskFull), o => o.MaskFull); + + public static readonly StyledProperty MaskProperty = + AvaloniaProperty.Register(nameof(Mask), string.Empty); + + public static new readonly StyledProperty PasswordCharProperty = + AvaloniaProperty.Register(nameof(PasswordChar), '\0'); + + public static readonly StyledProperty PromptCharProperty = + AvaloniaProperty.Register(nameof(PromptChar), '_'); + + public static readonly DirectProperty ResetOnPromptProperty = + AvaloniaProperty.RegisterDirect(nameof(ResetOnPrompt), o => o.ResetOnPrompt, (o, v) => o.ResetOnPrompt = v); + + public static readonly DirectProperty ResetOnSpaceProperty = + AvaloniaProperty.RegisterDirect(nameof(ResetOnSpace), o => o.ResetOnSpace, (o, v) => o.ResetOnSpace = v); + + private CultureInfo? _culture; + + private bool _resetOnPrompt = true; + + private bool _ignoreTextChanges; + + private bool _resetOnSpace = true; + + public MaskedTextBox() { } + + /// + /// Constructs the MaskedTextBox with the specified MaskedTextProvider object. + /// + public MaskedTextBox(MaskedTextProvider maskedTextProvider) + { + if (maskedTextProvider == null) + { + throw new ArgumentNullException(nameof(maskedTextProvider)); + } + AsciiOnly = maskedTextProvider.AsciiOnly; + Culture = maskedTextProvider.Culture; + Mask = maskedTextProvider.Mask; + PasswordChar = maskedTextProvider.PasswordChar; + PromptChar = maskedTextProvider.PromptChar; + } + + /// + /// Gets or sets a value indicating if the masked text box is restricted to accept only ASCII characters. + /// Default value is false. + /// + public bool AsciiOnly + { + get => GetValue(AsciiOnlyProperty); + set => SetValue(AsciiOnlyProperty, value); + } + + /// + /// Gets or sets the culture information associated with the masked text box. + /// + public CultureInfo? Culture + { + get => _culture; + set => SetAndRaise(CultureProperty, ref _culture, value); + } + + /// + /// Gets or sets a value indicating if the prompt character is hidden when the masked text box loses focus. + /// + public bool HidePromptOnLeave + { + get => GetValue(HidePromptOnLeaveProperty); + set => SetValue(HidePromptOnLeaveProperty, value); + } + + /// + /// Gets or sets the mask to apply to the TextBox. + /// + public string? Mask + { + get => GetValue(MaskProperty); + set => SetValue(MaskProperty, value); + } + + /// + /// Specifies whether the test string required input positions, as specified by the mask, have + /// all been assigned. + /// + public bool? MaskCompleted + { + get => MaskProvider?.MaskCompleted; + } + + /// + /// Specifies whether all inputs (required and optional) have been provided into the mask successfully. + /// + public bool? MaskFull + { + get => MaskProvider?.MaskFull; + } + + /// + /// Gets the MaskTextProvider for the specified Mask. + /// + public MaskedTextProvider? MaskProvider { get; private set; } + + /// + /// Gets or sets the character to be displayed in substitute for user input. + /// + public new char PasswordChar + { + get => GetValue(PasswordCharProperty); + set => SetValue(PasswordCharProperty, value); + } + + /// + /// Gets or sets the character used to represent the absence of user input in MaskedTextBox. + /// + public char PromptChar + { + get => GetValue(PromptCharProperty); + set => SetValue(PromptCharProperty, value); + } + + /// + /// Gets or sets a value indicating if selected characters should be reset when the prompt character is pressed. + /// + public bool ResetOnPrompt + { + get => _resetOnPrompt; + set + { + SetAndRaise(ResetOnPromptProperty, ref _resetOnPrompt, value); + if (MaskProvider != null) + { + MaskProvider.ResetOnPrompt = value; + } + + } + } + + /// + /// Gets or sets a value indicating if selected characters should be reset when the space character is pressed. + /// + public bool ResetOnSpace + { + get => _resetOnSpace; + set + { + SetAndRaise(ResetOnSpaceProperty, ref _resetOnSpace, value); + if (MaskProvider != null) + { + MaskProvider.ResetOnSpace = value; + } + + } + + + } + + Type IStyleable.StyleKey => typeof(TextBox); + + protected override void OnGotFocus(GotFocusEventArgs e) + { + if (HidePromptOnLeave == true && MaskProvider != null) + { + Text = MaskProvider.ToDisplayString(); + } + base.OnGotFocus(e); + } + + protected override async void OnKeyDown(KeyEventArgs e) + { + if (MaskProvider == null) + { + base.OnKeyDown(e); + return; + } + + var keymap = AvaloniaLocator.Current.GetService(); + + bool Match(List gestures) => gestures.Any(g => g.Matches(e)); + + if (Match(keymap.Paste)) + { + var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync(); + + if (text == null) + return; + + foreach (var item in text) + { + var index = GetNextCharacterPosition(CaretIndex); + if (MaskProvider.InsertAt(item, index)) + { + CaretIndex = ++index; + } + } + + Text = MaskProvider.ToDisplayString(); + e.Handled = true; + return; + } + + if (e.Key != Key.Back) + { + base.OnKeyDown(e); + } + + switch (e.Key) + { + case Key.Delete: + if (CaretIndex < Text.Length) + { + if (MaskProvider.RemoveAt(CaretIndex)) + { + RefreshText(MaskProvider, CaretIndex); + } + + e.Handled = true; + } + break; + case Key.Space: + if (!MaskProvider.ResetOnSpace || string.IsNullOrEmpty(SelectedText)) + { + if (MaskProvider.InsertAt(" ", CaretIndex)) + { + RefreshText(MaskProvider, CaretIndex); + } + } + + e.Handled = true; + break; + case Key.Back: + if (CaretIndex > 0) + { + MaskProvider.RemoveAt(CaretIndex - 1); + } + RefreshText(MaskProvider, CaretIndex - 1); + e.Handled = true; + break; + } + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + if (HidePromptOnLeave == true && MaskProvider != null) + { + Text = MaskProvider.ToString(!HidePromptOnLeave, true); + } + base.OnLostFocus(e); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + void UpdateMaskProvider() + { + MaskProvider = new MaskedTextProvider(Mask, Culture, true, PromptChar, PasswordChar, AsciiOnly) { ResetOnSpace = ResetOnSpace, ResetOnPrompt = ResetOnPrompt }; + if (Text != null) + { + MaskProvider.Set(Text); + } + RefreshText(MaskProvider, 0); + } + if (change.Property == TextProperty && MaskProvider != null && _ignoreTextChanges == false) + { + if (string.IsNullOrEmpty(Text)) + { + MaskProvider.Clear(); + RefreshText(MaskProvider, CaretIndex); + base.OnPropertyChanged(change); + return; + } + + MaskProvider.Set(Text); + RefreshText(MaskProvider, CaretIndex); + } + else if (change.Property == MaskProperty) + { + UpdateMaskProvider(); + + if (!string.IsNullOrEmpty(Mask)) + { + foreach (var c in Mask!) + { + if (!MaskedTextProvider.IsValidMaskChar(c)) + { + throw new ArgumentException("Specified mask contains characters that are not valid."); + } + } + } + } + else if (change.Property == PasswordCharProperty) + { + if (!MaskedTextProvider.IsValidPasswordChar(PasswordChar)) + { + throw new ArgumentException("Specified character value is not allowed for this property.", nameof(PasswordChar)); + } + if (MaskProvider != null && PasswordChar == MaskProvider.PromptChar) + { + // Prompt and password chars must be different. + throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same."); + } + if (MaskProvider != null && MaskProvider.PasswordChar != PasswordChar) + { + UpdateMaskProvider(); + } + } + else if (change.Property == PromptCharProperty) + { + if (!MaskedTextProvider.IsValidInputChar(PromptChar)) + { + throw new ArgumentException("Specified character value is not allowed for this property."); + } + if (PromptChar == PasswordChar) + { + throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same."); + } + if (MaskProvider != null && MaskProvider.PromptChar != PromptChar) + { + UpdateMaskProvider(); + } + } + else if (change.Property == AsciiOnlyProperty && MaskProvider != null && MaskProvider.AsciiOnly != AsciiOnly + || change.Property == CultureProperty && MaskProvider != null && !MaskProvider.Culture.Equals(Culture)) + { + UpdateMaskProvider(); + } + base.OnPropertyChanged(change); + } + protected override void OnTextInput(TextInputEventArgs e) + { + _ignoreTextChanges = true; + try + { + if (IsReadOnly) + { + e.Handled = true; + base.OnTextInput(e); + return; + } + if (MaskProvider == null) + { + base.OnTextInput(e); + return; + } + if ((MaskProvider.ResetOnSpace && e.Text == " " || MaskProvider.ResetOnPrompt && e.Text == MaskProvider.PromptChar.ToString()) && !string.IsNullOrEmpty(SelectedText)) + { + if (SelectionStart > SelectionEnd ? MaskProvider.RemoveAt(SelectionEnd, SelectionStart - 1) : MaskProvider.RemoveAt(SelectionStart, SelectionEnd - 1)) + { + SelectedText = string.Empty; + } + } + + if (CaretIndex < Text.Length) + { + CaretIndex = GetNextCharacterPosition(CaretIndex); + + if (MaskProvider.InsertAt(e.Text, CaretIndex)) + { + CaretIndex++; + } + var nextPos = GetNextCharacterPosition(CaretIndex); + if (nextPos != 0 && CaretIndex != Text.Length) + { + CaretIndex = nextPos; + } + } + + RefreshText(MaskProvider, CaretIndex); + + + e.Handled = true; + + base.OnTextInput(e); + } + finally + { + _ignoreTextChanges = false; + } + + } + + private int GetNextCharacterPosition(int startPosition) + { + if (MaskProvider != null) + { + var position = MaskProvider.FindEditPositionFrom(startPosition, true); + if (CaretIndex != -1) + { + return position; + } + } + return startPosition; + } + + private void RefreshText(MaskedTextProvider provider, int position) + { + if (provider != null) + { + Text = provider.ToDisplayString(); + CaretIndex = position; + } + } + + } +} diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index 0349df842b..6bfe5ebc82 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -52,15 +52,10 @@ namespace Avalonia.Controls } public static readonly AttachedProperty MenuProperty - = AvaloniaProperty.RegisterAttached("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("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()); + } }); } } diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 2eaf24d2f2..2ceaeb6dba 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/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() { diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 3a83d9ed7c..eba381e5fa 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -332,7 +332,9 @@ namespace Avalonia.Controls /// 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); diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index b7eeb065da..b182f9d261 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/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 by adding them to its /// collection. All children are layed out to fill the panel. /// - public class Panel : Control, IPanel + public class Panel : Control, IPanel, IChildIndexProvider { /// /// Defines the property. @@ -30,6 +32,8 @@ namespace Avalonia.Controls AffectsRender(BackgroundProperty); } + private EventHandler _childIndexChanged; + /// /// Initializes a new instance of the class. /// @@ -53,6 +57,12 @@ namespace Avalonia.Controls set { SetValue(BackgroundProperty, value); } } + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// /// Renders the visual to a . /// @@ -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; + } } } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 209feb351c..e361e7b736 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -275,7 +275,7 @@ namespace Avalonia.Controls.Platform return; } - if (item.HasSubMenu) + if (item.HasSubMenu && item.IsEffectivelyEnabled) { Open(item, true); } @@ -303,7 +303,8 @@ namespace Avalonia.Controls.Platform { item.Parent.SelectedItem.Close(); SelectItemAndAncestors(item); - Open(item, false); + if (item.HasSubMenu) + Open(item, false); } else { diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs index 3ac5f28956..9b779054f3 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs +++ b/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 diff --git a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs new file mode 100644 index 0000000000..9768d149f0 --- /dev/null +++ b/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 + { + /// + /// Sets the icon of this tray icon. + /// + void SetIcon(IWindowIconImpl? icon); + + /// + /// Sets the icon of this tray icon. + /// + void SetToolTipText(string? text); + + /// + /// Sets if the tray icon is visible or not. + /// + void SetIsVisible(bool visible); + + /// + /// Gets the MenuExporter to allow native menus to be exported to the TrayIcon. + /// + INativeMenuExporter? MenuExporter { get; } + + /// + /// Gets or Sets the Action that is called when the TrayIcon is clicked. + /// + Action? OnClicked { get; set; } + } +} diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index be8939e19a..21882b1271 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -1,8 +1,13 @@ +#nullable enable + namespace Avalonia.Platform { public interface IWindowingPlatform { IWindowImpl CreateWindow(); + IWindowImpl CreateEmbeddableWindow(); + + ITrayIconImpl? CreateTrayIcon(); } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index 19d034b4e2..e39f0b1e99 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/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(); + + 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(); diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 52f173fc71..aeead7bfd0 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/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 /// /// Base class for controls that present items inside an . /// - public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl + public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl, IChildIndexProvider { /// /// Defines the property. @@ -36,6 +37,7 @@ namespace Avalonia.Controls.Presenters private IDisposable _itemsSubscription; private bool _createdPanel; private IItemContainerGenerator _generator; + private EventHandler _childIndexChanged; /// /// Initializes static members of the class. @@ -129,6 +131,12 @@ namespace Avalonia.Controls.Presenters protected bool IsHosted => TemplatedParent is IItemsPresenterHost; + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// 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)); + } + } + /// 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); + } } } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index e3783febdd..a62ba306ab 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/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 @@ -511,8 +514,8 @@ namespace Avalonia.Controls.Presenters else if (scrollable.IsLogicalScrollEnabled) { Viewport = scrollable.Viewport; - Offset = scrollable.Offset; Extent = scrollable.Extent; + Offset = scrollable.Offset; } } diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index c42c6f100c..3c82386991 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/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, diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index e804c4b4a9..4c9b95c22d 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -20,10 +20,12 @@ namespace Avalonia.Controls.Primitives /// /// Displays a popup window. /// +#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 WindowManagerAddShadowHintProperty = - AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), true); + AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), false); /// /// Defines the property. @@ -53,6 +55,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register( nameof(PlacementConstraintAdjustment), PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY | + PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY | PopupPositionerConstraintAdjustment.ResizeX | PopupPositionerConstraintAdjustment.ResizeY); /// @@ -145,7 +148,9 @@ namespace Avalonia.Controls.Primitives { IsHitTestVisibleProperty.OverrideDefaultValue(false); ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); - IsOpenProperty.Changed.AddClassHandler((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs)e)); + IsOpenProperty.Changed.AddClassHandler((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs)e)); + VerticalOffsetProperty.Changed.AddClassHandler((x, _) => x.HandlePositionChange()); + HorizontalOffsetProperty.Changed.AddClassHandler((x, _) => x.HandlePositionChange()); } /// @@ -519,6 +524,24 @@ namespace Avalonia.Controls.Primitives base.OnDetachedFromLogicalTree(e); Close(); } + + private void HandlePositionChange() + { + if (_openState != null) + { + var placementTarget = PlacementTarget ?? this.FindLogicalAncestorOfType(); + if (placementTarget == null) + return; + _openState.PopupHost.ConfigurePosition( + placementTarget, + PlacementMode, + new Point(HorizontalOffset, VerticalOffset), + PlacementAnchor, + PlacementGravity, + PlacementConstraintAdjustment, + PlacementRect); + } + } private static IDisposable SubscribeToEventHandler(T target, TEventHandler handler, Action subscribe, Action unsubscribe) { diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 0f68f5d258..844069965b 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/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) diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 9ab5a73af0..a848901bc3 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/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( diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index 5c9017f127..d1743611cd 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs @@ -1,13 +1,9 @@ -using System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Metadata; -#if SIGNED_BUILD + [assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -#else -[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests")] -[assembly: InternalsVisibleTo("Avalonia.DesignerSupport")] -#endif + [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Embedding")] diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index 234960e87c..b839a8769a 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/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()) { diff --git a/src/Avalonia.Controls/RepeatButton.cs b/src/Avalonia.Controls/RepeatButton.cs index ba770634d9..a21725cadf 100644 --- a/src/Avalonia.Controls/RepeatButton.cs +++ b/src/Avalonia.Controls/RepeatButton.cs @@ -70,6 +70,16 @@ namespace Avalonia.Controls _repeatTimer?.Stop(); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IsPressedProperty && change.NewValue.GetValueOrDefault() == false) + { + StopTimer(); + } + } + protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 01200e87e3..ecc0fa3a48 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/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. /// - public class ItemsRepeater : Panel + public class ItemsRepeater : Panel, IChildIndexProvider { /// /// Defines the property. @@ -61,8 +62,9 @@ namespace Avalonia.Controls private readonly ViewportManager _viewportManager; private IEnumerable _items; private VirtualizingLayoutContext _layoutContext; - private NotifyCollectionChangedEventArgs _processingItemsSourceChange; + private EventHandler _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 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; + } + /// /// Occurs each time an element is cleared and made available to be re-used. /// @@ -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; diff --git a/src/Avalonia.Controls/Templates/IControlTemplate.cs b/src/Avalonia.Controls/Templates/IControlTemplate.cs index 7414f438a1..ab46884402 100644 --- a/src/Avalonia.Controls/Templates/IControlTemplate.cs +++ b/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 { 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; diff --git a/src/Avalonia.Controls/Templates/TemplateResult.cs b/src/Avalonia.Controls/Templates/TemplateResult.cs new file mode 100644 index 0000000000..770aecc329 --- /dev/null +++ b/src/Avalonia.Controls/Templates/TemplateResult.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Controls.Templates +{ + public class TemplateResult + { + 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; + } + } +} diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 9eae928eeb..0a29db555c 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/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; diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs new file mode 100644 index 0000000000..59edb6278a --- /dev/null +++ b/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 + { + } + + + + 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; + } + } + + /// + /// Raised when the TrayIcon is clicked. + /// Note, this is only supported on Win32 and some Linux DEs, + /// on OSX this event is not raised. + /// + public event EventHandler? Clicked; + + /// + /// Defines the property. + /// + public static readonly DirectProperty CommandProperty = + Button.CommandProperty.AddOwner( + trayIcon => trayIcon.Command, + (trayIcon, command) => trayIcon.Command = command, + enableDataValidation: true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CommandParameterProperty = + Button.CommandParameterProperty.AddOwner(); + + /// + /// Defines the attached property. + /// + public static readonly AttachedProperty IconsProperty + = AvaloniaProperty.RegisterAttached("Icons"); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MenuProperty + = AvaloniaProperty.Register(nameof(Menu)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IconProperty = + Window.IconProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ToolTipTextProperty = + AvaloniaProperty.Register(nameof(ToolTipText)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsVisibleProperty = + Visual.IsVisibleProperty.AddOwner(); + + public static void SetIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons); + + public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty); + + /// + /// Gets or sets the property of a TrayIcon. + /// + public ICommand? Command + { + get => _command; + set => SetAndRaise(CommandProperty, ref _command, value); + } + + /// + /// Gets or sets the parameter to pass to the property of a + /// . + /// + public object CommandParameter + { + get { return GetValue(CommandParameterProperty); } + set { SetValue(CommandParameterProperty, value); } + } + + /// + /// Gets or sets the Menu of the TrayIcon. + /// + public NativeMenu? Menu + { + get => GetValue(MenuProperty); + set => SetValue(MenuProperty, value); + } + + /// + /// Gets or sets the icon of the TrayIcon. + /// + public WindowIcon Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + /// + /// Gets or sets the tooltip text of the TrayIcon. + /// + public string? ToolTipText + { + get => GetValue(ToolTipTextProperty); + set => SetValue(ToolTipTextProperty, value); + } + + /// + /// Gets or sets the visibility of the TrayIcon. + /// + 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()); + } + + private static void RemoveIcons(IEnumerable icons) + { + foreach (var icon in icons) + { + icon.Dispose(); + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IconProperty) + { + _impl?.SetIcon(Icon.PlatformImpl); + } + else if (change.Property == IsVisibleProperty) + { + _impl?.SetIsVisible(change.NewValue.GetValueOrDefault()); + } + else if (change.Property == ToolTipTextProperty) + { + _impl?.SetToolTipText(change.NewValue.GetValueOrDefault()); + } + else if (change.Property == MenuProperty) + { + _impl?.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault()); + } + } + + /// + /// Disposes the tray icon (removing it from the tray area). + /// + public void Dispose() => _impl?.Dispose(); + } +} diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs index 9614d079d9..fa5a09e245 100644 --- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs +++ b/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 readOnly) { - return readOnly.Count; - } - else - { - return Enumerable.Count(items.Cast()); + 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()); + } else { return 0; diff --git a/src/Avalonia.Controls/WindowTransparencyLevel.cs b/src/Avalonia.Controls/WindowTransparencyLevel.cs index ce7c03efbb..f416b5de91 100644 --- a/src/Avalonia.Controls/WindowTransparencyLevel.cs +++ b/src/Avalonia.Controls/WindowTransparencyLevel.cs @@ -20,6 +20,11 @@ /// /// The window background is a blur-behind with a high blur radius. This level may fallback to Blur. /// - AcrylicBlur + AcrylicBlur, + + /// + /// The window background is based on desktop wallpaper tint with a blur. This will only work on Windows 11 + /// + Mica } } diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs index 804bb09510..6b1934ed06 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs @@ -25,7 +25,6 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport private AutoResetEvent _wakeup = new AutoResetEvent(false); private FrameMessage _lastFrameMessage = null; private FrameMessage _lastSentFrameMessage = null; - private RequestViewportResizeMessage _lastViewportRequest; private Action _onMessage; private Action _onException; @@ -177,6 +176,7 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport public void Dispose() { + _disposed = true; _pendingSocket?.Dispose(); _simpleServer.Dispose(); } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index 67b832318a..ada63f5326 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/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 PreFlightMessages = new List(); - + + public ITrayIconImpl CreateTrayIcon() => null; + public IWindowImpl CreateWindow() => new WindowStub(); public IWindowImpl CreateEmbeddableWindow() diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs index 4f493bdcc2..d986a11c45 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs +++ b/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; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 29e049a5d8..f515ba82cc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -20,7 +20,7 @@ namespace Avalonia.Diagnostics.ViewModels Name = property.IsAttached ? $"[{property.OwnerType.Name}.{property.Name}]" : property.Name; - + DeclaringType = property.OwnerType; _propertyType = GetTypeName(property.PropertyType); Update(); } @@ -52,6 +52,7 @@ namespace Avalonia.Diagnostics.ViewModels public override string Group => _group; + public override System.Type? DeclaringType { get; } public override string PropertyType => _propertyType; // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index fe48179150..e72345d317 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -25,7 +25,7 @@ namespace Avalonia.Diagnostics.ViewModels { Name = property.DeclaringType.Name + '.' + property.Name; } - + DeclaringType = property.DeclaringType; _propertyType = GetTypeName(property.PropertyType); Update(); @@ -59,6 +59,8 @@ namespace Avalonia.Diagnostics.ViewModels public override bool? IsAttached => default; + public override System.Type? DeclaringType { get; } + // [MemberNotNull(nameof(_type))] public override void Update() { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 3790951b0c..7b32e21fbd 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -17,42 +17,26 @@ namespace Avalonia.Diagnostics.ViewModels internal class ControlDetailsViewModel : ViewModelBase, IDisposable { private readonly IVisual _control; - private readonly IDictionary> _propertyIndex; + private IDictionary> _propertyIndex; private PropertyViewModel? _selectedProperty; + private DataGridCollectionView _propertiesView; private bool _snapshotStyles; private bool _showInactiveStyles; private string? _styleStatus; + private object _selectedEntity; + private readonly Stack<(string Name,object Entry)> _selectedEntitiesStack = new(); + private string _selectedEntityName; + private string _selectedEntityType; public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) { _control = control; TreePage = treePage; - - var properties = GetAvaloniaProperties(control) - .Concat(GetClrProperties(control)) - .OrderBy(x => x, PropertyComparer.Instance) - .ThenBy(x => x.Name) - .ToList(); - - _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList()); - - var view = new DataGridCollectionView(properties); - view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))); - view.Filter = FilterProperty; - PropertiesView = view; - + Layout = new ControlLayoutViewModel(control); - if (control is INotifyPropertyChanged inpc) - { - inpc.PropertyChanged += ControlPropertyChanged; - } - - if (control is AvaloniaObject ao) - { - ao.PropertyChanged += ControlPropertyChanged; - } + NavigateToProperty(control, (control as IControl)?.Name ?? control.ToString()); AppliedStyles = new ObservableCollection(); PseudoClasses = new ObservableCollection(); @@ -133,12 +117,46 @@ namespace Avalonia.Diagnostics.ViewModels public TreePageViewModel TreePage { get; } - public DataGridCollectionView PropertiesView { get; } + public DataGridCollectionView PropertiesView + { + get => _propertiesView; + private set => RaiseAndSetIfChanged(ref _propertiesView, value); + } public ObservableCollection AppliedStyles { get; } public ObservableCollection PseudoClasses { get; } + public object SelectedEntity + { + get => _selectedEntity; + set + { + RaiseAndSetIfChanged(ref _selectedEntity, value); + + } + } + + public string SelectedEntityName + { + get => _selectedEntityName; + set + { + RaiseAndSetIfChanged(ref _selectedEntityName, value); + + } + } + + public string SelectedEntityType + { + get => _selectedEntityType; + set + { + RaiseAndSetIfChanged(ref _selectedEntityType, value); + + } + } + public PropertyViewModel? SelectedProperty { get => _selectedProperty; @@ -378,5 +396,78 @@ namespace Avalonia.Diagnostics.ViewModels } } } + + public void ApplySelectedProperty() + { + var selectedProperty = SelectedProperty; + var selectedEntity = SelectedEntity; + var selectedEntityName = SelectedEntityName; + if (selectedProperty == null) + return; + + object? property; + if (selectedProperty.Key is AvaloniaProperty avaloniaProperty) + { + property = (_selectedEntity as IControl)?.GetValue(avaloniaProperty); + } + else + { + property = selectedEntity.GetType().GetProperties() + .FirstOrDefault(pi => pi.Name == selectedProperty.Name + && pi.DeclaringType == selectedProperty.DeclaringType + && pi.PropertyType.Name == selectedProperty.Type) + ?.GetValue(selectedEntity); + } + if (property == null) return; + _selectedEntitiesStack.Push((Name:selectedEntityName,Entry:selectedEntity)); + NavigateToProperty(property, selectedProperty.Name); + } + + public void ApplyParentProperty() + { + if (_selectedEntitiesStack.Any()) + { + var property = _selectedEntitiesStack.Pop(); + NavigateToProperty(property.Entry, property.Name); + } + } + + protected void NavigateToProperty(object o, string entityName) + { + var oldSelectedEntity = SelectedEntity; + if (oldSelectedEntity is IAvaloniaObject ao1) + { + ao1.PropertyChanged -= ControlPropertyChanged; + } + else if (oldSelectedEntity is INotifyPropertyChanged inpc1) + { + inpc1.PropertyChanged -= ControlPropertyChanged; + } + + SelectedEntity = o; + SelectedEntityName = entityName; + SelectedEntityType = o.ToString(); + var properties = GetAvaloniaProperties(o) + .Concat(GetClrProperties(o)) + .OrderBy(x => x, PropertyComparer.Instance) + .ThenBy(x => x.Name) + .ToList(); + + _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList()); + + var view = new DataGridCollectionView(properties); + view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))); + view.Filter = FilterProperty; + PropertiesView = view; + + if (o is IAvaloniaObject ao2) + { + ao2.PropertyChanged += ControlPropertyChanged; + } + else if (o is INotifyPropertyChanged inpc2) + { + inpc2.PropertyChanged += ControlPropertyChanged; + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs index 65fd81cc78..e8de22710a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs +++ b/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,35 @@ 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; + } + } + + if (!Dispatcher.UIThread.CheckAccess()) + Dispatcher.UIThread.Post(handler); + else + handler(); + } private static bool BelongsToDevTool(IVisual v) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs index 32df2f8745..a9f3182bef 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs @@ -63,7 +63,7 @@ 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) { @@ -72,6 +72,7 @@ namespace Avalonia.Diagnostics.ViewModels } EventChain.Add(link); + if (HandledBy == null && link.Handled) HandledBy = link; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index d3cdff6e71..f3fa9b3d5e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -19,6 +19,7 @@ namespace Avalonia.Diagnostics.ViewModels public abstract string Name { get; } public abstract string Group { get; } public abstract string AssignedType { get; } + public abstract Type? DeclaringType { get; } public abstract string Value { get; set; } public abstract string Priority { get; } public abstract bool? IsAttached { get; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index e6965884b1..1206f61d6d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -13,23 +13,30 @@ - + - +