using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Linq; using System.Windows.Forms; using BizHawk.Client.Common; using BizHawk.Client.EmuHawk.CustomControls; namespace BizHawk.Client.EmuHawk { // Row width depends on font size and padding // Column width is specified in column headers // Row width is specified for horizontal orientation public partial class InputRoll : Control { private readonly IControlRenderer _renderer; private readonly SortedSet _selectedItems = new SortedSet(new SortCell()); // scrollbar location(s) are calculated later (e.g. on resize) private readonly VScrollBar _vBar = new VScrollBar { Visible = false }; private readonly HScrollBar _hBar = new HScrollBar { Visible = false }; private readonly Timer _hoverTimer = new Timer(); private readonly byte[] _lagFrames = new byte[256]; // Large enough value that it shouldn't ever need resizing. // apparently not large enough for 4K private readonly Color _foreColor; private readonly Color _backColor; private RollColumns _columns = new RollColumns(); private bool _horizontalOrientation; private bool _programmaticallyUpdatingScrollBarValues; private int _rowCount; private SizeF _charSize; // Updated on paint private int[] _horizontalColumnHeights; private int[] _horizontalColumnTops; private RollColumn _columnDown; private RollColumn _columnResizing; private int? _currentX; private int? _currentY; private Cell _lastCell; // The previous cell the mouse was in private int _drawHeight; private int _drawWidth; // Hiding lag frames (Mainly intended for < 60fps play.) [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int LagFramesToHide { get; set; } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool HideWasLagFrames { get; set; } /// /// Gets or sets a value indicating whether or not the control will respond to right-click events with a context menu /// [Category("Behavior")] public bool AllowRightClickSelection { get; set; } = true; /// /// Gets or sets a value indicating whether or not Home and End will navigate to the beginning or end of the list /// [Category("Behavior")] public bool AllowMassNavigationShortcuts { get; set; } = true; [Category("Behavior")] public bool LetKeysModifySelection { get; set; } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool SuspendHotkeys { get; set; } public InputRoll() { SetStyle(ControlStyles.AllPaintingInWmPaint, true); SetStyle(ControlStyles.UserPaint, true); SetStyle(ControlStyles.SupportsTransparentBackColor, true); SetStyle(ControlStyles.Opaque, true); SetStyle(ControlStyles.OptimizedDoubleBuffer, true); _renderer = new GdiPlusRenderer(Font); UpdateCellSize(); _vBar.SmallChange = CellHeight; _vBar.LargeChange = CellHeight * 20; _hBar.SmallChange = CellWidth; _hBar.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(); _foreColor = ForeColor; _backColor = BackColor; } private void HoverTimerEventProcessor(object sender, EventArgs e) { _hoverTimer.Stop(); CellHovered?.Invoke(this, new CellEventArgs(_lastCell, CurrentCell)); } protected override void Dispose(bool disposing) { _renderer.Dispose(); base.Dispose(disposing); } public void ExpandColumnToFitText(string columnName, string text) { var column = AllColumns.SingleOrDefault(c => c.Name == columnName); if (column != null) { using var g = CreateGraphics(); using (_renderer.LockGraphics(g)) { var strLength = (int)_renderer.MeasureString(text, Font).Width + (CellWidthPadding * 2); if (column.Width < strLength) { column.Width = strLength; AllColumns.ColumnsChanged(); Refresh(); } } } } protected override void OnDoubleClick(EventArgs e) { if (IsHoveringOnColumnEdge) { if (HorizontalOrientation) { // TODO } else { var maxLength = CurrentCell.Column.Text?.Length ?? 0; for (int i = 0; i < RowCount; i++) { string text = ""; int offSetX = 0, offSetY = 0; QueryItemText?.Invoke(i, CurrentCell.Column, out text, ref offSetX, ref offSetY); if (text.Length > maxLength) { maxLength = text.Length; } } var newWidth = (maxLength * _charSize.Width) + (CellWidthPadding * 2); CurrentCell.Column.Width = (int)newWidth; _columns.ColumnsChanged(); Refresh(); } } base.OnDoubleClick(e); } #region Properties /// /// 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 a value indicating whether grid lines are displayed around cells /// [Category("Appearance")] [DefaultValue(true)] public bool GridLines { get; set; } = true; /// /// Gets or sets a value indicating whether the control is horizontal or vertical /// [Category("Behavior")] public bool HorizontalOrientation { get => _horizontalOrientation; set { if (_horizontalOrientation != value) { int temp = ScrollSpeed; _horizontalOrientation = value; OrientationChanged(); _hBar.SmallChange = CellWidth; _vBar.SmallChange = CellHeight; ScrollSpeed = temp; } } } /// /// Gets or sets the scrolling speed /// [Category("Behavior")] public int ScrollSpeed { get { if (HorizontalOrientation) { return _hBar.SmallChange / CellWidth; } return _vBar.SmallChange / CellHeight; } set { if (HorizontalOrientation) { _hBar.SmallChange = value * CellWidth; } else { _vBar.SmallChange = value * CellHeight; } } } /// /// Gets or sets the sets the virtual number of rows to be displayed. Does not include the column header row. /// [Category("Behavior")] public int RowCount { get => _rowCount; set { bool fullRefresh = false; if (_rowCount != value) { if (value < _rowCount && IsVisible(value)) { fullRefresh = true; } _rowCount = value; _selectedItems.RemoveWhere(i => i.RowIndex >= _rowCount); RecalculateScrollBars(); } // Similarly to ListView in virtual mode, we want to always refresh // when setting row count, that gives the calling code assurance that // redraw will happen if (fullRefresh) { Refresh(); } else { FastDraw(); } } } private void FastDraw() { if (HorizontalOrientation) { int x = MaxColumnWidth; int y = 0; int w = Width - x; int h = VisibleColumns.Any() ? GetHColBottom(VisibleColumns.Count() - 1) : 0; h = Math.Min(h, _drawHeight); Invalidate(new Rectangle(x, y, w, h)); } else { int x = 0; int y = ColumnHeight + 1; int w = VisibleColumns.Any() ? Math.Min(VisibleColumns.Max(c => c.Right) - _hBar.Value, Width) : 0; int h = Math.Min(RowCount * CellHeight, Height - y); Invalidate(new Rectangle(x, y, w, h)); } } /// /// Gets or sets a value indicating whether columns can be resized /// [Category("Behavior")] public bool AllowColumnResize { get; set; } /// /// Gets or sets a value indicating whether columns can be reordered /// [Category("Behavior")] public bool AllowColumnReorder { 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 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; } /// /// All visible columns /// [Category("Behavior")] public IEnumerable VisibleColumns => _columns.VisibleColumns; /// /// Gets or sets how the InputRoll scrolls when calling ScrollToIndex. /// [DefaultValue("near")] [Category("Behavior")] public string ScrollMethod { get; set; } = "near"; /// /// Gets or sets a value indicating how the scrolling behavior for the hover event /// [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; } /// /// Returns all columns including those that are not visible /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public RollColumns AllColumns => _columns; [DefaultValue(750)] [Category("Behavior")] public int HoverInterval { get => _hoverTimer.Interval; set => _hoverTimer.Interval = value; } /// /// Gets or sets a value indicating whether or not the control can be toggled into HorizontalOrientation mode /// [DefaultValue(false)] [Category("Behavior")] public bool Rotatable { get; set; } #endregion #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 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; /// /// Fire the QueryFrameLag event which checks if a given frame is a lag frame /// [Category("Virtual")] public event QueryFrameLagHandler QueryFrameLag; /// /// 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; /// /// Retrieve the text for a cell /// public delegate void QueryItemTextHandler(int index, RollColumn column, out string text, ref int offsetX, ref int offsetY); /// /// Retrieve the background color for a cell /// public delegate void QueryItemBkColorHandler(int index, RollColumn 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, RollColumn column, ref Bitmap icon, ref int offsetX, ref int offsetY); /// /// Check if a given frame is a lag frame /// public delegate bool QueryFrameLagHandler(int index, bool hideWasLag); 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); public class CellEventArgs { public CellEventArgs(Cell oldCell, Cell newCell) { OldCell = oldCell; NewCell = newCell; } public Cell OldCell { get; } public Cell NewCell { get; } } public class ColumnClickEventArgs { public ColumnClickEventArgs(RollColumn column) { Column = column; } public RollColumn Column { get; } } public class ColumnReorderedEventArgs { public ColumnReorderedEventArgs(int oldDisplayIndex, int newDisplayIndex, RollColumn column) { Column = column; OldDisplayIndex = oldDisplayIndex; NewDisplayIndex = newDisplayIndex; } public RollColumn Column { get; } public int OldDisplayIndex { get; } public int NewDisplayIndex { get; } } #endregion #region Api private int? _lastSelectedRow; public void SelectRow(int index, bool val) { if (_columns.VisibleColumns.Any()) { if (val) { SelectCell(new Cell { RowIndex = index, Column = _columns[0] }); _lastSelectedRow = index; } else { IEnumerable items = _selectedItems.Where(cell => cell.RowIndex == index); _selectedItems.RemoveWhere(items.Contains); _lastSelectedRow = _selectedItems.LastOrDefault()?.RowIndex; } } } public void SelectAll() { var oldFullRowVal = FullRowSelect; FullRowSelect = true; for (int i = 0; i < RowCount; i++) { SelectRow(i, true); } FullRowSelect = oldFullRowVal; _lastSelectedRow = RowCount; } public void DeselectAll() { _lastSelectedRow = null; _selectedItems.Clear(); } public void TruncateSelection(int index) { _selectedItems.RemoveWhere(cell => cell.RowIndex > index); _lastSelectedRow = _selectedItems.LastOrDefault()?.RowIndex; } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool IsPointingAtColumnHeader => IsHoveringOnColumnCell; [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int? FirstSelectedIndex => AnyRowsSelected ? SelectedRows.Min() : (int?)null; [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int? LastSelectedIndex => AnyRowsSelected ? SelectedRows.Max() : (int?)null; /// /// Gets or sets the current Cell that the mouse was in. /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Cell CurrentCell { get; set; } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool IsPaintDown { get; private set; } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool RightButtonHeld { get; private set; } 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 rollSettings) { _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 first visible row index, if scrolling is needed /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int FirstVisibleRow { get { if (HorizontalOrientation) { return _hBar.Value / CellWidth; } return _vBar.Value / CellHeight; } set { if (HorizontalOrientation) { if (NeedsHScrollbar) { _programmaticallyUpdatingScrollBarValues = true; if (value * CellWidth <= _hBar.Maximum) { _hBar.Value = value * CellWidth; } else { _hBar.Value = _hBar.Maximum; } _programmaticallyUpdatingScrollBarValues = false; } } else { if (NeedsVScrollbar) { _programmaticallyUpdatingScrollBarValues = true; if (value * CellHeight <= _vBar.Maximum) { _vBar.Value = value * CellHeight; } else { _vBar.Value = _vBar.Maximum; } _programmaticallyUpdatingScrollBarValues = false; } } PointMouseToNewCell(); } } private int LastFullyVisibleRow { get { int halfRow = 0; if (HorizontalOrientation) { halfRow = 1; // TODO: A more precise calculation, but it really isn't important, you have to be pixel perfect for this to be off by 1 and even then it doesn't look bad because the 1 pixel is the border } else { if ((_drawHeight - ColumnHeight - 3) % CellHeight < CellHeight / 2) { halfRow = 1; } } return FirstVisibleRow + VisibleRows - halfRow + CountLagFramesDisplay(VisibleRows - halfRow); } } private int LastVisibleRow { get => FirstVisibleRow + VisibleRows + CountLagFramesDisplay(VisibleRows); set { int halfRow = 0; if (HorizontalOrientation) { halfRow = 1; // TODO: A more precise calculation, but it really isn't important, you have to be pixel perfect for this to be off by 1 and even then it doesn't look bad because the 1 pixel is the border } else { if ((_drawHeight - ColumnHeight - 3) % CellHeight < CellHeight / 2) { halfRow = 1; } } if (LagFramesToHide == 0) { FirstVisibleRow = Math.Max(value - (VisibleRows - halfRow), 0); } else { if (Math.Abs(LastFullyVisibleRow - value) > VisibleRows) // Big jump { FirstVisibleRow = Math.Max(value - (ExpectedDisplayRange() - halfRow), 0); SetLagFramesArray(); } // Small jump, more accurate int lastVisible = LastFullyVisibleRow; do { if ((lastVisible - value) / (LagFramesToHide + 1) != 0) { FirstVisibleRow = Math.Max(FirstVisibleRow - ((lastVisible - value) / (LagFramesToHide + 1)), 0); } else { FirstVisibleRow -= Math.Sign(lastVisible - value); } SetLagFramesArray(); lastVisible = LastFullyVisibleRow; } while ((lastVisible - value < 0 || lastVisible - value > _lagFrames[VisibleRows - halfRow]) && FirstVisibleRow != 0); } PointMouseToNewCell(); } } private bool IsVisible(int index) { return index >= FirstVisibleRow && index <= LastFullyVisibleRow; } public bool IsPartiallyVisible(int index) { return index >= FirstVisibleRow && index <= LastVisibleRow; } /// /// Gets the number of rows currently visible including partially visible rows. /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int VisibleRows { get { if (HorizontalOrientation) { return (_drawWidth - MaxColumnWidth) / CellWidth; } return (_drawHeight - ColumnHeight - 3) / CellHeight; // Minus three makes it work } } private Cell _draggingCell; 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") { if (LagFramesToHide == 0) { FirstVisibleRow = Math.Max(index - (VisibleRows / 2), 0); } else { if (Math.Abs(FirstVisibleRow + CountLagFramesDisplay(VisibleRows / 2) - index) > VisibleRows) // Big jump { FirstVisibleRow = Math.Max(index - (ExpectedDisplayRange() / 2), 0); SetLagFramesArray(); } // Small jump, more accurate int lastVisible = FirstVisibleRow + CountLagFramesDisplay(VisibleRows / 2); do { if ((lastVisible - index) / (LagFramesToHide + 1) != 0) { FirstVisibleRow = Math.Max(FirstVisibleRow - ((lastVisible - index) / (LagFramesToHide + 1)), 0); } else { FirstVisibleRow -= Math.Sign(lastVisible - index); } SetLagFramesArray(); lastVisible = FirstVisibleRow + CountLagFramesDisplay(VisibleRows / 2); } while ((lastVisible - index < 0 || lastVisible - index > _lagFrames[VisibleRows]) && FirstVisibleRow != 0); } } } PointMouseToNewCell(); } private bool _programmaticallyChangingRow = false; /// /// 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)) { _programmaticallyChangingRow = true; if (FirstVisibleRow > index) { FirstVisibleRow = index; } else { LastVisibleRow = index; } } } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IEnumerable SelectedRows => _selectedItems .Where(cell => cell.RowIndex.HasValue) .Select(cell => cell.RowIndex.Value) .Distinct(); public bool AnyRowsSelected => _selectedItems.Any(cell => cell.RowIndex.HasValue); public IEnumerable GenerateContextMenuItems() { if (Rotatable) { yield return new ToolStripSeparator(); var rotate = new ToolStripMenuItem { Name = "RotateMenuItem", Text = "Rotate", ShortcutKeyDisplayString = RotateHotkeyStr, }; rotate.Click += (o, ev) => { HorizontalOrientation ^= true; }; yield return rotate; } } public string RotateHotkeyStr => "Ctrl+Shift+F"; #endregion #region Mouse and Key Events private bool _columnDownMoved; private int _previousX; // TODO: move me // It's necessary to call this anytime the control is programmatically scrolled // Since the mouse may not be pointing to the same cell anymore private void PointMouseToNewCell() { if (_currentX.HasValue && _currentY.HasValue) { var newCell = CalculatePointedCell(_currentX.Value, _currentY.Value); if (CurrentCell != newCell) { if (QueryFrameLag != null && newCell.RowIndex.HasValue) { newCell.RowIndex += CountLagFramesDisplay(newCell.RowIndex.Value); } newCell.RowIndex += FirstVisibleRow; if (newCell.RowIndex < 0) { newCell.RowIndex = 0; } if (!_programmaticallyChangingRow) { _programmaticallyChangingRow = false; CellChanged(newCell); } } } } protected override void OnMouseMove(MouseEventArgs e) { _previousX = _currentX ?? 0; _currentX = e.X; _currentY = e.Y; if (_columnResizing != null) { if (_currentX != _previousX) { _columnResizing.Width += _currentX.Value - _previousX; if (_columnResizing.Width <= 0) { _columnResizing.Width = 1; } _columns.ColumnsChanged(); Refresh(); } } else if (_columnDown != null) { _columnDownMoved = true; } Cell newCell = CalculatePointedCell(_currentX.Value, _currentY.Value); // SuuperW: Hide lag frames if (QueryFrameLag != null && newCell.RowIndex.HasValue) { newCell.RowIndex += CountLagFramesDisplay(newCell.RowIndex.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(); } Cursor = IsHoveringOnColumnEdge || _columnResizing != null ? Cursors.VSplit : 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) { bool refresh = false; _currentX = null; _currentY = null; if (IsHoveringOnColumnCell) { refresh = true; } CurrentCell = null; IsPaintDown = false; _columnResizing = null; _hoverTimer.Stop(); if (refresh) { Refresh(); } base.OnMouseLeave(e); } // TODO add query callback of whether to select the cell or not protected override void OnMouseDown(MouseEventArgs e) { if (e.Button == MouseButtons.Left) { if (IsHoveringOnColumnEdge) { _columnResizing = CurrentCell.Column; } if (IsHoveringOnColumnCell) { _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 == ColumnType.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 == ColumnType.Text) { SelectCell(CurrentCell, toggle: true); } else if (ModifierKeys != Keys.Shift) { _selectedItems.Clear(); SelectCell(CurrentCell); } Refresh(); SelectedIndexChanged?.Invoke(this, new EventArgs()); } } if (AllowRightClickSelection && e.Button == MouseButtons.Right) { if (!IsHoveringOnColumnCell && CurrentCell != null) { _currentX = e.X; _currentY = e.Y; Cell newCell = CalculatePointedCell(_currentX.Value, _currentY.Value); newCell.RowIndex += FirstVisibleRow; // If this cell is not currently selected, clear and select if (!_selectedItems.Contains(newCell)) { _selectedItems.Clear(); SelectCell(CurrentCell); } CellChanged(newCell); } } base.OnMouseDown(e); } protected override void OnMouseUp(MouseEventArgs e) { if (_columnResizing == null && IsHoveringOnColumnCell) { if (_columnDown != null && _columnDownMoved) { DoColumnReorder(); _columnDown = null; Refresh(); } else if (e.Button == MouseButtons.Left) { ColumnClickEvent(ColumnAtPixel(e.X)); } else if (e.Button == MouseButtons.Right) { ColumnRightClickEvent(ColumnAtPixel(e.X)); } } _columnResizing = null; _columnDown = null; _columnDownMoved = false; RightButtonHeld = false; IsPaintDown = false; base.OnMouseUp(e); } private void IncrementScrollBar(ScrollBar bar, bool increment) { int newVal; if (increment) { newVal = bar.Value + bar.SmallChange; if (newVal > bar.Maximum - bar.LargeChange) { newVal = bar.Maximum - bar.LargeChange; } } else { newVal = bar.Value - bar.SmallChange; if (newVal < 0) { newVal = 0; } } _programmaticallyUpdatingScrollBarValues = true; bar.Value = newVal; _programmaticallyUpdatingScrollBarValues = false; } protected override void OnMouseWheel(MouseEventArgs e) { if (RightButtonHeld) { DoRightMouseScroll(this, e); } else { if (HorizontalOrientation) { do { IncrementScrollBar(_hBar, e.Delta < 0); SetLagFramesFirst(); } while (_lagFrames[0] != 0 && _hBar.Value != 0 && _hBar.Value != _hBar.Maximum); } else { do { IncrementScrollBar(_vBar, e.Delta < 0); SetLagFramesFirst(); } while (_lagFrames[0] != 0 && _vBar.Value != 0 && _vBar.Value != _vBar.Maximum); } 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(RollColumn column) { ColumnClick?.Invoke(this, new ColumnClickEventArgs(column)); } private void ColumnRightClickEvent(RollColumn column) { ColumnRightClick?.Invoke(this, new ColumnClickEventArgs(column)); } // This allows arrow keys to be detected by KeyDown. protected override void OnPreviewKeyDown(PreviewKeyDownEventArgs e) { if (e.KeyCode == Keys.Left || e.KeyCode == Keys.Right || e.KeyCode == Keys.Up || e.KeyCode == Keys.Down) { e.IsInputKey = true; } } protected override void OnKeyDown(KeyEventArgs e) { if (!SuspendHotkeys) { if (e.Control && !e.Alt && e.Shift && e.KeyCode == Keys.F) // Ctrl+Shift+F { if (Rotatable) { HorizontalOrientation ^= true; } } // Scroll else if (!e.Control && !e.Alt && !e.Shift && e.KeyCode == Keys.PageUp) // Page Up { var selectedRow = SelectedRows.Any() ? SelectedRows.First() : FirstVisibleRow; var increment = LastVisibleRow - FirstVisibleRow; var newSelectedRow = selectedRow - increment; if (newSelectedRow < 0) { newSelectedRow = 0; } FirstVisibleRow = newSelectedRow; DeselectAll(); SelectRow(newSelectedRow, true); Refresh(); } else if (!e.Control && !e.Alt && !e.Shift && e.KeyCode == Keys.PageDown) // Page Down { var selectedRow = SelectedRows.Any() ? SelectedRows.First() : FirstVisibleRow; var increment = LastVisibleRow - FirstVisibleRow; var newSelectedRow = selectedRow + increment; if (newSelectedRow > RowCount - 1) { newSelectedRow = RowCount - 1; } LastVisibleRow = newSelectedRow; DeselectAll(); SelectRow(newSelectedRow, true); Refresh(); } else if (AllowMassNavigationShortcuts && !e.Control && !e.Alt && !e.Shift && e.KeyCode == Keys.Home) // Home { DeselectAll(); SelectRow(0, true); FirstVisibleRow = 0; Refresh(); } else if (AllowMassNavigationShortcuts && !e.Control && !e.Alt && !e.Shift && e.KeyCode == Keys.End) // End { DeselectAll(); SelectRow(RowCount - 1, true); LastVisibleRow = RowCount; Refresh(); } else if (!e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.Up) // Up { if (SelectedRows.Any()) { var selectedRow = SelectedRows.First(); if (selectedRow > 0) { var targetSelectedRow = selectedRow - 1; DeselectAll(); SelectRow(targetSelectedRow, true); ScrollToIndex(targetSelectedRow); Refresh(); } } } else if (!e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.Down) // Down { if (SelectedRows.Any()) { var selectedRow = SelectedRows.First(); if (selectedRow < RowCount - 1) { var targetSelectedRow = selectedRow + 1; DeselectAll(); SelectRow(targetSelectedRow, true); ScrollToIndex(targetSelectedRow); Refresh(); } } } else if (!e.Control && e.Shift && !e.Alt && e.KeyCode == Keys.Up) // Shift+Up { if (MultiSelect && _lastSelectedRow > 0) { if (_selectedItems.Any(i => i.RowIndex == _lastSelectedRow.Value) && _selectedItems.Any(i => i.RowIndex == _lastSelectedRow - 1)) // Unhighlight if already highlighted { SelectRow(_lastSelectedRow.Value, false); } else { SelectRow(_lastSelectedRow.Value - 1, true); } Refresh(); } } else if (!e.Control && e.Shift && !e.Alt && e.KeyCode == Keys.Down) // Shift+Down { if (MultiSelect && _lastSelectedRow < RowCount - 1) { if (_selectedItems.Any(i => i.RowIndex == _lastSelectedRow.Value) && _selectedItems.Any(i => i.RowIndex == _lastSelectedRow + 1)) // Unhighlight if already highlighted { var origIndex = _lastSelectedRow.Value; SelectRow(origIndex, false); // SelectRow assumed the max row should be selected, but in this edge case it isn't _lastSelectedRow = _selectedItems.FirstOrDefault()?.RowIndex; } else { SelectRow(_lastSelectedRow.Value + 1, true); } Refresh(); } } // Selection cursor 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()) // clones SelectedRows { SelectRow(row - 1, true); SelectRow(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()) // clones SelectedRows { SelectRow(row + 1, true); SelectRow(row, false); } } } else if (e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.Left) // Ctrl + Left { if (SelectedRows.Any() && LetKeysModifySelection) { SelectRow(SelectedRows.Last(), false); } } else if (e.Control && !e.Shift && !e.Alt && e.KeyCode == Keys.Right) // Ctrl + Right { if (SelectedRows.Any() && LetKeysModifySelection && SelectedRows.Last() < _rowCount - 1) { SelectRow(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) { SelectRow(SelectedRows.First() - 1, true); } } else if (e.Control && e.Shift && !e.Alt && e.KeyCode == Keys.Right) // Ctrl + Shift + Right { if (SelectedRows.Any() && LetKeysModifySelection) { SelectRow(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) { } } } base.OnKeyDown(e); } #endregion #region Change Events protected override void OnResize(EventArgs e) { RecalculateScrollBars(); base.OnResize(e); FastDraw(); } private void OrientationChanged() { // TODO scroll to correct positions ColumnChangedCallback(); 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(); } if (_horizontalOrientation) { ColumnScroll?.Invoke(_vBar, e); } else { RowScroll?.Invoke(_vBar, e); } } private void HorizontalBar_ValueChanged(object sender, EventArgs e) { if (!_programmaticallyUpdatingScrollBarValues) { Refresh(); } if (_horizontalOrientation) { RowScroll?.Invoke(_hBar, e); } else { ColumnScroll?.Invoke(_vBar, e); } } private void ColumnChangedCallback() { RecalculateScrollBars(); if (_columns.VisibleColumns.Any()) { MaxColumnWidth = _columns.VisibleColumns.Max(c => c.Width) + CellWidthPadding * 4; } } #endregion #region Helpers 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); } } // ScrollBar.Maximum = DesiredValue + ScrollBar.LargeChange - 1 // See MSDN Page for more information on the dumb ScrollBar.Maximum Property private void RecalculateScrollBars() { UpdateDrawSize(); var columns = _columns.VisibleColumns.ToList(); int iLastColumn = columns.Count - 1; if (HorizontalOrientation) { NeedsVScrollbar = GetHColBottom(iLastColumn) > _drawHeight; NeedsHScrollbar = RowCount > 1; } else { NeedsVScrollbar = ColumnHeight + (RowCount * CellHeight) > Height; NeedsHScrollbar = TotalColWidth - _drawWidth + 1 > 0; } UpdateDrawSize(); if (VisibleRows > 0) { if (HorizontalOrientation) { _hBar.Maximum = Math.Max((VisibleRows - 1) * CellWidth, _hBar.Maximum); _hBar.LargeChange = (VisibleRows - 1) * CellWidth; _vBar.LargeChange = Math.Max(0, _drawHeight / 2); } else { _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) { if (HorizontalOrientation) { _vBar.Maximum = GetHColBottom(iLastColumn) - _drawHeight + _vBar.LargeChange; if (_vBar.Maximum < 0) { _vBar.Maximum = 0; } } else { _vBar.Maximum = RowsToPixels(RowCount + 1) - (CellHeight * 3) + _vBar.LargeChange - 1; if (_vBar.Maximum < 0) { _vBar.Maximum = 0; } } _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) { if (HorizontalOrientation) { _hBar.Maximum = RowsToPixels(RowCount + 1) - (CellHeight * 3) + _hBar.LargeChange - 1; } else { _hBar.Maximum = TotalColWidth - _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() { _drawWidth = NeedsVScrollbar ? Width - _vBar.Width : Width; _drawHeight = NeedsHScrollbar ? Height - _hBar.Height : 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. /// Specifies whether or not to toggle the current state, rather than force the value to true private void SelectCell(Cell cell, bool toggle = false) { if (cell.RowIndex.HasValue && cell.RowIndex < RowCount) { if (!MultiSelect) { _selectedItems.Clear(); _lastSelectedRow = null; } if (FullRowSelect) { if (toggle && _selectedItems.Any(x => x.RowIndex.HasValue && x.RowIndex == cell.RowIndex)) { _selectedItems.RemoveWhere(x => x.RowIndex.HasValue && x.RowIndex == cell.RowIndex); _lastSelectedRow = _selectedItems.LastOrDefault()?.RowIndex; } else { foreach (var column in _columns) { _selectedItems.Add(new Cell { RowIndex = cell.RowIndex, Column = column }); _lastSelectedRow = cell.RowIndex; } } } else { _lastSelectedRow = null; // TODO: tracking this by cell is a lot more work 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 IsHoveringOnColumnCell => CurrentCell?.Column != null && !CurrentCell.RowIndex.HasValue; private bool IsHoveringOnColumnEdge => AllowColumnResize && IsHoveringOnColumnCell && IsPointingOnCellEdge(_currentX); private bool IsHoveringOnDataCell => CurrentCell?.Column != null && CurrentCell.RowIndex.HasValue; private bool WasHoveringOnColumnCell => _lastCell?.Column != null && !_lastCell.RowIndex.HasValue; private bool IsPointingOnCellEdge(int? x) { if (x.HasValue) { if (HorizontalOrientation) { return false; // TODO: support column resize in horizontal orientation } foreach (RollColumn column in _columns.VisibleColumns) { if (column.Left - _hBar.Value + (column.Width - column.Width / 6) <= x.Value && column.Right - _hBar.Value >= x.Value) { return true; } } } return false; } /// /// 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()) { if (HorizontalOrientation) { newCell.RowIndex = PixelsToRows(x); newCell.Column = ColumnAtPixel(y); } else { newCell.RowIndex = PixelsToRows(y); newCell.Column = ColumnAtPixel(x); } } if (!(IsPaintDown || RightButtonHeld) && newCell.RowIndex <= -1) // -2 if we're entering from the top { newCell.RowIndex = null; } return newCell; } // 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; } // Gets the total width of all the columns by using the last column's Right property. private int TotalColWidth => _columns.VisibleColumns.Any() ? _columns.VisibleColumns.Last().Right : 0; /// /// Returns the RollColumn object at the specified visible pixel coordinate. /// /// The pixel coordinate. /// RollColumn object that contains the pixel coordinate or null if none exists. private RollColumn ColumnAtPixel(int pixel) { if (_horizontalOrientation) { foreach (var item in _columns.VisibleColumns.Select((n, i) => new { Column = n, Index = i })) { if (GetHColTop(item.Index) - _vBar.Value <= pixel && GetHColBottom(item.Index) - _vBar.Value >= pixel) { return item.Column; } } } else { foreach (RollColumn column in _columns.VisibleColumns) { if (column.Left - _hBar.Value <= pixel && column.Right - _hBar.Value >= pixel) { 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) { if (_horizontalOrientation) { return (index * CellWidth) + MaxColumnWidth; } 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 data row, 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 (_horizontalOrientation) { return (int)Math.Floor((float)(pixels - MaxColumnWidth) / CellWidth); } return (int)Math.Floor((float)(pixels - ColumnHeight) / CellHeight); } private int GetHColHeight(int index) => _horizontalColumnHeights != null && index < _horizontalColumnHeights.Length ? _horizontalColumnHeights[index] : CellHeight; private int GetHColTop(int index) { if (_horizontalColumnTops == null) { return 0; } return index >= 0 && index < _horizontalColumnTops.Length ? _horizontalColumnTops[index] : _horizontalColumnTops.Last() + CellHeight; } private int GetHColBottom(int index) => GetHColTop(index) + GetHColHeight(index); // The width of the largest column cell in Horizontal Orientation private int MaxColumnWidth { get; set; } // The height of a column cell in Vertical Orientation. private int ColumnHeight => CellHeight + 2; // The width of a cell in Horizontal Orientation. Only can be changed by changing the Font or CellPadding. private int CellWidth { get; set; } /// /// 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; } = 8; /// /// Call when _charSize, MaxCharactersInHorizontal, or CellPadding is changed. /// private void UpdateCellSize() { using (var g = CreateGraphics()) using (_renderer.LockGraphics(g)) { // Measure width change to ignore extra padding at start/end var size1 = _renderer.MeasureString("A", Font); var size2 = _renderer.MeasureString("AA", Font); _charSize = new SizeF(size2.Width - size1.Width, size1.Height); // TODO make this a property so changing it updates other values. } // TODO: Should we round instead of truncate? CellHeight = (int)_charSize.Height + (CellHeightPadding * 2); CellWidth = (int)_charSize.Width + (CellWidthPadding * 4); // Double the padding for horizontal because it looks better if (_columns.VisibleColumns.Any()) { MaxColumnWidth = _columns.VisibleColumns.Max(c => c.Width) + CellWidthPadding * 4; } } protected override void OnFontChanged(EventArgs e) { UpdateCellSize(); } // 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() { int firstVisibleRow = FirstVisibleRow; int visibleRows = VisibleRows; 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() { int firstVisibleRow = FirstVisibleRow; 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; } } // Number of displayed + hidden frames, if fps is as expected private int ExpectedDisplayRange() { return (VisibleRows + 1) * LagFramesToHide; } #endregion } }