diff --git a/Avalonia.sln b/Avalonia.sln index 38f8e5f720..e8d5034fb0 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -206,6 +206,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -1921,6 +1923,30 @@ Global {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.Build.0 = Release|Any CPU {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhone.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhone.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|Any CPU.Build.0 = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhone.ActiveCfg = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhone.Build.0 = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build.sh b/build.sh index 40b1c225a6..a40e00f815 100755 --- a/build.sh +++ b/build.sh @@ -67,6 +67,8 @@ else fi fi +export PATH=$DOTNET_DIRECTORY:$PATH + echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index d61268173d..2b54ee3f56 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -11,6 +11,7 @@ + diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 7782165263..f9bfaf0b47 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -258,6 +258,7 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0; virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0; + virtual HRESULT SetBlurEnabled (bool enable) = 0; }; AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase @@ -386,6 +387,9 @@ AVNCOM(IAvnClipboard, 0f) : IUnknown virtual HRESULT SetText (char* type, void* utf8Text) = 0; virtual HRESULT ObtainFormats(IAvnStringArray**ppv) = 0; virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) = 0; + virtual HRESULT SetBytes(char* type, void* utf8Text, int len) = 0; + virtual HRESULT GetBytes(char* type, IAvnString**ppv) = 0; + virtual HRESULT Clear() = 0; }; diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h index 88bc4e6963..5d299374e5 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.h +++ b/native/Avalonia.Native/src/OSX/AvnString.h @@ -12,4 +12,5 @@ extern IAvnString* CreateAvnString(NSString* string); 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 6445a9fef1..00b748ef63 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -29,6 +29,13 @@ public: memcpy((void*)_cstring, (void*)cstring, _length); } + AvnStringImpl(void*ptr, int len) + { + _length = len; + _cstring = (const char*)malloc(_length); + memcpy((void*)_cstring, ptr, len); + } + virtual ~AvnStringImpl() { free((void*)_cstring); @@ -114,3 +121,8 @@ IAvnStringArray* CreateAvnStringArray(NSString* string) { return new AvnStringArrayImpl(string); } + +IAvnString* CreateByteArray(void* data, int len) +{ + return new AvnStringImpl(data, len); +} diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 18d60d3853..116a08670e 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -82,6 +82,40 @@ public: return S_OK; } + + virtual HRESULT SetBytes(char* type, void* bytes, int len) override + { + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + auto data = [NSData dataWithBytes:bytes length:len]; + if(_item == nil) + [_pb setData:data forType:typeString]; + else + [_item setData:data forType:typeString]; + return S_OK; + } + + virtual HRESULT GetBytes(char* type, IAvnString**ppv) override + { + *ppv = nil; + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + NSData*data; + @try + { + if(_item) + data = [_item dataForType:typeString]; + else + data = [_pb dataForType:typeString]; + if(data == nil) + return E_FAIL; + } + @catch(NSException* e) + { + return E_FAIL; + } + *ppv = CreateByteArray((void*)data.bytes, (int)data.length); + return S_OK; + } + virtual HRESULT Clear() override { diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index ca60914526..bdf3007a28 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -3,6 +3,9 @@ class WindowBaseImpl; +@interface AutoFitContentVisualEffectView : NSVisualEffectView +@end + @interface AvnView : NSView -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(NSEvent* _Nonnull) lastMouseDownEvent; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index fed2176580..abfae3cf1e 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -20,6 +20,7 @@ public: View = NULL; Window = NULL; } + NSVisualEffectView* VisualEffect; AvnView* View; AvnWindow* Window; ComPtr BaseEvents; @@ -47,6 +48,12 @@ public: [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; + + VisualEffect = [AutoFitContentVisualEffectView new]; + [VisualEffect setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + [VisualEffect setMaterial:NSVisualEffectMaterialLight]; + [VisualEffect setAutoresizesSubviews:true]; + [Window setContentView: View]; } @@ -383,6 +390,18 @@ public: return *ppv == nil ? E_FAIL : S_OK; } + virtual HRESULT SetBlurEnabled (bool enable) override + { + [Window setContentView: enable ? VisualEffect : View]; + + if(enable) + { + [VisualEffect addSubview:View]; + } + + return S_OK; + } + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) override @@ -911,6 +930,16 @@ protected: NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; +@implementation AutoFitContentVisualEffectView +-(void)setFrameSize:(NSSize)newSize +{ + [super setFrameSize:newSize]; + if([[self subviews] count] == 0) + return; + [[self subviews][0] setFrameSize: newSize]; +} +@end + @implementation AvnView { ComPtr _parent; diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index e40509dfda..c7a75f5a70 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -1,9 +1,7 @@ - - - + - + + + + diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs index 1d0c228a0e..5e555c8c91 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs @@ -5,5 +5,25 @@ namespace ControlCatalog.Pages { public class ButtonPage : UserControl { + private int repeatButtonClickCount = 0; + + public ButtonPage() + { + InitializeComponent(); + + this.FindControl("RepeatButton").Click += OnRepeatButtonClick; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public void OnRepeatButtonClick(object sender, object args) + { + repeatButtonClickCount++; + var textBlock = this.FindControl("RepeatButtonTextBlock"); + textBlock.Text = $"Repeat Button: {repeatButtonClickCount}"; + } } } diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml index bbfbd4b4cd..486cc55d44 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml @@ -6,7 +6,7 @@ A drop-down list. - + Inline Items Inline Item 2 Inline Item 3 diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index 58f7b881fe..c6f5521e60 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -9,12 +9,14 @@ diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index 6019d5f91f..789b45e62c 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -20,6 +20,7 @@ + Single diff --git a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs index d396ef2b3d..5bc23e2fe5 100644 --- a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs @@ -23,12 +23,14 @@ namespace ControlCatalog.ViewModels AddItemCommand = ReactiveCommand.Create(AddItem); RemoveItemCommand = ReactiveCommand.Create(RemoveItem); + SelectRandomItemCommand = ReactiveCommand.Create(SelectRandomItem); } public ObservableCollection Items { get; } public SelectionModel Selection { get; } public ReactiveCommand AddItemCommand { get; } public ReactiveCommand RemoveItemCommand { get; } + public ReactiveCommand SelectRandomItemCommand { get; } public SelectionMode SelectionMode { @@ -74,6 +76,15 @@ namespace ControlCatalog.ViewModels } } + private void SelectRandomItem() + { + var random = new Random(); + var depth = random.Next(4); + var indexes = Enumerable.Range(0, 4).Select(x => random.Next(10)); + var path = new IndexPath(indexes); + Selection.SelectedIndex = path; + } + private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) { var selected = string.Join(",", e.SelectedIndices); diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index b9075b957b..8fc91aca14 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -1,6 +1,7 @@ false + $(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\netcoreapp2.0\Avalonia.Designer.HostApp.dll diff --git a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs index 51e0a1e799..7802f336fb 100644 --- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs +++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs @@ -43,5 +43,11 @@ namespace Avalonia.Android.Platform return Task.FromResult(null); } + + public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException(); + + public Task GetFormatsAsync() => throw new PlatformNotSupportedException(); + + public Task GetDataAsync(string format) => throw new PlatformNotSupportedException(); } } diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 9e9b84537b..067d9f462f 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -93,17 +93,17 @@ namespace Avalonia.Animation var oldTransitions = change.OldValue.GetValueOrDefault(); var newTransitions = change.NewValue.GetValueOrDefault(); - if (oldTransitions is object) - { - oldTransitions.CollectionChanged -= TransitionsCollectionChanged; - RemoveTransitions(oldTransitions); - } - if (newTransitions is object) { newTransitions.CollectionChanged += TransitionsCollectionChanged; AddTransitions(newTransitions); } + + if (oldTransitions is object) + { + oldTransitions.CollectionChanged -= TransitionsCollectionChanged; + RemoveTransitions(oldTransitions); + } } else if (_transitionsEnabled && Transitions is object && @@ -111,8 +111,10 @@ namespace Avalonia.Animation !change.Property.IsDirect && change.Priority > BindingPriority.Animation) { - foreach (var transition in Transitions) + for (var i = Transitions.Count -1; i >= 0; --i) { + var transition = Transitions[i]; + if (transition.Property == change.Property) { var state = _transitionState[transition]; diff --git a/src/Avalonia.Base/Data/Converters/BoolConverters.cs b/src/Avalonia.Base/Data/Converters/BoolConverters.cs index 817d1cea9a..6740c2168f 100644 --- a/src/Avalonia.Base/Data/Converters/BoolConverters.cs +++ b/src/Avalonia.Base/Data/Converters/BoolConverters.cs @@ -3,7 +3,7 @@ using System.Linq; namespace Avalonia.Data.Converters { /// - /// Provides a set of useful s for working with string values. + /// Provides a set of useful s for working with bool values. /// public static class BoolConverters { diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index f2ed86d2aa..84ef0fb695 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -1,7 +1,5 @@ using System; using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; using System.Reflection; using Avalonia.Utilities; @@ -31,7 +29,11 @@ namespace Avalonia.Data.Core.Plugins Contract.Requires(propertyName != null); reference.TryGetTarget(out object instance); - var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(x => x.Name == propertyName); + + const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | + BindingFlags.Static | BindingFlags.Instance; + + var p = instance.GetType().GetProperty(propertyName, bindingFlags); if (p != null) { diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index c19ee8dba7..5d694f4cf9 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -1,13 +1,16 @@ using System; -using System.Linq; +using System.Collections.Generic; +using System.Linq.Expressions; using System.Reflection; namespace Avalonia.Data.Core.Plugins { - class MethodAccessorPlugin : IPropertyAccessorPlugin + public class MethodAccessorPlugin : IPropertyAccessorPlugin { - public bool Match(object obj, string methodName) - => obj.GetType().GetRuntimeMethods().Any(x => x.Name == methodName); + private readonly Dictionary<(Type, string), MethodInfo> _methodLookup = + new Dictionary<(Type, string), MethodInfo>(); + + public bool Match(object obj, string methodName) => GetFirstMethodWithName(obj.GetType(), methodName) != null; public IPropertyAccessor Start(WeakReference reference, string methodName) { @@ -15,17 +18,22 @@ namespace Avalonia.Data.Core.Plugins Contract.Requires(methodName != null); reference.TryGetTarget(out object instance); - var method = instance.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name == methodName); + + var method = GetFirstMethodWithName(instance.GetType(), methodName); if (method != null) { - if (method.GetParameters().Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8) + var parameters = method.GetParameters(); + + if (parameters.Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8) { - var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(methodName)); + var exception = new ArgumentException( + "Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", + nameof(methodName)); return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); } - return new Accessor(reference, method); + return new Accessor(reference, method, parameters); } else { @@ -35,31 +43,72 @@ namespace Avalonia.Data.Core.Plugins } } + private MethodInfo GetFirstMethodWithName(Type type, string methodName) + { + var key = (type, methodName); + + if (!_methodLookup.TryGetValue(key, out MethodInfo methodInfo)) + { + methodInfo = TryFindAndCacheMethod(type, methodName); + } + + return methodInfo; + } + + private MethodInfo TryFindAndCacheMethod(Type type, string methodName) + { + MethodInfo found = null; + + const BindingFlags bindingFlags = + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; + + var methods = type.GetMethods(bindingFlags); + + foreach (MethodInfo methodInfo in methods) + { + if (methodInfo.Name == methodName) + { + found = methodInfo; + + break; + } + } + + _methodLookup.Add((type, methodName), found); + + return found; + } + private sealed class Accessor : PropertyAccessorBase { - public Accessor(WeakReference reference, MethodInfo method) + public Accessor(WeakReference reference, MethodInfo method, ParameterInfo[] parameters) { Contract.Requires(reference != null); Contract.Requires(method != null); - var paramTypes = method.GetParameters().Select(param => param.ParameterType).ToArray(); var returnType = method.ReturnType; - - if (returnType == typeof(void)) + bool hasReturn = returnType != typeof(void); + + var signatureTypeCount = (hasReturn ? 1 : 0) + parameters.Length; + + var paramTypes = new Type[signatureTypeCount]; + + for (var i = 0; i < parameters.Length; i++) { - if (paramTypes.Length == 0) - { - PropertyType = typeof(Action); - } - else - { - PropertyType = Type.GetType($"System.Action`{paramTypes.Length}").MakeGenericType(paramTypes); - } + ParameterInfo parameter = parameters[i]; + + paramTypes[i] = parameter.ParameterType; + } + + if (hasReturn) + { + paramTypes[paramTypes.Length - 1] = returnType; + + PropertyType = Expression.GetFuncType(paramTypes); } else { - var genericTypeParameters = paramTypes.Concat(new[] { returnType }).ToArray(); - PropertyType = Type.GetType($"System.Func`{genericTypeParameters.Length}").MakeGenericType(genericTypeParameters); + PropertyType = Expression.GetActionType(paramTypes); } if (method.IsStatic) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index e71bcc24a7..6557346b1f 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -51,6 +51,18 @@ namespace Avalonia.Controls public static readonly StyledProperty VirtualizationModeProperty = ItemsPresenter.VirtualizationModeProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty PlaceholderTextProperty = + AvaloniaProperty.Register(nameof(PlaceholderText)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PlaceholderForegroundProperty = + AvaloniaProperty.Register(nameof(PlaceholderForeground)); + private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; @@ -94,6 +106,24 @@ namespace Avalonia.Controls set { SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); } } + /// + /// Gets or sets the PlaceHolder text. + /// + public string PlaceholderText + { + get { return GetValue(PlaceholderTextProperty); } + set { SetValue(PlaceholderTextProperty, value); } + } + + /// + /// Gets or sets the Brush that renders the placeholder text. + /// + public IBrush PlaceholderForeground + { + get { return GetValue(PlaceholderForegroundProperty); } + set { SetValue(PlaceholderForegroundProperty, value); } + } + /// /// Gets or sets the virtualization mode for the items. /// diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 29f0374301..09f9ed7c1b 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -49,6 +49,9 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action Paint { get; set; } public Action Resized { get; set; } public Action ScalingChanged { get; set; } + + public Action TransparencyLevelChanged { get; set; } + public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot; public virtual Point PointToClient(PixelPoint point) => point.ToPoint(1); @@ -61,6 +64,11 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action Closed { get; set; } public abstract IMouseDevice MouseDevice { get; } + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } + + public WindowTransparencyLevel TransparencyLevel { get; private set; } + public IPopupImpl CreatePopup() => null; } } diff --git a/src/Avalonia.Controls/IndexPath.cs b/src/Avalonia.Controls/IndexPath.cs index 6c5aaf7ad1..73b75bc23d 100644 --- a/src/Avalonia.Controls/IndexPath.cs +++ b/src/Avalonia.Controls/IndexPath.cs @@ -123,6 +123,26 @@ namespace Avalonia.Controls } } + public bool IsAncestorOf(in IndexPath other) + { + if (other.GetSize() <= GetSize()) + { + return false; + } + + var size = GetSize(); + + for (int i = 0; i < size; i++) + { + if (GetAt(i) != other.GetAt(i)) + { + return false; + } + } + + return true; + } + public override string ToString() { if (_path != null) diff --git a/src/Avalonia.Controls/IndexRange.cs b/src/Avalonia.Controls/IndexRange.cs index 1dc161c699..e45d013af4 100644 --- a/src/Avalonia.Controls/IndexRange.cs +++ b/src/Avalonia.Controls/IndexRange.cs @@ -132,6 +132,53 @@ namespace Avalonia.Controls return result; } + public static int Intersect( + IList ranges, + IndexRange range, + IList? removed = null) + { + var result = 0; + + for (var i = 0; i < ranges.Count && range != s_invalid; ++i) + { + var existing = ranges[i]; + + if (existing.End < range.Begin || existing.Begin > range.End) + { + removed?.Add(existing); + ranges.RemoveAt(i--); + result += existing.Count; + } + else + { + if (existing.Begin < range.Begin) + { + var except = new IndexRange(existing.Begin, range.Begin - 1); + removed?.Add(except); + ranges[i] = existing = new IndexRange(range.Begin, existing.End); + result += except.Count; + } + + if (existing.End > range.End) + { + var except = new IndexRange(range.End + 1, existing.End); + removed?.Add(except); + ranges[i] = new IndexRange(existing.Begin, range.End); + result += except.Count; + } + } + } + + MergeRanges(ranges); + + if (removed is object) + { + MergeRanges(removed); + } + + return result; + } + public static int Remove( IList ranges, IndexRange range, diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index 199c9e0ff8..e04c79987f 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -1,4 +1,5 @@ using Avalonia.Controls.Mixins; +using Avalonia.Input; namespace Avalonia.Controls { @@ -19,6 +20,7 @@ namespace Avalonia.Controls static ListBoxItem() { SelectableMixin.Attach(IsSelectedProperty); + PressedMixin.Attach(); FocusableProperty.OverrideDefaultValue(true); } diff --git a/src/Avalonia.Controls/Mixins/PressedMixin.cs b/src/Avalonia.Controls/Mixins/PressedMixin.cs new file mode 100644 index 0000000000..4c5792b4a0 --- /dev/null +++ b/src/Avalonia.Controls/Mixins/PressedMixin.cs @@ -0,0 +1,37 @@ +using Avalonia.Interactivity; +using Avalonia.Input; + +namespace Avalonia.Controls.Mixins +{ + /// + /// Adds pressed functionality to control classes. + /// + /// Adds the ':pressed' class when the item is pressed. + /// + public static class PressedMixin + { + /// + /// Initializes a new instance of the class. + /// + /// The control type. + public static void Attach() where TControl : Control + { + InputElement.PointerPressedEvent.AddClassHandler((x, e) => HandlePointerPressed(x, e), RoutingStrategies.Tunnel); + InputElement.PointerReleasedEvent.AddClassHandler((x, e) => HandlePointerReleased(x), RoutingStrategies.Tunnel); + InputElement.PointerCaptureLostEvent.AddClassHandler((x, e) => HandlePointerReleased(x), RoutingStrategies.Tunnel); + } + + private static void HandlePointerPressed(TControl sender, PointerPressedEventArgs e) where TControl : Control + { + if (e.GetCurrentPoint(sender).Properties.IsLeftButtonPressed) + { + ((IPseudoClasses)sender.Classes).Set(":pressed", true); + } + } + + private static void HandlePointerReleased(TControl sender) where TControl : Control + { + ((IPseudoClasses)sender.Classes).Set(":pressed", false); + } + } +} diff --git a/src/Avalonia.Controls/Platform/IPopupImpl.cs b/src/Avalonia.Controls/Platform/IPopupImpl.cs index 3944695f04..477d5fab43 100644 --- a/src/Avalonia.Controls/Platform/IPopupImpl.cs +++ b/src/Avalonia.Controls/Platform/IPopupImpl.cs @@ -8,5 +8,7 @@ namespace Avalonia.Platform public interface IPopupImpl : IWindowBaseImpl { IPopupPositioner PopupPositioner { get; } + + void SetWindowManagerAddShadowHint(bool enabled); } } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 98ee17ee1f..c7875f6413 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Rendering; @@ -58,6 +59,11 @@ namespace Avalonia.Platform /// Action ScalingChanged { get; set; } + /// + /// Gets or sets a method called when the toplevel's TransparencyLevel changes. + /// + Action TransparencyLevelChanged { get; set; } + /// /// Creates a new renderer for the toplevel. /// @@ -106,5 +112,15 @@ namespace Avalonia.Platform IMouseDevice MouseDevice { get; } IPopupImpl CreatePopup(); + + /// + /// Sets the hint of the TopLevel. + /// + void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel); + + /// + /// Gets the current of the TopLevel. + /// + WindowTransparencyLevel TransparencyLevel { get; } } } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 50aa8a9e71..f75184c686 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -365,12 +365,8 @@ namespace Avalonia.Controls.Presenters if (useLayoutRounding) { - sizeForChild = new Size( - Math.Ceiling(sizeForChild.Width * scale) / scale, - Math.Ceiling(sizeForChild.Height * scale) / scale); - availableSize = new Size( - Math.Ceiling(availableSize.Width * scale) / scale, - Math.Ceiling(availableSize.Height * scale) / scale); + sizeForChild = LayoutHelper.RoundLayoutSize(sizeForChild, scale, scale); + availableSize = LayoutHelper.RoundLayoutSize(availableSize, scale, scale); } switch (horizontalContentAlignment) @@ -395,8 +391,8 @@ namespace Avalonia.Controls.Presenters if (useLayoutRounding) { - originX = Math.Floor(originX * scale) / scale; - originY = Math.Floor(originY * scale) / scale; + originX = LayoutHelper.RoundLayoutValue(originX, scale); + originY = LayoutHelper.RoundLayoutValue(originY, scale); } var boundsForChild = diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 87ab6cf450..9834bf3d3b 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -3,6 +3,7 @@ using System.Collections.Specialized; using System.Linq; using Avalonia.Media; using Avalonia.Rendering; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -78,15 +79,20 @@ namespace Avalonia.Controls.Primitives private void UpdateClip(IControl control, TransformedBounds bounds) { - var clip = control.Clip as RectangleGeometry; - - if (clip == null) + if (!(control.Clip is RectangleGeometry clip)) { - clip = new RectangleGeometry { Transform = new MatrixTransform() }; + clip = new RectangleGeometry(); control.Clip = clip; } - clip.Rect = bounds.Bounds; + var clipBounds = bounds.Bounds; + + if (bounds.Transform.HasInverse) + { + clipBounds = bounds.Clip.TransformToAABB(bounds.Transform.Invert()); + } + + clip.Rect = clipBounds; } private void ChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index ff1201286c..4b35ceabf6 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -20,6 +20,9 @@ namespace Avalonia.Controls.Primitives /// public class Popup : Control, IVisualTreeHost { + public static readonly StyledProperty WindowManagerAddShadowHintProperty = + AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), true); + /// /// Defines the property. /// @@ -108,7 +111,7 @@ namespace Avalonia.Controls.Primitives { IsHitTestVisibleProperty.OverrideDefaultValue(false); ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); - IsOpenProperty.Changed.AddClassHandler((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs)e)); + IsOpenProperty.Changed.AddClassHandler((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs)e)); } /// @@ -123,6 +126,12 @@ namespace Avalonia.Controls.Primitives public IPopupHost? Host => _openState?.PopupHost; + public bool WindowManagerAddShadowHint + { + get { return GetValue(WindowManagerAddShadowHintProperty); } + set { SetValue(WindowManagerAddShadowHintProperty, value); } + } + /// /// Gets or sets the control to display in the popup. /// @@ -352,6 +361,8 @@ namespace Avalonia.Controls.Primitives _openState = new PopupOpenState(topLevel, popupHost, cleanupPopup); + WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint); + popupHost.Show(); using (BeginIgnoringIsOpen()) @@ -391,6 +402,14 @@ namespace Avalonia.Controls.Primitives return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler)); } + private void WindowManagerAddShadowHintChanged(IPopupHost host, bool hint) + { + if(host is PopupRoot pr) + { + pr.PlatformImpl.SetWindowManagerAddShadowHint(hint); + } + } + /// /// Called when the property changes. /// diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 3b167d52e1..eb9b847561 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -17,14 +17,14 @@ namespace Avalonia.Controls.Primitives public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost { private readonly TopLevel _parent; - private PopupPositionerParameters _positionerParameters; + private PopupPositionerParameters _positionerParameters; /// /// Initializes static members of the class. /// static PopupRoot() { - BackgroundProperty.OverrideDefaultValue(typeof(PopupRoot), Brushes.White); + BackgroundProperty.OverrideDefaultValue(typeof(PopupRoot), Brushes.White); } /// @@ -53,7 +53,7 @@ namespace Avalonia.Controls.Primitives /// Gets the platform-specific window implementation. /// [CanBeNull] - public new IPopupImpl PlatformImpl => (IPopupImpl)base.PlatformImpl; + public new IPopupImpl PlatformImpl => (IPopupImpl)base.PlatformImpl; /// /// Gets the parent control in the event route. diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index e104a8a664..1db47a13e7 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -41,13 +41,16 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty IsDirectionReversedProperty = AvaloniaProperty.Register(nameof(IsDirectionReversed)); + public static readonly StyledProperty IgnoreThumbDragProperty = + AvaloniaProperty.Register(nameof(IsThumbDragHandled)); + private double _minimum; private double _maximum = 100.0; private double _value; static Track() { - ThumbProperty.Changed.AddClassHandler((x,e) => x.ThumbChanged(e)); + ThumbProperty.Changed.AddClassHandler((x, e) => x.ThumbChanged(e)); IncreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); DecreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); AffectsArrange(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); @@ -113,6 +116,12 @@ namespace Avalonia.Controls.Primitives set { SetValue(IsDirectionReversedProperty, value); } } + public bool IsThumbDragHandled + { + get { return GetValue(IgnoreThumbDragProperty); } + set { SetValue(IgnoreThumbDragProperty, value); } + } + private double ThumbCenterOffset { get; set; } private double Density { get; set; } @@ -422,6 +431,9 @@ namespace Avalonia.Controls.Primitives private void ThumbDragged(object sender, VectorEventArgs e) { + if (IsThumbDragHandled) + return; + Value = MathUtilities.Clamp( Value + ValueFromDistance(e.Vector.X, e.Vector.Y), Minimum, diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 069da6e9ac..2c870b4efd 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -219,12 +219,21 @@ namespace Avalonia.Controls /// public IControl TryGetElement(int index) => GetElementFromIndexImpl(index); + /// + /// Retrieves the UIElement that corresponds to the item at the specified index in the + /// data source. + /// + /// The index of the item. + /// + /// An that corresponds to the item at the specified index. If the + /// item is not realized, a new UIElement is created. + /// + public IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); + internal void PinElement(IControl element) => _viewManager.UpdatePin(element, true); internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); - internal IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); - internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element) { var value = element.GetValue(VirtualizationInfoProperty); diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index 93699583e6..ff1c0260bb 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -46,17 +46,25 @@ namespace Avalonia.Controls if (_rootNode.Source != null) { - if (_rootNode.Source != null) + // Temporarily prevent auto-select when switching source. + var restoreAutoSelect = _autoSelect; + _autoSelect = false; + + try { using (var operation = new Operation(this)) { ClearSelection(resetAnchor: true); } } + finally + { + _autoSelect = restoreAutoSelect; + } } _rootNode.Source = value; - ApplyAutoSelect(); + ApplyAutoSelect(true); RaisePropertyChanged("Source"); @@ -114,7 +122,7 @@ namespace Avalonia.Controls if (_autoSelect != value) { _autoSelect = value; - ApplyAutoSelect(); + ApplyAutoSelect(true); } } } @@ -133,7 +141,7 @@ namespace Avalonia.Controls while (current?.AnchorIndex >= 0) { path.Add(current.AnchorIndex); - current = current.GetAt(current.AnchorIndex, false); + current = current.GetAt(current.AnchorIndex, false, default); } anchor = new IndexPath(path); @@ -188,7 +196,6 @@ namespace Avalonia.Controls using var operation = new Operation(this); ClearSelection(resetAnchor: true); SelectWithPathImpl(value, select: true); - ApplyAutoSelect(); } } } @@ -384,21 +391,18 @@ namespace Avalonia.Controls { using var operation = new Operation(this); SelectImpl(index, select: false); - ApplyAutoSelect(); } public void Deselect(int groupIndex, int itemIndex) { using var operation = new Operation(this); SelectWithGroupImpl(groupIndex, itemIndex, select: false); - ApplyAutoSelect(); } public void DeselectAt(IndexPath index) { using var operation = new Operation(this); SelectWithPathImpl(index, select: false); - ApplyAutoSelect(); } public bool IsSelected(int index) => _rootNode.IsSelected(index); @@ -416,7 +420,7 @@ namespace Avalonia.Controls for (int i = 0; i < path.GetSize() - 1; i++) { var childIndex = path.GetAt(i); - node = node.GetAt(childIndex, realizeChild: false); + node = node.GetAt(childIndex, false, default); if (node == null) { @@ -451,7 +455,7 @@ namespace Avalonia.Controls } var isSelected = (bool?)false; - var childNode = _rootNode.GetAt(groupIndex, realizeChild: false); + var childNode = _rootNode.GetAt(groupIndex, false, default); if (childNode != null) { @@ -470,7 +474,7 @@ namespace Avalonia.Controls for (int i = 0; i < path.GetSize() - 1; i++) { var childIndex = path.GetAt(i); - node = node.GetAt(childIndex, realizeChild: false); + node = node.GetAt(childIndex, false, default); if (node == null) { @@ -565,7 +569,6 @@ namespace Avalonia.Controls { using var operation = new Operation(this); ClearSelection(resetAnchor: true); - ApplyAutoSelect(); } public IDisposable Update() => new Operation(this); @@ -592,10 +595,13 @@ namespace Avalonia.Controls } OnSelectionChanged(e); - ApplyAutoSelect(); + ApplyAutoSelect(true); } - internal IObservable? ResolvePath(object data, IndexPath dataIndexPath) + internal IObservable? ResolvePath( + object data, + IndexPath dataIndexPath, + IndexPath finalIndexPath) { IObservable? resolved = null; @@ -604,18 +610,22 @@ namespace Avalonia.Controls { if (_childrenRequestedEventArgs == null) { - _childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(data, dataIndexPath, false); + _childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs( + data, + dataIndexPath, + finalIndexPath, + false); } else { - _childrenRequestedEventArgs.Initialize(data, dataIndexPath, false); + _childrenRequestedEventArgs.Initialize(data, dataIndexPath, finalIndexPath, false); } ChildrenRequested(this, _childrenRequestedEventArgs); resolved = _childrenRequestedEventArgs.Children; // Clear out the values in the args so that it cannot be used after the event handler call. - _childrenRequestedEventArgs.Initialize(null, default, true); + _childrenRequestedEventArgs.Initialize(null, default, default, true); } return resolved; @@ -632,6 +642,8 @@ namespace Avalonia.Controls { AnchorIndex = default; } + + OnSelectionChanged(); } private void OnSelectionChanged(SelectionModelSelectionChangedEventArgs? e = null) @@ -667,6 +679,8 @@ namespace Avalonia.Controls { AnchorIndex = new IndexPath(index); } + + OnSelectionChanged(); } private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select) @@ -676,13 +690,15 @@ namespace Avalonia.Controls ClearSelection(resetAnchor: true); } - var childNode = _rootNode.GetAt(groupIndex, realizeChild: true); + var childNode = _rootNode.GetAt(groupIndex, true, new IndexPath(groupIndex, itemIndex)); var selected = childNode!.Select(itemIndex, select); if (selected) { AnchorIndex = new IndexPath(groupIndex, itemIndex); } + + OnSelectionChanged(); } private void SelectWithPathImpl(IndexPath index, bool select) @@ -711,6 +727,8 @@ namespace Avalonia.Controls { AnchorIndex = index; } + + OnSelectionChanged(); } private void SelectRangeFromAnchorImpl(int index, bool select) @@ -724,6 +742,7 @@ namespace Avalonia.Controls } _rootNode.SelectRange(new IndexRange(anchorIndex, index), select); + OnSelectionChanged(); } private void SelectRangeFromAnchorWithGroupImpl(int endGroupIndex, int endItemIndex, bool select) @@ -752,11 +771,13 @@ namespace Avalonia.Controls for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++) { - var groupNode = _rootNode.GetAt(groupIdx, realizeChild: true)!; + var groupNode = _rootNode.GetAt(groupIdx, true, new IndexPath(endGroupIndex, endItemIndex))!; int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0; int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1; groupNode.SelectRange(new IndexRange(startIndex, endIndex), select); } + + OnSelectionChanged(); } private void SelectRangeImpl(IndexPath start, IndexPath end, bool select) @@ -784,6 +805,8 @@ namespace Avalonia.Controls info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select); } }); + + OnSelectionChanged(); } private void BeginOperation() @@ -806,6 +829,8 @@ namespace Avalonia.Controls if (--_operationCount == 0) { + ApplyAutoSelect(false); + var changes = new List(); _rootNode.EndOperation(changes); @@ -827,7 +852,7 @@ namespace Avalonia.Controls } } - private void ApplyAutoSelect() + private void ApplyAutoSelect(bool createOperation) { if (AutoSelect) { @@ -835,8 +860,15 @@ namespace Avalonia.Controls if (SelectedIndex == default && _rootNode.ItemsSourceView?.Count > 0) { - using var operation = new Operation(this); - SelectImpl(0, true); + if (createOperation) + { + using var operation = new Operation(this); + SelectImpl(0, true); + } + else + { + SelectImpl(0, true); + } } } } diff --git a/src/Avalonia.Controls/SelectionModelChangeSet.cs b/src/Avalonia.Controls/SelectionModelChangeSet.cs index 6e77dc5755..d1df38656a 100644 --- a/src/Avalonia.Controls/SelectionModelChangeSet.cs +++ b/src/Avalonia.Controls/SelectionModelChangeSet.cs @@ -135,7 +135,7 @@ namespace Avalonia.Controls if (index >= currentIndex && index < currentIndex + currentCount) { int targetIndex = GetIndexAt(getRanges(info), index - currentIndex); - item = info.Items?.GetAt(targetIndex); + item = info.Items?.Count > targetIndex ? info.Items?.GetAt(targetIndex) : null; break; } diff --git a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs b/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs index 974da0cf71..b1f3e0b2c4 100644 --- a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs +++ b/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs @@ -16,15 +16,17 @@ namespace Avalonia.Controls { private object? _source; private IndexPath _sourceIndexPath; + private IndexPath _finalIndexPath; private bool _throwOnAccess; internal SelectionModelChildrenRequestedEventArgs( object source, IndexPath sourceIndexPath, + IndexPath finalIndexPath, bool throwOnAccess) { source = source ?? throw new ArgumentNullException(nameof(source)); - Initialize(source, sourceIndexPath, throwOnAccess); + Initialize(source, sourceIndexPath, finalIndexPath, throwOnAccess); } /// @@ -65,9 +67,26 @@ namespace Avalonia.Controls } } + /// + /// Gets the index of the final object which is being attempted to be retrieved. + /// + public IndexPath FinalIndex + { + get + { + if (_throwOnAccess) + { + throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs)); + } + + return _finalIndexPath; + } + } + internal void Initialize( object? source, IndexPath sourceIndexPath, + IndexPath finalIndexPath, bool throwOnAccess) { if (!throwOnAccess && source == null) @@ -77,6 +96,7 @@ namespace Avalonia.Controls _source = source; _sourceIndexPath = sourceIndexPath; + _finalIndexPath = finalIndexPath; _throwOnAccess = throwOnAccess; } } diff --git a/src/Avalonia.Controls/SelectionNode.cs b/src/Avalonia.Controls/SelectionNode.cs index 0b00db88c3..d99606673e 100644 --- a/src/Avalonia.Controls/SelectionNode.cs +++ b/src/Avalonia.Controls/SelectionNode.cs @@ -101,6 +101,7 @@ namespace Avalonia.Controls ItemsSourceView = newDataSource; + TrimInvalidSelections(); PopulateSelectedItemsFromSelectedIndices(); HookupCollectionChangedHandler(); OnSelectionChanged(); @@ -108,6 +109,26 @@ namespace Avalonia.Controls } } + private void TrimInvalidSelections() + { + if (_selected == null || ItemsSourceView == null) + { + return; + } + + var validRange = ItemsSourceView.Count > 0 ? new IndexRange(0, ItemsSourceView.Count - 1) : new IndexRange(-1, -1); + var removed = new List(); + var removedCount = IndexRange.Intersect(_selected, validRange, removed); + + if (removedCount > 0) + { + using var operation = _manager.Update(); + SelectedCount -= removedCount; + OnSelectionChanged(); + _operation!.Deselected(removed); + } + } + public ItemsSourceView? ItemsSourceView { get; private set; } public int DataCount => ItemsSourceView?.Count ?? 0; public int ChildrenNodeCount => _childrenNodes.Count; @@ -141,7 +162,7 @@ namespace Avalonia.Controls // create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid // an explosion of node objects. However, I'm still creating the m_childrenNodes // collection unfortunately. - public SelectionNode? GetAt(int index, bool realizeChild) + public SelectionNode? GetAt(int index, bool realizeChild, IndexPath finalIndexPath) { SelectionNode? child = null; @@ -171,7 +192,7 @@ namespace Avalonia.Controls if (childData != null) { var childDataIndexPath = IndexPath.CloneWithChildIndex(index); - resolver = _manager.ResolvePath(childData, childDataIndexPath); + resolver = _manager.ResolvePath(childData, childDataIndexPath, finalIndexPath); } if (resolver != null) @@ -843,7 +864,7 @@ namespace Avalonia.Controls int notSelectedCount = 0; for (int i = 0; i < ChildrenNodeCount; i++) { - var child = GetAt(i, realizeChild: false); + var child = GetAt(i, false, default); if (child != null) { diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index e92c8faf20..ec23bfa396 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,12 +1,40 @@ using System; +using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; -using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; +using Avalonia.Utilities; namespace Avalonia.Controls { + + /// + /// Enum which describes how to position the ticks in a . + /// + public enum TickPlacement + { + /// + /// No tick marks will appear. + /// + None, + + /// + /// Tick marks will appear above the track for a horizontal , or to the left of the track for a vertical . + /// + TopLeft, + + /// + /// Tick marks will appear below the track for a horizontal , or to the right of the track for a vertical . + /// + BottomRight, + + /// + /// Tick marks appear on both sides of either a horizontal or vertical . + /// + Outside + } + /// /// A control that lets the user select from a range of values by moving a Thumb control along a Track. /// @@ -30,19 +58,31 @@ namespace Avalonia.Controls public static readonly StyledProperty TickFrequencyProperty = AvaloniaProperty.Register(nameof(TickFrequency), 0.0); + /// + /// Defines the property. + /// + public static readonly StyledProperty TickPlacementProperty = + AvaloniaProperty.Register(nameof(TickPlacement), 0d); + // Slider required parts + private bool _isDragging = false; private Track _track; private Button _decreaseButton; private Button _increaseButton; + private IDisposable _decreaseButtonPressDispose; + private IDisposable _decreaseButtonReleaseDispose; + private IDisposable _increaseButtonSubscription; + private IDisposable _increaseButtonReleaseDispose; + private IDisposable _pointerMovedDispose; /// /// Initializes static members of the class. /// static Slider() { + PressedMixin.Attach(); OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal); Thumb.DragStartedEvent.AddClassHandler((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble); - Thumb.DragDeltaEvent.AddClassHandler((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble); } @@ -81,57 +121,88 @@ namespace Avalonia.Controls set { SetValue(TickFrequencyProperty, value); } } + /// + /// Gets or sets a value that indicates where to draw + /// tick marks in relation to the track. + /// + public TickPlacement TickPlacement + { + get { return GetValue(TickPlacementProperty); } + set { SetValue(TickPlacementProperty, value); } + } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - if (_decreaseButton != null) - { - _decreaseButton.Click -= DecreaseClick; - } - - if (_increaseButton != null) - { - _increaseButton.Click -= IncreaseClick; - } + base.OnApplyTemplate(e); + + _decreaseButtonPressDispose?.Dispose(); + _decreaseButtonReleaseDispose?.Dispose(); + _increaseButtonSubscription?.Dispose(); + _increaseButtonReleaseDispose?.Dispose(); + _pointerMovedDispose?.Dispose(); _decreaseButton = e.NameScope.Find