Skip to content

Commit

Permalink
Document how to get nice undo labels
Browse files Browse the repository at this point in the history
  • Loading branch information
pierrebai-adsk committed Jan 13, 2025
1 parent 1e26e8b commit 48be5a2
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 2 deletions.
184 changes: 184 additions & 0 deletions lib/mayaUsd/undo/README-Nice-Undo.md
Original file line number Diff line number Diff line change
@@ -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")`
15 changes: 14 additions & 1 deletion lib/mayaUsd/undo/README-Op-Undo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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.
7 changes: 6 additions & 1 deletion lib/mayaUsd/undo/README-USD-Undo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions lib/mayaUsd/undo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 48be5a2

Please sign in to comment.