diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 6b910fc615..4e34d4b132 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,6 @@ about: Create a report to help us improve Avalonia title: '' labels: bug assignees: '' - --- **Describe the bug** @@ -12,6 +11,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,8 +24,9 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. Windows, Mac, Linux (State distribution)] - - Version [e.g. 0.10.0-rc1 or 0.9.12] + +- OS: [e.g. Windows, Mac, Linux (State distribution)] +- Version [e.g. 0.10.0-rc1 or 0.9.12] **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..687355d825 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Questions, Discussions, Ideas + url: https://github.com/AvaloniaUI/Avalonia/discussions/new + about: Please ask and answer questions here. + - name: Avalonia Community Support on Gitter + url: https://gitter.im/AvaloniaUI/Avalonia + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 11fc491ef1..5f0a04cee3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,6 @@ about: Suggest an idea for this project title: '' labels: enhancement assignees: '' - --- **Is your feature request related to a problem? Please describe.** 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/Screens.mm b/native/Avalonia.Native/src/OSX/Screens.mm index 455cfa2e41..10f698ff45 100644 --- a/native/Avalonia.Native/src/OSX/Screens.mm +++ b/native/Avalonia.Native/src/OSX/Screens.mm @@ -5,12 +5,6 @@ class Screens : public ComSingleObject public: FORWARD_IUNKNOWN() - private: - CGFloat PrimaryDisplayHeight() - { - return NSMaxY([[[NSScreen screens] firstObject] frame]); - } - public: virtual HRESULT GetScreenCount (int* ret) override { @@ -36,12 +30,12 @@ public: ret->Bounds.Height = [screen frame].size.height; ret->Bounds.Width = [screen frame].size.width; ret->Bounds.X = [screen frame].origin.x; - ret->Bounds.Y = PrimaryDisplayHeight() - [screen frame].origin.y - ret->Bounds.Height; + ret->Bounds.Y = ConvertPointY(ToAvnPoint([screen frame].origin)).Y - ret->Bounds.Height; ret->WorkingArea.Height = [screen visibleFrame].size.height; ret->WorkingArea.Width = [screen visibleFrame].size.width; ret->WorkingArea.X = [screen visibleFrame].origin.x; - ret->WorkingArea.Y = ret->Bounds.Height - [screen visibleFrame].origin.y - ret->WorkingArea.Height; + ret->WorkingArea.Y = ConvertPointY(ToAvnPoint([screen visibleFrame].origin)).Y - ret->WorkingArea.Height; ret->PixelDensity = [screen backingScaleFactor]; 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 871bca086d..0f7215f37c 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -28,12 +28,15 @@ 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); extern AvnPoint ConvertPointY (AvnPoint p); +extern CGFloat PrimaryDisplayHeight(); extern NSSize ToNSSize (AvnSize s); #ifdef DEBUG #define NSDebugLog(...) NSLog(__VA_ARGS__) diff --git a/native/Avalonia.Native/src/OSX/cursor.mm b/native/Avalonia.Native/src/OSX/cursor.mm index b6f9ed5071..1732d6e71f 100644 --- a/native/Avalonia.Native/src/OSX/cursor.mm +++ b/native/Avalonia.Native/src/OSX/cursor.mm @@ -62,6 +62,28 @@ public: return S_OK; } + + virtual HRESULT CreateCustomCursor (void* bitmapData, size_t length, AvnPixelSize hotPixel, IAvnCursor** retOut) override + { + if(bitmapData == nullptr || retOut == nullptr) + { + return E_POINTER; + } + + NSData *imageData = [NSData dataWithBytes:bitmapData length:length]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + + NSPoint hotSpot; + hotSpot.x = hotPixel.Width; + hotSpot.y = hotPixel.Height; + + *retOut = new Cursor([[NSCursor new] initWithImage: image hotSpot: hotSpot]); + + (*retOut)->AddRef(); + + return S_OK; + } }; extern IAvnCursorFactory* CreateCursorFactory() diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index cd6ef73826..11742e3b5c 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([self window]); if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) { - auto window = dynamic_cast(_parent.getRaw()); - - if(window != nullptr) + if(trigerInputWhenDisabled) { - window->WindowEvents->GotInputWhenDisabled(); + auto window = dynamic_cast(_parent.getRaw()); + + if(window != nullptr) + { + window->WindowEvents->GotInputWhenDisabled(); + } } return TRUE; @@ -1412,7 +1415,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type { - if([self ignoreUserInput]) + bool triggerInputWhenDisabled = type != Move; + + if([self ignoreUserInput: triggerInputWhenDisabled]) { return; } @@ -1578,7 +1583,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type { - if([self ignoreUserInput]) + if([self ignoreUserInput: false]) { return; } @@ -1872,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]; + } } } @@ -2063,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; diff --git a/samples/ControlCatalog/Assets/avalonia-32.png b/samples/ControlCatalog/Assets/avalonia-32.png new file mode 100644 index 0000000000..7b443e7a25 Binary files /dev/null and b/samples/ControlCatalog/Assets/avalonia-32.png differ diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index f001425964..142c532d75 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -22,6 +22,10 @@ + + + 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/CursorPage.xaml b/samples/ControlCatalog/Pages/CursorPage.xaml new file mode 100644 index 0000000000..a28039ea3f --- /dev/null +++ b/samples/ControlCatalog/Pages/CursorPage.xaml @@ -0,0 +1,29 @@ + + + + Cursor + Defines a cursor (mouse pointer) + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CursorPage.xaml.cs b/samples/ControlCatalog/Pages/CursorPage.xaml.cs new file mode 100644 index 0000000000..9e9e9ba8b9 --- /dev/null +++ b/samples/ControlCatalog/Pages/CursorPage.xaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public class CursorPage : UserControl + { + public CursorPage() + { + this.InitializeComponent(); + DataContext = new CursorPageViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index 6817d0698e..323eaa3463 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -1,5 +1,5 @@ @@ -26,7 +26,8 @@ - + + diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs index 2a30f4d91b..dc5cc49a90 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs @@ -24,8 +24,10 @@ namespace ControlCatalog.Pages dg1.LoadingRow += Dg1_LoadingRow; dg1.Sorting += (s, a) => { - var property = ((a.Column as DataGridBoundColumn)?.Binding as Binding).Path; - if (property == dataGridSortDescription.PropertyPath + var binding = (a.Column as DataGridBoundColumn)?.Binding as Binding; + + if (binding?.Path is string property + && property == dataGridSortDescription.PropertyPath && !collectionView1.SortDescriptions.Contains(dataGridSortDescription)) { collectionView1.SortDescriptions.Add(dataGridSortDescription); 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/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index c39f414b44..4edb0f137a 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -29,7 +29,7 @@ namespace ControlCatalog.Pages var screens = w.Screens.All; var scaling = ((IRenderRoot)w).RenderScaling; - var drawBrush = Brushes.Green; + var drawBrush = Brushes.Black; Pen p = new Pen(drawBrush); if (screens != null) foreach (Screen screen in screens) @@ -45,18 +45,16 @@ namespace ControlCatalog.Pages screen.Bounds.Height / 10f); Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f, screen.WorkingArea.Height / 10f); + context.DrawRectangle(p, boundsRect); context.DrawRectangle(p, workingAreaRect); - - FormattedText text = new FormattedText() - { - Typeface = Typeface.Default - }; - text.Text = $"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}"; + var text = new FormattedText() { Typeface = new Typeface("Arial"), FontSize = 18 }; + + text.Text = $"Bounds: {screen.Bounds.TopLeft} {screen.Bounds.Width}:{screen.Bounds.Height}"; context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height), text); - text.Text = $"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}"; + text.Text = $"WorkArea: {screen.WorkingArea.TopLeft} {screen.WorkingArea.Width}:{screen.WorkingArea.Height}"; context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text); text.Text = $"Scaling: {screen.PixelDensity * 100}%"; @@ -69,7 +67,7 @@ namespace ControlCatalog.Pages context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text); } - context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10, w.Bounds.Width / 10, w.Bounds.Height / 10)); + context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f, w.Bounds.Width / 10, w.Bounds.Height / 10)); } } } 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/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs b/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs new file mode 100644 index 0000000000..f1cc0637dc --- /dev/null +++ b/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.Input; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class CursorPageViewModel : ViewModelBase + { + public CursorPageViewModel() + { + StandardCursors = Enum.GetValues(typeof(StandardCursorType)) + .Cast() + .Select(x => new StandardCursorModel(x)) + .ToList(); + + var loader = AvaloniaLocator.Current.GetService(); + var s = loader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png")); + var bitmap = new Bitmap(s); + CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16)); + } + + public IEnumerable StandardCursors { get; } + + public Cursor CustomCursor { get; } + + public class StandardCursorModel + { + public StandardCursorModel(StandardCursorType type) + { + Type = type; + Cursor = new Cursor(type); + } + + public StandardCursorType Type { get; } + + public Cursor Cursor { get; } + } + } +} 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/EnumExtensions.cs b/src/Avalonia.Base/EnumExtensions.cs index 1e4864283f..bc1f8d36a9 100644 --- a/src/Avalonia.Base/EnumExtensions.cs +++ b/src/Avalonia.Base/EnumExtensions.cs @@ -11,10 +11,32 @@ namespace Avalonia [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe bool HasFlagCustom(this T value, T flag) where T : unmanaged, Enum { - var intValue = *(int*)&value; - var intFlag = *(int*)&flag; - - return (intValue & intFlag) == intFlag; + if (sizeof(T) == 1) + { + var byteValue = Unsafe.As(ref value); + var byteFlag = Unsafe.As(ref flag); + return (byteValue & byteFlag) == byteFlag; + } + else if (sizeof(T) == 2) + { + var shortValue = Unsafe.As(ref value); + var shortFlag = Unsafe.As(ref flag); + return (shortValue & shortFlag) == shortFlag; + } + else if (sizeof(T) == 4) + { + var intValue = Unsafe.As(ref value); + var intFlag = Unsafe.As(ref flag); + return (intValue & intFlag) == intFlag; + } + else if (sizeof(T) == 8) + { + var longValue = Unsafe.As(ref value); + var longFlag = Unsafe.As(ref flag); + return (longValue & longFlag) == longFlag; + } + else + throw new NotSupportedException("Enum with size of " + Unsafe.SizeOf() + " are not supported"); } } } diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 0d563947e7..38c1728cd9 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); + _isSubscribed = false; + OnCompleted(); } - public void OnCompleted() => _sink.Completed(Property, this, _value); + public void OnCompleted() + { + var oldValue = _value; + _value = default; + Priority = BindingPriority.Unset; + _sink.Completed(Property, this, oldValue); + } public void OnError(Exception error) { @@ -79,13 +99,36 @@ 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. + if (!_isSubscribed && (!_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..600d725187 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Data; #nullable enable @@ -17,7 +18,7 @@ namespace Avalonia.PropertyStore public ConstantValueEntry( StyledPropertyBase property, - T value, + [AllowNull] T value, BindingPriority priority, IValueSink sink) { @@ -28,7 +29,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 +37,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..1af6f21156 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) @@ -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/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index d0d88166a7..097731bc60 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -372,8 +372,8 @@ namespace Avalonia.Utilities const string implicitName = "op_Implicit"; const string explicitName = "op_Explicit"; - bool allowImplicit = (operatorType & OperatorType.Implicit) != 0; - bool allowExplicit = (operatorType & OperatorType.Explicit) != 0; + bool allowImplicit = operatorType.HasFlagCustom(OperatorType.Implicit); + bool allowExplicit = operatorType.HasFlagCustom(OperatorType.Explicit); foreach (MethodInfo method in fromType.GetMethods()) { diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 6b89fcbdb9..e32b20cc96 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Data; using Avalonia.PropertyStore; using Avalonia.Utilities; @@ -26,6 +27,7 @@ namespace Avalonia private readonly AvaloniaObject _owner; private readonly IValueSink _sink; private readonly AvaloniaPropertyValueStore _values; + private BatchUpdate? _batchUpdate; public ValueStore(AvaloniaObject owner) { @@ -33,6 +35,25 @@ 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)) @@ -90,23 +111,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; } } @@ -128,15 +147,13 @@ 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; } } @@ -149,23 +166,32 @@ namespace Avalonia { 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 (_batchUpdate is null) { - 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); } } } @@ -176,7 +202,7 @@ namespace Avalonia { if (slot is PriorityValue p) { - p.CoerceValue(); + p.UpdateEffectiveValue(); } } } @@ -198,7 +224,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 +242,17 @@ namespace Avalonia IPriorityValueEntry entry, Optional oldValue) { - if (_values.TryGetValue(property, out var slot)) + 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 +280,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 (_batchUpdate is object) + priorityValue.BeginBatchUpdate(); result = priorityValue.SetValue(value, priority); _values.SetValue(property, priorityValue); } @@ -273,6 +310,11 @@ namespace Avalonia if (slot is IPriorityValueEntry e) { priorityValue = new PriorityValue(_owner, property, this, e); + + if (_batchUpdate is object) + { + priorityValue.BeginBatchUpdate(); + } } else if (slot is PriorityValue p) { @@ -289,8 +331,162 @@ 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 (_batchUpdate is object && 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 class BatchUpdate + { + private ValueStore _owner; + private List? _notifications; + private int _batchUpdateCount; + private int _iterator = -1; + + public BatchUpdate(ValueStore owner) => _owner = owner; + + 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. + if (slot.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 92734b128d..7c81c90d82 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs @@ -2595,7 +2595,7 @@ namespace Avalonia.Collections /// Whether the specified flag is set private bool CheckFlag(CollectionViewFlags flags) { - return (_flags & flags) != 0; + return _flags.HasFlagCustom(flags); } /// @@ -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/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 1e72a07760..90401a00a2 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -10,7 +10,8 @@ using System.Reactive.Disposables; using System.Reactive.Subjects; using Avalonia.Reactive; using System.Diagnostics; -using Avalonia.Controls.Utils; +using Avalonia.Controls.Utils; +using Avalonia.Markup.Xaml.MarkupExtensions; namespace Avalonia.Controls { @@ -47,14 +48,15 @@ namespace Avalonia.Controls if (_binding != null) { - if(_binding is Avalonia.Data.Binding binding) + if(_binding is BindingBase binding) { if (binding.Mode == BindingMode.OneWayToSource) { throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource. Use BindingMode.TwoWay instead."); } - if (!String.IsNullOrEmpty(binding.Path) && binding.Mode == BindingMode.Default) + var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString(); + if (!string.IsNullOrEmpty(path) && binding.Mode == BindingMode.Default) { binding.Mode = BindingMode.TwoWay; } @@ -136,13 +138,16 @@ namespace Avalonia.Controls internal void SetHeaderFromBinding() { if (OwningGrid != null && OwningGrid.DataConnection.DataType != null - && Header == null && Binding != null && Binding is Binding binding - && !String.IsNullOrWhiteSpace(binding.Path)) + && Header == null && Binding != null && Binding is BindingBase binding) { - string header = OwningGrid.DataConnection.DataType.GetDisplayName(binding.Path); - if (header != null) + var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString(); + if (!string.IsNullOrWhiteSpace(path)) { - Header = header; + var header = OwningGrid.DataConnection.DataType.GetDisplayName(path); + if (header != null) + { + Header = header; + } } } } 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/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 92ddd4e736..407d6ff058 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -12,6 +12,7 @@ using System; using System.Linq; using System.Diagnostics; using Avalonia.Controls.Utils; +using Avalonia.Markup.Xaml.MarkupExtensions; namespace Avalonia.Controls { @@ -1033,13 +1034,16 @@ namespace Avalonia.Controls if (String.IsNullOrEmpty(result)) { - - if(this is DataGridBoundColumn boundColumn && - boundColumn.Binding != null && - boundColumn.Binding is Binding binding && - binding.Path != null) + if (this is DataGridBoundColumn boundColumn) { - result = binding.Path; + if (boundColumn.Binding is Binding binding) + { + result = binding.Path; + } + else if (boundColumn.Binding is CompiledBindingExtension compiledBinding) + { + result = compiledBinding.Path.ToString(); + } } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs index 46bcd0d347..a4577ee952 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Utils; using Avalonia.Data; +using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Utilities; using System; using System.Collections.Generic; @@ -141,9 +142,9 @@ namespace Avalonia.Controls Debug.Assert(dataGridColumn != null); if (dataGridColumn is DataGridBoundColumn dataGridBoundColumn && - dataGridBoundColumn.Binding is Binding binding) + dataGridBoundColumn.Binding is BindingBase binding) { - string path = binding.Path; + var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString(); if (string.IsNullOrWhiteSpace(path)) { 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 new file mode 100644 index 0000000000..aa8db78087 --- /dev/null +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -0,0 +1,7 @@ +Compat issues with assembly Avalonia.Controls: +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: 5 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/Button.cs b/src/Avalonia.Controls/Button.cs index 91eef3947b..c779e4b0cb 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -80,6 +80,7 @@ namespace Avalonia.Controls private ICommand _command; private bool _commandCanExecute = true; + private KeyGesture _hotkey; /// /// Initializes static members of the class. @@ -207,6 +208,11 @@ namespace Avalonia.Controls protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { + if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control + { + HotKey = _hotkey; + } + base.OnAttachedToLogicalTree(e); if (Command != null) @@ -217,6 +223,13 @@ namespace Avalonia.Controls protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { + // This will cause the hotkey manager to dispose the observer and the reference to this control + if (HotKey != null) + { + _hotkey = HotKey; + HotKey = null; + } + base.OnDetachedFromLogicalTree(e); if (Command != null) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 7f2acb58fe..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() { @@ -188,7 +206,7 @@ namespace Avalonia.Controls return; if (e.Key == Key.F4 || - ((e.Key == Key.Down || e.Key == Key.Up) && ((e.KeyModifiers & KeyModifiers.Alt) != 0))) + ((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))) { IsDropDownOpen = !IsDropDownOpen; e.Handled = true; @@ -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/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index fb8080f0d4..57e4909e39 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -269,7 +269,43 @@ namespace Avalonia.Controls } control ??= _attachedControls![0]; + Open(control, PlacementTarget ?? control); + } + + /// + /// Closes the menu. + /// + public override void Close() + { + if (!IsOpen) + { + return; + } + if (_popup != null && _popup.IsVisible) + { + _popup.IsOpen = false; + } + } + + void ISetterValue.Initialize(ISetter setter) + { + // ContextMenu can be assigned to the ContextMenu property in a setter. This overrides + // the behavior defined in Control which requires controls to be wrapped in a