/* **************************************************************************** * * 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 { /// /// Manages the CopyPaste and Drag and Drop scenarios for a Project. /// /// This is a partial class. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")] public partial class ProjectNode : IVsUIHierWinClipboardHelperEvents { private uint copyPasteCookie; private DropDataType _dropType; #region override of IVsHierarchyDropDataTarget methods /// /// Called as soon as the mouse drags an item over a new hierarchy or hierarchy window /// /// reference to interface IDataObject of the item being dragged /// Current state of the keyboard and the mouse modifier keys. See docs for a list of possible values /// Item identifier for the item currently being dragged /// On entry, a pointer to the current DropEffect. On return, must contain the new valid DropEffect /// If the method succeeds, it returns S_OK. If it fails, it returns an error code. 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; } /// /// 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. /// /// If the method succeeds, it returns S_OK. If it fails, it returns an error code. public int DragLeave() { _dropType = DropDataType.None; return VSConstants.S_OK; } /// /// Called when one or more items are dragged over the target hierarchy or hierarchy window. /// /// Current state of the keyboard keys and the mouse modifier buttons. See /// Item identifier of the drop data target over which the item is being dragged /// 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 /// If the method succeeds, it returns S_OK. If it fails, it returns an error code. 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; } /// /// Called when one or more items are dropped into the target hierarchy or hierarchy window when the mouse button is released. /// /// 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. /// Current state of the keyboard and the mouse modifier keys. See /// Item identifier of the drop data target over which the item is being dragged /// 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 /// If the method succeeds, it returns S_OK. If it fails, it returns an error code. 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 /// /// Returns information about one or more of the items being dragged /// /// 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. /// 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. /// Pointer to the IDropSource interface of the item being dragged. /// If the method succeeds, it returns S_OK. If it fails, it returns an error code. 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; } /// /// Notifies clients that the dragged item was dropped. /// /// If true, then the dragged item was dropped on the target. If false, then the drop did not occur. /// 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. /// If the method succeeds, it returns S_OK. If it fails, it returns an error code. 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; } /// /// 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. /// /// 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. /// Current state of the keyboard and the mouse modifier keys. /// If true, then the drop is cancelled by the source hierarchy. If false, then the drop can continue. /// If the method succeeds, it returns S_OK. If it fails, it returns an error code. 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 /// /// Called after your cut/copied items has been pasted /// ///If true, then the IDataObject has been successfully pasted into a target hierarchy. /// If false, then the cut or copy operation was cancelled. /// 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 /// If the method succeeds, it returns S_OK. If it fails, it returns an error code. 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; } /// /// Called when your cut/copied operation is canceled /// /// 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. /// If the method succeeds, it returns S_OK. If it fails, it returns an error code. 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 /// /// Returns a dataobject from selected nodes /// /// boolean that defines if the selected items must be cut /// data object for selected items private DataObject PackageSelectionDataObject(bool cutHighlightItems) { StringBuilder sb = new StringBuilder(); DataObject dataObject = null; IList 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 { /// /// This hierarchy which is having items added/moved /// private readonly ProjectNode Project; /// /// The node which we're adding/moving the items to /// private readonly HierarchyNode TargetNode; /// /// The references we're adding, using the format {Guid}|project|folderPath /// private readonly string[] ProjectReferences; /// /// True if this is the result of a mouse drop, false if this is the result of a paste /// private readonly bool MouseDropping; /// /// Move or Copy /// 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 additions = new List(); List folders = new List(); // 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; } /// /// Tests to see if we can add the folder to the project. Returns true if it's ok, false if it's not. /// /// Project reference (from data object) using the format: {Guid}|project|folderPath /// Node to add the new folder to 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 additions = new List(); 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); } /// /// 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. /// /// Hierarchy to walk /// Item ID where to start walking the hierarchy /// Node to start adding to /// Typically false on first call and true after that private bool WalkSourceProjectAndAdd(IVsHierarchy sourceHierarchy, uint itemId, string targetPath, bool addSiblings, List 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 nestedAdditions = new List(); 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; } /// /// 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. /// 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); } } } /// /// Given the reference used for drag and drop returns the path to the item and it's /// containing hierarchy. /// /// /// /// 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); } /// /// Adds an item from a project refererence to target node. /// /// /// 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(); } } } /// /// Add an existing item (file/folder) to the project if it already exist in our storage. /// /// Node to that this item to /// Name of the item being added /// Path of the item being added /// Node that was added 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 /// /// Handle the Cut operation to the clipboard /// 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; } /// /// Handle the Copy operation to the clipboard /// 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; } /// /// Handle the Paste operation to a targetNode /// 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; } /// /// Determines if the paste command should be allowed. /// /// 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; } } /// /// Register/Unregister for Clipboard events for the UiHierarchyWindow (solution explorer) /// /// true for register, false for unregister 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; } } /// /// Process dataobject from Drag/Drop/Cut/Copy/Paste operation /// /// drop indicates if it is a drag/drop or a cut/copy/paste. /// /// The targetNode is set if the method is called from a drop operation, otherwise it is null 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 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> addedItems = new List>(); 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> 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(node, existingFile)); node.AddChild(existingFile); } } } private HierarchyNode GetOrAddDirectory(HierarchyNode node, List> addedItems, string dir) { var existingDir = node.FindImmediateChildByName(Path.GetFileName(dir)); if (existingDir == null) { existingDir = CreateFolderNode(dir); addedItems.Add(new KeyValuePair(node, existingDir)); node.AddChild(existingDir); } return existingDir; } /// /// Get the dropdatatype from the dataobject /// /// The dataobject to be analysed for its format /// dropdatatype or none if dataobject does not contain known format 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; } /// /// Returns the drop effect. /// /// /// // 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) /// 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; } } /// /// Moves files from one part of our project to another. /// /// the targetHandler node /// List of projectref string /// true if succeeded 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 /// /// Empties all the data structures added to the clipboard and flushes the clipboard. /// 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 } }