// Copyright 1998-2019 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.Design; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace UnrealVS { /// /// Ole Command class for handling the quick build menu items (individual configs inside platform menus) /// One of these handles all the items in a platform menu. /// class QuickBuildMenuCommand : OleMenuCommand { /// The function used to select the range of commands handled by this object private Predicate OnMatchItem; /// /// Create a QuickBuildMenuCommand with a delegate for determining which command numbers it handles /// The other params are used to construct the base OleMenuCommand /// public QuickBuildMenuCommand(CommandID InDynamicStartCommandId, Predicate InOnMatchItem, EventHandler InOnInvokedDynamicItem, EventHandler InOnBeforeQueryStatusDynamicItem) : base(InOnInvokedDynamicItem, null /*changeHandler*/, InOnBeforeQueryStatusDynamicItem, InDynamicStartCommandId) { if (InOnMatchItem == null) { throw new ArgumentNullException("InOnMatchItem"); } OnMatchItem = InOnMatchItem; } /// /// Allows a dynamic item command to match the subsequent items in its list. /// Overridden from OleMenuCommand. /// Calls OnMatchItem to choose with command numbers are valid. /// public override bool DynamicItemMatch(int cmdId) { // Call the supplied predicate to test whether the given cmdId is a match. // If it is, store the command id in MatchedCommandid // for use by any BeforeQueryStatus handlers, and then return that it is a match. // Otherwise clear any previously stored matched cmdId and return that it is not a match. if (OnMatchItem(cmdId)) { MatchedCommandId = cmdId; return true; } MatchedCommandId = 0; return false; } } /// /// Class represents a single platform, quick build menu. Wraps QuickBuildMenuCommand. /// class PlaformMenuContents { /// The command id of the placeholder command that sets the start of the command range for this menu private readonly int DynamicStartCommandId; /// The name of the platform. Matches the platform string in the solution configs exactly. private readonly string PlaformName; /// /// Construct a platform menu for a given, named platform, starting at the given command id /// /// The name of the platform /// The start of the command range for this menu public PlaformMenuContents(string Name, int InDynamicStartCommandId) { DynamicStartCommandId = InDynamicStartCommandId; PlaformName = Name; // Create the dynamic menu var MenuOleCommand = new QuickBuildMenuCommand( new CommandID(GuidList.UnrealVSCmdSet, DynamicStartCommandId), IsValidDynamicItem, OnInvokedDynamicItem, OnBeforeQueryStatusDynamicItem ); UnrealVSPackage.Instance.MenuCommandService.AddCommand(MenuOleCommand); } /// /// The callback function passed to this object's QuickBuildMenuCommand, used to select the range of commands handled by this object /// It uses the cached list of solution configs in QuickBuild to determine how many commands should be in the menu. /// /// Command id the validate /// True, if the command is valid for this menu private bool IsValidDynamicItem(int cmdId) { int ConfigCount = QuickBuild.CachedSolutionConfigNames.Length; // The match is valid if the command ID is >= the id of our root dynamic start item // and the command ID minus the ID of our root dynamic start item // is less than or equal to the number of projects in the solution. return (cmdId >= DynamicStartCommandId) && ((cmdId - DynamicStartCommandId) < ConfigCount); } /// /// The invoke handler passed to this object's QuickBuildMenuCommand, called by the base OleMenuCommand when an item is clicked /// private void OnInvokedDynamicItem(object sender, EventArgs args) { var MenuCommand = (QuickBuildMenuCommand)sender; // Get the project clicked in the solution explorer by accessing the current selection and converting to a Project if possible. IntPtr HierarchyPtr, SelectionContainerPtr; uint ProjectItemId; IVsMultiItemSelect MultiItemSelect; UnrealVSPackage.Instance.SelectionManager.GetCurrentSelection(out HierarchyPtr, out ProjectItemId, out MultiItemSelect, out SelectionContainerPtr); if (HierarchyPtr == null) return; IVsHierarchy SelectedHierarchy = Marshal.GetTypedObjectForIUnknown(HierarchyPtr, typeof(IVsHierarchy)) as IVsHierarchy; if (SelectedHierarchy == null) return; var SelectedProject = Utils.HierarchyObjectToProject(SelectedHierarchy); if (SelectedProject == null) return; // Builds the selected project with the clicked platform and config Utils.ExecuteProjectBuild(SelectedProject, MenuCommand.Text, PlaformName, BatchBuilderToolControl.BuildJob.BuildJobType.Build, null, null); } /// /// Before-query handler passed to this object's QuickBuildMenuCommand, called by the base OleMenuCommand to update the state of an item. /// private void OnBeforeQueryStatusDynamicItem(object sender, EventArgs args) { var MenuCommand = (QuickBuildMenuCommand)sender; MenuCommand.Enabled = QuickBuild.IsActive; MenuCommand.Visible = true; // Determine the index of the item in the menu bool isRootItem = MenuCommand.MatchedCommandId == 0; int CommandId = isRootItem ? DynamicStartCommandId : MenuCommand.MatchedCommandId; int DynCommandIdx = CommandId - DynamicStartCommandId; // Set the text label based on the index and the solution configs array cached in QuickBuild if (DynCommandIdx < QuickBuild.CachedSolutionConfigNames.Length) { MenuCommand.Text = QuickBuild.CachedSolutionConfigNames[DynCommandIdx]; } // Clear the id now that the query is done MenuCommand.MatchedCommandId = 0; } } /// /// The Quick Build menu system /// public class QuickBuild { /** classes */ /// All the basic data needed to build each platform-specific submenu struct SubMenu { public string Name { get; set; } public int SubMenuId { get; set; } public int DynamicStartCommandId { get; set; } } /** constants */ private const int ProjectQuickBuildMenuID = 0x1410; // must match the values in the vsct file /** fields */ /// Solution configs and platforms cached once before each time the Quick Build menu opens private static string[] SolutionConfigNames = new string[0]; private static string[] SolutionConfigPlatforms = new string[0]; /// Hide/shows the while Quick Build menu tree private static bool bIsActive = false; /// List of submenus and their details - must match the values in the vsct file private readonly SubMenu[] SubMenus = new[] { new SubMenu {Name = "Win64", SubMenuId = 0x1500, DynamicStartCommandId = 0x1530}, new SubMenu {Name = "Win32", SubMenuId = 0x1600, DynamicStartCommandId = 0x1630}, new SubMenu {Name = "Mac", SubMenuId = 0x1700, DynamicStartCommandId = 0x1730}, new SubMenu {Name = "XboxOne", SubMenuId = 0x1800, DynamicStartCommandId = 0x1830}, new SubMenu {Name = "PS4", SubMenuId = 0x1900, DynamicStartCommandId = 0x1930}, new SubMenu {Name = "IOS", SubMenuId = 0x1A00, DynamicStartCommandId = 0x1A30}, new SubMenu {Name = "Android", SubMenuId = 0x1B00, DynamicStartCommandId = 0x1B30}, new SubMenu {Name = "WinRT", SubMenuId = 0x1C00, DynamicStartCommandId = 0x1C30}, new SubMenu {Name = "WinRT_ARM", SubMenuId = 0x1D00, DynamicStartCommandId = 0x1D30}, new SubMenu {Name = "HTML5", SubMenuId = 0x1E00, DynamicStartCommandId = 0x1E30}, new SubMenu {Name = "Linux", SubMenuId = 0x1F00, DynamicStartCommandId = 0x1F30}, new SubMenu {Name = "Switch", SubMenuId = 0x2000, DynamicStartCommandId = 0x2030}, }; /// The main root command of the Quick Build menu hierarchy - used to hide it when not active private readonly OleMenuCommand QuickBuildCommand; /// /// These represent the items that can be added to the menu that lists the platforms. /// Each one is a submenu containing items for each config. /// private readonly Dictionary AllPlaformMenus = new Dictionary(); /// /// These represent the items shown in the menu that lists the platforms. /// It is a subset of AllPlaformMenus with only the loaded platforms. /// Each one is a submenu containing items for each config. /// private readonly Dictionary ActivePlaformMenus = new Dictionary(); /// /// These represent the items in plaform-specific menus. /// private readonly Dictionary PlaformMenusContents = new Dictionary(); /// VSConstants.UICONTEXT_SolutionBuilding translated into a cookie used to access UI ctxt state private readonly uint SolutionBuildingUIContextCookie; /** properties */ public static bool IsActive { get { return bIsActive; } } public static string[] CachedSolutionConfigNames { get { return SolutionConfigNames; } } public static string[] CachedSolutionConfigPlatforms { get { return SolutionConfigPlatforms; } } /** methods */ public QuickBuild() { // root menu QuickBuildCommand = new OleMenuCommand(null, null, OnQuickBuildQuery, new CommandID(GuidList.UnrealVSCmdSet, ProjectQuickBuildMenuID)); QuickBuildCommand.BeforeQueryStatus += OnQuickBuildQuery; UnrealVSPackage.Instance.MenuCommandService.AddCommand(QuickBuildCommand); // platform sub-menus foreach (var SubMenu in SubMenus) { var SubMenuCommand = new OleMenuCommand(null, new CommandID(GuidList.UnrealVSCmdSet, SubMenu.SubMenuId)); SubMenuCommand.BeforeQueryStatus += OnQuickBuildSubMenuQuery; UnrealVSPackage.Instance.MenuCommandService.AddCommand(SubMenuCommand); AllPlaformMenus.Add(SubMenu.Name, SubMenuCommand); PlaformMenusContents.Add(SubMenu.Name, new PlaformMenuContents(SubMenu.Name, SubMenu.DynamicStartCommandId)); } // cache the cookie for UICONTEXT_SolutionBuilding UnrealVSPackage.Instance.SelectionManager.GetCmdUIContextCookie(VSConstants.UICONTEXT_SolutionBuilding, out SolutionBuildingUIContextCookie); // Initialize the active state based on whether the IDE is building anything UpdateActiveState(); UnrealVSPackage.Instance.OnUIContextChanged += OnUIContextChanged; UnrealVSPackage.Instance.OnSolutionOpened += OnSolutionChanged; } /// /// Updates the bIsActive flag using the UICONTEXT_SolutionBuilding state. Hides the Quick Build feature when the solution is building. /// private void UpdateActiveState() { int bIsBuilding; UnrealVSPackage.Instance.SelectionManager.IsCmdUIContextActive(SolutionBuildingUIContextCookie, out bIsBuilding); bIsActive = bIsBuilding == 0; } /// /// Before-query handler passed to the root menu item's OleMenuCommand and called to update the state of the item. /// private void OnQuickBuildQuery(object sender, EventArgs e) { // Always cache the list of solution build configs when the project menu is opening CacheBuildConfigs(); } /// /// Before-query handler passed to the sub-menu item's OleMenuCommand and called to update the state of the item. /// private void OnQuickBuildSubMenuQuery(object sender, EventArgs e) { var SubMenuCommand = (OleMenuCommand)sender; SubMenuCommand.Visible = ActivePlaformMenus.ContainsValue(SubMenuCommand); SubMenuCommand.Enabled = bIsActive; } /// /// Called when a solution loads. Caches the solution build configs and sets the platform menus' visibility. /// Only platforms found in the laoded solution's list are shown. /// private void OnSolutionChanged() { CacheBuildConfigs(); ActivePlaformMenus.Clear(); foreach (var SubMenu in SubMenus) { if (SolutionConfigPlatforms.Any(Platform => string.Compare(Platform, SubMenu.Name, StringComparison.InvariantCultureIgnoreCase) == 0)) { ActivePlaformMenus.Add(SubMenu.Name, AllPlaformMenus[SubMenu.Name]); } } } /// /// Caches the solution build configs /// private void CacheBuildConfigs() { SolutionConfigurations SolutionConfigs = UnrealVSPackage.Instance.DTE.Solution.SolutionBuild.SolutionConfigurations; SolutionConfigPlatforms = (from SolutionConfiguration2 Sc in SolutionConfigs select Sc.PlatformName).Distinct().ToArray(); SolutionConfigNames = (from SolutionConfiguration2 Sc in SolutionConfigs select Sc.Name).Distinct().ToArray(); } /// /// Called when the UI Context changes. If the building state changed, update the active state. /// private void OnUIContextChanged(uint CmdUICookie, bool bActive) { if (SolutionBuildingUIContextCookie == CmdUICookie) { UpdateActiveState(); } } } }