diff --git a/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj b/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj
index 6f9887d192..4cc252c3da 100644
--- a/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj
+++ b/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj
@@ -609,6 +609,30 @@
MsgBox.cs
+
+ PlatformAgnosticVirtualListView.cs
+ Component
+
+
+ PlatformAgnosticVirtualListView.cs
+ Component
+
+
+ PlatformAgnosticVirtualListView.cs
+ Component
+
+
+ PlatformAgnosticVirtualListView.cs
+ Component
+
+
+ PlatformAgnosticVirtualListView.cs
+ Component
+
+
+ PlatformAgnosticVirtualListView.cs
+ Component
+
Form
@@ -747,6 +771,9 @@
OpenAdvancedChooser.cs
+
+ Component
+
Form
diff --git a/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.API.cs b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.API.cs
new file mode 100644
index 0000000000..d664628eed
--- /dev/null
+++ b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.API.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+
+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
+ /// -------------------
+ /// *** API Related ***
+ /// -------------------
+ ///
+ public partial class PlatformAgnosticVirtualListView
+ {
+ private Cell _draggingCell;
+
+ #region Methods
+
+ ///
+ /// Parent form calls this to add columns
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void AddColumn(string columnName, string columnText, int columnWidth, ListColumn.InputType columnType = ListColumn.InputType.Boolean)
+ {
+ if (AllColumns[columnName] == null)
+ {
+ var column = new ListColumn
+ {
+ Name = columnName,
+ Text = columnText,
+ Width = columnWidth,
+ Type = columnType
+ };
+
+ AllColumns.Add(column);
+ }
+ }
+
+ ///
+ /// Sets the state of the passed row index
+ ///
+ ///
+ ///
+ public void SelectItem(int index, bool val)
+ {
+ if (_columns.VisibleColumns.Any())
+ {
+ if (val)
+ {
+ SelectCell(new Cell
+ {
+ RowIndex = index,
+ Column = _columns[0]
+ });
+ }
+ else
+ {
+ IEnumerable items = _selectedItems.Where(cell => cell.RowIndex == index);
+ _selectedItems.RemoveWhere(items.Contains);
+ }
+ }
+ }
+
+ public void SelectAll()
+ {
+ var oldFullRowVal = FullRowSelect;
+ FullRowSelect = true;
+ for (int i = 0; i < ItemCount; i++)
+ {
+ SelectItem(i, true);
+ }
+
+ FullRowSelect = oldFullRowVal;
+ }
+
+ public void DeselectAll()
+ {
+ _selectedItems.Clear();
+ }
+
+ public void TruncateSelection(int index)
+ {
+ _selectedItems.RemoveWhere(cell => cell.RowIndex > index);
+ }
+
+ public bool IsVisible(int index)
+ {
+ return (index >= FirstVisibleRow) && (index <= LastFullyVisibleRow);
+ }
+
+ public bool IsPartiallyVisible(int index)
+ {
+ return index >= FirstVisibleRow && index <= LastVisibleRow;
+ }
+
+ public void DragCurrentCell()
+ {
+ _draggingCell = CurrentCell;
+ }
+
+ public void ReleaseCurrentCell()
+ {
+ if (_draggingCell != null)
+ {
+ var draggedCell = _draggingCell;
+ _draggingCell = null;
+
+ if (CurrentCell != draggedCell)
+ {
+ CellDropped?.Invoke(this, new CellEventArgs(draggedCell, CurrentCell));
+ }
+ }
+ }
+
+ ///
+ /// Scrolls to the given index, according to the scroll settings.
+ ///
+ public void ScrollToIndex(int index)
+ {
+ if (ScrollMethod == "near")
+ {
+ MakeIndexVisible(index);
+ }
+
+ if (!IsVisible(index) || AlwaysScroll)
+ {
+ if (ScrollMethod == "top")
+ {
+ FirstVisibleRow = index;
+ }
+ else if (ScrollMethod == "bottom")
+ {
+ LastVisibleRow = index;
+ }
+ else if (ScrollMethod == "center")
+ {
+ FirstVisibleRow = Math.Max(index - (VisibleRows / 2), 0);
+ }
+ }
+ }
+
+ ///
+ /// Scrolls so that the given index is visible, if it isn't already; doesn't use scroll settings.
+ ///
+ public void MakeIndexVisible(int index)
+ {
+ if (!IsVisible(index))
+ {
+ if (FirstVisibleRow > index)
+ {
+ FirstVisibleRow = index;
+ }
+ else
+ {
+ LastVisibleRow = index;
+ }
+ }
+ }
+
+ public void ClearSelectedRows()
+ {
+ _selectedItems.Clear();
+ }
+
+ #endregion
+ }
+}
diff --git a/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Classes.cs b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Classes.cs
new file mode 100644
index 0000000000..3ccdb3f1c6
--- /dev/null
+++ b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Classes.cs
@@ -0,0 +1,315 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Reflection;
+
+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
+ /// ---------------
+ /// *** Classes ***
+ /// ---------------
+ ///
+ public partial class PlatformAgnosticVirtualListView
+ {
+ #region Event Args
+
+ public class CellEventArgs
+ {
+ public CellEventArgs(Cell oldCell, Cell newCell)
+ {
+ OldCell = oldCell;
+ NewCell = newCell;
+ }
+
+ public Cell OldCell { get; private set; }
+ public Cell NewCell { get; private set; }
+ }
+
+ public class ColumnClickEventArgs
+ {
+ public ColumnClickEventArgs(ListColumn column)
+ {
+ Column = column;
+ }
+
+ public ListColumn Column { get; private set; }
+ }
+
+ public class ColumnReorderedEventArgs
+ {
+ public ColumnReorderedEventArgs(int oldDisplayIndex, int newDisplayIndex, ListColumn column)
+ {
+ Column = column;
+ OldDisplayIndex = oldDisplayIndex;
+ NewDisplayIndex = newDisplayIndex;
+ }
+
+ public ListColumn Column { get; private set; }
+ public int OldDisplayIndex { get; private set; }
+ public int NewDisplayIndex { get; private set; }
+ }
+
+ #endregion
+
+ #region Columns
+
+ public class ListColumn
+ {
+ public enum InputType { Boolean, Float, Text, Image }
+
+ public int Index { get; set; }
+ public int OriginalIndex { get; set; } // for implementations that dont use ColumnReorderedEventArgs
+ public string Group { get; set; }
+ public int? Width { get; set; }
+ public int? Left { get; set; }
+ public int? Right { get; set; }
+ public string Name { get; set; }
+ public string Text { get; set; }
+ public InputType Type { get; set; }
+ public bool Visible { get; set; }
+
+ ///
+ /// Column will be drawn with an emphasized look, if true
+ ///
+ private bool _emphasis;
+ public bool Emphasis
+ {
+ get { return _emphasis; }
+ set { _emphasis = value; }
+ }
+
+ public ListColumn()
+ {
+ Visible = true;
+ }
+ }
+
+ public class ListColumns : List
+ {
+ public ListColumn this[string name]
+ {
+ get
+ {
+ return this.SingleOrDefault(column => column.Name == name);
+ }
+ }
+
+ public IEnumerable VisibleColumns
+ {
+ get
+ {
+ return this.Where(c => c.Visible);
+ }
+ }
+
+ public Action ChangedCallback { get; set; }
+
+ private void DoChangeCallback()
+ {
+ // no check will make it crash for user too, not sure which way of alarm we prefer. no alarm at all will cause all sorts of subtle bugs
+ if (ChangedCallback == null)
+ {
+ System.Diagnostics.Debug.Fail("ColumnChangedCallback has died!");
+ }
+ else
+ {
+ ChangedCallback();
+ }
+ }
+
+ // TODO: this shouldn't be exposed. But in order to not expose it, each RollColumn must have a change callback, and all property changes must call it, it is quicker and easier to just call this when needed
+ public void ColumnsChanged()
+ {
+ int pos = 0;
+
+ var columns = VisibleColumns.ToList();
+
+ for (int i = 0; i < columns.Count; i++)
+ {
+ columns[i].Left = pos;
+ pos += columns[i].Width.Value;
+ columns[i].Right = pos;
+ }
+
+ DoChangeCallback();
+ }
+
+ public new void Add(ListColumn column)
+ {
+ if (this.Any(c => c.Name == column.Name))
+ {
+ // The designer sucks, doing nothing for now
+ return;
+ //throw new InvalidOperationException("A column with this name already exists.");
+ }
+
+ base.Add(column);
+ // save the original index for implementations that do not use ColumnReorderedEventArgs
+ column.OriginalIndex = this.IndexOf(column);
+ ColumnsChanged();
+ }
+
+ public new void AddRange(IEnumerable collection)
+ {
+ foreach (var column in collection)
+ {
+ if (this.Any(c => c.Name == column.Name))
+ {
+ // The designer sucks, doing nothing for now
+ return;
+
+ throw new InvalidOperationException("A column with this name already exists.");
+ }
+ }
+
+ base.AddRange(collection);
+ ColumnsChanged();
+ }
+
+ public new void Insert(int index, ListColumn column)
+ {
+ if (this.Any(c => c.Name == column.Name))
+ {
+ throw new InvalidOperationException("A column with this name already exists.");
+ }
+
+ base.Insert(index, column);
+ ColumnsChanged();
+ }
+
+ public new void InsertRange(int index, IEnumerable collection)
+ {
+ foreach (var column in collection)
+ {
+ if (this.Any(c => c.Name == column.Name))
+ {
+ throw new InvalidOperationException("A column with this name already exists.");
+ }
+ }
+
+ base.InsertRange(index, collection);
+ ColumnsChanged();
+ }
+
+ public new bool Remove(ListColumn column)
+ {
+ var result = base.Remove(column);
+ ColumnsChanged();
+ return result;
+ }
+
+ public new int RemoveAll(Predicate match)
+ {
+ var result = base.RemoveAll(match);
+ ColumnsChanged();
+ return result;
+ }
+
+ public new void RemoveAt(int index)
+ {
+ base.RemoveAt(index);
+ ColumnsChanged();
+ }
+
+ public new void RemoveRange(int index, int count)
+ {
+ base.RemoveRange(index, count);
+ ColumnsChanged();
+ }
+
+ public new void Clear()
+ {
+ base.Clear();
+ ColumnsChanged();
+ }
+
+ public IEnumerable Groups
+ {
+ get
+ {
+ return this
+ .Select(x => x.Group)
+ .Distinct();
+ }
+ }
+ }
+
+ #endregion
+
+ #region Cells
+
+ ///
+ /// Represents a single cell of the Roll
+ ///
+ public class Cell
+ {
+ public ListColumn Column { get; internal set; }
+ public int? RowIndex { get; internal set; }
+ public string CurrentText { get; internal set; }
+
+ public Cell() { }
+
+ public Cell(Cell cell)
+ {
+ Column = cell.Column;
+ RowIndex = cell.RowIndex;
+ }
+
+ public bool IsDataCell => Column != null && RowIndex.HasValue;
+
+ public override bool Equals(object obj)
+ {
+ if (obj is Cell)
+ {
+ var cell = obj as Cell;
+ return this.Column == cell.Column && this.RowIndex == cell.RowIndex;
+ }
+
+ return base.Equals(obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return Column.GetHashCode() + RowIndex.GetHashCode();
+ }
+ }
+
+ private class SortCell : IComparer
+ {
+ int IComparer.Compare(Cell a, Cell b)
+ {
+ Cell c1 = a as Cell;
+ Cell c2 = b as Cell;
+ if (c1.RowIndex.HasValue)
+ {
+ if (c2.RowIndex.HasValue)
+ {
+ int row = c1.RowIndex.Value.CompareTo(c2.RowIndex.Value);
+ if (row == 0)
+ {
+ return c1.Column.Name.CompareTo(c2.Column.Name);
+ }
+
+ return row;
+ }
+
+ return 1;
+ }
+
+ if (c2.RowIndex.HasValue)
+ {
+ return -1;
+ }
+
+ return c1.Column.Name.CompareTo(c2.Column.Name);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Drawing.cs b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Drawing.cs
new file mode 100644
index 0000000000..49387455fc
--- /dev/null
+++ b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Drawing.cs
@@ -0,0 +1,523 @@
+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
+ 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, _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, _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 + 2 * 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, 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(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);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.EventHandlers.cs b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.EventHandlers.cs
new file mode 100644
index 0000000000..5bf1122f4e
--- /dev/null
+++ b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.EventHandlers.cs
@@ -0,0 +1,696 @@
+using System;
+using System.ComponentModel;
+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
+ /// -----------------------------------
+ /// *** Events ***
+ /// -----------------------------------
+ ///
+ public partial class PlatformAgnosticVirtualListView
+ {
+ #region Event Handlers
+
+ ///
+ /// Fire the event which requests the text for the passed cell
+ ///
+ [Category("Virtual")]
+ public event QueryItemTextHandler QueryItemText;
+
+ ///
+ /// Fire the event which requests the text for the passed cell
+ ///
+ [Category("Virtual")]
+ public event QueryItemTextHandlerAdvanced QueryItemTextAdvanced;
+
+ ///
+ /// Fire the event which requests the background color for the passed cell
+ ///
+ [Category("Virtual")]
+ public event QueryItemBkColorHandler QueryItemBkColor;
+
+ [Category("Virtual")]
+ public event QueryRowBkColorHandler QueryRowBkColor;
+
+ ///
+ /// Fire the event which requests an icon for a given cell
+ ///
+ [Category("Virtual")]
+ public event QueryItemIconHandler QueryItemIcon;
+
+ ///
+ /// Fires when the mouse moves from one cell to another (including column header cells)
+ ///
+ [Category("Mouse")]
+ public event CellChangeEventHandler PointedCellChanged;
+
+ ///
+ /// Fires when a cell is hovered on
+ ///
+ [Category("Mouse")]
+ public event HoverEventHandler CellHovered;
+
+ ///
+ /// Occurs when a column header is clicked
+ ///
+ [Category("Action")]
+ public event ColumnClickEventHandler ColumnClick;
+
+ ///
+ /// Occurs when a column header is right-clicked
+ ///
+ [Category("Action")]
+ public event ColumnClickEventHandler ColumnRightClick;
+
+ ///
+ /// Occurs whenever the 'SelectedItems' property for this control changes
+ ///
+ [Category("Behavior")]
+ public event EventHandler SelectedIndexChanged;
+
+ ///
+ /// Occurs whenever the mouse wheel is scrolled while the right mouse button is held
+ ///
+ [Category("Behavior")]
+ public event RightMouseScrollEventHandler RightMouseScrolled;
+
+ [Category("Property Changed")]
+ [Description("Occurs when the column header has been reordered")]
+ public event ColumnReorderedEventHandler ColumnReordered;
+
+ [Category("Action")]
+ [Description("Occurs when the scroll value of the visible rows change (in vertical orientation this is the vertical scroll bar change, and in horizontal it is the horizontal scroll bar)")]
+ public event RowScrollEvent RowScroll;
+
+ [Category("Action")]
+ [Description("Occurs when the scroll value of the columns (in vertical orientation this is the horizontal scroll bar change, and in horizontal it is the vertical scroll bar)")]
+ public event ColumnScrollEvent ColumnScroll;
+
+ [Category("Action")]
+ [Description("Occurs when a cell is dragged and then dropped into a new cell, old cell is the cell that was being dragged, new cell is its new destination")]
+ public event CellDroppedEvent CellDropped;
+
+ #endregion
+
+ #region Delegates
+
+ ///
+ /// Retrieve the text for a cell
+ ///
+ public delegate void QueryItemTextHandlerAdvanced(int index, ListColumn column, out string text, ref int offsetX, ref int offsetY);
+ public delegate void QueryItemTextHandler(int index, int column, out string text);
+
+ ///
+ /// Retrieve the background color for a cell
+ ///
+ public delegate void QueryItemBkColorHandler(int index, ListColumn column, ref Color color);
+ public delegate void QueryRowBkColorHandler(int index, ref Color color);
+
+ ///
+ /// Retrieve the image for a given cell
+ ///
+ public delegate void QueryItemIconHandler(int index, ListColumn column, ref Bitmap icon, ref int offsetX, ref int offsetY);
+
+ public delegate void CellChangeEventHandler(object sender, CellEventArgs e);
+
+ public delegate void HoverEventHandler(object sender, CellEventArgs e);
+
+ public delegate void RightMouseScrollEventHandler(object sender, MouseEventArgs e);
+
+ public delegate void ColumnClickEventHandler(object sender, ColumnClickEventArgs e);
+
+ public delegate void ColumnReorderedEventHandler(object sender, ColumnReorderedEventArgs e);
+
+ public delegate void RowScrollEvent(object sender, EventArgs e);
+
+ public delegate void ColumnScrollEvent(object sender, EventArgs e);
+
+ public delegate void CellDroppedEvent(object sender, CellEventArgs e);
+
+ #endregion
+
+ #region Mouse and Key Events
+
+ private bool _columnDownMoved;
+
+ protected override void OnMouseMove(MouseEventArgs e)
+ {
+ _currentX = e.X;
+ _currentY = e.Y;
+
+ if (_columnDown != null)
+ {
+ _columnDownMoved = true;
+ }
+
+ Cell newCell = CalculatePointedCell(_currentX.Value, _currentY.Value);
+
+ newCell.RowIndex += FirstVisibleRow;
+ if (newCell.RowIndex < 0)
+ {
+ newCell.RowIndex = 0;
+ }
+
+ if (!newCell.Equals(CurrentCell))
+ {
+ CellChanged(newCell);
+
+ if (IsHoveringOnColumnCell ||
+ (WasHoveringOnColumnCell && !IsHoveringOnColumnCell))
+ {
+ Refresh();
+ }
+ else if (_columnDown != null)
+ {
+ Refresh();
+ }
+ }
+ else if (_columnDown != null) // Kind of silly feeling to have this check twice, but the only alternative I can think of has it refreshing twice when pointed column changes with column down, and speed matters
+ {
+ Refresh();
+ }
+
+ if (_columnSeparatorDown != null)
+ {
+ // column is being resized
+ DoColumnResize();
+ Refresh();
+ }
+
+ // cursor changes
+ if (IsHoveringOnDraggableColumnDivide && AllowColumnResize)
+ Cursor.Current = Cursors.VSplit;
+ else if (IsHoveringOnColumnCell && AllowColumnReorder)
+ Cursor.Current = Cursors.Hand;
+ else
+ Cursor.Current = Cursors.Default;
+
+ base.OnMouseMove(e);
+ }
+
+ protected override void OnMouseEnter(EventArgs e)
+ {
+ CurrentCell = new Cell
+ {
+ Column = null,
+ RowIndex = null
+ };
+
+ base.OnMouseEnter(e);
+ }
+
+ protected override void OnMouseLeave(EventArgs e)
+ {
+ _currentX = null;
+ _currentY = null;
+ CurrentCell = null;
+ IsPaintDown = false;
+ _hoverTimer.Stop();
+ Cursor.Current = Cursors.Default;
+ Refresh();
+ base.OnMouseLeave(e);
+ }
+
+ // TODO add query callback of whether to select the cell or not
+ protected override void OnMouseDown(MouseEventArgs e)
+ {
+ if (!GlobalWin.MainForm.EmulatorPaused && _currentX.HasValue)
+ {
+ // copypaste from OnMouseMove()
+ Cell newCell = CalculatePointedCell(_currentX.Value, _currentY.Value);
+
+ newCell.RowIndex += FirstVisibleRow;
+ if (newCell.RowIndex < 0)
+ {
+ newCell.RowIndex = 0;
+ }
+
+ if (!newCell.Equals(CurrentCell))
+ {
+ CellChanged(newCell);
+
+ if (IsHoveringOnColumnCell ||
+ (WasHoveringOnColumnCell && !IsHoveringOnColumnCell))
+ {
+ Refresh();
+ }
+ else if (_columnDown != null)
+ {
+ Refresh();
+ }
+ }
+ else if (_columnDown != null)
+ {
+ Refresh();
+ }
+ }
+
+ if (e.Button == MouseButtons.Left)
+ {
+ if (IsHoveringOnDraggableColumnDivide && AllowColumnResize)
+ {
+ _columnSeparatorDown = ColumnAtX(_currentX.Value);
+ }
+ else if (IsHoveringOnColumnCell && AllowColumnReorder)
+ {
+ _columnDown = CurrentCell.Column;
+ }
+ else if (InputPaintingMode)
+ {
+ IsPaintDown = true;
+ }
+ }
+
+ if (e.Button == MouseButtons.Right)
+ {
+ if (!IsHoveringOnColumnCell)
+ {
+ RightButtonHeld = true;
+ }
+ }
+
+ if (e.Button == MouseButtons.Left)
+ {
+ if (IsHoveringOnDataCell)
+ {
+ if (ModifierKeys == Keys.Alt)
+ {
+ // do marker drag here
+ }
+ else if (ModifierKeys == Keys.Shift && (CurrentCell.Column.Type == ListColumn.InputType.Text))
+ {
+ if (_selectedItems.Any())
+ {
+ if (FullRowSelect)
+ {
+ var selected = _selectedItems.Any(c => c.RowIndex.HasValue && CurrentCell.RowIndex.HasValue && c.RowIndex == CurrentCell.RowIndex);
+
+ if (!selected)
+ {
+ var rowIndices = _selectedItems
+ .Where(c => c.RowIndex.HasValue)
+ .Select(c => c.RowIndex ?? -1)
+ .Where(c => c >= 0) // Hack to avoid possible Nullable exceptions
+ .Distinct()
+ .ToList();
+
+ var firstIndex = rowIndices.Min();
+ var lastIndex = rowIndices.Max();
+
+ if (CurrentCell.RowIndex.Value < firstIndex)
+ {
+ for (int i = CurrentCell.RowIndex.Value; i < firstIndex; i++)
+ {
+ SelectCell(new Cell
+ {
+ RowIndex = i,
+ Column = CurrentCell.Column
+ });
+ }
+ }
+ else if (CurrentCell.RowIndex.Value > lastIndex)
+ {
+ for (int i = lastIndex + 1; i <= CurrentCell.RowIndex.Value; i++)
+ {
+ SelectCell(new Cell
+ {
+ RowIndex = i,
+ Column = CurrentCell.Column
+ });
+ }
+ }
+ else // Somewhere in between, a scenario that can happen with ctrl-clicking, find the previous and highlight from there
+ {
+ var nearest = rowIndices
+ .Where(x => x < CurrentCell.RowIndex.Value)
+ .Max();
+
+ for (int i = nearest + 1; i <= CurrentCell.RowIndex.Value; i++)
+ {
+ SelectCell(new Cell
+ {
+ RowIndex = i,
+ Column = CurrentCell.Column
+ });
+ }
+ }
+ }
+ }
+ else
+ {
+ MessageBox.Show("Shift click logic for individual cells has not yet implemented");
+ }
+ }
+ else
+ {
+ SelectCell(CurrentCell);
+ }
+ }
+ else if (ModifierKeys == Keys.Control && (CurrentCell.Column.Type == ListColumn.InputType.Text))
+ {
+ SelectCell(CurrentCell, toggle: true);
+ }
+ else if (ModifierKeys != Keys.Shift)
+ {
+ var hadIndex = _selectedItems.Any();
+ _selectedItems.Clear();
+ SelectCell(CurrentCell);
+ }
+
+ Refresh();
+
+ SelectedIndexChanged?.Invoke(this, new EventArgs());
+ }
+ }
+
+ base.OnMouseDown(e);
+
+ if (AllowRightClickSelecton && e.Button == MouseButtons.Right)
+ {
+ if (!IsHoveringOnColumnCell)
+ {
+ _currentX = e.X;
+ _currentY = e.Y;
+ Cell newCell = CalculatePointedCell(_currentX.Value, _currentY.Value);
+ newCell.RowIndex += FirstVisibleRow;
+ CellChanged(newCell);
+ SelectCell(CurrentCell);
+ }
+ }
+ }
+
+ protected override void OnMouseUp(MouseEventArgs e)
+ {
+ if (_columnSeparatorDown != null && AllowColumnResize)
+ {
+ DoColumnResize();
+ Refresh();
+ }
+ else if (IsHoveringOnColumnCell && AllowColumnReorder)
+ {
+ if (_columnDown != null && _columnDownMoved)
+ {
+ DoColumnReorder();
+ _columnDown = null;
+ Refresh();
+ }
+ else if (e.Button == MouseButtons.Left)
+ {
+ ColumnClickEvent(ColumnAtX(e.X));
+ }
+ else if (e.Button == MouseButtons.Right)
+ {
+ ColumnRightClickEvent(ColumnAtX(e.X));
+ }
+ }
+
+ _columnDown = null;
+ _columnDownMoved = false;
+ _columnSeparatorDown = null;
+ RightButtonHeld = false;
+ IsPaintDown = false;
+ base.OnMouseUp(e);
+ }
+
+ private void IncrementScrollBar(ScrollBar bar, bool increment)
+ {
+ int newVal;
+ if (increment)
+ {
+ newVal = bar.Value + (bar.SmallChange * ScrollSpeed);
+ if (newVal > bar.Maximum - bar.LargeChange)
+ {
+ newVal = bar.Maximum - bar.LargeChange;
+ }
+ }
+ else
+ {
+ newVal = bar.Value - (bar.SmallChange * ScrollSpeed);
+ if (newVal < 0)
+ {
+ newVal = 0;
+ }
+ }
+
+ _programmaticallyUpdatingScrollBarValues = true;
+ bar.Value = newVal;
+ _programmaticallyUpdatingScrollBarValues = false;
+ }
+
+ protected override void OnMouseWheel(MouseEventArgs e)
+ {
+ IncrementScrollBar(_vBar, e.Delta < 0);
+ if (_currentX != null)
+ {
+ OnMouseMove(new MouseEventArgs(MouseButtons.None, 0, _currentX.Value, _currentY.Value, 0));
+ }
+
+ Refresh();
+ }
+
+ private void DoRightMouseScroll(object sender, MouseEventArgs e)
+ {
+ RightMouseScrolled?.Invoke(sender, e);
+ }
+
+ private void ColumnClickEvent(ListColumn column)
+ {
+ ColumnClick?.Invoke(this, new ColumnClickEventArgs(column));
+ }
+
+ private void ColumnRightClickEvent(ListColumn column)
+ {
+ ColumnRightClick?.Invoke(this, new ColumnClickEventArgs(column));
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ if (!SuspendHotkeys)
+ {
+ if (e.Control && !e.Alt && e.Shift && e.KeyCode == Keys.F) // Ctrl+Shift+F
+ {
+ //HorizontalOrientation ^= true;
+ }
+ // Scroll
+ else if (!e.Control && !e.Alt && !e.Shift && e.KeyCode == Keys.PageUp) // Page Up
+ {
+ if (FirstVisibleRow > 0)
+ {
+ LastVisibleRow = FirstVisibleRow;
+ Refresh();
+ }
+ }
+ else if (!e.Control && !e.Alt && !e.Shift && e.KeyCode == Keys.PageDown) // Page Down
+ {
+ var totalRows = LastVisibleRow - FirstVisibleRow;
+ if (totalRows <= ItemCount)
+ {
+ var final = LastVisibleRow + totalRows;
+ if (final > ItemCount)
+ {
+ final = ItemCount;
+ }
+
+ LastVisibleRow = final;
+ Refresh();
+ }
+ }
+ else if (!e.Control && !e.Alt && !e.Shift && e.KeyCode == Keys.Home) // Home
+ {
+ FirstVisibleRow = 0;
+ Refresh();
+ }
+ else if (!e.Control && !e.Alt && !e.Shift && e.KeyCode == Keys.End) // End
+ {
+ LastVisibleRow = ItemCount;
+ Refresh();
+ }
+ else if (!e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.Up) // Up
+ {
+ if (FirstVisibleRow > 0)
+ {
+ FirstVisibleRow--;
+ Refresh();
+ }
+ }
+ else if (!e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.Down) // Down
+ {
+ if (FirstVisibleRow < ItemCount - 1)
+ {
+ FirstVisibleRow++;
+ Refresh();
+ }
+ }
+ // Selection courser
+ else if (e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.Up) // Ctrl + Up
+ {
+ if (SelectedRows.Any() && LetKeysModifySelection && SelectedRows.First() > 0)
+ {
+ foreach (var row in SelectedRows.ToList())
+ {
+ SelectItem(row - 1, true);
+ SelectItem(row, false);
+ }
+ }
+ }
+ else if (e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.Down) // Ctrl + Down
+ {
+ if (SelectedRows.Any() && LetKeysModifySelection)
+ {
+ foreach (var row in SelectedRows.Reverse().ToList())
+ {
+ SelectItem(row + 1, true);
+ SelectItem(row, false);
+ }
+ }
+ }
+ else if (e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.Left) // Ctrl + Left
+ {
+ if (SelectedRows.Any() && LetKeysModifySelection)
+ {
+ SelectItem(SelectedRows.Last(), false);
+ }
+ }
+ else if (e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.Right) // Ctrl + Right
+ {
+ if (SelectedRows.Any() && LetKeysModifySelection && SelectedRows.Last() < _itemCount - 1)
+ {
+ SelectItem(SelectedRows.Last() + 1, true);
+ }
+ }
+ else if (e.Control && e.Shift && !e.Alt && e.KeyCode == Keys.Left) // Ctrl + Shift + Left
+ {
+ if (SelectedRows.Any() && LetKeysModifySelection && SelectedRows.First() > 0)
+ {
+ SelectItem(SelectedRows.First() - 1, true);
+ }
+ }
+ else if (e.Control && e.Shift && !e.Alt && e.KeyCode == Keys.Right) // Ctrl + Shift + Right
+ {
+ if (SelectedRows.Any() && LetKeysModifySelection)
+ {
+ SelectItem(SelectedRows.First(), false);
+ }
+ }
+ else if (e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.PageUp) // Ctrl + Page Up
+ {
+ //jump to above marker with selection courser
+ if (LetKeysModifySelection)
+ {
+
+ }
+ }
+ else if (e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.PageDown) // Ctrl + Page Down
+ {
+ //jump to below marker with selection courser
+ if (LetKeysModifySelection)
+ {
+
+ }
+
+ }
+ else if (e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.Home) // Ctrl + Home
+ {
+ //move selection courser to frame 0
+ if (LetKeysModifySelection)
+ {
+ DeselectAll();
+ SelectItem(0, true);
+ }
+ }
+ else if (e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.End) // Ctrl + End
+ {
+ //move selection courser to end of movie
+ if (LetKeysModifySelection)
+ {
+ DeselectAll();
+ SelectItem(ItemCount - 1, true);
+ }
+ }
+ }
+
+ base.OnKeyDown(e);
+ }
+
+ #endregion
+
+ #region Change Events
+
+ protected override void OnResize(EventArgs e)
+ {
+ RecalculateScrollBars();
+ if (BorderSize > 0 && this.Parent != null)
+ {
+ // refresh the parent control to regen the border
+ this.Parent.Refresh();
+ }
+ base.OnResize(e);
+ Refresh();
+
+
+
+ }
+
+ ///
+ /// Call this function to change the CurrentCell to newCell
+ ///
+ private void CellChanged(Cell newCell)
+ {
+ LastCell = CurrentCell;
+ CurrentCell = newCell;
+
+ if (PointedCellChanged != null &&
+ (LastCell.Column != CurrentCell.Column || LastCell.RowIndex != CurrentCell.RowIndex))
+ {
+ PointedCellChanged(this, new CellEventArgs(LastCell, CurrentCell));
+ }
+
+ if (CurrentCell?.Column != null && CurrentCell.RowIndex.HasValue)
+ {
+ _hoverTimer.Start();
+ }
+ else
+ {
+ _hoverTimer.Stop();
+ }
+ }
+
+ private void VerticalBar_ValueChanged(object sender, EventArgs e)
+ {
+ if (!_programmaticallyUpdatingScrollBarValues)
+ {
+ Refresh();
+ }
+
+ RowScroll?.Invoke(this, e);
+ }
+
+ private void HorizontalBar_ValueChanged(object sender, EventArgs e)
+ {
+ if (!_programmaticallyUpdatingScrollBarValues)
+ {
+ Refresh();
+ }
+
+ ColumnScroll?.Invoke(this, e);
+ }
+
+ private void ColumnChangedCallback()
+ {
+ RecalculateScrollBars();
+ if (_columns.VisibleColumns.Any())
+ {
+ ColumnWidth = _columns.VisibleColumns.Max(c => c.Width.Value) + CellWidthPadding * 4;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Helpers.cs b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Helpers.cs
new file mode 100644
index 0000000000..82d73b8b05
--- /dev/null
+++ b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Helpers.cs
@@ -0,0 +1,359 @@
+using System;
+using System.ComponentModel;
+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
+ /// ----------------------
+ /// *** Helper Methods ***
+ /// ----------------------
+ ///
+ public partial class PlatformAgnosticVirtualListView
+ {
+ // TODO: Make into an extension method
+ private static Color Add(Color color, int val)
+ {
+ var col = color.ToArgb();
+ col += val;
+ return Color.FromArgb(col);
+ }
+
+ private void DoColumnReorder()
+ {
+ if (_columnDown != CurrentCell.Column)
+ {
+ var oldIndex = _columns.IndexOf(_columnDown);
+ var newIndex = _columns.IndexOf(CurrentCell.Column);
+
+ ColumnReordered?.Invoke(this, new ColumnReorderedEventArgs(oldIndex, newIndex, _columnDown));
+
+ _columns.Remove(_columnDown);
+ _columns.Insert(newIndex, _columnDown);
+ }
+ }
+
+ ///
+ /// Helper method for implementations that do not make use of ColumnReorderedEventArgs related callbacks
+ /// Basically, each column stores its initial index when added in .OriginalIndex
+ ///
+ ///
+ ///
+ public int GetOriginalColumnIndex(int currIndex)
+ {
+ return AllColumns[currIndex].OriginalIndex;
+ }
+
+ // ScrollBar.Maximum = DesiredValue + ScrollBar.LargeChange - 1
+ // See MSDN Page for more information on the dumb ScrollBar.Maximum Property
+ private void RecalculateScrollBars()
+ {
+ if (_vBar == null || _hBar == null)
+ return;
+
+ UpdateDrawSize();
+
+ var columns = _columns.VisibleColumns.ToList();
+
+ if (CellHeight == 0) CellHeight++;
+ NeedsVScrollbar = ItemCount > 1;
+ NeedsHScrollbar = TotalColWidth.HasValue && TotalColWidth.Value - DrawWidth + 1 > 0;
+
+ UpdateDrawSize();
+ if (VisibleRows > 0)
+ {
+ _vBar.Maximum = Math.Max((VisibleRows - 1) * CellHeight, _vBar.Maximum); // ScrollBar.Maximum is dumb
+ _vBar.LargeChange = (VisibleRows - 1) * CellHeight;
+ // DrawWidth can be negative if the TAStudio window is small enough
+ // Clamp LargeChange to 0 here to prevent exceptions
+ _hBar.LargeChange = Math.Max(0, DrawWidth / 2);
+ }
+
+ // Update VBar
+ if (NeedsVScrollbar)
+ {
+ _vBar.Maximum = RowsToPixels(ItemCount + 1) - (CellHeight * 3) + _vBar.LargeChange - 1;
+
+ _vBar.Location = new Point(Width - _vBar.Width, 0);
+ _vBar.Height = Height;
+ _vBar.Visible = true;
+ }
+ else
+ {
+ _vBar.Visible = false;
+ _vBar.Value = 0;
+ }
+
+ // Update HBar
+ if (NeedsHScrollbar)
+ {
+ _hBar.Maximum = TotalColWidth.Value - DrawWidth + _hBar.LargeChange;
+
+ _hBar.Location = new Point(0, Height - _hBar.Height);
+ _hBar.Width = Width - (NeedsVScrollbar ? (_vBar.Width + 1) : 0);
+ _hBar.Visible = true;
+ }
+ else
+ {
+ _hBar.Visible = false;
+ _hBar.Value = 0;
+ }
+ }
+
+ private void UpdateDrawSize()
+ {
+ if (NeedsVScrollbar)
+ {
+ DrawWidth = Width - _vBar.Width;
+ }
+ else
+ {
+ DrawWidth = Width;
+ }
+ if (NeedsHScrollbar)
+ {
+ DrawHeight = Height - _hBar.Height;
+ }
+ else
+ {
+ DrawHeight = Height;
+ }
+ }
+
+ ///
+ /// If FullRowSelect is enabled, selects all cells in the row that contains the given cell. Otherwise only given cell is added.
+ ///
+ /// The cell to select.
+ private void SelectCell(Cell cell, bool toggle = false)
+ {
+ if (cell.RowIndex.HasValue && cell.RowIndex < ItemCount)
+ {
+ if (!MultiSelect)
+ {
+ _selectedItems.Clear();
+ }
+
+ if (FullRowSelect)
+ {
+ if (toggle && _selectedItems.Any(x => x.RowIndex.HasValue && x.RowIndex == cell.RowIndex))
+ {
+ var items = _selectedItems
+ .Where(x => x.RowIndex.HasValue && x.RowIndex == cell.RowIndex)
+ .ToList();
+
+ foreach (var item in items)
+ {
+ _selectedItems.Remove(item);
+ }
+ }
+ else
+ {
+ foreach (var column in _columns)
+ {
+ _selectedItems.Add(new Cell
+ {
+ RowIndex = cell.RowIndex,
+ Column = column
+ });
+ }
+ }
+ }
+ else
+ {
+ if (toggle && _selectedItems.Any(x => x.RowIndex.HasValue && x.RowIndex == cell.RowIndex))
+ {
+ var item = _selectedItems
+ .FirstOrDefault(x => x.Equals(cell));
+
+ if (item != null)
+ {
+ _selectedItems.Remove(item);
+ }
+ }
+ else
+ {
+ _selectedItems.Add(CurrentCell);
+ }
+ }
+ }
+ }
+
+ private bool IsHoveringOnDraggableColumnDivide =>
+ IsHoveringOnColumnCell &&
+ ((_currentX <= CurrentCell.Column.Left + 2 && CurrentCell.Column.Index != 0) ||
+ (_currentX >= CurrentCell.Column.Right - 2 && CurrentCell.Column.Index != _columns.Count - 1));
+
+ private bool IsHoveringOnColumnCell => CurrentCell?.Column != null && !CurrentCell.RowIndex.HasValue;
+
+ private bool IsHoveringOnDataCell => CurrentCell?.Column != null && CurrentCell.RowIndex.HasValue;
+
+ private bool WasHoveringOnColumnCell => LastCell?.Column != null && !LastCell.RowIndex.HasValue;
+
+ private bool WasHoveringOnDataCell => LastCell?.Column != null && LastCell.RowIndex.HasValue;
+
+ ///
+ /// Finds the specific cell that contains the (x, y) coordinate.
+ ///
+ /// The row number that it returns will be between 0 and VisibleRows, NOT the absolute row number.
+ /// X coordinate point.
+ /// Y coordinate point.
+ /// The cell with row number and RollColumn reference, both of which can be null.
+ private Cell CalculatePointedCell(int x, int y)
+ {
+ var newCell = new Cell();
+ var columns = _columns.VisibleColumns.ToList();
+
+ // If pointing to a column header
+ if (columns.Any())
+ {
+ newCell.RowIndex = PixelsToRows(y);
+ newCell.Column = ColumnAtX(x);
+ }
+
+ if (!(IsPaintDown || RightButtonHeld) && newCell.RowIndex <= -1) // -2 if we're entering from the top
+ {
+ newCell.RowIndex = null;
+ }
+
+ return newCell;
+ }
+
+
+ private void CalculateColumnToResize()
+ {
+ // if this is reached, we are already over a selectable column divide
+ _columnSeparatorDown = ColumnAtX(_currentX.Value);
+ }
+
+ private void DoColumnResize()
+ {
+ var widthChange = _currentX - _columnSeparatorDown.Right;
+ _columnSeparatorDown.Width += widthChange;
+ if (_columnSeparatorDown.Width < MinimumColumnSize)
+ _columnSeparatorDown.Width = MinimumColumnSize;
+ AllColumns.ColumnsChanged();
+ }
+
+ // A boolean that indicates if the InputRoll is too large vertically and requires a vertical scrollbar.
+ private bool NeedsVScrollbar { get; set; }
+
+ // A boolean that indicates if the InputRoll is too large horizontally and requires a horizontal scrollbar.
+ private bool NeedsHScrollbar { get; set; }
+
+ ///
+ /// Updates the width of the supplied column.
+ /// Call when changing the ColumnCell text, CellPadding, or text font.
+ ///
+ /// The RollColumn object to update.
+ /// The new width of the RollColumn object.
+ private int UpdateWidth(ListColumn col)
+ {
+ col.Width = (col.Text.Length * _charSize.Width) + (CellWidthPadding * 4);
+ return col.Width.Value;
+ }
+
+ ///
+ /// Gets the total width of all the columns by using the last column's Right property.
+ ///
+ /// A nullable Int representing total width.
+ private int? TotalColWidth
+ {
+ get
+ {
+ if (_columns.VisibleColumns.Any())
+ {
+ return _columns.VisibleColumns.Last().Right;
+ }
+
+ return null;
+ }
+ }
+
+ ///
+ /// Returns the RollColumn object at the specified visible x coordinate. Coordinate should be between 0 and Width of the InputRoll Control.
+ ///
+ /// The x coordinate.
+ /// RollColumn object that contains the x coordinate or null if none exists.
+ private ListColumn ColumnAtX(int x)
+ {
+ foreach (ListColumn column in _columns.VisibleColumns)
+ {
+ if (column.Left.Value - _hBar.Value <= x && column.Right.Value - _hBar.Value >= x)
+ {
+ return column;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Converts a row number to a horizontal or vertical coordinate.
+ ///
+ /// A vertical coordinate if Vertical Oriented, otherwise a horizontal coordinate.
+ private int RowsToPixels(int index)
+ {
+ return (index * CellHeight) + ColumnHeight;
+ }
+
+ ///
+ /// Converts a horizontal or vertical coordinate to a row number.
+ ///
+ /// A vertical coordinate if Vertical Oriented, otherwise a horizontal coordinate.
+ /// A row number between 0 and VisibleRows if it is a Datarow, otherwise a negative number if above all Datarows.
+ private int PixelsToRows(int pixels)
+ {
+ // Using Math.Floor and float because integer division rounds towards 0 but we want to round down.
+ if (CellHeight == 0)
+ CellHeight++;
+
+ return (int)Math.Floor((float)(pixels - ColumnHeight) / CellHeight);
+ }
+
+ // The width of the largest column cell in Horizontal Orientation
+ private int ColumnWidth { get; set; }
+
+ // The height of a column cell in Vertical Orientation.
+ private int ColumnHeight { get; set; }
+
+ // The width of a cell in Horizontal Orientation. Only can be changed by changing the Font or CellPadding.
+ private int CellWidth { get; set; }
+
+ [Browsable(false)]
+ public int RowHeight => CellHeight;
+
+ ///
+ /// Gets or sets a value indicating the height of a cell in Vertical Orientation. Only can be changed by changing the Font or CellPadding.
+ ///
+ private int CellHeight { get; set; }
+
+ ///
+ /// Call when _charSize, MaxCharactersInHorizontal, or CellPadding is changed.
+ ///
+ private void UpdateCellSize()
+ {
+ CellHeight = _charSize.Height + (CellHeightPadding * 2);
+ CellWidth = (_charSize.Width/* * MaxCharactersInHorizontal*/) + (CellWidthPadding * 4); // Double the padding for horizontal because it looks better
+ }
+ /*
+ ///
+ /// Call when _charSize, MaxCharactersInHorizontal, or CellPadding is changed.
+ ///
+ private void UpdateColumnSize()
+ {
+
+ }
+ */
+
+ // Number of displayed + hidden frames, if fps is as expected
+ private int ExpectedDisplayRange()
+ {
+ return (VisibleRows + 1); // * LagFramesToHide;
+ }
+ }
+}
diff --git a/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Properties.cs b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Properties.cs
new file mode 100644
index 0000000000..f1512204f5
--- /dev/null
+++ b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Properties.cs
@@ -0,0 +1,780 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Linq;
+
+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
+ /// -------------------------
+ /// *** Public Properties ***
+ /// -------------------------
+ ///
+ public partial class PlatformAgnosticVirtualListView
+ {
+ #region ListView Compatibility Properties
+
+ ///
+ /// This VirtualListView implementation doesn't really need this, but it is here for compatibility
+ ///
+ [Category("Behavior")]
+ public int VirtualListSize
+ {
+ get
+ {
+ return _itemCount;
+ }
+
+ set
+ {
+ _itemCount = value;
+ RecalculateScrollBars();
+ }
+ }
+
+ ///
+ /// ListView compatibility property
+ /// THIS DOES NOT WORK PROPERLY - AVOID!
+ ///
+ [System.ComponentModel.Browsable(false)]
+ public System.Windows.Forms.ListView.SelectedIndexCollection SelectedIndices
+ {
+ // !!! does not work properly, avoid using this in the calling implementation !!!
+ get
+ {
+ var tmpListView = new System.Windows.Forms.ListView();
+ //tmpListView.VirtualMode = true;
+ //var selectedIndexCollection = new System.Windows.Forms.ListView.SelectedIndexCollection(tmpListView);
+ //tmpListView.VirtualListSize = ItemCount;
+ for (int i = 0; i < ItemCount; i++)
+ {
+ tmpListView.Items.Add(i.ToString());
+ }
+
+ //tmpListView.Refresh();
+
+ if (AnyRowsSelected)
+ {
+ var indices = SelectedRows.ToList();
+ foreach (var i in indices)
+ {
+ tmpListView.SelectedIndices.Add(i);
+ //selectedIndexCollection.Add(i);
+ }
+ }
+
+ return tmpListView.SelectedIndices; // selectedIndexCollection;
+ }
+ }
+
+ ///
+ /// Compatibility property
+ /// With a standard ListView you can add columns in the Designer
+ /// We will ignore this (but leave it here for compatibility)
+ /// Columns must be added through the AddColumns() public method
+ ///
+ public System.Windows.Forms.ListView.ColumnHeaderCollection Columns = new System.Windows.Forms.ListView.ColumnHeaderCollection(new System.Windows.Forms.ListView());
+
+ ///
+ /// Compatibility with ListView class
+ /// This is not used in this implementation
+ ///
+ [Category("Behavior")]
+ public bool VirtualMode { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the selected item in the control remains highlighted when the control loses focus
+ ///
+ [Category("Behavior")]
+ public bool HideSelection { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the ListView uses state image behavior that is compatible with the .NET Framework 1.1 or the .NET Framework 2.0.
+ /// Here for ListView api compatibility (we dont care about this)
+ ///
+ [System.ComponentModel.Browsable(false)]
+ public bool UseCompatibleStateImageBehavior { get; set; }
+
+ ///
+ /// Gets or sets how items are displayed in the control.
+ /// Here for ListView api compatibility (we dont care about this)
+ ///
+ public System.Windows.Forms.View View { get; set; }
+
+ #endregion
+
+ #region VirtualListView Compatibility Properties
+
+ ///
+ /// Informs user that a select all event is in place, can be used in change events to wait until this is false
+ /// Not used in this implementation (yet)
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public bool SelectAllInProgress { get; set; }
+
+ ///
+ /// Gets/Sets the selected item
+ /// Here for compatibility with VirtualListView.cs
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public int selectedItem
+ {
+ get
+ {
+ if (SelectedRows.Count() == 0)
+ {
+ return -1;
+ }
+ else
+ {
+ return SelectedRows.First();
+ }
+ }
+ set
+ {
+ SelectItem(value, true);
+ }
+ }
+
+ [Category("Behavior")]
+ public bool BlazingFast { get; set; }
+
+ #endregion
+
+ #region Behavior
+
+ ///
+ /// Gets or sets the amount of left and right padding on the text inside a cell
+ ///
+ [DefaultValue(3)]
+ [Category("Behavior")]
+ public int CellWidthPadding { get; set; }
+
+ ///
+ /// Gets or sets the amount of top and bottom padding on the text inside a cell
+ ///
+ [DefaultValue(1)]
+ [Category("Behavior")]
+ public int CellHeightPadding { get; set; }
+
+ ///
+ /// Gets or sets the scrolling speed
+ ///
+ [Category("Behavior")]
+ public int ScrollSpeed
+ {
+ get
+ {
+ if (CellHeight == 0)
+ CellHeight++;
+ return _vBar.SmallChange / CellHeight;
+ }
+
+ set
+ {
+ _vBar.SmallChange = value * CellHeight;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether columns can be resized
+ ///
+ [Category("Behavior")]
+ [DefaultValue(true)]
+ public bool AllowColumnResize { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether columns can be reordered
+ ///
+ [Category("Behavior")]
+ [DefaultValue(true)]
+ public bool AllowColumnReorder { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether multiple items can to be selected
+ ///
+ [Category("Behavior")]
+ [DefaultValue(true)]
+ public bool MultiSelect { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the control is in input painting mode
+ ///
+ [Category("Behavior")]
+ [DefaultValue(false)]
+ public bool InputPaintingMode { get; set; }
+
+ ///
+ /// Gets or sets how the InputRoll scrolls when calling ScrollToIndex.
+ ///
+ [DefaultValue("near")]
+ [Category("Behavior")]
+ public string ScrollMethod { get; set; }
+
+ ///
+ /// Gets or sets a value indicating how the Intever for the hover event
+ ///
+ [DefaultValue(false)]
+ [Category("Behavior")]
+ public bool AlwaysScroll { get; set; }
+
+ ///
+ /// Gets or sets the lowest seek interval to activate the progress bar
+ ///
+ [Category("Behavior")]
+ public int SeekingCutoffInterval { get; set; }
+
+ [DefaultValue(750)]
+ [Category("Behavior")]
+ public int HoverInterval
+ {
+ get { return _hoverTimer.Interval; }
+ set { _hoverTimer.Interval = value; }
+ }
+
+ ///
+ /// Gets or sets whether you can use right click to select things
+ ///
+ [Category("Behavior")]
+ public bool AllowRightClickSelecton { get; set; }
+
+ ///
+ /// Gets or sets whether keys can modify selection
+ ///
+ [Category("Behavior")]
+ public bool LetKeysModifySelection { get; set; }
+
+ ///
+ /// Gets or sets whether hot keys are suspended
+ ///
+ [Category("Behavior")]
+ public bool SuspendHotkeys { get; set; }
+
+ #endregion
+
+ #region Appearance
+
+ ///
+ /// Gets or sets a value indicating whether grid lines are displayed around cells
+ ///
+ [Category("Appearance")]
+ [DefaultValue(true)]
+ public bool GridLines { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the entire row will always be selected
+ ///
+ [Category("Appearance")]
+ [DefaultValue(false)]
+ public bool FullRowSelect { get; set; }
+
+ ///
+ /// Gets or sets the font used for the column header text
+ /// Also forces a cell size re-evaluation
+ ///
+ [Category("Appearance")]
+ public Font ColumnHeaderFont
+ {
+ get
+ {
+ if (_columnHeaderFont == null)
+ {
+ ColumnHeaderFont = new Font("Arial", 8, FontStyle.Bold);
+ }
+
+ return _columnHeaderFont;
+ }
+ set
+ {
+ _columnHeaderFont = value;
+ SetCharSize();
+ }
+ }
+ private Font _columnHeaderFont;
+
+ ///
+ /// Gets or sets the color of the column header text
+ ///
+ [Category("Appearance")]
+ public Color ColumnHeaderFontColor
+ {
+ get
+ {
+ if (_columnHeaderFontColor == null)
+ _columnHeaderFontColor = Color.Black;
+ return _columnHeaderFontColor;
+ }
+ set { _columnHeaderFontColor = value; }
+ }
+ private Color _columnHeaderFontColor;
+
+ ///
+ /// Gets or sets the background color of the column header cells
+ ///
+ [Category("Appearance")]
+ public Color ColumnHeaderBackgroundColor
+ {
+ get
+ {
+ if (_columnHeaderBackgroundColor == null)
+ _columnHeaderBackgroundColor = Color.LightGray;
+ return _columnHeaderBackgroundColor;
+ }
+ set { _columnHeaderBackgroundColor = value; }
+ }
+ private Color _columnHeaderBackgroundColor;
+
+ ///
+ /// Gets or sets the background color of the column header cells when they are highlighted
+ ///
+ [Category("Appearance")]
+ public Color ColumnHeaderBackgroundHighlightColor
+ {
+ get
+ {
+ if (_columnHeaderBackgroundHighlightColor == null)
+ _columnHeaderBackgroundHighlightColor = SystemColors.HighlightText;
+ return _columnHeaderBackgroundHighlightColor;
+ }
+ set { _columnHeaderBackgroundHighlightColor = value; }
+ }
+ private Color _columnHeaderBackgroundHighlightColor;
+
+ ///
+ /// Gets or sets the color of the column header outline
+ ///
+ [Category("Appearance")]
+ public Color ColumnHeaderOutlineColor
+ {
+ get
+ {
+ if (_columnHeaderOutlineColor == null)
+ _columnHeaderOutlineColor = Color.Black;
+ return _columnHeaderOutlineColor;
+ }
+ set
+ {
+ _columnHeaderOutlineColor = value;
+ }
+ }
+ private Color _columnHeaderOutlineColor;
+
+
+ ///
+ /// Gets or sets the font used for every row cell
+ /// Also forces a cell size re-evaluation
+ ///
+ [Category("Appearance")]
+ public Font CellFont
+ {
+ get
+ {
+ if (_cellFont == null)
+ {
+ CellFont = new Font("Arial", 8, FontStyle.Regular);
+ }
+ return _cellFont;
+ }
+ set
+ {
+ _cellFont = value;
+ SetCharSize();
+ }
+ }
+ private Font _cellFont;
+
+
+ ///
+ /// Gets or sets the color of the font used for every row cell
+ ///
+ [Category("Appearance")]
+ public Color CellFontColor
+ {
+ get
+ {
+ if (_cellFontColor == null)
+ _cellFontColor = Color.Black;
+ return _cellFontColor;
+ }
+ set { _cellFontColor = value; }
+ }
+ private Color _cellFontColor;
+
+ ///
+ /// Gets or sets the background color for every row cell
+ ///
+ [Category("Appearance")]
+ public Color CellBackgroundColor
+ {
+ get
+ {
+ if (_cellBackgroundColor == null)
+ _cellBackgroundColor = Color.White;
+ return _cellBackgroundColor;
+ }
+ set { _cellBackgroundColor = value; }
+ }
+ private Color _cellBackgroundColor;
+
+ ///
+ /// Gets or sets the background color for every row cell that is highlighted
+ ///
+ [Category("Appearance")]
+ public Color CellBackgroundHighlightColor
+ {
+ get
+ {
+ if (_cellBackgroundHighlightColor == null)
+ _cellBackgroundHighlightColor = Color.Blue;
+ return _cellBackgroundHighlightColor;
+ }
+ set { _cellBackgroundHighlightColor = value; }
+ }
+ private Color _cellBackgroundHighlightColor;
+
+ ///
+ /// Gets or sets the color used to draw the ListView gridlines
+ ///
+ [Category("Appearance")]
+ public Color GridLineColor
+ {
+ get
+ {
+ if (_gridLineColor == null)
+ _gridLineColor = SystemColors.ControlLight;
+ return _gridLineColor;
+ }
+ set { _gridLineColor = value; }
+ }
+ private Color _gridLineColor;
+
+ ///
+ /// Gets or sets the size of control's border
+ /// Note: this is drawn directly onto the parent control, so large values will probably look terrible
+ ///
+ [Category("Appearance")]
+ public int BorderSize { get; set; }
+
+ ///
+ /// Defines the absolute minimum column size (used when manually resizing columns)
+ ///
+ [DefaultValue(50)]
+ [Category("Appearance")]
+ public int MinimumColumnSize { get; set; }
+
+ ///
+ /// The padding property is disabled for this control (as this is handled internally)
+ ///
+ [Category("Appearance")]
+ public new System.Windows.Forms.Padding Padding
+ {
+ get { return new System.Windows.Forms.Padding(0); }
+ set { }
+ }
+
+ ///
+ /// Gets or sets the color of the control's border
+ ///
+ [Category("Appearance")]
+ public Color BorderColor
+ {
+ get
+ {
+ if (_borderColor == null)
+ _borderColor = SystemColors.InactiveBorder;
+ return _borderColor;
+ }
+ set { _borderColor = value; }
+ }
+ private Color _borderColor;
+
+
+ #endregion
+
+ #region API
+
+ ///
+ /// All visible columns
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public IEnumerable VisibleColumns => _columns.VisibleColumns;
+
+ ///
+ /// Gets or sets the sets the virtual number of rows to be displayed. Does not include the column header row.
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public int ItemCount
+ {
+ get { return _itemCount; }
+ set
+ {
+ _itemCount = value;
+ RecalculateScrollBars();
+ }
+ }
+
+ ///
+ /// Returns all columns including those that are not visible
+ ///
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public ListColumns AllColumns => _columns;
+
+ ///
+ /// Gets whether the mouse is currently over a column cell
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public bool IsPointingAtColumnHeader => IsHoveringOnColumnCell;
+
+ ///
+ /// Returns the index of the first selected row (null if no selection)
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public int? FirstSelectedIndex
+ {
+ get
+ {
+ if (AnyRowsSelected)
+ {
+ return SelectedRows.Min();
+ }
+
+ return null;
+ }
+ }
+
+ ///
+ /// Returns the index of the last selected row (null if no selection)
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public int? LastSelectedIndex
+ {
+ get
+ {
+ if (AnyRowsSelected)
+ {
+ return SelectedRows.Max();
+ }
+
+ return null;
+ }
+ }
+
+ ///
+ /// Gets or sets the first visible row index, if scrolling is needed
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public int FirstVisibleRow
+ {
+ get // SuuperW: This was checking if the scroll bars were needed, which is useless because their Value is 0 if they aren't needed.
+ {
+ if (CellHeight == 0) CellHeight++;
+ return _vBar.Value / CellHeight;
+ }
+
+ set
+ {
+ if (NeedsVScrollbar)
+ {
+ _programmaticallyUpdatingScrollBarValues = true;
+ if (value * CellHeight <= _vBar.Maximum)
+ {
+ _vBar.Value = value * CellHeight;
+ }
+ else
+ {
+ _vBar.Value = _vBar.Maximum;
+ }
+
+ _programmaticallyUpdatingScrollBarValues = false;
+ }
+ }
+ }
+
+ ///
+ /// Gets the last row that is fully visible
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ private int LastFullyVisibleRow
+ {
+ get
+ {
+ int halfRow = 0;
+ if ((DrawHeight - ColumnHeight - 3) % CellHeight < CellHeight / 2)
+ {
+ halfRow = 1;
+ }
+
+ return FirstVisibleRow + VisibleRows - halfRow; // + CountLagFramesDisplay(VisibleRows - halfRow);
+ }
+ }
+
+ ///
+ /// Gets or sets the last visible row
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public int LastVisibleRow
+ {
+ get
+ {
+ return FirstVisibleRow + VisibleRows; // + CountLagFramesDisplay(VisibleRows);
+ }
+
+ set
+ {
+ int halfRow = 0;
+ if ((DrawHeight - ColumnHeight - 3) % CellHeight < CellHeight / 2)
+ {
+ halfRow = 1;
+ }
+
+ FirstVisibleRow = Math.Max(value - (VisibleRows - halfRow), 0);
+ }
+ }
+
+ ///
+ /// Gets the number of rows currently visible including partially visible rows.
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public int VisibleRows
+ {
+ get
+ {
+ if (CellHeight == 0) CellHeight++;
+ return (DrawHeight - ColumnHeight - 3) / CellHeight; // Minus three makes it work
+ }
+ }
+
+ ///
+ /// Gets the first visible column index, if scrolling is needed
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public int FirstVisibleColumn
+ {
+ get
+ {
+ if (CellHeight == 0) CellHeight++;
+ var columnList = VisibleColumns.ToList();
+ return columnList.FindIndex(c => c.Right > _hBar.Value);
+ }
+ }
+
+ ///
+ /// Gets the last visible column index, if scrolling is needed
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public int LastVisibleColumnIndex
+ {
+ get
+ {
+ if (CellHeight == 0) CellHeight++;
+ List columnList = VisibleColumns.ToList();
+ int ret;
+ ret = columnList.FindLastIndex(c => c.Left <= DrawWidth + _hBar.Value);
+ return ret;
+ }
+ }
+
+ ///
+ /// Gets or sets the current Cell that the mouse was in.
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public Cell CurrentCell { get; set; }
+
+ ///
+ /// Returns whether the current cell is a data cell or not
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public bool CurrentCellIsDataCell => CurrentCell?.RowIndex != null && CurrentCell.Column != null;
+
+ ///
+ /// Gets a list of selected row indexes
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public IEnumerable SelectedRows
+ {
+ get
+ {
+ return _selectedItems
+ .Where(cell => cell.RowIndex.HasValue)
+ .Select(cell => cell.RowIndex.Value)
+ .Distinct();
+ }
+ }
+
+ ///
+ /// Returns whether any rows are selected
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public bool AnyRowsSelected
+ {
+ get
+ {
+ return _selectedItems.Any(cell => cell.RowIndex.HasValue);
+ }
+ }
+
+ ///
+ /// Gets or sets the previous Cell that the mouse was in.
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public Cell LastCell { get; private set; }
+
+ ///
+ /// Gets or sets whether paint down is happening
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public bool IsPaintDown { get; private set; }
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public bool UseCustomBackground { get; set; }
+
+ ///
+ /// Gets or sets the current draw height
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public int DrawHeight { get; private set; }
+
+ ///
+ /// Gets or sets the current draw width
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public int DrawWidth { get; private set; }
+
+ ///
+ /// Gets or sets whether the right mouse button is held down
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public bool RightButtonHeld { get; private set; }
+
+ #endregion
+ }
+}
diff --git a/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.cs b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.cs
new file mode 100644
index 0000000000..e07c9d1923
--- /dev/null
+++ b/BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.cs
@@ -0,0 +1,356 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+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
+ ///
+ public partial class PlatformAgnosticVirtualListView : Control
+ {
+ private readonly SortedSet _selectedItems = new SortedSet(new SortCell());
+
+ private readonly VScrollBar _vBar;
+ private readonly HScrollBar _hBar;
+
+ private readonly Timer _hoverTimer = new Timer();
+
+ private ListColumns _columns = new ListColumns();
+
+ private bool _programmaticallyUpdatingScrollBarValues;
+
+ private int _itemCount;
+ private Size _charSize;
+
+ private ListColumn _columnDown;
+ private ListColumn _columnSeparatorDown;
+
+ private int? _currentX;
+ private int? _currentY;
+
+ public PlatformAgnosticVirtualListView()
+ {
+ ColumnHeaderFont = new Font("Arial", 8, FontStyle.Bold);
+ ColumnHeaderFontColor = Color.Black;
+ ColumnHeaderBackgroundColor = Color.LightGray;
+ ColumnHeaderBackgroundHighlightColor = SystemColors.HighlightText;
+ ColumnHeaderOutlineColor = Color.Black;
+
+ CellFont = new Font("Arial", 8, FontStyle.Regular);
+ CellFontColor = Color.Black;
+ CellBackgroundColor = Color.White;
+ CellBackgroundHighlightColor = Color.Blue;
+
+ GridLines = true;
+ GridLineColor = SystemColors.ControlLight;
+
+ UseCustomBackground = true;
+
+ BorderColor = Color.DarkGray;
+ BorderSize = 1;
+
+ MinimumColumnSize = 50;
+
+ CellWidthPadding = 3;
+ CellHeightPadding = 0;
+ CurrentCell = null;
+ ScrollMethod = "near";
+
+ SetStyle(ControlStyles.AllPaintingInWmPaint, true);
+ SetStyle(ControlStyles.UserPaint, true);
+ SetStyle(ControlStyles.SupportsTransparentBackColor, true);
+ SetStyle(ControlStyles.Opaque, true);
+ SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
+
+ _vBar = new VScrollBar
+ {
+ // Location gets calculated later (e.g. on resize)
+ Visible = false,
+ SmallChange = CellHeight,
+ LargeChange = CellHeight * 20
+ };
+
+ _hBar = new HScrollBar
+ {
+ // Location gets calculated later (e.g. on resize)
+ Visible = false,
+ SmallChange = CellWidth,
+ LargeChange = 20
+ };
+
+ Controls.Add(_vBar);
+ Controls.Add(_hBar);
+
+ _vBar.ValueChanged += VerticalBar_ValueChanged;
+ _hBar.ValueChanged += HorizontalBar_ValueChanged;
+
+ RecalculateScrollBars();
+
+ _columns.ChangedCallback = ColumnChangedCallback;
+
+ _hoverTimer.Interval = 750;
+ _hoverTimer.Tick += HoverTimerEventProcessor;
+ _hoverTimer.Stop();
+ }
+
+ private void HoverTimerEventProcessor(object sender, EventArgs e)
+ {
+ _hoverTimer.Stop();
+
+ CellHovered?.Invoke(this, new CellEventArgs(LastCell, CurrentCell));
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ }
+
+ #region Pending Removal
+
+ /*
+ *
+
+ //private readonly byte[] _lagFrames = new byte[256]; // Large enough value that it shouldn't ever need resizing. // apparently not large enough for 4K
+
+ private int _maxCharactersInHorizontal = 1;
+ private bool _horizontalOrientation;
+
+ public string UserSettingsSerialized()
+ {
+ var settings = ConfigService.SaveWithType(Settings);
+ return settings;
+ }
+
+
+
+ public void LoadSettingsSerialized(string settingsJson)
+ {
+ var settings = ConfigService.LoadWithType(settingsJson);
+
+ // TODO: don't silently fail, inform the user somehow
+ if (settings is InputRollSettings)
+ {
+ var rollSettings = settings as InputRollSettings;
+ _columns = rollSettings.Columns;
+ _columns.ChangedCallback = ColumnChangedCallback;
+ HorizontalOrientation = rollSettings.HorizontalOrientation;
+ //LagFramesToHide = rollSettings.LagFramesToHide;
+ //HideWasLagFrames = rollSettings.HideWasLagFrames;
+ }
+ }
+
+ private InputRollSettings Settings => new InputRollSettings
+ {
+ Columns = _columns,
+ HorizontalOrientation = HorizontalOrientation,
+ //LagFramesToHide = LagFramesToHide,
+ //HideWasLagFrames = HideWasLagFrames
+ };
+
+ public class InputRollSettings
+ {
+ public RollColumns Columns { get; set; }
+ public bool HorizontalOrientation { get; set; }
+ public int LagFramesToHide { get; set; }
+ public bool HideWasLagFrames { get; set; }
+ }
+
+
+ ///
+ /// Gets or sets the width of data cells when in Horizontal orientation.
+ ///
+ public int MaxCharactersInHorizontal
+ {
+ get
+ {
+ return _maxCharactersInHorizontal;
+ }
+
+ set
+ {
+ _maxCharactersInHorizontal = value;
+ UpdateCellSize();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the control is horizontal or vertical
+ ///
+ [Category("Behavior")]
+ public bool HorizontalOrientation
+ {
+ get
+ {
+ return _horizontalOrientation;
+ }
+ set
+ {
+ if (_horizontalOrientation != value)
+ {
+ int temp = ScrollSpeed;
+ _horizontalOrientation = value;
+ OrientationChanged();
+ _hBar.SmallChange = CellWidth;
+ _vBar.SmallChange = CellHeight;
+ ScrollSpeed = temp;
+ }
+ }
+ }
+
+ public IEnumerable GenerateContextMenuItems()
+ {
+ yield return new ToolStripSeparator();
+
+ var rotate = new ToolStripMenuItem
+ {
+ Name = "RotateMenuItem",
+ Text = "Rotate",
+ ShortcutKeyDisplayString = RotateHotkeyStr,
+ };
+
+ rotate.Click += (o, ev) =>
+ {
+ HorizontalOrientation ^= true;
+ };
+
+ yield return rotate;
+ }
+
+ // SuuperW: Count lag frames between FirstDisplayed and given display position
+ private int CountLagFramesDisplay(int relativeIndex)
+ {
+ if (QueryFrameLag != null && LagFramesToHide != 0)
+ {
+ int count = 0;
+ for (int i = 0; i <= relativeIndex; i++)
+ {
+ count += _lagFrames[i];
+ }
+
+ return count;
+ }
+
+ return 0;
+ }
+
+ // Count lag frames between FirstDisplayed and given relative frame index
+ private int CountLagFramesAbsolute(int relativeIndex)
+ {
+ if (QueryFrameLag != null) // && LagFramesToHide != 0)
+ {
+ int count = 0;
+ for (int i = 0; i + count <= relativeIndex; i++)
+ {
+ count += _lagFrames[i];
+ }
+
+ return count;
+ }
+
+ return 0;
+ }
+
+ private void SetLagFramesArray()
+ {
+ if (QueryFrameLag != null) // && LagFramesToHide != 0)
+ {
+ bool showNext = false;
+
+ // First one needs to check BACKWARDS for lag frame count.
+ SetLagFramesFirst();
+ int f = _lagFrames[0];
+
+ if (QueryFrameLag(FirstVisibleRow + f, HideWasLagFrames))
+ {
+ showNext = true;
+ }
+
+ for (int i = 1; i <= VisibleRows; i++)
+ {
+ _lagFrames[i] = 0;
+ if (!showNext)
+ {
+ for (; _lagFrames[i] < LagFramesToHide; _lagFrames[i]++)
+ {
+ if (!QueryFrameLag(FirstVisibleRow + i + f, HideWasLagFrames))
+ {
+ break;
+ }
+
+ f++;
+ }
+ }
+ else
+ {
+ if (!QueryFrameLag(FirstVisibleRow + i + f, HideWasLagFrames))
+ {
+ showNext = false;
+ }
+ }
+
+ if (_lagFrames[i] == LagFramesToHide && QueryFrameLag(FirstVisibleRow + i + f, HideWasLagFrames))
+ {
+ showNext = true;
+ }
+ }
+ }
+ else
+ {
+ for (int i = 0; i <= VisibleRows; i++)
+ {
+ _lagFrames[i] = 0;
+ }
+ }
+ }
+
+ private void SetLagFramesFirst()
+ {
+ if (QueryFrameLag != null && LagFramesToHide != 0)
+ {
+ // Count how many lag frames are above displayed area.
+ int count = 0;
+ do
+ {
+ count++;
+ }
+ while (QueryFrameLag(FirstVisibleRow - count, HideWasLagFrames) && count <= LagFramesToHide);
+ count--;
+
+ // Count forward
+ int fCount = -1;
+ do
+ {
+ fCount++;
+ }
+ while (QueryFrameLag(FirstVisibleRow + fCount, HideWasLagFrames) && count + fCount < LagFramesToHide);
+ _lagFrames[0] = (byte)fCount;
+ }
+ else
+ {
+ _lagFrames[0] = 0;
+ }
+ }
+
+ public string RotateHotkeyStr => "Ctrl+Shift+F";
+
+ ///
+ /// Check if a given frame is a lag frame
+ ///
+ public delegate bool QueryFrameLagHandler(int index, bool hideWasLag);
+
+
+ ///
+ /// Fire the QueryFrameLag event which checks if a given frame is a lag frame
+ ///
+ [Category("Virtual")]
+ public event QueryFrameLagHandler QueryFrameLag;
+
+ */
+
+ #endregion
+ }
+}
diff --git a/BizHawk.Client.EmuHawk/tools/TraceLogger.Designer.cs b/BizHawk.Client.EmuHawk/tools/TraceLogger.Designer.cs
index b9afaa15a2..eaf2db91a2 100644
--- a/BizHawk.Client.EmuHawk/tools/TraceLogger.Designer.cs
+++ b/BizHawk.Client.EmuHawk/tools/TraceLogger.Designer.cs
@@ -31,13 +31,13 @@
this.components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(TraceLogger));
this.TracerBox = new System.Windows.Forms.GroupBox();
- this.TraceView = new BizHawk.Client.EmuHawk.VirtualListView();
- this.Disasm = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
- this.Registers = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
+ this.TraceView = new BizHawk.Client.EmuHawk.PlatformAgnosticVirtualListView();
this.TraceContextMenu = new System.Windows.Forms.ContextMenuStrip(this.components);
this.CopyContextMenu = new System.Windows.Forms.ToolStripMenuItem();
this.SelectAllContextMenu = new System.Windows.Forms.ToolStripMenuItem();
this.ClearContextMenu = new System.Windows.Forms.ToolStripMenuItem();
+ this.Disasm = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
+ this.Registers = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.menuStrip1 = new MenuStripEx();
this.FileSubMenu = new System.Windows.Forms.ToolStripMenuItem();
this.SaveLogMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@@ -49,6 +49,8 @@
this.ClearMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.OptionsSubMenu = new System.Windows.Forms.ToolStripMenuItem();
this.MaxLinesMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.SegmentSizeMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.AutoScrollMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.groupBox2 = new System.Windows.Forms.GroupBox();
this.OpenLogFile = new System.Windows.Forms.Button();
this.BrowseBox = new System.Windows.Forms.Button();
@@ -56,7 +58,6 @@
this.ToFileRadio = new System.Windows.Forms.RadioButton();
this.ToWindowRadio = new System.Windows.Forms.RadioButton();
this.LoggingEnabled = new System.Windows.Forms.CheckBox();
- this.SegmentSizeMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.TracerBox.SuspendLayout();
this.TraceContextMenu.SuspendLayout();
this.menuStrip1.SuspendLayout();
@@ -78,39 +79,44 @@
//
// TraceView
//
+ this.TraceView.AllowColumnReorder = false;
+ this.TraceView.AllowColumnResize = false;
+ this.TraceView.AllowRightClickSelecton = false;
this.TraceView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.TraceView.BlazingFast = false;
- this.TraceView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
- this.Disasm,
- this.Registers});
+ this.TraceView.BorderColor = System.Drawing.Color.DarkGray;
+ this.TraceView.BorderSize = 1;
+ this.TraceView.CellBackgroundColor = System.Drawing.Color.White;
+ this.TraceView.CellBackgroundHighlightColor = System.Drawing.Color.Blue;
+ this.TraceView.CellFont = new System.Drawing.Font("Arial", 8F);
+ this.TraceView.CellFontColor = System.Drawing.Color.Black;
+ this.TraceView.CellHeightPadding = 0;
+ this.TraceView.ColumnHeaderBackgroundColor = System.Drawing.Color.LightGray;
+ this.TraceView.ColumnHeaderBackgroundHighlightColor = System.Drawing.SystemColors.HighlightText;
+ this.TraceView.ColumnHeaderFont = new System.Drawing.Font("Arial", 8F, System.Drawing.FontStyle.Bold);
+ this.TraceView.ColumnHeaderFontColor = System.Drawing.Color.Black;
+ this.TraceView.ColumnHeaderOutlineColor = System.Drawing.Color.Black;
this.TraceView.ContextMenuStrip = this.TraceContextMenu;
this.TraceView.Font = new System.Drawing.Font("Courier New", 8F);
this.TraceView.FullRowSelect = true;
- this.TraceView.GridLines = true;
+ this.TraceView.GridLineColor = System.Drawing.SystemColors.ControlLight;
this.TraceView.HideSelection = false;
- this.TraceView.ItemCount = 0;
+ this.TraceView.LetKeysModifySelection = false;
this.TraceView.Location = new System.Drawing.Point(8, 18);
+ this.TraceView.MultiSelect = false;
this.TraceView.Name = "TraceView";
- this.TraceView.SelectAllInProgress = false;
- this.TraceView.selectedItem = -1;
+ this.TraceView.ScrollSpeed = 1;
+ this.TraceView.SeekingCutoffInterval = 0;
this.TraceView.Size = new System.Drawing.Size(603, 414);
+ this.TraceView.SuspendHotkeys = false;
this.TraceView.TabIndex = 4;
this.TraceView.TabStop = false;
this.TraceView.UseCompatibleStateImageBehavior = false;
- this.TraceView.UseCustomBackground = true;
this.TraceView.View = System.Windows.Forms.View.Details;
- //
- // Disasm
- //
- this.Disasm.Text = "Disasm";
- this.Disasm.Width = 239;
- //
- // Registers
- //
- this.Registers.Text = "Registers";
- this.Registers.Width = 357;
+ this.TraceView.VirtualListSize = 0;
+ this.TraceView.VirtualMode = false;
//
// TraceContextMenu
//
@@ -144,6 +150,16 @@
this.ClearContextMenu.Text = "Clear";
this.ClearContextMenu.Click += new System.EventHandler(this.ClearMenuItem_Click);
//
+ // Disasm
+ //
+ this.Disasm.Text = "Disasm";
+ this.Disasm.Width = 239;
+ //
+ // Registers
+ //
+ this.Registers.Text = "Registers";
+ this.Registers.Width = 357;
+ //
// menuStrip1
//
this.menuStrip1.ClickThrough = true;
@@ -164,27 +180,27 @@
this.toolStripSeparator1,
this.ExitMenuItem});
this.FileSubMenu.Name = "FileSubMenu";
- this.FileSubMenu.Size = new System.Drawing.Size(35, 20);
+ this.FileSubMenu.Size = new System.Drawing.Size(37, 20);
this.FileSubMenu.Text = "&File";
//
// SaveLogMenuItem
//
this.SaveLogMenuItem.Image = global::BizHawk.Client.EmuHawk.Properties.Resources.SaveAs;
this.SaveLogMenuItem.Name = "SaveLogMenuItem";
- this.SaveLogMenuItem.Size = new System.Drawing.Size(143, 22);
+ this.SaveLogMenuItem.Size = new System.Drawing.Size(134, 22);
this.SaveLogMenuItem.Text = "&Save Log";
this.SaveLogMenuItem.Click += new System.EventHandler(this.SaveLogMenuItem_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
- this.toolStripSeparator1.Size = new System.Drawing.Size(140, 6);
+ this.toolStripSeparator1.Size = new System.Drawing.Size(131, 6);
//
// ExitMenuItem
//
this.ExitMenuItem.Name = "ExitMenuItem";
this.ExitMenuItem.ShortcutKeyDisplayString = "Alt+F4";
- this.ExitMenuItem.Size = new System.Drawing.Size(143, 22);
+ this.ExitMenuItem.Size = new System.Drawing.Size(134, 22);
this.ExitMenuItem.Text = "E&xit";
this.ExitMenuItem.Click += new System.EventHandler(this.ExitMenuItem_Click);
//
@@ -195,7 +211,7 @@
this.SelectAllMenuItem,
this.ClearMenuItem});
this.EditSubMenu.Name = "EditSubMenu";
- this.EditSubMenu.Size = new System.Drawing.Size(37, 20);
+ this.EditSubMenu.Size = new System.Drawing.Size(39, 20);
this.EditSubMenu.Text = "Edit";
//
// CopyMenuItem
@@ -203,7 +219,7 @@
this.CopyMenuItem.Name = "CopyMenuItem";
this.CopyMenuItem.ShortcutKeyDisplayString = "";
this.CopyMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.C)));
- this.CopyMenuItem.Size = new System.Drawing.Size(167, 22);
+ this.CopyMenuItem.Size = new System.Drawing.Size(164, 22);
this.CopyMenuItem.Text = "&Copy";
this.CopyMenuItem.Click += new System.EventHandler(this.CopyMenuItem_Click);
//
@@ -212,14 +228,14 @@
this.SelectAllMenuItem.Name = "SelectAllMenuItem";
this.SelectAllMenuItem.ShortcutKeyDisplayString = "";
this.SelectAllMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.A)));
- this.SelectAllMenuItem.Size = new System.Drawing.Size(167, 22);
+ this.SelectAllMenuItem.Size = new System.Drawing.Size(164, 22);
this.SelectAllMenuItem.Text = "Select &All";
this.SelectAllMenuItem.Click += new System.EventHandler(this.SelectAllMenuItem_Click);
//
// ClearMenuItem
//
this.ClearMenuItem.Name = "ClearMenuItem";
- this.ClearMenuItem.Size = new System.Drawing.Size(167, 22);
+ this.ClearMenuItem.Size = new System.Drawing.Size(164, 22);
this.ClearMenuItem.Text = "Clear";
this.ClearMenuItem.Click += new System.EventHandler(this.ClearMenuItem_Click);
//
@@ -227,9 +243,10 @@
//
this.OptionsSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.MaxLinesMenuItem,
- this.SegmentSizeMenuItem});
+ this.SegmentSizeMenuItem,
+ this.AutoScrollMenuItem});
this.OptionsSubMenu.Name = "OptionsSubMenu";
- this.OptionsSubMenu.Size = new System.Drawing.Size(58, 20);
+ this.OptionsSubMenu.Size = new System.Drawing.Size(61, 20);
this.OptionsSubMenu.Text = "&Settings";
//
// MaxLinesMenuItem
@@ -239,6 +256,21 @@
this.MaxLinesMenuItem.Text = "&Set Max Lines...";
this.MaxLinesMenuItem.Click += new System.EventHandler(this.MaxLinesMenuItem_Click);
//
+ // SegmentSizeMenuItem
+ //
+ this.SegmentSizeMenuItem.Name = "SegmentSizeMenuItem";
+ this.SegmentSizeMenuItem.Size = new System.Drawing.Size(180, 22);
+ this.SegmentSizeMenuItem.Text = "Set Segment Size...";
+ this.SegmentSizeMenuItem.Click += new System.EventHandler(this.SegmentSizeMenuItem_Click);
+ //
+ // AutoScrollMenuItem
+ //
+ this.AutoScrollMenuItem.CheckOnClick = true;
+ this.AutoScrollMenuItem.Name = "AutoScrollMenuItem";
+ this.AutoScrollMenuItem.Size = new System.Drawing.Size(180, 22);
+ this.AutoScrollMenuItem.Text = "Auto Scroll";
+ this.AutoScrollMenuItem.Click += new System.EventHandler(this.AutoScrollMenuItem_Click);
+ //
// groupBox2
//
this.groupBox2.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
@@ -326,13 +358,6 @@
this.LoggingEnabled.UseVisualStyleBackColor = true;
this.LoggingEnabled.CheckedChanged += new System.EventHandler(this.LoggingEnabled_CheckedChanged);
//
- // SegmentSizeMenuItem
- //
- this.SegmentSizeMenuItem.Name = "SegmentSizeMenuItem";
- this.SegmentSizeMenuItem.Size = new System.Drawing.Size(180, 22);
- this.SegmentSizeMenuItem.Text = "Set Segment Size...";
- this.SegmentSizeMenuItem.Click += new System.EventHandler(this.SegmentSizeMenuItem_Click);
- //
// TraceLogger
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@@ -371,7 +396,7 @@
private System.Windows.Forms.GroupBox groupBox2;
private System.Windows.Forms.CheckBox LoggingEnabled;
private System.Windows.Forms.ToolStripMenuItem OptionsSubMenu;
- private VirtualListView TraceView;
+ private BizHawk.Client.EmuHawk.PlatformAgnosticVirtualListView TraceView;
public System.Windows.Forms.ColumnHeader Disasm;
private System.Windows.Forms.ToolStripMenuItem MaxLinesMenuItem;
private System.Windows.Forms.RadioButton ToFileRadio;
@@ -389,5 +414,6 @@
private System.Windows.Forms.ToolStripMenuItem ClearContextMenu;
private System.Windows.Forms.Button OpenLogFile;
private System.Windows.Forms.ToolStripMenuItem SegmentSizeMenuItem;
+ private System.Windows.Forms.ToolStripMenuItem AutoScrollMenuItem;
}
}
\ No newline at end of file
diff --git a/BizHawk.Client.EmuHawk/tools/TraceLogger.cs b/BizHawk.Client.EmuHawk/tools/TraceLogger.cs
index 45fac3a489..b6f93873bf 100644
--- a/BizHawk.Client.EmuHawk/tools/TraceLogger.cs
+++ b/BizHawk.Client.EmuHawk/tools/TraceLogger.cs
@@ -36,6 +36,9 @@ namespace BizHawk.Client.EmuHawk
set { this.Registers.Width = value; }
}
+ [ConfigPersist]
+ public override bool AutoScroll { get; set; }
+
private FileInfo _logFile;
private FileInfo LogFile
{
@@ -72,6 +75,33 @@ namespace BizHawk.Client.EmuHawk
MaxLines = 10000;
FileSizeCap = 150; // make 1 frame of tracelog for n64/psx fit in
_splitFile = FileSizeCap != 0;
+
+ SetupTraceViewSettings();
+ }
+
+ private void SetupColumns()
+ {
+ TraceView.AllColumns.Clear();
+ TraceView.AddColumn("Disasm", "Disasm", 239, PlatformAgnosticVirtualListView.ListColumn.InputType.Text);
+ TraceView.AddColumn("Registers", "Registers", 357, PlatformAgnosticVirtualListView.ListColumn.InputType.Text);
+ }
+
+ private void SetupTraceViewSettings()
+ {
+ TraceView.MultiSelect = true;
+ TraceView.CellWidthPadding = 3;
+ TraceView.CellHeightPadding = 2;
+ TraceView.ScrollSpeed = 5;
+ TraceView.AllowColumnResize = true;
+ TraceView.AllowColumnReorder = true;
+ TraceView.ColumnHeaderFont = new System.Drawing.Font("Courier New", 8F);
+ TraceView.ColumnHeaderFontColor = System.Drawing.Color.Black;
+ TraceView.ColumnHeaderBackgroundColor = System.Drawing.Color.White;
+ TraceView.ColumnHeaderBackgroundHighlightColor = System.Drawing.Color.LightSteelBlue;
+ TraceView.ColumnHeaderOutlineColor = System.Drawing.Color.White;
+ TraceView.CellFont = new System.Drawing.Font("Courier New", 8F);
+ TraceView.CellFontColor = System.Drawing.Color.Black;
+ TraceView.CellBackgroundColor = System.Drawing.Color.White;
}
public bool UpdateBefore
@@ -94,7 +124,9 @@ namespace BizHawk.Client.EmuHawk
text = "";
if (index < _instructions.Count)
{
- switch (column)
+ var test = TraceView.AllColumns;
+
+ switch (TraceView.GetOriginalColumnIndex(column))
{
case 0:
text = _instructions[index].Disassembly.TrimEnd();
@@ -111,8 +143,10 @@ namespace BizHawk.Client.EmuHawk
ClearList();
OpenLogFile.Enabled = false;
LoggingEnabled.Checked = false;
+ AutoScrollMenuItem.Checked = AutoScroll;
Tracer.Sink = null;
SetTracerBoxTitle();
+ SetupColumns();
}
class CallbackSink : ITraceSink
@@ -134,6 +168,12 @@ namespace BizHawk.Client.EmuHawk
if (ToWindowRadio.Checked)
{
TraceView.VirtualListSize = _instructions.Count;
+ if (GlobalWin.MainForm.EmulatorPaused)
+ {
+ if (AutoScroll && _instructions.Count != 0)
+ TraceView.ScrollToIndex(_instructions.IndexOf(_instructions.Last()));
+ TraceView.Refresh();
+ }
}
else
{
@@ -148,8 +188,10 @@ namespace BizHawk.Client.EmuHawk
//connect tracer to sink for next frame
if (ToWindowRadio.Checked)
{
- //update listview with most recentr results
- TraceView.BlazingFast = !GlobalWin.MainForm.EmulatorPaused;
+ if (AutoScroll && _instructions.Count != 0)
+ TraceView.ScrollToIndex(_instructions.IndexOf(_instructions.Last()));
+
+ TraceView.Refresh();
Tracer.Sink = new CallbackSink()
{
@@ -161,8 +203,7 @@ namespace BizHawk.Client.EmuHawk
}
_instructions.Add(info);
}
- };
- _instructions.Clear();
+ };
}
else
{
@@ -327,7 +368,8 @@ namespace BizHawk.Client.EmuHawk
private void CopyMenuItem_Click(object sender, EventArgs e)
{
- var indices = TraceView.SelectedIndices;
+ //var indices = TraceView.SelectedIndices;
+ var indices = TraceView.SelectedRows.ToList();
if (indices.Count > 0)
{
@@ -389,6 +431,11 @@ namespace BizHawk.Client.EmuHawk
}
}
+ private void AutoScrollMenuItem_Click(object sender, EventArgs e)
+ {
+ AutoScroll = ((ToolStripMenuItem)sender as ToolStripMenuItem).Checked;
+ }
+
#endregion
#region Dialog and ListView Events
| | | | |