Copy the statistics table and graph to the clipboard
Copy All takes whichever tab is open, as N1MM's does: the table off the table tab, a picture of the graph off the graph tab. Copy Table as Text and Copy Graph as Image do either one from either tab. `StatisticsTable` lays the pivot out as the window prints it — the headings, the rows, the Total line — and pads it into columns for the text form, so the grid, the clipboard and Print to File all say the same thing. X11 asks for a picture when the other program pastes rather than when it is copied, so the bitmap given to the clipboard is held until the clipboard is given a different one. Disposing it at the end of the copy killed the program on the next paste; the table pasted into a second Avalonia program reads back lined up, and the picture reads back as a 916x438 bitmap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
This commit is contained in:
@@ -7,6 +7,8 @@
|
||||
<Menu DockPanel.Dock="Top">
|
||||
<MenuItem Header="_File">
|
||||
<MenuItem Header="Copy All" Click="OnCopyAll" InputGesture="Ctrl+C" />
|
||||
<MenuItem Header="Copy Table as Text" Click="OnCopyTable" />
|
||||
<MenuItem Header="Copy Graph as Image" Click="OnCopyGraph" />
|
||||
<MenuItem Name="PrintToFileItem" Header="Print to File…" Click="OnPrintToFile" />
|
||||
<MenuItem Name="SaveGraphItem" Header="Save Graph…" Click="OnSaveGraph" />
|
||||
<MenuItem Name="ShowTitleItem" Header="Show Title" ToggleType="CheckBox"
|
||||
|
||||
@@ -26,6 +26,13 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
||||
/// does not count as the operator choosing it.
|
||||
private bool loading = true;
|
||||
|
||||
/// The table as it is printed, kept so the copy and print actions do not
|
||||
/// read the values back out of the grid.
|
||||
private StatisticsTable? laid;
|
||||
|
||||
/// The picture the clipboard is showing, if any.
|
||||
private RenderTargetBitmap? copied;
|
||||
|
||||
public StatisticsWindow(AppSession session)
|
||||
{
|
||||
this.session = session;
|
||||
@@ -66,6 +73,7 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
||||
Chart.Columns = [];
|
||||
Chart.Series = [];
|
||||
Chart.InvalidateVisual();
|
||||
laid = null;
|
||||
Empty("no contest is open");
|
||||
return;
|
||||
}
|
||||
@@ -86,6 +94,7 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
||||
Chart.Columns = [];
|
||||
Chart.Series = [];
|
||||
Chart.InvalidateVisual();
|
||||
laid = null;
|
||||
Empty("no contacts are counted");
|
||||
return;
|
||||
}
|
||||
@@ -104,63 +113,22 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
||||
|
||||
private void FillTable(StatisticsPivot pivot)
|
||||
{
|
||||
List<string> headers =
|
||||
[
|
||||
.. pivot.Row.Headings,
|
||||
.. pivot.Columns.Select(c => c.Label.Length > 0 ? c.Label : "(none)"),
|
||||
"Tot",
|
||||
"Accum",
|
||||
];
|
||||
laid = StatisticsTable.Of(pivot);
|
||||
Table.Columns.Clear();
|
||||
for (int at = 0; at < headers.Count; at++)
|
||||
for (int at = 0; at < laid.Headings.Count; at++)
|
||||
{
|
||||
Table.Columns.Add(new DataGridTextColumn
|
||||
{
|
||||
Header = headers[at],
|
||||
Header = laid.Headings[at],
|
||||
Width = DataGridLength.Auto,
|
||||
Binding = new Binding($"[{at}]"),
|
||||
});
|
||||
}
|
||||
Table.ItemsSource = Lines(pivot);
|
||||
Table.ItemsSource = laid.Rows
|
||||
.Select((cells, at) => new StatisticsRow(cells, isTotal: at == laid.Rows.Count - 1))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<StatisticsRow> Lines(StatisticsPivot pivot)
|
||||
{
|
||||
List<StatisticsRow> lines = [];
|
||||
int labels = pivot.Row.Headings.Count;
|
||||
for (int down = 0; down < pivot.Rows.Count; down++)
|
||||
{
|
||||
List<string> cells = [pivot.Rows[down].First];
|
||||
if (labels == 2)
|
||||
{
|
||||
cells.Add(pivot.Rows[down].Second);
|
||||
}
|
||||
for (int across = 0; across < pivot.Columns.Count; across++)
|
||||
{
|
||||
cells.Add(Number(pivot.Count(down, across)));
|
||||
}
|
||||
cells.Add(Number(pivot.RowTotals[down]));
|
||||
cells.Add(Number(pivot.Accumulated[down]));
|
||||
lines.Add(new StatisticsRow(cells));
|
||||
}
|
||||
|
||||
List<string> total = ["Total"];
|
||||
if (labels == 2)
|
||||
{
|
||||
total.Add("");
|
||||
}
|
||||
total.AddRange(pivot.ColumnTotals.Select(Number));
|
||||
total.Add(Number(pivot.Total));
|
||||
total.Add(Number(pivot.Total));
|
||||
lines.Add(new StatisticsRow(total, isTotal: true));
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// N1MM leaves a cell with nothing in it blank rather than printing a zero,
|
||||
/// so the shape of the operating shows through the table.
|
||||
private static string Number(int count) =>
|
||||
count == 0 ? "" : count.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
private void FillChart(StatisticsPivot pivot)
|
||||
{
|
||||
Chart.Series = [.. pivot.Columns.Select(c => c.Label)];
|
||||
@@ -249,12 +217,43 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// N1MM's Copy All takes whichever tab is open: the table off the table
|
||||
/// tab, a picture of the graph off the graph tab.
|
||||
private async void OnCopyAll(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Clipboard is { } clipboard)
|
||||
if (Tabs.SelectedIndex == 1)
|
||||
{
|
||||
await clipboard.SetTextAsync(AsText(", "));
|
||||
await CopyGraph();
|
||||
return;
|
||||
}
|
||||
await CopyTable();
|
||||
}
|
||||
|
||||
private async void OnCopyTable(object? sender, RoutedEventArgs e) => await CopyTable();
|
||||
|
||||
private async void OnCopyGraph(object? sender, RoutedEventArgs e) => await CopyGraph();
|
||||
|
||||
private async Task CopyTable()
|
||||
{
|
||||
if (Clipboard is { } clipboard && laid is not null)
|
||||
{
|
||||
await clipboard.SetTextAsync(laid.ToText());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CopyGraph()
|
||||
{
|
||||
if (Clipboard is not { } clipboard)
|
||||
{
|
||||
return;
|
||||
}
|
||||
RenderTargetBitmap picture = Picture();
|
||||
await clipboard.SetBitmapAsync(picture);
|
||||
// X11 does not take a copy: it asks this program for the bytes when
|
||||
// another one pastes, so the picture has to stay alive until the
|
||||
// clipboard holds a different one
|
||||
copied?.Dispose();
|
||||
copied = picture;
|
||||
}
|
||||
|
||||
private async void OnPrintToFile(object? sender, RoutedEventArgs e)
|
||||
@@ -265,9 +264,9 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
||||
SuggestedFileName = "statistics.txt",
|
||||
SuggestedStartLocation = await Folder(session.Paths.ExportFiles),
|
||||
});
|
||||
if (file?.TryGetLocalPath() is { } path)
|
||||
if (file?.TryGetLocalPath() is { } path && laid is not null)
|
||||
{
|
||||
await File.WriteAllTextAsync(path, AsText("\t"));
|
||||
await File.WriteAllTextAsync(path, laid.ToText());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,30 +282,21 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
||||
{
|
||||
return;
|
||||
}
|
||||
using RenderTargetBitmap picture = Picture();
|
||||
picture.Save(path, new PngBitmapEncoderOptions());
|
||||
}
|
||||
|
||||
/// The graph as a picture, at the size it is on the screen.
|
||||
private RenderTargetBitmap Picture()
|
||||
{
|
||||
PixelSize size = Chart.Bounds.Width >= 1 && Chart.Bounds.Height >= 1
|
||||
? new PixelSize((int)Chart.Bounds.Width, (int)Chart.Bounds.Height)
|
||||
: GraphSize;
|
||||
using RenderTargetBitmap bitmap = new(size);
|
||||
bitmap.Render(Chart);
|
||||
bitmap.Save(path, new PngBitmapEncoderOptions());
|
||||
RenderTargetBitmap picture = new(size);
|
||||
picture.Render(Chart);
|
||||
return picture;
|
||||
}
|
||||
|
||||
private async Task<IStorageFolder?> Folder(string path) =>
|
||||
Directory.Exists(path) ? await StorageProvider.TryGetFolderFromPathAsync(path) : null;
|
||||
|
||||
/// The table as text, one line per row. N1MM's Copy All separates the
|
||||
/// values with commas and its Print to File lines the columns up.
|
||||
private string AsText(string between)
|
||||
{
|
||||
StringBuilder text = new();
|
||||
text.AppendLine(string.Join(between, Table.Columns.Select(c => c.Header)));
|
||||
if (Table.ItemsSource is IEnumerable<StatisticsRow> lines)
|
||||
{
|
||||
foreach (StatisticsRow line in lines)
|
||||
{
|
||||
text.AppendLine(string.Join(between, line.Cells));
|
||||
}
|
||||
}
|
||||
return text.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user