//
// ProjectNodeBuilder.cs
//
// Author:
// Lluis Sanchez Gual
//
// Copyright (C) 2005 Novell, Inc (http://www.novell.com)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using MonoDevelop.Projects;
using MonoDevelop.Core;
using MonoDevelop.Ide.Commands;
using MonoDevelop.Ide.Gui;
using MonoDevelop.Components.Commands;
using MonoDevelop.Ide.Gui.Components;
using MonoDevelop.Ide.Gui.Dialogs;
using System.Linq;
using MonoDevelop.Ide.Tasks;
namespace MonoDevelop.Ide.Gui.Pads.ProjectPad
{
class ProjectNodeBuilder: FolderNodeBuilder
{
public override Type NodeDataType {
get { return typeof(Project); }
}
public override Type CommandHandlerType {
get { return typeof(ProjectNodeCommandHandler); }
}
protected override void Initialize ()
{
IdeApp.Workspace.FileAddedToProject += OnAddFile;
IdeApp.Workspace.FileRemovedFromProject += OnRemoveFile;
IdeApp.Workspace.FileRenamedInProject += OnRenameFile;
IdeApp.Workspace.FilePropertyChangedInProject += OnFilePropertyChanged;
IdeApp.Workspace.ActiveConfigurationChanged += IdeAppWorkspaceActiveConfigurationChanged;
FileService.FileRemoved += OnSystemFileDeleted;
}
public override void Dispose ()
{
IdeApp.Workspace.FileAddedToProject -= OnAddFile;
IdeApp.Workspace.FileRemovedFromProject -= OnRemoveFile;
IdeApp.Workspace.FileRenamedInProject -= OnRenameFile;
IdeApp.Workspace.FilePropertyChangedInProject -= OnFilePropertyChanged;
IdeApp.Workspace.ActiveConfigurationChanged -= IdeAppWorkspaceActiveConfigurationChanged;
FileService.FileRemoved -= OnSystemFileDeleted;
}
public override void OnNodeAdded (object dataObject)
{
base.OnNodeAdded (dataObject);
Project project = (Project) dataObject;
project.Modified += OnProjectModified;
}
public override void OnNodeRemoved (object dataObject)
{
base.OnNodeRemoved (dataObject);
Project project = (Project) dataObject;
project.Modified -= OnProjectModified;
}
public override string GetNodeName (ITreeNavigator thisNode, object dataObject)
{
return ((Project)dataObject).Name;
}
public override string GetFolderPath (object dataObject)
{
return ((Project)dataObject).BaseDirectory;
}
public override void BuildNode (ITreeBuilder treeBuilder, object dataObject, NodeInfo nodeInfo)
{
base.BuildNode (treeBuilder, dataObject, nodeInfo);
Project p = dataObject as Project;
string escapedProjectName = GLib.Markup.EscapeText (p.Name);
if (p is DotNetProject && ((DotNetProject)p).LanguageBinding == null) {
nodeInfo.Icon = Context.GetIcon (Stock.Project);
nodeInfo.Label = escapedProjectName;
nodeInfo.StatusSeverity = TaskSeverity.Error;
nodeInfo.StatusMessage = GettextCatalog.GetString ("Unknown language '{0}'", ((DotNetProject)p).LanguageName);
nodeInfo.DisabledStyle = true;
return;
} else if (p is UnknownProject) {
var up = (UnknownProject)p;
nodeInfo.StatusSeverity = TaskSeverity.Warning;
nodeInfo.StatusMessage = up.UnsupportedProjectMessage.TrimEnd ('.');
nodeInfo.Label = escapedProjectName;
nodeInfo.DisabledStyle = true;
nodeInfo.Icon = Context.GetIcon (p.StockIcon);
return;
}
nodeInfo.Icon = Context.GetIcon (p.StockIcon);
var sc = p.ParentSolution?.StartupConfiguration;
if (sc != null && IsStartupProject (p, sc)) {
nodeInfo.Label = "" + escapedProjectName + "";
} else
nodeInfo.Label = escapedProjectName;
// Gray out the project name if it is not selected in the current build configuration
SolutionConfiguration conf = p.ParentSolution.GetConfiguration (IdeApp.Workspace.ActiveConfiguration);
SolutionConfigurationEntry ce = null;
bool noMapping = conf == null || (ce = conf.GetEntryForItem (p)) == null;
bool missingConfig = false;
if (p.SupportsBuild () && (noMapping || !ce.Build || (missingConfig = p.Configurations [ce.ItemConfiguration] == null))) {
nodeInfo.DisabledStyle = true;
if (missingConfig) {
nodeInfo.StatusSeverity = TaskSeverity.Error;
nodeInfo.StatusMessage = GettextCatalog.GetString ("Invalid configuration mapping");
} else {
nodeInfo.StatusSeverity = TaskSeverity.Information;
nodeInfo.StatusMessage = GettextCatalog.GetString ("Project not built in active configuration");
}
}
}
bool IsStartupProject (Project p, SolutionRunConfiguration sc)
{
var single = sc as SingleItemSolutionRunConfiguration;
if (single != null)
return single.Item == p;
var multi = sc as MultiItemSolutionRunConfiguration;
return multi != null && multi.Items.Any (si => si.SolutionItem == p);
}
public override void BuildChildNodes (ITreeBuilder builder, object dataObject)
{
Project project = (Project) dataObject;
if (project is DotNetProject) {
builder.AddChild (((DotNetProject)project).References);
}
base.BuildChildNodes (builder, dataObject);
}
public override bool HasChildNodes (ITreeBuilder builder, object dataObject)
{
return true;
}
public override object GetParentObject (object dataObject)
{
SolutionFolderItem it = (SolutionFolderItem) dataObject;
if (it.ParentFolder == null)
return null;
return it.ParentFolder.IsRoot ? (object) it.ParentSolution : (object) it.ParentFolder;
}
void OnAddFile (object sender, ProjectFileEventArgs args)
{
if (args.CommonProject != null && args.Count > 2 && args.SingleVirtualDirectory) {
ITreeBuilder tb = GetFolder (args.CommonProject, args.CommonVirtualRootDirectory);
if (tb != null)
tb.UpdateChildren ();
}
else {
foreach (ProjectFileEventInfo e in args)
AddFile (e.ProjectFile, e.Project);
}
}
void OnRemoveFile (object sender, ProjectFileEventArgs args)
{
foreach (ProjectFileEventInfo e in args)
RemoveFile (e.ProjectFile, e.Project);
}
void OnSystemFileDeleted (object sender, FileEventArgs args)
{
if (!args.Any (f => f.IsDirectory))
return;
// When a folder is deleted, we need to remove all references in the tree, for all projects
ITreeBuilder tb = Context.GetTreeBuilder ();
var dirs = args.Where (d => d.IsDirectory).Select (d => d.FileName).ToArray ();
foreach (var p in IdeApp.Workspace.GetAllProjects ()) {
foreach (var dir in dirs) {
if (tb.MoveToObject (new ProjectFolder (dir, p)) && tb.MoveToParent ())
tb.UpdateAll ();
}
}
}
void AddFile (ProjectFile file, Project project)
{
if (!file.Visible || file.Flags.HasFlag (ProjectItemFlags.Hidden))
return;
ITreeBuilder tb = Context.GetTreeBuilder ();
if (file.DependsOnFile != null) {
if (!tb.MoveToObject (file.DependsOnFile)) {
// The parent is not in the tree. Add it now, and it will add this file as a child.
AddFile (file.DependsOnFile, project);
}
else
tb.AddChild (file);
return;
}
object data;
if (file.Subtype == Subtype.Directory)
data = new ProjectFolder (file.Name, project);
else
data = file;
// Already there?
if (tb.MoveToObject (data))
return;
string filePath = file.IsLink
? project.BaseDirectory.Combine (file.ProjectVirtualPath).ParentDirectory
: file.FilePath.ParentDirectory;
tb = GetFolder (project, filePath);
if (tb != null)
tb.AddChild (data);
}
ITreeBuilder GetFolder (Project project, FilePath filePath)
{
ITreeBuilder tb = Context.GetTreeBuilder ();
if (filePath != project.BaseDirectory) {
if (tb.MoveToObject (new ProjectFolder (filePath, project))) {
return tb;
}
else {
// Make sure there is a path to that folder
tb = FindParentFolderNode (filePath, project);
if (tb != null) {
tb.UpdateChildren ();
return null;
}
}
} else {
if (tb.MoveToObject (project))
return tb;
}
return null;
}
ITreeBuilder FindParentFolderNode (string path, Project project)
{
int i = path.LastIndexOf (Path.DirectorySeparatorChar);
if (i == -1) return null;
string basePath = path.Substring (0, i);
if (basePath == project.BaseDirectory)
return Context.GetTreeBuilder (project);
ITreeBuilder tb = Context.GetTreeBuilder (new ProjectFolder (basePath, project));
if (tb != null) return tb;
return FindParentFolderNode (basePath, project);
}
void RemoveFile (ProjectFile file, Project project)
{
ITreeBuilder tb = Context.GetTreeBuilder ();
if (file.Subtype == Subtype.Directory) {
if (!tb.MoveToObject (new ProjectFolder (file.Name, project)))
return;
tb.MoveToParent ();
tb.UpdateAll ();
return;
} else {
if (tb.MoveToObject (file)) {
tb.Remove (true);
} else {
// We can't use IsExternalToProject here since the ProjectFile has
// already been removed from the project
string parentPath = file.IsLink
? project.BaseDirectory.Combine (file.Link.IsNullOrEmpty? file.FilePath.FileName : file.Link.ToString ()).ParentDirectory
: file.FilePath.ParentDirectory;
if (!tb.MoveToObject (new ProjectFolder (parentPath, project)))
return;
}
}
while (tb.DataItem is ProjectFolder) {
ProjectFolder f = (ProjectFolder) tb.DataItem;
if (!Directory.Exists (f.Path) && !project.Files.GetFilesInVirtualPath (f.Path.ToRelative (project.BaseDirectory)).Any ())
tb.Remove (true);
else
break;
}
}
void OnRenameFile (object sender, ProjectFileRenamedEventArgs args)
{
foreach (ProjectFileEventInfo e in args) {
ITreeBuilder tb = Context.GetTreeBuilder (e.ProjectFile);
if (tb != null) tb.Update ();
}
}
void OnProjectModified (object sender, SolutionItemModifiedEventArgs args)
{
foreach (SolutionItemModifiedEventInfo e in args) {
if (e.Hint == "References" || e.Hint == "Files")
continue;
ITreeBuilder tb = Context.GetTreeBuilder (e.SolutionItem);
if (tb != null) {
if (e.Hint == "BaseDirectory" || e.Hint == "TargetFramework")
tb.UpdateAll ();
else
tb.Update ();
}
}
}
static HashSet propertiesThatAffectDisplay = new HashSet (new string[] { null, "DependsOn", "Link", "Visible" });
void OnFilePropertyChanged (object sender, ProjectFileEventArgs e)
{
foreach (var project in e.Where (x => propertiesThatAffectDisplay.Contains (x.Property)).Select (x => x.Project).Distinct ()) {
ITreeBuilder tb = Context.GetTreeBuilder (project);
if (tb != null) tb.UpdateAll ();
}
}
void IdeAppWorkspaceActiveConfigurationChanged (object sender, EventArgs e)
{
foreach (Project p in IdeApp.Workspace.GetAllProjects ()) {
ITreeBuilder tb = Context.GetTreeBuilder (p);
if (tb != null)
tb.Update ();
}
}
}
class ProjectNodeCommandHandler: FolderCommandHandler
{
public override string GetFolderPath (object dataObject)
{
return ((Project)dataObject).BaseDirectory;
}
public override void RenameItem (string newName)
{
Project project = (Project) CurrentNode.DataItem;
if (project.Name != newName)
IdeApp.ProjectOperations.RenameItem (project, newName);
}
public override void ActivateItem ()
{
Project project = (Project) CurrentNode.DataItem;
IdeApp.ProjectOperations.ShowOptions (project);
}
[CommandUpdateHandler (ProjectCommands.SetAsStartupProject)]
public void UpdateSetAsStartupProject (CommandInfo ci)
{
Project project = (Project) CurrentNode.DataItem;
ci.Visible = project.SupportsExecute ();
}
[CommandHandler (ProjectCommands.SetAsStartupProject)]
public async void SetAsStartupProject ()
{
Project project = CurrentNode.DataItem as Project;
project.ParentSolution.StartupItem = project;
await project.ParentSolution.SaveUserProperties ();
}
public override void DeleteItem ()
{
Project prj = CurrentNode.DataItem as Project;
IdeApp.ProjectOperations.RemoveSolutionItem (prj);
}
[CommandHandler (ProjectCommands.AddReference)]
public async void AddReferenceToProject ()
{
DotNetProject p = (DotNetProject) CurrentNode.DataItem;
if (IdeApp.ProjectOperations.AddReferenceToProject (p))
await IdeApp.ProjectOperations.SaveAsync (p);
}
[CommandUpdateHandler (ProjectCommands.AddReference)]
public void UpdateAddReferenceToProject (CommandInfo ci)
{
ci.Visible = CurrentNode.DataItem is DotNetProject;
}
[CommandHandler (ProjectCommands.Reload)]
[AllowMultiSelection]
public void OnReload ()
{
using (ProgressMonitor m = IdeApp.Workbench.ProgressMonitors.GetProjectLoadProgressMonitor (true)) {
m.BeginTask (null, CurrentNodes.Length);
foreach (ITreeNavigator nav in CurrentNodes) {
Project p = (Project) nav.DataItem;
p.ParentFolder.ReloadItem (m, p);
m.Step (1);
}
m.EndTask ();
}
}
[CommandUpdateHandler (ProjectCommands.Reload)]
public void OnUpdateReload (CommandInfo info)
{
foreach (ITreeNavigator nav in CurrentNodes) {
Project p = (Project) nav.DataItem;
if (p.ParentFolder == null || !p.NeedsReload) {
info.Visible = false;
return;
}
}
}
[CommandHandler (ProjectCommands.Unload)]
[AllowMultiSelection]
public async void OnUnload ()
{
HashSet solutions = new HashSet ();
using (ProgressMonitor m = IdeApp.Workbench.ProgressMonitors.GetProjectLoadProgressMonitor (true)) {
m.BeginTask (null, CurrentNodes.Length);
foreach (ITreeNavigator nav in CurrentNodes) {
Project p = (Project) nav.DataItem;
p.Enabled = false;
solutions.Add (p.ParentSolution);
await p.ParentFolder.ReloadItem (m, p);
m.Step (1);
}
m.EndTask ();
}
await IdeApp.ProjectOperations.SaveAsync (solutions);
}
[CommandUpdateHandler (ProjectCommands.Unload)]
public void OnUpdateUnload (CommandInfo info)
{
info.Enabled = CurrentNodes.All (nav => ((Project)nav.DataItem).Enabled);
}
[CommandHandler (ProjectCommands.EditSolutionItem)]
[AllowMultiSelection]
public void OnEditProject ()
{
foreach (var nav in CurrentNodes) {
IdeApp.Workbench.OpenDocument (((Project)nav.DataItem).FileName, (Project)nav.DataItem);
}
}
[CommandUpdateHandler (ProjectCommands.EditSolutionItem)]
public void OnEditProjectUpdate (CommandInfo info)
{
info.Visible = info.Enabled = CurrentNodes.All (nav => File.Exists (((Project)nav.DataItem).FileName));
}
public override DragOperation CanDragNode ()
{
return DragOperation.Copy | DragOperation.Move;
}
public override bool CanDropNode (object dataObject, DragOperation operation)
{
return base.CanDropNode (dataObject, operation);
}
public override void OnNodeDrop (object dataObject, DragOperation operation)
{
base.OnNodeDrop (dataObject, operation);
}
}
}