private void toString(int?maxWidth, Action <string> outputString, Action <ConsoleColoredString> outputColoredString) { int rows = _cells.Count; if (rows == 0) { return; } int cols = _cells.Max(row => row.Count); // Create a lookup array which, for each column, and for each possible value of colspan, tells you which cells in that column have this colspan and end in this column var cellsByColspan = new SortedDictionary <int, List <int> > [cols]; for (var col = 0; col < cols; col++) { var cellsInThisColumn = new SortedDictionary <int, List <int> >(); for (int row = 0; row < rows; row++) { if (col >= _cells[row].Count) { continue; } var cel = _cells[row][col]; if (cel == null) { continue; } if (cel is surrogateCell && ((surrogateCell)cel).RealRow != row) { continue; } int realCol = cel is surrogateCell ? ((surrogateCell)cel).RealCol : col; var realCell = (trueCell)_cells[row][realCol]; if (realCol + realCell.ColSpan - 1 != col) { continue; } cellsInThisColumn.AddSafe(realCell.ColSpan, row); } cellsByColspan[col] = cellsInThisColumn; } // Find out the width that each column would have if the text wasn't wrapped. // If this fits into the total width, then we want each column to be at least this wide. var columnWidths = generateColumnWidths(cols, cellsByColspan, c => Math.Max(1, c.LongestParagraph())); var unwrapped = true; // If the table is now too wide, use the length of the longest word, or longest paragraph if nowrap if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing) { columnWidths = generateColumnWidths(cols, cellsByColspan, c => Math.Max(1, c.MinWidth())); unwrapped = false; } // If the table is still too wide, use the length of the longest paragraph if nowrap, otherwise 0 if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing) { columnWidths = generateColumnWidths(cols, cellsByColspan, c => c.NoWrap ? Math.Max(1, c.LongestParagraph()) : 1); } // If the table is still too wide, we will have to wrap like crazy. if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing) { columnWidths = new int[cols]; for (int i = 0; i < cols; i++) { columnWidths[i] = 1; } } // If the table is STILL too wide, all bets are off. if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing) { throw new InvalidOperationException(@"The specified table width is too narrow. It is not possible to fit the {0} columns and the column spacing of {1} per column into a total width of {2} characters.".Fmt(cols, ColumnSpacing, maxWidth)); } // If we have any extra width to spare... var missingTotalWidth = maxWidth == null ? 0 : maxWidth - columnWidths.Sum() - (cols - 1) * ColumnSpacing; if (missingTotalWidth > 0 && (UseFullWidth || !unwrapped)) { // Use the length of the longest paragraph in each column to calculate a proportion by which to enlarge each column var widthProportionByCol = new int[cols]; for (var col = 0; col < cols; col++) { foreach (var kvp in cellsByColspan[col]) { distributeEvenly( widthProportionByCol, col, kvp.Key, kvp.Value.Max(row => ((trueCell)_cells[row][col - kvp.Key + 1]).LongestParagraph()) - widthProportionByCol.Skip(col - kvp.Key + 1).Take(kvp.Key).Sum() - (unwrapped ? 0 : columnWidths.Skip(col - kvp.Key + 1).Take(kvp.Key).Sum()) ); } } var widthProportionTotal = widthProportionByCol.Sum(); // Adjust the width of the columns according to the calculated proportions so that they fill the missing width. // We do this in two steps. Step one: enlarge the column widths by the integer part of the calculated portion (round down). // After this the width remaining will be smaller than the number of columns, so each column is missing at most 1 character. var widthRemaining = missingTotalWidth; var fractionalParts = new double[cols]; for (int col = 0; col < cols; col++) { var widthToAdd = (double)(widthProportionByCol[col] * missingTotalWidth) / widthProportionTotal; var integerPart = (int)widthToAdd; columnWidths[col] += integerPart; fractionalParts[col] = widthToAdd - integerPart; widthRemaining -= integerPart; } // Step two: enlarge a few more columns by 1 character so that we reach the desired width. // The columns with the largest fractional parts here are the furthest from what we ideally want, so we favour those. foreach (var elem in fractionalParts.Select((frac, col) => new { Value = frac, Col = col }).OrderByDescending(e => e.Value)) { if (widthRemaining < 1) { break; } columnWidths[elem.Col]++; widthRemaining--; } } // Word-wrap all the contents of all the cells trueCell truCel; foreach (var row in _cells) { for (int col = 0; col < row.Count; col++) { if ((truCel = row[col] as trueCell) != null) { truCel.Wordwrap(columnWidths.Skip(col).Take(truCel.ColSpan).Sum() + (truCel.ColSpan - 1) * ColumnSpacing); } } } // Calculate the string index for each column var strIndexByCol = new int[cols + 1]; for (var i = 0; i < cols; i++) { strIndexByCol[i + 1] = strIndexByCol[i] + columnWidths[i] + ColumnSpacing; } var realWidth = strIndexByCol[cols] - ColumnSpacing; // Make sure we don't render rules if we can't bool verticalRules = VerticalRules && ColumnSpacing > 0; bool horizontalRules = HorizontalRules && RowSpacing > 0; // If we do render vertical rules, where should it be (at which string offset, counted backwards from the end of the column spacing) var vertRuleOffset = (ColumnSpacing + 1) / 2; // Finally, render the entire output List <ConsoleColoredString> currentLine = null; for (int row = 0; row < rows; row++) { var rowList = _cells[row]; var extraRows = RowSpacing + 1; var isFirstIteration = true; bool anyMoreContentInThisRow; do { ConsoleColoredString previousLine = currentLine == null ? null : new ConsoleColoredString(currentLine.ToArray()); currentLine = new List <ConsoleColoredString>(); anyMoreContentInThisRow = false; for (int col = 0; col < cols; col++) { var cel = col < rowList.Count ? rowList[col] : null; // For cells with colspan, consider only the first cell they're spanning and skip the rest if (cel is surrogateCell && ((surrogateCell)cel).RealCol != col) { continue; } // If the cell has rowspan, what row did this cell start in? var valueRow = cel is surrogateCell ? ((surrogateCell)cel).RealRow : row; // Retrieve the data for the cell var realCell = col < _cells[valueRow].Count ? (trueCell)_cells[valueRow][col] : null; var colspan = realCell == null ? 1 : realCell.ColSpan; var rowspan = realCell == null ? 1 : realCell.RowSpan; var rowBackground = row >= _rowBackgrounds.Count ? null : _rowBackgrounds[row]; // Does this cell end in this row? var isLastRow = valueRow + rowspan - 1 == row; // If we are inside the cell, render one line of the contents of the cell if (realCell != null && realCell.WordwrappedValue.Length > realCell.WordwrappedIndex) { var align = realCell.Alignment ?? DefaultAlignment; var curLineLength = currentLine.Sum(c => c.Length); var cellBackground = realCell.Background ?? rowBackground; if (strIndexByCol[col] > curLineLength) { currentLine.Add(new string(' ', strIndexByCol[col] - curLineLength).Color(null, cellBackground)); } object textRaw = realCell.WordwrappedValue[realCell.WordwrappedIndex]; ConsoleColoredString text = textRaw is ConsoleColoredString ? (ConsoleColoredString)textRaw : (string)textRaw; // implicit conversion to ConsoleColoredString if (align == HorizontalTextAlignment.Center) { currentLine.Add(new string(' ', (strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length) / 2).Color(null, cellBackground)); } else if (align == HorizontalTextAlignment.Right) { currentLine.Add(new string(' ', strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length).Color(null, cellBackground)); } if (cellBackground == null) { currentLine.Add(text); } else { currentLine.Add(text.ColorBackgroundWhereNull(cellBackground.Value)); if (align == HorizontalTextAlignment.Center) { currentLine.Add(new string(' ', (strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length + 1) / 2).Color(null, cellBackground)); } else if (align == HorizontalTextAlignment.Left) { currentLine.Add(new string(' ', strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length).Color(null, cellBackground)); } } realCell.WordwrappedIndex++; } // If we are at the end of a row, render horizontal rules var horizRuleStart = col > 0 ? strIndexByCol[col] - vertRuleOffset + 1 : 0; var horizRuleEnd = (col + colspan < cols) ? strIndexByCol[col + colspan] - vertRuleOffset + (verticalRules ? 0 : 1) : realWidth; var renderingHorizontalRules = horizontalRules && isLastRow && extraRows == 1; if (renderingHorizontalRules) { currentLine.Add(new string(' ', horizRuleStart - currentLine.Sum(c => c.Length))); currentLine.Add(new string((row == HeaderRows - 1) ? '=' : '-', horizRuleEnd - horizRuleStart)); } else { var subtract = (col + colspan == cols ? ColumnSpacing : vertRuleOffset) + currentLine.Sum(c => c.Length); currentLine.Add(new string(' ', strIndexByCol[col + colspan] - subtract).Color(null, (realCell == null ? null : realCell.Background) ?? rowBackground)); } // If we are at the beginning of a row, render the horizontal rules for the row above by modifying the previous line. // We want to do this because it may have an unwanted vertical rule if this is a cell with colspan and there are // multiple cells with smaller colspans above it. if (isFirstIteration && horizontalRules && row > 0 && cel is trueCell) { previousLine = new ConsoleColoredString(previousLine.Substring(0, horizRuleStart), new string((row == HeaderRows) ? '=' : '-', horizRuleEnd - horizRuleStart), previousLine.Substring(horizRuleEnd)); } // Render vertical rules if (verticalRules && (col + colspan < cols)) { currentLine.Add((new string(' ', strIndexByCol[col + colspan] - vertRuleOffset - currentLine.Sum(c => c.Length)) + "|").Color(null, renderingHorizontalRules ? null : rowBackground)); } // Does this cell still contain any more content that needs to be output before this row can be finished? anyMoreContentInThisRow = anyMoreContentInThisRow || (realCell != null && isLastRow && realCell.WordwrappedValue.Length > realCell.WordwrappedIndex); } if (previousLine != null) { if (LeftMargin > 0) { outputString(new string(' ', LeftMargin)); } outputColoredString(previousLine); outputString(Environment.NewLine); } isFirstIteration = false; // If none of the cells in this row contain any more content, start counting down the row spacing if (!anyMoreContentInThisRow) { extraRows--; } }while (anyMoreContentInThisRow || (extraRows > 0 && row < rows - 1)); } // Output the last line if (LeftMargin > 0) { outputString(new string(' ', LeftMargin)); } outputColoredString(new ConsoleColoredString(currentLine.ToArray())); outputString(Environment.NewLine); }
private void DiscordNamesToolStripMenuItem_Click(object sender, EventArgs e) { var regex = new Regex(@"(?:^|\n*)(?:(?:[^\n:]|\|)*(?:dc|discord)[ \t\v\f\r]*:?[ \t\v\f\r]*)?([^\n]*#\d{4})", RegexOptions.Multiline | RegexOptions.IgnoreCase); // .*#(\d{4}) var confirmResult = MessageBox.Show( $"This will search through all your friends statuses and extract the ones that might be discord names (matching {regex.ToString()}).\n\nYou can select multiple rows and copy them with [CTRL] + [C]" , "Search for discord names?", MessageBoxButtons.OKCancel, MessageBoxIcon.Question); if (confirmResult != DialogResult.OK) { return; } var dict = new SortedDictionary <string, string>(); foreach (TreeNode node in tree_users.Nodes.GetAllChilds()) { var tag = node.Tag; if (tag is null) { continue; } var Tag = tag as TreeNodeTag; if (Tag.userBriefResponse is null) { continue; } if (!string.IsNullOrWhiteSpace(Tag.userBriefResponse.statusDescription)) { foreach (Match m in regex.Matches(input: Tag.userBriefResponse.statusDescription)) { dict.AddSafe(Tag.userBriefResponse.displayName.Trim(), m.Value.Trim()); } } } if (dict.Count < 1) { MessageBox.Show("Sorry, we could not find any discord names in your friends statuses :c"); return; } dict.OrderBy(kv => kv.Key); Form form = new Form(); form.Text = $"Discord names of your friends ({dict.Count})"; form.Height *= 2; form.Width *= 3; form.Icon = ActiveForm.Icon; DataTable DiscordNames = new DataTable("DiscordNames"); DiscordNames.Columns.Clear(); DataColumn c0 = new DataColumn("Ingame Name"); c0.ReadOnly = true; DiscordNames.Columns.Add(c0); DataColumn c1 = new DataColumn("Status"); /* c1.ReadOnly = true; */ DiscordNames.Columns.Add(c1); foreach (var user in dict) { DataRow row = DiscordNames.NewRow(); row[c0.ColumnName] = user.Key; row[c1.ColumnName] = user.Value; DiscordNames.Rows.Add(row); } var table = new DataGridView(); table.Dock = DockStyle.Fill; table.DataSource = DiscordNames; form.Controls.Add(table); form.Show(); table.Columns[c0.ColumnName].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; table.Columns[c1.ColumnName].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; table.Sort(table.Columns[c0.ColumnName], ListSortDirection.Ascending); }
private void toString(int? maxWidth, Action<string> outputString, Action<ConsoleColoredString> outputColoredString) { int rows = _cells.Count; if (rows == 0) return; int cols = _cells.Max(row => row.Count); // Create a lookup array which, for each column, and for each possible value of colspan, tells you which cells in that column have this colspan and end in this column var cellsByColspan = new SortedDictionary<int, List<int>>[cols]; for (var col = 0; col < cols; col++) { var cellsInThisColumn = new SortedDictionary<int, List<int>>(); for (int row = 0; row < rows; row++) { if (col >= _cells[row].Count) continue; var cel = _cells[row][col]; if (cel == null) continue; if (cel is surrogateCell && ((surrogateCell) cel).RealRow != row) continue; int realCol = cel is surrogateCell ? ((surrogateCell) cel).RealCol : col; var realCell = (trueCell) _cells[row][realCol]; if (realCol + realCell.ColSpan - 1 != col) continue; cellsInThisColumn.AddSafe(realCell.ColSpan, row); } cellsByColspan[col] = cellsInThisColumn; } // Find out the width that each column would have if the text wasn't wrapped. // If this fits into the total width, then we want each column to be at least this wide. var columnWidths = generateColumnWidths(cols, cellsByColspan, c => Math.Max(1, c.LongestParagraph())); var unwrapped = true; // If the table is now too wide, use the length of the longest word, or longest paragraph if nowrap if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing) { columnWidths = generateColumnWidths(cols, cellsByColspan, c => Math.Max(1, c.MinWidth())); unwrapped = false; } // If the table is still too wide, use the length of the longest paragraph if nowrap, otherwise 0 if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing) columnWidths = generateColumnWidths(cols, cellsByColspan, c => c.NoWrap ? Math.Max(1, c.LongestParagraph()) : 1); // If the table is still too wide, we will have to wrap like crazy. if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing) { columnWidths = new int[cols]; for (int i = 0; i < cols; i++) columnWidths[i] = 1; } // If the table is STILL too wide, all bets are off. if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing) throw new InvalidOperationException(@"The specified table width is too narrow. It is not possible to fit the {0} columns and the column spacing of {1} per column into a total width of {2} characters.".Fmt(cols, ColumnSpacing, maxWidth)); // If we have any extra width to spare... var missingTotalWidth = maxWidth == null ? 0 : maxWidth - columnWidths.Sum() - (cols - 1) * ColumnSpacing; if (missingTotalWidth > 0 && (UseFullWidth || !unwrapped)) { // Use the length of the longest paragraph in each column to calculate a proportion by which to enlarge each column var widthProportionByCol = new int[cols]; for (var col = 0; col < cols; col++) foreach (var kvp in cellsByColspan[col]) distributeEvenly( widthProportionByCol, col, kvp.Key, kvp.Value.Max(row => ((trueCell) _cells[row][col - kvp.Key + 1]).LongestParagraph()) - widthProportionByCol.Skip(col - kvp.Key + 1).Take(kvp.Key).Sum() - (unwrapped ? 0 : columnWidths.Skip(col - kvp.Key + 1).Take(kvp.Key).Sum()) ); var widthProportionTotal = widthProportionByCol.Sum(); // Adjust the width of the columns according to the calculated proportions so that they fill the missing width. // We do this in two steps. Step one: enlarge the column widths by the integer part of the calculated portion (round down). // After this the width remaining will be smaller than the number of columns, so each column is missing at most 1 character. var widthRemaining = missingTotalWidth; var fractionalParts = new double[cols]; for (int col = 0; col < cols; col++) { var widthToAdd = (double) (widthProportionByCol[col] * missingTotalWidth) / widthProportionTotal; var integerPart = (int) widthToAdd; columnWidths[col] += integerPart; fractionalParts[col] = widthToAdd - integerPart; widthRemaining -= integerPart; } // Step two: enlarge a few more columns by 1 character so that we reach the desired width. // The columns with the largest fractional parts here are the furthest from what we ideally want, so we favour those. foreach (var elem in fractionalParts.Select((frac, col) => new { Value = frac, Col = col }).OrderByDescending(e => e.Value)) { if (widthRemaining < 1) break; columnWidths[elem.Col]++; widthRemaining--; } } // Word-wrap all the contents of all the cells trueCell truCel; foreach (var row in _cells) for (int col = 0; col < row.Count; col++) if ((truCel = row[col] as trueCell) != null) truCel.Wordwrap(columnWidths.Skip(col).Take(truCel.ColSpan).Sum() + (truCel.ColSpan - 1) * ColumnSpacing); // Calculate the string index for each column var strIndexByCol = new int[cols + 1]; for (var i = 0; i < cols; i++) strIndexByCol[i + 1] = strIndexByCol[i] + columnWidths[i] + ColumnSpacing; var realWidth = strIndexByCol[cols] - ColumnSpacing; // Make sure we don't render rules if we can't bool verticalRules = VerticalRules && ColumnSpacing > 0; bool horizontalRules = HorizontalRules && RowSpacing > 0; // If we do render vertical rules, where should it be (at which string offset, counted backwards from the end of the column spacing) var vertRuleOffset = (ColumnSpacing + 1) / 2; // Finally, render the entire output List<ConsoleColoredString> currentLine = null; for (int row = 0; row < rows; row++) { var rowList = _cells[row]; var extraRows = RowSpacing + 1; var isFirstIteration = true; bool anyMoreContentInThisRow; do { ConsoleColoredString previousLine = currentLine == null ? null : new ConsoleColoredString(currentLine.ToArray()); currentLine = new List<ConsoleColoredString>(); anyMoreContentInThisRow = false; for (int col = 0; col < cols; col++) { var cel = col < rowList.Count ? rowList[col] : null; // For cells with colspan, consider only the first cell they're spanning and skip the rest if (cel is surrogateCell && ((surrogateCell) cel).RealCol != col) continue; // If the cell has rowspan, what row did this cell start in? var valueRow = cel is surrogateCell ? ((surrogateCell) cel).RealRow : row; // Retrieve the data for the cell var realCell = col < _cells[valueRow].Count ? (trueCell) _cells[valueRow][col] : null; var colspan = realCell == null ? 1 : realCell.ColSpan; var rowspan = realCell == null ? 1 : realCell.RowSpan; var rowBackground = row >= _rowBackgrounds.Count ? null : _rowBackgrounds[row]; // Does this cell end in this row? var isLastRow = valueRow + rowspan - 1 == row; // If we are inside the cell, render one line of the contents of the cell if (realCell != null && realCell.WordwrappedValue.Length > realCell.WordwrappedIndex) { var align = realCell.Alignment ?? DefaultAlignment; var curLineLength = currentLine.Sum(c => c.Length); var cellBackground = realCell.Background ?? rowBackground; if (strIndexByCol[col] > curLineLength) currentLine.Add(new string(' ', strIndexByCol[col] - curLineLength).Color(null, cellBackground)); object textRaw = realCell.WordwrappedValue[realCell.WordwrappedIndex]; ConsoleColoredString text = textRaw is ConsoleColoredString ? (ConsoleColoredString) textRaw : (string) textRaw; // implicit conversion to ConsoleColoredString if (align == HorizontalTextAlignment.Center) currentLine.Add(new string(' ', (strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length) / 2).Color(null, cellBackground)); else if (align == HorizontalTextAlignment.Right) currentLine.Add(new string(' ', strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length).Color(null, cellBackground)); if (cellBackground == null) currentLine.Add(text); else { currentLine.Add(text.ColorBackgroundWhereNull(cellBackground.Value)); if (align == HorizontalTextAlignment.Center) currentLine.Add(new string(' ', (strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length + 1) / 2).Color(null, cellBackground)); else if (align == HorizontalTextAlignment.Left) currentLine.Add(new string(' ', strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length).Color(null, cellBackground)); } realCell.WordwrappedIndex++; } // If we are at the end of a row, render horizontal rules var horizRuleStart = col > 0 ? strIndexByCol[col] - vertRuleOffset + 1 : 0; var horizRuleEnd = (col + colspan < cols) ? strIndexByCol[col + colspan] - vertRuleOffset + (verticalRules ? 0 : 1) : realWidth; var renderingHorizontalRules = horizontalRules && isLastRow && extraRows == 1; if (renderingHorizontalRules) { currentLine.Add(new string(' ', horizRuleStart - currentLine.Sum(c => c.Length))); currentLine.Add(new string((row == HeaderRows - 1) ? '=' : '-', horizRuleEnd - horizRuleStart)); } else { var subtract = (col + colspan == cols ? ColumnSpacing : vertRuleOffset) + currentLine.Sum(c => c.Length); currentLine.Add(new string(' ', strIndexByCol[col + colspan] - subtract).Color(null, (realCell == null ? null : realCell.Background) ?? rowBackground)); } // If we are at the beginning of a row, render the horizontal rules for the row above by modifying the previous line. // We want to do this because it may have an unwanted vertical rule if this is a cell with colspan and there are // multiple cells with smaller colspans above it. if (isFirstIteration && horizontalRules && row > 0 && cel is trueCell) previousLine = new ConsoleColoredString(previousLine.Substring(0, horizRuleStart), new string((row == HeaderRows) ? '=' : '-', horizRuleEnd - horizRuleStart), previousLine.Substring(horizRuleEnd)); // Render vertical rules if (verticalRules && (col + colspan < cols)) currentLine.Add((new string(' ', strIndexByCol[col + colspan] - vertRuleOffset - currentLine.Sum(c => c.Length)) + "|").Color(null, renderingHorizontalRules ? null : rowBackground)); // Does this cell still contain any more content that needs to be output before this row can be finished? anyMoreContentInThisRow = anyMoreContentInThisRow || (realCell != null && isLastRow && realCell.WordwrappedValue.Length > realCell.WordwrappedIndex); } if (previousLine != null) { if (LeftMargin > 0) outputString(new string(' ', LeftMargin)); outputColoredString(previousLine); outputString(Environment.NewLine); } isFirstIteration = false; // If none of the cells in this row contain any more content, start counting down the row spacing if (!anyMoreContentInThisRow) extraRows--; } while (anyMoreContentInThisRow || (extraRows > 0 && row < rows - 1)); } // Output the last line if (LeftMargin > 0) outputString(new string(' ', LeftMargin)); outputColoredString(new ConsoleColoredString(currentLine.ToArray())); outputString(Environment.NewLine); }