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 GDIRenderer Gdi; private readonly SortedSet SelectedItems = new SortedSet(new sortCell()); private readonly VScrollBar VBar; private readonly HScrollBar HBar; private RollColumns _columns = new RollColumns(); private bool _horizontalOrientation; private bool _programmaticallyUpdatingScrollBarValues; private int _maxCharactersInHorizontal = 1; private int _rowCount; private Size _charSize; private RollColumn _columnDown; private int? _currentX; private int? _currentY; // Hiding lag frames (Mainly intended for < 60fps play.) public int LagFramesToHide { get; set; } public bool HideWasLagFrames { get; set; } private byte[] lagFrames = new byte[100]; // Large enough value that it shouldn't ever need resizing. public bool denoteStatesWithIcons { get; set; } public bool denoteStatesWithBGColor { get; set; } public bool denoteMarkersWithIcons { get; set; } public bool denoteMarkersWithBGColor { get; set; } private IntPtr RotatedFont; private readonly Font NormalFont; private Color _foreColor; private Color _backColor; public InputRoll() { UseCustomBackground = true; GridLines = true; CellWidthPadding = 3; CellHeightPadding = 0; CurrentCell = null; ScrollMethod = "near"; NormalFont = new Font("Courier New", 8); // Only support fixed width // PrepDrawString doesn't actually set the font, so this is rather useless. // I'm leaving this stuff as-is so it will be a bit easier to fix up with another rendering method. RotatedFont = GDIRenderer.CreateRotatedHFont(Font, true); SetStyle(ControlStyles.AllPaintingInWmPaint, true); SetStyle(ControlStyles.UserPaint, true); SetStyle(ControlStyles.SupportsTransparentBackColor, true); SetStyle(ControlStyles.Opaque, true); Gdi = new GDIRenderer(); using (var g = CreateGraphics()) using (var LCK = Gdi.LockGraphics(g)) { _charSize = Gdi.MeasureString("A", NormalFont); // TODO make this a property so changing it updates other values. } UpdateCellSize(); ColumnWidth = CellWidth; ColumnHeight = CellHeight + 2; 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; HorizontalOrientation = false; 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(); if (CellHovered != null) { CellHovered(this, new CellEventArgs(LastCell, CurrentCell)); } } protected override void Dispose(bool disposing) { Gdi.Dispose(); NormalFont.Dispose(); GDIRenderer.DestroyHFont(RotatedFont); base.Dispose(disposing); } private Timer _hoverTimer = new Timer(); #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; } /// /// Displays grid lines around cells /// [Category("Appearance")] [DefaultValue(true)] public bool GridLines { get; set; } /// /// Gets or sets 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; } } } /// /// Gets or sets the scrolling speed /// [Category("Behavior")] public int ScrollSpeed { get { if (HorizontalOrientation) return HBar.SmallChange / CellWidth; else 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 { return _rowCount; } set { _rowCount = value; RecalculateScrollBars(); } } /// /// Gets or sets the sets the columns can be resized /// [Category("Behavior")] public bool AllowColumnResize { get; set; } /// /// Gets or sets the sets the columns can be reordered /// [Category("Behavior")] public bool AllowColumnReorder { get; set; } /// /// Indicates whether the entire row will always be selected /// [Category("Appearance")] [DefaultValue(false)] public bool FullRowSelect { get; set; } /// /// Allows multiple items to be selected /// [Category("Behavior")] [DefaultValue(true)] public bool MultiSelect { get; set; } /// /// Gets or sets whether or not the control is in input painting mode /// [Category("Behavior")] [DefaultValue(false)] public bool InputPaintingMode { get; set; } /// /// All visible columns /// [Category("Behavior")] public IEnumerable VisibleColumns { get { return _columns.VisibleColumns; } } /// /// Gets or sets how the InputRoll scrolls when calling ScrollToIndex. /// [DefaultValue("near")] [Category("Behavior")] public string ScrollMethod { get; set; } /// /// Gets or sets how the Intever for the hover event /// [Category("Behavior")] public bool AlwaysScroll { get; set; } /// /// Returns all columns including those that are not visible /// /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public RollColumns AllColumns { get { return _columns; } } [DefaultValue(750)] [Category("Behavior")] public int HoverInterval { get { return _hoverTimer.Interval; } set { _hoverTimer.Interval = value; } } #endregion #region Event Handlers /// /// Fire the QueryItemText event which requests the text for the passed cell /// [Category("Virtual")] public event QueryItemTextHandler QueryItemText; /// /// Fire the QueryItemBkColor event which requests the background color for the passed cell /// [Category("Virtual")] public event QueryItemBkColorHandler QueryItemBkColor; [Category("Virtual")] public event QueryRowBkColorHandler QueryRowBkColor; /// /// Fire the QueryItemIconHandler event which requests an icon for a given cell /// [Category("Virtual")] public event QueryItemIconHandler QueryItemIcon; /// /// SuuperW: 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); /// /// Retrive the image for a given cell /// public delegate void QueryItemIconHandler(int index, RollColumn column, ref Bitmap icon, ref int offsetX, ref int offsetY); /// /// SuuperW: 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; private set; } public Cell NewCell { get; private set; } } public class ColumnClickEventArgs { public ColumnClickEventArgs(RollColumn column) { Column = column; } public RollColumn Column { get; private set; } } public class ColumnReorderedEventArgs { public ColumnReorderedEventArgs(int oldDisplayIndex, int newDisplayIndex, RollColumn column) { Column = column; OldDisplayIndex = oldDisplayIndex; NewDisplayIndex = NewDisplayIndex; } public RollColumn Column { get; private set; } public int OldDisplayIndex { get; private set; } public int NewDisplayIndex { get; private set; } } #endregion #region Api public void SelectRow(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 < RowCount; i++) { SelectRow(i, true); } FullRowSelect = oldFullRowVal; } public void DeselectAll() { SelectedItems.Clear(); } public void TruncateSelection(int index) { SelectedItems.RemoveWhere(cell => cell.RowIndex > index); } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool IsPointingAtColumnHeader { get { return IsHoveringOnColumnCell; } } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int? FirstSelectedIndex { get { if (AnyRowsSelected) { return SelectedRows.Min(); } return null; } } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int? LastSelectedIndex { get { if (AnyRowsSelected) { return SelectedRows.Max(); } return null; } } /// /// 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 CurrentCellIsDataCell { get { return CurrentCell != null && CurrentCell.RowIndex.HasValue && CurrentCell.Column != null; } } /// /// The previous Cell that the mouse was in. /// [Browsable(false)] [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)] public Cell LastCell { get; set; } [Browsable(false)] [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)] public bool IsPaintDown { get; set; } [Browsable(false)] [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)] public bool UseCustomBackground { get; set; } [Browsable(false)] [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)] public int DrawHeight { get; private set; } [Browsable(false)] [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)] public int DrawWidth { get; private set; } /// /// Sets the width of data cells when in Horizontal orientation. /// public int MaxCharactersInHorizontal { get { return _maxCharactersInHorizontal; } set { _maxCharactersInHorizontal = value; UpdateCellSize(); } } [Browsable(false)] [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)] public bool RightButtonHeld { get; 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) { var rollSettings = settings as InputRollSettings; _columns = rollSettings.Columns; HorizontalOrientation = rollSettings.HorizontalOrientation; LagFramesToHide = rollSettings.LagFramesToHide; HideWasLagFrames = rollSettings.HideWasLagFrames; } } private InputRollSettings Settings { get { return 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 // SuuperW: This was checking if the scroll bars were needed, which is useless because their Value is 0 if they aren't needed. { 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; } } } } [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); } } [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; 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); } } } public 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 - ColumnWidth) / CellWidth; } 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 (HorizontalOrientation) { return VBar.Value / CellHeight; } else { List columnList = VisibleColumns.ToList(); return columnList.FindIndex(c => c.Right > HBar.Value); } } } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int LastVisibleColumnIndex { get { List columnList = VisibleColumns.ToList(); int ret; if (HorizontalOrientation) { ret = (VBar.Value + DrawHeight) / CellHeight; if (ret >= columnList.Count) ret = columnList.Count - 1; } else ret = columnList.FindLastIndex(c => c.Left <= DrawWidth + HBar.Value); return ret; } } private Cell DraggingCell = null; public void DragCurrentCell() { DraggingCell = CurrentCell; } public void ReleaseCurrentCell() { if (DraggingCell != null) { var draggedCell = DraggingCell; DraggingCell = null; if (CurrentCell != draggedCell) { if (CellDropped != null) { CellDropped(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); } } } } /// /// 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; } } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IEnumerable SelectedRows { get { return SelectedItems .Where(cell => cell.RowIndex.HasValue) .Select(cell => cell.RowIndex.Value) .Distinct(); } } public bool AnyRowsSelected { get { return SelectedItems.Any(cell => cell.RowIndex.HasValue); } } public void ClearSelectedRows() { SelectedItems.Clear(); } 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; } public string RotateHotkeyStr { get { return "Ctrl+Shift+F"; } } #endregion #region Mouse and Key Events private bool _columnDownMoved = false; protected override void OnMouseMove(MouseEventArgs e) { _currentX = e.X; _currentY = e.Y; 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(); } 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(); 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 (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) { // MessageBox.Show("Alt click logic is not yet implemented"); // do marker drag here } else if (ModifierKeys == Keys.Shift) { 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) { SelectCell(CurrentCell, toggle: true); } else { var hadIndex = SelectedItems.Any(); SelectedItems.Clear(); SelectCell(CurrentCell); // In this case the SelectCell did not invoke the change event since there was nothing to select // But we went from selected to unselected, that is a change, so catch it here if (hadIndex && CurrentCell.RowIndex.HasValue && CurrentCell.RowIndex > RowCount) { if (SelectedIndexChanged != null) { SelectedIndexChanged(this, new EventArgs()); } } } Refresh(); } } base.OnMouseDown(e); } protected override void OnMouseUp(MouseEventArgs e) { if (IsHoveringOnColumnCell) { 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; 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(System.Windows.Forms.MouseButtons.None, 0, _currentX.Value, _currentY.Value, 0)); Refresh(); } } private void DoRightMouseScroll(object sender, MouseEventArgs e) { if (RightMouseScrolled != null) { RightMouseScrolled(sender, e); } } private void ColumnClickEvent(RollColumn column) { if (ColumnClick != null) { ColumnClick(this, new ColumnClickEventArgs(column)); } } private void ColumnRightClickEvent(RollColumn column) { if (ColumnRightClick != null) { ColumnRightClick(this, new ColumnClickEventArgs(column)); } } protected override void OnKeyDown(KeyEventArgs e) { if (e.Control && !e.Alt && e.Shift && e.KeyCode == Keys.F) // Ctrl+Shift+F { HorizontalOrientation ^= true; } 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 <= RowCount) { var final = LastVisibleRow + totalRows; if (final > RowCount) { final = RowCount; } 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 = RowCount; Refresh(); } base.OnKeyDown(e); } #endregion #region Change Events protected override void OnResize(EventArgs e) { RecalculateScrollBars(); base.OnResize(e); Refresh(); } private void OrientationChanged() { RecalculateScrollBars(); // TODO scroll to correct positions ColumnChangedCallback(); RecalculateScrollBars(); 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 != null && CurrentCell.Column != null && CurrentCell.RowIndex.HasValue) { _hoverTimer.Start(); } else { _hoverTimer.Stop(); } } private void VerticalBar_ValueChanged(object sender, EventArgs e) { if (!_programmaticallyUpdatingScrollBarValues) { Refresh(); } if (_horizontalOrientation) { if (ColumnScroll != null) { ColumnScroll(this, e); } } else { if (RowScroll != null) { RowScroll(this, e); } } } private void HorizontalBar_ValueChanged(object sender, EventArgs e) { if (!_programmaticallyUpdatingScrollBarValues) { Refresh(); } if (_horizontalOrientation) { if (RowScroll != null) { RowScroll(this, e); } } else { if (ColumnScroll != null) { ColumnScroll(this, e); } } } private void ColumnChangedCallback() { RecalculateScrollBars(); if (_columns.VisibleColumns.Any()) { ColumnWidth = _columns.VisibleColumns.Max(c => c.Width.Value) + CellWidthPadding * 4; } } #endregion #region Helpers // 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); if (ColumnReordered != null) { ColumnReordered(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(); if (HorizontalOrientation) { NeedsVScrollbar = columns.Count > DrawHeight / CellHeight; NeedsHScrollbar = RowCount > 1; } else { NeedsVScrollbar = RowCount > 1; NeedsHScrollbar = TotalColWidth.HasValue && TotalColWidth.Value - DrawWidth + 1 > 0; } UpdateDrawSize(); if (VisibleRows > 0) { if (HorizontalOrientation) { VBar.LargeChange = DrawHeight / 2; HBar.Maximum = Math.Max((VisibleRows - 1) * CellHeight, HBar.Maximum); HBar.LargeChange = (VisibleRows - 1) * CellHeight; } else { VBar.Maximum = Math.Max((VisibleRows - 1) * CellHeight, VBar.Maximum); // ScrollBar.Maximum is dumb VBar.LargeChange = (VisibleRows - 1) * CellHeight; HBar.LargeChange = DrawWidth / 2; } } //Update VBar if (NeedsVScrollbar) { if (HorizontalOrientation) { VBar.Maximum = ((columns.Count() * CellHeight) - DrawHeight) + VBar.LargeChange; } else { VBar.Maximum = RowsToPixels(RowCount + 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) { if (HorizontalOrientation) { HBar.Maximum = RowsToPixels(RowCount + 1) - (CellHeight * 3) + HBar.LargeChange - 1; } else { 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 < RowCount) { 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); } } if (SelectedIndexChanged != null) { SelectedIndexChanged(this, new EventArgs()); } } } /// /// Bool that indicates if CurrentCell is a Column Cell. /// private bool IsHoveringOnColumnCell { get { return CurrentCell != null && CurrentCell.Column != null && !CurrentCell.RowIndex.HasValue; } } /// /// Bool that indicates if CurrentCell is a Data Cell. /// private bool IsHoveringOnDataCell { get { return CurrentCell != null && CurrentCell.Column != null && CurrentCell.RowIndex.HasValue; } } /// /// Bool that indicates if CurrentCell is a Column Cell. /// private bool WasHoveringOnColumnCell { get { return LastCell != null && LastCell.Column != null && !LastCell.RowIndex.HasValue; } } /// /// Bool that indicates if CurrentCell is a Data Cell. /// private bool WasHoveringOnDataCell { get { return LastCell != null && 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()) { if (HorizontalOrientation) { newCell.RowIndex = PixelsToRows(x); int colIndex = (y + VBar.Value) / CellHeight; if (colIndex >= 0 && colIndex < columns.Count) { newCell.Column = columns[colIndex]; } } else { newCell.RowIndex = PixelsToRows(y); newCell.Column = ColumnAtX(x); } } if (!(IsPaintDown || RightButtonHeld) && newCell.RowIndex == -1) 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; } /// /// 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(RollColumn 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 RollColumn ColumnAtX(int x) { foreach (RollColumn 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) { if (_horizontalOrientation) { return (index * CellWidth) + ColumnWidth; } 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 (_horizontalOrientation) { return (int)Math.Floor((float)(pixels - ColumnWidth) / CellWidth); } 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; } //Cell defaults /// /// 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 { get { return CellHeight; } } /// /// 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 } // 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; } // Number of displayed + hidden frames, if fps is as expected private int ExpectedDisplayRange() { return (VisibleRows + 1) * LagFramesToHide; } #endregion #region Classes public class RollColumns : List { public RollColumn 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() { if (ChangedCallback != null) { ChangedCallback(); } } // TODO: this shouldn't be exposed. But in order to not expose it, each RollColumn must have a chane 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(RollColumn 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); 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, RollColumn 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(RollColumn 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(); } } } public class RollColumn { public enum InputType { Boolean, Float, Text, Image } 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 RollColumn() { Visible = true; } } /// /// /// public class Cell { public RollColumn 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 { get { return 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); } else return row; } else return 1; } else if (c2.RowIndex.HasValue) return -1; else return c1.Column.Name.CompareTo(c2.Column.Name); } } #endregion } }