HelloWorldTutorial - robmcmullen/peppy GitHub Wiki
This guide describes a very simple action and how it's added to the peppy menu system and keyboard processing system. All the action does is insert the string 'Hello, world!' at the current cursor position. Very simple action, but a good place to start as it illustrates all the basic highlights needed to begin to extend peppy.
Actions are the fundamental way to process user interaction in peppy. The same action can be tied to one or all of the following: a menu item, a toolbar item, or a keyboard command. This is possible by turning user interaction into a class, and having peppy automatically tie the action into the menu bar, toolbar, or keyboard processor as appropriate. No more explicit binding of a widget ID into a menu with a callback into a new method in your wx.Frame. Peppy actions compartmentalize all of the information about the action into one class (a subclass of SelectAction, and then you just announce the action to peppy. It takes care of the rest.
Note that a key feature of actions in peppy is that the action must keep track of its state extrinsically; that is, outside of its instance. Actions instances are created and destroyed many times in the lifecycle of a major mode -- whenever the user changes to a new tab or changes major modes, the menu system is rebuilt and new actions are created. So, actions must not depend on any instance variables existing the next time the action is called. State information should be referenced from the major mode, the frame (aka the window), class attributes of the action, or (bad programming practice alert) global variables.
You must have the full source code distribution of peppy in order to add actions. If you're using a binary installer for Windows or OSX, you won't be able to directly edit the files that are described below. Get the source code if you want to follow along at home.
Plugins are beyond the scope of this tutorial, but since plugins are necessary for incorporating new actions in peppy, there is an example plugin in the peppy disribution that includes the code from this tutorial. It is in the source distribution as peppy/plugin/tutorial_plugin.py. You can use the tutorial plugin as a place to try out new actions.
For now, we'll ignore most of the details in the tutorial_plugin.py file and concentrate on the `InsertHelloWorld` class. The only thing outside of `InsertHelloWorld` that is needed for the action to show up in the menu and keyboard processing system is a way to broadcast to peppy that `InsertHelloWorld` exists. That's the only thing that the `TutorialPlugin` provides for us, but we'll cover that little detail later. First, the `InsertHelloWorld` class itself.
There are a lot of comments in the source code of the `InsertHelloWorld` class; in fact, many more comments than actual code. I'll not duplicate the comments here when I include the code, but try to expand on the comments.
For basic actions, the only required file is:
from peppy.actions import *which places the `SelectAction` class (among others) into the current namespace.
The first thing to notice about `InsertHelloWorld` is that it is a subclass of `SelectAction`:
#!python class InsertHelloWorld(SelectAction):because SelectAction is the base of all actions, through a direct subclassing of `SelectAction` or by subclassing from another class that in turn subclasses from `SelectAction`.
Actions use several class attributes to describe themselves. There are many more than are shown in this example, but these are some of the most basic. The first attribute is
alias = "insert-hello-world"which is optional and shows peppy's emacs influence. `alias` names an alternate keyword that can be used in keyboard lookups. Emacs lisp used dash-separated words as function names, and for someone with an allergy to the shift key it can be helpful to have an alternate name.
Next is a required attribute:
name = "Hello World Action"which is the name that will be used as the menu item. This string will be automatically localized through the i18n subsystem during menu creation, so there's no need to wrap this in any i18n functions.
Tooltip is an important attribute:
tooltip = "Insert 'Hello, world' at the current cursor position"that is used to display a text string in the status bar when the mouse hovers over the menu item
Beyond the scope of this guide is the icon:
icon = Nonewhich allows an icon to be associated with the menu item or toolbar. Setting this to None means that this action will never be added to a toolbar. See IconResources for more information on icons in peppy.
In order for an action to appear in a menu, you must tell it where it goes:
default_menu = ("&Help/Tests", -801)
There's lots more info at MenuOverview, but basically the `default_menu` class attribute needs to contain a tuple of two entries, or None if you don't want it to appear in the menu at all. But, since we do want it to appear, the tuple's first entry contains the menu title (including submenus), and the second entry contains the relative position within the chosen menu.
Submenus are denoted by using a `/` character (which also means that a `/` may never be used as a menu or submenu title). So, the first element of the tuple is saying that this item will be placed in the Tests submenu of the Help main menubar entry.
The relative position is a number whose absolute value must be between 0 and 1000. (If the value is negative, it means put a separator before the entry and use the positive value for the relative position.) This specifies its position relative to other entries in the menu. This is not an ideal solution, because it then depends on other actions as to where it will actually show up. But, it's simpler and works better than some other solutions I tried. See MenuOverview for more commentary.
The default keyboard bindings are included in the action:
key_bindings = {'win': "Ctrl-F9", 'emacs': "C-F9 C-F9",}
This is a dictionary that can include the following keys: `win`, `mac`, `emacs`, and `default`. The value for each of the keys should be a string describing the key combination. See KeyboardProcessing for more information on valid keybinding specifiers, but this says that on the windows platform the command will be bound to Ctrl-F9, and when using emacs keybindings it will be bound to the sequence Ctrl-F9 Ctrl-F9 (i.e. two Ctrl-F9s in a row). If you're not interested in emacs-style keybindings, just ignore that last bit.
We want the action to insert the string "Hello, world!" at the current cursor position in the text. This means we are assuming that we are operating on a major mode that has some means of inserting text. The `FundamentalMode` is a sublcass of `wx.StyledTextCtrl`, and has a method called `AddText` that inserts a string at the current cursor position. So, that's what we'll use.
Because peppy is flexible and has many different major modes, there needs to be some way to tell it what actions can operate with each major mode. Typical python editors don't have to deal with this problem because they assume every file can be edited by the StyledTextCtrl. Peppy doesn't assume that, so you have to tell it if an action is compatible with a major mode.
The way that this is done in peppy is through a class method of the action. Before the menubar, toolbar and keyboard processor are created for a major mode, every action that peppy knows about is queried to see if it works with this major mode. The `worksWithMajorMode` class method needs to return a boolean if it works with the major mode that is passed in as an argument:
@classmethod
def worksWithMajorMode(cls, mode):
return hasattr(mode, 'AddText')
Now, you must be careful with this class method, because every conceivable type of major mode will be passed to this class method at some point. You can't assume that the major mode will be the one that you're designing the action for; it may not have any attributes that you know about. So, this example tests for a method that it needs, hasattr(mode, 'AddText') rather than seeing if some value is present. A direct test like if mode.someInstanceVariable will undoubtedly fail because there will exist some esoteric major mode designed by somebody down the road that won't have `someInstanceVariable`. So, use `hasattr` before checking for any instance variables, and if it fails the hasattr test, you know that it won't work anyway.
The menu item or toolbar button can be grayed out if the action is not available. This is controlled by the instance method `isEnabled`, which uses the boolean return value to indicate its availability:
def isEnabled(self):
return not self.mode.buffer.busy
This is worth a bit of discussion. First, note that `isEnabled` is an instance method, which means that when this method is called the action has already been instantiated and associated with a major mode. During the creation process, the major mode is saved as an instance attribute as `self.mode` and the window is stored as `self.frame` so that you can refer to them inside your action. Since you can't store data intrinsic to the action, using `self.mode` and `self.frame` is the way to examine state data.
The next thing to note is the variable we're looking at is `self.mode.buffer.busy`, which is a bit complicated. The buffer attribute of `self.mode` points to a Buffer object which represents the file that you're editing with that major mode. The `busy` attribute of the Buffer object is a flag that is used to indicate the buffer is being modified by another thread. This is a bit of an advanced topic and won't be covered here, but for now just be aware that `self.mode.buffer.busy` is a boolean.
When subclassing directly from `SelectAction`, you must override the `action` method to perform the operation. Other subclasses of `SelectAction` are available that simplify things a bit, see SimpleActionTutorial for more examples.
But, this example uses `SelectAction` directly, so to insert our Hello, world! string, we need to override `action` here:
def action(self, index=-1, multiplier=1):
hello = "Hello, world"
for i in range(0, multiplier):
self.mode.AddText(hello)
The first thing to note is the arguments that are passed in to the `action` method. The argument `index` is only used for lists, so is ignored by simple actions. The `multiplier` argument is only used if the action is called through keyboard processing, and then only used if an emacs-style numeric argument is passed to it. I have chosen to handle the optional numeric processing here, however, and the method will output multiple 'Hello, world!' strings corresponding to the number passed in as the prefix argument. See KeyboardProcessing for more information on keyboard prefix arguments.
The text is inserted using the call to `AddText`, which is a `wx.StyledTextCtrl` method that adds text at the current cursor position. Recall that self.mode will be set to the instance of the major mode when the action is created by the menu system. Because we already checked for the compatibility of this major mode with our action in the `worksWithMajorMode` class method, we don't have to check for compabibility again.
Now that we have this whiz-bang action created, the only way to tell peppy about it is through the plugin system. Creating an entirely new plugin is beyond the scope of this guide, but let's focus on a single method of the plugin that's already defined in the `tutorial_plugin.py` file:
def getActions(self):
return [InsertHelloWorld]
To find out about all the actions, peppy gets a list of every plugin that is available. For each one of those plugins, it calls its `getActions` method to get a list of the actions that plugin provides. It builds up a list of all available actions in this manner, and then proceeds to determine what actions are applicable to the major mode by calling the action's `worksWithMajorMode` method.
That's all you need to know about the plugin system for now. Creating a plugin will be a topic for another tutorial.
To test the plugin, you should run peppy with the `-t` option which disables communication to existing peppy frames and also prints any errors to the console. If there is a syntax error in your plugin, it will show up in the output:
$ python peppy.py -t
ERROR:root:Unable to execute the code in plugin: /home/rob/src/peppy-git/peppy/plugins/tutorial_plugin
ERROR:root: The following problem occured:
Traceback (most recent call last):
File "/home/rob/src/peppy-git/peppy/yapsy/PluginManager.py", line 276, in loadPlugins
execfile(candidate_filepath+".py",candidate_globals)
File "/home/rob/src/peppy-git/peppy/plugins/tutorial_plugin.py", line 94
hello = STEVEN A SMITH LOVES CHEEZ DOODLES
^
SyntaxError: invalid syntax
If you don't get any errors from the plugin manager, you should be able to see the plugin in the menu system when you have a file of the appropriate major mode loaded. For instance, in this case, you won't be able to see the plugin when looking at the peppy title screen, because you can't insert into HTML. (More correctly it doesn't appear due to the `worksWithMajorMode` test: the `HTMLMode` doesn't have an AddText method.)
If you open up the `tutorial_plugin.py` file, you should be able to see 'Hello World Action' in the menu Help -> Tests.
Selecting the `Hello World Action` should place the string "Hello, world!" at the current cursor position, and as a side effect of the `AddText` method, reposition the cursor to the character following the `! `.
See the SimpleActionTutorial for a more complete example of creating an action.