From 48be5a20cc5445312671c603f4e0e78580303fa1 Mon Sep 17 00:00:00 2001 From: Pierre Baillargeon Date: Mon, 13 Jan 2025 10:42:07 -0500 Subject: [PATCH] Document how to get nice undo labels --- lib/mayaUsd/undo/README-Nice-Undo.md | 184 +++++++++++++++++++++++++++ lib/mayaUsd/undo/README-Op-Undo.md | 15 ++- lib/mayaUsd/undo/README-USD-Undo.md | 7 +- lib/mayaUsd/undo/README.md | 1 + 4 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 lib/mayaUsd/undo/README-Nice-Undo.md diff --git a/lib/mayaUsd/undo/README-Nice-Undo.md b/lib/mayaUsd/undo/README-Nice-Undo.md new file mode 100644 index 0000000000..b2b57e8465 --- /dev/null +++ b/lib/mayaUsd/undo/README-Nice-Undo.md @@ -0,0 +1,184 @@ +# Nice Undo Labels + +While having undo/redo support at all is necessary, it is less useful if the user +does not understand what are the operations that can be undone and redone. In +order to help the user, it is necessary to have each operation kept in Maya's +undo stack and shown in Maya UI have readable labels that convey what would be +undone or redone. + +This document provides recipes and guidance on how to get nice labels in the +Maya UI for each individual undo item. This is necessary because it is not always +obvious how to achieve it and many easy way to implement an undoable command or +operation would result in bad labels. Getting nice labels often requires extra +efforts. + +## Maya Built-in Label Generator + +For its UI `Edit` menu, which contains the name of the operation that can be +undone and redone as a menu item label, Maya always filter the raw undo label. +It first split the raw label at the first space, then it limits its length to +at most 25 characters. These facts affect how you should design your undo label. + +The first thing is to ensure that the first 25 characters will be sufficient to +identify what would be undone or redone. This means wording your undo label with +the most important information at the beginning. + +The second thing is actually a trick to make nicer labels. The label will be +split at the first space character, but you can use non-breakable spaces to +still have spaces in your label. The unbreakable space character is UTF hex +code A0. That is the `"\xA0"` string in C++ or Python. + +## Scripting Callbacks + +When implementing actions triggered by UI in scripting, it is often done by +registering a callback. For example callbacks for menu items or for clickable +buttons. + +For MEL callbacks, we don't have a nice solution. We recommend to instead use +Python. When implemented in Python, the Maya undo system uses the callback +function Python module name and function name to build the UI label. These +are often forced unto you, and may not be clear to the user, so it is better +to force a nice label. This is easy to achieve since Python allows editing the +metadata of a function. The module name is kept in the function's `__module__` +property and its name in the `__name__` property. + +What we suggest is that you either write a function to modify other functions +or use a Python function decorator to modify these function properties. For +example, the menu items to add and remove USD schemas use a function to +generate its callback with nice undo label by editing the properties. We +needed a function to generate a function because the nice UI label can only +be known and generated at run-time from the name of the USD schema. See the +`_createApplySchemaCommand` function in `plugin\adsk\scripts\mayaUsdMenu.py`. + +Alternatively, you could use a function decorator. For example, this decorator +sets a nice label on the given function: + +```Python +def setUndoLabel(label): + ''' + This function decorator sets the function metadata so that it has + a nice label in the Maya undo system and UI. + ''' + def wrap(func): + nonBreakSpace = '\xa0' + func.__module__ = label.replace(' ', nonBreakSpace) + func.__name__ = '' + return func + return wrap + +# Example of using the decorator. +@setUndoLabel("nice label") +def example(c): + print(c) +``` + +## Creating Maya Commands + +For `MPxCommand`, Maya uses the command name as the UI label. So you need +to ensure your command name is sufficient to clearly describe what would be +undone. Unfortunately, this goes somewhat against a desirable trait of making +a command be flexible. Indeed, if a command can do multiple things, then the name +of the command would not be enough to really know what would be undone. In this +case, it might be beneficial to create a base command class that contains the +whole funcitonality but is *not* registered with Maya as a command. Instead, +multiple sub-classes that only declare different command names are registered +with Maya for each specific actions. + +An example of this was done for the USD collection editing. A single base class +deriving from `MPxCommand` contains the whole implementation of the command but +is not registered with Maya and multiple sub-classes are registered. See the +file `lib\mayaUsd\resources\ae\usdschemabase\collectionMayaHost.py`. + +In short, this is what is done. First create the base command with all the +implementation code: `doIt`, `undoIt`, etc + +```Python +class _BaseCommand(MPxCommand): + def __init__(self): + super().__init__() + + # MPxCommand command implementation. + + @classmethod + def creator(cls): + # Create the right-sub-class instance. + return cls() + + @classmethod + def createSyntax(cls): + syntax = MSyntax() + # Add your syntax arguments and flags + return syntax + + def doIt(self, args): + # implement your whole command + pass + + def undoIt(self): + # implement your whole command + pass + + def redoIt(self): + # implement your whole command + pass +``` + +Then create the concrete sub-commands: + +```Python +class FirstConcreteCommand(_BaseCommand): + commandName = 'NiceComprehensibleName' + def __init__(self): + super().__init__() + + +class SecondConcreteCommand(_BaseCommand): + commandName = 'AnotherNiceName' + def __init__(self): + super().__init__() + +_allCommandClasses = [ + FirstConcreteCommand, + SecondConcreteCommand, +] + +def registerCommands(pluginName): + plugin = MFnPlugin.findPlugin(pluginName) + if not plugin: + MGlobal.displayWarning('Cannot register commands') + return + + plugin = MFnPlugin(plugin) + + for cls in _allCommandClasses: + try: + plugin.registerCommand(cls.commandName, cls.creator, cls.createSyntax) + except Exception as ex: + print(ex) + + +def deregisterCommands(pluginName): + plugin = MFnPlugin.findPlugin(pluginName) + if not plugin: + MGlobal.displayWarning('Cannot deregister commands') + return + + plugin = MFnPlugin(plugin) + + for cls in _allCommandClasses: + try: + plugin.deregisterCommand(cls.commandName) + except Exception as ex: + print(ex) +``` + +## Invoking Maya Commands + +Giving commands a nice name is important, but unfortunately, it is not always +sufficient. In particular, Maya does not create proper UI label for commands +invoked from Python! + +So, unfortunately, to get a nice UI label from Python, you must invoke the command +from MEL instead. This is simple, but is important to remember. For example, to +invoke the `NiceComprehensibleName` command we declared above, we must *not* call +`cmds.NiceComprehensibleName` but instead `mel.eval("NiceComprehensibleName")` diff --git a/lib/mayaUsd/undo/README-Op-Undo.md b/lib/mayaUsd/undo/README-Op-Undo.md index 178e775b53..2cf552ac72 100644 --- a/lib/mayaUsd/undo/README-Op-Undo.md +++ b/lib/mayaUsd/undo/README-Op-Undo.md @@ -2,7 +2,13 @@ ## Goal -The goal of the OpUndo system is to support undo/redo when using any framework. +The OpUndo system is a set of C++ classes with the goal of supporting undo/redo +when using any combination of frameworks, each with their own way of recording +work that would ptentially need to be undone and redone later. + +Note that the goal OpUndo system is *only* to record what is done and would need +to be undone and redone. It is *not* an undo/redo stack nor an undo manager. +For that, we still rely on Maya's built-in undo system. The reason we need this is that the MayaUSD plugin uses multiple frameworks that each have their own undo system or none at all. For example, the plugin uses: @@ -14,6 +20,7 @@ each have their own undo system or none at all. For example, the plugin uses: - pure C++ code - Python + ## Building Blocks The OpUndo system is made of three main building blocks: @@ -40,3 +47,9 @@ The general pattern to use the framework is as follow: `OpUndoItemRecorder` will do that automatically for you. - When Maya asks your operation to be undone or redone, call `undo` or `redo` on the `OpUndoItemList` that holds all the undo items. + +So, a concrete example would be a Maya command derived from `MPxCommand`. Your +`MPxCommand` would contain an `OpUndoItemList` as a member variable. In your +commands' `doIt` function, you would declare an `OpUndoItemRecorder` and use +various `OpUndoItem` sub-classes. In The `MPxCommand` `undoIt` and `redoIt` +functions, you would call the `OpUndoItemList` `undo` and `redo` functions. diff --git a/lib/mayaUsd/undo/README-USD-Undo.md b/lib/mayaUsd/undo/README-USD-Undo.md index 6025b5b071..dd427ecf18 100644 --- a/lib/mayaUsd/undo/README-USD-Undo.md +++ b/lib/mayaUsd/undo/README-USD-Undo.md @@ -2,7 +2,12 @@ ## Motivation -The primary motivation for this service is to restore USD data model changes to its correct state after undo/redo calls. The current implementation for this system uses [SdfLayerStateDelegateBase](https://graphics.pixar.com/usd/docs/api/class_sdf_layer_state_delegate_base.html#details) by implementing a mechanism to collect an inverse of each authoring operation for undo purposes. This mechanism was inspired by Luma's USD undo/redo facilities found in [usdQt](https://github.com/LumaPictures/usd-qt) +The primary motivation for this service is to restore USD data model changes to +its correct state after undo/redo calls. The current implementation for this +system uses [SdfLayerStateDelegateBase](https://graphics.pixar.com/usd/docs/api/class_sdf_layer_state_delegate_base.html#details) +by implementing a mechanism to collect an inverse of each authoring operation +for undo purposes. This mechanism was inspired by Luma's USD undo/redo facilities +found in [usdQt](https://github.com/LumaPictures/usd-qt) ## Building Blocks diff --git a/lib/mayaUsd/undo/README.md b/lib/mayaUsd/undo/README.md index c9f9865c5c..a2d9fe3017 100644 --- a/lib/mayaUsd/undo/README.md +++ b/lib/mayaUsd/undo/README.md @@ -5,3 +5,4 @@ They are documented here: - [USD Undo/Redo](README-USD-Undo.md) - [General Undo/Redo](README-Op-Undo.md) +- [Nice Undo Labels](README-Nice-Undo.md)