//
// ProjectFileNodeBuilder.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.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using MonoDevelop.Projects;
using MonoDevelop.Core;
using MonoDevelop.Ide.Commands;
using MonoDevelop.Components.Commands;
using MonoDevelop.Core.Collections;
using MonoDevelop.Ide.Gui.Components;
using MonoDevelop.Ide.Projects.FileNesting;
namespace MonoDevelop.Ide.Gui.Pads.ProjectPad
{
class ProjectFileNodeBuilder: TypeNodeBuilder
{
public override Type NodeDataType {
get { return typeof(ProjectFile); }
}
public override Type CommandHandlerType {
get { return typeof(ProjectFileNodeCommandHandler); }
}
public override string GetNodeName (ITreeNavigator thisNode, object dataObject)
{
var file = (ProjectFile) dataObject;
return file.Link.IsNullOrEmpty ? file.FilePath.FileName : file.Link.FileName;
}
public override void GetNodeAttributes (ITreeNavigator parentNode, object dataObject, ref NodeAttributes attributes)
{
var file = (ProjectFile) dataObject;
if ((file.Flags & ProjectItemFlags.Hidden) != 0) {
attributes |= NodeAttributes.Hidden;
return;
}
attributes |= NodeAttributes.AllowRename;
if (!file.Visible && !parentNode.Options ["ShowAllFiles"])
attributes |= NodeAttributes.Hidden;
}
public override void BuildNode (ITreeBuilder treeBuilder, object dataObject, NodeInfo nodeInfo)
{
ProjectFile file = (ProjectFile) dataObject;
nodeInfo.Label = GLib.Markup.EscapeText (file.Link.IsNullOrEmpty ? file.FilePath.FileName : file.Link.FileName);
if (!File.Exists (file.FilePath)) {
nodeInfo.Label = "" + nodeInfo.Label + "";
}
nodeInfo.Icon = IdeServices.DesktopService.GetIconForFile (file.FilePath, Gtk.IconSize.Menu);
if (file.IsLink && nodeInfo.Icon != null) {
var overlay = ImageService.GetIcon ("md-link-overlay").WithSize (Xwt.IconSize.Small);
nodeInfo.OverlayBottomRight = overlay;
}
}
public override object GetParentObject (object dataObject)
{
var file = (ProjectFile) dataObject;
var dir = !file.IsLink ? file.FilePath.ParentDirectory : file.Project.BaseDirectory.Combine (file.ProjectVirtualPath).ParentDirectory;
if (!string.IsNullOrEmpty (file.DependsOn)) {
ProjectFile groupUnder = file.Project.Files.GetFile (file.FilePath.ParentDirectory.Combine (file.DependsOn));
if (groupUnder != null)
return groupUnder;
} else {
// File nesting
var parentFile = FileNestingService.GetParentFile (file);
if (parentFile != null) {
return parentFile;
}
}
if (dir == file.Project.BaseDirectory)
return file.Project;
return new ProjectFolder (dir, file.Project, null);
}
public override int CompareObjects (ITreeNavigator thisNode, ITreeNavigator otherNode)
{
if (!(thisNode.DataItem is ProjectFile))
return DefaultSort;
if (!(otherNode.DataItem is ProjectFile))
return DefaultSort;
string name1 = thisNode.NodeName;
string name2 = otherNode.NodeName;
//Compare filenames without extension
string path1 = Path.GetFileNameWithoutExtension (name1);
string path2 = Path.GetFileNameWithoutExtension (name2);
int cmp = string.Compare (path1, path2, StringComparison.CurrentCultureIgnoreCase);
if (cmp != 0)
return cmp;
//Compare extensions
string ext1 = Path.GetExtension (name1);
string ext2 = Path.GetExtension (name2);
return string.Compare (ext1, ext2, StringComparison.CurrentCultureIgnoreCase);
}
public override bool HasChildNodes (ITreeBuilder builder, object dataObject)
{
ProjectFile file = (ProjectFile) dataObject;
return file.HasChildren || FileNestingService.HasChildren (file);
}
public override void BuildChildNodes (ITreeBuilder treeBuilder, object dataObject)
{
base.BuildChildNodes (treeBuilder, dataObject);
ProjectFile file = (ProjectFile) dataObject;
if (file.HasChildren)
treeBuilder.AddChildren (file.DependentChildren);
else {
var children = FileNestingService.GetChildren (file);
if ((children?.Count ?? 0) > 0) {
treeBuilder.AddChildren (children);
}
}
}
}
class ProjectFileNodeCommandHandler: NodeCommandHandler
{
public override void OnRenameStarting (ref int selectionStart, ref int selectionLength)
{
string name = CurrentNode.NodeName;
selectionStart = 0;
selectionLength = Path.GetFileNameWithoutExtension(name).Length;
}
public async override void RenameItem (string newName)
{
var file = (ProjectFile) CurrentNode.DataItem;
string oldFileName = file.FilePath;
string newFileName = Path.Combine (Path.GetDirectoryName (oldFileName), newName);
if (oldFileName == newFileName)
return;
var dependentFilesToRename = ProjectOperations.GetDependentFilesToRename (file, newName);
try {
if (CanRenameFile (file, newName)) {
if (dependentFilesToRename != null) {
if (dependentFilesToRename.Any (f => !CanRenameFile (f.File, f.NewName))) {
return;
}
}
FileService.RenameFile (file.FilePath, newName);
if (dependentFilesToRename != null) {
foreach (var dependentFile in dependentFilesToRename) {
FileService.RenameFile (dependentFile.File.FilePath, dependentFile.NewName);
}
}
if (file.Project != null)
await IdeApp.ProjectOperations.SaveAsync (file.Project);
}
} catch (ArgumentException) { // new file name with wildcard (*, ?) characters in it
MessageService.ShowWarning (GettextCatalog.GetString ("The name you have chosen contains illegal characters. Please choose a different name."));
} catch (IOException ex) {
MessageService.ShowError (GettextCatalog.GetString ("There was an error renaming the file."), ex);
}
}
static FilePath GetRenamedFilePath (ProjectFile file, string newName)
{
if (file.IsLink) {
var oldLink = file.ProjectVirtualPath;
var newLink = oldLink.ParentDirectory.Combine (newName);
return file.Project.BaseDirectory.Combine (newLink);
}
return file.FilePath.ParentDirectory.Combine (newName);
}
static bool CanRenameFile (ProjectFile file, string newName)
{
ProjectFile newProjectFile = null;
FilePath newPath = GetRenamedFilePath (file, newName);
if (file.Project != null)
newProjectFile = file.Project.Files.GetFileWithVirtualPath (newPath.ToRelative (file.Project.BaseDirectory));
if (!FileService.IsValidPath (newPath) || ProjectFolderCommandHandler.ContainsDirectorySeparator (newName)) {
MessageService.ShowWarning (GettextCatalog.GetString ("The name you have chosen contains illegal characters. Please choose a different name."));
return false;
} else if ((newProjectFile != null && newProjectFile != file) || FileExistsCaseSensitive (file.FilePath.ParentDirectory, newName)) {
// If there is already a file under the newPath which is *different*, then throw an exception
MessageService.ShowWarning (GettextCatalog.GetString ("File or directory name is already in use. Please choose a different one."));
return false;
}
return true;
}
static bool FileExistsCaseSensitive (FilePath parentDirectory, string fileName)
{
if (!Directory.Exists (parentDirectory))
return false;
return Directory.EnumerateFiles (parentDirectory, fileName)
.Any (file => Path.GetFileName (file) == fileName);
}
public override void ActivateItem ()
{
ProjectFile file = (ProjectFile) CurrentNode.DataItem;
IdeApp.Workbench.OpenDocument (file.FilePath, file.Project);
}
public override void ActivateMultipleItems ()
{
ProjectFile file;
for (int i = 0; i < CurrentNodes.Length; i++) {
// Only bring the last file to the front
file = (ProjectFile) CurrentNodes [i].DataItem;
IdeApp.Workbench.OpenDocument (file.FilePath, file.Project, i == CurrentNodes.Length - 1);
}
}
public override DragOperation CanDragNode ()
{
return DragOperation.Copy | DragOperation.Move;
}
public override bool CanDeleteItem ()
{
return true;
}
[CommandHandler (EditCommands.Delete)]
[AllowMultiSelection]
public override void DeleteMultipleItems ()
{
var projects = new Set ();
var files = new List ();
bool hasChildren = false;
foreach (var node in CurrentNodes) {
var pf = (ProjectFile) node.DataItem;
projects.Add (pf.Project);
if (pf.HasChildren || FileNestingService.HasChildren (pf))
hasChildren = true;
files.Add (pf);
}
string question;
bool allLinkedFiles = CheckAllLinkedFile (files);
if (allLinkedFiles) {
RemoveFilesFromProject (false, files);
} else {
bool filesExist = CheckAnyFileExists (files);
if (hasChildren) {
if (files.Count == 1)
question = GettextCatalog.GetString ("Are you sure you want to delete the file {0} and " +
"its code-behind children from project {1}?",
Path.GetFileName (files [0].Name), files [0].Project.Name);
else
question = GettextCatalog.GetString ("Are you sure you want to delete the selected files and " +
"their code-behind children from the project?");
} else {
if (files.Count == 1)
question = GettextCatalog.GetString ("Are you sure you want to delete file {0} from project {1}?",
Path.GetFileName (files [0].Name), files [0].Project.Name);
else
question = GettextCatalog.GetString ("Are you sure you want to delete the selected files from the project?");
}
var result = MessageService.AskQuestion (question, GetDeleteConfirmationButtons (filesExist));
if (result != AlertButton.Delete)
return;
RemoveFilesFromProject (true, files);
}
IdeApp.ProjectOperations.SaveAsync (projects);
}
[CommandUpdateHandler (EditCommands.Delete)]
[AllowMultiSelection]
void OnUpdateDeleteMultipleItems (CommandInfo info)
{
foreach (var node in CurrentNodes) {
var pf = (ProjectFile)node.DataItem;
if (pf.IsLink)
info.Text = GettextCatalog.GetString ("Remove");
}
}
static AlertButton [] GetDeleteConfirmationButtons (bool includeDelete)
{
if (includeDelete)
return new [] { AlertButton.Cancel, AlertButton.Delete };
return new [] { AlertButton.Cancel };
}
[CommandHandler (ProjectCommands.ExcludeFromProject)]
[AllowMultiSelection]
void OnExcludeFilesFromProject ()
{
var projects = new Set ();
var files = new List ();
foreach (var node in CurrentNodes) {
var pf = (ProjectFile)node.DataItem;
projects.Add (pf.Project);
files.Add (pf);
}
RemoveFilesFromProject (false, files);
IdeApp.ProjectOperations.SaveAsync (projects);
}
public void RemoveFilesFromProject (bool delete, List files)
{
foreach (var file in files) {
var project = file.Project;
var inFolder = project.Files.GetFilesInVirtualPath (file.ProjectVirtualPath.ParentDirectory).ToList ();
if (inFolder.Count == 1 && inFolder [0] == file && project.Files.GetFileWithVirtualPath (file.ProjectVirtualPath.ParentDirectory) == null) {
// This is the last project file in the folder. Make sure we keep
// a reference to the folder, so it is not deleted from the tree.
var folderFile = new ProjectFile (project.BaseDirectory.Combine (file.ProjectVirtualPath.ParentDirectory));
folderFile.Subtype = Subtype.Directory;
project.Files.Add (folderFile);
}
var children = FileNestingService.GetDependentOrNestedTree (file);
if (children != null) {
foreach (var child in children.ToArray ()) {
project.Files.Remove (child);
if(delete)
FileService.DeleteFile (child.Name);
}
}
project.Files.Remove (file);
if (delete && !file.IsLink)
FileService.DeleteFile (file.Name);
}
}
[CommandUpdateHandler (ProjectCommands.ExcludeFromProject)]
void UpdateExcludeFiles (CommandInfo info)
{
info.Enabled = CanDeleteMultipleItems ();
foreach (var node in CurrentNodes) {
var pf = (ProjectFile)node.DataItem;
info.Visible = !pf.IsLink;
}
}
static bool CheckAnyFileExists (IEnumerable files)
{
foreach (ProjectFile file in files) {
if (!file.IsLink && File.Exists (file.Name))
return true;
var children = FileNestingService.GetDependentOrNestedChildren (file);
if (children != null) {
foreach (var child in children.ToArray ()) {
if (File.Exists (child.Name))
return true;
}
}
}
return false;
}
static bool CheckAllLinkedFile (IEnumerable files)
{
foreach (ProjectFile file in files) {
if (!file.IsLink)
return false;
}
return true;
}
[CommandUpdateHandler (EditCommands.Delete)]
public void UpdateRemoveItem (CommandInfo info)
{
//don't allow removing children from parents. The parent can be removed and will remove the whole group.
info.Enabled = CanDeleteMultipleItems ();
}
[CommandHandler (ViewCommands.OpenWithList)]
public void OnOpenWith (object ob)
{
ProjectFile finfo = (ProjectFile) CurrentNode.DataItem;
((FileViewer)ob).OpenFile (finfo.Name);
}
[CommandUpdateHandler (ViewCommands.OpenWithList)]
public async Task OnOpenWithUpdate (CommandArrayInfo info, CancellationToken cancellationToken)
{
var pf = (ProjectFile) CurrentNode.DataItem;
await PopulateOpenWithViewers (info, pf.Project, pf.FilePath);
}
internal static async Task PopulateOpenWithViewers (CommandArrayInfo info, Project project, string filePath)
{
var viewers = (await IdeServices.DisplayBindingService.GetFileViewers (filePath, project)).ToList ();
//show the default viewer first
var def = viewers.FirstOrDefault (v => v.CanUseAsDefault) ?? viewers.FirstOrDefault (v => v.IsExternal);
if (def != null) {
CommandInfo ci = info.Add (def.Title, def);
ci.Description = GettextCatalog.GetString ("Open with '{0}'", def.Title);
if (viewers.Count > 1)
info.AddSeparator ();
}
//then the builtins, followed by externals
FileViewer prev = null;
foreach (FileViewer fv in viewers) {
if (def != null && fv.Equals (def))
continue;
if (prev != null && fv.IsExternal != prev.IsExternal)
info.AddSeparator ();
CommandInfo ci = info.Add (fv.Title, fv);
ci.Description = GettextCatalog.GetString ("Open with '{0}'", fv.Title);
prev = fv;
}
}
[CommandHandler (FileCommands.SetBuildAction)]
[AllowMultiSelection]
public void OnSetBuildAction (object ob)
{
Set projects = new Set ();
string action = (string)ob;
foreach (ITreeNavigator node in CurrentNodes) {
ProjectFile file = (ProjectFile) node.DataItem;
file.BuildAction = action;
projects.Add (file.Project);
}
IdeApp.ProjectOperations.SaveAsync (projects);
}
[CommandUpdateHandler (FileCommands.SetBuildAction)]
public void OnSetBuildActionUpdate (CommandArrayInfo info)
{
var toggledActions = new Set ();
Project proj = null;
ProjectFile finfo = null;
foreach (var node in CurrentNodes) {
finfo = (ProjectFile) node.DataItem;
//disallow multi-slect on more than one project, since available build actions may differ
if (proj == null && finfo.Project != null) {
proj = finfo.Project;
} else if (proj == null || proj != finfo.Project) {
info.Clear ();
return;
}
toggledActions.Add (finfo.BuildAction);
}
if (proj == null)
return;
foreach (string action in proj.GetBuildActions (finfo.FilePath)) {
if (action == "--") {
info.AddSeparator ();
} else {
CommandInfo ci = info.Add (action, action);
ci.Checked = toggledActions.Contains (action);
if (ci.Checked)
ci.CheckedInconsistent = toggledActions.Count > 1;
}
}
}
[CommandHandler (FileCommands.ShowProperties)]
[AllowMultiSelection]
public void OnShowProperties ()
{
IdeApp.Workbench.Pads.PropertyPad.BringToFront (true);
}
//NOTE: This command is slightly odd, as it operates on a tri-state value,
//when only being a dual-state control. However, it's straightforward enough.
// Enabled == (PreserveNewest | Always)
// Disabled == None
// Disabling == !None -> None
// Enabling == None -> PreserveNewest
//So there is no way to use.
[CommandHandler (FileCommands.CopyToOutputDirectory)]
[AllowMultiSelection]
public void OnCopyToOutputDirectory ()
{
//if all of the selection is already checked, then toggle checks them off
//else it turns them on. hence we need to find if they're all checked,
bool allChecked = true;
foreach (ITreeNavigator node in CurrentNodes) {
ProjectFile file = (ProjectFile) node.DataItem;
if (file.CopyToOutputDirectory == FileCopyMode.None) {
allChecked = false;
break;
}
}
Set projects = new Set ();
foreach (ITreeNavigator node in CurrentNodes) {
ProjectFile file = (ProjectFile) node.DataItem;
projects.Add (file.Project);
if (allChecked) {
file.CopyToOutputDirectory = FileCopyMode.None;
} else {
file.CopyToOutputDirectory = FileCopyMode.PreserveNewest;
}
}
IdeApp.ProjectOperations.SaveAsync (projects);
}
[CommandUpdateHandler (FileCommands.CopyToOutputDirectory)]
public void OnCopyToOutputDirectoryUpdate (CommandInfo info)
{
foreach (ITreeNavigator node in CurrentNodes) {
ProjectFile file = (ProjectFile) node.DataItem;
if (file.CopyToOutputDirectory != FileCopyMode.None) {
info.Checked = true;
} else if (info.Checked) {
info.CheckedInconsistent = true;
}
}
}
}
}