RaUI/Source/ryControls/ObjectListView/DragDrop/DropSink.cs
鑫Intel 523add43be ### 2021-01-12 dev更新
------
#### ryControls    V2.1.2101.1201
- *.[更新]内置的ObjectListView从1.13更新到2.9.1版本,并对主要属性进行汉化。
- *.[修复]修复新版ObjectListView选中项有筛选结果时,筛选结果白色字体看不清的BUG。
- *.[改进]TextBoxEx2默认事件改为TextChanged2。
2021-01-12 16:32:29 +08:00

1455 lines
54 KiB
C#

/*
* 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 <http://www.gnu.org/licenses/>.
*
* 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
{
/// <summary>
/// Objects that implement this interface can acts as the receiver for drop
/// operation for an ObjectListView.
/// </summary>
public interface IDropSink
{
/// <summary>
/// Gets or sets the ObjectListView that is the drop sink
/// </summary>
ObjectListView ListView { get; set; }
/// <summary>
/// Draw any feedback that is appropriate to the current drop state.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="g">A Graphic for drawing</param>
/// <param name="bounds">The contents bounds of the ListView (not including any header)</param>
void DrawFeedback(Graphics g, Rectangle bounds);
/// <summary>
/// The user has released the drop over this control
/// </summary>
/// <remarks>
/// Implementators should set args.Effect to the appropriate DragDropEffects. This value is returned
/// to the originator of the drag.
/// </remarks>
/// <param name="args"></param>
void Drop(DragEventArgs args);
/// <summary>
/// A drag has entered this control.
/// </summary>
/// <remarks>Implementators should set args.Effect to the appropriate DragDropEffects.</remarks>
/// <param name="args"></param>
void Enter(DragEventArgs args);
/// <summary>
/// Change the cursor to reflect the current drag operation.
/// </summary>
/// <param name="args"></param>
void GiveFeedback(GiveFeedbackEventArgs args);
/// <summary>
/// The drag has left the bounds of this control
/// </summary>
void Leave();
/// <summary>
/// The drag is moving over this control.
/// </summary>
/// <remarks>This is where any drop target should be calculated.
/// Implementators should set args.Effect to the appropriate DragDropEffects.
/// </remarks>
/// <param name="args"></param>
void Over(DragEventArgs args);
/// <summary>
/// Should the drag be allowed to continue?
/// </summary>
/// <param name="args"></param>
void QueryContinue(QueryContinueDragEventArgs args);
}
/// <summary>
/// This is a do-nothing implementation of IDropSink that is a useful
/// base class for more sophisticated implementations.
/// </summary>
public class AbstractDropSink : IDropSink
{
#region IDropSink Members
/// <summary>
/// Gets or sets the ObjectListView that is the drop sink
/// </summary>
public virtual ObjectListView ListView {
get { return listView; }
set { this.listView = value; }
}
private ObjectListView listView;
/// <summary>
/// Draw any feedback that is appropriate to the current drop state.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="g">A Graphic for drawing</param>
/// <param name="bounds">The contents bounds of the ListView (not including any header)</param>
public virtual void DrawFeedback(Graphics g, Rectangle bounds) {
}
/// <summary>
/// The user has released the drop over this control
/// </summary>
/// <remarks>
/// Implementators should set args.Effect to the appropriate DragDropEffects. This value is returned
/// to the originator of the drag.
/// </remarks>
/// <param name="args"></param>
public virtual void Drop(DragEventArgs args) {
this.Cleanup();
}
/// <summary>
/// A drag has entered this control.
/// </summary>
/// <remarks>Implementators should set args.Effect to the appropriate DragDropEffects.</remarks>
/// <param name="args"></param>
public virtual void Enter(DragEventArgs args) {
}
/// <summary>
/// The drag has left the bounds of this control
/// </summary>
public virtual void Leave() {
this.Cleanup();
}
/// <summary>
/// The drag is moving over this control.
/// </summary>
/// <remarks>This is where any drop target should be calculated.
/// Implementators should set args.Effect to the appropriate DragDropEffects.
/// </remarks>
/// <param name="args"></param>
public virtual void Over(DragEventArgs args) {
}
/// <summary>
/// Change the cursor to reflect the current drag operation.
/// </summary>
/// <remarks>You only need to override this if you want non-standard cursors.
/// The standard cursors are supplied automatically.</remarks>
/// <param name="args"></param>
public virtual void GiveFeedback(GiveFeedbackEventArgs args) {
args.UseDefaultCursors = true;
}
/// <summary>
/// Should the drag be allowed to continue?
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="args"></param>
public virtual void QueryContinue(QueryContinueDragEventArgs args) {
}
#endregion
#region Commands
/// <summary>
/// This is called when the mouse leaves the drop region and after the
/// drop has completed.
/// </summary>
protected virtual void Cleanup() {
}
#endregion
}
/// <summary>
/// The enum indicates which target has been found for a drop operation
/// </summary>
[Flags]
public enum DropTargetLocation
{
/// <summary>
/// No applicable target has been found
/// </summary>
None = 0,
/// <summary>
/// The list itself is the target of the drop
/// </summary>
Background = 0x01,
/// <summary>
/// An item is the target
/// </summary>
Item = 0x02,
/// <summary>
/// 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
/// </summary>
BetweenItems = 0x04,
/// <summary>
/// Above an item is the target
/// </summary>
AboveItem = 0x08,
/// <summary>
/// Below an item is the target
/// </summary>
BelowItem = 0x10,
/// <summary>
/// A subitem is the target of the drop
/// </summary>
SubItem = 0x20,
/// <summary>
/// On the right of an item is the target (not currently used)
/// </summary>
RightOfItem = 0x40,
/// <summary>
/// On the left of an item is the target (not currently used)
/// </summary>
LeftOfItem = 0x80
}
/// <summary>
/// This class represents a simple implementation of a drop sink.
/// </summary>
/// <remarks>
/// Actually, it should be called CleverDropSink -- it's far from simple and can do quite a lot in its own right.
/// </remarks>
public class SimpleDropSink : AbstractDropSink
{
#region Life and death
/// <summary>
/// Make a new drop sink
/// </summary>
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
/// <summary>
/// Get or set the locations where a drop is allowed to occur (OR-ed together)
/// </summary>
public DropTargetLocation AcceptableLocations {
get { return this.acceptableLocations; }
set { this.acceptableLocations = value; }
}
private DropTargetLocation acceptableLocations;
/// <summary>
/// Gets or sets whether this sink allows model objects to be dragged from other lists. Defaults to true.
/// </summary>
public bool AcceptExternal {
get { return this.acceptExternal; }
set { this.acceptExternal = value; }
}
private bool acceptExternal = true;
/// <summary>
/// Gets or sets whether the ObjectListView should scroll when the user drags
/// something near to the top or bottom rows. Defaults to true.
/// </summary>
/// <remarks>AutoScroll does not scroll horizontally.</remarks>
public bool AutoScroll {
get { return this.autoScroll; }
set { this.autoScroll = value; }
}
private bool autoScroll = true;
/// <summary>
/// Gets the billboard overlay that will be used to display feedback
/// messages during a drag operation.
/// </summary>
/// <remarks>Set this to null to stop the feedback.</remarks>
public BillboardOverlay Billboard {
get { return this.billboard; }
set { this.billboard = value; }
}
private BillboardOverlay billboard;
/// <summary>
/// Get or set whether a drop can occur between items of the list
/// </summary>
public bool CanDropBetween {
get { return (this.AcceptableLocations & DropTargetLocation.BetweenItems) == DropTargetLocation.BetweenItems; }
set {
if (value)
this.AcceptableLocations |= DropTargetLocation.BetweenItems;
else
this.AcceptableLocations &= ~DropTargetLocation.BetweenItems;
}
}
/// <summary>
/// Get or set whether a drop can occur on the listview itself
/// </summary>
public bool CanDropOnBackground {
get { return (this.AcceptableLocations & DropTargetLocation.Background) == DropTargetLocation.Background; }
set {
if (value)
this.AcceptableLocations |= DropTargetLocation.Background;
else
this.AcceptableLocations &= ~DropTargetLocation.Background;
}
}
/// <summary>
/// Get or set whether a drop can occur on items in the list
/// </summary>
public bool CanDropOnItem {
get { return (this.AcceptableLocations & DropTargetLocation.Item) == DropTargetLocation.Item; }
set {
if (value)
this.AcceptableLocations |= DropTargetLocation.Item;
else
this.AcceptableLocations &= ~DropTargetLocation.Item;
}
}
/// <summary>
/// Get or set whether a drop can occur on a subitem in the list
/// </summary>
public bool CanDropOnSubItem {
get { return (this.AcceptableLocations & DropTargetLocation.SubItem) == DropTargetLocation.SubItem; }
set {
if (value)
this.AcceptableLocations |= DropTargetLocation.SubItem;
else
this.AcceptableLocations &= ~DropTargetLocation.SubItem;
}
}
/// <summary>
/// Gets or sets whether the drop sink should draw feedback onto the given list
/// during the drag operation. Defaults to true.
/// </summary>
/// <remarks>If this is false, you will have to give the user feedback in some
/// other fashion, like cursor changes</remarks>
public bool EnableFeedback {
get { return enableFeedback; }
set { enableFeedback = value; }
}
private bool enableFeedback = true;
/// <summary>
/// Get or set the index of the item that is the target of the drop
/// </summary>
public int DropTargetIndex {
get { return dropTargetIndex; }
set {
if (this.dropTargetIndex != value) {
this.dropTargetIndex = value;
this.ListView.Invalidate();
}
}
}
private int dropTargetIndex = -1;
/// <summary>
/// Get the item that is the target of the drop
/// </summary>
public OLVListItem DropTargetItem {
get {
return this.ListView.GetItem(this.DropTargetIndex);
}
}
/// <summary>
/// Get or set the location of the target of the drop
/// </summary>
public DropTargetLocation DropTargetLocation {
get { return dropTargetLocation; }
set {
if (this.dropTargetLocation != value) {
this.dropTargetLocation = value;
this.ListView.Invalidate();
}
}
}
private DropTargetLocation dropTargetLocation;
/// <summary>
/// Get or set the index of the subitem that is the target of the drop
/// </summary>
public int DropTargetSubItemIndex {
get { return dropTargetSubItemIndex; }
set {
if (this.dropTargetSubItemIndex != value) {
this.dropTargetSubItemIndex = value;
this.ListView.Invalidate();
}
}
}
private int dropTargetSubItemIndex = -1;
/// <summary>
/// Get or set the color that will be used to provide drop feedback
/// </summary>
public Color FeedbackColor {
get { return this.feedbackColor; }
set { this.feedbackColor = value; }
}
private Color feedbackColor;
/// <summary>
/// Get whether the alt key was down during this drop event
/// </summary>
public bool IsAltDown {
get { return (this.KeyState & 32) == 32; }
}
/// <summary>
/// Get whether any modifier key was down during this drop event
/// </summary>
public bool IsAnyModifierDown {
get { return (this.KeyState & (4 + 8 + 32)) != 0; }
}
/// <summary>
/// Get whether the control key was down during this drop event
/// </summary>
public bool IsControlDown {
get { return (this.KeyState & 8) == 8; }
}
/// <summary>
/// Get whether the left mouse button was down during this drop event
/// </summary>
public bool IsLeftMouseButtonDown {
get { return (this.KeyState & 1) == 1; }
}
/// <summary>
/// Get whether the right mouse button was down during this drop event
/// </summary>
public bool IsMiddleMouseButtonDown {
get { return (this.KeyState & 16) == 16; }
}
/// <summary>
/// Get whether the right mouse button was down during this drop event
/// </summary>
public bool IsRightMouseButtonDown {
get { return (this.KeyState & 2) == 2; }
}
/// <summary>
/// Get whether the shift key was down during this drop event
/// </summary>
public bool IsShiftDown {
get { return (this.KeyState & 4) == 4; }
}
/// <summary>
/// Get or set the state of the keys during this drop event
/// </summary>
public int KeyState {
get { return this.keyState; }
set { this.keyState = value; }
}
private int keyState;
/// <summary>
/// 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.
/// </summary>
public bool UseDefaultCursors {
get { return useDefaultCursors; }
set { useDefaultCursors = value; }
}
private bool useDefaultCursors = true;
#endregion
#region Events
/// <summary>
/// Triggered when the sink needs to know if a drop can occur.
/// </summary>
/// <remarks>
/// Handlers should set Effect to indicate what is possible.
/// Handlers can change any of the DropTarget* setttings to change
/// the target of the drop.
/// </remarks>
public event EventHandler<OlvDropEventArgs> CanDrop;
/// <summary>
/// Triggered when the drop is made.
/// </summary>
public event EventHandler<OlvDropEventArgs> Dropped;
/// <summary>
/// Triggered when the sink needs to know if a drop can occur
/// AND the source is an ObjectListView
/// </summary>
/// <remarks>
/// Handlers should set Effect to indicate what is possible.
/// Handlers can change any of the DropTarget* setttings to change
/// the target of the drop.
/// </remarks>
public event EventHandler<ModelDropEventArgs> ModelCanDrop;
/// <summary>
/// Triggered when the drop is made.
/// AND the source is an ObjectListView
/// </summary>
public event EventHandler<ModelDropEventArgs> ModelDropped;
#endregion
#region DropSink Interface
/// <summary>
/// Cleanup the drop sink when the mouse has left the control or
/// the drag has finished.
/// </summary>
protected override void Cleanup() {
this.DropTargetLocation = DropTargetLocation.None;
this.ListView.FullRowSelect = this.originalFullRowSelect;
this.Billboard.Text = null;
}
/// <summary>
/// Draw any feedback that is appropriate to the current drop state.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="g">A Graphic for drawing</param>
/// <param name="bounds">The contents bounds of the ListView (not including any header)</param>
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);
}
}
/// <summary>
/// The user has released the drop over this control
/// </summary>
/// <param name="args"></param>
public override void Drop(DragEventArgs args) {
this.dropEventArgs.DragEventArgs = args;
this.TriggerDroppedEvent(args);
this.timer.Stop();
this.Cleanup();
}
/// <summary>
/// A drag has entered this control.
/// </summary>
/// <remarks>Implementators should set args.Effect to the appropriate DragDropEffects.</remarks>
/// <param name="args"></param>
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);
}
/// <summary>
/// Change the cursor to reflect the current drag operation.
/// </summary>
/// <param name="args"></param>
public override void GiveFeedback(GiveFeedbackEventArgs args) {
args.UseDefaultCursors = this.UseDefaultCursors;
}
/// <summary>
/// The drag is moving over this control.
/// </summary>
/// <param name="args"></param>
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
/// <summary>
/// Trigger the Dropped events
/// </summary>
/// <param name="args"></param>
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);
}
/// <summary>
/// Trigger CanDrop
/// </summary>
/// <param name="args"></param>
protected virtual void OnCanDrop(OlvDropEventArgs args) {
if (this.CanDrop != null)
this.CanDrop(this, args);
}
/// <summary>
/// Trigger Dropped
/// </summary>
/// <param name="args"></param>
protected virtual void OnDropped(OlvDropEventArgs args) {
if (this.Dropped != null)
this.Dropped(this, args);
}
/// <summary>
/// Trigger ModelCanDrop
/// </summary>
/// <param name="args"></param>
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);
}
/// <summary>
/// Trigger ModelDropped
/// </summary>
/// <param name="args"></param>
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();
}
/// <summary>
/// Handle the timer tick event, which is sent when the listview should
/// scroll
/// </summary>
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);
}
}
/// <summary>
/// When the mouse is at the given point, what should the target of the drop be?
/// </summary>
/// <remarks>This method should update the DropTarget* members of the given arg block</remarks>
/// <param name="args"></param>
/// <param name="pt">The mouse point, in client co-ordinates</param>
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;
}
/// <summary>
/// What sort of action is possible when the mouse is at the given point?
/// </summary>
/// <remarks>
/// </remarks>
/// <param name="args"></param>
/// <param name="pt"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Based solely on the state of the modifier keys, what drop operation should
/// be used?
/// </summary>
/// <returns>The drop operation that matches the state of the keys</returns>
public DragDropEffects CalculateStandardDropActionFromKeys() {
if (this.IsControlDown) {
if (this.IsShiftDown)
return DragDropEffects.Link;
else
return DragDropEffects.Copy;
} else {
return DragDropEffects.Move;
}
}
/// <summary>
/// Should the listview be made to scroll when the mouse is at the given point?
/// </summary>
/// <param name="pt"></param>
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();
}
}
}
/// <summary>
/// Update the state of our sink to reflect the information that
/// may have been written into the drop event args.
/// </summary>
/// <param name="args"></param>
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
/// <summary>
/// Draw the feedback that shows that the background is the target
/// </summary>
/// <param name="g"></param>
/// <param name="bounds"></param>
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);
}
}
}
/// <summary>
/// Draw the feedback that shows that an item (or a subitem) is the target
/// </summary>
/// <param name="g"></param>
/// <param name="bounds"></param>
/// <remarks>
/// DropTargetItem and DropTargetSubItemIndex tells what is the target
/// </remarks>
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);
}
}
}
/// <summary>
/// Draw the feedback that shows the drop will occur before target
/// </summary>
/// <param name="g"></param>
/// <param name="bounds"></param>
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);
}
/// <summary>
/// Draw the feedback that shows the drop will occur after target
/// </summary>
/// <param name="g"></param>
/// <param name="bounds"></param>
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);
}
/// <summary>
/// Return a GraphicPath that is round corner rectangle.
/// </summary>
/// <param name="rect"></param>
/// <param name="diameter"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Calculate the target rectangle when the given item (and possible subitem)
/// is the target of the drop.
/// </summary>
/// <param name="item"></param>
/// <param name="subItem"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Draw a "between items" line at the given co-ordinates
/// </summary>
/// <param name="g"></param>
/// <param name="x1"></param>
/// <param name="y1"></param>
/// <param name="x2"></param>
/// <param name="y2"></param>
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;
}
/// <summary>
/// This drop sink allows items within the same list to be rearranged,
/// as well as allowing items to be dropped from other lists.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// This class does not work with grouping.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
public class RearrangingDropSink : SimpleDropSink
{
/// <summary>
/// Create a RearrangingDropSink
/// </summary>
public RearrangingDropSink() {
this.CanDropBetween = true;
this.CanDropOnBackground = true;
this.CanDropOnItem = false;
}
/// <summary>
/// Create a RearrangingDropSink
/// </summary>
/// <param name="acceptDropsFromOtherLists"></param>
public RearrangingDropSink(bool acceptDropsFromOtherLists)
: this() {
this.AcceptExternal = acceptDropsFromOtherLists;
}
/// <summary>
/// Trigger OnModelCanDrop
/// </summary>
/// <param name="args"></param>
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;
}
}
/// <summary>
/// Trigger OnModelDropped
/// </summary>
/// <param name="args"></param>
protected override void OnModelDropped(ModelDropEventArgs args) {
base.OnModelDropped(args);
if (!args.Handled)
this.RearrangeModels(args);
}
/// <summary>
/// Do the work of processing the dropped items
/// </summary>
/// <param name="args"></param>
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);
}
}
}
/// <summary>
/// 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.
/// </summary>
public class OlvDropEventArgs : EventArgs
{
/// <summary>
/// Create a OlvDropEventArgs
/// </summary>
public OlvDropEventArgs() {
}
#region Data Properties
/// <summary>
/// Get the original drag-drop event args
/// </summary>
public DragEventArgs DragEventArgs
{
get { return this.dragEventArgs; }
internal set { this.dragEventArgs = value; }
}
private DragEventArgs dragEventArgs;
/// <summary>
/// Get the data object that is being dragged
/// </summary>
public object DataObject
{
get { return this.dataObject; }
internal set { this.dataObject = value; }
}
private object dataObject;
/// <summary>
/// Get the drop sink that originated this event
/// </summary>
public SimpleDropSink DropSink {
get { return this.dropSink; }
internal set { this.dropSink = value; }
}
private SimpleDropSink dropSink;
/// <summary>
/// Get or set the index of the item that is the target of the drop
/// </summary>
public int DropTargetIndex {
get { return dropTargetIndex; }
set { this.dropTargetIndex = value; }
}
private int dropTargetIndex = -1;
/// <summary>
/// Get or set the location of the target of the drop
/// </summary>
public DropTargetLocation DropTargetLocation {
get { return dropTargetLocation; }
set { this.dropTargetLocation = value; }
}
private DropTargetLocation dropTargetLocation;
/// <summary>
/// Get or set the index of the subitem that is the target of the drop
/// </summary>
public int DropTargetSubItemIndex {
get { return dropTargetSubItemIndex; }
set { this.dropTargetSubItemIndex = value; }
}
private int dropTargetSubItemIndex = -1;
/// <summary>
/// Get the item that is the target of the drop
/// </summary>
public OLVListItem DropTargetItem {
get {
return this.ListView.GetItem(this.DropTargetIndex);
}
set {
if (value == null)
this.DropTargetIndex = -1;
else
this.DropTargetIndex = value.Index;
}
}
/// <summary>
/// Get or set the drag effect that should be used for this operation
/// </summary>
public DragDropEffects Effect {
get { return this.effect; }
set { this.effect = value; }
}
private DragDropEffects effect;
/// <summary>
/// Get or set if this event was handled. No further processing will be done for a handled event.
/// </summary>
public bool Handled {
get { return this.handled; }
set { this.handled = value; }
}
private bool handled;
/// <summary>
/// Get or set the feedback message for this operation
/// </summary>
/// <remarks>
/// If this is not null, it will be displayed as a feedback message
/// during the drag.
/// </remarks>
public string InfoMessage {
get { return this.infoMessage; }
set { this.infoMessage = value; }
}
private string infoMessage;
/// <summary>
/// Get the ObjectListView that is being dropped on
/// </summary>
public ObjectListView ListView {
get { return this.listView; }
internal set { this.listView = value; }
}
private ObjectListView listView;
/// <summary>
/// Get the location of the mouse (in target ListView co-ords)
/// </summary>
public Point MouseLocation {
get { return this.mouseLocation; }
internal set { this.mouseLocation = value; }
}
private Point mouseLocation;
/// <summary>
/// Get the drop action indicated solely by the state of the modifier keys
/// </summary>
public DragDropEffects StandardDropActionFromKeys {
get {
return this.DropSink.CalculateStandardDropActionFromKeys();
}
}
#endregion
}
/// <summary>
/// These events are triggered when the drag source is an ObjectListView.
/// </summary>
public class ModelDropEventArgs : OlvDropEventArgs
{
/// <summary>
/// Create a ModelDropEventArgs
/// </summary>
public ModelDropEventArgs()
{
}
/// <summary>
/// Gets the model objects that are being dragged.
/// </summary>
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();
/// <summary>
/// Gets the ObjectListView that is the source of the dragged objects.
/// </summary>
public ObjectListView SourceListView {
get { return this.sourceListView; }
internal set { this.sourceListView = value; }
}
private ObjectListView sourceListView;
/// <summary>
/// Get the model object that is being dropped upon.
/// </summary>
/// <remarks>This is only value for TargetLocation == Item</remarks>
public object TargetModel {
get { return this.targetModel; }
internal set { this.targetModel = value; }
}
private object targetModel;
/// <summary>
/// Refresh all the objects involved in the operation
/// </summary>
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);
}
}
}