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

This commit is contained in:
Codex CLI 2025-08-27 23:44:29 -05:00
commit 2f803db1c8
3 changed files with 157 additions and 23 deletions

View file

@ -11,6 +11,7 @@ namespace AdvancedCalculator.ViewModels;
public partial class MainViewModel : ViewModelBase
{
private readonly ICalculatorService _calculatorService;
public event EventHandler<string>? 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}");
}
}

View file

@ -75,13 +75,79 @@
AutomationProperties.Name="History list">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="m:HistoryItem">
<Grid ColumnDefinitions="Auto,*" Margin="4,2">
<TextBlock FontFamily="{StaticResource MDI}" Text="{x:Static m:IconFont.ArrowRightDropCircle}"
<!-- Root row container so we can bind IsPointerOver for hover-only controls -->
<Grid x:Name="HistoryRow" ColumnDefinitions="Auto,*,Auto" Margin="4,2">
<TextBlock Grid.Column="0" FontFamily="{StaticResource MDI}" Text="{x:Static m:IconFont.ArrowRightDropCircle}"
FontSize="{DynamicResource IconSizeM}" VerticalAlignment="Center" Margin="0,0,8,0" />
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding Input}" MaxLines="3" />
<TextBlock Text="{Binding Output}" FontWeight="Bold" MaxLines="2" />
</StackPanel>
<!-- Hover-only copy button for Desktop/Web with options flyout -->
<Button Grid.Column="2"
IsVisible="{Binding #HistoryRow.IsPointerOver}"
Margin="8,0,0,0"
Padding="8,4"
MinHeight="28"
Background="Transparent"
AutomationProperties.Name="Copy options">
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedRight">
<StackPanel>
<Button x:DataType="vm:MainViewModel"
DataContext="{Binding #Root.DataContext}"
Command="{Binding CopyHistoryInputCommand}"
CommandParameter="{Binding #HistoryRow.DataContext}"
AutomationProperties.Name="Copy input"
Background="Transparent" BorderThickness="0" Padding="8,4">
<TextBlock Text="Copy Input"/>
</Button>
<Button x:DataType="vm:MainViewModel"
DataContext="{Binding #Root.DataContext}"
Command="{Binding CopyHistoryOutputCommand}"
CommandParameter="{Binding #HistoryRow.DataContext}"
AutomationProperties.Name="Copy output"
Background="Transparent" BorderThickness="0" Padding="8,4">
<TextBlock Text="Copy Output"/>
</Button>
<Button x:DataType="vm:MainViewModel"
DataContext="{Binding #Root.DataContext}"
Command="{Binding CopyHistoryBothCommand}"
CommandParameter="{Binding #HistoryRow.DataContext}"
AutomationProperties.Name="Copy input and output"
Background="Transparent" BorderThickness="0" Padding="8,4">
<TextBlock Text="Copy Input = Output"/>
</Button>
</StackPanel>
</Flyout>
</Button.Flyout>
<TextBlock Text="Copy"/>
</Button>
<!-- Context menu for right-click / long-press (Android) -->
<Grid.ContextMenu>
<ContextMenu>
<MenuItem x:DataType="vm:MainViewModel"
DataContext="{Binding #Root.DataContext}"
Header="Copy Input"
Command="{Binding CopyHistoryInputCommand}"
CommandParameter="{Binding #HistoryRow.DataContext}"
/>
<MenuItem x:DataType="vm:MainViewModel"
DataContext="{Binding #Root.DataContext}"
Header="Copy Output"
Command="{Binding CopyHistoryOutputCommand}"
CommandParameter="{Binding #HistoryRow.DataContext}"
/>
<MenuItem x:DataType="vm:MainViewModel"
DataContext="{Binding #Root.DataContext}"
Header="Copy Input = Output"
Command="{Binding CopyHistoryBothCommand}"
CommandParameter="{Binding #HistoryRow.DataContext}"
/>
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>

View file

@ -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
}
}
}