// Copyright (C) Josh Smith - January 2007
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Input;
using GeekDesk.Util;
using GeekDesk.Constant;
using HandyControl.Controls;
namespace GeekDesk.Util
{
#region ListBoxDragDropManager
///
/// Manages the dragging and dropping of ListBoxItems in a ListBox.
/// The ItemType type parameter indicates the type of the objects in
/// the ListBox's items source. The ListBox's ItemsSource must be
/// set to an instance of ObservableCollection of ItemType, or an
/// Exception will be thrown.
///
/// The type of the ListBox's items.
public class ListBoxDragDropManager where ItemType : class
{
#region Data
bool canInitiateDrag;
DragAdorner dragAdorner;
double dragAdornerOpacity;
int indexToSelect;
bool isDragInProgress;
ItemType itemUnderDragCursor;
ListBox listBox;
Point ptMouseDown;
bool showDragAdorner;
#endregion // Data
#region Constructors
///
/// Initializes a new instance of ListBoxDragManager.
///
public ListBoxDragDropManager()
{
this.canInitiateDrag = false;
this.dragAdornerOpacity = 0.7;
this.indexToSelect = -1;
this.showDragAdorner = true;
}
///
/// Initializes a new instance of ListBoxDragManager.
///
///
public ListBoxDragDropManager( ListBox listBox )
: this()
{
this.ListBox = listBox;
}
///
/// Initializes a new instance of ListBoxDragManager.
///
///
///
public ListBoxDragDropManager( ListBox listBox, double dragAdornerOpacity )
: this( listBox )
{
this.DragAdornerOpacity = dragAdornerOpacity;
}
///
/// Initializes a new instance of ListBoxDragManager.
///
///
///
public ListBoxDragDropManager( ListBox listBox, bool showDragAdorner )
: this( listBox )
{
this.ShowDragAdorner = showDragAdorner;
}
#endregion // Constructors
#region Public Interface
#region DragAdornerOpacity
///
/// Gets/sets the opacity of the drag adorner. This property has no
/// effect if ShowDragAdorner is false. The default value is 0.7
///
public double DragAdornerOpacity
{
get { return this.dragAdornerOpacity; }
set
{
if( this.IsDragInProgress )
throw new InvalidOperationException( "Cannot set the DragAdornerOpacity property during a drag operation." );
if( value < 0.0 || value > 1.0 )
throw new ArgumentOutOfRangeException( "DragAdornerOpacity", value, "Must be between 0 and 1." );
this.dragAdornerOpacity = value;
}
}
#endregion // DragAdornerOpacity
#region IsDragInProgress
///
/// Returns true if there is currently a drag operation being managed.
///
public bool IsDragInProgress
{
get { return this.isDragInProgress; }
private set { this.isDragInProgress = value; }
}
#endregion // IsDragInProgress
#region ListBox
///
/// Gets/sets the ListBox whose dragging is managed. This property
/// can be set to null, to prevent drag management from occuring. If
/// the ListBox's AllowDrop property is false, it will be set to true.
///
public ListBox ListBox
{
get { return listBox; }
set
{
if( this.IsDragInProgress )
throw new InvalidOperationException( "Cannot set the ListBox property during a drag operation." );
if( this.listBox != null )
{
#region Unhook Events
this.listBox.PreviewMouseLeftButtonDown -= LIstBox_PreviewMouseLeftButtonDown;
this.listBox.PreviewMouseMove -= LIstBox_PreviewMouseMove;
this.listBox.DragOver -= LIstBox_DragOver;
this.listBox.DragLeave -= LIstBox_DragLeave;
this.listBox.DragEnter -= LIstBox_DragEnter;
this.listBox.Drop -= LIstBox_Drop;
#endregion // Unhook Events
}
this.listBox = value;
if( this.listBox != null )
{
if( !this.listBox.AllowDrop )
this.listBox.AllowDrop = true;
#region Hook Events
this.listBox.PreviewMouseLeftButtonDown += LIstBox_PreviewMouseLeftButtonDown;
this.listBox.PreviewMouseMove += LIstBox_PreviewMouseMove;
this.listBox.DragOver += LIstBox_DragOver;
this.listBox.DragLeave += LIstBox_DragLeave;
this.listBox.DragEnter += LIstBox_DragEnter;
this.listBox.Drop += LIstBox_Drop;
#endregion // Hook Events
}
}
}
#endregion // ListBox
#region ProcessDrop [event]
///
/// Raised when a drop occurs. By default the dropped item will be moved
/// to the target index. Handle this event if relocating the dropped item
/// requires custom behavior. Note, if this event is handled the default
/// item dropping logic will not occur.
///
public event EventHandler> ProcessDrop;
#endregion // ProcessDrop [event]
#region ShowDragAdorner
///
/// Gets/sets whether a visual representation of the ListBoxItem being dragged
/// follows the mouse cursor during a drag operation. The default value is true.
///
public bool ShowDragAdorner
{
get { return this.showDragAdorner; }
set
{
if( this.IsDragInProgress )
throw new InvalidOperationException( "Cannot set the ShowDragAdorner property during a drag operation." );
this.showDragAdorner = value;
}
}
#endregion // ShowDragAdorner
#endregion // Public Interface
#region Event Handling Methods
#region LIstBox_PreviewMouseLeftButtonDown
void LIstBox_PreviewMouseLeftButtonDown( object sender, MouseButtonEventArgs e )
{
if( this.IsMouseOverScrollbar )
{
// 4/13/2007 - Set the flag to false when cursor is over scrollbar.
this.canInitiateDrag = false;
return;
}
int index = this.IndexUnderDragCursor;
this.canInitiateDrag = index > -1;
if( this.canInitiateDrag )
{
// Remember the location and index of the ListBoxItem the user clicked on for later.
this.ptMouseDown = MouseUtil.GetMousePosition();
//this.ptMouseDown = MouseUtil.GetMousePosition( this.listBox );
this.indexToSelect = index;
}
else
{
this.ptMouseDown = new Point( -10000, -10000 );
this.indexToSelect = -1;
}
}
#endregion // LIstBox_PreviewMouseLeftButtonDown
#region LIstBox_PreviewMouseMove
void LIstBox_PreviewMouseMove( object sender, MouseEventArgs e )
{
if( !this.CanStartDragOperation )
return;
// Select the item the user clicked on.
if( this.listBox.SelectedIndex != this.indexToSelect )
this.listBox.SelectedIndex = this.indexToSelect;
// If the item at the selected index is null, there's nothing
// we can do, so just return;
if( this.listBox.SelectedItem == null )
return;
ListBoxItem itemToDrag = this.GetListBoxItem( this.listBox.SelectedIndex );
if( itemToDrag == null )
return;
AdornerLayer adornerLayer = this.ShowDragAdornerResolved ? this.InitializeAdornerLayer( itemToDrag ) : null;
this.InitializeDragOperation( itemToDrag );
this.PerformDragOperation();
this.FinishDragOperation( itemToDrag, adornerLayer );
}
#endregion // LIstBox_PreviewMouseMove
#region LIstBox_DragOver
void LIstBox_DragOver( object sender, DragEventArgs e )
{
e.Effects = DragDropEffects.Move;
if ( this.ShowDragAdornerResolved )
this.UpdateDragAdornerLocation();
// Update the item which is known to be currently under the drag cursor.
int index = this.IndexUnderDragCursor;
this.ItemUnderDragCursor = index < 0 ? null : this.ListBox.Items[index] as ItemType;
}
#endregion // LIstBox_DragOver
#region LIstBox_DragLeave
void LIstBox_DragLeave( object sender, DragEventArgs e )
{
if( !this.IsMouseOver( this.listBox ) )
{
if( this.ItemUnderDragCursor != null )
this.ItemUnderDragCursor = null;
if( this.dragAdorner != null )
this.dragAdorner.Visibility = Visibility.Collapsed;
}
}
#endregion // LIstBox_DragLeave
#region LIstBox_DragEnter
void LIstBox_DragEnter( object sender, DragEventArgs e )
{
if( this.dragAdorner != null && this.dragAdorner.Visibility != Visibility.Visible )
{
// Update the location of the adorner and then show it.
this.UpdateDragAdornerLocation();
this.dragAdorner.Visibility = Visibility.Visible;
}
}
#endregion // LIstBox_DragEnter
#region LIstBox_Drop
void LIstBox_Drop( object sender, DragEventArgs e )
{
if( this.ItemUnderDragCursor != null )
this.ItemUnderDragCursor = null;
e.Effects = DragDropEffects.None;
if( !e.Data.GetDataPresent( typeof( ItemType ) ) )
return;
// Get the data object which was dropped.
ItemType data = e.Data.GetData( typeof( ItemType ) ) as ItemType;
if( data == null )
return;
// Get the ObservableCollection which contains the dropped data object.
ObservableCollection itemsSource = this.listBox.ItemsSource as ObservableCollection;
if( itemsSource == null )
throw new Exception(
"A ListBox managed by ListBoxDragManager must have its ItemsSource set to an ObservableCollection." );
int oldIndex = itemsSource.IndexOf( data );
int newIndex = this.IndexUnderDragCursor;
if( newIndex < 0 )
{
// The drag started somewhere else, and our ListBox is empty
// so make the new item the first in the list.
if( itemsSource.Count == 0 )
newIndex = 0;
// The drag started somewhere else, but our ListBox has items
// so make the new item the last in the list.
else if( oldIndex < 0 )
newIndex = itemsSource.Count;
// The user is trying to drop an item from our ListBox into
// our ListBox, but the mouse is not over an item, so don't
// let them drop it.
else
return;
}
// Dropping an item back onto itself is not considered an actual 'drop'.
if( oldIndex == newIndex )
return;
if( this.ProcessDrop != null )
{
// Let the client code process the drop.
ProcessDropEventArgs args = new ProcessDropEventArgs( itemsSource, data, oldIndex, newIndex, e.AllowedEffects );
this.ProcessDrop( this, args );
e.Effects = args.Effects;
}
else
{
// Move the dragged data object from it's original index to the
// new index (according to where the mouse cursor is). If it was
// not previously in the ListBox, then insert the item.
if( oldIndex > -1 )
{
// 外部业务代码
itemsSource.Move(oldIndex, newIndex);
if (MainWindow.appData.AppConfig.IconSortType != SortType.CUSTOM)
{
MainWindow.appData.AppConfig.IconSortType = SortType.CUSTOM;
if (MainWindow.mainWindow.Visibility == Visibility.Collapsed
|| MainWindow.mainWindow.Opacity != 1)
{
Growl.WarningGlobal("已将图标排序规则重置为自定义!");
}
else
{
Growl.Warning("已将图标排序规则重置为自定义!", "MainWindowGrowl");
}
}
}
else
{
itemsSource.Insert(newIndex, data);
}
// Set the Effects property so that the call to DoDragDrop will return 'Move'.
e.Effects = DragDropEffects.Move;
}
}
#endregion // LIstBox_Drop
#endregion // Event Handling Methods
#region Private Helpers
#region CanStartDragOperation
bool CanStartDragOperation
{
get
{
if( Mouse.LeftButton != MouseButtonState.Pressed )
return false;
if( !this.canInitiateDrag )
return false;
if( this.indexToSelect == -1 )
return false;
if( !this.HasCursorLeftDragThreshold )
return false;
return true;
}
}
#endregion // CanStartDragOperation
#region FinishDragOperation
void FinishDragOperation( ListBoxItem draggedItem, AdornerLayer adornerLayer )
{
// Let the ListBoxItem know that it is not being dragged anymore.
ListBoxItemDragState.SetIsBeingDragged( draggedItem, false );
this.IsDragInProgress = false;
if( this.ItemUnderDragCursor != null )
this.ItemUnderDragCursor = null;
// Remove the drag adorner from the adorner layer.
if( adornerLayer != null )
{
adornerLayer.Remove( this.dragAdorner );
this.dragAdorner = null;
}
}
#endregion // FinishDragOperation
#region GetListBoxItem
ListBoxItem GetListBoxItem( int index )
{
if( this.listBox.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated )
return null;
return this.listBox.ItemContainerGenerator.ContainerFromIndex( index ) as ListBoxItem;
}
ListBoxItem GetListBoxItem( ItemType dataItem )
{
if( this.listBox.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated )
return null;
return this.listBox.ItemContainerGenerator.ContainerFromItem( dataItem ) as ListBoxItem;
}
#endregion // GetListBoxItem
#region HasCursorLeftDragThreshold
bool HasCursorLeftDragThreshold
{
get
{
if( this.indexToSelect < 0 )
return false;
ListBoxItem item = this.GetListBoxItem( this.indexToSelect );
if (item == null) return false;
Rect bounds = VisualTreeHelper.GetDescendantBounds( item );
Point ptInItem = this.listBox.TranslatePoint( this.ptMouseDown, item );
// In case the cursor is at the very top or bottom of the ListBoxItem
// we want to make the vertical threshold very small so that dragging
// over an adjacent item does not select it.
double topOffset = Math.Abs( ptInItem.Y );
double btmOffset = Math.Abs( bounds.Height - ptInItem.Y );
double vertOffset = Math.Min( topOffset, btmOffset );
double width = SystemParameters.MinimumHorizontalDragDistance * 2;
double height = Math.Min( SystemParameters.MinimumVerticalDragDistance, vertOffset ) * 2;
Size szThreshold = new Size( width, height );
Rect rect = new Rect( this.ptMouseDown, szThreshold );
rect.Offset( szThreshold.Width / -2, szThreshold.Height / -2 );
//Point ptInListBox = MouseUtil.GetMousePosition(this.listBox);
Point ptInListBox = MouseUtil.GetMousePosition();
return !rect.Contains( ptInListBox );
}
}
#endregion // HasCursorLeftDragThreshold
#region IndexUnderDragCursor
///
/// Returns the index of the ListBoxItem underneath the
/// drag cursor, or -1 if the cursor is not over an item.
///
int IndexUnderDragCursor
{
get
{
int index = -1;
for( int i = 0; i < this.listBox.Items.Count; ++i )
{
ListBoxItem item = this.GetListBoxItem( i );
if( this.IsMouseOver( item ) )
{
index = i;
break;
}
}
return index;
}
}
#endregion // IndexUnderDragCursor
#region InitializeAdornerLayer
AdornerLayer InitializeAdornerLayer( ListBoxItem itemToDrag )
{
// Create a brush which will paint the ListBoxItem onto
// a visual in the adorner layer.
VisualBrush brush = new VisualBrush( itemToDrag );
// Create an element which displays the source item while it is dragged.
this.dragAdorner = new DragAdorner( this.listBox, itemToDrag.RenderSize, brush );
// Set the drag adorner's opacity.
this.dragAdorner.Opacity = this.DragAdornerOpacity;
AdornerLayer layer = AdornerLayer.GetAdornerLayer( this.listBox );
layer.Add( dragAdorner );
// Save the location of the cursor when the left mouse button was pressed.
this.ptMouseDown = MouseUtil.GetMousePosition(this.listBox);
//this.ptMouseDown = MouseUtil.GetMousePosition();
return layer;
}
#endregion // InitializeAdornerLayer
#region InitializeDragOperation
void InitializeDragOperation( ListBoxItem itemToDrag )
{
// Set some flags used during the drag operation.
this.IsDragInProgress = true;
this.canInitiateDrag = false;
// Let the ListBoxItem know that it is being dragged.
ListBoxItemDragState.SetIsBeingDragged( itemToDrag, true );
}
#endregion // InitializeDragOperation
#region IsMouseOver
bool IsMouseOver( Visual target )
{
// We need to use MouseUtil to figure out the cursor
// coordinates because, during a drag-drop operation, the WPF
// mechanisms for getting the coordinates behave strangely.
Rect bounds = VisualTreeHelper.GetDescendantBounds( target );
Point mousePos = MouseUtil.GetMousePosition( target );
return bounds.Contains( mousePos );
}
#endregion // IsMouseOver
#region IsMouseOverScrollbar
///
/// Returns true if the mouse cursor is over a scrollbar in the ListBox.
///
bool IsMouseOverScrollbar
{
get
{
Point ptMouse = MouseUtil.GetMousePosition( this.listBox );
HitTestResult res = VisualTreeHelper.HitTest( this.listBox, ptMouse );
if( res == null )
return false;
DependencyObject depObj = res.VisualHit;
while( depObj != null )
{
if( depObj is ScrollBar )
return true;
// VisualTreeHelper works with objects of type Visual or Visual3D.
// If the current object is not derived from Visual or Visual3D,
// then use the LogicalTreeHelper to find the parent element.
if( depObj is Visual || depObj is System.Windows.Media.Media3D.Visual3D )
depObj = VisualTreeHelper.GetParent( depObj );
else
depObj = LogicalTreeHelper.GetParent( depObj );
}
return false;
}
}
#endregion // IsMouseOverScrollbar
#region ItemUnderDragCursor
ItemType ItemUnderDragCursor
{
get { return this.itemUnderDragCursor; }
set
{
if( this.itemUnderDragCursor == value )
return;
// The first pass handles the previous item under the cursor.
// The second pass handles the new one.
for( int i = 0; i < 2; ++i )
{
if( i == 1 )
this.itemUnderDragCursor = value;
if( this.itemUnderDragCursor != null )
{
ListBoxItem listBoxItem = this.GetListBoxItem( this.itemUnderDragCursor );
if( listBoxItem != null )
ListBoxItemDragState.SetIsUnderDragCursor( listBoxItem, i == 1 );
}
}
}
}
#endregion // ItemUnderDragCursor
#region PerformDragOperation
void PerformDragOperation()
{
ItemType selectedItem = this.listBox.SelectedItem as ItemType;
DragDropEffects allowedEffects = DragDropEffects.Move | DragDropEffects.Move | DragDropEffects.Link;
if( DragDrop.DoDragDrop( this.listBox, selectedItem, allowedEffects ) != DragDropEffects.None )
{
// The item was dropped into a new location,
// so make it the new selected item.
this.listBox.SelectedItem = selectedItem;
}
}
#endregion // PerformDragOperation
#region ShowDragAdornerResolved
bool ShowDragAdornerResolved
{
get { return this.ShowDragAdorner && this.DragAdornerOpacity > 0.0; }
}
#endregion // ShowDragAdornerResolved
#region UpdateDragAdornerLocation
void UpdateDragAdornerLocation()
{
if( this.dragAdorner != null )
{
//Point ptCursor = MouseUtil.GetMousePosition(this.ListBox);
Point ptCursor = MouseUtil.GetMousePosition(this.ListBox);
//double left = ptCursor.X - this.ptMouseDown.X;
//double left2 = ptCursor.X;
ListBoxItem itemBeingDragged = this.GetListBoxItem(this.indexToSelect);
// 4/13/2007 - Made the top offset relative to the item being dragged.
Point itemLoc = itemBeingDragged.TranslatePoint( new Point( 0, 0 ), this.ListBox );
double top = itemLoc.Y + ptCursor.Y - this.ptMouseDown.Y;
double left = itemLoc.X + ptCursor.X - this.ptMouseDown.X;
this.dragAdorner.SetOffsets(left, top );
}
}
#endregion // UpdateDragAdornerLocation
#endregion // Private Helpers
}
#endregion // ListBoxDragDropManager
#region ListBoxItemDragState
///
/// Exposes attached properties used in conjunction with the ListBoxDragDropManager class.
/// Those properties can be used to allow triggers to modify the appearance of ListBoxItems
/// in a ListBox during a drag-drop operation.
///
public static class ListBoxItemDragState
{
#region IsBeingDragged
///
/// Identifies the ListBoxItemDragState's IsBeingDragged attached property.
/// This field is read-only.
///
public static readonly DependencyProperty IsBeingDraggedProperty =
DependencyProperty.RegisterAttached(
"IsBeingDragged",
typeof( bool ),
typeof( ListBoxItemDragState ),
new UIPropertyMetadata( false ) );
///
/// Returns true if the specified ListBoxItem is being dragged, else false.
///
/// The ListBoxItem to check.
public static bool GetIsBeingDragged( ListBoxItem item )
{
return (bool)item.GetValue( IsBeingDraggedProperty );
}
///
/// Sets the IsBeingDragged attached property for the specified ListBoxItem.
///
/// The ListBoxItem to set the property on.
/// Pass true if the element is being dragged, else false.
internal static void SetIsBeingDragged( ListBoxItem item, bool value )
{
item.SetValue( IsBeingDraggedProperty, value );
}
#endregion // IsBeingDragged
#region IsUnderDragCursor
///
/// Identifies the ListBoxItemDragState's IsUnderDragCursor attached property.
/// This field is read-only.
///
public static readonly DependencyProperty IsUnderDragCursorProperty =
DependencyProperty.RegisterAttached(
"IsUnderDragCursor",
typeof( bool ),
typeof( ListBoxItemDragState ),
new UIPropertyMetadata( false ) );
///
/// Returns true if the specified ListBoxItem is currently underneath the cursor
/// during a drag-drop operation, else false.
///
/// The ListBoxItem to check.
public static bool GetIsUnderDragCursor( ListBoxItem item )
{
return (bool)item.GetValue( IsUnderDragCursorProperty );
}
///
/// Sets the IsUnderDragCursor attached property for the specified ListBoxItem.
///
/// The ListBoxItem to set the property on.
/// Pass true if the element is underneath the drag cursor, else false.
internal static void SetIsUnderDragCursor( ListBoxItem item, bool value )
{
item.SetValue( IsUnderDragCursorProperty, value );
}
#endregion // IsUnderDragCursor
}
#endregion // ListBoxItemDragState
#region ProcessDropEventArgs
///
/// Event arguments used by the ListBoxDragDropManager.ProcessDrop event.
///
/// The type of data object being dropped.
public class ProcessDropEventArgs : EventArgs where ItemType : class
{
#region Data
ObservableCollection itemsSource;
ItemType dataItem;
int oldIndex;
int newIndex;
DragDropEffects allowedEffects = DragDropEffects.None;
DragDropEffects effects = DragDropEffects.None;
#endregion // Data
#region Constructor
internal ProcessDropEventArgs(
ObservableCollection itemsSource,
ItemType dataItem,
int oldIndex,
int newIndex,
DragDropEffects allowedEffects )
{
this.itemsSource = itemsSource;
this.dataItem = dataItem;
this.oldIndex = oldIndex;
this.newIndex = newIndex;
this.allowedEffects = allowedEffects;
}
#endregion // Constructor
#region Public Properties
///
/// The items source of the ListBox where the drop occurred.
///
public ObservableCollection ItemsSource
{
get { return this.itemsSource; }
}
///
/// The data object which was dropped.
///
public ItemType DataItem
{
get { return this.dataItem; }
}
///
/// The current index of the data item being dropped, in the ItemsSource collection.
///
public int OldIndex
{
get { return this.oldIndex; }
}
///
/// The target index of the data item being dropped, in the ItemsSource collection.
///
public int NewIndex
{
get { return this.newIndex; }
}
///
/// The drag drop effects allowed to be performed.
///
public DragDropEffects AllowedEffects
{
get { return allowedEffects; }
}
///
/// The drag drop effect(s) performed on the dropped item.
///
public DragDropEffects Effects
{
get { return effects; }
set { effects = value; }
}
#endregion // Public Properties
}
#endregion // ProcessDropEventArgs
}