I recently presented Frode Tørresdal's unofficial custom ribbon button implementation showing how you can add a custom button that Revit knows nothing about to the ribbon and hook it up with the Idling event to perform some action requiring access to the Revit API.
It would be much simpler, safer and more effective if we could implement a normal official Revit external command instead and create a button anywhere we want in the Revit ribbon to drive that.
Well, happily, we can, almost, as Scott Wilson points out in his comment on that post.
The Living
Before getting to that, though, let me mention something of interest to architects, techies and all AEC visionaries alike, the acquisition of the very forward-thinking architectural design studio The Living, headed by David Benjamin. It is the latest addition to the Autodesk research network, the first of its kind Autodesk Studio, in a move which Benjamin says 'will enable The Living to do more of what we are already doing and super-charge it.'
Here is the official announcement and articles about it by Architect, ArchDaily and Architectural Record.
Scott
I thought I'd let you know of something I found a little while ago while participating in the Revit API forum thread on adding a button to an existing ribbon panel.
Basically, there is a way of adding your own custom panels and buttons to the system tabs and have them behave as standard command buttons with full API access, no need for Idling callbacks and other stuff.
Here is my brief explanation from the thread: "If you add your command button using standard API methods to its own ribbon panel on either your own tab or the built in add-ins tab, you can then find the panel using the above methods and insert it into any of the system tabs using the Autodesk.Windows.RibbonTab.Panels.Add method. The button will then behave in the same way as it would on the original tab and the API doesn't seem to notice – to the extent of my brief testing, anyway. You can then remove the panel from the add-ins tab if desired and the panel will remain happily in its new home."
I dug up my test code, cleaned it up into a little demo project and posted it to Dropbox.com in RibbonMoveExample.zip.
Please see the comments included in the source code below for the rest of Scott's detailed explanation.
Jeremy
Jeremy adds: wow, very cool!
What a beautiful workaround, working around the official Revit API limitations and yet retaining all the official functionality goodness – much better than the completely unofficial hack using the .NET UI Automation library.
I compiled and tested this.
It creates a new panel containing a new button and moves them to the Revit ribbon Manage tab. Here is the result:
It supports all the standard external command functionality, e.g. an external command availability class.
External Command for Testing
The external command used for testing is defined as follows:
using System; using Autodesk.Revit.Attributes; using Autodesk.Revit.DB; using Autodesk.Revit.UI; namespace RibbonMoveExample { public class ExampleCommand_Availability : IExternalCommandAvailability { public bool IsCommandAvailable( UIApplication uiApp, CategorySet categorySet ) { Autodesk.Revit.UI.UIDocument uiDoc = uiApp.ActiveUIDocument; if( uiDoc != null ) { return true; } else { return false; } } } [Transaction( TransactionMode.Manual )] public class ExampleCommand : IExternalCommand { public Autodesk.Revit.UI.Result Execute( ExternalCommandData commandData, ref String message, ElementSet elements ) { try { Document dbDoc = commandData.Application .ActiveUIDocument.Document; TaskDialog.Show( "ExampleCommand", "Current document title: " + dbDoc.ProjectInformation.Name + "\nLocation: " + dbDoc.PathName ); } catch( Exception e ) { message = e.Message; return Result.Failed; } return Result.Succeeded; } } }
External Application Implementation
As said, the external application sports Scott's interesting explanations, including a detailed dissection of the ribbon panel and ribbon item Id property values required for identification:
using System; using System.Collections.Generic; using System.Reflection; using System.Windows.Forms; using Autodesk.Revit.ApplicationServices; using Autodesk.Revit.UI; using adWin = Autodesk.Windows; namespace RibbonMoveExample { /// <summary> /// Demonstrates the use of Autodesk.Windows.RibbonControl /// to move API created ribbon buttons and / or panels from /// their original location to any other tab / panel including /// system created ones. /// I'm pretty sure that this technique would also work in /// reverse to allow grabbing system panels / buttons and adding /// them to API created Tabs and even other system tabs, meaning /// that some drastic customisation of Revit's ribbon menu is /// possible. /// Revit CUI addin anybody? :) /// /// Created by Scott Wilson, released into the public domain as /// demonstration of concept only. Use at own risk. /// </summary> public class RibbonMoveExampleApp : IExternalApplication { // System tab and panel ids to use as targets for the // demo to find other valid ids for system tabs and // panels, simply dump out the id of each item visited // in the foreach loops below String SystemTabId = "Manage"; String SystemPanelId = "settings_shr"; // Example names of API created tabs, panels and buttons // To try this demo on ribbon items already created by // another loaded addin, enter the appropriate details // here and comment out everything in the OnStartup method // except for the ApplicationInitialized registration String ApiTabName = "New Tab"; String ApiPanelName = "New Panel"; String ApiButtonName = "NewButton"; String ApiButtonText = "New\nButton"; public Result OnStartup( UIControlledApplication uiConApp ) { // Add our own tab, panel and command button // to the ribbon using the API uiConApp.CreateRibbonTab( ApiTabName ); RibbonPanel apiRibbonPanel = uiConApp.CreateRibbonPanel( ApiTabName, ApiPanelName ); PushButtonData buttonDataTest = new PushButtonData( ApiButtonName, ApiButtonText, Assembly.GetExecutingAssembly().Location, "RibbonMoveExample.ExampleCommand" ); buttonDataTest.AvailabilityClassName = "RibbonMoveExample.ExampleCommand_Availability"; apiRibbonPanel.AddItem( buttonDataTest ); // Subscribe to the "ApplicationInitialized" event // then continue from there once it is fired. // This is to ensure that the ribbon is fully // populated before we mess with it. uiConApp.ControlledApplication.ApplicationInitialized += OnApplicationInitialized; return Result.Succeeded; } void OnApplicationInitialized( object sender, Autodesk.Revit.DB.Events.ApplicationInitializedEventArgs e ) { // Find a system ribbon tab and panel to house // our API items // Also find our API tab, panel and button within // the Autodesk.Windows.RibbonControl context adWin.RibbonControl adWinRibbon = adWin.ComponentManager.Ribbon; adWin.RibbonTab adWinSysTab = null; adWin.RibbonPanel adWinSysPanel = null; adWin.RibbonTab adWinApiTab = null; adWin.RibbonPanel adWinApiPanel = null; adWin.RibbonItem adWinApiItem = null; foreach( adWin.RibbonTab ribbonTab in adWinRibbon.Tabs ) { // Look for the specified system tab if( ribbonTab.Id == SystemTabId ) { adWinSysTab = ribbonTab; foreach( adWin.RibbonPanel ribbonPanel in ribbonTab.Panels ) { // Look for the specified panel // within the system tab if( ribbonPanel.Source.Id == SystemPanelId ) { adWinSysPanel = ribbonPanel; } } } else { // Look for our API tab if( ribbonTab.Id == ApiTabName ) { adWinApiTab = ribbonTab; foreach( adWin.RibbonPanel ribbonPanel in ribbonTab.Panels ) { // Look for our API panel. // The Source.Id property of an API created // ribbon panel has the following format: // CustomCtrl_%[TabName]%[PanelName] // Where PanelName correlates with the string // entered as the name of the panel at creation // The Source.AutomationName property can also // be used as it is also a direct correlation // of the panel name, but without all the cruft // Be sure to include any new line characters // (\n) used for the panel name at creation as // they still form part of the Id & AutomationName //if(ribbonPanel.Source.AutomationName // == ApiPanelName) // Alternative method if( ribbonPanel.Source.Id == "CustomCtrl_%" + ApiTabName + "%" + ApiPanelName ) { adWinApiPanel = ribbonPanel; foreach( adWin.RibbonItem ribbonItem in ribbonPanel.Source.Items ) { // Look for our command button // The Id property of an API created ribbon // item has the following format: // CustomCtrl_%CustomCtrl_%[TabName]%[PanelName]%[ItemName] // Where ItemName correlates with the string // entered as the first parameter (name) // of the PushButtonData() constructor // While AutomationName correlates with // the string entered as the second // parameter (text) of the PushButtonData() // constructor // Be sure to include any new line // characters (\n) used for the button // name and text at creation as they // still form part of the ItemName // & AutomationName //if(ribbonItem.AutomationName // == ApiButtonText) // alternative method if( ribbonItem.Id == "CustomCtrl_%CustomCtrl_%" + ApiTabName + "%" + ApiPanelName + "%" + ApiButtonName ) { adWinApiItem = ribbonItem; } } } } } } } // Make sure we got everything we need if( adWinSysTab != null && adWinSysPanel != null && adWinApiTab != null && adWinApiPanel != null && adWinApiItem != null ) { // First we'll add the whole panel including // the button to the system tab adWinSysTab.Panels.Add( adWinApiPanel ); // now lets also add the button itself // to a system panel adWinSysPanel.Source.Items.Add( adWinApiItem ); // Remove panel from original API tab // It can also be left there if needed, // there doesn't seem to be any problems with // duplicate panels / buttons on seperate tabs // / panels respectively adWinApiTab.Panels.Remove( adWinApiPanel ); // Remove our original API tab from the ribbon adWinRibbon.Tabs.Remove( adWinApiTab ); } // A new panel should now be added to the // specified system tab. Its command buttons // will behave as they normally would, including // API access and ExternalCommandAvailability tests. // There will also be a second copy of the command // button from the panel added to the specified // system panel. } public Result OnShutdown( UIControlledApplication a ) { return Result.Succeeded; } } }
Many thanks to Scott for this very creative workaround!
Use at Your Own Risk
By the way, please note our standard disclaimer.
The Revit API development team have good reasons for limiting the places that an add-in can place its command buttons.
You work around those limitations at your own risk.
This functionality may very well be disabled at any time with no warning.
Wow this is interesting. Over the years I have had many requests by people if we could disable the explode command. Now I'm assuming we could remove that button altogether from the ribbon? I wonder how this works on contextual panels???
Posted by: Phillip Miller | July 03, 2014 at 06:22
Dear Phillip,
Power to the people, or at least to you.
Cheers, Jeremy.
Posted by: Jeremy Tammik | July 03, 2014 at 06:35
Yes you can indeed remove the explode buttons (I tried it out) and they stay removed for the remainder of the Revit session. The trick is to catch them while they are showing, I did it by selecting an imported cad file to bring up the context panel and then ran an external command that searches for the buttons and removes them, it's a manual process though.
For automation I think it would be possible to place the seek and remove code into an IExternalCommandAvailability class associated with an API button that has been added to the modify tab. It would then get triggered automatically along with the display of the contextual panel. The button could then even remove itself once it has done its job (the perfect crime).
There's probably a race condition though, as the explode buttons might not always be added before the availability checker is triggered, but its worth a try.
Posted by: Scott Wilson | July 03, 2014 at 11:36
Dear Jeremy
I have a question in another issue and i need your advise :)
How to tag element in a linked project ?
I use the this method to tag a linked pipe element but it throw an error please advise thanks :)
LocationCurve loc = pipe.Location as LocationCurve;
XYZ startp = loc.Curve.GetEndPoint(0);
XYZ endp = loc.Curve.GetEndPoint(1);
XYZ midp = loc.Curve.Evaluate(0.5, true);
newtag = DOC.Create.NewTag(view, pipe, false, tagmode, tagorian, midp);
Posted by: Gamal Kheder | July 11, 2014 at 05:58
I've got some code working based on this article. The code loops through an xml file and sets ribbonItem.IsVisible per button in my addin. This works for all of my buttons except for a PullDownButton. The buttons that are added to the PullDownButton aren't included as items in RibbonPanel.Source.Items. Any idea how to get the buttons in the PullDownButton that is found in there?
Thanks
Posted by: TroyGates | October 21, 2014 at 19:56
@ TroyGates. If the button is PullDownButton you should be able to cast it to a PullDownButton. Then use the GetItems() method to get an IList that you can then iterate through to set the visibility. I've implemented this in an addin before.
Posted by: Michael | November 07, 2014 at 08:32