/* Copyright (c) Citrix Systems Inc. * All rights reserved. * * Redistribution and use in source and binary forms, * with or without modification, are permitted provided * that the following conditions are met: * * * Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other * materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Data; using System.Text; using System.Windows.Forms; using System.Runtime.InteropServices; using System.Drawing.Text; using XenAdmin.Core; using XenAdmin.XenSearch; namespace XenAdmin.Controls.CustomGridView { public enum MouseButtonAction { SingleClick, DoubleClick, MouseDown, StartDrag, SingleRightClick } public partial class GridView : Panel { public List Rows = new List(); // If currently painting the refresh call will set the PaintingRequired flag which will cause the panel to repaint after public bool Painting = false; public bool PaintingRequired = false; // This draws to the control private Graphics Graphics; // This draws to the BackBuffer private Graphics BufferGraphics; // Stores the image while being drawn private Bitmap BackBuffer; /// /// The width of the column containing the expander triangle, if required. /// private bool hasLeftExpanders = true; public bool HasLeftExpanders { get { return hasLeftExpanders; } set { hasLeftExpanders = value; } } private const int LEFT_OFFSET = 20; public int LeftOffset { get { return HasLeftExpanders ? LEFT_OFFSET : 0; } } // Row selection protected string lastClickedRowPath = null; public GridRow LastClickedRow { set { lastClickedRowPath = (value == null ? null : value.Path); } } Point lastClickedPoint = new Point(0, 0); // Iterate over all rows including child rows, in display order public IEnumerable RowsAndChildren { get { foreach (GridRow row in Rows) { yield return row; foreach (GridRow child in row.RowsAndChildren) yield return child; } } } public GridView() { InitBuffer(); InitializeComponent(); } // The top row in the list, NEVER set this! <- no need to dispose explicitly as will be disposed when the contents of Rows is disposed public GridHeaderRow HeaderRow; private bool hold_cursor_change = false; private Cursor new_cursor = null; public override Cursor Cursor { set { if (hold_cursor_change) new_cursor = value; else base.Cursor = value; } } /// /// Set the size of the back buffer to the size of the control /// private void InitBuffer() { Graphics = this.CreateGraphics(); Rectangle disp = RealRectangle; BackBuffer = new Bitmap(disp.Width > 0 ? disp.Width : 1, disp.Height > 0 ? disp.Height : 1); BufferGraphics = Graphics.FromImage(BackBuffer); BufferGraphics.TextRenderingHint = Graphics.TextRenderingHint; } /// /// Update all values in the grid /// public override void Refresh() { //if (Painting) //{ // we are currently painting, set a flag and return // the painting will occur when the current paint has finished // the idea is that if we get loads of calls to this then we only repaint at most once after each paint // this is why i dont think we should have a lock as they would wait then paint all afterwards // PaintingRequired = true; //} //else //{ // we are painting //Painting = true; Draw(RealRectangle); Painting = false; // slight chance of a race occuring but if we end up painting 3 times instead of just 2 it wont be the world // if (PaintingRequired) // { // // we need to paint again // PaintingRequired = false; // Refresh(); // } //} } protected override void OnInvalidated(InvalidateEventArgs e) { //OnPaint(null); } /// /// Gets the currently displayed rectangle of the control (ie includes how far we have scrolled) /// protected Rectangle RealRectangle { get { Rectangle r = ClientRectangle; r.Offset(-AutoScrollPosition.X, -AutoScrollPosition.Y); return r; } } // refresh on scroll protected override void OnScroll(ScrollEventArgs se) { Refresh(); } protected override void OnResize(EventArgs eventargs) { // ignore this size changed } protected override void OnClientSizeChanged(EventArgs e) { // ignore this size changed } protected override void OnSizeChanged(EventArgs e) { // use this one instead base.OnResize(e); // do size change <= calling on resize here may seem barking but it appears to be the only combination that works... DisposeBuffer(); // reinitalise buffer InitBuffer(); Refresh(); //refresh } // dispose private void DisposeBuffer() { BackBuffer.Dispose(); BufferGraphics.Dispose(); Graphics.Dispose(); } /// /// Render the contents of the rectangle to the buffer /// /// protected void Draw(Rectangle rectangle) { RenderToBuffer(new PaintEventArgs(BufferGraphics, rectangle)); OnPaint(null); } private void RenderToBuffer(PaintEventArgs paintEventArgs) { if (Disposing || IsDisposed || Program.Exiting) return; // make rectangle a little bigger just in case Rectangle invalid = paintEventArgs.ClipRectangle; invalid.Offset(AutoScrollPosition); // paint background base.OnPaintBackground(new PaintEventArgs(paintEventArgs.Graphics, invalid)); // paint rows int height = Padding.Top + AutoScrollPosition.Y; int width = TotalWidth(); int left = Padding.Left + AutoScrollPosition.X; // draw header if (HeaderRow != null) { int headerrowheight = HeaderRow.RowAndChildrenHeight; if (height >= invalid.Top - headerrowheight && height <= invalid.Bottom + headerrowheight) HeaderRow.OnPaint(new RowPaintArgs(paintEventArgs.Graphics, invalid, new Rectangle(left, height, width, headerrowheight))); height += headerrowheight + HeaderRow.SpaceAfter; } int numRows = Rows.Count; for (int i = 0; i < numRows; ++i) { GridRow row = Rows[i]; int rowheight = row.RowAndChildrenHeight; if (height >= invalid.Top - rowheight && height <= invalid.Bottom + rowheight) row.OnPaint(new RowPaintArgs(paintEventArgs.Graphics, invalid, new Rectangle(left, height, width, rowheight))); height += rowheight; if (i < numRows - 1) height += row.SpaceAfter; } height += Padding.Bottom; // set the clientrecangle to update the autoscrollbar AutoScrollMinSize = new Size(GetMinColumn(), height - AutoScrollPosition.Y); //base.OnPaint(paintEventArgs); } private int TotalWidth() { int allowedwidth = ClientSize.Width - Padding.Horizontal; if (HeaderRow == null) return allowedwidth; int actualwidth = GetMinColumn() - Padding.Horizontal; return actualwidth > allowedwidth ? actualwidth : allowedwidth; } private int GetMinColumn() { if (HeaderRow == null) return 0; int total = Padding.Horizontal + LeftOffset; foreach (string col in HeaderRow.Columns) { total += HeaderRow.GetColumnWidth(col); } return total; } protected override void OnPaint(PaintEventArgs e) { if (Disposing || IsDisposed || Program.Exiting) return; if (!DraggingColumns) { // we dont need to render again apparently :) Core.Drawing.QuickDraw(Graphics, BackBuffer); } else { //paint control IntPtr pTarget = DragColumnsGraphics.GetHdc(); IntPtr pSource = Core.Drawing.CreateCompatibleDC(pTarget); IntPtr pOrig = Core.Drawing.SelectObject(pSource, BackBuffer.GetHbitmap()); Core.Drawing.BitBlt(pTarget, 0, 0, BackBuffer.Width, BackBuffer.Height, pSource, 0, 0, Core.Drawing.TernaryRasterOperations.SRCCOPY); IntPtr pNew = Core.Drawing.SelectObject(pSource, pOrig); Core.Drawing.DeleteObject(pNew); Core.Drawing.DeleteDC(pSource); //DragGraphics.ReleaseHdc(pTarget); // paint dragged stuff //IntPtr pTarget2 = DragGraphics.GetHdc(); IntPtr pSource2 = Core.Drawing.CreateCompatibleDC(pTarget); IntPtr pOrig2 = Core.Drawing.SelectObject(pSource2, DragColumnsData.GetHbitmap()); Core.Drawing.BitBlt(pTarget, DragColumnsWindowPosition.X, DragColumnsWindowPosition.Y, DragColumnsData.Width, DragColumnsData.Height, pSource2, 0, 0, Core.Drawing.TernaryRasterOperations.SRCAND); IntPtr pNew2 = Core.Drawing.SelectObject(pSource2, pOrig2); Core.Drawing.DeleteObject(pNew2); Core.Drawing.DeleteDC(pSource2); DragColumnsGraphics.ReleaseHdc(pTarget); // paint everything to screen IntPtr pTarget3 = Graphics.GetHdc(); IntPtr pSource3 = Core.Drawing.CreateCompatibleDC(pTarget3); IntPtr pOrig3 = Core.Drawing.SelectObject(pSource3, DragColumnsBuffer.GetHbitmap()); Core.Drawing.BitBlt(pTarget3, 0, 0, DragColumnsBuffer.Width, DragColumnsBuffer.Height, pSource3, 0, 0, Core.Drawing.TernaryRasterOperations.SRCCOPY); IntPtr pNew3 = Core.Drawing.SelectObject(pSource3, pOrig3); Core.Drawing.DeleteObject(pNew3); Core.Drawing.DeleteDC(pSource3); Graphics.ReleaseHdc(pTarget3); } } protected override void OnPaintBackground(PaintEventArgs e) { //base.OnPaintBackground(e); } private bool DraggingItems; private bool DraggingColumns = false; private Bitmap DragColumnsData; private Point DragColumnsWindowPosition; private Point DragColumnsMouseLocation; private string ActiveColumn = ""; private Bitmap DragColumnsBuffer; Graphics DragColumnsGraphics; string DragColumnsLastEntered = ""; GridRow LastRow; bool ResizingColumns = false; Point ResizeColumnsInitialMouseLocation; protected override void OnMouseUp(MouseEventArgs e) { if (e.Button == MouseButtons.Left && DraggingColumns) { string swapwith = ""; int left = Padding.Left; foreach (string index in HeaderRow.Columns) { int width = GetColumnWidth(index, 1); if (e.X - AutoScrollPosition.X < left + (width / 2) && !((GridHeaderItem)HeaderRow.Items[index]).Immovable) { swapwith = index; break; } left += width; } if (string.IsNullOrEmpty(swapwith)) swapwith = "end"; DraggingColumns = false; if (!HeaderRow.Items.ContainsKey(ActiveColumn)) return; ((GridHeaderItem)HeaderRow.Items[ActiveColumn]).GreyOut = false; if (DragColumnsData != null) { DragColumnsData.Dispose(); DragColumnsData = null; } if (DragColumnsBuffer != null) { DragColumnsBuffer.Dispose(); DragColumnsBuffer = null; } if (DragColumnsGraphics != null) { DragColumnsGraphics.Dispose(); DragColumnsGraphics = null; } if (swapwith == "end") { HeaderRow.Columns.Remove(ActiveColumn); HeaderRow.Columns.Add(ActiveColumn); } else { int movedn = HeaderRow.Columns.IndexOf(ActiveColumn); int replacedn = HeaderRow.Columns.IndexOf(swapwith); if (movedn > replacedn) { HeaderRow.Columns.Remove(ActiveColumn); HeaderRow.Columns.Insert(replacedn, ActiveColumn); } else if (movedn < replacedn) { HeaderRow.Columns.Insert(replacedn, ActiveColumn); HeaderRow.Columns.Remove(ActiveColumn); } } DragColumnsLastEntered = ""; Refresh(); } else if (e.Button == MouseButtons.Left && ResizingColumns) { ResizingColumns = false; } ActiveColumn = ""; } public void AddRow(GridRow row) { Rows.Add(row); row.GridView = this; } public void SetHeaderRow(GridHeaderRow hr) { hr.GridView = this; } /// /// Returns the width of the specified columns (ie index to index + span) /// internal int GetColumnWidth(string col, int span) { if (HeaderRow == null) return 0; int total = 0; int index = HeaderRow.Columns.IndexOf(col); for (int i = index; i < index + span; i++) { if (HeaderRow.Columns.Count <= i) break; total += HeaderRow.GetColumnWidth(HeaderRow.Columns[i]); } return total; } /// /// Scroll up or down the list only if it has been focused /// protected override void OnMouseWheel(MouseEventArgs e) { AutoScrollPosition = new Point(-AutoScrollPosition.X, -AutoScrollPosition.Y - e.Delta); OnScroll(new ScrollEventArgs(ScrollEventType.EndScroll, 0)); } protected virtual void OnMouseButtonAction(MouseEventArgs e, MouseButtonAction type) { if (HeaderRow == null) return; if (!DraggingColumns && !ResizingColumns) { this.Focus(); Point p; GridRow row = FindRowFromPoint(e.Location, out p); if (row == null) return; if (e.Button == MouseButtons.Right && row is GridHeaderRow) { OpenChooseColumnsMenu(e.Location); } else if (e.Button == MouseButtons.Left) { if (Cursor == Cursors.SizeWE) { if (type == MouseButtonAction.DoubleClick) FitColumnWidthToHeaderAndContents(row, p); } else { row.OnMouseButtonAction(p, type); } } else if (type == MouseButtonAction.SingleRightClick) { row.OnMouseButtonAction(p, type); } } } private void FitColumnWidthToHeaderAndContents(GridRow currentRow, Point p) { GridHeaderItem headerItem = (GridHeaderItem)currentRow.FindItemFromPoint(new Point(p.X - GridHeaderItem.ResizeGutter, p.Y), out ResizeColumnsInitialMouseLocation); ResizeColumnsInitialMouseLocation.X += GridHeaderItem.ResizeGutter; ActiveColumn = headerItem.ColumnName; if (HeaderRow.Items.ContainsKey(ActiveColumn)) { int maxWidth = headerItem.MinimumWidth; foreach (GridRow row in RowsAndChildren) { GridItemBase _item = row.GetItem(ActiveColumn); int itemWidth = _item.GetGridItemWidth(ActiveColumn); if (itemWidth > maxWidth) maxWidth = itemWidth; } headerItem.Width = maxWidth; Refresh(); } } protected override void OnMouseClick(MouseEventArgs e) { OnMouseButtonAction(e, e.Button == MouseButtons.Left ? MouseButtonAction.SingleClick : MouseButtonAction.SingleRightClick); } protected override void OnMouseDoubleClick(MouseEventArgs e) { OnMouseButtonAction(e, MouseButtonAction.DoubleClick); } protected override void OnMouseDown(MouseEventArgs e) { lastClickedPoint = e.Location; OnMouseButtonAction(e, MouseButtonAction.MouseDown); } public virtual void OpenChooseColumnsMenu(Point p) { } protected override void OnMouseMove(MouseEventArgs e) { HoldCursorChange(delegate() { OnMouseMove_(e); }); } private void HoldCursorChange(MethodInvoker handler) { hold_cursor_change = true; try { handler(); } finally { hold_cursor_change = false; if (new_cursor != null) { Cursor = new_cursor; new_cursor = null; } } } private void OnMouseMove_(MouseEventArgs e) { if (Disposing || IsDisposed || Program.Exiting) return; if (HeaderRow == null) return; if (DraggingItems) return; if (DraggingColumns) { int x = e.Location.X - DragColumnsMouseLocation.X; int xmax = TotalWidth() - DragColumnsData.Width; DragColumnsWindowPosition = new Point(x <= 0 ? 0 : x < xmax ? x : xmax, 0); string swapwith = ""; int left = Padding.Left; foreach (string index in HeaderRow.Columns) { int width = GetColumnWidth(index, 1); if (((GridHeaderItem)HeaderRow.Items[index]).Immovable) { left += width; continue; } if (e.X - AutoScrollPosition.X < left + (width / 2)) { swapwith = index; break; } left += width; } if (string.IsNullOrEmpty(swapwith)) swapwith = "end"; if (swapwith != DragColumnsLastEntered) { if (swapwith == "end") { HeaderRow.Columns.Remove(ActiveColumn); HeaderRow.Columns.Add(ActiveColumn); } else { int movedn = HeaderRow.Columns.IndexOf(ActiveColumn); int replacedn = HeaderRow.Columns.IndexOf(swapwith); if (movedn > replacedn) { HeaderRow.Columns.Remove(ActiveColumn); HeaderRow.Columns.Insert(replacedn, ActiveColumn); } else if (movedn < replacedn) { HeaderRow.Columns.Insert(replacedn, ActiveColumn); HeaderRow.Columns.Remove(ActiveColumn); } } DragColumnsLastEntered = swapwith; Refresh(); } else { OnPaint(null); } } else if (ResizingColumns) { if (HeaderRow.Items.ContainsKey(ActiveColumn)) { GridHeaderItem item = (HeaderRow.Items[ActiveColumn] as GridHeaderItem); int newWidth = item.Width + (e.Location.X - ResizeColumnsInitialMouseLocation.X); if (newWidth > item.MinimumWidth) { item.Width = newWidth; ResizeColumnsInitialMouseLocation = e.Location; } else { item.Width = item.MinimumWidth; } Refresh(); } } else if (e.Button == MouseButtons.Left && string.IsNullOrEmpty(ActiveColumn) && DragDistance(lastClickedPoint, e.Location) >= 2) // don't start dragging unles we've moved a little distance // TODO: consider replacing the above with SystemParameters.MinimumHorizontalDragDistance etc. when we move to .NET 3.0 { Point p; GridRow row = FindRowFromPoint(lastClickedPoint, out p); // the drag is then based on the mouse-down location if (row is GridHeaderRow) { GridHeaderItem item = (GridHeaderItem)row.FindItemFromPoint(new Point(p.X - GridHeaderItem.ResizeGutter, p.Y), out DragColumnsMouseLocation); DragColumnsMouseLocation.X += GridHeaderItem.ResizeGutter; if (item == null) return; ActiveColumn = item.ColumnName; int colwidth = GetColumnWidth(ActiveColumn, 1); if (!item.UnSizable && DragColumnsMouseLocation.X >= colwidth - GridHeaderItem.ResizeGutter) { ResizingColumns = true; ResizeColumnsInitialMouseLocation = lastClickedPoint; } else if (!item.Immovable) { DraggingColumns = true; item.GreyOut = true; DragColumnsData = new Bitmap(GetColumnWidth(ActiveColumn, 1), ClientSize.Height); Graphics ddGraphics = Graphics.FromImage(DragColumnsData); ddGraphics.Clear(BackColor); XenAdmin.Core.Drawing.QuickDraw(ddGraphics, BackBuffer, new Point((lastClickedPoint.X - DragColumnsMouseLocation.X), 0), new Rectangle(0, 0, DragColumnsData.Width, DragColumnsData.Height)); ddGraphics.Dispose(); DragColumnsBuffer = new Bitmap(ClientSize.Width, ClientSize.Height); DragColumnsGraphics = Graphics.FromImage(DragColumnsBuffer); } } else if (row != null) row.OnMouseButtonAction(p, MouseButtonAction.StartDrag); } else { Point p; GridRow row = FindRowFromPoint(e.Location, out p); if (LastRow == null && row == null) return; if (LastRow == row) { LastRow.OnMouseMove(p); } else { if (LastRow != null) LastRow.OnLeave(); LastRow = row; if (LastRow != null) LastRow.OnEnter(p); } } } // The L_1 distance between two points private int DragDistance(Point p1, Point p2) { int xDisp = p1.X - p2.X; int yDisp = p1.Y - p2.Y; int totDisp = (xDisp >= 0 ? xDisp : -xDisp) + (yDisp >= 0 ? yDisp : -yDisp); return totDisp; } protected override void OnMouseLeave(EventArgs e) { HoldCursorChange(delegate() { if (LastRow != null) LastRow.OnLeave(); }); } /// /// Takes the point in question (eg location of the mouseclick) and returns the row /// that was clicked and the point on that row (ie adjusted so that 0,0 is top left of that row) /// private GridRow FindRowFromPoint(Point point, out Point p) { int height = Padding.Top + AutoScrollPosition.Y; p = Point.Empty; if (HeaderRow != null) { if (RowContainsPoint(HeaderRow, point, ref height, ref p)) return HeaderRow; } foreach (GridRow row in Rows) { if (RowContainsPoint(row, point, ref height, ref p)) return row; } return null; } private bool RowContainsPoint(GridRow row, Point point, ref int height, ref Point p) { int rowheight = row.RowAndChildrenHeight; if (point.Y >= height && point.Y < height + rowheight) { p = new Point(point.X - Padding.Left - AutoScrollPosition.X, point.Y - height); return true; } else { height += rowheight + row.SpaceAfter; return false; } } private Dictionary saveState; protected void SaveRowStates() { saveState = new Dictionary(); foreach (GridRow row in RowsAndChildren) saveState[row.Path] = row.State; } protected void RestoreRowStates() { RowState state; foreach (GridRow row in RowsAndChildren) { if (saveState.TryGetValue(row.Path, out state)) row.State = state; } } internal void UnselectAllRowsExcept(GridRow notThisOne) { foreach (GridRow row in RowsAndChildren) { if (row != notThisOne) row.Selected = false; } Refresh(); } internal void UnselectAllRows() { UnselectAllRowsExcept(null); } internal void SelectRowRange(GridRow newRow) { UnselectAllRows(); // Once through to check we haven't lost either of the endpoints // for some reason bool foundLast = false; bool foundNew = false; if (lastClickedRowPath != null) { foreach (GridRow row in RowsAndChildren) { if (row.Path == lastClickedRowPath) foundLast = true; if (row == newRow) foundNew = true; if (foundLast && foundNew) break; } } // If endpoint is missing, turn it into a normal select if (!foundLast || !foundNew) { newRow.Selected = true; LastClickedRow = newRow; } // Otherwise do the proper range select. // Make sure to include both endpoints. foundLast = foundNew = false; foreach (GridRow row in RowsAndChildren) { if (row.Path == lastClickedRowPath) foundLast = true; if (row == newRow) foundNew = true; if (foundLast || foundNew) row.Selected = true; if (foundLast && foundNew) break; } } public virtual void StartDragDrop() { GridRowCollection rows = new GridRowCollection(); foreach (GridRow row in RowsAndChildren) { if (row.Selected && IsDraggableRow(row)) rows.Add(row); } if (rows.Count > 0) DoDragDrop(rows, DragDropEffects.Move); } public virtual bool IsDraggableRow(GridRow row) { return true; } /// /// Equivalent to Clear(false). /// internal void Clear() { Clear(false); } /// /// Removes all rows from the GridView. /// /// If true, removes GridHeaderRows too. internal void Clear(bool removeHeaders) { Rows.RemoveAll(new Predicate(delegate(GridRow row) { return removeHeaders || !(row is GridHeaderRow); })); } internal virtual void Sort() { Rows.Sort(); foreach (GridRow row in Rows) { row.Sort(); } LastClickedRow = null; } public List> ColumnsAndWidths { get { List> columns = new List>(); if (HeaderRow == null) return columns; foreach (String columnName in HeaderRow.Columns) { if (!HeaderRow.Items.ContainsKey(columnName)) continue; GridHeaderItem item = HeaderRow.Items[columnName] as GridHeaderItem; if (item == null) continue; columns.Add(new KeyValuePair(columnName, item.Width)); } return columns; } } protected override void OnDragEnter(DragEventArgs e) { DraggingItems = true; e.Effect = DragDropEffects.None; base.OnDragEnter(e); } protected override void OnDragLeave(EventArgs e) { DraggingItems = false; base.OnDragLeave(e); } } }