From 2f803db1c856d0c4cb7ea77cf1a99abf709bf771 Mon Sep 17 00:00:00 2001 From: Codex CLI Date: Wed, 27 Aug 2025 23:44:29 -0500 Subject: [PATCH] Add MVVM-pure clipboard copy for history: hover-only copy button with flyout on Desktop/Web and long-press/right-click context menu; View writes to clipboard via CopyRequested event --- .../ViewModels/MainViewModel.cs | 29 +++++ src/AdvancedCalculator/Views/MainView.axaml | 110 ++++++++++++++---- .../Views/MainView.axaml.cs | 41 ++++++- 3 files changed, 157 insertions(+), 23 deletions(-) diff --git a/src/AdvancedCalculator/ViewModels/MainViewModel.cs b/src/AdvancedCalculator/ViewModels/MainViewModel.cs index 9983f0e..011b3b7 100644 --- a/src/AdvancedCalculator/ViewModels/MainViewModel.cs +++ b/src/AdvancedCalculator/ViewModels/MainViewModel.cs @@ -11,6 +11,7 @@ namespace AdvancedCalculator.ViewModels; public partial class MainViewModel : ViewModelBase { private readonly ICalculatorService _calculatorService; + public event EventHandler? CopyRequested; public MainViewModel() : this(new CalculatorService()) @@ -97,4 +98,32 @@ public partial class MainViewModel : ViewModelBase InputText = string.Empty; SelectedHistoryIndex = History.Count - 1; } + + // Copy helpers (MVVM-pure): build text in VM, View handles clipboard via CopyRequested + [RelayCommand] + private void CopyHistoryInput(HistoryItem? item) + { + if (item is null || string.IsNullOrWhiteSpace(item.Input)) return; + CopyRequested?.Invoke(this, item.Input); + } + + [RelayCommand] + private void CopyHistoryOutput(HistoryItem? item) + { + if (item is null || string.IsNullOrWhiteSpace(item.Output)) return; + CopyRequested?.Invoke(this, item.Output); + } + + [RelayCommand] + private void CopyHistoryBoth(HistoryItem? item) + { + if (item is null) return; + var input = item.Input?.Trim(); + var output = item.Output?.Trim(); + if (string.IsNullOrEmpty(input) && string.IsNullOrEmpty(output)) return; + if (string.IsNullOrEmpty(input)) { CopyRequested?.Invoke(this, output!); return; } + if (string.IsNullOrEmpty(output)) { CopyRequested?.Invoke(this, input!); return; } + + CopyRequested?.Invoke(this, $"{input} = {output}"); + } } diff --git a/src/AdvancedCalculator/Views/MainView.axaml b/src/AdvancedCalculator/Views/MainView.axaml index eca810d..2a17de1 100644 --- a/src/AdvancedCalculator/Views/MainView.axaml +++ b/src/AdvancedCalculator/Views/MainView.axaml @@ -70,28 +70,94 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AdvancedCalculator/Views/MainView.axaml.cs b/src/AdvancedCalculator/Views/MainView.axaml.cs index 61e43cc..0f4853e 100644 --- a/src/AdvancedCalculator/Views/MainView.axaml.cs +++ b/src/AdvancedCalculator/Views/MainView.axaml.cs @@ -1,4 +1,7 @@ -using Avalonia.Controls; +using System; +using Avalonia; +using Avalonia.Controls; +using AdvancedCalculator.ViewModels; namespace AdvancedCalculator.Views; @@ -7,5 +10,41 @@ public partial class MainView : UserControl public MainView() { InitializeComponent(); + this.AttachedToVisualTree += OnAttachedToVisualTree; + this.DetachedFromVisualTree += OnDetachedFromVisualTree; + } + + private MainViewModel? _vm; + + private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + _vm = DataContext as MainViewModel; + if (_vm is not null) + { + _vm.CopyRequested += OnCopyRequested; + } + } + + private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + if (_vm is not null) + { + _vm.CopyRequested -= OnCopyRequested; + } + _vm = null; + } + + private async void OnCopyRequested(object? sender, string text) + { + var top = TopLevel.GetTopLevel(this); + if (top?.Clipboard is null) return; + try + { + await top.Clipboard.SetTextAsync(text); + } + catch + { + // Ignore clipboard errors; e.g., browser permission or missing gesture context + } } }