UI Customization
See Also: Class ActionTable, Class ActionCallback, Class ActionContext, Class IActionManager, Class DynamicMenu, Structure ActionDescription, Class DynamicMenuCallback, Class ClassDesc, Class Interface.
Overview
This section describes the various classes and functions used to customize the user interface in 3ds max R4. This includes the ability to customize the 3ds max main pulldown menus, the toolbars, quad menus and keyboard shortcuts.
Discussed below is the Action System. This is a new system used for customizing the user interface. Plug-ins may use this to customize the 3ds max main menu, toolbars, quad menus and keyboard shortuts. This system supercedes the system used by previous version of 3ds max for keyboard accelerators.
The system relies on plug-ins creating Action Tables. These are tables of UI related operations. These operations are called Action Items. When a UI operation is performed the code that carries out that operation is done by code in an Action Callback object.
Overview of the Principal Classes of the Action System
The class ActionTable is a generalization of the ShortcutTable class from R3. An ActionTable holds a set of ActionItems, which are operations that can be tied to various UI elements, such as keyboard shortcuts, Custom User Interface buttons, the 3ds max main menu and the Quad menu. MAX’s core code exports several ActionTables for built-in operations in MAX. Plug-ins can also export their own action tables via methods available in Class ClassDesc.
The class ActionItem is used to represent a single operation that lives in an ActionTable. ActionItem is an abstract class with operations to support various UI operations. The system provides a default implementation of this class that works when the table is build with the ActionTable::BuildActionTable() method. However, developers may want to specialize this class for more specific-purpose applications. For example, MAXScipt does this to export macroScripts to an ActionTable.
This structure is used when creating Action Tables. An array of these structures is created with one element for each action in the table to be built. This array of structures is then passed as a parameter to the ActionTable constructor.
This is the callback class that actually execute the action when a user requests it. The ExecuteAction() method of this class is called and performs the work. Developers create a sub-class of this class and implement ExecuteAction(). This usually consists of a case statement which switches on the ID of the command being executed.
This class functions like an identifier for a group of actions. Several Action Tables can share a single ActionContext. The class maintains an ID, a name, and an active state for the context and has methods to retieve these. When a Action Context is active all the Action Tables that use that context are active as well.
This class provides methods to work with the available Action Tables. There are methods to get the number of and a pointer to specific action tables, activate and deactivate action tables, work with action contexts and determine if they are active. A pointer which may be used to call the methods of this class is returned from Interface::GetActionManager().
This is a helper class used in putting up additional quad or toolbar menus 'on the fly'. This class has a few methods related to adding new menu items, beginning and ending sub-menus, checking menu flags, and getting a pointer to the menu after it has been created. The dynamic menu created by this class is provided to the method ActionItem::GetDynamicMenu().
This is the callback used when creating dynamic menus. An instance of this class is passed to the constructor of the helper class DynamicMenu. This class has a single method called MenuItemSelected() which is called when the user has choosen an item from the menu.
There are two new method of this class related to ActionTables. When a plug-in wants to make its keyboard accelerators available it uses the following new methods:
virtual int NumActionTables();
virtual ActionTable* GetActionTable(int i);
There is a new method to access the Action Manager interface. This is:
virtual IActionManager* GetActionManager() = 0;
See Class IActionManager for details.
Building Action Tables
This section discusses the approach developers may use to build action tables and make them available to the system. In most cases building an ActionTable is fairly easy. It is a bit more work if you choose to implement your own custom sub-class of ActionItem, but in most cases that isn’t needed (see next section).
The system provides a helper class called ActionDescription that helps in building tables.
struct ActionDescription {
// A unique identifier for the command (must be uniqe per table)
int mCmdID;
// A string resource id that describes the command
int mDescriptionResourceID;
// A string resource ID for a short name for the action
int mShortNameResourceID;
// A string resource for the category of an operation
int mCategoryResourceID;
};
An ActionTable is built by making a static table of action descriptions and passing it to the constructor for ActionTable. For example, here is the code that builds the action table for the FFD modifier:
#define NumElements(array) (sizeof(array) / sizeof(array[0]))
static ActionDescription spActions[] = {
ID_SUBOBJ_TOP,
IDS_SWITCH_TOP,
IDS_SWITCH_TOP,
IDS_RB_FFDGEN,
ID_SUBOBJ_CP,
IDS_SWITCH_CP,
IDS_SWITCH_CP,
IDS_RB_FFDGEN,
ID_SUBOBJ_LATTICE,
IDS_SWITCH_LATTICE,
IDS_SWITCH_LATTICE,
IDS_RB_FFDGEN,
ID_SUBOBJ_SETVOLUME,
IDS_SWITCH_SETVOLUME,
IDS_SWITCH_SETVOLUME,
IDS_RB_FFDGEN,
};
ActionTable* BuildActionTable()
{
TSTR name = GetString(IDS_RB_FFDGEN);
HACCEL hAccel = LoadAccelerators(hInstance,
MAKEINTRESOURCE(IDR_FFD_SHORTCUTS));
int numOps = NumElements(spActions);
ActionTable* pTab;
pTab = new ActionTable(kFFDActions, kFFDContext, name, hAccel,
numOps, spActions, hInstance);
GetCOREInterface()->GetActionManager()->RegisterActionContext (kFFDContext, name.data());
return pTab;
}
The constructor for ActionTable takes the ID of the table, the context id, a name for the table, a windows accelerator table that gives default keyboard assignments for the operations, the number of items, the table of operation descriptions, and the instance of the module where the string resources in the table are stored.
At the same time the table is built, you also need to register the action context ID with the system. This is done with the IActionManager::RegisterActionContext() method.
The other part of implementing an ActionTable is implementing an ActionCallback class. This is an abstract class with a virtual method called ExecuteAction(int id). You need to sub-class this and pass an instance of it to the system when you activate the ActionTable. Then when the system wants to execute an action, you will get a callback to ActionCallback::ExecuteAction().
For the FFD modifier, this looks like:
template <class T>
class FFDActionCB : public ActionCallback
{
public:
T* ffd;
FFDActionCB(T *ffd) { this->ffd = ffd; }
BOOL ExecuteAction(int id);
};
template <class T>
BOOL FFDActionCB<T>::ExecuteAction(int id) {
switch (id) {
case ID_SUBOBJ_TOP:
ffd->ip->SetSubObjectLevel(SEL_OBJECT);
ffd->ip->RedrawViews(ffd->ip->GetTime());
return TRUE;
case ID_SUBOBJ_CP:
ffd->ip->SetSubObjectLevel(SEL_POINTS);
return TRUE;
case ID_SUBOBJ_LATTICE:
ffd->ip->SetSubObjectLevel(SEL_LATTICE);
return TRUE;
case ID_SUBOBJ_SETVOLUME:
ffd->ip->SetSubObjectLevel(SEL_SETVOLUME);
return TRUE;
}
return FALSE;
}
FFD uses a template class to implement several versions of this callback, but this is not required.
Finally, the system needs to activate and deactivate the table at the appropriate time. When to do this depends on the scope of applicability of the table. If your ActionTable is exported from an editable object of modifier plug-in, then you typically want it only to be active when editing the object or modifier. This is done by activating it in the BeginEditParams() method, and deactivating it in EndEditParams().
For FFD, this looks like this:
ffdActionCB = new FFDActionCB<FFDMod >(this);
ip->GetActionManager()->ActivateActionTable(ffdActionCB, kFFDActions);
The first parameter is the ID of the table to activate, and the second is an instance of the ActionCallback class that is responsible for executing actions. In EndEditParams(), we deactivate the table:
ip->GetActionManager()->DeactivateActionTable(ffdActionCB, kFFDActions);
delete ffdActionCB;
For other types of plug-ins the table can be activated at different times. For example, you could write a GUP plug-in that activates the table when the plug-in is loaded to provide actions that are always available.
To Sub-Class or not to Sub-Class?
Developers have the option of sub-classing both the ActionTable class and the ActionItem class, but are not required to do either. Only the ActionCallback is required to be sub-classed in all cases.
For the ActionItem class, developers only need to sub-class in rare cases. The default implementation of this class stores internal strings for its name, description, category, icon, etc. In some cases this might lead to duplicate storage if that information is stored elsewhere. In that case, you might want to provided a specialization of this class for memory efficiency.
The ActionTable class also has several virtual methods that the developer might want to implement. All these methods have default implementations, so sub-classing is not required unless special behavior is needed.
The default handlers for some of the handlers may not be appropriate. For example IsEnabled(int cmdId) returns TRUE in every case in the default implementation. If you want command to be disabled under some conditions, then you will need to build a specialization of ActionTable and implement this method. Other methods that you might want to implement are:
virtual BOOL GetButtonText(int cmdId, TSTR& buttonText);
virtual BOOL GetMenuText(int cmdId, TSTR& menuText)
virtual BOOL GetDescriptionText(int cmdId, TSTR& descText);
virtual BOOL IsChecked(int cmdId);
virtual BOOL IsItemVisible(int cmdId);
virtual BOOL IsEnabled(int cmdId);
virtual void WritePersistentActionId(int cmdId, TSTR& idString);
virtual int ReadPersistentActionId(TSTR& idString);
You only need to implement the last two methods if your table uses command identifiers that are not persistent from session to session. An example of this is the ActionTable that exports macroScripts from MAXScript. This system reads all the macroScripts in your system and assigns command IDs that are based on the order the macroScripts are read from the file. This might change from session to session, so when we want a persistent ID to write out to our keyboard or CUI files, we need one that doesn’t change. For macroScripts, it writes the name of the script concatenated with the category of the script. The ReadPersistentID method takes that string and returns its integer command ID for the current session.
For tables that use constant integer identifiers that don’ t change from session to session, like the FDD example above, there is no need to implement this method.
Registering Action Tables
In order for the system to use an action table, it need to be registered. For most plug-ins, this is done by returning it’s action table in the following methods in ClassDesc:
virtual int NumActionTables();
virtual ActionTable* GetActionTable(int i);
The system will call these methods on start-up, so if your plug-in exports action tables, it cannot be demand loaded. This is required because the ActionItems need to be displayed in the customization dialogs even if your plug-in is not in use.
Action Context IDs used by MAX
The following action context IDs are used internally in MAX.
const ActionContextId kActionMainUIContext = 0;
const ActionContextId kActionTrackViewContext = 1;
const ActionContextId kActionMaterialEditorContext = 2;
const ActionContextId kActionVideoPostContext = 3;
const ActionContextId kActionSchematicViewContext = 5;
const ActionContextId kActionIReshadeContext = 6;
Menu Manager
The menu manager API lets plug-ins register menus and menu contexts that are saved in MAX’s menus customization file, and can be configured by the user.
Menus are populated with ActionItems published from ActionTables. Menu contexts are places where menus can appear in MAX’s UI. Menu contexts can be either for the main menu bar, or for places where a Quad menu can be displayed. 3ds max ships with a few pre-defined menu contexts, including the main menu bar, the viewport Quad menu and the ActiveShade Quad menu. Only a single menu-bar context is allowed, but plug-ins may register new Quad menu context as appropriate. For example, the UVW Unwrap plug-in in the 3ds max SDK defines a Quad menu context that is used when the user right-clicks in the UVW Unwrap window.
Menu creation is handled with the IMenu API defined in the iMenus.h header in the 3ds max SDK.
Adding to MAX’s default menus
A plug-in can use a MenuContext to register new menu items on to MAX’s default menu bar or Quad menus. Since 3ds max gets its menu configuration from a file (in UI\MaxMenus.mnu by default), plug-ins should only register their extensions a single time. To determine if the menu extensions have been registered yet, the plug-in should register a menu context with the ImenuManager::RegisterMenuContext() method. This method will return false if the context has not been registered, and true if it has. After the context is registered, it is saved in the menu file, and the next time 3ds max starts, the call to RegisterMenuContext() will return false.
To add items to MAX’s main menu, the plug-in should check the return value of RegisterMenuContext(), and if it is true, that means that this is the first time it has been registered, and the plug-in can then create new menus, add items to MAX’s main menu and Quad menus. In this case the MenuContext is used only as a place holder for determining when to add items to menus, not as a place where menus can appear.
Plug-ins can also register Quad menu contexts to be used as places Quad menus can appear in their UI. Typically this only applies if the plug-in creates its own floating window, such as UVW Unwrap. Normally, plug-in just need to register ActionItems, and possibly add items to MAX’s main menu or default menus.
Once items have been added to menus, and new menus have been registered, they will appear in MAX’s menu customization dialogs. Users can then move the items around, or remove them from menus.
If a plug-in wants to add menus and items to MAX’s existing menus, or create its own Quad menu context, it should be done when 3ds max starts. This is best done with a GUP-style plug-in. These plug-ins have a Start() ,method that is called when MAX’s first starts up. It is in that method that plug-ins should register new contexts and add menus and items.
Menu Context Ids
Every menu context needs a unique 32-bit integer identifier. The ids for MAX’s built-in contexts are defined in the iMenuMan.h header file. New plug-ins should use a random, fixed, integer value for the context. This can be generated using the "gencid.exe" program in MAX’s SDK. Just use one of the 2 32-bit values generated.
Class IMenuBarContext
3ds max comes with one pre-defined MenuBarContext, which is used to obtain the menu used on MAX’s main menu bar. Its context id is kMenuContextMenuBar. When a plug-in wants to add items or sub-menus to the main menu bar, it should get the menu from the main menu bar context as follows:
IMenuBarContext* pMenuBarContext = (IMenuBarContext*) GetCOREInterface()->GetMenuManager->GetContext(kMenuContextMenuBar);
IMenu* pMenu = pMenuBarContext->GetMenu();
The program can then use the IMenu class methods to add items or sub-menus to the main menu. It is recommended that plug-ins add their own top-level menu rather than adding items to existing menus.
The other use of the ImenuBar context is mentioned above. Plug-ins can register a menu bar context with its own ID to determine whether it should extend the main menu or not.
Sample Code
Examples of Action Table APIs in use are found in many places in the SDK. A simple example is \MAXSDK\MODIFIERS\FFD.