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..9fa79ec5ba 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,21 +1,28 @@ +variables: + MSBuildEnableWorkloadResolver: 'false' + 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 6.0.100' + inputs: + version: 6.0.100 + - 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 +30,7 @@ jobs: testResultsFiles: '$(Build.SourcesDirectory)/artifacts/test-results/*.trx' condition: not(canceled()) + - job: macOS variables: SolutionDir: '$(Build.SourcesDirectory)' @@ -30,10 +38,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 6.0.100' + inputs: + version: 6.0.100 + - task: CmdLine@2 displayName: 'Install Mono 5.18' inputs: @@ -45,7 +58,8 @@ jobs: displayName: 'Generate avalonia-native' inputs: script: | - cd src/tools/MicroComGenerator; dotnet run -i ../../Avalonia.Native/avn.idl --cpp ../../../native/Avalonia.Native/inc/avalonia-native.h + export PATH="`pwd`/sdk:$PATH" + cd src/tools/MicroComGenerator; dotnet run -f net6.0 -i ../../Avalonia.Native/avn.idl --cpp ../../../native/Avalonia.Native/inc/avalonia-native.h - task: Xcode@5 inputs: @@ -58,13 +72,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 +80,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 +108,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 6.0.100' inputs: - version: 3.1.401 + version: 6.0.100 - 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/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 13419eb173..16aab3911e 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/build/MicroCom.targets b/build/MicroCom.targets index b48e377fd4..1ed388f689 100644 --- a/build/MicroCom.targets +++ b/build/MicroCom.targets @@ -15,7 +15,8 @@ Inputs="@(AvnComIdl);$(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/**/*.cs" Outputs="%(AvnComIdl.OutputFile)"> - + 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/build/SkiaSharp.props b/build/SkiaSharp.props index f2e7df36cd..97b29a192d 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/build/SourceLink.props b/build/SourceLink.props index 1e007e01eb..0f77c2a613 100644 --- a/build/SourceLink.props +++ b/build/SourceLink.props @@ -3,7 +3,7 @@ true false true - embedded + full $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb diff --git a/global.json b/global.json index b2b2da7c4f..e3e652761c 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { - "sdk": { - "version": "3.1.401" - }, + "sdk": { + "version": "6.0.100" + }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", "MSBuild.Sdk.Extras": "2.0.54", 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/KeyTransform.mm b/native/Avalonia.Native/src/OSX/KeyTransform.mm index 6b7d95b619..4817ad0ccf 100644 --- a/native/Avalonia.Native/src/OSX/KeyTransform.mm +++ b/native/Avalonia.Native/src/OSX/KeyTransform.mm @@ -222,6 +222,7 @@ std::map s_QwertyKeyMap = { 45, "n" }, { 46, "m" }, { 47, "." }, + { 48, "\t" }, { 49, " " }, { 50, "`" }, { 51, "" }, 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 9c6a0e6187..d5289f5229 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) @@ -205,7 +206,11 @@ public: auto window = Window; Window = nullptr; - [window close]; + try{ + // Seems to throw sometimes on application exit. + [window close]; + } + catch(NSException*){} } return S_OK; @@ -231,6 +236,8 @@ public: virtual HRESULT GetFrameSize(AvnSize* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -321,6 +328,7 @@ public: BaseEvents->Resized(AvnSize{x,y}, reason); } + [StandardContainer setFrameSize:NSSize{x,y}]; [Window setContentSize:NSSize{x, y}]; } @finally @@ -641,6 +649,7 @@ private: [Window setCanBecomeKeyAndMain]; [Window disableCursorRects]; [Window setTabbingMode:NSWindowTabbingModeDisallowed]; + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; } void HideOrShowTrafficLights () @@ -713,6 +722,13 @@ private: if(cparent == nullptr) return E_INVALIDARG; + // If one tries to show a child window with a minimized parent window, then the parent window will be + // restored but MacOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive + // state. Detect this and explicitly restore the parent window ourselves to avoid this situation. + if (cparent->WindowState() == Minimized) + cparent->SetWindowState(Normal); + + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary]; [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; UpdateStyle(); @@ -1085,14 +1101,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]; } @@ -1204,6 +1213,7 @@ private: } _actualWindowState = _lastWindowState; + WindowEvents->WindowStateChanged(_actualWindowState); } @@ -1540,7 +1550,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return pt; } -- (AvnPoint)toAvnPoint:(CGPoint)p ++ (AvnPoint)toAvnPoint:(CGPoint)p { AvnPoint result; @@ -1597,7 +1607,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; @@ -1665,6 +1675,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent switch(event.buttonNumber) { + case 2: case 3: _isMiddlePressed = true; [self mouseEvent:event withType:MiddleButtonDown]; @@ -1697,6 +1708,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { switch(event.buttonNumber) { + case 2: case 3: _isMiddlePressed = false; [self mouseEvent:event withType:MiddleButtonUp]; @@ -1940,7 +1952,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]; @@ -2373,6 +2385,56 @@ 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: + { + AvnView* view = _parent->View; + NSPoint windowPoint = [event locationInWindow]; + NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil]; + + if (!NSPointInRect(viewPoint, view.bounds)) + { + auto avnPoint = [AvnView toAvnPoint:windowPoint]; + 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/nukebuild/_build.csproj b/nukebuild/_build.csproj index e08ffd0413..b28d3eb700 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -15,6 +15,7 @@ + @@ -36,10 +37,10 @@ - - - - + + MicroComGenerator\%(Filename)%(Extension) + + diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 45a7f1aa44..d43a5c1624 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -42,12 +42,25 @@ - $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences + $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences;_GenerateAvaloniaResourcesDependencyCache + + + + + + + + + + + + + @@ -75,6 +88,7 @@ $(IntermediateOutputPath)/Avalonia/references $(IntermediateOutputPath)/Avalonia/original.dll false + false + 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)] @@ -95,7 +93,8 @@ Support this project by becoming a sponsor. Your logo will show up here with a l - + + ## .NET Foundation diff --git a/samples/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj index d898b737a9..2c6ff74e5e 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp3.1 + net6.0 diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 3c2d2ee359..2d4fc45171 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -2,7 +2,7 @@ WinExe - netcoreapp3.1 + net6.0 true 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 + xmlns:sys="using:System" + xmlns:col="using:System.Collections"> ComboBox A drop-down list. - + + + + Inline Items Inline Item 2 @@ -14,6 +22,24 @@ Inline Item 4 + + + + + Hello + World + + + + + + + + + + + + @@ -46,7 +72,7 @@ - + diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index 820c1324e3..340b3376f5 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -28,7 +28,7 @@ DockPanel.Dock="Top"/> - + diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml index a0e82663bf..0497fe9c3b 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml @@ -3,15 +3,15 @@ x:Class="ControlCatalog.Pages.DialogsPage"> 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/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 7c911e91e9..2b5215a3fe 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -16,7 +16,6 @@ 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/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 86d762a5bc..9660d2a90d 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index cfedb7ad9e..c1d14cba26 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp3.1 + net6.0 diff --git a/samples/RemoteDemo/RemoteDemo.csproj b/samples/RemoteDemo/RemoteDemo.csproj index 530cad805f..607222c2e2 100644 --- a/samples/RemoteDemo/RemoteDemo.csproj +++ b/samples/RemoteDemo/RemoteDemo.csproj @@ -1,7 +1,7 @@ Exe - netcoreapp3.1 + net6.0 diff --git a/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj index 0d33b4c111..eed6fa9e89 100644 --- a/samples/RenderDemo/RenderDemo.csproj +++ b/samples/RenderDemo/RenderDemo.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp3.1 + net6.0 diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml index fd23067f61..b82a7b0514 100644 --- a/samples/RenderDemo/SideBar.xaml +++ b/samples/RenderDemo/SideBar.xaml @@ -7,7 +7,6 @@ diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index 0c19440a1e..8f2812e048 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -2,7 +2,7 @@ WinExe - netcoreapp3.1 + net6.0 true diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index d898b737a9..2c6ff74e5e 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp3.1 + net6.0 diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 172782c5a9..a4515db514 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -353,6 +353,12 @@ namespace Avalonia.Animation return new CompositeDisposable(subscriptions); } + /// + public Task RunAsync(Animatable control, IClock clock = null) + { + return RunAsync(control, clock, default); + } + /// public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default) { diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs index d784227620..23afa76bf6 100644 --- a/src/Avalonia.Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Animation/Animators/Animator`1.cs @@ -79,15 +79,15 @@ namespace Avalonia.Animation.Animators T oldValue, newValue; - if (firstKeyframe.isNeutral) - oldValue = neutralValue; + if (!firstKeyframe.isNeutral && firstKeyframe.Value is T firstKeyframeValue) + oldValue = firstKeyframeValue; else - oldValue = (T)firstKeyframe.Value; + oldValue = neutralValue; - if (lastKeyframe.isNeutral) - newValue = neutralValue; + if (!lastKeyframe.isNeutral && lastKeyframe.Value is T lastKeyframeValue) + newValue = lastKeyframeValue; else - newValue = (T)lastKeyframe.Value; + newValue = neutralValue; if (lastKeyframe.KeySpline != null) progress = lastKeyframe.KeySpline.GetSplineProgress(progress); diff --git a/src/Avalonia.Animation/ApiCompatBaseline.txt b/src/Avalonia.Animation/ApiCompatBaseline.txt index 58cb7830e7..973698f872 100644 --- a/src/Avalonia.Animation/ApiCompatBaseline.txt +++ b/src/Avalonia.Animation/ApiCompatBaseline.txt @@ -1,6 +1,5 @@ Compat issues with assembly Avalonia.Animation: -MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.Animation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' is present in the contract but not in the implementation. MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock, System.Threading.CancellationToken)' is present in the implementation but not in the contract. -Total Issues: 4 +Total Issues: 3 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/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 6a9cff6b71..ce5b37043f 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -861,7 +861,7 @@ namespace Avalonia } /// - /// Logs a mesage if the notification represents a binding error. + /// Logs a message if the notification represents a binding error. /// /// The property being bound. /// The binding notification. diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 5117fdb170..94aefb8869 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -465,9 +465,9 @@ namespace Avalonia /// Uses the visitor pattern to resolve an untyped property to a typed property. /// /// The type of user data passed. - /// The visitor which will accept the typed property. + /// The visitor which will accept the typed property. /// The user data to pass. - public abstract void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + public abstract void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) where TData : struct; /// diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs index c1a2832fde..896d86e29d 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs @@ -58,8 +58,8 @@ namespace Avalonia /// /// This will usually be true, except in /// - /// which recieves notifications for all changes to property values, whether a value with a higher - /// priority is present or not. When this property is false, the change that is being signalled + /// which receives notifications for all changes to property values, whether a value with a higher + /// priority is present or not. When this property is false, the change that is being signaled /// has not resulted in a change to the property value on the object. /// public bool IsEffectiveValueChange { get; private set; } diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 2c7f34c5be..2f1cb2888e 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -280,8 +280,8 @@ namespace Avalonia.Collections /// /// Gets a range of items from the collection. /// - /// The first index to remove. - /// The number of items to remove. + /// The zero-based index at which the range starts. + /// The number of elements in the range. public IEnumerable GetRange(int index, int count) { return _inner.GetRange(index, count); @@ -455,7 +455,7 @@ namespace Avalonia.Collections } /// - /// Ensures that the capacity of the list is at least . + /// Ensures that the capacity of the list is at least . /// /// The capacity. public void EnsureCapacity(int capacity) diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index e50e100d32..2cd9758f12 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -1271,7 +1271,7 @@ namespace Avalonia.Collections.Pooled /// Reverses the elements in a range of this list. Following a call to this /// method, an element in the range given by index and count /// which was previously located at index i will now be located at - /// index index + (index + count - i - 1). + /// index + (index + count - i - 1). /// public void Reverse(int index, int count) { diff --git a/src/Avalonia.Base/Data/Converters/BoolConverters.cs b/src/Avalonia.Base/Data/Converters/BoolConverters.cs index 9329cdd6af..3985c5e32f 100644 --- a/src/Avalonia.Base/Data/Converters/BoolConverters.cs +++ b/src/Avalonia.Base/Data/Converters/BoolConverters.cs @@ -18,5 +18,11 @@ namespace Avalonia.Data.Converters /// public static readonly IMultiValueConverter Or = new FuncMultiValueConverter(x => x.Any(y => y)); + + /// + /// A value converter that returns true when input is false and false when input is true. + /// + public static readonly IValueConverter Not = + new FuncValueConverter(x => !x); } } 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/Data/Converters/MethodToCommandConverter.cs b/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs index 7ff0a8ceca..4a4644317d 100644 --- a/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs +++ b/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs @@ -140,18 +140,9 @@ namespace Avalonia.Data.Converters ); } - Action action = null; - try - { - action = Expression - .Lambda>(body, parameter) - .Compile(); - } - catch (Exception ex) - { - throw ex; - } - return action; + return Expression + .Lambda>(body, parameter) + .Compile(); } static Func CreateCanExecute(object target diff --git a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs index 324279e9f0..2a580fe75f 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs @@ -20,7 +20,7 @@ namespace Avalonia.Data.Core.Plugins /// /// A weak reference to the object. /// The property name. - /// The inner property accessor used to aceess the property. + /// The inner property accessor used to access the property. /// /// An interface through which future interactions with the /// property will be made. diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index e6cc1edfdf..a057ad2254 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -13,7 +13,7 @@ namespace Avalonia /// The type of the property's value. /// /// Whereas is typed on the owner type, this base - /// class provides a non-owner-typed interface to a direct poperty. + /// class provides a non-owner-typed interface to a direct property. /// public abstract class DirectPropertyBase : AvaloniaProperty { @@ -123,9 +123,9 @@ namespace Avalonia } /// - public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + public override void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) { - vistor.Visit(this, ref data); + visitor.Visit(this, ref data); } /// diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs index 205967984d..eabdef05ed 100644 --- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs +++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs @@ -38,7 +38,7 @@ namespace Avalonia /// /// Data validation is validation performed at the target of a binding, for example in a /// view model using the INotifyDataErrorInfo interface. Only certain properties on a - /// control (such as a TextBox's Text property) will be interested in recieving data + /// control (such as a TextBox's Text property) will be interested in receiving data /// validation messages so this feature must be explicitly enabled by setting this flag. /// public bool? EnableDataValidation { get; private set; } 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 9f2308a062..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,13 +94,36 @@ 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. + /// + /// The type to cast to + /// The value to check if cast possible + /// True if the cast is possible, otherwise false. + public static bool CanCast(object value) + { + return value is T || (value is null && AcceptsNull()); + } + /// /// Try to convert a value to a type by any means possible. /// - /// The type to cast to. - /// The value to cast. + /// The type to convert to. + /// The value to convert. /// The culture to use. - /// If successful, contains the cast value. + /// If successful, contains the convert value. /// True if the cast was successful, otherwise false. public static bool TryConvert(Type to, object value, CultureInfo culture, out object result) { @@ -216,10 +240,10 @@ namespace Avalonia.Utilities /// Try to convert a value to a type using the implicit conversions allowed by the C# /// language. /// - /// The type to cast to. - /// The value to cast. - /// If successful, contains the cast value. - /// True if the cast was successful, otherwise false. + /// The type to convert to. + /// The value to convert. + /// If successful, contains the converted value. + /// True if the convert was successful, otherwise false. public static bool TryConvertImplicit(Type to, object value, out object result) { if (value == null) @@ -278,8 +302,8 @@ namespace Avalonia.Utilities /// Convert a value to a type by any means possible, returning the default for that type /// if the value could not be converted. /// - /// The value to cast. - /// The type to cast to.. + /// The value to convert. + /// The type to convert to.. /// The culture to use. /// A value of . public static object ConvertOrDefault(object value, Type type, CultureInfo culture) @@ -291,8 +315,8 @@ namespace Avalonia.Utilities /// Convert a value to a type using the implicit conversions allowed by the C# language or /// return the default for the type if the value could not be converted. /// - /// The value to cast. - /// The type to cast to.. + /// The value to convert. + /// The type to convert to. /// A value of . public static object ConvertImplicitOrDefault(object value, Type type) { diff --git a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs index b85991fb77..62a9ed27be 100644 --- a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs +++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs @@ -1,9 +1,6 @@ using System; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; -using System.Threading; using Microsoft.Build.Framework; namespace Avalonia.Build.Tasks @@ -41,7 +38,7 @@ namespace Avalonia.Build.Tasks File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(), ProjectDirectory, OutputPath, VerifyIl, outputImportance, (SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null, - EnableComInteropPatching, SkipXamlCompilation); + EnableComInteropPatching, SkipXamlCompilation, DebuggerLaunch); if (!res.Success) return false; if (!res.WrittenFile) @@ -87,5 +84,7 @@ namespace Avalonia.Build.Tasks public IBuildEngine BuildEngine { get; set; } public ITaskHost HostObject { get; set; } + + public bool DebuggerLaunch { get; set; } } } diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 508045dccb..593d79471e 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -1,13 +1,10 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using System.Text; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; using Microsoft.Build.Framework; using Mono.Cecil; -using Avalonia.Utilities; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; using XamlX; @@ -44,16 +41,23 @@ namespace Avalonia.Build.Tasks string projectDirectory, string output, bool verifyIl, MessageImportance logImportance, string strongNameKey, bool patchCom, bool skipXamlCompilation) + { + return Compile(engine, input, references, projectDirectory, output, verifyIl, logImportance, strongNameKey, patchCom, skipXamlCompilation, debuggerLaunch:false); + } + + internal static CompileResult Compile(IBuildEngine engine, string input, string[] references, + string projectDirectory, + string output, bool verifyIl, MessageImportance logImportance, string strongNameKey, bool patchCom, bool skipXamlCompilation, bool debuggerLaunch) { var typeSystem = new CecilTypeSystem(references .Where(r => !r.ToLowerInvariant().EndsWith("avalonia.build.tasks.dll")) .Concat(new[] { input }), input); - + var asm = typeSystem.TargetAssemblyDefinition; if (!skipXamlCompilation) { - var compileRes = CompileCore(engine, typeSystem, projectDirectory, verifyIl, logImportance); + var compileRes = CompileCore(engine, typeSystem, projectDirectory, verifyIl, logImportance, debuggerLaunch); if (compileRes == null && !patchCom) return new CompileResult(true); if (compileRes == false) @@ -62,7 +66,7 @@ namespace Avalonia.Build.Tasks if (patchCom) ComInteropHelper.PatchAssembly(asm, typeSystem); - + var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols }; if (!string.IsNullOrWhiteSpace(strongNameKey)) writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey); @@ -70,13 +74,43 @@ namespace Avalonia.Build.Tasks asm.Write(output, writerParameters); return new CompileResult(true, true); - + } - + static bool? CompileCore(IBuildEngine engine, CecilTypeSystem typeSystem, string projectDirectory, bool verifyIl, - MessageImportance logImportance) + MessageImportance logImportance + , bool debuggerLaunch = false) { + if (debuggerLaunch) + { + // According this https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.debugger.launch?view=net-6.0#remarks + // documentation, on not windows platform Debugger.Launch() always return true without running a debugger. + if (System.Diagnostics.Debugger.Launch()) + { + // Set timeout at 1 minut. + var time = new System.Diagnostics.Stopwatch(); + var timeout = TimeSpan.FromMinutes(1); + time.Start(); + + // wait for the debugger to be attacked or timeout. + while (!System.Diagnostics.Debugger.IsAttached && time.Elapsed < timeout) + { + engine.LogMessage($"[PID:{System.Diagnostics.Process.GetCurrentProcess().Id}] Wating attach debugger. Elapsed {time.Elapsed}...", MessageImportance.High); + System.Threading.Thread.Sleep(100); + } + + time.Stop(); + if (time.Elapsed >= timeout) + { + engine.LogMessage("Wating attach debugger timeout.", MessageImportance.Normal); + } + } + else + { + engine.LogMessage("Debugging cancelled.", MessageImportance.Normal); + } + } var asm = typeSystem.TargetAssemblyDefinition; var emres = new EmbeddedResources(asm); var avares = new AvaloniaResources(asm, projectDirectory); diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs index 5b1e43b8f4..fe6acdc532 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs @@ -877,7 +877,7 @@ namespace Avalonia.Collections if (!CheckFlag(CollectionViewFlags.IsMoveToPageDeferred)) { // if the temporaryGroup was not created yet and is out of sync - // then create it so that we can use it as a refernce while paging. + // then create it so that we can use it as a reference while paging. if (IsGrouping && _temporaryGroup.ItemCount != InternalList.Count) { PrepareTemporaryGroups(); @@ -889,7 +889,7 @@ namespace Avalonia.Collections else if (IsGrouping) { // if the temporaryGroup was not created yet and is out of sync - // then create it so that we can use it as a refernce while paging. + // then create it so that we can use it as a reference while paging. if (_temporaryGroup.ItemCount != InternalList.Count) { // update the groups that get created for the @@ -1951,7 +1951,7 @@ namespace Avalonia.Collections EnsureCollectionInSync(); VerifyRefreshNotDeferred(); - // for indicies larger than the count + // for indices larger than the count if (index >= Count || index < 0) { throw new ArgumentOutOfRangeException("index"); @@ -3800,7 +3800,7 @@ namespace Avalonia.Collections /// /// /// This method can be called from a constructor - it does not call - /// any virtuals. The 'count' parameter is substitute for the real Count, + /// any virtuals. The 'count' parameter is substitute for the real Count, /// used only when newItem is null. /// In that case, this method sets IsCurrentAfterLast to true if and only /// if newPosition >= count. This distinguishes between a null belonging diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs index 9d8ebbfac1..587dd228a3 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs @@ -83,8 +83,9 @@ namespace Avalonia.Collections if (key == null) key = item; - if (_valueConverter != null) - key = _valueConverter.Convert(key, typeof(object), level, culture); + var valueConverter = ValueConverter; + if (valueConverter != null) + key = valueConverter.Convert(key, typeof(object), level, culture); return key; } @@ -99,6 +100,8 @@ namespace Avalonia.Collections } public override string PropertyName => _propertyPath; + public IValueConverter ValueConverter { get => _valueConverter; set => _valueConverter = value; } + private Type GetPropertyType(object o) { return o.GetType().GetNestedPropertyType(_propertyPath); diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 83f13fe199..10c7c16488 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -25,6 +25,7 @@ using System.ComponentModel.DataAnnotations; using Avalonia.Controls.Utils; using Avalonia.Layout; using Avalonia.Controls.Metadata; +using Avalonia.Input.GestureRecognizers; namespace Avalonia.Controls { @@ -67,7 +68,7 @@ namespace Avalonia.Controls private const double DATAGRID_minimumColumnHeaderHeight = 4; internal const double DATAGRID_maximumStarColumnWidth = 10000; internal const double DATAGRID_minimumStarColumnWidth = 0.001; - private const double DATAGRID_mouseWheelDelta = 72.0; + private const double DATAGRID_mouseWheelDelta = 50.0; private const double DATAGRID_maxHeadersThickness = 32768; private const double DATAGRID_defaultRowHeight = 22; @@ -2214,32 +2215,75 @@ namespace Avalonia.Controls /// PointerWheelEventArgs protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { - if (IsEnabled && !e.Handled && DisplayData.NumDisplayedScrollingElements > 0) + e.Handled = e.Handled || UpdateScroll(e.Delta * DATAGRID_mouseWheelDelta); + } + + internal bool UpdateScroll(Vector delta) + { + if (IsEnabled && DisplayData.NumDisplayedScrollingElements > 0) { - double scrollHeight = 0; - if (e.Delta.Y > 0) + var handled = false; + var ignoreInvalidate = false; + var scrollHeight = 0d; + + // Vertical scroll handling + if (delta.Y > 0) { - scrollHeight = Math.Max(-_verticalOffset, -DATAGRID_mouseWheelDelta); + scrollHeight = Math.Max(-_verticalOffset, -delta.Y); } - else if (e.Delta.Y < 0) + else if (delta.Y < 0) { if (_vScrollBar != null && VerticalScrollBarVisibility == ScrollBarVisibility.Visible) { - scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), DATAGRID_mouseWheelDelta); + scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), -delta.Y); } else { double maximum = EdgedRowsHeightCalculated - CellsHeight; - scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), DATAGRID_mouseWheelDelta); + scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), -delta.Y); } } + if (scrollHeight != 0) { DisplayData.PendingVerticalScrollHeight = scrollHeight; - InvalidateRowsMeasure(invalidateIndividualElements: false); - e.Handled = true; + handled = true; + } + + // Horizontal scroll handling + if (delta.X != 0) + { + var horizontalOffset = HorizontalOffset - delta.X; + var widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth); + + if (horizontalOffset < 0) + { + horizontalOffset = 0; + } + if (horizontalOffset > widthNotVisible) + { + horizontalOffset = widthNotVisible; + } + + if (UpdateHorizontalOffset(horizontalOffset)) + { + // We don't need to invalidate once again after UpdateHorizontalOffset. + ignoreInvalidate = true; + handled = true; + } + } + + if (handled) + { + if (!ignoreInvalidate) + { + InvalidateRowsMeasure(invalidateIndividualElements: false); + } + return true; } } + + return false; } /// @@ -2892,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) { @@ -2900,7 +2944,9 @@ namespace Avalonia.Controls InvalidateColumnHeadersMeasure(); InvalidateRowsMeasure(true); + return true; } + return false; } internal bool UpdateSelectionAndCurrency(int columnIndex, int slot, DataGridSelectionAction action, bool scrollIntoView) @@ -2999,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) { @@ -4449,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) @@ -5671,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) { @@ -5731,7 +5822,7 @@ namespace Avalonia.Controls { if (SelectionMode == DataGridSelectionMode.Single || !ctrl) { - // Unselect the currectly selected rows except the new selected row + // Unselect the currently selected rows except the new selected row action = DataGridSelectionAction.SelectCurrent; } else 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 445dc541a7..e3f150f5c4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -161,21 +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) { - if (!e.Handled) - //if (!e.Handled && OwningGrid.IsTabStop) - { - OwningGrid.Focus(); - } - if (OwningRow != null) + OwningGrid.Focus(); + } + 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 = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); - OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; + e.Handled = handled; } + + 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); } } } @@ -197,7 +218,7 @@ namespace Avalonia.Controls } // Makes sure the right gridline has the proper stroke and visibility. If lastVisibleColumn is specified, the - // right gridline will be collapsed if this cell belongs to the lastVisibileColumn and there is no filler column + // right gridline will be collapsed if this cell belongs to the lastVisibleColumn and there is no filler column internal void EnsureGridLine(DataGridColumn lastVisibleColumn) { if (OwningGrid != null && _rightGridLine != null) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs b/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs index 2f723154be..26f0b53952 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs @@ -40,7 +40,7 @@ namespace Avalonia.Controls return false; } - // There is build warning if this is missiing + // There is build warning if this is missing public override int GetHashCode() { return base.GetHashCode(); diff --git a/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs b/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs index a4bab8b304..ee69f1c768 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs @@ -189,7 +189,7 @@ namespace Avalonia.Controls } /// - /// DataGrid row item used for proparing the ClipboardRowContent. + /// DataGrid row item used for preparing the ClipboardRowContent. /// public object Item { diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 5c09bab678..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. /// @@ -795,7 +824,7 @@ namespace Avalonia.Controls } /// - /// If the DataGrid is using using layout rounding, the pixel snapping will force all widths to + /// If the DataGrid is using layout rounding, the pixel snapping will force all widths to /// whole numbers. Since the column widths aren't visual elements, they don't go through the normal /// rounding process, so we need to do it ourselves. If we don't, then we'll end up with some /// pixel gaps and/or overlaps between columns. 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/DataGridDisplayData.cs b/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs index e659438b43..2b8055dd22 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs @@ -79,7 +79,7 @@ namespace Avalonia.Controls set; } - internal void AddRecylableRow(DataGridRow row) + internal void AddRecyclableRow(DataGridRow row) { Debug.Assert(!_recyclableRows.Contains(row)); row.DetachFromDataGrid(true); @@ -120,7 +120,7 @@ namespace Avalonia.Controls { if (row.IsRecyclable) { - AddRecylableRow(row); + AddRecyclableRow(row); } else { @@ -193,7 +193,7 @@ namespace Avalonia.Controls internal void FullyRecycleElements() { - // Fully recycle Recycleable rows and transfer them to Recycled rows + // Fully recycle Recyclable rows and transfer them to Recycled rows while (_recyclableRows.Count > 0) { DataGridRow row = _recyclableRows.Pop(); @@ -202,7 +202,7 @@ namespace Avalonia.Controls Debug.Assert(!_fullyRecycledRows.Contains(row)); _fullyRecycledRows.Push(row); } - // Fully recycle Recycleable GroupHeaders and transfer them to Recycled GroupHeaders + // Fully recycle Recyclable GroupHeaders and transfer them to Recycled GroupHeaders while (_recyclableGroupHeaders.Count > 0) { DataGridRowGroupHeader groupHeader = _recyclableGroupHeaders.Pop(); diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index c3562c53a4..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 { @@ -392,7 +392,7 @@ namespace Avalonia.Controls set; } - // Height that the row will eventually end up at after a possible detalis animation has completed + // Height that the row will eventually end up at after a possible details animation has completed internal double TargetHeight { get @@ -517,7 +517,7 @@ namespace Avalonia.Controls return base.MeasureOverride(availableSize); } - //Allow the DataGrid specific componets to adjust themselves based on new values + //Allow the DataGrid specific components to adjust themselves based on new values if (_headerElement != null) { _headerElement.InvalidateMeasure(); @@ -638,7 +638,7 @@ namespace Avalonia.Controls PseudoClasses.Set(":editing", IsEditing); PseudoClasses.Set(":invalid", !IsValid); ApplyHeaderStatus(); - } + } } //TODO Animation @@ -722,7 +722,7 @@ namespace Avalonia.Controls if (_bottomGridLine != null) { // It looks like setting Visibility sometimes has side effects so make sure the value is actually - // diffferent before setting it + // different before setting it bool newVisibility = OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.All; if (newVisibility != _bottomGridLine.IsVisible) @@ -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/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index 4bfbd7d818..1d5c899993 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -1193,7 +1193,7 @@ namespace Avalonia.Controls else { groupHeader = element as DataGridRowGroupHeader; - Debug.Assert(groupHeader != null); // Nothig other and Rows and RowGroups now + Debug.Assert(groupHeader != null); // Nothing other and Rows and RowGroups now if (groupHeader != null) { groupHeader.TotalIndent = (groupHeader.Level == 0) ? 0 : RowGroupSublevelIndents[groupHeader.Level - 1]; @@ -1636,7 +1636,7 @@ namespace Avalonia.Controls if (slot >= DisplayData.FirstScrollingSlot && slot <= DisplayData.LastScrollingSlot) { - // Additional row takes the spot of a displayed row - it is necessarilly displayed + // Additional row takes the spot of a displayed row - it is necessarily displayed return true; } else if (DisplayData.FirstScrollingSlot == -1 && @@ -1825,7 +1825,7 @@ namespace Avalonia.Controls if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset)) { // We've scrolled off more of the first row than what's possible. This can happen - // if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling + // if the first row got shorter (Ex: Collapsing RowDetails) or if the user has a recycling // cleanup issue. In this case, simply try to display the next row as the first row instead if (newFirstScrollingSlot < SlotCount - 1) { @@ -2014,7 +2014,7 @@ namespace Avalonia.Controls if (recycleRow) { - DisplayData.AddRecylableRow(dataGridRow); + DisplayData.AddRecyclableRow(dataGridRow); } else { @@ -2265,7 +2265,7 @@ namespace Avalonia.Controls if (parentGroupInfo.LastSubItemSlot - parentGroupInfo.Slot == 1) { // We just added the first item to a RowGroup so the header should transition from Empty to either Expanded or Collapsed - EnsureAnscestorsExpanderButtonChecked(parentGroupInfo); + EnsureAncestorsExpanderButtonChecked(parentGroupInfo); } } } @@ -2407,7 +2407,7 @@ namespace Avalonia.Controls return treeCount; } - private void EnsureAnscestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo) + private void EnsureAncestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo) { if (IsSlotVisible(parentGroupInfo.Slot)) { @@ -2789,11 +2789,11 @@ namespace Avalonia.Controls return null; } - internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisibile, bool setCurrent) + internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisible, bool setCurrent) { Debug.Assert(groupHeader.RowGroupInfo.CollectionViewGroup.ItemCount > 0); - if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisibile, setCurrent); }) || !CommitEdit()) + if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisible, setCurrent); }) || !CommitEdit()) { return; } @@ -2804,7 +2804,7 @@ namespace Avalonia.Controls UpdateSelectionAndCurrency(CurrentColumnIndex, groupHeader.RowGroupInfo.Slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false); } - UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisibile, isDisplayed: true); + UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisible, isDisplayed: true); ComputeScrollBarsLayout(); // We need force arrange since our Scrollings Rows could update without automatically triggering layout diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs index 1c350a4f14..4eed119240 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs @@ -140,7 +140,7 @@ namespace Avalonia.Controls.Primitives if (dataGridColumn.IsFrozen) { columnHeader.Arrange(new Rect(frozenLeftEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height)); - columnHeader.Clip = null; // The layout system could have clipped this becaues it's not aware of our render transform + columnHeader.Clip = null; // The layout system could have clipped this because it's not aware of our render transform if (DragColumn == dataGridColumn && DragIndicator != null) { dragIndicatorLeftEdge = frozenLeftEdge + DragIndicatorOffset; diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs index 0d19f4c479..308ebc69d4 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs @@ -5,6 +5,9 @@ using System; using System.Diagnostics; + +using Avalonia.Input; +using Avalonia.Input.GestureRecognizers; using Avalonia.Layout; using Avalonia.Media; @@ -16,6 +19,11 @@ namespace Avalonia.Controls.Primitives /// public sealed class DataGridRowsPresenter : Panel { + public DataGridRowsPresenter() + { + AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture); + } + internal DataGrid OwningGrid { get; @@ -176,6 +184,11 @@ namespace Avalonia.Controls.Primitives return new Size(totalCellsWidth + headerWidth, totalHeight); } + private void OnScrollGesture(object sender, ScrollGestureEventArgs e) + { + e.Handled = e.Handled || OwningGrid.UpdateScroll(-e.Delta); + } + #if DEBUG internal void PrintChildren() { 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.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 09d19c8e43..ca0873e183 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -247,7 +247,11 @@ - + + + + + diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index f2dbf42196..8ccf717d94 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -601,7 +601,11 @@ + Grid.ColumnSpan="3"> + + + + 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/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index f616a42cac..d44b2ab0db 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -273,7 +273,7 @@ namespace Avalonia.Controls } /// - /// Sets up the platform-speciic services for the . + /// Sets up the platform-specific services for the . /// private void Setup() { diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c656ba6f6c..0e946126ea 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -2005,7 +2005,7 @@ namespace Avalonia.Controls // The TextBox.TextChanged event was not firing immediately and // was causing an immediate update, even with wrapping. If there is // a selection currently, no update should happen. - if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != TextBox.Text.Length) + if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != (TextBox.Text?.Length ?? 0)) { return; } @@ -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) @@ -2303,7 +2317,7 @@ namespace Avalonia.Controls { if (IsTextCompletionEnabled && TextBox != null && userInitiated) { - int currentLength = TextBox.Text.Length; + int currentLength = TextBox.Text?.Length ?? 0; int selectionStart = TextBoxSelectionStart; if (selectionStart == text.Length && selectionStart > _textSelectionStart) { 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