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; } } }