// Copyright Epic Games, Inc. All Rights Reserved. using EnvDTE; using EnvDTE80; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Animation; using System.Xml; namespace UnrealVS { public class Utils { public const string UProjectExtension = "uproject"; public class SafeProjectReference { public string FullName { get; set; } public string Name { get; set; } public Project GetProjectSlow() { ThreadHelper.ThrowIfNotOnUIThread(); Project[] Projects = GetAllProjectsFromDTE(); #pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread return Projects.FirstOrDefault(Proj => string.CompareOrdinal(Proj.FullName, FullName) == 0); #pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread } } /// /// Converts a Project to an IVsHierarchy /// /// Project object /// IVsHierarchy for the specified project public static IVsHierarchy ProjectToHierarchyObject(Project Project) { ThreadHelper.ThrowIfNotOnUIThread(); UnrealVSPackage.Instance.SolutionManager.GetProjectOfUniqueName(Project.FullName, out IVsHierarchy HierarchyObject); return HierarchyObject; } /// /// Converts an IVsHierarchy object to a Project /// /// IVsHierarchy object /// Visual Studio project object public static Project HierarchyObjectToProject(IVsHierarchy HierarchyObject) { ThreadHelper.ThrowIfNotOnUIThread(); // Get the actual Project object from the IVsHierarchy object that was supplied HierarchyObject.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID.VSHPROPID_ExtObject, out object ProjectObject); return (Project)ProjectObject; } /// /// Converts an IVsHierarchy object to a config provider interface /// /// IVsHierarchy object /// Visual Studio project object public static IVsCfgProvider2 HierarchyObjectToCfgProvider(IVsHierarchy HierarchyObject) { ThreadHelper.ThrowIfNotOnUIThread(); // Get the actual Project object from the IVsHierarchy object that was supplied HierarchyObject.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID.VSHPROPID_BrowseObject, out object BrowseObject); IVsCfgProvider2 CfgProvider = null; if (BrowseObject != null) { CfgProvider = GetCfgProviderFromObject(BrowseObject); } if (CfgProvider == null) { CfgProvider = GetCfgProviderFromObject(HierarchyObject); } return CfgProvider; } private static IVsCfgProvider2 GetCfgProviderFromObject(object SomeObject) { ThreadHelper.ThrowIfNotOnUIThread(); IVsCfgProvider2 CfgProvider2 = null; if (SomeObject is IVsGetCfgProvider GetCfgProvider) { GetCfgProvider.GetCfgProvider(out IVsCfgProvider CfgProvider); if (CfgProvider != null) { CfgProvider2 = CfgProvider as IVsCfgProvider2; } } if (CfgProvider2 == null) { CfgProvider2 = SomeObject as IVsCfgProvider2; } return CfgProvider2; } /// /// Locates a specific project property for the active configuration and returns it (or null if not found.) /// /// Project to search for the property /// Name of the property /// Property object or null if not found public static Property GetProjectProperty(Project Project, string PropertyName) { ThreadHelper.ThrowIfNotOnUIThread(); var Properties = Project.Properties; if (Properties != null) { foreach (var RawProperty in Properties) { var Property = (Property)RawProperty; if (Property.Name.Equals(PropertyName, StringComparison.InvariantCultureIgnoreCase)) { return Property; } } } // Not found return null; } /// /// Locates a specific project property for the active configuration and attempts to set its value /// /// The property object to set /// Value to set for this property public static void SetPropertyValue(Property Property, object PropertyValue) { ThreadHelper.ThrowIfNotOnUIThread(); Property.Value = PropertyValue; // @todo: Not sure if actually needed for command-line property (saved in .user files, not in project) // Mark the project as modified // @todo: Throws exception for C++ projects, doesn't mark as saved // Project.IsDirty = true; // Project.Saved = false; } /// /// Helper class used by the GetUIxxx functions below. /// Callers use this to easily traverse UIHierarchies. /// public class UITreeItem { public UIHierarchyItem Item { get; set; } public UITreeItem[] Children { get; set; } public string Name { get { ThreadHelper.ThrowIfNotOnUIThread(); return Item != null ? Item.Name : "None"; } } public object Object { get { return Item?.Object; } } } /// /// Converts a UIHierarchy into an easy to use tree of helper class UITreeItem. /// public static UITreeItem GetUIHierarchyTree(UIHierarchy Hierarchy) { ThreadHelper.ThrowIfNotOnUIThread(); return new UITreeItem { Item = null, Children = (from UIHierarchyItem Child in Hierarchy.UIHierarchyItems select GetUIHierarchyTree(Child)).ToArray() }; } /// /// Called by the public GetUIHierarchyTree() function above. /// private static UITreeItem GetUIHierarchyTree(UIHierarchyItem HierarchyItem) { ThreadHelper.ThrowIfNotOnUIThread(); return new UITreeItem { Item = HierarchyItem, Children = (from UIHierarchyItem Child in HierarchyItem.UIHierarchyItems select GetUIHierarchyTree(Child)).ToArray() }; } /// /// Helper function to easily extract a list of objects of type T from a UIHierarchy tree. /// /// The type of object to find in the tree. Extracts everything that "Is a" T. /// The root of the UIHierarchy to search (converted to UITreeItem via GetUIHierarchyTree()) /// An enumerable of objects of type T found beneath the root item. public static IEnumerable GetUITreeItemObjectsByType(UITreeItem RootItem) where T : class { List Results = new List(); if (RootItem.Object is T Obj) { Results.Add(Obj); } foreach (var Child in RootItem.Children) { Results.AddRange(GetUITreeItemObjectsByType(Child)); } return Results; } public static IEnumerable GetUITreeItemsByObjectType(UITreeItem RootItem) where T : class { List Results = new List(); if (RootItem.Object is T) { Results.Add(RootItem.Item); } foreach (var Child in RootItem.Children) { Results.AddRange(GetUITreeItemsByObjectType(Child)); } return Results; } /// /// Helper to check the file ext of a binary against known library file exts. /// FileExt should include the dot e.g. ".dll" /// public static bool IsLibraryFileExtension(string FileExt) { if (FileExt.Equals(".dll", StringComparison.InvariantCultureIgnoreCase)) return true; if (FileExt.Equals(".lib", StringComparison.InvariantCultureIgnoreCase)) return true; if (FileExt.Equals(".ocx", StringComparison.InvariantCultureIgnoreCase)) return true; if (FileExt.Equals(".a", StringComparison.InvariantCultureIgnoreCase)) return true; if (FileExt.Equals(".so", StringComparison.InvariantCultureIgnoreCase)) return true; if (FileExt.Equals(".dylib", StringComparison.InvariantCultureIgnoreCase)) return true; return false; } /// /// Helper to check the properties of a project and determine whether it can be built in VS. /// public static bool IsProjectBuildable(Project Project) { ThreadHelper.ThrowIfNotOnUIThread(); return Project.Kind == GuidList.VCSharpProjectKindGuidString || Project.Kind == GuidList.VCProjectKindGuidString; } /// Helper function to get the full list of all projects in the DTE Solution /// Recurses into items because these are actually in a tree structure public static Project[] GetAllProjectsFromDTE() { ThreadHelper.ThrowIfNotOnUIThread(); try { List Projects = new List(); foreach (Project Project in UnrealVSPackage.Instance.DTE.Solution.Projects) { Projects.Add(Project); if (Project.ProjectItems != null) { foreach (ProjectItem Item in Project.ProjectItems) { GetSubProjectsOfProjectItem(Item, Projects); } } } return Projects.ToArray(); } catch (Exception ex) { Exception AppEx = new ApplicationException("GetAllProjectsFromDTE() failed", ex); Logging.WriteLine(AppEx.ToString()); throw AppEx; } } public static void ExecuteProjectBuild(Project Project, string SolutionConfig, string SolutionPlatform, BatchBuilderToolControl.BuildJob.BuildJobType BuildType, Action ExecutingDelegate, Action FailedToStartDelegate) { ThreadHelper.ThrowIfNotOnUIThread(); IVsHierarchy ProjHierarchy = Utils.ProjectToHierarchyObject(Project); if (ProjHierarchy != null) { SolutionConfigurations SolutionConfigs = UnrealVSPackage.Instance.DTE.Solution.SolutionBuild.SolutionConfigurations; var MatchedSolutionConfig = (from SolutionConfiguration2 Sc in SolutionConfigs select Sc).FirstOrDefault( Sc => { ThreadHelper.ThrowIfNotOnUIThread(); return String.CompareOrdinal(Sc.Name, SolutionConfig) == 0 && String.CompareOrdinal(Sc.PlatformName, SolutionPlatform) == 0; }); if (MatchedSolutionConfig != null) { SolutionContext ProjectSolutionCtxt = MatchedSolutionConfig.SolutionContexts.Item(Project.UniqueName); if (ProjectSolutionCtxt != null) { IVsCfgProvider2 CfgProvider2 = Utils.HierarchyObjectToCfgProvider(ProjHierarchy); if (CfgProvider2 != null) { CfgProvider2.GetCfgOfName(ProjectSolutionCtxt.ConfigurationName, ProjectSolutionCtxt.PlatformName, out IVsCfg Cfg); if (Cfg != null) { ExecutingDelegate?.Invoke(); int JobResult = VSConstants.E_FAIL; if (BuildType == BatchBuilderToolControl.BuildJob.BuildJobType.Build) { JobResult = UnrealVSPackage.Instance.SolutionBuildManager.StartUpdateSpecificProjectConfigurations( 1, new[] { ProjHierarchy }, new[] { Cfg }, null, new uint[] { 0 }, null, (uint)VSSOLNBUILDUPDATEFLAGS.SBF_OPERATION_BUILD, 0); } else if (BuildType == BatchBuilderToolControl.BuildJob.BuildJobType.Rebuild) { JobResult = UnrealVSPackage.Instance.SolutionBuildManager.StartUpdateSpecificProjectConfigurations( 1, new[] { ProjHierarchy }, new[] { Cfg }, new uint[] { 0 }, null, null, (uint)(VSSOLNBUILDUPDATEFLAGS.SBF_OPERATION_BUILD | VSSOLNBUILDUPDATEFLAGS.SBF_OPERATION_FORCE_UPDATE), 0); } else if (BuildType == BatchBuilderToolControl.BuildJob.BuildJobType.Clean) { JobResult = UnrealVSPackage.Instance.SolutionBuildManager.StartUpdateSpecificProjectConfigurations( 1, new[] { ProjHierarchy }, new[] { Cfg }, new uint[] { 0 }, null, null, (uint)VSSOLNBUILDUPDATEFLAGS.SBF_OPERATION_CLEAN, 0); } if (JobResult == VSConstants.S_OK) { // Job running - show output PrepareOutputPane(); } else { FailedToStartDelegate?.Invoke(); } } } } } } } private static bool LoadConfigFromUBT(Project SelectedProject) { ThreadHelper.ThrowIfNotOnUIThread(); string ProjectPath = Path.GetDirectoryName(SelectedProject.FullName); string ConfigFileName = Path.Combine(ProjectPath, "UnrealVS.xml"); // only try to load the xml configuration once if (CachedUBTConfigFileName != ConfigFileName) { CachedUBTConfigFileName = ConfigFileName; CachedUBTConfigXml = null; try { XmlDocument ConfigXml = new XmlDocument(); ConfigXml.Load(ConfigFileName); CachedUBTConfigXml = ConfigXml.SelectSingleNode("UnrealVS"); } catch (Exception) { } } return (CachedUBTConfigXml != null); } public static List GetExtraDebuggerCommandArguments(string PlatformName, Project SelectedProject) { ThreadHelper.ThrowIfNotOnUIThread(); List Result = new List(); if (LoadConfigFromUBT(SelectedProject)) { XmlNode PlatformNode = CachedUBTConfigXml.SelectSingleNode(PlatformName); if (PlatformNode != null) { foreach (XmlNode ChildNode in PlatformNode.ChildNodes) { if (string.Equals(ChildNode.Name, "DebuggerName", StringComparison.CurrentCultureIgnoreCase)) { Result.Add(ChildNode.InnerText); } } } } return Result; } public static bool IsGameProject(Project Project) { ThreadHelper.ThrowIfNotOnUIThread(); return GetUProjects().ContainsKey(Project.Name); } public static bool IsTestTargetProject(Project Project) { ThreadHelper.ThrowIfNotOnUIThread(); if (Project.Globals.VariableExists["IsTestTarget"]) { return Convert.ToBoolean(Project.Globals["IsTestTarget"]); } return false; } /// /// Does the config build something that takes a .uproject on the command line? /// public static bool HasUProjectCommandLineArg(string Config) { return Config.EndsWith("Editor", StringComparison.InvariantCultureIgnoreCase); } public static string GetUProjectFileName(Project Project) { ThreadHelper.ThrowIfNotOnUIThread(); return Project.Name + "." + UProjectExtension; } public static string GetAutoUProjectCommandLinePrefix(Project Project) { ThreadHelper.ThrowIfNotOnUIThread(); var UProjectFileName = GetUProjectFileName(Project); var AllUProjects = GetUProjects(); if (!AllUProjects.TryGetValue(Project.Name, out string UProjectPath)) { // Search the project folder var ProjectFolder = Path.GetDirectoryName(Project.FullName); var UProjUnderProject = Directory.GetFiles(ProjectFolder, UProjectFileName, SearchOption.TopDirectoryOnly); if (UProjUnderProject.Length == 1) { UProjectPath = UProjUnderProject[0]; } } return '\"' + UProjectPath + '\"'; } public static void AddProjects(DirectoryInfo ProjectDir, List Files) { Files.AddRange(ProjectDir.EnumerateFiles("*.uproject")); } /// /// Enumerate projects under the given directory /// /// Base directory to enumerate /// List of project files static List EnumerateProjects(DirectoryInfo SolutionDir) { // Enumerate all the projects in the same directory as the solution. If there's one here, we don't need to consider any other. List ProjectFiles = new List(SolutionDir.EnumerateFiles("*.uproject")); if (ProjectFiles.Count == 0) { // Build a list of all the parent directories for projects. This includes the UE root, plus any directories referenced via .uprojectdirs files. List ParentProjectDirs = new List { SolutionDir }; // Read all the .uprojectdirs files foreach (FileInfo ProjectDirsFile in SolutionDir.EnumerateFiles("*.uprojectdirs")) { foreach (string Line in File.ReadAllLines(ProjectDirsFile.FullName)) { string TrimLine = Line.Trim().Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar).Trim(Path.DirectorySeparatorChar); if (TrimLine.Length > 0 && !TrimLine.StartsWith(";")) { try { ParentProjectDirs.Add(new DirectoryInfo(Path.Combine(SolutionDir.FullName, TrimLine))); } catch (Exception Ex) { Logging.WriteLine(String.Format("EnumerateProjects: Exception trying to resolve project directory '{0}': {1}", TrimLine, Ex.Message)); } } } } // Add projects in any subfolders of the parent directories HashSet CheckedParentDirs = new HashSet(StringComparer.InvariantCultureIgnoreCase); foreach (DirectoryInfo ParentProjectDir in ParentProjectDirs) { if (CheckedParentDirs.Add(ParentProjectDir.FullName) && ParentProjectDir.Exists) { foreach (DirectoryInfo ProjectDir in ParentProjectDir.EnumerateDirectories()) { try { ProjectFiles.AddRange(ProjectDir.EnumerateFiles("*.uproject")); } catch(Exception Ex) { Logging.WriteLine($"AddingProjects: Exception trying to add projects from directory '{ProjectDir}': {Ex.Message}"); } } } } } return ProjectFiles; } /// /// Returns all the .uprojects found under the solution root folder. /// public static IDictionary GetUProjects() { ThreadHelper.ThrowIfNotOnUIThread(); var Folder = GetSolutionFolder(); if (string.IsNullOrEmpty(Folder)) { return new Dictionary(); } if (Folder != CachedUProjectRootFolder) { Logging.WriteLine("GetUProjects: recaching uproject paths..."); DateTime Start = DateTime.Now; CachedUProjectRootFolder = Folder; CachedUProjectPaths = EnumerateProjects(new DirectoryInfo(Folder)).Select(x => x.FullName); CachedUProjects = null; TimeSpan TimeTaken = DateTime.Now - Start; Logging.WriteLine(string.Format("GetUProjects: EnumerateProjects took {0} sec", TimeTaken.TotalSeconds)); foreach (string CachedUProjectPath in CachedUProjectPaths) { Logging.WriteLine(String.Format("GetUProjects: found {0}", CachedUProjectPath)); } Logging.WriteLine(" DONE"); } if (CachedUProjects == null) { Logging.WriteLine("GetUProjects: recaching uproject names..."); var ProjectPaths = UnrealVSPackage.Instance.GetLoadedProjectPaths(); var ProjectNames = (from path in ProjectPaths select Path.GetFileNameWithoutExtension(path)).ToArray(); var CodeUProjects = from UProjectPath in CachedUProjectPaths let ProjectName = Path.GetFileNameWithoutExtension(UProjectPath) where ProjectNames.Any(name => string.Compare(name, ProjectName, StringComparison.OrdinalIgnoreCase) == 0) select new { Name = ProjectName, FilePath = UProjectPath }; CachedUProjects = new Dictionary(); foreach (var UProject in CodeUProjects) { if (!CachedUProjects.ContainsKey(UProject.Name)) { CachedUProjects.Add(UProject.Name, UProject.FilePath); } } Logging.WriteLine(" DONE"); } return CachedUProjects; } public static void GetSolutionConfigsAndPlatforms(out string[] SolutionConfigs, out string[] SolutionPlatforms) { ThreadHelper.ThrowIfNotOnUIThread(); var UniqueConfigs = new List(); var UniquePlatforms = new List(); SolutionConfigurations DteSolutionConfigs = UnrealVSPackage.Instance.DTE.Solution.SolutionBuild.SolutionConfigurations; foreach (SolutionConfiguration2 SolutionConfig in DteSolutionConfigs) { if (!UniqueConfigs.Contains(SolutionConfig.Name)) { UniqueConfigs.Add(SolutionConfig.Name); } if (!UniquePlatforms.Contains(SolutionConfig.PlatformName)) { UniquePlatforms.Add(SolutionConfig.PlatformName); } } SolutionConfigs = UniqueConfigs.ToArray(); SolutionPlatforms = UniquePlatforms.ToArray(); } public static bool SetActiveSolutionConfiguration(string ConfigName, string PlatformName) { ThreadHelper.ThrowIfNotOnUIThread(); SolutionConfigurations DteSolutionConfigs = UnrealVSPackage.Instance.DTE.Solution.SolutionBuild.SolutionConfigurations; foreach (SolutionConfiguration2 SolutionConfig in DteSolutionConfigs) { if (string.Compare(SolutionConfig.Name, ConfigName, StringComparison.Ordinal) == 0 && string.Compare(SolutionConfig.PlatformName, PlatformName, StringComparison.Ordinal) == 0) { SolutionConfig.Activate(); return true; } } return false; } public static bool SelectProjectInSolutionExplorer(Project Project) { ThreadHelper.ThrowIfNotOnUIThread(); UnrealVSPackage.Instance.DTE.ExecuteCommand("View.SolutionExplorer"); if (Project.ParentProjectItem != null) { Project.ParentProjectItem.ExpandView(); } UIHierarchy SolutionExplorerHierarachy = UnrealVSPackage.Instance.DTE2.ToolWindows.SolutionExplorer; Utils.UITreeItem SolutionExplorerTree = Utils.GetUIHierarchyTree(SolutionExplorerHierarachy); var UIHierarachyProjects = Utils.GetUITreeItemsByObjectType(SolutionExplorerTree); #pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread var SelectableUIItem = UIHierarachyProjects.FirstOrDefault(uihp => uihp.Object as Project == Project); #pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread if (SelectableUIItem != null) { if (Project.ParentProjectItem != null) { SelectableUIItem.Select(vsUISelectionType.vsUISelectionTypeSelect); return true; } } return false; } public static void OnProjectListChanged() { CachedUProjects = null; } private static void PrepareOutputPane() { ThreadHelper.ThrowIfNotOnUIThread(); UnrealVSPackage.Instance.DTE.ExecuteCommand("View.Output"); var Pane = UnrealVSPackage.Instance.GetOutputPane(); if (Pane != null) { // Clear and activate the output pane. Pane.Clear(); // @todo: Activating doesn't seem to really bring the pane to front like we would expect it to. Pane.Activate(); } } /// Called by GetAllProjectsFromDTE() to list items from the project tree private static void GetSubProjectsOfProjectItem(ProjectItem Item, List Projects) { ThreadHelper.ThrowIfNotOnUIThread(); if (Item.SubProject != null) { Projects.Add(Item.SubProject); if (Item.SubProject.ProjectItems != null) { foreach (ProjectItem SubItem in Item.SubProject.ProjectItems) { GetSubProjectsOfProjectItem(SubItem, Projects); } } } if (Item.ProjectItems != null) { foreach (ProjectItem SubItem in Item.ProjectItems) { GetSubProjectsOfProjectItem(SubItem, Projects); } } } public static string GetSolutionFolder() { ThreadHelper.ThrowIfNotOnUIThread(); if (!UnrealVSPackage.Instance.DTE.Solution.IsOpen) { return string.Empty; } return Path.GetDirectoryName(UnrealVSPackage.Instance.SolutionFilepath); } public static string SolutionTitle { set { if (!SolutionTextBlockSearched) { SolutionTextBlockSearched = true; if (Utils.FindChild(System.Windows.Application.Current.MainWindow, "PART_SolutionNameTextBlock") is var textBlock) { SolutionTextBlock = Utils.FindChild(textBlock, null); } } if (SolutionTextBlock != null) { SolutionTextBlockText = value; if (value != null) { SolutionTextBlockChanging = true; SolutionTextBlock.Text = value; SolutionTextBlockChanging = false; if (!SolutionTextBlockTracked) { SolutionTextBlockTracked = true; var dp = DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, typeof(TextBlock)); dp.AddValueChanged(SolutionTextBlock, SolutionTextChanged); } } else { if (SolutionTextBlockTracked) { SolutionTextBlockTracked = false; var dp = DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, typeof(TextBlock)); dp.RemoveValueChanged(SolutionTextBlock, SolutionTextChanged); } SolutionTextBlock.InvalidateVisual(); } } } } static void SolutionTextChanged(object sender, EventArgs e) { // Needed to prevent visual studio from changing text back to name from property if (!SolutionTextBlockChanging && SolutionTextBlockText != null) { SolutionTextBlockChanging = true; SolutionTextBlock.Text = SolutionTextBlockText; SolutionTextBlockChanging = false; } } public static string MainWindowTitle { get => System.Windows.Application.Current.MainWindow.Title; set { System.Windows.Application.Current.MainWindow.Title = value; } } public static T FindChild(System.Windows.DependencyObject Parent, string ChildName) where T : System.Windows.DependencyObject { if (Parent == null) { return null; } int ChildrenCount = VisualTreeHelper.GetChildrenCount(Parent); for (int i = 0; i < ChildrenCount; i++) { var Child = VisualTreeHelper.GetChild(Parent, i); if (ChildName != null) { // If the child's name is set for search var FrameworkElement = Child as System.Windows.FrameworkElement; if (FrameworkElement != null) { if (FrameworkElement.Name == ChildName) { // if the child's name is of the request name return (T)Child; } } } else { if (Child is T TypedChild) { return TypedChild; } } // recursively drill down the tree T FoundChild = FindChild(Child, ChildName); if (FoundChild != null) { return FoundChild; } } return null; } private static string SolutionTextBlockText; private static TextBlock SolutionTextBlock; private static bool SolutionTextBlockTracked; private static bool SolutionTextBlockChanging; private static bool SolutionTextBlockSearched; private static string CachedUProjectRootFolder = string.Empty; private static IEnumerable CachedUProjectPaths = new string[0]; private static IDictionary CachedUProjects = null; private static string CachedUBTConfigFileName = string.Empty; private static XmlNode CachedUBTConfigXml = null; } }