From 3d100923fc688519a9ee264f1cee5280f6c3dc2a Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 22 Oct 2019 19:26:08 +0300 Subject: [PATCH 01/13] add some configurable build output to xamlil build tasks --- packages/Avalonia/AvaloniaBuildTasks.targets | 5 ++++- .../CompileAvaloniaXamlTask.cs | 11 ++++++++-- src/Avalonia.Build.Tasks/Extensions.cs | 9 ++++++-- .../GenerateAvaloniaResourcesTask.cs | 21 ++++++++++++++++--- .../XamlCompilerTaskExecutor.cs | 4 +++- 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 552713f94b..578fadff97 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -38,7 +38,8 @@ Output="$(AvaloniaResourcesTemporaryFilePath)" Root="$(MSBuildProjectDirectory)" Resources="@(AvaloniaResource)" - EmbeddedResources="@(EmbeddedResources)"/> + EmbeddedResources="@(EmbeddedResources)" + ReportImportance="$(AvaloniaXamlReportImportance)"/> @@ -54,6 +55,7 @@ $(IntermediateOutputPath)/Avalonia/references $(IntermediateOutputPath)/Avalonia/original.dll false + low AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}"; + BuildEngine.LogMessage(msg, outputImportance < MessageImportance.Low ? MessageImportance.High : outputImportance); + var res = XamlCompilerTaskExecutor.Compile(BuildEngine, input, File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(), - ProjectDirectory, OutputPath, VerifyIl); + ProjectDirectory, OutputPath, VerifyIl, outputImportance); if (!res.Success) return false; if (!res.WrittenFile) @@ -68,7 +73,9 @@ namespace Avalonia.Build.Tasks public string OutputPath { get; set; } public bool VerifyIl { get; set; } - + + public string ReportImportance { get; set; } + public IBuildEngine BuildEngine { get; set; } public ITaskHost HostObject { get; set; } } diff --git a/src/Avalonia.Build.Tasks/Extensions.cs b/src/Avalonia.Build.Tasks/Extensions.cs index 440c6d7489..46c12eaf3d 100644 --- a/src/Avalonia.Build.Tasks/Extensions.cs +++ b/src/Avalonia.Build.Tasks/Extensions.cs @@ -9,14 +9,19 @@ namespace Avalonia.Build.Tasks public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message) { - engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, + engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, "", "Avalonia")); } - + public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message) { engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, "", "Avalonia")); } + + public static void LogMessage(this IBuildEngine engine, string message, MessageImportance imp) + { + engine.LogMessageEvent(new BuildMessageEventArgs(message, "", "Avalonia", imp)); + } } } diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index 98ebb3e7d1..493d5ecc1b 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -22,6 +22,10 @@ namespace Avalonia.Build.Tasks [Required] public ITaskItem[] EmbeddedResources { get; set; } + public string ReportImportance { get; set; } + + private MessageImportance _reportImportance; + class Source { public string Path { get; set; } @@ -65,7 +69,14 @@ namespace Avalonia.Build.Tasks } } - List BuildResourceSources() => Resources.Select(r => new Source(r.ItemSpec, Root)).ToList(); + List BuildResourceSources() + => Resources.Select(r => + { + + var src = new Source(r.ItemSpec, Root); + BuildEngine.LogMessage($"avares -> name:{src.Path}, path: {src.SystemPath}, size:{src.Size}, ItemSpec:{r.ItemSpec}", _reportImportance); + return src; + }).ToList(); private void Pack(Stream output, List sources) { @@ -136,10 +147,14 @@ namespace Avalonia.Build.Tasks sources.Add(new Source("/!AvaloniaResourceXamlInfo", ms.ToArray())); return true; } - + public bool Execute() { - foreach(var r in EmbeddedResources.Where(r=>r.ItemSpec.EndsWith(".xaml")||r.ItemSpec.EndsWith(".paml"))) + Enum.TryParse(ReportImportance, out _reportImportance); + + BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance); + + foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml"))) BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec, "XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work"); var resources = BuildResourceSources(); diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index e348eb0fbc..3b69109e68 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -40,7 +40,7 @@ namespace Avalonia.Build.Tasks } public static CompileResult Compile(IBuildEngine engine, string input, string[] references, string projectDirectory, - string output, bool verifyIl) + string output, bool verifyIl, MessageImportance logImportance) { var typeSystem = new CecilTypeSystem(references.Concat(new[] {input}), input); var asm = typeSystem.TargetAssemblyDefinition; @@ -121,6 +121,8 @@ namespace Avalonia.Build.Tasks { try { + engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance); + // StreamReader is needed here to handle BOM var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd(); var parsed = XDocumentXamlIlParser.Parse(xaml); From 22ae93ed952fead90b50cd92f8782ab5b43809a3 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Wed, 23 Oct 2019 16:26:09 +0300 Subject: [PATCH 02/13] fix a problem in xamlil resource path generation, sometimes resource path look like avares://AssemblyName.dll/../../../ResourceName.xaml --- .../GenerateAvaloniaResourcesTask.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index 493d5ecc1b..406abe6f99 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -33,15 +33,11 @@ namespace Avalonia.Build.Tasks private byte[] _data; private string _sourcePath; - public Source(string file, string root) + public Source(string relativePath, string root) { - file = SPath.GetFullPath(file); root = SPath.GetFullPath(root); - var fileUri = new Uri(file, UriKind.Absolute); - var rootUri = new Uri(root, UriKind.Absolute); - rootUri = new Uri(rootUri.ToString().TrimEnd('/') + '/'); - Path = '/' + rootUri.MakeRelativeUri(fileUri).ToString().TrimStart('/'); - _sourcePath = file; + Path = "/" + relativePath.Replace('\\', '/'); + _sourcePath = SPath.Combine(root, relativePath); Size = (int)new FileInfo(_sourcePath).Length; } From 89e7665e8ec617743a23421548461aa260e813a5 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Wed, 23 Oct 2019 16:52:09 +0300 Subject: [PATCH 03/13] move AvaloniaXamlReportImportance to more visible place --- packages/Avalonia/AvaloniaBuildTasks.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 578fadff97..537495fcad 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -2,6 +2,7 @@ <_AvaloniaUseExternalMSBuild>$(AvaloniaUseExternalMSBuild) <_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false + low $(IntermediateOutputPath)/Avalonia/references $(IntermediateOutputPath)/Avalonia/original.dll false - low Date: Thu, 30 Apr 2020 17:39:19 +0200 Subject: [PATCH 04/13] Use OnApplyTemplate instead of OnTemplateApplied. - More similar to WPF API - Less bug-prone (see #3744) `OnTemplateApplied` still exists, but is deprecated and does nothing. --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 2 +- src/Avalonia.Controls.DataGrid/DataGridCell.cs | 4 +--- src/Avalonia.Controls.DataGrid/DataGridRow.cs | 4 +--- .../DataGridRowGroupHeader.cs | 4 +--- src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs | 4 +--- src/Avalonia.Controls/AutoCompleteBox.cs | 4 ++-- src/Avalonia.Controls/ButtonSpinner.cs | 2 +- src/Avalonia.Controls/Calendar/Calendar.cs | 4 +--- src/Avalonia.Controls/Calendar/CalendarButton.cs | 3 +-- src/Avalonia.Controls/Calendar/CalendarDayButton.cs | 4 ++-- src/Avalonia.Controls/Calendar/CalendarItem.cs | 4 +--- src/Avalonia.Controls/Calendar/DatePicker.cs | 4 +--- src/Avalonia.Controls/ComboBox.cs | 4 +--- src/Avalonia.Controls/ListBox.cs | 3 +-- src/Avalonia.Controls/MenuItem.cs | 4 +--- .../Notifications/WindowNotificationManager.cs | 4 +--- src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs | 2 +- src/Avalonia.Controls/Primitives/ScrollBar.cs | 4 +--- src/Avalonia.Controls/Primitives/TemplatedControl.cs | 10 ++++++++-- src/Avalonia.Controls/ProgressBar.cs | 2 +- src/Avalonia.Controls/Slider.cs | 2 +- src/Avalonia.Controls/TabControl.cs | 4 +--- src/Avalonia.Controls/TextBox.cs | 2 +- src/Avalonia.Controls/TreeViewItem.cs | 3 +-- .../Primitives/PopupRootTests.cs | 2 +- .../TestTemplatedControl.cs | 3 +-- 26 files changed, 35 insertions(+), 57 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 844316741a..3a1e612a05 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -2245,7 +2245,7 @@ namespace Avalonia.Controls /// Builds the visual tree for the column header when a new template is applied. /// //TODO Validation UI - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { // The template has changed, so we need to refresh the visuals _measured = false; diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index e56c534f50..76b064c328 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -121,10 +121,8 @@ namespace Avalonia.Controls /// /// Builds the visual tree for the cell control when a new template is applied. /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(e); - UpdatePseudoClasses(); _rightGridLine = e.NameScope.Find(DATAGRIDCELL_elementRightGridLine); if (_rightGridLine != null && OwningColumn == null) diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index c9924660be..0c801a5b11 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -536,10 +536,8 @@ namespace Avalonia.Controls /// /// Builds the visual tree for the column header when a new template is applied. /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(e); - RootElement = e.NameScope.Find(DATAGRIDROW_elementRoot); if (RootElement != null) { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 69dfed761f..f6628b47d8 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -168,7 +168,7 @@ namespace Avalonia.Controls private IDisposable _expanderButtonSubscription; - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _rootElement = e.NameScope.Find(DataGridRow.DATAGRIDROW_elementRoot); @@ -199,8 +199,6 @@ namespace Avalonia.Controls _itemCountElement = e.NameScope.Find(DATAGRIDROWGROUPHEADER_itemCountElement); _propertyNameElement = e.NameScope.Find(DATAGRIDROWGROUPHEADER_propertyNameElement); UpdateTitleElements(); - - base.OnTemplateApplied(e); } internal void ApplyHeaderStatus() diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index 5bfe449b63..ef88e4a946 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -94,10 +94,8 @@ namespace Avalonia.Controls.Primitives /// /// Builds the visual tree for the row header when a new template is applied. /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(e); - _rootElement = e.NameScope.Find(DATAGRIDROWHEADER_elementRootName); if (_rootElement != null) { diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 9bc7ba9e2f..3e4f47ec8a 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1212,7 +1212,7 @@ namespace Avalonia.Controls /// control /// when a new template is applied. /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { if (DropDownPopup != null) @@ -1240,7 +1240,7 @@ namespace Avalonia.Controls OpeningDropDown(false); } - base.OnTemplateApplied(e); + base.OnApplyTemplate(e); } /// diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 2ac9319478..7945d63b06 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -121,7 +121,7 @@ namespace Avalonia.Controls } /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { IncreaseButton = e.NameScope.Find - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(e); - Root = e.NameScope.Find(PART_ElementRoot); SelectedMonth = DisplayDate; diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index a273e68d56..35c082634f 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -98,9 +98,8 @@ namespace Avalonia.Controls.Primitives /// /// when a new template is applied. /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(e); SetPseudoClasses(); } diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index e62a1ce1f4..91213acfb3 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -150,11 +150,11 @@ namespace Avalonia.Controls.Primitives } } - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(e); SetPseudoClasses(); } + private void SetPseudoClasses() { if (_ignoringMouseOverState) diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 5a2d1bbfd5..ece0ef97d9 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -268,10 +268,8 @@ namespace Avalonia.Controls.Primitives /// /// when a new template is applied. /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(e); - HeaderButton = e.NameScope.Find - diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs index 5893796b8b..3fb990459f 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs @@ -1,9 +1,6 @@ -using System.Collections.ObjectModel; -using System.Linq; -using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using ReactiveUI; +using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { @@ -12,105 +9,12 @@ namespace ControlCatalog.Pages public TreeViewPage() { InitializeComponent(); - DataContext = new PageViewModel(); + DataContext = new TreeViewPageViewModel(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - - private class PageViewModel : ReactiveObject - { - private SelectionMode _selectionMode; - - public PageViewModel() - { - Node root = new Node(); - Items = root.Children; - Selection = new SelectionModel(); - - AddItemCommand = ReactiveCommand.Create(() => - { - Node parentItem = Selection.SelectedItems.Count > 0 ? - (Node)Selection.SelectedItems[0] : root; - parentItem.AddNewItem(); - }); - - RemoveItemCommand = ReactiveCommand.Create(() => - { - while (Selection.SelectedItems.Count > 0) - { - Node lastItem = (Node)Selection.SelectedItems[0]; - RecursiveRemove(Items, lastItem); - Selection.DeselectAt(Selection.SelectedIndices[0]); - } - - bool RecursiveRemove(ObservableCollection items, Node selectedItem) - { - if (items.Remove(selectedItem)) - { - return true; - } - - foreach (Node item in items) - { - if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) - { - return true; - } - } - - return false; - } - }); - } - - public ObservableCollection Items { get; } - - public SelectionModel Selection { get; } - - public ReactiveCommand AddItemCommand { get; } - - public ReactiveCommand RemoveItemCommand { get; } - - public SelectionMode SelectionMode - { - get => _selectionMode; - set - { - Selection.ClearSelection(); - this.RaiseAndSetIfChanged(ref _selectionMode, value); - } - } - } - - private class Node - { - private int _counter; - private ObservableCollection _children; - - public string Header { get; private set; } - - public bool AreChildrenInitialized => _children != null; - - public ObservableCollection Children - { - get - { - if (_children == null) - { - _children = new ObservableCollection(Enumerable.Range(1, 10).Select(i => CreateNewNode())); - } - return _children; - } - } - - public void AddNewItem() => Children.Add(CreateNewNode()); - - public override string ToString() => Header; - - private Node CreateNewNode() => new Node { Header = $"Item {_counter++}" }; - } } } diff --git a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs new file mode 100644 index 0000000000..d396ef2b3d --- /dev/null +++ b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using Avalonia.Controls; +using ReactiveUI; + +namespace ControlCatalog.ViewModels +{ + public class TreeViewPageViewModel : ReactiveObject + { + private readonly Node _root; + private SelectionMode _selectionMode; + + public TreeViewPageViewModel() + { + _root = new Node(); + + Items = _root.Children; + Selection = new SelectionModel(); + Selection.SelectionChanged += SelectionChanged; + + AddItemCommand = ReactiveCommand.Create(AddItem); + RemoveItemCommand = ReactiveCommand.Create(RemoveItem); + } + + public ObservableCollection Items { get; } + public SelectionModel Selection { get; } + public ReactiveCommand AddItemCommand { get; } + public ReactiveCommand RemoveItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + Selection.ClearSelection(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + + private void AddItem() + { + var parentItem = Selection.SelectedItems.Count > 0 ? (Node)Selection.SelectedItems[0] : _root; + parentItem.AddItem(); + } + + private void RemoveItem() + { + while (Selection.SelectedItems.Count > 0) + { + Node lastItem = (Node)Selection.SelectedItems[0]; + RecursiveRemove(Items, lastItem); + Selection.DeselectAt(Selection.SelectedIndices[0]); + } + + bool RecursiveRemove(ObservableCollection items, Node selectedItem) + { + if (items.Remove(selectedItem)) + { + return true; + } + + foreach (Node item in items) + { + if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) + { + return true; + } + } + + return false; + } + } + + private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) + { + var selected = string.Join(",", e.SelectedIndices); + var deselected = string.Join(",", e.DeselectedIndices); + System.Diagnostics.Debug.WriteLine($"Selected '{selected}', Deselected '{deselected}'"); + } + + public class Node + { + private ObservableCollection _children; + private int _childIndex = 10; + + public Node() + { + Header = "Item"; + } + + public Node(Node parent, int index) + { + Parent = parent; + Header = parent.Header + ' ' + index; + } + + public Node Parent { get; } + public string Header { get; } + public bool AreChildrenInitialized => _children != null; + public ObservableCollection Children => _children ??= CreateChildren(); + public void AddItem() => Children.Add(new Node(this, _childIndex++)); + public void RemoveItem(Node child) => Children.Remove(child); + public override string ToString() => Header; + + private ObservableCollection CreateChildren() + { + return new ObservableCollection( + Enumerable.Range(0, 10).Select(i => new Node(this, i))); + } + } + } +} From 8230684a415552334a69d8a0e53b03c5853a247e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 11 May 2020 22:08:23 +0200 Subject: [PATCH 07/13] Added failing test for #3918. --- .../SelectionModelTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index c4a682cc54..5d5669189e 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1696,6 +1696,37 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, raised); } + [Fact] + public void Batch_Update_Clear_Nested_Data_Raises_SelectionChanged() + { + var target = new SelectionModel(); + var raised = 0; + + target.Source = CreateNestedData(3, 2, 2); + target.SelectRange(new IndexPath(0), new IndexPath(1, 1)); + + Assert.Equal(24, target.SelectedIndices.Count); + + var indices = target.SelectedIndices.ToList(); + var items = target.SelectedItems.ToList(); + + target.SelectionChanged += (s, e) => + { + Assert.Equal(indices, e.DeselectedIndices); + Assert.Equal(items, e.DeselectedItems); + Assert.Empty(e.SelectedIndices); + Assert.Empty(e.SelectedItems); + ++raised; + }; + + using (target.Update()) + { + target.ClearSelection(); + } + + Assert.Equal(1, raised); + } + [Fact] public void AutoSelect_Selects_When_Enabled() { From 1c5d0485436a4097f7bd5e8f385db5c4d0767a4b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 11 May 2020 22:26:29 +0200 Subject: [PATCH 08/13] Only do a node cleanup when all operations have finished. Fixes #3918. --- src/Avalonia.Controls/SelectionModel.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index d930edc529..e8958438f1 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -808,7 +808,11 @@ namespace Avalonia.Controls } OnSelectionChanged(e); - _rootNode.Cleanup(); + + if (_operationCount == 0) + { + _rootNode.Cleanup(); + } } private void ApplyAutoSelect() From d5f0110ca6a8e7a15308811ef3e314db3743a42e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 12 May 2020 12:57:58 +0200 Subject: [PATCH 09/13] Raise SelectionChanged when nested children change. --- src/Avalonia.Controls/SelectionNode.cs | 22 ++- .../SelectionModelTests.cs | 148 +++++++++++------- 2 files changed, 115 insertions(+), 55 deletions(-) diff --git a/src/Avalonia.Controls/SelectionNode.cs b/src/Avalonia.Controls/SelectionNode.cs index e25f88ff29..2fa7c5f697 100644 --- a/src/Avalonia.Controls/SelectionNode.cs +++ b/src/Avalonia.Controls/SelectionNode.cs @@ -8,6 +8,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using Avalonia.Controls.Utils; #nullable enable @@ -214,7 +215,21 @@ namespace Avalonia.Controls public void SetChildrenObservable(IObservable resolver) { - _childrenSubscription = resolver.Subscribe(x => Source = x); + _childrenSubscription = resolver.Subscribe(x => + { + if (Source != null) + { + using (_manager.Update()) + { + SelectionTreeHelper.Traverse( + this, + realizeChildren: false, + info => info.Node.Clear()); + } + } + + Source = x; + }); } public int SelectedCount { get; private set; } @@ -544,11 +559,14 @@ namespace Avalonia.Controls private void ClearChildNodes() { - foreach (var child in _childrenNodes) + for (int i = 0; i < _childrenNodes.Count; i++) { + var child = _childrenNodes[i]; + if (child != null && child != _manager.SharedLeafNode) { child.Dispose(); + _childrenNodes[i] = null; } } diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index 5d5669189e..337c0e6c5f 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1929,19 +1929,61 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Can_Replace_Children_Collection() + public void Can_Replace_Parent_Children_Collection() { var root = new Node("Root"); var target = new SelectionModel { Source = new[] { root } }; + var raised = 0; + target.ChildrenRequested += (s, e) => e.Children = ((Node)e.Source).WhenAnyValue(x => x.Children); target.Select(0, 9); - Assert.Equal("Child 9", ((Node)target.SelectedItem).Header); + var selected = (Node)target.SelectedItem; + Assert.Equal("Child 9", selected.Header); + + target.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { Path(0, 9) }, e.DeselectedIndices); + Assert.Equal(new[] { selected }, e.DeselectedItems); + Assert.Empty(e.SelectedIndices); + Assert.Empty(e.SelectedItems); + ++raised; + }; + + root.ReplaceChildren(); + + Assert.Null(target.SelectedItem); + Assert.Equal(1, raised); + } + + [Fact] + public void Can_Replace_Grandparent_Children_Collection() + { + var root = new Node("Root"); + var target = new SelectionModel { Source = new[] { root } }; + var raised = 0; + + target.ChildrenRequested += (s, e) => e.Children = ((Node)e.Source).WhenAnyValue(x => x.Children); + + target.SelectAt(Path(0, 9, 1)); + + var selected = (Node)target.SelectedItem; + Assert.Equal("Child 1", selected.Header); + + target.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { Path(0, 9, 1) }, e.DeselectedIndices); + Assert.Equal(new[] { selected }, e.DeselectedItems); + Assert.Empty(e.SelectedIndices); + Assert.Empty(e.SelectedItems); + ++raised; + }; root.ReplaceChildren(); Assert.Null(target.SelectedItem); + Assert.Equal(1, raised); } [Fact] @@ -1979,57 +2021,6 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, node.PropertyChangedSubscriptions); } - private class Node : INotifyPropertyChanged - { - private ObservableCollection _children; - private PropertyChangedEventHandler _propertyChanged; - - public Node(string header) - { - Header = header; - } - - public string Header { get; } - - public ObservableCollection Children - { - get => _children ??= CreateChildren(10); - private set - { - _children = value; - _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Children))); - } - } - - public event PropertyChangedEventHandler PropertyChanged - { - add - { - _propertyChanged += value; - ++PropertyChangedSubscriptions; - } - - remove - { - _propertyChanged -= value; - --PropertyChangedSubscriptions; - } - } - - public int PropertyChangedSubscriptions { get; private set; } - - public void ReplaceChildren() - { - Children = CreateChildren(5); - } - - private ObservableCollection CreateChildren(int count) - { - return new ObservableCollection( - Enumerable.Range(0, count).Select(x => new Node("Child " + x))); - } - } - private int GetSubscriberCount(AvaloniaList list) { return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0; @@ -2396,6 +2387,57 @@ namespace Avalonia.Controls.UnitTests new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } + + private class Node : INotifyPropertyChanged + { + private ObservableCollection _children; + private PropertyChangedEventHandler _propertyChanged; + + public Node(string header) + { + Header = header; + } + + public string Header { get; } + + public ObservableCollection Children + { + get => _children ??= CreateChildren(10); + private set + { + _children = value; + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Children))); + } + } + + public event PropertyChangedEventHandler PropertyChanged + { + add + { + _propertyChanged += value; + ++PropertyChangedSubscriptions; + } + + remove + { + _propertyChanged -= value; + --PropertyChangedSubscriptions; + } + } + + public int PropertyChangedSubscriptions { get; private set; } + + public void ReplaceChildren() + { + Children = CreateChildren(5); + } + + private ObservableCollection CreateChildren(int count) + { + return new ObservableCollection( + Enumerable.Range(0, count).Select(x => new Node("Child " + x))); + } + } } class CustomSelectionModel : SelectionModel From 2b5d7fb28d88aa2c4399eb55a71a41ac74fd63d8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 12 May 2020 13:34:03 +0200 Subject: [PATCH 10/13] Don't allow selection of non-expanded TreeViewItems. And remove child selection when a `TreeViewItem` is un-expanded. This is necessary because we don't get enough information about a materialized `TreeViewItem` to select it when materialized, so the `SelectionModel` and `TreeViewItem` selection state gets out of sync. --- src/Avalonia.Controls/TreeView.cs | 10 +++++++++- tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 13 +++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 165276c539..a55274afb3 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Reactive.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; @@ -395,7 +396,14 @@ namespace Avalonia.Controls private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e) { var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as ItemsControl; - e.Children = container?.GetObservable(ItemsProperty); + + if (container is object) + { + e.Children = Observable.CombineLatest( + container.GetObservable(TreeViewItem.IsExpandedProperty), + container.GetObservable(ItemsProperty), + (expanded, items) => expanded ? items : null); + } } private TreeViewItem GetContainerInDirection( diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 1563df5aae..373f3e6861 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -130,6 +130,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var item = tree[0].Children[1].Children[0]; var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); @@ -157,6 +158,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var item = tree[0].Children[1].Children[0]; var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); @@ -188,6 +190,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var item1 = tree[0].Children[1].Children[0]; var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); @@ -225,6 +228,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; @@ -264,6 +268,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; @@ -297,6 +302,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; @@ -330,6 +336,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; @@ -376,6 +383,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var item = tree[0].Children[1].Children[0]; var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); @@ -402,6 +410,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var item = tree[0].Children[1].Children[0]; @@ -579,6 +588,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var item = data[0].Children[0]; var node = target.ItemContainerGenerator.Index.ContainerFromItem(item); @@ -614,6 +624,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; @@ -651,6 +662,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; @@ -697,6 +709,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; From fcdac73e7574eff48fda91d03c54efde5102152f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 12 May 2020 17:29:53 +0200 Subject: [PATCH 11/13] Fix select range logic. Make sure the item we're selecting is within the requested range. Also refactored the unit tests to do a simple test on `SelectedIndices` instead of using `IsSelectedWithPartial` because both can't really be tested together using the old testing method. --- src/Avalonia.Controls/SelectionModel.cs | 5 +- .../SelectionModelTests.cs | 610 ++++-------------- 2 files changed, 135 insertions(+), 480 deletions(-) diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index e8958438f1..dd4934f9e5 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -774,7 +774,10 @@ namespace Avalonia.Controls winrtEnd, info => { - info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select); + if (info.Path >= winrtStart && info.Path <= winrtEnd) + { + info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select); + } }); } diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index 337c0e6c5f..cea41526ad 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -34,9 +34,9 @@ namespace Avalonia.Controls.UnitTests SelectionModel selectionModel = new SelectionModel() { SingleSelect = true }; _output.WriteLine("No source set."); Select(selectionModel, 4, true); - ValidateSelection(selectionModel, new List() { Path(4) }); + ValidateSelection(selectionModel, Path(4)); Select(selectionModel, 4, false); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); } [Fact] @@ -46,9 +46,9 @@ namespace Avalonia.Controls.UnitTests _output.WriteLine("Set the source to 10 items"); selectionModel.Source = Enumerable.Range(0, 10).ToList(); Select(selectionModel, 3, true); - ValidateSelection(selectionModel, new List() { Path(3) }, new List() { Path() }); + ValidateSelection(selectionModel, Path(3)); Select(selectionModel, 3, false); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); } [Fact] @@ -61,11 +61,11 @@ namespace Avalonia.Controls.UnitTests selectionModel.SelectionChanged += delegate (object sender, SelectionModelSelectionChangedEventArgs args) { selectionChangedFiredCount++; - ValidateSelection(selectionModel, new List() { Path(4) }, new List() { Path() }); + ValidateSelection(selectionModel, Path(4)); }; Select(selectionModel, 4, true); - ValidateSelection(selectionModel, new List() { Path(4) }, new List() { Path() }); + ValidateSelection(selectionModel, Path(4)); Assert.Equal(1, selectionChangedFiredCount); } @@ -85,41 +85,29 @@ namespace Avalonia.Controls.UnitTests selectionModel.Source = Enumerable.Range(0, 10).ToList(); Select(selectionModel, 4, true); - ValidateSelection(selectionModel, new List() { Path(4) }, new List() { Path() }); + ValidateSelection(selectionModel, Path(4)); SelectRangeFromAnchor(selectionModel, 8, true /* select */); ValidateSelection(selectionModel, - new List() - { - Path(4), - Path(5), - Path(6), - Path(7), - Path(8) - }, - new List() { Path() }); + Path(4), + Path(5), + Path(6), + Path(7), + Path(8)); ClearSelection(selectionModel); SetAnchorIndex(selectionModel, 6); SelectRangeFromAnchor(selectionModel, 3, true /* select */); ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(4), - Path(5), - Path(6) - }, - new List() { Path() }); + Path(3), + Path(4), + Path(5), + Path(6)); SetAnchorIndex(selectionModel, 4); SelectRangeFromAnchor(selectionModel, 5, false /* select */); ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(6) - }, - new List() { Path() }); + Path(3), + Path(6)); } [Fact] @@ -129,10 +117,9 @@ namespace Avalonia.Controls.UnitTests _output.WriteLine("Setting the source"); selectionModel.Source = CreateNestedData(1 /* levels */ , 2 /* groupsAtLevel */, 2 /* countAtLeaf */); Select(selectionModel, 1, 1, true); - ValidateSelection(selectionModel, - new List() { Path(1, 1) }, new List() { Path(), Path(1) }); + ValidateSelection(selectionModel, Path(1, 1)); Select(selectionModel, 1, 1, false); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); } [Fact] @@ -143,68 +130,36 @@ namespace Avalonia.Controls.UnitTests selectionModel.Source = CreateNestedData(1 /* levels */ , 3 /* groupsAtLevel */, 3 /* countAtLeaf */); Select(selectionModel, 1, 2, true); - ValidateSelection(selectionModel, new List() { Path(1, 2) }, new List() { Path(), Path(1) }); + ValidateSelection(selectionModel, Path(1, 2)); SelectRangeFromAnchor(selectionModel, 2, 2, true /* select */); ValidateSelection(selectionModel, - new List() - { - Path(1, 2), - Path(2), // Inner node should be selected since everything 2.* is selected - Path(2, 0), - Path(2, 1), - Path(2, 2) - }, - new List() - { - Path(), - Path(1) - }, - 1 /* selectedInnerNodes */); + Path(1, 2), + Path(2, 0), + Path(2, 1), + Path(2, 2)); ClearSelection(selectionModel); SetAnchorIndex(selectionModel, 2, 1); SelectRangeFromAnchor(selectionModel, 0, 1, true /* select */); ValidateSelection(selectionModel, - new List() - { - Path(0, 1), - Path(0, 2), - Path(1, 0), - Path(1, 1), - Path(1, 2), - Path(1), - Path(2, 0), - Path(2, 1) - }, - new List() - { - Path(), - Path(0), - Path(2), - }, - 1 /* selectedInnerNodes */); + Path(0, 1), + Path(0, 2), + Path(1, 0), + Path(1, 1), + Path(1, 2), + Path(2, 0), + Path(2, 1)); SetAnchorIndex(selectionModel, 1, 1); SelectRangeFromAnchor(selectionModel, 2, 0, false /* select */); ValidateSelection(selectionModel, - new List() - { - Path(0, 1), - Path(0, 2), - Path(1, 0), - Path(2, 1) - }, - new List() - { - Path(), - Path(1), - Path(0), - Path(2), - }, - 0 /* selectedInnerNodes */); + Path(0, 1), + Path(0, 2), + Path(1, 0), + Path(2, 1)); ClearSelection(selectionModel); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); } [Fact] @@ -215,30 +170,11 @@ namespace Avalonia.Controls.UnitTests selectionModel.Source = CreateNestedData(3 /* levels */ , 2 /* groupsAtLevel */, 2 /* countAtLeaf */); var path = Path(1, 0, 1, 1); Select(selectionModel, path, true); - ValidateSelection(selectionModel, - new List() { path }, - new List() - { - Path(), - Path(1), - Path(1, 0), - Path(1, 0, 1), - }); + ValidateSelection(selectionModel, path); Select(selectionModel, Path(0, 0, 1, 0), true); - ValidateSelection(selectionModel, - new List() - { - Path(0, 0, 1, 0) - }, - new List() - { - Path(), - Path(0), - Path(0, 0), - Path(0, 0, 1) - }); + ValidateSelection(selectionModel, Path(0, 0, 1, 0)); Select(selectionModel, Path(0, 0, 1, 0), false); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); } [Theory] @@ -263,15 +199,7 @@ namespace Avalonia.Controls.UnitTests var startPath = Path(1, 0, 1, 0); Select(selectionModel, startPath, true); - ValidateSelection(selectionModel, - new List() { startPath }, - new List() - { - Path(), - Path(1), - Path(1, 0), - Path(1, 0, 1) - }); + ValidateSelection(selectionModel, startPath); var endPath = Path(1, 1, 1, 0); SelectRangeFromAnchor(selectionModel, endPath, true /* select */); @@ -306,72 +234,45 @@ namespace Avalonia.Controls.UnitTests } ValidateSelection(selectionModel, - new List() - { - Path(1, 0), - Path(1, 1), - Path(1, 0, 1), - Path(1, 0, 1, 0), - Path(1, 0, 1, 1), - Path(1, 0, 1, 2), - Path(1, 0, 1, 3), - Path(1, 1, 0), - Path(1, 1, 1), - Path(1, 1, 0, 0), - Path(1, 1, 0, 1), - Path(1, 1, 0, 2), - Path(1, 1, 0, 3), - Path(1, 1, 1, 0), - }, - new List() - { - Path(), - Path(1), - Path(1, 0), - Path(1, 1), - Path(1, 1, 1), - }); + Path(1, 1), + Path(1, 0, 1, 0), + Path(1, 0, 1, 1), + Path(1, 0, 1, 2), + Path(1, 0, 1, 3), + Path(1, 1, 0), + Path(1, 1, 1), + Path(1, 1, 0, 0), + Path(1, 1, 0, 1), + Path(1, 1, 0, 2), + Path(1, 1, 0, 3), + Path(1, 1, 1, 0)); ClearSelection(selectionModel); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); startPath = Path(0, 1, 0, 2); SetAnchorIndex(selectionModel, startPath); endPath = Path(0, 0, 0, 2); SelectRangeFromAnchor(selectionModel, endPath, true /* select */); ValidateSelection(selectionModel, - new List() - { - Path(0, 0), - Path(0, 1), - Path(0, 0, 0), - Path(0, 0, 1), - Path(0, 0, 0, 2), - Path(0, 0, 0, 3), - Path(0, 0, 1, 0), - Path(0, 0, 1, 1), - Path(0, 0, 1, 2), - Path(0, 0, 1, 3), - Path(0, 1, 0), - Path(0, 1, 0, 0), - Path(0, 1, 0, 1), - Path(0, 1, 0, 2), - }, - new List() - { - Path(), - Path(0), - Path(0, 0), - Path(0, 0, 0), - Path(0, 1), - Path(0, 1, 0), - }); + Path(0, 1), + Path(0, 0, 1), + Path(0, 0, 0, 2), + Path(0, 0, 0, 3), + Path(0, 0, 1, 0), + Path(0, 0, 1, 1), + Path(0, 0, 1, 2), + Path(0, 0, 1, 3), + Path(0, 1, 0), + Path(0, 1, 0, 0), + Path(0, 1, 0, 1), + Path(0, 1, 0, 2)); startPath = Path(0, 1, 0, 2); SetAnchorIndex(selectionModel, startPath); endPath = Path(0, 0, 0, 2); SelectRangeFromAnchor(selectionModel, endPath, false /* select */); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); } [Fact] @@ -385,64 +286,36 @@ namespace Avalonia.Controls.UnitTests selectionModel.Select(4); selectionModel.Select(5); ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(4), - Path(5), - }, - new List() - { - Path() - }); + Path(3), + Path(4), + Path(5)); _output.WriteLine("Insert in selected range: Inserting 3 items at index 4"); data.Insert(4, 41); data.Insert(4, 42); data.Insert(4, 43); ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(7), - Path(8), - }, - new List() - { - Path() - }); + Path(3), + Path(7), + Path(8)); _output.WriteLine("Insert before selected range: Inserting 3 items at index 0"); data.Insert(0, 100); data.Insert(0, 101); data.Insert(0, 102); ValidateSelection(selectionModel, - new List() - { - Path(6), - Path(10), - Path(11), - }, - new List() - { - Path() - }); + Path(6), + Path(10), + Path(11)); _output.WriteLine("Insert after selected range: Inserting 3 items at index 12"); data.Insert(12, 1000); data.Insert(12, 1001); data.Insert(12, 1002); ValidateSelection(selectionModel, - new List() - { - Path(6), - Path(10), - Path(11), - }, - new List() - { - Path() - }); + Path(6), + Path(10), + Path(11)); } [Fact] @@ -453,42 +326,15 @@ namespace Avalonia.Controls.UnitTests selectionModel.Source = data; selectionModel.Select(1, 1); - ValidateSelection(selectionModel, - new List() - { - Path(1, 1), - }, - new List() - { - Path(), - Path(1), - }); + ValidateSelection(selectionModel, Path(1, 1)); _output.WriteLine("Insert before selected range: Inserting item at group index 0"); data.Insert(0, 100); - ValidateSelection(selectionModel, - new List() - { - Path(2, 1) - }, - new List() - { - Path(), - Path(2), - }); + ValidateSelection(selectionModel, Path(2, 1)); _output.WriteLine("Insert after selected range: Inserting item at group index 3"); data.Insert(3, 1000); - ValidateSelection(selectionModel, - new List() - { - Path(2, 1) - }, - new List() - { - Path(), - Path(2), - }); + ValidateSelection(selectionModel, Path(2, 1)); } [Fact] @@ -502,58 +348,26 @@ namespace Avalonia.Controls.UnitTests selectionModel.Select(7); selectionModel.Select(8); ValidateSelection(selectionModel, - new List() - { - Path(6), - Path(7), - Path(8) - }, - new List() - { - Path() - }); + Path(6), + Path(7), + Path(8)); _output.WriteLine("Remove before selected range: Removing item at index 0"); data.RemoveAt(0); ValidateSelection(selectionModel, - new List() - { - Path(5), - Path(6), - Path(7) - }, - new List() - { - Path() - }); + Path(5), + Path(6), + Path(7)); _output.WriteLine("Remove from before to middle of selected range: Removing items at index 3, 4, 5"); data.RemoveAt(3); data.RemoveAt(3); data.RemoveAt(3); - ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(4) - }, - new List() - { - Path() - }); + ValidateSelection(selectionModel, Path(3), Path(4)); _output.WriteLine("Remove after selected range: Removing item at index 5"); data.RemoveAt(5); - ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(4) - }, - new List() - { - Path() - }); + ValidateSelection(selectionModel, Path(3), Path(4)); } [Fact] @@ -565,49 +379,19 @@ namespace Avalonia.Controls.UnitTests selectionModel.Select(1, 1); selectionModel.Select(1, 2); - ValidateSelection(selectionModel, - new List() - { - Path(1, 1), - Path(1, 2) - }, - new List() - { - Path(), - Path(1), - }); + ValidateSelection(selectionModel, Path(1, 1), Path(1, 2)); _output.WriteLine("Remove before selected range: Removing item at group index 0"); data.RemoveAt(0); - ValidateSelection(selectionModel, - new List() - { - Path(0, 1), - Path(0, 2) - }, - new List() - { - Path(), - Path(0), - }); + ValidateSelection(selectionModel, Path(0, 1), Path(0, 2)); _output.WriteLine("Remove after selected range: Removing item at group index 1"); data.RemoveAt(1); - ValidateSelection(selectionModel, - new List() - { - Path(0, 1), - Path(0, 2) - }, - new List() - { - Path(), - Path(0), - }); + ValidateSelection(selectionModel, Path(0, 1), Path(0, 2)); _output.WriteLine("Remove group containing selected items"); data.RemoveAt(0); - ValidateSelection(selectionModel, new List()); + ValidateSelection(selectionModel); } [Fact] @@ -620,29 +404,11 @@ namespace Avalonia.Controls.UnitTests selectionModel.Select(3); selectionModel.Select(4); selectionModel.Select(5); - ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(4), - Path(5), - }, - new List() - { - Path() - }); + ValidateSelection(selectionModel, Path(3), Path(4), Path(5)); data[3] = 300; data[4] = 400; - ValidateSelection(selectionModel, - new List() - { - Path(5), - }, - new List() - { - Path() - }); + ValidateSelection(selectionModel, Path(5)); } [Fact] @@ -653,19 +419,10 @@ namespace Avalonia.Controls.UnitTests selectionModel.Source = data; selectionModel.Select(1, 1); - ValidateSelection(selectionModel, - new List() - { - Path(1, 1) - }, - new List() - { - Path(), - Path(1) - }); + ValidateSelection(selectionModel, Path(1, 1)); data[1] = new ObservableCollection(Enumerable.Range(0, 5)); - ValidateSelection(selectionModel, new List()); + ValidateSelection(selectionModel); } [Fact] @@ -678,20 +435,10 @@ namespace Avalonia.Controls.UnitTests selectionModel.Select(3); selectionModel.Select(4); selectionModel.Select(5); - ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(4), - Path(5), - }, - new List() - { - Path() - }); + ValidateSelection(selectionModel, Path(3), Path(4), Path(5)); data.Clear(); - ValidateSelection(selectionModel, new List()); + ValidateSelection(selectionModel); } [Fact] @@ -702,19 +449,10 @@ namespace Avalonia.Controls.UnitTests selectionModel.Source = data; selectionModel.Select(1, 1); - ValidateSelection(selectionModel, - new List() - { - Path(1, 1) - }, - new List() - { - Path(), - Path(1) - }); + ValidateSelection(selectionModel, Path(1, 1)); (data[1] as IList).Clear(); - ValidateSelection(selectionModel, new List()); + ValidateSelection(selectionModel); } // In some cases the leaf node might get a collection change that affects an ancestors selection @@ -734,54 +472,22 @@ namespace Avalonia.Controls.UnitTests selectionModel.Select(1, 0); selectionModel.Select(1, 1); selectionModel.Select(1, 2); - ValidateSelection(selectionModel, - new List() - { - Path(1, 0), - Path(1, 1), - Path(1, 2), - Path(1) - }, - new List() - { - Path(), - }, - 1 /* selectedInnerNodes */); + ValidateSelection(selectionModel, Path(1, 0), Path(1, 1), Path(1, 2)); _output.WriteLine("Inserting 1.0"); selectionChangedRaised = false; (data[1] as AvaloniaList).Insert(0, 100); Assert.True(selectionChangedRaised, "SelectionChanged event was not raised"); - ValidateSelection(selectionModel, - new List() - { - Path(1, 1), - Path(1, 2), - Path(1, 3), - }, - new List() - { - Path(), - Path(1), - }); + ValidateSelection(selectionModel, Path(1, 1), Path(1, 2), Path(1, 3)); _output.WriteLine("Removing 1.0"); selectionChangedRaised = false; (data[1] as AvaloniaList).RemoveAt(0); Assert.True(selectionChangedRaised, "SelectionChanged event was not raised"); ValidateSelection(selectionModel, - new List() - { - Path(1, 0), - Path(1, 1), - Path(1, 2), - Path(1) - }, - new List() - { - Path(), - }, - 1 /* selectedInnerNodes */); + Path(1, 0), + Path(1, 1), + Path(1, 2)); } [Fact] @@ -859,21 +565,33 @@ namespace Avalonia.Controls.UnitTests selectionModel.SelectRange(IndexPath.CreateFrom(0), IndexPath.CreateFrom(1, 1)); ValidateSelection(selectionModel, - new List() + Path(0), + Path(1), + Path(0, 0), + Path(0, 1), + Path(0, 2), + Path(1, 0), + Path(1, 1)); + } + + [Fact] + public void SelectRange_Should_Select_Nested_Items_On_Different_Levels() + { + var target = new SelectionModel(); + var data = CreateNestedData(1, 2, 3); + + target.Source = data; + target.AnchorIndex = new IndexPath(0, 1); + target.SelectRange(Path(0, 1), Path(1)); + + Assert.Equal( + new[] { - Path(0), Path(1), - Path(0, 0), Path(0, 1), Path(0, 2), - Path(1, 0), - Path(1, 1) }, - new List() - { - Path(), - Path(1) - }); + target.SelectedIndices); } [Fact] @@ -2145,75 +1863,9 @@ namespace Avalonia.Controls.UnitTests private void ValidateSelection( SelectionModel selectionModel, - List expectedSelected, - List expectedPartialSelected = null, - int selectedInnerNodes = 0) + params IndexPath[] expectedSelected) { - _output.WriteLine("Validating Selection..."); - - _output.WriteLine("Selection contains indices:"); - foreach (var index in selectionModel.SelectedIndices) - { - _output.WriteLine(" " + index.ToString()); - } - - _output.WriteLine("Selection contains items:"); - foreach (var item in selectionModel.SelectedItems) - { - _output.WriteLine(" " + item.ToString()); - } - - if (selectionModel.Source != null) - { - List allIndices = GetIndexPathsInSource(selectionModel.Source); - foreach (var index in allIndices) - { - bool? isSelected = selectionModel.IsSelectedWithPartialAt(index); - if (Contains(expectedSelected, index) && !Contains(expectedPartialSelected, index)) - { - Assert.True(isSelected.Value, index + " is Selected"); - } - else if (expectedPartialSelected != null && Contains(expectedPartialSelected, index)) - { - Assert.True(isSelected == null, index + " is partially Selected"); - } - else - { - if (isSelected == null) - { - _output.WriteLine("*************" + index + " is null"); - Assert.True(false, "Expected false but got null");; - } - else - { - Assert.False(isSelected.Value, index + " is not Selected"); - } - } - } - } - else - { - foreach (var index in expectedSelected) - { - Assert.True(selectionModel.IsSelectedWithPartialAt(index), index + " is Selected"); - } - } - if (expectedSelected.Count > 0) - { - _output.WriteLine("SelectedIndex is " + selectionModel.SelectedIndex); - Assert.Equal(expectedSelected[0], selectionModel.SelectedIndex); - if (selectionModel.Source != null) - { - Assert.Equal(selectionModel.SelectedItem, GetData(selectionModel, expectedSelected[0])); - } - - int itemsCount = selectionModel.SelectedItems.Count(); - Assert.Equal(selectionModel.Source != null ? expectedSelected.Count - selectedInnerNodes : 0, itemsCount); - int indicesCount = selectionModel.SelectedIndices.Count(); - Assert.Equal(expectedSelected.Count - selectedInnerNodes, indicesCount); - } - - _output.WriteLine("Validating Selection... done"); + Assert.Equal(expectedSelected, selectionModel.SelectedIndices); } private object GetData(SelectionModel selectionModel, IndexPath indexPath) From 9ef91f9e10640bd683d49255777b51a432c4b31c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 12 May 2020 17:59:11 +0200 Subject: [PATCH 12/13] Fix notifications when removing parent item. --- src/Avalonia.Controls/SelectionNode.cs | 112 ++++++------------ .../SelectionModelTests.cs | 13 ++ 2 files changed, 52 insertions(+), 73 deletions(-) diff --git a/src/Avalonia.Controls/SelectionNode.cs b/src/Avalonia.Controls/SelectionNode.cs index 2fa7c5f697..0b00db88c3 100644 --- a/src/Avalonia.Controls/SelectionNode.cs +++ b/src/Avalonia.Controls/SelectionNode.cs @@ -731,101 +731,67 @@ namespace Avalonia.Controls var selectionInvalidated = false; var removed = new List(); var count = items.Count; - - // Remove the items from the selection for leaf - if (ItemsSourceView!.Count > 0) - { - bool isSelected = false; + var isSelected = false; - for (int i = 0; i <= count - 1; i++) + for (int i = 0; i <= count - 1; i++) + { + if (IsSelected(index + i)) { - if (IsSelected(index + i)) - { - isSelected = true; - removed.Add(items[i]); - } + isSelected = true; + removed.Add(items[i]); } + } - if (isSelected) - { - var removeRange = new IndexRange(index, index + count - 1); - SelectedCount -= IndexRange.Remove(_selected, removeRange); - selectionInvalidated = true; - - if (_selectedItems != null) - { - foreach (var i in items) - { - _selectedItems.Remove(i); - } - } - } + if (isSelected) + { + var removeRange = new IndexRange(index, index + count - 1); + SelectedCount -= IndexRange.Remove(_selected, removeRange); + selectionInvalidated = true; - for (int i = 0; i < _selected.Count; i++) + if (_selectedItems != null) { - var range = _selected[i]; - - // The range is after the removed items, need to shift the range left - if (range.End > index) + foreach (var i in items) { - // Shift the range to the left - _selected[i] = new IndexRange(range.Begin - count, range.End - count); - selectionInvalidated = true; + _selectedItems.Remove(i); } } + } - // Update for non-leaf if we are tracking non-leaf nodes - if (_childrenNodes.Count > 0) - { - selectionInvalidated = true; - for (int i = 0; i < count; i++) - { - if (_childrenNodes[index] != null) - { - removed.AddRange(_childrenNodes[index]!.SelectedItems); - RealizedChildrenNodeCount--; - _childrenNodes[index]!.Dispose(); - } - _childrenNodes.RemoveAt(index); - } - } + for (int i = 0; i < _selected.Count; i++) + { + var range = _selected[i]; - //Adjust the anchor - if (AnchorIndex >= index) + // The range is after the removed items, need to shift the range left + if (range.End > index) { - AnchorIndex -= count; + // Shift the range to the left + _selected[i] = new IndexRange(range.Begin - count, range.End - count); + selectionInvalidated = true; } } - else - { - // No more items in the list, clear - ClearSelection(); - RealizedChildrenNodeCount = 0; - selectionInvalidated = true; - } - // Check if removing a node invalidated an ancestors - // selection state. For example if parent was partially selected before - // removing an item, it could be selected now. - if (!selectionInvalidated) + // Update for non-leaf if we are tracking non-leaf nodes + if (_childrenNodes.Count > 0) { - var parent = _parent; - - while (parent != null) + selectionInvalidated = true; + for (int i = 0; i < count; i++) { - var isSelected = parent.IsSelectedWithPartial(); - // If a parent is partially selected, then it will become selected. - // If it is selected or not selected - there is no change. - if (!isSelected.HasValue) + if (_childrenNodes[index] != null) { - selectionInvalidated = true; - break; + removed.AddRange(_childrenNodes[index]!.SelectedItems); + RealizedChildrenNodeCount--; + _childrenNodes[index]!.Dispose(); } - - parent = parent._parent; + _childrenNodes.RemoveAt(index); } } + //Adjust the anchor + if (AnchorIndex >= index) + { + AnchorIndex -= count; + } + return (selectionInvalidated, removed); } diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index cea41526ad..246ff723a1 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -390,8 +390,21 @@ namespace Avalonia.Controls.UnitTests ValidateSelection(selectionModel, Path(0, 1), Path(0, 2)); _output.WriteLine("Remove group containing selected items"); + + var raised = 0; + + selectionModel.SelectionChanged += (s, e) => + { + Assert.Empty(e.DeselectedIndices); + Assert.Equal(new object[] { 4, 5, }, e.DeselectedItems); + Assert.Empty(e.SelectedIndices); + Assert.Empty(e.SelectedItems); + ++raised; + }; + data.RemoveAt(0); ValidateSelection(selectionModel); + Assert.Equal(1, raised); } [Fact] From 39501e5b3009dfc76ed769ae40cf5416ddc23e00 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 12 May 2020 21:10:49 +0300 Subject: [PATCH 13/13] DnD support for OSX --- native/Avalonia.Native/inc/avalonia-native.h | 49 +++++++- .../project.pbxproj | 4 + native/Avalonia.Native/src/OSX/AvnString.h | 3 +- native/Avalonia.Native/src/OSX/AvnString.mm | 49 ++++++++ native/Avalonia.Native/src/OSX/clipboard.mm | 90 ++++++++++++--- native/Avalonia.Native/src/OSX/common.h | 8 +- native/Avalonia.Native/src/OSX/dnd.mm | 89 +++++++++++++++ native/Avalonia.Native/src/OSX/main.mm | 19 ++- native/Avalonia.Native/src/OSX/window.h | 2 +- native/Avalonia.Native/src/OSX/window.mm | 108 +++++++++++++++++- .../ControlCatalog/Pages/DragAndDropPage.xaml | 12 +- .../Pages/DragAndDropPage.xaml.cs | 92 +++++++++------ src/Avalonia.Controls/Application.cs | 8 +- .../AvaloniaNativeDragSource.cs | 76 ++++++++++++ src/Avalonia.Native/AvaloniaNativePlatform.cs | 16 ++- src/Avalonia.Native/AvnString.cs | 39 +++++++ src/Avalonia.Native/ClipboardImpl.cs | 98 ++++++++++++++-- src/Avalonia.Native/WindowImplBase.cs | 31 +++++ 18 files changed, 712 insertions(+), 81 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/dnd.mm create mode 100644 src/Avalonia.Native/AvaloniaNativeDragSource.cs create mode 100644 src/Avalonia.Native/AvnString.cs diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 38d99db5c9..c2a9faf70c 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -22,6 +22,9 @@ struct IAvnGlSurfaceRenderTarget; struct IAvnGlSurfaceRenderingSession; struct IAvnMenu; struct IAvnMenuItem; +struct IAvnStringArray; +struct IAvnDndResultCallback; +struct IAvnGCHandleDeallocatorCallback; struct IAvnMenuEvents; enum SystemDecorations { @@ -130,6 +133,22 @@ enum AvnInputModifiers XButton2MouseButton = 256 }; +enum class AvnDragDropEffects +{ + None = 0, + Copy = 1, + Move = 2, + Link = 4, +}; + +enum class AvnDragEventType +{ + Enter, + Over, + Leave, + Drop +}; + enum AvnWindowState { Normal, @@ -188,7 +207,7 @@ enum AvnMenuItemToggleType AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown { public: - virtual HRESULT Initialize() = 0; + virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) = 0; virtual IAvnMacOptions* GetMacOptions() = 0; virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) = 0; virtual HRESULT CreatePopup (IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) = 0; @@ -196,6 +215,7 @@ public: virtual HRESULT CreateSystemDialogs (IAvnSystemDialogs** ppv) = 0; virtual HRESULT CreateScreens (IAvnScreens** ppv) = 0; virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; + virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0; virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0; @@ -236,6 +256,8 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0; virtual HRESULT ObtainNSViewHandle(void** retOut) = 0; virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0; + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, + IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0; }; AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase @@ -271,6 +293,9 @@ AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown virtual bool RawTextInputEvent (unsigned int timeStamp, const char* text) = 0; virtual void ScalingChanged(double scaling) = 0; virtual void RunRenderPriorityJobs() = 0; + virtual AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, + AvnInputModifiers modifiers, AvnDragDropEffects effects, + IAvnClipboard* clipboard, void* dataObjectHandle) = 0; }; @@ -354,8 +379,10 @@ AVNCOM(IAvnScreens, 0e) : IUnknown AVNCOM(IAvnClipboard, 0f) : IUnknown { - virtual HRESULT GetText (IAvnString**ppv) = 0; - virtual HRESULT SetText (void* utf8Text) = 0; + virtual HRESULT GetText (char* type, IAvnString**ppv) = 0; + virtual HRESULT SetText (char* type, void* utf8Text) = 0; + virtual HRESULT ObtainFormats(IAvnStringArray**ppv) = 0; + virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) = 0; virtual HRESULT Clear() = 0; }; @@ -428,4 +455,20 @@ AVNCOM(IAvnMenuEvents, 1A) : IUnknown virtual void NeedsUpdate () = 0; }; +AVNCOM(IAvnStringArray, 20) : IUnknown +{ + virtual unsigned int GetCount() = 0; + virtual HRESULT Get(unsigned int index, IAvnString**ppv) = 0; +}; + +AVNCOM(IAvnDndResultCallback, 21) : IUnknown +{ + virtual void OnDragAndDropComplete(AvnDragDropEffects effecct) = 0; +}; + +AVNCOM(IAvnGCHandleDeallocatorCallback, 22) : IUnknown +{ + virtual void FreeGCHandle(void* handle) = 0; +}; + extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative(); diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 50a85bdf9f..ea28780c81 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */; }; 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; }; 1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; }; + 1A465D10246AB61600C5858B /* dnd.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A465D0F246AB61600C5858B /* dnd.mm */; }; 37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; @@ -33,6 +34,7 @@ 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; }; 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = ""; }; 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + 1A465D0F246AB61600C5858B /* dnd.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = dnd.mm; sourceTree = ""; }; 37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = ""; }; 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; @@ -92,6 +94,7 @@ 5BF943652167AD1D009CAE35 /* cursor.h */, 5B21A981216530F500CEE36E /* cursor.mm */, 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */, + 1A465D0F246AB61600C5858B /* dnd.mm */, AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */, AB661C212148288600291242 /* common.h */, 379860FE214DA0C000CD0246 /* KeyTransform.h */, @@ -196,6 +199,7 @@ 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, + 1A465D10246AB61600C5858B /* dnd.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, AB661C202148286E00291242 /* window.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h index 9a8f5a1318..88bc4e6963 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.h +++ b/native/Avalonia.Native/src/OSX/AvnString.h @@ -10,5 +10,6 @@ #define AvnString_h extern IAvnString* CreateAvnString(NSString* string); - +extern IAvnStringArray* CreateAvnStringArray(NSArray* array); +extern IAvnStringArray* CreateAvnStringArray(NSString* string); #endif /* AvnString_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index b62fe8a968..6445a9fef1 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -7,6 +7,7 @@ // #include "common.h" +#include class AvnStringImpl : public virtual ComSingleObject { @@ -61,7 +62,55 @@ public: } }; +class AvnStringArrayImpl : public virtual ComSingleObject +{ +private: + std::vector> _list; +public: + FORWARD_IUNKNOWN() + AvnStringArrayImpl(NSArray* array) + { + for(int c = 0; c < [array count]; c++) + { + ComPtr s; + *s.getPPV() = new AvnStringImpl([array objectAtIndex:c]); + _list.push_back(s); + } + } + + AvnStringArrayImpl(NSString* string) + { + ComPtr s; + *s.getPPV() = new AvnStringImpl(string); + _list.push_back(s); + } + + virtual unsigned int GetCount() override + { + return (unsigned int)_list.size(); + } + + virtual HRESULT Get(unsigned int index, IAvnString**ppv) override + { + if(_list.size() <= index) + return E_INVALIDARG; + *ppv = _list[index].getRetainedReference(); + return S_OK; + } +}; + IAvnString* CreateAvnString(NSString* string) { return new AvnStringImpl(string); } + + +IAvnStringArray* CreateAvnStringArray(NSArray * array) +{ + return new AvnStringArrayImpl(array); +} + +IAvnStringArray* CreateAvnStringArray(NSString* string) +{ + return new AvnStringArrayImpl(string); +} diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index c2cf1f1f61..18d60d3853 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -3,16 +3,27 @@ class Clipboard : public ComSingleObject { +private: + NSPasteboard* _pb; + NSPasteboardItem* _item; public: FORWARD_IUNKNOWN() - Clipboard() + Clipboard(NSPasteboard* pasteboard, NSPasteboardItem* item) { - NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; - [pasteBoard stringForType:NSPasteboardTypeString]; + if(pasteboard == nil && item == nil) + pasteboard = [NSPasteboard generalPasteboard]; + + _pb = pasteboard; + _item = item; } - virtual HRESULT GetText (IAvnString**ppv) override + NSPasteboardItem* TryGetItem() + { + return _item; + } + + virtual HRESULT GetText (char* type, IAvnString**ppv) override { @autoreleasepool { @@ -20,20 +31,53 @@ public: { return E_POINTER; } + NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; + NSString* string = _item == nil ? [_pb stringForType:typeString] : [_item stringForType:typeString]; - *ppv = CreateAvnString([[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]); + *ppv = CreateAvnString(string); return S_OK; } } - virtual HRESULT SetText (void* utf8String) override + virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override { @autoreleasepool { - NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; - [pasteBoard clearContents]; - [pasteBoard setString:[NSString stringWithUTF8String:(const char*)utf8String] forType:NSPasteboardTypeString]; + *ppv= nil; + NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; + NSObject* data = _item == nil ? [_pb propertyListForType: typeString] : [_item propertyListForType: typeString]; + if(data == nil) + return S_OK; + + if([data isKindOfClass: [NSString class]]) + { + *ppv = CreateAvnStringArray((NSString*) data); + return S_OK; + } + + NSArray* arr = (NSArray*)data; + + for(int c = 0; c < [arr count]; c++) + if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]]) + return E_INVALIDARG; + + *ppv = CreateAvnStringArray(arr); + return S_OK; + } + } + + virtual HRESULT SetText (char* type, void* utf8String) override + { + Clear(); + @autoreleasepool + { + auto string = [NSString stringWithUTF8String:(const char*)utf8String]; + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + if(_item == nil) + [_pb setString: string forType: typeString]; + else + [_item setString: string forType:typeString]; } return S_OK; @@ -43,16 +87,34 @@ public: { @autoreleasepool { - NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; - [pasteBoard clearContents]; - [pasteBoard setString:@"" forType:NSPasteboardTypeString]; + if(_item != nil) + _item = [NSPasteboardItem new]; + else + { + [_pb clearContents]; + [_pb setString:@"" forType:NSPasteboardTypeString]; + } } return S_OK; } + + virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override + { + *ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]); + return S_OK; + } }; -extern IAvnClipboard* CreateClipboard() +extern IAvnClipboard* CreateClipboard(NSPasteboard* pb, NSPasteboardItem* item) +{ + return new Clipboard(pb, item); +} + +extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*cb) { - return new Clipboard(); + auto clipboard = dynamic_cast(cb); + if(clipboard == nil) + return nil; + return clipboard->TryGetItem(); } diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 7a433bfd9f..df6a7be91c 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -8,11 +8,17 @@ #include extern IAvnPlatformThreadingInterface* CreatePlatformThreading(); +extern void FreeAvnGCHandle(void* handle); extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl); extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl); extern IAvnSystemDialogs* CreateSystemDialogs(); extern IAvnScreens* CreateScreens(); -extern IAvnClipboard* CreateClipboard(); +extern IAvnClipboard* CreateClipboard(NSPasteboard*, NSPasteboardItem*); +extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*); +extern NSObject* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle); +extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject* info); +extern NSString* GetAvnCustomDataType(); +extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); diff --git a/native/Avalonia.Native/src/OSX/dnd.mm b/native/Avalonia.Native/src/OSX/dnd.mm new file mode 100644 index 0000000000..294b8ee8ea --- /dev/null +++ b/native/Avalonia.Native/src/OSX/dnd.mm @@ -0,0 +1,89 @@ +#include "common.h" + +extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop) +{ + int effects = 0; + if((nsop & NSDragOperationCopy) != 0) + effects |= (int)AvnDragDropEffects::Copy; + if((nsop & NSDragOperationMove) != 0) + effects |= (int)AvnDragDropEffects::Move; + if((nsop & NSDragOperationLink) != 0) + effects |= (int)AvnDragDropEffects::Link; + return (AvnDragDropEffects)effects; +}; + +extern NSString* GetAvnCustomDataType() +{ + char buffer[256]; + sprintf(buffer, "net.avaloniaui.inproc.uti.n%in", getpid()); + return [NSString stringWithUTF8String:buffer]; +} + +@interface AvnDndSource : NSObject + +@end + +@implementation AvnDndSource +{ + NSDragOperation _operation; + ComPtr _cb; + void* _sourceHandle; +}; + +- (NSDragOperation)draggingSession:(nonnull NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context +{ + return NSDragOperationCopy; +} + +- (AvnDndSource*) initWithOperation: (NSDragOperation)operation + andCallback: (IAvnDndResultCallback*) cb + andSourceHandle: (void*) handle +{ + self = [super init]; + _operation = operation; + _cb = cb; + _sourceHandle = handle; + return self; +} + +- (void)draggingSession:(NSDraggingSession *)session + endedAtPoint:(NSPoint)screenPoint + operation:(NSDragOperation)operation +{ + if(_cb != nil) + { + auto cb = _cb; + _cb = nil; + cb->OnDragAndDropComplete(ConvertDragDropEffects(operation)); + } + if(_sourceHandle != nil) + { + FreeAvnGCHandle(_sourceHandle); + _sourceHandle = nil; + } +} + +- (void*) gcHandle +{ + return _sourceHandle; +} + +@end + +extern NSObject* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle) +{ + return [[AvnDndSource alloc] initWithOperation:op andCallback:cb andSourceHandle:handle]; +}; + +extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject* info) +{ + id obj = [info draggingSource]; + if(obj == nil) + return nil; + if([obj isKindOfClass: [AvnDndSource class]]) + { + auto src = (AvnDndSource*)obj; + return [src gcHandle]; + } + return nil; +} diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index a63353bc0a..e6c4a861fd 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -150,14 +150,15 @@ public: } @end - +static ComPtr _deallocator; class AvaloniaNative : public ComSingleObject { public: FORWARD_IUNKNOWN() - virtual HRESULT Initialize() override + virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) override { + _deallocator = deallocator; @autoreleasepool{ [[ThreadingInitializer new] do]; } @@ -207,7 +208,13 @@ public: virtual HRESULT CreateClipboard(IAvnClipboard** ppv) override { - *ppv = ::CreateClipboard (); + *ppv = ::CreateClipboard (nil, nil); + return S_OK; + } + + virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) override + { + *ppv = ::CreateClipboard (nil, [NSPasteboardItem new]); return S_OK; } @@ -257,6 +264,12 @@ extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() return new AvaloniaNative(); }; +extern void FreeAvnGCHandle(void* handle) +{ + if(_deallocator != nil) + _deallocator->FreeGCHandle(handle); +} + NSSize ToNSSize (AvnSize s) { NSSize result; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index ec8fe9e6ee..163db36800 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -3,7 +3,7 @@ class WindowBaseImpl; -@interface AvnView : NSView +@interface AvnView : NSView -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(NSEvent* _Nonnull) lastMouseDownEvent; -(AvnPoint) translateLocalPoint:(AvnPoint)pt; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 091219fc72..06b0c50456 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -382,6 +382,50 @@ public: *ppv = [renderTarget createSurfaceRenderTarget]; return *ppv == nil ? E_FAIL : S_OK; } + + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, + IAvnClipboard* clipboard, IAvnDndResultCallback* cb, + void* sourceHandle) override + { + auto item = TryGetPasteboardItem(clipboard); + [item setString:@"" forType:GetAvnCustomDataType()]; + if(item == nil) + return E_INVALIDARG; + if(View == NULL) + return E_FAIL; + + auto nsevent = [NSApp currentEvent]; + auto nseventType = [nsevent type]; + + // If current event isn't a mouse one (probably due to malfunctioning user app) + // attempt to forge a new one + if(!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited) + || (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged))) + { + auto nspoint = [Window convertBaseToScreen: ToNSPoint(point)]; + CGPoint cgpoint = NSPointToCGPoint(nspoint); + auto cgevent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, cgpoint, kCGMouseButtonLeft); + nsevent = [NSEvent eventWithCGEvent: cgevent]; + CFRelease(cgevent); + } + + auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter: item]; + + auto dragItemImage = [NSImage imageNamed:NSImageNameMultipleDocuments]; + NSRect dragItemRect = {(float)point.X, (float)point.Y, [dragItemImage size].width, [dragItemImage size].height}; + [dragItem setDraggingFrame: dragItemRect contents: dragItemImage]; + + int op = 0; int ieffects = (int)effects; + if((ieffects & (int)AvnDragDropEffects::Copy) != 0) + op |= NSDragOperationCopy; + if((ieffects & (int)AvnDragDropEffects::Link) != 0) + op |= NSDragOperationLink; + if((ieffects & (int)AvnDragDropEffects::Move) != 0) + op |= NSDragOperationMove; + [View beginDraggingSessionWithItems: @[dragItem] event: nsevent + source: CreateDraggingSource((NSDragOperation) op, cb, sourceHandle)]; + return S_OK; + } protected: virtual NSWindowStyleMask GetStyle() @@ -911,7 +955,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _area = nullptr; _lastPixelSize.Height = 100; _lastPixelSize.Width = 100; - + [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; return self; } @@ -1302,6 +1346,68 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return result; } + +- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info +{ + auto localPoint = [self convertPoint:[info draggingLocation] toView:self]; + auto avnPoint = [self toAvnPoint:localPoint]; + auto point = [self translateLocalPoint:avnPoint]; + auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]]; + NSDragOperation nsop = [info draggingSourceOperationMask]; + + auto effects = ConvertDragDropEffects(nsop); + int reffects = (int)_parent->BaseEvents + ->DragEvent(type, point, modifiers, effects, + CreateClipboard([info draggingPasteboard], nil), + GetAvnDataObjectHandleFromDraggingInfo(info)); + + NSDragOperation ret = 0; + + // Ensure that the managed part didn't add any new effects + reffects = (int)effects & (int)reffects; + + // OSX requires exactly one operation + if((reffects & (int)AvnDragDropEffects::Copy) != 0) + ret = NSDragOperationCopy; + else if((reffects & (int)AvnDragDropEffects::Move) != 0) + ret = NSDragOperationMove; + else if((reffects & (int)AvnDragDropEffects::Link) != 0) + ret = NSDragOperationLink; + if(ret == 0) + ret = NSDragOperationNone; + return ret; +} + +- (NSDragOperation)draggingEntered:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender]; +} + +- (NSDragOperation)draggingUpdated:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender]; +} + +- (void)draggingExited:(id )sender +{ + [self triggerAvnDragEvent: AvnDragEventType::Leave info:sender]; +} + +- (BOOL)prepareForDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone; +} + +- (BOOL)performDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone; +} + +- (void)concludeDragOperation:(nullable id )sender +{ + +} + @end diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index 9bfcd90149..65a798e53c 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -9,9 +9,15 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - - Drag Me - + + + Drag Me + + + Drag Me (custom) + + + Drop some text or files here diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs index 0bf21c2820..5a52dbe12b 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -3,69 +3,85 @@ using Avalonia.Input; using Avalonia.Markup.Xaml; using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; using System.Text; namespace ControlCatalog.Pages { public class DragAndDropPage : UserControl { - private TextBlock _DropState; - private TextBlock _DragState; - private Border _DragMe; - private int DragCount = 0; - + TextBlock _DropState; + private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom"; public DragAndDropPage() { this.InitializeComponent(); + _DropState = this.Find("DropState"); - _DragMe.PointerPressed += DoDrag; + int textCount = 0; + SetupDnd("Text", d => d.Set(DataFormats.Text, + $"Text was dragged {++textCount} times")); - AddHandler(DragDrop.DropEvent, Drop); - AddHandler(DragDrop.DragOverEvent, DragOver); + SetupDnd("Custom", d => d.Set(CustomFormat, "Test123")); } - private async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e) + void SetupDnd(string suffix, Action factory, DragDropEffects effects = DragDropEffects.Copy) { - DataObject dragData = new DataObject(); - dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times"); + var dragMe = this.Find("DragMe" + suffix); + var dragState = this.Find("DragState"+suffix); - var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy); - switch(result) + async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e) { - case DragDropEffects.Copy: - _DragState.Text = "The text was copied"; break; - case DragDropEffects.Link: - _DragState.Text = "The text was linked"; break; - case DragDropEffects.None: - _DragState.Text = "The drag operation was canceled"; break; + var dragData = new DataObject(); + factory(dragData); + + var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy); + switch (result) + { + case DragDropEffects.Copy: + dragState.Text = "Data was copied"; + break; + case DragDropEffects.Link: + dragState.Text = "Data was linked"; + break; + case DragDropEffects.None: + dragState.Text = "The drag operation was canceled"; + break; + } } - } - private void DragOver(object sender, DragEventArgs e) - { - // Only allow Copy or Link as Drop Operations. - e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link); + void DragOver(object sender, DragEventArgs e) + { + // Only allow Copy or Link as Drop Operations. + e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link); - // Only allow if the dragged data contains text or filenames. - if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.FileNames)) - e.DragEffects = DragDropEffects.None; - } + // Only allow if the dragged data contains text or filenames. + if (!e.Data.Contains(DataFormats.Text) + && !e.Data.Contains(DataFormats.FileNames) + && !e.Data.Contains(CustomFormat)) + e.DragEffects = DragDropEffects.None; + } - private void Drop(object sender, DragEventArgs e) - { - if (e.Data.Contains(DataFormats.Text)) - _DropState.Text = e.Data.GetText(); - else if (e.Data.Contains(DataFormats.FileNames)) - _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames()); + void Drop(object sender, DragEventArgs e) + { + if (e.Data.Contains(DataFormats.Text)) + _DropState.Text = e.Data.GetText(); + else if (e.Data.Contains(DataFormats.FileNames)) + _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames()); + else if (e.Data.Contains(CustomFormat)) + _DropState.Text = "Custom: " + e.Data.Get(CustomFormat); + } + + dragMe.PointerPressed += DoDrag; + + AddHandler(DragDrop.DropEvent, Drop); + AddHandler(DragDrop.DragOverEvent, DragOver); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - - _DropState = this.Find("DropState"); - _DragState = this.Find("DragState"); - _DragMe = this.Find("DragMe"); } } } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 5cae798dc8..02f47e07b4 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -254,8 +254,12 @@ namespace Avalonia .Bind().ToTransient() .Bind().ToConstant(_styler) .Bind().ToConstant(AvaloniaScheduler.Instance) - .Bind().ToConstant(DragDropDevice.Instance) - .Bind().ToTransient(); + .Bind().ToConstant(DragDropDevice.Instance); + + // TODO: Fix this, for now we keep this behavior since someone might be relying on it in 0.9.x + if (AvaloniaLocator.Current.GetService() == null) + AvaloniaLocator.CurrentMutable + .Bind().ToTransient(); var clock = new RenderLoopClock(); AvaloniaLocator.CurrentMutable diff --git a/src/Avalonia.Native/AvaloniaNativeDragSource.cs b/src/Avalonia.Native/AvaloniaNativeDragSource.cs new file mode 100644 index 0000000000..80d54d8a10 --- /dev/null +++ b/src/Avalonia.Native/AvaloniaNativeDragSource.cs @@ -0,0 +1,76 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Interactivity; +using Avalonia.Native.Interop; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Native +{ + class AvaloniaNativeDragSource : IPlatformDragSource + { + private readonly IAvaloniaNativeFactory _factory; + + public AvaloniaNativeDragSource(IAvaloniaNativeFactory factory) + { + _factory = factory; + } + + TopLevel FindRoot(IInteractive interactive) + { + while (interactive != null && !(interactive is IVisual)) + interactive = interactive.InteractiveParent; + if (interactive == null) + return null; + var visual = (IVisual)interactive; + return visual.VisualRoot as TopLevel; + } + + class DndCallback : CallbackBase, IAvnDndResultCallback + { + private TaskCompletionSource _tcs; + + public DndCallback(TaskCompletionSource tcs) + { + _tcs = tcs; + } + public void OnDragAndDropComplete(AvnDragDropEffects effect) + { + _tcs?.TrySetResult((DragDropEffects)effect); + _tcs = null; + } + } + + public Task DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects) + { + // Sanity check + var tl = FindRoot(triggerEvent.Source); + var view = tl?.PlatformImpl as WindowBaseImpl; + if (view == null) + throw new ArgumentException(); + + triggerEvent.Pointer.Capture(null); + + var tcs = new TaskCompletionSource(); + + var clipboardImpl = _factory.CreateDndClipboard(); + using (var clipboard = new ClipboardImpl(clipboardImpl)) + using (var cb = new DndCallback(tcs)) + { + if (data.Contains(DataFormats.Text)) + // API is synchronous, so it's OK + clipboard.SetTextAsync(data.GetText()).Wait(); + + view.BeginDraggingSession((AvnDragDropEffects)allowedEffects, + triggerEvent.GetPosition(tl).ToAvnPoint(), clipboardImpl, cb, + GCHandle.ToIntPtr(GCHandle.Alloc(data))); + } + + return tcs.Task; + } + } +} diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index f1be2285d8..e46f4d4a15 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -77,17 +77,25 @@ namespace Avalonia.Native _factory = factory; } + class GCHandleDeallocator : CallbackBase, IAvnGCHandleDeallocatorCallback + { + public void FreeGCHandle(IntPtr handle) + { + GCHandle.FromIntPtr(handle).Free(); + } + } + void DoInitialize(AvaloniaNativePlatformOptions options) { _options = options; - _factory.Initialize(); + _factory.Initialize(new GCHandleDeallocator()); if (_factory.MacOptions != null) { var macOpts = AvaloniaLocator.Current.GetService(); _factory.MacOptions.ShowInDock = macOpts?.ShowInDock != false ? 1 : 0; } - + AvaloniaLocator.CurrentMutable .Bind() .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface())) @@ -101,7 +109,9 @@ namespace Avalonia.Native .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta)) - .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()); + .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) + .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)) + ; if (_options.UseGpu) AvaloniaLocator.CurrentMutable.Bind() .ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay())); diff --git a/src/Avalonia.Native/AvnString.cs b/src/Avalonia.Native/AvnString.cs new file mode 100644 index 0000000000..ba427b6aac --- /dev/null +++ b/src/Avalonia.Native/AvnString.cs @@ -0,0 +1,39 @@ +using System.Runtime.InteropServices; + +namespace Avalonia.Native.Interop +{ + unsafe partial class IAvnString + { + private string _managed; + + public string String + { + get + { + if (_managed == null) + { + var ptr = Pointer(); + if (ptr == null) + return null; + _managed = System.Text.Encoding.UTF8.GetString((byte*)ptr.ToPointer(), Length()); + } + + return _managed; + } + } + + public override string ToString() => String; + } + + partial class IAvnStringArray + { + public string[] ToStringArray() + { + var arr = new string[Count]; + for(uint c = 0; c GetTextAsync() + public Task GetTextAsync() { - using (var text = _native.GetText()) - { - var result = System.Text.Encoding.UTF8.GetString((byte*)text.Pointer(), text.Length()); - - return Task.FromResult(result); - } + using (var text = _native.GetText(NSPasteboardTypeString)) + return Task.FromResult(text.String); } public Task SetTextAsync(string text) @@ -40,11 +43,84 @@ namespace Avalonia.Native { using (var buffer = new Utf8Buffer(text)) { - _native.SetText(buffer.DangerousGetHandle()); + _native.SetText(NSPasteboardTypeString, buffer.DangerousGetHandle()); } } return Task.CompletedTask; } + + public IEnumerable GetFormats() + { + var rv = new List(); + using (var formats = _native.ObtainFormats()) + { + var cnt = formats.Count; + for (uint c = 0; c < cnt; c++) + { + using (var fmt = formats.Get(c)) + { + if(fmt.String == NSPasteboardTypeString) + rv.Add(DataFormats.Text); + if(fmt.String == NSFilenamesPboardType) + rv.Add(DataFormats.FileNames); + } + } + } + + return rv; + } + + public void Dispose() + { + _native?.Dispose(); + _native = null; + } + + public IEnumerable GetFileNames() + { + using (var strings = _native.GetStrings(NSFilenamesPboardType)) + return strings.ToStringArray(); + } + } + + class ClipboardDataObject : IDataObject, IDisposable + { + private ClipboardImpl _clipboard; + private List _formats; + + public ClipboardDataObject(IAvnClipboard clipboard) + { + _clipboard = new ClipboardImpl(clipboard); + } + + public void Dispose() + { + _clipboard?.Dispose(); + _clipboard = null; + } + + List Formats => _formats ??= _clipboard.GetFormats().ToList(); + + public IEnumerable GetDataFormats() => Formats; + + public bool Contains(string dataFormat) => Formats.Contains(dataFormat); + + public string GetText() + { + // bad idea in general, but API is synchronous anyway + return _clipboard.GetTextAsync().Result; + } + + public IEnumerable GetFileNames() => _clipboard.GetFileNames(); + + public object Get(string dataFormat) + { + if (dataFormat == DataFormats.Text) + return GetText(); + if (dataFormat == DataFormats.FileNames) + return GetFileNames(); + return null; + } } } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 98febdbe2c..47127fafe0 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; @@ -200,6 +201,30 @@ namespace Avalonia.Native { Dispatcher.UIThread.RunJobs(DispatcherPriority.Render); } + + public AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, + AvnInputModifiers modifiers, + AvnDragDropEffects effects, + IAvnClipboard clipboard, IntPtr dataObjectHandle) + { + var device = AvaloniaLocator.Current.GetService(); + + IDataObject dataObject = null; + if (dataObjectHandle != IntPtr.Zero) + dataObject = GCHandle.FromIntPtr(dataObjectHandle).Target as IDataObject; + + using(var clipboardDataObject = new ClipboardDataObject(clipboard)) + { + if (dataObject == null) + dataObject = clipboardDataObject; + + var args = new RawDragEvent(device, (RawDragEventType)type, + _parent._inputRoot, position.ToAvaloniaPoint(), dataObject, (DragDropEffects)effects, + (RawInputModifiers)modifiers); + _parent.Input(args); + return (AvnDragDropEffects)args.Effects; + } + } } public void Activate() @@ -358,6 +383,12 @@ namespace Avalonia.Native } + internal void BeginDraggingSession(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard clipboard, + IAvnDndResultCallback callback, IntPtr sourceHandle) + { + _native.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle); + } + public IPlatformHandle Handle { get; private set; } } }