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:
@@ -0,0 +1,119 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Nonemm.Session;
|
||||
|
||||
/// The pivot laid out the way the statistics window prints it: N1MM's headings,
|
||||
/// one line per row with the label columns first and the `Tot` and `Accum`
|
||||
/// columns last, and the `Total` line at the bottom.
|
||||
public sealed class StatisticsTable
|
||||
{
|
||||
/// What goes between two columns in the text form.
|
||||
private const string Between = " ";
|
||||
|
||||
private StatisticsTable(
|
||||
IReadOnlyList<string> headings,
|
||||
IReadOnlyList<IReadOnlyList<string>> rows,
|
||||
int labelColumns)
|
||||
{
|
||||
Headings = headings;
|
||||
Rows = rows;
|
||||
LabelColumns = labelColumns;
|
||||
}
|
||||
|
||||
public static StatisticsTable Of(StatisticsPivot pivot)
|
||||
{
|
||||
List<string> headings =
|
||||
[
|
||||
.. pivot.Row.Headings,
|
||||
.. pivot.Columns.Select(Heading),
|
||||
"Tot",
|
||||
"Accum",
|
||||
];
|
||||
int labels = pivot.Row.Headings.Count;
|
||||
List<IReadOnlyList<string>> rows = [];
|
||||
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(Count(pivot.Count(down, across)));
|
||||
}
|
||||
cells.Add(Count(pivot.RowTotals[down]));
|
||||
cells.Add(Count(pivot.Accumulated[down]));
|
||||
rows.Add(cells);
|
||||
}
|
||||
|
||||
List<string> total = ["Total"];
|
||||
if (labels == 2)
|
||||
{
|
||||
total.Add("");
|
||||
}
|
||||
total.AddRange(pivot.ColumnTotals.Select(Count));
|
||||
total.Add(Count(pivot.Total));
|
||||
total.Add(Count(pivot.Total));
|
||||
rows.Add(total);
|
||||
return new StatisticsTable(headings, rows, labels);
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> Headings { get; }
|
||||
|
||||
/// Every line, the `Total` line last.
|
||||
public IReadOnlyList<IReadOnlyList<string>> Rows { get; }
|
||||
|
||||
/// How many columns at the left hold the row's own value rather than a
|
||||
/// count. Two for Hour and Mode & Band, one for every other field.
|
||||
public int LabelColumns { get; }
|
||||
|
||||
/// The table with spaces between the columns, so it lines up under a
|
||||
/// monospace font. The label columns are left-aligned and the counts are
|
||||
/// right-aligned, as they are on the screen.
|
||||
public string ToText()
|
||||
{
|
||||
int[] widths = new int[Headings.Count];
|
||||
for (int at = 0; at < Headings.Count; at++)
|
||||
{
|
||||
widths[at] = Math.Max(Headings[at].Length, Rows.Max(r => At(r, at).Length));
|
||||
}
|
||||
StringBuilder text = new();
|
||||
Line(text, Headings, widths);
|
||||
foreach (IReadOnlyList<string> row in Rows)
|
||||
{
|
||||
Line(text, row, widths);
|
||||
}
|
||||
return text.ToString();
|
||||
}
|
||||
|
||||
private void Line(StringBuilder text, IReadOnlyList<string> cells, int[] widths)
|
||||
{
|
||||
for (int at = 0; at < widths.Length; at++)
|
||||
{
|
||||
string cell = At(cells, at);
|
||||
text.Append(at < LabelColumns ? cell.PadRight(widths[at]) : cell.PadLeft(widths[at]));
|
||||
if (at < widths.Length - 1)
|
||||
{
|
||||
text.Append(Between);
|
||||
}
|
||||
}
|
||||
// a row of blanks at the right would otherwise leave the line ragged
|
||||
while (text.Length > 0 && text[^1] == ' ')
|
||||
{
|
||||
text.Length--;
|
||||
}
|
||||
text.Append('\n');
|
||||
}
|
||||
|
||||
private static string At(IReadOnlyList<string> cells, int at) => at < cells.Count ? cells[at] : "";
|
||||
|
||||
/// A column with no value of its own is headed the way the window heads it,
|
||||
/// so the text and the screen say the same thing.
|
||||
private static string Heading(PivotKey column) =>
|
||||
column.Label.Length > 0 ? column.Label : "(none)";
|
||||
|
||||
/// 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 Count(int count) => count == 0 ? "" : count.ToString();
|
||||
}
|
||||
Reference in New Issue
Block a user