Replaced the old VS templates with ones that offer more flexiblity. Started replacing the Content Project for the samples with our custom project type. Inlcuded a basic not yet working AssimpImporter.
1780 lines
72 KiB
C#
1780 lines
72 KiB
C#
/* ****************************************************************************
|
|
*
|
|
* 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.Generic;
|
|
using System.ComponentModel.Design;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.Contracts;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using System.Windows.Threading;
|
|
using Microsoft.VisualStudio;
|
|
using Microsoft.VisualStudio.Shell;
|
|
using Microsoft.VisualStudio.Shell.Interop;
|
|
using Microsoft.VisualStudio.Navigation;
|
|
using Microsoft.VisualStudio.Project.Automation;
|
|
using MSBuild = Microsoft.Build.Evaluation;
|
|
using VsCommands2K = Microsoft.VisualStudio.VSConstants.VSStd2KCmdID;
|
|
using VSConstants = Microsoft.VisualStudio.VSConstants;
|
|
|
|
namespace Microsoft.VisualStudio.Project {
|
|
|
|
public enum CommonImageName {
|
|
File = 0,
|
|
Project = 1,
|
|
SearchPathContainer,
|
|
SearchPath,
|
|
MissingSearchPath,
|
|
StartupFile,
|
|
InterpretersContainer = SearchPathContainer,
|
|
Interpreter = SearchPath,
|
|
InterpretersPackage = SearchPath
|
|
}
|
|
|
|
public abstract class CommonProjectNode : ProjectNode, IVsProjectSpecificEditorMap2, IVsDeferredSaveProject {
|
|
private CommonProjectPackage/*!*/ _package;
|
|
private Guid _mruPageGuid = new Guid(CommonConstants.AddReferenceMRUPageGuid);
|
|
private VSLangProj.VSProject _vsProject = null;
|
|
private static ImageList _imageList;
|
|
private ProjectDocumentsListenerForStartupFileUpdates _projectDocListenerForStartupFileUpdates;
|
|
private static int _imageOffset;
|
|
private FileSystemWatcher _watcher, _attributesWatcher;
|
|
private int _suppressFileWatcherCount;
|
|
private bool _isRefreshing;
|
|
public bool _boldedStartupItem;
|
|
private bool _showingAllFiles;
|
|
private object _automationObject;
|
|
private PropertyPage _propPage;
|
|
private readonly Dictionary<string, FileSystemEventHandler> _fileChangedHandlers = new Dictionary<string, FileSystemEventHandler>();
|
|
private Queue<FileSystemChange> _fileSystemChanges = new Queue<FileSystemChange>();
|
|
private object _fileSystemChangesLock = new object();
|
|
public UIThreadSynchronizer _uiSync;
|
|
private MSBuild.Project userBuildProject;
|
|
private readonly Dictionary<string, FileSystemWatcher> _symlinkWatchers = new Dictionary<string, FileSystemWatcher>();
|
|
private DiskMerger _currentMerger;
|
|
private int _idleTriggered;
|
|
|
|
public CommonProjectNode(CommonProjectPackage/*!*/ package, ImageList/*!*/ imageList)
|
|
: base(package)
|
|
{
|
|
Contract.Assert(package != null);
|
|
Contract.Assert(imageList != null);
|
|
|
|
_package = package;
|
|
CanFileNodesHaveChilds = true;
|
|
OleServiceProvider.AddService(typeof(VSLangProj.VSProject), new OleServiceProvider.ServiceCreatorCallback(CreateServices), false);
|
|
SupportsProjectDesigner = true;
|
|
_imageList = imageList;
|
|
|
|
//Store the number of images in ProjectNode so we know the offset of the language icons.
|
|
_imageOffset = ImageHandler.ImageList.Images.Count;
|
|
foreach (Image img in ImageList.Images) {
|
|
ImageHandler.AddImage(img);
|
|
}
|
|
|
|
InitializeCATIDs();
|
|
|
|
_uiSync = new UIThreadSynchronizer();
|
|
package.OnIdle += OnIdle;
|
|
}
|
|
|
|
#region abstract methods
|
|
|
|
public abstract Type GetEditorFactoryType();
|
|
public abstract string GetProjectName();
|
|
|
|
public virtual CommonFileNode CreateCodeFileNode(ProjectElement item) {
|
|
return new CommonFileNode(this, item);
|
|
}
|
|
public virtual CommonFileNode CreateNonCodeFileNode(ProjectElement item) {
|
|
return new CommonNonCodeFileNode(this, item);
|
|
}
|
|
public abstract string GetFormatList();
|
|
public abstract Type GetLibraryManagerType();
|
|
public abstract Type GetProjectFactoryType();
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
public new CommonProjectPackage/*!*/ Package {
|
|
get { return _package; }
|
|
}
|
|
|
|
public static int ImageOffset {
|
|
get { return _imageOffset; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the VSProject corresponding to this project
|
|
/// </summary>
|
|
public VSLangProj.VSProject VSProject {
|
|
get {
|
|
if (_vsProject == null)
|
|
_vsProject = new OAVSProject(this);
|
|
return _vsProject;
|
|
}
|
|
}
|
|
|
|
private IVsHierarchy InteropSafeHierarchy {
|
|
get {
|
|
IntPtr unknownPtr = VsUtilities.QueryInterfaceIUnknown(this);
|
|
if (IntPtr.Zero == unknownPtr) {
|
|
return null;
|
|
}
|
|
IVsHierarchy hier = Marshal.GetObjectForIUnknown(unknownPtr) as IVsHierarchy;
|
|
return hier;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates whether the project is currently is busy refreshing its hierarchy.
|
|
/// </summary>
|
|
public bool IsRefreshing {
|
|
get { return _isRefreshing; }
|
|
set { _isRefreshing = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Language specific project images
|
|
/// </summary>
|
|
public static ImageList ImageList {
|
|
get {
|
|
return _imageList;
|
|
}
|
|
set {
|
|
_imageList = value;
|
|
}
|
|
}
|
|
|
|
public PropertyPage PropertyPage
|
|
{
|
|
get { return _propPage; }
|
|
set { _propPage = value; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region overridden properties
|
|
|
|
public override bool CanShowAllFiles {
|
|
get {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public override bool IsShowingAllFiles {
|
|
get {
|
|
return _showingAllFiles;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Since we appended the language images to the base image list in the constructor,
|
|
/// this should be the offset in the ImageList of the langauge project icon.
|
|
/// </summary>
|
|
public override int ImageIndex {
|
|
get {
|
|
return _imageOffset + (int)CommonImageName.Project;
|
|
}
|
|
}
|
|
|
|
public override Guid ProjectGuid {
|
|
get {
|
|
return GetProjectFactoryType().GUID;
|
|
}
|
|
}
|
|
public override string ProjectType {
|
|
get {
|
|
return GetProjectName();
|
|
}
|
|
}
|
|
public override object Object {
|
|
get {
|
|
return null;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region overridden methods
|
|
|
|
public override object GetAutomationObject() {
|
|
if (_automationObject == null) {
|
|
_automationObject = base.GetAutomationObject();
|
|
}
|
|
return _automationObject;
|
|
}
|
|
|
|
public override int QueryStatusOnNode(Guid cmdGroup, uint cmd, IntPtr pCmdText, ref QueryStatusResult result) {
|
|
if (cmdGroup == CommonConstants.Std97CmdGroupGuid) {
|
|
switch ((VSConstants.VSStd97CmdID)cmd) {
|
|
case VSConstants.VSStd97CmdID.BuildCtx:
|
|
case VSConstants.VSStd97CmdID.RebuildCtx:
|
|
case VSConstants.VSStd97CmdID.CleanCtx:
|
|
result = QueryStatusResult.SUPPORTED | QueryStatusResult.INVISIBLE;
|
|
return VSConstants.S_OK;
|
|
}
|
|
} else if (cmdGroup == Microsoft.VisualStudio.Project.VsMenus.guidStandardCommandSet2K) {
|
|
switch ((VsCommands2K)cmd) {
|
|
case VsCommands2K.ECMD_PUBLISHSELECTION:
|
|
if (pCmdText != IntPtr.Zero && NativeMethods.OLECMDTEXT.GetFlags(pCmdText) == NativeMethods.OLECMDTEXT.OLECMDTEXTF.OLECMDTEXTF_NAME) {
|
|
NativeMethods.OLECMDTEXT.SetText(pCmdText, "Publish " + this.Caption);
|
|
}
|
|
|
|
if (IsPublishingEnabled) {
|
|
result |= QueryStatusResult.SUPPORTED | QueryStatusResult.ENABLED;
|
|
} else {
|
|
result |= QueryStatusResult.SUPPORTED;
|
|
}
|
|
return VSConstants.S_OK;
|
|
|
|
case VsCommands2K.ECMD_PUBLISHSLNCTX:
|
|
if (IsPublishingEnabled) {
|
|
result |= QueryStatusResult.SUPPORTED | QueryStatusResult.ENABLED;
|
|
} else {
|
|
result |= QueryStatusResult.SUPPORTED;
|
|
}
|
|
return VSConstants.S_OK;
|
|
case CommonConstants.OpenFolderInExplorerCmdId:
|
|
result |= QueryStatusResult.SUPPORTED | QueryStatusResult.ENABLED;
|
|
return VSConstants.S_OK;
|
|
}
|
|
} else if (cmdGroup == SharedCommandGuid) {
|
|
switch ((SharedCommands)cmd) {
|
|
case SharedCommands.AddExistingFolder:
|
|
result |= QueryStatusResult.SUPPORTED | QueryStatusResult.ENABLED;
|
|
return VSConstants.S_OK;
|
|
}
|
|
}
|
|
|
|
return base.QueryStatusOnNode(cmdGroup, cmd, pCmdText, ref result);
|
|
}
|
|
|
|
private bool IsPublishingEnabled {
|
|
get {
|
|
return !String.IsNullOrWhiteSpace(GetProjectProperty(CommonConstants.PublishUrl));
|
|
}
|
|
}
|
|
|
|
public override int ExecCommandOnNode(Guid cmdGroup, uint cmd, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) {
|
|
if (cmdGroup == Microsoft.VisualStudio.Project.VsMenus.guidStandardCommandSet2K) {
|
|
switch ((VsCommands2K)cmd) {
|
|
case VsCommands2K.ECMD_PUBLISHSELECTION:
|
|
case VsCommands2K.ECMD_PUBLISHSLNCTX:
|
|
Publish(PublishProjectOptions.Default, true);
|
|
return VSConstants.S_OK;
|
|
case CommonConstants.OpenFolderInExplorerCmdId:
|
|
Process.Start(this.ProjectHome);
|
|
return VSConstants.S_OK;
|
|
}
|
|
} else if (cmdGroup == SharedCommandGuid) {
|
|
switch ((SharedCommands)cmd) {
|
|
case SharedCommands.AddExistingFolder:
|
|
return AddExistingFolderToNode(this);
|
|
}
|
|
}
|
|
return base.ExecCommandOnNode(cmdGroup, cmd, nCmdexecopt, pvaIn, pvaOut);
|
|
}
|
|
|
|
public int AddExistingFolderToNode(HierarchyNode parent) {
|
|
var dir = BrowseForFolder(String.Format("Add Existing Folder - {0}",Caption), parent.FullPathToChildren);
|
|
if (dir != null) {
|
|
DropFilesOrFolders(new[] { dir }, parent);
|
|
}
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publishes the project as configured by the user in the Publish option page.
|
|
///
|
|
/// If async is true this function begins the publishing and returns w/o waiting for it to complete. No errors are reported.
|
|
///
|
|
/// If async is false this function waits for the publish to finish and raises a PublishFailedException with an
|
|
/// inner exception indicating the underlying reason for the publishing failure.
|
|
///
|
|
/// Returns true if the publish was succeessfully started, false if the project is not configured for publishing
|
|
/// </summary>
|
|
public bool Publish(PublishProjectOptions publishOptions, bool async) {
|
|
string publishUrl = publishOptions.DestinationUrl ?? GetProjectProperty(CommonConstants.PublishUrl);
|
|
bool found = false;
|
|
if (!String.IsNullOrWhiteSpace(publishUrl)) {
|
|
var url = new Url(publishUrl);
|
|
|
|
var publishers = CommonPackage.ComponentModel.GetExtensions<IProjectPublisher>();
|
|
foreach (var publisher in publishers) {
|
|
if (publisher.Schema == url.Uri.Scheme) {
|
|
var project = new PublishProject(this, publishOptions);
|
|
Exception failure = null;
|
|
var frame = new DispatcherFrame();
|
|
var thread = new System.Threading.Thread(x => {
|
|
try {
|
|
publisher.PublishFiles(project, url.Uri);
|
|
project.Done();
|
|
frame.Continue = false;
|
|
} catch (Exception e) {
|
|
failure = e;
|
|
project.Failed(e.Message);
|
|
frame.Continue = false;
|
|
}
|
|
});
|
|
thread.Start();
|
|
found = true;
|
|
if (!async) {
|
|
Dispatcher.PushFrame(frame);
|
|
if (failure != null) {
|
|
throw new PublishFailedException(String.Format("Publishing of the project {0} failed", Caption), failure);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
var statusBar = (IVsStatusbar)CommonPackage.GetGlobalService(typeof(SVsStatusbar));
|
|
statusBar.SetText(String.Format("Publish failed: Unknown publish scheme ({0})", url.Uri.Scheme));
|
|
}
|
|
} else {
|
|
var statusBar = (IVsStatusbar)CommonPackage.GetGlobalService(typeof(SVsStatusbar));
|
|
statusBar.SetText(String.Format("Project is not configured for publishing in project properties."));
|
|
}
|
|
return found;
|
|
}
|
|
|
|
public virtual CommonConfig MakeConfiguration(string activeConfigName, string activePlatformName) {
|
|
return new CommonConfig(this, activeConfigName, activePlatformName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// As we don't register files/folders in the project file, removing an item is a noop.
|
|
/// </summary>
|
|
public override int RemoveItem(uint reserved, uint itemId, out int result) {
|
|
result = 1;
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
public override void BuildAsync(uint vsopts, string config, string platform, IVsOutputWindowPane output, string target, IEnumerable<string> files, Action<MSBuildResult, string> uiThreadCallback) {
|
|
uiThreadCallback(MSBuildResult.Successful, target);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overriding main project loading method to inject our hierarachy of nodes.
|
|
/// </summary>
|
|
protected override void Reload() {
|
|
base.Reload();
|
|
|
|
OnProjectPropertyChanged += CommonProjectNode_OnProjectPropertyChanged;
|
|
|
|
// track file creation/deletes and update our glyphs when files start/stop existing for files in the project.
|
|
if (_watcher != null) {
|
|
_watcher.EnableRaisingEvents = false;
|
|
_watcher.Dispose();
|
|
}
|
|
|
|
string userProjectFilename = FileName + ".user";
|
|
if (File.Exists(userProjectFilename)) {
|
|
userBuildProject = BuildProject.ProjectCollection.LoadProject(userProjectFilename);
|
|
}
|
|
|
|
bool? showAllFiles = null;
|
|
if (userBuildProject != null) {
|
|
showAllFiles = GetShowAllFilesSetting(userBuildProject.GetPropertyValue(CommonConstants.ProjectView));
|
|
}
|
|
|
|
_showingAllFiles = showAllFiles ??
|
|
GetShowAllFilesSetting(BuildProject.GetPropertyValue(CommonConstants.ProjectView)) ??
|
|
false;
|
|
|
|
_watcher = CreateFileSystemWatcher(ProjectHome);
|
|
_attributesWatcher = CreateAttributesWatcher(ProjectHome);
|
|
|
|
// add everything that's on disk that we don't have in the project
|
|
MergeDiskNodes(this, ProjectHome);
|
|
|
|
this.SetProjectFileDirty(false);
|
|
}
|
|
|
|
private FileSystemWatcher CreateFileSystemWatcher(string dir) {
|
|
var watcher = new FileSystemWatcher(dir);
|
|
watcher.IncludeSubdirectories = true;
|
|
watcher.Created += new FileSystemEventHandler(FileExistanceChanged);
|
|
watcher.Deleted += new FileSystemEventHandler(FileExistanceChanged);
|
|
watcher.Renamed += new RenamedEventHandler(FileNameChanged);
|
|
watcher.Changed += FileContentsChanged;
|
|
watcher.Error += WatcherError;
|
|
watcher.EnableRaisingEvents = true;
|
|
watcher.InternalBufferSize = 1024 * 4; // 4k is minimum buffer size
|
|
return watcher;
|
|
}
|
|
|
|
private FileSystemWatcher CreateAttributesWatcher(string dir) {
|
|
var watcher = new FileSystemWatcher(dir);
|
|
watcher.NotifyFilter = NotifyFilters.Attributes;
|
|
watcher.Changed += FileAttributesChanged;
|
|
watcher.Error += WatcherError;
|
|
watcher.EnableRaisingEvents = true;
|
|
watcher.InternalBufferSize = 1024 * 4; // 4k is minimum buffer size
|
|
return watcher;
|
|
}
|
|
|
|
/// <summary>
|
|
/// When the file system watcher buffer overflows we need to scan the entire
|
|
/// directory for changes.
|
|
/// </summary>
|
|
private void WatcherError(object sender, ErrorEventArgs e) {
|
|
lock (_fileSystemChanges) {
|
|
_fileSystemChanges.Clear(); // none of the other changes matter now, we'll rescan the world
|
|
_currentMerger = null; // abort any current merge now that we have a new one
|
|
_fileSystemChanges.Enqueue(new FileSystemChange(this, WatcherChangeTypes.All, null));
|
|
TriggerIdle();
|
|
}
|
|
}
|
|
|
|
protected override void SaveMSBuildProjectFileAs(string newFileName) {
|
|
base.SaveMSBuildProjectFileAs(newFileName);
|
|
|
|
if (userBuildProject != null) {
|
|
userBuildProject.Save(FileName + ".user");
|
|
}
|
|
}
|
|
|
|
protected override void SaveMSBuildProjectFile(string filename) {
|
|
base.SaveMSBuildProjectFile(filename);
|
|
|
|
if (userBuildProject != null) {
|
|
userBuildProject.Save(filename + ".user");
|
|
}
|
|
}
|
|
|
|
protected override void Dispose(bool disposing) {
|
|
base.Dispose(disposing);
|
|
if (this.userBuildProject != null) {
|
|
userBuildProject.ProjectCollection.UnloadProject(userBuildProject);
|
|
}
|
|
_package.OnIdle -= OnIdle;
|
|
}
|
|
|
|
public override int ShowAllFiles() {
|
|
if (!QueryEditProjectFile(false)) {
|
|
return VSConstants.E_FAIL;
|
|
}
|
|
|
|
if (_showingAllFiles) {
|
|
UpdateShowAllFiles(this, enabled: false);
|
|
} else {
|
|
UpdateShowAllFiles(this, enabled: true);
|
|
ExpandItem(EXPANDFLAGS.EXPF_ExpandFolder);
|
|
}
|
|
|
|
_showingAllFiles = !_showingAllFiles;
|
|
|
|
string newPropValue = _showingAllFiles ? CommonConstants.ShowAllFiles : CommonConstants.ProjectFiles;
|
|
|
|
var projProperty = BuildProject.GetProperty(CommonConstants.ProjectView);
|
|
if (projProperty != null &&
|
|
!projProperty.IsImported &&
|
|
!String.IsNullOrWhiteSpace(projProperty.EvaluatedValue)) {
|
|
// setting is persisted in main project file, update it there.
|
|
BuildProject.SetProperty(CommonConstants.ProjectView, newPropValue);
|
|
} else {
|
|
// save setting in user project file
|
|
if (userBuildProject == null) {
|
|
// user project file doesn't exist yet, create it.
|
|
userBuildProject = new MSBuild.Project(BuildProject.ProjectCollection);
|
|
userBuildProject.FullPath = FileName + ".user";
|
|
}
|
|
userBuildProject.SetProperty(CommonConstants.ProjectView, newPropValue);
|
|
|
|
}
|
|
SetProjectFileDirty(true);
|
|
|
|
// update project state
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
private void UpdateShowAllFiles(HierarchyNode node, bool enabled) {
|
|
for (var curNode = node.FirstChild; curNode != null; curNode = curNode.NextSibling) {
|
|
UpdateShowAllFiles(curNode, enabled);
|
|
|
|
var allFiles = curNode.ItemNode as AllFilesProjectElement;
|
|
if (allFiles != null) {
|
|
curNode.IsVisible = enabled;
|
|
OnInvalidateItems(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool? GetShowAllFilesSetting(string showAllFilesValue) {
|
|
bool? showAllFiles = null;
|
|
string showAllFilesSetting = showAllFilesValue.Trim();
|
|
if (String.Equals(showAllFilesSetting, CommonConstants.ProjectFiles)) {
|
|
showAllFiles = false;
|
|
} else if (String.Equals(showAllFilesSetting, CommonConstants.ShowAllFiles)) {
|
|
showAllFiles = true;
|
|
}
|
|
return showAllFiles;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs merging of the file system state with the current project hierarchy, bringing them
|
|
/// back into sync.
|
|
///
|
|
/// The class can be created, and ContinueMerge should be called until it returns false, at which
|
|
/// point the file system has been merged.
|
|
///
|
|
/// You can wait between calls to ContinueMerge to enable not blocking the UI.
|
|
///
|
|
/// If there were changes which came in while the DiskMerger was processing then those changes will still need
|
|
/// to be processed after the DiskMerger completes.
|
|
/// </summary>
|
|
class DiskMerger {
|
|
private readonly string _initialDir;
|
|
private readonly Stack<DirState> _remainingDirs = new Stack<DirState>();
|
|
private readonly CommonProjectNode _project;
|
|
|
|
public DiskMerger(CommonProjectNode project, HierarchyNode parent, string dir) {
|
|
_project = project;
|
|
_initialDir = dir;
|
|
_remainingDirs.Push(new DirState(dir, parent));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Continues processing the merge request, performing a portion of the full merge possibly
|
|
/// returning before the merge has completed.
|
|
///
|
|
/// Returns true if the merge needs to continue, or false if the merge has completed.
|
|
/// </summary>
|
|
public bool ContinueMerge() {
|
|
if (_remainingDirs.Count == 0 || // all done
|
|
!Directory.Exists(_initialDir)) { // directory went away
|
|
return false;
|
|
}
|
|
|
|
var dir = _remainingDirs.Pop();
|
|
Debug.WriteLine(dir.Name);
|
|
if (!Directory.Exists(dir.Name)) {
|
|
return true;
|
|
}
|
|
|
|
HashSet<HierarchyNode> missingChildren = new HashSet<HierarchyNode>(dir.Parent.AllChildren);
|
|
IEnumerable<string> dirs;
|
|
try {
|
|
dirs = Directory.EnumerateDirectories(dir.Name);
|
|
} catch {
|
|
// directory was deleted, we don't have access, etc...
|
|
return true;
|
|
}
|
|
|
|
foreach (var curDir in dirs) {
|
|
if (_project.IsFileHidden(curDir)) {
|
|
continue;
|
|
}
|
|
if (IsFileSymLink(curDir)) {
|
|
if (IsRecursiveSymLink(_initialDir, curDir)) {
|
|
// don't add recursive sym links
|
|
continue;
|
|
}
|
|
|
|
// track symlinks, we won't get events on the directory
|
|
_project.CreateSymLinkWatcher(curDir);
|
|
}
|
|
|
|
var existing = _project.AddAllFilesFolder(dir.Parent, curDir + Path.DirectorySeparatorChar);
|
|
missingChildren.Remove(existing);
|
|
_remainingDirs.Push(new DirState(curDir, existing));
|
|
}
|
|
|
|
IEnumerable<string> files;
|
|
try {
|
|
files = Directory.EnumerateFiles(dir.Name);
|
|
} catch {
|
|
// directory was deleted, we don't have access, etc...
|
|
return true;
|
|
}
|
|
|
|
foreach (var file in files) {
|
|
if (!Directory.Exists(dir.Name)) {
|
|
// file went away
|
|
break;
|
|
}
|
|
if (_project.IsFileHidden(file)) {
|
|
continue;
|
|
}
|
|
missingChildren.Remove(_project.AddAllFilesFile(dir.Parent, file));
|
|
}
|
|
|
|
// remove the excluded children which are no longer there
|
|
foreach (var child in missingChildren) {
|
|
if (child.ItemNode.IsExcluded) {
|
|
_project.RemoveSubTree(child);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
class DirState {
|
|
public readonly string Name;
|
|
public readonly HierarchyNode Parent;
|
|
|
|
public DirState(string name, HierarchyNode parent) {
|
|
Name = name;
|
|
Parent = parent;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void MergeDiskNodes(HierarchyNode curParent, string dir) {
|
|
var merger = new DiskMerger(this, curParent, dir);
|
|
while (merger.ContinueMerge()) {
|
|
}
|
|
}
|
|
|
|
private void RemoveSubTree(HierarchyNode node) {
|
|
foreach (var child in node.AllChildren) {
|
|
RemoveSubTree(child);
|
|
}
|
|
node.Parent.RemoveChild(node);
|
|
_diskNodes.Remove(node.Url);
|
|
}
|
|
|
|
private static string GetFinalPathName(string dir) {
|
|
using (var dirHandle = NativeMethods.CreateFile(
|
|
dir,
|
|
NativeMethods.FileDesiredAccess.FILE_LIST_DIRECTORY,
|
|
NativeMethods.FileShareFlags.FILE_SHARE_DELETE |
|
|
NativeMethods.FileShareFlags.FILE_SHARE_READ |
|
|
NativeMethods.FileShareFlags.FILE_SHARE_WRITE,
|
|
IntPtr.Zero,
|
|
NativeMethods.FileCreationDisposition.OPEN_EXISTING,
|
|
NativeMethods.FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS,
|
|
IntPtr.Zero
|
|
)) {
|
|
if (!dirHandle.IsInvalid) {
|
|
uint pathLen = NativeMethods.MAX_PATH + 1;
|
|
uint res;
|
|
StringBuilder filePathBuilder;
|
|
for (; ; ) {
|
|
filePathBuilder = new StringBuilder(checked((int)pathLen));
|
|
res = NativeMethods.GetFinalPathNameByHandle(
|
|
dirHandle,
|
|
filePathBuilder,
|
|
pathLen,
|
|
0
|
|
);
|
|
if (res != 0 && res < pathLen) {
|
|
// we had enough space, and got the filename.
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (res != 0) {
|
|
Debug.Assert(filePathBuilder.ToString().StartsWith("\\\\?\\"));
|
|
return filePathBuilder.ToString().Substring(4);
|
|
}
|
|
}
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
private static bool IsRecursiveSymLink(string parentDir, string childDir) {
|
|
if (IsFileSymLink(parentDir)) {
|
|
// figure out the real parent dir so the check below works w/ multiple
|
|
// symlinks pointing at each other
|
|
parentDir = GetFinalPathName(parentDir);
|
|
}
|
|
|
|
string finalPath = GetFinalPathName(childDir);
|
|
// check and see if we're recursing infinitely...
|
|
if (CommonUtils.IsSubpathOf(finalPath, parentDir)) {
|
|
// skip this file
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static bool IsFileSymLink(string path) {
|
|
try {
|
|
return (File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0;
|
|
} catch (UnauthorizedAccessException) {
|
|
return false;
|
|
} catch (DirectoryNotFoundException) {
|
|
return false;
|
|
} catch (FileNotFoundException) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private bool IsFileHidden(string path) {
|
|
if (String.Equals(path, FileName, StringComparison.OrdinalIgnoreCase) ||
|
|
String.Equals(path, FileName + ".user", StringComparison.OrdinalIgnoreCase)) {
|
|
return true;
|
|
}
|
|
|
|
if(!File.Exists(path) && !Directory.Exists(path)) {
|
|
// if the file has disappeared avoid the exception...
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return (File.GetAttributes(path) & (FileAttributes.Hidden | FileAttributes.System)) != 0;
|
|
} catch (UnauthorizedAccessException) {
|
|
return false;
|
|
} catch (DirectoryNotFoundException) {
|
|
return false;
|
|
} catch (FileNotFoundException) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a file which is displayed when Show All Files is enabled
|
|
/// </summary>
|
|
private HierarchyNode AddAllFilesFile(HierarchyNode curParent, string file) {
|
|
var existing = FindNodeByFullPath(file);
|
|
if (existing == null) {
|
|
var newFile = CreateFileNode(new AllFilesProjectElement(file, GetItemType(file), this));
|
|
AddAllFilesNode(curParent, newFile);
|
|
return newFile;
|
|
}
|
|
return existing;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a folder which is displayed when Show All files is enabled
|
|
/// </summary>
|
|
private HierarchyNode AddAllFilesFolder(HierarchyNode curParent, string curDir) {
|
|
var folderNode = FindNodeByFullPath(curDir);
|
|
if (folderNode == null) {
|
|
folderNode = CreateFolderNode(new AllFilesProjectElement(curDir, "Folder", this));
|
|
AddAllFilesNode(curParent, folderNode);
|
|
|
|
// Solution Explorer will expand the parent when an item is
|
|
// added, which we don't want
|
|
folderNode.ExpandItem(EXPANDFLAGS.EXPF_CollapseFolder);
|
|
}
|
|
return folderNode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes and adds a file or folder visible only when Show All files is enabled
|
|
/// </summary>
|
|
private void AddAllFilesNode(HierarchyNode parent, HierarchyNode newNode) {
|
|
newNode.IsVisible = IsShowingAllFiles;
|
|
newNode.ID = ItemIdMap.Add(newNode);
|
|
parent.AddChild(newNode);
|
|
}
|
|
|
|
private void FileContentsChanged(object sender, FileSystemEventArgs e) {
|
|
if (IsClosed) {
|
|
return;
|
|
}
|
|
|
|
FileSystemEventHandler handler;
|
|
if (_fileChangedHandlers.TryGetValue(e.FullPath, out handler)) {
|
|
handler(sender, e);
|
|
}
|
|
}
|
|
|
|
private void FileAttributesChanged(object sender, FileSystemEventArgs e) {
|
|
lock (_fileSystemChanges) {
|
|
if (NoPendingFileSystemRescan()) {
|
|
_fileSystemChanges.Enqueue(new FileSystemChange(this, WatcherChangeTypes.Changed, e.FullPath));
|
|
TriggerIdle();
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool NoPendingFileSystemRescan() {
|
|
return _fileSystemChanges.Count == 0 || _fileSystemChanges.Peek()._type != WatcherChangeTypes.All;
|
|
}
|
|
|
|
public void RegisterFileChangeNotification(FileNode node, FileSystemEventHandler handler) {
|
|
_fileChangedHandlers[node.Url] = handler;
|
|
}
|
|
|
|
public void UnregisterFileChangeNotification(FileNode node) {
|
|
_fileChangedHandlers.Remove(node.Url);
|
|
}
|
|
|
|
protected override ReferenceContainerNode CreateReferenceContainerNode() {
|
|
return new CommonReferenceContainerNode(this);
|
|
}
|
|
|
|
private void FileNameChanged(object sender, RenamedEventArgs e) {
|
|
if (IsClosed) {
|
|
return;
|
|
}
|
|
|
|
lock (_fileSystemChanges) {
|
|
// we just generate a delete and creation here - we're just updating the hierarchy
|
|
// either changing icons or updating the non-project elements, so we don't need to
|
|
// generate rename events or anything like that. This saves us from having to
|
|
// handle updating the hierarchy in a special way for renames.
|
|
if (NoPendingFileSystemRescan()) {
|
|
_fileSystemChanges.Enqueue(new FileSystemChange(this, WatcherChangeTypes.Deleted, e.OldFullPath, true));
|
|
_fileSystemChanges.Enqueue(new FileSystemChange(this, WatcherChangeTypes.Created, e.FullPath, true));
|
|
TriggerIdle();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void FileExistanceChanged(object sender, FileSystemEventArgs e) {
|
|
if (IsClosed) {
|
|
return;
|
|
}
|
|
|
|
lock (_fileSystemChanges) {
|
|
if (NoPendingFileSystemRescan()) {
|
|
_fileSystemChanges.Enqueue(new FileSystemChange(this, e.ChangeType, e.FullPath));
|
|
TriggerIdle();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// If VS is already idle, we won't keep getting idle events, so we need to post a
|
|
/// new event to the queue to flip away from idle and back again.
|
|
/// </summary>
|
|
private void TriggerIdle() {
|
|
if (Interlocked.CompareExchange(ref _idleTriggered, 1, 0) == 0) {
|
|
_uiSync.BeginInvoke(Nop, Type.EmptyTypes);
|
|
}
|
|
}
|
|
|
|
private static readonly Action Nop = () => { };
|
|
|
|
public void CreateSymLinkWatcher(string curDir) {
|
|
if (!CommonUtils.HasEndSeparator(curDir)) {
|
|
curDir = curDir + Path.DirectorySeparatorChar;
|
|
}
|
|
_symlinkWatchers[curDir] = CreateFileSystemWatcher(curDir);
|
|
}
|
|
|
|
public bool TryDeactivateSymLinkWatcher(HierarchyNode child) {
|
|
FileSystemWatcher watcher;
|
|
if (_symlinkWatchers.TryGetValue(child.Url, out watcher)) {
|
|
_symlinkWatchers.Remove(child.Url);
|
|
watcher.EnableRaisingEvents = false;
|
|
watcher.Dispose();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void OnIdle(object sender, ComponentManagerEventArgs e) {
|
|
Interlocked.Exchange(ref _idleTriggered, 0);
|
|
do {
|
|
using (new DebugTimer("ProcessFileChanges while Idle", 100)) {
|
|
if (IsClosed) {
|
|
return;
|
|
}
|
|
|
|
DiskMerger merger;
|
|
FileSystemChange change = null;
|
|
lock (_fileSystemChanges) {
|
|
merger = _currentMerger;
|
|
if (merger == null) {
|
|
if (_fileSystemChanges.Count == 0) {
|
|
break;
|
|
}
|
|
|
|
change = _fileSystemChanges.Dequeue();
|
|
}
|
|
}
|
|
|
|
if (merger != null) {
|
|
// we have more file merges to process, do this
|
|
// before reflecting any other pending updates...
|
|
if (!merger.ContinueMerge()) {
|
|
_currentMerger = null;
|
|
}
|
|
continue;
|
|
}
|
|
#if DEBUG
|
|
try {
|
|
#endif
|
|
if (change._type == WatcherChangeTypes.All) {
|
|
_currentMerger = new DiskMerger(this, this, ProjectHome);
|
|
continue;
|
|
} else {
|
|
change.ProcessChange();
|
|
}
|
|
#if DEBUG
|
|
} catch (Exception ex) {
|
|
Debug.Fail("Unexpected exception while processing change: {0}", ex.ToString());
|
|
throw;
|
|
}
|
|
#endif
|
|
}
|
|
} while (e.ComponentManager.FContinueIdle() != 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents an individual change to the file system. We process these in bulk on the
|
|
/// UI thread.
|
|
/// </summary>
|
|
class FileSystemChange {
|
|
private readonly CommonProjectNode _project;
|
|
public readonly WatcherChangeTypes _type;
|
|
private readonly string _path;
|
|
private readonly bool _isRename;
|
|
|
|
public FileSystemChange(CommonProjectNode node, WatcherChangeTypes changeType, string path, bool isRename = false) {
|
|
_project = node;
|
|
_type = changeType;
|
|
_path = path;
|
|
_isRename = isRename;
|
|
}
|
|
|
|
public override string ToString() {
|
|
return "FileSystemChange: " + _isRename + " " + _type + " " + _path;
|
|
}
|
|
|
|
private void RedrawIcon(HierarchyNode node) {
|
|
_project.ReDrawNode(node, UIHierarchyElement.Icon);
|
|
|
|
for (var child = node.FirstChild; child != null; child = child.NextSibling) {
|
|
RedrawIcon(child);
|
|
}
|
|
}
|
|
|
|
public void ProcessChange() {
|
|
var child = _project.FindNodeByFullPath(_path);
|
|
if ((_type == WatcherChangeTypes.Deleted || _type == WatcherChangeTypes.Changed) && child == null) {
|
|
child = _project.FindNodeByFullPath(_path + Path.DirectorySeparatorChar);
|
|
}
|
|
switch (_type) {
|
|
case WatcherChangeTypes.Deleted:
|
|
ChildDeleted(child);
|
|
break;
|
|
case WatcherChangeTypes.Created: ChildCreated(child); break;
|
|
case WatcherChangeTypes.Changed:
|
|
// we only care about the attributes
|
|
if (_project.IsFileHidden(_path)) {
|
|
if (child != null) {
|
|
// attributes must of changed to hidden, remove the file
|
|
goto case WatcherChangeTypes.Deleted;
|
|
}
|
|
} else {
|
|
if (child == null) {
|
|
// attributes must of changed from hidden, add the file
|
|
goto case WatcherChangeTypes.Created;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void RemoveAllFilesChildren(HierarchyNode parent) {
|
|
bool removed = false;
|
|
|
|
for (var current = parent.FirstChild; current != null; current = current.NextSibling) {
|
|
// remove our children first
|
|
RemoveAllFilesChildren(current);
|
|
|
|
_project.TryDeactivateSymLinkWatcher(current);
|
|
|
|
// then remove us if we're an all files node
|
|
if (current.ItemNode is AllFilesProjectElement) {
|
|
parent.RemoveChild(current);
|
|
_project.OnItemDeleted(current);
|
|
removed = true;
|
|
}
|
|
}
|
|
|
|
if (removed) {
|
|
_project.OnInvalidateItems(parent);
|
|
}
|
|
}
|
|
|
|
private void ChildDeleted(HierarchyNode child) {
|
|
if (child != null) {
|
|
_project.TryDeactivateSymLinkWatcher(child);
|
|
|
|
if (child.ItemNode.IsExcluded) {
|
|
RemoveAllFilesChildren(child);
|
|
|
|
// deleting a show all files item, remove the node.
|
|
child.Parent.RemoveChild(child);
|
|
_project.OnItemDeleted(child);
|
|
} else {
|
|
Debug.Assert(!child.IsNonMemberItem);
|
|
// deleting an item in the project, fix the icon, also
|
|
// fix the icon of all children which we may have not
|
|
// received delete notifications for
|
|
RedrawIcon(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ChildCreated(HierarchyNode child) {
|
|
if (child != null) {
|
|
// creating an item which was in the project, fix the icon.
|
|
_project.ReDrawNode(child, UIHierarchyElement.Icon);
|
|
} else {
|
|
if (_project.IsFileHidden(_path)) {
|
|
// don't add hidden files/folders
|
|
return;
|
|
}
|
|
|
|
// creating a new item, need to create the on all files node
|
|
string parentDir = Path.GetDirectoryName(CommonUtils.TrimEndSeparator(_path)) + Path.DirectorySeparatorChar;
|
|
HierarchyNode parent;
|
|
if (CommonUtils.IsSamePath(parentDir, _project.ProjectHome)) {
|
|
parent = _project;
|
|
} else {
|
|
parent = _project.FindNodeByFullPath(parentDir);
|
|
}
|
|
|
|
if (parent == null) {
|
|
// we've hit an error while adding too many files, the file system watcher
|
|
// couldn't keep up. That's alright, we'll merge the files in correctly
|
|
// in a little while...
|
|
return;
|
|
}
|
|
|
|
IVsUIHierarchyWindow uiHierarchy = UIHierarchyUtilities.GetUIHierarchyWindow(_project.Site, HierarchyNode.SolutionExplorer);
|
|
uint curState;
|
|
uiHierarchy.GetItemState(_project.GetOuterInterface<IVsUIHierarchy>(),
|
|
parent.ID,
|
|
(uint)__VSHIERARCHYITEMSTATE.HIS_Expanded,
|
|
out curState
|
|
);
|
|
|
|
if (Directory.Exists(_path)) {
|
|
if (IsFileSymLink(_path)) {
|
|
if (IsRecursiveSymLink(parentDir, _path)) {
|
|
// don't add recusrive sym link directory
|
|
return;
|
|
}
|
|
|
|
// otherwise we're going to need a new file system watcher
|
|
_project.CreateSymLinkWatcher(_path);
|
|
}
|
|
|
|
var folderNode = _project.AddAllFilesFolder(parent, _path + Path.DirectorySeparatorChar);
|
|
// we may have just moved a directory from another location (e.g. drag and
|
|
// and drop in explorer), in which case we also need to process the items
|
|
// which are in the folder that we won't receive create notifications for.
|
|
|
|
if (_isRename) {
|
|
// First, make sure we don't have any children
|
|
RemoveAllFilesChildren(folderNode);
|
|
|
|
// then add the folder nodes
|
|
_project.MergeDiskNodes(folderNode, _path);
|
|
}
|
|
|
|
_project.OnInvalidateItems(folderNode);
|
|
} else {
|
|
_project.AddAllFilesFile(parent, _path);
|
|
}
|
|
|
|
if ((curState & (uint)__VSHIERARCHYITEMSTATE.HIS_Expanded) == 0) {
|
|
// Solution Explorer will expand the parent when an item is
|
|
// added, which we don't want, so we check it's state before
|
|
// adding, and then collapse the folder if it was expanded.
|
|
parent.ExpandItem(EXPANDFLAGS.EXPF_CollapseFolder);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public override int GetGuidProperty(int propid, out Guid guid) {
|
|
if ((__VSHPROPID)propid == __VSHPROPID.VSHPROPID_PreferredLanguageSID) {
|
|
guid = new Guid("{EFB9A1D6-EA71-4F38-9BA7-368C33FCE8DC}");// GetLanguageServiceType().GUID;
|
|
} else {
|
|
return base.GetGuidProperty(propid, out guid);
|
|
}
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
protected override bool IsItemTypeFileType(string type) {
|
|
if (!base.IsItemTypeFileType(type)) {
|
|
if (String.Compare(type, "Page", StringComparison.OrdinalIgnoreCase) == 0
|
|
|| String.Compare(type, "ApplicationDefinition", StringComparison.OrdinalIgnoreCase) == 0
|
|
|| String.Compare(type, "Resource", StringComparison.OrdinalIgnoreCase) == 0) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
//This is a well known item node type, so return true.
|
|
return true;
|
|
}
|
|
}
|
|
|
|
protected override NodeProperties CreatePropertiesObject() {
|
|
return new CommonProjectNodeProperties(this);
|
|
}
|
|
|
|
public override int SetSite(Microsoft.VisualStudio.OLE.Interop.IServiceProvider site) {
|
|
base.SetSite(site);
|
|
|
|
//Initialize a new object to track project document changes so that we can update the StartupFile Property accordingly
|
|
_projectDocListenerForStartupFileUpdates = new ProjectDocumentsListenerForStartupFileUpdates((ServiceProvider)Site, this);
|
|
_projectDocListenerForStartupFileUpdates.Init();
|
|
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
public override void Close() {
|
|
if (null != _projectDocListenerForStartupFileUpdates) {
|
|
_projectDocListenerForStartupFileUpdates.Dispose();
|
|
_projectDocListenerForStartupFileUpdates = null;
|
|
}
|
|
|
|
if (GetLibraryManagerType() != null)
|
|
{
|
|
LibraryManager libraryManager = ((IServiceContainer)Package).GetService(GetLibraryManagerType()) as LibraryManager;
|
|
if (null != libraryManager)
|
|
{
|
|
libraryManager.UnregisterHierarchy(InteropSafeHierarchy);
|
|
}
|
|
}
|
|
|
|
if (_watcher != null)
|
|
{
|
|
_watcher.EnableRaisingEvents = false;
|
|
_watcher.Dispose();
|
|
_watcher = null;
|
|
}
|
|
|
|
if (_attributesWatcher != null)
|
|
{
|
|
_attributesWatcher.EnableRaisingEvents = false;
|
|
_attributesWatcher.Dispose();
|
|
_attributesWatcher = null;
|
|
}
|
|
|
|
base.Close();
|
|
}
|
|
|
|
public override void Load(string filename, string location, string name, uint flags, ref Guid iidProject, out int canceled) {
|
|
base.Load(filename, location, name, flags, ref iidProject, out canceled);
|
|
LibraryManager libraryManager = Site.GetService(GetLibraryManagerType()) as LibraryManager;
|
|
if (null != libraryManager) {
|
|
libraryManager.RegisterHierarchy(InteropSafeHierarchy);
|
|
}
|
|
|
|
|
|
//If this is a WPFFlavor-ed project, then add a project-level DesignerContext service to provide
|
|
//event handler generation (EventBindingProvider) for the XAML designer.
|
|
//this.OleServiceProvider.AddService(typeof(DesignerContext), new OleServiceProvider.ServiceCreatorCallback(this.CreateServices), false);
|
|
}
|
|
|
|
public void BoldStartupItem(HierarchyNode startupItem) {
|
|
if (!_boldedStartupItem) {
|
|
if (BoldItem(startupItem, true)) {
|
|
_boldedStartupItem = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool BoldItem(HierarchyNode node, bool isBold) {
|
|
IVsUIHierarchyWindow2 windows = GetUIHierarchyWindow(
|
|
Site as IServiceProvider,
|
|
new Guid(ToolWindowGuids80.SolutionExplorer)) as IVsUIHierarchyWindow2;
|
|
|
|
if (ErrorHandler.Succeeded(windows.SetItemAttribute(
|
|
this.GetOuterInterface<IVsUIHierarchy>(),
|
|
node.ID,
|
|
(uint)__VSHIERITEMATTRIBUTE.VSHIERITEMATTRIBUTE_Bold,
|
|
isBold
|
|
))) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overriding to provide project general property page
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
protected override Guid[] GetConfigurationIndependentPropertyPages() {
|
|
return new Guid[0];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a file node based on an msbuild item.
|
|
/// </summary>
|
|
/// <param name="item">The msbuild item to be analyzed</param>
|
|
public override FileNode CreateFileNode(ProjectElement item) {
|
|
VsUtilities.ArgumentNotNull("item", item);
|
|
|
|
CommonFileNode newNode;
|
|
if (IsCodeFile(item.GetFullPathForElement())) {
|
|
newNode = CreateCodeFileNode(item);
|
|
} else {
|
|
newNode = CreateNonCodeFileNode(item);
|
|
}
|
|
|
|
string link = item.GetMetadata(ProjectFileConstants.Link);
|
|
if (!String.IsNullOrWhiteSpace(link) ||
|
|
!CommonUtils.IsSubpathOf(ProjectHome, item.GetFullPathForElement())) {
|
|
newNode.SetIsLinkFile(true);
|
|
}
|
|
|
|
string include = item.GetMetadata(ProjectFileConstants.Include);
|
|
|
|
newNode.OleServiceProvider.AddService(typeof(EnvDTE.Project),
|
|
new OleServiceProvider.ServiceCreatorCallback(CreateServices), false);
|
|
newNode.OleServiceProvider.AddService(typeof(EnvDTE.ProjectItem), newNode.ServiceCreator, false);
|
|
|
|
/*if (!string.IsNullOrEmpty(include) && Path.GetExtension(include).Equals(".xaml", StringComparison.OrdinalIgnoreCase)) {
|
|
//Create a DesignerContext for the XAML designer for this file
|
|
newNode.OleServiceProvider.AddService(typeof(DesignerContext), newNode.ServiceCreator, false);
|
|
}*/
|
|
|
|
newNode.OleServiceProvider.AddService(typeof(VSLangProj.VSProject),
|
|
new OleServiceProvider.ServiceCreatorCallback(CreateServices), false);
|
|
return newNode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a file node based on absolute file name.
|
|
/// </summary>
|
|
public override FileNode CreateFileNode(string absFileName) {
|
|
// Avoid adding files to the project multiple times. Ultimately
|
|
// we should not use project items and instead should have virtual items.
|
|
|
|
string path = CommonUtils.GetRelativeFilePath(ProjectHome, absFileName);
|
|
var prjItem = BuildProject.AddItem(GetItemType(path), path)[0];
|
|
|
|
return CreateFileNode(new MsBuildProjectElement(this, prjItem));
|
|
}
|
|
|
|
public override FolderNode CreateFolderNode(ProjectElement element)
|
|
{
|
|
return new CommonFolderNode(this, element);
|
|
}
|
|
|
|
public string GetItemType(string filename) {
|
|
if (IsCodeFile(filename)) {
|
|
return "Compile";
|
|
} else {
|
|
return "Content";
|
|
}
|
|
}
|
|
|
|
public ProjectElement MakeProjectElement(string type, string path) {
|
|
var item = BuildProject.AddItem(type, path)[0];
|
|
return new MsBuildProjectElement(this, item);
|
|
}
|
|
|
|
public override int IsDirty(out int isDirty) {
|
|
isDirty = 0;
|
|
if (IsProjectFileDirty) {
|
|
isDirty = 1;
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
isDirty = IsFlavorDirty();
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
protected override void AddNewFileNodeToHierarchy(HierarchyNode parentNode, string fileName) {
|
|
base.AddNewFileNodeToHierarchy(parentNode, fileName);
|
|
|
|
SetProjectFileDirty(true);
|
|
}
|
|
|
|
public override DependentFileNode CreateDependentFileNode(MsBuildProjectElement item) {
|
|
DependentFileNode node = base.CreateDependentFileNode(item);
|
|
if (null != node) {
|
|
string include = item.GetMetadata(ProjectFileConstants.Include);
|
|
if (IsCodeFile(include)) {
|
|
node.OleServiceProvider.AddService(
|
|
typeof(SVSMDCodeDomProvider), new OleServiceProvider.ServiceCreatorCallback(CreateServices), false);
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the format list for the open file dialog
|
|
/// </summary>
|
|
/// <param name="formatlist">The formatlist to return</param>
|
|
/// <returns>Success</returns>
|
|
public override int GetFormatList(out string formatlist) {
|
|
formatlist = GetFormatList();
|
|
formatlist = formatlist.Replace('|', '\0') + '\0';
|
|
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
protected override ConfigProvider CreateConfigProvider() {
|
|
return new CommonConfigProvider(this);
|
|
}
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
/// <summary>
|
|
/// This method retrieves an instance of a service that
|
|
/// allows to start a project or a file with or without debugging.
|
|
/// </summary>
|
|
public abstract IProjectLauncher/*!*/ GetLauncher();
|
|
|
|
/// <summary>
|
|
/// Returns resolved value of the current working directory property.
|
|
/// </summary>
|
|
public string GetWorkingDirectory() {
|
|
string workDir = GetProjectProperty(CommonConstants.WorkingDirectory, true);
|
|
|
|
return CommonUtils.GetAbsoluteDirectoryPath(ProjectHome, workDir);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns resolved value of the startup file property.
|
|
/// </summary>
|
|
public string GetStartupFile() {
|
|
string startupFile = GetProjectProperty(CommonConstants.StartupFile, true);
|
|
|
|
if (string.IsNullOrEmpty(startupFile)) {
|
|
//No startup file is assigned
|
|
return null;
|
|
}
|
|
|
|
return CommonUtils.GetAbsoluteFilePath(ProjectHome, startupFile);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whenever project property has changed - refresh project hierarachy.
|
|
/// </summary>
|
|
private void CommonProjectNode_OnProjectPropertyChanged(object sender, ProjectPropertyChangedArgs e) {
|
|
switch (e.PropertyName) {
|
|
case CommonConstants.StartupFile:
|
|
RefreshStartupFile(this,
|
|
CommonUtils.GetAbsoluteFilePath(ProjectHome, e.OldValue),
|
|
CommonUtils.GetAbsoluteFilePath(ProjectHome, e.NewValue));
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Same as VsShellUtilities.GetUIHierarchyWindow, but it doesn't contain a useless cast to IVsWindowPane
|
|
/// which fails on Dev10 with the solution explorer window.
|
|
/// </summary>
|
|
private static IVsUIHierarchyWindow GetUIHierarchyWindow(IServiceProvider serviceProvider, Guid guidPersistenceSlot) {
|
|
if (serviceProvider == null) {
|
|
throw new ArgumentException("serviceProvider");
|
|
}
|
|
IVsUIShell service = serviceProvider.GetService(typeof(SVsUIShell)) as IVsUIShell;
|
|
if (service == null) {
|
|
throw new InvalidOperationException();
|
|
}
|
|
object pvar = null;
|
|
IVsWindowFrame ppWindowFrame = null;
|
|
IVsUIHierarchyWindow window = null;
|
|
try {
|
|
ErrorHandler.ThrowOnFailure(service.FindToolWindow(0, ref guidPersistenceSlot, out ppWindowFrame));
|
|
ErrorHandler.ThrowOnFailure(ppWindowFrame.GetProperty(-3001, out pvar));
|
|
} catch (COMException exception) {
|
|
Trace.WriteLine("Exception :" + exception.Message);
|
|
} finally {
|
|
if (pvar != null) {
|
|
window = (IVsUIHierarchyWindow)pvar;
|
|
}
|
|
}
|
|
return window;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns first immediate child node (non-recursive) of a given type.
|
|
/// </summary>
|
|
private void RefreshStartupFile(HierarchyNode parent, string oldFile, string newFile) {
|
|
IVsUIHierarchyWindow2 windows = GetUIHierarchyWindow(
|
|
Site,
|
|
new Guid(ToolWindowGuids80.SolutionExplorer)) as IVsUIHierarchyWindow2;
|
|
|
|
for (HierarchyNode n = parent.FirstChild; n != null; n = n.NextSibling) {
|
|
// TODO: Distinguish between real Urls and fake ones (eg. "References")
|
|
if (windows != null) {
|
|
var absUrl = CommonUtils.GetAbsoluteFilePath(parent.ProjectMgr.ProjectHome, n.Url);
|
|
if (CommonUtils.IsSamePath(oldFile, absUrl)) {
|
|
windows.SetItemAttribute(
|
|
this,
|
|
n.ID,
|
|
(uint)__VSHIERITEMATTRIBUTE.VSHIERITEMATTRIBUTE_Bold,
|
|
false
|
|
);
|
|
ReDrawNode(n, UIHierarchyElement.Icon);
|
|
} else if (CommonUtils.IsSamePath(newFile, absUrl)) {
|
|
windows.SetItemAttribute(
|
|
this,
|
|
n.ID,
|
|
(uint)__VSHIERITEMATTRIBUTE.VSHIERITEMATTRIBUTE_Bold,
|
|
true
|
|
);
|
|
ReDrawNode(n, UIHierarchyElement.Icon);
|
|
}
|
|
}
|
|
|
|
RefreshStartupFile(n, oldFile, newFile);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Provide mapping from our browse objects and automation objects to our CATIDs
|
|
/// </summary>
|
|
private void InitializeCATIDs() {
|
|
// The following properties classes are specific to current language so we can use their GUIDs directly
|
|
AddCATIDMapping(typeof(OAProject), typeof(OAProject).GUID);
|
|
// The following is not language specific and as such we need a separate GUID
|
|
AddCATIDMapping(typeof(FolderNodeProperties), new Guid(CommonConstants.FolderNodePropertiesGuid));
|
|
// These ones we use the same as language file nodes since both refer to files
|
|
AddCATIDMapping(typeof(FileNodeProperties), typeof(FileNodeProperties).GUID);
|
|
AddCATIDMapping(typeof(IncludedFileNodeProperties), typeof(IncludedFileNodeProperties).GUID);
|
|
// We could also provide CATIDs for references and the references container node, if we wanted to.
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses SearchPath property into a list of distinct absolute paths, preserving the order.
|
|
/// </summary>
|
|
protected IList<string> ParseSearchPath() {
|
|
var searchPath = GetProjectProperty(CommonConstants.SearchPath, true);
|
|
return ParseSearchPath(searchPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses SearchPath string into a list of distinct absolute paths, preserving the order.
|
|
/// </summary>
|
|
protected IList<string> ParseSearchPath(string searchPath) {
|
|
var result = new List<string>();
|
|
|
|
if (!string.IsNullOrEmpty(searchPath)) {
|
|
var seen = new HashSet<string>();
|
|
foreach (var path in searchPath.Split(';')) {
|
|
if (string.IsNullOrEmpty(path)) {
|
|
continue;
|
|
}
|
|
|
|
var absPath = CommonUtils.GetAbsoluteFilePath(ProjectHome, path);
|
|
if (seen.Add(absPath)) {
|
|
result.Add(absPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves list of paths back as SearchPath project property.
|
|
/// </summary>
|
|
private void SaveSearchPath(IList<string> value) {
|
|
var valueStr = string.Join(";", value.Select(path => {
|
|
var relPath = CommonUtils.GetRelativeFilePath(ProjectHome, path);
|
|
if (string.IsNullOrEmpty(relPath)) {
|
|
relPath = ".";
|
|
}
|
|
return relPath;
|
|
}));
|
|
SetProjectProperty(CommonConstants.SearchPath, valueStr);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds new search path to the SearchPath project property.
|
|
/// </summary>
|
|
public void AddSearchPathEntry(string newpath) {
|
|
VsUtilities.ArgumentNotNull("newpath", newpath);
|
|
|
|
IList<string> searchPath = ParseSearchPath();
|
|
var absPath = CommonUtils.GetAbsoluteFilePath(ProjectHome, newpath);
|
|
if (searchPath.Contains(absPath, StringComparer.OrdinalIgnoreCase)) {
|
|
return;
|
|
}
|
|
searchPath.Add(absPath);
|
|
SaveSearchPath(searchPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a given path from the SearchPath property.
|
|
/// </summary>
|
|
public void RemoveSearchPathEntry(string path) {
|
|
IList<string> searchPath = ParseSearchPath();
|
|
var absPath = CommonUtils.GetAbsoluteFilePath(ProjectHome, path);
|
|
if (searchPath.Remove(path)) {
|
|
SaveSearchPath(searchPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the services exposed by this project.
|
|
/// </summary>
|
|
private object CreateServices(Type serviceType) {
|
|
object service = null;
|
|
if (typeof(VSLangProj.VSProject) == serviceType) {
|
|
service = VSProject;
|
|
} else if (typeof(EnvDTE.Project) == serviceType) {
|
|
service = GetAutomationObject();
|
|
} /*else if (typeof(DesignerContext) == serviceType) {
|
|
service = this.DesignerContext;
|
|
}*/
|
|
|
|
return service;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes Add Search Path menu command.
|
|
/// </summary>
|
|
public int AddSearchPath() {
|
|
string dirName = BrowseForFolder(
|
|
DynamicProjectSR.GetString(DynamicProjectSR.SelectFolderForSearchPath),
|
|
ProjectHome);
|
|
|
|
if (dirName != null) {
|
|
AddSearchPathEntry(dirName);
|
|
}
|
|
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
public string BrowseForFolder(string title, string initialDir) {
|
|
// Get a reference to the UIShell.
|
|
IVsUIShell uiShell = GetService(typeof(SVsUIShell)) as IVsUIShell;
|
|
if (null == uiShell) {
|
|
return null;
|
|
}
|
|
|
|
//Create a fill in a structure that defines Browse for folder dialog
|
|
VSBROWSEINFOW[] browseInfo = new VSBROWSEINFOW[1];
|
|
//Dialog title
|
|
browseInfo[0].pwzDlgTitle = title;
|
|
//Initial directory - project directory
|
|
browseInfo[0].pwzInitialDir = initialDir;
|
|
//Parent window
|
|
uiShell.GetDialogOwnerHwnd(out browseInfo[0].hwndOwner);
|
|
//Max path length
|
|
//This is WCHARS not bytes
|
|
browseInfo[0].nMaxDirName = (uint)NativeMethods.MAX_PATH;
|
|
//This struct size
|
|
browseInfo[0].lStructSize = (uint)Marshal.SizeOf(typeof(VSBROWSEINFOW));
|
|
//Memory to write selected directory to.
|
|
//Note: this one allocates unmanaged memory, which must be freed later
|
|
// Add 1 for the null terminator and double since we are WCHARs not bytes
|
|
IntPtr pDirName = Marshal.AllocCoTaskMem((NativeMethods.MAX_PATH + 1)*2);
|
|
browseInfo[0].pwzDirName = pDirName;
|
|
try {
|
|
//Show the dialog
|
|
int hr = uiShell.GetDirectoryViaBrowseDlg(browseInfo);
|
|
if (hr == VSConstants.OLE_E_PROMPTSAVECANCELLED) {
|
|
//User cancelled the dialog
|
|
return null;
|
|
}
|
|
//Check for any failures
|
|
ErrorHandler.ThrowOnFailure(hr);
|
|
//Get selected directory
|
|
return Marshal.PtrToStringAuto(browseInfo[0].pwzDirName);
|
|
} finally {
|
|
//Free allocated unmanaged memory
|
|
if (pDirName != IntPtr.Zero) {
|
|
Marshal.FreeCoTaskMem(pDirName);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IVsProjectSpecificEditorMap2 Members
|
|
|
|
public int GetSpecificEditorProperty(string mkDocument, int propid, out object result) {
|
|
// initialize output params
|
|
result = null;
|
|
|
|
//Validate input
|
|
if (string.IsNullOrEmpty(mkDocument))
|
|
throw new ArgumentException("Was null or empty", "mkDocument");
|
|
|
|
// Make sure that the document moniker passed to us is part of this project
|
|
// We also don't care if it is not a dynamic language file node
|
|
uint itemid;
|
|
int hr;
|
|
if (ErrorHandler.Failed(hr = ParseCanonicalName(mkDocument, out itemid))) {
|
|
return hr;
|
|
}
|
|
HierarchyNode hierNode = NodeFromItemId(itemid);
|
|
if (hierNode == null || ((hierNode as CommonFileNode) == null))
|
|
return VSConstants.E_NOTIMPL;
|
|
|
|
switch (propid) {
|
|
case (int)__VSPSEPROPID.VSPSEPROPID_UseGlobalEditorByDefault:
|
|
// don't show project default editor, every file supports Python.
|
|
result = false;
|
|
return VSConstants.E_FAIL;
|
|
/*case (int)__VSPSEPROPID.VSPSEPROPID_ProjectDefaultEditorName:
|
|
result = "Python Editor";
|
|
break;*/
|
|
}
|
|
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
public int GetSpecificEditorType(string mkDocument, out Guid guidEditorType) {
|
|
// Ideally we should at this point initalize a File extension to EditorFactory guid Map e.g.
|
|
// in the registry hive so that more editors can be added without changing this part of the
|
|
// code. Dynamic languages only make usage of one Editor Factory and therefore we will return
|
|
// that guid
|
|
guidEditorType = GetEditorFactoryType().GUID;
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
public int GetSpecificLanguageService(string mkDocument, out Guid guidLanguageService) {
|
|
guidLanguageService = Guid.Empty;
|
|
return VSConstants.E_NOTIMPL;
|
|
}
|
|
|
|
public int SetSpecificEditorProperty(string mkDocument, int propid, object value) {
|
|
return VSConstants.E_NOTIMPL;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IVsDeferredSaveProject Members
|
|
|
|
/// <summary>
|
|
/// Implements deferred save support. Enabled by unchecking Tools->Options->Solutions and Projects->Save New Projects Created.
|
|
///
|
|
/// In this mode we save the project when the user selects Save All. We need to move all the files in the project
|
|
/// over to the new location.
|
|
/// </summary>
|
|
public virtual int SaveProjectToLocation(string pszProjectFilename) {
|
|
string oldName = Url;
|
|
string basePath = CommonUtils.NormalizeDirectoryPath(Path.GetDirectoryName(this.FileName));
|
|
string newName = Path.GetDirectoryName(pszProjectFilename);
|
|
|
|
IVsUIShell shell = this.Site.GetService(typeof(SVsUIShell)) as IVsUIShell;
|
|
IVsSolution vsSolution = (IVsSolution)this.GetService(typeof(SVsSolution));
|
|
|
|
int canContinue;
|
|
vsSolution.QueryRenameProject(this, FileName, pszProjectFilename, 0, out canContinue);
|
|
if (canContinue == 0) {
|
|
return VSConstants.OLE_E_PROMPTSAVECANCELLED;
|
|
}
|
|
|
|
_watcher.EnableRaisingEvents = false;
|
|
_watcher.Dispose();
|
|
|
|
// we don't use RenameProjectFile because it sends the OnAfterRenameProject event too soon
|
|
// and causes VS to think the solution has changed on disk. We need to send it after all
|
|
// updates are complete.
|
|
|
|
// save the new project to to disk
|
|
SaveMSBuildProjectFileAs(pszProjectFilename);
|
|
|
|
if (CommonUtils.IsSameDirectory(ProjectHome, basePath)) {
|
|
// ProjectHome was set by SaveMSBuildProjectFileAs to point to the temporary directory.
|
|
BuildProject.SetProperty(CommonConstants.ProjectHome, ".");
|
|
|
|
// save the project again w/ updated file info
|
|
BuildProjectLocationChanged();
|
|
|
|
// remove all the children, saving any dirty files, and collecting the list of open files
|
|
MoveFilesForDeferredSave(this, basePath, newName);
|
|
} else {
|
|
// Project referenced external files only, so just update its location without moving
|
|
// files around.
|
|
BuildProjectLocationChanged();
|
|
}
|
|
|
|
BuildProject.Save();
|
|
|
|
SetProjectFileDirty(false);
|
|
|
|
// update VS that we've changed the project
|
|
this.OnPropertyChanged(this, (int)__VSHPROPID.VSHPROPID_Caption, 0);
|
|
|
|
// Update solution
|
|
// Note we ignore the errors here because reporting them to the user isn't really helpful.
|
|
// We've already completed all of the work to rename everything here. If OnAfterNameProject
|
|
// fails for some reason then telling the user it failed is just confusing because all of
|
|
// the work is done. And if someone wanted to prevent us from renaming the project file they
|
|
// should have responded to QueryRenameProject. Likewise if we can't refresh the property browser
|
|
// for any reason then that's not too interesting either - the users project has been saved to
|
|
// the new location.
|
|
// http://pytools.codeplex.com/workitem/489
|
|
vsSolution.OnAfterRenameProject((IVsProject)this, oldName, pszProjectFilename, 0);
|
|
|
|
shell.RefreshPropertyBrowser(0);
|
|
|
|
_watcher = CreateFileSystemWatcher(ProjectHome);
|
|
_attributesWatcher = CreateAttributesWatcher(ProjectHome);
|
|
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
private void MoveFilesForDeferredSave(HierarchyNode node, string basePath, string baseNewPath) {
|
|
if (node != null) {
|
|
for (var child = node.FirstChild; child != null; child = child.NextSibling) {
|
|
bool isOpen, isDirty, isOpenedByUs;
|
|
uint docCookie;
|
|
IVsPersistDocData persist;
|
|
var docMgr = child.GetDocumentManager();
|
|
if (docMgr != null) {
|
|
docMgr.GetDocInfo(out isOpen, out isDirty, out isOpenedByUs, out docCookie, out persist);
|
|
int cancelled;
|
|
if (isDirty) {
|
|
child.ProjectMgr.SaveItem(VSSAVEFLAGS.VSSAVE_Save, null, docCookie, IntPtr.Zero, out cancelled);
|
|
}
|
|
}
|
|
|
|
IDiskBasedNode diskNode = child as IDiskBasedNode;
|
|
if (diskNode != null) {
|
|
diskNode.RenameForDeferredSave(basePath, baseNewPath);
|
|
}
|
|
|
|
MoveFilesForDeferredSave(child, basePath, baseNewPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
public void SuppressFileChangeNotifications() {
|
|
_watcher.EnableRaisingEvents = false;
|
|
_suppressFileWatcherCount++;
|
|
}
|
|
|
|
public void RestoreFileChangeNotifications() {
|
|
if (--_suppressFileWatcherCount == 0) {
|
|
_watcher.EnableRaisingEvents = true;
|
|
}
|
|
}
|
|
|
|
#if DEV11_OR_LATER
|
|
public override object GetProperty(int propId) {
|
|
CommonFolderNode.BoldStartupOnIcon(propId, this);
|
|
|
|
return base.GetProperty(propId);
|
|
}
|
|
#else
|
|
public override int SetProperty(int propid, object value) {
|
|
CommonFolderNode.BoldStartupOnExpand(propid, this);
|
|
|
|
return base.SetProperty(propid, value);
|
|
}
|
|
#endif
|
|
}
|
|
}
|