From fb29663ce6a4ed557faf808458a72dfce075f82a Mon Sep 17 00:00:00 2001 From: YoshiRulz Date: Fri, 25 Oct 2019 21:11:54 +1000 Subject: [PATCH] Platform-agnostic VirtualListView --- .../BizHawk.Client.EmuHawk.csproj | 27 + .../PlatformAgnosticVirtualListView.API.cs | 173 ++++ ...PlatformAgnosticVirtualListView.Classes.cs | 315 +++++++ ...PlatformAgnosticVirtualListView.Drawing.cs | 523 ++++++++++++ ...rmAgnosticVirtualListView.EventHandlers.cs | 696 ++++++++++++++++ ...PlatformAgnosticVirtualListView.Helpers.cs | 359 ++++++++ ...tformAgnosticVirtualListView.Properties.cs | 780 ++++++++++++++++++ .../PlatformAgnosticVirtualListView.cs | 356 ++++++++ .../tools/TraceLogger.Designer.cs | 50 +- BizHawk.Client.EmuHawk/tools/TraceLogger.cs | 60 +- 10 files changed, 3320 insertions(+), 19 deletions(-) create mode 100644 BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.API.cs create mode 100644 BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Classes.cs create mode 100644 BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Drawing.cs create mode 100644 BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.EventHandlers.cs create mode 100644 BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Helpers.cs create mode 100644 BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.Properties.cs create mode 100644 BizHawk.Client.EmuHawk/CustomControls/PlatformAgnosticVirtualListView.cs diff --git a/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj b/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj index ce2464e823..d5e7b52341 100644 --- a/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj +++ b/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj @@ -612,6 +612,30 @@ MsgBox.cs + + PlatformAgnosticVirtualListView.cs + Component + + + PlatformAgnosticVirtualListView.cs + Component + + + PlatformAgnosticVirtualListView.cs + Component + + + PlatformAgnosticVirtualListView.cs + Component + + + PlatformAgnosticVirtualListView.cs + Component + + + PlatformAgnosticVirtualListView.cs + Component + Form @@ -755,6 +779,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..94fd8cf87a --- /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..9e3569e91e --- /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..91c2b624eb --- /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..e000f2f41c --- /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..555c2bd141 --- /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..971f8c43aa --- /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..b3b7383069 --- /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..132ce7a40e 100644 --- a/BizHawk.Client.EmuHawk/tools/TraceLogger.Designer.cs +++ b/BizHawk.Client.EmuHawk/tools/TraceLogger.Designer.cs @@ -31,7 +31,7 @@ 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.TraceView = new BizHawk.Client.EmuHawk.PlatformAgnosticVirtualListView(); this.Disasm = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.Registers = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.TraceContextMenu = new System.Windows.Forms.ContextMenuStrip(this.components); @@ -57,6 +57,7 @@ this.ToWindowRadio = new System.Windows.Forms.RadioButton(); this.LoggingEnabled = new System.Windows.Forms.CheckBox(); this.SegmentSizeMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.AutoScrollMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.TracerBox.SuspendLayout(); this.TraceContextMenu.SuspendLayout(); this.menuStrip1.SuspendLayout(); @@ -78,29 +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; + this.TraceView.VirtualListSize = 0; + this.TraceView.VirtualMode = false; // // Disasm // @@ -227,7 +243,8 @@ // 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.Text = "&Settings"; @@ -239,6 +256,14 @@ this.MaxLinesMenuItem.Text = "&Set Max Lines..."; this.MaxLinesMenuItem.Click += new System.EventHandler(this.MaxLinesMenuItem_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) @@ -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 d2690cf4cd..f53b0ee5da 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,8 @@ 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 +142,10 @@ namespace BizHawk.Client.EmuHawk ClearList(); OpenLogFile.Enabled = false; LoggingEnabled.Checked = false; + AutoScrollMenuItem.Checked = AutoScroll; Tracer.Sink = null; SetTracerBoxTitle(); + SetupColumns(); } class CallbackSink : ITraceSink @@ -137,7 +170,13 @@ namespace BizHawk.Client.EmuHawk // how or why I don't know // it's hidden behind an internal class ListViewNativeItemCollection TraceView.VirtualListSize = 0; - TraceView.VirtualListSize = _instructions.Count; + TraceView.VirtualListSize = _instructions.Count; + if (GlobalWin.MainForm.EmulatorPaused) + { + if (AutoScroll && _instructions.Count != 0) + TraceView.ScrollToIndex(_instructions.IndexOf(_instructions.Last())); + TraceView.Refresh(); + } } else { @@ -152,9 +191,11 @@ namespace BizHawk.Client.EmuHawk //connect tracer to sink for next frame if (ToWindowRadio.Checked) { - //update listview with most recent results - TraceView.BlazingFast = !GlobalWin.MainForm.EmulatorPaused; - + if (AutoScroll && _instructions.Count != 0) + { + TraceView.ScrollToIndex(_instructions.IndexOf(_instructions.Last())); + } + TraceView.Refresh(); Tracer.Sink = new CallbackSink() { putter = (info) => @@ -166,7 +207,6 @@ namespace BizHawk.Client.EmuHawk _instructions.Add(info); } }; - _instructions.Clear(); } else { @@ -331,7 +371,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) { @@ -391,6 +432,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