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
-
+
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