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.
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.
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()
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')
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())
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.
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'
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.
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.
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()))
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())
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())
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())
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())
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
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.
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.
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'.
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.
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)
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))
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)
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:
'activate' and 'deactivate' hooks have been removed because they do not work as expected.
'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'.
'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'.
'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.
'@url1' and '@url2' hooks are only executed if the 'icondclick1' hook returns None.
These hooks are useful for testing.
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.
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.
The New command calls 'new'.
The 'new_c' key is deprecated. Use the 'c' key instead.
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.
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.
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.
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.
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.
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.
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()
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)
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.
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()
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.
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.
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')
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')
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')
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')
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 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.
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()
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()
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"))
