using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; namespace BizHawk.Client.EmuHawk { /// /// A performant VirtualListView implementation that doesn't rely on native Win32 API calls /// (and in fact does not inherit the ListView class at all) /// It is an enhanced version of the work done with GDI+ rendering in InputRoll.cs /// ------------------------------ /// *** GDI+ Rendering Methods *** /// ------------------------------ /// public partial class PlatformAgnosticVirtualListView { // reusable Pen and Brush objects private Pen sPen = null; private Brush sBrush = null; /// /// Called when font sizes are changed /// Recalculates cell sizes /// private void SetCharSize() { using (var g = CreateGraphics()) { var sizeC = Size.Round(g.MeasureString("A", ColumnHeaderFont)); var sizeI = Size.Round(g.MeasureString("A", CellFont)); if (sizeC.Width > sizeI.Width) _charSize = sizeC; else _charSize = sizeI; } UpdateCellSize(); ColumnWidth = CellWidth; ColumnHeight = CellHeight + 2; } /// /// We draw everthing manually and never call base.OnPaint() /// /// protected override void OnPaint(PaintEventArgs e) { // white background sBrush = new SolidBrush(Color.White); sPen = new Pen(Color.White); Rectangle rect = e.ClipRectangle; e.Graphics.FillRectangle(sBrush, rect); e.Graphics.Flush(); var visibleColumns = _columns.VisibleColumns.ToList(); if (visibleColumns.Any()) { DrawColumnBg(e, visibleColumns); DrawColumnText(e, visibleColumns); } // Background DrawBg(e, visibleColumns); // Foreground DrawData(e, visibleColumns); DrawColumnDrag(e); DrawCellDrag(e); if (BorderSize > 0) { // paint parent border if (this.Parent != null) { // apparently mono can sometimes call OnPaint before attached to the parent?? using (var gParent = this.Parent.CreateGraphics()) { Pen borderPen = new Pen(BorderColor); for (int b = 1, c = 1; b <= BorderSize; b++, c += 2) { gParent.DrawRectangle(borderPen, this.Left - b, this.Top - b, this.Width + c, this.Height + c); } } } } } private void DrawColumnDrag(PaintEventArgs e) { if (_draggingCell != null) { var text = ""; int offsetX = 0; int offsetY = 0; QueryItemText?.Invoke(_draggingCell.RowIndex.Value, _columns.IndexOf(_draggingCell.Column), out text); QueryItemTextAdvanced?.Invoke(_draggingCell.RowIndex.Value, _draggingCell.Column, out text, ref offsetX, ref offsetY); Color bgColor = ColumnHeaderBackgroundColor; QueryItemBkColor?.Invoke(_draggingCell.RowIndex.Value, _columns.IndexOf(_draggingCell.Column), ref bgColor); QueryItemBkColorAdvanced?.Invoke(_draggingCell.RowIndex.Value, _draggingCell.Column, ref bgColor); int x1 = _currentX.Value - (_draggingCell.Column.Width.Value / 2); int y1 = _currentY.Value - (CellHeight / 2); int x2 = x1 + _draggingCell.Column.Width.Value; int y2 = y1 + CellHeight; sBrush = new SolidBrush(bgColor); e.Graphics.FillRectangle(sBrush, x1, y1, x2 - x1, y2 - y1); sBrush = new SolidBrush(ColumnHeaderFontColor); e.Graphics.DrawString(text, ColumnHeaderFont, sBrush, (PointF)(new Point(x1 + CellWidthPadding + offsetX, y1 + CellHeightPadding + offsetY))); } } private void DrawCellDrag(PaintEventArgs e) { if (_draggingCell != null) { var text = ""; int offsetX = 0; int offsetY = 0; QueryItemText?.Invoke(_draggingCell.RowIndex.Value, _columns.IndexOf(_draggingCell.Column), out text); QueryItemTextAdvanced?.Invoke(_draggingCell.RowIndex.Value, _draggingCell.Column, out text, ref offsetX, ref offsetY); Color bgColor = CellBackgroundColor; QueryItemBkColor?.Invoke(_draggingCell.RowIndex.Value, _columns.IndexOf(_draggingCell.Column), ref bgColor); QueryItemBkColorAdvanced?.Invoke(_draggingCell.RowIndex.Value, _draggingCell.Column, ref bgColor); int x1 = _currentX.Value - (_draggingCell.Column.Width.Value / 2); int y1 = _currentY.Value - (CellHeight / 2); int x2 = x1 + _draggingCell.Column.Width.Value; int y2 = y1 + CellHeight; sBrush = new SolidBrush(bgColor); e.Graphics.FillRectangle(sBrush, x1, y1, x2 - x1, y2 - y1); sBrush = new SolidBrush(CellFontColor); e.Graphics.DrawString(text, CellFont, sBrush, (PointF)(new Point(x1 + CellWidthPadding + offsetX, y1 + CellHeightPadding + offsetY))); } } private void DrawColumnText(PaintEventArgs e, List visibleColumns) { sBrush = new SolidBrush(ColumnHeaderFontColor); foreach (var column in visibleColumns) { var point = new Point(column.Left.Value + CellWidthPadding - _hBar.Value, CellHeightPadding); // TODO: fix this CellPadding issue (2 * CellPadding vs just CellPadding) string t = column.Text; ResizeTextToFit(ref t, column.Width.Value, ColumnHeaderFont); if (IsHoveringOnColumnCell && column == CurrentCell.Column) { sBrush = new SolidBrush(InvertColor(ColumnHeaderBackgroundHighlightColor)); e.Graphics.DrawString(t, ColumnHeaderFont, sBrush, (PointF)(point)); sBrush = new SolidBrush(ColumnHeaderFontColor); } else { e.Graphics.DrawString(t, ColumnHeaderFont, sBrush, (PointF)(point)); } } } private void DrawData(PaintEventArgs e, List visibleColumns) { // Prevent exceptions with small windows if (visibleColumns.Count == 0) { return; } if (QueryItemText != null || QueryItemTextAdvanced != null) { int startRow = FirstVisibleRow; int range = Math.Min(LastVisibleRow, ItemCount - 1) - startRow + 1; sBrush = new SolidBrush(CellFontColor); int xPadding = CellWidthPadding + 1 - _hBar.Value; for (int i = 0, f = 0; f < range; i++, f++) // Vertical { //f += _lagFrames[i]; int LastVisible = LastVisibleColumnIndex; for (int j = FirstVisibleColumn; j <= LastVisible; j++) // Horizontal { ListColumn col = visibleColumns[j]; string text = ""; int strOffsetX = 0; int strOffsetY = 0; Point point = new Point(col.Left.Value + xPadding, RowsToPixels(i) + CellHeightPadding); Bitmap image = null; int bitmapOffsetX = 0; int bitmapOffsetY = 0; QueryItemIcon?.Invoke(f + startRow, visibleColumns[j], ref image, ref bitmapOffsetX, ref bitmapOffsetY); if (image != null) { e.Graphics.DrawImage(image, new Point(point.X + bitmapOffsetX, point.Y + bitmapOffsetY + CellHeightPadding)); } QueryItemText?.Invoke(f + startRow, _columns.IndexOf(visibleColumns[j]), out text); QueryItemTextAdvanced?.Invoke(f + startRow, visibleColumns[j], out text, ref strOffsetX, ref strOffsetY); bool rePrep = false; if (_selectedItems.Contains(new Cell { Column = visibleColumns[j], RowIndex = f + startRow })) { sBrush = new SolidBrush(InvertColor(CellBackgroundHighlightColor)); rePrep = true; } if (!string.IsNullOrWhiteSpace(text)) { ResizeTextToFit(ref text, col.Width.Value, CellFont); e.Graphics.DrawString(text, CellFont, sBrush, (PointF)(new Point(point.X + strOffsetX, point.Y + strOffsetY))); } if (rePrep) { sBrush = new SolidBrush(CellFontColor); } } } } } private void ResizeTextToFit(ref string text, int destinationSize, Font font) { Size strLen; using (var g = CreateGraphics()) { strLen = Size.Round(g.MeasureString(text, font)); } if (strLen.Width > destinationSize - CellWidthPadding) { // text needs trimming List chars = new List(); for (int s = 0; s < text.Length; s++) { chars.Add(text[s]); Size tS; Size dotS; using (var g = CreateGraphics()) { tS = Size.Round(g.MeasureString(new string(chars.ToArray()), CellFont)); dotS = Size.Round(g.MeasureString(".", CellFont)); } int dotWidth = dotS.Width * 3; if (tS.Width >= destinationSize - CellWidthPadding - dotWidth) { text = new string(chars.ToArray()) + "..."; break; } } } } // https://stackoverflow.com/a/34107015/6813055 private Color InvertColor(Color color) { var inverted = Color.FromArgb(color.ToArgb() ^ 0xffffff); if (inverted.R > 110 && inverted.R < 150 && inverted.G > 110 && inverted.G < 150 && inverted.B > 110 && inverted.B < 150) { int avg = (inverted.R + inverted.G + inverted.B) / 3; avg = avg > 128 ? 200 : 60; inverted = Color.FromArgb(avg, avg, avg); } return inverted; } private void DrawColumnBg(PaintEventArgs e, List visibleColumns) { sBrush = new SolidBrush(ColumnHeaderBackgroundColor); sPen = new Pen(ColumnHeaderOutlineColor); int bottomEdge = RowsToPixels(0); // Gray column box and black line underneath e.Graphics.FillRectangle(sBrush, 0, 0, Width + 1, bottomEdge + 1); e.Graphics.DrawLine(sPen, 0, 0, TotalColWidth.Value + 1, 0); e.Graphics.DrawLine(sPen, 0, bottomEdge, TotalColWidth.Value + 1, bottomEdge); // Vertical black seperators for (int i = 0; i < visibleColumns.Count; i++) { int pos = visibleColumns[i].Left.Value - _hBar.Value; e.Graphics.DrawLine(sPen, pos, 0, pos, bottomEdge); } // Draw right most line if (visibleColumns.Any()) { int right = TotalColWidth.Value - _hBar.Value; e.Graphics.DrawLine(sPen, right, 0, right, bottomEdge); } // Emphasis foreach (var column in visibleColumns.Where(c => c.Emphasis)) { sBrush = new SolidBrush(SystemColors.ActiveBorder); e.Graphics.FillRectangle(sBrush, column.Left.Value + 1 - _hBar.Value, 1, column.Width.Value - 1, ColumnHeight - 1); } // If the user is hovering over a column if (IsHoveringOnColumnCell) { // TODO multiple selected columns for (int i = 0; i < visibleColumns.Count; i++) { if (visibleColumns[i] == CurrentCell.Column) { // Left of column is to the right of the viewable area or right of column is to the left of the viewable area if (visibleColumns[i].Left.Value - _hBar.Value > Width || visibleColumns[i].Right.Value - _hBar.Value < 0) { continue; } int left = visibleColumns[i].Left.Value - _hBar.Value; int width = visibleColumns[i].Right.Value - _hBar.Value - left; if (CurrentCell.Column.Emphasis) { sBrush = new SolidBrush(Color.FromArgb(ColumnHeaderBackgroundHighlightColor.ToArgb() + 0x00550000)); } else { sBrush = new SolidBrush(ColumnHeaderBackgroundHighlightColor); } e.Graphics.FillRectangle(sBrush, left + 1, 1, width - 1, ColumnHeight - 1); } } } } // TODO refactor this and DoBackGroundCallback functions. /// /// Draw Gridlines and background colors using QueryItemBkColor. /// private void DrawBg(PaintEventArgs e, List visibleColumns) { if (UseCustomBackground && QueryItemBkColor != null) { DoBackGroundCallback(e, visibleColumns); } if (GridLines) { sPen = new Pen(GridLineColor); // Columns int y = ColumnHeight + 1; int? totalColWidth = TotalColWidth; foreach (var column in visibleColumns) { int x = column.Left.Value - _hBar.Value; e.Graphics.DrawLine(sPen, x, y, x, Height - 1); } if (visibleColumns.Any()) { e.Graphics.DrawLine(sPen, totalColWidth.Value - _hBar.Value, y, totalColWidth.Value - _hBar.Value, Height - 1); } // Rows for (int i = 1; i < VisibleRows + 1; i++) { e.Graphics.DrawLine(sPen, 0, RowsToPixels(i), Width + 1, RowsToPixels(i)); } } if (_selectedItems.Any() && !HideSelection) { DoSelectionBG(e, visibleColumns); } } /// /// Given a cell with rowindex inbetween 0 and VisibleRows, it draws the background color specified. Do not call with absolute rowindices. /// private void DrawCellBG(PaintEventArgs e, Color color, Cell cell, List visibleColumns) { int x, y, w, h; w = cell.Column.Width.Value - 1; x = cell.Column.Left.Value - _hBar.Value + 1; y = RowsToPixels(cell.RowIndex.Value) + 1; // We can't draw without row and column, so assume they exist and fail catastrophically if they don't h = CellHeight - 1; if (y < ColumnHeight) { return; } if (x > DrawWidth || y > DrawHeight) { return; } // Don't draw if off screen. var col = cell.Column.Name; if (color.A == 0) { sBrush = new SolidBrush(Color.FromArgb(255, color)); } else { sBrush = new SolidBrush(color); } e.Graphics.FillRectangle(sBrush, x, y, w, h); } protected override void OnPaintBackground(PaintEventArgs pevent) { // Do nothing, and this should never be called } private void DoSelectionBG(PaintEventArgs e, List visibleColumns) { // SuuperW: This allows user to see other colors in selected frames. Color rowColor = CellBackgroundColor; // Color.White; int _lastVisibleRow = LastVisibleRow; int lastRow = -1; foreach (Cell cell in _selectedItems) { if (cell.RowIndex > _lastVisibleRow || cell.RowIndex < FirstVisibleRow || !VisibleColumns.Contains(cell.Column)) { continue; } Cell relativeCell = new Cell { RowIndex = cell.RowIndex - FirstVisibleRow, Column = cell.Column, }; if (QueryRowBkColor != null && lastRow != cell.RowIndex.Value) { QueryRowBkColor(cell.RowIndex.Value, ref rowColor); lastRow = cell.RowIndex.Value; } Color cellColor = rowColor; QueryItemBkColor?.Invoke(cell.RowIndex.Value, _columns.IndexOf(cell.Column), ref cellColor); QueryItemBkColorAdvanced?.Invoke(cell.RowIndex.Value, cell.Column, ref cellColor); // Alpha layering for cell before selection float alpha = (float)cellColor.A / 255; if (cellColor.A != 255 && cellColor.A != 0) { cellColor = Color.FromArgb(rowColor.R - (int)((rowColor.R - cellColor.R) * alpha), rowColor.G - (int)((rowColor.G - cellColor.G) * alpha), rowColor.B - (int)((rowColor.B - cellColor.B) * alpha)); } // Alpha layering for selection alpha = 0.85f; cellColor = Color.FromArgb(cellColor.R - (int)((cellColor.R - CellBackgroundHighlightColor.R) * alpha), cellColor.G - (int)((cellColor.G - CellBackgroundHighlightColor.G) * alpha), cellColor.B - (int)((cellColor.B - CellBackgroundHighlightColor.B) * alpha)); DrawCellBG(e, cellColor, relativeCell, visibleColumns); } } /// /// Calls QueryItemBkColor callback for all visible cells and fills in the background of those cells. /// /// private void DoBackGroundCallback(PaintEventArgs e, List visibleColumns) { int startIndex = FirstVisibleRow; int range = Math.Min(LastVisibleRow, ItemCount - 1) - startIndex + 1; int lastVisible = LastVisibleColumnIndex; int firstVisibleColumn = FirstVisibleColumn; // Prevent exceptions with small windows if (firstVisibleColumn < 0) { return; } for (int i = 0, f = 0; f < range; i++, f++) // Vertical { //f += _lagFrames[i]; Color rowColor = CellBackgroundColor; QueryRowBkColor?.Invoke(f + startIndex, ref rowColor); for (int j = FirstVisibleColumn; j <= lastVisible; j++) // Horizontal { Color itemColor = CellBackgroundColor; QueryItemBkColor?.Invoke(f + startIndex, _columns.IndexOf(visibleColumns[j]), ref itemColor); QueryItemBkColorAdvanced?.Invoke(f + startIndex, visibleColumns[j], ref itemColor); if (itemColor == CellBackgroundColor) { itemColor = rowColor; } else if (itemColor.A != 255 && itemColor.A != 0) { float alpha = (float)itemColor.A / 255; itemColor = Color.FromArgb(rowColor.R - (int)((rowColor.R - itemColor.R) * alpha), rowColor.G - (int)((rowColor.G - itemColor.G) * alpha), rowColor.B - (int)((rowColor.B - itemColor.B) * alpha)); } if (itemColor != Color.White) // An easy optimization, don't draw unless the user specified something other than the default { var cell = new Cell { Column = visibleColumns[j], RowIndex = i }; DrawCellBG(e, itemColor, cell, visibleColumns); } } } } } }