diff --git a/Avalonia.sln b/Avalonia.sln index ac678ba9ba..568a16ce0e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2027 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29102.190 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" EndProject @@ -197,7 +197,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -1842,6 +1846,54 @@ Global {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.Build.0 = Release|Any CPU {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhone.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhone.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.Build.0 = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhone.ActiveCfg = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhone.Build.0 = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhone.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhone.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.Build.0 = Release|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhone.ActiveCfg = Release|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhone.Build.0 = Release|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7e3532ee23..92e4afdca8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -134,3 +134,4 @@ jobs: pathToPublish: '$(Build.SourcesDirectory)/artifacts/zip' artifactName: 'Samples' condition: succeeded() + diff --git a/build/AndroidWorkarounds.props b/build/AndroidWorkarounds.props index 8a5c18e1ae..67947296b3 100644 --- a/build/AndroidWorkarounds.props +++ b/build/AndroidWorkarounds.props @@ -5,4 +5,12 @@ + + + + + + + false + diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index d989e643b8..3923bdeeda 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -13,6 +13,7 @@ + diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index a35f4f3eeb..36e16c24d1 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -280,7 +280,7 @@ AVNCOM(IAvnPlatformThreadingInterface, 0b) : IUnknown virtual bool GetCurrentThreadIsLoopThread() = 0; virtual void SetSignaledCallback(IAvnSignaledCallback* cb) = 0; virtual IAvnLoopCancellation* CreateLoopCancellation() = 0; - virtual void RunLoop(IAvnLoopCancellation* cancel) = 0; + virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) = 0; // Can't pass int* to sharpgentools for some reason virtual void Signal(int priority) = 0; virtual IUnknown* StartTimer(int priority, int ms, IAvnActionCallback* callback) = 0; 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 cc74d5669f..1870ef7ab3 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 @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; }; @@ -22,6 +23,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; 37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = ""; }; @@ -68,6 +70,7 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + 1A002B9D232135EE00021753 /* app.mm */, 37DDA9B121933371002E132B /* AvnString.h */, 37DDA9AF219330F8002E132B /* AvnString.mm */, 37A4E71A2178846A00EACBCD /* headers */, @@ -164,6 +167,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1A002B9E232135EE00021753 /* app.mm in Sources */, 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */, 5B21A982216530F500CEE36E /* cursor.mm in Sources */, 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index b491cf2a92..b62fe8a968 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -11,14 +11,26 @@ class AvnStringImpl : public virtual ComSingleObject { private: - NSString* _string; + int _length; + const char* _cstring; public: FORWARD_IUNKNOWN() AvnStringImpl(NSString* string) + { + auto cstring = [string cStringUsingEncoding:NSUTF8StringEncoding]; + _length = (int)[string lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + + _cstring = (const char*)malloc(_length + 5); + + memset((void*)_cstring, 0, _length + 5); + memcpy((void*)_cstring, (void*)cstring, _length); + } + + virtual ~AvnStringImpl() { - _string = string; + free((void*)_cstring); } virtual HRESULT Pointer(void**retOut) override @@ -30,7 +42,7 @@ public: return E_POINTER; } - *retOut = (void*)_string.UTF8String; + *retOut = (void*)_cstring; return S_OK; } @@ -43,7 +55,7 @@ public: return E_POINTER; } - *retOut = (int)_string.length; + *retOut = _length; return S_OK; } diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm new file mode 100644 index 0000000000..81855995b7 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -0,0 +1,24 @@ +#include "common.h" +@interface AvnAppDelegate : NSObject +@end +extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; +@implementation AvnAppDelegate +- (void)applicationWillFinishLaunching:(NSNotification *)notification +{ + [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy]; +} + +- (void)applicationDidFinishLaunching:(NSNotification *)notification +{ + [NSApp activateIgnoringOtherApps:true]; +} + +@end + +extern void InitializeAvnApp() +{ + NSApplication* app = [NSApplication sharedApplication]; + id delegate = [AvnAppDelegate new]; + [app setDelegate:delegate]; + +} diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 53c1fe3c2c..6e4d3ce668 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -8,6 +8,13 @@ class Clipboard : public ComSingleObject { public: FORWARD_IUNKNOWN() + + Clipboard() + { + NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; + [pasteBoard stringForType:NSPasteboardTypeString]; + } + virtual HRESULT GetText (IAvnString**ppv) override { @autoreleasepool @@ -39,7 +46,9 @@ public: { @autoreleasepool { - [[NSPasteboard generalPasteboard] clearContents]; + NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; + [pasteBoard clearContents]; + [pasteBoard setString:@"" forType:NSPasteboardTypeString]; } return S_OK; diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index f748dd59ad..45ec40c361 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -19,12 +19,12 @@ extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlFeature* GetGlFeature(); extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view); - +extern void InitializeAvnApp(); +extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); extern AvnPoint ToAvnPoint (NSPoint p); extern AvnPoint ConvertPointY (AvnPoint p); extern NSSize ToNSSize (AvnSize s); - #ifdef DEBUG #define NSDebugLog(...) NSLog(__VA_ARGS__) #else diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 5043246c53..70bd1e67f6 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -5,21 +5,14 @@ #define COM_GUIDS_MATERIALIZE #include "common.h" -static BOOL ShowInDock = 1; - -static void SetActivationPolicy() -{ - [[NSApplication sharedApplication] setActivationPolicy: (ShowInDock ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory)]; -} - class MacOptions : public ComSingleObject { public: FORWARD_IUNKNOWN() virtual HRESULT SetShowInDock(int show) override { - ShowInDock = show; - SetActivationPolicy(); + AvnDesiredActivationPolicy = show + ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; return S_OK; } }; @@ -64,8 +57,9 @@ public: { @autoreleasepool{ [[ThreadingInitializer new] do]; - return S_OK; } + InitializeAvnApp(); + return S_OK; }; virtual IAvnMacOptions* GetMacOptions() override diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index 0c814b2088..297097584a 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -57,16 +57,36 @@ class PlatformThreadingInterface : public ComSingleObject { public: FORWARD_IUNKNOWN() - bool Cancelled = 0; - virtual void Cancel() override + bool Running = false; + bool Cancelled = false; + virtual void Cancel() { - Cancelled = 1; + Cancelled = true; + if(Running) + { + Running = false; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSApplication sharedApplication] stop:nil]; + NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:NSMakePoint(0, 0) + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:0 + data1:0 + data2:0]; + [NSApp postEvent:event atStart:YES]; + }); + } } + }; public: @@ -99,30 +119,17 @@ public: return new LoopCancellation(); } - virtual void RunLoop(IAvnLoopCancellation* cancel) override + virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) override { - @autoreleasepool { - auto can = dynamic_cast(cancel); - [[NSApplication sharedApplication] activateIgnoringOtherApps:true]; - while(true) - { - @autoreleasepool - { - if(can != NULL && can->Cancelled) - return; - NSEvent* ev = [[NSApplication sharedApplication] - nextEventMatchingMask:NSEventMaskAny - untilDate: [NSDate dateWithTimeIntervalSinceNow:1] - inMode:NSDefaultRunLoopMode - dequeue:true]; - if(can != NULL && can->Cancelled) - return; - if(ev != NULL) - [[NSApplication sharedApplication] sendEvent:ev]; - } - } - NSDebugLog(@"RunLoop exited"); - } + auto can = dynamic_cast(cancel); + if(can->Cancelled) + return S_OK; + if(_wasRunningAtLeastOnce) + return E_FAIL; + can->Running = true; + _wasRunningAtLeastOnce = true; + [NSApp run]; + return S_OK; } virtual void Signal(int priority) override diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 6a40f7187d..7919c3ac5a 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -7,6 +7,7 @@ + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index de9ca02ed1..d683092edf 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -1,21 +1,19 @@ using System; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Threading; using Avalonia; -using Avalonia.Controls; -using Avalonia.LinuxFramebuffer.Output; -using Avalonia.Skia; using Avalonia.ReactiveUI; +using Avalonia.Dialogs; namespace ControlCatalog.NetCore { static class Program { - + [STAThread] static int Main(string[] args) { - Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA); if (args.Contains("--wait-for-attach")) { Console.WriteLine("Attach debugger and use 'Set next statement'"); @@ -28,34 +26,44 @@ namespace ControlCatalog.NetCore } var builder = BuildAvaloniaApp(); + + double GetScaling() + { + var idx = Array.IndexOf(args, "--scaling"); + if (idx != 0 && args.Length > idx + 1 && + double.TryParse(args[idx + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out var scaling)) + return scaling; + return 1; + } if (args.Contains("--fbdev")) { SilenceConsole(); - return builder.StartLinuxFbDev(args); + return builder.StartLinuxFbDev(args, scaling: GetScaling()); } else if (args.Contains("--drm")) { SilenceConsole(); - return builder.StartLinuxDrm(args); + return builder.StartLinuxDrm(args, scaling: GetScaling()); } else return builder.StartWithClassicDesktopLifetime(args); } - + /// /// This method is needed for IDE previewer infrastructure /// public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() - .With(new X11PlatformOptions {EnableMultiTouch = true}) + .With(new X11PlatformOptions { EnableMultiTouch = true }) .With(new Win32PlatformOptions { EnableMultitouch = true, AllowEglInitialization = true }) .UseSkia() - .UseReactiveUI(); + .UseReactiveUI() + .UseManagedSystemDialogs(); static void SilenceConsole() { @@ -64,7 +72,8 @@ namespace ControlCatalog.NetCore Console.CursorVisible = false; while (true) Console.ReadKey(true); - }) {IsBackground = true}.Start(); + }) + { IsBackground = true }.Start(); } } } diff --git a/samples/ControlCatalog/DecoratedWindow.xaml.cs b/samples/ControlCatalog/DecoratedWindow.xaml.cs index 749f83c1ab..2e7218b956 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml.cs +++ b/samples/ControlCatalog/DecoratedWindow.xaml.cs @@ -34,7 +34,7 @@ namespace ControlCatalog SetupSide("Left", StandardCursorType.LeftSide, WindowEdge.West); SetupSide("Right", StandardCursorType.RightSide, WindowEdge.East); SetupSide("Top", StandardCursorType.TopSide, WindowEdge.North); - SetupSide("Bottom", StandardCursorType.BottomSize, WindowEdge.South); + SetupSide("Bottom", StandardCursorType.BottomSide, WindowEdge.South); SetupSide("TopLeft", StandardCursorType.TopLeftCorner, WindowEdge.NorthWest); SetupSide("TopRight", StandardCursorType.TopRightCorner, WindowEdge.NorthEast); SetupSide("BottomLeft", StandardCursorType.BottomLeftCorner, WindowEdge.SouthWest); diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 5f24c8062e..c35f8a3c0c 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -6,10 +6,13 @@ Foreground="{DynamicResource ThemeForegroundBrush}" FontSize="{DynamicResource FontSizeNormal}"> - - Light - Dark - + + + @@ -21,7 +24,12 @@ - + + + + @@ -42,6 +50,12 @@ + + + Light + Dark + + diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 6a9e865e26..9527ac3b4e 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -1,4 +1,5 @@ ItemsRepeater A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization. - + Stack - Vertical Stack - Horizontal UniformGrid - Vertical UniformGrid - Horizontal + - + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index 214de89253..1a607342f3 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -1,8 +1,11 @@ +using System; using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { @@ -16,7 +19,8 @@ namespace ControlCatalog.Pages this.InitializeComponent(); _repeater = this.FindControl("repeater"); _scroller = this.FindControl("scroller"); - DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ).ToArray(); + _repeater.PointerPressed += RepeaterClick; + DataContext = new ItemsRepeaterPageViewModel(); } private void InitializeComponent() @@ -67,5 +71,11 @@ namespace ControlCatalog.Pages break; } } + + private void RepeaterClick(object sender, PointerPressedEventArgs e) + { + var item = (e.Source as TextBlock)?.DataContext as string; + ((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item; + } } } diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 4783c8cfb8..49e9aafc4a 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -9,7 +9,20 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - + + + + + + + + + Single + Multiple + Toggle + AlwaysSelected + + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs index dbe6c74800..8a67766c76 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ReactiveUI; namespace ControlCatalog.Pages { @@ -11,9 +11,8 @@ namespace ControlCatalog.Pages { public ListBoxPage() { - this.InitializeComponent(); - DataContext = Enumerable.Range(1, 10).Select(i => $"Item {i}" ) - .ToArray(); + InitializeComponent(); + DataContext = new PageViewModel(); } private void InitializeComponent() @@ -21,5 +20,46 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } + private class PageViewModel : ReactiveObject + { + private int _counter; + private SelectionMode _selectionMode; + + public PageViewModel() + { + Items = new ObservableCollection(Enumerable.Range(1, 10).Select(i => GenerateItem())); + SelectedItems = new ObservableCollection(); + + AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); + + RemoveItemCommand = ReactiveCommand.Create(() => + { + while (SelectedItems.Count > 0) + { + Items.Remove(SelectedItems[0]); + } + }); + } + + public ObservableCollection Items { get; } + + public ObservableCollection SelectedItems { get; } + + public ReactiveCommand AddItemCommand { get; } + + public ReactiveCommand RemoveItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + SelectedItems.Clear(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + + private string GenerateItem() => $"Item {_counter++}"; + } } } diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs index a1359519e6..60e946dfbe 100644 --- a/samples/ControlCatalog/Pages/PointersPage.cs +++ b/samples/ControlCatalog/Pages/PointersPage.cs @@ -69,16 +69,25 @@ namespace ControlCatalog.Pages { UpdatePointer(e); e.Pointer.Capture(this); + e.Handled = true; base.OnPointerPressed(e); } protected override void OnPointerMoved(PointerEventArgs e) { UpdatePointer(e); + e.Handled = true; base.OnPointerMoved(e); } protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + _pointers.Remove(e.Pointer); + e.Handled = true; + InvalidateVisual(); + } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { _pointers.Remove(e.Pointer); InvalidateVisual(); diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index b9b384e8fe..13c1667ed2 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -22,7 +22,10 @@ namespace ControlCatalog.Pages public override void Render(DrawingContext context) { base.Render(context); - Window w = (Window)VisualRoot; + if (!(VisualRoot is Window w)) + { + return; + } var screens = w.Screens.All; var scaling = ((IRenderRoot)w).RenderScaling; diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index c03edb8b03..3a81e2ed02 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -6,16 +6,29 @@ Displays a hierachical tree of data. - - - - - - - + Margin="0,16,0,0" + HorizontalAlignment="Center" + Spacing="16"> + + + + + + + + + + + + + + + Single + Multiple + Toggle + AlwaysSelected + + diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs index a83f9cf43f..1f35f05f1d 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs @@ -1,8 +1,9 @@ -using System.Collections; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ReactiveUI; namespace ControlCatalog.Pages { @@ -10,8 +11,8 @@ namespace ControlCatalog.Pages { public TreeViewPage() { - this.InitializeComponent(); - DataContext = new Node().Children; + InitializeComponent(); + DataContext = new PageViewModel(); } private void InitializeComponent() @@ -19,22 +20,96 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } - public class Node + private class PageViewModel : ReactiveObject { - private IList _children; + private SelectionMode _selectionMode; + + public PageViewModel() + { + Node root = new Node(); + Items = root.Children; + SelectedItems = new ObservableCollection(); + + AddItemCommand = ReactiveCommand.Create(() => + { + Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root; + parentItem.AddNewItem(); + }); + + RemoveItemCommand = ReactiveCommand.Create(() => + { + while (SelectedItems.Count > 0) + { + Node lastItem = SelectedItems[0]; + RecursiveRemove(Items, lastItem); + SelectedItems.Remove(lastItem); + } + + bool RecursiveRemove(ObservableCollection items, Node selectedItem) + { + if (items.Remove(selectedItem)) + { + return true; + } + + foreach (Node item in items) + { + if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) + { + return true; + } + } + + return false; + } + }); + } + + public ObservableCollection Items { get; } + + public ObservableCollection SelectedItems { get; } + + public ReactiveCommand AddItemCommand { get; } + + public ReactiveCommand RemoveItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + SelectedItems.Clear(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + } + + private class Node + { + private int _counter; + private ObservableCollection _children; + public string Header { get; private set; } - public IList Children + + public bool AreChildrenInitialized => _children != null; + + public ObservableCollection Children { get { if (_children == null) { - _children = Enumerable.Range(1, 10).Select(i => new Node() {Header = $"Item {i}"}) - .ToArray(); + _children = new ObservableCollection(Enumerable.Range(1, 10).Select(i => CreateNewNode())); } return _children; } } + + public void AddNewItem() => Children.Add(CreateNewNode()); + + public override string ToString() => Header; + + private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"}; } } } diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 3513e94107..26d25a6266 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -24,7 +24,8 @@ Name="PART_ScrollViewer" HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" - Background="{TemplateBinding Background}"> + Background="{TemplateBinding Background}" + DockPanel.Dock="Left"> - - + + + + + @@ -58,6 +64,8 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs new file mode 100644 index 0000000000..b967b40c0d --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Dialogs +{ + internal class ManagedFileChooser : UserControl + { + private Control _quickLinksRoot; + private ListBox _filesView; + + public ManagedFileChooser() + { + AvaloniaXamlLoader.Load(this); + AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel); + _quickLinksRoot = this.FindControl("QuickLinks"); + _filesView = this.FindControl("Files"); + } + + ManagedFileChooserViewModel Model => DataContext as ManagedFileChooserViewModel; + + private void OnPointerPressed(object sender, PointerPressedEventArgs e) + { + var model = (e.Source as StyledElement)?.DataContext as ManagedFileChooserItemViewModel; + + if (model == null) + { + return; + } + + var isQuickLink = _quickLinksRoot.IsLogicalParentOf(e.Source as Control); + if (e.ClickCount == 2 || isQuickLink) + { + if (model.ItemType == ManagedFileChooserItemType.File) + { + Model?.SelectSingleFile(model); + } + else + { + Model?.Navigate(model.Path); + } + + e.Handled = true; + } + } + + protected override async void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + var model = (DataContext as ManagedFileChooserViewModel); + + if (model == null) + { + return; + } + + var preselected = model.SelectedItems.FirstOrDefault(); + + if (preselected == null) + { + return; + } + + //Let everything to settle down and scroll to selected item + await Task.Delay(100); + + if (preselected != model.SelectedItems.FirstOrDefault()) + { + return; + } + + // Workaround for ListBox bug, scroll to the previous file + var indexOfPreselected = model.Items.IndexOf(preselected); + + if (indexOfPreselected > 1) + { + _filesView.ScrollIntoView(model.Items[indexOfPreselected - 1]); + } + } + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs new file mode 100644 index 0000000000..a0cb664b40 --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; + +namespace Avalonia.Dialogs +{ + internal class ManagedFileChooserFilterViewModel : InternalViewModelBase + { + private readonly string[] _extensions; + public string Name { get; } + + public ManagedFileChooserFilterViewModel(FileDialogFilter filter) + { + Name = filter.Name; + + if (filter.Extensions.Contains("*")) + { + return; + } + + _extensions = filter.Extensions?.Select(e => "." + e.ToLowerInvariant()).ToArray(); + } + + public ManagedFileChooserFilterViewModel() + { + Name = "All files"; + } + + public bool Match(string filename) + { + if (_extensions == null) + { + return true; + } + + foreach (var ext in _extensions) + { + if (filename.EndsWith(ext, StringComparison.InvariantCultureIgnoreCase)) + { + return true; + } + } + + return false; + } + + public override string ToString() => Name; + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileChooserItemType.cs b/src/Avalonia.Dialogs/ManagedFileChooserItemType.cs new file mode 100644 index 0000000000..835e64a59a --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooserItemType.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Dialogs +{ + public enum ManagedFileChooserItemType + { + File, + Folder, + Volume + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileChooserItemViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserItemViewModel.cs new file mode 100644 index 0000000000..2801930028 --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooserItemViewModel.cs @@ -0,0 +1,77 @@ +using System; + +namespace Avalonia.Dialogs +{ + internal class ManagedFileChooserItemViewModel : InternalViewModelBase + { + private string _displayName; + private string _path; + private DateTime _modified; + private string _type; + private long _size; + private ManagedFileChooserItemType _itemType; + + public string DisplayName + { + get => _displayName; + set => this.RaiseAndSetIfChanged(ref _displayName, value); + } + + public string Path + { + get => _path; + set => this.RaiseAndSetIfChanged(ref _path, value); + } + + public DateTime Modified + { + get => _modified; + set => this.RaiseAndSetIfChanged(ref _modified, value); + } + + public string Type + { + get => _type; + set => this.RaiseAndSetIfChanged(ref _type, value); + } + + public long Size + { + get => _size; + set => this.RaiseAndSetIfChanged(ref _size, value); + } + + public ManagedFileChooserItemType ItemType + { + get => _itemType; + set => this.RaiseAndSetIfChanged(ref _itemType, value); + } + + public string IconKey + { + get + { + switch (ItemType) + { + case ManagedFileChooserItemType.Folder: + return "Icon_Folder"; + case ManagedFileChooserItemType.Volume: + return "Icon_Volume"; + default: + return "Icon_File"; + } + } + } + + public ManagedFileChooserItemViewModel() + { + } + + public ManagedFileChooserItemViewModel(ManagedFileChooserNavigationItem item) + { + ItemType = item.ItemType; + Path = item.Path; + DisplayName = item.DisplayName; + } + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileChooserNavigationItem.cs b/src/Avalonia.Dialogs/ManagedFileChooserNavigationItem.cs new file mode 100644 index 0000000000..8dac14bf8b --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooserNavigationItem.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Dialogs +{ + internal class ManagedFileChooserNavigationItem + { + public string DisplayName { get; set; } + public string Path { get; set; } + public ManagedFileChooserItemType ItemType { get; set; } + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs new file mode 100644 index 0000000000..0dc024c4dd --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform; +using Avalonia.Threading; + +namespace Avalonia.Dialogs +{ + internal class ManagedFileChooserSources + { + public Func GetUserDirectories { get; set; } + = DefaultGetUserDirectories; + + public Func GetFileSystemRoots { get; set; } + = DefaultGetFileSystemRoots; + + public Func GetAllItemsDelegate { get; set; } + = DefaultGetAllItems; + + public ManagedFileChooserNavigationItem[] GetAllItems() => GetAllItemsDelegate(this); + public static readonly ObservableCollection MountedVolumes = new ObservableCollection(); + + public static ManagedFileChooserNavigationItem[] DefaultGetAllItems(ManagedFileChooserSources sources) + { + return sources.GetUserDirectories().Concat(sources.GetFileSystemRoots()).ToArray(); + } + + private static Environment.SpecialFolder[] s_folders = new[] + { + Environment.SpecialFolder.Desktop, + Environment.SpecialFolder.UserProfile, + Environment.SpecialFolder.MyDocuments, + Environment.SpecialFolder.MyMusic, + Environment.SpecialFolder.MyPictures, + Environment.SpecialFolder.MyVideos + }; + + public static ManagedFileChooserNavigationItem[] DefaultGetUserDirectories() + { + return s_folders.Select(Environment.GetFolderPath).Distinct() + .Where(d => !string.IsNullOrWhiteSpace(d)) + .Where(Directory.Exists) + .Select(d => new ManagedFileChooserNavigationItem + { + ItemType = ManagedFileChooserItemType.Folder, + Path = d, + DisplayName = Path.GetFileName(d) + }).ToArray(); + } + + public static ManagedFileChooserNavigationItem[] DefaultGetFileSystemRoots() + { + return MountedVolumes + .Select(x => + { + var displayName = x.VolumeLabel; + + if (displayName == null & x.VolumeSizeBytes > 0) + { + displayName = $"{ByteSizeHelper.ToString(x.VolumeSizeBytes)} Volume"; + }; + + try + { + Directory.GetFiles(x.VolumePath); + } + catch (UnauthorizedAccessException _) + { + return null; + } + + return new ManagedFileChooserNavigationItem + { + ItemType = ManagedFileChooserItemType.Volume, + DisplayName = displayName, + Path = x.VolumePath + }; + }) + .Where(x => x != null) + .ToArray(); + } + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs new file mode 100644 index 0000000000..03e05e7a75 --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs @@ -0,0 +1,368 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Runtime.InteropServices; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Threading; + +namespace Avalonia.Dialogs +{ + internal class ManagedFileChooserViewModel : InternalViewModelBase + { + public event Action CancelRequested; + public event Action CompleteRequested; + + public AvaloniaList QuickLinks { get; } = + new AvaloniaList(); + + public AvaloniaList Items { get; } = + new AvaloniaList(); + + public AvaloniaList Filters { get; } = + new AvaloniaList(); + + public AvaloniaList SelectedItems { get; } = + new AvaloniaList(); + + string _location; + string _fileName; + private bool _showHiddenFiles; + private ManagedFileChooserFilterViewModel _selectedFilter; + private bool _selectingDirectory; + private bool _savingFile; + private bool _scheduledSelectionValidation; + private bool _alreadyCancelled = false; + private string _defaultExtension; + private CompositeDisposable _disposables; + + public string Location + { + get => _location; + private set => this.RaiseAndSetIfChanged(ref _location, value); + } + + public string FileName + { + get => _fileName; + private set => this.RaiseAndSetIfChanged(ref _fileName, value); + } + + public bool SelectingFolder => _selectingDirectory; + + public bool ShowFilters { get; } + public SelectionMode SelectionMode { get; } + public string Title { get; } + + public int QuickLinksSelectedIndex + { + get + { + for (var index = 0; index < QuickLinks.Count; index++) + { + var i = QuickLinks[index]; + + if (i.Path == Location) + { + return index; + } + } + + return -1; + } + set => this.RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); + } + + public ManagedFileChooserFilterViewModel SelectedFilter + { + get => _selectedFilter; + set + { + this.RaiseAndSetIfChanged(ref _selectedFilter, value); + Refresh(); + } + } + + public bool ShowHiddenFiles + { + get => _showHiddenFiles; + set + { + this.RaiseAndSetIfChanged(ref _showHiddenFiles, value); + Refresh(); + } + } + + private void RefreshQuickLinks(ManagedFileChooserSources quickSources) + { + QuickLinks.Clear(); + QuickLinks.AddRange(quickSources.GetAllItems().Select(i => new ManagedFileChooserItemViewModel(i))); + } + + public ManagedFileChooserViewModel(FileSystemDialog dialog) + { + _disposables = new CompositeDisposable(); + + var quickSources = AvaloniaLocator.Current + .GetService() + ?? new ManagedFileChooserSources(); + + var sub1 = AvaloniaLocator.Current + .GetService() + .Listen(ManagedFileChooserSources.MountedVolumes); + + var sub2 = Observable.FromEventPattern(ManagedFileChooserSources.MountedVolumes, + nameof(ManagedFileChooserSources.MountedVolumes.CollectionChanged)) + .ObserveOn(AvaloniaScheduler.Instance) + .Subscribe(x => RefreshQuickLinks(quickSources)); + + _disposables.Add(sub1); + _disposables.Add(sub2); + + CompleteRequested += delegate { _disposables?.Dispose(); }; + CancelRequested += delegate { _disposables?.Dispose(); }; + + RefreshQuickLinks(quickSources); + + Title = dialog.Title ?? ( + dialog is OpenFileDialog ? "Open file" + : dialog is SaveFileDialog ? "Save file" + : dialog is OpenFolderDialog ? "Select directory" + : throw new ArgumentException(nameof(dialog))); + + var directory = dialog.InitialDirectory; + + if (directory == null || !Directory.Exists(directory)) + { + directory = Directory.GetCurrentDirectory(); + } + + if (dialog is FileDialog fd) + { + if (fd.Filters?.Count > 0) + { + Filters.AddRange(fd.Filters.Select(f => new ManagedFileChooserFilterViewModel(f))); + _selectedFilter = Filters[0]; + ShowFilters = true; + } + + if (dialog is OpenFileDialog ofd) + { + if (ofd.AllowMultiple) + { + SelectionMode = SelectionMode.Multiple; + } + } + } + + _selectingDirectory = dialog is OpenFolderDialog; + + if (dialog is SaveFileDialog sfd) + { + _savingFile = true; + _defaultExtension = sfd.DefaultExtension; + FileName = sfd.InitialFileName; + } + + Navigate(directory, (dialog as FileDialog)?.InitialFileName); + SelectedItems.CollectionChanged += OnSelectionChangedAsync; + } + + public void EnterPressed() + { + if (Directory.Exists(Location)) + { + Navigate(Location); + } + else if (File.Exists(Location)) + { + CompleteRequested?.Invoke(new[] { Location }); + } + } + + private async void OnSelectionChangedAsync(object sender, NotifyCollectionChangedEventArgs e) + { + if (_scheduledSelectionValidation) + { + return; + } + + _scheduledSelectionValidation = true; + await Dispatcher.UIThread.InvokeAsync(() => + { + try + { + if (_selectingDirectory) + { + SelectedItems.Clear(); + } + else + { + var invalidItems = SelectedItems.Where(i => i.ItemType == ManagedFileChooserItemType.Folder).ToList(); + foreach (var item in invalidItems) + { + SelectedItems.Remove(item); + } + + if (!_selectingDirectory) + { + FileName = SelectedItems.FirstOrDefault()?.DisplayName; + } + } + } + finally + { + _scheduledSelectionValidation = false; + } + }); + } + + void NavigateRoot(string initialSelectionName) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Navigate(Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.System)), initialSelectionName); + } + else + { + Navigate("/", initialSelectionName); + } + } + + public void Refresh() => Navigate(Location); + + public void Navigate(string path, string initialSelectionName = null) + { + if (!Directory.Exists(path)) + { + NavigateRoot(initialSelectionName); + } + else + { + Location = path; + Items.Clear(); + SelectedItems.Clear(); + + try + { + var infos = new DirectoryInfo(path).EnumerateFileSystemInfos(); + + if (!ShowHiddenFiles) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + infos = infos.Where(i => (i.Attributes & (FileAttributes.Hidden | FileAttributes.System)) == 0); + } + else + { + infos = infos.Where(i => !i.Name.StartsWith(".")); + } + } + + if (SelectedFilter != null) + { + infos = infos.Where(i => i is DirectoryInfo || SelectedFilter.Match(i.Name)); + } + + Items.AddRange(infos.Where(x => + { + if (_selectingDirectory) + { + if (!(x is DirectoryInfo)) + { + return false; + } + } + + return true; + }) + .Where(x => x.Exists) + .Select(info => new ManagedFileChooserItemViewModel + { + DisplayName = info.Name, + Path = info.FullName, + Type = info is FileInfo ? info.Extension : "File Folder", + ItemType = info is FileInfo ? ManagedFileChooserItemType.File + : ManagedFileChooserItemType.Folder, + Size = info is FileInfo f ? f.Length : 0, + Modified = info.LastWriteTime + }) + .OrderByDescending(x => x.ItemType == ManagedFileChooserItemType.Folder) + .ThenBy(x => x.DisplayName, StringComparer.InvariantCultureIgnoreCase)); + + if (initialSelectionName != null) + { + var sel = Items.FirstOrDefault(i => i.ItemType == ManagedFileChooserItemType.File && i.DisplayName == initialSelectionName); + + if (sel != null) + { + SelectedItems.Add(sel); + } + } + + this.RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); + } + catch (System.UnauthorizedAccessException) + { + } + } + } + + public void GoUp() + { + var parent = Path.GetDirectoryName(Location); + + if (string.IsNullOrWhiteSpace(parent)) + { + return; + } + + Navigate(parent); + } + + public void Cancel() + { + if (!_alreadyCancelled) + { + // INFO: Don't misplace this check or it might cause + // StackOverflowException because of recursive + // event invokes. + _alreadyCancelled = true; + CancelRequested?.Invoke(); + } + } + + public void Ok() + { + if (_selectingDirectory) + { + CompleteRequested?.Invoke(new[] { Location }); + } + else if (_savingFile) + { + if (!string.IsNullOrWhiteSpace(FileName)) + { + if (!Path.HasExtension(FileName) && !string.IsNullOrWhiteSpace(_defaultExtension)) + { + FileName = Path.ChangeExtension(FileName, _defaultExtension); + } + + CompleteRequested?.Invoke(new[] { Path.Combine(Location, FileName) }); + } + } + else + { + CompleteRequested?.Invoke(SelectedItems.Select(i => i.Path).ToArray()); + } + } + + public void SelectSingleFile(ManagedFileChooserItemViewModel item) + { + CompleteRequested?.Invoke(new[] { item.Path }); + } + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs new file mode 100644 index 0000000000..771d2b1b5e --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Dialogs; +using Avalonia.Platform; + +namespace Avalonia.Dialogs +{ + public static class ManagedFileDialogExtensions + { + class ManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new() + { + async Task Show(SystemDialog d, IWindowImpl parent) + { + var model = new ManagedFileChooserViewModel((FileSystemDialog)d); + + var dialog = new T + { + Content = new ManagedFileChooser(), + DataContext = model + }; + + dialog.Closed += delegate { model.Cancel(); }; + + string[] result = null; + + model.CompleteRequested += items => + { + result = items; + dialog.Close(); + }; + + model.CancelRequested += dialog.Close; + + await dialog.ShowDialog(parent); + return result; + } + + public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + { + return await Show(dialog, parent); + } + + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + return (await Show(dialog, parent))?.FirstOrDefault(); + } + } + + public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) + where TAppBuilder : AppBuilderBase, new() + { + builder.AfterSetup(_ => + AvaloniaLocator.CurrentMutable.Bind().ToSingleton>()); + return builder; + } + + public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) + where TAppBuilder : AppBuilderBase, new() where TWindow : Window, new() + { + builder.AfterSetup(_ => + AvaloniaLocator.CurrentMutable.Bind().ToSingleton>()); + return builder; + } + } +} diff --git a/src/Avalonia.Dialogs/ResourceSelectorConverter.cs b/src/Avalonia.Dialogs/ResourceSelectorConverter.cs new file mode 100644 index 0000000000..9d8b6cb1c7 --- /dev/null +++ b/src/Avalonia.Dialogs/ResourceSelectorConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data.Converters; + +namespace Avalonia.Dialogs +{ + internal class ResourceSelectorConverter : ResourceDictionary, IValueConverter + { + public object Convert(object key, Type targetType, object parameter, CultureInfo culture) + { + TryGetResource((string)key, out var value); + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj new file mode 100644 index 0000000000..d7e1d8cdb3 --- /dev/null +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs new file mode 100644 index 0000000000..8081528e55 --- /dev/null +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Avalonia.Controls.Platform; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Text.RegularExpressions; +using System.Runtime.InteropServices; +using System.Text; + +namespace Avalonia.FreeDesktop +{ + internal class LinuxMountedVolumeInfoListener : IDisposable + { + private const string DevByLabelDir = "/dev/disk/by-label/"; + private const string ProcPartitionsDir = "/proc/partitions"; + private const string ProcMountsDir = "/proc/mounts"; + private CompositeDisposable _disposables; + private ObservableCollection _targetObs; + private bool _beenDisposed = false; + + public LinuxMountedVolumeInfoListener(ref ObservableCollection target) + { + _disposables = new CompositeDisposable(); + this._targetObs = target; + + var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) + .Subscribe(Poll); + + _disposables.Add(pollTimer); + + Poll(0); + } + + private string GetSymlinkTarget(string x) => Path.GetFullPath(Path.Combine(DevByLabelDir, NativeMethods.ReadLink(x))); + + private void Poll(long _) + { + var fProcPartitions = File.ReadAllLines(ProcPartitionsDir) + .Skip(1) + .Where(p => !string.IsNullOrEmpty(p)) + .Select(p => Regex.Replace(p, @"\s{2,}", " ").Trim().Split(' ')) + .Select(p => (p[2].Trim(), p[3].Trim())) + .Select(p => (Convert.ToUInt64(p.Item1) * 1024, "/dev/" + p.Item2)); + + var fProcMounts = File.ReadAllLines(ProcMountsDir) + .Select(x => x.Split(' ')) + .Select(x => (x[0], x[1])); + + var labelDirEnum = Directory.Exists(DevByLabelDir) ? + new DirectoryInfo(DevByLabelDir).GetFiles() : Enumerable.Empty(); + + var labelDevPathPairs = labelDirEnum + .Select(x => (GetSymlinkTarget(x.FullName), x.Name)); + + var q1 = from mount in fProcMounts + join device in fProcPartitions on mount.Item1 equals device.Item2 + join label in labelDevPathPairs on device.Item2 equals label.Item1 into labelMatches + from x in labelMatches.DefaultIfEmpty() + select new MountedVolumeInfo() + { + VolumePath = mount.Item2, + VolumeSizeBytes = device.Item1, + VolumeLabel = x.Name + }; + + var mountVolInfos = q1.ToArray(); + + if (_targetObs.SequenceEqual(mountVolInfos)) + return; + else + { + _targetObs.Clear(); + + foreach (var i in mountVolInfos) + _targetObs.Add(i); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_beenDisposed) + { + if (disposing) + { + _disposables.Dispose(); + _targetObs.Clear(); + } + + _beenDisposed = true; + } + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs new file mode 100644 index 0000000000..d68c02bfd6 --- /dev/null +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.ObjectModel; + +using Avalonia.Controls.Platform; + +namespace Avalonia.FreeDesktop +{ + public class LinuxMountedVolumeInfoProvider : IMountedVolumeInfoProvider + { + public IDisposable Listen(ObservableCollection mountedDrives) + { + Contract.Requires(mountedDrives != null); + return new LinuxMountedVolumeInfoListener(ref mountedDrives); + } + } +} diff --git a/src/Avalonia.FreeDesktop/NativeMethods.cs b/src/Avalonia.FreeDesktop/NativeMethods.cs new file mode 100644 index 0000000000..d9b6dce082 --- /dev/null +++ b/src/Avalonia.FreeDesktop/NativeMethods.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Avalonia.Controls.Platform; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Text.RegularExpressions; +using System.Runtime.InteropServices; +using System.Text; + +namespace Avalonia.FreeDesktop +{ + internal static class NativeMethods + { + [DllImport("libc", SetLastError = true)] + private static extern long readlink([MarshalAs(UnmanagedType.LPArray)] byte[] filename, + [MarshalAs(UnmanagedType.LPArray)] byte[] buffer, + long len); + + public static string ReadLink(string path) + { + var symlink = Encoding.UTF8.GetBytes(path); + var result = new byte[4095]; + readlink(symlink, result, result.Length); + var rawstr = Encoding.UTF8.GetString(result); + return rawstr.Substring(0, rawstr.IndexOf('\0')); + } + } +} diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursors.cs index 8139af1659..f72ccf1850 100644 --- a/src/Avalonia.Input/Cursors.cs +++ b/src/Avalonia.Input/Cursors.cs @@ -28,7 +28,7 @@ namespace Avalonia.Input AppStarting, Help, TopSide, - BottomSize, + BottomSide, LeftSide, RightSide, TopLeftCorner, @@ -40,6 +40,9 @@ namespace Avalonia.Input DragLink, None, + [Obsolete("Use BottomSide")] + BottomSize = BottomSide + // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image // SizeNorthWestSouthEast, diff --git a/src/Avalonia.Input/DragDrop.cs b/src/Avalonia.Input/DragDrop.cs index c58b764b1d..d39659cee3 100644 --- a/src/Avalonia.Input/DragDrop.cs +++ b/src/Avalonia.Input/DragDrop.cs @@ -45,10 +45,10 @@ namespace Avalonia.Input /// Starts a dragging operation with the given and returns the applied drop effect from the target. /// /// - public static Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects) + public static Task DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects) { var src = AvaloniaLocator.Current.GetService(); - return src?.DoDragDrop(data, allowedEffects) ?? Task.FromResult(DragDropEffects.None); + return src?.DoDragDrop(triggerEvent, data, allowedEffects) ?? Task.FromResult(DragDropEffects.None); } } } diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 02dda45e99..bb8c8b8c40 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -31,7 +31,7 @@ namespace Avalonia.Input RoutedEvent.Register( "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); - private static WeakReference s_lastPress; + private static WeakReference s_lastPress; static Gestures() { @@ -47,11 +47,11 @@ namespace Avalonia.Input if (e.ClickCount <= 1) { - s_lastPress = new WeakReference(e.Source); + s_lastPress = new WeakReference(e.Source); } - else if (s_lastPress?.IsAlive == true && e.ClickCount == 2 && s_lastPress.Target == e.Source) + else if (s_lastPress != null && e.ClickCount == 2 && e.MouseButton != MouseButton.Right) { - if (e.MouseButton != MouseButton.Right) + if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); } @@ -65,10 +65,10 @@ namespace Avalonia.Input { var e = (PointerReleasedEventArgs)ev; - if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source) + if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { var et = e.MouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent; - ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(et)); + e.Source.RaiseEvent(new RoutedEventArgs(et)); } } } diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index 1410476267..1a82f7d671 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -6,7 +6,7 @@ using System.ComponentModel; namespace Avalonia.Input { - [Flags] + [Flags, Obsolete("Use KeyModifiers and PointerPointProperties")] public enum InputModifiers { None = 0, @@ -19,6 +19,16 @@ namespace Avalonia.Input MiddleMouseButton = 64 } + [Flags] + public enum KeyModifiers + { + None = 0, + Alt = 1, + Control = 2, + Shift = 4, + Meta = 8, + } + [Flags] public enum KeyStates { @@ -27,6 +37,26 @@ namespace Avalonia.Input Toggled = 2, } + [Flags] + public enum RawInputModifiers + { + None = 0, + Alt = 1, + Control = 2, + Shift = 4, + Meta = 8, + LeftMouseButton = 16, + RightMouseButton = 32, + MiddleMouseButton = 64, + KeyboardMask = Alt | Control | Shift | Meta + } + + internal static class KeyModifiersUtils + { + public static KeyModifiers ConvertToKey(RawInputModifiers modifiers) => + (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); + } + public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged { IInputElement FocusedElement { get; } diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs index 7e6bf657ae..a641544f7a 100644 --- a/src/Avalonia.Input/IMouseDevice.cs +++ b/src/Avalonia.Input/IMouseDevice.cs @@ -16,6 +16,8 @@ namespace Avalonia.Input [Obsolete("Use PointerEventArgs.GetPosition")] PixelPoint Position { get; } + void TopLevelClosed(IInputRoot root); + void SceneInvalidated(IInputRoot root, Rect rect); } } diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index c1d0729560..f83c41e266 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -13,6 +13,8 @@ namespace Avalonia.Input /// public static class InputExtensions { + private static readonly Func s_hitTestDelegate = IsHitTestVisible; + /// /// Returns the active input elements at a point on an . /// @@ -25,7 +27,7 @@ namespace Avalonia.Input { Contract.Requires(element != null); - return element.GetVisualsAt(p, IsHitTestVisible).Cast(); + return element.GetVisualsAt(p, s_hitTestDelegate).Cast(); } /// diff --git a/src/Avalonia.Input/KeyEventArgs.cs b/src/Avalonia.Input/KeyEventArgs.cs index 89053c6f61..8aef81503f 100644 --- a/src/Avalonia.Input/KeyEventArgs.cs +++ b/src/Avalonia.Input/KeyEventArgs.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using Avalonia.Interactivity; namespace Avalonia.Input @@ -11,6 +12,8 @@ namespace Avalonia.Input public Key Key { get; set; } - public InputModifiers Modifiers { get; set; } + [Obsolete("Use KeyModifiers")] + public InputModifiers Modifiers => (InputModifiers)KeyModifiers; + public KeyModifiers KeyModifiers { get; set; } } } diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index c945902def..2377edf640 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/src/Avalonia.Input/KeyGesture.cs @@ -51,7 +51,14 @@ namespace Avalonia.Input public Key Key { get; set; } - public InputModifiers Modifiers { get; set; } + [Obsolete("Use KeyModifiers")] + public InputModifiers Modifiers + { + get => (InputModifiers)KeyModifiers; + set => KeyModifiers = (KeyModifiers)(((int)value) & 0xf); + } + + public KeyModifiers KeyModifiers { get; set; } static readonly Dictionary KeySynonyms = new Dictionary diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 00606bd9b1..a02c580ad2 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -91,7 +91,7 @@ namespace Avalonia.Input RoutedEvent = routedEvent, Device = this, Key = keyInput.Key, - Modifiers = keyInput.Modifiers, + KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers), Source = element, }; diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index ee7d0c9501..0d5471f790 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -86,6 +86,11 @@ namespace Avalonia.Input ProcessRawEvent(margs); } + public void TopLevelClosed(IInputRoot root) + { + ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); + } + public void SceneInvalidated(IInputRoot root, Rect rect) { var clientPoint = root.PointToClient(Position); @@ -94,11 +99,13 @@ namespace Avalonia.Input { if (_pointer.Captured == null) { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, InputModifiers.None); + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, + PointerPointProperties.None, KeyModifiers.None); } else { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, InputModifiers.None); + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, + PointerPointProperties.None, KeyModifiers.None); } } } @@ -123,69 +130,73 @@ namespace Avalonia.Input Position = e.Root.PointToScreen(e.Position); var props = CreateProperties(e); + var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); switch (e.Type) { case RawPointerEventType.LeaveWindow: - LeaveWindow(mouse, e.Timestamp, e.Root, e.InputModifiers); + LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); break; case RawPointerEventType.LeftButtonDown: case RawPointerEventType.RightButtonDown: case RawPointerEventType.MiddleButtonDown: if (ButtonCount(props) > 1) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers); + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); else e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, - props, e.InputModifiers); + props, keyModifiers); break; case RawPointerEventType.LeftButtonUp: case RawPointerEventType.RightButtonUp: case RawPointerEventType.MiddleButtonUp: if (ButtonCount(props) != 0) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers); + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); else - e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers); + e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; case RawPointerEventType.Move: - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers); + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; case RawPointerEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers); + e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); break; } } - private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers) + private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, + KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); - ClearPointerOver(this, timestamp, root, inputModifiers); + ClearPointerOver(this, timestamp, root, properties, inputModifiers); } PointerPointProperties CreateProperties(RawPointerEventArgs args) { - var rv = new PointerPointProperties(args.InputModifiers); + + var kind = PointerUpdateKind.Other; if (args.Type == RawPointerEventType.LeftButtonDown) - rv.IsLeftButtonPressed = true; + kind = PointerUpdateKind.LeftButtonPressed; if (args.Type == RawPointerEventType.MiddleButtonDown) - rv.IsMiddleButtonPressed = true; + kind = PointerUpdateKind.MiddleButtonPressed; if (args.Type == RawPointerEventType.RightButtonDown) - rv.IsRightButtonPressed = true; + kind = PointerUpdateKind.RightButtonPressed; if (args.Type == RawPointerEventType.LeftButtonUp) - rv.IsLeftButtonPressed = false; + kind = PointerUpdateKind.LeftButtonReleased; if (args.Type == RawPointerEventType.MiddleButtonUp) - rv.IsMiddleButtonPressed = false; + kind = PointerUpdateKind.MiddleButtonReleased; if (args.Type == RawPointerEventType.RightButtonUp) - rv.IsRightButtonPressed = false; - return rv; + kind = PointerUpdateKind.RightButtonReleased; + + return new PointerPointProperties(args.InputModifiers, kind); } private MouseButton _lastMouseDownButton; private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, PointerPointProperties properties, - InputModifiers inputModifiers) + KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -221,7 +232,7 @@ namespace Avalonia.Input } private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - InputModifiers inputModifiers) + KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -230,11 +241,11 @@ namespace Avalonia.Input if (_pointer.Captured == null) { - source = SetPointerOver(this, timestamp, root, p, inputModifiers); + source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); } else { - SetPointerOver(this, timestamp, root, _pointer.Captured, inputModifiers); + SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); source = _pointer.Captured; } @@ -246,7 +257,7 @@ namespace Avalonia.Input } private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, - InputModifiers inputModifiers) + KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -256,8 +267,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, - _lastMouseDownButton); + var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers); source?.RaiseEvent(e); _pointer.Capture(null); @@ -269,7 +279,7 @@ namespace Avalonia.Input private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, - Vector delta, InputModifiers inputModifiers) + Vector delta, KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -304,19 +314,23 @@ namespace Avalonia.Input return _pointer.Captured ?? root.InputHitTest(p); } - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source, InputModifiers inputModifiers) + PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source, + PointerPointProperties properties, + KeyModifiers inputModifiers) { return new PointerEventArgs(ev, source, _pointer, null, default, - timestamp, new PointerPointProperties(inputModifiers), inputModifiers); + timestamp, properties, inputModifiers); } - private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers) + private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, + PointerPointProperties properties, + KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var element = root.PointerOverElement; - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, inputModifiers); + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); if (element!=null && !element.IsAttachedToVisualTree) { @@ -353,7 +367,9 @@ namespace Avalonia.Input } } - private IInputElement SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, InputModifiers inputModifiers) + private IInputElement SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties properties, + KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -364,18 +380,20 @@ namespace Avalonia.Input { if (element != null) { - SetPointerOver(device, timestamp, root, element, inputModifiers); + SetPointerOver(device, timestamp, root, element, properties, inputModifiers); } else { - ClearPointerOver(device, timestamp, root, inputModifiers); + ClearPointerOver(device, timestamp, root, properties, inputModifiers); } } return element; } - private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, InputModifiers inputModifiers) + private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, + PointerPointProperties properties, + KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -397,7 +415,7 @@ namespace Avalonia.Input el = root.PointerOverElement; - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, inputModifiers); + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); if (el!=null && branch!=null && !el.IsAttachedToVisualTree) { ClearChildrenPointerOver(e,branch,false); diff --git a/src/Avalonia.Input/Platform/IPlatformDragSource.cs b/src/Avalonia.Input/Platform/IPlatformDragSource.cs index bfe69f3a90..30d8ee5337 100644 --- a/src/Avalonia.Input/Platform/IPlatformDragSource.cs +++ b/src/Avalonia.Input/Platform/IPlatformDragSource.cs @@ -4,6 +4,6 @@ namespace Avalonia.Input.Platform { public interface IPlatformDragSource { - Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects); + Task DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects); } } diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index c827822192..5b3c43e4df 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -20,7 +20,7 @@ namespace Avalonia.Input IVisual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, - InputModifiers modifiers) + KeyModifiers modifiers) : base(routedEvent) { Source = source; @@ -29,7 +29,7 @@ namespace Avalonia.Input _properties = properties; Pointer = pointer; Timestamp = timestamp; - InputModifiers = modifiers; + KeyModifiers = modifiers; } class EmulatedDevice : IPointerDevice @@ -60,7 +60,24 @@ namespace Avalonia.Input [Obsolete("Use Pointer to get pointer-specific information")] public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this)); - public InputModifiers InputModifiers { get; } + [Obsolete("Use KeyModifiers and PointerPointProperties")] + public InputModifiers InputModifiers + { + get + { + var mods = (InputModifiers)KeyModifiers; + if (_properties.IsLeftButtonPressed) + mods |= InputModifiers.LeftMouseButton; + if (_properties.IsMiddleButtonPressed) + mods |= InputModifiers.MiddleMouseButton; + if (_properties.IsRightButtonPressed) + mods |= InputModifiers.RightMouseButton; + + return mods; + } + } + + public KeyModifiers KeyModifiers { get; } public Point GetPosition(IVisual relativeTo) { @@ -73,6 +90,8 @@ namespace Avalonia.Input public PointerPoint GetPointerPoint(IVisual relativeTo) => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); + + protected PointerPointProperties Properties => _properties; } public enum MouseButton @@ -93,7 +112,7 @@ namespace Avalonia.Input IVisual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, - InputModifiers modifiers, + KeyModifiers modifiers, int obsoleteClickCount = 1) : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) @@ -104,7 +123,8 @@ namespace Avalonia.Input [Obsolete("Use DoubleTapped or DoubleRightTapped event instead")] public int ClickCount => _obsoleteClickCount; - [Obsolete] public MouseButton MouseButton => GetPointerPoint(null).Properties.GetObsoleteMouseButton(); + [Obsolete("Use PointerUpdateKind")] + public MouseButton MouseButton => Properties.GetObsoleteMouseButton(); } public class PointerReleasedEventArgs : PointerEventArgs @@ -112,15 +132,15 @@ namespace Avalonia.Input public PointerReleasedEventArgs( IInteractive source, IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp, - PointerPointProperties properties, InputModifiers modifiers, MouseButton obsoleteMouseButton) + PointerPointProperties properties, KeyModifiers modifiers) : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) { - MouseButton = obsoleteMouseButton; + } - [Obsolete()] - public MouseButton MouseButton { get; private set; } + [Obsolete("Use PointerUpdateKind")] + public MouseButton MouseButton => Properties.GetObsoleteMouseButton(); } public class PointerCaptureLostEventArgs : RoutedEventArgs diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index 7117b5709c..d823a78090 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -15,31 +15,61 @@ namespace Avalonia.Input public sealed class PointerPointProperties { - public bool IsLeftButtonPressed { get; set; } - public bool IsMiddleButtonPressed { get; set; } - public bool IsRightButtonPressed { get; set; } - - public PointerPointProperties() + public bool IsLeftButtonPressed { get; } + public bool IsMiddleButtonPressed { get; } + public bool IsRightButtonPressed { get; } + public PointerUpdateKind PointerUpdateKind { get; } + private PointerPointProperties() { } - public PointerPointProperties(InputModifiers modifiers) + public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind) { - IsLeftButtonPressed = modifiers.HasFlag(InputModifiers.LeftMouseButton); - IsMiddleButtonPressed = modifiers.HasFlag(InputModifiers.MiddleMouseButton); - IsRightButtonPressed = modifiers.HasFlag(InputModifiers.RightMouseButton); + PointerUpdateKind = kind; + IsLeftButtonPressed = modifiers.HasFlag(RawInputModifiers.LeftMouseButton); + IsMiddleButtonPressed = modifiers.HasFlag(RawInputModifiers.MiddleMouseButton); + IsRightButtonPressed = modifiers.HasFlag(RawInputModifiers.RightMouseButton); + + // The underlying input source might be reporting the previous state, + // so make sure that we reflect the current state + + if (kind == PointerUpdateKind.LeftButtonPressed) + IsLeftButtonPressed = true; + if (kind == PointerUpdateKind.LeftButtonReleased) + IsLeftButtonPressed = false; + if (kind == PointerUpdateKind.MiddleButtonPressed) + IsMiddleButtonPressed = true; + if (kind == PointerUpdateKind.MiddleButtonReleased) + IsMiddleButtonPressed = false; + if (kind == PointerUpdateKind.RightButtonPressed) + IsRightButtonPressed = true; + if (kind == PointerUpdateKind.RightButtonReleased) + IsRightButtonPressed = false; } + + public static PointerPointProperties None { get; } = new PointerPointProperties(); public MouseButton GetObsoleteMouseButton() { - if (IsLeftButtonPressed) + if (PointerUpdateKind == PointerUpdateKind.LeftButtonPressed || PointerUpdateKind == PointerUpdateKind.LeftButtonReleased) return MouseButton.Left; - if (IsMiddleButtonPressed) + if (PointerUpdateKind == PointerUpdateKind.MiddleButtonPressed || PointerUpdateKind == PointerUpdateKind.MiddleButtonReleased) return MouseButton.Middle; - if (IsRightButtonPressed) + if (PointerUpdateKind == PointerUpdateKind.RightButtonPressed || PointerUpdateKind == PointerUpdateKind.RightButtonReleased) return MouseButton.Right; return MouseButton.None; } } + + public enum PointerUpdateKind + { + LeftButtonPressed, + MiddleButtonPressed, + RightButtonPressed, + LeftButtonReleased, + MiddleButtonReleased, + RightButtonReleased, + Other + } } diff --git a/src/Avalonia.Input/PointerWheelEventArgs.cs b/src/Avalonia.Input/PointerWheelEventArgs.cs index de1badfe96..3d7f93ddde 100644 --- a/src/Avalonia.Input/PointerWheelEventArgs.cs +++ b/src/Avalonia.Input/PointerWheelEventArgs.cs @@ -12,7 +12,7 @@ namespace Avalonia.Input public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp, - PointerPointProperties properties, InputModifiers modifiers, Vector delta) + PointerPointProperties properties, KeyModifiers modifiers, Vector delta) : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) { diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs index 208d898a8a..1480f92f03 100644 --- a/src/Avalonia.Input/Raw/RawDragEvent.cs +++ b/src/Avalonia.Input/Raw/RawDragEvent.cs @@ -10,7 +10,7 @@ public InputModifiers Modifiers { get; } public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, - IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) :base(inputDevice, 0) { Type = type; @@ -18,7 +18,7 @@ Location = location; Data = data; Effects = effects; - Modifiers = modifiers; + Modifiers = (InputModifiers)modifiers; } } } diff --git a/src/Avalonia.Input/Raw/RawKeyEventArgs.cs b/src/Avalonia.Input/Raw/RawKeyEventArgs.cs index 044f244138..cd8b2eacf7 100644 --- a/src/Avalonia.Input/Raw/RawKeyEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawKeyEventArgs.cs @@ -15,7 +15,7 @@ namespace Avalonia.Input.Raw IKeyboardDevice device, ulong timestamp, RawKeyEventType type, - Key key, InputModifiers modifiers) + Key key, RawInputModifiers modifiers) : base(device, timestamp) { Key = key; @@ -25,7 +25,7 @@ namespace Avalonia.Input.Raw public Key Key { get; set; } - public InputModifiers Modifiers { get; set; } + public RawInputModifiers Modifiers { get; set; } public RawKeyEventType Type { get; set; } } diff --git a/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs b/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs index 186ad99efc..516234de7e 100644 --- a/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs @@ -11,7 +11,7 @@ namespace Avalonia.Input.Raw ulong timestamp, IInputRoot root, Point position, - Vector delta, InputModifiers inputModifiers) + Vector delta, RawInputModifiers inputModifiers) : base(device, timestamp, root, RawPointerEventType.Wheel, position, inputModifiers) { Delta = delta; diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 6fac90f255..88f6daf11f 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -43,7 +43,7 @@ namespace Avalonia.Input.Raw IInputRoot root, RawPointerEventType type, Point position, - InputModifiers inputModifiers) + RawInputModifiers inputModifiers) : base(device, timestamp) { Contract.Requires(device != null); @@ -73,6 +73,6 @@ namespace Avalonia.Input.Raw /// /// Gets the input modifiers. /// - public InputModifiers InputModifiers { get; private set; } + public RawInputModifiers InputModifiers { get; private set; } } } diff --git a/src/Avalonia.Input/Raw/RawTouchEventArgs.cs b/src/Avalonia.Input/Raw/RawTouchEventArgs.cs index 5299633b26..020b40e55b 100644 --- a/src/Avalonia.Input/Raw/RawTouchEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawTouchEventArgs.cs @@ -3,7 +3,7 @@ namespace Avalonia.Input.Raw public class RawTouchEventArgs : RawPointerEventArgs { public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root, - RawPointerEventType type, Point position, InputModifiers inputModifiers, + RawPointerEventType type, Point position, RawInputModifiers inputModifiers, long touchPointId) : base(device, timestamp, root, type, position, inputModifiers) { diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index c85f98b04a..765e02848f 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -15,14 +15,15 @@ namespace Avalonia.Input { Dictionary _pointers = new Dictionary(); - static InputModifiers GetModifiers(InputModifiers modifiers, bool left) + KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) => + (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); + + RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown) { - var mask = (InputModifiers)0x7fffffff ^ InputModifiers.LeftMouseButton ^ InputModifiers.MiddleMouseButton ^ - InputModifiers.RightMouseButton; - modifiers &= mask; - if (left) - modifiers |= InputModifiers.LeftMouseButton; - return modifiers; + var rv = modifiers &= RawInputModifiers.KeyboardMask; + if (isLeftButtonDown) + rv |= RawInputModifiers.LeftMouseButton; + return rv; } public void ProcessRawEvent(RawInputEventArgs ev) @@ -45,8 +46,9 @@ namespace Avalonia.Input { target.RaiseEvent(new PointerPressedEventArgs(target, pointer, args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, pointer.IsPrimary)), - GetModifiers(args.InputModifiers, false))); + new PointerPointProperties(GetModifiers(args.InputModifiers, true), + PointerUpdateKind.LeftButtonPressed), + GetKeyModifiers(args.InputModifiers))); } if (args.Type == RawPointerEventType.TouchEnd) @@ -56,11 +58,12 @@ namespace Avalonia.Input { target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, false)), - GetModifiers(args.InputModifiers, pointer.IsPrimary), - pointer.IsPrimary ? MouseButton.Left : MouseButton.None)); + new PointerPointProperties(GetModifiers(args.InputModifiers, false), + PointerUpdateKind.LeftButtonReleased), + GetKeyModifiers(args.InputModifiers))); } } + if (args.Type == RawPointerEventType.TouchCancel) { _pointers.Remove(args.TouchPointId); @@ -72,7 +75,9 @@ namespace Avalonia.Input { var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary); target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, - args.Position, ev.Timestamp, new PointerPointProperties(modifiers), modifiers)); + args.Position, ev.Timestamp, + new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other), + GetKeyModifiers(args.InputModifiers))); } diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs index 4557a78d37..e6d75bcf35 100644 --- a/src/Avalonia.Layout/UniformGridLayoutState.cs +++ b/src/Avalonia.Layout/UniformGridLayoutState.cs @@ -72,12 +72,6 @@ namespace Avalonia.Layout _cachedFirstElement.Measure(availableSize); - // This doesn't need to be done in the UWP version and I'm not sure why. If we - // don't do this here, and we receive a recycled element then it will be shown - // at its previous arrange point, but we don't want it shown at all until its - // arranged. - _cachedFirstElement.Arrange(new Rect(-10000.0, -10000.0, 0, 0)); - SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache. diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index adb27d348d..0da97b915c 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -84,8 +84,8 @@ namespace Avalonia.Native .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind().ToConstant(new GlPlatformFeature(_factory.ObtainGlFeature())) - .Bind() - .ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows)); + .Bind().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows)) + .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()); } public IWindowImpl CreateWindow() @@ -97,11 +97,6 @@ namespace Avalonia.Native { throw new NotImplementedException(); } - - public IPopupImpl CreatePopup() - { - return new PopupImpl(_factory, _options); - } } public class AvaloniaNativeMacOptions diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 09f822cf46..02810ed155 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -24,6 +24,7 @@ namespace Avalonia { public bool UseDeferredRendering { get; set; } = true; public bool UseGpu { get; set; } = true; + public bool OverlayPopups { get; set; } public string AvaloniaNativeLibraryPath { get; set; } } diff --git a/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs new file mode 100644 index 0000000000..eea695d77e --- /dev/null +++ b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Controls.Platform; + +namespace Avalonia.Native +{ + internal class WindowsMountedVolumeInfoListener : IDisposable + { + private readonly CompositeDisposable _disposables; + private readonly ObservableCollection _targetObs; + private bool _beenDisposed = false; + private ObservableCollection mountedDrives; + + public WindowsMountedVolumeInfoListener(ObservableCollection mountedDrives) + { + this.mountedDrives = mountedDrives; + _disposables = new CompositeDisposable(); + + var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) + .Subscribe(Poll); + + _disposables.Add(pollTimer); + + Poll(0); + } + + private void Poll(long _) + { + var mountVolInfos = Directory.GetDirectories("/Volumes") + .Select(p => new MountedVolumeInfo() + { + VolumeLabel = Path.GetFileName(p), + VolumePath = p, + VolumeSizeBytes = 0 + }) + .ToArray(); + + if (_targetObs.SequenceEqual(mountVolInfos)) + return; + else + { + _targetObs.Clear(); + + foreach (var i in mountVolInfos) + _targetObs.Add(i); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_beenDisposed) + { + if (disposing) + { + + } + _beenDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + } + + public class MacOSMountedVolumeInfoProvider : IMountedVolumeInfoProvider + { + public IDisposable Listen(ObservableCollection mountedDrives) + { + Contract.Requires(mountedDrives != null); + return new WindowsMountedVolumeInfoListener(mountedDrives); + } + } +} diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index a470caa80e..f776ee0132 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Native.Interop; using Avalonia.Platform; @@ -9,12 +10,26 @@ namespace Avalonia.Native { public class PopupImpl : WindowBaseImpl, IPopupImpl { - public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts) + private readonly IAvaloniaNativeFactory _factory; + private readonly AvaloniaNativePlatformOptions _opts; + public PopupImpl(IAvaloniaNativeFactory factory, + AvaloniaNativePlatformOptions opts, + IWindowBaseImpl parent) : base(opts) { + _factory = factory; + _opts = opts; using (var e = new PopupEvents(this)) { Init(factory.CreatePopup(e), factory.CreateScreens()); } + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); + } + + private void MoveResize(PixelPoint position, Size size, double scaling) + { + Position = position; + Resize(size); + //TODO: We ignore the scaling override for now } class PopupEvents : WindowBaseEvents, IAvnWindowEvents @@ -35,5 +50,8 @@ namespace Avalonia.Native { } } + + public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, this); + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 076fe9ccae..490d5688a8 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -11,9 +11,13 @@ namespace Avalonia.Native { public class WindowImpl : WindowBaseImpl, IWindowImpl { + private readonly IAvaloniaNativeFactory _factory; + private readonly AvaloniaNativePlatformOptions _opts; IAvnWindow _native; public WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts) { + _factory = factory; + _opts = opts; using (var e = new WindowEvents(this)) { Init(_native = factory.CreateWindow(e), factory.CreateScreens()); @@ -100,5 +104,9 @@ namespace Avalonia.Native } public Func Closing { get; set; } + public void Move(PixelPoint point) => Position = point; + + public override IPopupImpl CreatePopup() => + _opts.OverlayPopups ? null : new PopupImpl(_factory, _opts, this); } } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 638879ba14..217fb4b078 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -15,7 +15,7 @@ using Avalonia.Threading; namespace Avalonia.Native { - public class WindowBaseImpl : IWindowBaseImpl, + public abstract class WindowBaseImpl : IWindowBaseImpl, IFramebufferPlatformSurface { IInputRoot _inputRoot; @@ -91,6 +91,7 @@ namespace Avalonia.Native public Action Resized { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice => AvaloniaNativePlatform.MouseDevice; + public abstract IPopupImpl CreatePopup(); class FramebufferWrapper : ILockedFramebuffer @@ -208,7 +209,7 @@ namespace Avalonia.Native { Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); - var args = new RawKeyEventArgs(_keyboard, timeStamp, (RawKeyEventType)type, (Key)key, (InputModifiers)modifiers); + var args = new RawKeyEventArgs(_keyboard, timeStamp, (RawKeyEventType)type, (Key)key, (RawInputModifiers)modifiers); Input?.Invoke(args); @@ -222,11 +223,11 @@ namespace Avalonia.Native switch (type) { case AvnRawMouseEventType.Wheel: - Input?.Invoke(new RawMouseWheelEventArgs(_mouse, timeStamp, _inputRoot, point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (InputModifiers)modifiers)); + Input?.Invoke(new RawMouseWheelEventArgs(_mouse, timeStamp, _inputRoot, point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers)); break; default: - Input?.Invoke(new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, point.ToAvaloniaPoint(), (InputModifiers)modifiers)); + Input?.Invoke(new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, point.ToAvaloniaPoint(), (RawInputModifiers)modifiers)); break; } } diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index 3d525955b4..c44cc358e8 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -12,11 +12,11 @@ - - + + - + - + diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index 0c2d33bc7b..675234c16a 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -40,16 +40,14 @@ StaysOpen="False"> - - - - - + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 9c60b29193..114979fba2 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -19,6 +19,7 @@ + diff --git a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml index bc06ab010e..1fd168c009 100644 --- a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml @@ -4,13 +4,13 @@ - + - + - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/OverlayPopupHost.xaml b/src/Avalonia.Themes.Default/OverlayPopupHost.xaml new file mode 100644 index 0000000000..35d3a8cff4 --- /dev/null +++ b/src/Avalonia.Themes.Default/OverlayPopupHost.xaml @@ -0,0 +1,14 @@ + diff --git a/src/Avalonia.Themes.Default/PopupRoot.xaml b/src/Avalonia.Themes.Default/PopupRoot.xaml index cc23367ac0..71042f2a98 100644 --- a/src/Avalonia.Themes.Default/PopupRoot.xaml +++ b/src/Avalonia.Themes.Default/PopupRoot.xaml @@ -2,11 +2,13 @@ - + + + - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/TreeViewItem.xaml b/src/Avalonia.Themes.Default/TreeViewItem.xaml index 5dd082cf7a..0d826806d0 100644 --- a/src/Avalonia.Themes.Default/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Default/TreeViewItem.xaml @@ -16,7 +16,9 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" TemplatedControl.IsTemplateFocusTarget="True"> - - + - + diff --git a/src/Avalonia.Visuals/Media/PixelPoint.cs b/src/Avalonia.Visuals/Media/PixelPoint.cs index 995781ee9f..d62c2a2e55 100644 --- a/src/Avalonia.Visuals/Media/PixelPoint.cs +++ b/src/Avalonia.Visuals/Media/PixelPoint.cs @@ -59,6 +59,59 @@ namespace Avalonia { return !(left == right); } + + /// + /// Converts the to a . + /// + /// The point. + public static implicit operator PixelVector(PixelPoint p) + { + return new PixelVector(p.X, p.Y); + } + + /// + /// Adds two points. + /// + /// The first point. + /// The second point. + /// A point that is the result of the addition. + public static PixelPoint operator +(PixelPoint a, PixelPoint b) + { + return new PixelPoint(a.X + b.X, a.Y + b.Y); + } + + /// + /// Adds a vector to a point. + /// + /// The point. + /// The vector. + /// A point that is the result of the addition. + public static PixelPoint operator +(PixelPoint a, PixelVector b) + { + return new PixelPoint(a.X + b.X, a.Y + b.Y); + } + + /// + /// Subtracts two points. + /// + /// The first point. + /// The second point. + /// A point that is the result of the subtraction. + public static PixelPoint operator -(PixelPoint a, PixelPoint b) + { + return new PixelPoint(a.X - b.X, a.Y - b.Y); + } + + /// + /// Subtracts a vector from a point. + /// + /// The point. + /// The vector. + /// A point that is the result of the subtraction. + public static PixelPoint operator -(PixelPoint a, PixelVector b) + { + return new PixelPoint(a.X - b.X, a.Y - b.Y); + } /// /// Parses a string. @@ -106,7 +159,7 @@ namespace Avalonia return hash; } } - + /// /// Returns a new with the same Y co-ordinate and the specified X co-ordinate. /// diff --git a/src/Avalonia.Visuals/Media/PixelRect.cs b/src/Avalonia.Visuals/Media/PixelRect.cs index 9c8e5ad1c4..0e2094da07 100644 --- a/src/Avalonia.Visuals/Media/PixelRect.cs +++ b/src/Avalonia.Visuals/Media/PixelRect.cs @@ -261,6 +261,16 @@ namespace Avalonia { return (rect.X < Right) && (X < rect.Right) && (rect.Y < Bottom) && (Y < rect.Bottom); } + + /// + /// Translates the rectangle by an offset. + /// + /// The offset. + /// The translated rectangle. + public PixelRect Translate(PixelVector offset) + { + return new PixelRect(Position + offset, Size); + } /// /// Gets the union of two rectangles. diff --git a/src/Avalonia.Visuals/Media/PixelVector.cs b/src/Avalonia.Visuals/Media/PixelVector.cs new file mode 100644 index 0000000000..4a623e3bc2 --- /dev/null +++ b/src/Avalonia.Visuals/Media/PixelVector.cs @@ -0,0 +1,203 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Globalization; +using Avalonia.Animation.Animators; +using JetBrains.Annotations; + +namespace Avalonia +{ + /// + /// Defines a vector. + /// + public readonly struct PixelVector + { + /// + /// The X vector. + /// + private readonly int _x; + + /// + /// The Y vector. + /// + private readonly int _y; + + /// + /// Initializes a new instance of the structure. + /// + /// The X vector. + /// The Y vector. + public PixelVector(int x, int y) + { + _x = x; + _y = y; + } + + /// + /// Gets the X vector. + /// + public int X => _x; + + /// + /// Gets the Y vector. + /// + public int Y => _y; + + /// + /// Converts the to a . + /// + /// The vector. + public static explicit operator PixelPoint(PixelVector a) + { + return new PixelPoint(a._x, a._y); + } + + /// + /// Calculates the dot product of two vectors + /// + /// First vector + /// Second vector + /// The dot product + public static int operator *(PixelVector a, PixelVector b) + { + return a.X * b.X + a.Y * b.Y; + } + + /// + /// Scales a vector. + /// + /// The vector + /// The scaling factor. + /// The scaled vector. + public static PixelVector operator *(PixelVector vector, int scale) + { + return new PixelVector(vector._x * scale, vector._y * scale); + } + + /// + /// Scales a vector. + /// + /// The vector + /// The divisor. + /// The scaled vector. + public static PixelVector operator /(PixelVector vector, int scale) + { + return new PixelVector(vector._x / scale, vector._y / scale); + } + + /// + /// Length of the vector + /// + public double Length => Math.Sqrt(X * X + Y * Y); + + /// + /// Negates a vector. + /// + /// The vector. + /// The negated vector. + public static PixelVector operator -(PixelVector a) + { + return new PixelVector(-a._x, -a._y); + } + + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// A vector that is the result of the addition. + public static PixelVector operator +(PixelVector a, PixelVector b) + { + return new PixelVector(a._x + b._x, a._y + b._y); + } + + /// + /// Subtracts two vectors. + /// + /// The first vector. + /// The second vector. + /// A vector that is the result of the subtraction. + public static PixelVector operator -(PixelVector a, PixelVector b) + { + return new PixelVector(a._x - b._x, a._y - b._y); + } + + /// + /// Check if two vectors are equal (bitwise). + /// + /// + /// + public bool Equals(PixelVector other) + { + return _x == other._x && _y == other._y; + } + + /// + /// Check if two vectors are nearly equal (numerically). + /// + /// The other vector. + /// True if vectors are nearly equal. + [Pure] + public bool NearlyEquals(PixelVector other) + { + const float tolerance = float.Epsilon; + + return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + + return obj is PixelVector vector && Equals(vector); + } + + public override int GetHashCode() + { + unchecked + { + return (_x.GetHashCode() * 397) ^ _y.GetHashCode(); + } + } + + public static bool operator ==(PixelVector left, PixelVector right) + { + return left.Equals(right); + } + + public static bool operator !=(PixelVector left, PixelVector right) + { + return !left.Equals(right); + } + + /// + /// Returns the string representation of the point. + /// + /// The string representation of the point. + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y); + } + + /// + /// Returns a new vector with the specified X coordinate. + /// + /// The X coordinate. + /// The new vector. + public PixelVector WithX(int x) + { + return new PixelVector(x, _y); + } + + /// + /// Returns a new vector with the specified Y coordinate. + /// + /// The Y coordinate. + /// The new vector. + public PixelVector WithY(int y) + { + return new PixelVector(_x, y); + } + } +} diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index 49d4724b9a..8f08f7f51f 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -371,12 +371,12 @@ namespace Avalonia /// The bounding box public Rect TransformToAABB(Matrix matrix) { - var points = new[] + ReadOnlySpan points = stackalloc Point[4] { TopLeft.Transform(matrix), TopRight.Transform(matrix), BottomRight.Transform(matrix), - BottomLeft.Transform(matrix), + BottomLeft.Transform(matrix) }; var left = double.MaxValue; diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj index 59afc877de..c160fd7726 100644 --- a/src/Avalonia.X11/Avalonia.X11.csproj +++ b/src/Avalonia.X11/Avalonia.X11.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 0a8b1ee9c4..bed6f4693b 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -24,7 +24,7 @@ namespace Avalonia.X11 {StandardCursorType.No, CursorFontShape.XC_X_cursor}, {StandardCursorType.Wait, CursorFontShape.XC_watch}, {StandardCursorType.AppStarting, CursorFontShape.XC_watch}, - {StandardCursorType.BottomSize, CursorFontShape.XC_bottom_side}, + {StandardCursorType.BottomSide, CursorFontShape.XC_bottom_side}, {StandardCursorType.DragCopy, CursorFontShape.XC_center_ptr}, {StandardCursorType.DragLink, CursorFontShape.XC_fleur}, {StandardCursorType.DragMove, CursorFontShape.XC_diamond_cross}, diff --git a/src/Avalonia.X11/X11KeyTransform.cs b/src/Avalonia.X11/X11KeyTransform.cs index c68cb04733..87a4174c06 100644 --- a/src/Avalonia.X11/X11KeyTransform.cs +++ b/src/Avalonia.X11/X11KeyTransform.cs @@ -52,16 +52,6 @@ namespace Avalonia.X11 {X11Key.Delete, Key.Delete}, {X11Key.KP_Delete, Key.Delete}, {X11Key.Help, Key.Help}, - {X11Key.XK_0, Key.D0}, - {X11Key.XK_1, Key.D1}, - {X11Key.XK_2, Key.D2}, - {X11Key.XK_3, Key.D3}, - {X11Key.XK_4, Key.D4}, - {X11Key.XK_5, Key.D5}, - {X11Key.XK_6, Key.D6}, - {X11Key.XK_7, Key.D7}, - {X11Key.XK_8, Key.D8}, - {X11Key.XK_9, Key.D9}, {X11Key.A, Key.A}, {X11Key.B, Key.B}, {X11Key.C, Key.C}, @@ -114,8 +104,8 @@ namespace Avalonia.X11 {X11Key.x, Key.X}, {X11Key.y, Key.Y}, {X11Key.z, Key.Z}, - //{ X11Key.?, Key.LWin } - //{ X11Key.?, Key.RWin } + {X11Key.Meta_L, Key.LWin }, + {X11Key.Meta_R, Key.RWin }, {X11Key.Menu, Key.Apps}, //{ X11Key.?, Key.Sleep } {X11Key.KP_0, Key.NumPad0}, @@ -185,20 +175,51 @@ namespace Avalonia.X11 //{ X11Key.?, Key.SelectMedia } //{ X11Key.?, Key.LaunchApplication1 } //{ X11Key.?, Key.LaunchApplication2 } - {X11Key.semicolon, Key.OemSemicolon}, + {X11Key.minus, Key.OemMinus}, + {X11Key.underscore, Key.OemMinus}, {X11Key.plus, Key.OemPlus}, {X11Key.equal, Key.OemPlus}, + {X11Key.bracketleft, Key.OemOpenBrackets}, + {X11Key.braceleft, Key.OemOpenBrackets}, + {X11Key.bracketright, Key.OemCloseBrackets}, + {X11Key.braceright, Key.OemCloseBrackets}, + {X11Key.backslash, Key.OemPipe}, + {X11Key.bar, Key.OemPipe}, + {X11Key.semicolon, Key.OemSemicolon}, + {X11Key.colon, Key.OemSemicolon}, + {X11Key.apostrophe, Key.OemQuotes}, + {X11Key.quotedbl, Key.OemQuotes}, {X11Key.comma, Key.OemComma}, - {X11Key.minus, Key.OemMinus}, + {X11Key.less, Key.OemComma}, {X11Key.period, Key.OemPeriod}, + {X11Key.greater, Key.OemPeriod}, {X11Key.slash, Key.Oem2}, + {X11Key.question, Key.Oem2}, {X11Key.grave, Key.OemTilde}, + {X11Key.asciitilde, Key.OemTilde}, + {X11Key.XK_1, Key.D1}, + {X11Key.exclam, Key.D1}, + {X11Key.XK_2, Key.D2}, + {X11Key.at, Key.D2}, + {X11Key.XK_3, Key.D3}, + {X11Key.numbersign, Key.D3}, + {X11Key.XK_4, Key.D4}, + {X11Key.dollar, Key.D4}, + {X11Key.XK_5, Key.D5}, + {X11Key.percent, Key.D5}, + {X11Key.XK_6, Key.D6}, + {X11Key.asciicircum, Key.D6}, + {X11Key.XK_7, Key.D7}, + {X11Key.ampersand, Key.D7}, + {X11Key.XK_8, Key.D8}, + {X11Key.asterisk, Key.D8}, + {X11Key.XK_9, Key.D9}, + {X11Key.parenleft, Key.D9}, + {X11Key.XK_0, Key.D0}, + {X11Key.parenright, Key.D0}, + //{ X11Key.?, Key.AbntC1 } //{ X11Key.?, Key.AbntC2 } - {X11Key.bracketleft, Key.OemOpenBrackets}, - {X11Key.backslash, Key.OemPipe}, - {X11Key.bracketright, Key.OemCloseBrackets}, - {X11Key.apostrophe, Key.OemQuotes}, //{ X11Key.?, Key.Oem8 } //{ X11Key.?, Key.Oem102 } //{ X11Key.?, Key.ImeProcessed } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 7bdc61eb28..1d2290236c 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Reflection; using Avalonia.Controls; using Avalonia.Controls.Platform; +using Avalonia.FreeDesktop; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; @@ -12,6 +13,7 @@ using Avalonia.X11; using Avalonia.X11.Glx; using Avalonia.X11.NativeDialogs; using static Avalonia.X11.XLib; + namespace Avalonia.X11 { class AvaloniaX11Platform : IWindowingPlatform @@ -48,7 +50,8 @@ namespace Avalonia.X11 .Bind().ToConstant(new X11Clipboard(this)) .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) - .Bind().ToConstant(new GtkSystemDialog()); + .Bind().ToConstant(new GtkSystemDialog()) + .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()); X11Screens = Avalonia.X11.X11Screens.Init(this); Screens = new X11Screens(X11Screens); @@ -74,18 +77,13 @@ namespace Avalonia.X11 public IntPtr Display { get; set; } public IWindowImpl CreateWindow() { - return new X11Window(this, false); + return new X11Window(this, null); } public IEmbeddableWindowImpl CreateEmbeddableWindow() { throw new NotSupportedException(); } - - public IPopupImpl CreatePopup() - { - return new X11Window(this, true); - } } } @@ -96,6 +94,7 @@ namespace Avalonia { public bool UseEGL { get; set; } public bool UseGpu { get; set; } = true; + public bool OverlayPopups { get; set; } public List GlxRendererBlacklist { get; set; } = new List { diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 18c23aa31e..975b3d11d7 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reactive.Disposables; using System.Text; using Avalonia.Controls; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.OpenGL; @@ -21,6 +22,7 @@ namespace Avalonia.X11 unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client { private readonly AvaloniaX11Platform _platform; + private readonly IWindowImpl _popupParent; private readonly bool _popup; private readonly X11Info _x11; private bool _invalidated; @@ -38,6 +40,7 @@ namespace Avalonia.X11 private bool _mapped; private HashSet _transientChildren = new HashSet(); private X11Window _transientParent; + private double? _scalingOverride; public object SyncRoot { get; } = new object(); class InputEventContainer @@ -47,10 +50,10 @@ namespace Avalonia.X11 private readonly Queue _inputQueue = new Queue(); private InputEventContainer _lastEvent; private bool _useRenderWindow = false; - public X11Window(AvaloniaX11Platform platform, bool popup) + public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent) { _platform = platform; - _popup = popup; + _popup = popupParent != null; _x11 = platform.Info; _mouse = platform.MouseDevice; _keyboard = platform.KeyboardDevice; @@ -66,7 +69,7 @@ namespace Avalonia.X11 | SetWindowValuemask.BackPixmap | SetWindowValuemask.BackingStore | SetWindowValuemask.BitGravity | SetWindowValuemask.WinGravity; - if (popup) + if (_popup) { attr.override_redirect = true; valueMask |= SetWindowValuemask.OverrideRedirect; @@ -150,6 +153,8 @@ namespace Avalonia.X11 _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing, XNames.XNClientWindow, _handle, IntPtr.Zero); XFlush(_x11.Display); + if(_popup) + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); } class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo @@ -453,22 +458,28 @@ namespace Avalonia.X11 } } - private bool UpdateScaling() + private bool UpdateScaling(bool skipResize = false) { lock (SyncRoot) { - var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity) - .FirstOrDefault(m => m.Bounds.Contains(Position)); - var newScaling = monitor?.PixelDensity ?? Scaling; + double newScaling; + if (_scalingOverride.HasValue) + newScaling = _scalingOverride.Value; + else + { + var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity) + .FirstOrDefault(m => m.Bounds.Contains(Position)); + newScaling = monitor?.PixelDensity ?? Scaling; + } + if (Scaling != newScaling) { - Console.WriteLine( - $"Updating scaling from {Scaling} to {newScaling} as a response to position change to {Position}"); var oldScaledSize = ClientSize; Scaling = newScaling; ScalingChanged?.Invoke(Scaling); SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize); - Resize(oldScaledSize, true); + if(!skipResize) + Resize(oldScaledSize, true); return true; } @@ -547,24 +558,23 @@ namespace Avalonia.X11 } - - InputModifiers TranslateModifiers(XModifierMask state) + RawInputModifiers TranslateModifiers(XModifierMask state) { - var rv = default(InputModifiers); + var rv = default(RawInputModifiers); if (state.HasFlag(XModifierMask.Button1Mask)) - rv |= InputModifiers.LeftMouseButton; + rv |= RawInputModifiers.LeftMouseButton; if (state.HasFlag(XModifierMask.Button2Mask)) - rv |= InputModifiers.RightMouseButton; + rv |= RawInputModifiers.RightMouseButton; if (state.HasFlag(XModifierMask.Button2Mask)) - rv |= InputModifiers.MiddleMouseButton; + rv |= RawInputModifiers.MiddleMouseButton; if (state.HasFlag(XModifierMask.ShiftMask)) - rv |= InputModifiers.Shift; + rv |= RawInputModifiers.Shift; if (state.HasFlag(XModifierMask.ControlMask)) - rv |= InputModifiers.Control; + rv |= RawInputModifiers.Control; if (state.HasFlag(XModifierMask.Mod1Mask)) - rv |= InputModifiers.Alt; + rv |= RawInputModifiers.Alt; if (state.HasFlag(XModifierMask.Mod4Mask)) - rv |= InputModifiers.Windows; + rv |= RawInputModifiers.Meta; return rv; } @@ -730,6 +740,14 @@ namespace Avalonia.X11 public void Resize(Size clientSize) => Resize(clientSize, false); + public void Move(PixelPoint point) => Position = point; + private void MoveResize(PixelPoint position, Size size, double scaling) + { + Move(position); + _scalingOverride = scaling; + UpdateScaling(true); + Resize(size, true); + } PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * Scaling), (int)(size.Height * Scaling)); @@ -793,7 +811,9 @@ namespace Avalonia.X11 } public IMouseDevice MouseDevice => _mouse; - + public IPopupImpl CreatePopup() + => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this); + public void Activate() { if (_x11.Atoms._NET_ACTIVE_WINDOW != IntPtr.Zero) @@ -937,6 +957,8 @@ namespace Avalonia.X11 { SendNetWMMessage(_x11.Atoms._NET_WM_STATE, (IntPtr)(value ? 0 : 1), _x11.Atoms._NET_WM_STATE_SKIP_TASKBAR, IntPtr.Zero); - } + } + + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index 0a78c0dfd9..cf75572601 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -247,7 +247,7 @@ namespace Avalonia.X11 unsafe class ParsedDeviceEvent { public XiEventType Type { get; } - public InputModifiers Modifiers { get; } + public RawInputModifiers Modifiers { get; } public ulong Timestamp { get; } public Point Position { get; } public int Button { get; set; } @@ -260,25 +260,25 @@ namespace Avalonia.X11 Timestamp = (ulong)ev->time.ToInt64(); var state = (XModifierMask)ev->mods.Effective; if (state.HasFlag(XModifierMask.ShiftMask)) - Modifiers |= InputModifiers.Shift; + Modifiers |= RawInputModifiers.Shift; if (state.HasFlag(XModifierMask.ControlMask)) - Modifiers |= InputModifiers.Control; + Modifiers |= RawInputModifiers.Control; if (state.HasFlag(XModifierMask.Mod1Mask)) - Modifiers |= InputModifiers.Alt; + Modifiers |= RawInputModifiers.Alt; if (state.HasFlag(XModifierMask.Mod4Mask)) - Modifiers |= InputModifiers.Windows; + Modifiers |= RawInputModifiers.Meta; if (ev->buttons.MaskLen > 0) { var buttons = ev->buttons.Mask; if (XIMaskIsSet(buttons, 1)) - Modifiers |= InputModifiers.LeftMouseButton; + Modifiers |= RawInputModifiers.LeftMouseButton; if (XIMaskIsSet(buttons, 2)) - Modifiers |= InputModifiers.MiddleMouseButton; + Modifiers |= RawInputModifiers.MiddleMouseButton; if (XIMaskIsSet(buttons, 3)) - Modifiers |= InputModifiers.RightMouseButton; + Modifiers |= RawInputModifiers.RightMouseButton; } Valuators = new Dictionary(); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 5e2ba51caf..2dc112f3d3 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -59,7 +59,9 @@ namespace Avalonia.LinuxFramebuffer public Size ClientSize => ScaledSize; public IMouseDevice MouseDevice => new MouseDevice(); - public double Scaling => 1; + public IPopupImpl CreatePopup() => null; + + public double Scaling => _outputBackend.Scaling; public IEnumerable Surfaces => new object[] {_outputBackend}; public Action Input { get; set; } public Action Paint { get; set; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 723028c666..432344955a 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -125,7 +125,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput : type == LibInputEventType.LIBINPUT_EVENT_TOUCH_UP ? RawPointerEventType.TouchEnd : type == LibInputEventType.LIBINPUT_EVENT_TOUCH_MOTION ? RawPointerEventType.TouchUpdate : RawPointerEventType.TouchCancel, - pt, InputModifiers.None, slot)); + pt, RawInputModifiers.None, slot)); } } @@ -140,7 +140,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput _mousePosition = new Point(libinput_event_pointer_get_absolute_x_transformed(pev, (int)info.Width), libinput_event_pointer_get_absolute_y_transformed(pev, (int)info.Height)); ScheduleInput(new RawPointerEventArgs(_mouse, ts, _inputRoot, RawPointerEventType.Move, _mousePosition, - InputModifiers.None)); + RawInputModifiers.None)); } else if (type == LibInputEventType.LIBINPUT_EVENT_POINTER_BUTTON) { @@ -162,7 +162,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput ScheduleInput( - new RawPointerEventArgs(_mouse, ts, _inputRoot, evnt, _mousePosition, InputModifiers.None)); + new RawPointerEventArgs(_mouse, ts, _inputRoot, evnt, _mousePosition, RawInputModifiers.None)); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 2cc1f65202..8fc555aac2 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -107,11 +107,12 @@ namespace Avalonia.LinuxFramebuffer public static class LinuxFramebufferPlatformExtensions { - public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null) - where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new FbdevOutput(fbdev)); + public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null, double scaling = 1) + where T : AppBuilderBase, new() => + StartLinuxDirect(builder, args, new FbdevOutput(fbdev) {Scaling = scaling}); - public static int StartLinuxDrm(this T builder, string[] args, string card = null) - where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card)); + public static int StartLinuxDrm(this T builder, string[] args, string card = null, double scaling = 1) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card) {Scaling = scaling}); public static int StartLinuxDirect(this T builder, string[] args, IOutputBackend backend) where T : AppBuilderBase, new() diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 6a76977352..273265a6dc 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -14,7 +14,7 @@ namespace Avalonia.LinuxFramebuffer.Output private DrmCard _card; private readonly EglGlPlatformSurface _eglPlatformSurface; public PixelSize PixelSize => _mode.Resolution; - + public double Scaling { get; set; } public DrmOutput(string path = null) { var card = new DrmCard(path); @@ -233,7 +233,7 @@ namespace Avalonia.LinuxFramebuffer.Output public PixelSize Size => _parent._mode.Resolution; - public double Scaling => 1; + public double Scaling => _parent.Scaling; } public IGlPlatformSurfaceRenderingSession BeginDraw() @@ -241,6 +241,8 @@ namespace Avalonia.LinuxFramebuffer.Output _parent._deferredContext.MakeCurrent(_parent._eglSurface); return new RenderSession(_parent); } + + } IGlContext IWindowingPlatformGlFeature.ImmediateContext => _immediateContext; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index 3021c29015..b83fe6cbe8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -9,16 +9,15 @@ namespace Avalonia.LinuxFramebuffer { public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposable, IOutputBackend { - private readonly Vector _dpi; private int _fd; private fb_fix_screeninfo _fixedInfo; private fb_var_screeninfo _varInfo; private IntPtr _mappedLength; private IntPtr _mappedAddress; + public double Scaling { get; set; } - public FbdevOutput(string fileName = null, Vector? dpi = null) + public FbdevOutput(string fileName = null) { - _dpi = dpi ?? new Vector(96, 96); fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; _fd = NativeUnsafeMethods.open(fileName, 2, 0); if (_fd <= 0) @@ -101,7 +100,7 @@ namespace Avalonia.LinuxFramebuffer { if (_fd <= 0) throw new ObjectDisposedException("LinuxFramebuffer"); - return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, _dpi); + return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, new Vector(96, 96) * Scaling); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs index 01690f07ac..17a39b0219 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs @@ -3,5 +3,6 @@ namespace Avalonia.LinuxFramebuffer.Output public interface IOutputBackend { PixelSize PixelSize { get; } + double Scaling { get; set; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ArrayElementPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ArrayElementPlugin.cs index 718c695c45..7cbe4942e5 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ArrayElementPlugin.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ArrayElementPlugin.cs @@ -22,17 +22,21 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings throw new InvalidOperationException("The ArrayElementPlugin does not support dynamic matching"); } - public IPropertyAccessor Start(WeakReference reference, string propertyName) + public IPropertyAccessor Start(WeakReference reference, string propertyName) { - return new Accessor(reference, _indices, _elementType); + if (reference.TryGetTarget(out var target) && target is Array arr) + { + return new Accessor(new WeakReference(arr), _indices, _elementType); + } + return null; } class Accessor : PropertyAccessorBase { private readonly int[] _indices; - private readonly WeakReference _reference; + private readonly WeakReference _reference; - public Accessor(WeakReference reference, int[] indices, Type elementType) + public Accessor(WeakReference reference, int[] indices, Type elementType) { _reference = reference; _indices = indices; @@ -41,11 +45,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings public override Type PropertyType { get; } - public override object Value => _reference.Target is Array arr ? arr.GetValue(_indices) : null; + public override object Value => _reference.TryGetTarget(out var arr) ? arr.GetValue(_indices) : null; public override bool SetValue(object value, BindingPriority priority) { - if (_reference.Target is Array arr) + if (_reference.TryGetTarget(out var arr)) { arr.SetValue(value, _indices); return true; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs index b45fde7798..3324dd7319 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -72,7 +72,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings return this; } - public CompiledBindingPathBuilder Property(IPropertyInfo info, Func accessorFactory) + public CompiledBindingPathBuilder Property(IPropertyInfo info, Func, IPropertyInfo, IPropertyAccessor> accessorFactory) { _elements.Add(new PropertyElement(info, accessorFactory)); return this; @@ -130,7 +130,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings internal class PropertyElement : ICompiledBindingPathElement { - public PropertyElement(IPropertyInfo property, Func accessorFactory) + public PropertyElement(IPropertyInfo property, Func, IPropertyInfo, IPropertyAccessor> accessorFactory) { Property = property; AccessorFactory = accessorFactory; @@ -138,7 +138,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings public IPropertyInfo Property { get; } - public Func AccessorFactory { get; } + public Func, IPropertyInfo, IPropertyAccessor> AccessorFactory { get; } } internal interface IStronglyTypedStreamElement : ICompiledBindingPathElement diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs index 361cbef740..3e3174123a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs @@ -8,21 +8,23 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { class ObservableStreamPlugin : IStreamPlugin { - public bool Match(WeakReference reference) + public bool Match(WeakReference reference) { - return reference is IObservable; + return reference.TryGetTarget(out var target) && target is IObservable; } - public IObservable Start(WeakReference reference) + public IObservable Start(WeakReference reference) { - var target = reference.Target as IObservable; - - if (target is IObservable obj) + if (!(reference.TryGetTarget(out var target) && target is IObservable obs)) + { + return Observable.Empty(); + } + else if (target is IObservable obj) { return obj; } - return target.Select(x => (object)x); + return obs.Select(x => (object)x); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs index a4534a7396..b3f78bfbe3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs @@ -12,13 +12,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { public static class PropertyInfoAccessorFactory { - public static IPropertyAccessor CreateInpcPropertyAccessor(WeakReference target, IPropertyInfo property) + public static IPropertyAccessor CreateInpcPropertyAccessor(WeakReference target, IPropertyInfo property) => new InpcPropertyAccessor(target, property); - public static IPropertyAccessor CreateAvaloniaPropertyAccessor(WeakReference target, IPropertyInfo property) - => new AvaloniaPropertyAccessor(new WeakReference((AvaloniaObject)target.Target), (AvaloniaProperty)property); + public static IPropertyAccessor CreateAvaloniaPropertyAccessor(WeakReference target, IPropertyInfo property) + => new AvaloniaPropertyAccessor(new WeakReference((AvaloniaObject)(target.TryGetTarget(out var o) ? o : null)), (AvaloniaProperty)property); - public static IPropertyAccessor CreateIndexerPropertyAccessor(WeakReference target, IPropertyInfo property, int argument) + public static IPropertyAccessor CreateIndexerPropertyAccessor(WeakReference target, IPropertyInfo property, int argument) => new IndexerAccessor(target, property, argument); } @@ -74,10 +74,10 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings internal class InpcPropertyAccessor : PropertyAccessorBase { - protected readonly WeakReference _reference; + protected readonly WeakReference _reference; private readonly IPropertyInfo _property; - public InpcPropertyAccessor(WeakReference reference, IPropertyInfo property) + public InpcPropertyAccessor(WeakReference reference, IPropertyInfo property) { Contract.Requires(reference != null); Contract.Requires(property != null); @@ -92,16 +92,15 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { get { - var o = _reference.Target; - return (o != null) ? _property.Get(o) : null; + return _reference.TryGetTarget(out var o) ? _property.Get(o) : null; } } public override bool SetValue(object value, BindingPriority priority) { - if (_property.CanSet && _reference.IsAlive) + if (_property.CanSet && _reference.TryGetTarget(out var o)) { - _property.Set(_reference.Target, value); + _property.Set(o, value); SendCurrentValue(); @@ -127,9 +126,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings protected override void UnsubscribeCore() { - var inpc = _reference.Target as INotifyPropertyChanged; - - if (inpc != null) + if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) { WeakEventHandlerManager.Unsubscribe( inpc, @@ -150,9 +147,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings private void SubscribeToChanges() { - var inpc = _reference.Target as INotifyPropertyChanged; - - if (inpc != null) + if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) { WeakEventHandlerManager.Subscribe( inpc, @@ -166,7 +161,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { private int _index; - public IndexerAccessor(WeakReference target, IPropertyInfo basePropertyInfo, int argument) + public IndexerAccessor(WeakReference target, IPropertyInfo basePropertyInfo, int argument) :base(target, basePropertyInfo) { _index = argument; @@ -176,7 +171,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings protected override void SubscribeCore() { base.SubscribeCore(); - if (_reference.Target is INotifyCollectionChanged incc) + if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc) { WeakEventHandlerManager.Subscribe( incc, @@ -188,7 +183,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings protected override void UnsubscribeCore() { base.UnsubscribeCore(); - if (_reference.Target is INotifyCollectionChanged incc) + if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc) { WeakEventHandlerManager.Unsubscribe( incc, diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs index 368a68f06b..6e7463776b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs @@ -11,9 +11,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings class PropertyInfoAccessorPlugin : IPropertyAccessorPlugin { private readonly IPropertyInfo _propertyInfo; - private readonly Func _accessorFactory; + private readonly Func, IPropertyInfo, IPropertyAccessor> _accessorFactory; - public PropertyInfoAccessorPlugin(IPropertyInfo propertyInfo, Func accessorFactory) + public PropertyInfoAccessorPlugin(IPropertyInfo propertyInfo, Func, IPropertyInfo, IPropertyAccessor> accessorFactory) { _propertyInfo = propertyInfo; _accessorFactory = accessorFactory; @@ -24,7 +24,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings throw new InvalidOperationException("The PropertyInfoAccessorPlugin does not support dynamic matching"); } - public IPropertyAccessor Start(WeakReference reference, string propertyName) + public IPropertyAccessor Start(WeakReference reference, string propertyName) { Debug.Assert(_propertyInfo.Name == propertyName); return _accessorFactory(reference, _propertyInfo); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs index 30fd1fdaa2..164d38e3ae 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs @@ -9,14 +9,14 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { class TaskStreamPlugin : IStreamPlugin { - public bool Match(WeakReference reference) + public bool Match(WeakReference reference) { - return reference.Target is Task; + return reference.TryGetTarget(out var target) && target is Task; } - public IObservable Start(WeakReference reference) + public IObservable Start(WeakReference reference) { - if (!(reference.Target is Task task)) + if(!(reference.TryGetTarget(out var target) && target is Task task)) { return Observable.Empty(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs index 0adbba72b1..548f0161d6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs @@ -164,6 +164,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public bool IsStatic => true; public string Name { get; protected set; } public IXamlIlType DeclaringType { get; } + public IXamlIlMethod MakeGenericMethod(IReadOnlyList typeArguments) + => throw new System.NotSupportedException(); public bool Equals(IXamlIlMethod other) => @@ -187,11 +189,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers emitter.Unbox_Any(Parent.PropertyType); } - - public IXamlIlMethod MakeGenericMethod(IReadOnlyList typeArguments) - { - throw new System.InvalidOperationException(); - } } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs index 970c52a2f0..42910581ff 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs @@ -36,7 +36,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions private void EmitLoadPropertyAccessorFactory(XamlIlEmitContext context, IXamlIlEmitter codeGen, IXamlIlType type, string accessorFactoryName, bool isStatic = true) { var types = context.GetAvaloniaTypes(); - var weakReferenceType = context.Configuration.TypeSystem.GetType("System.WeakReference"); + var weakReferenceType = context.Configuration.TypeSystem.GetType("System.WeakReference`1").MakeGenericType(context.Configuration.WellKnownTypes.Object); FindMethodMethodSignature accessorFactorySignature = new FindMethodMethodSignature(accessorFactoryName, types.IPropertyAccessor, weakReferenceType, types.IPropertyInfo) { IsStatic = isStatic @@ -62,7 +62,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { var types = context.GetAvaloniaTypes(); var intType = context.Configuration.TypeSystem.GetType("System.Int32"); - var weakReferenceType = context.Configuration.TypeSystem.GetType("System.WeakReference"); + var weakReferenceType = context.Configuration.TypeSystem.GetType("System.WeakReference`1").MakeGenericType(context.Configuration.WellKnownTypes.Object); var indexAccessorFactoryMethod = context.GetAvaloniaTypes().PropertyInfoAccessorFactory.GetMethod( new FindMethodMethodSignature( "CreateIndexerPropertyAccessor", @@ -100,7 +100,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions private IXamlIlType EmitCreateAccessorFactoryDelegate(XamlIlEmitContext context, IXamlIlEmitter codeGen) { var types = context.GetAvaloniaTypes(); - var weakReferenceType = context.Configuration.TypeSystem.GetType("System.WeakReference"); + var weakReferenceType = context.Configuration.TypeSystem.GetType("System.WeakReference`1").MakeGenericType(context.Configuration.WellKnownTypes.Object); var funcType = context.Configuration.TypeSystem.GetType("System.Func`3").MakeGenericType( weakReferenceType, types.IPropertyInfo, diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 8a8dd6e688..c7155c5f6c 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 8a8dd6e688d3610745bf8735394eb005ad3dfcd8 +Subproject commit c7155c5f6c1a5153ee2d8cd78e5d1524dd6744cf diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 19f92149ec..29945e25c3 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -76,7 +76,12 @@ namespace Avalonia.Data } var children = Bindings.Select(x => x.Initiate(target, null)); - var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType, converter)); + + var input = children.Select(x => x.Observable) + .CombineLatest() + .Select(x => ConvertValue(x, targetType, converter)) + .Where(x => x != BindingOperations.DoNothing); + var mode = Mode == BindingMode.Default ? targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; @@ -97,11 +102,6 @@ namespace Avalonia.Data var culture = CultureInfo.CurrentCulture; var converted = converter.Convert(values, targetType, ConverterParameter, culture); - if (converted == BindingOperations.DoNothing) - { - return converted; - } - if (converted == AvaloniaProperty.UnsetValue) { converted = FallbackValue; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index 08f9df0701..97198145a8 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Markup.Parsers.Nodes public override string Description => $"#{_name}"; - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { if (_nameScope.TryGetTarget(out var scope)) _subscription = NameScopeLocator.Track(scope, _name).Subscribe(ValueChanged); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index 9f441ad360..f304d1e9a2 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -31,9 +31,9 @@ namespace Avalonia.Markup.Parsers.Nodes } } - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { - if (reference.Target is ILogical logical) + if (reference.TryGetTarget(out object target) && target is ILogical logical) { _subscription = ControlLocator.Track(logical, _level, _ancestorType).Subscribe(ValueChanged); } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs index ea847bde11..a11879238b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs @@ -26,9 +26,11 @@ namespace Avalonia.Markup.Parsers.Nodes protected override bool SetTargetValueCore(object value, BindingPriority priority) { - var typeInfo = Target.Target.GetType().GetTypeInfo(); - var list = Target.Target as IList; - var dictionary = Target.Target as IDictionary; + Target.TryGetTarget(out object target); + + var typeInfo = target.GetType().GetTypeInfo(); + var list = target as IList; + var dictionary = target as IDictionary; var indexerProperty = GetIndexer(typeInfo); var indexerParameters = indexerProperty?.GetIndexParameters(); @@ -53,7 +55,7 @@ namespace Avalonia.Markup.Parsers.Nodes // Try special cases where we can validate indices if (typeInfo.IsArray) { - return SetValueInArray((Array)Target.Target, intArgs, value); + return SetValueInArray((Array)target, intArgs, value); } else if (Arguments.Count == 1) { @@ -83,14 +85,14 @@ namespace Avalonia.Markup.Parsers.Nodes else { // Fallback to unchecked access - indexerProperty.SetValue(Target.Target, value, convertedObjectArray); + indexerProperty.SetValue(target, value, convertedObjectArray); return true; } } else { // Fallback to unchecked access - indexerProperty.SetValue(Target.Target, value, convertedObjectArray); + indexerProperty.SetValue(target, value, convertedObjectArray); return true; } } @@ -98,7 +100,7 @@ namespace Avalonia.Markup.Parsers.Nodes // multidimensional indexer, which doesn't take the same number of arguments else if (typeInfo.IsArray) { - SetValueInArray((Array)Target.Target, value); + SetValueInArray((Array)target, value); return true; } return false; @@ -126,7 +128,15 @@ namespace Avalonia.Markup.Parsers.Nodes public IList Arguments { get; } - public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; + public override Type PropertyType + { + get + { + Target.TryGetTarget(out object target); + + return GetIndexer(target.GetType().GetTypeInfo())?.PropertyType; + } + } protected override object GetValue(object target) { diff --git a/src/Skia/Avalonia.Skia/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/GlRenderTarget.cs index a7c1d0a38b..61ccf09e52 100644 --- a/src/Skia/Avalonia.Skia/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/GlRenderTarget.cs @@ -26,51 +26,64 @@ namespace Avalonia.Skia public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) { var session = _surface.BeginDraw(); - var disp = session.Display; - var gl = disp.GlInterface; - gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var fb); - - var size = session.Size; - var scaling = session.Scaling; - if (size.Width <= 0 || size.Height <= 0 || scaling < 0) - { - throw new InvalidOperationException( - $"Can't create drawing context for surface with {size} size and {scaling} scaling"); - } - - gl.Viewport(0, 0, size.Width, size.Height); - gl.ClearStencil(0); - gl.ClearColor(0, 0, 0, 0); - gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - lock (_grContext) + bool success = false; + try { - _grContext.ResetContext(); - - GRBackendRenderTarget renderTarget = - new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize, - new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat())); - var surface = SKSurface.Create(_grContext, renderTarget, - GRSurfaceOrigin.BottomLeft, - GRPixelConfig.Rgba8888.ToColorType()); + var disp = session.Display; + var gl = disp.GlInterface; + gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var fb); - var nfo = new DrawingContextImpl.CreateInfo + var size = session.Size; + var scaling = session.Scaling; + if (size.Width <= 0 || size.Height <= 0 || scaling < 0) { - GrContext = _grContext, - Canvas = surface.Canvas, - Dpi = SkiaPlatform.DefaultDpi * scaling, - VisualBrushRenderer = visualBrushRenderer, - DisableTextLcdRendering = true - }; + session.Dispose(); + throw new InvalidOperationException( + $"Can't create drawing context for surface with {size} size and {scaling} scaling"); + } - return new DrawingContextImpl(nfo, Disposable.Create(() => + gl.Viewport(0, 0, size.Width, size.Height); + gl.ClearStencil(0); + gl.ClearColor(0, 0, 0, 0); + gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + lock (_grContext) { + _grContext.ResetContext(); + + GRBackendRenderTarget renderTarget = + new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize, + new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat())); + var surface = SKSurface.Create(_grContext, renderTarget, + GRSurfaceOrigin.BottomLeft, + GRPixelConfig.Rgba8888.ToColorType()); + + var nfo = new DrawingContextImpl.CreateInfo + { + GrContext = _grContext, + Canvas = surface.Canvas, + Dpi = SkiaPlatform.DefaultDpi * scaling, + VisualBrushRenderer = visualBrushRenderer, + DisableTextLcdRendering = true + }; + - surface.Canvas.Flush(); - surface.Dispose(); - renderTarget.Dispose(); - _grContext.Flush(); + var ctx = new DrawingContextImpl(nfo, Disposable.Create(() => + { + + surface.Canvas.Flush(); + surface.Dispose(); + renderTarget.Dispose(); + _grContext.Flush(); + session.Dispose(); + })); + success = true; + return ctx; + } + } + finally + { + if(!success) session.Dispose(); - })); } } } diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index c89d0a15cf..71c398481b 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -142,27 +142,33 @@ namespace Avalonia.Win32.Interop.Wpf protected override void OnLostFocus(RoutedEventArgs e) => LostFocus?.Invoke(); - InputModifiers GetModifiers() + RawInputModifiers GetModifiers(MouseEventArgs e) { var state = Keyboard.Modifiers; - var rv = default(InputModifiers); + var rv = default(RawInputModifiers); if (state.HasFlag(ModifierKeys.Windows)) - rv |= InputModifiers.Windows; + rv |= RawInputModifiers.Meta; if (state.HasFlag(ModifierKeys.Alt)) - rv |= InputModifiers.Alt; + rv |= RawInputModifiers.Alt; if (state.HasFlag(ModifierKeys.Control)) - rv |= InputModifiers.Control; + rv |= RawInputModifiers.Control; if (state.HasFlag(ModifierKeys.Shift)) - rv |= InputModifiers.Shift; - //TODO: mouse modifiers - - + rv |= RawInputModifiers.Shift; + if (e != null) + { + if (e.LeftButton == MouseButtonState.Pressed) + rv |= RawInputModifiers.LeftMouseButton; + if (e.RightButton == MouseButtonState.Pressed) + rv |= RawInputModifiers.RightMouseButton; + if (e.MiddleButton == MouseButtonState.Pressed) + rv |= RawInputModifiers.MiddleMouseButton; + } return rv; } void MouseEvent(RawPointerEventType type, MouseEventArgs e) => _ttl.Input?.Invoke(new RawPointerEventArgs(_mouse, (uint)e.Timestamp, _inputRoot, type, - e.GetPosition(this).ToAvaloniaPoint(), GetModifiers())); + e.GetPosition(this).ToAvaloniaPoint(), GetModifiers(e))); protected override void OnMouseDown(MouseButtonEventArgs e) { @@ -201,19 +207,19 @@ namespace Avalonia.Win32.Interop.Wpf protected override void OnMouseWheel(MouseWheelEventArgs e) => _ttl.Input?.Invoke(new RawMouseWheelEventArgs(_mouse, (uint) e.Timestamp, _inputRoot, - e.GetPosition(this).ToAvaloniaPoint(), new Vector(0, e.Delta), GetModifiers())); + e.GetPosition(this).ToAvaloniaPoint(), new Vector(0, e.Delta), GetModifiers(e))); protected override void OnMouseLeave(MouseEventArgs e) => MouseEvent(RawPointerEventType.LeaveWindow, e); protected override void OnKeyDown(KeyEventArgs e) => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint) e.Timestamp, RawKeyEventType.KeyDown, (Key) e.Key, - GetModifiers())); + GetModifiers(null))); protected override void OnKeyUp(KeyEventArgs e) => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint)e.Timestamp, RawKeyEventType.KeyUp, (Key)e.Key, - GetModifiers())); + GetModifiers(null))); protected override void OnTextInput(TextCompositionEventArgs e) => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, e.Text)); @@ -240,5 +246,7 @@ namespace Avalonia.Win32.Interop.Wpf return new Vector(1, 1); return new Vector(src.TransformToDevice.M11, src.TransformToDevice.M22); } + + public IPopupImpl CreatePopup() => null; } } diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index f1fd74f931..b45138c27a 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -56,7 +56,7 @@ namespace Avalonia.Win32 {StandardCursorType.Wait, 32514}, //Same as SizeNorthSouth {StandardCursorType.TopSide, 32645}, - {StandardCursorType.BottomSize, 32645}, + {StandardCursorType.BottomSide, 32645}, //Same as SizeWestEast {StandardCursorType.LeftSide, 32644}, {StandardCursorType.RightSide, 32644}, diff --git a/src/Windows/Avalonia.Win32/DragSource.cs b/src/Windows/Avalonia.Win32/DragSource.cs index a1bc5023a5..a8d74571a1 100644 --- a/src/Windows/Avalonia.Win32/DragSource.cs +++ b/src/Windows/Avalonia.Win32/DragSource.cs @@ -8,10 +8,11 @@ namespace Avalonia.Win32 { class DragSource : IPlatformDragSource { - public Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects) + public Task DoDragDrop(PointerEventArgs triggerEvent, + IDataObject data, DragDropEffects allowedEffects) { Dispatcher.UIThread.VerifyAccess(); - + triggerEvent.Pointer.Capture(null); OleDragSource src = new OleDragSource(); DataObject dataObject = new DataObject(data); int allowed = (int)OleDropTarget.ConvertDropEffect(allowedEffects); diff --git a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs index fee1fe2ae6..fda5483b00 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs @@ -14,31 +14,31 @@ namespace Avalonia.Win32.Input public new static WindowsKeyboardDevice Instance { get; } = new WindowsKeyboardDevice(); - public InputModifiers Modifiers + public RawInputModifiers Modifiers { get { UpdateKeyStates(); - InputModifiers result = 0; + RawInputModifiers result = 0; if (IsDown(Key.LeftAlt) || IsDown(Key.RightAlt)) { - result |= InputModifiers.Alt; + result |= RawInputModifiers.Alt; } if (IsDown(Key.LeftCtrl) || IsDown(Key.RightCtrl)) { - result |= InputModifiers.Control; + result |= RawInputModifiers.Control; } if (IsDown(Key.LeftShift) || IsDown(Key.RightShift)) { - result |= InputModifiers.Shift; + result |= RawInputModifiers.Shift; } if (IsDown(Key.LWin) || IsDown(Key.RWin)) { - result |= InputModifiers.Windows; + result |= RawInputModifiers.Meta; } return result; diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index 28065566d0..8ac0f25598 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -45,23 +45,23 @@ namespace Avalonia.Win32 return result; } - private static InputModifiers ConvertKeyState(int grfKeyState) + private static RawInputModifiers ConvertKeyState(int grfKeyState) { - InputModifiers modifiers = InputModifiers.None; + var modifiers = RawInputModifiers.None; var state = (UnmanagedMethods.ModifierKeys)grfKeyState; if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) - modifiers |= InputModifiers.LeftMouseButton; + modifiers |= RawInputModifiers.LeftMouseButton; if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) - modifiers |= InputModifiers.MiddleMouseButton; + modifiers |= RawInputModifiers.MiddleMouseButton; if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) - modifiers |= InputModifiers.RightMouseButton; + modifiers |= RawInputModifiers.RightMouseButton; if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_SHIFT)) - modifiers |= InputModifiers.Shift; + modifiers |= RawInputModifiers.Shift; if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_CONTROL)) - modifiers |= InputModifiers.Control; + modifiers |= RawInputModifiers.Control; if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_ALT)) - modifiers |= InputModifiers.Alt; + modifiers |= RawInputModifiers.Alt; return modifiers; } @@ -127,7 +127,7 @@ namespace Avalonia.Win32 default(Point), null, DragDropEffects.None, - InputModifiers.None + RawInputModifiers.None )); return UnmanagedMethods.HRESULT.S_OK; } diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 39f1a95466..c9aa1ce4e7 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Platform; using Avalonia.Win32.Interop; @@ -57,5 +58,19 @@ namespace Avalonia.Win32 return base.WndProc(hWnd, msg, wParam, lParam); } } + + public PopupImpl(IWindowBaseImpl parent) + { + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); + } + + private void MoveResize(PixelPoint position, Size size, double scaling) + { + Move(position); + Resize(size); + //TODO: We ignore the scaling override for now + } + + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Windows/Avalonia.Win32/RenderTimer.cs b/src/Windows/Avalonia.Win32/RenderTimer.cs deleted file mode 100644 index 7dbb745a23..0000000000 --- a/src/Windows/Avalonia.Win32/RenderTimer.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Reactive.Disposables; -using System.Threading; -using Avalonia.Rendering; -using Avalonia.Win32.Interop; - -namespace Avalonia.Win32 -{ - internal class RenderTimer : DefaultRenderTimer - { - private UnmanagedMethods.WaitOrTimerCallback timerDelegate; - - private static IntPtr _timerQueue; - - private static void EnsureTimerQueueCreated() - { - if (Volatile.Read(ref _timerQueue) == null) - { - var queue = UnmanagedMethods.CreateTimerQueue(); - if (Interlocked.CompareExchange(ref _timerQueue, queue, IntPtr.Zero) != IntPtr.Zero) - { - UnmanagedMethods.DeleteTimerQueueEx(queue, IntPtr.Zero); - } - } - } - - public RenderTimer(int framesPerSecond) - : base(framesPerSecond) - { - } - - protected override IDisposable StartCore(Action tick) - { - EnsureTimerQueueCreated(); - var msPerFrame = 1000 / FramesPerSecond; - - timerDelegate = (_, __) => tick(TimeSpan.FromMilliseconds(Environment.TickCount)); - - UnmanagedMethods.CreateTimerQueueTimer( - out var timer, - _timerQueue, - timerDelegate, - IntPtr.Zero, - (uint)msPerFrame, - (uint)msPerFrame, - 0 - ); - - return Disposable.Create(() => - { - timerDelegate = null; - UnmanagedMethods.DeleteTimerQueueTimer(_timerQueue, timer, IntPtr.Zero); - }); - } - } -} diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index c45bf6389e..ac3fa021f1 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -41,6 +41,7 @@ namespace Avalonia public bool UseDeferredRendering { get; set; } = true; public bool AllowEglInitialization { get; set; } public bool? EnableMultitouch { get; set; } + public bool OverlayPopups { get; set; } } } @@ -61,6 +62,7 @@ namespace Avalonia.Win32 } public static bool UseDeferredRendering => Options.UseDeferredRendering; + internal static bool UseOverlayPopups => Options.OverlayPopups; public static Win32PlatformOptions Options { get; private set; } public Size DoubleClickSize => new Size( @@ -84,11 +86,13 @@ namespace Avalonia.Win32 .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(new RenderTimer(60)) + .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToSingleton() .Bind().ToConstant(s_instance) .Bind().ToSingleton() - .Bind().ToConstant(s_instance); + .Bind().ToConstant(s_instance) + .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()); + if (options.AllowEglInitialization) Win32GlManager.Initialize(); @@ -210,11 +214,6 @@ namespace Avalonia.Win32 return embedded; } - public IPopupImpl CreatePopup() - { - return new PopupImpl(); - } - public IWindowIconImpl LoadIcon(string fileName) { using (var stream = File.OpenRead(fileName)) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 2f7805884d..9bd58c10bc 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -131,6 +131,8 @@ namespace Avalonia.Win32 } } + public void Move(PixelPoint point) => Position = point; + public void SetMinMaxSize(Size minSize, Size maxSize) { _minSize = minSize; @@ -248,10 +250,7 @@ namespace Avalonia.Win32 UnmanagedMethods.SetActiveWindow(_hwnd); } - public IPopupImpl CreatePopup() - { - return new PopupImpl(); - } + public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this); public void Dispose() { @@ -763,16 +762,16 @@ namespace Avalonia.Win32 return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); } - static InputModifiers GetMouseModifiers(IntPtr wParam) + static RawInputModifiers GetMouseModifiers(IntPtr wParam) { var keys = (UnmanagedMethods.ModifierKeys)ToInt32(wParam); var modifiers = WindowsKeyboardDevice.Instance.Modifiers; if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) - modifiers |= InputModifiers.LeftMouseButton; + modifiers |= RawInputModifiers.LeftMouseButton; if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) - modifiers |= InputModifiers.RightMouseButton; + modifiers |= RawInputModifiers.RightMouseButton; if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) - modifiers |= InputModifiers.MiddleMouseButton; + modifiers |= RawInputModifiers.MiddleMouseButton; return modifiers; } diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs new file mode 100644 index 0000000000..102e027584 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Controls.Platform; + +namespace Avalonia.Win32 +{ + internal class WindowsMountedVolumeInfoListener : IDisposable + { + private readonly CompositeDisposable _disposables; + private readonly ObservableCollection _targetObs = new ObservableCollection(); + private bool _beenDisposed = false; + private ObservableCollection mountedDrives; + + public WindowsMountedVolumeInfoListener(ObservableCollection mountedDrives) + { + this.mountedDrives = mountedDrives; + _disposables = new CompositeDisposable(); + + var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) + .Subscribe(Poll); + + _disposables.Add(pollTimer); + + Poll(0); + } + + private void Poll(long _) + { + var allDrives = DriveInfo.GetDrives(); + + var mountVolInfos = allDrives + .Select(p => new MountedVolumeInfo() + { + VolumeLabel = p.VolumeLabel, + VolumePath = p.RootDirectory.FullName, + VolumeSizeBytes = (ulong)p.TotalSize + }) + .ToArray(); + + if (_targetObs.SequenceEqual(mountVolInfos)) + return; + else + { + _targetObs.Clear(); + + foreach (var i in mountVolInfos) + _targetObs.Add(i); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_beenDisposed) + { + if (disposing) + { + + } + _beenDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs new file mode 100644 index 0000000000..e1b5f5a3a0 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.ObjectModel; +using Avalonia.Controls.Platform; + +namespace Avalonia.Win32 +{ + public class WindowsMountedVolumeInfoProvider : IMountedVolumeInfoProvider + { + public IDisposable Listen(ObservableCollection mountedDrives) + { + Contract.Requires(mountedDrives != null); + return new WindowsMountedVolumeInfoListener(mountedDrives); + } + } +} diff --git a/src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs b/src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs index be32d12315..cdf244d330 100644 --- a/src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs +++ b/src/iOS/Avalonia.iOS/Specific/KeyboardEventsHelper.cs @@ -76,7 +76,7 @@ namespace Avalonia.iOS.Specific private void HandleKey(Key key, RawKeyEventType type) { - var rawKeyEvent = new RawKeyEventArgs(KeyboardDevice.Instance, (uint)DateTime.Now.Ticks, type, key, InputModifiers.None); + var rawKeyEvent = new RawKeyEventArgs(KeyboardDevice.Instance, (uint)DateTime.Now.Ticks, type, key, RawInputModifiers.None); _view.Input(rawKeyEvent); } diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs index 15e8b35056..a5342b227f 100644 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs @@ -92,7 +92,7 @@ namespace Avalonia.iOS _inputRoot, RawPointerEventType.LeftButtonUp, location, - InputModifiers.None)); + RawInputModifiers.None)); } } @@ -105,10 +105,10 @@ namespace Avalonia.iOS var location = touch.LocationInView(this).ToAvalonia(); _touchLastPoint = location; Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot, - RawPointerEventType.Move, location, InputModifiers.None)); + RawPointerEventType.Move, location, RawInputModifiers.None)); Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot, - RawPointerEventType.LeftButtonDown, location, InputModifiers.None)); + RawPointerEventType.LeftButtonDown, location, RawInputModifiers.None)); } } @@ -120,19 +120,21 @@ namespace Avalonia.iOS var location = touch.LocationInView(this).ToAvalonia(); if (iOSPlatform.MouseDevice.Captured != null) Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot, - RawPointerEventType.Move, location, InputModifiers.LeftMouseButton)); + RawPointerEventType.Move, location, RawInputModifiers.LeftMouseButton)); else { //magic number based on test - correction of 0.02 is working perfect double correction = 0.02; Input?.Invoke(new RawMouseWheelEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, - _inputRoot, location, (location - _touchLastPoint) * correction, InputModifiers.LeftMouseButton)); + _inputRoot, location, (location - _touchLastPoint) * correction, RawInputModifiers.LeftMouseButton)); } _touchLastPoint = location; } } public ILockedFramebuffer Lock() => new EmulatedFramebuffer(this); + + public IPopupImpl CreatePopup() => null; } } diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs index 8a38a00493..5c01e6a588 100644 --- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs +++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs @@ -148,6 +148,23 @@ namespace Avalonia.Base.UnitTests.Collections Assert.True(raised); } + [Fact] + public void AddRange_Items_Should_Raise_Correct_CollectionChanged() + { + var target = new AvaloniaList(); + + var eventItems = new List(); + + target.CollectionChanged += (sender, args) => + { + eventItems.AddRange(args.NewItems.Cast()); + }; + + target.AddRange(Enumerable.Range(0,10).Select(i => new object())); + + Assert.Equal(eventItems, target); + } + [Fact] public void Replacing_Item_Should_Raise_CollectionChanged() { diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs index b66dd610dd..c472fffb38 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs @@ -100,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Data.Core // Value is first signalled without an error as validation hasn't been updated. new BindingNotification(-5), - new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, -5), + new BindingNotification(new DataValidationException("Must be positive"), BindingErrorType.DataValidationError, -5), // Exception is thrown by trying to set value to "foo". new BindingNotification( @@ -108,7 +108,7 @@ namespace Avalonia.Base.UnitTests.Data.Core BindingErrorType.DataValidationError), // Value is set then validation is updated. - new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, 5), + new BindingNotification(new DataValidationException("Must be positive"), BindingErrorType.DataValidationError, 5), new BindingNotification(5), }, result); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index b56afa33a4..a2ef8eedad 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -574,7 +574,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var source = new Class1 { Foo = "foo" }; var target = new PropertyAccessorNode("Foo", false); Assert.NotNull(target); - target.Target = new WeakReference(source); + target.Target = new WeakReference(source); target.Subscribe(_ => { }); target.Unsubscribe(); target.Unsubscribe(); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs index 378c225e23..435ead0b80 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs @@ -20,7 +20,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var target = new DataAnnotationsValidationPlugin(); var data = new Data(); - Assert.True(target.Match(new WeakReference(data), nameof(Data.Between5And10))); + Assert.True(target.Match(new WeakReference(data), nameof(Data.Between5And10))); } [Fact] @@ -29,7 +29,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var target = new DataAnnotationsValidationPlugin(); var data = new Data(); - Assert.True(target.Match(new WeakReference(data), nameof(Data.PhoneNumber))); + Assert.True(target.Match(new WeakReference(data), nameof(Data.PhoneNumber))); } [Fact] @@ -38,7 +38,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var target = new DataAnnotationsValidationPlugin(); var data = new Data(); - Assert.False(target.Match(new WeakReference(data), nameof(Data.Unvalidated))); + Assert.False(target.Match(new WeakReference(data), nameof(Data.Unvalidated))); } [Fact] @@ -47,8 +47,8 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new DataAnnotationsValidationPlugin(); var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Between5And10)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Between5And10), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Between5And10)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Between5And10), accessor); var result = new List(); var errmsg = new RangeAttribute(5, 10).FormatErrorMessage(nameof(Data.Between5And10)); @@ -79,8 +79,8 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new DataAnnotationsValidationPlugin(); var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber), accessor); var result = new List(); validator.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs index 2a307f9a61..6bd5fe5093 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs @@ -19,8 +19,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new ExceptionValidationPlugin(); var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor); var result = new List(); validator.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs index 383030cb6c..26b64182e1 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs @@ -18,8 +18,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new IndeiValidationPlugin(); var data = new Data { Maximum = 5 }; - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); var result = new List(); validator.Subscribe(x => result.Add(x)); @@ -37,13 +37,13 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins new BindingNotification(6), // Then the ErrorsChanged event is fired. - new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6), + new BindingNotification(new DataValidationException("Must be less than Maximum"), BindingErrorType.DataValidationError, 6), // Maximum is changed to 10 so value is now valid. new BindingNotification(6), // And Maximum is changed back to 5. - new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6), + new BindingNotification(new DataValidationException("Must be less than Maximum"), BindingErrorType.DataValidationError, 6), }, result); } @@ -53,8 +53,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new IndeiValidationPlugin(); var data = new Data { Maximum = 5 }; - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); validator.Subscribe(_ => { }); diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs new file mode 100644 index 0000000000..06716f7102 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs @@ -0,0 +1,15 @@ +using Avalonia.Controls; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Base +{ + [MemoryDiagnoser] + public class AvaloniaObjectInitializationBenchmark + { + [Benchmark(OperationsPerInvoke = 1000)] + public Button InitializeButton() + { + return new Button(); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 015a122677..ef7dc33f76 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -982,6 +982,8 @@ namespace Avalonia.Controls.UnitTests AutoCompleteBox control = CreateControl(); control.Items = CreateSimpleStringArray(); TextBox textBox = GetTextBox(control); + var window = new Window {Content = control}; + window.ApplyTemplate(); Dispatcher.UIThread.RunJobs(); test.Invoke(control, textBox); } @@ -1027,7 +1029,8 @@ namespace Avalonia.Controls.UnitTests var popup = new Popup { - Name = "PART_Popup" + Name = "PART_Popup", + PlacementTarget = control }.RegisterInNameScope(scope); var panel = new Panel(); diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 58d205deaa..522afc9546 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }; + new Window { Content = target }.ApplyTemplate(); int openedCount = 0; @@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests openedCount++; }; - sut.Open(null); + sut.Open(target); Assert.Equal(1, openedCount); } @@ -53,9 +53,9 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }; + new Window { Content = target }.ApplyTemplate(); - sut.Open(null); + sut.Open(target); int closedCount = 0; @@ -84,7 +84,8 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }; + var window = new Window {Content = target}; + window.ApplyTemplate(); _mouse.Click(target, MouseButton.Right); @@ -112,7 +113,8 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - var window = new Window { Content = target }; + var window = new Window {Content = target}; + window.ApplyTemplate(); _mouse.Click(target, MouseButton.Right); @@ -151,7 +153,7 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] + [Fact(Skip = "The only reason this test was 'passing' before was that the author forgot to call Window.ApplyTemplate()")] public void Cancelling_Closing_Leaves_ContextMenuOpen() { using (Application()) @@ -165,7 +167,9 @@ namespace Avalonia.Controls.UnitTests { ContextMenu = sut }; - new Window { Content = target }; + + var window = new Window {Content = target}; + window.ApplyTemplate(); sut.ContextMenuClosing += (c, e) => { eventCalled = true; e.Cancel = true; }; @@ -190,12 +194,12 @@ namespace Avalonia.Controls.UnitTests screenImpl.Setup(x => x.ScreenCount).Returns(1); screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(screen, screen, true) }); - var windowImpl = new Mock(); - windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); - - popupImpl = new Mock(); + popupImpl = MockWindowingPlatform.CreatePopupMock(); popupImpl.SetupGet(x => x.Scaling).Returns(1); + var windowImpl = MockWindowingPlatform.CreateWindowMock(() => popupImpl.Object); + windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); + var services = TestServices.StyledWindow.With( inputManager: new InputManager(), windowImpl: windowImpl.Object, diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index ba4d6ca9c5..ff11bc513d 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -11,14 +11,14 @@ namespace Avalonia.Controls.UnitTests.Platform public class DefaultMenuInteractionHandlerTests { static PointerEventArgs CreateArgs(RoutedEvent ev, IInteractive source) - => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, 0, new PointerPointProperties(), default); + => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, 0, PointerPointProperties.None, default); static PointerPressedEventArgs CreatePressed(IInteractive source) => new PointerPressedEventArgs(source, - new FakePointer(), (IVisual)source, default,0, new PointerPointProperties {IsLeftButtonPressed = true}, + new FakePointer(), (IVisual)source, default,0, new PointerPointProperties (RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), default); static PointerReleasedEventArgs CreateReleased(IInteractive source) => new PointerReleasedEventArgs(source, - new FakePointer(), (IVisual)source, default,0, new PointerPointProperties(), default, MouseButton.Left); + new FakePointer(), (IVisual)source, default,0, new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonReleased), default); public class TopLevel { diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index 7d05547799..6ab9c345d4 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -1,7 +1,11 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.ComponentModel; using System.Linq; +using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -256,7 +260,6 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.IsType(target.Child); } - [Fact] public void Should_Not_Bind_Old_Child_To_New_DataContext() { @@ -281,6 +284,65 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Content = 42; } + [Fact] + public void Should_Not_Bind_Child_To_Wrong_DataContext_When_Removing() + { + // Test for issue #2823 + var canvas = new Canvas(); + var (target, host) = CreateTarget(); + var viewModel = new TestViewModel { Content = "foo" }; + var dataContexts = new List(); + + target.Bind(ContentPresenter.ContentProperty, (IBinding)new TemplateBinding(ContentControl.ContentProperty)); + canvas.GetObservable(ContentPresenter.DataContextProperty).Subscribe(x => dataContexts.Add(x)); + + host.DataTemplates.Add(new FuncDataTemplate((_, __) => canvas)); + host.Bind(ContentControl.ContentProperty, new Binding(nameof(TestViewModel.Content))); + host.DataContext = viewModel; + + Assert.Same(canvas, target.Child); + + viewModel.Content = 42; + + Assert.Equal(new object[] + { + null, + "foo", + null, + }, dataContexts); + } + + [Fact] + public void Should_Set_InheritanceParent_Even_When_LogicalParent_Is_Already_Set() + { + var logicalParent = new Canvas(); + var child = new TextBlock(); + var (target, host) = CreateTarget(); + + ((ISetLogicalParent)child).SetParent(logicalParent); + target.Content = child; + + Assert.Same(logicalParent, child.Parent); + + // InheritanceParent is exposed via StylingParent. + Assert.Same(target, ((IStyledElement)child).StylingParent); + } + + [Fact] + public void Should_Reset_InheritanceParent_When_Child_Removed() + { + var logicalParent = new Canvas(); + var child = new TextBlock(); + var (target, _) = CreateTarget(); + + ((ISetLogicalParent)child).SetParent(logicalParent); + target.Content = child; + target.Content = null; + + // InheritanceParent is exposed via StylingParent. + Assert.Same(logicalParent, ((IStyledElement)child).StylingParent); + } + (ContentPresenter presenter, ContentControl templatedParent) CreateTarget() { var templatedParent = new ContentControl @@ -302,5 +364,25 @@ namespace Avalonia.Controls.UnitTests.Presenters { public IControl Child { get; set; } } + + private class TestViewModel : INotifyPropertyChanged + { + private object _content; + + public object Content + { + get => _content; + set + { + if (_content != value) + { + _content = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Content))); + } + } + } + + public event PropertyChangedEventHandler PropertyChanged; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index ab75a87110..59f3ae44c2 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -251,5 +251,21 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Content = 42; } + + [Fact] + public void Should_Reset_InheritanceParent_When_Child_Removed() + { + var logicalParent = new Canvas(); + var child = new TextBlock(); + var target = new ContentPresenter(); + var root = new TestRoot(target); + + ((ISetLogicalParent)child).SetParent(logicalParent); + target.Content = child; + target.Content = null; + + // InheritanceParent is exposed via StylingParent. + Assert.Same(logicalParent, ((IStyledElement)child).StylingParent); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs index 09970926fa..c30e81a1cb 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs @@ -98,5 +98,22 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); Assert.IsType(target.Child); } + + [Fact] + public void Should_Reset_InheritanceParent_When_Child_Removed() + { + var logicalParent = new Canvas(); + var child = new TextBlock(); + var target = new ContentPresenter(); + + ((ISetLogicalParent)child).SetParent(logicalParent); + target.Content = child; + target.UpdateChild(); + target.Content = null; + target.UpdateChild(); + + // InheritanceParent is exposed via StylingParent. + Assert.Same(logicalParent, ((IStyledElement)child).StylingParent); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 059146f17d..0ebe6833d3 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -21,7 +21,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var target = CreateTarget(); + var target = CreateTarget(new Window()); Assert.True(((ILogical)target).IsAttachedToLogicalTree); } @@ -32,7 +32,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var target = CreateTarget(); + var target = CreateTarget(new Window()); Assert.True(target.Presenter.IsAttachedToLogicalTree); } @@ -43,28 +43,70 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { + var window = new Window(); var target = new TemplatedControlWithPopup { PopupContent = new Canvas(), }; + window.Content = target; - var root = new TestRoot { Child = target }; - + window.ApplyTemplate(); target.ApplyTemplate(); target.Popup.Open(); - Assert.Equal(target.Popup, ((IStyleHost)target.Popup.PopupRoot).StylingParent); + Assert.Equal(target.Popup, ((IStyleHost)target.Popup.Host).StylingParent); } } + [Fact] + public void PopupRoot_Should_Have_Template_Applied() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + var target = new Popup {PlacementMode = PlacementMode.Pointer}; + var child = new Control(); + + window.Content = target; + window.ApplyTemplate(); + target.Open(); + + + Assert.Single(((Visual)target.Host).GetVisualChildren()); + + var templatedChild = ((Visual)target.Host).GetVisualChildren().Single(); + + Assert.IsType(templatedChild); + var contentPresenter = templatedChild.VisualChildren.Single(); + Assert.IsType(contentPresenter); + + + Assert.Equal((PopupRoot)target.Host, ((IControl)templatedChild).TemplatedParent); + Assert.Equal((PopupRoot)target.Host, ((IControl)contentPresenter).TemplatedParent); + } + } + + [Fact] + public void PopupRoot_Should_Have_Null_VisualParent() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Popup() {PlacementTarget = new Window()}; + + target.Open(); + + Assert.Null(((Visual)target.Host).GetVisualParent()); + } + } + [Fact] public void Attaching_PopupRoot_To_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { var child = new Decorator(); - var target = CreateTarget(); var window = new Window(); + var target = CreateTarget(window); var detachedCount = 0; var attachedCount = 0; @@ -88,8 +130,8 @@ namespace Avalonia.Controls.UnitTests.Primitives using (UnitTestApplication.Start(TestServices.StyledWindow)) { var child = new Decorator(); - var target = CreateTarget(); var window = new Window(); + var target = CreateTarget(window); var detachedCount = 0; var attachedCount = 0; @@ -117,22 +159,23 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { + var window = new Window(); var target = new TemplatedControlWithPopup { PopupContent = new Canvas(), }; + window.Content = target; - var root = new TestRoot { Child = target }; - + window.ApplyTemplate(); target.ApplyTemplate(); target.Popup.Open(); target.PopupContent = null; } } - private PopupRoot CreateTarget() + private PopupRoot CreateTarget(TopLevel popupParent) { - var result = new PopupRoot + var result = new PopupRoot(popupParent, popupParent.PlatformImpl.CreatePopup()) { Template = new FuncControlTemplate((parent, scope) => new ContentPresenter @@ -158,6 +201,7 @@ namespace Avalonia.Controls.UnitTests.Primitives new Popup { [!Popup.ChildProperty] = parent[!TemplatedControlWithPopup.PopupContentProperty], + PlacementTarget = parent }); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 2e22725125..7cb9fccee8 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -22,6 +22,8 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class PopupTests { + protected bool UsePopupHost; + [Fact] public void Setting_Child_Should_Set_Child_Controls_LogicalParent() { @@ -137,20 +139,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new Popup(); - Assert.Null(target.PopupRoot); - } - } - - [Fact] - public void PopupRoot_Should_Have_Null_VisualParent() - { - using (CreateServices()) - { - var target = new Popup(); - - target.Open(); - - Assert.Null(target.PopupRoot.GetVisualParent()); + Assert.Null(((Visual)target.Host)); } } @@ -159,12 +148,12 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var target = new Popup(); + var target = new Popup() {PlacementTarget = PreparedWindow()}; target.Open(); - Assert.Equal(target, target.PopupRoot.Parent); - Assert.Equal(target, target.PopupRoot.GetLogicalParent()); + Assert.Equal(target, ((Visual)target.Host).Parent); + Assert.Equal(target, ((Visual)target.Host).GetLogicalParent()); } } @@ -173,15 +162,15 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var target = new Popup(); - var root = new TestRoot { Child = target }; + var target = new Popup() {PlacementMode = PlacementMode.Pointer}; + var root = PreparedWindow(target); target.Open(); - var popupRoot = (ILogical)target.PopupRoot; + var popupRoot = (ILogical)((Visual)target.Host); Assert.True(popupRoot.IsAttachedToLogicalTree); - root.Child = null; + root.Content = null; Assert.False(((ILogical)target).IsAttachedToLogicalTree); } } @@ -191,8 +180,8 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var window = new Window(); - var target = new Popup(); + var window = PreparedWindow(); + var target = new Popup() {PlacementMode = PlacementMode.Pointer}; window.Content = target; @@ -214,10 +203,11 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var window = new Window(); - var target = new Popup(); + var window = PreparedWindow(); + var target = new Popup() {PlacementMode = PlacementMode.Pointer}; window.Content = target; + window.ApplyTemplate(); target.Open(); int closedCount = 0; @@ -233,46 +223,28 @@ namespace Avalonia.Controls.UnitTests.Primitives } } - [Fact] - public void PopupRoot_Should_Have_Template_Applied() - { - using (CreateServices()) - { - var window = new Window(); - var target = new Popup(); - var child = new Control(); - - window.Content = target; - target.Open(); - - Assert.Single(target.PopupRoot.GetVisualChildren()); - - var templatedChild = target.PopupRoot.GetVisualChildren().Single(); - Assert.IsType(templatedChild); - Assert.Equal(target.PopupRoot, ((IControl)templatedChild).TemplatedParent); - } - } - + [Fact] public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent() { using (CreateServices()) { PopupContentControl target; - var root = new TestRoot + var root = PreparedWindow(target = new PopupContentControl { - Child = target = new PopupContentControl - { - Content = new Border(), - Template = new FuncControlTemplate(PopupContentControlTemplate), - }, - StylingParent = AvaloniaLocator.Current.GetService() - }; + Content = new Border(), + Template = new FuncControlTemplate(PopupContentControlTemplate), + }); + root.Show(); target.ApplyTemplate(); + var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup"); popup.Open(); - var popupRoot = popup.PopupRoot; + + var popupRoot = (Control)popup.Host; + popupRoot.Measure(Size.Infinity); + popupRoot.Arrange(new Rect(popupRoot.DesiredSize)); var children = popupRoot.GetVisualDescendants().ToList(); var types = children.Select(x => x.GetType().Name).ToList(); @@ -280,6 +252,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new[] { + "VisualLayerManager", "ContentPresenter", "ContentPresenter", "Border", @@ -293,6 +266,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new object[] { + popupRoot, popupRoot, target, null, @@ -301,6 +275,13 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + Window PreparedWindow(object content = null) + { + var w = new Window {Content = content}; + w.ApplyTemplate(); + return w; + } + [Fact] public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit() { @@ -311,6 +292,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = child = new TestControl(), DataContext = "foo", + PlacementTarget = PreparedWindow() }; var beginCalled = false; @@ -330,46 +312,32 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.False(beginCalled); } } - - - private static IDisposable CreateServices() + + [Fact] + public void Popup_Host_Type_Should_Match_Platform_Preference() { - var result = AvaloniaLocator.EnterScope(); - - var styles = new Styles + using (CreateServices()) { - new Style(x => x.OfType()) - { - Setters = new[] - { - new Setter(TemplatedControl.TemplateProperty, new FuncControlTemplate(PopupRootTemplate)), - } - }, - }; - - var globalStyles = new Mock(); - globalStyles.Setup(x => x.IsStylesInitialized).Returns(true); - globalStyles.Setup(x => x.Styles).Returns(styles); - - var renderInterface = new Mock(); - - AvaloniaLocator.CurrentMutable - .Bind().ToFunc(() => globalStyles.Object) - .Bind().ToConstant(new WindowingPlatformMock()) - .Bind().ToTransient() - .Bind().ToFunc(() => renderInterface.Object) - .Bind().ToConstant(new InputManager()); - - return result; + var target = new Popup() {PlacementTarget = PreparedWindow()}; + + target.Open(); + if (UsePopupHost) + Assert.IsType(target.Host); + else + Assert.IsType(target.Host); + } } - private static IControl PopupRootTemplate(PopupRoot control, INameScope scope) + private IDisposable CreateServices() { - return new ContentPresenter - { - Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], - }.RegisterInNameScope(scope); + return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: + new MockWindowingPlatform(null, + () => + { + if(UsePopupHost) + return null; + return MockWindowingPlatform.CreatePopupMock().Object; + }))); } private static IControl PopupContentControlTemplate(PopupContentControl control, INameScope scope) @@ -377,6 +345,7 @@ namespace Avalonia.Controls.UnitTests.Primitives return new Popup { Name = "popup", + PlacementTarget = control, Child = new ContentPresenter { [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], @@ -401,4 +370,12 @@ namespace Avalonia.Controls.UnitTests.Primitives } } } + + public class PopupTestsWithPopupRoot : PopupTests + { + public PopupTestsWithPopupRoot() + { + UsePopupHost = true; + } + } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index 72f2b8022f..a7010c521b 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -102,6 +102,25 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Null(target.SelectedItem); } + [Fact] + public void Removing_Selected_First_Item_Should_Select_Next_Item() + { + var items = new AvaloniaList(new[] { "foo", "bar" }); + var target = new TestSelector + { + Items = items, + Template = Template(), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + items.RemoveAt(0); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[0].Classes); + } + private FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) => diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 35f0b39210..225eca17b2 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls.UnitTests Text = "1234" }; - RaiseKeyEvent(target, Key.A, InputModifiers.Control); + RaiseKeyEvent(target, Key.A, KeyModifiers.Control); Assert.Equal(0, target.SelectionStart); Assert.Equal(4, target.SelectionEnd); @@ -72,7 +72,7 @@ namespace Avalonia.Controls.UnitTests Template = CreateTemplate() }; - RaiseKeyEvent(target, Key.A, InputModifiers.Control); + RaiseKeyEvent(target, Key.A, KeyModifiers.Control); Assert.Equal(0, target.SelectionStart); Assert.Equal(0, target.SelectionEnd); @@ -90,7 +90,7 @@ namespace Avalonia.Controls.UnitTests Text = "1234" }; - RaiseKeyEvent(target, Key.Z, InputModifiers.Control); + RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); Assert.Equal("1234", target.Text); } @@ -136,29 +136,29 @@ namespace Avalonia.Controls.UnitTests }; // (First| Second Third Fourth) - RaiseKeyEvent(textBox, Key.Back, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); Assert.Equal(" Second Third Fourth", textBox.Text); // ( Second |Third Fourth) textBox.CaretIndex = 8; - RaiseKeyEvent(textBox, Key.Back, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); Assert.Equal(" Third Fourth", textBox.Text); // ( Thi|rd Fourth) textBox.CaretIndex = 4; - RaiseKeyEvent(textBox, Key.Back, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); Assert.Equal(" rd Fourth", textBox.Text); // ( rd F[ou]rth) textBox.SelectionStart = 5; textBox.SelectionEnd = 7; - RaiseKeyEvent(textBox, Key.Back, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); Assert.Equal(" rd Frth", textBox.Text); // ( |rd Frth) textBox.CaretIndex = 1; - RaiseKeyEvent(textBox, Key.Back, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); Assert.Equal("rd Frth", textBox.Text); } } @@ -175,30 +175,30 @@ namespace Avalonia.Controls.UnitTests }; // (First Second Third |Fourth) - RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); Assert.Equal("First Second Third ", textBox.Text); // (First Second |Third ) textBox.CaretIndex = 13; - RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); Assert.Equal("First Second ", textBox.Text); // (First Sec|ond ) textBox.CaretIndex = 9; - RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); Assert.Equal("First Sec", textBox.Text); // (Fi[rs]t Sec ) textBox.SelectionStart = 2; textBox.SelectionEnd = 4; - RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); Assert.Equal("Fit Sec", textBox.Text); // (Fit Sec| ) textBox.Text += " "; textBox.CaretIndex = 7; - RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); + RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); Assert.Equal("Fit Sec", textBox.Text); } } @@ -460,6 +460,24 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Text_Box_MaxLength_Work_Properly() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "abc", + MaxLength = 3, + }; + + RaiseKeyEvent(target, Key.D, KeyModifiers.None); + + Assert.Equal("abc", target.Text); + } + } + private static TestServices FocusServices => TestServices.MockThreadingInterface.With( focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(), @@ -486,12 +504,12 @@ namespace Avalonia.Controls.UnitTests }.RegisterInNameScope(scope)); } - private void RaiseKeyEvent(TextBox textBox, Key key, InputModifiers inputModifiers) + private void RaiseKeyEvent(TextBox textBox, Key key, KeyModifiers inputModifiers) { textBox.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, - Modifiers = inputModifiers, + KeyModifiers = inputModifiers, Key = key }); } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs index 4aaf0ab5b7..0f4a0759aa 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs @@ -58,7 +58,7 @@ namespace Avalonia.Controls.UnitTests Assert.Null(DataValidationErrors.GetErrors(target)); target.Text = "20"; - IEnumerable errors = DataValidationErrors.GetErrors(target); + IEnumerable errors = DataValidationErrors.GetErrors(target); Assert.Single(errors); Assert.IsType(errors.Single()); target.Text = "1"; diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 0ee772425b..901d780f16 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -183,7 +183,7 @@ namespace Avalonia.Controls.UnitTests new Mock().Object, 0, RawKeyEventType.KeyDown, - Key.A, InputModifiers.None); + Key.A, RawInputModifiers.None); impl.Object.Input(input); inputManagerMock.Verify(x => x.ProcessInput(input)); @@ -224,6 +224,24 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Close_Should_Notify_MouseDevice() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + var mouseDevice = new Mock(); + impl.SetupAllProperties(); + impl.Setup(x => x.MouseDevice).Returns(mouseDevice.Object); + + var target = new TestTopLevel(impl.Object); + + impl.Object.Closed(); + + mouseDevice.Verify(x => x.TopLevelClosed(target)); + } + } + private FuncControlTemplate CreateTemplate() { return new FuncControlTemplate((x, scope) => diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 5646e86f7a..f51a50c0d5 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -619,7 +619,7 @@ namespace Avalonia.Controls.UnitTests { RoutedEvent = InputElement.KeyDownEvent, Key = selectAllGesture.Key, - Modifiers = selectAllGesture.Modifiers + KeyModifiers = selectAllGesture.KeyModifiers }; target.RaiseEvent(keyEvent); @@ -665,7 +665,7 @@ namespace Avalonia.Controls.UnitTests { RoutedEvent = InputElement.KeyDownEvent, Key = selectAllGesture.Key, - Modifiers = selectAllGesture.Modifiers + KeyModifiers = selectAllGesture.KeyModifiers }; target.RaiseEvent(keyEvent); @@ -711,7 +711,7 @@ namespace Avalonia.Controls.UnitTests { RoutedEvent = InputElement.KeyDownEvent, Key = selectAllGesture.Key, - Modifiers = selectAllGesture.Modifiers + KeyModifiers = selectAllGesture.KeyModifiers }; target.RaiseEvent(keyEvent); diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 3ee6a50e69..55e8ae0115 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -20,33 +20,6 @@ namespace Avalonia.Controls.UnitTests { public class WindowBaseTests { - [Fact] - public void Impl_ClientSize_Should_Be_Set_After_Layout_Pass() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var impl = Mock.Of(x => x.Scaling == 1); - - Mock.Get(impl).Setup(x => x.Resize(It.IsAny())).Callback(() => { }); - - var target = new TestWindowBase(impl) - { - Template = CreateTemplate(), - Content = new TextBlock - { - Width = 321, - Height = 432, - }, - IsVisible = true, - }; - - target.LayoutManager.ExecuteInitialLayoutPass(target); - - Mock.Get(impl).Verify(x => x.Resize(new Size(321, 432))); - } - } - - [Fact] public void Activate_Should_Call_Impl_Activate() { diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index f4d9a91d0c..75239f014f 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -277,8 +277,7 @@ namespace Avalonia.Controls.UnitTests var screens = new Mock(); screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object, screen2.Object }); - var windowImpl = new Mock(); - windowImpl.SetupProperty(x => x.Position); + var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); windowImpl.Setup(x => x.Scaling).Returns(1); windowImpl.Setup(x => x.Screen).Returns(screens.Object); @@ -302,14 +301,12 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Window_Should_Be_Centered_Relative_To_Owner_When_WindowStartupLocation_Is_CenterOwner() { - var parentWindowImpl = new Mock(); - parentWindowImpl.SetupProperty(x => x.Position); + var parentWindowImpl = MockWindowingPlatform.CreateWindowMock(); parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); parentWindowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); parentWindowImpl.Setup(x => x.Scaling).Returns(1); - var windowImpl = new Mock(); - windowImpl.SetupProperty(x => x.Position); + var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200)); windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); windowImpl.Setup(x => x.Scaling).Returns(1); diff --git a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs index 983f541c2a..214aead521 100644 --- a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs @@ -231,7 +231,7 @@ namespace Avalonia.Input.UnitTests root, RawPointerEventType.Move, p, - InputModifiers.None)); + RawInputModifiers.None)); } private void SetHit(Mock renderer, IControl hit) diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index d19accb0ad..0ba06980af 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -60,6 +60,80 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("baz", source.Foo); } + [Fact] + public void TwoWay_Binding_Should_Be_Set_Up_GC_Collect() + { + var source = new WeakRefSource { Foo = null }; + var target = new TestControl { DataContext = source }; + + var binding = new Binding + { + Path = "Foo", + Mode = BindingMode.TwoWay + }; + + target.Bind(TestControl.ValueProperty, binding); + + var ref1 = AssignValue(target, "ref1"); + + Assert.Equal(ref1.Target, source.Foo); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + var ref2 = AssignValue(target, "ref2"); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + target.Value = null; + + Assert.Null(source.Foo); + } + + private class DummyObject : ICloneable + { + private readonly string _val; + + public DummyObject(string val) + { + _val = val; + } + + public object Clone() + { + return new DummyObject(_val); + } + + protected bool Equals(DummyObject other) + { + return string.Equals(_val, other._val); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DummyObject) obj); + } + + public override int GetHashCode() + { + return (_val != null ? _val.GetHashCode() : 0); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private WeakReference AssignValue(TestControl source, string val) + { + var obj = new DummyObject(val); + + source.Value = obj; + + return new WeakReference(obj); + } + [Fact] public void OneTime_Binding_Should_Be_Set_Up() { @@ -568,12 +642,70 @@ namespace Avalonia.Markup.UnitTests.Data } } + public class WeakRefSource : INotifyPropertyChanged + { + private WeakReference _foo; + + public object Foo + { + get + { + if (_foo == null) + { + return null; + } + + if (_foo.TryGetTarget(out object target)) + { + if (target is ICloneable cloneable) + { + return cloneable.Clone(); + } + + return target; + } + + return null; + } + set + { + _foo = new WeakReference(value); + + RaisePropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void RaisePropertyChanged([CallerMemberName] string prop = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); + } + } + private class OldDataContextViewModel { public int Foo { get; set; } = 1; public int Bar { get; set; } = 2; } + private class TestControl : Control + { + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect( + nameof(Value), + o => o.Value, + (o, v) => o.Value = v); + + private object _value; + + public object Value + { + get => _value; + set => SetAndRaise(ValueProperty, ref _value, value); + } + } + private class OldDataContextTest : Control { public static readonly StyledProperty FooProperty = diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs new file mode 100644 index 0000000000..a77723afe1 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Converters +{ + public class MultiValueConverterTests : XamlTestBase + { + [Fact] + public void MultiValueConverter_Special_Values_Work() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.ApplyTemplate(); + + window.DataContext = Tuple.Create(2, 2); + Assert.Equal("foo", textBlock.Text); + + window.DataContext = Tuple.Create(-3, 3); + Assert.Equal("foo", textBlock.Text); + + window.DataContext = Tuple.Create(0, 2); + Assert.Equal("bar", textBlock.Text); + } + } + } + + public class TestMultiValueConverter : IMultiValueConverter + { + public static readonly TestMultiValueConverter Instance = new TestMultiValueConverter(); + + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (values[0] is int i && values[1] is int j) + { + var p = i * j; + + if (p > 0) + { + return "foo"; + } + + if (p == 0) + { + return AvaloniaProperty.UnsetValue; + } + + return BindingOperations.DoNothing; + } + + return "(default)"; + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs index 93cad9a68e..c3bc649abb 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text; using Avalonia.Controls; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Styling; using Avalonia.UnitTests; @@ -59,11 +60,15 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions new Setter( Window.TemplateProperty, new FuncControlTemplate((x, scope) => - new ContentPresenter + new VisualLayerManager { - Name = "PART_ContentPresenter", - [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty], - }.RegisterInNameScope(scope))) + Child = + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty], + }.RegisterInNameScope(scope) + })) } }; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 7281542bc1..b1abc9ea54 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -309,8 +309,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } - [Fact] - public void Binding_To_TextBlock_Text_With_StringConverter_Works() + [Theory, + InlineData(@"Hello \{0\}"), + InlineData(@"'Hello {0}'"), + InlineData(@"Hello {0}")] + + public void Binding_To_TextBlock_Text_With_StringConverter_Works(string fmt) { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -318,8 +322,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml - -"; + +"; var loader = new AvaloniaXamlLoader(); var window = (Window)loader.Load(xaml); var textBlock = window.FindControl("textBlock"); @@ -331,8 +335,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } - [Fact(Skip="Issue #2592")] - public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works() + [Theory, + InlineData("{}{0} {1}!"), + InlineData(@"\{0\} \{1\}!")] + public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works(string fmt) { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -342,7 +348,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'> - + diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index ae901ca2f2..272b1fc489 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -5,6 +5,7 @@ false Library false + latest diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 36297bf58b..c33ec72141 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -1,4 +1,6 @@ using System; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Input; using Moq; using Avalonia.Platform; @@ -15,16 +17,48 @@ namespace Avalonia.UnitTests _popupImpl = popupImpl; } + public static Mock CreateWindowMock(Func popupImpl = null) + { + var win = Mock.Of(x => x.Scaling == 1); + var mock = Mock.Get(win); + mock.Setup(x => x.CreatePopup()).Returns(() => + { + if (popupImpl != null) + return popupImpl(); + return CreatePopupMock().Object; + + }); + PixelPoint pos = default; + mock.SetupGet(x => x.Position).Returns(() => pos); + mock.Setup(x => x.Move(It.IsAny())).Callback(new Action(np => pos = np)); + SetupToplevel(mock); + return mock; + } + + static void SetupToplevel(Mock mock) where T : class, ITopLevelImpl + { + mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice()); + } + + public static Mock CreatePopupMock() + { + var positioner = Mock.Of(); + var popup = Mock.Of(x => x.Scaling == 1); + var mock = Mock.Get(popup); + mock.SetupGet(x => x.PopupPositioner).Returns(positioner); + SetupToplevel(mock); + + return mock; + } + public IWindowImpl CreateWindow() { - return _windowImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); + return _windowImpl?.Invoke() ?? CreateWindowMock(_popupImpl).Object; } public IEmbeddableWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } - - public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); } -} \ No newline at end of file +} diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index 00ad850cf8..d6e64936c7 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -32,7 +32,8 @@ namespace Avalonia.UnitTests private MouseButton _pressedButton; - InputModifiers GetModifiers(InputModifiers modifiers) => modifiers | _pressedButtons; + KeyModifiers GetModifiers(InputModifiers modifiers) => + (KeyModifiers)((int)modifiers & (int)RawInputModifiers.KeyboardMask); public void Down(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default, InputModifiers modifiers = default, int clickCount = 1) @@ -44,7 +45,11 @@ namespace Avalonia.UnitTests Point position = default, InputModifiers modifiers = default, int clickCount = 1) { _pressedButtons |= Convert(mouseButton); - var props = new PointerPointProperties(_pressedButtons); + var props = new PointerPointProperties((RawInputModifiers)_pressedButtons, + mouseButton == MouseButton.Left ? PointerUpdateKind.LeftButtonPressed + : mouseButton == MouseButton.Middle ? PointerUpdateKind.MiddleButtonPressed + : mouseButton == MouseButton.Right ? PointerUpdateKind.RightButtonPressed : PointerUpdateKind.Other + ); if (ButtonCount(props) > 1) Move(target, source, position); else @@ -60,7 +65,7 @@ namespace Avalonia.UnitTests public void Move(IInteractive target, IInteractive source, in Point position, InputModifiers modifiers = default) { target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (IVisual)target, position, - Timestamp(), new PointerPointProperties(_pressedButtons), GetModifiers(modifiers))); + Timestamp(), new PointerPointProperties((RawInputModifiers)_pressedButtons, PointerUpdateKind.Other), GetModifiers(modifiers))); } public void Up(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default, @@ -72,13 +77,17 @@ namespace Avalonia.UnitTests { var conv = Convert(mouseButton); _pressedButtons = (_pressedButtons | conv) ^ conv; - var props = new PointerPointProperties(_pressedButtons); + var props = new PointerPointProperties((RawInputModifiers)_pressedButtons, + mouseButton == MouseButton.Left ? PointerUpdateKind.LeftButtonReleased + : mouseButton == MouseButton.Middle ? PointerUpdateKind.MiddleButtonReleased + : mouseButton == MouseButton.Right ? PointerUpdateKind.RightButtonReleased : PointerUpdateKind.Other + ); if (ButtonCount(props) == 0) { _pointer.Capture(null); target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, Timestamp(), props, - GetModifiers(modifiers), _pressedButton)); + GetModifiers(modifiers))); } else Move(target, source, position); @@ -97,13 +106,13 @@ namespace Avalonia.UnitTests public void Enter(IInteractive target) { target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnterEvent, target, _pointer, (IVisual)target, default, - Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons)); + Timestamp(), new PointerPointProperties((RawInputModifiers)_pressedButtons, PointerUpdateKind.Other), KeyModifiers.None)); } public void Leave(IInteractive target) { target.RaiseEvent(new PointerEventArgs(InputElement.PointerLeaveEvent, target, _pointer, (IVisual)target, default, - Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons)); + Timestamp(), new PointerPointProperties((RawInputModifiers)_pressedButtons, PointerUpdateKind.Other), KeyModifiers.None)); } }