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; /// /// Schránka pro (Ctrl+C / Ctrl+V) přes ClipboardContentBinding 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: beh:GridPaste.Enabled="True". /// /// - Ctrl+C: obálku výběru zkopíruje jako TSV (tab mezi sloupci, CRLF mezi řádky) a přes /// (s persistem) – Excel i vlastní grid ji pak převezmou. /// - Ctrl+V: 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 (bez edit /// módu), nekonvertibilní hodnota buňku tiše vynechá – shodně s Theming.GridFill. /// - Ctrl+D: „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. /// 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í /// Zkopíruje obálku výběru jako TSV na schránku (s persistem, ať ji převezme i Excel). 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) /// 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. 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; } /// 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. 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; } } /// 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 GridFill.CopyCell. 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é /// 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 .) 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(); // 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); } /// Kotva vkládání bloku: aktivní buňka (řádek, DisplayIndex sloupce), fallback první vybraná. 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 ColumnsByDisplay(DataGrid grid) => grid.Columns.OrderBy(c => c.DisplayIndex).ToList(); /// 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í. private static List 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; /// Je fokus v editačním prvku buňky? Pak Ctrl+C/V necháme na editoru. private static bool IsEditing(object? source) => source is System.Windows.Controls.Primitives.TextBoxBase or ComboBox; }