diff --git a/.editorconfig b/.editorconfig index b7a03207a4..5f08d1e940 100644 --- a/.editorconfig +++ b/.editorconfig @@ -132,6 +132,9 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false +# Wrapping preferences +csharp_wrap_before_ternary_opsigns = false + # Xaml files [*.xaml] indent_size = 4 diff --git a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs index 2ab6c26e68..df80931367 100644 --- a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs @@ -21,7 +21,7 @@ namespace BindingDemo.ViewModels } else { - throw new ArgumentOutOfRangeException("Value must be less than 10."); + throw new ArgumentOutOfRangeException(nameof(value), "Value must be less than 10."); } } } diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml b/samples/ControlCatalog/Pages/ContextMenuPage.xaml index 932f3292ff..8f147638b5 100644 --- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml +++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml @@ -10,7 +10,8 @@ HorizontalAlignment="Center" Spacing="16"> + Margin="16" + Padding="48,48,48,48"> @@ -31,7 +32,24 @@ - + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs index dc73bef07a..96e8b49f89 100644 --- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs @@ -1,5 +1,6 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { @@ -8,6 +9,7 @@ namespace ControlCatalog.Pages public ContextMenuPage() { this.InitializeComponent(); + DataContext = new ContextMenuPageViewModel(); } private void InitializeComponent() diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml.cs b/samples/ControlCatalog/Pages/MenuPage.xaml.cs index 5a07cae89f..0a77607719 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml.cs +++ b/samples/ControlCatalog/Pages/MenuPage.xaml.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Windows.Input; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; using ReactiveUI; namespace ControlCatalog.Pages @@ -13,51 +14,7 @@ namespace ControlCatalog.Pages public MenuPage() { this.InitializeComponent(); - var vm = new MenuPageViewModel(); - - vm.MenuItems = new[] - { - new MenuItemViewModel - { - Header = "_File", - Items = new[] - { - new MenuItemViewModel { Header = "_Open...", Command = vm.OpenCommand }, - new MenuItemViewModel { Header = "Save", Command = vm.SaveCommand }, - new MenuItemViewModel { Header = "-" }, - new MenuItemViewModel - { - Header = "Recent", - Items = new[] - { - new MenuItemViewModel - { - Header = "File1.txt", - Command = vm.OpenRecentCommand, - CommandParameter = @"c:\foo\File1.txt" - }, - new MenuItemViewModel - { - Header = "File2.txt", - Command = vm.OpenRecentCommand, - CommandParameter = @"c:\foo\File2.txt" - }, - } - }, - } - }, - new MenuItemViewModel - { - Header = "_Edit", - Items = new[] - { - new MenuItemViewModel { Header = "_Copy" }, - new MenuItemViewModel { Header = "_Paste" }, - } - } - }; - - DataContext = vm; + DataContext = new MenuPageViewModel(); } private void InitializeComponent() @@ -65,51 +22,4 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } } - - public class MenuPageViewModel - { - public MenuPageViewModel() - { - OpenCommand = ReactiveCommand.CreateFromTask(Open); - SaveCommand = ReactiveCommand.Create(Save); - OpenRecentCommand = ReactiveCommand.Create(OpenRecent); - } - - public IReadOnlyList MenuItems { get; set; } - public ReactiveCommand OpenCommand { get; } - public ReactiveCommand SaveCommand { get; } - public ReactiveCommand OpenRecentCommand { get; } - - public async Task Open() - { - var dialog = new OpenFileDialog(); - var result = await dialog.ShowAsync(App.Current.MainWindow); - - if (result != null) - { - foreach (var path in result) - { - System.Diagnostics.Debug.WriteLine($"Opened: {path}"); - } - } - } - - public void Save() - { - System.Diagnostics.Debug.WriteLine("Save"); - } - - public void OpenRecent(string path) - { - System.Diagnostics.Debug.WriteLine($"Open recent: {path}"); - } - } - - public class MenuItemViewModel - { - public string Header { get; set; } - public ICommand Command { get; set; } - public object CommandParameter { get; set; } - public IList Items { get; set; } - } } diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index f8f3cd5848..c03edb8b03 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -9,7 +9,7 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - + diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 625b344b8c..fea55bcb07 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -1,6 +1,14 @@ + + + + + + + + - + \ No newline at end of file diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index 135935498c..e48c671a13 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -21,7 +21,7 @@ namespace Avalonia.Data.Core.Plugins { if (method.GetParameters().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(method)); + 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)); } diff --git a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs new file mode 100644 index 0000000000..0ade1af249 --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs @@ -0,0 +1,219 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Avalonia.Utilities +{ + /// + /// Manages subscriptions to events using weak listeners. + /// + public static class WeakEventHandlerManager + { + /// + /// Subscribes to an event on an object using a weak subscription. + /// + /// The type of the target. + /// The type of the event arguments. + /// The event source. + /// The name of the event. + /// The subscriber. + public static void Subscribe(TTarget target, string eventName, EventHandler subscriber) + where TEventArgs : EventArgs where TSubscriber : class + { + var dic = SubscriptionTypeStorage.Subscribers.GetOrCreateValue(target); + Subscription sub; + + if (!dic.TryGetValue(eventName, out sub)) + { + dic[eventName] = sub = new Subscription(dic, typeof(TTarget), target, eventName); + } + + sub.Add(subscriber); + } + + /// + /// Unsubscribes from an event. + /// + /// The type of the event arguments. + /// The event source. + /// The name of the event. + /// The subscriber. + public static void Unsubscribe(object target, string eventName, EventHandler subscriber) + where TEventArgs : EventArgs where TSubscriber : class + { + SubscriptionDic dic; + + if (SubscriptionTypeStorage.Subscribers.TryGetValue(target, out dic)) + { + Subscription sub; + + if (dic.TryGetValue(eventName, out sub)) + { + sub.Remove(subscriber); + } + } + } + + private static class SubscriptionTypeStorage + where TArgs : EventArgs where TSubscriber : class + { + public static readonly ConditionalWeakTable> Subscribers + = new ConditionalWeakTable>(); + } + + private class SubscriptionDic : Dictionary> + where T : EventArgs where TSubscriber : class + { + } + + private static readonly Dictionary> Accessors + = new Dictionary>(); + + private class Subscription where T : EventArgs where TSubscriber : class + { + private readonly EventInfo _info; + private readonly SubscriptionDic _sdic; + private readonly object _target; + private readonly string _eventName; + private readonly Delegate _delegate; + + private Descriptor[] _data = new Descriptor[2]; + private int _count = 0; + + delegate void CallerDelegate(TSubscriber s, object sender, T args); + + struct Descriptor + { + public WeakReference Subscriber; + public CallerDelegate Caller; + } + + private static Dictionary s_Callers = + new Dictionary(); + + public Subscription(SubscriptionDic sdic, Type targetType, object target, string eventName) + { + _sdic = sdic; + _target = target; + _eventName = eventName; + Dictionary evDic; + if (!Accessors.TryGetValue(targetType, out evDic)) + Accessors[targetType] = evDic = new Dictionary(); + + if (!evDic.TryGetValue(eventName, out _info)) + { + var ev = targetType.GetRuntimeEvents().FirstOrDefault(x => x.Name == eventName); + + if (ev == null) + { + throw new ArgumentException( + $"The event {eventName} was not found on {target.GetType()}."); + } + + evDic[eventName] = _info = ev; + } + + var del = new Action(OnEvent); + _delegate = del.GetMethodInfo().CreateDelegate(_info.EventHandlerType, del.Target); + _info.AddMethod.Invoke(target, new[] { _delegate }); + } + + void Destroy() + { + _info.RemoveMethod.Invoke(_target, new[] { _delegate }); + _sdic.Remove(_eventName); + } + + public void Add(EventHandler s) + { + Compact(true); + if (_count == _data.Length) + { + //Extend capacity + var ndata = new Descriptor[_data.Length*2]; + Array.Copy(_data, ndata, _data.Length); + _data = ndata; + } + + var subscriber = (TSubscriber)s.Target; + if (!s_Callers.TryGetValue(s.Method, out var caller)) + s_Callers[s.Method] = caller = + (CallerDelegate)Delegate.CreateDelegate(typeof(CallerDelegate), null, s.Method); + _data[_count] = new Descriptor + { + Caller = caller, + Subscriber = new WeakReference(subscriber) + }; + _count++; + } + + public void Remove(EventHandler s) + { + var removed = false; + + for (int c = 0; c < _count; ++c) + { + var reference = _data[c].Subscriber; + TSubscriber instance; + + if (reference != null && reference.TryGetTarget(out instance) && instance == s) + { + _data[c] = default; + removed = true; + } + } + + if (removed) + { + Compact(); + } + } + + void Compact(bool preventDestroy = false) + { + int empty = -1; + for (int c = 0; c < _count; c++) + { + var r = _data[c]; + //Mark current index as first empty + if (r.Subscriber == null && empty == -1) + empty = c; + //If current element isn't null and we have an empty one + if (r.Subscriber != null && empty != -1) + { + _data[c] = default; + _data[empty] = r; + empty++; + } + } + if (empty != -1) + _count = empty; + if (_count == 0 && !preventDestroy) + Destroy(); + } + + void OnEvent(object sender, T eventArgs) + { + var needCompact = false; + for(var c=0; c<_count; c++) + { + var r = _data[c].Subscriber; + TSubscriber sub; + if (r.TryGetTarget(out sub)) + { + _data[c].Caller(sub, sender, eventArgs); + } + else + needCompact = true; + } + if (needCompact) + Compact(); + } + } + } +} diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index c5dd072d8a..1f6870d60d 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -210,7 +210,7 @@ namespace Avalonia.Controls var platformClassName = assemblyName.Replace("Avalonia.", string.Empty) + "Platform"; var platformClassFullName = assemblyName + "." + platformClassName; var platformClass = assembly.GetType(platformClassFullName); - var init = platformClass.GetRuntimeMethod("Initialize", new Type[0]); + var init = platformClass.GetRuntimeMethod("Initialize", Type.EmptyTypes); init.Invoke(null, null); }; @@ -245,7 +245,7 @@ namespace Avalonia.Controls select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors where constructor.GetParameters().Length == 0 && !constructor.IsStatic select constructor).Single() into constructor - select (Action)(() => constructor.Invoke(new object[0])); + select (Action)(() => constructor.Invoke(Array.Empty())); Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke(); } diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index f572c67284..5ed0abf25d 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -32,6 +32,8 @@ namespace Avalonia.Controls /// public class Button : ContentControl { + private ICommand _command; + /// /// Defines the property. /// @@ -69,8 +71,6 @@ namespace Avalonia.Controls public static readonly RoutedEvent ClickEvent = RoutedEvent.Register(nameof(Click), RoutingStrategies.Bubble); - private ICommand _command; - public static readonly StyledProperty IsPressedProperty = AvaloniaProperty.Register(nameof(IsPressed)); diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index a69152c42b..92293a32d6 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,34 +1,31 @@ using System; -using System.Reactive.Linq; -using System.Linq; using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; -using System.Collections.Generic; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.LogicalTree; -using Avalonia.Controls.Primitives; namespace Avalonia.Controls { - public class ContextMenu : SelectingItemsControl, IMenu + /// + /// A control context menu. + /// + public class ContextMenu : MenuBase { - private readonly IMenuInteractionHandler _interaction; - private bool _isOpen; + private static readonly ITemplate DefaultPanel = + new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical }); private Popup _popup; - /// - /// Defines the property. - /// - public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect(nameof(IsOpen), o => o.IsOpen); - /// /// Initializes a new instance of the class. /// public ContextMenu() + : this(new DefaultMenuInteractionHandler(true)) { - _interaction = AvaloniaLocator.Current.GetService() ?? - new DefaultMenuInteractionHandler(); } /// @@ -36,10 +33,8 @@ namespace Avalonia.Controls /// /// The menu interaction handler. public ContextMenu(IMenuInteractionHandler interactionHandler) + : base(interactionHandler) { - Contract.Requires(interactionHandler != null); - - _interaction = interactionHandler; } /// @@ -47,44 +42,10 @@ namespace Avalonia.Controls /// static ContextMenu() { + ItemsPanelProperty.OverrideDefaultValue(typeof(ContextMenu), DefaultPanel); ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); } - /// - /// Gets a value indicating whether the popup is open - /// - public bool IsOpen => _isOpen; - - /// - IMenuInteractionHandler IMenu.InteractionHandler => _interaction; - - /// - IMenuItem IMenuElement.SelectedItem - { - get - { - var index = SelectedIndex; - return (index != -1) ? - (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : - null; - } - set - { - SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); - } - } - - /// - IEnumerable IMenuElement.SubItems - { - get - { - return ItemContainerGenerator.Containers - .Select(x => x.ContainerControl) - .OfType(); - } - } - /// /// Occurs when the value of the /// @@ -121,7 +82,7 @@ namespace Avalonia.Controls /// /// Opens the menu. /// - public void Open() => Open(null); + public override void Open() => Open(null); /// /// Opens a context menu on the specified control. @@ -139,21 +100,20 @@ namespace Avalonia.Controls ObeyScreenEdges = true }; + _popup.Opened += PopupOpened; _popup.Closed += PopupClosed; - _interaction.Attach(this); } ((ISetLogicalParent)_popup).SetParent(control); _popup.Child = this; _popup.IsOpen = true; - - SetAndRaise(IsOpenProperty, ref _isOpen, true); + IsOpen = true; } /// /// Closes the menu. /// - public void Close() + public override void Close() { if (_popup != null && _popup.IsVisible) { @@ -161,8 +121,17 @@ namespace Avalonia.Controls } SelectedIndex = -1; + IsOpen = false; + } + + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new MenuItemContainerGenerator(this); + } - SetAndRaise(IsOpenProperty, ref _isOpen, false); + private void PopupOpened(object sender, EventArgs e) + { + Focus(); } private void PopupClosed(object sender, EventArgs e) @@ -176,7 +145,7 @@ namespace Avalonia.Controls i.IsSubMenuOpen = false; } - contextMenu._isOpen = false; + contextMenu.IsOpen = false; contextMenu.SelectedIndex = -1; } } @@ -186,7 +155,7 @@ namespace Avalonia.Controls var control = (Control)sender; var contextMenu = control.ContextMenu; - if (control.ContextMenu._isOpen) + if (control.ContextMenu.IsOpen) { if (contextMenu.CancelClosing()) return; @@ -218,10 +187,5 @@ namespace Avalonia.Controls ContextMenuOpening?.Invoke(this, eventArgs); return eventArgs.Cancel; } - - bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) - { - throw new NotImplementedException(); - } } } diff --git a/src/Avalonia.Controls/Design.cs b/src/Avalonia.Controls/Design.cs index 894240a09f..0d05e19e53 100644 --- a/src/Avalonia.Controls/Design.cs +++ b/src/Avalonia.Controls/Design.cs @@ -48,14 +48,14 @@ namespace Avalonia.Controls } public static readonly AttachedProperty PreviewWithProperty = AvaloniaProperty - .RegisterAttached("PreviewWith", typeof (Design)); + .RegisterAttached("PreviewWith", typeof (Design)); - public static void SetPreviewWith(Style target, Control control) + public static void SetPreviewWith(AvaloniaObject target, Control control) { target.SetValue(PreviewWithProperty, control); } - public static Control GetPreviewWith(Style target) + public static Control GetPreviewWith(AvaloniaObject target) { return target.GetValue(PreviewWithProperty); } diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index d328e1ee88..9c53dc0c10 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -11,11 +11,13 @@ namespace Avalonia.Controls.Embedding.Offscreen { private double _scaling = 1; private Size _clientSize; + public IInputRoot InputRoot { get; private set; } + public bool IsDisposed { get; private set; } public virtual void Dispose() { - //No-op + IsDisposed = true; } public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer(root); diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 00fca385a0..b0fb3f2b3b 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -1,56 +1,26 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; -using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.LogicalTree; namespace Avalonia.Controls { /// /// A top-level menu control. /// - public class Menu : SelectingItemsControl, IFocusScope, IMainMenu, IMenu + public class Menu : MenuBase, IMainMenu { - /// - /// Defines the property. - /// - public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsOpen), - o => o.IsOpen); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent MenuOpenedEvent = - RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent MenuClosedEvent = - RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); - private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); - private readonly IMenuInteractionHandler _interaction; - private bool _isOpen; /// /// Initializes a new instance of the class. /// public Menu() { - _interaction = AvaloniaLocator.Current.GetService() ?? - new DefaultMenuInteractionHandler(); } /// @@ -58,82 +28,17 @@ namespace Avalonia.Controls /// /// The menu interaction handler. public Menu(IMenuInteractionHandler interactionHandler) + : base(interactionHandler) { - Contract.Requires(interactionHandler != null); - - _interaction = interactionHandler; } - /// - /// Initializes static members of the class. - /// static Menu() { ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); - MenuItem.SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); - } - - /// - /// Gets a value indicating whether the menu is open. - /// - public bool IsOpen - { - get { return _isOpen; } - private set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } - } - - /// - IMenuInteractionHandler IMenu.InteractionHandler => _interaction; - - /// - IMenuItem IMenuElement.SelectedItem - { - get - { - var index = SelectedIndex; - return (index != -1) ? - (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : - null; - } - set - { - SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); - } } /// - IEnumerable IMenuElement.SubItems - { - get - { - return ItemContainerGenerator.Containers - .Select(x => x.ContainerControl) - .OfType(); - } - } - - /// - /// Occurs when a is opened. - /// - public event EventHandler MenuOpened - { - add { AddHandler(MenuOpenedEvent, value); } - remove { RemoveHandler(MenuOpenedEvent, value); } - } - - /// - /// Occurs when a is closed. - /// - public event EventHandler MenuClosed - { - add { AddHandler(MenuClosedEvent, value); } - remove { RemoveHandler(MenuClosedEvent, value); } - } - - /// - /// Closes the menu. - /// - public void Close() + public override void Close() { if (IsOpen) { @@ -153,10 +58,8 @@ namespace Avalonia.Controls } } - /// - /// Opens the menu in response to the Alt/F10 key. - /// - public void Open() + /// + public override void Open() { if (!IsOpen) { @@ -170,15 +73,6 @@ namespace Avalonia.Controls } } - /// - bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); - - /// - protected override IItemContainerGenerator CreateItemContainerGenerator() - { - return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); - } - /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -190,41 +84,6 @@ namespace Avalonia.Controls { inputRoot.AccessKeyHandler.MainMenu = this; } - - _interaction.Attach(this); - } - - /// - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - _interaction.Detach(this); - } - - /// - protected override void OnKeyDown(KeyEventArgs e) - { - // Don't handle here: let the interaction handler handle it. - } - - /// - /// Called when a submenu opens somewhere in the menu. - /// - /// The event args. - protected virtual void OnSubmenuOpened(RoutedEventArgs e) - { - if (e.Source is MenuItem menuItem && menuItem.Parent == this) - { - foreach (var child in this.GetLogicalChildren().OfType()) - { - if (child != menuItem && child.IsSubMenuOpen) - { - child.IsSubMenuOpen = false; - } - } - } - - IsOpen = true; } } } diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs new file mode 100644 index 0000000000..d6eb40360b --- /dev/null +++ b/src/Avalonia.Controls/MenuBase.cs @@ -0,0 +1,192 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls.Generators; +using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; + +namespace Avalonia.Controls +{ + /// + /// Base class for menu controls. + /// + public abstract class MenuBase : SelectingItemsControl, IFocusScope, IMenu + { + /// + /// Defines the property. + /// + public static readonly DirectProperty IsOpenProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsOpen), + o => o.IsOpen); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent MenuOpenedEvent = + RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent MenuClosedEvent = + RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); + + private bool _isOpen; + + /// + /// Initializes a new instance of the class. + /// + public MenuBase() + { + InteractionHandler = new DefaultMenuInteractionHandler(false); + } + + /// + /// Initializes a new instance of the class. + /// + /// The menu interaction handler. + public MenuBase(IMenuInteractionHandler interactionHandler) + { + Contract.Requires(interactionHandler != null); + + InteractionHandler = interactionHandler; + } + + /// + /// Initializes static members of the class. + /// + static MenuBase() + { + MenuItem.SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); + } + + /// + /// Gets a value indicating whether the menu is open. + /// + public bool IsOpen + { + get { return _isOpen; } + protected set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } + } + + /// + IMenuInteractionHandler IMenu.InteractionHandler => InteractionHandler; + + /// + IMenuItem IMenuElement.SelectedItem + { + get + { + var index = SelectedIndex; + return (index != -1) ? + (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : + null; + } + set + { + SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); + } + } + + /// + IEnumerable IMenuElement.SubItems + { + get + { + return ItemContainerGenerator.Containers + .Select(x => x.ContainerControl) + .OfType(); + } + } + + /// + /// Gets the interaction handler for the menu. + /// + protected IMenuInteractionHandler InteractionHandler { get; } + + /// + /// Occurs when a is opened. + /// + public event EventHandler MenuOpened + { + add { AddHandler(MenuOpenedEvent, value); } + remove { RemoveHandler(MenuOpenedEvent, value); } + } + + /// + /// Occurs when a is closed. + /// + public event EventHandler MenuClosed + { + add { AddHandler(MenuClosedEvent, value); } + remove { RemoveHandler(MenuClosedEvent, value); } + } + + /// + /// Closes the menu. + /// + public abstract void Close(); + + /// + /// Opens the menu. + /// + public abstract void Open(); + + /// + bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); + + /// + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + // Don't handle here: let the interaction handler handle it. + } + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + InteractionHandler.Attach(this); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + InteractionHandler.Detach(this); + } + + /// + /// Called when a submenu opens somewhere in the menu. + /// + /// The event args. + protected virtual void OnSubmenuOpened(RoutedEventArgs e) + { + if (e.Source is MenuItem menuItem && menuItem.Parent == this) + { + foreach (var child in this.GetLogicalChildren().OfType()) + { + if (child != menuItem && child.IsSubMenuOpen) + { + child.IsSubMenuOpen = false; + } + } + } + + IsOpen = true; + } + } +} diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index c2227b9dfc..64daa133a3 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -20,11 +20,16 @@ namespace Avalonia.Controls /// public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable { + private ICommand _command; + /// /// Defines the property. /// - public static readonly StyledProperty CommandProperty = - AvaloniaProperty.Register(nameof(Command)); + public static readonly DirectProperty CommandProperty = + Button.CommandProperty.AddOwner( + menuItem => menuItem.Command, + (menuItem, command) => menuItem.Command = command, + enableDataValidation: true); /// /// Defines the property. @@ -159,8 +164,8 @@ namespace Avalonia.Controls /// public ICommand Command { - get { return GetValue(CommandProperty); } - set { SetValue(CommandProperty, value); } + get { return _command; } + set { SetAndRaise(CommandProperty, ref _command, value); } } /// diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 18cef7d64e..f8ae5c9690 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -965,11 +965,11 @@ namespace Avalonia.Controls { if (value < Minimum) { - throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum)); + throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be greater than Minimum value of {0}", Minimum)); } else if (value > Maximum) { - throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum)); + throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be less than Maximum value of {0}", Maximum)); } } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 6b03e67897..942104d61b 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -13,18 +13,21 @@ namespace Avalonia.Controls.Platform /// public class DefaultMenuInteractionHandler : IMenuInteractionHandler { + private readonly bool _isContextMenu; private IDisposable _inputManagerSubscription; private IRenderRoot _root; - public DefaultMenuInteractionHandler() - : this(Input.InputManager.Instance, DefaultDelayRun) + public DefaultMenuInteractionHandler(bool isContextMenu) + : this(isContextMenu, Input.InputManager.Instance, DefaultDelayRun) { } public DefaultMenuInteractionHandler( + bool isContextMenu, IInputManager inputManager, Action delayRun) { + _isContextMenu = isContextMenu; InputManager = inputManager; DelayRun = delayRun; } @@ -59,7 +62,7 @@ namespace Avalonia.Controls.Platform window.Deactivated += WindowDeactivated; } - _inputManagerSubscription = InputManager.Process.Subscribe(RawInput); + _inputManagerSubscription = InputManager?.Process.Subscribe(RawInput); } public virtual void Detach(IMenu menu) @@ -125,23 +128,16 @@ namespace Avalonia.Controls.Platform protected internal virtual void KeyDown(object sender, KeyEventArgs e) { - var item = GetMenuItem(e.Source as IControl); - - if (item != null) - { - KeyDown(item, e); - } + KeyDown(GetMenuItem(e.Source as IControl), e); } protected internal virtual void KeyDown(IMenuItem item, KeyEventArgs e) { - Contract.Requires(item != null); - switch (e.Key) { case Key.Up: case Key.Down: - if (item.IsTopLevel) + if (item?.IsTopLevel == true) { if (item.HasSubMenu && !item.IsSubMenuOpen) { @@ -156,7 +152,7 @@ namespace Avalonia.Controls.Platform break; case Key.Left: - if (item.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) + if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) { parent.Close(); parent.Focus(); @@ -169,7 +165,7 @@ namespace Avalonia.Controls.Platform break; case Key.Right: - if (!item.IsTopLevel && item.HasSubMenu) + if (item != null && !item.IsTopLevel && item.HasSubMenu) { Open(item, true); e.Handled = true; @@ -181,47 +177,65 @@ namespace Avalonia.Controls.Platform break; case Key.Enter: - if (!item.HasSubMenu) + if (item != null) { - Click(item); - } - else - { - Open(item, true); - } + if (!item.HasSubMenu) + { + Click(item); + } + else + { + Open(item, true); + } - e.Handled = true; + e.Handled = true; + } break; case Key.Escape: - if (item.Parent != null) + if (item?.Parent != null) { item.Parent.Close(); item.Parent.Focus(); - e.Handled = true; } + else + { + Menu.Close(); + } + + e.Handled = true; break; default: var direction = e.Key.ToNavigationDirection(); - if (direction.HasValue && item.Parent?.MoveSelection(direction.Value, true) == true) + if (direction.HasValue) { - // If the the parent is an IMenu which successfully moved its selection, - // and the current menu is open then close the current menu and open the - // new menu. - if (item.IsSubMenuOpen && item.Parent is IMenu) + if (item == null && _isContextMenu) { - item.Close(); - Open(item.Parent.SelectedItem, true); + if (Menu.MoveSelection(direction.Value, true) == true) + { + e.Handled = true; + } + } + else if (item.Parent?.MoveSelection(direction.Value, true) == true) + { + // If the the parent is an IMenu which successfully moved its selection, + // and the current menu is open then close the current menu and open the + // new menu. + if (item.IsSubMenuOpen && item.Parent is IMenu) + { + item.Close(); + Open(item.Parent.SelectedItem, true); + } + e.Handled = true; } - e.Handled = true; } break; } - if (!e.Handled && item.Parent is IMenuItem parentItem) + if (!e.Handled && item?.Parent is IMenuItem parentItem) { KeyDown(parentItem, e); } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index c40ddc37ad..a54bb502c0 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -99,7 +99,7 @@ namespace Avalonia.Controls.Primitives "SelectionChanged", RoutingStrategies.Bubble); - private static readonly IList Empty = new object[0]; + private static readonly IList Empty = Array.Empty(); private int _selectedIndex = -1; private object _selectedItem; @@ -614,32 +614,12 @@ namespace Avalonia.Controls.Primitives return false; } - /// - /// Gets a range of items from an IEnumerable. - /// - /// The items. - /// The index of the first item. - /// The index of the last item. - /// The items. - private static IEnumerable GetRange(IEnumerable items, int first, int last) - { - var list = (items as IList) ?? items.Cast().ToList(); - int step = first > last ? -1 : 1; - - for (int i = first; i != last; i += step) - { - yield return list[i]; - } - - yield return list[last]; - } - /// /// Makes a list of objects equal another. /// /// The items collection. /// The desired items. - private static void SynchronizeItems(IList items, IEnumerable desired) + internal static void SynchronizeItems(IList items, IEnumerable desired) { int index = 0; @@ -666,6 +646,26 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Gets a range of items from an IEnumerable. + /// + /// The items. + /// The index of the first item. + /// The index of the last item. + /// The items. + private static IEnumerable GetRange(IEnumerable items, int first, int last) + { + var list = (items as IList) ?? items.Cast().ToList(); + int step = first > last ? -1 : 1; + + for (int i = first; i != last; i += step) + { + yield return list[i]; + } + + yield return list[last]; + } + /// /// Called when a container raises the . /// diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index e1767fca36..6293cbfbfd 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -61,6 +61,11 @@ namespace Avalonia.Controls.Remote.Server { var result = InputModifiers.None; + if (modifiers == null) + { + return result; + } + foreach(var modifier in modifiers) { switch (modifier) @@ -265,11 +270,15 @@ namespace Avalonia.Controls.Remote.Server var bpp = fmt == ProtocolPixelFormat.Rgb565 ? 2 : 4; var data = new byte[width * height * bpp]; var handle = GCHandle.Alloc(data, GCHandleType.Pinned); + try { - _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, (PixelFormat)fmt, - null); - Paint?.Invoke(new Rect(0, 0, width, height)); + if (width > 0 && height > 0) + { + _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, (PixelFormat)fmt, + null); + Paint?.Invoke(new Rect(0, 0, width, height)); + } } finally { @@ -301,8 +310,7 @@ namespace Avalonia.Controls.Remote.Server return; } - if (ClientSize.Width < 1 || ClientSize.Height < 1) - return; + var format = ProtocolPixelFormat.Rgba8888; foreach(var fmt in _supportedFormats) if (fmt <= ProtocolPixelFormat.MaxValue) @@ -323,8 +331,11 @@ namespace Avalonia.Controls.Remote.Server public override void Invalidate(Rect rect) { - _invalidated = true; - Dispatcher.UIThread.Post(RenderIfNeeded); + if (!IsDisposed) + { + _invalidated = true; + Dispatcher.UIThread.Post(RenderIfNeeded); + } } public override IMouseDevice MouseDevice { get; } = new MouseDevice(); diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 0387328a46..57dbeba1cc 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -195,7 +195,7 @@ namespace Avalonia.Controls.Shapes if (deferCalculateTransform) { _calculateTransformOnArrange = true; - return DefiningGeometry.Bounds.Size; + return DefiningGeometry?.Bounds.Size ?? Size.Empty; } else { @@ -217,17 +217,22 @@ namespace Avalonia.Controls.Shapes private Size CalculateShapeSizeAndSetTransform(Size availableSize) { - // This should probably use GetRenderBounds(strokeThickness) but then the calculations - // will multiply the stroke thickness as well, which isn't correct. - var (size, transform) = CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch); - - if (_transform != transform) + if (DefiningGeometry != null) { - _transform = transform; - _renderedGeometry = null; + // This should probably use GetRenderBounds(strokeThickness) but then the calculations + // will multiply the stroke thickness as well, which isn't correct. + var (size, transform) = CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch); + + if (_transform != transform) + { + _transform = transform; + _renderedGeometry = null; + } + + return size; } - return size; + return Size.Empty; } internal static (Size, Matrix) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch) diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index e7cb4763ed..f321625bcc 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls throw new ArgumentNullException(nameof(parent)); return ((await AvaloniaLocator.Current.GetService() .ShowFileDialogAsync(this, parent?.PlatformImpl)) ?? - new string[0]).FirstOrDefault(); + Array.Empty()).FirstOrDefault(); } } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index c574799724..db8de49494 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -2,13 +2,15 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Styling; using Avalonia.Threading; using Avalonia.VisualTree; @@ -34,14 +36,24 @@ namespace Avalonia.Controls (o, v) => o.SelectedItem = v); /// - /// Defines the event. + /// Defines the property. /// - public static readonly RoutedEvent SelectedItemChangedEvent = - RoutedEvent.Register( - "SelectedItemChanged", - RoutingStrategies.Bubble); + public static readonly DirectProperty SelectedItemsProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedItems), + o => o.SelectedItems, + (o, v) => o.SelectedItems = v); + /// + /// Defines the property. + /// + protected static readonly StyledProperty SelectionModeProperty = + AvaloniaProperty.Register( + nameof(SelectionMode)); + + private static readonly IList Empty = new object[0]; private object _selectedItem; + private IList _selectedItems; /// /// Initializes static members of the class. @@ -54,16 +66,16 @@ namespace Avalonia.Controls /// /// Occurs when the control's selection changes. /// - public event EventHandler SelectedItemChanged + public event EventHandler SelectionChanged { - add { AddHandler(SelectedItemChangedEvent, value); } - remove { RemoveHandler(SelectedItemChangedEvent, value); } + add => AddHandler(SelectingItemsControl.SelectionChangedEvent, value); + remove => RemoveHandler(SelectingItemsControl.SelectionChangedEvent, value); } /// /// Gets the for the tree view. /// - public new ITreeItemContainerGenerator ItemContainerGenerator => + public new ITreeItemContainerGenerator ItemContainerGenerator => (ITreeItemContainerGenerator)base.ItemContainerGenerator; /// @@ -71,67 +83,258 @@ namespace Avalonia.Controls /// public bool AutoScrollToSelectedItem { - get { return GetValue(AutoScrollToSelectedItemProperty); } - set { SetValue(AutoScrollToSelectedItemProperty, value); } + get => GetValue(AutoScrollToSelectedItemProperty); + set => SetValue(AutoScrollToSelectedItemProperty, value); + } + + private bool _syncingSelectedItems; + + /// + /// Gets or sets the selection mode. + /// + public SelectionMode SelectionMode + { + get => GetValue(SelectionModeProperty); + set => SetValue(SelectionModeProperty, value); } /// /// Gets or sets the selected item. /// public object SelectedItem + { + get => _selectedItem; + set + { + SetAndRaise(SelectedItemProperty, ref _selectedItem, + (object val, ref object backing, Action notifyWrapper) => + { + var old = backing; + backing = val; + + notifyWrapper(() => + RaisePropertyChanged( + SelectedItemProperty, + old, + val)); + + if (val != null) + { + if (SelectedItems.Count != 1 || SelectedItems[0] != val) + { + _syncingSelectedItems = true; + SelectSingleItem(val); + _syncingSelectedItems = false; + } + } + else if (SelectedItems.Count > 0) + { + SelectedItems.Clear(); + } + }, value); + } + } + + /// + /// Gets the selected items. + /// + public IList SelectedItems { get { - return _selectedItem; + if (_selectedItems == null) + { + _selectedItems = new AvaloniaList(); + SubscribeToSelectedItems(); + } + + return _selectedItems; } set { - if (_selectedItem != null) + if (value?.IsFixedSize == true || value?.IsReadOnly == true) { - var container = ItemContainerGenerator.Index.ContainerFromItem(_selectedItem); - MarkContainerSelected(container, false); + throw new NotSupportedException( + "Cannot use a fixed size or read-only collection as SelectedItems."); } - var oldItem = _selectedItem; - SetAndRaise(SelectedItemProperty, ref _selectedItem, value); + UnsubscribeFromSelectedItems(); + _selectedItems = value ?? new AvaloniaList(); + SubscribeToSelectedItems(); + } + } - if (_selectedItem != null) - { - var container = ItemContainerGenerator.Index.ContainerFromItem(_selectedItem); - MarkContainerSelected(container, true); + /// + /// Subscribes to the CollectionChanged event, if any. + /// + private void SubscribeToSelectedItems() + { + if (_selectedItems is INotifyCollectionChanged incc) + { + incc.CollectionChanged += SelectedItemsCollectionChanged; + } + + SelectedItemsCollectionChanged( + _selectedItems, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } - if (AutoScrollToSelectedItem && container != null) + private void SelectSingleItem(object item) + { + SelectedItems.Clear(); + SelectedItems.Add(item); + } + + /// + /// Called when the CollectionChanged event is raised. + /// + /// The event sender. + /// The event args. + private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + IList added = null; + IList removed = null; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + + SelectedItemsAdded(e.NewItems.Cast().ToArray()); + + if (AutoScrollToSelectedItem) { - container.BringIntoView(); + var container = (TreeViewItem)ItemContainerGenerator.Index.ContainerFromItem(e.NewItems[0]); + + container?.BringIntoView(); } - } - if (oldItem != _selectedItem) - { - // Fire the SelectionChanged event - List removed = new List(); - if (oldItem != null) + added = e.NewItems; + + break; + case NotifyCollectionChangedAction.Remove: + + if (!_syncingSelectedItems) + { + if (SelectedItems.Count == 0) + { + SelectedItem = null; + } + else + { + var selectedIndex = SelectedItems.IndexOf(_selectedItem); + + if (selectedIndex == -1) + { + var old = _selectedItem; + _selectedItem = SelectedItems[0]; + + RaisePropertyChanged(SelectedItemProperty, old, _selectedItem); + } + } + } + + foreach (var item in e.OldItems) { - removed.Add(oldItem); + MarkItemSelected(item, false); } - List added = new List(); - if (_selectedItem != null) + removed = e.OldItems; + + break; + case NotifyCollectionChangedAction.Reset: + + foreach (IControl container in ItemContainerGenerator.Index.Items) { - added.Add(_selectedItem); + MarkContainerSelected(container, false); } - var changed = new SelectionChangedEventArgs( - SelectedItemChangedEvent, - added, - removed); - RaiseEvent(changed); - } + if (SelectedItems.Count > 0) + { + SelectedItemsAdded(SelectedItems); + + added = SelectedItems; + } + else if (!_syncingSelectedItems) + { + SelectedItem = null; + } + + break; + case NotifyCollectionChangedAction.Replace: + + foreach (var item in e.OldItems) + { + MarkItemSelected(item, false); + } + + foreach (var item in e.NewItems) + { + MarkItemSelected(item, true); + } + + if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems) + { + var oldItem = SelectedItem; + var item = SelectedItems[0]; + _selectedItem = item; + RaisePropertyChanged(SelectedItemProperty, oldItem, item); + } + + added = e.NewItems; + removed = e.OldItems; + + break; } + + if (added?.Count > 0 || removed?.Count > 0) + { + var changed = new SelectionChangedEventArgs( + SelectingItemsControl.SelectionChangedEvent, + added ?? Empty, + removed ?? Empty); + RaiseEvent(changed); + } + } + + private void MarkItemSelected(object item, bool selected) + { + var container = ItemContainerGenerator.Index.ContainerFromItem(item); + + MarkContainerSelected(container, selected); } - (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, NavigationDirection direction) + private void SelectedItemsAdded(IList items) + { + if (items.Count == 0) + { + return; + } + + foreach (object item in items) + { + MarkItemSelected(item, true); + } + + if (SelectedItem == null && !_syncingSelectedItems) + { + SetAndRaise(SelectedItemProperty, ref _selectedItem, items[0]); + } + } + + /// + /// Unsubscribes from the CollectionChanged event, if any. + /// + private void UnsubscribeFromSelectedItems() + { + if (_selectedItems is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= SelectedItemsCollectionChanged; + } + } + + (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, + NavigationDirection direction) { if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) { @@ -142,10 +345,8 @@ namespace Avalonia.Controls ItemContainerGenerator.ContainerFromIndex(0); return (true, result); } - else - { - return (true, null); - } + + return (true, null); } return (false, null); @@ -186,7 +387,7 @@ namespace Avalonia.Controls if (SelectedItem != null) { var next = GetContainerInDirection( - GetContainerFromEventSource(e.Source) as TreeViewItem, + GetContainerFromEventSource(e.Source), direction.Value, true); @@ -208,17 +409,9 @@ namespace Avalonia.Controls NavigationDirection direction, bool intoChildren) { - IItemContainerGenerator parentGenerator; + IItemContainerGenerator parentGenerator = GetParentContainerGenerator(from); - if (from?.Parent is TreeView treeView) - { - parentGenerator = treeView.ItemContainerGenerator; - } - else if (from?.Parent is TreeViewItem item) - { - parentGenerator = item.ItemContainerGenerator; - } - else + if (parentGenerator == null) { return null; } @@ -257,6 +450,7 @@ namespace Avalonia.Controls { return GetContainerInDirection(parentItem, direction, false); } + break; } @@ -293,20 +487,184 @@ namespace Avalonia.Controls { var item = ItemContainerGenerator.Index.ItemFromContainer(container); - if (item != null) + if (item == null) { - if (SelectedItem != null) + return; + } + + IControl selectedContainer = null; + + if (SelectedItem != null) + { + selectedContainer = ItemContainerGenerator.Index.ContainerFromItem(SelectedItem); + } + + var mode = SelectionMode; + var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0; + var multi = (mode & SelectionMode.Multiple) != 0; + var range = multi && selectedContainer != null && rangeModifier; + + if (!toggle && !range) + { + SelectSingleItem(item); + } + else if (multi && range) + { + SelectingItemsControl.SynchronizeItems( + SelectedItems, + GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem)); + } + else + { + var i = SelectedItems.IndexOf(item); + + if (i != -1) { - var old = ItemContainerGenerator.Index.ContainerFromItem(SelectedItem); - MarkContainerSelected(old, false); + SelectedItems.Remove(item); } + else + { + if (multi) + { + SelectedItems.Add(item); + } + else + { + SelectedItem = item; + } + } + } + } - SelectedItem = item; + private static IItemContainerGenerator GetParentContainerGenerator(TreeViewItem item) + { + if (item == null) + { + return null; + } - MarkContainerSelected(container, true); + switch (item.Parent) + { + case TreeView treeView: + return treeView.ItemContainerGenerator; + case TreeViewItem treeViewItem: + return treeViewItem.ItemContainerGenerator; + default: + return null; } } + /// + /// Find which node is first in hierarchy. + /// + /// Search root. + /// Nodes to find. + /// Node to find. + /// Found first node. + private static TreeViewItem FindFirstNode(TreeView treeView, TreeViewItem nodeA, TreeViewItem nodeB) + { + return FindInContainers(treeView.ItemContainerGenerator, nodeA, nodeB); + } + + private static TreeViewItem FindInContainers(ITreeItemContainerGenerator containerGenerator, + TreeViewItem nodeA, + TreeViewItem nodeB) + { + IEnumerable containers = containerGenerator.Containers; + + foreach (ItemContainerInfo container in containers) + { + TreeViewItem node = FindFirstNode(container.ContainerControl as TreeViewItem, nodeA, nodeB); + + if (node != null) + { + return node; + } + } + + return null; + } + + private static TreeViewItem FindFirstNode(TreeViewItem node, TreeViewItem nodeA, TreeViewItem nodeB) + { + if (node == null) + { + return null; + } + + TreeViewItem match = node == nodeA ? nodeA : node == nodeB ? nodeB : null; + + if (match != null) + { + return match; + } + + return FindInContainers(node.ItemContainerGenerator, nodeA, nodeB); + } + + /// + /// Returns all items that belong to containers between and . + /// The range is inclusive. + /// + /// From container. + /// To container. + private List GetItemsInRange(TreeViewItem from, TreeViewItem to) + { + var items = new List(); + + if (from == null || to == null) + { + return items; + } + + TreeViewItem firstItem = FindFirstNode(this, from, to); + + if (firstItem == null) + { + return items; + } + + bool wasReversed = false; + + if (firstItem == to) + { + var temp = from; + + from = to; + to = temp; + + wasReversed = true; + } + + TreeViewItem node = from; + + while (node != to) + { + var item = ItemContainerGenerator.Index.ItemFromContainer(node); + + if (item != null) + { + items.Add(item); + } + + node = GetContainerInDirection(node, NavigationDirection.Down, true); + } + + var toItem = ItemContainerGenerator.Index.ItemFromContainer(to); + + if (toItem != null) + { + items.Add(toItem); + } + + if (wasReversed) + { + items.Reverse(); + } + + return items; + } + /// /// Updates the selection based on an event that may have originated in a container that /// belongs to the control. @@ -341,7 +699,7 @@ namespace Avalonia.Controls /// /// The control that raised the event. /// The container or null if the event did not originate in a container. - protected IControl GetContainerFromEventSource(IInteractive eventSource) + protected TreeViewItem GetContainerFromEventSource(IInteractive eventSource) { var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() .OfType() @@ -349,7 +707,7 @@ namespace Avalonia.Controls if (item != null) { - if (item.ItemContainerGenerator.Index == this.ItemContainerGenerator.Index) + if (item.ItemContainerGenerator.Index == ItemContainerGenerator.Index) { return item; } @@ -367,21 +725,23 @@ namespace Avalonia.Controls { var selectedItem = SelectedItem; - if (selectedItem != null) + if (selectedItem == null) { - foreach (var container in e.Containers) - { - if (container.Item == selectedItem) - { - ((TreeViewItem)container.ContainerControl).IsSelected = true; + return; + } - if (AutoScrollToSelectedItem) - { - Dispatcher.UIThread.Post(container.ContainerControl.BringIntoView); - } + foreach (var container in e.Containers) + { + if (container.Item == selectedItem) + { + ((TreeViewItem)container.ContainerControl).IsSelected = true; - break; + if (AutoScrollToSelectedItem) + { + Dispatcher.UIThread.Post(container.ContainerControl.BringIntoView); } + + break; } } } @@ -393,18 +753,18 @@ namespace Avalonia.Controls /// Whether the control is selected private void MarkContainerSelected(IControl container, bool selected) { - if (container != null) + if (container == null) { - var selectable = container as ISelectable; + return; + } - if (selectable != null) - { - selectable.IsSelected = selected; - } - else - { - ((IPseudoClasses)container.Classes).Set(":selected", selected); - } + if (container is ISelectable selectable) + { + selectable.IsSelected = selected; + } + else + { + container.Classes.Set(":selected", selected); } } } diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index 8fee31408f..a7d4b96974 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -35,13 +35,13 @@ namespace Avalonia.DesignerSupport var localAsm = assemblyPath != null ? Assembly.LoadFile(Path.GetFullPath(assemblyPath)) : null; var loaded = loader.Load(stream, localAsm, null, baseUri); - var styles = loaded as Styles; - if (styles != null) + var style = loaded as IStyle; + if (style != null) { - var substitute = styles.OfType \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/Separator.xaml b/src/Avalonia.Themes.Default/Separator.xaml index 6312a14df5..cf0db16ee6 100644 --- a/src/Avalonia.Themes.Default/Separator.xaml +++ b/src/Avalonia.Themes.Default/Separator.xaml @@ -11,13 +11,7 @@ - - -