BizHawk/BizHawk.Client.EmuHawk/CustomControls/VirtualListView.cs

855 lines
21 KiB
C#

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace BizHawk.Client.EmuHawk
{
#region win32interop
[StructLayout(LayoutKind.Sequential)]
internal struct LvDispInfo
{
public NmHdr Hdr;
public LvItem Item;
}
[StructLayout(LayoutKind.Sequential)]
internal struct NmHdr
{
public IntPtr HwndFrom;
public IntPtr IdFrom;
public int Code;
}
[StructLayout(LayoutKind.Sequential)]
internal struct NmItemActivate
{
public NmHdr Hdr;
public int Item;
public int SubItem;
public uint NewState;
public uint OldState;
public uint uChanged;
public POINT Action;
public uint lParam;
public uint KeyFlags;
}
[StructLayout(LayoutKind.Sequential)]
internal struct RECT
{
public int Top;
public int Left;
public int Bottom;
public int Right;
}
[StructLayout(LayoutKind.Sequential)]
internal struct NmCustomDrawInfo
{
public NmHdr Hdr;
public uint dwDrawStage;
public IntPtr Hdc;
public RECT Rect;
public IntPtr dwItemSpec;
public uint ItemState;
private int _pad64bits;
public IntPtr lItemlParam;
}
[StructLayout(LayoutKind.Sequential)]
internal struct NmLvCustomDraw
{
public NmCustomDrawInfo Nmcd;
public int ClearText;
public int ClearTextBackground;
public int SubItem;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct LvItem
{
public uint Mask;
public int Item;
public int SubItem;
public uint State;
public uint StateMask;
public IntPtr PszText;
public int cchTextMax;
public int Image;
public IntPtr lParam;
public int Indent;
}
[FlagsAttribute]
internal enum CustomDrawReturnFlags
{
CDRF_DODEFAULT = 0x00000000,
CDRF_NEWFONT = 0x00000002,
CDRF_SKIPDEFAULT = 0x00000004,
CDRF_NOTIFYPOSTPAINT = 0x00000010,
CDRF_NOTIFYITEMDRAW = 0x00000020,
CDRF_NOTIFYSUBITEMDRAW = 0x00000020,
CDRF_NOTIFYPOSTERASE = 0x00000040,
}
[FlagsAttribute]
internal enum CustomDrawDrawStageFlags
{
CDDS_PREPAINT = 0x00000001,
CDDS_POSTPAINT = 0x00000002,
CDDS_PREERASE = 0x00000003,
CDDS_POSTERASE = 0x00000004,
// the 0x000010000 bit means it's individual item specific
CDDS_ITEM = 0x00010000,
CDDS_ITEMPREPAINT = (CDDS_ITEM | CDDS_PREPAINT),
CDDS_ITEMPOSTPAINT = (CDDS_ITEM | CDDS_POSTPAINT),
CDDS_ITEMPREERASE = (CDDS_ITEM | CDDS_PREERASE),
CDDS_ITEMPOSTERASE = (CDDS_ITEM | CDDS_POSTERASE),
CDDS_SUBITEM = 0x00020000,
CDDS_SUBITEMPREPAINT = (CDDS_SUBITEM | CDDS_ITEMPREPAINT),
CDDS_SUBITEMPOSTPAINT = (CDDS_SUBITEM | CDDS_ITEMPOSTPAINT),
CDDS_SUBITEMPREERASE = (CDDS_SUBITEM | CDDS_ITEMPREERASE),
CDDS_SUBITEMPOSTERASE = (CDDS_SUBITEM | CDDS_ITEMPOSTERASE),
}
[FlagsAttribute]
internal enum LvHitTestFlags
{
LVHT_NOWHERE = 0x0001,
LVHT_ONITEMICON = 0x0002,
LVHT_ONITEMLABEL = 0x0004,
LVHT_ONITEMSTATEICON = 0x0008,
LVHT_ONITEM = (LVHT_ONITEMICON | LVHT_ONITEMLABEL | LVHT_ONITEMSTATEICON),
LVHT_ABOVE = 0x0008,
LVHT_BELOW = 0x0010,
LVHT_TORIGHT = 0x0020,
LVHT_TOLEFT = 0x0040
}
[StructLayout(LayoutKind.Sequential)]
internal struct POINT
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential)]
internal class LvHitTestInfo
{
public POINT Point;
public uint Flags;
public int Item;
public int SubItem;
}
[StructLayout(LayoutKind.Sequential)]
internal struct NMLISTVIEW
{
public NmHdr hdr;
public int iItem;
public int iSubItem;
public uint uNewState;
public uint uOldState;
public uint uChanged;
public POINT ptAction;
public IntPtr lParam;
}
internal enum ListViewItemMask : short
{
LVIF_TEXT = 0x0001,
LVIF_IMAGE = 0x0002,
LVIF_PARAM = 0x0004,
LVIF_STATE = 0x0008,
LVIF_INDENT = 0x0010,
LVIF_NORECOMPUTE = 0x0800,
LVIF_GROUPID = 0x0100,
LVIF_COLUMNS = 0x0200
}
internal enum LvNi
{
ALL = 0x0000,
FOCUSED = 0x0001,
SELECTED = 0x0002,
CUT = 0x0004,
DROPHILITED = 0x0008,
ABOVE = 0x0100,
BELOW = 0x0200,
TOLEFT = 0x0400,
TORIGHT = 0x0800
}
internal enum ListViewMessages
{
LVM_FIRST = 0x1000,
LVM_GETITEMCOUNT = (LVM_FIRST + 4),
LVM_SETCALLBACKMASK = (LVM_FIRST + 11),
LVM_GETNEXTITEM = (LVM_FIRST + 12),
LVM_HITTEST = (LVM_FIRST + 18),
LVM_ENSUREVISIBLE = (LVM_FIRST + 19),
LVM_SETITEMSTATE = (LVM_FIRST + 43),
LVM_GETITEMSTATE = (LVM_FIRST + 44),
LVM_SETITEMCOUNT = (LVM_FIRST + 47),
LVM_GETSUBITEMRECT = (LVM_FIRST + 56)
}
internal enum ListViewStyles : short
{
LVS_OWNERDATA = 0x1000,
LVS_SORTASCENDING = 0x0010,
LVS_SORTDESCENDING = 0x0020,
LVS_SHAREIMAGELISTS = 0x0040,
LVS_NOLABELWRAP = 0x0080,
LVS_AUTOARRANGE = 0x0100
}
internal enum ListViewStylesICF : uint
{
LVSICF_NOINVALIDATEALL = 0x00000001,
LVSICF_NOSCROLL = 0x00000002
}
internal enum WindowsMessage : uint
{
WM_ERASEBKGND = 0x0014,
WM_SCROLL = 0x115,
WM_LBUTTONDOWN = 0x0201,
WM_LBUTTONUP = 0x0202,
WM_LBUTTONDBLCLK = 0x0203,
WM_RBUTTONDOWN = 0x0204,
WM_RBUTTONUP = 0x0205,
WM_RBUTTONDBLCLK = 0x0206,
WM_SETFOCUS = 0x0007,
WM_NOTIFY = 0x004E,
WM_USER = 0x0400,
WM_REFLECT = WM_USER + 0x1c00
}
internal enum Notices
{
NM_FIRST = 0,
NM_CUSTOMDRAW = NM_FIRST - 12,
NM_CLICK = NM_FIRST - 2,
NM_DBLCLICK = NM_FIRST - 3,
}
internal enum ListViewNotices
{
LVN_FIRST = (0 - 100),
LVN_LAST = (0 - 199),
LVN_BEGINDRAG = LVN_FIRST - 9,
LVN_BEGINRDRAG = LVN_FIRST - 11,
LVN_GETDISPINFOA = LVN_FIRST - 50,
LVN_GETDISPINFOW = LVN_FIRST - 77,
LVN_SETDISPINFOA = LVN_FIRST - 51,
LVN_SETDISPINFOW = LVN_FIRST - 78,
LVN_ODCACHEHINT = LVN_FIRST - 13,
LVN_ODFINDITEMW = LVN_FIRST - 79
}
[Flags]
internal enum ListViewCallBackMask : uint
{
LVIS_FOCUSED = 0x0001,
LVIS_SELECTED = 0x0002,
LVIS_CUT = 0x0004,
LVIS_DROPHILITED = 0x0008,
LVIS_GLOW = 0x0010,
LVIS_ACTIVATING = 0x0020,
LVIS_OVERLAYMASK = 0x0F00,
LVIS_STATEIMAGEMASK = 0xF000,
}
#endregion
#region VirtualListView Delegates
/// <summary>
/// Retrieve the background color for a Listview cell (item and subitem).
/// </summary>
/// <param name="item">Listview item (row).</param>
/// <param name="subItem">Listview subitem (column).</param>
/// <param name="color">Background color to use</param>
public delegate void QueryItemBkColorHandler(int item, int subItem, ref Color color);
/// <summary>
/// Retrieve the text for a Listview cell (item and subitem).
/// </summary>
/// <param name="item">Listview item (row).</param>
/// <param name="subItem">Listview subitem (column).</param>
/// <param name="text">Text to display.</param>
public delegate void QueryItemTextHandler(int item, int subItem, out string text);
/// <summary>
/// Retrieve the image index for a Listview item.
/// </summary>
/// <param name="item">Listview item (row).</param>
/// <param name="subItem">Listview subitem (column) - should always be zero.</param>
/// <param name="imageIndex">Index of associated ImageList.</param>
public delegate void QueryItemImageHandler(int item, int subItem, out int imageIndex);
/// <summary>
/// Retrieve the indent for a Listview item. The indent is always width of an image.
/// For example, 1 indents the Listview item one image width.
/// </summary>
/// <param name="item">Listview item (row).</param>
/// <param name="itemIndent">The amount to indent the Listview item.</param>
public delegate void QueryItemIndentHandler(int item, out int itemIndent);
public delegate void QueryItemHandler(int idx, out ListViewItem item);
#endregion
/// <summary>
/// VirtualListView is a virtual Listview which allows for a large number of items(rows)
/// to be displayed. The virtual Listview contains very little actual information -
/// mainly item selection and focus information.
/// </summary>
public class VirtualListView : ListView
{
// store the item count to prevent the call to SendMessage(LVM_GETITEMCOUNT)
private int _itemCount;
#region Display query callbacks
/// <summary>
/// Fire the QueryItemBkColor event which requests the background color for the passed Listview cell
/// </summary>
public event QueryItemBkColorHandler QueryItemBkColor;
/// <summary>
/// Fire the QueryItemText event which requests the text for the passed Listview cell.
/// </summary>
[Category("Data")]
public event QueryItemTextHandler QueryItemText;
/// <summary>
/// Fire the QueryItemImage event which requests the ImageIndex for the passed Listview item.
/// </summary>
[Category("Data")]
public event QueryItemImageHandler QueryItemImage;
/// <summary>
/// Fire the QueryItemIndent event which requests the indent for the passed Listview item.
/// </summary>
[Category("Data")]
public event QueryItemIndentHandler QueryItemIndent;
[Category("Data")]
public event QueryItemHandler QueryItem;
#endregion
#region Properties
/// <summary>
/// Gets or sets the sets the virtual number of items to be displayed.
/// </summary>
[Category("Behavior")]
public int ItemCount
{
get
{
return _itemCount;
}
set
{
_itemCount = value;
// If the virtual item count is set before the handle is created
// then the image lists don't get loaded properly
if (!IsHandleCreated)
{
return;
}
SetVirtualItemCount();
}
}
/// <summary>
/// Gets or sets how list items are to be displayed.
/// Hide the ListView.View property.
/// Virtual Listviews only allow Details or List.
/// </summary>
public new View View
{
get
{
return base.View;
}
set
{
if (value == View.LargeIcon ||
value == View.SmallIcon)
{
throw new ArgumentException("Icon views are invalid for virtual ListViews", "View");
}
base.View = value;
}
}
/// <summary>
/// Gets the required creation parameters when the control handle is created.
/// Use LVS_OWNERDATA to set this as a virtual Listview.
/// </summary>
protected override CreateParams CreateParams
{
get
{
var cp = base.CreateParams;
// LVS_OWNERDATA style must be set when the control is created
cp.Style |= (int)ListViewStyles.LVS_OWNERDATA;
return cp;
}
}
#endregion
[Browsable(false)]
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)]
public int LineHeight { get; private set; }
[Browsable(false)]
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)]
public int NumberOfVisibleRows
{
get
{
return Height / LineHeight;
}
}
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="VirtualListView"/> class.
/// Create a new instance of this control.
/// </summary>
public VirtualListView()
{
// virtual listviews must be Details or List view with no sorting
View = View.Details;
Sorting = SortOrder.None;
UseCustomBackground = true;
ptrlvhti = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(LvHitTestInfo)));
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.Opaque, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
LineHeight = this.Font.Height + 5;
}
~VirtualListView()
{
Marshal.FreeHGlobal(ptrlvhti);
}
#endregion
#region Methods
/// <summary>
/// Set the state of the passed Listview item's index.
/// </summary>
/// <param name="index">Listview item's index.</param>
/// <param name="selected">Select the passed item?</param>
public void SelectItem(int index, bool selected)
{
var ptrItem = IntPtr.Zero;
try
{
// Determine whether selecting or unselecting.
uint select = selected ? (uint)ListViewCallBackMask.LVIS_SELECTED : 0;
// Fill in the LVITEM structure with state fields.
var stateItem = new LvItem
{
Mask = (uint)ListViewItemMask.LVIF_STATE,
Item = index,
SubItem = 0,
State = @select,
StateMask = (uint)ListViewCallBackMask.LVIS_SELECTED
};
// Copy the structure to unmanaged memory.
ptrItem = Marshal.AllocHGlobal(Marshal.SizeOf(stateItem.GetType()));
Marshal.StructureToPtr(stateItem, ptrItem, true);
// Send the message to the control window.
Win32.SendMessage(
this.Handle,
(int)ListViewMessages.LVM_SETITEMSTATE,
(IntPtr)index,
ptrItem);
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine("VirtualListView.SetItemState error=" + ex.Message);
// TODO: should this eat any exceptions?
throw;
}
finally
{
// Always release the unmanaged memory.
if (ptrItem != IntPtr.Zero)
{
Marshal.FreeHGlobal(ptrItem);
}
}
}
private void SetVirtualItemCount()
{
Win32.SendMessage(
this.Handle,
(int)ListViewMessages.LVM_SETITEMCOUNT,
(IntPtr)this._itemCount,
IntPtr.Zero);
}
protected void OnDispInfoNotice(ref Message m, bool useAnsi)
{
var info = (LvDispInfo)m.GetLParam(typeof(LvDispInfo));
if ((info.Item.Mask & (uint)ListViewItemMask.LVIF_TEXT) > 0)
{
if (QueryItemText != null)
{
string lvtext;
QueryItemText(info.Item.Item, info.Item.SubItem, out lvtext);
if (lvtext != null)
{
try
{
int maxIndex = Math.Min(info.Item.cchTextMax - 1, lvtext.Length);
var data = new char[maxIndex + 1];
lvtext.CopyTo(0, data, 0, lvtext.Length);
data[maxIndex] = '\0';
Marshal.Copy(data, 0, info.Item.PszText, data.Length);
}
catch (Exception e)
{
Debug.WriteLine("Failed to copy text name from client: " + e, "VirtualListView.OnDispInfoNotice");
}
}
}
}
if ((info.Item.Mask & (uint)ListViewItemMask.LVIF_IMAGE) > 0)
{
int imageIndex = 0;
if (QueryItemImage != null)
{
QueryItemImage(info.Item.Item, info.Item.SubItem, out imageIndex);
}
info.Item.Image = imageIndex;
Marshal.StructureToPtr(info, m.LParam, false);
}
if ((info.Item.Mask & (uint)ListViewItemMask.LVIF_INDENT) > 0)
{
int itemIndent = 0;
if (QueryItemIndent != null)
{
QueryItemIndent(info.Item.Item, out itemIndent);
}
info.Item.Indent = itemIndent;
Marshal.StructureToPtr(info, m.LParam, false);
}
m.Result = new IntPtr(0);
}
protected void OnCustomDrawNotice(ref Message m)
{
var cd = (NmLvCustomDraw)m.GetLParam(typeof(NmLvCustomDraw));
switch (cd.Nmcd.dwDrawStage)
{
case (int)CustomDrawDrawStageFlags.CDDS_ITEMPREPAINT:
case (int)CustomDrawDrawStageFlags.CDDS_PREPAINT:
m.Result = new IntPtr((int)CustomDrawReturnFlags.CDRF_NOTIFYSUBITEMDRAW);
break;
case (int)CustomDrawDrawStageFlags.CDDS_SUBITEMPREPAINT:
if (QueryItemBkColor != null)
{
var color = Color.FromArgb(cd.ClearTextBackground & 0xFF, (cd.ClearTextBackground >> 8) & 0xFF, (cd.ClearTextBackground >> 16) & 0xFF);
QueryItemBkColor(cd.Nmcd.dwItemSpec.ToInt32(), cd.SubItem, ref color);
cd.ClearTextBackground = (color.B << 16) | (color.G << 8) | color.R;
Marshal.StructureToPtr(cd, m.LParam, false);
}
m.Result = new IntPtr((int)CustomDrawReturnFlags.CDRF_DODEFAULT);
break;
}
}
/// <summary>
/// Event to be fired whenever the control scrolls
/// </summary>
public event ScrollEventHandler Scroll;
protected virtual void OnScroll(ScrollEventArgs e)
{
var handler = this.Scroll;
if (handler != null)
{
handler(this, e);
}
}
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int GetScrollPos(IntPtr hWnd, Orientation nBar);
/// <summary>
/// Gets the Vertical Scroll position of the control.
/// </summary>
public int VScrollPos
{
get { return GetScrollPos(this.Handle, Orientation.Vertical); }
}
protected override void WndProc(ref Message m)
{
var messageProcessed = false;
switch (m.Msg)
{
case (int)WindowsMessage.WM_REFLECT + (int)WindowsMessage.WM_NOTIFY:
var nm1 = (NmHdr)m.GetLParam(typeof(NmHdr));
switch (nm1.Code)
{
case (int)Notices.NM_CUSTOMDRAW:
OnCustomDrawNotice(ref m);
messageProcessed = true;
if (QueryItemBkColor == null || !UseCustomBackground)
{
m.Result = (IntPtr)0;
}
break;
case (int)ListViewNotices.LVN_GETDISPINFOW:
OnDispInfoNotice(ref m, false);
messageProcessed = true;
break;
case (int)ListViewNotices.LVN_BEGINDRAG:
OnBeginItemDrag(MouseButtons.Left, ref m);
messageProcessed = true;
break;
case (int)ListViewNotices.LVN_BEGINRDRAG:
OnBeginItemDrag(MouseButtons.Right, ref m);
messageProcessed = true;
break;
}
break;
case (int)WindowsMessage.WM_SCROLL:
// http://stackoverflow.com/questions/1851620/handling-scroll-event-on-listview-in-c-sharp
OnScroll(new ScrollEventArgs((ScrollEventType)(m.WParam.ToInt32() & 0xffff), m.WParam.ToInt32()));
break;
case (int)WindowsMessage.WM_ERASEBKGND:
if (BlazingFast)
{
messageProcessed = true;
m.Result = new IntPtr(1);
}
break;
}
if (!messageProcessed)
{
try
{
base.WndProc(ref m);
}
catch (Exception ex)
{
Trace.WriteLine(string.Format("Message {0} caused an exception: {1}", m, ex.Message));
}
}
}
public bool BlazingFast { get; set; }
public bool UseCustomBackground { get; set; }
protected ListViewItem GetItem(int idx)
{
ListViewItem item = null;
if (QueryItem != null)
{
QueryItem(idx, out item);
}
if (item == null)
{
throw new ArgumentException("cannot find item " + idx + " via QueryItem event");
}
return item;
}
protected void OnBeginItemDrag(MouseButtons mouseButton, ref Message m)
{
var info = (NMLISTVIEW)m.GetLParam(typeof(NMLISTVIEW));
ListViewItem item = null;
if (QueryItem != null)
{
QueryItem(info.iItem, out item);
}
OnItemDrag(new ItemDragEventArgs(mouseButton, item));
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
// ensure the value for ItemCount is sent to the control properly if the user set it
// before the handle was created
SetVirtualItemCount();
}
protected override void OnHandleDestroyed(EventArgs e)
{
// the ListView OnHandleDestroyed accesses the Items list for all selected items
ItemCount = 0;
base.OnHandleDestroyed(e);
}
#endregion
LvHitTestInfo lvhti = new LvHitTestInfo();
IntPtr ptrlvhti;
int selection = -1;
public int hitTest(int x, int y)
{
lvhti.Point.X = x;
lvhti.Point.Y = y;
Marshal.StructureToPtr(lvhti, ptrlvhti, true);
int z = (int)Win32.SendMessage(this.Handle, (int)ListViewMessages.LVM_HITTEST, (IntPtr)0, ptrlvhti);
Marshal.PtrToStructure(ptrlvhti, lvhti);
return z;
}
public void ensureVisible(int index)
{
Win32.SendMessage(Handle, (int)ListViewMessages.LVM_ENSUREVISIBLE, (IntPtr)index, (IntPtr)1);
}
public void ensureVisible()
{
ensureVisible(selectedItem);
}
public void setSelection(int index)
{
clearSelection();
selection = index;
SelectItem(selection, true);
}
public int selectedItem
{
get
{
if (SelectedIndices.Count == 0)
{
return -1;
}
else
{
return SelectedIndices[0];
}
}
set
{
setSelection(value);
}
}
public void clearSelection()
{
if (selection != -1)
{
SelectItem(selection, false);
}
selection = -1;
}
// Informs user that a select all event is in place, can be used in change events to wait until this is false
public bool SelectAllInProgress { get; set; }
public void SelectAll()
{
this.BeginUpdate();
SelectAllInProgress = true;
for (var i = 0; i < _itemCount; i++)
{
if (i == _itemCount - 1)
{
SelectAllInProgress = false;
}
this.SelectItem(i, true);
}
this.EndUpdate();
}
public void DeselectAll()
{
this.BeginUpdate();
SelectAllInProgress = true;
for (var i = 0; i < _itemCount; i++)
{
if (i == _itemCount - 1)
{
SelectAllInProgress = false;
}
this.SelectItem(i, false);
}
this.EndUpdate();
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.KeyCode == Keys.A && e.Control && !e.Alt && !e.Shift) // Select All
{
SelectAll();
}
base.OnKeyDown(e);
}
}
}