anx.framework/Visual Studio/MPF11/Dev11/Src/CSharp/ProjectNode.CopyPaste.cs

1636 lines
78 KiB
C#
Raw Normal View History

/* ****************************************************************************
*
* Copyright (c) Microsoft Corporation.
*
* This source code is subject to terms and conditions of the Apache License, Version 2.0. A
* copy of the license can be found in the License.html file at the root of this distribution. If
* you cannot locate the Apache License, Version 2.0, please send an email to
* vspython@microsoft.com. By using this source code in any fashion, you are agreeing to be bound
* by the terms of the Apache License, Version 2.0.
*
* You must not remove this notice, or any other, from this software.
*
* ***************************************************************************/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Text;
using System.Windows;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using IOleDataObject = Microsoft.VisualStudio.OLE.Interop.IDataObject;
using OleConstants = Microsoft.VisualStudio.OLE.Interop.Constants;
namespace Microsoft.VisualStudio.Project {
/// <summary>
/// Manages the CopyPaste and Drag and Drop scenarios for a Project.
/// </summary>
/// <remarks>This is a partial class.</remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")]
public partial class ProjectNode : IVsUIHierWinClipboardHelperEvents {
private uint copyPasteCookie;
private DropDataType _dropType;
#region override of IVsHierarchyDropDataTarget methods
/// <summary>
/// Called as soon as the mouse drags an item over a new hierarchy or hierarchy window
/// </summary>
/// <param name="pDataObject">reference to interface IDataObject of the item being dragged</param>
/// <param name="grfKeyState">Current state of the keyboard and the mouse modifier keys. See docs for a list of possible values</param>
/// <param name="itemid">Item identifier for the item currently being dragged</param>
/// <param name="pdwEffect">On entry, a pointer to the current DropEffect. On return, must contain the new valid DropEffect</param>
/// <returns>If the method succeeds, it returns S_OK. If it fails, it returns an error code.</returns>
public int DragEnter(IOleDataObject pDataObject, uint grfKeyState, uint itemid, ref uint pdwEffect) {
pdwEffect = (uint)DropEffect.None;
var item = NodeFromItemId(itemid);
if (item.GetDragTargetHandlerNode().CanAddFiles) {
_dropType = QueryDropDataType(pDataObject);
if (_dropType != DropDataType.None) {
pdwEffect = (uint)QueryDropEffect(grfKeyState);
}
}
return VSConstants.S_OK;
}
/// <summary>
/// Called when one or more items are dragged out of the hierarchy or hierarchy window, or when the drag-and-drop operation is cancelled or completed.
/// </summary>
/// <returns>If the method succeeds, it returns S_OK. If it fails, it returns an error code.</returns>
public int DragLeave() {
_dropType = DropDataType.None;
return VSConstants.S_OK;
}
/// <summary>
/// Called when one or more items are dragged over the target hierarchy or hierarchy window.
/// </summary>
/// <param name="grfKeyState">Current state of the keyboard keys and the mouse modifier buttons. See <seealso cref="IVsHierarchyDropDataTarget"/></param>
/// <param name="itemid">Item identifier of the drop data target over which the item is being dragged</param>
/// <param name="pdwEffect"> On entry, reference to the value of the pdwEffect parameter of the IVsHierarchy object, identifying all effects that the hierarchy supports.
/// On return, the pdwEffect parameter must contain one of the effect flags that indicate the result of the drop operation. For a list of pwdEffects values, see <seealso cref="DragEnter"/></param>
/// <returns>If the method succeeds, it returns S_OK. If it fails, it returns an error code.</returns>
public int DragOver(uint grfKeyState, uint itemid, ref uint pdwEffect) {
pdwEffect = (uint)DropEffect.None;
// Dragging items to a project that is being debugged is not supported
// (see VSWhidbey 144785)
DBGMODE dbgMode = VsShellUtilities.GetDebugMode(this.Site) & ~DBGMODE.DBGMODE_EncMask;
if (dbgMode == DBGMODE.DBGMODE_Run || dbgMode == DBGMODE.DBGMODE_Break) {
return VSConstants.S_OK;
}
if (this.isClosed || this.site == null) {
return VSConstants.E_UNEXPECTED;
}
// TODO: We should also analyze if the node being dragged over can accept the drop.
pdwEffect = (uint)QueryDropEffect(grfKeyState);
return VSConstants.S_OK;
}
/// <summary>
/// Called when one or more items are dropped into the target hierarchy or hierarchy window when the mouse button is released.
/// </summary>
/// <param name="pDataObject">Reference to the IDataObject interface on the item being dragged. This data object contains the data being transferred in the drag-and-drop operation.
/// If the drop occurs, then this data object (item) is incorporated into the target hierarchy or hierarchy window.</param>
/// <param name="grfKeyState">Current state of the keyboard and the mouse modifier keys. See <seealso cref="IVsHierarchyDropDataTarget"/></param>
/// <param name="itemid">Item identifier of the drop data target over which the item is being dragged</param>
/// <param name="pdwEffect">Visual effects associated with the drag-and drop-operation, such as a cursor, bitmap, and so on.
/// The value of dwEffects passed to the source object via the OnDropNotify method is the value of pdwEffects returned by the Drop method</param>
/// <returns>If the method succeeds, it returns S_OK. If it fails, it returns an error code. </returns>
public int Drop(IOleDataObject pDataObject, uint grfKeyState, uint itemid, ref uint pdwEffect) {
if (pDataObject == null) {
return VSConstants.E_INVALIDARG;
}
pdwEffect = (uint)DropEffect.None;
// Get the node that is being dragged over and ask it which node should handle this call
HierarchyNode targetNode = NodeFromItemId(itemid);
if (targetNode == null) {
// There is no target node. The drop can not be completed.
return VSConstants.S_FALSE;
}
int returnValue;
try {
DropDataType dropDataType = DropDataType.None;
pdwEffect = (uint)QueryDropEffect(grfKeyState);
dropDataType = ProcessSelectionDataObject(pDataObject, targetNode, true, (DropEffect)pdwEffect);
if (dropDataType == DropDataType.None) {
pdwEffect = (uint)DropEffect.None;
}
// If it is a drop from windows and we get any kind of error we return S_FALSE and dropeffect none. This
// prevents bogus messages from the shell from being displayed
returnValue = (dropDataType != DropDataType.Shell) ? VSConstants.E_FAIL : VSConstants.S_OK;
} catch (System.IO.FileNotFoundException e) {
Trace.WriteLine("Exception : " + e.Message);
if (!VsUtilities.IsInAutomationFunction(this.Site)) {
string message = e.Message;
string title = string.Empty;
OLEMSGICON icon = OLEMSGICON.OLEMSGICON_CRITICAL;
OLEMSGBUTTON buttons = OLEMSGBUTTON.OLEMSGBUTTON_OK;
OLEMSGDEFBUTTON defaultButton = OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST;
VsShellUtilities.ShowMessageBox(this.Site, title, message, icon, buttons, defaultButton);
}
returnValue = VSConstants.E_FAIL;
}
if (ItemsDraggedOrCutOrCopied != null)
{
ItemsDraggedOrCutOrCopied.Clear();
}
SourceDraggedOrCutOrCopied = CopyPasteDragSource.None;
return returnValue;
}
#endregion
#region override of IVsHierarchyDropDataSource2 methods
/// <summary>
/// Returns information about one or more of the items being dragged
/// </summary>
/// <param name="pdwOKEffects">Pointer to a DWORD value describing the effects displayed while the item is being dragged,
/// such as cursor icons that change during the drag-and-drop operation.
/// For example, if the item is dragged over an invalid target point
/// (such as the item's original location), the cursor icon changes to a circle with a line through it.
/// Similarly, if the item is dragged over a valid target point, the cursor icon changes to a file or folder.</param>
/// <param name="ppDataObject">Pointer to the IDataObject interface on the item being dragged.
/// This data object contains the data being transferred in the drag-and-drop operation.
/// If the drop occurs, then this data object (item) is incorporated into the target hierarchy or hierarchy window.</param>
/// <param name="ppDropSource">Pointer to the IDropSource interface of the item being dragged.</param>
/// <returns>If the method succeeds, it returns S_OK. If it fails, it returns an error code.</returns>
public int GetDropInfo(out uint pdwOKEffects, out IOleDataObject ppDataObject, out IDropSource ppDropSource) {
//init out params
pdwOKEffects = (uint)DropEffect.None;
ppDataObject = null;
ppDropSource = null;
IOleDataObject dataObject = PackageSelectionDataObject(false);
if (dataObject == null) {
return VSConstants.E_NOTIMPL;
}
SourceDraggedOrCutOrCopied = CopyPasteDragSource.Dragged;
pdwOKEffects = (uint)(DropEffect.Move | DropEffect.Copy);
ppDataObject = dataObject;
return VSConstants.S_OK;
}
/// <summary>
/// Notifies clients that the dragged item was dropped.
/// </summary>
/// <param name="fDropped">If true, then the dragged item was dropped on the target. If false, then the drop did not occur.</param>
/// <param name="dwEffects">Visual effects associated with the drag-and-drop operation, such as cursors, bitmaps, and so on.
/// The value of dwEffects passed to the source object via OnDropNotify method is the value of pdwEffects returned by Drop method.</param>
/// <returns>If the method succeeds, it returns S_OK. If it fails, it returns an error code. </returns>
public int OnDropNotify(int fDropped, uint dwEffects) {
if (SourceDraggedOrCutOrCopied == CopyPasteDragSource.None) {
return VSConstants.S_FALSE;
}
if (dwEffects == (uint)DropEffect.Move) {
foreach (var item in ItemsDraggedOrCutOrCopied) {
item.Remove(true);
}
}
ItemsDraggedOrCutOrCopied.Clear();
SourceDraggedOrCutOrCopied = CopyPasteDragSource.None;
return VSConstants.S_OK;
}
/// <summary>
/// Allows the drag source to prompt to save unsaved items being dropped.
/// Notifies the source hierarchy that information dragged from it is about to be dropped on a target.
/// This method is called immediately after the mouse button is released on a drop.
/// </summary>
/// <param name="o">Reference to the IDataObject interface on the item being dragged.
/// This data object contains the data being transferred in the drag-and-drop operation.
/// If the drop occurs, then this data object (item) is incorporated into the hierarchy window of the new hierarchy.</param>
/// <param name="dwEffect">Current state of the keyboard and the mouse modifier keys.</param>
/// <param name="fCancelDrop">If true, then the drop is cancelled by the source hierarchy. If false, then the drop can continue.</param>
/// <returns>If the method succeeds, it returns S_OK. If it fails, it returns an error code. </returns>
public int OnBeforeDropNotify(IOleDataObject o, uint dwEffect, out int fCancelDrop) {
// If there is nothing to be dropped just return that drop should be cancelled.
if (this.ItemsDraggedOrCutOrCopied == null) {
fCancelDrop = 1;
return VSConstants.S_OK;
}
fCancelDrop = 0;
bool dirty = false;
foreach (HierarchyNode node in this.ItemsDraggedOrCutOrCopied) {
bool isDirty, isOpen, isOpenedByUs;
uint docCookie;
IVsPersistDocData ppIVsPersistDocData;
DocumentManager manager = node.GetDocumentManager();
if (node.IsLinkFile) {
continue;
}
if (manager != null) {
manager.GetDocInfo(out isOpen, out isDirty, out isOpenedByUs, out docCookie, out ppIVsPersistDocData);
if (isDirty && isOpenedByUs) {
dirty = true;
break;
}
}
}
// if there are no dirty docs we are ok to proceed
if (!dirty) {
return VSConstants.S_OK;
}
// Prompt to save if there are dirty docs
string message = SR.GetString(SR.SaveModifiedDocuments, CultureInfo.CurrentUICulture);
string title = string.Empty;
OLEMSGICON icon = OLEMSGICON.OLEMSGICON_WARNING;
OLEMSGBUTTON buttons = OLEMSGBUTTON.OLEMSGBUTTON_YESNOCANCEL;
OLEMSGDEFBUTTON defaultButton = OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST;
int result = VsShellUtilities.ShowMessageBox(Site, title, message, icon, buttons, defaultButton);
switch (result) {
case NativeMethods.IDYES:
break;
case NativeMethods.IDNO:
return VSConstants.S_OK;
case NativeMethods.IDCANCEL: goto default;
default:
fCancelDrop = 1;
return VSConstants.S_OK;
}
// Save all dirty documents
foreach (HierarchyNode node in this.ItemsDraggedOrCutOrCopied) {
DocumentManager manager = node.GetDocumentManager();
if (manager != null) {
manager.Save(true);
}
}
return VSConstants.S_OK;
}
#endregion
#region IVsUIHierWinClipboardHelperEvents Members
/// <summary>
/// Called after your cut/copied items has been pasted
/// </summary>
///<param name="wasCut">If true, then the IDataObject has been successfully pasted into a target hierarchy.
/// If false, then the cut or copy operation was cancelled.</param>
/// <param name="dropEffect">Visual effects associated with the drag and drop operation, such as cursors, bitmaps, and so on.
/// These should be the same visual effects used in OnDropNotify</param>
/// <returns>If the method succeeds, it returns S_OK. If it fails, it returns an error code. </returns>
public virtual int OnPaste(int wasCut, uint dropEffect) {
if (SourceDraggedOrCutOrCopied == CopyPasteDragSource.None) {
return VSConstants.S_FALSE;
}
if (dropEffect == (uint)DropEffect.None) {
return OnClear(wasCut);
}
if (wasCut != 0 || dropEffect == (uint)DropEffect.Move) {
// If we just did a cut, then we need to free the data object. Otherwise, we leave it
// alone so that you can continue to paste the data in new locations.
CleanAndFlushClipboard();
foreach (HierarchyNode node in ItemsDraggedOrCutOrCopied) {
node.Remove(true);
}
ItemsDraggedOrCutOrCopied.Clear();
}
this.SourceDraggedOrCutOrCopied = CopyPasteDragSource.None;
return VSConstants.S_OK;
}
/// <summary>
/// Called when your cut/copied operation is canceled
/// </summary>
/// <param name="wasCut">This flag informs the source that the Cut method was called (true),
/// rather than Copy (false), so the source knows whether to "un-cut-highlight" the items that were cut.</param>
/// <returns>If the method succeeds, it returns S_OK. If it fails, it returns an error code. </returns>
public virtual int OnClear(int wasCut) {
if (SourceDraggedOrCutOrCopied == CopyPasteDragSource.None) {
return VSConstants.S_FALSE;
}
if (wasCut != 0) {
IVsUIHierarchyWindow w = UIHierarchyUtilities.GetUIHierarchyWindow(this.site, HierarchyNode.SolutionExplorer);
if (w != null) {
foreach (HierarchyNode node in ItemsDraggedOrCutOrCopied) {
node.ExpandItem(EXPANDFLAGS.EXPF_UnCutHighlightItem);
}
}
}
ItemsDraggedOrCutOrCopied.Clear();
this.SourceDraggedOrCutOrCopied = CopyPasteDragSource.None;
return VSConstants.S_OK;
}
#endregion
#region virtual methods
/// <summary>
/// Returns a dataobject from selected nodes
/// </summary>
/// <param name="cutHighlightItems">boolean that defines if the selected items must be cut</param>
/// <returns>data object for selected items</returns>
private DataObject PackageSelectionDataObject(bool cutHighlightItems) {
StringBuilder sb = new StringBuilder();
DataObject dataObject = null;
IList<HierarchyNode> selectedNodes = this.GetSelectedNodes();
if (selectedNodes != null) {
this.InstantiateItemsDraggedOrCutOrCopiedList();
// If there is a selection package the data
foreach (HierarchyNode node in selectedNodes) {
string selectionContent = node.PrepareSelectedNodesForClipBoard();
if (selectionContent != null) {
sb.Append(selectionContent);
}
}
}
// Add the project items first.
IntPtr ptrToItems = this.PackageSelectionData(sb, false);
if (ptrToItems == IntPtr.Zero) {
return null;
}
FORMATETC fmt = DragDropHelper.CreateFormatEtc(DragDropHelper.CF_VSSTGPROJECTITEMS);
dataObject = new DataObject();
dataObject.SetData(fmt, ptrToItems);
// Now add the project path that sourced data. We just write the project file path.
IntPtr ptrToProjectPath = this.PackageSelectionData(new StringBuilder(this.GetMkDocument()), true);
if (ptrToProjectPath != IntPtr.Zero) {
dataObject.SetData(DragDropHelper.CreateFormatEtc(DragDropHelper.CF_VSPROJECTCLIPDESCRIPTOR), ptrToProjectPath);
}
if (cutHighlightItems) {
bool first = true;
foreach (HierarchyNode node in this.ItemsDraggedOrCutOrCopied) {
node.ExpandItem(first ? EXPANDFLAGS.EXPF_CutHighlightItem : EXPANDFLAGS.EXPF_AddCutHighlightItem);
first = false;
}
}
return dataObject;
}
class ProjectReferenceFileAdder {
/// <summary>
/// This hierarchy which is having items added/moved
/// </summary>
private readonly ProjectNode Project;
/// <summary>
/// The node which we're adding/moving the items to
/// </summary>
private readonly HierarchyNode TargetNode;
/// <summary>
/// The references we're adding, using the format {Guid}|project|folderPath
/// </summary>
private readonly string[] ProjectReferences;
/// <summary>
/// True if this is the result of a mouse drop, false if this is the result of a paste
/// </summary>
private readonly bool MouseDropping;
/// <summary>
/// Move or Copy
/// </summary>
private readonly DropEffect DropEffect;
private bool? OverwriteAllItems;
public ProjectReferenceFileAdder(ProjectNode project, HierarchyNode targetNode, string[] projectReferences, bool mouseDropping, DropEffect dropEffect) {
VsUtilities.ArgumentNotNull("targetNode", targetNode);
Debug.Assert(project != null);
TargetNode = targetNode;
Project = project;
ProjectReferences = projectReferences;
MouseDropping = mouseDropping;
DropEffect = dropEffect;
}
public bool AddFiles() {
// Collect all of the additions.
List<Addition> additions = new List<Addition>();
List<string> folders = new List<string>();
// process folders first
foreach (string projectReference in ProjectReferences) {
if (projectReference == null) {
// bad projectref, bail out
return false;
}
if (CommonUtils.HasEndSeparator(projectReference)) {
var addition = CanAddFolderFromProjectReference(projectReference);
if (addition == null) {
return false;
}
additions.Add(addition);
FolderAddition folderAddition = addition as FolderAddition;
if (folderAddition != null) {
folders.Add(folderAddition.SourceFolder);
}
}
}
foreach (string projectReference in ProjectReferences) {
if (projectReference == null) {
// bad projectref, bail out
return false;
}
if (!CommonUtils.HasEndSeparator(projectReference)) {
var addition = CanAddFileFromProjectReference(projectReference, TargetNode.GetDragTargetHandlerNode().FullPathToChildren);
if (addition == null) {
return false;
}
FileAddition fileAddition = addition as FileAddition;
bool add = true;
if (fileAddition != null) {
foreach (var folder in folders) {
if (fileAddition.SourceMoniker.StartsWith(folder, StringComparison.OrdinalIgnoreCase)) {
// this will be moved/copied by the folder, it doesn't need another move/copy
add = false;
break;
}
}
}
if (add) {
additions.Add(addition);
}
}
}
foreach (var addition in additions) {
addition.DoAddition();
}
return true;
}
/// <summary>
/// Tests to see if we can add the folder to the project. Returns true if it's ok, false if it's not.
/// </summary>
/// <param name="folderToAdd">Project reference (from data object) using the format: {Guid}|project|folderPath</param>
/// <param name="targetNode">Node to add the new folder to</param>
private Addition CanAddFolderFromProjectReference(string folderToAdd) {
VsUtilities.ArgumentNotNullOrEmpty(folderToAdd, "folderToAdd");
var targetFolderNode = TargetNode.GetDragTargetHandlerNode();
string folder;
IVsHierarchy sourceHierarchy;
GetPathAndHierarchy(folderToAdd, out folder, out sourceHierarchy);
// Ensure we don't end up in an endless recursion
if (VsUtilities.IsSameComObject(Project, sourceHierarchy)) {
if (String.Equals(folder, targetFolderNode.FullPathToChildren, StringComparison.OrdinalIgnoreCase)) {
if (DropEffect == DropEffect.Move &&
IsBadMove(targetFolderNode.FullPathToChildren, folder, false)) {
return null;
}
}
if (targetFolderNode.FullPathToChildren.StartsWith(folder, StringComparison.OrdinalIgnoreCase) &&
!String.Equals(targetFolderNode.FullPathToChildren, folder, StringComparison.OrdinalIgnoreCase)) {
// dragging a folder into a child, that's not allowed
VsShellUtilities.ShowMessageBox(
Project.Site,
String.Format("Cannot move '{0}'. The destination folder is a subfolder of the source folder.", Path.GetFileName(CommonUtils.TrimEndSeparator(folder))),
null,
OLEMSGICON.OLEMSGICON_CRITICAL,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
return null;
}
}
var targetPath = Path.Combine(targetFolderNode.FullPathToChildren, Path.GetFileName(CommonUtils.TrimEndSeparator(folder)));
if (File.Exists(targetPath)) {
VsShellUtilities.ShowMessageBox(
Project.Site,
String.Format("Unable to add '{0}'. A file with that name already exists.", Path.GetFileName(CommonUtils.TrimEndSeparator(folder))),
null,
OLEMSGICON.OLEMSGICON_CRITICAL,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
return null;
}
if (Directory.Exists(targetPath)) {
if (DropEffect == DropEffect.Move) {
if (targetPath == folderToAdd) {
CannotMoveSameLocation(folderToAdd);
} else {
VsShellUtilities.ShowMessageBox(
Project.Site,
String.Format("Cannot move the folder '{0}'. A folder with that name already exists in the destination directory.", Path.GetFileName(CommonUtils.TrimEndSeparator(folder))),
null,
OLEMSGICON.OLEMSGICON_CRITICAL,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}
return null;
}
var dialog = new OverwriteFileDialog(String.Format(
@"This folder already contains a folder called '{0}'
If the files in the existing folder have the same names as files in the
folder you are copying, do you want to replace the existing files?", Path.GetFileName(CommonUtils.TrimEndSeparator(folder))), false);
dialog.Owner = Application.Current.MainWindow;
var res = dialog.ShowDialog();
if (res != true) {
if (res == null) {
// cancel, abort the whole copy
return null;
} else {
// no, don't copy the folder
return NopAddition.Instance;
}
}
// otherwise yes, and we'll prompt about the files.
}
string targetFileName = Path.GetFileName(CommonUtils.TrimEndSeparator(folder));
if (VsUtilities.IsSameComObject(Project, sourceHierarchy) &&
String.Equals(targetFolderNode.FullPathToChildren, folder, StringComparison.OrdinalIgnoreCase)) {
// copying a folder onto its self, make a copy
targetFileName = GetCopyName(targetFolderNode.FullPathToChildren);
}
List<Addition> additions = new List<Addition>();
uint folderId;
if (ErrorHandler.Failed(sourceHierarchy.ParseCanonicalName(folder, out folderId))) {
// the folder may have been deleted between the copy & paste
ReportMissingItem(folder);
return null;
}
if (Path.Combine(targetFolderNode.FullPathToChildren, targetFileName).Length >= NativeMethods.MAX_FOLDER_PATH) {
VsShellUtilities.ShowMessageBox(
Project.Site,
"The folder name is too long.",
null,
OLEMSGICON.OLEMSGICON_CRITICAL,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
return null;
}
if (!WalkSourceProjectAndAdd(sourceHierarchy, folderId, targetFolderNode.FullPathToChildren, false, additions, targetFileName)) {
return null;
}
if (additions.Count == 1) {
return (FolderAddition)additions[0];
}
Debug.Assert(additions.Count == 0);
return null;
}
private void ReportMissingItem(string folder) {
VsShellUtilities.ShowMessageBox(
Project.Site,
String.Format("The source URL '{0}' could not be found.", Path.GetFileName(CommonUtils.TrimEndSeparator(folder))),
null,
OLEMSGICON.OLEMSGICON_CRITICAL,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}
/// <summary>
/// Recursive method that walk a hierarchy and add items it find to our project.
/// Note that this is meant as an helper to the Copy&Paste/Drag&Drop functionality.
/// </summary>
/// <param name="sourceHierarchy">Hierarchy to walk</param>
/// <param name="itemId">Item ID where to start walking the hierarchy</param>
/// <param name="targetNode">Node to start adding to</param>
/// <param name="addSibblings">Typically false on first call and true after that</param>
private bool WalkSourceProjectAndAdd(IVsHierarchy sourceHierarchy, uint itemId, string targetPath, bool addSiblings, List<Addition> additions, string name = null) {
Debug.Assert(sourceHierarchy != null);
if (itemId != VSConstants.VSITEMID_NIL) {
// Before we start the walk, add the current node
object variant = null;
// Calculate the corresponding path in our project
string source;
ErrorHandler.ThrowOnFailure(((IVsProject)sourceHierarchy).GetMkDocument(itemId, out source));
if (name == null) {
name = Path.GetFileName(CommonUtils.TrimEndSeparator(source));
}
Guid guidType;
ErrorHandler.ThrowOnFailure(sourceHierarchy.GetGuidProperty(itemId, (int)__VSHPROPID.VSHPROPID_TypeGuid, out guidType));
IVsSolution solution = Project.GetService(typeof(IVsSolution)) as IVsSolution;
if (solution != null) {
if (guidType == VSConstants.GUID_ItemType_PhysicalFile) {
string projRef;
ErrorHandler.ThrowOnFailure(solution.GetProjrefOfItem(sourceHierarchy, itemId, out projRef));
var addition = CanAddFileFromProjectReference(projRef, targetPath);
if (addition == null) {
// cancelled
return false;
}
additions.Add(addition);
}
}
// Start with child nodes (depth first)
if (guidType == VSConstants.GUID_ItemType_PhysicalFolder) {
variant = null;
ErrorHandler.ThrowOnFailure(sourceHierarchy.GetProperty(itemId, (int)__VSHPROPID.VSHPROPID_FirstVisibleChild, out variant));
uint currentItemID = (uint)(int)variant;
List<Addition> nestedAdditions = new List<Addition>();
string newPath = Path.Combine(targetPath, name);
if (!WalkSourceProjectAndAdd(sourceHierarchy, currentItemID, newPath, true, nestedAdditions)) {
// cancelled
return false;
}
additions.Add(new FolderAddition(Project, Path.Combine(targetPath, name), source, DropEffect, nestedAdditions.ToArray()));
}
if (addSiblings) {
// Then look at siblings
uint currentItemID = itemId;
while (currentItemID != VSConstants.VSITEMID_NIL) {
variant = null;
// http://mpfproj10.codeplex.com/workitem/11618 - pass currentItemID instead of itemId
ErrorHandler.ThrowOnFailure(sourceHierarchy.GetProperty(currentItemID, (int)__VSHPROPID.VSHPROPID_NextVisibleSibling, out variant));
currentItemID = (uint)(int)variant;
if (!WalkSourceProjectAndAdd(sourceHierarchy, currentItemID, targetPath, false, additions)) {
// cancelled
return false;
}
}
}
}
return true;
}
private static string GetCopyName(string existingFullPath) {
string newDir, name, extension;
if (CommonUtils.HasEndSeparator(existingFullPath)) {
name = Path.GetFileName(CommonUtils.TrimEndSeparator(existingFullPath));
extension = "";
} else {
extension = Path.GetExtension(existingFullPath);
name = Path.GetFileNameWithoutExtension(existingFullPath);
}
string folder = Path.GetDirectoryName(CommonUtils.TrimEndSeparator(existingFullPath));
int copyCount = 1;
do {
string newName = name + " - Copy";
if (copyCount != 1) {
newName += " (" + copyCount + ")";
}
newName += extension;
copyCount++;
newDir = Path.Combine(folder, newName);
} while (File.Exists(newDir) || Directory.Exists(newDir));
return newDir;
}
/// <summary>
/// This is used to recursively add a folder from an other project.
/// Note that while we copy the folder content completely, we only
/// add to the project items which are part of the source project.
/// </summary>
class FolderAddition : Addition {
private readonly ProjectNode Project;
private readonly string NewFolderPath;
public readonly string SourceFolder;
private readonly Addition[] Additions;
private readonly DropEffect DropEffect;
public FolderAddition(ProjectNode project, string newFolderPath, string sourceFolder, DropEffect dropEffect, Addition[] additions) {
Project = project;
NewFolderPath = newFolderPath;
SourceFolder = sourceFolder;
Additions = additions;
DropEffect = dropEffect;
}
public override void DoAddition() {
var newNode = Project.CreateFolderNodes(NewFolderPath);
foreach (var addition in Additions) {
addition.DoAddition();
}
var sourceFolder = Project.FindNodeByFullPath(SourceFolder);
if (sourceFolder != null && sourceFolder.IsNonMemberItem) {
// copying or moving an existing excluded folder, new folder
// is excluded too.
ErrorHandler.ThrowOnFailure(newNode.ExcludeFromProject());
}
if (DropEffect == DropEffect.Move) {
sourceFolder.Remove(true);
}
}
}
/// <summary>
/// Given the reference used for drag and drop returns the path to the item and it's
/// containing hierarchy.
/// </summary>
/// <param name="projectReference"></param>
/// <param name="path"></param>
/// <param name="sourceHierarchy"></param>
private void GetPathAndHierarchy(string projectReference, out string path, out IVsHierarchy sourceHierarchy) {
Guid projectInstanceGuid;
GetPathAndProjectId(projectReference, out projectInstanceGuid, out path);
// normalize the casing in case the project system gave us casing different from the file system
if (CommonUtils.HasEndSeparator(path)) {
try {
var trimmedPath = CommonUtils.TrimEndSeparator(path);
foreach (var dir in Directory.GetDirectories(Path.GetDirectoryName(trimmedPath), Path.GetFileName(trimmedPath))) {
if (String.Equals(dir, trimmedPath, StringComparison.OrdinalIgnoreCase)) {
path = dir + Path.DirectorySeparatorChar;
break;
}
}
} catch {
}
} else {
try {
foreach (var file in Directory.GetFiles(Path.GetDirectoryName(path))) {
if (String.Equals(file, path, StringComparison.OrdinalIgnoreCase)) {
path = file;
break;
}
}
} catch {
}
}
// Retrieve the project from which the items are being copied
IVsSolution solution = (IVsSolution)Project.GetService(typeof(SVsSolution));
ErrorHandler.ThrowOnFailure(solution.GetProjectOfGuid(ref projectInstanceGuid, out sourceHierarchy));
}
private static void GetPathAndProjectId(string projectReference, out Guid projectInstanceGuid, out string folder) {
// Split the reference in its 3 parts
int index1 = Guid.Empty.ToString("B").Length;
if (index1 + 1 >= projectReference.Length)
throw new ArgumentOutOfRangeException("folderToAdd");
// Get the Guid
string guidString = projectReference.Substring(1, index1 - 2);
projectInstanceGuid = new Guid(guidString);
// Get the project path
int index2 = projectReference.IndexOf('|', index1 + 1);
if (index2 < 0 || index2 + 1 >= projectReference.Length)
throw new ArgumentOutOfRangeException("folderToAdd");
// Finally get the source path
folder = projectReference.Substring(index2 + 1);
}
/// <summary>
/// Adds an item from a project refererence to target node.
/// </summary>
/// <param name="projectRef"></param>
/// <param name="targetNode"></param>
private Addition CanAddFileFromProjectReference(string projectRef, string targetFolder, bool fromFolder = false) {
VsUtilities.ArgumentNotNullOrEmpty("projectRef", projectRef);
IVsSolution solution = Project.GetService(typeof(IVsSolution)) as IVsSolution;
VsUtilities.CheckNotNull(solution);
uint itemidLoc;
IVsHierarchy hierarchy;
string str;
VSUPDATEPROJREFREASON[] reason = new VSUPDATEPROJREFREASON[1];
if (ErrorHandler.Failed(solution.GetItemOfProjref(projectRef, out hierarchy, out itemidLoc, out str, reason))) {
// the file may have been deleted between the copy & paste
string path;
Guid projectGuid;
GetPathAndProjectId(projectRef, out projectGuid, out path);
ReportMissingItem(path);
return null;
}
VsUtilities.CheckNotNull(hierarchy);
// This will throw invalid cast exception if the hierrachy is not a project.
IVsProject project = (IVsProject)hierarchy;
string moniker;
ErrorHandler.ThrowOnFailure(project.GetMkDocument(itemidLoc, out moniker));
if (DropEffect == DropEffect.Move && IsBadMove(targetFolder, moniker, true)) {
return null;
}
if (!File.Exists(moniker)) {
VsShellUtilities.ShowMessageBox(
Project.Site,
String.Format("The item '{0}' does not exist in the project directory. It may have been moved, renamed or deleted.", Path.GetFileName(moniker)),
null,
OLEMSGICON.OLEMSGICON_CRITICAL,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
return null;
}
string newPath = Path.Combine(targetFolder, Path.GetFileName(moniker));
var existingChild = Project.FindNodeByFullPath(moniker);
if (existingChild != null && existingChild.IsLinkFile) {
if (DropEffect == DropEffect.Move) {
// moving a link file, just update it's location in the hierarchy
return new ReparentLinkedFileAddition(Project, targetFolder, moniker);
} else {
//
VsShellUtilities.ShowMessageBox(
Project.Site,
String.Format("Cannot copy linked files within the same project. You cannot have more than one link to the same file in a project."),
null,
OLEMSGICON.OLEMSGICON_CRITICAL,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
return null;
}
} else if (File.Exists(newPath) && CommonUtils.IsSamePath(newPath, moniker)) {
newPath = GetCopyName(newPath);
}
bool ok = false;
if (DropEffect == DropEffect.Move && VsUtilities.IsSameComObject(project, Project)) {
ok = Project.Tracker.CanRenameItem(moniker, newPath, VSRENAMEFILEFLAGS.VSRENAMEFILEFLAGS_NoFlags);
} else {
ok = Project.Tracker.CanAddItems(
new[] { newPath },
new VSQUERYADDFILEFLAGS[] { VSQUERYADDFILEFLAGS.VSQUERYADDFILEFLAGS_NoFlags });
}
if (ok) {
if (File.Exists(newPath)) {
if (DropEffect == DropEffect.Move &&
VsUtilities.IsSameComObject(project, Project) &&
Project.FindNodeByFullPath(newPath) != null) {
// if we're overwriting an item, we're moving it, make sure that's ok.
// OverwriteFileAddition will handle the remove from the hierarchy
if (!Project.Tracker.CanRemoveItems(new[] { newPath }, new[] { VSQUERYREMOVEFILEFLAGS.VSQUERYREMOVEFILEFLAGS_NoFlags })) {
return null;
}
}
bool? overwrite = OverwriteAllItems;
if (overwrite == null) {
var dialog = new OverwriteFileDialog(String.Format("A file with the name '{0}' already exists. Do you want to replace it?", Path.GetFileName(moniker)), true);
dialog.Owner = Application.Current.MainWindow;
bool? dialogResult = dialog.ShowDialog();
if (dialogResult != null && !dialogResult.Value) {
// user cancelled
return null;
}
overwrite = dialog.ShouldOverwrite;
if (dialog.AllItems) {
OverwriteAllItems = overwrite;
}
}
if (overwrite.Value) {
return new OverwriteFileAddition(Project, targetFolder, DropEffect, moniker, Path.GetFileName(newPath), project);
} else {
return NopAddition.Instance;
}
}
if (newPath.Length >= NativeMethods.MAX_PATH) {
VsShellUtilities.ShowMessageBox(
Project.Site,
"The filename is too long.",
null,
OLEMSGICON.OLEMSGICON_CRITICAL,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
return null;
}
return new FileAddition(Project, targetFolder, DropEffect, moniker, Path.GetFileName(newPath), project);
}
return null;
}
private bool IsBadMove(string targetFolder, string moniker, bool file) {
if (TargetNode.GetMkDocument() == moniker) {
// we are moving the file onto it's self. If it's a single file via mouse
// we'll ignore it. If it's multiple files, or a cut and paste, then we'll
// report the error.
if (ProjectReferences.Length > 1 || !MouseDropping) {
CannotMoveSameLocation(moniker);
}
return true;
}
if ((file || !MouseDropping) &&
Directory.Exists(targetFolder) &&
CommonUtils.IsSameDirectory(Path.GetDirectoryName(moniker), targetFolder)) {
// we're moving a file into it's own folder, report an error.
CannotMoveSameLocation(moniker);
return true;
}
return false;
}
private void CannotMoveSameLocation(string moniker) {
VsShellUtilities.ShowMessageBox(
Project.Site,
String.Format("Cannot move '{0}'. The destination folder is the same as the source folder.", Path.GetFileName(CommonUtils.TrimEndSeparator(moniker))),
null,
OLEMSGICON.OLEMSGICON_CRITICAL,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}
private bool IsOurProject(IVsProject project) {
string projectDoc;
project.GetMkDocument((uint)VSConstants.VSITEMID.Root, out projectDoc);
return projectDoc == Project.Url;
}
abstract class Addition {
public abstract void DoAddition();
}
class NopAddition : Addition {
public static NopAddition Instance = new NopAddition();
public override void DoAddition() {
}
}
class ReparentLinkedFileAddition : Addition {
private readonly ProjectNode Project;
private readonly string TargetFolder;
private readonly string Moniker;
public ReparentLinkedFileAddition(ProjectNode project, string targetFolder, string moniker) {
Project = project;
TargetFolder = targetFolder;
Moniker = moniker;
}
public override void DoAddition() {
var existing = Project.FindNodeByFullPath(Moniker);
existing.Parent.RemoveChild(existing);
Project.OnItemDeleted(existing);
existing.ID = Project.ItemIdMap.Add(existing);
var newParent = TargetFolder == Project.ProjectHome ? Project : Project.FindNodeByFullPath(TargetFolder);
newParent.AddChild(existing);
Project.ItemsDraggedOrCutOrCopied.Remove(existing); // we don't need to remove the file after Paste
var link = existing.ItemNode.GetMetadata(ProjectFileConstants.Link);
if (link != null) {
// update the link to the new location within solution explorer
existing.ItemNode.SetMetadata(
ProjectFileConstants.Link,
Path.Combine(
CommonUtils.GetRelativeDirectoryPath(
Project.ProjectHome,
TargetFolder
),
Path.GetFileName(Moniker)
)
);
}
}
}
class FileAddition : Addition {
public readonly ProjectNode Project;
public readonly string TargetFolder;
public readonly DropEffect DropEffect;
public readonly string SourceMoniker;
public readonly IVsProject SourceHierarchy;
public readonly string NewFileName;
public FileAddition(ProjectNode project, string targetFolder, DropEffect dropEffect, string sourceMoniker, string newFileName, IVsProject sourceHierarchy) {
Project = project;
TargetFolder = targetFolder;
DropEffect = dropEffect;
SourceMoniker = sourceMoniker;
SourceHierarchy = sourceHierarchy;
NewFileName = newFileName;
}
public override void DoAddition() {
string newPath = Path.Combine(TargetFolder, NewFileName);
if (DropEffect == DropEffect.Move && VsUtilities.IsSameComObject(Project, SourceHierarchy)) {
// we are doing a move, we need to remove the old item, and add the new.
var fileNode = Project.FindNodeByFullPath(SourceMoniker);
Debug.Assert(fileNode is FileNode);
FileNode file = fileNode as FileNode;
file.RenameInStorage(fileNode.Url, newPath);
file.RenameFileNode(fileNode.Url, newPath);
Project.Tracker.OnItemRenamed(SourceMoniker, newPath, VSRENAMEFILEFLAGS.VSRENAMEFILEFLAGS_NoFlags);
Project.ItemsDraggedOrCutOrCopied.Remove(fileNode); // we don't need to remove the file after Paste
} else {
// we are copying and adding a new file node
File.Copy(SourceMoniker, newPath, true);
var existing = Project.FindNodeByFullPath(newPath);
if (existing == null) {
var fileNode = Project.CreateFileNode(newPath);
if (String.Equals(TargetFolder, Project.FullPathToChildren, StringComparison.OrdinalIgnoreCase)) {
Project.AddChild(fileNode);
} else {
var targetFolder = Project.CreateFolderNodes(TargetFolder);
if (targetFolder.IsNonMemberItem) {
// dropping/pasting folder into non-member folder, non member folder
// should get included into the project.
ErrorHandler.ThrowOnFailure(targetFolder.IncludeInProject(false));
}
targetFolder.AddChild(fileNode);
}
}
}
Project.SetProjectFileDirty(true);
}
}
class OverwriteFileAddition : FileAddition {
public OverwriteFileAddition(ProjectNode project, string targetFolder, DropEffect dropEffect, string sourceMoniker, string newFileName, IVsProject sourceHierarchy)
: base(project, targetFolder, dropEffect, sourceMoniker, newFileName, sourceHierarchy) {
}
public override void DoAddition() {
if (DropEffect == DropEffect.Move) {
// File.Move won't overwrite, do it now.
File.Delete(Path.Combine(TargetFolder, Path.GetFileName(NewFileName)));
HierarchyNode existingNode;
if (VsUtilities.IsSameComObject(SourceHierarchy, Project) &&
(existingNode = Project.FindNodeByFullPath(Path.Combine(TargetFolder, NewFileName))) != null) {
// remove the existing item from the hierarchy, base.DoAddition will add a new one
existingNode.Remove(true);
}
}
base.DoAddition();
}
}
}
/// <summary>
/// Add an existing item (file/folder) to the project if it already exist in our storage.
/// </summary>
/// <param name="parentNode">Node to that this item to</param>
/// <param name="name">Name of the item being added</param>
/// <param name="targetPath">Path of the item being added</param>
/// <returns>Node that was added</returns>
protected virtual HierarchyNode AddNodeIfTargetExistInStorage(HierarchyNode parentNode, string name, string targetPath) {
if (parentNode == null) {
return null;
}
HierarchyNode newNode = parentNode;
// If the file/directory exist, add a node for it
if (File.Exists(targetPath)) {
VSADDRESULT[] result = new VSADDRESULT[1];
ErrorHandler.ThrowOnFailure(this.AddItem(parentNode.ID, VSADDITEMOPERATION.VSADDITEMOP_OPENFILE, name, 1, new string[] { targetPath }, IntPtr.Zero, result));
if (result[0] != VSADDRESULT.ADDRESULT_Success)
throw new Exception();
newNode = this.FindNodeByFullPath(targetPath);
if (newNode == null)
throw new Exception();
} else if (Directory.Exists(targetPath)) {
newNode = this.CreateFolderNodes(targetPath);
}
return newNode;
}
#endregion
#region non-virtual methods
/// <summary>
/// Handle the Cut operation to the clipboard
/// </summary>
public int CutToClipboard() {
int returnValue = (int)OleConstants.OLECMDERR_E_NOTSUPPORTED;
this.RegisterClipboardNotifications(true);
// Create our data object and change the selection to show item(s) being cut
IOleDataObject dataObject = this.PackageSelectionDataObject(true);
if (dataObject != null) {
this.SourceDraggedOrCutOrCopied = CopyPasteDragSource.Cut;
// Add our cut item(s) to the clipboard
ErrorHandler.ThrowOnFailure(UnsafeNativeMethods.OleSetClipboard(dataObject));
// Inform VS (UiHierarchyWindow) of the cut
IVsUIHierWinClipboardHelper clipboardHelper = (IVsUIHierWinClipboardHelper)GetService(typeof(SVsUIHierWinClipboardHelper));
if (clipboardHelper == null) {
return VSConstants.E_FAIL;
}
returnValue = ErrorHandler.ThrowOnFailure(clipboardHelper.Cut(dataObject));
}
return returnValue;
}
/// <summary>
/// Handle the Copy operation to the clipboard
/// </summary>
public int CopyToClipboard() {
int returnValue = (int)OleConstants.OLECMDERR_E_NOTSUPPORTED;
this.RegisterClipboardNotifications(true);
// Create our data object and change the selection to show item(s) being copy
IOleDataObject dataObject = this.PackageSelectionDataObject(false);
if (dataObject != null) {
this.SourceDraggedOrCutOrCopied = CopyPasteDragSource.Copied;
// Add our copy item(s) to the clipboard
ErrorHandler.ThrowOnFailure(UnsafeNativeMethods.OleSetClipboard(dataObject));
// Inform VS (UiHierarchyWindow) of the copy
IVsUIHierWinClipboardHelper clipboardHelper = (IVsUIHierWinClipboardHelper)GetService(typeof(SVsUIHierWinClipboardHelper));
if (clipboardHelper == null) {
return VSConstants.E_FAIL;
}
returnValue = ErrorHandler.ThrowOnFailure(clipboardHelper.Copy(dataObject));
}
return returnValue;
}
/// <summary>
/// Handle the Paste operation to a targetNode
/// </summary>
public int PasteFromClipboard(HierarchyNode targetNode) {
int returnValue = (int)OleConstants.OLECMDERR_E_NOTSUPPORTED;
if (targetNode == null) {
return VSConstants.E_INVALIDARG;
}
//Get the clipboardhelper service and use it after processing dataobject
IVsUIHierWinClipboardHelper clipboardHelper = (IVsUIHierWinClipboardHelper)GetService(typeof(SVsUIHierWinClipboardHelper));
if (clipboardHelper == null) {
return VSConstants.E_FAIL;
}
try {
//Get dataobject from clipboard
IOleDataObject dataObject;
ErrorHandler.ThrowOnFailure(UnsafeNativeMethods.OleGetClipboard(out dataObject));
if (dataObject == null) {
return VSConstants.E_UNEXPECTED;
}
DropEffect dropEffect = DropEffect.None;
DropDataType dropDataType = DropDataType.None;
try {
dropEffect = SourceDraggedOrCutOrCopied == CopyPasteDragSource.Cut ? DropEffect.Move : DropEffect.Copy;
dropDataType = this.ProcessSelectionDataObject(dataObject, targetNode, false, dropEffect);
if (dropDataType == DropDataType.None) {
dropEffect = DropEffect.None;
}
} catch (ExternalException e) {
Trace.WriteLine("Exception : " + e.Message);
// If it is a drop from windows and we get any kind of error ignore it. This
// prevents bogus messages from the shell from being displayed
if (dropDataType != DropDataType.Shell) {
throw;
}
} finally {
// Inform VS (UiHierarchyWindow) of the paste
returnValue = clipboardHelper.Paste(dataObject, (uint)(SourceDraggedOrCutOrCopied == CopyPasteDragSource.None ? DropEffect.Move : dropEffect));
}
} catch (COMException e) {
Trace.WriteLine("Exception : " + e.Message);
returnValue = e.ErrorCode;
}
return returnValue;
}
/// <summary>
/// Determines if the paste command should be allowed.
/// </summary>
/// <returns></returns>
public bool AllowPasteCommand() {
IOleDataObject dataObject = null;
try {
ErrorHandler.ThrowOnFailure(UnsafeNativeMethods.OleGetClipboard(out dataObject));
if (dataObject == null) {
return false;
}
// First see if this is a set of storage based items
FORMATETC format = DragDropHelper.CreateFormatEtc((ushort)DragDropHelper.CF_VSSTGPROJECTITEMS);
if (dataObject.QueryGetData(new FORMATETC[] { format }) == VSConstants.S_OK)
return true;
// Try reference based items
format = DragDropHelper.CreateFormatEtc((ushort)DragDropHelper.CF_VSREFPROJECTITEMS);
if (dataObject.QueryGetData(new FORMATETC[] { format }) == VSConstants.S_OK)
return true;
// Try windows explorer files format
format = DragDropHelper.CreateFormatEtc((ushort)NativeMethods.CF_HDROP);
return (dataObject.QueryGetData(new FORMATETC[] { format }) == VSConstants.S_OK);
}
// We catch External exceptions since it might be that it is not our data on the clipboard.
catch (ExternalException e) {
Trace.WriteLine("Exception :" + e.Message);
return false;
}
}
/// <summary>
/// Register/Unregister for Clipboard events for the UiHierarchyWindow (solution explorer)
/// </summary>
/// <param name="register">true for register, false for unregister</param>
public void RegisterClipboardNotifications(bool register) {
// Get the UiHierarchy window clipboard helper service
IVsUIHierWinClipboardHelper clipboardHelper = (IVsUIHierWinClipboardHelper)GetService(typeof(SVsUIHierWinClipboardHelper));
if (clipboardHelper == null) {
return;
}
if (register && this.copyPasteCookie == 0) {
// Register
ErrorHandler.ThrowOnFailure(clipboardHelper.AdviseClipboardHelperEvents(this, out this.copyPasteCookie));
Debug.Assert(this.copyPasteCookie != 0, "AdviseClipboardHelperEvents returned an invalid cookie");
} else if (!register && this.copyPasteCookie != 0) {
// Unregister
ErrorHandler.ThrowOnFailure(clipboardHelper.UnadviseClipboardHelperEvents(this.copyPasteCookie));
this.copyPasteCookie = 0;
}
}
/// <summary>
/// Process dataobject from Drag/Drop/Cut/Copy/Paste operation
///
/// drop indicates if it is a drag/drop or a cut/copy/paste.
/// </summary>
/// <remarks>The targetNode is set if the method is called from a drop operation, otherwise it is null</remarks>
public DropDataType ProcessSelectionDataObject(IOleDataObject dataObject, HierarchyNode targetNode, bool drop, DropEffect dropEffect) {
Debug.Assert(targetNode != null);
DropDataType dropDataType = DropDataType.None;
bool isWindowsFormat = false;
// Try to get it as a directory based project.
List<string> filesDropped = DragDropHelper.GetDroppedFiles(DragDropHelper.CF_VSSTGPROJECTITEMS, dataObject, out dropDataType);
if (filesDropped.Count == 0) {
filesDropped = DragDropHelper.GetDroppedFiles(DragDropHelper.CF_VSREFPROJECTITEMS, dataObject, out dropDataType);
}
if (filesDropped.Count == 0) {
filesDropped = DragDropHelper.GetDroppedFiles(NativeMethods.CF_HDROP, dataObject, out dropDataType);
isWindowsFormat = (filesDropped.Count > 0);
}
if (dropDataType != DropDataType.None && filesDropped.Count > 0) {
string[] filesDroppedAsArray = filesDropped.ToArray();
HierarchyNode node = targetNode;
// For directory based projects the content of the clipboard is a double-NULL terminated list of Projref strings.
if (isWindowsFormat) {
DropFilesOrFolders(filesDroppedAsArray, node);
return dropDataType;
} else {
if (AddFilesFromProjectReferences(node, filesDroppedAsArray, drop, dropEffect)) {
return dropDataType;
}
}
}
// If we reached this point then the drop data must be set to None.
// Otherwise the OnPaste will be called with a valid DropData and that would actually delete the item.
return DropDataType.None;
}
public void DropFilesOrFolders(string[] filesDropped, HierarchyNode ontoNode) {
var waitDialog = (IVsThreadedWaitDialog)Site.GetService(typeof(SVsThreadedWaitDialog));
int waitResult = waitDialog.StartWaitDialog(
"Adding files and folders...",
"Adding files to your project, this may take several seconds...",
null,
0,
null,
null
);
try {
ontoNode = ontoNode.GetDragTargetHandlerNode();
string nodePath = ontoNode.FullPathToChildren;
bool droppingExistingDirectory = true;
foreach (var droppedFile in filesDropped) {
if (!Directory.Exists(droppedFile) ||
!String.Equals(Path.GetDirectoryName(droppedFile), nodePath, StringComparison.OrdinalIgnoreCase)) {
droppingExistingDirectory = false;
break;
}
}
if (droppingExistingDirectory) {
// we're dragging a directory/directories that already exist
// into the location where they exist, we can do this via a fast path,
// and pop up a nice progress bar.
AddExistingDirectories(ontoNode, filesDropped);
} else {
foreach (var droppedFile in filesDropped) {
if (Directory.Exists(droppedFile) &&
CommonUtils.IsSubpathOf(droppedFile, nodePath)) {
int cancelled = 0;
waitDialog.EndWaitDialog(ref cancelled);
waitResult = VSConstants.E_FAIL; // don't end twice
VsShellUtilities.ShowMessageBox(
Site,
String.Format("Cannot add folder '{0}' as a child or decedent of self.", Path.GetFileName(CommonUtils.TrimEndSeparator(droppedFile))),
null,
OLEMSGICON.OLEMSGICON_CRITICAL,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
return;
}
}
// This is the code path when source is windows explorer
VSADDRESULT[] vsaddresults = new VSADDRESULT[1];
vsaddresults[0] = VSADDRESULT.ADDRESULT_Failure;
int addResult = AddItem(ontoNode.ID, VSADDITEMOPERATION.VSADDITEMOP_OPENFILE, null, (uint)filesDropped.Length, filesDropped, IntPtr.Zero, vsaddresults);
if (addResult != VSConstants.S_OK && addResult != VSConstants.S_FALSE && addResult != (int)OleConstants.OLECMDERR_E_CANCELED
&& vsaddresults[0] != VSADDRESULT.ADDRESULT_Success) {
ErrorHandler.ThrowOnFailure(addResult);
}
}
} finally {
if (ErrorHandler.Succeeded(waitResult)) {
int cancelled = 0;
waitDialog.EndWaitDialog(ref cancelled);
}
}
}
public void AddExistingDirectories(HierarchyNode node, string[] filesDropped) {
List<KeyValuePair<HierarchyNode, HierarchyNode>> addedItems = new List<KeyValuePair<HierarchyNode, HierarchyNode>>();
var oldTriggerFlag = this.EventTriggeringFlag;
EventTriggeringFlag |= ProjectNode.EventTriggering.DoNotTriggerHierarchyEvents;
try {
foreach (var dir in filesDropped) {
AddExistingDirectory(GetOrAddDirectory(node, addedItems, dir), dir, addedItems);
}
} finally {
EventTriggeringFlag = oldTriggerFlag;
}
if (addedItems.Count > 0) {
foreach (var item in addedItems) {
OnItemAdded(item.Key, item.Value);
this.tracker.OnItemAdded(item.Value.Url, VSADDFILEFLAGS.VSADDFILEFLAGS_NoFlags);
}
OnInvalidateItems(node);
SetProjectFileDirty(true);
}
}
private void AddExistingDirectory(HierarchyNode node, string path, List<KeyValuePair<HierarchyNode, HierarchyNode>> addedItems) {
foreach (var dir in Directory.GetDirectories(path)) {
var existingDir = GetOrAddDirectory(node, addedItems, dir);
AddExistingDirectory(existingDir, dir, addedItems);
}
foreach (var file in Directory.GetFiles(path)) {
var existingFile = node.FindImmediateChildByName(Path.GetFileName(file));
if (existingFile == null) {
existingFile = CreateFileNode(file);
addedItems.Add(new KeyValuePair<HierarchyNode, HierarchyNode>(node, existingFile));
node.AddChild(existingFile);
}
}
}
private HierarchyNode GetOrAddDirectory(HierarchyNode node, List<KeyValuePair<HierarchyNode, HierarchyNode>> addedItems, string dir) {
var existingDir = node.FindImmediateChildByName(Path.GetFileName(dir));
if (existingDir == null) {
existingDir = CreateFolderNode(dir);
addedItems.Add(new KeyValuePair<HierarchyNode, HierarchyNode>(node, existingDir));
node.AddChild(existingDir);
}
return existingDir;
}
/// <summary>
/// Get the dropdatatype from the dataobject
/// </summary>
/// <param name="pDataObject">The dataobject to be analysed for its format</param>
/// <returns>dropdatatype or none if dataobject does not contain known format</returns>
public static DropDataType QueryDropDataType(IOleDataObject pDataObject) {
if (pDataObject == null) {
return DropDataType.None;
}
// known formats include File Drops (as from WindowsExplorer),
// VSProject Reference Items and VSProject Storage Items.
FORMATETC fmt = DragDropHelper.CreateFormatEtc(NativeMethods.CF_HDROP);
if (DragDropHelper.QueryGetData(pDataObject, ref fmt) == VSConstants.S_OK) {
return DropDataType.Shell;
}
fmt.cfFormat = DragDropHelper.CF_VSREFPROJECTITEMS;
if (DragDropHelper.QueryGetData(pDataObject, ref fmt) == VSConstants.S_OK) {
// Data is from a Ref-based project.
return DropDataType.VsRef;
}
fmt.cfFormat = DragDropHelper.CF_VSSTGPROJECTITEMS;
if (DragDropHelper.QueryGetData(pDataObject, ref fmt) == VSConstants.S_OK) {
return DropDataType.VsStg;
}
return DropDataType.None;
}
/// <summary>
/// Returns the drop effect.
/// </summary>
/// <remarks>
/// // A directory based project should perform as follow:
/// NO MODIFIER
/// - COPY if not from current hierarchy,
/// - MOVE if from current hierarchy
/// SHIFT DRAG - MOVE
/// CTRL DRAG - COPY
/// CTRL-SHIFT DRAG - NO DROP (used for reference based projects only)
/// </remarks>
public DropEffect QueryDropEffect(uint grfKeyState) {
//Validate the dropdatatype
if ((_dropType != DropDataType.Shell) && (_dropType != DropDataType.VsRef) && (_dropType != DropDataType.VsStg)) {
return DropEffect.None;
}
// CTRL-SHIFT
if ((grfKeyState & NativeMethods.MK_CONTROL) != 0 && (grfKeyState & NativeMethods.MK_SHIFT) != 0) {
// Because we are not referenced base, we don't support link
return DropEffect.None;
}
// CTRL
if ((grfKeyState & NativeMethods.MK_CONTROL) != 0)
return DropEffect.Copy;
// SHIFT
if ((grfKeyState & NativeMethods.MK_SHIFT) != 0)
return DropEffect.Move;
// no modifier
if (SourceDraggedOrCutOrCopied == CopyPasteDragSource.Cut ||
(ItemsDraggedOrCutOrCopied != null &&
ItemsDraggedOrCutOrCopied.Count > 0)) {
return DropEffect.Move;
} else {
return DropEffect.Copy;
}
}
/// <summary>
/// Moves files from one part of our project to another.
/// </summary>
/// <param name="targetNode">the targetHandler node</param>
/// <param name="projectReferences">List of projectref string</param>
/// <returns>true if succeeded</returns>
public bool AddFilesFromProjectReferences(HierarchyNode targetNode, string[] projectReferences, bool mouseDropping, DropEffect dropEffect) {
//Validate input
VsUtilities.ArgumentNotNull("projectReferences", projectReferences);
VsUtilities.CheckNotNull(targetNode);
if (!QueryEditProjectFile(false)) {
throw Marshal.GetExceptionForHR(VSConstants.OLE_E_PROMPTSAVECANCELLED);
}
return new ProjectReferenceFileAdder(this, targetNode, projectReferences, mouseDropping, dropEffect).AddFiles();
}
#endregion
#region private helper methods
/// <summary>
/// Empties all the data structures added to the clipboard and flushes the clipboard.
/// </summary>
private void CleanAndFlushClipboard() {
IOleDataObject oleDataObject = null;
ErrorHandler.ThrowOnFailure(UnsafeNativeMethods.OleGetClipboard(out oleDataObject));
if (oleDataObject == null) {
return;
}
string sourceProjectPath = DragDropHelper.GetSourceProjectPath(oleDataObject);
if (!String.IsNullOrEmpty(sourceProjectPath) && CommonUtils.IsSamePath(sourceProjectPath, this.GetMkDocument())) {
ErrorHandler.ThrowOnFailure(UnsafeNativeMethods.OleFlushClipboard());
int clipboardOpened = 0;
try {
ErrorHandler.ThrowOnFailure(clipboardOpened = UnsafeNativeMethods.OpenClipboard(IntPtr.Zero));
ErrorHandler.ThrowOnFailure(UnsafeNativeMethods.EmptyClipboard());
} finally {
if (clipboardOpened == 1) {
ErrorHandler.ThrowOnFailure(UnsafeNativeMethods.CloseClipboard());
}
}
}
}
private IntPtr PackageSelectionData(StringBuilder sb, bool addEndFormatDelimiter) {
if (sb == null || sb.ToString().Length == 0 || this.ItemsDraggedOrCutOrCopied.Count == 0) {
return IntPtr.Zero;
}
// Double null at end.
if (addEndFormatDelimiter) {
if (sb.ToString()[sb.Length - 1] != '\0') {
sb.Append('\0');
}
}
// We request unmanaged permission to execute the below.
new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Demand();
_DROPFILES df = new _DROPFILES();
int dwSize = Marshal.SizeOf(df);
Int16 wideChar = 0;
int dwChar = Marshal.SizeOf(wideChar);
int structSize = dwSize + ((sb.Length + 1) * dwChar);
IntPtr ptr = Marshal.AllocHGlobal(structSize);
df.pFiles = dwSize;
df.fWide = 1;
IntPtr data = IntPtr.Zero;
try {
data = UnsafeNativeMethods.GlobalLock(ptr);
Marshal.StructureToPtr(df, data, false);
IntPtr strData = new IntPtr((long)data + dwSize);
DragDropHelper.CopyStringToHGlobal(sb.ToString(), strData, structSize);
} finally {
if (data != IntPtr.Zero)
UnsafeNativeMethods.GlobalUnLock(data);
}
return ptr;
}
#endregion
}
}