/* * DropSink.cs - Add drop sink ability to an ObjectListView * * Author: Phillip Piper * Date: 2009-03-17 5:15 PM * * Change log: * v2.9 * 2015-07-08 JPP - Added SimpleDropSink.EnableFeedback to allow all the pretty and helpful * user feedback during drags to be turned off * v2.7 * 2011-04-20 JPP - Rewrote how ModelDropEventArgs.RefreshObjects() works on TreeListViews * v2.4.1 * 2010-08-24 JPP - Moved AcceptExternal property up to SimpleDragSource. * v2.3 * 2009-09-01 JPP - Correctly handle case where RefreshObjects() is called for * objects that were children but are now roots. * 2009-08-27 JPP - Added ModelDropEventArgs.RefreshObjects() to simplify updating after * a drag-drop operation * 2009-08-19 JPP - Changed to use OlvHitTest() * v2.2.1 * 2007-07-06 JPP - Added StandardDropActionFromKeys property to OlvDropEventArgs * v2.2 * 2009-05-17 JPP - Added a Handled flag to OlvDropEventArgs * - Tweaked the appearance of the drop-on-background feedback * 2009-04-15 JPP - Separated DragDrop.cs into DropSink.cs * 2009-03-17 JPP - Initial version * * Copyright (C) 2009-2014 Phillip Piper * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. */ using System; using System.Collections; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace BrightIdeasSoftware { /// /// Objects that implement this interface can acts as the receiver for drop /// operation for an ObjectListView. /// public interface IDropSink { /// /// Gets or sets the ObjectListView that is the drop sink /// ObjectListView ListView { get; set; } /// /// Draw any feedback that is appropriate to the current drop state. /// /// /// Any drawing is done over the top of the ListView. This operation should disturb /// the Graphic as little as possible. Specifically, do not erase the area into which /// you draw. /// /// A Graphic for drawing /// The contents bounds of the ListView (not including any header) void DrawFeedback(Graphics g, Rectangle bounds); /// /// The user has released the drop over this control /// /// /// Implementators should set args.Effect to the appropriate DragDropEffects. This value is returned /// to the originator of the drag. /// /// void Drop(DragEventArgs args); /// /// A drag has entered this control. /// /// Implementators should set args.Effect to the appropriate DragDropEffects. /// void Enter(DragEventArgs args); /// /// Change the cursor to reflect the current drag operation. /// /// void GiveFeedback(GiveFeedbackEventArgs args); /// /// The drag has left the bounds of this control /// void Leave(); /// /// The drag is moving over this control. /// /// This is where any drop target should be calculated. /// Implementators should set args.Effect to the appropriate DragDropEffects. /// /// void Over(DragEventArgs args); /// /// Should the drag be allowed to continue? /// /// void QueryContinue(QueryContinueDragEventArgs args); } /// /// This is a do-nothing implementation of IDropSink that is a useful /// base class for more sophisticated implementations. /// public class AbstractDropSink : IDropSink { #region IDropSink Members /// /// Gets or sets the ObjectListView that is the drop sink /// public virtual ObjectListView ListView { get { return listView; } set { this.listView = value; } } private ObjectListView listView; /// /// Draw any feedback that is appropriate to the current drop state. /// /// /// Any drawing is done over the top of the ListView. This operation should disturb /// the Graphic as little as possible. Specifically, do not erase the area into which /// you draw. /// /// A Graphic for drawing /// The contents bounds of the ListView (not including any header) public virtual void DrawFeedback(Graphics g, Rectangle bounds) { } /// /// The user has released the drop over this control /// /// /// Implementators should set args.Effect to the appropriate DragDropEffects. This value is returned /// to the originator of the drag. /// /// public virtual void Drop(DragEventArgs args) { this.Cleanup(); } /// /// A drag has entered this control. /// /// Implementators should set args.Effect to the appropriate DragDropEffects. /// public virtual void Enter(DragEventArgs args) { } /// /// The drag has left the bounds of this control /// public virtual void Leave() { this.Cleanup(); } /// /// The drag is moving over this control. /// /// This is where any drop target should be calculated. /// Implementators should set args.Effect to the appropriate DragDropEffects. /// /// public virtual void Over(DragEventArgs args) { } /// /// Change the cursor to reflect the current drag operation. /// /// You only need to override this if you want non-standard cursors. /// The standard cursors are supplied automatically. /// public virtual void GiveFeedback(GiveFeedbackEventArgs args) { args.UseDefaultCursors = true; } /// /// Should the drag be allowed to continue? /// /// /// You only need to override this if you want the user to be able /// to end the drop in some non-standard way, e.g. dragging to a /// certain point even without releasing the mouse, or going outside /// the bounds of the application. /// /// public virtual void QueryContinue(QueryContinueDragEventArgs args) { } #endregion #region Commands /// /// This is called when the mouse leaves the drop region and after the /// drop has completed. /// protected virtual void Cleanup() { } #endregion } /// /// The enum indicates which target has been found for a drop operation /// [Flags] public enum DropTargetLocation { /// /// No applicable target has been found /// None = 0, /// /// The list itself is the target of the drop /// Background = 0x01, /// /// An item is the target /// Item = 0x02, /// /// Between two items (or above the top item or below the bottom item) /// can be the target. This is not actually ever a target, only a value indicate /// that it is valid to drop between items /// BetweenItems = 0x04, /// /// Above an item is the target /// AboveItem = 0x08, /// /// Below an item is the target /// BelowItem = 0x10, /// /// A subitem is the target of the drop /// SubItem = 0x20, /// /// On the right of an item is the target (not currently used) /// RightOfItem = 0x40, /// /// On the left of an item is the target (not currently used) /// LeftOfItem = 0x80 } /// /// This class represents a simple implementation of a drop sink. /// /// /// Actually, it should be called CleverDropSink -- it's far from simple and can do quite a lot in its own right. /// public class SimpleDropSink : AbstractDropSink { #region Life and death /// /// Make a new drop sink /// public SimpleDropSink() { this.timer = new Timer(); this.timer.Interval = 250; this.timer.Tick += new EventHandler(this.timer_Tick); this.CanDropOnItem = true; //this.CanDropOnSubItem = true; //this.CanDropOnBackground = true; //this.CanDropBetween = true; this.FeedbackColor = Color.FromArgb(180, Color.MediumBlue); this.billboard = new BillboardOverlay(); } #endregion #region Public properties /// /// Get or set the locations where a drop is allowed to occur (OR-ed together) /// public DropTargetLocation AcceptableLocations { get { return this.acceptableLocations; } set { this.acceptableLocations = value; } } private DropTargetLocation acceptableLocations; /// /// Gets or sets whether this sink allows model objects to be dragged from other lists. Defaults to true. /// public bool AcceptExternal { get { return this.acceptExternal; } set { this.acceptExternal = value; } } private bool acceptExternal = true; /// /// Gets or sets whether the ObjectListView should scroll when the user drags /// something near to the top or bottom rows. Defaults to true. /// /// AutoScroll does not scroll horizontally. public bool AutoScroll { get { return this.autoScroll; } set { this.autoScroll = value; } } private bool autoScroll = true; /// /// Gets the billboard overlay that will be used to display feedback /// messages during a drag operation. /// /// Set this to null to stop the feedback. public BillboardOverlay Billboard { get { return this.billboard; } set { this.billboard = value; } } private BillboardOverlay billboard; /// /// Get or set whether a drop can occur between items of the list /// public bool CanDropBetween { get { return (this.AcceptableLocations & DropTargetLocation.BetweenItems) == DropTargetLocation.BetweenItems; } set { if (value) this.AcceptableLocations |= DropTargetLocation.BetweenItems; else this.AcceptableLocations &= ~DropTargetLocation.BetweenItems; } } /// /// Get or set whether a drop can occur on the listview itself /// public bool CanDropOnBackground { get { return (this.AcceptableLocations & DropTargetLocation.Background) == DropTargetLocation.Background; } set { if (value) this.AcceptableLocations |= DropTargetLocation.Background; else this.AcceptableLocations &= ~DropTargetLocation.Background; } } /// /// Get or set whether a drop can occur on items in the list /// public bool CanDropOnItem { get { return (this.AcceptableLocations & DropTargetLocation.Item) == DropTargetLocation.Item; } set { if (value) this.AcceptableLocations |= DropTargetLocation.Item; else this.AcceptableLocations &= ~DropTargetLocation.Item; } } /// /// Get or set whether a drop can occur on a subitem in the list /// public bool CanDropOnSubItem { get { return (this.AcceptableLocations & DropTargetLocation.SubItem) == DropTargetLocation.SubItem; } set { if (value) this.AcceptableLocations |= DropTargetLocation.SubItem; else this.AcceptableLocations &= ~DropTargetLocation.SubItem; } } /// /// Gets or sets whether the drop sink should draw feedback onto the given list /// during the drag operation. Defaults to true. /// /// If this is false, you will have to give the user feedback in some /// other fashion, like cursor changes public bool EnableFeedback { get { return enableFeedback; } set { enableFeedback = value; } } private bool enableFeedback = true; /// /// Get or set the index of the item that is the target of the drop /// public int DropTargetIndex { get { return dropTargetIndex; } set { if (this.dropTargetIndex != value) { this.dropTargetIndex = value; this.ListView.Invalidate(); } } } private int dropTargetIndex = -1; /// /// Get the item that is the target of the drop /// public OLVListItem DropTargetItem { get { return this.ListView.GetItem(this.DropTargetIndex); } } /// /// Get or set the location of the target of the drop /// public DropTargetLocation DropTargetLocation { get { return dropTargetLocation; } set { if (this.dropTargetLocation != value) { this.dropTargetLocation = value; this.ListView.Invalidate(); } } } private DropTargetLocation dropTargetLocation; /// /// Get or set the index of the subitem that is the target of the drop /// public int DropTargetSubItemIndex { get { return dropTargetSubItemIndex; } set { if (this.dropTargetSubItemIndex != value) { this.dropTargetSubItemIndex = value; this.ListView.Invalidate(); } } } private int dropTargetSubItemIndex = -1; /// /// Get or set the color that will be used to provide drop feedback /// public Color FeedbackColor { get { return this.feedbackColor; } set { this.feedbackColor = value; } } private Color feedbackColor; /// /// Get whether the alt key was down during this drop event /// public bool IsAltDown { get { return (this.KeyState & 32) == 32; } } /// /// Get whether any modifier key was down during this drop event /// public bool IsAnyModifierDown { get { return (this.KeyState & (4 + 8 + 32)) != 0; } } /// /// Get whether the control key was down during this drop event /// public bool IsControlDown { get { return (this.KeyState & 8) == 8; } } /// /// Get whether the left mouse button was down during this drop event /// public bool IsLeftMouseButtonDown { get { return (this.KeyState & 1) == 1; } } /// /// Get whether the right mouse button was down during this drop event /// public bool IsMiddleMouseButtonDown { get { return (this.KeyState & 16) == 16; } } /// /// Get whether the right mouse button was down during this drop event /// public bool IsRightMouseButtonDown { get { return (this.KeyState & 2) == 2; } } /// /// Get whether the shift key was down during this drop event /// public bool IsShiftDown { get { return (this.KeyState & 4) == 4; } } /// /// Get or set the state of the keys during this drop event /// public int KeyState { get { return this.keyState; } set { this.keyState = value; } } private int keyState; /// /// Gets or sets whether the drop sink will automatically use cursors /// based on the drop effect. By default, this is true. If this is /// set to false, you must set the Cursor yourself. /// public bool UseDefaultCursors { get { return useDefaultCursors; } set { useDefaultCursors = value; } } private bool useDefaultCursors = true; #endregion #region Events /// /// Triggered when the sink needs to know if a drop can occur. /// /// /// Handlers should set Effect to indicate what is possible. /// Handlers can change any of the DropTarget* setttings to change /// the target of the drop. /// public event EventHandler CanDrop; /// /// Triggered when the drop is made. /// public event EventHandler Dropped; /// /// Triggered when the sink needs to know if a drop can occur /// AND the source is an ObjectListView /// /// /// Handlers should set Effect to indicate what is possible. /// Handlers can change any of the DropTarget* setttings to change /// the target of the drop. /// public event EventHandler ModelCanDrop; /// /// Triggered when the drop is made. /// AND the source is an ObjectListView /// public event EventHandler ModelDropped; #endregion #region DropSink Interface /// /// Cleanup the drop sink when the mouse has left the control or /// the drag has finished. /// protected override void Cleanup() { this.DropTargetLocation = DropTargetLocation.None; this.ListView.FullRowSelect = this.originalFullRowSelect; this.Billboard.Text = null; } /// /// Draw any feedback that is appropriate to the current drop state. /// /// /// Any drawing is done over the top of the ListView. This operation should disturb /// the Graphic as little as possible. Specifically, do not erase the area into which /// you draw. /// /// A Graphic for drawing /// The contents bounds of the ListView (not including any header) public override void DrawFeedback(Graphics g, Rectangle bounds) { g.SmoothingMode = ObjectListView.SmoothingMode; if (this.EnableFeedback) { switch (this.DropTargetLocation) { case DropTargetLocation.Background: this.DrawFeedbackBackgroundTarget(g, bounds); break; case DropTargetLocation.Item: this.DrawFeedbackItemTarget(g, bounds); break; case DropTargetLocation.AboveItem: this.DrawFeedbackAboveItemTarget(g, bounds); break; case DropTargetLocation.BelowItem: this.DrawFeedbackBelowItemTarget(g, bounds); break; } } if (this.Billboard != null) { this.Billboard.Draw(this.ListView, g, bounds); } } /// /// The user has released the drop over this control /// /// public override void Drop(DragEventArgs args) { this.dropEventArgs.DragEventArgs = args; this.TriggerDroppedEvent(args); this.timer.Stop(); this.Cleanup(); } /// /// A drag has entered this control. /// /// Implementators should set args.Effect to the appropriate DragDropEffects. /// public override void Enter(DragEventArgs args) { //System.Diagnostics.Debug.WriteLine("Enter"); /* * When FullRowSelect is true, we have two problems: * 1) GetItemRect(ItemOnly) returns the whole row rather than just the icon/text, which messes * up our calculation of the drop rectangle. * 2) during the drag, the Timer events will not fire! This is the major problem, since without * those events we can't autoscroll. * * The first problem we can solve through coding, but the second is more difficult. * We avoid both problems by turning off FullRowSelect during the drop operation. */ this.originalFullRowSelect = this.ListView.FullRowSelect; this.ListView.FullRowSelect = false; // Setup our drop event args block this.dropEventArgs = new ModelDropEventArgs(); this.dropEventArgs.DropSink = this; this.dropEventArgs.ListView = this.ListView; this.dropEventArgs.DragEventArgs = args; this.dropEventArgs.DataObject = args.Data; OLVDataObject olvData = args.Data as OLVDataObject; if (olvData != null) { this.dropEventArgs.SourceListView = olvData.ListView; this.dropEventArgs.SourceModels = olvData.ModelObjects; } this.Over(args); } /// /// Change the cursor to reflect the current drag operation. /// /// public override void GiveFeedback(GiveFeedbackEventArgs args) { args.UseDefaultCursors = this.UseDefaultCursors; } /// /// The drag is moving over this control. /// /// public override void Over(DragEventArgs args) { //System.Diagnostics.Debug.WriteLine("Over"); this.dropEventArgs.DragEventArgs = args; this.KeyState = args.KeyState; Point pt = this.ListView.PointToClient(new Point(args.X, args.Y)); args.Effect = this.CalculateDropAction(args, pt); this.CheckScrolling(pt); } #endregion #region Events /// /// Trigger the Dropped events /// /// protected virtual void TriggerDroppedEvent(DragEventArgs args) { this.dropEventArgs.Handled = false; // If the source is an ObjectListView, trigger the ModelDropped event if (this.dropEventArgs.SourceListView != null) this.OnModelDropped(this.dropEventArgs); if (!this.dropEventArgs.Handled) this.OnDropped(this.dropEventArgs); } /// /// Trigger CanDrop /// /// protected virtual void OnCanDrop(OlvDropEventArgs args) { if (this.CanDrop != null) this.CanDrop(this, args); } /// /// Trigger Dropped /// /// protected virtual void OnDropped(OlvDropEventArgs args) { if (this.Dropped != null) this.Dropped(this, args); } /// /// Trigger ModelCanDrop /// /// protected virtual void OnModelCanDrop(ModelDropEventArgs args) { // Don't allow drops from other list, if that's what's configured if (!this.AcceptExternal && args.SourceListView != null && args.SourceListView != this.ListView) { args.Effect = DragDropEffects.None; args.DropTargetLocation = DropTargetLocation.None; args.InfoMessage = "This list doesn't accept drops from other lists"; return; } if (this.ModelCanDrop != null) this.ModelCanDrop(this, args); } /// /// Trigger ModelDropped /// /// protected virtual void OnModelDropped(ModelDropEventArgs args) { if (this.ModelDropped != null) this.ModelDropped(this, args); } #endregion #region Implementation private void timer_Tick(object sender, EventArgs e) { this.HandleTimerTick(); } /// /// Handle the timer tick event, which is sent when the listview should /// scroll /// protected virtual void HandleTimerTick() { // If the mouse has been released, stop scrolling. // This is only necessary if the mouse is released outside of the control. // If the mouse is released inside the control, we would receive a Drop event. if ((this.IsLeftMouseButtonDown && (Control.MouseButtons & MouseButtons.Left) != MouseButtons.Left) || (this.IsMiddleMouseButtonDown && (Control.MouseButtons & MouseButtons.Middle) != MouseButtons.Middle) || (this.IsRightMouseButtonDown && (Control.MouseButtons & MouseButtons.Right) != MouseButtons.Right)) { this.timer.Stop(); this.Cleanup(); return; } // Auto scrolling will continune while the mouse is close to the ListView const int GRACE_PERIMETER = 30; Point pt = this.ListView.PointToClient(Cursor.Position); Rectangle r2 = this.ListView.ClientRectangle; r2.Inflate(GRACE_PERIMETER, GRACE_PERIMETER); if (r2.Contains(pt)) { this.ListView.LowLevelScroll(0, this.scrollAmount); } } /// /// When the mouse is at the given point, what should the target of the drop be? /// /// This method should update the DropTarget* members of the given arg block /// /// The mouse point, in client co-ordinates protected virtual void CalculateDropTarget(OlvDropEventArgs args, Point pt) { const int SMALL_VALUE = 3; DropTargetLocation location = DropTargetLocation.None; int targetIndex = -1; int targetSubIndex = 0; if (this.CanDropOnBackground) location = DropTargetLocation.Background; // Which item is the mouse over? // If it is not over any item, it's over the background. //ListViewHitTestInfo info = this.ListView.HitTest(pt.X, pt.Y); OlvListViewHitTestInfo info = this.ListView.OlvHitTest(pt.X, pt.Y); if (info.Item != null && this.CanDropOnItem) { location = DropTargetLocation.Item; targetIndex = info.Item.Index; if (info.SubItem != null && this.CanDropOnSubItem) targetSubIndex = info.Item.SubItems.IndexOf(info.SubItem); } // Check to see if the mouse is "between" rows. // ("between" is somewhat loosely defined) if (this.CanDropBetween && this.ListView.GetItemCount() > 0) { // If the mouse is over an item, check to see if it is near the top or bottom if (location == DropTargetLocation.Item) { if (pt.Y - SMALL_VALUE <= info.Item.Bounds.Top) location = DropTargetLocation.AboveItem; if (pt.Y + SMALL_VALUE >= info.Item.Bounds.Bottom) location = DropTargetLocation.BelowItem; } else { // Is there an item a little below the mouse? // If so, we say the drop point is above that row info = this.ListView.OlvHitTest(pt.X, pt.Y + SMALL_VALUE); if (info.Item != null) { targetIndex = info.Item.Index; location = DropTargetLocation.AboveItem; } else { // Is there an item a little above the mouse? info = this.ListView.OlvHitTest(pt.X, pt.Y - SMALL_VALUE); if (info.Item != null) { targetIndex = info.Item.Index; location = DropTargetLocation.BelowItem; } } } } args.DropTargetLocation = location; args.DropTargetIndex = targetIndex; args.DropTargetSubItemIndex = targetSubIndex; } /// /// What sort of action is possible when the mouse is at the given point? /// /// /// /// /// /// public virtual DragDropEffects CalculateDropAction(DragEventArgs args, Point pt) { this.CalculateDropTarget(this.dropEventArgs, pt); this.dropEventArgs.MouseLocation = pt; this.dropEventArgs.InfoMessage = null; this.dropEventArgs.Handled = false; if (this.dropEventArgs.SourceListView != null) { this.dropEventArgs.TargetModel = this.ListView.GetModelObject(this.dropEventArgs.DropTargetIndex); this.OnModelCanDrop(this.dropEventArgs); } if (!this.dropEventArgs.Handled) this.OnCanDrop(this.dropEventArgs); this.UpdateAfterCanDropEvent(this.dropEventArgs); return this.dropEventArgs.Effect; } /// /// Based solely on the state of the modifier keys, what drop operation should /// be used? /// /// The drop operation that matches the state of the keys public DragDropEffects CalculateStandardDropActionFromKeys() { if (this.IsControlDown) { if (this.IsShiftDown) return DragDropEffects.Link; else return DragDropEffects.Copy; } else { return DragDropEffects.Move; } } /// /// Should the listview be made to scroll when the mouse is at the given point? /// /// protected virtual void CheckScrolling(Point pt) { if (!this.AutoScroll) return; Rectangle r = this.ListView.ContentRectangle; int rowHeight = this.ListView.RowHeightEffective; int close = rowHeight; // In Tile view, using the whole row height is too much if (this.ListView.View == View.Tile) close /= 2; if (pt.Y <= (r.Top + close)) { // Scroll faster if the mouse is closer to the top this.timer.Interval = ((pt.Y <= (r.Top + close / 2)) ? 100 : 350); this.timer.Start(); this.scrollAmount = -rowHeight; } else { if (pt.Y >= (r.Bottom - close)) { this.timer.Interval = ((pt.Y >= (r.Bottom - close / 2)) ? 100 : 350); this.timer.Start(); this.scrollAmount = rowHeight; } else { this.timer.Stop(); } } } /// /// Update the state of our sink to reflect the information that /// may have been written into the drop event args. /// /// protected virtual void UpdateAfterCanDropEvent(OlvDropEventArgs args) { this.DropTargetIndex = args.DropTargetIndex; this.DropTargetLocation = args.DropTargetLocation; this.DropTargetSubItemIndex = args.DropTargetSubItemIndex; if (this.Billboard != null) { Point pt = args.MouseLocation; pt.Offset(5, 5); if (this.Billboard.Text != this.dropEventArgs.InfoMessage || this.Billboard.Location != pt) { this.Billboard.Text = this.dropEventArgs.InfoMessage; this.Billboard.Location = pt; this.ListView.Invalidate(); } } } #endregion #region Rendering /// /// Draw the feedback that shows that the background is the target /// /// /// protected virtual void DrawFeedbackBackgroundTarget(Graphics g, Rectangle bounds) { float penWidth = 12.0f; Rectangle r = bounds; r.Inflate((int)-penWidth / 2, (int)-penWidth / 2); using (Pen p = new Pen(Color.FromArgb(128, this.FeedbackColor), penWidth)) { using (GraphicsPath path = this.GetRoundedRect(r, 30.0f)) { g.DrawPath(p, path); } } } /// /// Draw the feedback that shows that an item (or a subitem) is the target /// /// /// /// /// DropTargetItem and DropTargetSubItemIndex tells what is the target /// protected virtual void DrawFeedbackItemTarget(Graphics g, Rectangle bounds) { if (this.DropTargetItem == null) return; Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); r.Inflate(1, 1); float diameter = r.Height / 3; using (GraphicsPath path = this.GetRoundedRect(r, diameter)) { using (SolidBrush b = new SolidBrush(Color.FromArgb(48, this.FeedbackColor))) { g.FillPath(b, path); } using (Pen p = new Pen(this.FeedbackColor, 3.0f)) { g.DrawPath(p, path); } } } /// /// Draw the feedback that shows the drop will occur before target /// /// /// protected virtual void DrawFeedbackAboveItemTarget(Graphics g, Rectangle bounds) { if (this.DropTargetItem == null) return; Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); this.DrawBetweenLine(g, r.Left, r.Top, r.Right, r.Top); } /// /// Draw the feedback that shows the drop will occur after target /// /// /// protected virtual void DrawFeedbackBelowItemTarget(Graphics g, Rectangle bounds) { if (this.DropTargetItem == null) return; Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); this.DrawBetweenLine(g, r.Left, r.Bottom, r.Right, r.Bottom); } /// /// Return a GraphicPath that is round corner rectangle. /// /// /// /// protected GraphicsPath GetRoundedRect(Rectangle rect, float diameter) { GraphicsPath path = new GraphicsPath(); RectangleF arc = new RectangleF(rect.X, rect.Y, diameter, diameter); path.AddArc(arc, 180, 90); arc.X = rect.Right - diameter; path.AddArc(arc, 270, 90); arc.Y = rect.Bottom - diameter; path.AddArc(arc, 0, 90); arc.X = rect.Left; path.AddArc(arc, 90, 90); path.CloseFigure(); return path; } /// /// Calculate the target rectangle when the given item (and possible subitem) /// is the target of the drop. /// /// /// /// protected virtual Rectangle CalculateDropTargetRectangle(OLVListItem item, int subItem) { if (subItem > 0) return item.SubItems[subItem].Bounds; Rectangle r = this.ListView.CalculateCellTextBounds(item, subItem); // Allow for indent if (item.IndentCount > 0) { int indentWidth = this.ListView.SmallImageSize.Width; r.X += (indentWidth * item.IndentCount); r.Width -= (indentWidth * item.IndentCount); } return r; } /// /// Draw a "between items" line at the given co-ordinates /// /// /// /// /// /// protected virtual void DrawBetweenLine(Graphics g, int x1, int y1, int x2, int y2) { using (Brush b = new SolidBrush(this.FeedbackColor)) { int x = x1; int y = y1; using (GraphicsPath gp = new GraphicsPath()) { gp.AddLine( x, y + 5, x, y - 5); gp.AddBezier( x, y - 6, x + 3, y - 2, x + 6, y - 1, x + 11, y); gp.AddBezier( x + 11, y, x + 6, y + 1, x + 3, y + 2, x, y + 6); gp.CloseFigure(); g.FillPath(b, gp); } x = x2; y = y2; using (GraphicsPath gp = new GraphicsPath()) { gp.AddLine( x, y + 6, x, y - 6); gp.AddBezier( x, y - 7, x - 3, y - 2, x - 6, y - 1, x - 11, y); gp.AddBezier( x - 11, y, x - 6, y + 1, x - 3, y + 2, x, y + 7); gp.CloseFigure(); g.FillPath(b, gp); } } using (Pen p = new Pen(this.FeedbackColor, 3.0f)) { g.DrawLine(p, x1, y1, x2, y2); } } #endregion private Timer timer; private int scrollAmount; private bool originalFullRowSelect; private ModelDropEventArgs dropEventArgs; } /// /// This drop sink allows items within the same list to be rearranged, /// as well as allowing items to be dropped from other lists. /// /// /// /// This class can only be used on plain ObjectListViews and FastObjectListViews. /// The other flavours have no way to implement the insert operation that is required. /// /// /// This class does not work with grouping. /// /// /// This class works when the OLV is sorted, but it is up to the programmer /// to decide what rearranging such lists "means". Example: if the control is sorting /// students by academic grade, and the user drags a "Fail" grade student up amonst the "A+" /// students, it is the responsibility of the programmer to makes the appropriate changes /// to the model and redraw/rebuild the control so that the users action makes sense. /// /// /// Users of this class should listen for the CanDrop event to decide /// if models from another OLV can be moved to OLV under this sink. /// /// public class RearrangingDropSink : SimpleDropSink { /// /// Create a RearrangingDropSink /// public RearrangingDropSink() { this.CanDropBetween = true; this.CanDropOnBackground = true; this.CanDropOnItem = false; } /// /// Create a RearrangingDropSink /// /// public RearrangingDropSink(bool acceptDropsFromOtherLists) : this() { this.AcceptExternal = acceptDropsFromOtherLists; } /// /// Trigger OnModelCanDrop /// /// protected override void OnModelCanDrop(ModelDropEventArgs args) { base.OnModelCanDrop(args); if (args.Handled) return; args.Effect = DragDropEffects.Move; // Don't allow drops from other list, if that's what's configured if (!this.AcceptExternal && args.SourceListView != this.ListView) { args.Effect = DragDropEffects.None; args.DropTargetLocation = DropTargetLocation.None; args.InfoMessage = "This list doesn't accept drops from other lists"; } // If we are rearranging a list, don't allow drops on the background if (args.DropTargetLocation == DropTargetLocation.Background && args.SourceListView == this.ListView) { args.Effect = DragDropEffects.None; args.DropTargetLocation = DropTargetLocation.None; } } /// /// Trigger OnModelDropped /// /// protected override void OnModelDropped(ModelDropEventArgs args) { base.OnModelDropped(args); if (!args.Handled) this.RearrangeModels(args); } /// /// Do the work of processing the dropped items /// /// public virtual void RearrangeModels(ModelDropEventArgs args) { switch (args.DropTargetLocation) { case DropTargetLocation.AboveItem: this.ListView.MoveObjects(args.DropTargetIndex, args.SourceModels); break; case DropTargetLocation.BelowItem: this.ListView.MoveObjects(args.DropTargetIndex + 1, args.SourceModels); break; case DropTargetLocation.Background: this.ListView.AddObjects(args.SourceModels); break; default: return; } if (args.SourceListView != this.ListView) { args.SourceListView.RemoveObjects(args.SourceModels); } } } /// /// When a drop sink needs to know if something can be dropped, or /// to notify that a drop has occured, it uses an instance of this class. /// public class OlvDropEventArgs : EventArgs { /// /// Create a OlvDropEventArgs /// public OlvDropEventArgs() { } #region Data Properties /// /// Get the original drag-drop event args /// public DragEventArgs DragEventArgs { get { return this.dragEventArgs; } internal set { this.dragEventArgs = value; } } private DragEventArgs dragEventArgs; /// /// Get the data object that is being dragged /// public object DataObject { get { return this.dataObject; } internal set { this.dataObject = value; } } private object dataObject; /// /// Get the drop sink that originated this event /// public SimpleDropSink DropSink { get { return this.dropSink; } internal set { this.dropSink = value; } } private SimpleDropSink dropSink; /// /// Get or set the index of the item that is the target of the drop /// public int DropTargetIndex { get { return dropTargetIndex; } set { this.dropTargetIndex = value; } } private int dropTargetIndex = -1; /// /// Get or set the location of the target of the drop /// public DropTargetLocation DropTargetLocation { get { return dropTargetLocation; } set { this.dropTargetLocation = value; } } private DropTargetLocation dropTargetLocation; /// /// Get or set the index of the subitem that is the target of the drop /// public int DropTargetSubItemIndex { get { return dropTargetSubItemIndex; } set { this.dropTargetSubItemIndex = value; } } private int dropTargetSubItemIndex = -1; /// /// Get the item that is the target of the drop /// public OLVListItem DropTargetItem { get { return this.ListView.GetItem(this.DropTargetIndex); } set { if (value == null) this.DropTargetIndex = -1; else this.DropTargetIndex = value.Index; } } /// /// Get or set the drag effect that should be used for this operation /// public DragDropEffects Effect { get { return this.effect; } set { this.effect = value; } } private DragDropEffects effect; /// /// Get or set if this event was handled. No further processing will be done for a handled event. /// public bool Handled { get { return this.handled; } set { this.handled = value; } } private bool handled; /// /// Get or set the feedback message for this operation /// /// /// If this is not null, it will be displayed as a feedback message /// during the drag. /// public string InfoMessage { get { return this.infoMessage; } set { this.infoMessage = value; } } private string infoMessage; /// /// Get the ObjectListView that is being dropped on /// public ObjectListView ListView { get { return this.listView; } internal set { this.listView = value; } } private ObjectListView listView; /// /// Get the location of the mouse (in target ListView co-ords) /// public Point MouseLocation { get { return this.mouseLocation; } internal set { this.mouseLocation = value; } } private Point mouseLocation; /// /// Get the drop action indicated solely by the state of the modifier keys /// public DragDropEffects StandardDropActionFromKeys { get { return this.DropSink.CalculateStandardDropActionFromKeys(); } } #endregion } /// /// These events are triggered when the drag source is an ObjectListView. /// public class ModelDropEventArgs : OlvDropEventArgs { /// /// Create a ModelDropEventArgs /// public ModelDropEventArgs() { } /// /// Gets the model objects that are being dragged. /// public IList SourceModels { get { return this.dragModels; } internal set { this.dragModels = value; TreeListView tlv = this.SourceListView as TreeListView; if (tlv != null) { foreach (object model in this.SourceModels) { object parent = tlv.GetParent(model); if (!toBeRefreshed.Contains(parent)) toBeRefreshed.Add(parent); } } } } private IList dragModels; private ArrayList toBeRefreshed = new ArrayList(); /// /// Gets the ObjectListView that is the source of the dragged objects. /// public ObjectListView SourceListView { get { return this.sourceListView; } internal set { this.sourceListView = value; } } private ObjectListView sourceListView; /// /// Get the model object that is being dropped upon. /// /// This is only value for TargetLocation == Item public object TargetModel { get { return this.targetModel; } internal set { this.targetModel = value; } } private object targetModel; /// /// Refresh all the objects involved in the operation /// public void RefreshObjects() { toBeRefreshed.AddRange(this.SourceModels); TreeListView tlv = this.SourceListView as TreeListView; if (tlv == null) this.SourceListView.RefreshObjects(toBeRefreshed); else tlv.RebuildAll(true); TreeListView tlv2 = this.ListView as TreeListView; if (tlv2 == null) this.ListView.RefreshObject(this.TargetModel); else tlv2.RebuildAll(true); } } }