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 public partial class MainViewModel : ViewModelBase
{ {
private readonly ICalculatorService _calculatorService; private readonly ICalculatorService _calculatorService;
public event EventHandler<string>? CopyRequested;
public MainViewModel() public MainViewModel()
: this(new CalculatorService()) : this(new CalculatorService())
@ -97,4 +98,32 @@ public partial class MainViewModel : ViewModelBase
InputText = string.Empty; InputText = string.Empty;
SelectedHistoryIndex = History.Count - 1; 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

@ -70,28 +70,94 @@
<SplitView.Content> <SplitView.Content>
<Grid RowDefinitions="*,Auto"> <Grid RowDefinitions="*,Auto">
<!-- History --> <!-- History -->
<Grid Grid.Row="0"> <Grid Grid.Row="0">
<ListBox ItemsSource="{Binding History}" SelectedIndex="{Binding SelectedHistoryIndex}" <ListBox ItemsSource="{Binding History}" SelectedIndex="{Binding SelectedHistoryIndex}"
AutomationProperties.Name="History list"> AutomationProperties.Name="History list">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="m:HistoryItem"> <DataTemplate x:DataType="m:HistoryItem">
<Grid ColumnDefinitions="Auto,*" Margin="4,2"> <!-- Root row container so we can bind IsPointerOver for hover-only controls -->
<TextBlock FontFamily="{StaticResource MDI}" Text="{x:Static m:IconFont.ArrowRightDropCircle}" <Grid x:Name="HistoryRow" ColumnDefinitions="Auto,*,Auto" Margin="4,2">
FontSize="{DynamicResource IconSizeM}" VerticalAlignment="Center" Margin="0,0,8,0" /> <TextBlock Grid.Column="0" FontFamily="{StaticResource MDI}" Text="{x:Static m:IconFont.ArrowRightDropCircle}"
<StackPanel Grid.Column="1"> FontSize="{DynamicResource IconSizeM}" VerticalAlignment="Center" Margin="0,0,8,0" />
<TextBlock Text="{Binding Input}" MaxLines="3" /> <StackPanel Grid.Column="1">
<TextBlock Text="{Binding Output}" FontWeight="Bold" MaxLines="2" /> <TextBlock Text="{Binding Input}" MaxLines="3" />
</StackPanel> <TextBlock Text="{Binding Output}" FontWeight="Bold" MaxLines="2" />
</Grid> </StackPanel>
</DataTemplate>
</ListBox.ItemTemplate> <!-- Hover-only copy button for Desktop/Web with options flyout -->
</ListBox> <Button Grid.Column="2"
<TextBlock Text="No history yet. Enter an expression and Evaluate." IsVisible="{Binding #HistoryRow.IsPointerOver}"
IsVisible="{Binding History.Count, Converter={StaticResource IsZeroConverter}}" Margin="8,0,0,0"
IsHitTestVisible="False" Padding="8,4"
Opacity="0.6" MinHeight="28"
HorizontalAlignment="Center" VerticalAlignment="Center" /> Background="Transparent"
</Grid> 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>
</ListBox>
<TextBlock Text="No history yet. Enter an expression and Evaluate."
IsVisible="{Binding History.Count, Converter={StaticResource IsZeroConverter}}"
IsHitTestVisible="False"
Opacity="0.6"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
<!-- Input Row --> <!-- Input Row -->
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,*,Auto" Margin="4"> <Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,*,Auto" Margin="4">

View file

@ -1,4 +1,7 @@
using Avalonia.Controls; using System;
using Avalonia;
using Avalonia.Controls;
using AdvancedCalculator.ViewModels;
namespace AdvancedCalculator.Views; namespace AdvancedCalculator.Views;
@ -7,5 +10,41 @@ public partial class MainView : UserControl
public MainView() public MainView()
{ {
InitializeComponent(); 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
}
} }
} }