This chapter tells how to customize Leo.
We first briefly discuss the three main ways of customizing Leo. Later sections provide all the details.
When Leo first starts, Leo looks for a file called leoConfig.txt, derived from leoConfig.leo, which contains extensive documentation for each setting. The settings in leoConfig.txt include:
Warning: Naively using plugins and hooks can expose you and your .leo files to malicious attacks. See the section called "Security warnings" below.
You may customize how Leo works by placing your own Python code in the plugins folder. When executing commands or handling events Leo calls hook routines defined in mod_*.py files in the plugins folder. The arguments to hook routines are "tag", a string telling the kind of command or event about to be executed, and "keywords", a Python dictionary of information whose contents depend on the specific command or event. The code Leo calls when a command or event happens is called the "hook" for that tag.
This is a simple, powerful and general mechanism for customizing Leo. There are dozens of kinds of hooks, including the "command1" and "command2" hooks, that are called before and after each of Leo's menu commands. Leo will allow you to override most commands and event handling. In many cases, if a hook returns any value except None Leo will assume that the hook has completely handled the command or event and will take no further action.
Leo catches all exceptions raised in hooks, so syntax errors and other exceptions are not serious concerns.
Naively using hooks can expose you and your .leo files to malicious attacks. You will be safe as long as you follow these basic principles:
Hooks can import any file in Leo's source code and execute routines in that file. Leo's contains a number of convenience routines designed to make common customization tasks easier. Hooks can use these routines to create your own menus, to translate menus into other languages, and to create entries in the Open With menu. These convenience routines are discussed in detail below.
Leo will override settings in .leo files if it finds a file called leoConfig.txt. You should generate leoConfig.txt from leoConfig.leo. Leo works just as before if it does not find a leoConfig.txt file. The next section contains an example of leoConfig.txt showing all the options that may be set.
Leo looks for leoConfig.txt first in the directory specified by the Python variable sys.leo_config_directory. You would typically set this variable in Python's sitecustomize.py file. If this variable does not exist, Leo looks in the directory from which Leo was loaded.
Settings in leoConfig.txt overrides preferences in .leo files, but only for those items actually in leoConfig.txt, so you can choose which settings you want to override. Also, a Leo ignores any setting in leoConfig.txt whose value is "ignore" (without the quotes). For example:
[prefs panel options] tab_width = ignore
If a setting is overridden, it is _not_ written to the .leo file when the outline is saved. Note that this does not change the file format: all previous versions of Leo will be able to read such .leo files.
The preceding is probably all you need to know to use leoConfig.txt. The following discuss some minor details:
The following sections discuss important security considerations. You should be familiar with these if you plan to use hooks:
I'd like to thank Stephen Schaefer for gently insisting that we guard against malicious code in shared .leo files. To quote Stephen directly:
"I foresee a future in which the majority of leo projects come from marginally trusted sources... I see a world of leo documents sent hither and yon--resumes, project proposals, textbooks, magazines, contracts-- and as a race of Pandora's, we cannot resist wanting to see 'What's in the box?' Are we going to fire up a text editor to make a detailed examination of the file? Never! We're going to double click on the cute leo file icon, and leo will fire up in all its raging glory. Just like Word (and its macros) or Excel (and its macros)."
In short, when we share "our" .leo files we can not assume that we know what is our "own" documents. So hooks that naively searches through .leo files looking for scripts to execute is looking for big trouble.
Never use this kind of code in a hook:
@ WARNING Using the following routine exposes you to malicious code in .leo files! Do not EVER use code that blindly executes code in .leo files! Someone could send you malicious code embedded in the .leo file. WARNING 1: Changing "@onloadpythonscript" to something else will NOT protect you if you EVER share either your files with anyone else. WARNING 2: Replacing exec by rexec below provides NO additional protection! A malicious rexec script could trash your .leo file in subtle ways. @c
# WRONG: This blindly execute scripts found in an .leo file! def onLoadFile(): v = top().rootVnode() while v: h = v.headString().lower() if match_word(h,0,"@onloadpythonscript"): s = v.bodyString() if s and len(s) > 0: try: # SECURITY BREACH: s may be malicious! exec(s+'\n',__builtins__,__builtins__) except: es_exception() v = v.threadNext()
Do not expect rexec to protect you against malicious code contained in .leo files. Remember that Leo is a repository of source code, so any text operation is potentially malicious.
For example, consider the following script--a script is valid in rexec mode:
c = top() thisNode = c.currentVnode() v = c.rootVnode() while v: << change all instances of rexec to exec in v's body >> v = v.threadNext() << delete thisNode >> << clear the undo stack >>
This script will introduce a security hole the .leo file without doing anything prohibited by rexec, and without leaving any traces of the perpetrating script behind. The damage will become permanent outside this script when the user saves the .leo file. Many other kinds of mischief could be done by similar scripts.
New in 3.11. Leo now looks in the plugins folder for files whose name matches mod_*.py. Such plugin files define routines called hooks. Leo calls these hooks before and after all commands and many important events. You can use hooks to:
Hooks have full access to all of Leo's source code. Several convenience methods have been added to make customizing menus and commands easier. These convenience methods are described in details below.
Plugins will not change when Leo is updated. You can take advantage of the latest CVS updates without having to throw away your modifications.
Plugin files should define hook routines and should register those routines when the plugin is imported. See LeoPy.leo for the code for many examples of defining plugins.
For example, the file plugins\mod_open_with.py contains the plugin that handles the Open With menu. The @file node for plugins\mod_open_with.py is:
"""Open With handler"""
from customizeLeo import *
from leoGlobals import *
@others
if 1: # Register the handlers...
registerHandler("idle",on_idle)
registerHandler(("start2","open2","command2"),
create_open_with_menu)
es("...open with")
The actual hooks (the on_idle and open_with_menu routines) are defined in child nodes. See LeoPy.leo for the code for these routines. At startup time Leo will import mod_open_with and the code shown will be executed.
The registerHandler function registers a function as a hook for one or more tags. Plugins may call registerHandler with a hook name like "idle" or a list of hook names like ("start2","open2","command2"). Using "all" as the hook name registers the function for all hooks. This is useful for tracing hooks.
The registerExclusiveHandler function registers a function for hooks that should not be redefined in other plugins.
The code in each plugin registered to each tag is known as the "hook" routine for that tag. Leo calls hook routines at various times during execution. Leo passes two arguments to each hook:
tag, a string identifying the time at which the hook has been called
keywords, a Python dictionary containing information unique to each hook. For example, keywords["label"] indicates the kind of command for "command1" and "command2" hooks.
Leo catches exceptions, including syntax errors in this code, so it is safe to hack away on this code.
For some hooks, returning anything other than None "overrides" Leo's default action. Hooks have full access to all of Leo's source code. Just import the relevant file. For example, top() returns the commander for the topmost Leo window.
The following table summarizes the arguments passed to customizeLeo().
Overrides is "yes" if the hook will override Leo's normal processing by returning anything other than None.
|
hook name |
overrides |
when called |
keys in keywords |
| "bodyclick1" | yes | before normal click in body | c,v,event (new in 3.11) |
| "bodyclick2" | after normal click in body | c,v,event (new in 3.11) | |
| "bodydclick1" | yes | before double click in body | c,v,event (new in 3.11) |
| "bodydclick2" | after double click in body | c,v,event (new in 3.11) | |
|
"bodykey1" |
yes |
before body keystrokes | c,v,ch,oldSel,undoType |
|
"bodykey2" |
after body keystrokes | c,v,ch,oldSel,undoType | |
| "bodyrclick1" | yes | before right click in body | c,v,event (new in 3.11) |
| "bodyrclick2" | after right click in body | c,v,event (new in 3.11) | |
| "boxclick1" | yes | before click in +- box | c,v,event (new in 3.11) |
| "boxclick2" | after click in +- box | c,v,event (new in 3.11) | |
|
"command1" |
yes |
before each command | c,v,label (note 2) |
|
"command2" |
after each command | c,v,label (note 2) | |
| "drag1" | yes | before start of drag | c,v,event (new in 3.11) |
| "drag2" | after start of drag | c,v,event (new in 3.11) | |
| "dragging1" | yes | before continuing to drag | c,v,event (new in 3.11) |
| "dragging2" | after continuing to drag | c,v,event (new in 3.11) | |
| "end1" | start of app.quit() | ||
| "enddrag1" | yes | before end of drag | c,v,event (new in 3.11) |
| "enddrag2" | after end of drag | c,v,event (new in 3.11) | |
| "headclick1" | yes | before normal click in headline | c,v,event (new in 3.11) |
| "headclick2" | after normal click in headline | c,v,event (new in 3.11) | |
| "headrclick1" | yes | before right click in headline | c,v,event (new in 3.11) |
| "headrclick2" | after right click in headline | c,v,event (new in 3.11) | |
| "headkey1" |
no |
before headline keystrokes | c,v,ch |
| "headkey2" | after headline keystrokes | c,v,ch | |
| "hypercclick1" | yes | before control click in hyperlink | c,v,event (new in 3.11) |
| "hypercclick2" | after control click in hyperlink | c,v,event (new in 3.11) | |
| "hyperenter1" | yes | before entering hyperlink | c,v,event (new in 3.11) |
| "hyperenter2" | after entering hyperlink | c,v,event (new in 3.11) | |
| "hyperleave1" | yes | before leaving hyperlink | c,v,event (new in 3.11) |
| "hyperleave2" | after leaving hyperlink | c,v,event (new in 3.11) | |
| "iconclick1" | yes | before single click in icon box | c,v,event (new in 3.11) |
| "iconclick2" | after single click in icon box | c,v,event (new in 3.11) | |
| "iconrclick1" | yes | before right click in icon box | c,v,event (new in 3.11) |
| "iconrclick2" | after right click in icon box | c,v,event (new in 3.11) | |
| "icondclick1" | yes | before double click in icon box | c,v,event (new in 3.11) |
| "icondclick2" | after double click in icon box | c,v,event (new in 3.11) | |
| "idle" | periodically (at idle time) | c,v | |
| "menu1" |
yes |
before creating menus | c,v (note 3) |
| "menu2" |
yes |
before updating menus | c,v |
| "open1" | yes | before opening any file | old_c,new_c,fileName |
| "open2" | after opening any file | old_c,new_c,fileName | |
| "openwith1" | yes | before Open With command | c,v,openType,arg,ext (note 4) |
| "openwith2" | after Open With command | c,v,openType,arg,ext (note 4) | |
| "recentfiles1" | yes | before Recent Files command | c,v,fileName,closeFlag |
| "recentfiles2" | after Recent Files command | c,v,fileName,closeFlag | |
| "select1" | yes | before selecting a vnode | c,new_v,old_v |
| "select2" | after selecting a vnode | c,new_v,old_v | |
| "start1" | no | after app.finishCreate() | |
| "start2" | after opening first Leo window | fileName | |
| "unselect1" | yes | before unselecting a vnode | c,new_v,old_v (new in 3.11) |
| "unselect2" | after unselecting a vnode | c,new_v,old_v (new in 3.11) | |
| "@url1" | yes | before @url event | c,v (note 5) |
| "@url2" | after @url event | c,v (note 5) |
Notes:
(1) "activate" and "deactivate" hooks: These have been removed because they do not work as expected.
(2) "commands" hooks: The label entry in the keywords dict contains the "canonicalized" form of the command, that is, the lowercase name of the command with all non-alphabetic characters removed. New for 3.11: The label for all variants of the Undo and Redo commands are "undo" and "redo".
(3) "menu1" hook: Setting app().realMenuNameDict in this hook is an easy way of translating menu names to other languages. Note: the "new" names created this way affect only the actual spelling of the menu items, they do _not_ affect how you specify shortcuts in leoConfig.txt, nor do they affect the "official" command names passed in app().commandName. For example, suppose you set app().realMenuNameDict["Open..."] = "Ouvre".
(4) "open1" and "open2" hooks: These are called with a keywords dict containing the following entries:
old_c: The commander of the previously open window.
new_c: The commander of the newly opened window.
fileName: The name of the file being opened.
You can use old_c.currentVnode() and new_c.currentVnode() to get the current vnode in the old and new windows.
Leo calls the "open1" and "open2" hooks only if the file is already open. Leo will also call the "open1" and "open2" hooks if a) a file is opened using the Recent Files menu and b) the file is not already open.
(5) Leo calls the "@url1" and "@url2" hooks only if "icondclick1" hook returns None.
Hooks have full access to all of Leo's source code simply by importing it. Moreover, several convenience methods have been added to make customizing menus and commands easier. The following paragraphs discuss these routines and how to use them.
The following routines enable and disable "idle" hooks. They are defined in leoGlobals.py. Idle hooks are good places to check for changed temporary files created by the Open With command.
enableIdleTimeHook(idleTimeDelay=100)
Enables the "idle" hook. After this routine is called Leo will call customizeLeo("idle") approximately every idleTimeDelay milliseconds. Leo will continue to call customizeLeo("idle") periodically until disableIdleTimeHook() is called.
disableIdleTimeHook()
Disables the "idle" hook.
The following convenience routines make creating menus easier. These are methods of the leoFrame class. Use top().frame to get the frame object for the presently active Leo window. These convenience methods all do complete error checking and write messages to the log pane and to the console if errors are encountered. LeoPy.leo gives examples of how to use these routines to create custom menus and to add items to the Open With menu.
createMenuItemsFromTable (self,menuName,table,openWith=0)
This method adds items to the menu whose name is menuName. The table argument describes the entries to be created. This table is a sequence of items of the form (name,shortcut,command).
- name is the command name
- shortcut is the shortcut, or None to indicate no shortcut.
- command is the routine to execute when the menu item is selected.
An entry of the form ("-",None,None) indicates a separator line between menu items. For example:
table = ("Toggle Active Pane","Ctrl-T",self.OnToggleActivePane), ("-",None,None), ("Toggle Split Direction",None,self.OnToggleSplitDirection)) top().frame.createMenuItemsFromTable("Window",table)If the openWith keyword argument is 1 the items are added to a submenu of the Open With menu. However, it will be more convenient to use the createOpenWithMenuFromTable method to create the Open With menu.
createNewMenu (self,menuName,parentName="top")
This method creates a new menu:
- menuName is the name of the menu to be created.
- parentName is the name of the parent menu,
or "top" if the menu is to created in the menu bar.This method returns the menu object that was created, or None if there was a problem. Your code need not remember the value returned by this method. Instead, your code will refer to menus by name.
createOpenWithMenuFromTable(self,table)
This method adds items to submenu of the Open With menu item in the File menu.
The table argument describes the entries to be created; table is a sequence of items of the form (name,shortcut,data).
- name is the command name
- shortcut is the shortcut, or None to indicate no shortcut.
- data is a tuple of the form (command,arg,ext)
- command is one of "os.system", "os.startfile", "os.spawnl", "os.spawnv" or "exec".
- arg is an argument to be passed to the given command.
- path is the full path name of the temporary file to be passed to the external editor.
- ext is a file extension or None.
When the user selects the Open With item corresponding to the table item Leo executes command(arg+path) where path is the full path to the temp file. If ext is not None, the temp file has the given extension. Otherwise, Leo computes an extension based on what @language directive is in effect. For example:
table = ( ("Idle", "Alt+Shift+I",("os.system",idle_arg,".py")), ("Word", "Alt+Shift+W",("os.startfile",None,".doc")), ("WordPad","Alt+Shift+T",("os.startfile",None,".txt"))) top().frame.createOpenWithMenuFromTable(table)
deleteMenu (self,menuName)
Deletes the menu whose name is given, including all entries in the menu.
deleteMenuItem (self,itemName,menuName="top")
Deletes the item whose name is itemName from the menu whose name is menuName. To delete a menu in the menubar, specify menuName="top".
The leoFrame class creates the Window menu as follows:
windowMenu = self.createNewMenu("Window")table = ( ("Equal Sized Panes","Ctrl-E",self.OnEqualSizedPanes), ("Toggle Active Pane","Ctrl-T",self.OnToggleActivePane), ("Toggle Split Direction",None,self.OnToggleSplitDirection), ("-",None,None), ("Cascade",None,self.OnCascade), ("Minimize All",None,self.OnMinimizeAll), ("-",None,None), ("Open Compare Window",None,self.OnOpenCompareWindow), ("Open Python Window","Alt+P",self.OnOpenPythonWindow))self.createMenuEntries(windowMenu,table)
It is easy for a hook to translate menus into another language. For example, code similar to the following code would typically be found in the "start2" hook:
table = ( ("Open...","Ouvre"), ("Open With...","Ouvre Avec..."), ("Close","Ferme"))# Call the convenience routine to do the work. app().setRealMenuNamesFromTable(table)
Leo will now draw the LeoDoc.ico from the Icons directory in Leo windows, provided you have installed Fredrik Lundh's PIL and tkIcon packages.
Download PIL from http://www.pythonware.com/downloads/index.htm#pil
Download tkIcon from http://www.effbot.org/downloads/#tkIcon
Many thanks to Jonathan M. Gilligan for suggesting this code. At present, the icon is not drawn very well. This may be corrected in version 3.10.