Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Svg.Editor.Avalonia.Forms/Dialog/IUserInteraction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Task<ConfirmThreeButtonsResponse> ConfirmThreeButtonsAsync(
string neutral = "Cancel",
bool cancellable = true);

Task<string> ActionSheetAsync(string message, IEnumerable<string> options, string? title = null, string cancelButton = "Cancel", CancellationToken? cancelToken = null, bool cancellable = false);
Task<string> ActionSheetAsync(string message, IEnumerable<string> options, string? title = null, string cancelButton = "Cancel", CancellationToken? cancelToken = null, bool cancellable = false, int selectedIndex = -1);

/* Ignored deliberately by Alex: Do we really need these "input dialogs" anymore now that Avalonia Date/Time picker _work_?
Task<DateTime?> PromptDateAsync(DateTime? selectedDate);
Expand Down
4 changes: 2 additions & 2 deletions Svg.Editor.Avalonia.Forms/Dialog/UserInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ public Task<Color> ColorPickerAsync(string? title = null, string okButton = "OK"
});
}

public Task<string> ActionSheetAsync(string message, IEnumerable<string> options, string? title = null, string cancelButton = "Cancel", CancellationToken? cancelToken = null, bool cancellable = false)
public Task<string> ActionSheetAsync(string message, IEnumerable<string> options, string? title = null, string cancelButton = "Cancel", CancellationToken? cancelToken = null, bool cancellable = false, int selectedIndex = -1)
{
return Dispatcher.UIThread.InvokeAsync(async () =>
{
Expand All @@ -204,7 +204,7 @@ public Task<string> ActionSheetAsync(string message, IEnumerable<string> options
PrimaryButtonText = "",
CloseButtonText = cancellable ? cancelButton : null,
};
var vm = new ActionSheetDialogResultViewModel(options);
var vm = new ActionSheetDialogResultViewModel(options, selectedIndex);
vm.Initialize(d);
vm.CanCancel = cancellable;
d.Content = new ActionSheetDialogContent() { DataContext = vm };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ public ActionSheetItem? SelectedItem
set => SetField(ref _selectedItem, value);
}

public ActionSheetDialogResultViewModel(IEnumerable<string> items)
public ActionSheetDialogResultViewModel(IEnumerable<string> items, int selectedIndex = -1)
{
Items = new ObservableCollection<ActionSheetItem>(items.Select(i => new ActionSheetItem(i, i)));
SelectedItem = Items.FirstOrDefault();
SelectedItem = GetInitialSelection(selectedIndex);
}

public ActionSheetDialogResultViewModel(IEnumerable<KeyValuePair<string, object?>> items)
Expand All @@ -27,6 +27,14 @@ public ActionSheetDialogResultViewModel(IEnumerable<KeyValuePair<string, object?
SelectedItem = Items.FirstOrDefault();
}

private ActionSheetItem? GetInitialSelection(int selectedIndex)
{
if (selectedIndex >= 0 && selectedIndex < Items.Count)
return Items[selectedIndex];

return Items.FirstOrDefault();
}

public ObservableCollection<ActionSheetItem> Items { get; }

// Can only close if
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ public async Task<int[]> GetUserInput(IEnumerable<string> markerStartOptions, in
{

var mso = markerStartOptions.ToList();
var start = await _userInteractionService.ActionSheetAsync(_localizationService.GetString("Svg.Editor.Marker.Options.Start"), mso.ToArray(), cancelButton: _localizationService.GetString("Svg.Editor.Global.Cancel"), cancellable: true);
var start = await _userInteractionService.ActionSheetAsync(_localizationService.GetString("Svg.Editor.Marker.Options.Start"), mso.ToArray(), cancelButton: _localizationService.GetString("Svg.Editor.Global.Cancel"), cancellable: true, selectedIndex: markerStartSelected);


var startIndex = mso.IndexOf(start);
if (start == null || startIndex < 0)
return new[] { markerStartSelected, markerEndSelected };

var meo = markerEndOptions.ToList();
var end = await _userInteractionService.ActionSheetAsync(_localizationService.GetString("Svg.Editor.Marker.Options.End"), meo.ToArray(), cancelButton: _localizationService.GetString("Svg.Editor.Global.Cancel"), cancellable: true);
var end = await _userInteractionService.ActionSheetAsync(_localizationService.GetString("Svg.Editor.Marker.Options.End"), meo.ToArray(), cancelButton: _localizationService.GetString("Svg.Editor.Global.Cancel"), cancellable: true, selectedIndex: markerEndSelected);

var endIndex = meo.IndexOf(end);
if (end == null || endIndex < 0)
Expand Down
8 changes: 5 additions & 3 deletions Svg.Editor.Avalonia.Forms/Services/PinInputService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Avalonia.Controls;
using Svg.Editor.Avalon.Forms.Dialog;
using Svg.Editor.Interfaces;
using Svg.Editor.Services.Localization;
using Svg.Editor.Tools;
using System;
using System.Collections.Generic;
Expand All @@ -13,20 +14,21 @@ namespace Svg.Editor.Avalon.Forms.Services
public class PinInputService : IPinInputService
{
private readonly IUserInteraction _userInteractionService;

private readonly ILocalizationService _localizationService;

public PinInputService()
{
_userInteractionService = SvgEngine.Resolve<IUserInteraction>();
_localizationService = SvgEngine.Resolve<ILocalizationService>();
}
public async Task<PinTool.PinSize> GetUserInput(IEnumerable<string> pinSizeOptions, int oldSizeIndex = 1)
{
var defaultResult = (PinTool.PinSize)oldSizeIndex;

var sizeResult = await _userInteractionService.ActionSheetAsync("Select pin size", pinSizeOptions.ToArray());
var sizeResult = await _userInteractionService.ActionSheetAsync(_localizationService.GetString("Svg.Editor.Global.Pin.Size.Selection"), pinSizeOptions.ToArray(), cancelButton: _localizationService.GetString("Svg.Editor.Global.Cancel"), cancellable: true, selectedIndex: oldSizeIndex);
PinTool.PinSize.Medium.ToString();

if (sizeResult == "Cancel")
if (sizeResult == null)
{
return defaultResult;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ public StrokeStyleOptionsInputService()
int strokeWidthSelected)
{
var dashes = strokeDashOptions.ToArray();
var dash = await _userInteractionService.ActionSheetAsync(title, dashes, cancelButton: _localizationService.GetString("Svg.Editor.Global.Cancel"), cancellable: true);
var dash = await _userInteractionService.ActionSheetAsync(title, dashes, cancelButton: _localizationService.GetString("Svg.Editor.Global.Cancel"), cancellable: true, selectedIndex: strokeDashSelected);

if (dash == null)
return null;
var widths = strokeWidthOptions.ToArray();
var width = await _userInteractionService.ActionSheetAsync(title, widths, cancelButton: _localizationService.GetString("Svg.Editor.Global.Cancel"), cancellable: true);
var width = await _userInteractionService.ActionSheetAsync(title, widths, cancelButton: _localizationService.GetString("Svg.Editor.Global.Cancel"), cancellable: true, selectedIndex: strokeWidthSelected);
if (width == null)
return null;
return new StrokeStyleTool.StrokeStyleOptions() { StrokeDashIndex = Array.IndexOf(dashes, dash), StrokeWidthIndex = Array.IndexOf(widths, width) };
Expand Down
4 changes: 3 additions & 1 deletion Svg.Editor.Avalonia.Forms/Svg.Editor.Avalon.Forms.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<Version>3.2.0-optiq05</Version>
<Version>3.2.0-optiq06</Version>
<PackageReleaseNotes>
#3.2.0-optiq06
Improved GetDescendant and ActionSheetDialog has selected item index
#3.2.0-optiq05
Improved TextInputService and Colorpicker
#3.2.0-optiq04
Expand Down
4 changes: 3 additions & 1 deletion Svg.Editor.Avalonia.Views/Svg.Editor.Avalon.Views.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<Version>3.2.0-optiq05</Version>
<Version>3.2.0-optiq06</Version>
<PackageReleaseNotes>
#3.2.0-optiq06
Improved GetDescendant and ActionSheetDialog has selected item index
#3.2.0-optiq05
Improved TextInputService and Colorpicker
#3.2.0-optiq04
Expand Down
242 changes: 242 additions & 0 deletions Svg.Editor.Core.Tests/Forms/ActionSheetDialogResultViewModelTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
using Avalonia.Labs.Controls;
using NUnit.Framework;
using Shouldly;
using Svg.Editor.Avalon.Forms.Dialog.Views;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;

namespace Svg.Editor.Core.Tests.Forms
{
[TestFixture]
public class ActionSheetDialogResultViewModelTests
{
private static readonly string[] StringItems = { "Small", "Medium", "Large" };

private static readonly List<KeyValuePair<string, object?>> KvpItems = new()
{
new("Small", 1),
new("Medium", 2),
new("Large", 3),
};

[Test]
public void Constructor_WithStringItems_CreatesOneItemPerInput()
{
var vm = new ActionSheetDialogResultViewModel(StringItems);

vm.Items.Count.ShouldBe(StringItems.Length);
}

[Test]
public void Constructor_WithStringItems_TitleAndValueAreTheSameString()
{
var vm = new ActionSheetDialogResultViewModel(StringItems);

vm.Items.Select(i => i.Title).ShouldBe(StringItems);
vm.Items.Select(i => i.Value).ShouldBe(StringItems.Cast<object?>());
}

[Test]
public void Constructor_WithStringItems_NoSelectedIndexProvided_SelectsFirstItem()
{
var vm = new ActionSheetDialogResultViewModel(StringItems);

vm.SelectedItem.ShouldNotBeNull();
vm.SelectedItem!.Title.ShouldBe("Small");
}

[Test]
public void Constructor_WithEmptyStringItems_SelectedItemIsNull()
{
var vm = new ActionSheetDialogResultViewModel(Enumerable.Empty<string>());

vm.Items.ShouldBeEmpty();
vm.SelectedItem.ShouldBeNull();
}


[Test]
public void Constructor_WithValidSelectedIndex_SelectsThatItem()
{
var vm = new ActionSheetDialogResultViewModel(StringItems, selectedIndex: 1);

vm.SelectedItem.ShouldNotBeNull();
vm.SelectedItem!.Title.ShouldBe("Medium");
}

[Test]
public void Constructor_WithSelectedIndexAtLastPosition_SelectsLastItem()
{
var vm = new ActionSheetDialogResultViewModel(StringItems, selectedIndex: StringItems.Length - 1);

vm.SelectedItem!.Title.ShouldBe("Large");
}

[Test]
public void Constructor_WithNegativeSelectedIndex_FallsBackToFirstItem()
{
var vm = new ActionSheetDialogResultViewModel(StringItems, selectedIndex: -1);

vm.SelectedItem!.Title.ShouldBe("Small");
}

[Test]
public void Constructor_WithSelectedIndexEqualToCount_FallsBackToFirstItem()
{
var vm = new ActionSheetDialogResultViewModel(StringItems, selectedIndex: StringItems.Length);

vm.SelectedItem!.Title.ShouldBe("Small");
}

[Test]
public void Constructor_WithSelectedIndexGreaterThanCount_FallsBackToFirstItem()
{
var vm = new ActionSheetDialogResultViewModel(StringItems, selectedIndex: 999);

vm.SelectedItem!.Title.ShouldBe("Small");
}

[Test]
public void Constructor_WithSelectedIndexOnEmptyItems_SelectedItemIsNull()
{
var vm = new ActionSheetDialogResultViewModel(Enumerable.Empty<string>(), selectedIndex: 0);

vm.SelectedItem.ShouldBeNull();
}

[Test]
public void Constructor_WithKeyValuePairs_CreatesItemsWithCorrectTitleAndValue()
{
var vm = new ActionSheetDialogResultViewModel(KvpItems);

vm.Items.Count.ShouldBe(KvpItems.Count);
vm.Items.Select(i => i.Title).ShouldBe(KvpItems.Select(k => k.Key));
vm.Items.Select(i => i.Value).ShouldBe(KvpItems.Select(k => k.Value));
}

[Test]
public void Constructor_WithKeyValuePairs_SelectsFirstItemByDefault()
{
var vm = new ActionSheetDialogResultViewModel(KvpItems);

vm.SelectedItem!.Value.ShouldBe(1);
}

[Test]
public void GetResult_ReturnsValueOfSelectedItem()
{
var vm = new ActionSheetDialogResultViewModel(StringItems, selectedIndex: 2);

vm.GetResult().ShouldBe("Large");
}

[Test]
public void GetResult_WhenSelectedItemIsNull_ReturnsNull()
{
var vm = new ActionSheetDialogResultViewModel(Enumerable.Empty<string>());

vm.GetResult().ShouldBeNull();
}

[Test]
public void SelectedItem_CanBeChangedAfterConstruction()
{
var vm = new ActionSheetDialogResultViewModel(StringItems);

vm.SelectedItem = vm.Items[2];

vm.SelectedItem.ShouldBe(vm.Items[2]);
vm.GetResult().ShouldBe("Large");
}

[Test]
public void SelectedItem_Set_RaisesPropertyChanged()
{
var vm = new ActionSheetDialogResultViewModel(StringItems);
var raisedProperties = new List<string?>();

((INotifyPropertyChanged)vm).PropertyChanged += (_, e) => raisedProperties.Add(e.PropertyName);

vm.SelectedItem = vm.Items[1];

raisedProperties.ShouldContain(nameof(ActionSheetDialogResultViewModel.SelectedItem));
}

[Test]
public void SelectedItem_SetToSameValue_DoesNotRaisePropertyChanged()
{
var vm = new ActionSheetDialogResultViewModel(StringItems);
var current = vm.SelectedItem;
var raiseCount = 0;

((INotifyPropertyChanged)vm).PropertyChanged += (_, _) => raiseCount++;

vm.SelectedItem = current;

raiseCount.ShouldBe(0);
}

[Test]
public void CanClose_PrimaryResultWithSelection_ReturnsTrue()
{
var vm = new ActionSheetDialogResultViewModel(StringItems, selectedIndex: 0);

var result = InvokeCanClose(vm, ContentDialogResult.Primary);

result.ShouldBeTrue();
}

[Test]
public void CanClose_PrimaryResultWithNoSelection_ReturnsFalse()
{
var vm = new ActionSheetDialogResultViewModel(Enumerable.Empty<string>());

var result = InvokeCanClose(vm, ContentDialogResult.Primary);

result.ShouldBeFalse();
}

[Test]
public void CanClose_PrimaryResult_AfterClearingSelection_ReturnsFalse()
{
var vm = new ActionSheetDialogResultViewModel(StringItems);
vm.SelectedItem = null;

var result = InvokeCanClose(vm, ContentDialogResult.Primary);

result.ShouldBeFalse();
}

private static bool InvokeCanClose(ActionSheetDialogResultViewModel vm, ContentDialogResult result)
{
var method = typeof(ActionSheetDialogResultViewModel)
.GetMethod("CanClose", BindingFlags.NonPublic | BindingFlags.Instance);

method.ShouldNotBeNull("CanClose method should exist via reflection");

return (bool)method!.Invoke(vm, new object[] { result })!;
}

[Test]
public void ActionSheetItem_ExposesTitleAndValue()
{
var item = new ActionSheetItem("MyTitle", 42);

item.Title.ShouldBe("MyTitle");
item.Value.ShouldBe(42);
}

[Test]
public void ActionSheetItem_AllowsNullValue()
{
var item = new ActionSheetItem("MyTitle", null);

item.Value.ShouldBeNull();
}

}
}
Loading
Loading