// 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;
}
}
}