Difference between revisions of "Python Command Overview"
(Made it clear that user-facing strings should be defined in configs, not by implementing methods.)
m (Jangell moved page Command Overview to Python Command Overview: Clarify that this is for Python commands, vs C++, etc.)
Latest revision as of 11:43, 9 October 2019
- 1 Python API Command Overview
- 1.1 The Entire Command
- 1.2 Breakdown
- 1.3 User Facing Strings: Command Help
Python API Command Overview
This page will attempt to give some "boilerplate" code for creating a Python API command (with arguments) in MODO. There is much more you can do with commands, but this should cover common uses.
We'll start with listing the entire code and then breaking it down.
The Entire Command
#!/usr/bin/env python import lx import lxu.command class MyCommand_Cmd(lxu.command.BasicCommand): def __init__(self): lxu.command.BasicCommand.__init__ (self) self.dyna_Add ("myBoolean", lx.symbol.sTYPE_BOOLEAN) self.dyna_Add ("myDistance", lx.symbol.sTYPE_DISTANCE) self.basic_SetFlags (1, lx.symbol.fCMDARG_OPTIONAL) self.dyna_Add ("myString", lx.symbol.sTYPE_STRING) self.basic_SetFlags (2, lx.symbol.fCMDARG_OPTIONAL) def cmd_Flags(self): return lx.symbol.fCMD_UNDO | lx.symbol.fCMD_MODEL def basic_Execute(self, msg, flags): boolean = self.dyna_Bool (0, False) distance = self.dyna_Float (1, 0.0) string = self.dyna_String (2, "") lx.out("Boolean: %s" % boolean) lx.out("Distance: %sm" % distance) if not self.dyna_IsSet (1): lx.out("...but you didn't set it.") lx.out("String: %s" % string) if not self.dyna_IsSet (2): lx.out("...but you didn't set it.") lx.bless (MyCommand_Cmd, "my.command")
#!/usr/bin/env python import lx import lxu.command
This piece simply tells MODO this is a Python file, and imports the basic modules we need for creating a command. You may require other modules depending on the functions you intend to carry out in your command.
The Command Class & Init
class MyCommand_Cmd(lxu.command.BasicCommand): def __init__(self): lxu.command.BasicCommand.__init__ (self)
This section defines a new class which inherits from lxu.command.BasicCommand.
The __init__ function is called when the command is loaded by MODO at start up.
At the very least, this function should contain lxu.command.BasicCommand.__init__ (self)
Argument Definition (Optional)
self.dyna_Add ("myboolean", lx.symbol.sTYPE_BOOLEAN) self.dyna_Add ("mydistance", lx.symbol.sTYPE_DISTANCE) self.basic_SetFlags (1, lx.symbol.fCMDARG_OPTIONAL) self.dyna_Add ("mystring", lx.symbol.sTYPE_STRING) self.basic_SetFlags (2, lx.symbol.fCMDARG_OPTIONAL
In this example, we have also defined arguments for our command. These are not required, but must go inside the __init__ function if you wish your command to have arguments. They are added to the command in the order they are defined here and assigned corresponding indices. NOTE: This order is important for accessing the argument values later on, as we will be accessing them via their index.
At their most basic, arguments are defined with a name and a type. The name is the internal name of the argument and should not contain spaces - we will give them user-friendly display names later on. The names can be used by the Command Help for this command to define user-friendly display names from a config file (which allows for localisation), but we won't be covering that here.
By default, all arguments are required to be set for the command to run. If, when the command is called and all the required arguments are not set, a dialog will be displayed to the user allowing them to enter the values.
However, you'll note that we've specified flags on two of the arguments. The flags are specified by referencing the index of the argument they apply to (see, order is important!) and giving the flag itself.
In this case, we've specified the flag of "OPTIONAL", which means that the user does not have to specify a value for the second and third arguments. We will have to make sure that we assign any unspecified arguments default values in the main execution of the command later on.
As a brief aside...
Although MODO appears to have several different argument types, they are all user-friendly wrappers for the storage of 4 core types; integers, floats, strings and objects.
You'll have seen these friendly wrappers for things like distance fields, where you can enter a value in metres, millimetres, feet, microns, etc... However, internally, these are read and stored as a simple float value which is the displayed distance as metres.
Similarly, angle fields, where you can enter a value in degrees, are stored internally as a float of the displayed angle in radians.
Boolean values (often shown as checkboxes or toggle buttons) are simply stored as integers which are 0 or 1.
NOTE: These internal values are what you'll be dealing with when you write commands.
def cmd_Flags(self): return lx.symbol.fCMD_UNDO | lx.symbol.fCMD_MODEL
This is a very important part of a command if you are using the Python API (or TD SDK) to edit the scene in the command execution. The command flags tell MODO what the command is expected to do when it executes and how to handle it.
In our case, we specify the standard flags; MODEL and UNDO. These flags are bit masks and as such are joined together into a single integer return value using the pipe | separator.
The MODEL flag tells MODO that we will be editing a part of the scene. The name is slightly misleading as it implies changes to a mesh only, however it means any change to anything in the scene; channels, meshes, selection changes, adding or removing items, etc...
The UNDO flag is specified by default as part of the MODEL flag, however it's not harmful to add the flag to be clear. This tells MODO that the command should be undoable and that it should set up an undo state for it. NOTE: It is very important that this flag is set, as changing the scene without this flag set causes instability in MODO and usually leads to a crash (if not immediately then very soon).
Generally, these should be your standard flags unless you have specific reason to change them.
def basic_Execute(self, msg, flags): boolean = self.dyna_Bool (0, False) distance = self.dyna_Float (1, 0.0) string = self.dyna_String (2, "") lx.out("Boolean: %s" % boolean) lx.out("Distance: %sm" % distance) if not self.dyna_IsSet (1): lx.out("...but you didn't set it.") lx.out("String: %s" % string) if not self.dyna_IsSet (2): lx.out("...but you didn't set it.")
This is the meat of the command - the code that's actually run when the command is fired.
Here, we're not doing anything other than reading the arguments and writing to the Event Log.
You can see here that those core types are how we read the arguments in the command, with dyna_Bool (a friendly wrapper for dyna_Int, checking if it's equal to 1 or 0), dyna_Float and dyna_String. These are accessed via index, the same indices we've used throughout the command. Optionally, a default value is given as the second parameter, which is the value returned if the argument has not been set by the user.
We can also make use of the command's dyna_IsSet function, which will return True or False depending on whether the argument with that index was specified by the user (this function is what is used internally for the dyna_Float and related functions, to determine whether to return the default value or not).
It is important to note that any arguments which have UI hints (such as minimum or maximum values - including text hints) are for UI purposes only, and that arbitrary values can be entered as arguments. This means that if you have a UI hint that gives it a range of 10-50, the user can still enter 12000 manually from a script or the command entry, or enter an out-of-range integer instead of a text hint string. So be sure to manage such values and take appropriate action if such values would cause problems in your code (e.g. aborting execution or limiting the value supplied by the user to the desired range).
Also note that, as this is part of the Python API, we can freely use lx.eval() for calling commands and querying values, just like regular fire-and-forget Python scripts.
Making Main Execution Useful
This is a simple outline of writing a command with arguments. It doesn't actually do anything. For further reading, you can find other examples of code to go into the Execute function on the wiki (such as Creating a Selection Set).
Blessing the Command
Here, we call the bless command. This promotes the class we created to be a fully fledged command inside of MODO, as opposed to simply a Python script. It takes two arguments. One is the command class we defined, the second is the command that we'll want to assign to this inside MODO.
This means that the script is not run via the usual @scriptName.py command that fire-and-forget scripts are. Instead, this command is run be entering my.command as it is a proper command in MODO.
lx.bless (MyCommand_Cmd, "my.command")
User Facing Strings: Command Help
All user-facing strings, such as for the command name, argument names, and so on, should be defined in Command Help config files whenever possible. Config files are considered to be part of any command or kit, and not as optional extras. This is also the only place that the Command List gets user-friendly information that can teach others how to use your commands.
This is an example of what the command help for this command would look like. This would be inside a .cfg file that is distributed with the .py file of the actual command.
If distributed with this config, you would be able to remove all of the above command's functions which relate to returning user-friendly names for things and MODO would use this file to get the relevant values automatically.
It is also worth noting that these can be localized by duplicating the <hash type="Command" key="my.command@en_US"> fragment and it's contents and changing the @en_US' to @[localized language code] then replacing the contents of the atom fragments accordingly.
By default, MODO will look for @en_US and this will also be the fallback if the user's specified language isn't found.
- Command: this command's internal name and a language code. These are used to find the user strings for the current system locale.
- UserName: The name displayed for the command in the Command List and other parts of the UI.
- ButtonName: The label of the control when the command is in in a form.
- ToolTip: The text displayed in a tooltip when hovering the mouse over the control in a form.
- Example: A simple example usage of the command, shown in the Command List.
- Desc: Basic documentation for the command, as shown in the command list.
- Argument: Given an argument's internal name, this defines its username ad description. The username is shown in command dialogs and the command list. The command list also displays the description, which should be considered documentation for how to use that argument, the default behavior if the argument is optional and not set, and so on.
<?xml version="1.0"?> <configuration> <atom type="CommandHelp"> <!-- note the command's name in here; my.command --> <hash type="Command" key="my.command@en_US"> <atom type="UserName">My Command Dialog - also shown in the command list.</atom> <atom type="ButtonName">My Command</atom> <atom type="Tooltip">My command's tooltip</atom> <atom type="Desc">My command's description - shown in the command list.</atom> <atom type="Example">my.command true 1.5 "hello!"</atom><!-- An example of how my command would be called - shown in the command list. --> <hash type="Argument" key="myBoolean"> <atom type="UserName">A Boolean</atom> <atom type="Desc">The boolean argument of my command - shown in the command list.</atom> </hash> <hash type="Argument" key="myDistance"> <atom type="UserName">A Distance</atom> <atom type="Desc">The distance argument of my command - shown in the command list.</atom> </hash> <hash type="Argument" key="myString"> <atom type="UserName">A String</atom> <atom type="Desc">The string argument of my command - shown in the command list.</atom> </hash> </hash> </atom> </configuration>
User-Facing String Overrides
Generally speaking, you should define all user-facing strings in cmdhelp configs. The configs allow the command to be localized into different languages, and are used in the Command List part of the Command History to provide usage information for users.
However, it is sometimes useful to the able to override the cmdhelp-based strings with dynamic strings. These should never be used to return static strings -- those should be in cmdhelp configs. Even when returning dynamic strings with these methods, the strings should come from message tables whenever possible to ensure that they can also be localized. Cases where strings are used directly in code are those that provided by the user, like the name of an item, but care should be take to avoid hard-coding any user-facing text as string literals directly into the code itself.
The ButtonName() method provides a short name for when the control is displayed in a form. UserName() is usually a somewhat more verbose name that is display in other parts of the interface. Tooltip() defines a string to display in the tooltip window when the user hovers over the control in a form. UIHints() has a variety of methods, but can be used to set the label for a specific command argument when displayed in the UI, such as in a command dialog. Most of these should never be ended, as these values are usually static and should be set in cmdhelp configs.
def basic_ButtonName(self): return dynamicButtonNameStringHere def cmd_Tooltip(self): return dynamicTooltipStringHere def cmd_UserName(self): return dynamicUsernameStringHere def arg_UIHints (self, index, hints): if index == 0: hints.Label (dynamicArgumentName0Here) elif index == 1: hints.Label (dynamicArgumentName1Here) elif index == 2: hints.Label (dynamicArgumentName2Here)