diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h index 5d299374e5..3ce83d370a 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.h +++ b/native/Avalonia.Native/src/OSX/AvnString.h @@ -11,6 +11,7 @@ extern IAvnString* CreateAvnString(NSString* string); extern IAvnStringArray* CreateAvnStringArray(NSArray* array); +extern IAvnStringArray* CreateAvnStringArray(NSArray* array); extern IAvnStringArray* CreateAvnStringArray(NSString* string); extern IAvnString* CreateByteArray(void* data, int len); #endif /* AvnString_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index 00b748ef63..001cf151d8 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -85,6 +85,16 @@ public: } } + AvnStringArrayImpl(NSArray* array) + { + for(int c = 0; c < [array count]; c++) + { + ComPtr s; + *s.getPPV() = new AvnStringImpl([array objectAtIndex:c].absoluteString); + _list.push_back(s); + } + } + AvnStringArrayImpl(NSString* string) { ComPtr s; @@ -117,6 +127,11 @@ IAvnStringArray* CreateAvnStringArray(NSArray * array) return new AvnStringArrayImpl(array); } +IAvnStringArray* CreateAvnStringArray(NSArray * array) +{ + return new AvnStringArrayImpl(array); +} + IAvnStringArray* CreateAvnStringArray(NSString* string) { return new AvnStringArrayImpl(string); diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 814b91cb62..460c24ea3a 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -1,10 +1,20 @@ #include "common.h" +#include "AvnString.h" @interface AvnAppDelegate : NSObject +-(AvnAppDelegate* _Nonnull) initWithEvents: (IAvnApplicationEvents* _Nonnull) events; @end NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; @implementation AvnAppDelegate +ComPtr _events; + +- (AvnAppDelegate *)initWithEvents:(IAvnApplicationEvents *)events +{ + _events = events; + return self; +} + - (void)applicationWillFinishLaunching:(NSNotification *)notification { if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy) @@ -27,11 +37,23 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps]; } +- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames +{ + auto array = CreateAvnStringArray(filenames); + + _events->FilesOpened(array); +} + +- (void)application:(NSApplication *)application openURLs:(NSArray *)urls +{ + auto array = CreateAvnStringArray(urls); + + _events->FilesOpened(array); +} @end @interface AvnApplication : NSApplication - @end @implementation AvnApplication @@ -63,9 +85,9 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati @end -extern void InitializeAvnApp() +extern void InitializeAvnApp(IAvnApplicationEvents* events) { NSApplication* app = [AvnApplication sharedApplication]; - id delegate = [AvnAppDelegate new]; + id delegate = [[AvnAppDelegate alloc] initWithEvents:events]; [app setDelegate:delegate]; } diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 303f727317..f148374759 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -56,7 +56,7 @@ public: return S_OK; } - NSArray* arr = (NSArray*)data; + NSArray* arr = (NSArray*)data; for(int c = 0; c < [arr count]; c++) if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]]) diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index ea48a19874..c082003ccf 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -23,13 +23,15 @@ extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); extern IAvnMenuItem* CreateAppMenuItem(); -extern IAvnMenuItem* CreateAppMenuItemSeperator(); +extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); +extern void SetAutoGenerateDefaultAppMenuItems (bool enabled); +extern bool GetAutoGenerateDefaultAppMenuItems (); -extern void InitializeAvnApp(); +extern void InitializeAvnApp(IAvnApplicationEvents* events); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); extern AvnPoint ToAvnPoint (NSPoint p); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 72f5aa0a7d..aaaf381b26 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -2,6 +2,7 @@ #define COM_GUIDS_MATERIALIZE #include "common.h" +static bool s_generateDefaultAppMenuItems = true; static NSString* s_appTitle = @"Avalonia"; // Copyright (c) 2011 The Chromium Authors. All rights reserved. @@ -122,6 +123,12 @@ public: ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; return S_OK; } + + virtual HRESULT SetDisableDefaultApplicationMenuItems (bool enabled) override + { + SetAutoGenerateDefaultAppMenuItems(!enabled); + return S_OK; + } }; /// See "Using POSIX Threads in a Cocoa Application" section here: @@ -156,13 +163,13 @@ class AvaloniaNative : public ComSingleObject { private: @@ -71,10 +70,12 @@ public: FORWARD_IUNKNOWN() AvnAppMenu(IAvnMenuEvents* events); - + AvnMenu* GetNative(); void RaiseNeedsUpdate (); + void RaiseOpening(); + void RaiseClosed(); virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 198b01714f..b9a95e7b3c 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -71,12 +71,12 @@ } @end -AvnAppMenuItem::AvnAppMenuItem(bool isSeperator) +AvnAppMenuItem::AvnAppMenuItem(bool isSeparator) { _isCheckable = false; - _isSeperator = isSeperator; + _isSeparator = isSeparator; - if(isSeperator) + if(isSeparator) { _native = [NSMenuItem separatorItem]; } @@ -298,6 +298,23 @@ void AvnAppMenu::RaiseNeedsUpdate() } } +void AvnAppMenu::RaiseOpening() +{ + if(_baseEvents != nullptr) + { + _baseEvents->Opening(); + } +} + +void AvnAppMenu::RaiseClosed() +{ + if(_baseEvents != nullptr) + { + _baseEvents->Closed(); + } +} + + HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) { @autoreleasepool @@ -382,6 +399,15 @@ HRESULT AvnAppMenu::Clear() _parent->RaiseNeedsUpdate(); } +- (void)menuWillOpen:(NSMenu *)menu +{ + _parent->RaiseOpening(); +} + +- (void)menuDidClose:(NSMenu *)menu +{ + _parent->RaiseClosed(); +} @end @@ -401,7 +427,7 @@ extern IAvnMenuItem* CreateAppMenuItem() } } -extern IAvnMenuItem* CreateAppMenuItemSeperator() +extern IAvnMenuItem* CreateAppMenuItemSeparator() { @autoreleasepool { @@ -445,47 +471,50 @@ extern void SetAppMenu (NSString* appName, IAvnMenu* menu) auto appMenu = [s_appMenuItem submenu]; - [appMenu addItem:[NSMenuItem separatorItem]]; - - // Services item and menu - auto servicesItem = [[NSMenuItem alloc] init]; - servicesItem.title = @"Services"; - NSMenu *servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"]; - servicesItem.submenu = servicesMenu; - [NSApplication sharedApplication].servicesMenu = servicesMenu; - [appMenu addItem:servicesItem]; - - [appMenu addItem:[NSMenuItem separatorItem]]; - - // Hide Application - auto hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName] action:@selector(hide:) keyEquivalent:@"h"]; - - [appMenu addItem:hideItem]; - - // Hide Others - auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others" - action:@selector(hideOtherApplications:) - keyEquivalent:@"h"]; - - hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption; - [appMenu addItem:hideAllOthersItem]; - - // Show All - auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All" - action:@selector(unhideAllApplications:) - keyEquivalent:@""]; - - [appMenu addItem:showAllItem]; - - [appMenu addItem:[NSMenuItem separatorItem]]; - - // Quit Application - auto quitItem = [[NSMenuItem alloc] init]; - quitItem.title = [@"Quit " stringByAppendingString:appName]; - quitItem.keyEquivalent = @"q"; - quitItem.target = [AvnWindow class]; - quitItem.action = @selector(closeAll); - [appMenu addItem:quitItem]; + if(GetAutoGenerateDefaultAppMenuItems()) + { + [appMenu addItem:[NSMenuItem separatorItem]]; + + // Services item and menu + auto servicesItem = [[NSMenuItem alloc] init]; + servicesItem.title = @"Services"; + NSMenu *servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"]; + servicesItem.submenu = servicesMenu; + [NSApplication sharedApplication].servicesMenu = servicesMenu; + [appMenu addItem:servicesItem]; + + [appMenu addItem:[NSMenuItem separatorItem]]; + + // Hide Application + auto hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName] action:@selector(hide:) keyEquivalent:@"h"]; + + [appMenu addItem:hideItem]; + + // Hide Others + auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others" + action:@selector(hideOtherApplications:) + keyEquivalent:@"h"]; + + hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption; + [appMenu addItem:hideAllOthersItem]; + + // Show All + auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All" + action:@selector(unhideAllApplications:) + keyEquivalent:@""]; + + [appMenu addItem:showAllItem]; + + [appMenu addItem:[NSMenuItem separatorItem]]; + + // Quit Application + auto quitItem = [[NSMenuItem alloc] init]; + quitItem.title = [@"Quit " stringByAppendingString:appName]; + quitItem.keyEquivalent = @"q"; + quitItem.target = [AvnWindow class]; + quitItem.action = @selector(closeAll); + [appMenu addItem:quitItem]; + } } else { diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 9d49025398..b46655c7df 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1877,7 +1877,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent for(int i = 0; i < numWindows; i++) { - [[windows objectAtIndex:i] performClose:nil]; + auto window = (AvnWindow*)[windows objectAtIndex:i]; + + if([window parentWindow] == nullptr) // Avalonia will handle the child windows. + { + [window performClose:nil]; + } } } @@ -2068,17 +2073,17 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent -(void)becomeKeyWindow { + [self showWindowMenuWithAppMenu]; + if([self activateAppropriateChild: true]) { - [self showWindowMenuWithAppMenu]; - if(_parent != nullptr) { _parent->BaseEvents->Activated(); } - - [super becomeKeyWindow]; } + + [super becomeKeyWindow]; } -(void) restoreParentWindow; @@ -2226,9 +2231,12 @@ protected: { @autoreleasepool { - [Window setContentSize:NSSize{x, y}]; + if (Window != nullptr) + { + [Window setContentSize:NSSize{x, y}]; - [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; + [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; + } return S_OK; } diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 8e331edab4..d627a2bf19 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -89,10 +89,6 @@ partial class Build : NukeBuild Process.Start(new ProcessStartInfo(command, args) {UseShellExecute = false}).WaitForExit(); } ExecWait("dotnet version:", "dotnet", "--version"); - if (Parameters.IsRunningOnUnix) - ExecWait("Mono version:", "mono", "--version"); - - } IReadOnlyCollection MsBuildCommon( diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 20ca0576d4..1a68c4d732 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v10.0 + v11.0 Properties\AndroidManifest.xml diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml index 02e97f3065..9effda7e79 100644 --- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml +++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 6a70bb082f..a107ee2163 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -18,11 +18,11 @@ - + - + diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml index 96cfcc5288..7635e1ccc3 100644 --- a/samples/ControlCatalog/Pages/AcrylicPage.xaml +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -16,13 +16,13 @@ - - + + - - + + diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml b/samples/ControlCatalog/Pages/ProgressBarPage.xaml index 2ec0b48c76..da8ef6cf07 100644 --- a/samples/ControlCatalog/Pages/ProgressBarPage.xaml +++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml @@ -15,6 +15,13 @@ + + + + + + + diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index eeb198976b..b4901ec780 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -45,6 +45,12 @@ + + diff --git a/src/Android/Avalonia.Android/ActivityTracker.cs b/src/Android/Avalonia.Android/ActivityTracker.cs deleted file mode 100644 index 2ad1d9e361..0000000000 --- a/src/Android/Avalonia.Android/ActivityTracker.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Android.App; -using Android.OS; - -namespace Avalonia.Android -{ - internal class ActivityTracker : Java.Lang.Object, global::Android.App.Application.IActivityLifecycleCallbacks - { - public static Activity Current { get; private set; } - public void OnActivityCreated(Activity activity, Bundle savedInstanceState) - { - Current = activity; - } - - public void OnActivityDestroyed(Activity activity) - { - if (Current == activity) - Current = null; - } - - public void OnActivityPaused(Activity activity) - { - if (Current == activity) - Current = null; - } - - public void OnActivityResumed(Activity activity) - { - Current = activity; - } - - public void OnActivitySaveInstanceState(Activity activity, Bundle outState) - { - Current = activity; - } - - public void OnActivityStarted(Activity activity) - { - Current = activity; - } - - public void OnActivityStopped(Activity activity) - { - if (Current == activity) - Current = null; - } - } -} \ No newline at end of file diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index e0ceb0c8b7..5e11d8eab2 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -29,60 +29,42 @@ namespace Avalonia namespace Avalonia.Android { - class AndroidPlatform : IPlatformSettings, IWindowingPlatform + class AndroidPlatform : IPlatformSettings { public static readonly AndroidPlatform Instance = new AndroidPlatform(); + public static AndroidPlatformOptions Options { get; private set; } public Size DoubleClickSize => new Size(4, 4); public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200); - public double RenderScalingFactor => _scalingFactor; - public double LayoutScalingFactor => _scalingFactor; - - private readonly double _scalingFactor = 1; - - public AndroidPlatform() - { - _scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity; - } public static void Initialize(Type appType, AndroidPlatformOptions options) { + Options = options; + AvaloniaLocator.CurrentMutable .Bind().ToTransient() - .Bind().ToTransient() + .Bind().ToTransient() .Bind().ToSingleton() .Bind().ToConstant(Instance) .Bind().ToConstant(new AndroidThreadingInterface()) .Bind().ToTransient() - .Bind().ToConstant(Instance) .Bind().ToSingleton() - .Bind().ToConstant(new DefaultRenderTimer(60)) + .Bind().ToConstant(new ChoreographerTimer()) .Bind().ToConstant(new RenderLoop()) .Bind().ToSingleton() .Bind().ToConstant(new AssetLoader(appType.Assembly)); SkiaPlatform.Initialize(); - ((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext) - .RegisterActivityLifecycleCallbacks(new ActivityTracker()); if (options.UseGpu) { EglPlatformOpenGlInterface.TryInitialize(); } } - - public IWindowImpl CreateWindow() - { - throw new NotSupportedException(); - } - - public IWindowImpl CreateEmbeddableWindow() - { - throw new NotSupportedException(); - } } public sealed class AndroidPlatformOptions { + public bool UseDeferredRendering { get; set; } = true; public bool UseGpu { get; set; } = true; } } diff --git a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs index 6fe77adca1..e72f0aed90 100644 --- a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs +++ b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs @@ -1,25 +1,26 @@ using System; using System.Reactive.Disposables; using System.Threading; + using Android.OS; + using Avalonia.Platform; using Avalonia.Threading; +using App = Android.App.Application; + namespace Avalonia.Android { - class AndroidThreadingInterface : IPlatformThreadingInterface + internal sealed class AndroidThreadingInterface : IPlatformThreadingInterface { private Handler _handler; public AndroidThreadingInterface() { - _handler = new Handler(global::Android.App.Application.Context.MainLooper); + _handler = new Handler(App.Context.MainLooper); } - public void RunLoop(CancellationToken cancellationToken) - { - return; - } + public void RunLoop(CancellationToken cancellationToken) => throw new NotSupportedException(); public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { @@ -57,7 +58,7 @@ namespace Avalonia.Android }); } }, null, TimeSpan.Zero, interval); - + return Disposable.Create(() => { lock (l) diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index c170e8449c..8c6775733f 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -1,6 +1,6 @@  - monoandroid90 + monoandroid11.0 true diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs index d3696aa41d..b3a7585520 100644 --- a/src/Android/Avalonia.Android/AvaloniaActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaActivity.cs @@ -1,4 +1,3 @@ - using Android.App; using Android.OS; using Android.Views; @@ -7,15 +6,13 @@ namespace Avalonia.Android { public abstract class AvaloniaActivity : Activity { - internal AvaloniaView View; object _content; protected override void OnCreate(Bundle savedInstanceState) { - RequestWindowFeature(WindowFeatures.NoTitle); View = new AvaloniaView(this); - if(_content != null) + if (_content != null) View.Content = _content; SetContentView(View); TakeKeyEvents(true); @@ -36,9 +33,7 @@ namespace Avalonia.Android } } - public override bool DispatchKeyEvent(KeyEvent e) - { - return View.DispatchKeyEvent(e); - } + public override bool DispatchKeyEvent(KeyEvent e) => + View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e); } } diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 72732a1f95..8de3657283 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -1,11 +1,12 @@ using System; using Android.Content; +using Android.Runtime; using Android.Views; using Android.Widget; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; using Avalonia.Controls.Embedding; -using Avalonia.Platform; +using Avalonia.Rendering; namespace Avalonia.Android { @@ -14,6 +15,8 @@ namespace Avalonia.Android private readonly EmbeddableControlRoot _root; private readonly ViewImpl _view; + private IDisposable? _timerSubscription; + public AvaloniaView(Context context) : base(context) { _view = new ViewImpl(context); @@ -33,6 +36,36 @@ namespace Avalonia.Android return _view.View.DispatchKeyEvent(e); } + public override void OnVisibilityAggregated(bool isVisible) + { + base.OnVisibilityAggregated(isVisible); + OnVisibilityChanged(isVisible); + } + + protected override void OnVisibilityChanged(View changedView, [GeneratedEnum] ViewStates visibility) + { + base.OnVisibilityChanged(changedView, visibility); + OnVisibilityChanged(visibility == ViewStates.Visible); + } + + private void OnVisibilityChanged(bool isVisible) + { + if (isVisible) + { + if (AvaloniaLocator.Current.GetService() is ChoreographerTimer timer) + { + _timerSubscription = timer.SubscribeView(this); + } + + _root.Renderer.Start(); + } + else + { + _root.Renderer.Stop(); + _timerSubscription?.Dispose(); + } + } + class ViewImpl : TopLevelImpl { public ViewImpl(Context context) : base(context) diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs new file mode 100644 index 0000000000..1d898261a3 --- /dev/null +++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Threading.Tasks; + +using Android.OS; +using Android.Views; + +using Avalonia.Rendering; + +using Java.Lang; + +namespace Avalonia.Android +{ + internal sealed class ChoreographerTimer : Java.Lang.Object, IRenderTimer, Choreographer.IFrameCallback + { + private readonly object _lock = new object(); + + private readonly Thread _thread; + private readonly TaskCompletionSource _choreographer = new TaskCompletionSource(); + + private readonly ISet _views = new HashSet(); + + private Action _tick; + private int _count; + + public ChoreographerTimer() + { + _thread = new Thread(Loop); + _thread.Start(); + } + + public event Action Tick + { + add + { + lock (_lock) + { + _tick += value; + _count++; + + if (_count == 1) + { + _choreographer.Task.Result.PostFrameCallback(this); + } + } + } + remove + { + lock (_lock) + { + _tick -= value; + _count--; + } + } + } + + internal IDisposable SubscribeView(AvaloniaView view) + { + lock (_lock) + { + _views.Add(view); + + if (_views.Count == 1) + { + _choreographer.Task.Result.PostFrameCallback(this); + } + } + + return Disposable.Create( + () => + { + lock (_lock) + { + _views.Remove(view); + } + } + ); + } + + private void Loop() + { + Looper.Prepare(); + _choreographer.SetResult(Choreographer.Instance); + Looper.Loop(); + } + + public void DoFrame(long frameTimeNanos) + { + _tick?.Invoke(TimeSpan.FromTicks(frameTimeNanos / 100)); + + lock (_lock) + { + if (_count > 0 && _views.Count > 0) + { + Choreographer.Instance.PostFrameCallback(this); + } + } + } + } +} diff --git a/src/Android/Avalonia.Android/CursorFactory.cs b/src/Android/Avalonia.Android/CursorFactory.cs index 9eb28c67f9..6293637d4e 100644 --- a/src/Android/Avalonia.Android/CursorFactory.cs +++ b/src/Android/Avalonia.Android/CursorFactory.cs @@ -1,12 +1,21 @@ -using System; using Avalonia.Input; using Avalonia.Platform; namespace Avalonia.Android { - internal class CursorFactory : IStandardCursorFactory + internal class CursorFactory : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) - => new PlatformHandle(IntPtr.Zero, "ZeroCursor"); + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => CursorImpl.ZeroCursor; + + public ICursorImpl GetCursor(StandardCursorType cursorType) => CursorImpl.ZeroCursor; + + private sealed class CursorImpl : ICursorImpl + { + public static CursorImpl ZeroCursor { get; } = new CursorImpl(); + + private CursorImpl() { } + + public void Dispose() { } + } } } diff --git a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs index 4f4c03fe77..a9710039f8 100644 --- a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs +++ b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs @@ -1,6 +1,4 @@ -using System.Linq; - -using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; namespace Avalonia.Android.OpenGL @@ -17,7 +15,7 @@ namespace Avalonia.Android.OpenGL } public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() => - new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle)); + new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle), _info.Handle); public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info) { diff --git a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs index 75bbd15e3e..f9071d9b27 100644 --- a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs +++ b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs @@ -1,23 +1,30 @@ -using Avalonia.OpenGL.Egl; +using System; + +using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; namespace Avalonia.Android.OpenGL { - internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase + internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase, IGlPlatformSurfaceRenderTargetWithCorruptionInfo { private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; private readonly EglSurface _surface; + private readonly IntPtr _handle; public GlRenderTarget( EglPlatformOpenGlInterface egl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, - EglSurface surface) + EglSurface surface, + IntPtr handle) : base(egl) { _info = info; _surface = surface; + _handle = handle; } + public bool IsCorrupted => _handle != _info.Handle; + public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info); } } diff --git a/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs b/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs deleted file mode 100644 index d52eeb15e4..0000000000 --- a/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Avalonia.Input; - -namespace Avalonia.Android.Platform.Input -{ - public class AndroidMouseDevice : MouseDevice - { - public static AndroidMouseDevice Instance { get; } = new AndroidMouseDevice(); - - public AndroidMouseDevice() - { - - } - } -} \ No newline at end of file diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs index 2afa4e83f1..b115917622 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs @@ -10,7 +10,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform { private IntPtr _window; - public AndroidFramebuffer(Surface surface) + public AndroidFramebuffer(Surface surface, double scaling) { if(surface == null) throw new ArgumentNullException(nameof(surface)); @@ -31,6 +31,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform RowBytes = buffer.stride * (Format == PixelFormat.Rgb565 ? 2 : 4); Address = buffer.bits; + + Dpi = scaling * new Vector(96, 96); } public void Dispose() @@ -44,7 +46,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IntPtr Address { get; set; } public PixelSize Size { get; } public int RowBytes { get; } - public Vector Dpi { get; } = new Vector(96, 96); + public Vector Dpi { get; } public PixelFormat Format { get; } [DllImport("android")] diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs index 18c4796fae..56a4eb22d4 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs @@ -12,6 +12,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform _topLevel = topLevel; } - public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface); + public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface, _topLevel.RenderScaling); } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 02ea702236..16c5bdae3d 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -43,11 +43,13 @@ namespace Avalonia.Android } } + [Obsolete("deprecated")] public override void Invalidate(global::Android.Graphics.Rect dirty) { Invalidate(); } + [Obsolete("deprecated")] public override void Invalidate(int l, int t, int r, int b) { Invalidate(); diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index a8c7f7af9b..fe237a1719 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -6,7 +6,6 @@ using Android.Runtime; using Android.Views; using Avalonia.Android.OpenGL; -using Avalonia.Android.Platform.Input; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Controls; @@ -35,16 +34,16 @@ namespace Avalonia.Android.Platform.SkiaPlatform _view = new ViewImpl(context, this, placeOnTop); _keyboardHelper = new AndroidKeyboardEventsHelper(this); _touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot, - p => GetAvaloniaPointFromEvent(p)); + GetAvaloniaPointFromEvent); _gl = GlPlatformSurface.TryCreate(this); _framebuffer = new FramebufferManager(this); - MaxClientSize = new Size(_view.Resources.DisplayMetrics.WidthPixels, - _view.Resources.DisplayMetrics.HeightPixels); - } - + RenderScaling = (int)_view.Resources.DisplayMetrics.Density; + MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, + _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); + } private bool _handleEvents; @@ -58,25 +57,14 @@ namespace Avalonia.Android.Platform.SkiaPlatform } } - public virtual Point GetAvaloniaPointFromEvent(MotionEvent e) => new Point(e.GetX(), e.GetY()); + public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) => + new Point(e.GetX(pointerIndex), e.GetY(pointerIndex)) / RenderScaling; public IInputRoot InputRoot { get; private set; } - public virtual Size ClientSize - { - get - { - if (_view == null) - return new Size(0, 0); - return new Size(_view.Width, _view.Height); - } - set - { - - } - } + public virtual Size ClientSize => Size.ToSize(RenderScaling); - public IMouseDevice MouseDevice => AndroidMouseDevice.Instance; + public IMouseDevice MouseDevice { get; } = new MouseDevice(); public Action Closed { get; set; } @@ -98,10 +86,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IEnumerable Surfaces => new object[] { _gl, _framebuffer }; - public IRenderer CreateRenderer(IRenderRoot root) - { - return new ImmediateRenderer(root); - } + public IRenderer CreateRenderer(IRenderRoot root) => + AndroidPlatform.Options.UseDeferredRendering + ? new DeferredRenderer(root, AvaloniaLocator.Current.GetService()) { RenderOnlyOnRenderThread = true } + : new ImmediateRenderer(root); public virtual void Hide() { @@ -115,15 +103,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform public Point PointToClient(PixelPoint point) { - return point.ToPoint(1); + return point.ToPoint(RenderScaling); } public PixelPoint PointToScreen(Point point) { - return PixelPoint.FromPoint(point, 1); + return PixelPoint.FromPoint(point, RenderScaling); } - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { //still not implemented } @@ -138,7 +126,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _view.Visibility = ViewStates.Visible; } - public double RenderScaling => 1; + public double RenderScaling { get; } void Draw() { @@ -193,7 +181,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) { - var newSize = new Size(width, height); + var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling); if (newSize != _oldSize) { diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs index 0bfbb1c2df..6142598514 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs @@ -11,7 +11,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers private TView _view; public bool HandleEvents { get; set; } - public AndroidTouchEventsHelper(TView view, Func getInputRoot, Func getPointfunc) + public AndroidTouchEventsHelper(TView view, Func getInputRoot, Func getPointfunc) { this._view = view; HandleEvents = true; @@ -19,11 +19,9 @@ namespace Avalonia.Android.Platform.Specific.Helpers _getInputRoot = getInputRoot; } - private DateTime _lastTouchMoveEventTime = DateTime.Now; - private Point? _lastTouchMovePoint; - private Func _getPointFunc; + private TouchDevice _touchDevice = new TouchDevice(); + private Func _getPointFunc; private Func _getInputRoot; - private Point _point; public bool? DispatchTouchEvent(MotionEvent e, out bool callBase) { @@ -33,89 +31,44 @@ namespace Avalonia.Android.Platform.Specific.Helpers return null; } - RawPointerEventType? mouseEventType = null; var eventTime = DateTime.Now; + //Basic touch support - switch (e.Action) + var pointerEventType = e.Action switch { - case MotionEventActions.Move: - //may be bot flood the evnt system with too many event especially on not so powerfull mobile devices - if ((eventTime - _lastTouchMoveEventTime).TotalMilliseconds > 10) - { - mouseEventType = RawPointerEventType.Move; - } - break; - - case MotionEventActions.Down: - mouseEventType = RawPointerEventType.LeftButtonDown; + MotionEventActions.Down => RawPointerEventType.TouchBegin, + MotionEventActions.Up => RawPointerEventType.TouchEnd, + MotionEventActions.Cancel => RawPointerEventType.TouchCancel, + _ => RawPointerEventType.TouchUpdate + }; - break; + if (e.Action.HasFlag(MotionEventActions.PointerDown)) + { + pointerEventType = RawPointerEventType.TouchBegin; + } - case MotionEventActions.Up: - mouseEventType = RawPointerEventType.LeftButtonUp; - break; + if (e.Action.HasFlag(MotionEventActions.PointerUp)) + { + pointerEventType = RawPointerEventType.TouchEnd; } - if (mouseEventType != null) + for (int i = 0; i < e.PointerCount; i++) { //if point is in view otherwise it's possible avalonia not to find the proper window to dispatch the event - _point = _getPointFunc(e); + var point = _getPointFunc(e, i); double x = _view.View.GetX(); double y = _view.View.GetY(); double r = x + _view.View.Width; double b = y + _view.View.Height; - if (x <= _point.X && r >= _point.X && y <= _point.Y && b >= _point.Y) + if (x <= point.X && r >= point.X && y <= point.Y && b >= point.Y) { var inputRoot = _getInputRoot(); - var mouseDevice = Avalonia.Android.Platform.Input.AndroidMouseDevice.Instance; - - //in order the controls to work in a predictable way - //we need to generate mouse move before first mouse down event - //as this is the way buttons are working every time - //otherwise there is a problem sometimes - if (mouseEventType == RawPointerEventType.LeftButtonDown) - { - var me = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot, - RawPointerEventType.Move, _point, RawInputModifiers.None); - _view.Input(me); - } - var mouseEvent = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot, - mouseEventType.Value, _point, RawInputModifiers.LeftMouseButton); + var mouseEvent = new RawTouchEventArgs(_touchDevice, (uint)eventTime.Ticks, inputRoot, + i == e.ActionIndex ? pointerEventType : RawPointerEventType.TouchUpdate, point, RawInputModifiers.None, e.GetPointerId(i)); _view.Input(mouseEvent); - - if (e.Action == MotionEventActions.Move && mouseDevice.Captured == null) - { - if (_lastTouchMovePoint != null) - { - //raise mouse scroll event so the scrollers - //are moving with the cursor - double vectorX = _point.X - _lastTouchMovePoint.Value.X; - double vectorY = _point.Y - _lastTouchMovePoint.Value.Y; - //based on test correction of 0.02 is working perfect - double correction = 0.02; - var ps = AndroidPlatform.Instance.LayoutScalingFactor; - var mouseWheelEvent = new RawMouseWheelEventArgs( - mouseDevice, - (uint)eventTime.Ticks, - inputRoot, - _point, - new Vector(vectorX * correction / ps, vectorY * correction / ps), RawInputModifiers.LeftMouseButton); - _view.Input(mouseWheelEvent); - } - _lastTouchMovePoint = _point; - _lastTouchMoveEventTime = eventTime; - } - else if (e.Action == MotionEventActions.Down) - { - _lastTouchMovePoint = _point; - } - else - { - _lastTouchMovePoint = null; - } } } diff --git a/src/Android/Avalonia.Android/app.config b/src/Android/Avalonia.Android/app.config deleted file mode 100644 index fc064cedfb..0000000000 --- a/src/Android/Avalonia.Android/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index 4f49f3a863..9104f1618c 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v10.0 + v11.0 Properties\AndroidManifest.xml @@ -150,4 +150,4 @@ - \ No newline at end of file + diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml index e8e81da9de..57ee503005 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml +++ b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 067d9f462f..a415046513 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Data; #nullable enable @@ -93,16 +94,35 @@ namespace Avalonia.Animation var oldTransitions = change.OldValue.GetValueOrDefault(); var newTransitions = change.NewValue.GetValueOrDefault(); + // When transitions are replaced, we add the new transitions before removing the old + // transitions, so that when the old transition being disposed causes the value to + // change, there is a corresponding entry in `_transitionStates`. This means that we + // need to account for any transitions present in both the old and new transitions + // collections. if (newTransitions is object) { + var toAdd = (IList)newTransitions; + + if (newTransitions.Count > 0 && oldTransitions?.Count > 0) + { + toAdd = newTransitions.Except(oldTransitions).ToList(); + } + newTransitions.CollectionChanged += TransitionsCollectionChanged; - AddTransitions(newTransitions); + AddTransitions(toAdd); } if (oldTransitions is object) { + var toRemove = (IList)oldTransitions; + + if (oldTransitions.Count > 0 && newTransitions?.Count > 0) + { + toRemove = oldTransitions.Except(newTransitions).ToList(); + } + oldTransitions.CollectionChanged -= TransitionsCollectionChanged; - RemoveTransitions(oldTransitions); + RemoveTransitions(toRemove); } } else if (_transitionsEnabled && @@ -115,9 +135,9 @@ namespace Avalonia.Animation { var transition = Transitions[i]; - if (transition.Property == change.Property) + if (transition.Property == change.Property && + _transitionState.TryGetValue(transition, out var state)) { - var state = _transitionState[transition]; var oldValue = state.BaseValue; var newValue = GetAnimationBaseValue(transition.Property); diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index ca1d97290e..05142532e9 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -22,7 +22,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty DurationProperty = AvaloniaProperty.RegisterDirect( - nameof(_duration), + nameof(Duration), o => o._duration, (o, v) => o._duration = v); @@ -31,7 +31,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty IterationCountProperty = AvaloniaProperty.RegisterDirect( - nameof(_iterationCount), + nameof(IterationCount), o => o._iterationCount, (o, v) => o._iterationCount = v); @@ -40,7 +40,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty PlaybackDirectionProperty = AvaloniaProperty.RegisterDirect( - nameof(_playbackDirection), + nameof(PlaybackDirection), o => o._playbackDirection, (o, v) => o._playbackDirection = v); @@ -49,7 +49,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty FillModeProperty = AvaloniaProperty.RegisterDirect( - nameof(_fillMode), + nameof(FillMode), o => o._fillMode, (o, v) => o._fillMode = v); @@ -58,7 +58,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty EasingProperty = AvaloniaProperty.RegisterDirect( - nameof(_easing), + nameof(Easing), o => o._easing, (o, v) => o._easing = v); @@ -67,7 +67,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty DelayProperty = AvaloniaProperty.RegisterDirect( - nameof(_delay), + nameof(Delay), o => o._delay, (o, v) => o._delay = v); @@ -76,7 +76,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty DelayBetweenIterationsProperty = AvaloniaProperty.RegisterDirect( - nameof(_delayBetweenIterations), + nameof(DelayBetweenIterations), o => o._delayBetweenIterations, (o, v) => o._delayBetweenIterations = v); @@ -85,7 +85,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty SpeedRatioProperty = AvaloniaProperty.RegisterDirect( - nameof(_speedRatio), + nameof(SpeedRatio), o => o._speedRatio, (o, v) => o._speedRatio = v, defaultBindingMode: BindingMode.TwoWay); diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 6645d25b5d..fdd688cf9d 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -23,7 +23,7 @@ namespace Avalonia private EventHandler _propertyChanged; private List _inheritanceChildren; private ValueStore _values; - private ValueStore Values => _values ?? (_values = new ValueStore(this)); + private bool _batchUpdate; /// /// Initializes a new instance of the class. @@ -117,6 +117,22 @@ namespace Avalonia set { this.Bind(binding.Property, value); } } + private ValueStore Values + { + get + { + if (_values is null) + { + _values = new ValueStore(this); + + if (_batchUpdate) + _values.BeginBatchUpdate(); + } + + return _values; + } + } + public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); @@ -434,6 +450,28 @@ namespace Avalonia _values?.CoerceValue(property); } + public void BeginBatchUpdate() + { + if (_batchUpdate) + { + throw new InvalidOperationException("Batch update already in progress."); + } + + _batchUpdate = true; + _values?.BeginBatchUpdate(); + } + + public void EndBatchUpdate() + { + if (!_batchUpdate) + { + throw new InvalidOperationException("No batch update in progress."); + } + + _batchUpdate = false; + _values?.EndBatchUpdate(); + } + /// void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child) { diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index dc8421fb35..42f941da0c 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -45,7 +45,7 @@ namespace Avalonia.Data case BindingMode.OneWay: return target.Bind(property, binding.Observable ?? binding.Subject, binding.Priority); case BindingMode.TwoWay: - return new CompositeDisposable( + return new TwoWayBindingDisposable( target.Bind(property, binding.Subject, binding.Priority), target.GetObservable(property).Subscribe(binding.Subject)); case BindingMode.OneTime: @@ -88,6 +88,32 @@ namespace Avalonia.Data throw new ArgumentException("Invalid binding mode."); } } + + private sealed class TwoWayBindingDisposable : IDisposable + { + private readonly IDisposable _first; + private readonly IDisposable _second; + private bool _isDisposed; + + public TwoWayBindingDisposable(IDisposable first, IDisposable second) + { + _first = first; + _second = second; + } + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + _first.Dispose(); + _second.Dispose(); + + _isDisposed = true; + } + } } public sealed class DoNothingType diff --git a/src/Avalonia.Base/Data/Converters/BoolConverters.cs b/src/Avalonia.Base/Data/Converters/BoolConverters.cs index 6740c2168f..9329cdd6af 100644 --- a/src/Avalonia.Base/Data/Converters/BoolConverters.cs +++ b/src/Avalonia.Base/Data/Converters/BoolConverters.cs @@ -12,5 +12,11 @@ namespace Avalonia.Data.Converters /// public static readonly IMultiValueConverter And = new FuncMultiValueConverter(x => x.All(y => y)); + + /// + /// A multi-value converter that returns true if any of the inputs is true. + /// + public static readonly IMultiValueConverter Or = + new FuncMultiValueConverter(x => x.Any(y => y)); } } diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 0d563947e7..3e17a81dd8 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -9,8 +9,9 @@ namespace Avalonia.PropertyStore /// /// Represents an untyped interface to . /// - internal interface IBindingEntry : IPriorityValueEntry, IDisposable + internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable { + void Start(bool ignoreBatchUpdate); } /// @@ -22,6 +23,8 @@ namespace Avalonia.PropertyStore private readonly IAvaloniaObject _owner; private IValueSink _sink; private IDisposable? _subscription; + private bool _isSubscribed; + private bool _batchUpdate; private Optional _value; public BindingEntry( @@ -39,10 +42,20 @@ namespace Avalonia.PropertyStore } public StyledPropertyBase Property { get; } - public BindingPriority Priority { get; } + public BindingPriority Priority { get; private set; } public IObservable> Source { get; } Optional IValue.GetValue() => _value.ToObject(); + public void BeginBatchUpdate() => _batchUpdate = true; + + public void EndBatchUpdate() + { + _batchUpdate = false; + + if (_sink is ValueStore) + Start(); + } + public Optional GetValue(BindingPriority maxPriority) { return Priority >= maxPriority ? _value : Optional.Empty; @@ -52,10 +65,17 @@ namespace Avalonia.PropertyStore { _subscription?.Dispose(); _subscription = null; - _sink.Completed(Property, this, _value); + OnCompleted(); } - public void OnCompleted() => _sink.Completed(Property, this, _value); + public void OnCompleted() + { + var oldValue = _value; + _value = default; + Priority = BindingPriority.Unset; + _isSubscribed = false; + _sink.Completed(Property, this, oldValue); + } public void OnError(Exception error) { @@ -79,13 +99,39 @@ namespace Avalonia.PropertyStore } } - public void Start() + public void Start() => Start(false); + + public void Start(bool ignoreBatchUpdate) { - _subscription = Source.Subscribe(this); + // We can't use _subscription to check whether we're subscribed because it won't be set + // until Subscribe has finished, which will be too late to prevent reentrancy. In addition + // don't re-subscribe to completed/disposed bindings (indicated by Unset priority). + if (!_isSubscribed && + Priority != BindingPriority.Unset && + (!_batchUpdate || ignoreBatchUpdate)) + { + _isSubscribed = true; + _subscription = Source.Subscribe(this); + } } public void Reparent(IValueSink sink) => _sink = sink; - + + public void RaiseValueChanged( + IValueSink sink, + IAvaloniaObject owner, + AvaloniaProperty property, + Optional oldValue, + Optional newValue) + { + sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + owner, + (AvaloniaProperty)property, + oldValue.GetValueOrDefault(), + newValue.GetValueOrDefault(), + Priority)); + } + private void UpdateValue(BindingValue value) { if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false) diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index 46f6f9a137..d39fc3bb1e 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -1,23 +1,31 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Data; #nullable enable namespace Avalonia.PropertyStore { + /// + /// Represents an untyped interface to . + /// + internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable + { + } + /// /// Stores a value with a priority in a or /// . /// /// The property type. - internal class ConstantValueEntry : IPriorityValueEntry, IDisposable + internal class ConstantValueEntry : IPriorityValueEntry, IConstantValueEntry { private IValueSink _sink; private Optional _value; public ConstantValueEntry( StyledPropertyBase property, - T value, + [AllowNull] T value, BindingPriority priority, IValueSink sink) { @@ -28,7 +36,7 @@ namespace Avalonia.PropertyStore } public StyledPropertyBase Property { get; } - public BindingPriority Priority { get; } + public BindingPriority Priority { get; private set; } Optional IValue.GetValue() => _value.ToObject(); public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation) @@ -36,7 +44,30 @@ namespace Avalonia.PropertyStore return Priority >= maxPriority ? _value : Optional.Empty; } - public void Dispose() => _sink.Completed(Property, this, _value); + public void Dispose() + { + var oldValue = _value; + _value = default; + Priority = BindingPriority.Unset; + _sink.Completed(Property, this, oldValue); + } + public void Reparent(IValueSink sink) => _sink = sink; + public void Start() { } + + public void RaiseValueChanged( + IValueSink sink, + IAvaloniaObject owner, + AvaloniaProperty property, + Optional oldValue, + Optional newValue) + { + sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + owner, + (AvaloniaProperty)property, + oldValue.GetValueOrDefault(), + newValue.GetValueOrDefault(), + Priority)); + } } } diff --git a/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs b/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs new file mode 100644 index 0000000000..af4faf989c --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs @@ -0,0 +1,8 @@ +namespace Avalonia.PropertyStore +{ + internal interface IBatchUpdate + { + void BeginBatchUpdate(); + void EndBatchUpdate(); + } +} diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs index 249cfc360c..7f5245bb45 100644 --- a/src/Avalonia.Base/PropertyStore/IValue.cs +++ b/src/Avalonia.Base/PropertyStore/IValue.cs @@ -9,8 +9,15 @@ namespace Avalonia.PropertyStore /// internal interface IValue { - Optional GetValue(); BindingPriority Priority { get; } + Optional GetValue(); + void Start(); + void RaiseValueChanged( + IValueSink sink, + IAvaloniaObject owner, + AvaloniaProperty property, + Optional oldValue, + Optional newValue); } /// diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index 859e9ba81c..f49b74f4a8 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -24,5 +24,21 @@ namespace Avalonia.PropertyStore } public void SetValue(T value) => _value = value; + public void Start() { } + + public void RaiseValueChanged( + IValueSink sink, + IAvaloniaObject owner, + AvaloniaProperty property, + Optional oldValue, + Optional newValue) + { + sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + owner, + (AvaloniaProperty)property, + oldValue.GetValueOrDefault(), + newValue.GetValueOrDefault(), + BindingPriority.LocalValue)); + } } } diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 5e223cad60..80496fc045 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore /// entries (sorted first by priority and then in the order /// they were added) plus a local value. /// - internal class PriorityValue : IValue, IValueSink + internal class PriorityValue : IValue, IValueSink, IBatchUpdate { private readonly IAvaloniaObject _owner; private readonly IValueSink _sink; @@ -26,6 +26,8 @@ namespace Avalonia.PropertyStore private readonly Func? _coerceValue; private Optional _localValue; private Optional _value; + private bool _isCalculatingValue; + private bool _batchUpdate; public PriorityValue( IAvaloniaObject owner, @@ -53,6 +55,18 @@ namespace Avalonia.PropertyStore existing.Reparent(this); _entries.Add(existing); + if (existing is IBindingEntry binding && + existing.Priority == BindingPriority.LocalValue) + { + // Bit of a special case here: if we have a local value binding that is being + // promoted to a priority value we need to make sure the binding is subscribed + // even if we've got a batch operation in progress because otherwise we don't know + // whether the binding or a subsequent SetValue with local priority will win. A + // notification won't be sent during batch update anyway because it will be + // caught and stored for later by the ValueStore. + binding.Start(ignoreBatchUpdate: true); + } + var v = existing.GetValue(); if (v.HasValue) @@ -78,6 +92,28 @@ namespace Avalonia.PropertyStore public IReadOnlyList> Entries => _entries; Optional IValue.GetValue() => _value.ToObject(); + public void BeginBatchUpdate() + { + _batchUpdate = true; + + foreach (var entry in _entries) + { + (entry as IBatchUpdate)?.BeginBatchUpdate(); + } + } + + public void EndBatchUpdate() + { + _batchUpdate = false; + + foreach (var entry in _entries) + { + (entry as IBatchUpdate)?.EndBatchUpdate(); + } + + UpdateEffectiveValue(null); + } + public void ClearLocalValue() { UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( @@ -134,10 +170,37 @@ namespace Avalonia.PropertyStore var binding = new BindingEntry(_owner, Property, source, priority, this); var insert = FindInsertPoint(binding.Priority); _entries.Insert(insert, binding); + + if (_batchUpdate) + { + binding.BeginBatchUpdate(); + + if (priority == BindingPriority.LocalValue) + { + binding.Start(ignoreBatchUpdate: true); + } + } + return binding; } - public void CoerceValue() => UpdateEffectiveValue(null); + public void UpdateEffectiveValue() => UpdateEffectiveValue(null); + public void Start() => UpdateEffectiveValue(null); + + public void RaiseValueChanged( + IValueSink sink, + IAvaloniaObject owner, + AvaloniaProperty property, + Optional oldValue, + Optional newValue) + { + sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + owner, + (AvaloniaProperty)property, + oldValue.GetValueOrDefault(), + newValue.GetValueOrDefault(), + Priority)); + } void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) { @@ -146,7 +209,7 @@ namespace Avalonia.PropertyStore _localValue = default; } - if (change is AvaloniaPropertyChangedEventArgs c) + if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs c) { UpdateEffectiveValue(c); } @@ -188,41 +251,47 @@ namespace Avalonia.PropertyStore public (Optional, BindingPriority) CalculateValue(BindingPriority maxPriority) { - var reachedLocalValues = false; + _isCalculatingValue = true; - for (var i = _entries.Count - 1; i >= 0; --i) + try { - var entry = _entries[i]; - - if (entry.Priority < maxPriority) + for (var i = _entries.Count - 1; i >= 0; --i) { - continue; + var entry = _entries[i]; + + if (entry.Priority < maxPriority) + { + continue; + } + + entry.Start(); + + if (entry.Priority >= BindingPriority.LocalValue && + maxPriority <= BindingPriority.LocalValue && + _localValue.HasValue) + { + return (_localValue, BindingPriority.LocalValue); + } + + var entryValue = entry.GetValue(); + + if (entryValue.HasValue) + { + return (entryValue, entry.Priority); + } } - if (!reachedLocalValues && - entry.Priority >= BindingPriority.LocalValue && - maxPriority <= BindingPriority.LocalValue && - _localValue.HasValue) + if (maxPriority <= BindingPriority.LocalValue && _localValue.HasValue) { return (_localValue, BindingPriority.LocalValue); } - var entryValue = entry.GetValue(); - - if (entryValue.HasValue) - { - return (entryValue, entry.Priority); - } + return (default, BindingPriority.Unset); } - - if (!reachedLocalValues && - maxPriority <= BindingPriority.LocalValue && - _localValue.HasValue) + finally { - return (_localValue, BindingPriority.LocalValue); + _isCalculatingValue = false; } - - return (default, BindingPriority.Unset); } private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs? change) diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs index 238aba5c96..6a3f9b0b30 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Avalonia.Reactive { @@ -55,9 +56,9 @@ namespace Avalonia.Reactive newValue = (T)e.Sender.GetValue(e.Property); } - if (!Equals(newValue, _value)) + if (!EqualityComparer.Default.Equals(newValue, _value)) { - _value = (T)newValue; + _value = newValue; PublishNext(_value); } } diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs index 6e52b6770a..c513f75962 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -22,6 +22,9 @@ namespace Avalonia.Utilities _entries = s_emptyEntries; } + public int Count => _entries.Length - 1; + public TValue this[int index] => _entries[index].Value; + private (int, bool) TryFindEntry(int propertyId) { if (_entries.Length <= 12) @@ -91,7 +94,7 @@ namespace Avalonia.Utilities return (0, false); } - public bool TryGetValue(AvaloniaProperty property, [MaybeNull] out TValue value) + public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) { (int index, bool found) = TryFindEntry(property.Id); if (!found) @@ -163,18 +166,6 @@ namespace Avalonia.Utilities } } - public Dictionary ToDictionary() - { - var dict = new Dictionary(_entries.Length - 1); - - for (int i = 0; i < _entries.Length - 1; ++i) - { - dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); - } - - return dict; - } - private struct Entry { internal int PropertyId; diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 6b89fcbdb9..495f13e1a9 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Avalonia.Data; using Avalonia.PropertyStore; using Avalonia.Utilities; @@ -26,6 +28,7 @@ namespace Avalonia private readonly AvaloniaObject _owner; private readonly IValueSink _sink; private readonly AvaloniaPropertyValueStore _values; + private BatchUpdate? _batchUpdate; public ValueStore(AvaloniaObject owner) { @@ -33,9 +36,28 @@ namespace Avalonia _values = new AvaloniaPropertyValueStore(); } + public void BeginBatchUpdate() + { + _batchUpdate ??= new BatchUpdate(this); + _batchUpdate.Begin(); + } + + public void EndBatchUpdate() + { + if (_batchUpdate is null) + { + throw new InvalidOperationException("No batch update in progress."); + } + + if (_batchUpdate.End()) + { + _batchUpdate = null; + } + } + public bool IsAnimating(AvaloniaProperty property) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { return slot.Priority < BindingPriority.LocalValue; } @@ -45,7 +67,7 @@ namespace Avalonia public bool IsSet(AvaloniaProperty property) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { return slot.GetValue().HasValue; } @@ -58,7 +80,7 @@ namespace Avalonia BindingPriority maxPriority, out T value) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { var v = ((IValue)slot).GetValue(maxPriority); @@ -82,7 +104,7 @@ namespace Avalonia IDisposable? result = null; - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { result = SetExisting(slot, property, value, priority); } @@ -90,23 +112,21 @@ namespace Avalonia { // If the property has any coercion callbacks then always create a PriorityValue. var entry = new PriorityValue(_owner, property, this); - _values.AddValue(property, entry); + AddValue(property, entry); result = entry.SetValue(value, priority); } else { - var change = new AvaloniaPropertyChangedEventArgs(_owner, property, default, value, priority); - if (priority == BindingPriority.LocalValue) { - _values.AddValue(property, new LocalValueEntry(value)); - _sink.ValueChanged(change); + AddValue(property, new LocalValueEntry(value)); + NotifyValueChanged(property, default, value, priority); } else { var entry = new ConstantValueEntry(property, value, priority, this); - _values.AddValue(property, entry); - _sink.ValueChanged(change); + AddValue(property, entry); + NotifyValueChanged(property, default, value, priority); result = entry; } } @@ -119,7 +139,7 @@ namespace Avalonia IObservable> source, BindingPriority priority) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { return BindExisting(slot, property, source, priority); } @@ -128,62 +148,69 @@ namespace Avalonia // If the property has any coercion callbacks then always create a PriorityValue. var entry = new PriorityValue(_owner, property, this); var binding = entry.AddBinding(source, priority); - _values.AddValue(property, entry); - binding.Start(); + AddValue(property, entry); return binding; } else { var entry = new BindingEntry(_owner, property, source, priority, this); - _values.AddValue(property, entry); - entry.Start(); + AddValue(property, entry); return entry; } } public void ClearLocalValue(StyledPropertyBase property) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { if (slot is PriorityValue p) { p.ClearLocalValue(); } - else + else if (slot.Priority == BindingPriority.LocalValue) { - var remove = slot is ConstantValueEntry c ? - c.Priority == BindingPriority.LocalValue : - !(slot is IPriorityValueEntry); + var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default; - if (remove) + // During batch update values can't be removed immediately because they're needed to raise + // a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal + // by setting their priority to Unset. + if (!IsBatchUpdating()) { - var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default; _values.Remove(property); - _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( - _owner, - property, - new Optional(old), - default, - BindingPriority.Unset)); } + else if (slot is IDisposable d) + { + d.Dispose(); + } + else + { + // Local value entries are optimized and contain only a single value field to save space, + // so there's no way to mark them for removal at the end of a batch update. Instead convert + // them to a constant value entry with Unset priority in the event of a local value being + // cleared during a batch update. + var sentinel = new ConstantValueEntry(property, default, BindingPriority.Unset, _sink); + _values.SetValue(property, sentinel); + } + + NotifyValueChanged(property, old, default, BindingPriority.Unset); } } } public void CoerceValue(StyledPropertyBase property) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { if (slot is PriorityValue p) { - p.CoerceValue(); + p.UpdateEffectiveValue(); } } } public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { var slotValue = slot.GetValue(); return new Diagnostics.AvaloniaPropertyValue( @@ -198,7 +225,17 @@ namespace Avalonia void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) { - _sink.ValueChanged(change); + if (_batchUpdate is object) + { + if (change.IsEffectiveValueChange) + { + NotifyValueChanged(change.Property, change.OldValue, change.NewValue, change.Priority); + } + } + else + { + _sink.ValueChanged(change); + } } void IValueSink.Completed( @@ -206,13 +243,18 @@ namespace Avalonia IPriorityValueEntry entry, Optional oldValue) { - if (_values.TryGetValue(property, out var slot)) + // We need to include remove sentinels here so call `_values.TryGetValue` directly. + if (_values.TryGetValue(property, out var slot) && slot == entry) { - if (slot == entry) + if (_batchUpdate is null) { _values.Remove(property); _sink.Completed(property, entry, oldValue); } + else + { + _batchUpdate.ValueChanged(property, oldValue.ToObject()); + } } } @@ -240,16 +282,13 @@ namespace Avalonia { var old = l.GetValue(BindingPriority.LocalValue); l.SetValue(value); - _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( - _owner, - property, - old, - value, - priority)); + NotifyValueChanged(property, old, value, priority); } else { var priorityValue = new PriorityValue(_owner, property, this, l); + if (IsBatchUpdating()) + priorityValue.BeginBatchUpdate(); result = priorityValue.SetValue(value, priority); _values.SetValue(property, priorityValue); } @@ -273,6 +312,11 @@ namespace Avalonia if (slot is IPriorityValueEntry e) { priorityValue = new PriorityValue(_owner, property, this, e); + + if (IsBatchUpdating()) + { + priorityValue.BeginBatchUpdate(); + } } else if (slot is PriorityValue p) { @@ -289,8 +333,181 @@ namespace Avalonia var binding = priorityValue.AddBinding(source, priority); _values.SetValue(property, priorityValue); - binding.Start(); + priorityValue.UpdateEffectiveValue(); return binding; } + + private void AddValue(AvaloniaProperty property, IValue value) + { + _values.AddValue(property, value); + if (IsBatchUpdating() && value is IBatchUpdate batch) + batch.BeginBatchUpdate(); + value.Start(); + } + + private void NotifyValueChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) + { + if (_batchUpdate is null) + { + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner, + property, + oldValue, + newValue, + priority)); + } + else + { + _batchUpdate.ValueChanged(property, oldValue.ToObject()); + } + } + + private bool IsBatchUpdating() => _batchUpdate?.IsBatchUpdating == true; + + private bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out IValue value) + { + return _values.TryGetValue(property, out value) && !IsRemoveSentinel(value); + } + + private static bool IsRemoveSentinel(IValue value) + { + // Local value entries are optimized and contain only a single value field to save space, + // so there's no way to mark them for removal at the end of a batch update. Instead a + // ConstantValueEntry with a priority of Unset is used as a sentinel value. + return value is IConstantValueEntry t && t.Priority == BindingPriority.Unset; + } + + private class BatchUpdate + { + private ValueStore _owner; + private List? _notifications; + private int _batchUpdateCount; + private int _iterator = -1; + + public BatchUpdate(ValueStore owner) => _owner = owner; + + public bool IsBatchUpdating => _batchUpdateCount > 0; + + public void Begin() + { + if (_batchUpdateCount++ == 0) + { + var values = _owner._values; + + for (var i = 0; i < values.Count; ++i) + { + (values[i] as IBatchUpdate)?.BeginBatchUpdate(); + } + } + } + + public bool End() + { + if (--_batchUpdateCount > 0) + return false; + + var values = _owner._values; + + // First call EndBatchUpdate on all bindings. This should cause the active binding to be subscribed + // but notifications will still not be raised because the owner ValueStore will still have a reference + // to this batch update object. + for (var i = 0; i < values.Count; ++i) + { + (values[i] as IBatchUpdate)?.EndBatchUpdate(); + + // Somehow subscribing to a binding caused a new batch update. This shouldn't happen but in case it + // does, abort and continue batch updating. + if (_batchUpdateCount > 0) + return false; + } + + if (_notifications is object) + { + // Raise all batched notifications. Doing this can cause other notifications to be added and even + // cause a new batch update to start, so we need to handle _notifications being modified by storing + // the index in field. + _iterator = 0; + + for (; _iterator < _notifications.Count; ++_iterator) + { + var entry = _notifications[_iterator]; + + if (values.TryGetValue(entry.property, out var slot)) + { + var oldValue = entry.oldValue; + var newValue = slot.GetValue(); + + // Raising this notification can cause a new batch update to be started, which in turn + // results in another change to the property. In this case we need to update the old value + // so that the *next* notification has an oldValue which follows on from the newValue + // raised here. + _notifications[_iterator] = new Notification + { + property = entry.property, + oldValue = newValue, + }; + + // Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs. + slot.RaiseValueChanged(_owner._sink, _owner._owner, entry.property, oldValue, newValue); + + // During batch update values can't be removed immediately because they're needed to raise + // the _sink.ValueChanged notification. They instead mark themselves for removal by setting + // their priority to Unset. We need to re-read the slot here because raising ValueChanged + // could have caused it to be updated. + if (values.TryGetValue(entry.property, out var updatedSlot) && + updatedSlot.Priority == BindingPriority.Unset) + { + values.Remove(entry.property); + } + } + else + { + throw new AvaloniaInternalException("Value could not be found at the end of batch update."); + } + + // If a new batch update was started while ending this one, abort. + if (_batchUpdateCount > 0) + return false; + } + } + + _iterator = int.MaxValue - 1; + return true; + } + + public void ValueChanged(AvaloniaProperty property, Optional oldValue) + { + _notifications ??= new List(); + + for (var i = 0; i < _notifications.Count; ++i) + { + if (_notifications[i].property == property) + { + oldValue = _notifications[i].oldValue; + _notifications.RemoveAt(i); + + if (i <= _iterator) + --_iterator; + break; + } + } + + _notifications.Add(new Notification + { + property = property, + oldValue = oldValue, + }); + } + + private struct Notification + { + public AvaloniaProperty property; + public Optional oldValue; + } + } } } diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs index b97f2a2bcb..7c81c90d82 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs @@ -3275,7 +3275,7 @@ namespace Avalonia.Collections addIndex); // next check if we need to add an item into the current group - // bool needsGrouping = false; + bool needsGrouping = false; if (Count == 1 && GroupDescriptions.Count > 0) { // if this is the first item being added @@ -3302,7 +3302,7 @@ namespace Avalonia.Collections // otherwise, we need to validate that it is within the current page. if (PageSize == 0 || (PageIndex + 1) * PageSize > leafIndex) { - //needsGrouping = true; + needsGrouping = true; int pageStartIndex = PageIndex * PageSize; @@ -3340,6 +3340,13 @@ namespace Avalonia.Collections } } + // if we need to add the item into the current group + // that will be displayed + if (needsGrouping) + { + this._group.AddToSubgroups(addedItem, false /*loading*/); + } + int addedIndex = IndexOf(addedItem); // if the item is within the current page diff --git a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs index e2a067ac61..ccf1f3f77a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs @@ -17,7 +17,6 @@ namespace Avalonia.Controls /// public class DataGridCheckBoxColumn : DataGridBoundColumn { - private bool _beganEditWithKeyboard; private CheckBox _currentCheckBox; private DataGrid _owningGrid; @@ -153,23 +152,7 @@ namespace Avalonia.Controls { if (editingElement is CheckBox editingCheckBox) { - bool? uneditedValue = editingCheckBox.IsChecked; - bool editValue = false; - if(editingEventArgs is PointerPressedEventArgs args) - { - // Editing was triggered by a mouse click - Point position = args.GetPosition(editingCheckBox); - Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height); - editValue = rect.Contains(position); - } - else if (_beganEditWithKeyboard) - { - // Editing began by a user pressing spacebar - editValue = true; - _beganEditWithKeyboard = false; - } - - if (editValue) + void EditValue() { // User clicked the checkbox itself or pressed space, let's toggle the IsChecked value if (editingCheckBox.IsThreeState) @@ -192,6 +175,40 @@ namespace Avalonia.Controls editingCheckBox.IsChecked = !editingCheckBox.IsChecked; } } + + bool? uneditedValue = editingCheckBox.IsChecked; + if(editingEventArgs is PointerPressedEventArgs args) + { + void ProcessPointerArgs() + { + // Editing was triggered by a mouse click + Point position = args.GetPosition(editingCheckBox); + Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height); + if(rect.Contains(position)) + { + EditValue(); + } + } + + void OnLayoutUpdated(object sender, EventArgs e) + { + if(!editingCheckBox.Bounds.IsEmpty) + { + editingCheckBox.LayoutUpdated -= OnLayoutUpdated; + ProcessPointerArgs(); + } + } + + if(editingCheckBox.Bounds.IsEmpty) + { + editingCheckBox.LayoutUpdated += OnLayoutUpdated; + } + else + { + ProcessPointerArgs(); + } + } + return uneditedValue; } return false; @@ -284,13 +301,10 @@ namespace Avalonia.Controls CheckBox checkBox = GetCellContent(row) as CheckBox; if (checkBox == _currentCheckBox) { - _beganEditWithKeyboard = true; OwningGrid.BeginEdit(); - return; } } } - _beganEditWithKeyboard = false; } private void OwningGrid_LoadingRow(object sender, DataGridRowEventArgs e) diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index e513a7b678..7e95dd100c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Metadata; using Avalonia.Utilities; namespace Avalonia.Controls @@ -22,6 +23,7 @@ namespace Avalonia.Controls o => o.CellTemplate, (o, v) => o.CellTemplate = v); + [Content] public IDataTemplate CellTemplate { get { return _cellTemplate; } diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index e5adc8c6ed..0284463f1c 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -1,6 +1,9 @@ Compat issues with assembly Avalonia.Controls: +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -Total Issues: 4 +Total Issues: 7 diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 3bf72460df..54c576bb76 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -30,7 +30,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost, IApplicationPlatformEvents { /// /// The application-global data templates. @@ -55,6 +55,8 @@ namespace Avalonia /// public event EventHandler ResourcesChanged; + public event EventHandler UrlsOpened; + /// /// Creates an instance of the class. /// @@ -247,7 +249,11 @@ namespace Avalonia public virtual void OnFrameworkInitializationCompleted() { - + } + + void IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls) + { + UrlsOpened?.Invoke(this, new UrlOpenedEventArgs (urls)); } private void NotifyResourcesChanged(ResourcesChangedEventArgs e) @@ -288,5 +294,6 @@ namespace Avalonia get => _name; set => SetAndRaise(NameProperty, ref _name, value); } + } } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index e2c8e7e8e2..aa4342f075 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -5,6 +5,7 @@ using System.Threading; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Interactivity; +using Avalonia.Platform; using Avalonia.Threading; namespace Avalonia.Controls.ApplicationLifetimes @@ -102,6 +103,14 @@ namespace Avalonia.Controls.ApplicationLifetimes public int Start(string[] args) { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); + + var options = AvaloniaLocator.Current.GetService(); + + if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0) + { + ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); + } + _cts = new CancellationTokenSource(); MainWindow?.Show(); Dispatcher.UIThread.MainLoop(_cts.Token); @@ -115,6 +124,11 @@ namespace Avalonia.Controls.ApplicationLifetimes _activeLifetime = null; } } + + public class ClassicDesktopStyleApplicationLifetimeOptions + { + public bool ProcessUrlActivationCommandLine { get; set; } + } } namespace Avalonia diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index b59fd7abde..aab6a41890 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -483,7 +483,9 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect( nameof(SelectedItem), o => o.SelectedItem, - (o, v) => o.SelectedItem = v); + (o, v) => o.SelectedItem = v, + defaultBindingMode: BindingMode.TwoWay, + enableDataValidation: true); /// /// Identifies the @@ -1333,7 +1335,7 @@ namespace Avalonia.Controls base.OnApplyTemplate(e); } - + /// /// Called to update the validation state for properties for which data validation is /// enabled. @@ -1342,7 +1344,7 @@ namespace Avalonia.Controls /// The new binding value for the property. protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { - if (property == TextProperty) + if (property == TextProperty || property == SelectedItemProperty) { DataValidationErrors.SetError(this, value.Error); } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 20ca41bc57..c5af5ffa7a 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -10,6 +10,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; +using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -76,6 +77,14 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsTextSearchEnabledProperty = + AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); + + private string _textSearchTerm = string.Empty; + private DispatcherTimer _textSearchTimer; private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; @@ -164,6 +173,15 @@ namespace Avalonia.Controls set { SetValue(VerticalContentAlignmentProperty, value); } } + /// + /// Gets or sets a value that specifies whether a user can jump to a value by typing. + /// + public bool IsTextSearchEnabled + { + get { return GetValue(IsTextSearchEnabledProperty); } + set { SetValue(IsTextSearchEnabledProperty, value); } + } + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { @@ -229,6 +247,32 @@ namespace Avalonia.Controls } } + /// + protected override void OnTextInput(TextInputEventArgs e) + { + if (!IsTextSearchEnabled || e.Handled) + return; + + StopTextSearchTimer(); + + _textSearchTerm += e.Text; + + bool match(ItemContainerInfo info) => + info.ContainerControl is IContentControl control && + control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + + var info = ItemContainerGenerator.Containers.FirstOrDefault(match); + + if (info != null) + { + SelectedIndex = info.Index; + } + + StartTextSearchTimer(); + + e.Handled = true; + } + /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { @@ -426,5 +470,31 @@ namespace Avalonia.Controls SelectedIndex = prev; } + + private void StartTextSearchTimer() + { + _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + _textSearchTimer.Tick += TextSearchTimer_Tick; + _textSearchTimer.Start(); + } + + private void StopTextSearchTimer() + { + if (_textSearchTimer == null) + { + return; + } + + _textSearchTimer.Stop(); + _textSearchTimer.Tick -= TextSearchTimer_Tick; + + _textSearchTimer = null; + } + + private void TextSearchTimer_Tick(object sender, EventArgs e) + { + _textSearchTerm = string.Empty; + StopTextSearchTimer(); + } } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 41370d8464..4aab92c428 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -8,6 +8,8 @@ using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls { /// @@ -18,25 +20,25 @@ namespace Avalonia.Controls /// /// - A property to allow user-defined data to be attached to the control. /// - public class Control : InputElement, IControl, INamed, ISupportInitialize, IVisualBrushInitialize, ISetterValue + public class Control : InputElement, IControl, INamed, IVisualBrushInitialize, ISetterValue { /// /// Defines the property. /// - public static readonly StyledProperty> FocusAdornerProperty = - AvaloniaProperty.Register>(nameof(FocusAdorner)); + public static readonly StyledProperty?> FocusAdornerProperty = + AvaloniaProperty.Register?>(nameof(FocusAdorner)); /// /// Defines the property. /// - public static readonly StyledProperty TagProperty = - AvaloniaProperty.Register(nameof(Tag)); + public static readonly StyledProperty TagProperty = + AvaloniaProperty.Register(nameof(Tag)); /// /// Defines the property. /// - public static readonly StyledProperty ContextMenuProperty = - AvaloniaProperty.Register(nameof(ContextMenu)); + public static readonly StyledProperty ContextMenuProperty = + AvaloniaProperty.Register(nameof(ContextMenu)); /// /// Event raised when an element wishes to be scrolled into view. @@ -44,16 +46,16 @@ namespace Avalonia.Controls public static readonly RoutedEvent RequestBringIntoViewEvent = RoutedEvent.Register("RequestBringIntoView", RoutingStrategies.Bubble); - private DataTemplates _dataTemplates; - private IControl _focusAdorner; + private DataTemplates? _dataTemplates; + private IControl? _focusAdorner; /// /// Gets or sets the control's focus adorner. /// - public ITemplate FocusAdorner + public ITemplate? FocusAdorner { - get { return GetValue(FocusAdornerProperty); } - set { SetValue(FocusAdornerProperty, value); } + get => GetValue(FocusAdornerProperty); + set => SetValue(FocusAdornerProperty, value); } /// @@ -63,27 +65,27 @@ namespace Avalonia.Controls /// Each control may define data templates which are applied to the control itself and its /// children. /// - public DataTemplates DataTemplates => _dataTemplates ?? (_dataTemplates = new DataTemplates()); + public DataTemplates DataTemplates => _dataTemplates ??= new DataTemplates(); /// /// Gets or sets a context menu to the control. /// - public ContextMenu ContextMenu + public ContextMenu? ContextMenu { - get { return GetValue(ContextMenuProperty); } - set { SetValue(ContextMenuProperty, value); } + get => GetValue(ContextMenuProperty); + set => SetValue(ContextMenuProperty, value); } /// /// Gets or sets a user-defined object attached to the control. /// - public object Tag + public object? Tag { - get { return GetValue(TagProperty); } - set { SetValue(TagProperty, value); } + get => GetValue(TagProperty); + set => SetValue(TagProperty, value); } - public new IControl Parent => (IControl)base.Parent; + public new IControl? Parent => (IControl?)base.Parent; /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; @@ -106,15 +108,10 @@ namespace Avalonia.Controls { var c = i as IControl; - if (c?.IsInitialized == false) + if (c?.IsInitialized == false && c is ISupportInitialize init) { - var init = c as ISupportInitialize; - - if (init != null) - { - init.BeginInit(); - init.EndInit(); - } + init.BeginInit(); + init.EndInit(); } } } @@ -131,10 +128,7 @@ namespace Avalonia.Controls /// Gets the element that receives the focus adorner. /// /// The control that receives the focus adorner. - protected virtual IControl GetTemplateFocusTarget() - { - return this; - } + protected virtual IControl? GetTemplateFocusTarget() => this; /// protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) @@ -173,15 +167,10 @@ namespace Avalonia.Controls } } - if (_focusAdorner != null) + if (_focusAdorner != null && GetTemplateFocusTarget() is Visual target) { - var target = (Visual)GetTemplateFocusTarget(); - - if (target != null) - { - AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target); - adornerLayer.Children.Add(_focusAdorner); - } + AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target); + adornerLayer.Children.Add(_focusAdorner); } } } diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 38ebbe5bf9..3237f6f37b 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -662,31 +662,64 @@ namespace Avalonia.Controls { DefinitionBase definitionBase = _registry[i]; - if (sharedMinSizeChanged || definitionBase.LayoutWasUpdated) + // we'll set d.UseSharedMinimum to maintain the invariant: + // d.UseSharedMinimum iff d._minSize < this.MinSize + // i.e. iff d is not a "long-pole" definition. + // + // Measure/Arrange of d's Grid uses d._minSize for long-pole + // definitions, and max(d._minSize, shared size) for + // short-pole definitions. This distinction allows us to react + // to changes in "long-pole-ness" more efficiently and correctly, + // by avoiding remeasures when a long-pole definition changes. + bool useSharedMinimum = !MathUtilities.AreClose(definitionBase._minSize, sharedMinSize); + + // before doing that, determine whether d's Grid needs to be remeasured. + // It's important _not_ to remeasure if the last measure is still + // valid, otherwise infinite loops are possible + bool measureIsValid; + + if(!definitionBase.UseSharedMinimum) { - // if definition's min size is different, then need to re-measure - if (!MathUtilities.AreClose(sharedMinSize, definitionBase.MinSize)) - { - Grid parentGrid = (Grid)definitionBase.Parent; - parentGrid.InvalidateMeasure(); - definitionBase.UseSharedMinimum = true; - } - else - { - definitionBase.UseSharedMinimum = false; - - // if measure is valid then also need to check arrange. - // Note: definitionBase.SizeCache is volatile but at this point - // it contains up-to-date final size - if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache)) - { - Grid parentGrid = (Grid)definitionBase.Parent; - parentGrid.InvalidateArrange(); - } - } + // d was a long-pole. measure is valid iff it's still a long-pole, + // since previous measure didn't use shared size. + measureIsValid = !useSharedMinimum; + } + else if(useSharedMinimum) + { + // d was a short-pole, and still is. measure is valid + // iff the shared size didn't change + measureIsValid = !sharedMinSizeChanged; + } + else + { + // d was a short-pole, but is now a long-pole. This can + // happen in several ways: + // a. d's minSize increased to or past the old shared size + // b. other long-pole definitions decreased, leaving + // d as the new winner + // In the former case, the measure is valid - it used + // d's new larger minSize. In the latter case, the + // measure is invalid - it used the old shared size, + // which is larger than d's (possibly changed) minSize + measureIsValid = (definitionBase.LayoutWasUpdated && + MathUtilities.GreaterThanOrClose(definitionBase._minSize, this.MinSize)); + } - definitionBase.LayoutWasUpdated = false; + if(!measureIsValid) + { + definitionBase.Parent.InvalidateMeasure(); } + else if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache)) + { + // if measure is valid then also need to check arrange. + // Note: definitionBase.SizeCache is volatile but at this point + // it contains up-to-date final size + definitionBase.Parent.InvalidateArrange(); + } + + // now we can restore the invariant, and clear the layout flag + definitionBase.UseSharedMinimum = useSharedMinimum; + definitionBase.LayoutWasUpdated = false; } _minSize = sharedMinSize; diff --git a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs index d2e05ee136..840a1f66f1 100644 --- a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs @@ -1,4 +1,10 @@ +using System; +using System.Collections.Generic; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.LogicalTree; +using Avalonia.Reactive; +using Avalonia.VisualTree; namespace Avalonia.Controls.Generators { @@ -16,11 +22,15 @@ namespace Avalonia.Controls.Generators { var tabItem = (TabItem)base.CreateContainer(item); - tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty]; + tabItem.Bind(TabItem.TabStripPlacementProperty, new OwnerBinding( + tabItem, + TabControl.TabStripPlacementProperty)); if (tabItem.HeaderTemplate == null) { - tabItem[~HeaderedContentControl.HeaderTemplateProperty] = Owner[~ItemsControl.ItemTemplateProperty]; + tabItem.Bind(TabItem.HeaderTemplateProperty, new OwnerBinding( + tabItem, + TabControl.ItemTemplateProperty)); } if (tabItem.Header == null) @@ -40,10 +50,49 @@ namespace Avalonia.Controls.Generators if (!(tabItem.Content is IControl)) { - tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty]; + tabItem.Bind(TabItem.ContentTemplateProperty, new OwnerBinding( + tabItem, + TabControl.ContentTemplateProperty)); } return tabItem; } + + private class OwnerBinding : SingleSubscriberObservableBase + { + private readonly TabItem _item; + private readonly StyledProperty _ownerProperty; + private IDisposable _ownerSubscription; + private IDisposable _propertySubscription; + + public OwnerBinding(TabItem item, StyledProperty ownerProperty) + { + _item = item; + _ownerProperty = ownerProperty; + } + + protected override void Subscribed() + { + _ownerSubscription = ControlLocator.Track(_item, 0, typeof(TabControl)).Subscribe(OwnerChanged); + } + + protected override void Unsubscribed() + { + _ownerSubscription?.Dispose(); + _ownerSubscription = null; + } + + private void OwnerChanged(ILogical c) + { + _propertySubscription?.Dispose(); + _propertySubscription = null; + + if (c is TabControl tabControl) + { + _propertySubscription = tabControl.GetObservable(_ownerProperty) + .Subscribe(x => PublishNext(x)); + } + } + } } } diff --git a/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs b/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs index 672d5c1a13..f492e6ca0f 100644 --- a/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs +++ b/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs @@ -3,5 +3,7 @@ namespace Avalonia.Controls public interface INativeMenuExporterEventsImplBridge { void RaiseNeedsUpdate (); + void RaiseOpening(); + void RaiseClosed(); } } diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs index 20eac11c2c..64414b1f47 100644 --- a/src/Avalonia.Controls/NativeControlHost.cs +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -157,10 +157,14 @@ namespace Avalonia.Controls var needsShow = IsEffectivelyVisible && bounds.HasValue; if (needsShow) + { + if (bounds.Value.IsEmpty) + return false; _attachment?.ShowInBounds(bounds.Value); + } else _attachment?.HideWithSize(Bounds.Size); - return false; + return true; } private void CheckDestruction() diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 38a9f03d29..58ee99722f 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -12,13 +12,34 @@ namespace Avalonia.Controls private readonly AvaloniaList _items = new AvaloniaList { ResetBehavior = ResetBehavior.Remove }; private NativeMenuItem _parent; + [Content] public IList Items => _items; /// - /// Raised when the user clicks the menu and before its opened. Use this event to update the menu dynamically. + /// Raised when the menu requests an update. + /// + /// + /// Use this event to add, remove or modify menu items before a menu is + /// shown or a hotkey is pressed. + /// + public event EventHandler NeedsUpdate; + + /// + /// Raised before the menu is opened. /// + /// + /// Do not update the menu in this event; use . + /// public event EventHandler Opening; + + /// + /// Raised after the menu is closed. + /// + /// + /// Do not update the menu in this event; use . + /// + public event EventHandler Closed; public NativeMenu() { @@ -27,10 +48,20 @@ namespace Avalonia.Controls } void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate() + { + NeedsUpdate?.Invoke(this, EventArgs.Empty); + } + + void INativeMenuExporterEventsImplBridge.RaiseOpening() { Opening?.Invoke(this, EventArgs.Empty); } + void INativeMenuExporterEventsImplBridge.RaiseClosed() + { + Closed?.Invoke(this, EventArgs.Empty); + } + private void Validator(NativeMenuItemBase obj) { if (obj.Parent != null) diff --git a/src/Avalonia.Controls/NativeMenuItemSeparator.cs b/src/Avalonia.Controls/NativeMenuItemSeparator.cs new file mode 100644 index 0000000000..d3d3721c89 --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuItemSeparator.cs @@ -0,0 +1,16 @@ +using System; + +namespace Avalonia.Controls +{ + + [Obsolete("This class exists to maintain backwards compatiblity with existing code. Use NativeMenuItemSeparator instead")] + public class NativeMenuItemSeperator : NativeMenuItemSeparator + { + } + + public class NativeMenuItemSeparator : NativeMenuItemBase + { + [Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)] + public string Header => "-"; + } +} diff --git a/src/Avalonia.Controls/NativeMenuItemSeperator.cs b/src/Avalonia.Controls/NativeMenuItemSeperator.cs deleted file mode 100644 index e743483dab..0000000000 --- a/src/Avalonia.Controls/NativeMenuItemSeperator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Avalonia.Controls -{ - public class NativeMenuItemSeperator : NativeMenuItemBase - { - [Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)] - public string Header => "-"; - } -} diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index cdbace3ced..ad883c8076 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -16,6 +16,11 @@ namespace Avalonia.Controls.Notifications private bool _isClosed; private bool _isClosing; + static NotificationCard() + { + CloseOnClickProperty.Changed.AddClassHandler