Files
Rozpisky/Rozpisky/Behaviors/GridPaste.cs
T
marekandClaude Opus 5 a6377ab583 Baseline: současný stav projektu Rozpisky
První commit — WPF aplikace pro generování výkresových rohových razítek
(.NET 10, SkiaSharp, ACadSharp, ClosedXML) včetně xUnit testů, DXF šablony,
log a dokumentace. Build výstupy (bin/, obj/, .vs/) jsou ignorovány.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 09:45:49 +02:00

238 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace Rozpisky.Behaviors;
/// <summary>
/// Schránka pro <see cref="DataGrid"/> (Ctrl+C / Ctrl+V) přes <c>ClipboardContentBinding</c> sloupců
/// WPF DataGrid vkládání nativně neumí a jeho kopírování se v jiných aplikacích (Excel) nemusí projevit
/// (data dává na schránku bez „flush"). Zapíná se per-grid: <c>beh:GridPaste.Enabled="True"</c>.
///
/// - <b>Ctrl+C</b>: obálku výběru zkopíruje jako TSV (tab mezi sloupci, CRLF mezi řádky) a přes
/// <see cref="Clipboard.SetText(string)"/> (s persistem) Excel i vlastní grid ji pak převezmou.
/// - <b>Ctrl+V</b>: schránku (TSV) vloží do buněk. Při vícebuňkovém výběru se oblast výběru vyplní
/// dlaždicově (vzor ze schránky se opakuje jako fill handle); jinak se blok vloží od aktivní buňky
/// doprava/dolů. Řádky se nepřidávají (přetečení se zahodí), mimo rozsah sloupců i read-only sloupce
/// se přeskočí. Zápis jde přes <see cref="DataGridColumn.OnPastingCellClipboardContent"/> (bez edit
/// módu), nekonvertibilní hodnota buňku tiše vynechá shodně s <c>Theming.GridFill</c>.
/// - <b>Ctrl+D</b>: „fill down" jako v Excelu při výběru přes víc řádků vyplní horní řádek výběru
/// dolů do zbytku výběru; u výběru v jednom řádku zkopíruje hodnoty z řádku nad ním. Kopíruje se
/// přes ClipboardContentBinding sloupce (bez edit módu), stejně jako fill handle.
///
/// Při otevřené editaci buňky se klávesa nechá projít (kopíruje/vkládá se text v editoru). Bez
/// CSV-uvozovkového escapování a bez Ctrl+Z shodně s fill handle.
/// </summary>
public static class GridPaste
{
public static readonly DependencyProperty EnabledProperty =
DependencyProperty.RegisterAttached("Enabled", typeof(bool), typeof(GridPaste),
new PropertyMetadata(false, OnEnabledChanged));
public static void SetEnabled(DependencyObject o, bool v) => o.SetValue(EnabledProperty, v);
public static bool GetEnabled(DependencyObject o) => (bool)o.GetValue(EnabledProperty);
private static void OnEnabledChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is not DataGrid grid) return;
if ((bool)e.NewValue) grid.PreviewKeyDown += OnKeyDown;
else grid.PreviewKeyDown -= OnKeyDown;
}
private static void OnKeyDown(object sender, KeyEventArgs e)
{
if ((Keyboard.Modifiers & ModifierKeys.Control) == 0) return;
if (sender is not DataGrid grid) return;
if (IsEditing(e.OriginalSource)) return; // rozeditovaná buňka → schránku řeší editor
if (e.Key == Key.C) { if (Copy(grid)) e.Handled = true; }
else if (e.Key == Key.V) { if (Paste(grid)) e.Handled = true; }
else if (e.Key == Key.D) { if (FillDown(grid)) e.Handled = true; }
}
// ---------------------------------------------------------------- kopírování
/// <summary>Zkopíruje obálku výběru jako TSV na schránku (s persistem, ať ji převezme i Excel).</summary>
private static bool Copy(DataGrid grid)
{
if (SelectionBounds(grid) is not (int r0, int r1, int c0, int c1)) return false;
var cols = ColumnsByDisplay(grid);
var sb = new StringBuilder();
for (int r = r0; r <= r1; r++)
{
if (r < 0 || r >= grid.Items.Count) continue;
var item = grid.Items[r];
if (item == CollectionView.NewItemPlaceholder) continue;
for (int c = c0; c <= c1; c++)
{
if (c > c0) sb.Append('\t');
if (c >= 0 && c < cols.Count)
sb.Append(cols[c].OnCopyingCellClipboardContent(item)?.ToString());
}
sb.Append("\r\n");
}
try { Clipboard.SetText(sb.ToString(), TextDataFormat.UnicodeText); return true; }
catch { return false; } // schránku drží jiný proces → nech případně proběhnout nativní kopii
}
// ---------------------------------------------------------------- vkládání
private static bool Paste(DataGrid grid)
{
if (grid.IsReadOnly) return false;
var text = Clipboard.ContainsText() ? Clipboard.GetText() : null;
if (string.IsNullOrEmpty(text)) return false;
grid.CommitEdit(DataGridEditingUnit.Row, true);
var data = ParseTsv(text);
if (data.Count == 0) return false;
var cols = ColumnsByDisplay(grid);
// Vícebuňkový výběr → vyplň jeho obálku dlaždicově vzorem ze schránky (jako fill handle);
// 1×1 schránka tím vyplní celý výběr, 1×N se v širším výběru opakuje.
if (SelectionBounds(grid) is (int r0, int r1, int c0, int c1) && (r1 > r0 || c1 > c0))
{
for (int r = r0; r <= r1; r++)
{
if (r < 0 || r >= grid.Items.Count) continue;
var item = grid.Items[r];
if (item == CollectionView.NewItemPlaceholder) continue;
var src = data[Mod(r - r0, data.Count)];
if (src.Length == 0) continue;
for (int c = c0; c <= c1; c++)
if (c >= 0 && c < cols.Count)
WriteCell(cols[c], item, src[Mod(c - c0, src.Length)]);
}
return true;
}
// Jedna buňka (nebo bez výběru) → vlož celý blok od aktivní buňky doprava/dolů.
if (Anchor(grid) is not (int ar, int ac)) return false;
for (int i = 0; i < data.Count; i++)
{
int r = ar + i;
if (r >= grid.Items.Count) break;
var item = grid.Items[r];
if (item == CollectionView.NewItemPlaceholder) continue;
var src = data[i];
for (int j = 0; j < src.Length; j++)
{
int c = ac + j;
if (c >= 0 && c < cols.Count) WriteCell(cols[c], item, src[j]);
}
}
return true;
}
// ---------------------------------------------------------------- fill down (Ctrl+D)
/// <summary>Excelovské „fill down": vícebuňkový výběr přes řádky vyplní horní řádek výběru dolů;
/// výběr v jediném řádku převezme hodnoty z řádku nad ním. Kopíruje se přes ClipboardContentBinding
/// sloupce (bez edit módu). Vrací true, když se aspoň něco dalo zapsat.</summary>
private static bool FillDown(DataGrid grid)
{
if (grid.IsReadOnly) return false;
if (SelectionBounds(grid) is not (int r0, int r1, int c0, int c1)) return false;
grid.CommitEdit(DataGridEditingUnit.Row, true);
var cols = ColumnsByDisplay(grid);
// Výběr v jednom řádku → zdroj je řádek nad výběrem (jinak horní řádek výběru).
int srcRow = r1 > r0 ? r0 : r0 - 1;
if (srcRow < 0 || srcRow >= grid.Items.Count) return false;
var srcItem = grid.Items[srcRow];
if (srcItem == CollectionView.NewItemPlaceholder) return false;
bool any = false;
for (int r = r1 > r0 ? r0 + 1 : r0; r <= r1; r++)
{
if (r < 0 || r >= grid.Items.Count) continue;
var item = grid.Items[r];
if (item == CollectionView.NewItemPlaceholder) continue;
for (int c = c0; c <= c1; c++)
if (c >= 0 && c < cols.Count && CopyCell(cols[c], srcItem, item)) any = true;
}
return any;
}
/// <summary>Zkopíruje hodnotu buňky ze zdrojové do cílové položky přes clipboard-binding sloupce
/// (bez edit módu). Read-only sloupec a nekonvertibilní hodnotu tiše vynechá. Vrací true při zápisu.</summary>
private static bool CopyCell(DataGridColumn column, object srcItem, object dstItem)
{
if (column.IsReadOnly) return false;
try { column.OnPastingCellClipboardContent(dstItem, column.OnCopyingCellClipboardContent(srcItem)); return true; }
catch { return false; }
}
/// <summary>Zapíše hodnotu do buňky přes clipboard-binding sloupce (bez edit módu). Read-only sloupec
/// a nekonvertibilní hodnotu tiše vynechá stejně jako <c>GridFill.CopyCell</c>.</summary>
private static void WriteCell(DataGridColumn column, object item, string value)
{
if (column.IsReadOnly) return;
try { column.OnPastingCellClipboardContent(item, value); }
catch { /* nekonvertibilní hodnota (např. text do bool/čísla) → buňku vynech */ }
}
// ---------------------------------------------------------------- pomocné
/// <summary>Obálka výběru: min/max index řádku a min/max DisplayIndex sloupce, nebo null.
/// (DisplayIndex je souvislý 0..n-1, takže odpovídá indexu do <see cref="ColumnsByDisplay"/>.)</summary>
private static (int r0, int r1, int c0, int c1)? SelectionBounds(DataGrid grid)
{
var cells = grid.SelectedCells;
if (cells.Count == 0) return null;
int r0 = int.MaxValue, r1 = -1, c0 = int.MaxValue, c1 = -1;
var rowIndex = new Dictionary<object, int>(); // Items.IndexOf je O(n), memoizuj
foreach (var cell in cells)
{
if (!cell.IsValid || cell.Column is null || cell.Item is null) continue;
if (cell.Item == CollectionView.NewItemPlaceholder) continue;
if (!rowIndex.TryGetValue(cell.Item, out int r))
rowIndex[cell.Item] = r = grid.Items.IndexOf(cell.Item);
if (r < 0) continue;
if (r < r0) r0 = r;
if (r > r1) r1 = r;
int c = cell.Column.DisplayIndex;
if (c < c0) c0 = c;
if (c > c1) c1 = c;
}
return r1 < 0 ? null : (r0, r1, c0, c1);
}
/// <summary>Kotva vkládání bloku: aktivní buňka (řádek, DisplayIndex sloupce), fallback první vybraná.</summary>
private static (int r, int c)? Anchor(DataGrid grid)
{
var cur = grid.CurrentCell;
if (cur.IsValid && cur.Column is not null && cur.Item is not null
&& cur.Item != CollectionView.NewItemPlaceholder)
{
int r = grid.Items.IndexOf(cur.Item);
if (r >= 0) return (r, cur.Column.DisplayIndex);
}
return SelectionBounds(grid) is (int r0, _, int c0, _) ? (r0, c0) : null;
}
private static List<DataGridColumn> ColumnsByDisplay(DataGrid grid)
=> grid.Columns.OrderBy(c => c.DisplayIndex).ToList();
/// <summary>Rozparsuje TSV: řádky dle CR/LF, buňky dle tabulátoru. Koncový prázdný řádek
/// (Excel přidává závěrečné CRLF) se zahodí.</summary>
private static List<string[]> ParseTsv(string text)
{
var radky = text.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n').ToList();
while (radky.Count > 0 && radky[^1].Length == 0) radky.RemoveAt(radky.Count - 1);
return radky.Select(r => r.Split('\t')).ToList();
}
private static int Mod(int a, int m) => ((a % m) + m) % m;
/// <summary>Je fokus v editačním prvku buňky? Pak Ctrl+C/V necháme na editoru.</summary>
private static bool IsEditing(object? source)
=> source is System.Windows.Controls.Primitives.TextBoxBase or ComboBox;
}