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>
238 lines
11 KiB
C#
238 lines
11 KiB
C#
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;
|
||
}
|