// Copyright Epic Games, Inc. All Rights Reserved. using EnvDTE; using EnvDTE80; using Microsoft.VisualStudio; using Microsoft.VisualStudio.OLE.Interop; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using System; using System.Collections.Generic; using System.ComponentModel.Design; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using Thread = System.Threading.Thread; using Microsoft.VisualStudio.ComponentModelHost; using Microsoft.VisualStudio.Text.Editor; using Microsoft; using System.Text; namespace UnrealVS { public class UnrealSolutionProperties { public UnrealSolutionProperties(string[] InAvailablePlatforms) { AvailablePlatforms = InAvailablePlatforms; } public string[] AvailablePlatforms { get; } } // This attribute tells the PkgDef creation utility (CreatePkgDef.exe) that this class is a VS package. [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] // This attribute is used to register the informations needed to show the this package in the Help/About dialog of Visual Studio. [InstalledProductRegistration("#110", "#112", VersionString, IconResourceID = 400)] // Adds data for our interface elements defined in UnrealVS.vsct. The name "Menus.ctmenu" here is arbitrary, but must // match the ItemGroup inside the UnrealVS.csproj MSBuild file (hand-typed.) [ProvideMenuResource("Menus.ctmenu", 1)] // Force the package to load whenever a solution exists //[ProvideAutoLoad(VSConstants.UICONTEXT.SolutionExistsAndFullyLoaded_string, PackageAutoLoadFlags.BackgroundLoad)] [ProvideAutoLoad(UIContextGuids80.SolutionExists, PackageAutoLoadFlags.BackgroundLoad)] // Register the settings implementing class as providing support to UnrealVSPackage. //[ProvideProfile(typeof (ProfileManager), "UnrealVS", "UnrealVSPackage", 110, 113, false)] // GUID for this class. This needs to match in quite a few places in the project. [Guid(GuidList.UnrealVSPackageString)] // This attribute registers a tool window exposed by this package. [ProvideToolWindow(typeof(BatchBuilderToolWindow))] // File browser [ProvideToolWindow(typeof(FileBrowserWindow))] // UbaVisualizer [ProvideToolWindow(typeof(UbaVisualizerWindow))] [ProvideToolWindowVisibility(typeof(UbaVisualizerWindow), VSConstants.UICONTEXT.SolutionExists_string)] // This attribute registers an options page for the package. [ProvideOptionPage(typeof(UnrealVsOptions), ExtensionName, "General", 101, 102, true)] [ProvideSolutionProperties(GuidList.UnrealVSPackageString)] /// /// UnrealVSPackage implements Package abstract class. This is the main class that is registered /// with Visual Studio shell and serves as the entry point into our extension /// public sealed class UnrealVSPackage : AsyncPackage, // We inherit from AsyncPackage which allows us to become a "plugin" within the Visual Studio shell IVsSolutionEvents, // This interface allows us to register to be notified of events such as opening a project IVsUpdateSolutionEvents,// Allows us to register to be notified of events such as active config changes IVsSelectionEvents, // Allows us to be notified when the startup project has changed to a different project IVsHierarchyEvents, // Allows us to be notified when a hierarchy (the startup project) has had properties changed IVsPersistSolutionProps, // Allows us to read props added to the solution to determine if the solution is a unreal solution IVsDebuggerEvents, IDisposable { /** Constants */ private const string VersionString = "v1.84"; private const string UnrealSolutionFileNamePrefix = "UE"; private const string ExtensionName = "UnrealVS"; private const string CommandLineOptionKey = ExtensionName + "CommandLineMRU"; private const string BatchBuildSetsOptionKey = ExtensionName + "BatchBuildSetsV002"; private readonly TimeSpan TickPeriod = TimeSpan.FromSeconds(1.0); /** Events */ /// Called when a new startup project is set in Visual Studio public delegate void OnStartupProjectChangedDelegate(Project NewStartupProject); public event OnStartupProjectChangedDelegate OnStartupProjectChanged; /// Called when a project is opened or created in Visual Studio public delegate void OnProjectOpenedDelegate(Project OpenedProject); public event OnProjectOpenedDelegate OnProjectOpened; /// Called right before a project is closed public delegate void OnProjectClosedDelegate(Project ClosedProject); public event OnProjectClosedDelegate OnProjectClosed; /// Called when a project is loaded in Visual Studio public delegate void OnProjectLoadedDelegate(Project LoadedProject); public event OnProjectLoadedDelegate OnProjectLoaded; /// Called right before a project is unloaded in Visual Studio public delegate void OnProjectUnloadingDelegate(Project UnloadedProject); public event OnProjectUnloadingDelegate OnProjectUnloading; /// Called when the startup project is edited in Visual Studio public delegate void OnStartupProjectPropertyChangedDelegate(UInt32 itemid, Int32 propid, UInt32 flags); public event OnStartupProjectPropertyChangedDelegate OnStartupProjectPropertyChanged; /// Called right after a solution is opened public delegate void OnSolutionOpenedDelegate(); public event OnSolutionOpenedDelegate OnSolutionOpened; /// Called right before/after a solution is closed public delegate void OnSolutionClosedDelegate(); public event OnSolutionClosedDelegate OnSolutionClosing; public event OnSolutionClosedDelegate OnSolutionClosed; /// Called when the active project config changes for any project public delegate void OnStartupProjectConfigChangedDelegate(Project Project); public event OnStartupProjectConfigChangedDelegate OnStartupProjectConfigChanged; /// Called when a build/update action begins public delegate void OnBuildBeginDelegate(out int Cancel); public event OnBuildBeginDelegate OnBuildBegin; /// Called when a build/update action completes public delegate void OnBuildDoneDelegate(bool bSucceeded, bool bModified, bool bWasCancelled); public event OnBuildDoneDelegate OnBuildDone; /// Called when the UIContext changes public delegate void OnUIContextChangedDelegate(uint CmdUICookie, bool bActive); public event OnUIContextChangedDelegate OnUIContextChanged; public delegate void OnDocumentActivatedDelegate(Document Document); public event OnDocumentActivatedDelegate OnDocumentActivated; /** Public Fields & Properties */ /// Returns singleton instance of UnrealVSPackage public static UnrealVSPackage Instance { get { return PrivateInstance; } } /// Visual Studio menu command service public IMenuCommandService MenuCommandService { get; private set; } /// Visual Studio solution build manager interface. This is used to change the active startup /// Project, among other things. We expose public access to the solution build manager through /// our singleton instance public IVsSolutionBuildManager2 SolutionBuildManager { get; private set; } /// Visual Studio solution build manager interface. This is used to change the active startup /// Project, among other things. We expose public access to the solution build manager through /// our singleton instance public IVsDebugger Debugger { get; private set; } /// Visual Studio solution "manager" interface. We register with this to receive events /// about projects being added and such. This needs to be cleaned up at shutdown. public IVsSolution2 SolutionManager { get; private set; } /// Our startup project selector component public StartupProjectSelector StartupProjectSelector { get; private set; } /// Our quick build component public QuickBuild QuickBuilder { get; private set; } /// Visual Studio shell selection manager interface. Used to receive notifications about /// startup projects changes, among other things. public IVsMonitorSelection SelectionManager { get; private set; } /// Variable keeps track of whether a supported Unreal solution is loaded public bool IsUESolutionLoaded { get { return _IsUESolutionLoaded.GetValueOrDefault(false); } private set { _IsUESolutionLoaded = value; } } /// Variable keeps track of the loaded solution private string _SolutionFilepath; public string SolutionFilepath { get { return _SolutionFilepath; } } public UnrealSolutionProperties UnrealSolutionProperties { get; private set; } /** Methods */ /// /// Package constructor. The package is being created but Visual Studio isn't fully initialized yet, so /// it's NOT SAFE to call into Visual Studio services from here. Do that in Initialize() instead. /// public UnrealVSPackage() { // Register this key string so the package can save command line data to solution options files. // See OnLoadOptions() & OnSaveOptions() AddOptionKey(CommandLineOptionKey); AddOptionKey(BatchBuildSetsOptionKey); // Setup singleton instance PrivateInstance = this; Logging.Initialize(ExtensionName, VersionString); Logging.WriteLine("Loading UnrealVS extension package..."); } /// /// Initializes the package right after it's been "sited" into the fully-initialized Visual Studio IDE. /// protected override async System.Threading.Tasks.Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { Logging.WriteLine("Initializing UnrealVS extension..."); await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); // Grab the MenuCommandService MenuCommandService = await GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService; // Get access to Visual Studio's DTE object. This object has various hooks into the Visual Studio // shell that are useful for writing extensions. DTE = await GetServiceAsync(typeof(DTE)) as DTE; Assumes.Present(DTE); Logging.WriteLine("DTE version " + DTE.Version); // We previously deferred getting the DTE2 object but there are some cases where it's not running yet, so try here _DTE2 = await GetServiceAsync(typeof(SDTE)) as DTE2; Assumes.Present(_DTE2); Assumes.True(_DTE2.DTE == DTE); //TextManager = await GetServiceAsync(typeof(VsTextManagerClass)) as IVsTextManager3; var componentModel = (IComponentModel)GetGlobalService(typeof(SComponentModel)); EditorOptionsFactory = componentModel.GetService(); // Get selection manager and register to receive events SelectionManager = await GetServiceAsync(typeof(SVsShellMonitorSelection)) as IVsMonitorSelection; Assumes.Present(SelectionManager); SelectionManager.AdviseSelectionEvents(this, out SelectionEventsHandle); // Get solution and register to receive events SolutionManager = await GetServiceAsync(typeof(SVsSolution)) as IVsSolution2; Assumes.Present(SolutionManager); UpdateUnrealLoadedStatus(); SolutionManager.AdviseSolutionEvents(this, out SolutionEventsHandle); // Grab the solution build manager. We need this in order to change certain things about the Visual // Studio environment, like what the active startup project is // Get solution build manager SolutionBuildManager = await GetServiceAsync(typeof(SVsSolutionBuildManager)) as IVsSolutionBuildManager2; Assumes.Present(SolutionBuildManager); SolutionBuildManager.AdviseUpdateSolutionEvents(this, out UpdateSolutionEventsHandle); // Get debugger Debugger = await GetServiceAsync(typeof(SVsShellDebugger)) as IVsDebugger; Assumes.Present(Debugger); Debugger.AdviseDebuggerEvents(this, out DebuggerEventsHandle); // Create our command-line editor CommandLineEditor.Initialize(); // Create our startup project selector StartupProjectSelector = new StartupProjectSelector(); // Create 'BuildStartupProject' instance BuildStartupProject = new BuildStartupProject(); // Create 'CompileSingleFile' instance CompileSingleFile = new CompileSingleFile(); // Create 'GenerateProjectFiles' tools GenerateProjectFiles = new GenerateProjectFiles(); // Create Batch Builder tools BatchBuilder.Initialize(); // Create the project menu quick builder QuickBuilder = new QuickBuild(); // Create 'Perforce menu' instance P4CommandsGroup = new P4Commands(); // Create 'FileBrowser' instance FileBrowser = new FileBrowser(); // Create 'UbaVisualizer' instance UbaVisualizer = new UbaVisualizer(); // Call parent implementation base.Initialize(); if (DTE.Solution.IsOpen) { StartTicker(); } DTE2.Events.WindowEvents.WindowActivated += WindowEvents_WindowActivated; } private void WindowEvents_WindowActivated(Window GotFocus, Window LostFocus) { ThreadHelper.ThrowIfNotOnUIThread(); if (GotFocus.Document != null) OnDocumentActivated(GotFocus.Document); } private void StartTicker() { bRefreshTitleInTick = true; // Create a "ticker" on a background thread that ticks the package on the UI thread Interlocked.Exchange(ref bCancelTicker, 0); Ticker = new Thread(TickAsyncMain) { Priority = ThreadPriority.Lowest }; Ticker.Start(); } private void StopTicker() { if (bCancelTicker == 0) { Interlocked.Exchange(ref bCancelTicker, 1); } } /// /// Tick loop on worker thread /// private void TickAsyncMain() { try { bool bLastTick = false; while (true) { ThreadHelper.JoinableTaskFactory.Run(async () => { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); if (bCancelTicker != 0) bLastTick = true; Tick(bLastTick); }); if (bLastTick) return; Thread.Sleep(TickPeriod); } } catch (ThreadAbortException) { } } /// /// Tick function on main UI thread /// private void Tick(bool bLastTick) { ThreadHelper.ThrowIfNotOnUIThread(); TickRefreshTitle(bLastTick); BatchBuilder.Tick(); } public void TickRefreshTitle(bool bLastTick) { ThreadHelper.ThrowIfNotOnUIThread(); if (bLastTick) { Utils.SolutionTitle = null; return; } if (bRefreshTitleInTick) { bRefreshTitleInTick = false; if (!UnrealVSPackage.Instance.OptionsPage.IncludeFolderInUE5SolutionName) { return; } string SolutionFileName = UnrealVSPackage.Instance.DTE.Solution.FileName; string SolutionName = Path.GetFileNameWithoutExtension(SolutionFileName); // Only affect UE5 solution if (SolutionName != "UE5") { return; } string FolderName = Path.GetFileName(Path.GetDirectoryName(SolutionFileName)); string Mode = ""; if (DbgMode == DBGMODE.DBGMODE_Run) Mode = " (Running)"; if (DbgMode == DBGMODE.DBGMODE_Break) Mode = " (Debugging)"; string NewTitle = FolderName + '\\' + SolutionName; Utils.SolutionTitle = NewTitle; Utils.MainWindowTitle = NewTitle + Mode + " - Microsoft Visual Studio"; } } /// /// Implementation from IDisposable /// public void Dispose() { ThreadHelper.ThrowIfNotOnUIThread(); Dispose(true); } /// IDispose pattern lets us clean up our stuff! protected override void Dispose(bool disposing) { ThreadHelper.ThrowIfNotOnUIThread(); if (Ticker != null && Ticker.IsAlive) { Thread.Sleep(TickPeriod + TickPeriod); if (Ticker.IsAlive) { Logging.WriteLine("WARNING: Force aborting Ticker thread"); Ticker.Abort(); } } base.Dispose(disposing); // Clean up singleton instance PrivateInstance = null; StartupProjectSelector = null; BatchBuilder = null; QuickBuilder = null; CompileSingleFile?.Dispose(); CompileSingleFile = null; P4CommandsGroup?.Dispose(); P4CommandsGroup = null; CommandLineEditor?.Dispose(); CommandLineEditor = null; // No longer want solution events if (SolutionEventsHandle != 0) { SolutionManager.UnadviseSolutionEvents(SolutionEventsHandle); SolutionEventsHandle = 0; } SolutionManager = null; // No longer want selection events if (SelectionEventsHandle != 0) { SelectionManager.UnadviseSelectionEvents(SelectionEventsHandle); SelectionEventsHandle = 0; } SelectionManager = null; if (DebuggerEventsHandle != 0) Debugger.UnadviseDebuggerEvents(DebuggerEventsHandle); Debugger = null; // No longer want update solution events if (UpdateSolutionEventsHandle != 0) { SolutionBuildManager.UnadviseUpdateSolutionEvents(UpdateSolutionEventsHandle); UpdateSolutionEventsHandle = 0; } SolutionBuildManager = null; Logging.WriteLine("Closing UnrealVS extension"); Logging.Close(); } /// Visual Studio shell DTE interface public DTE DTE { get; private set; } /// Visual Studio shell DTE2 interface private DTE2 _DTE2; public DTE2 DTE2 { get { ThreadHelper.ThrowIfNotOnUIThread(); if (_DTE2 == null) { // Get the interface when first used. // This method fails during the Initialize() of the // package due to the strange method it requires. _DTE2 = GetDTE2ForCurrentInstance(DTE); } return _DTE2; } } //public IVsTextManager3 TextManager //{ // get; // private set; //} public IEditorOptionsFactoryService EditorOptionsFactory { get; private set; } /// The package's options page public UnrealVsOptions OptionsPage { get { return (UnrealVsOptions)GetDialogPage(typeof(UnrealVsOptions)); } } /// /// Launches a program /// /// Path to the program to run /// Command-line arguments /// Optional callback whent he program exits /// If supplied, std-out and std-error will be redirected to this function and no shell window will be created /// The newly-created process, or null if it wasn't able to start. Exceptions are swallowed, but a debug output string is emitted on errors public static System.Diagnostics.Process LaunchProgram(string ProgramFile, string Arguments, EventHandler OnExit = null, DataReceivedEventHandler OutputHandler = null, bool bWaitForCompletion = false) { // Create the action's process. ProcessStartInfo ActionStartInfo = new ProcessStartInfo { FileName = ProgramFile, Arguments = Arguments }; if (OutputHandler != null) { ActionStartInfo.RedirectStandardInput = true; ActionStartInfo.RedirectStandardOutput = true; ActionStartInfo.RedirectStandardError = true; // True to use a DOS box to run the program in, otherwise false to run directly. In order to redirect // output, UseShellExecute must be disabled ActionStartInfo.UseShellExecute = false; // Don't show the DOS box, since we're redirecting everything ActionStartInfo.CreateNoWindow = true; } Logging.WriteLine(String.Format("Executing: {0} {1}", ActionStartInfo.FileName, ActionStartInfo.Arguments)); System.Diagnostics.Process ActionProcess; try { ActionProcess = new System.Diagnostics.Process { StartInfo = ActionStartInfo }; if (OnExit != null) { ActionProcess.EnableRaisingEvents = true; ActionProcess.Exited += OnExit; } if (ActionStartInfo.RedirectStandardOutput) { ActionProcess.EnableRaisingEvents = true; ActionProcess.OutputDataReceived += OutputHandler; ActionProcess.ErrorDataReceived += OutputHandler; } // Launch the program ActionProcess.Start(); if (ActionStartInfo.RedirectStandardOutput) { ActionProcess.BeginOutputReadLine(); ActionProcess.BeginErrorReadLine(); } if (bWaitForCompletion) { while ((ActionProcess != null) && (!ActionProcess.HasExited)) { ActionProcess.WaitForExit(50); } } } catch (Exception Ex) { // Couldn't launch program Logging.WriteLine("Couldn't launch program: " + ActionStartInfo.FileName); Logging.WriteLine("Exception: " + Ex.Message); ActionProcess = null; } return ActionProcess; } /// /// Gets a Visual Studio pane to output text to, or creates one if not visible. Does not bring the pane to front (you can call Activate() to do that.) /// /// The pane to output to, or null on error public IVsOutputWindowPane GetOutputPane() { return GetOutputPane(VSConstants.OutputWindowPaneGuid.BuildOutputPane_guid, "Build"); } /// /// Gets a Visual Studio pane to output text to for P4 ops, or creates one if not visible. Does not bring the pane to front (you can call Activate() to do that.) /// /// The pane to output to, or null on error public IVsOutputWindowPane GetP4OutputPane() { return GetOutputPane(VSConstants.OutputWindowPaneGuid.GeneralPane_guid, "UnrealVS.P4"); } public IEnumerable GetLoadedProjectPaths() { return LoadedProjectPaths; } /// /// Overrides Package.OnLoadOptions() /// Invoked by the package class when there are options to be read out of the solution file. /// /// The name of the option key to load. /// The stream to load the option data from. protected override void OnLoadOptions(string key, Stream stream) { Logging.WriteLine("Loading Options for key: " + key); try { if (0 == string.Compare(key, CommandLineOptionKey)) { Logging.WriteLine("Restoring CommandLineEditor options"); CommandLineEditor.LoadOptions(stream); } else if (0 == string.Compare(key, BatchBuildSetsOptionKey)) { Logging.WriteLine("Restoring BatchBuilder options"); BatchBuilder.LoadOptions(stream); } } catch (Exception Ex) { // Couldn't load options Exception AppEx = new ApplicationException("OnLoadOptions() failed with key " + key, Ex); Logging.WriteLine(AppEx.ToString()); throw AppEx; } } /// /// Overrides Package.OnSaveOptions() /// Invoked by the Package class when there are options to be saved to the solution file. /// /// The name of the option key to save. /// The stream to save the option data to. protected override void OnSaveOptions(string key, Stream stream) { try { if (0 == string.Compare(key, CommandLineOptionKey)) { Logging.WriteLine("Saving CommandLineEditor options"); CommandLineEditor.SaveOptions(stream); } else if (0 == string.Compare(key, BatchBuildSetsOptionKey)) { Logging.WriteLine("Saving BatchBuilder options"); BatchBuilder.SaveOptions(stream); } } catch (Exception Ex) { // Couldn't save options Exception AppEx = new ApplicationException("OnSaveOptions() failed with key " + key, Ex); Logging.WriteLine(AppEx.ToString()); throw AppEx; } } /// /// IVsSolutionEvents implementation /// int IVsSolutionEvents.OnAfterCloseSolution(object pUnkReserved) { ThreadHelper.ThrowIfNotOnUIThread(); UpdateUnrealLoadedStatus(); OnSolutionClosed?.Invoke(); return VSConstants.S_OK; } int IVsSolutionEvents.OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy) { ThreadHelper.ThrowIfNotOnUIThread(); var LoadedProject = Utils.HierarchyObjectToProject(pRealHierarchy); if (LoadedProject != null && OnProjectLoaded != null) OnProjectLoaded.Invoke(LoadedProject); return VSConstants.S_OK; } int IVsSolutionEvents.OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded) { ThreadHelper.ThrowIfNotOnUIThread(); // This function is called after a Visual Studio project is opened (or a new project is created.) // Get the actual Project object from the IVsHierarchy object that was supplied var OpenedProject = Utils.HierarchyObjectToProject(pHierarchy); Utils.OnProjectListChanged(); if (OpenedProject != null && OnProjectOpened != null) { LoadedProjectPaths.Add(OpenedProject.FullName); OnProjectOpened(OpenedProject); } return VSConstants.S_OK; } int IVsSolutionEvents.OnAfterOpenSolution(object pUnkReserved, int fNewSolution) { ThreadHelper.ThrowIfNotOnUIThread(); UpdateUnrealLoadedStatus(); StartTicker(); OnSolutionOpened?.Invoke(); return VSConstants.S_OK; } int IVsSolutionEvents.OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved) { ThreadHelper.ThrowIfNotOnUIThread(); // This function is called after a Visual Studio project is closed // Get the actual Project object from the IVsHierarchy object that was supplied var ClosedProject = Utils.HierarchyObjectToProject(pHierarchy); if (ClosedProject != null && OnProjectClosed != null) { LoadedProjectPaths.Remove(ClosedProject.FullName); OnProjectClosed(ClosedProject); } return VSConstants.S_OK; } int IVsSolutionEvents.OnBeforeCloseSolution(object pUnkReserved) { StopTicker(); OnSolutionClosing?.Invoke(); return VSConstants.S_OK; } int IVsSolutionEvents.OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy) { ThreadHelper.ThrowIfNotOnUIThread(); var UnloadedProject = Utils.HierarchyObjectToProject(pRealHierarchy); if (UnloadedProject != null && OnProjectUnloading != null) OnProjectUnloading.Invoke(UnloadedProject); return VSConstants.S_OK; } int IVsSolutionEvents.OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel) { return VSConstants.S_OK; } int IVsSolutionEvents.OnQueryCloseSolution(object pUnkReserved, ref int pfCancel) { if (BatchBuilder.IsBusy) { pfCancel = 1; } return VSConstants.S_OK; } int IVsSolutionEvents.OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel) { return VSConstants.S_OK; } /// /// IVsSelectionEvents implementation /// int IVsSelectionEvents.OnCmdUIContextChanged(uint dwCmdUICookie, int fActive) { OnUIContextChanged?.Invoke(dwCmdUICookie, fActive != 0); return VSConstants.S_OK; } int IVsSelectionEvents.OnElementValueChanged(uint elementid, object varValueOld, object varValueNew) { ThreadHelper.ThrowIfNotOnUIThread(); // This function is called when selection changes in various Visual Studio tool windows // and sub-systems. // Handle startup project changes if (elementid == (uint)VSConstants.VSSELELEMID.SEID_StartupProject) { // If we are registered to a project hierarchy for events, unregister var OldStartupProjectHierarchy = (IVsHierarchy)varValueOld; if (OldStartupProjectHierarchy != null && ProjectHierarchyEventsHandle != 0) { OldStartupProjectHierarchy.UnadviseHierarchyEvents(ProjectHierarchyEventsHandle); ProjectHierarchyEventsHandle = 0; } Project NewStartupProject = null; // Incoming hierarchy object could be null (if no startup project is set yet, or during shutdown.) var NewStartupProjectHierarchy = (IVsHierarchy)varValueNew; if (NewStartupProjectHierarchy != null) { // Get the actual Project object from the IVsHierarchy object that was supplied NewStartupProject = Utils.HierarchyObjectToProject(NewStartupProjectHierarchy); if (NewStartupProject != null) { // Register for events from the project NewStartupProjectHierarchy.AdviseHierarchyEvents(this, out ProjectHierarchyEventsHandle); } } if (NewStartupProject != null) { OnStartupProjectChanged(NewStartupProject); } } return VSConstants.S_OK; } int IVsSelectionEvents.OnSelectionChanged(IVsHierarchy pHierOld, uint itemidOld, IVsMultiItemSelect pMISOld, ISelectionContainer pSCOld, IVsHierarchy pHierNew, uint itemidNew, IVsMultiItemSelect pMISNew, ISelectionContainer pSCNew) { return VSConstants.S_OK; } // IVsHierarchyEvents Interface Int32 IVsHierarchyEvents.OnItemAdded(UInt32 itemidParent, UInt32 itemidSiblingPrev, UInt32 itemidAdded) { return VSConstants.S_OK; } Int32 IVsHierarchyEvents.OnPropertyChanged(UInt32 itemid, Int32 propid, UInt32 flags) { OnStartupProjectPropertyChanged?.Invoke(itemid, propid, flags); return VSConstants.S_OK; } Int32 IVsHierarchyEvents.OnItemsAppended(UInt32 itemidParent) { return VSConstants.S_OK; } Int32 IVsHierarchyEvents.OnItemDeleted(UInt32 itemid) { return VSConstants.S_OK; } Int32 IVsHierarchyEvents.OnInvalidateItems(UInt32 itemidParent) { return VSConstants.S_OK; } Int32 IVsHierarchyEvents.OnInvalidateIcon(IntPtr hicon) { return VSConstants.S_OK; } #region IVsPersistSolutionProps int IVsPersistSolutionProps.SaveUserOptions(IVsSolutionPersistence pPersistence) { return VSConstants.S_OK; } int IVsPersistSolutionProps.LoadUserOptions(IVsSolutionPersistence pPersistence, uint grfLoadOpts) { return VSConstants.S_OK; } int IVsPersistSolutionProps.WriteUserOptions(IStream pOptionsStream, string pszKey) { return VSConstants.S_OK; } int IVsPersistSolutionProps.ReadUserOptions(IStream pOptionsStream, string pszKey) { return VSConstants.S_OK; } int IVsPersistSolutionProps.QuerySaveSolutionProps(IVsHierarchy pHierarchy, VSQUERYSAVESLNPROPS[] pqsspSave) { return VSConstants.S_OK; } int IVsPersistSolutionProps.SaveSolutionProps(IVsHierarchy pHierarchy, IVsSolutionPersistence pPersistence) { return VSConstants.S_OK; } int IVsPersistSolutionProps.WriteSolutionProps(IVsHierarchy pHierarchy, string pszKey, IPropertyBag pPropBag) { return VSConstants.S_OK; } int IVsPersistSolutionProps.ReadSolutionProps(IVsHierarchy pHierarchy, string pszProjectName, string pszProjectMk, string pszKey, int fPreLoad, IPropertyBag pPropBag) { ThreadHelper.ThrowIfNotOnUIThread(); if (!string.Equals(pszKey, GuidList.UnrealVSPackageString, StringComparison.InvariantCultureIgnoreCase)) return VSConstants.S_OK; pPropBag.Read("AvailablePlatforms", out object availablePlatformsObject, null, (uint)VarEnum.VT_BSTR, pPropBag); string[] availablePlatforms = null; if (availablePlatformsObject != null) { string availablePlatformsString = availablePlatformsObject as string; availablePlatforms = availablePlatformsString.Split(';'); } UnrealSolutionProperties = new UnrealSolutionProperties(availablePlatforms); // a UnrealVS section was found so we consider this solution a unreal solution IsUESolutionLoaded = true; return VSConstants.S_OK; } int IVsPersistSolutionProps.OnProjectLoadFailure(IVsHierarchy pStubHierarchy, string pszProjectName, string pszProjectMk, string pszKey) { return VSConstants.S_OK; } #endregion int IVsDebuggerEvents.OnModeChange(DBGMODE dbgmodeNew) { DbgMode = dbgmodeNew; bRefreshTitleInTick = true; return VSConstants.S_OK; } // IVsUpdateSolutionEvents Interface public int UpdateSolution_Begin(ref int pfCancelUpdate) { OnBuildBegin?.Invoke(out pfCancelUpdate); return VSConstants.S_OK; } public int UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand) { OnBuildDone?.Invoke(fSucceeded != 0, fModified != 0, fCancelCommand != 0); return VSConstants.S_OK; } public int UpdateSolution_StartUpdate(ref int pfCancelUpdate) { return VSConstants.S_OK; } public int UpdateSolution_Cancel() { return VSConstants.S_OK; } public int OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy) { ThreadHelper.ThrowIfNotOnUIThread(); // This function is called after a Visual Studio project has its active config changed // Check whether the project is the current startup project SolutionBuildManager.get_StartupProject(out IVsHierarchy StartupProjectHierarchy); if (StartupProjectHierarchy != null && StartupProjectHierarchy == pIVsHierarchy) { // Get the actual Project object from the IVsHierarchy object that was supplied var Project = Utils.HierarchyObjectToProject(pIVsHierarchy); if (Project != null && OnStartupProjectConfigChanged != null) { OnStartupProjectConfigChanged(Project); } } return VSConstants.S_OK; } private void UpdateUnrealLoadedStatus() { ThreadHelper.ThrowIfNotOnUIThread(); if (!DTE.Solution.IsOpen) { IsUESolutionLoaded = false; return; } SolutionManager.GetSolutionInfo(out string SolutionDirectory, out _SolutionFilepath, out string UserOptsFile); // if ReadProps found a valud UE solution we do not need to check these legacy definitions of a solution if (_IsUESolutionLoaded.HasValue) return; // Legacy paths for determing if a solution is a unreal solution by checking for tags & solution name. string[] SolutionLines = Array.Empty(); try { SolutionLines = File.ReadAllLines(_SolutionFilepath); } catch { } const string UBTTag = "# UnrealEngineGeneratedSolutionVersion="; var UBTLine = SolutionLines.FirstOrDefault(TextLine => TextLine.Trim().StartsWith(UBTTag)); if (UBTLine != null) { _UBTVersion = UBTLine.Trim().Substring(UBTTag.Length); IsUESolutionLoaded = true; } else { _UBTVersion = string.Empty; IsUESolutionLoaded = ( _SolutionFilepath != null && Path.GetFileName(_SolutionFilepath).StartsWith(UnrealSolutionFileNamePrefix, StringComparison.OrdinalIgnoreCase) ); } } /** Private Fields & Properties */ private static UnrealVSPackage PrivateInstance = null; /// Handle that we used at shutdown to unregister for selection manager events private UInt32 SelectionEventsHandle; /// Handle that we use at shutdown to unregister for events about solution activity private UInt32 SolutionEventsHandle; /// Handle that we use at shutdown to unregister for events about solution build activity private UInt32 UpdateSolutionEventsHandle; /// Handle that we use at shutdown to unregister for events about solution build activity private UInt32 DebuggerEventsHandle; /// Handle that we use to unregister for events about startup project hierarchy activity UInt32 ProjectHierarchyEventsHandle; /// Our command-line editing component private CommandLineEditor CommandLineEditor = new CommandLineEditor(); /// BuildStartupProject feature private BuildStartupProject BuildStartupProject; /// FileBrowser feature private FileBrowser FileBrowser; /// UbaVisualizer feature private UbaVisualizer UbaVisualizer; /// CompileSingleFile feature private CompileSingleFile CompileSingleFile; /// Project file generator button private GenerateProjectFiles GenerateProjectFiles; /// Batch Builder button/command handler private BatchBuilder BatchBuilder = new BatchBuilder(); /// Perforce features private P4Commands P4CommandsGroup; /// Ticker thread private Thread Ticker; /// Ticker thread cancel flag private int bCancelTicker = 0; private string _UBTVersion = string.Empty; private readonly List LoadedProjectPaths = new List(); private bool? _IsUESolutionLoaded; private DBGMODE DbgMode = DBGMODE.DBGMODE_Design; private bool bRefreshTitleInTick; /// Obtains the DTE2 interface for this instance of VS from the RunningObjectTable private static DTE2 GetDTE2ForCurrentInstance(DTE DTE) { ThreadHelper.ThrowIfNotOnUIThread(); // Find the ROT entry for visual studio running under current process. int HResult = NativeMethods.GetRunningObjectTable(0, out IRunningObjectTable Rot); if (HResult == 0) { Rot.EnumRunning(out IEnumMoniker EnumMoniker); EnumMoniker.Reset(); IMoniker[] Moniker = new IMoniker[1]; while (EnumMoniker.Next(1, Moniker, out _) == 0) { Rot.GetObject(Moniker[0], out object ComObject); if (ComObject is DTE2 CandidateDTE2) { if (CandidateDTE2.DTE == DTE) { return CandidateDTE2; } } } } return null; } } internal static class NativeMethods { /// ROT function in ole32.dll needed by GetDTE2ForCurrentInstance() [DllImport("ole32.dll")] internal static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable prot); public enum MapType : uint { MAPVK_VK_TO_VSC = 0x0, MAPVK_VSC_TO_VK = 0x1, MAPVK_VK_TO_CHAR = 0x2, MAPVK_VSC_TO_VK_EX = 0x3, } [DllImport("user32.dll")] public static extern int ToUnicode( uint wVirtKey, uint wScanCode, byte[] lpKeyState, [Out, MarshalAs(UnmanagedType.LPWStr, SizeParamIndex = 4)] StringBuilder pwszBuff, int cchBuff, uint wFlags); [DllImport("user32.dll")] public static extern bool GetKeyboardState(byte[] lpKeyState); [DllImport("user32.dll")] public static extern uint MapVirtualKey(uint uCode, MapType uMapType); [DllImport("user32.dll", EntryPoint = "CreateWindowEx", CharSet = CharSet.Unicode)] internal static extern IntPtr CreateWindowEx(int dwExStyle, string lpszClassName, string lpszWindowName, int style, int x, int y, int width, int height, IntPtr hwndParent, IntPtr hMenu, IntPtr hInst, [MarshalAs(UnmanagedType.AsAny)] object pvParam); [DllImport("user32.dll", EntryPoint = "DestroyWindow", CharSet = CharSet.Unicode)] internal static extern bool DestroyWindow(IntPtr hwnd); [DllImport("user32.dll", EntryPoint = "SetWindowTextW", CharSet = CharSet.Unicode)] internal static extern bool SetWindowTextW(IntPtr hWnd, string lpString); [DllImport("user32.dll", EntryPoint = "SetWindowPos", CharSet = CharSet.Unicode)] internal static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport("user32.dll", EntryPoint = "UpdateWindow", CharSet = CharSet.Unicode)] internal static extern bool UpdateWindow(IntPtr hWnd); [DllImport("user32.dll", EntryPoint = "IsWindow", CharSet = CharSet.Unicode)] internal static extern bool IsWindow(IntPtr hWnd); [DllImport("user32.dll", EntryPoint = "PostMessageW", CharSet = CharSet.Unicode)] internal static extern bool PostMessageW(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Unicode)] internal static extern IntPtr SetFocus(IntPtr hWnd); [DllImport("user32.dll")] internal static extern IntPtr LoadCursorW(IntPtr hInstance, IntPtr lpCursorName); [DllImport("user32.dll", EntryPoint = "RegisterClassExW", SetLastError = true)] internal static extern UInt16 RegisterClassExW(ref WNDCLASSEX lpWndClass); [DllImport("user32.dll")] internal static extern IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] internal struct WNDCLASSEX { [MarshalAs(UnmanagedType.U4)] public int cbSize; [MarshalAs(UnmanagedType.U4)] public int style; public IntPtr lpfnWndProc; // not WndProc public int cbClsExtra; public int cbWndExtra; public IntPtr hInstance; public IntPtr hIcon; public IntPtr hCursor; public IntPtr hbrBackground; public string lpszMenuName; public string lpszClassName; public IntPtr hIconSm; } } }