diff --git a/.gitignore b/.gitignore index 7d672c77558..abf7674560f 100644 --- a/.gitignore +++ b/.gitignore @@ -106,6 +106,9 @@ _NCrunch_*/ *.ncrunchsolution.user nCrunchTemp_* +# CodeRush +.cr/ + # Others sql/ *.Cache diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings index 2c0a6b9dc8c..b0692905e7b 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/azure-pipelines.yml b/azure-pipelines.yml index fbd85071931..bc8d0651375 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,21 +1,25 @@ jobs: - job: Linux pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-20.04' steps: - - task: CmdLine@2 - displayName: 'Install Nuke' + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 3.1.414' inputs: - script: | - dotnet tool install --global Nuke.GlobalTool --version 0.24.0 + version: 3.1.414 + + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 5.0.402' + inputs: + version: 5.0.402 + - task: CmdLine@2 - displayName: 'Run Nuke' + displayName: 'Run Build' inputs: script: | - export PATH="$PATH:$HOME/.dotnet/tools" dotnet --info printenv - nuke --target CiAzureLinux --configuration=Release + ./build.sh --target CiAzureLinux --configuration=Release - task: PublishTestResults@2 inputs: @@ -23,6 +27,7 @@ jobs: testResultsFiles: '$(Build.SourcesDirectory)/artifacts/test-results/*.trx' condition: not(canceled()) + - job: macOS variables: SolutionDir: '$(Build.SourcesDirectory)' @@ -30,10 +35,15 @@ jobs: vmImage: 'macOS-10.15' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.401' + displayName: 'Use .NET Core SDK 3.1.414' inputs: - version: 3.1.401 + version: 3.1.414 + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 5.0.402' + inputs: + version: 5.0.402 + - task: CmdLine@2 displayName: 'Install Mono 5.18' inputs: @@ -45,6 +55,7 @@ jobs: displayName: 'Generate avalonia-native' inputs: script: | + export PATH="`pwd`/sdk:$PATH" cd src/tools/MicroComGenerator; dotnet run -i ../../Avalonia.Native/avn.idl --cpp ../../../native/Avalonia.Native/inc/avalonia-native.h - task: Xcode@5 @@ -58,13 +69,7 @@ jobs: args: '-derivedDataPath ./' - task: CmdLine@2 - displayName: 'Install Nuke' - inputs: - script: | - dotnet tool install --global Nuke.GlobalTool --version 0.24.0 - - - task: CmdLine@2 - displayName: 'Run Nuke' + displayName: 'Run Build' inputs: script: | export COREHOST_TRACE=0 @@ -72,10 +77,8 @@ jobs: export DOTNET_CLI_TELEMETRY_OPTOUT=1 which dotnet dotnet --info - export PATH="$PATH:$HOME/.dotnet/tools" - dotnet --info printenv - nuke --target CiAzureOSX --configuration Release --skip-previewer + ./build.sh --target CiAzureOSX --configuration Release --skip-previewer - task: PublishTestResults@2 inputs: @@ -102,9 +105,14 @@ jobs: SolutionDir: '$(Build.SourcesDirectory)' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.401' + displayName: 'Use .NET Core SDK 3.1.414' + inputs: + version: 3.1.414 + + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 5.0.402' inputs: - version: 3.1.401 + version: 5.0.402 - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/build.sh b/build.sh index bd162fab9bd..9532b4fbe01 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://github.com/raw/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/MicroCom.targets b/build/MicroCom.targets index b48e377fd42..49d2cdce726 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 2d9d40dbb1d..f2d0b1bec00 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> Avalonia - 0.10.6 + 0.10.10 Copyright 2021 © The AvaloniaUI Project https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/ diff --git a/global.json b/global.json index 6d2315337ec..9f83c1ea1e8 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version_dotnotuse": "3.1.401" + "version": "5.0.402" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", diff --git a/native/Avalonia.Native/inc/comimpl.h b/native/Avalonia.Native/inc/comimpl.h index 0ff64b72158..47b0a3c5f27 100644 --- a/native/Avalonia.Native/inc/comimpl.h +++ b/native/Avalonia.Native/inc/comimpl.h @@ -8,8 +8,109 @@ #include +/** + START_COM_CALL causes AddRef to be called at the beggining of a function. + When a function is exited, it causes ReleaseRef to be called. + This ensures that the object cannot be destroyed whilst the function is running. + For example: Window Show is called, which triggers an event, and user calls Close inside the event + causing the refcount to reach 0, and the object to be destroyed. Function then continues and this pointer + will now be invalid. + + START_COM_CALL protects against this scenario. + */ +#define START_COM_CALL auto r = this->UnknownSelf() + __IID_DEF(IUnknown, 0, 0, 0, C0, 00, 00, 00, 00, 00, 00, 46); +template +class ComPtr +{ +private: + TInterface* _obj; +public: + ComPtr() + { + _obj = 0; + } + + ComPtr(TInterface* pObj) + { + _obj = 0; + + if (pObj) + { + _obj = pObj; + _obj->AddRef(); + } + } + + ComPtr(const ComPtr& ptr) + { + _obj = 0; + + if (ptr._obj) + { + _obj = ptr._obj; + _obj->AddRef(); + } + + } + + ComPtr& operator=(ComPtr other) + { + if(_obj != NULL) + _obj->Release(); + _obj = other._obj; + if(_obj != NULL) + _obj->AddRef(); + return *this; + } + + ~ComPtr() + { + if (_obj) + { + _obj->Release(); + _obj = 0; + } + } + + TInterface* getRaw() + { + return _obj; + } + + TInterface* getRetainedReference() + { + if(_obj == NULL) + return NULL; + _obj->AddRef(); + return _obj; + } + + TInterface** getPPV() + { + return &_obj; + } + + operator TInterface*() const + { + return _obj; + } + TInterface& operator*() const + { + return *_obj; + } + TInterface** operator&() + { + return &_obj; + } + TInterface* operator->() const + { + return _obj; + } +}; + class ComObject : public virtual IUnknown { private: @@ -58,6 +159,12 @@ class ComObject : public virtual IUnknown _refCount++; return S_OK; } + +protected: + ComPtr UnknownSelf() + { + return this; + } }; @@ -104,94 +211,5 @@ template class ComSingleObject : public ComO virtual ~ComSingleObject(){} }; -template -class ComPtr -{ -private: - TInterface* _obj; -public: - ComPtr() - { - _obj = 0; - } - - ComPtr(TInterface* pObj) - { - _obj = 0; - - if (pObj) - { - _obj = pObj; - _obj->AddRef(); - } - } - - ComPtr(const ComPtr& ptr) - { - _obj = 0; - - if (ptr._obj) - { - _obj = ptr._obj; - _obj->AddRef(); - } - - } - - ComPtr& operator=(ComPtr other) - { - if(_obj != NULL) - _obj->Release(); - _obj = other._obj; - if(_obj != NULL) - _obj->AddRef(); - return *this; - } - - ~ComPtr() - { - if (_obj) - { - _obj->Release(); - _obj = 0; - } - } - - TInterface* getRaw() - { - return _obj; - } - - TInterface* getRetainedReference() - { - if(_obj == NULL) - return NULL; - _obj->AddRef(); - return _obj; - } - - TInterface** getPPV() - { - return &_obj; - } - - operator TInterface*() const - { - return _obj; - } - TInterface& operator*() const - { - return *_obj; - } - TInterface** operator&() - { - return &_obj; - } - TInterface* operator->() const - { - return _obj; - } -}; - #endif // COMIMPL_H_INCLUDED #pragma clang diagnostic pop 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 dba3ee6d31d..85fcf20034f 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/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index 001cf151d87..cd0e2cdf941 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -43,6 +43,8 @@ virtual HRESULT Pointer(void**retOut) override { + START_COM_CALL; + @autoreleasepool { if(retOut == nullptr) @@ -58,14 +60,19 @@ virtual HRESULT Pointer(void**retOut) override virtual HRESULT Length(int*retOut) override { - if(retOut == nullptr) + START_COM_CALL; + + @autoreleasepool { - return E_POINTER; + if(retOut == nullptr) + { + return E_POINTER; + } + + *retOut = _length; + + return S_OK; } - - *retOut = _length; - - return S_OK; } }; @@ -109,10 +116,15 @@ virtual unsigned int GetCount() override virtual HRESULT Get(unsigned int index, IAvnString**ppv) override { - if(_list.size() <= index) - return E_INVALIDARG; - *ppv = _list[index].getRetainedReference(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + if(_list.size() <= index) + return E_INVALIDARG; + *ppv = _list[index].getRetainedReference(); + return S_OK; + } } }; diff --git a/native/Avalonia.Native/src/OSX/KeyTransform.mm b/native/Avalonia.Native/src/OSX/KeyTransform.mm index 6b7d95b6191..4817ad0ccf4 100644 --- a/native/Avalonia.Native/src/OSX/KeyTransform.mm +++ b/native/Avalonia.Native/src/OSX/KeyTransform.mm @@ -222,6 +222,7 @@ { 45, "n" }, { 46, "m" }, { 47, "." }, + { 48, "\t" }, { 49, " " }, { 50, "`" }, { 51, "" }, diff --git a/native/Avalonia.Native/src/OSX/Screens.mm b/native/Avalonia.Native/src/OSX/Screens.mm index 10f698ff457..b9c75ed7427 100644 --- a/native/Avalonia.Native/src/OSX/Screens.mm +++ b/native/Avalonia.Native/src/OSX/Screens.mm @@ -8,6 +8,8 @@ public: virtual HRESULT GetScreenCount (int* ret) override { + START_COM_CALL; + @autoreleasepool { *ret = (int)[NSScreen screens].count; @@ -18,6 +20,8 @@ virtual HRESULT GetScreenCount (int* ret) override virtual HRESULT GetScreen (int index, AvnScreen* ret) override { + START_COM_CALL; + @autoreleasepool { if(index < 0 || index >= [NSScreen screens].count) diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 460c24ea3a6..e1972b22f4d 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -50,6 +50,12 @@ - (void)application:(NSApplication *)application openURLs:(NSArray *)ur _events->FilesOpened(array); } + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender +{ + return _events->TryShutdown() ? NSTerminateNow : NSTerminateCancel; +} + @end @interface AvnApplication : NSApplication diff --git a/native/Avalonia.Native/src/OSX/cgl.mm b/native/Avalonia.Native/src/OSX/cgl.mm index a9d94cdf04d..085037978eb 100644 --- a/native/Avalonia.Native/src/OSX/cgl.mm +++ b/native/Avalonia.Native/src/OSX/cgl.mm @@ -69,6 +69,8 @@ static CGLContextObj CreateCglContext(CGLContextObj share) virtual HRESULT LegacyMakeCurrent() override { + START_COM_CALL; + if(CGLSetCurrentContext(Context) != 0) return E_FAIL; return S_OK; @@ -76,6 +78,8 @@ virtual HRESULT LegacyMakeCurrent() override virtual HRESULT MakeCurrent(IUnknown** ppv) override { + START_COM_CALL; + CGLContextObj saved = CGLGetCurrentContext(); CGLLockContext(Context); if(CGLSetCurrentContext(Context) != 0) @@ -128,6 +132,8 @@ virtual int GetStencilSize() override virtual HRESULT CreateContext(IAvnGlContext* share, IAvnGlContext**ppv) override { + START_COM_CALL; + CGLContextObj shareContext = nil; if(share != nil) { @@ -144,6 +150,8 @@ virtual HRESULT CreateContext(IAvnGlContext* share, IAvnGlContext**ppv) override virtual HRESULT WrapContext(void* native, IAvnGlContext**ppv) override { + START_COM_CALL; + if(native == nil) return E_INVALIDARG; *ppv = new AvnGlContext((CGLContextObj) native); diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index f1483747593..9966971b735 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -25,6 +25,8 @@ virtual HRESULT GetText (char* type, IAvnString**ppv) override { + START_COM_CALL; + @autoreleasepool { if(ppv == nullptr) @@ -42,6 +44,8 @@ virtual HRESULT GetText (char* type, IAvnString**ppv) override virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override { + START_COM_CALL; + @autoreleasepool { *ppv= nil; @@ -69,56 +73,71 @@ virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override virtual HRESULT SetText (char* type, char* utf8String) override { - Clear(); + START_COM_CALL; + @autoreleasepool { + Clear(); + auto string = [NSString stringWithUTF8String:(const char*)utf8String]; auto typeString = [NSString stringWithUTF8String:(const char*)type]; if(_item == nil) [_pb setString: string forType: typeString]; else [_item setString: string forType:typeString]; - } - return S_OK; + return S_OK; + } } virtual HRESULT SetBytes(char* type, void* bytes, int len) override { - auto typeString = [NSString stringWithUTF8String:(const char*)type]; - auto data = [NSData dataWithBytes:bytes length:len]; - if(_item == nil) - [_pb setData:data forType:typeString]; - else - [_item setData:data forType:typeString]; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + auto data = [NSData dataWithBytes:bytes length:len]; + if(_item == nil) + [_pb setData:data forType:typeString]; + else + [_item setData:data forType:typeString]; + return S_OK; + } } virtual HRESULT GetBytes(char* type, IAvnString**ppv) override { - *ppv = nil; - auto typeString = [NSString stringWithUTF8String:(const char*)type]; - NSData*data; - @try + START_COM_CALL; + + @autoreleasepool { - if(_item) - data = [_item dataForType:typeString]; - else - data = [_pb dataForType:typeString]; - if(data == nil) + *ppv = nil; + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + NSData*data; + @try + { + if(_item) + data = [_item dataForType:typeString]; + else + data = [_pb dataForType:typeString]; + if(data == nil) + return E_FAIL; + } + @catch(NSException* e) + { return E_FAIL; + } + *ppv = CreateByteArray((void*)data.bytes, (int)data.length); + return S_OK; } - @catch(NSException* e) - { - return E_FAIL; - } - *ppv = CreateByteArray((void*)data.bytes, (int)data.length); - return S_OK; } virtual HRESULT Clear() override { + START_COM_CALL; + @autoreleasepool { if(_item != nil) @@ -128,15 +147,20 @@ virtual HRESULT Clear() override [_pb clearContents]; [_pb setString:@"" forType:NSPasteboardTypeString]; } - } - return S_OK; + return S_OK; + } } virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override { - *ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]); + return S_OK; + } } }; diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index c082003ccfd..8896fbe88b3 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/controlhost.mm b/native/Avalonia.Native/src/OSX/controlhost.mm index 5ee2344ac75..f8e9a3b6d18 100644 --- a/native/Avalonia.Native/src/OSX/controlhost.mm +++ b/native/Avalonia.Native/src/OSX/controlhost.mm @@ -16,11 +16,16 @@ virtual HRESULT CreateDefaultChild(void* parent, void** retOut) override { - NSView* view = [NSView new]; - [view setWantsLayer: true]; + START_COM_CALL; - *retOut = (__bridge_retained void*)view; - return S_OK; + @autoreleasepool + { + NSView* view = [NSView new]; + [view setWantsLayer: true]; + + *retOut = (__bridge_retained void*)view; + return S_OK; + } }; virtual IAvnNativeControlHostTopLevelAttachment* CreateAttachment() override @@ -69,32 +74,42 @@ virtual void DestroyDefaultChild(void* child) override virtual HRESULT InitializeWithChildHandle(void* child) override { - if(_child != nil) - return E_FAIL; - _child = (__bridge NSView*)child; - if(_child == nil) - return E_FAIL; - [_holder addSubview:_child]; - [_child setHidden: false]; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + if(_child != nil) + return E_FAIL; + _child = (__bridge NSView*)child; + if(_child == nil) + return E_FAIL; + [_holder addSubview:_child]; + [_child setHidden: false]; + return S_OK; + } }; virtual HRESULT AttachTo(IAvnNativeControlHost* host) override { - if(host == nil) - { - [_holder removeFromSuperview]; - [_holder setHidden: true]; - } - else + START_COM_CALL; + + @autoreleasepool { - AvnNativeControlHost* chost = dynamic_cast(host); - if(chost == nil || chost->View == nil) - return E_FAIL; - [_holder setHidden:true]; - [chost->View addSubview:_holder]; + if(host == nil) + { + [_holder removeFromSuperview]; + [_holder setHidden: true]; + } + else + { + AvnNativeControlHost* chost = dynamic_cast(host); + if(chost == nil || chost->View == nil) + return E_FAIL; + [_holder setHidden:true]; + [chost->View addSubview:_holder]; + } + return S_OK; } - return S_OK; }; virtual void ShowInBounds(float x, float y, float width, float height) override diff --git a/native/Avalonia.Native/src/OSX/cursor.mm b/native/Avalonia.Native/src/OSX/cursor.mm index 1732d6e71fe..dc38294a189 100644 --- a/native/Avalonia.Native/src/OSX/cursor.mm +++ b/native/Avalonia.Native/src/OSX/cursor.mm @@ -53,36 +53,46 @@ virtual HRESULT GetCursor (AvnStandardCursorType cursorType, IAvnCursor** retOut) override { - *retOut = s_cursorMap[cursorType]; + START_COM_CALL; - if(*retOut != nullptr) + @autoreleasepool { - (*retOut)->AddRef(); - } + *retOut = s_cursorMap[cursorType]; - return S_OK; + if(*retOut != nullptr) + { + (*retOut)->AddRef(); + } + + return S_OK; + } } virtual HRESULT CreateCustomCursor (void* bitmapData, size_t length, AvnPixelSize hotPixel, IAvnCursor** retOut) override { - if(bitmapData == nullptr || retOut == nullptr) + START_COM_CALL; + + @autoreleasepool { - return E_POINTER; + if(bitmapData == nullptr || retOut == nullptr) + { + return E_POINTER; + } + + NSData *imageData = [NSData dataWithBytes:bitmapData length:length]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + + NSPoint hotSpot; + hotSpot.x = hotPixel.Width; + hotSpot.y = hotPixel.Height; + + *retOut = new Cursor([[NSCursor new] initWithImage: image hotSpot: hotSpot]); + + (*retOut)->AddRef(); + + return S_OK; } - - NSData *imageData = [NSData dataWithBytes:bitmapData length:length]; - NSImage *image = [[NSImage alloc] initWithData:imageData]; - - - NSPoint hotSpot; - hotSpot.x = hotPixel.Width; - hotSpot.y = hotPixel.Height; - - *retOut = new Cursor([[NSCursor new] initWithImage: image hotSpot: hotSpot]); - - (*retOut)->AddRef(); - - return S_OK; } }; diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index aaaf381b26c..eeaaecfdbde 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -107,27 +107,42 @@ typedef OSStatus (*LSSetApplicationInformationItemType)(int, PrivateLSASN, virtual HRESULT SetApplicationTitle(char* utf8String) override { - auto appTitle = [NSString stringWithUTF8String: utf8String]; + START_COM_CALL; - [[NSProcessInfo processInfo] setProcessName:appTitle]; - - - SetProcessName(appTitle); - - return S_OK; + @autoreleasepool + { + auto appTitle = [NSString stringWithUTF8String: utf8String]; + + [[NSProcessInfo processInfo] setProcessName:appTitle]; + + + SetProcessName(appTitle); + + return S_OK; + } } virtual HRESULT SetShowInDock(int show) override { - AvnDesiredActivationPolicy = show - ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + AvnDesiredActivationPolicy = show + ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; + return S_OK; + } } virtual HRESULT SetDisableDefaultApplicationMenuItems (bool enabled) override { - SetAutoGenerateDefaultAppMenuItems(!enabled); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + SetAutoGenerateDefaultAppMenuItems(!enabled); + return S_OK; + } } }; @@ -165,6 +180,8 @@ - (void) do FORWARD_IUNKNOWN() virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* events) override { + START_COM_CALL; + _deallocator = deallocator; @autoreleasepool{ [[ThreadingInitializer new] do]; @@ -180,89 +197,165 @@ virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApp virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) override { - if(cb == nullptr || ppv == nullptr) - return E_POINTER; - *ppv = CreateAvnWindow(cb, gl); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + if(cb == nullptr || ppv == nullptr) + return E_POINTER; + *ppv = CreateAvnWindow(cb, gl); + return S_OK; + } }; virtual HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) override { - if(cb == nullptr || ppv == nullptr) - return E_POINTER; + START_COM_CALL; - *ppv = CreateAvnPopup(cb, gl); - return S_OK; + @autoreleasepool + { + if(cb == nullptr || ppv == nullptr) + return E_POINTER; + + *ppv = CreateAvnPopup(cb, gl); + return S_OK; + } } virtual HRESULT CreatePlatformThreadingInterface(IAvnPlatformThreadingInterface** ppv) override { - *ppv = CreatePlatformThreading(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = CreatePlatformThreading(); + return S_OK; + } } virtual HRESULT CreateSystemDialogs(IAvnSystemDialogs** ppv) override { - *ppv = ::CreateSystemDialogs(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateSystemDialogs(); + return S_OK; + } } virtual HRESULT CreateScreens (IAvnScreens** ppv) override { - *ppv = ::CreateScreens (); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateScreens (); + return S_OK; + } } virtual HRESULT CreateClipboard(IAvnClipboard** ppv) override { - *ppv = ::CreateClipboard (nil, nil); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateClipboard (nil, nil); + return S_OK; + } } virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) override { - *ppv = ::CreateClipboard (nil, [NSPasteboardItem new]); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateClipboard (nil, [NSPasteboardItem new]); + return S_OK; + } } virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) override { - *ppv = ::CreateCursorFactory(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateCursorFactory(); + return S_OK; + } } virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) override { - auto rv = ::GetGlDisplay(); - if(rv == NULL) - return E_FAIL; - rv->AddRef(); - *ppv = rv; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + auto rv = ::GetGlDisplay(); + if(rv == NULL) + return E_FAIL; + rv->AddRef(); + *ppv = rv; + return S_OK; + } + } + + virtual HRESULT CreateTrayIcon (IAvnTrayIcon** ppv) override + { + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateTrayIcon(); + return S_OK; + } } virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override { - *ppv = ::CreateAppMenu(cb); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateAppMenu(cb); + return S_OK; + } } virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) override { - *ppv = ::CreateAppMenuItem(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateAppMenuItem(); + return S_OK; + } } virtual HRESULT CreateMenuItemSeparator (IAvnMenuItem** ppv) override { - *ppv = ::CreateAppMenuItemSeparator(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateAppMenuItemSeparator(); + return S_OK; + } } virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override { - ::SetAppMenu(s_appTitle, appMenu); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + ::SetAppMenu(s_appTitle, appMenu); + return S_OK; + } } }; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index b9a95e7b3cc..38f8c2a7cb2 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -95,6 +95,8 @@ - (void)didSelectItem:(nullable id)sender HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu) { + START_COM_CALL; + @autoreleasepool { if(menu != nullptr) @@ -114,6 +116,8 @@ - (void)didSelectItem:(nullable id)sender HRESULT AvnAppMenuItem::SetTitle (char* utf8String) { + START_COM_CALL; + @autoreleasepool { if (utf8String != nullptr) @@ -128,6 +132,8 @@ - (void)didSelectItem:(nullable id)sender HRESULT AvnAppMenuItem::SetGesture (AvnKey key, AvnInputModifiers modifiers) { + START_COM_CALL; + @autoreleasepool { if(key != AvnKeyNone) @@ -183,6 +189,8 @@ - (void)didSelectItem:(nullable id)sender HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) { + START_COM_CALL; + @autoreleasepool { _predicate = predicate; @@ -193,6 +201,8 @@ - (void)didSelectItem:(nullable id)sender HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked) { + START_COM_CALL; + @autoreleasepool { [_native setState:(isChecked && _isCheckable ? NSOnState : NSOffState)]; @@ -202,6 +212,8 @@ - (void)didSelectItem:(nullable id)sender HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType) { + START_COM_CALL; + @autoreleasepool { switch(toggleType) @@ -231,6 +243,8 @@ - (void)didSelectItem:(nullable id)sender HRESULT AvnAppMenuItem::SetIcon(void *data, size_t length) { + START_COM_CALL; + @autoreleasepool { if(data != nullptr) @@ -317,6 +331,8 @@ - (void)didSelectItem:(nullable id)sender HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) { + START_COM_CALL; + @autoreleasepool { if([_native hasGlobalMenuItem]) @@ -337,6 +353,8 @@ - (void)didSelectItem:(nullable id)sender HRESULT AvnAppMenu::RemoveItem (IAvnMenuItem* item) { + START_COM_CALL; + @autoreleasepool { auto avnMenuItem = dynamic_cast(item); @@ -352,6 +370,8 @@ - (void)didSelectItem:(nullable id)sender HRESULT AvnAppMenu::SetTitle (char* utf8String) { + START_COM_CALL; + @autoreleasepool { if (utf8String != nullptr) @@ -365,6 +385,8 @@ - (void)didSelectItem:(nullable id)sender HRESULT AvnAppMenu::Clear() { + START_COM_CALL; + @autoreleasepool { [_native removeAllItems]; diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index e83bf53331a..6d5bd4aa02c 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -114,6 +114,8 @@ virtual void SetSignaledCallback(IAvnSignaledCallback* cb) override virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) override { + START_COM_CALL; + auto can = dynamic_cast(cancel); if(can->Cancelled) return S_OK; diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm index b2d4341bb9d..dc5c24e41e5 100644 --- a/native/Avalonia.Native/src/OSX/rendertarget.mm +++ b/native/Avalonia.Native/src/OSX/rendertarget.mm @@ -247,6 +247,8 @@ -(IAvnGlSurfaceRenderTarget*) createSurfaceRenderTarget virtual HRESULT GetPixelSize(AvnPixelSize* ret) override { + START_COM_CALL; + if(!_surface) return E_FAIL; *ret = _surface->size; @@ -255,6 +257,8 @@ virtual HRESULT GetPixelSize(AvnPixelSize* ret) override virtual HRESULT GetScaling(double* ret) override { + START_COM_CALL; + if(!_surface) return E_FAIL; *ret = _surface->scale; @@ -281,6 +285,8 @@ virtual HRESULT GetScaling(double* ret) override virtual HRESULT BeginDrawing(IAvnGlSurfaceRenderingSession** ret) override { + START_COM_CALL; + ComPtr releaseContext; @synchronized (_target->lock) { if(_target->surface == nil) diff --git a/native/Avalonia.Native/src/OSX/trayicon.h b/native/Avalonia.Native/src/OSX/trayicon.h new file mode 100644 index 00000000000..f94f9a871b1 --- /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 00000000000..151990cfb12 --- /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 b1f64bca880..1dc091a48d5 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -10,6 +10,9 @@ class WindowBaseImpl; -(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; -(void) onClosed; -(AvnPixelSize) getPixelSize; +-(AvnPlatformResizeReason) getResizeReason; +-(void) setResizeReason:(AvnPlatformResizeReason)reason; ++ (AvnPoint)toAvnPoint:(CGPoint)p; @end @interface AutoFitContentView : NSView @@ -34,6 +37,7 @@ class WindowBaseImpl; -(double) getScaling; -(double) getExtendedTitleBarHeight; -(void) setIsExtended:(bool)value; +-(bool) isDialog; @end struct INSWindowHolder @@ -50,4 +54,23 @@ struct IWindowStateChanged virtual AvnWindowState WindowState () = 0; }; +class ResizeScope +{ +public: + ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason) + { + _view = view; + _restore = [view getResizeReason]; + [view setResizeReason:reason]; + } + + ~ResizeScope() + { + [_view setResizeReason:_restore]; + } +private: + AvnView* _Nonnull _view; + AvnPlatformResizeReason _restore; +}; + #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 870345e5435..3e941f4236f 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -29,10 +29,12 @@ IAvnMenu* _mainMenu; bool _shown; + bool _inResize; WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl) { _shown = false; + _inResize = false; _mainMenu = nullptr; BaseEvents = events; _glContext = gl; @@ -54,6 +56,8 @@ virtual HRESULT ObtainNSWindowHandle(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -66,6 +70,8 @@ virtual HRESULT ObtainNSWindowHandle(void** ret) override virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -78,6 +84,8 @@ virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override virtual HRESULT ObtainNSViewHandle(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -90,6 +98,8 @@ virtual HRESULT ObtainNSViewHandle(void** ret) override virtual HRESULT ObtainNSViewHandleRetained(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -105,8 +115,10 @@ virtual HRESULT ObtainNSViewHandleRetained(void** ret) override return Window; } - virtual HRESULT Show(bool activate) override + virtual HRESULT Show(bool activate, bool isDialog) override { + START_COM_CALL; + @autoreleasepool { SetPosition(lastPositionSet); @@ -114,16 +126,19 @@ virtual HRESULT Show(bool activate) override [Window setContentView: StandardContainer]; + [Window setTitle:_lastTitle]; + if(ShouldTakeFocusOnShow() && activate) { + [Window orderFront: Window]; [Window makeKeyAndOrderFront:Window]; + [Window makeFirstResponder:View]; [NSApp activateIgnoringOtherApps:YES]; } else { [Window orderFront: Window]; } - [Window setTitle:_lastTitle]; _shown = true; @@ -138,6 +153,8 @@ virtual bool ShouldTakeFocusOnShow() virtual HRESULT Hide () override { + START_COM_CALL; + @autoreleasepool { if(Window != nullptr) @@ -152,6 +169,8 @@ virtual HRESULT Hide () override virtual HRESULT Activate () override { + START_COM_CALL; + @autoreleasepool { if(Window != nullptr) @@ -166,6 +185,8 @@ virtual HRESULT Activate () override virtual HRESULT SetTopMost (bool value) override { + START_COM_CALL; + @autoreleasepool { [Window setLevel: value ? NSFloatingWindowLevel : NSNormalWindowLevel]; @@ -176,11 +197,16 @@ virtual HRESULT SetTopMost (bool value) override virtual HRESULT Close() override { + START_COM_CALL; + @autoreleasepool { if (Window != nullptr) { - [Window close]; + auto window = Window; + Window = nullptr; + + [window close]; } return S_OK; @@ -189,6 +215,8 @@ virtual HRESULT Close() override virtual HRESULT GetClientSize(AvnSize* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -202,8 +230,27 @@ virtual HRESULT GetClientSize(AvnSize* ret) override } } + virtual HRESULT GetFrameSize(AvnSize* ret) override + { + START_COM_CALL; + + @autoreleasepool + { + if(ret == nullptr) + return E_POINTER; + + auto frame = [Window frame]; + ret->Width = frame.size.width; + ret->Height = frame.size.height; + + return S_OK; + } + } + virtual HRESULT GetScaling (double* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -222,6 +269,8 @@ virtual HRESULT GetScaling (double* ret) override virtual HRESULT SetMinMaxSize (AvnSize minSize, AvnSize maxSize) override { + START_COM_CALL; + @autoreleasepool { [Window setMinSize: ToNSSize(minSize)]; @@ -231,8 +280,18 @@ virtual HRESULT SetMinMaxSize (AvnSize minSize, AvnSize maxSize) override } } - virtual HRESULT Resize(double x, double y) override + virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override { + if(_inResize) + { + return S_OK; + } + + _inResize = true; + + START_COM_CALL; + auto resizeBlock = ResizeScope(View, reason); + @autoreleasepool { auto maxSize = [Window maxSize]; @@ -258,13 +317,20 @@ virtual HRESULT Resize(double x, double y) override y = maxSize.height; } - if(!_shown) + @try { - BaseEvents->Resized(AvnSize{x,y}); + if(!_shown) + { + BaseEvents->Resized(AvnSize{x,y}, reason); + } + + [StandardContainer setFrameSize:NSSize{x,y}]; + [Window setContentSize:NSSize{x, y}]; + } + @finally + { + _inResize = false; } - - [StandardContainer setFrameSize:NSSize{x,y}]; - [Window setContentSize:NSSize{x, y}]; return S_OK; } @@ -272,6 +338,8 @@ virtual HRESULT Resize(double x, double y) override virtual HRESULT Invalidate (AvnRect rect) override { + START_COM_CALL; + @autoreleasepool { [View setNeedsDisplayInRect:[View frame]]; @@ -282,6 +350,8 @@ virtual HRESULT Invalidate (AvnRect rect) override virtual HRESULT SetMainMenu(IAvnMenu* menu) override { + START_COM_CALL; + _mainMenu = menu; auto nativeMenu = dynamic_cast(menu); @@ -300,6 +370,8 @@ virtual HRESULT SetMainMenu(IAvnMenu* menu) override virtual HRESULT BeginMoveDrag () override { + START_COM_CALL; + @autoreleasepool { auto lastEvent = [View lastMouseDownEvent]; @@ -317,11 +389,15 @@ virtual HRESULT BeginMoveDrag () override virtual HRESULT BeginResizeDrag (AvnWindowEdge edge) override { + START_COM_CALL; + return S_OK; } virtual HRESULT GetPosition (AvnPoint* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -342,6 +418,8 @@ virtual HRESULT GetPosition (AvnPoint* ret) override virtual HRESULT SetPosition (AvnPoint point) override { + START_COM_CALL; + @autoreleasepool { lastPositionSet = point; @@ -353,6 +431,8 @@ virtual HRESULT SetPosition (AvnPoint point) override virtual HRESULT PointToClient (AvnPoint point, AvnPoint* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -371,6 +451,8 @@ virtual HRESULT PointToClient (AvnPoint point, AvnPoint* ret) override virtual HRESULT PointToScreen (AvnPoint point, AvnPoint* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -388,12 +470,16 @@ virtual HRESULT PointToScreen (AvnPoint point, AvnPoint* ret) override virtual HRESULT ThreadSafeSetSwRenderedFrame(AvnFramebuffer* fb, IUnknown* dispose) override { + START_COM_CALL; + [View setSwRenderedFrame: fb dispose: dispose]; return S_OK; } virtual HRESULT SetCursor(IAvnCursor* cursor) override { + START_COM_CALL; + @autoreleasepool { Cursor* avnCursor = dynamic_cast(cursor); @@ -423,6 +509,8 @@ virtual void UpdateCursor() virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ppv) override { + START_COM_CALL; + if(View == NULL) return E_FAIL; *ppv = [renderTarget createSurfaceRenderTarget]; @@ -431,6 +519,8 @@ virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ppv) override virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) override { + START_COM_CALL; + if(View == NULL) return E_FAIL; *retOut = ::CreateNativeControlHost(View); @@ -439,6 +529,8 @@ virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) override virtual HRESULT SetBlurEnabled (bool enable) override { + START_COM_CALL; + [StandardContainer ShowBlur:enable]; return S_OK; @@ -448,6 +540,8 @@ virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint p IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) override { + START_COM_CALL; + auto item = TryGetPasteboardItem(clipboard); [item setString:@"" forType:GetAvnCustomDataType()]; if(item == nil) @@ -488,6 +582,11 @@ virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint p return S_OK; } + virtual bool IsDialog() + { + return false; + } + protected: virtual NSWindowStyleMask GetStyle() { @@ -513,10 +612,12 @@ virtual void OnResized () bool _fullScreenActive; SystemDecorations _decorations; AvnWindowState _lastWindowState; + AvnWindowState _actualWindowState; bool _inSetWindowState; NSRect _preZoomSize; bool _transitioningWindowState; bool _isClientAreaExtended; + bool _isDialog; AvnExtendClientAreaChromeHints _extendClientHints; FORWARD_IUNKNOWN() @@ -539,14 +640,21 @@ virtual void OnResized () _transitioningWindowState = false; _inSetWindowState = false; _lastWindowState = Normal; + _actualWindowState = Normal; WindowEvents = events; [Window setCanBecomeKeyAndMain]; [Window disableCursorRects]; [Window setTabbingMode:NSWindowTabbingModeDisallowed]; + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; } void HideOrShowTrafficLights () { + if (Window == nil) + { + return; + } + for (id subview in Window.contentView.superview.subviews) { if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) { NSView *titlebarView = [subview subviews][0]; @@ -571,11 +679,14 @@ void HideOrShowTrafficLights () } } - virtual HRESULT Show (bool activate) override + virtual HRESULT Show (bool activate, bool isDialog) override { + START_COM_CALL; + @autoreleasepool - { - WindowBaseImpl::Show(activate); + { + _isDialog = isDialog; + WindowBaseImpl::Show(activate, isDialog); HideOrShowTrafficLights(); @@ -585,6 +696,8 @@ virtual HRESULT Show (bool activate) override virtual HRESULT SetEnabled (bool enable) override { + START_COM_CALL; + @autoreleasepool { [Window setEnabled:enable]; @@ -594,6 +707,8 @@ virtual HRESULT SetEnabled (bool enable) override virtual HRESULT SetParent (IAvnWindow* parent) override { + START_COM_CALL; + @autoreleasepool { if(parent == nullptr) @@ -603,6 +718,12 @@ virtual HRESULT SetParent (IAvnWindow* parent) override 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); + [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; UpdateStyle(); @@ -633,7 +754,7 @@ AvnWindowState WindowState () override void WindowStateChanged () override { - if(!_inSetWindowState && !_transitioningWindowState) + if(_shown && !_inSetWindowState && !_transitioningWindowState) { AvnWindowState state; GetWindowState(&state); @@ -670,6 +791,7 @@ void WindowStateChanged () override } _lastWindowState = state; + _actualWindowState = state; WindowEvents->WindowStateChanged(state); } } @@ -705,6 +827,8 @@ void DoZoom() virtual HRESULT SetCanResize(bool value) override { + START_COM_CALL; + @autoreleasepool { _canResize = value; @@ -715,6 +839,8 @@ virtual HRESULT SetCanResize(bool value) override virtual HRESULT SetDecorations(SystemDecorations value) override { + START_COM_CALL; + @autoreleasepool { auto currentWindowState = _lastWindowState; @@ -780,6 +906,8 @@ virtual HRESULT SetDecorations(SystemDecorations value) override virtual HRESULT SetTitle (char* utf8title) override { + START_COM_CALL; + @autoreleasepool { _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; @@ -791,6 +919,8 @@ virtual HRESULT SetTitle (char* utf8title) override virtual HRESULT SetTitleBarColor(AvnColor color) override { + START_COM_CALL; + @autoreleasepool { float a = (float)color.Alpha / 255.0f; @@ -820,6 +950,8 @@ virtual HRESULT SetTitleBarColor(AvnColor color) override virtual HRESULT GetWindowState (AvnWindowState*ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -853,100 +985,118 @@ virtual HRESULT GetWindowState (AvnWindowState*ret) override virtual HRESULT TakeFocusFromChildren () override { - if(Window == nil) - return S_OK; - if([Window isKeyWindow]) - [Window makeFirstResponder: View]; + START_COM_CALL; - return S_OK; + @autoreleasepool + { + if(Window == nil) + return S_OK; + if([Window isKeyWindow]) + [Window makeFirstResponder: View]; + + return S_OK; + } } virtual HRESULT SetExtendClientArea (bool enable) override { - _isClientAreaExtended = enable; + START_COM_CALL; - if(enable) + @autoreleasepool { - Window.titleVisibility = NSWindowTitleHidden; + _isClientAreaExtended = enable; - [Window setTitlebarAppearsTransparent:true]; - - auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - - if (wantsTitleBar) - { - [StandardContainer ShowTitleBar:true]; - } - else - { - [StandardContainer ShowTitleBar:false]; - } - - if(_extendClientHints & AvnOSXThickTitleBar) + if(enable) { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; + Window.titleVisibility = NSWindowTitleHidden; + + [Window setTitlebarAppearsTransparent:true]; + + auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + if (wantsTitleBar) + { + [StandardContainer ShowTitleBar:true]; + } + else + { + [StandardContainer ShowTitleBar:false]; + } + + if(_extendClientHints & AvnOSXThickTitleBar) + { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } + else + { + Window.toolbar = nullptr; + } } else { + Window.titleVisibility = NSWindowTitleVisible; Window.toolbar = nullptr; + [Window setTitlebarAppearsTransparent:false]; + View.layer.zPosition = 0; } + + [Window setIsExtended:enable]; + + HideOrShowTrafficLights(); + + UpdateStyle(); + + return S_OK; } - else - { - Window.titleVisibility = NSWindowTitleVisible; - Window.toolbar = nullptr; - [Window setTitlebarAppearsTransparent:false]; - View.layer.zPosition = 0; - } - - [Window setIsExtended:enable]; - - HideOrShowTrafficLights(); - - UpdateStyle(); - - return S_OK; } virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override { - _extendClientHints = hints; + START_COM_CALL; - SetExtendClientArea(_isClientAreaExtended); - return S_OK; + @autoreleasepool + { + _extendClientHints = hints; + + SetExtendClientArea(_isClientAreaExtended); + return S_OK; + } } virtual HRESULT GetExtendTitleBarHeight (double*ret) override { - if(ret == nullptr) + START_COM_CALL; + + @autoreleasepool { - return E_POINTER; + if(ret == nullptr) + { + return E_POINTER; + } + + *ret = [Window getExtendedTitleBarHeight]; + + return S_OK; } - - *ret = [Window getExtendedTitleBarHeight]; - - return S_OK; } virtual HRESULT SetExtendTitleBarHeight (double value) override { - [StandardContainer SetTitleBarHeightHint:value]; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + [StandardContainer SetTitleBarHeightHint:value]; + return S_OK; + } } void EnterFullScreenMode () { _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]; } @@ -961,16 +1111,23 @@ void ExitFullScreenMode () virtual HRESULT SetWindowState (AvnWindowState state) override { + START_COM_CALL; + @autoreleasepool { - if(_lastWindowState == state) + if(Window == nullptr) + { + return S_OK; + } + + if(_actualWindowState == state) { return S_OK; } _inSetWindowState = true; - auto currentState = _lastWindowState; + auto currentState = _actualWindowState; _lastWindowState = state; if(currentState == Normal) @@ -1049,8 +1206,12 @@ virtual HRESULT SetWindowState (AvnWindowState state) override } break; } + + _actualWindowState = _lastWindowState; + WindowEvents->WindowStateChanged(_actualWindowState); } + _inSetWindowState = false; return S_OK; @@ -1065,6 +1226,11 @@ virtual void OnResized () override } } + virtual bool IsDialog() override + { + return _isDialog; + } + protected: virtual NSWindowStyleMask GetStyle() override { @@ -1144,6 +1310,9 @@ -(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content [_blurBehind setWantsLayer:true]; _blurBehind.hidden = true; + [_blurBehind setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [_content setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [self addSubview:_blurBehind]; [self addSubview:_content]; @@ -1179,9 +1348,6 @@ -(void)setFrameSize:(NSSize)newSize _settingSize = true; [super setFrameSize:newSize]; - [_blurBehind setFrameSize:newSize]; - [_content setFrameSize:newSize]; - auto window = objc_cast([self window]); // TODO get actual titlebar size @@ -1197,6 +1363,7 @@ -(void)setFrameSize:(NSSize)newSize [_titleBarMaterial setFrame:tbar]; tbar.size.height = height < 1 ? 0 : 1; [_titleBarUnderline setFrame:tbar]; + _settingSize = false; } @@ -1224,6 +1391,7 @@ @implementation AvnView bool _lastKeyHandled; AvnPixelSize _lastPixelSize; NSObject* _renderTarget; + AvnPlatformResizeReason _resizeReason; } - (void)onClosed @@ -1335,7 +1503,8 @@ -(void)setFrameSize:(NSSize)newSize _lastPixelSize.Height = (int)fsize.height; [self updateRenderTarget]; - _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); + auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; + _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); } } @@ -1376,7 +1545,7 @@ - (AvnPoint) translateLocalPoint:(AvnPoint)pt return pt; } -- (AvnPoint)toAvnPoint:(CGPoint)p ++ (AvnPoint)toAvnPoint:(CGPoint)p { AvnPoint result; @@ -1433,7 +1602,7 @@ - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type } 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; @@ -1501,6 +1670,7 @@ - (void)otherMouseDown:(NSEvent *)event switch(event.buttonNumber) { + case 2: case 3: _isMiddlePressed = true; [self mouseEvent:event withType:MiddleButtonDown]; @@ -1533,6 +1703,7 @@ - (void)otherMouseUp:(NSEvent *)event { switch(event.buttonNumber) { + case 2: case 3: _isMiddlePressed = false; [self mouseEvent:event withType:MiddleButtonUp]; @@ -1589,7 +1760,7 @@ - (void)mouseExited:(NSEvent *)event _isMouseOver = false; [self mouseEvent:event withType:LeaveWindow]; [super mouseExited:event]; -} +} - (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type { @@ -1776,7 +1947,7 @@ - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer) - (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]; @@ -1834,6 +2005,16 @@ - (void)concludeDragOperation:(nullable id )sender } +- (AvnPlatformResizeReason)getResizeReason +{ + return _resizeReason; +} + +- (void)setResizeReason:(AvnPlatformResizeReason)reason +{ + _resizeReason = reason; +} + @end @@ -1853,6 +2034,11 @@ -(void) setIsExtended:(bool)value; _isExtended = value; } +-(bool) isDialog +{ + return _parent->IsDialog(); +} + -(double) getScaling { return _lastScaling; @@ -1882,18 +2068,7 @@ -(double) getExtendedTitleBarHeight +(void)closeAll { - NSArray* windows = [NSArray arrayWithArray:[NSApp windows]]; - auto numWindows = [windows count]; - - for(int i = 0; i < numWindows; i++) - { - auto window = (AvnWindow*)[windows objectAtIndex:i]; - - if([window parentWindow] == nullptr) // Avalonia will handle the child windows. - { - [window performClose:nil]; - } - } + [[NSApplication sharedApplication] terminate:self]; } - (void)performClose:(id)sender @@ -1906,7 +2081,7 @@ - (void)performClose:(id)sender { if(![self windowShouldClose:self]) return; } - + [self close]; } @@ -2042,7 +2217,22 @@ - (void)windowWillClose:(NSNotification *)notification -(BOOL)canBecomeKeyWindow { - return _canBecomeKeyAndMain; + if (_canBecomeKeyAndMain) + { + // If the window has a child window being shown as a dialog then don't allow it to become the key window. + for(NSWindow* uch in [self childWindows]) + { + auto ch = objc_cast(uch); + if(ch == nil) + continue; + if (ch.isDialog) + return false; + } + + return true; + } + + return false; } -(BOOL)canBecomeMainWindow @@ -2050,22 +2240,6 @@ -(BOOL)canBecomeMainWindow return _canBecomeKeyAndMain; } --(bool) activateAppropriateChild: (bool)activating -{ - for(NSWindow* uch in [self childWindows]) - { - auto ch = objc_cast(uch); - if(ch == nil) - continue; - [ch activateAppropriateChild:false]; - return FALSE; - } - - if(!activating) - [self makeKeyAndOrderFront:self]; - return TRUE; -} - -(bool)shouldTryToHandleEvents { return _isEnabled; @@ -2076,26 +2250,15 @@ -(void) setEnabled:(bool)enable _isEnabled = enable; } --(void)makeKeyWindow -{ - if([self activateAppropriateChild: true]) - { - [super makeKeyWindow]; - } -} - -(void)becomeKeyWindow { [self showWindowMenuWithAppMenu]; - if([self activateAppropriateChild: true]) + if(_parent != nullptr) { - if(_parent != nullptr) - { - _parent->BaseEvents->Activated(); - } + _parent->BaseEvents->Activated(); } - + [super becomeKeyWindow]; } @@ -2105,7 +2268,6 @@ -(void) restoreParentWindow; if(parent != nil) { [parent removeChildWindow:self]; - [parent activateAppropriateChild: false]; } } @@ -2218,6 +2380,49 @@ - (void)windowDidMove:(NSNotification *)notification _parent->BaseEvents->PositionChanged(position); } } + +- (AvnPoint) translateLocalPoint:(AvnPoint)pt +{ + pt.Y = [self frame].size.height - pt.Y; + return pt; +} + +- (void)sendEvent:(NSEvent *)event +{ + [super sendEvent:event]; + + /// This is to detect non-client clicks. This can only be done on Windows... not popups, hence the dynamic_cast. + if(_parent != nullptr && dynamic_cast(_parent.getRaw()) != nullptr) + { + switch(event.type) + { + case NSEventTypeLeftMouseDown: + { + auto avnPoint = [AvnView toAvnPoint:[event locationInWindow]]; + auto point = [self translateLocalPoint:avnPoint]; + AvnVector delta; + + _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta); + } + break; + + case NSEventTypeMouseEntered: + { + _parent->UpdateCursor(); + } + break; + + case NSEventTypeMouseExited: + { + [[NSCursor arrowCursor] set]; + } + break; + + default: + break; + } + } +} @end class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup @@ -2240,13 +2445,14 @@ virtual NSWindowStyleMask GetStyle() override return NSWindowStyleMaskBorderless; } - virtual HRESULT Resize(double x, double y) override + virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override { + START_COM_CALL; + @autoreleasepool { if (Window != nullptr) { - [StandardContainer setFrameSize:NSSize{x,y}]; [Window setContentSize:NSSize{x, y}]; [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 45a7f1aa449..3f9ccb04eb3 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -42,12 +42,25 @@ - $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences + $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences;_GenerateAvaloniaResourcesDependencyCache + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 6aad44c0d50..6e57686e007 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 020fb2fff36..36b6fc2dcdb 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") @@ -39,6 +46,10 @@ public class App : Application public static Styles DefaultLight = new Styles { + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml") + }, new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml") @@ -60,6 +71,10 @@ public class App : Application public static Styles DefaultDark = new Styles { + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml") + }, new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml") @@ -89,7 +104,9 @@ public override void Initialize() 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 6537c470d5a..f61b59e6cde 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 a107ee2163d..375345f64e6 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 723351ae572..a9900471c5b 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -17,7 +17,10 @@ public class MainWindow : Window public MainWindow() { this.InitializeComponent(); - this.AttachDevTools(); + this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() + { + StartupScreenIndex = 1, + }); //Renderer.DrawFps = true; //Renderer.DrawDirtyRects = Renderer.DrawFps = true; @@ -32,6 +35,8 @@ public MainWindow() 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 be114bbbc92..b35c112a683 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 1359cfa2efc..769ef26699f 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/ContextFlyoutPage.axaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml deleted file mode 100644 index e15637aa0f3..00000000000 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - Context Flyout - A right click Flyout that can be applied to any control. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs deleted file mode 100644 index e64d4a2cddb..00000000000 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using ControlCatalog.ViewModels; -using Avalonia.Interactivity; -namespace ControlCatalog.Pages -{ - public class ContextFlyoutPage : UserControl - { - private TextBox _textBox; - - public ContextFlyoutPage() - { - InitializeComponent(); - - var vm = new ContextFlyoutPageViewModel(); - vm.View = this; - DataContext = vm; - - _textBox = this.FindControl("TextBox"); - - var cutButton = this.FindControl + + + + + + + + Hello world + + + Inner context flyout + + + + + + + diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs new file mode 100644 index 00000000000..10f9ee1de8f --- /dev/null +++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs @@ -0,0 +1,91 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; +using Avalonia.Interactivity; +using System; +using System.ComponentModel; + +namespace ControlCatalog.Pages +{ + public class ContextFlyoutPage : UserControl + { + private TextBox _textBox; + + public ContextFlyoutPage() + { + InitializeComponent(); + + DataContext = new ContextPageViewModel(); + + _textBox = this.FindControl("TextBox"); + + var cutButton = this.FindControl - - - - - - - - - + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 392ccb57c34..4d0bd663dfc 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 f515db84d40..b36629fb2ae 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 bf31c40e2a2..408f9c2411c 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 161ee2ee169..7a65275a8e8 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 b90f43c3b6c..caab42e98c4 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 7c911e91e9e..2b5215a3feb 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 00000000000..7eea7b06576 --- /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/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs b/samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs deleted file mode 100644 index 8e7fc442f65..00000000000 --- a/samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Collections.Generic; -using System.Reactive; -using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.VisualTree; -using MiniMvvm; - -namespace ControlCatalog.ViewModels -{ - public class ContextFlyoutPageViewModel - { - public Control View { get; set; } - public ContextFlyoutPageViewModel() - { - OpenCommand = MiniCommand.CreateFromTask(Open); - SaveCommand = MiniCommand.Create(Save); - OpenRecentCommand = MiniCommand.Create(OpenRecent); - - MenuItems = new[] - { - new MenuItemViewModel { Header = "_Open...", Command = OpenCommand }, - new MenuItemViewModel { Header = "Save", Command = SaveCommand }, - new MenuItemViewModel { Header = "-" }, - new MenuItemViewModel - { - Header = "Recent", - Items = new[] - { - new MenuItemViewModel - { - Header = "File1.txt", - Command = OpenRecentCommand, - CommandParameter = @"c:\foo\File1.txt" - }, - new MenuItemViewModel - { - Header = "File2.txt", - Command = OpenRecentCommand, - CommandParameter = @"c:\foo\File2.txt" - }, - } - }, - }; - } - - public IReadOnlyList MenuItems { get; set; } - public MiniCommand OpenCommand { get; } - public MiniCommand SaveCommand { get; } - public MiniCommand OpenRecentCommand { get; } - - public async Task Open() - { - var window = View?.GetVisualRoot() as Window; - if (window == null) - return; - var dialog = new OpenFileDialog(); - var result = await dialog.ShowAsync(window); - - if (result != null) - { - foreach (var path in result) - { - System.Diagnostics.Debug.WriteLine($"Opened: {path}"); - } - } - } - - public void Save() - { - System.Diagnostics.Debug.WriteLine("Save"); - } - - public void OpenRecent(string path) - { - System.Diagnostics.Debug.WriteLine($"Open recent: {path}"); - } - } -} diff --git a/samples/ControlCatalog/ViewModels/ContextMenuPageViewModel.cs b/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs similarity index 96% rename from samples/ControlCatalog/ViewModels/ContextMenuPageViewModel.cs rename to samples/ControlCatalog/ViewModels/ContextPageViewModel.cs index 3f5d0cd93cc..b46ffeb0074 100644 --- a/samples/ControlCatalog/ViewModels/ContextMenuPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs @@ -7,10 +7,10 @@ namespace ControlCatalog.ViewModels { - public class ContextMenuPageViewModel + public class ContextPageViewModel { public Control View { get; set; } - public ContextMenuPageViewModel() + public ContextPageViewModel() { OpenCommand = MiniCommand.CreateFromTask(Open); SaveCommand = MiniCommand.Create(Save); diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index aa165d13f72..f37df56b737 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -36,6 +36,9 @@ + + + diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml index 21c7d68b5d6..9043bac33e7 100644 --- a/samples/RenderDemo/Pages/AnimationsPage.xaml +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml @@ -161,6 +161,151 @@ + + + + + + + + @@ -181,6 +326,10 @@ + + + + diff --git a/samples/RenderDemo/Pages/CustomAnimatorPage.xaml b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml new file mode 100644 index 00000000000..b386636caed --- /dev/null +++ b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs new file mode 100644 index 00000000000..eed8ee29cea --- /dev/null +++ b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs @@ -0,0 +1,27 @@ +using System.Reactive.Linq; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using RenderDemo.ViewModels; + +namespace RenderDemo.Pages +{ + public class CustomAnimatorPage : UserControl + { + public CustomAnimatorPage() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/RenderDemo/Pages/CustomStringAnimator.cs b/samples/RenderDemo/Pages/CustomStringAnimator.cs new file mode 100644 index 00000000000..851a2d01877 --- /dev/null +++ b/samples/RenderDemo/Pages/CustomStringAnimator.cs @@ -0,0 +1,16 @@ +using Avalonia.Animation.Animators; + +namespace RenderDemo.Pages +{ + public class CustomStringAnimator : Animator + { + public override string Interpolate(double progress, string oldValue, string newValue) + { + if (newValue.Length == 0) return ""; + var step = 1.0 / newValue.Length; + var length = (int)(progress / step); + var result = newValue.Substring(0, length + 1); + return result; + } + } +} diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml index f9f69fb3417..71b6ea0713a 100644 --- a/samples/RenderDemo/Pages/TransitionsPage.xaml +++ b/samples/RenderDemo/Pages/TransitionsPage.xaml @@ -141,6 +141,106 @@ + + + + + + + + + + + + + + + + + + + + @@ -166,6 +266,18 @@ + + + + + + + + + + + + diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml index fd23067f611..b82a7b0514a 100644 --- a/samples/RenderDemo/SideBar.xaml +++ b/samples/RenderDemo/SideBar.xaml @@ -7,7 +7,6 @@ diff --git a/samples/Sandbox/Program.cs b/samples/Sandbox/Program.cs index 1e74105196f..52321b46a93 100644 --- a/samples/Sandbox/Program.cs +++ b/samples/Sandbox/Program.cs @@ -4,12 +4,12 @@ namespace Sandbox { public class Program { - static void Main(string[] args) - { + static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() - .LogToTrace() - .StartWithClassicDesktopLifetime(args); - } + .LogToTrace(); } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 60b772a1838..0afb1db1413 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -55,6 +55,8 @@ public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) public virtual Size ClientSize => Size.ToSize(RenderScaling); + public Size? FrameSize => null; + public IMouseDevice MouseDevice { get; } = new MouseDevice(); public Action Closed { get; set; } @@ -65,7 +67,7 @@ public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } @@ -132,7 +134,7 @@ public virtual void Dispose() protected virtual void OnResized(Size size) { - Resized?.Invoke(size); + Resized?.Invoke(size, PlatformResizeReason.Unspecified); } class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index c42153ec4f5..daa4793ef05 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -194,6 +194,33 @@ public string RepeatCount [Content] public KeyFrames Children { get; } = new KeyFrames(); + // Store values for the Animator attached properties for IAnimationSetter objects. + private static readonly Dictionary s_animators = new Dictionary(); + + /// + /// Gets the value of the Animator attached property for a setter. + /// + /// The animation setter. + /// The property animator type. + public static Type GetAnimator(IAnimationSetter setter) + { + if (s_animators.TryGetValue(setter, out var type)) + { + return type; + } + return null; + } + + /// + /// Sets the value of the Animator attached property for a setter. + /// + /// The animation setter. + /// The property animator value. + public static void SetAnimator(IAnimationSetter setter, Type value) + { + s_animators[setter] = value; + } + private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)> { ( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator) ), @@ -248,7 +275,7 @@ private static Type GetAnimatorType(AvaloniaProperty property) { foreach (var setter in keyframe.Setters) { - var handler = GetAnimatorType(setter.Property); + var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property); if (handler == null) { diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index 6f601a3e136..cf796401500 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -5,6 +5,7 @@ using Avalonia.Animation.Utils; using Avalonia.Data; using Avalonia.Reactive; +using JetBrains.Annotations; namespace Avalonia.Animation { @@ -36,6 +37,7 @@ internal class AnimationInstance : SingleSubscriberObservableBase private IDisposable _timerSub; private readonly IClock _baseClock; private IClock _clock; + private EventHandler _propertyChangedDelegate; public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action OnComplete, Func Interpolator) { @@ -45,8 +47,6 @@ public AnimationInstance(Animation animation, Animatable control, Animator an _onCompleteAction = OnComplete; _interpolator = Interpolator; _baseClock = baseClock; - _neutralValue = (T)_targetControl.GetValue(_animator.Property); - FetchProperties(); } @@ -80,6 +80,7 @@ protected override void Unsubscribed() // Animation may have been stopped before it has finished. ApplyFinalFill(); + _targetControl.PropertyChanged -= _propertyChangedDelegate; _timerSub?.Dispose(); _clock.PlayState = PlayState.Stop; } @@ -88,6 +89,9 @@ protected override void Subscribed() { _clock = new Clock(_baseClock); _timerSub = _clock.Subscribe(Step); + _propertyChangedDelegate ??= ControlPropertyChanged; + _targetControl.PropertyChanged += _propertyChangedDelegate; + UpdateNeutralValue(); } public void Step(TimeSpan frameTick) @@ -216,5 +220,22 @@ private void InternalStep(TimeSpan time) } } } + + private void UpdateNeutralValue() + { + var property = _animator.Property; + var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue); + + _neutralValue = baseValue != AvaloniaProperty.UnsetValue ? + (T)baseValue : (T)_targetControl.GetValue(property); + } + + private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _animator.Property && e.Priority > BindingPriority.Animation) + { + UpdateNeutralValue(); + } + } } } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 6a9cff6b71a..ce5b37043f5 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -861,7 +861,7 @@ private string GetDescription(object o) } /// - /// 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 5117fdb170d..94aefb8869b 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -465,9 +465,9 @@ public override string ToString() /// 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 c1a2832fdee..896d86e29da 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs @@ -58,8 +58,8 @@ public AvaloniaPropertyChangedEventArgs( /// /// 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 5681214222a..2f1cb2888ec 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -280,8 +280,8 @@ public Enumerator GetEnumerator() /// /// 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); @@ -454,6 +454,28 @@ public void MoveRange(int oldIndex, int count, int newIndex) } } + /// + /// Ensures that the capacity of the list is at least . + /// + /// The capacity. + public void EnsureCapacity(int capacity) + { + // Adapted from List implementation. + var currentCapacity = _inner.Capacity; + + if (currentCapacity < capacity) + { + var newCapacity = currentCapacity == 0 ? 4 : currentCapacity * 2; + + if (newCapacity < capacity) + { + newCapacity = capacity; + } + + _inner.Capacity = newCapacity; + } + } + /// /// Removes an item from the collection. /// @@ -633,24 +655,6 @@ void ICollection.CopyTo(Array array, int index) /// Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList(); - private void EnsureCapacity(int capacity) - { - // Adapted from List implementation. - var currentCapacity = _inner.Capacity; - - if (currentCapacity < capacity) - { - var newCapacity = currentCapacity == 0 ? 4 : currentCapacity * 2; - - if (newCapacity < capacity) - { - newCapacity = capacity; - } - - _inner.Capacity = newCapacity; - } - } - /// /// Raises the event with an add action. /// diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index e50e100d327..2cd9758f128 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -1271,7 +1271,7 @@ public void Reverse() /// 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 9329cdd6af5..3985c5e32f4 100644 --- a/src/Avalonia.Base/Data/Converters/BoolConverters.cs +++ b/src/Avalonia.Base/Data/Converters/BoolConverters.cs @@ -18,5 +18,11 @@ public static class BoolConverters /// 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 9ec600d2bc9..2385d4981c4 100644 --- a/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs @@ -26,7 +26,7 @@ public FuncValueConverter(Func convert) /// 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/Core/Plugins/IDataValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs index 324279e9f0b..2a580fe75fb 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs @@ -20,7 +20,7 @@ public interface IDataValidationPlugin /// /// 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 e6cc1edfdf3..a057ad22544 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 @@ public void OverrideMetadata(Type type, DirectPropertyMetadata metadata) } /// - 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 205967984d7..eabdef05ed6 100644 --- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs +++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs @@ -38,7 +38,7 @@ public DirectPropertyMetadata( /// /// 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 2ad220dddd7..c049f9e7638 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -39,5 +39,10 @@ public static class LogArea /// 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/NullableAttributes.cs b/src/Avalonia.Base/Metadata/NullableAttributes.cs index 91f5e81863d..b6f0f3a47c5 100644 --- a/src/Avalonia.Base/Metadata/NullableAttributes.cs +++ b/src/Avalonia.Base/Metadata/NullableAttributes.cs @@ -1,6 +1,5 @@ #pragma warning disable MA0048 // File name must match type name #define INTERNAL_NULLABLE_ATTRIBUTES -#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 // https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs @@ -10,6 +9,7 @@ namespace System.Diagnostics.CodeAnalysis { +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 /// Specifies that null is allowed as an input even if the corresponding type disallows it. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] #if INTERNAL_NULLABLE_ATTRIBUTES @@ -136,5 +136,82 @@ sealed class DoesNotReturnIfAttribute : Attribute /// Gets the condition parameter value. public bool ParameterValue { get; } } -} +#endif // NETSTANDARD2_0 attributes + +#if NETSTANDARD2_1 || NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + /// + /// Specifies that the method or property will ensure that the listed field and property members have + /// not- values. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public #endif + sealed class MemberNotNullAttribute : Attribute + { + /// Gets field or property member names. + public string[] Members { get; } + + /// Initializes the attribute with a field or property member. + /// The field or property member that is promised to be not-null. + public MemberNotNullAttribute(string member) + { + Members = new[] { member }; + } + + /// Initializes the attribute with the list of field and property members. + /// The list of field and property members that are promised to be not-null. + public MemberNotNullAttribute(params string[] members) + { + Members = members; + } + } + + /// + /// Specifies that the method or property will ensure that the listed field and property members have + /// non- values when returning with the specified return value condition. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MemberNotNullWhenAttribute : Attribute + { + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, + /// the associated parameter will not be . + /// + /// The field or property member that is promised to be not-. + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// + /// The return value condition. If the method returns this value, + /// the associated parameter will not be . + /// + /// The list of field and property members that are promised to be not-null. + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + } +#endif // NETSTANDARD2_1 attributes +} + diff --git a/src/Avalonia.Base/Metadata/TemplateContent.cs b/src/Avalonia.Base/Metadata/TemplateContent.cs index fcd7d69e7bb..7f9e878419e 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/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index 1a787921736..326d1a3f535 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -39,7 +39,7 @@ public override void Send(SendOrPostCallback d, object state) 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 9f2308a062c..0978308ef6c 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; namespace Avalonia.Utilities { @@ -93,13 +94,36 @@ public static bool AcceptsNull(Type type) 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 @@ public static bool TryConvert(Type to, object value, CultureInfo culture, out ob /// 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 @@ public static bool TryConvertImplicit(Type to, object value, out object result) /// 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 @@ public static object ConvertOrDefault(object value, Type type, CultureInfo cultu /// 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.Controls.DataGrid/Collections/DataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs index 5b1e43b8f48..fe6acdc5320 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs @@ -877,7 +877,7 @@ public int PageSize 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 @@ public int PageSize 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 @@ public object GetItemAt(int index) 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 @@ private void SetCurrent(object newItem, int newPosition) /// /// /// 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 9d8ebbfac14..587dd228a3a 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs @@ -83,8 +83,9 @@ object GetKey(object o) 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 @@ public override bool KeysMatch(object groupKey, object itemKey) } 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 1b4632d3680..10c7c16488b 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -25,6 +25,7 @@ using Avalonia.Controls.Utils; using Avalonia.Layout; using Avalonia.Controls.Metadata; +using Avalonia.Input.GestureRecognizers; namespace Avalonia.Controls { @@ -67,7 +68,7 @@ public partial class DataGrid : TemplatedControl 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; @@ -75,7 +76,6 @@ public partial class DataGrid : TemplatedControl private const double DATAGRID_defaultMinColumnWidth = 20; private const double DATAGRID_defaultMaxColumnWidth = double.PositiveInfinity; - private List _validationErrors; private List _bindingValidationErrors; private IDisposable _validationSubscription; @@ -102,7 +102,6 @@ public partial class DataGrid : TemplatedControl private bool _areHandlersSuspended; private bool _autoSizingColumns; private IndexToValueTable _collapsedSlotsTable; - private DataGridCellCoordinates _currentCellCoordinates; private Control _clickedElement; // used to store the current column during a Reset @@ -141,7 +140,6 @@ public partial class DataGrid : TemplatedControl private DataGridSelectedItemsCollection _selectedItems; private bool _temporarilyResetCurrentCell; private object _uneditedValue; // Represents the original current cell value at the time it enters editing mode. - private ICellEditBinding _currentCellEditBinding; // An approximation of the sum of the heights in pixels of the scrolling rows preceding // the first displayed scrolling row. Since the scrolled off rows are discarded, the grid @@ -2217,32 +2215,75 @@ protected virtual void OnLoadingRow(DataGridRowEventArgs e) /// 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; } /// @@ -2895,7 +2936,7 @@ internal bool SetCurrentCellCore(int columnIndex, int slot) return SetCurrentCellCore(columnIndex, slot, commitEdit: true, endRowEdit: true); } - internal void UpdateHorizontalOffset(double newValue) + internal bool UpdateHorizontalOffset(double newValue) { if (HorizontalOffset != newValue) { @@ -2903,7 +2944,9 @@ internal void UpdateHorizontalOffset(double newValue) InvalidateColumnHeadersMeasure(); InvalidateRowsMeasure(true); + return true; } + return false; } internal bool UpdateSelectionAndCurrency(int columnIndex, int slot, DataGridSelectionAction action, bool scrollIntoView) @@ -3002,6 +3045,12 @@ internal void UpdateStateOnCurrentChanged(object currentItem, int currentPositio } } + //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) { @@ -4452,17 +4501,27 @@ private void PopulateCellContent(bool isCellEdited, 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) @@ -5674,6 +5733,35 @@ private void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e) 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) { @@ -5734,7 +5822,7 @@ private bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPre { 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 90401a00a26..97e247bdc61 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -133,7 +133,7 @@ private static ICellEditBinding BindEditingElement(IAvaloniaObject target, Avalo 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 445dc541a7b..e3f150f5c47 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -161,21 +161,42 @@ protected override void OnPointerLeave(PointerEventArgs e) 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 @@ internal void UpdatePseudoClasses() } // 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 2f723154be4..26f0b539520 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs @@ -40,7 +40,7 @@ public override bool Equals(object o) 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 a4bab8b304f..ee69f1c768b 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs @@ -189,7 +189,7 @@ public bool IsColumnHeadersRow } /// - /// 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 9141fb24636..54992571717 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -9,6 +9,7 @@ using Avalonia.Collections; using Avalonia.Utilities; using System; +using System.ComponentModel; using System.Linq; using System.Diagnostics; using Avalonia.Controls.Utils; @@ -27,7 +28,6 @@ public abstract class DataGridColumn : AvaloniaObject private double? _minWidth; private bool _settingWidthInternally; private int _displayIndexWithFiller; - private bool _isVisible; private object _header; private DataGridColumnHeader _headerCell; private IControl _editingElement; @@ -40,7 +40,6 @@ public abstract class DataGridColumn : AvaloniaObject /// protected internal DataGridColumn() { - _isVisible = true; _displayIndexWithFiller = -1; IsInitialDesiredWidthDetermined = false; InheritsWidth = true; @@ -174,32 +173,42 @@ internal ICellEditBinding CellEditBinding get => _editBinding; } + + /// + /// Defines the property. + /// + public static StyledProperty IsVisibleProperty = + Control.IsVisibleProperty.AddOwner(); + /// /// Determines whether or not this column is visible. /// public bool IsVisible { - get - { - return _isVisible; - } - set - { - if (value != IsVisible) - { - OwningGrid?.OnColumnVisibleStateChanging(this); - _isVisible = value; + get => GetValue(IsVisibleProperty); + set => SetValue(IsVisibleProperty, value); + } - if (_headerCell != null) - { - _headerCell.IsVisible = value; - } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IsVisibleProperty) + { + OwningGrid?.OnColumnVisibleStateChanging(this); + var isVisible = (change as AvaloniaPropertyChangedEventArgs).NewValue.Value; - OwningGrid?.OnColumnVisibleStateChanged(this); + if (_headerCell != null) + { + _headerCell.IsVisible = isVisible; } + + OwningGrid?.OnColumnVisibleStateChanged(this); + NotifyPropertyChanged(change.Property.Name); } } + /// /// Actual visible width after Width, MinWidth, and MaxWidth setting at the Column level and DataGrid level /// have been taken into account @@ -645,6 +654,34 @@ public static DataGridColumn GetColumnContainingElement(IControl element) 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. /// @@ -787,7 +824,7 @@ internal DataGridLength CoerceWidth(DataGridLength width) } /// - /// 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 6f957497cbf..915b36687c9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -35,6 +35,7 @@ private enum DragMode 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 @@ internal void OnMouseLeftButtonUp_Click(KeyModifiers keyModifiers, ref bool hand 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 @@ internal void ProcessSort(KeyModifiers keyModifiers) { 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 @@ internal void ProcessSort(KeyModifiers keyModifiers) } 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 @@ internal void ProcessSort(KeyModifiers keyModifiers) } 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 @@ internal void OnMouseMove(PointerEventArgs args, Point mousePosition, Point mous 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 @@ private void OnMouseMove_Reorder(ref bool handled, Point mousePosition, Point mo { 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 a94acdec57d..fade597ca13 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs @@ -233,7 +233,7 @@ public bool BeginEdit(object dataItem) else { editableCollectionView.EditItem(dataItem); - return editableCollectionView.IsEditingItem; + return editableCollectionView.IsEditingItem || editableCollectionView.IsAddingNew; } } @@ -314,7 +314,14 @@ public bool EndEdit(object dataItem) 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 e659438b435..2b8055dd225 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs @@ -79,7 +79,7 @@ internal double PendingVerticalScrollHeight set; } - internal void AddRecylableRow(DataGridRow row) + internal void AddRecyclableRow(DataGridRow row) { Debug.Assert(!_recyclableRows.Contains(row)); row.DetachFromDataGrid(true); @@ -120,7 +120,7 @@ internal void ClearElements(bool recycle) { if (row.IsRecyclable) { - AddRecylableRow(row); + AddRecyclableRow(row); } else { @@ -193,7 +193,7 @@ private int GetCircularListIndex(int slot, bool wrap) 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 @@ internal void FullyRecycleElements() 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 c3562c53a4f..1efce7c0b87 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -378,13 +378,13 @@ internal int? MouseOverColumnIndex } } } - } + } internal Panel RootElement { get; private set; - } + } internal int Slot { @@ -392,7 +392,7 @@ internal int Slot 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 @@ protected override Size MeasureOverride(Size availableSize) 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 @@ internal void UpdatePseudoClasses() PseudoClasses.Set(":editing", IsEditing); PseudoClasses.Set(":invalid", !IsValid); ApplyHeaderStatus(); - } + } } //TODO Animation @@ -722,7 +722,7 @@ internal void EnsureGridLines() 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 @@ internal void EnsureDetailsContentHeight() _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 @@ private void EnsureDetailsDesiredHeight() //TODO Cleanup double? _previousDetailsHeight = null; - + //TODO Animation private void DetailsContent_HeightChanged(double newValue) { @@ -1022,7 +1022,7 @@ internal void SetDetailsVisibilityInternal(bool isVisible, bool raiseNotificatio } } } - + internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight) { if (_detailsElement != null && AreDetailsVisible) @@ -1066,7 +1066,7 @@ internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight) .Subscribe(DetailsContent_MarginChanged); } - + _detailsElement.Children.Add(_detailsContent); } } @@ -1090,6 +1090,28 @@ internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight) } } } + + + 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 1e03b134b1b..49ca23d34c3 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -283,7 +283,11 @@ internal void ClearFrozenStates() //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 @@ private void DataGridRowGroupHeader_PointerPressed(PointerPressedEventArgs e) 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 0cd3589a576..510072174fe 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -179,12 +179,12 @@ protected override void OnPointerLeave(PointerEventArgs e) //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 @@ private void DataGridRowHeader_PointerPressed(object sender, PointerPressedEvent 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 a69b8eafe1c..1d5c8999932 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -425,7 +425,7 @@ internal bool ScrollSlotIntoView(int slot, bool scrolledHorizontally) UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsHeight); } - if (DisplayData.FirstScrollingSlot < slot && DisplayData.LastScrollingSlot > slot) + if (DisplayData.FirstScrollingSlot < slot && (DisplayData.LastScrollingSlot > slot || DisplayData.LastScrollingSlot == -1)) { // The row is already displayed in its entirety return true; @@ -1193,7 +1193,7 @@ private void InsertDisplayedElement(int slot, Control element, bool wasNewlyAdde 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 @@ private bool SlotIsDisplayed(int slot) 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 @@ private void ScrollSlotsByHeight(double height) 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 @@ private void UnloadRow(DataGridRow dataGridRow) if (recycleRow) { - DisplayData.AddRecylableRow(dataGridRow); + DisplayData.AddRecyclableRow(dataGridRow); } else { @@ -2265,7 +2265,7 @@ private void CollectionViewGroup_CollectionChanged_Add(object sender, NotifyColl 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 @@ private int CountAndPopulateGroupHeaders(object group, int rootSlot, int level) return treeCount; } - private void EnsureAnscestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo) + private void EnsureAncestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo) { if (IsSlotVisible(parentGroupInfo.Slot)) { @@ -2789,11 +2789,11 @@ private DataGridRowGroupInfo GetParentGroupInfo(object collection) 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 @@ internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool n 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 1c350a4f145..4eed119240d 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs @@ -140,7 +140,7 @@ protected override Size ArrangeOverride(Size finalSize) 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 0d19f4c479d..308ebc69d40 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 @@ protected override Size MeasureOverride(Size availableSize) 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/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 09d19c8e43b..ca0873e1834 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 0c4f8249ceb..8ccf717d945 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -6,8 +6,8 @@ 0.6 0.8 - M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z - M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z + M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z + M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z M1939 1581l90 -90l-1005 -1005l-1005 1005l90 90l915 -915z @@ -601,7 +601,11 @@ + Grid.ColumnSpan="3"> + + + + bindingSourceSubject, CellEditBinding edi private void SetSourceValue(object value) { - _settingSourceValue = true; + if (!_settingSourceValue) + { + _settingSourceValue = true; - _sourceSubject.OnNext(value); + _sourceSubject.OnNext(value); - _settingSourceValue = false; + _settingSourceValue = false; + } } private void SetControlValue(object value) { @@ -157,4 +158,4 @@ public void CommitEdit() } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs b/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs index 47d1ac1c0b7..38e18b57dee 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs @@ -340,10 +340,30 @@ internal static Type GetNonNullableType(this Type type) internal static PropertyInfo GetPropertyOrIndexer(this Type type, string propertyPath, out object[] index) { index = null; + // Return the default value of GetProperty if the first character is not an indexer token. if (string.IsNullOrEmpty(propertyPath) || propertyPath[0] != LeftIndexerToken) { - // Return the default value of GetProperty if the first character is not an indexer token. - return type.GetProperty(propertyPath); + var property = type.GetProperty(propertyPath); + if (property != null) + { + return property; + } + + // GetProperty does not return inherited interface properties, + // so we need to enumerate them manually. + if (type.IsInterface) + { + foreach (var typeInterface in type.GetInterfaces()) + { + property = type.GetProperty(propertyPath); + if (property != null) + { + return property; + } + } + } + + return null; } if (propertyPath.Length < 2 || propertyPath[propertyPath.Length - 1] != RightIndexerToken) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 3a6810eed94..1c449ff678a 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -1,10 +1,31 @@ Compat issues with assembly Avalonia.Controls: InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. +MembersMustExist : Member 'public System.Action Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract. +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. +InterfacesShouldHaveSameMembers : Interface member 'public System.Action Avalonia.Platform.ITopLevelImpl.Resized.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Action Avalonia.Platform.ITopLevelImpl.Resized.get()' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Action Avalonia.Platform.ITopLevelImpl.Resized.get()' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -Total Issues: 7 +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean, System.Boolean)' is present in the implementation but not in the contract. +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. +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 f616a42cace..d44b2ab0dbe 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -273,7 +273,7 @@ public TAppBuilder With(Func options) } /// - /// 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/Application.cs b/src/Avalonia.Controls/Application.cs index 54c576bb769..157bebe02b0 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -13,6 +13,7 @@ using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Threading; +#nullable enable namespace Avalonia { @@ -35,27 +36,27 @@ public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemp /// /// The application-global data templates. /// - private DataTemplates _dataTemplates; + private DataTemplates? _dataTemplates; private readonly Lazy _clipboard = new Lazy(() => (IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))); private readonly Styler _styler = new Styler(); - private Styles _styles; - private IResourceDictionary _resources; + private Styles? _styles; + private IResourceDictionary? _resources; private bool _notifyingResourcesChanged; - private Action> _stylesAdded; - private Action> _stylesRemoved; + private Action>? _stylesAdded; + private Action>? _stylesRemoved; /// /// Defines the property. /// - public static readonly StyledProperty DataContextProperty = + public static readonly StyledProperty DataContextProperty = StyledElement.DataContextProperty.AddOwner(); /// - public event EventHandler ResourcesChanged; + public event EventHandler? ResourcesChanged; - public event EventHandler UrlsOpened; + public event EventHandler? UrlsOpened; /// /// Creates an instance of the class. @@ -72,7 +73,7 @@ public Application() /// The data context property specifies the default object that will /// be used for data binding. /// - public object DataContext + public object? DataContext { get { return GetValue(DataContextProperty); } set { SetValue(DataContextProperty, value); } @@ -162,7 +163,7 @@ public IResourceDictionary Resources /// /// Gets the styling parent of the application, which is null. /// - IStyleHost IStyleHost.StylingParent => null; + IStyleHost? IStyleHost.StylingParent => null; /// bool IStyleHost.IsStylesInitialized => _styles != null; @@ -194,7 +195,7 @@ event Action> IGlobalStyles.GlobalStylesRemoved public virtual void Initialize() { } /// - bool IResourceNode.TryGetResource(object key, out object value) + bool IResourceNode.TryGetResource(object key, out object? value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || @@ -279,17 +280,17 @@ private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e) NotifyResourcesChanged(e); } - private string _name; + private string? _name; /// /// Defines Name property /// - public static readonly DirectProperty NameProperty = - AvaloniaProperty.RegisterDirect("Name", o => o.Name, (o, v) => o.Name = v); + public static readonly DirectProperty NameProperty = + AvaloniaProperty.RegisterDirect("Name", o => o.Name, (o, v) => o.Name = v); /// /// Application name to be used for various platform-specific purposes /// - public string Name + public string? Name { get => _name; set => SetAndRaise(NameProperty, ref _name, value); diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index aa4342f0754..2a42d99ac5c 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Threading; using Avalonia.Controls; @@ -42,9 +43,13 @@ public ClassicDesktopStyleApplicationLifetime() "Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed"); _activeLifetime = this; } - + /// public event EventHandler Startup; + + /// + public event EventHandler ShutdownRequested; + /// public event EventHandler Exit; @@ -111,6 +116,11 @@ public int Start(string[] args) ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); } + var lifetimeEvents = AvaloniaLocator.Current.GetService(); + + if (lifetimeEvents != null) + lifetimeEvents.ShutdownRequested += OnShutdownRequested; + _cts = new CancellationTokenSource(); MainWindow?.Show(); Dispatcher.UIThread.MainLoop(_cts.Token); @@ -123,6 +133,23 @@ public void Dispose() if (_activeLifetime == this) _activeLifetime = null; } + + private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) + { + ShutdownRequested?.Invoke(this, e); + + if (e.Cancel) + return; + + // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel + // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their + // owners. + foreach (var w in Windows) + if (w.Owner is null) + w.Close(); + if (Windows.Count > 0) + e.Cancel = true; + } } public class ClassicDesktopStyleApplicationLifetimeOptions diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index 212f0b8617f..a70d5dd2f18 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; namespace Avalonia.Controls.ApplicationLifetimes { @@ -34,5 +35,23 @@ public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicatio Window MainWindow { get; set; } IReadOnlyList Windows { get; } + + /// + /// Raised by the platform when an application shutdown is requested. + /// + /// + /// Application Shutdown can be requested for various reasons like OS shutdown. + /// + /// On Windows this will be called when an OS Session (logout or shutdown) terminates. Cancelling the eventargs will + /// block OS shutdown. + /// + /// On OSX this has the same behavior as on Windows and in addition: + /// This event is raised via the Quit menu or right-clicking on the application icon and selecting Quit. + /// + /// This event provides a first-chance to cancel application shutdown; if shutdown is not canceled at this point the application + /// will try to close each non-owned open window, invoking the event on each and allowing + /// each window to cancel the shutdown of the application. Windows cannot however prevent OS shutdown. + /// + event EventHandler ShutdownRequested; } } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs new file mode 100644 index 00000000000..62bc3a89048 --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace Avalonia.Controls.ApplicationLifetimes +{ + public class ShutdownRequestedEventArgs : CancelEventArgs + { + + } +} diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c656ba6f6ce..0e946126ea4 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -2005,7 +2005,7 @@ private void TextUpdated(string newText, bool userInitiated) // 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 @@ private void RefreshView() 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 @@ private void UpdateTextCompletion(bool userInitiated) { 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/Button.cs b/src/Avalonia.Controls/Button.cs index 6912b2db635..8b22cdd4ec2 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -99,6 +99,7 @@ static Button() CommandParameterProperty.Changed.Subscribe(CommandParameterChanged); IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); IsCancelProperty.Changed.Subscribe(IsCancelChanged); + AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler