using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; using Core.Common.Controls.Swf.Properties; using Core.Common.Utils; using log4net; namespace Core.Common.Controls.Swf.Table { /// /// Class add copy paste functionality to a tableview. Based on ITableView /// public class TableViewPasteController : ITableViewPasteController { public event EventHandler> PasteFailed; public event EventHandler PasteFinished; private const int NewRowSelectedIndex = int.MinValue + 1; private static readonly ILog Log = LogManager.GetLogger(typeof(TableViewPasteController)); public TableViewPasteController(TableView tableView) { TableView = tableView; PastedRows = new List(); PastedBlocks = new List(); } /// /// Gets or sets the paste behaviour value /// public TableViewPasteBehaviourOptions PasteBehaviour { get; set; } public bool IsPasting { get; private set; } /// /// This method does most of the work. It Pastes Clipboard content to the XtraGrid. /// If OptionsSelection.EnableAppearanceFocusedRow == true then it will paste row (if /// any row is present in the clipboard) to the currently selected row. It will not /// replace Readonly or Not Editable Fields as well as key fields for the child tables /// and details. It works not on the XtraGrid level but on the DataTable level. /// If the currently selected row in the XtraGrid is the New Row then it will populate /// it with the values from the row in the Clipboard. /// public void PasteClipboardContents() { // for now only allow to paste text if (!Clipboard.ContainsText()) { Log.Debug(Resources.TableViewPasteController_PasteClipboardContents_Clipboard_does_not_contain_text_so_it_cannot_be_pasted_to_the_grid); return; } string[] clipboardLines = GetClipboardLines(); //nothing to paste. if (clipboardLines.Length == 0) { return; } PasteLines(clipboardLines); } /// /// Paste value string into tableview at current selection /// /// public void PasteLines(string[] lines) { IsPasting = true; lines = RemoveHeaderIfPresent(lines); //get the selection in which we are to paste var targetSelection = GetPasteTargetSelection(); //check if we can paste or fail string message; if (!CanPaste(lines, out message)) { OnPasteFailed(message); IsPasting = false; return; //don't paste } //pastevalues returns the pasted selection var tableViewImpl = TableView as TableView; if (tableViewImpl != null) { tableViewImpl.BeginInit(); } PastedRows.Clear(); PastedBlocks.Clear(); PasteValues(lines, targetSelection, (!TableView.IsSorted()) && (TableView.AllowAddNewRow)); if (tableViewImpl != null) { tableViewImpl.EndInit(); } SetSelection(); PastedRows.Clear(); PastedBlocks.Clear(); IsPasting = false; OnPasteFinished(); } protected TableView TableView { get; private set; } protected List PastedBlocks { private get; set; } /// /// Overload without message parameter. /// /// protected RectangleSelection GetPasteTargetSelection() { string message; return GetPasteTargetSelection(out message); } /// /// Gets the target selection from tableview or raises paste failed event in case /// the target selection is invalid (non-rectangular) /// /// protected RectangleSelection GetPasteTargetSelection(out string errorMessage) { errorMessage = ""; var topLeft = new TableViewCell(0, null); //in RowSelect don't used selected cells because they don't work if (TableView.RowSelect) { return GetRowSelectSelection(); } var cells = TableView.SelectedCells; if (cells.Count == 0) { //base selection on focused cell var focusedCell = TableView.GetFocusedCell(); if (focusedCell != null) { return new RectangleSelection(focusedCell, focusedCell); } //no focused cell must mean empty table..start pasting left,top return new RectangleSelection(topLeft, topLeft); } //return a selection based on the first and last cell in the selection. Tricky..should check the most upper etc.. RectangleSelection selection = GetRectangleSelection(cells); if (selection == null) { errorMessage = Resources.TableViewPasteController_GetPasteTargetSelection_Cannot_paste_into_non_rectangular_selection; } return selection; } protected RectangleSelection GetRowSelectSelection() { var selectedRows = TableView.SelectedRowsIndices; var topLeft = selectedRows.Length == 0 ? new TableViewCell(TableView.RowCount, TableView.GetColumnByDisplayIndex(0)) : new TableViewCell(selectedRows[0], TableView.GetColumnByDisplayIndex(0)); return new RectangleSelection(topLeft, topLeft); } /// /// Returns rectangle for a collection of cells. Or null if cells don't make up a rectangle /// /// /// protected RectangleSelection GetRectangleSelection(IList cells) { int left = cells.Min(cell => cell.Column.DisplayIndex); int right = cells.Max(cell => cell.Column.DisplayIndex); int top = cells.Min(cell => cell.RowIndex); int bottom = cells.Max(cell => cell.RowIndex); //if the selection is on 'new' row change to rowcount.. if ((bottom == NewRowSelectedIndex) && (top == NewRowSelectedIndex)) { bottom = TableView.RowCount; top = TableView.RowCount; } //do a check if we have enought cells... if (cells.Count != (right + 1 - left)*(bottom + 1 - top)) { return null; } return new RectangleSelection(new TableViewCell(top, TableView.GetColumnByDisplayIndex(left)), new TableViewCell(bottom, TableView.GetColumnByDisplayIndex(right))); } /// /// Checks whether the target tableView is filtered or the target selection contains a sorted column /// /// /// /// protected virtual bool CanPaste(string[] clipboardLines, out string errorMessage) { //get a targetselection. Can fail if target is non square. Then error message is filled var targetSelection = GetPasteTargetSelection(out errorMessage); if (targetSelection == null) { return false; } if (clipboardLines.Length == 0) { errorMessage = Resources.TableViewPasteController_CanPaste_There_are_no_values_to_paste_Headers_are_skipped; return false; } //how many columns do if hit when we paste? var pasteColumnSpan = SplitToCells(clipboardLines[0]).Length; int[] pastedColumnIndexes = Enumerable.Range(targetSelection.Left, pasteColumnSpan).ToArray(); //is there sorted column in our pasted columns? var sortedColumnExists = (from col in TableView.Columns where col.SortOrder != SortOrder.None && pastedColumnIndexes.Contains(col.DisplayIndex) select col).Any(); if (sortedColumnExists) { errorMessage = Resources.TableViewPasteController_CanPaste_Cannot_paste_into_sorted_column; //todo: add name of column here? return false; } if (TableView.Columns.Any(col => !string.IsNullOrEmpty(col.FilterString))) { errorMessage = Resources.TableViewPasteController_CanPaste_Cannot_paste_into_filtered_tableview; //todo: add name of column here? return false; } return true; } /// /// Pastes values into target selection. /// protected virtual void PasteValues(string[] clipboardLines, RectangleSelection targetSelection, bool allowNewRows, bool wrapInEditAction = true) { if (wrapInEditAction) { PasteValuesCore(clipboardLines, targetSelection, allowNewRows); } else { PasteValuesCore(clipboardLines, targetSelection, allowNewRows); } } /// /// Pastes the given contents to the table at row startRowIndex, increasing the number of rows if necessary and allowed. /// /// The row index at which the pasting effectively occured, -1 if no pasting was done. protected int PasteCellsToRow(string[] content, int startRowIndex, int startColumnIndex, int pasteWidth, bool addNewRow, bool cellBased) { var index = startRowIndex; if (addNewRow) { TableView.AddNewRowToDataSource(); } else { if (startRowIndex >= TableView.RowCount) { return -1; } } var exceptionMode = TableView.ExceptionMode; TableView.ExceptionMode = TableView.ValidationExceptionMode.ThrowException; //throw exception try { if (cellBased) { var contentWidth = content.Length; for (var i = 0; i < pasteWidth; i++) { if (!SafeSetCellValue(index, startColumnIndex + i, content[i%contentWidth])) { Log.ErrorFormat(Resources.TableViewPasteController_PasteCellsToRow_Can_not_paste_value_into_cell_0_1_Row_0_will_be_skipped, startRowIndex, startColumnIndex + i); if (addNewRow) { TableView.SelectRow(index); TableView.DeleteCurrentSelection(); } return -1; } } UpdatePastedBlocks(startColumnIndex, pasteWidth, index); } else { var contentWidth = content.Length; var values = new List(); for (var i = 0; i < pasteWidth; i++) { values.Add(content[i%contentWidth]); } if (!SafeSetRowCellValues(index, startColumnIndex, values)) { Log.ErrorFormat(Resources.TableViewPasteController_PasteCellsToRow_Skipping_invalid_row_0_from_pasting, startRowIndex); if (addNewRow) { TableView.SelectRow(index); TableView.DeleteCurrentSelection(); } return -1; } PastedRows.Add(index); } } catch (Exception e) { Log.ErrorFormat(Resources.TableViewPasteController_PasteCellsToRow_Pasting_values_failed_0_, e.Message); } finally { TableView.ExceptionMode = exceptionMode; } return index; } protected static string[] SplitToCells(string p) { //tab delimited data: excel, and xtragrid copy (todo think about how to implement pasting of space delimited data) return p.Split(new[] { "\t" }, StringSplitOptions.None); } private List PastedRows { get; set; } private string[] RemoveHeaderIfPresent(string[] lines) { if (lines.Length > 0 && string.Equals(lines[0], GetTableHeaderString())) { return lines.Skip(1).ToArray(); //skip header } return lines; } private string GetTableHeaderString() { //The function xtragrid uses to get the header string is protected and uses dxColumn.GetTextCaption(), //which may differ from dxColumn.GetCaption we use. In those cases this may break. Also if the ordering //of columns is out of sync (can that happen?) return String.Join("\t", TableView.Columns.Where(c => c.Visible).Select(c => c.Caption).ToArray()); } private void OnPasteFailed(string message) { Log.Warn(message); if (PasteFailed != null) { PasteFailed(this, new EventArgs(message)); } } private void OnPasteFinished() { if (PasteFinished != null) { PasteFinished(this, new EventArgs()); } } private void PasteValuesCore(string[] clipboardLines, RectangleSelection targetSelection, bool allowNewRows) { var startRowIndex = targetSelection.Top; var startColumnIndex = targetSelection.Left; if (startRowIndex < 0) { throw new ArgumentException(string.Format(Resources.TableViewPasteController_PasteValuesCore_Invalid_row_number_0_, startRowIndex), "targetSelection"); } //paste all the lines once... int lastRowIndex = Math.Max(startRowIndex + clipboardLines.Length - 1, targetSelection.Bottom); int currentClipBoardLineIndex = 0; int canceledNewRows = 0; for (int j = startRowIndex; j <= lastRowIndex; j++) { string[] cols = SplitToCells(clipboardLines[currentClipBoardLineIndex]); //Simply set values as text. var pasteWidth = Math.Max(cols.Length, targetSelection.Right - targetSelection.Left + 1); //paste cannot be wider than tablewidth - startColumnIndex pasteWidth = Math.Min(pasteWidth, TableView.Columns.Count(c => c.Visible) - startColumnIndex); //do the pasting bool checkCells = (PasteBehaviour == TableViewPasteBehaviourOptions.SkipRowWhenValueIsInvalid); bool addNewRow = allowNewRows && (j >= TableView.RowCount); var pasteStartRowIndex = j - canceledNewRows; var pasteResult = PasteCellsToRow(cols, pasteStartRowIndex, startColumnIndex, pasteWidth, addNewRow, checkCells); if (addNewRow && pasteResult == -1) { canceledNewRows++; } //update clipboard line index..loop currentClipBoardLineIndex++; if (currentClipBoardLineIndex == clipboardLines.Length) { //break;//do really want repeat data like below? currentClipBoardLineIndex = 0; } } } private bool SafeSetRowCellValues(int index, int startColumnIndex, List values) { try { return TableView.SetRowCellValues(index, startColumnIndex, values.ToArray()); } catch (Exception e) { Log.ErrorFormat(Resources.TableViewPasteController_SafeSetCellValue_Invalid_row_reason_0_, e.Message); return false; } } private bool SafeSetCellValue(int index, int columnIndex, string value) { try { return TableView.SetCellValue(index, columnIndex, value); } catch (Exception e) { Log.ErrorFormat(Resources.TableViewPasteController_SafeSetCellValue_Invalid_row_reason_0_, e.Message); return false; } } private void UpdatePastedBlocks(int startColumnIndex, int pasteWidth, int index) { var topLeftCell = new TableViewCell(index, TableView.GetColumnByDisplayIndex(startColumnIndex)); var bottomRightCell = new TableViewCell(index, TableView.GetColumnByDisplayIndex(startColumnIndex + pasteWidth - 1)); if (PastedBlocks.Count == 0) { PastedBlocks.Add(new RectangleSelection(topLeftCell, bottomRightCell)); } else { // if we can extend a rectangular block, do it... var selectBlock = PastedBlocks.Where(b => b.Bottom + 1 == index); if (selectBlock.Any()) { selectBlock.First().Bottom++; } else { PastedBlocks.Add(new RectangleSelection(topLeftCell, bottomRightCell)); } } } private void SetSelection() { if (PastedRows.Count != 0) { TableView.ClearSelection(); TableView.SelectRows(PastedRows.ToArray()); } if (PastedBlocks.Count != 0) { TableView.ClearSelection(); foreach (var selection in PastedBlocks) { TableView.SelectCells(selection.Top, selection.Left, selection.Bottom, selection.Right, false); } } } private static string[] GetClipboardLines() { var strPasteText = Clipboard.GetText(); var lines = strPasteText.Split(new[] { "\r\n" }, StringSplitOptions.None); return RemoveLastLineIfEmpty(lines); } private static string[] RemoveLastLineIfEmpty(string[] lines) { if (lines[lines.Length - 1] == "") { return lines.ToList().Take(lines.Length - 1).ToArray(); } return lines; } /// /// Rectangular selection on a tableview. /// protected class RectangleSelection { public RectangleSelection(TableViewCell topLeft, TableViewCell bottomRight) { Top = topLeft.RowIndex; Left = topLeft.Column == null ? 0 : topLeft.Column.DisplayIndex; Bottom = bottomRight.RowIndex; Right = bottomRight.Column == null ? 0 : bottomRight.Column.DisplayIndex; } public int Top { get; set; } public int Left { get; set; } public int Bottom { get; set; } public int Right { get; set; } } } }