From 2e1e59c6911bfef6d9dc4fb5beb26d6444d8b4fc Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Thu, 23 Apr 2026 18:32:54 -0500 Subject: [PATCH] feat: add Ctrl+V shortcut for FileMaker paste Bind Ctrl+V at the Window level to PasteFileMakerClipData. Avalonia processes the focused control's KeyDown first and only routes to the Window's KeyBindings when the event is unhandled, so text inputs (TextBox, AvaloniaEdit) keep their normal text-paste behavior and the shortcut only fires when focus is outside an editable field. After paste, select the most recently added clip so the editor opens to it and the list scrolls to it (ListBox.AutoScrollToSelectedItem). Without this, a successful paste sent the clip to the bottom of the list with no visual confirmation. Improve MockClipboardService.GetFormatsAsync to surface the keys it holds, which unblocks paste-related tests. --- src/SharpFM/MainWindow.axaml | 1 + src/SharpFM/ViewModels/MainWindowViewModel.cs | 9 ++++++- .../ViewModels/MainWindowViewModelTests.cs | 27 ++++++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/SharpFM/MainWindow.axaml b/src/SharpFM/MainWindow.axaml index 4682e3e..36d6d91 100644 --- a/src/SharpFM/MainWindow.axaml +++ b/src/SharpFM/MainWindow.axaml @@ -22,6 +22,7 @@ + diff --git a/src/SharpFM/ViewModels/MainWindowViewModel.cs b/src/SharpFM/ViewModels/MainWindowViewModel.cs index 8a6486c..19993f8 100644 --- a/src/SharpFM/ViewModels/MainWindowViewModel.cs +++ b/src/SharpFM/ViewModels/MainWindowViewModel.cs @@ -230,6 +230,7 @@ public async Task PasteFileMakerClipData() { var formats = await _clipboard.GetFormatsAsync(); int count = 0; + ClipViewModel? lastAdded = null; foreach (var format in formats.Where(f => f.StartsWith("Mac-", StringComparison.CurrentCultureIgnoreCase)).Distinct()) { @@ -244,10 +245,16 @@ public async Task PasteFileMakerClipData() // don't add duplicates if (FileMakerClips.Any(k => k.Clip.XmlData == clip.XmlData)) continue; - FileMakerClips.Add(new ClipViewModel(clip)); + lastAdded = new ClipViewModel(clip); + FileMakerClips.Add(lastAdded); count++; } + if (lastAdded is not null) + { + SelectedClip = lastAdded; + } + ShowStatus(count > 0 ? $"Pasted {count} clip(s) from FileMaker" : "No FileMaker clips found on clipboard"); } catch (Exception e) diff --git a/tests/SharpFM.Tests/ViewModels/MainWindowViewModelTests.cs b/tests/SharpFM.Tests/ViewModels/MainWindowViewModelTests.cs index 6dad2d1..5f454d0 100644 --- a/tests/SharpFM.Tests/ViewModels/MainWindowViewModelTests.cs +++ b/tests/SharpFM.Tests/ViewModels/MainWindowViewModelTests.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; using System.Threading.Tasks; using Avalonia.Controls; using Microsoft.Extensions.Logging; @@ -21,7 +23,7 @@ public class MockClipboardService : IClipboardService public Task SetTextAsync(string text) { LastText = text; return Task.CompletedTask; } public Task SetDataAsync(string format, byte[] data) { LastFormat = format; LastData = data; return Task.CompletedTask; } - public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); + public Task GetFormatsAsync() => Task.FromResult(ClipboardData.Keys.ToArray()); public Task GetDataAsync(string format) => Task.FromResult(ClipboardData.TryGetValue(format, out var v) ? v : null); } @@ -113,6 +115,29 @@ public async Task PasteFileMakerClipData_NoFormats_ShowsStatus() Assert.Contains("No FileMaker clips found", vm.StatusMessage); } + [Fact] + public async Task PasteFileMakerClipData_SelectsLastPastedClip() + { + var clipboard = new MockClipboardService(); + clipboard.ClipboardData["Mac-XMSC"] = BuildClipBytes(""); + clipboard.ClipboardData["Mac-XMSS"] = BuildClipBytes(""); + var vm = CreateVm(clipboard); + var initialCount = vm.FileMakerClips.Count; + + await vm.PasteFileMakerClipData(); + + Assert.NotNull(vm.SelectedClip); + Assert.Equal(initialCount + 2, vm.FileMakerClips.Count); + Assert.Same(vm.FileMakerClips[^1], vm.SelectedClip); + Assert.Contains("Pasted 2 clip(s)", vm.StatusMessage); + } + + private static byte[] BuildClipBytes(string xml) + { + var payload = Encoding.UTF8.GetBytes(xml); + return BitConverter.GetBytes(payload.Length).Concat(payload).ToArray(); + } + [Fact] public void StatusMessage_NotifiesPropertyChanged() {