back leo next

Chapter 7: Scripting Leo with Python

This chapter is a tutorial describing how to write Python scripts that get and modify data contained in Leo outlines. Scripts use Leo's, vnode, tnode and position classes to access data in Leo outlines. The combination of a vnode v and its tnode v.t represents a node's data. Leo needs both vnodes and tnodes to represent clones efficiently. Indeed, a tnode represents all data shared by cloned nodes. When Leo clones a vnode v, Leo creates a new, independent vnode v2 and sets v2.t == v.t. Thus, cloning a node does not duplicate any information in descendant trees, but descendants of a cloned node appear more than once on the screen (when all nodes are expanded). Positions represent a node at a particular place on the screen. Equivalently, a position indicates a particular place in a tree traversal. Iterators of the position class define various kinds of tree traversals.

Leo's execute-script command predefines several symbols. This makes it easy to access the data in Leo outlines and the Leo's own source code. g is predefined to the leoGlobals module. Scripts commonly use utility functions such as g.es, g.trace, etc. The g.app object represents Leo itself. The instance vars (ivars) of g.app are Leo's global variables and constants. The execute-script command predefines c to be the commander (see below) of the Leo outline in which the script is contained. Whenever possible, scripts should use the high-level methods of the commander to insert, delete or clone nodes. Finally, the execute-script commands predefines p to be the presently selected position.

This chapter describes only some of Leo's functions, classes and methods. However, your scripts have complete access to all of Leo's source code, that is, all the code in LeoPy.leo. You are not limited by what you see in this chapter.

Commonly used classes

Leo's source code is a collection of classes, along with utility functions in leoGlobals.py. Here are the classes and objects that scripts will commonly use:

g.app
The application object representing the entire Leo application. The ivars (instance variables) of g.app represent Leo's global variables.
g.app.gui
This is a wrapper class that shields Leo's core code from gui-dependent details. As described below, scripts can invoke dialogs using g.app.gui convenience methods.
commander
An instance of the Commands class in leoCommands.py. Commanders represent commands for a particular window. Each open Leo window has its own commander. By convention, any variable named c is a commander.
frame
An instance of the base leoFrame class in leoFrame.py. Frames contains all the internal data needed to manage a Leo window. Given a commander c, c.frame is commanders frame. Given a frame f, f.c is the frame's commander.
position

An instance of the position class in leoNodes.py. A position object represents the location of a particular node in a tree traversal. By convention, variables named p, p1 or p2 are positions. For any position p, p.v is the vnode at that position and p.v.t is the tnode at that position. Positions are the primary way to access data. c.currentPosition and c.rootPosition return positions. From those starting point, it is possible to access the data in any node.

Important: Positions can become invalid when the structure of the outline changes. As discussed below, plugins and scripts that store positions for use at a later time should make sure the position p is still valid by calling c.positionExists(p)

Important: For compatibility with old (pre-4.2) scripts, c.currentVnode and c.rootVnode methods return positions not vnodes. Old scripts appear to be using vnodes; in fact they are using positions. I call such scripts confused scripts. Confused scripts work because the position class is designed to make them work. We'll see how this works in detail in About copying positions. This section is supremely important.

vnode
An instance of the vnode class in leoNodes.py. vnodes represent one or more outline nodes on the screen. Normally, scripts access vnodes via the position class described below. By convention, variables named v, v1 or v2 refer to vnodes. Important: scripts normally should use positions, not vnodes.
tnode
An instance of the tnode class in leoNodes.py. tnodes represent the actual data in a vnode, including headline and body text. For any vnode v, v.t is v's tnode. Cloned vnodes v1 and v2 share the same tnode. That is v1.t == v2. Important: If p is a position, p.v.t is the tnode associated with that position. Many positions may share the same tnode.

Important: With the exception of the p.v and v.t ivars, scripts should be careful to use only the methods of the position, vnode and tnode classes rather than the internal ivars of these classes. Doing so will ensure that the script is as simple as possible and that the script will continue to work regardless of future changes to the internals of these classes.

Predefined objects

Leo's Execute Script command predefines c to be the commander of the outline containing the script. g and p are predefined as follows:

import leoGlobals as g
p = c.currentPosition()

These definitions provide an easy way to access or change any information in a Leo outline. For example, as discussed below, the following script will print every headline in the Leo outline in which the script occurs.

for z in c.allNodes_iter():
print z.headString()

g.es writes to the log pane

The g.es method prints its arguments to the Log tab of the log pane:

g.es("Hello world")

g.es converts non-string arguments using repr:

g.es(c)

g.es prints multiple arguments separated by commas:

g.es("Hello","world")

To create a tab named 'Test' or make it visible if it already exists:

c.frame.log.selectTab('Test')

When first created, a tab contains a Tk.Text widget. To write to this widget, add the tabName argument to g.es:

g.es('Test',color='blue',tabName='Test')

app.windowList: the list of all open frames

The windowlist attribute of the application instance contains the list of the frames of all open windows. The commands ivar of the frame gives the commander for that frame:

windows = g.app.windowList # get the list of all open frames.
g.es("windows...")
for f in windows:
    c = f.c # c is f's commander
    g.es(f)
    g.es(f.shortFileName())
    g.es(c)
    g.es(c.rootPosition())

Getting and setting headline and body text

Here is how to access the data of a Leo window:

g.es(p) # p is already defined.
p = c.currentPosition() # get the current position.
g.es(p)
g.es("head:",p.headString())
g.es("body:",p.bodyString())

Here is how to access data at position p. Note: these methods work whether or not p is the current position:

body = p.bodyString()
head = p.headString()
c.setBodyString(p,body) # set body text of p to body.
c.setHeadString(p,head) # set headline text of p to head.

Ensuring that positions are valid

Positions become invalid when the user deletes or moves the node to which the position refers. Plugins and scripts that store positions for use at a later time should make sure the position p is still valid by calling c.positionExists(p).

The following code will find a position p2 describing the same node as p:

if not c.positionExists(p):
    for p2 in c.allNodes_iter():
        if p2.v == p.v:
            # found
            c.selectPosition(p2)
    else:
        print 'position no longer exists'

About copying positions

Understanding this section is essential. By default, all iterators discussed below use a single position to move through the outline. This is a vital optimization; otherwise Leo would generate one or more position object for each node of a tree traversal. However, it means that it is useless to try to capture a position with:

p2 = p  # Wrong.  p2 will change after this assignment.

Instead, scripts and plugins should use p.copy() to 'capture' the value of a position:

p2 = p.copy()   # Correct: p2 will not change when p changes later.

Another way to solve this problem is to set copy=True when using an iterator:

d = {}
for p in c.allNodes_iter(copy=True):
    d[p.v.t] = p

This creates a dictionary of (unchanging!) positions, indexed via tnode. Warning The positions in this dictionary will become invalid when the outline's structure changes. It would be wrong to save a dictionary like this for use between commands.

Setting the copy=True argument to iterators is an acceptable strategy for infrequently used scripts; it is not acceptable for heavily used code in Leo's core: it would create huge numbers of positions that would immediately be candidates for garbage collection.

Important: 'Confused' scripts work because the position methods that simulate the old vnode methods automatically create copies of positions when 'moving' through an outline. Thus, confused scripts generate many more positions than would the equivalent script that uses position iterators. Such is the price of compatibility.

Traversing outlines

The proper way to traverse an outline is with an iterator. Iterators are defined only by the position class; vnodes can not have iterators because vnodes may appear in multiple places in an outline.

c.allNodes_iter

The c.allNodes_iter iterator returns a list of all positions in the outline. This script makes a list of all the nodes in an outline:

nodes = [p for p in c.allNodes_iter()]
g.es("This outline contains %d nodes" % len(nodes))

Here is one way to count the nodes of an outline:

count = 0
for p in c.allNodes_iter():
    count += 1
g.es("This outline contains %d nodes" % count)

Here is a way to count the distinct vnodes of an outline:

positions = 0 ; tnodes = {}
for p in c.allNodes_iter():
    positions += 1
    if not tnodes.get(p.v.t):
        tnodes[p.v.t] = p.v.t
g.es("%8s positions" % positions)
g.es("%8s vnodes" % len(tnodes.keys()))

p.children_iter

The p.children_iter iterator returns a list of all children of position p:

parent = p.parent()
g.es("children of %s" % parent.headString(),color="purple")
for p in parent.children_iter():
    g.es(p.headString())

p.parents_iter & p.self_and_parents_iter

The p.parents_iter iterator returns a list of all parents of position p, excluding p:

current = p.copy()
g.es("inclusive parents of %s" % current.headString(),color="purple")
for p in current.self_and_parents_iter():
    g.es(p.headString())

The p.self_and_parents_iter iterator returns a list of all parents of position p, including p:

current = p.copy()
g.es("exclusive of %s" % current.headString(),color="purple")
for p in current.parents_iter():
    g.es(p.headString())

p.siblings_iter & p.following_siblings_iter

The p.siblings_iter iterator returns a list of all siblings of position p:

current = c.currentPosition()
g.es("all siblings of %s" % current.headString(),color="purple")
for p in current.self_and_siblings_iter():
    g.es(p.headString())

The p.following_siblings_iter iterator returns a list of all siblings that follow position p:

current = c.currentPosition()
g.es("following siblings of %s" % current.headString(),color="purple")
for p in current.following_siblings_iter():
    g.es(p.headString())

p.subtree_iter & p.self_and_subtree_iter

The p.subtree_iter iterator returns a list of all positions in p's subtree, excluding p:

parent = p.parent()
g.es("exclusive subtree of %s" % parent.headString(),color="purple")
for p in parent.subtree_iter():
    g.es(p.headString())

The p.self_and_subtree_iter iterator returns a list of all positions in p's subtree, including p:

parent = p.parent()
g.es("inclusive subtree of %s" % parent.headString(),color="purple")
for p in parent.self_and_subtree_iter():
    g.es(p.headString())

Testing whether a position is valid

The tests:

if p:       # Right
if not p:   # Right

are the only correct ways to test whether a position p is valid. In particular, the following will not work:

if p is None:       # Wrong
if p is not None:   # Wrong

Visiting each node once

Joined nodes represent the same data. Joined nodes are vnodes v1 and v2 such that v1.t == v2.t. Joined vnodes are distinct (v1 != v2) if the vnodes are clones of each other. Joined nodes are in fact the same node (v1 == v2) if they are descendants of clone nodes. In particular, we can say that p1.v is joined to p2.v if p1.v.t == p2.v.t regardless of whether p1.v == p2.v. Thus a script can process nodes exactly once if it ignores nodes joined to previously visited nodes. A later section will provide an example of this common scripting pattern.

The following script illustrates a common idiom. It prints each headline of an outline, eliminating duplications that would happen as the result of cloned trees:

d = {}
for p in c.allNodes_iter():
    if p.v.t not in d:
        print p.headString()
        d[p.v.t] = p.v.t

As mentioned in the introduction, joined nodes share the same tnode. Thus, when we visit a position p we print p.headString() only if p.v.t is not already in the dictionary. We then enter p.v.t in the dictionary to prevent printing the headlines of any future node joined to p.v.

Updating the screen

You can use c.redraw_now to redraw the entire screen immediately:

c.redraw_now()

However, Leo's code redraws the screen using the following pattern:

c.beginUpdate()
try:
    << whatever >>
finally:
    c.endUpdate()

This suppresses redraws inside <<whatever>> that would otherwise be caused by c.endUpdate. c.endUpdate takes an optional argument:

c.endUpdate(flag)

redraws the screen only if flag is True. This is an important pattern. Leo uses c.beginUpdate and c.endUpdate almost everywhere to redraw the screen, so provided that << whatever >> contains no calls to c.redraw_now this pattern insures that at most one redraw occurs.

Invoking commands from scripts

Leo dispatches commands using c.doCommand, which calls the "command1" and "command2" hook routines for the given label. c.doCommand catches all exceptions thrown by the command:

c.doCommand(c.markHeadline,label="markheadline")

You can also call command handlers directly so that hooks will not be called:

c.markHeadline()

You can invoke minibuffer commands by name. For example:

c.executeMinibufferCommand('open-outline')

c.keyHandler.funcReturn contains the value returned from the command. In many cases, as above, this value is simply 'break'.

Getting settings from @settings trees

Any .leo file may contain an @settings tree, so settings may be different for each commander. Plugins and other scripts can get the value of settings as follows:

format_headlines = c.config.getBool('rst3_format_headlines')
g.es('format_headlines',format_headlines)

The c.config class has the following getters. See the configSettings in leoCommands.py for details:

c.config.getBool(settingName,default=None)
c.config.getColor(settingName)
c.config.getDirectory(settingName)
c.config.getFloat(settingName)
c.config.getInt(settingName)
c.config.getLanguage(settingName)
c.config.getRatio(settingName)
c.config.getShortcut(settingName)
c.config.getString(settingName)

These methods return None if no setting exists. The getBool 'default' argument to getBool gives the value to be returned if the setting does not exist.

You can set any existing item in an @settings tree with c.config.set(p,setting,val). For example:

for val in (False,True):
    c.config.set(p,'rst3_format_headlines',val)
    format_headlines = c.config.getBool('rst3_format_headlines')
    g.es('format_headlines',format_headlines)

c.config.set does not change the @settings tree; it simply changes the values returned by the getters.

Getting and setting preferences

Each commander maintains its own preferences. Your scripts can get the following ivars:

ivars = (
    'output_doc_flag',
    'page_width',
    'page_width',
    'tab_width',
    'tangle_batch_flag',
    'tangle_directory',
    'target_language',
    'untangle_batch_flag',
    'use_header_flag',
)

g.es("Prefs ivars...\n",color="purple")
for ivar in ivars:
    g.es(getattr(c,ivar))

If your script sets c.tab_width your script may call f.setTabWidth to redraw the screen:

c.tab_width = -4    # Change this and see what happens.
c.frame.setTabWidth(c.tab_width)

Functions for finding and changing text from scripts

The file leoFindScript.py contains functions for finding and changing text from within scripts. See leoFindScript.py in LeoPy.leo for full details.

The findall function returns a list of tuples (v,pos) describing matches in c's entire tree:

import leoFindScript

pattern="import leoGlobals as g"
result = leoFindScript.findAll(c,pattern,bodyFlag=1)

g.es("%-3d instances of: '%s'...\n" % (len(result),pattern),color="purple")

for v,pos in result:
    body = v.bodyString()
    g.es('\n%-4d %s' % (pos,v.headString()))
    g.es(g.get_line_after(body,pos))

The reFindall function returns a list of tuples (v,mo,pos), where mo is a MatchObject. The reFlags argument are flags to re.search:

import leoFindScript

pattern="from .* import"
result = leoFindScript.reFindAll(c,pattern,bodyFlag=1,reFlags=None)

g.es("%-3d instances of: '%s'...\n" % (len(result),pattern),color="purple")
for v,mo,pos in result:
    body = v.bodyString()
    g.es('\n%-4d %s' % (pos,v.headString()))
    g.es(g.get_line_after(body,pos))

Functions defined in leoGlobals.py

leoGlobals.py contains many utility functions and constants. The following script prints all the names defined in leoGlobals.py:

g.es("Names defined in leoGlobals.py",color="purple")
names = g.__dict__.keys()
names.sort()
for name in names:
    g.es(name)

Event handlers

Plugins and other scripts can register event handlers (also known as hooks) with code such as:

leoPlugins.registerHandler("after-create-leo-frame",onCreate)
leoPlugins.registerHandler("idle", on_idle)
leoPlugins.registerHandler(("start2","open2","command2"), create_open_with_menu)

As shown above, a plugin may register one or more event handlers with a single call to leoPlugins.registerHandler. Once a hook is registered, Leo will call the registered function' at the named hook time. For example:

leoPlugins.registerHandler("idle", on_idle)

causes Leo to call on_idle at "idle" time.

Event handlers must have the following signature:

def myHook (tag, keywords):
    whatever
  • tag is the name of the hook (a string).
  • keywords is a Python dictionary containing additional information. The following section describes the contents of the keywords dictionary in detail.

Important: hooks should get the proper commander this way:

c = keywords.get('c')

The following table tells about each event handler: its name, when it is called, and the additional arguments passed to the hook in the keywords dictionary. For some kind of hooks, Leo will skip its own normal processing if the hook returns anything other than None. The table indicates such hooks with 'yes' in the 'Stop?' column.

Important: Ever since Leo 4.2, the v, old_v and new_v keys in the keyword dictionary contain positions, not vnodes. These keys are deprecated. The new_c key is also deprecated. Plugins should use the c key instead.

Event name (tag argument) Stop? When called Keys in keywords dict
'after-create-leo-frame'   after creating any frame c
'after-redraw-outline'   end of tree.redraw c (note 6)
'before-create-leo-frame'   before frame.finishCreate c
'bodyclick1' yes before normal click in body c,p,v,event
'bodyclick2'   after normal click in body c,p,v,event
'bodydclick1' yes before double click in body c,p,v,event
'bodydclick2'   after double click in body c,p,v,event
'bodykey1' yes before body keystrokes c,p,v,ch,oldSel,undoType
'bodykey2'   after body keystrokes c,p,v,ch,oldSel,undoType
'bodyrclick1' yes before right click in body c,p,v,event
'bodyrclick2'   after right click in body c,p,v,event
'boxclick1' yes before click in +- box c,p,v,event
'boxclick2'   after click in +- box c,p,v,event
'clear-all-marks'   after clear-all-marks command c,p,v
'clear-mark'   when mark is set c,p,v
'close-frame'   in app.closeLeoWindow c
'color-optional-markup' yes * (note 7) colorer,p,v,s,i,j,colortag (note 7)
'command1' yes before each command c,p,v,label (note 2)
'command2'   after each command c,p,v,label (note 2)
'create-optional-menus'   (note 8) c (note 8)
'create-popup-menu-items'   in tree.OnPopup c,p,v,event (new)
'destroy-all-global-windows'   (note 12) None
'draw-outline-box' yes when drawing +- box tree,p,v,x,y
'draw-outline-icon' yes when drawing icon tree,p,v,x,y
'draw-outline-node' yes when drawing node tree,p,v,x,y
'draw-outline-text-box' yes when drawing headline tree,p,v,x,y
'drag1' yes before start of drag c,p,v,event
'drag2'   after start of drag c,p,v,event
'dragging1' yes before continuing to drag c,p,v,event
'dragging2'   after continuing to drag c,p,v,event
'enable-popup-menu-items'   in tree.OnPopup c,p,v,event
'end1'   start of app.quit() None
'enddrag1' yes before end of drag c,p,v,event
'enddrag2'   after end of drag c,p,v,event
'headclick1' yes before normal click in headline c,p,v,event
'headclick2'   after normal click in headline c,p,v,event
'headrclick1' yes before right click in headline c,p,v,event
'headrclick2'   after right click in headline c,p,v,event
'headkey1' yes before headline keystrokes c,p,v,ch (note 13)
'headkey2'   after headline keystrokes c,p,v,ch (note 13)
'hoist-changed'   whenever the hoist stack changes c
'hypercclick1' yes before control click in hyperlink c,p,v,event
'hypercclick2'   after control click in hyperlink c,p,v,event
'hyperenter1' yes before entering hyperlink c,p,v,event
'hyperenter2'   after entering hyperlink c,p,v,event
'hyperleave1' yes before leaving hyperlink c,p,v,event
'hyperleave2'   after leaving hyperlink c,p,v,event
'iconclick1' yes before single click in icon box c,p,v,event
'iconclick2'   after single click in icon box c,p,v,event
'iconrclick1' yes before right click in icon box c,p,v,event
'iconrclick2'   after right click in icon box c,p,v,event
'icondclick1' yes before double click in icon box c,p,v,event
'icondclick2'   after double click in icon box c,p,v,event
'idle'   periodically (at idle time) c
'init-color-markup'   (note 7) colorer,p,v (note 7)
'menu1' yes before creating menus c,p,v (note 3)
'menu2' yes during creating menus c,p,v (note 3)
'menu-update' yes before updating menus c,p,v
'new'   start of New command c,old_c,new_c (note 9)
'open1' yes before opening any file c,old_c,new_c,fileName (note 4)
'open2'   after opening any file c,old_c,new_c,fileName (note 4)
'openwith1' yes before Open With command c,p,v,openType,arg,ext
'openwith2'   after Open With command c,p,v,openType,arg,ext
'recentfiles1' yes before Recent Files command c,p,v,fileName,closeFlag
'recentfiles2'   after Recent Files command c,p,v,fileName,closeFlag
'redraw-entire-outline' yes start of tree.redraw c (note 6)
'save1' yes before any Save command c,p,v,fileName
'save2'   after any Save command c,p,v,fileName
'scan-directives'   in scanDirectives c,p,v,s,old_dict,dict,pluginsList (note 10)
'select1' yes before selecting a position c,new_p,old_p,new_v,new_v
'select2'   after selecting a position c,new_p,old_p,new_v,old_v
'select3'   after selecting a position c,new_p,old_p,new_v,old_v
'set-mark'   when a mark is set c,p,v
'show-popup-menu'   in tree.OnPopup c,p,v,event
'start1'   after app.finishCreate() None
'start2'   after opening first Leo window c,p,v,fileName
'unselect1' yes before unselecting a vnode c,new_p,old_p,new_v,old_v
'unselect2'   after unselecting a vnode c,new_p,old_p,old_v,old_v
'@url1' yes before double-click @url node c,p,v,url (note 5)
'@url2'   after double-click @url node c,p,v(note 5)

Notes:

  1. 'activate' and 'deactivate' hooks 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. Commands hooks now set the label for undo and redo commands 'undo' and 'redo' rather than 'cantundo' and 'cantredo'.

  3. 'menu1' hook: Setting g.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 settings, nor do they affect the 'official' command names passed in g.app.commandName. For example:

    app().realMenuNameDict['Open...'] = 'Ouvre'.
    
  4. 'open1' and 'open2' hooks: These are called with a keywords dict containing the following entries:

    • c: The commander of the newly opened window.
    • old_c: The commander of the previously open window.
    • new_c: (deprecated: use 'c' instead) The commander of the newly opened window.
    • fileName: The name of the file being opened.

    You can use old_c.currentPosition() and c.currentPosition() to get the current position in the old and new windows. Leo calls the 'open1' and 'open2' hooks only if the file is not 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. '@url1' and '@url2' hooks are only executed if the 'icondclick1' hook returns None.

  6. These hooks are useful for testing.

  7. These hooks allow plugins to parse and handle markup within doc parts, comments and Python ''' strings. Note that these hooks are not called in Python ''' strings. See the color_markup plugin for a complete example of how to use these hooks.

  8. Leo calls the 'create-optional-menus' hook when creating menus. This hook need only create new menus in the correct order, without worrying about the placement of the menus in the menu bar. See the plugins_menu and scripts_menu plugins for examples of how to use this hook.

  9. The New command calls 'new'. The 'new_c' key is deprecated. Use the 'c' key instead.

  10. g.scanDirectives calls 'scan-directives' hook. g.scanDirectives returns a dictionary, say d. d.get('pluginsList') is an a list of tuples (d,v,s,k) where:

    • d is the spelling of the @directive, without the leading @.
    • v is the vnode containing the directive, _not_ the original vnode.
    • s[k:] is a string containing whatever follows the @directive. k has already been moved past any whitespace that follows the @directive.

    See the add_directives plugins directive for a complete example of how to use the 'scan-directives' hook.

  11. g.app.closeLeoWindow calls the 'close-frame' hook just before removing the window from g.app.windowList. The hook code may remove the window from app.windowList to prevent g.app.closeLeoWindow from destroying the window.

  12. g.app.destroyAllGlobalWindows calls the 'destroy-all-global-windows' hook. This hook gives plugins the chance to clean up after themselves when Leo shuts down.

  13. New in Leo 4.4: Leo calls the 'headkey1' and 'headkey2' hooks only when the user completes the editing of a headline, and ch is always 'r', regardless of platform.

Enabling idle time event handlers

Two methods in leoGlobals.py allow scripts and plugins to enable and disable 'idle' events. g.enableIdleTimeHook(idleTimeDelay=100) enables the "idle" hook. Afterwards, Leo will call the "idle" hook approximately every idleTimeDelay milliseconds. Leo will continue to call the "idle" hook periodically until disableIdleTimeHook is called. g.disableIdleTimeHook() disables the "idle" hook.

How to make operations undoable

Plugins and scripts should call u.beforeX and u.afterX methods ato describe the operation that is being performed. Note: u is shorthand for c.undoer. Most u.beforeX methods return undoData that the client code merely passes to the corresponding u.afterX method. This data contains the 'before' snapshot. The u.afterX methods then create a bead containing both the 'before' and 'after' snapshots.

u.beforeChangeGroup and u.afterChangeGroup allow multiple calls to u.beforeX and u.afterX methods to be treated as a single undoable entry. See the code for the Change All, Sort, Promote and Demote commands for examples. The u.beforeChangeGroup and u.afterChangeGroup methods substantially reduce the number of u.beforeX and afterX methods needed.

Plugins and scripts may define their own u.beforeX and afterX methods. Indeed, u.afterX merely needs to set the bunch.undoHelper and bunch.redoHelper ivars to the methods used to undo and redo the operation. See the code for the various u.beforeX and afterX methods for guidance.

p.setDirty and p.setAllAncestorAtFileNodesDirty now return a dirtyVnodeList that all vnodes that became dirty as the result of an operation. More than one list may be generated: client code is responsible for merging lists using the pattern dirtyVnodeList.extend(dirtyVnodeList2)

See the section << How Leo implements unlimited undo >> in leoUndo.py for more details. In general, the best way to see how to implement undo is to see how Leo's core calls the u.beforeX and afterX methods.

Redirecting output from scripts

leoGlobals.py defines 6 convenience methods for redirecting stdout and stderr:

g.redirectStderr() # Redirect stderr to the current log pane.
g.redirectStdout() # Redirect stdout to the current log pane.
g.restoreStderr()  # Restores stderr so it prints to the console window.
g.restoreStdout()  # Restores stdout so it prints to the console window.
g.stdErrIsRedirected() # Returns True if the stderr stream is redirected to the log pane.
g.stdOutIsRedirected() # Returns True if the stdout stream is redirected to the log pane.

Calls need not be paired. Redundant calls are ignored and the last call made controls where output for each stream goes. Note: you must execute Leo in a console window to see non-redirected output from the print statement:

print "stdout isRedirected:", g.stdOutIsRedirected()
print "stderr isRedirected:", g.stdErrIsRedirected()

g.redirectStderr()
print "stdout isRedirected:", g.stdOutIsRedirected()
print "stderr isRedirected:", g.stdErrIsRedirected()

g.redirectStdout()
print "stdout isRedirected:", g.stdOutIsRedirected()
print "stderr isRedirected:", g.stdErrIsRedirected()

g.restoreStderr()
print "stdout isRedirected:", g.stdOutIsRedirected()
print "stderr isRedirected:", g.stdErrIsRedirected()

g.restoreStdout()
print "stdout isRedirected:", g.stdOutIsRedirected()
print "stderr isRedirected:", g.stdErrIsRedirected()

Writing to different log tabs

Plugins and scripts can create new tabs in the log panel. The following creates a tab named test or make it visible if it already exists:

c.frame.log.selectTab('Test')

g.es, g.enl, g.ecnl, g.ecnls write to the log tab specified by the optional tabName argument. The default for tabName is 'Log'. The put and putnl methods of the tkinterLog class also take an optional tabName argument which defaults to 'Log'.

Plugins and scripts may call the c.frame.canvas.createCanvas method to create a log tab containing a Tk.Canvas widget. Here is an example script:

log = c.frame.log ; tag = 'my-canvas'
w = log.canvasDict.get(tag)
if not w:
    w = log.createCanvas(tag)
    w.configure(bg='yellow')
log.selectTab(tag)

Invoking dialogs using the g.app.gui class

Scripts can invoke various dialogs using the following methods of the g.app.gui object. Here is a partial list. You can use typing completion(default bindings: Alt-1 and Alt-2) to get the full list!

g.app.gui.runAskOkCancelNumberDialog(c,title,message)
g.app.gui.runAskOkCancelStringDialog(c,title,message)
g.app.gui.runAskOkDialog(c,title,message=None,text='Ok')
g.app.gui.runAskYesNoCancelDialog(c,title,message=None,
    yesMessage='Yes',noMessage='No',defaultButton='Yes')
g.app.gui.runAskYesNoDialog(c,title,message=None)

The values returned are in ('ok','yes','no','cancel'), as indicated by the method names. Some dialogs also return strings or numbers, again as indicated by their names.

Scripts can run File Open and Save dialogs with these methods:

g.app.gui.runOpenFileDialog(title,filetypes,defaultextension,multiple=False)
g.app.gui.runSaveFileDialog(initialfile,title,filetypes,defaultextension)

For details about how to use these file dialogs, look for examples in Leo's own source code. The runOpenFileDialog returns a list of file names.

Inserting and deleting icons

You can add an icon to the presently selected node with c.editCommands.insertIconFromFile(path). path is an absolute path or a path relative to the leo/Icons folder. A relative path is recommended if you plan to use the icons on machines with different directory structures.

For example:

path = 'rt_arrow_disabled.gif'
c.editCommands.insertIconFromFile(path)

Scripts can delete icons from the presently selected node using the following methods:

c.editCommands.deleteFirstIcon()
c.editCommands.deleteLastIcon()
c.editCommands.deleteNodeIcons()

Customizing panes with different widgets

Tk/Tkinter make it easy to customize the contents of any of Leo's panes. The following sections will discuss the 'official' ivars that make it possible for scripts to access and alter the contents of panes. The next three sections will give examples of modifying each pane.

Official ivars

The c.frame.log class contains the following 'official' ivars:

g.es('tabName',c.frame.log.tabName)     # The name of the active tab.
g.es('tabFrame',c.frame.log.tabFrame)   # The Tk.Frame containing all the other widgets of the tab.
g.es('logCtrl',c.frame.log.logCtrl)     # Tk.Text widget containing the log text.

The following ivars provide access to the body pane:

g.es('bodyFrame',c.frame.body.frame)    # The Tk.Frame widget containing the c.frame.body.bodyCtrl

The following ivars provide access to the outline pane:

g.es('canvas',c.frame.tree.canvas) # The Tk.Canvas on which Leo's outline is drawn.

Tkinter provides a way of determining the enclosing widget of any widget. The body text is enclosed in a Pmw.PanedWidget to support multiple editors.

w = c.frame.body.bodyCtrl parent = w.pack_info().get('in') g.es('bodyCtrl.parent',parent) # The Tk.Frame containing the body text.

Common operations on Tk.Text widgets

The following is no substitute for a full discussion of programming the Tk.Text widget: it can do lots.

To clear the log:

w = c.frame.log.logCtrl
w.delete('1.0','end')

To write a line to the end of the log:

w = c.frame.log.logCtrl
w.insert('end','This is a test\n')

To get the entire contents of the log:

w = c.frame.log.logCtrl
g.es(w.get('1.0','end')+'\n')

Customizing the log pane

The following line removes the initial text widget:

c.frame.log.logCtrl.pack_forget()

To make the text widget visible again:

c.frame.log.logCtrl.pack(side='top',expand=1,fill='both')

Plugins and scripts can pack any other widgets into c.frame.log.tabFrame. For example, the following replaces the default text widget with a red box:

import Tkinter as Tk

# Remove the old contents.
w = c.frame.log.logCtrl
parent =  w.pack_info().get('in')
w.pack_forget()

# Replace with a red frame.
f = c.frame.newLog = Tk.Frame(parent,background='red')
f.pack(side='left',expand=1,fill='both')

And the following will restore the original pane:

c.frame.newLog.pack_forget()
w = c.frame.log.logCtrl
w.pack(side='left',expand=1,fill='both')

Customizing the body pane

Warning: you will find it hard to execute scripts after removing the body pane, so you had best make the following two scripts into script buttons before executing them :-)

Plugins and scripts can pack any other widgets into c.frame.log.tabFrame. For example, the following replaces the default text widget with a red box:

import Tkinter as Tk

w = c.frame.body.bodyCtrl
parent =  w.pack_info().get('in')
w.pack_forget()

f = c.frame.newBody = Tk.Frame(parent,background='red')
f.pack(side='left',expand=1,fill='both')

To restore:

c.frame.newBody.pack_forget()
w = c.frame.body.bodyCtrl
w.pack(side='left',expand=1,fill='both')

Customizing the outine pane

The following replaces the outline pane with a red frame:

import Tkinter as Tk

w = c.frame.tree.canvas
parent =  w.pack_info().get('in')
w.pack_forget()

f = c.frame.newTree = Tk.Frame(parent,background='red')
f.pack(side='left',expand=1,fill='both')

And this script restores the outline:

c.frame.newTree.pack_forget()
c.frame.tree.canvas.pack(side='left',expand=1,fill='both')

Summary of the vnode and position classes

Most scripts will use methods of the position class to access information in an outline. The following sections summarizes the most useful methods that your scripts can use. For a complete list, see the leoNodes.py in of LeoPy.leo.

Iterators

Iterators exist only in the position class:

c.allNodes_iter            # returns all positions in c's outline.
p.children_iter            # returns all children of p.
p.parents_iter             # returns all parents of p.
p.self_and_parents_iter    # returns p and all parents of p.
p.siblings_iter            # returns all siblings of p, including p.
p.following_siblings_iter  # returns all siblings following p.
p.subtree_iter             # returns all positions in p's subtree, excluding p.
p.self_and_subtree_iter    # returns all positions in p's subtree, including p.

Getters

Here are the most useful getters of the vnode and position classes.

Returning strings:

p.bodyString() # the body string of p.
p.headString() # the headline string of p.

Returning ints:

p.childIndex()
p.numberOfChildren()
p.level()

Returning bools representing property bits:

p.hasChildren()
p.isAncestorOf(v2) # True if v2 is a child, grandchild, etc. of p.
p.isCloned()
p.isDirty()
p.isExpanded()
p.isMarked()
p.isVisible()
p.isVisited()

Setters

Here are the most useful setters of the Commands and position classes. The following setters of the position class regardless of whether p is the presently selected position:

c.setBodyString(p,s)  # Sets the body text of p.
c.setHeadString(p,s)  # Sets the headline text of p.

Moving nodes:

p.moveAfter(v2)           # move p after v2
p.moveToNthChildOf(v2,n)  # move p to the n'th child of v2
p.moveToRoot(oldRoot)     # make p the root position.
                          # oldRoot must be the old root position if it exists.

The "visited" bit may be used by commands or scripts for any purpose. Many commands use this bits for tree traversal, so these bits do not persist:

c.clearAllVisited() # Clears all visited bits in c's tree.
p.clearVisited()
p.setVisited()

Creating script buttons

Creating a script button should be your first thought whenever you want to automate any task. The scripting plugin, mod_scripting.py, puts two buttons in the icon menu, a pink Run Script button and a yellow Script Button button. The Run Script button does the same thing as the Execute Script command. The Script Button button is the interesting one. It creates a button, confusingly called a script button in the icon area. A script button executes a script when you push it.

Suppose node N is selected. When you press the Script Button button a new (pink) script button is created. The name of the new button is N's headline text. The script associated with the new button is N's body text. Now whenever you press the new button, N's script is executed on the presently selected node. Script buttons are extraordinarily useful. In effect, each script button defines an instant command! For example, sometimes my fingers get tired of saving a file. I simply put Save in a node's headline and c.save() in the body text. I hit the Script Button button and I get a new button called Save that will save the outline when I press it.

Here's a more interesting example. The following script searches the present node and its ancestors looking for an @rst node. When such a node is found the script calls the rst3 plugin to format it. I don't have to select the actual @rst node; I can select any of its children:

import leoPlugins
rst3 = leoPlugins.getPluginModule('rst3')
if rst3: # already loaded.
    controller = rst3.controllers.get(c)
    if controller:
        for p in p.self_and_parents_iter():
            if p.headString().startswith('@rst '):
                controller.processTree(p)
                break
else: # Just load the plugin.
    rst3 = leoPlugins.loadOnePlugin('rst3',verbose=True)
    if rst3:
        g.es('rst3 loaded')
        rst3.onCreate('tag',{'c':c})
    else:
        # Ask to be removed.
        g.app.scriptDict['removeMe'] = True

Notes:

  • The scripting plugin pre-defines the c, g and p symbols just as the Execute Script command does.
  • By default a script button executes the present body text of the node that original created the script button. This is very handy: you can modify a script button's script at any time without having to recreate the script button.
  • You can delete any script button by right-clicking on it.
  • On startup, the scripting plugin scans the entire .leo file and creates a script button for every node whose headline starts with @button scriptName. Warning: this is indeed a security risk of the kind discussed later. This feature can be disabled by setting atButtonNodes = True at the start of mod_scripting.py.

Running Leo in batch mode

On startup, Leo looks for two arguments of the form:

--script scriptFile

If found, Leo enters batch mode. In batch mode Leo does not show any windows. Leo assumes the scriptFile contains a Python script and executes the contents of that file using Leo's Execute Script command. By default, Leo sends all output to the console window. Scripts in the scriptFile may disable or enable this output by calling app.log.disable or app.log.enable

Scripts in the scriptFile may execute any of Leo's commands except the Edit Body and Edit Headline commands. Those commands require interaction with the user. For example, the following batch script reads a Leo file and prints all the headlines in that file:

path = r"c:\prog\leoCVS\leo\test\test.leo"

g.app.log.disable() # disable reading messages while opening the file
flag,newFrame = g.openWithFileName(path,None)
g.app.log.enable() # re-enable the log.

for p in newFrame.c.allNodes_iter():
    g.es(g.toEncodedString(p.headString(),"utf-8"))

back leo next