using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
namespace Oli.Controls
{
///
/// ListBox which provides a fully integrated drag-and-drop functionality, which also works when multiple ListBox items are selected (multiselect).
/// Items can be moved and copied between different DragDropListBoxes or can be moved inside of one DragDropListBox in order to be reordered.
/// Drag-and-drop works also with or types of controls implementing IDragDropSource.
///
/// Provides additional properties for the fine-tuning of the drag-and-drop behavior in the section
/// "Behavior (drag-and-drop)" of the properties window of the form designer.
public class DragDropListBox : ListBox, IDragDropSource
{
private Rectangle _dragOriginBox = Rectangle.Empty;
private VisualCue _visualCue;
private int[] _selectionSave = new int[0];
private bool _restoringSelection = false;
public DragDropListBox()
: base()
{
AllowDrop = true;
_visualCue = new VisualCue(this);
DragEventArgsConverters = new List();
}
#region IDragDropSource Members
private string _dragDropGroup = "";
///
/// Drag-and-drop group to which the control belongs. Drag-and-drop is restricted to happen between controls having the same DragDropGroup.
///
/// Let's assume that we have a form with four DragDropListBoxes on it. Two of them contain cats and two of them contain dogs.
/// We only want to be able to move cats between the cat lists and dogs between the dog lists (cats and dogs don't like each other).
/// We can achieve this simply by setting the DragDropGroup property of the cat lists to "cats". In the dog lists we can leave the
/// DragDropGroup empty or we can set it to "dogs" for instance. It just has to be different from the DragDropGroup in the cat lists.
/// catList1.DragDropGroup = "cats"; catList2.DragDropGroup = "cats";
///
[Category("Behavior (drag-and-drop)"), DefaultValue(""), Description("Drag-and-drop group to which the control belongs. Drag-and-drop is restricted to happen between controls having the same DragDropGroup.")]
public string DragDropGroup
{
get { return _dragDropGroup; }
set { _dragDropGroup = value; }
}
private bool _isDragDropCopySource = true;
///
/// Indicates whether the user can copy items from this control by draging them to another control.
///
[Category("Behavior (drag-and-drop)"), DefaultValue(true), Description("Indicates whether the user can copy items from this control by draging them to another control.")]
public bool IsDragDropCopySource
{
get { return _isDragDropCopySource; }
set { _isDragDropCopySource = value; }
}
private bool _isDragDropMoveSource = true;
///
/// Indicates whether the user can remove items from this control by draging them to another control.
///
[Category("Behavior (drag-and-drop)"), DefaultValue(true), Description("Indicates whether the user can remove items from this control by draging them to another control.")]
public bool IsDragDropMoveSource
{
get { return _isDragDropMoveSource; }
set { _isDragDropMoveSource = value; }
}
///
/// Returns the selected list items in a array.
///
/// Array with the selected items.
public object[] GetSelectedItems()
{
object[] items = new object[SelectedItems.Count];
SelectedItems.CopyTo(items, 0);
return items;
}
///
/// Removes the selected items from the list and adjusts the item-index passed to this method,
/// so that this index points to the same item afterwards.
///
/// Item-index to be adjusted.
public void RemoveSelectedItems(ref int itemIndexToAjust)
{
for (int i = SelectedIndices.Count - 1; i >= 0; i--) {
int at = SelectedIndices[i];
Items.RemoveAt(at);
if (at < itemIndexToAjust) {
itemIndexToAjust--; // Adjust index pointing to stuff behind the delete position.
}
}
}
///
/// Is called when a drag-and-drop operation is completed in order to raise the Dropped event.
///
/// Event arguments which hold information on the completed operation.
/// Is called for the target as well as for the source.
/// The role a control plays (source or target) can be determined from e.Operation.
public virtual void OnDropped(DroppedEventArgs e)
{
var dropEvent = Dropped;
if (dropEvent != null) {
dropEvent(this, e);
}
}
#endregion
#region Other Public Properties
private bool _allowReorder = true;
///
/// Indicates whether the user can redorder the list by dragging items.
///
[Category("Behavior (drag-and-drop)"), DefaultValue(true), Description("Indicates whether the user can redorder the list by dragging items.")]
public bool AllowReorder
{
get { return _allowReorder; }
set
{
_allowReorder = value;
base.AllowDrop = _isDragDropTarget || _allowReorder;
}
}
private bool _isDragDropTarget = true;
///
/// Indicates whether the user can drop items from another control.
///
[Category("Behavior (drag-and-drop)"), DefaultValue(true), Description("Indicates whether the user can drop items from another control.")]
public bool IsDragDropTarget
{
get { return _isDragDropTarget; }
set
{
_isDragDropTarget = value;
base.AllowDrop = _isDragDropTarget || _allowReorder;
}
}
///
/// Occurs when a extended DragDropListBox drag-and-drop operation is completed.
///
/// This event is raised for the target as well as for the source.
/// The role a control plays (source or target) can be determined from the Operation property of the DroppedEventArgs.
[Category("Drag Drop"), Description("Occurs when a extended DragDropListBox drag-and-drop operation is completed.")]
public event EventHandler Dropped;
public List DragEventArgsConverters { get; private set; }
#endregion
#region Overridden Event Methods
protected override void OnDragDrop(DragEventArgs drgevent)
{
base.OnDragDrop(drgevent);
_visualCue.Clear();
// Retrieve the drag item data.
// Conditions have been testet in OnDragEnter and OnDragOver, so everything should be ok here.
IDragDropSource src = ConvertDragEventArgs(drgevent);
object[] srcItems = src.GetSelectedItems();
// If the list box is sorted, we don't know where the items will be inserted
// and we will have troubles selecting the inserted items. So let's disable sorting here.
bool sortedSave = Sorted;
Sorted = false;
// Insert at the currently hovered row.
int row = DropIndex(drgevent.Y);
int insertPoint = row;
if (row >= Items.Count) { // Append items to the end.
Items.AddRange(srcItems);
} else { // Insert items before row.
foreach (object item in srcItems) {
Items.Insert(row++, item);
}
}
// Remove all the selected items from the source, if moving.
DropOperation operation; // Remembers the operation for the event we'll raise.
if (drgevent.Effect == DragDropEffects.Move) {
int adjustedInsertPoint = insertPoint;
src.RemoveSelectedItems(ref adjustedInsertPoint);
if (src == this) { // Items are being reordered.
insertPoint = adjustedInsertPoint;
operation = DropOperation.Reorder;
} else {
operation = DropOperation.MoveToHere;
}
} else {
operation = DropOperation.CopyToHere;
}
// Adjust the selected items in the target.
ClearSelected();
if (SelectionMode == SelectionMode.One) { // Select the first item inserted.
SelectedIndex = insertPoint;
} else if (SelectionMode != SelectionMode.None) { // Select the inserted items.
for (int i = insertPoint; i < insertPoint + srcItems.Length; i++) {
SetSelected(i, true);
}
}
// Now that we've selected the inserted items, restore the "Sorted" property.
Sorted = sortedSave;
// Notify the target (this control).
DroppedEventArgs e = new DroppedEventArgs() {
Operation = operation,
Source = src,
Target = this,
DroppedItems = srcItems
};
OnDropped(e);
// Notify the source (the other control).
if (operation != DropOperation.Reorder) {
e = new DroppedEventArgs() {
Operation = operation == DropOperation.MoveToHere ? DropOperation.MoveFromHere : DropOperation.CopyFromHere,
Source = src,
Target = this,
DroppedItems = srcItems
};
src.OnDropped(e);
}
}
protected override void OnDragOver(DragEventArgs drgevent)
{
base.OnDragOver(drgevent);
drgevent.Effect = GetDragDropEffect(drgevent);
if (drgevent.Effect == DragDropEffects.None) {
return;
}
// Everything is fine, give a visual cue
int dropIndex = DropIndex(drgevent.Y);
if (dropIndex != _visualCue.Index) {
_visualCue.Clear();
_visualCue.Draw(dropIndex);
}
}
protected override void OnDragEnter(DragEventArgs drgevent)
{
base.OnDragEnter(drgevent);
drgevent.Effect = GetDragDropEffect(drgevent);
}
protected override void OnDragLeave(EventArgs e)
{
base.OnDragLeave(e);
_visualCue.Clear();
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
int clickedItemIndex = IndexFromPoint(e.Location);
if (clickedItemIndex >= 0 && MouseButtons == MouseButtons.Left &&
(_isDragDropCopySource || _isDragDropMoveSource || _allowReorder) &&
(GetSelected(clickedItemIndex) || Control.ModifierKeys == Keys.Shift)) {
RestoreSelection(clickedItemIndex);
// Remember start position of possible drag operation.
Size dragSize = SystemInformation.DragSize; // Size that the mouse must move before a drag operation starts.
_dragOriginBox = new Rectangle(new Point(e.X - (dragSize.Width / 2), e.Y - (dragSize.Height / 2)), dragSize);
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
_dragOriginBox = Rectangle.Empty; // Reset drag drop.
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (_dragOriginBox != Rectangle.Empty && !_dragOriginBox.Contains(e.X, e.Y)) { // Initiate drag-and-drop
DoDragDrop(new DataObject("IDragDropSource", this), DragDropEffects.All);
_dragOriginBox = Rectangle.Empty;
}
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
base.OnSelectedIndexChanged(e);
SaveSelection();
}
#endregion
#region private Helper methods
///
/// Restores the lost selection from the array selectionSave.
///
/// If the user clicks into the selection, he might want to drag the selected items,
/// but the mouse click destroys the current selection, if more than one item is selected.
/// Current mouse location.
private void RestoreSelection(int clickedItemIndex)
{
// Restore the selection, unless modifier keys are pressed, which indicates that the user is currently editing the selection.
// The item the user clickes at must have been selected before the click (_selectionSave stores the state before the click).
if (SelectionMode == SelectionMode.MultiExtended && Control.ModifierKeys == Keys.None && Array.IndexOf(_selectionSave, clickedItemIndex) >= 0) {
_restoringSelection = true; // Disable saving the selection while it is restored. (SetSelected raises the SelectedIndexChanged
// event, where we call SaveSelection.)
foreach (int i in _selectionSave) {
SetSelected(i, true);
}
// Select the item that was clicked, in order to make it the current item. This also fixes a bug, where the listbox
// selects too many items, if the list is clicked after items have been selected programmatically.
SetSelected(clickedItemIndex, true);
_restoringSelection = false;
}
}
///
/// Saves the current selection to the array selectionSave.
///
private void SaveSelection()
{
if (!_restoringSelection && SelectionMode == SelectionMode.MultiExtended) {
SelectedIndexCollection sel = SelectedIndices;
if (_selectionSave.Length != sel.Count) {
_selectionSave = new int[sel.Count];
}
SelectedIndices.CopyTo(_selectionSave, 0);
}
}
///
/// Gets the index of the item before which items are being dropped. The index is calculated from the vertical position of the mouse.
/// If the drop position lies after the last item in the list, then the index of the last item + 1 (which is equal to Item.Count) is returned instead.
///
/// y-coordinate of the mouse expressed in screen coordinates.
/// Index of an item in the list or Items.Count
private int DropIndex(int yScreen)
{
// The DragEventArgs gives us screen coordinates. Convert the screen coordinates to client coordinates.
int y = PointToClient(new Point(0, yScreen)).Y;
// Make sure we are inside of the client rectangle.
// If we are on the border of the ListBox, then IndexFromPoint does not return a match.
if (y < 0) {
y = 0;
} else if (y > ClientRectangle.Bottom - 1) {
y = ClientRectangle.Bottom - 1;
}
int index = IndexFromPoint(0, y); // The x-coordinate doesn't make any difference.
if (index == ListBox.NoMatches) { // Not hovering over an item
return Items.Count; // Append to the end of the list.
}
// If hovering below the middle of the item, then insert after the item.
Rectangle rect = GetItemRectangle(index);
if (y > rect.Top + rect.Height / 2) {
index++;
}
int lastFullyVisibleItemIndex = TopIndex + ClientRectangle.Height / ItemHeight;
if (index > lastFullyVisibleItemIndex) { // Do not insert after the last fully visible item
return lastFullyVisibleItemIndex;
}
return index;
}
///
/// Determines the drag-and-drop operation which is beeing performed, which can be either None, Move or Copy.
///
/// DragEventArgs.
/// The current drag-and-drop operation.
private DragDropEffects GetDragDropEffect(DragEventArgs drgevent)
{
const int CtrlKeyPlusLeftMouseButton = 9; // KeyState.
DragDropEffects effect = DragDropEffects.None;
// Retrieve the source control of the drag-and-drop operation.
IDragDropSource src = ConvertDragEventArgs(drgevent);
if (src != null && _dragDropGroup == src.DragDropGroup) { // The stuff being draged is compatible.
if (src == this) { // Drag-and-drop happens within this control.
if (_allowReorder && !this.Sorted) {
effect = DragDropEffects.Move;
}
} else if (_isDragDropTarget) {
// If only Copy is allowed then copy. If Copy and Move are allowed, then Move, unless the Ctrl-key is pressed.
if (src.IsDragDropCopySource && (!src.IsDragDropMoveSource || drgevent.KeyState == CtrlKeyPlusLeftMouseButton)) {
effect = DragDropEffects.Copy;
} else if (src.IsDragDropMoveSource) {
effect = DragDropEffects.Move;
}
}
}
return effect;
}
///
/// Extracts IDragDropSource data from DragEventArgs. If the DragEventArgs do not implement IDragDropSource
/// then an attempt is made to convert them using available DragEventArgsConverters.
///
/// The DragEventArgs to convert.
/// A IDragDropSource or null if the DragEventArgs could not be converted.
/// Add DragEventArgsConverters to a DragDropListBox like this:
/// myDragDropListBox.DragEventArgsConverters.Add(new MyDropDragEventArgsConverter());
///
private IDragDropSource ConvertDragEventArgs(DragEventArgs drgevent)
{
IDragDropSource src = drgevent.Data.GetData("IDragDropSource") as IDragDropSource;
// If the source was not a IDragDropSource then try to convert it to a IDragDropSource
if (src == null && DragEventArgsConverters != null) {
foreach (IDragEventArgsConverter converter in DragEventArgsConverters) {
src = converter.Convert(drgevent);
if (src != null) {
break;
}
}
}
return src;
}
#endregion
}
}