diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index a35f4f3eeb..36e16c24d1 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -280,7 +280,7 @@ AVNCOM(IAvnPlatformThreadingInterface, 0b) : IUnknown virtual bool GetCurrentThreadIsLoopThread() = 0; virtual void SetSignaledCallback(IAvnSignaledCallback* cb) = 0; virtual IAvnLoopCancellation* CreateLoopCancellation() = 0; - virtual void RunLoop(IAvnLoopCancellation* cancel) = 0; + virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) = 0; // Can't pass int* to sharpgentools for some reason virtual void Signal(int priority) = 0; virtual IUnknown* StartTimer(int priority, int ms, IAvnActionCallback* callback) = 0; diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index cc74d5669f..1870ef7ab3 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; }; @@ -22,6 +23,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; 37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = ""; }; @@ -68,6 +70,7 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + 1A002B9D232135EE00021753 /* app.mm */, 37DDA9B121933371002E132B /* AvnString.h */, 37DDA9AF219330F8002E132B /* AvnString.mm */, 37A4E71A2178846A00EACBCD /* headers */, @@ -164,6 +167,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1A002B9E232135EE00021753 /* app.mm in Sources */, 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */, 5B21A982216530F500CEE36E /* cursor.mm in Sources */, 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm new file mode 100644 index 0000000000..81855995b7 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -0,0 +1,24 @@ +#include "common.h" +@interface AvnAppDelegate : NSObject +@end +extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; +@implementation AvnAppDelegate +- (void)applicationWillFinishLaunching:(NSNotification *)notification +{ + [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy]; +} + +- (void)applicationDidFinishLaunching:(NSNotification *)notification +{ + [NSApp activateIgnoringOtherApps:true]; +} + +@end + +extern void InitializeAvnApp() +{ + NSApplication* app = [NSApplication sharedApplication]; + id delegate = [AvnAppDelegate new]; + [app setDelegate:delegate]; + +} diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index f748dd59ad..45ec40c361 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -19,12 +19,12 @@ extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlFeature* GetGlFeature(); extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view); - +extern void InitializeAvnApp(); +extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); extern AvnPoint ToAvnPoint (NSPoint p); extern AvnPoint ConvertPointY (AvnPoint p); extern NSSize ToNSSize (AvnSize s); - #ifdef DEBUG #define NSDebugLog(...) NSLog(__VA_ARGS__) #else diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 5043246c53..70bd1e67f6 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -5,21 +5,14 @@ #define COM_GUIDS_MATERIALIZE #include "common.h" -static BOOL ShowInDock = 1; - -static void SetActivationPolicy() -{ - [[NSApplication sharedApplication] setActivationPolicy: (ShowInDock ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory)]; -} - class MacOptions : public ComSingleObject { public: FORWARD_IUNKNOWN() virtual HRESULT SetShowInDock(int show) override { - ShowInDock = show; - SetActivationPolicy(); + AvnDesiredActivationPolicy = show + ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; return S_OK; } }; @@ -64,8 +57,9 @@ public: { @autoreleasepool{ [[ThreadingInitializer new] do]; - return S_OK; } + InitializeAvnApp(); + return S_OK; }; virtual IAvnMacOptions* GetMacOptions() override diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index 0c814b2088..297097584a 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -57,16 +57,36 @@ class PlatformThreadingInterface : public ComSingleObject { public: FORWARD_IUNKNOWN() - bool Cancelled = 0; - virtual void Cancel() override + bool Running = false; + bool Cancelled = false; + virtual void Cancel() { - Cancelled = 1; + Cancelled = true; + if(Running) + { + Running = false; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSApplication sharedApplication] stop:nil]; + NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:NSMakePoint(0, 0) + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:0 + data1:0 + data2:0]; + [NSApp postEvent:event atStart:YES]; + }); + } } + }; public: @@ -99,30 +119,17 @@ public: return new LoopCancellation(); } - virtual void RunLoop(IAvnLoopCancellation* cancel) override + virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) override { - @autoreleasepool { - auto can = dynamic_cast(cancel); - [[NSApplication sharedApplication] activateIgnoringOtherApps:true]; - while(true) - { - @autoreleasepool - { - if(can != NULL && can->Cancelled) - return; - NSEvent* ev = [[NSApplication sharedApplication] - nextEventMatchingMask:NSEventMaskAny - untilDate: [NSDate dateWithTimeIntervalSinceNow:1] - inMode:NSDefaultRunLoopMode - dequeue:true]; - if(can != NULL && can->Cancelled) - return; - if(ev != NULL) - [[NSApplication sharedApplication] sendEvent:ev]; - } - } - NSDebugLog(@"RunLoop exited"); - } + auto can = dynamic_cast(cancel); + if(can->Cancelled) + return S_OK; + if(_wasRunningAtLeastOnce) + return E_FAIL; + can->Running = true; + _wasRunningAtLeastOnce = true; + [NSApp run]; + return S_OK; } virtual void Signal(int priority) override diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 0e2f0feada..94558c4367 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -208,20 +208,9 @@ namespace Avalonia { return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this); } - else if (_values != null) - { - var result = Values.GetValue(property); - - if (result == AvaloniaProperty.UnsetValue) - { - result = GetDefaultValue(property); - } - - return result; - } else { - return GetDefaultValue(property); + return GetValueOrDefaultUnchecked(property); } } @@ -598,10 +587,46 @@ namespace Avalonia private object GetDefaultValue(AvaloniaProperty property) { if (property.Inherits && InheritanceParent is AvaloniaObject aobj) - return aobj.GetValue(property); + return aobj.GetValueOrDefaultUnchecked(property); return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); } + /// + /// Gets the value or default value for a property. + /// + /// The property. + /// The default value. + private object GetValueOrDefaultUnchecked(AvaloniaProperty property) + { + var aobj = this; + var valuestore = aobj._values; + if (valuestore != null) + { + var result = valuestore.GetValue(property); + if (result != AvaloniaProperty.UnsetValue) + { + return result; + } + } + if (property.Inherits) + { + while (aobj.InheritanceParent is AvaloniaObject parent) + { + aobj = parent; + valuestore = aobj._values; + if (valuestore != null) + { + var result = valuestore.GetValue(property); + if (result != AvaloniaProperty.UnsetValue) + { + return result; + } + } + } + } + return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); + } + /// /// Sets the value of a direct property. /// diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index eaa267ba66..7b6870fec3 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -217,12 +217,12 @@ - + - + diff --git a/src/Avalonia.Controls/ControlExtensions.cs b/src/Avalonia.Controls/ControlExtensions.cs index 2fccb15acc..3bd222d132 100644 --- a/src/Avalonia.Controls/ControlExtensions.cs +++ b/src/Avalonia.Controls/ControlExtensions.cs @@ -34,14 +34,17 @@ namespace Avalonia.Controls { Contract.Requires(control != null); - var ev = new RequestBringIntoViewEventArgs + if (control.IsEffectivelyVisible) { - RoutedEvent = Control.RequestBringIntoViewEvent, - TargetObject = control, - TargetRect = rect, - }; + var ev = new RequestBringIntoViewEventArgs + { + RoutedEvent = Control.RequestBringIntoViewEvent, + TargetObject = control, + TargetRect = rect, + }; - control.RaiseEvent(ev); + control.RaiseEvent(ev); + } } /// diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index e7d8018a42..ec6a228421 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -141,7 +141,7 @@ namespace Avalonia.Controls.Presenters /// True if the scroll offset was changed; otherwise false. public bool BringDescendantIntoView(IVisual target, Rect targetRect) { - if (Child == null) + if (Child?.IsEffectivelyVisible != true) { return false; } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 87100ceeb0..e0acab1133 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -269,8 +269,8 @@ namespace Avalonia.Controls /// protected virtual void HandleClosed() { + (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; - Closed?.Invoke(this, EventArgs.Empty); Renderer?.Dispose(); Renderer = null; diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 4d229390cb..e185bc227b 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -42,6 +42,7 @@ namespace Avalonia.Controls new FuncTemplate(() => new StackPanel()); private TreeView _treeView; + private IControl _header; private bool _isExpanded; private int _level; @@ -53,6 +54,7 @@ namespace Avalonia.Controls SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(true); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + RequestBringIntoViewEvent.AddClassHandler(x => x.OnRequestBringIntoView); } /// @@ -120,6 +122,21 @@ namespace Avalonia.Controls ItemContainerGenerator.Clear(); } + protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e) + { + if (e.TargetObject == this && _header != null) + { + var m = _header.TransformToVisual(this); + + if (m.HasValue) + { + var bounds = new Rect(_header.Bounds.Size); + var rect = bounds.TransformToAABB(m.Value); + e.TargetRect = rect; + } + } + } + /// protected override void OnKeyDown(KeyEventArgs e) { @@ -146,6 +163,12 @@ namespace Avalonia.Controls // Don't call base.OnKeyDown - let events bubble up to containing TreeView. } + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + _header = e.NameScope.Find("PART_Header"); + } + private static int CalculateDistanceFromLogicalParent(ILogical logical, int @default = -1) where T : class { var result = 0; diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs index 7e6bf657ae..a641544f7a 100644 --- a/src/Avalonia.Input/IMouseDevice.cs +++ b/src/Avalonia.Input/IMouseDevice.cs @@ -16,6 +16,8 @@ namespace Avalonia.Input [Obsolete("Use PointerEventArgs.GetPosition")] PixelPoint Position { get; } + void TopLevelClosed(IInputRoot root); + void SceneInvalidated(IInputRoot root, Rect rect); } } diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index c1d0729560..f83c41e266 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -13,6 +13,8 @@ namespace Avalonia.Input /// public static class InputExtensions { + private static readonly Func s_hitTestDelegate = IsHitTestVisible; + /// /// Returns the active input elements at a point on an . /// @@ -25,7 +27,7 @@ namespace Avalonia.Input { Contract.Requires(element != null); - return element.GetVisualsAt(p, IsHitTestVisible).Cast(); + return element.GetVisualsAt(p, s_hitTestDelegate).Cast(); } /// diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index d5152f58d5..0d5471f790 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -86,6 +86,11 @@ namespace Avalonia.Input ProcessRawEvent(margs); } + public void TopLevelClosed(IInputRoot root) + { + ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); + } + public void SceneInvalidated(IInputRoot root, Rect rect) { var clientPoint = root.PointToClient(Position); diff --git a/src/Avalonia.Themes.Default/TreeViewItem.xaml b/src/Avalonia.Themes.Default/TreeViewItem.xaml index 5dd082cf7a..0d826806d0 100644 --- a/src/Avalonia.Themes.Default/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Default/TreeViewItem.xaml @@ -16,7 +16,9 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" TemplatedControl.IsTemplateFocusTarget="True"> - x.Initiate(target, null)); - var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType, converter)); + + var input = children.Select(x => x.Observable) + .CombineLatest() + .Select(x => ConvertValue(x, targetType, converter)) + .Where(x => x != BindingOperations.DoNothing); + var mode = Mode == BindingMode.Default ? targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; @@ -97,11 +102,6 @@ namespace Avalonia.Data var culture = CultureInfo.CurrentCulture; var converted = converter.Convert(values, targetType, ConverterParameter, culture); - if (converted == BindingOperations.DoNothing) - { - return converted; - } - if (converted == AvaloniaProperty.UnsetValue) { converted = FallbackValue; diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index c744543f99..901d780f16 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -224,6 +224,24 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Close_Should_Notify_MouseDevice() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + var mouseDevice = new Mock(); + impl.SetupAllProperties(); + impl.Setup(x => x.MouseDevice).Returns(mouseDevice.Object); + + var target = new TestTopLevel(impl.Object); + + impl.Object.Closed(); + + mouseDevice.Verify(x => x.TopLevelClosed(target)); + } + } + private FuncControlTemplate CreateTemplate() { return new FuncControlTemplate((x, scope) => diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs new file mode 100644 index 0000000000..a77723afe1 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Converters +{ + public class MultiValueConverterTests : XamlTestBase + { + [Fact] + public void MultiValueConverter_Special_Values_Work() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.ApplyTemplate(); + + window.DataContext = Tuple.Create(2, 2); + Assert.Equal("foo", textBlock.Text); + + window.DataContext = Tuple.Create(-3, 3); + Assert.Equal("foo", textBlock.Text); + + window.DataContext = Tuple.Create(0, 2); + Assert.Equal("bar", textBlock.Text); + } + } + } + + public class TestMultiValueConverter : IMultiValueConverter + { + public static readonly TestMultiValueConverter Instance = new TestMultiValueConverter(); + + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (values[0] is int i && values[1] is int j) + { + var p = i * j; + + if (p > 0) + { + return "foo"; + } + + if (p == 0) + { + return AvaloniaProperty.UnsetValue; + } + + return BindingOperations.DoNothing; + } + + return "(default)"; + } + } +}