#@+leo-ver=4-thin #@+node:edream.110203113231:@thin pluginsNotes.txt #@@nocolor #@+all #@+node:edream.110203113231.3:Documentation and security warnings @nocolor This file contains code for all plugins distributed with Leo. #@nonl #@+node:edream.110203113231.4:Overview of plugins and hooks @nocolor What is a plugin? A plugin is a .py file that appears in Leo's plugin subdirectory. Leo tries to import all such files while starting up. This is done at "start1" time, that is, in the call to doHook("start1") in the run function in leo.py. Plugins exist to define "hook code". What is hook code? Leo automatically calls hook code at various times during Leo's execution. Typical hooks are: - "start2", called at the end of Leo's startup process, - "command1", called before executing any of Leo's commands - "idle", called at Tk's idle time, when nothing else is happening, See the documentation for hooks in this file for full details. N.B. Plugins can use "start2" hooks to override any of Leo's classes. See "Overriding functions, methods & classes" in leoPlugins.leo for several examples of how to do this. How do plugins work? Plugins register themselves when Leo imports them using the following code, which should appear as the outer level of the .py file: @color """A description of this plugin""" leoPlugins.registerHandler("xxx", onXXX) __version__ = "1.2" g.plugin_signon(__name__) @nocolor Line by line: 1) """A description of this plugin""" This is a Python doc string describing the plugin. This string becomes the value of the __name__ attribute for the module (.py file). 2) leoPlugins.registerHandler("xxx", onXXX) This line registers a handler called onXXX for the hook called "xxx". See the documentation for hooks for a complete list of hook names that you can use instead of "xxx". onXXX is the name of a function or method, presumably defined in the plugin file, that Leo will call at "xxx" time. For example: leoPlugins.registerHandler("idle",onIdle) will cause Leo to call the onIdle function at "idle" time. You can pass a list of hook names as the first argument to leoPlugins.registerHandler. For example: leoPlugins.registerHandler(("bodykey1","bodykey2","headkey1","headkey2"), onKey) 3) __version__ = "1.2" This assigns a version attribute to module. At present this attribute is used by the plugin code that creates Leo's Plugins menu. 4) g.plugin_signon(__name__) This line increases the count of loaded modules. The present code for plugin_signon does not actually print separate signons--it seems to verbose. About hook handlers See the "About hooks" section of leoPlugins.leo for full documentation about how to write hook handler routines, like onXXX or onKey in the examples above. Basically, each hook should have this pattern: @color def onXXX (tag,keywords): c = keywords.get("c") otherKeyword = keywords.get("otherKeyword") << do something with c and otherKeyword, etc. >> @nocolor In other words, keywords is a Python dictionary containing the keyword arguments passed to the hook handler. See the See the "About hooks" section of leoPlugins.leo for a list of the keywords passed to each hook handler. The tag is the name of the hook. For example, tag would one of "bodykey1","bodykey2","headkey1","headkey2", for the onKey hook handler in the example above. What's next? It's one thing to know how to create a plugin. It's another to know how to actually change Leo. Obviously, some study is needed. The place to start your study is LeoPy.leo. In particular, study very carefully the section called "Overview of code". Leo is a highly modular program, and Python is ideally suited to this style of programming. #@nonl #@-node:edream.110203113231.4:Overview of plugins and hooks #@+node:edream.110203113231.5:Intro to scripts @nocolor Scripting is fully documented in Leo's Users Guide, and the following should be enough to get you started: @color top() # The commander of the top (current) windows. top().rootVnode() # The root vnode of the outline. top().currentVnode() # The presently selected vnode. @nocolor If v is any vnode: @color v.headString() # is the headline of v. v.bodyString() # is the body of v. v.threadNext() # is node after v in outline order. @nocolor For example, this prints every headline of an outline: @color v = top().rootVnode() while v: print v.headString() v = v.threadNext() #@-node:edream.110203113231.5:Intro to scripts #@+node:edream.110203113231.6:About hooks At startup time Leo looks all files of the form mod_*.py in the plugins directory. Leo assumes that these files are "plugins" containing Python modules that customizes Leo's operation. Leo attempts to import each file. Each module should register routines that are called at various times during execution. Such times are identified by strings known as "tags". The code in in one or more plugins corresponding to each tag are known as the "hook" routines for that tag. Leo catches exceptions (including syntax errors) in hook routines, so it is safe to hack away on this code. Leo passes two argument to all hook routines: the tag and a keywords dictionary containing additional information. For example, keywords["label"] indicates the kind of command for "command1" and "command2" hooks. 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 hooks: ( Overrides is "yes" if returning anything other than None overrides Leo's normal command or event processing.) tag argument keys in keywords (hook name) overrides when called dictionary argument --------- --------- ----------- ------------------- "bodyclick1" yes before normal click in body c,v,event "bodyclick2" after normal click in body c,v,event "bodydclick1" yes before double click in body c,v,event "bodydclick2" after double click in body c,v,event "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 "bodyrclick2" after right click in body c,v,event "boxclick1" yes before click in +- box c,v,event "boxclick2" after click in +- box c,v,event "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 "drag2" after start of drag c,v,event "dragging1" yes before continuing to drag c,v,event "dragging2" after continuing to drag c,v,event "end1" start of g.app.quit() "enddrag1" yes before end of drag c,v,event "enddrag2" after end of drag c,v,event "headkey1" yes before headline keystrokes c,v,ch "headkey2" after headline keystrokes c,v,ch "headclick1" yes before normal click in headline c,v,event "headclick2" after normal click in headline c,v,event "headrclick1" yes before right click in headline c,v,event "headrclick2" after right click in headline c,v,event "hypercclick1" yes before control click in hyperlink c,v,event "hypercclick2" after control click in hyperlink c,v,event "hyperenter1" yes before entering hyperlink c,v,event "hyperenter2" after entering hyperlink c,v,event "hyperleave1" yes before leaving hyperlink c,v,event "hyperleave2" after leaving hyperlink c,v,event "iconclick1" yes before single click in icon box c,v,event "iconclick2" after single click in icon box c,v,event "iconrclick1" yes before right click in icon box c,v,event "iconrclick2" after right click in icon box c,v,event "icondclick1" yes before double click in icon box c,v,event "icondclick2" after double click in icon box c,v,event "idle" periodically (at idle time) c,v "menu1" yes before creating menus c,v (note 3) "menu2" yes before updating menus c,v "new" no during New command old_c,new_c "open1" yes before opening any file old_c,new_c,fileName (note 4) "open2" after opening any file old_c,new_c,fileName (note 4) "openwith1" yes before Open With command c,v,openType,arg,ext "openwith2" after Open With command c,v,openType,arg,ext "recentfiles1" yes before Recent Files command c,v,fileName,closeFlag "recentfiles2" after Recent Files command c,v,fileName,closeFlag "save1" yes before any Save command c,v,fileName "save2" after any Save command c,v,fileName "select1" yes before selecting a vnode c,new_v,old_v "select2" after selecting a vnode c,new_v,old_v "start1" no after g.app.finishCreate() "start2" after opening first Leo window c,v,fileName "unselect1" yes before unselecting a vnode c,new_v,old_v "unselect2" after unselecting a vnode c,old_v,old_v "@url1" yes before double-click @url node c,v (note 5) "@url2" after double-click @url node c,v (note 5) The following hooks are a new breed of hook, called "stub hooks". Conceptually, these hooks are like calls to stub routines that can be filled in by code in plugins. tag argument keys in keywords (hook name) overrides when called dictionary argument --------- --------- ----------- ------------------- "create-optional-menus" no (note 8) (note 8) "draw-outine-box" yes start of drawBox tree,p,v,x,y (note 6) "draw-outline-icon" yes start of tree.drawIcon tree,p,v,x,y (note 6) "draw-outline-node" yes start of tree.drawNode tree,p,v,x,y (note 6) "draw-outline-text-box" yes start of tree.drawText tree,p,v,x,y (note 6) "draw-sub-outline" yes start of tree.drawTree tree,p,v,x,y,h,level,hoistFlag(note 6) "redraw-entire-outline" yes start of tree.redraw c (note 6) "color-optional-markup" yes * (note 7) colorer,v (note 7) "init-color-markup" no (note 7) colorer,v,s,i,j,colortag (note 7) "new" no start of New command old_c,new_c (note 9) "scan-directives" no in scanDirectives c,v,s,old_dict,dict,pluginsList (note 10) 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. 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 in leoConfig.txt, nor do they affect the "official" command names passed in g.app().commandName. For example, suppose you set g.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) "@url1" and "@url2" hooks are only executed if "icondclick1" hook returns None. (6) These stub hooks allow plugins to revise or completely replace how Leo draws outlines. For example, you could change tree.drawIcon to add additional icons. These stub hooks are really methods of the leoTree class, with the "tree" keyword corresponding to the "self" parameter. These stub hooks are called at the beginning of the indicated method. See the method themselves for a description of the paramters. (7) These stub hooks allow plugins to parse and handle markup withing 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) The "create-optional-menus" stub hook is called when Leo is creating menus, at the place at which creating new menus is appropriate. Therefore, 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. Leo now executes hooks in alphabetical order, so that the Plugins menu will be created before the Scripts menu. (9) The "new" stub hook is called at the beginning of the code that implements the New command. This provides a much easier way of recognizing when Leo is about to create a new Leo window. (10) The "scan-directives" stub hooks is called inside the scanDirectives global function. The dictionary returned by scanDirectives now contains an item whose key is "pluginsList". The value of this item is 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" stub hook. #@nonl #@-node:edream.110203113231.6:About hooks #@+node:edream.110203113231.7:Hooks should never blindly Python scripts Naively using hooks can expose you and your .leo files to malicious attacks. ** Hooks should never blindly execute Python scripts in .leo files. It is safe to import and execute code from Leo itself, provided that you got Leo from Leo's SourceForge site. #@nonl #@-node:edream.110203113231.7:Hooks should never blindly Python scripts #@+node:edream.110203113231.8:NEVER use this kind of code in a hook!! @color @ WARNING ** Using the following routine exposes you 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 if 0: # WRONG: This blindly execute scripts found in an .leo file! NEVER DO THIS! 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' in {} except: es_exception() v = v.threadNext() #@nonl #@-node:edream.110203113231.8:NEVER use this kind of code in a hook!! #@+node:ekr.20040401054523:New coding conventions for plugins @nocolor Leo 4.2 uses new coding conventions internally. I recommend using the new coding conventions in all plugins, although the old way will work inside LeoPlugins.leo. The new way replaces: from leoPlugins import * from leoGlobals import * with: import leoPlugins import leoGlobals as g This removes a major source of namespace pollution and it makes clear where all functions are defined. To access functions in leoGlobals.py you must preceed the names of such functions with g. N.B. script in the node called "Script to find and replace all functions in leoGlobals.py" does this for you. This script make replacements only in a named, top-level subtree. To change the name of the subtree change the name of the argument in the statement: p = g.findTopLevelNode("New or improved in 4.2") Besides changing the imports as shown above, you should do the following: 1. Change app or app() to g.app 2. Change registerHandler(...) to leoPlugins.registerHandler(...) 3. The new standard for importing and using Tkinter is as follows: try: import Tkinter as Tk except ImportError: Tk = None if Tk: if g.app.gui is None: g.app.createTkGui(__file__) if g.app.gui.guiName() == "tkinter": leoPlugins.registerHandler(...) g.plugin_signon(__name__) This means that the code would refer to Tk.widget instead of Tkinter.widget. Also, please avoid the use of the completely useless Tk constants such as Tk.RIGHT, Tk.TOP. Use "right" or "top" instead. Typical calls to functions in leoGlobals.py include the following: g.plugin_signon(__name__) g.es(...) # etc. Please use @file trees unless you _must_ use an @root tree for some reason. Please put each class, method and function in a separate node. #@nonl #@-node:ekr.20040401054523:New coding conventions for plugins #@-node:edream.110203113231.3:Documentation and security warnings #@+node:EKR.20040517074402:Diary #@+node:ekr.20040119102021:2004 #@+node:EKR.20040602113117:June 2, 2004: registering an "idle" event now automatically calls g.enableIdleTimeHook #@-node:EKR.20040602113117:June 2, 2004: registering an "idle" event now automatically calls g.enableIdleTimeHook #@+node:ekr.20040325075338:March 2004 #@+node:ekr.20040325075338.1:3/25: revised for 4.2 code base - Changed g.app() to g.app. - Changed scanDirectives(c,v=whatever) to scanDirectives(c,p=whatever) This is a reversion somehow. I need to rerun the script that converts to the "import leoGlobals as g" code style. #@nonl #@-node:ekr.20040325075338.1:3/25: revised for 4.2 code base #@-node:ekr.20040325075338:March 2004 #@+node:ekr.20040201061422:February 2004 #@+node:ekr.20040226104623:2/26 Inserted @language python in all plugins - Except @file-nosent. #@-node:ekr.20040226104623:2/26 Inserted @language python in all plugins #@+node:ekr.20040201061422.1:2/1 #@+node:ekr.20040201061422.2:Created status_line.py plugin This creates a status area at the bottom of the Leo Window. Plugins may write to this area using the status area convenience routines in leoTkinterFrame.py. #@nonl #@-node:ekr.20040201061422.2:Created status_line.py plugin #@-node:ekr.20040201061422.1:2/1 #@-node:ekr.20040201061422:February 2004 #@+node:ekr.20040130165735:1/30 Fixed crashers in open_shell.py #@-node:ekr.20040130165735:1/30 Fixed crashers in open_shell.py #@+node:ekr.20040129151121:1/29 Activate xemacs plugin only on double-clicks #@-node:ekr.20040129151121:1/29 Activate xemacs plugin only on double-clicks #@+node:ekr.20040119102021.1:1/19 Added http plugin #@-node:ekr.20040119102021.1:1/19 Added http plugin #@-node:ekr.20040119102021:2004 #@+node:ekr.20040119102021.2:2003 #@+node:edream.031217094146:12/17 #@+node:edream.031216103123:Fix problems in plugins @nocolor http://sourceforge.net/forum/message.php?msg_id=2336153 By: dsalomoni - redefine_put.py: Traceback (most recent call last): File "/user/davides/Software/leo-4.1-rc1/src/leoGlobals.py", line 1977, in doHook return g.app.hookFunction(tag,keywords) File "/user/davides/Software/leo-4.1-rc1/src/leoPlugins.py", line 27, in doPlugins return doHandlersForTag(tag,keywords) File "/user/davides/Software/leo-4.1-rc1/src/leoPlugins.py", line 94, in doHandlersForTag ret = handle_fn(tag,keywords) File "/user/davides/Software/leo-4.1-rc1/plugins/redefine_put.py", line 22, in onStart funcToMethod(newPut,leoFrame.LeoFrame,"put") AttributeError: 'module' object has no attribute 'LeoFrame' ** Changed LeoFrame to leoTkinterLog - trace_gc.py: Traceback (most recent call last): File "/user/davides/Software/leo-4.1-rc1/src/leoGlobals.py", line 1977, in doHook return g.app.hookFunction(tag,keywords) File "/user/davides/Software/leo-4.1-rc1/src/leoPlugins.py", line 27, in doPlugins return doHandlersForTag(tag,keywords) File "/user/davides/Software/leo-4.1-rc1/src/leoPlugins.py", line 94, in doHandlersForTag ret = handle_fn(tag,keywords) File "/user/davides/Software/leo-4.1-rc1/plugins/trace_gc.py", line 17, in printIdleGC global count ; count += 1 NameError: global name 'count' is not defined ** Used gcCount (in plugin) rather than count in leoGlobals.py. #@nonl #@-node:edream.031216103123:Fix problems in plugins #@-node:edream.031217094146:12/17 #@+node:edream.120603100522:12/6/03 Removed compile-time enable/disable code leoPlugins.py now enables plugins using pluginsManager.txt. #@nonl #@-node:edream.120603100522:12/6/03 Removed compile-time enable/disable code #@+node:edream.112103192146:11/21 #@+node:edream.112303075625:Added wxVersion to signon #@-node:edream.112303075625:Added wxVersion to signon #@+node:edream.112103192146.1:Fixed and test vim plugin #@-node:edream.112103192146.1:Fixed and test vim plugin #@-node:edream.112103192146:11/21 #@+node:edream.111903190444:11/19 #@+node:edream.111903190444.1:Fixed reorg bugs in plugin_menu and spelli checking plugins #@-node:edream.111903190444.1:Fixed reorg bugs in plugin_menu and spelli checking plugins #@+node:edream.111903105454:Moved all "protected" @others inside "if tkinter" statement #@-node:edream.111903105454:Moved all "protected" @others inside "if tkinter" statement #@-node:edream.111903190444:11/19 #@+node:edream.111803100856:11/18 Added new version of rst plugin from Timo Honkasalo #@-node:edream.111803100856:11/18 Added new version of rst plugin from Timo Honkasalo #@+node:edream.111603130528:11/17 A number of changes made to accomodate the new reorg... #@nonl #@-node:edream.111603130528:11/17 #@+node:edream.110703035301:11/7 Corrected crashes in the following plugins due to g.app.gui reorg: - script_io_to_body.py - mod_spelling.py Changed several other plugins so they load only if g.app.gui.guiName == "tkinter". Converted all .ini files to @silentfile #@nonl #@-node:edream.110703035301:11/7 #@+node:edream.110203113231.1:10/27/02 Updated color_markup.py to use c.body.bodyCtrl. #@nonl #@-node:edream.110203113231.1:10/27/02 #@+node:edream.110203113231.2:10/26/03 - Made open_with.py gui-independent by calling g.app.gui dialog routines. - Updated nav_buttons.py to use new tkinter class: from leoTkinterDialog import tkinterListBoxDialog #@nonl #@-node:edream.110203113231.2:10/26/03 #@-node:ekr.20040119102021.2:2003 #@-node:EKR.20040517074402:Diary #@+node:edream.110203113231.9:Unfinished projects #@+node:edream.110203113231.10:(Settings menu) #@+node:edream.110203113231.11:To do @nocolor Allow different default types for each section. Create _all_ menus from settings ? [plugins] pluginName = 1/0 # enables or disables plugin # option = type (overrides defaults) [types] __default_type = bool page_width = int tab_width = int default_tangle_directory = string use type name in option? _color: color _font: font _key: keystroke _flag: bool everything else: bool bool: checkmark color: picker (or use dialog) [menus] child = parent #@nonl #@-node:edream.110203113231.11:To do #@+node:edream.110203113231.12:Design of Settings menu #@-node:edream.110203113231.12:Design of Settings menu #@+node:edream.110203113231.13:settings_menu.py """Create a settings menu to replace LeoConfig.leo""" import leoGlobals as g import leoPlugins try: import Tkinter as Tk except ImportError: Tk = None import leoApp,leoAtFile,leoTkinterDialog,leoFileCommands,leoFrame,leoNodes if Tk and 0: # Register the handlers... settingsMenu = None @others # leoPlugins.registerHandler("start1",onAfterFinishCreate) leoPlugins.registerHandler("create-optional-menus",createSettingsMenu) __version__ = "0.1" g.plugin_signon(__name__) #@nonl #@+node:edream.110203113231.14:createSettingsMenu def createSettingsMenu (tag,keywords): c = keywords.get("c") global settingsMenu settingsMenu = c.frame.createNewMenu("&Settings") #@nonl #@-node:edream.110203113231.14:createSettingsMenu #@-node:edream.110203113231.13:settings_menu.py #@-node:edream.110203113231.10:(Settings menu) #@+node:edream.112003032318:About html plugin @nocolor http://sourceforge.net/forum/message.php?msg_id=2295621 By: billp9619 As I was explaining my ideas about opml it occurred to me that it uses DIV tags and a Javascript as the output of the stylesheet. But then that is what ALL the HTML collapsing outlines use! So the html behavior is nothing special to opml...it is a behavior of scripted DIV tags in html. I found a style sheet named outline_leo,xsl on my computer. This is really tiny and works pretty good. Don't know where I got it from though. Anyway, as I expected the insertion of a html FORM and a TEXTAREA box into one of these DIV tags would collapse out of sight then reappear when clicked just like any other node.. Actually this is one of those odd things that maybe noone ever thought of trying,,,,I've never seen it in one of these html outlines before. Interestingly, the familiar collapsing xml display in Internet Explorer is really just a xsl stylesheet and produces DIV tags and a script like all these other html outlines. Somewhere I found a version with minor revisions to make it work with standard xpath. There is a program called xpathvisualizer that runs in IE and gives basically the same xml display but highlights an inputted xpath expression. With this you can view source and see the script and DIV tags. At the end of the day the only benefit to opml is that others are using it (standard?) and perhaps there is something especially good about its Javascript implementation. BTW, to load leo into internet explorer just save a copy with an .xml file extension. Then drag it to IE. And of course the file could have an embedded xml-stylesheet instruction to use leo_outline.xsl which just shows headlines, etc, Also BTW... I have been using Python for some time now but I don't know if i can handle plug-in development at this stage. :-) (But I might be up to refining the html/script environment further. ) #@nonl #@-node:edream.112003032318:About html plugin #@+node:ekr.20040331072431:Custom @file plugin (prototype) #@+node:ekr.20040331072431.1:Documentation @nocolor #@nonl #@+node:ekr.20040331072431.2:Summary of the problem Leo's @file concept is very powerful but there are numerous occasions when the user wants to read and write outlines in specialized formats which cannot be handled using the @comment, @first etc directives. Examples would be, - exporting to restructured text (where additional markup steps are required - exporting to MS Word - custom file formats/XML which require restructuring before input/output Currently there exist two approaches to get around this, 1. Use tangle_done (or other script) to post-process the file - not very efficient since all knowledge of the structure of the outline has to be reinvented by the tangle_done script 2. Use a plugin (eg rst or mod_wordexport) - more efficient but still not ideal since each plugin writer ends up spinning his/her own node traversal scheme and strategy for handling directives, <> etc #@-node:ekr.20040331072431.2:Summary of the problem #@+node:ekr.20040331072431.3:Proposed solution The solution proposed here is to allow handlers to register themselves to hook into the process of deriving/underiving a file. Handlers would be dispatched on a file type basis (determined by the file extension) and would be able to take over all or part of the (un)derivation process. The tricky part of the proposal is to design the hooks to allow the user enough control to completely redefine the format of the derived file whilst not requiring them to rewrite the hard parts of the @file code. Based on the proof-of-concept example presented here, the following hooks may be sufficient for writing, 1. start (begining to derive a file) 2. body (complete handling of the body) 3. sentinel (handle a sentinel) 4. line (handle a line or part of a line) 5. startnode (when a new node is being processed) 6. endnode (the node and all its children have been processed) 7. end (ready to write the file) At this time it is not clear whether hooking the read code is actually the best way to go - it may be that the handler would have to override the entire read process. #@nonl #@-node:ekr.20040331072431.3:Proposed solution #@+node:ekr.20040331072431.4:Proof-of-concept A proof-of-concept has been built to try out the feasibility on the writing side. The example chosen is to write a FreeMind file. FreeMind (freemind.sourceforge.net) is an open source mind-mapping program which fits very well with Leo as an outlining system. A two-way link between the two would offer interesting possibilities. The FreeMind file is an XML file which is very similar to Leo's. It would be rather easy to write a converter but it serves as a good starting point. #@nonl #@+node:ekr.20040331072431.5:Base Implementation The code is implemented as a plugin which subclasses leoAtFile.baseNewDerivedFile and replaces it. The new class implements a dispatching mechanism similar to the plugin system itself, allowing handlers to register themselves to take over various stages of the derivation process. Most of the heavy lifting is done in the _doDispatch method which checks to see if a handler is available for the current action for the current file type. Many of the method of baseNewDerivedFile are override to pass through _doDispatch. In addition, it was necessary to wrap some methods in pre and post calls to allow, for example, the startnode and endnode functions. One example of this is putAtOthersChild. A slightly more ugly wrapping of the 'write' method was required since this method performs many actions which we would like to control. In this case these methods had to be switched out with dummy calls so that the handler could call them when it required. #@+node:ekr.20040331072431.6:_doDispatch << class dispatchedDerivedFile methods >>= def _doDispatch(self, ontype, default, *args, **kw): """Do a dispatch""" base, ext = os.path.splitext(self.derivedname) print "Putting %s, dispatching on '%s' ('%s')" % (ontype, ext, self.derivedname) try: function = self.dispatch[ontype][ext[1:]] # [1:] to remove the "." from the extension except KeyError: function = default if function: return function(self, *args, **kw) #@-node:ekr.20040331072431.6:_doDispatch #@+node:ekr.20040331072431.7:putAtOthersChild << class dispatchedDerivedFile methods >>= def putAtOthersChild(self, *args, **kw): self._handleStartNode(*args, **kw) leoAtFile.baseNewDerivedFile.putAtOthersChild(self, *args, **kw) self._handleEndNode(*args, **kw) #@-node:ekr.20040331072431.7:putAtOthersChild #@+node:ekr.20040331072431.8:write << class dispatchedDerivedFile methods >>= def write(self, *args, **kw): root = args[0] self.derivedname = root.t.headString self._handleStart(*args, **kw) # # Make sure we hold the write file open oldclose = self.closeWriteFile oldreplace = self.replaceTargetFileIfDifferent self.closeWriteFile = lambda : None self.replaceTargetFileIfDifferent = lambda : None # leoAtFile.baseNewDerivedFile.write(self, *args, **kw) self._handleEnd(*args, **kw) # # Now we can close it self.closeWriteFile = oldclose self.closeWriteFile() self.replaceTargetFileIfDifferent = oldreplace self.replaceTargetFileIfDifferent() #@-node:ekr.20040331072431.8:write #@-node:ekr.20040331072431.5:Base Implementation #@+node:ekr.20040331072431.9:Handler calls Handler clients register themselves using the registerDispatcher call which takes three parameters - extension (the file extension to register for) - ontype (the handler event to hook, see declarations) - handler (the function to recieve the call) In addition, a convenience blank handler (ie ignore the event) can be inserted by calling the registerNullDispatcher for a particular extension and ontype. #@nonl #@+node:ekr.20040331072431.10:<< class dispatchedDerivedFile declarations >> @code """New type of derived file which can dispatch on the file type""" dispatch = {"startnode" : {}, "endnode" : {}, "body" : {}, # Only dispatch 'body' if you know what you are doing! "code" : {}, "sentinel" : {}, "start" : {}, "end" : {}, } #@-node:ekr.20040331072431.10:<< class dispatchedDerivedFile declarations >> #@+node:ekr.20040331072431.11:registerDispatcher << atfile_dispatch methods >>= leoAtFile.newDerivedFile = dispatchedDerivedFile def registerDispatcher(extension, ontype, handler): """Register a handler for an file extension""" dispatchedDerivedFile.dispatch[ontype][extension] = handler #@-node:ekr.20040331072431.11:registerDispatcher #@+node:ekr.20040331072431.12:registerNullDispatcher << atfile_dispatch methods >>= def registerNullDispatcher(extension, ontype): """Register that a handler shouldn't be called""" dispatchedDerivedFile.dispatch[ontype][extension] = None #@-node:ekr.20040331072431.12:registerNullDispatcher #@-node:ekr.20040331072431.9:Handler calls #@+node:ekr.20040331072431.13:FreeMind Implementation The FreeMind example shows that the plugin begins by registering its dispatchers. It is then called during the normal process of deriving a file (with a ".mm" extension). It hooks nearly all the events with the exception of body (see the child of this node for a discussion of why not!). The handler functions are rather simple although I took a slightly more complex route than is required in order to exercise all the events. The implementation builds a separate node structure to represent the tree and then writes this out when the process is complete. #@nonl #@+node:ekr.20040331072431.14:Hooking the body call This hooks the putBody method call. It is tough to override this as this basically means the handler must do all the hard work of traversing the tree and remembering to call v.setVisited. Finer control of this process is probably a good idea since getting someone to override this properly is quite tricky. #@nonl #@-node:ekr.20040331072431.14:Hooking the body call #@-node:ekr.20040331072431.13:FreeMind Implementation #@-node:ekr.20040331072431.4:Proof-of-concept #@+node:ekr.20040331072431.15:Conclusions from the proof-of-concept 1. It works for writing the derived file. 2. The handler code is pretty clean and doesn't require the handler writer to know about the internals of the @file writing code. 3. Overriding "putBody" is probably a bit too tough to expect people to do. 4. It isn't clear that the same approach will work for reading derived files. This might have to be delegated completely to the handler. #@-node:ekr.20040331072431.15:Conclusions from the proof-of-concept #@+node:ekr.20040331072431.16:Next steps I want to try out something on the read side (mainly because I want to see if I can get a two way link with FreeMind!). This would complete the proof-of-concept and achieve the aim, which was to demonstrate that Leo could incorporate custom handlers for derived files without any structural changes. Following that a more complete implementation could be put together if this is thought to be a valuable way to go. #@nonl #@-node:ekr.20040331072431.16:Next steps #@-node:ekr.20040331072431.1:Documentation #@+node:ekr.20040331072431.17:Code #@+node:ekr.20040331072431.18:atfile_dispatch.py @ignore @root atfile_dispatch.py @language python << atfile_dispatch declarations >> << atfile_dispatch methods >> if 1: # Register the handlers... g.plugin_signon(__name__) #@+node:ekr.20040331072431.19:<< atfile_dispatch declarations >> @code """Enable dispatching of @file writing to custom handlers""" __version__ = "0.1" import leoGlobals as g import leoPlugins import leoAtFile import os #@nonl #@-node:ekr.20040331072431.19:<< atfile_dispatch declarations >> #@+node:ekr.20040331072431.20:class dispatchedDerivedFile << atfile_dispatch methods >>= class dispatchedDerivedFile(leoAtFile.baseNewDerivedFile): << class dispatchedDerivedFile declarations >> << class dispatchedDerivedFile methods >> #@+node:ekr.20040331072431.10:<< class dispatchedDerivedFile declarations >> @code """New type of derived file which can dispatch on the file type""" dispatch = {"startnode" : {}, "endnode" : {}, "body" : {}, # Only dispatch 'body' if you know what you are doing! "code" : {}, "sentinel" : {}, "start" : {}, "end" : {}, } #@-node:ekr.20040331072431.10:<< class dispatchedDerivedFile declarations >> #@+node:ekr.20040331072431.6:_doDispatch << class dispatchedDerivedFile methods >>= def _doDispatch(self, ontype, default, *args, **kw): """Do a dispatch""" base, ext = os.path.splitext(self.derivedname) print "Putting %s, dispatching on '%s' ('%s')" % (ontype, ext, self.derivedname) try: function = self.dispatch[ontype][ext[1:]] # [1:] to remove the "." from the extension except KeyError: function = default if function: return function(self, *args, **kw) #@-node:ekr.20040331072431.6:_doDispatch #@+node:ekr.20040331072431.21:putBody << class dispatchedDerivedFile methods >>= def putBody(self, *args, **kw): """Put the body text""" return self._doDispatch("body", leoAtFile.baseNewDerivedFile.putBody, *args, **kw) #@-node:ekr.20040331072431.21:putBody #@+node:ekr.20040331072431.22:putCodeLine << class dispatchedDerivedFile methods >>= def putCodeLine(self, *args, **kw): """Put the code line""" return self._doDispatch("code", leoAtFile.baseNewDerivedFile.putCodeLine, *args, **kw) #@-node:ekr.20040331072431.22:putCodeLine #@+node:ekr.20040331072431.23:putSentinel << class dispatchedDerivedFile methods >>= def putSentinel(self, *args, **kw): """Put the sentinel line""" return self._doDispatch("sentinel", leoAtFile.baseNewDerivedFile.putSentinel, *args, **kw) #@-node:ekr.20040331072431.23:putSentinel #@+node:ekr.20040331072431.24:_handleStartNode << class dispatchedDerivedFile methods >>= def _handleStartNode(self, *args, **kw): return self._doDispatch("startnode", None, *args, **kw) #@-node:ekr.20040331072431.24:_handleStartNode #@+node:ekr.20040331072431.25:_handleEndNode << class dispatchedDerivedFile methods >>= def _handleEndNode(self, *args, **kw): return self._doDispatch("endnode", None, *args, **kw) #@-node:ekr.20040331072431.25:_handleEndNode #@+node:ekr.20040331072431.7:putAtOthersChild << class dispatchedDerivedFile methods >>= def putAtOthersChild(self, *args, **kw): self._handleStartNode(*args, **kw) leoAtFile.baseNewDerivedFile.putAtOthersChild(self, *args, **kw) self._handleEndNode(*args, **kw) #@-node:ekr.20040331072431.7:putAtOthersChild #@+node:ekr.20040331072431.26:_handleStart << class dispatchedDerivedFile methods >>= def _handleStart(self, *args, **kw): return self._doDispatch("start", None, *args, **kw) #@-node:ekr.20040331072431.26:_handleStart #@+node:ekr.20040331072431.27:_handleEnd << class dispatchedDerivedFile methods >>= def _handleEnd(self, *args, **kw): return self._doDispatch("end", None, *args, **kw) #@-node:ekr.20040331072431.27:_handleEnd #@+node:ekr.20040331072431.8:write << class dispatchedDerivedFile methods >>= def write(self, *args, **kw): root = args[0] self.derivedname = root.t.headString self._handleStart(*args, **kw) # # Make sure we hold the write file open oldclose = self.closeWriteFile oldreplace = self.replaceTargetFileIfDifferent self.closeWriteFile = lambda : None self.replaceTargetFileIfDifferent = lambda : None # leoAtFile.baseNewDerivedFile.write(self, *args, **kw) self._handleEnd(*args, **kw) # # Now we can close it self.closeWriteFile = oldclose self.closeWriteFile() self.replaceTargetFileIfDifferent = oldreplace self.replaceTargetFileIfDifferent() #@-node:ekr.20040331072431.8:write #@-node:ekr.20040331072431.20:class dispatchedDerivedFile #@+node:ekr.20040331072431.11:registerDispatcher << atfile_dispatch methods >>= leoAtFile.newDerivedFile = dispatchedDerivedFile def registerDispatcher(extension, ontype, handler): """Register a handler for an file extension""" dispatchedDerivedFile.dispatch[ontype][extension] = handler #@-node:ekr.20040331072431.11:registerDispatcher #@+node:ekr.20040331072431.12:registerNullDispatcher << atfile_dispatch methods >>= def registerNullDispatcher(extension, ontype): """Register that a handler shouldn't be called""" dispatchedDerivedFile.dispatch[ontype][extension] = None #@-node:ekr.20040331072431.12:registerNullDispatcher #@-node:ekr.20040331072431.18:atfile_dispatch.py #@+node:ekr.20040331072431.28:freemind_export.py @ignore @root freemind_export.py @language python << freemind_export declarations >> << freemind_export methods >> if 1: # Register the handlers... g.plugin_signon(__name__) import atfile_dispatch # Register dispatch functions for writing atfile_dispatch.registerDispatcher("mm", "code", handleMindMapLine) atfile_dispatch.registerDispatcher("mm", "startnode", handleMindMapStartNode) atfile_dispatch.registerDispatcher("mm", "endnode", handleMindMapEndNode) atfile_dispatch.registerNullDispatcher("mm", "sentinel") atfile_dispatch.registerDispatcher("mm", "start", handleStart) atfile_dispatch.registerDispatcher("mm", "end", handleEnd) #@+node:ekr.20040331072431.29:<< freemind_export declarations >> @code """Export part of a Leo outline to FreeMind""" __version__ = "0.1" import leoGlobals as g import leoPlugins #@-node:ekr.20040331072431.29:<< freemind_export declarations >> #@+node:ekr.20040331072431.30:class MindMapNode << freemind_export methods >>= class MindMapNode(list): << class MindMapNode declarations >> << class MindMapNode methods >> #@+node:ekr.20040331072431.31:<< class MindMapNode declarations >> @code """Class representing a mind map node""" #@-node:ekr.20040331072431.31:<< class MindMapNode declarations >> #@+node:ekr.20040331072431.32:__init__ << class MindMapNode methods >>= def __init__(self, header, parent, body=""): """Initialize""" self.header = header self.parent = parent self.body = body #@-node:ekr.20040331072431.32:__init__ #@+node:ekr.20040331072431.33:render << class MindMapNode methods >>= def render(self, at): """Render to the file""" if not self.body: txt = self.header else: txt = "%s\n%s" % (self.header, self.body) txt = txt.replace("\n", "e ") at.os('\n' % (txt,)) for child in self: child.render(at) at.os('\n') #@-node:ekr.20040331072431.33:render #@-node:ekr.20040331072431.30:class MindMapNode #@+node:ekr.20040331072431.34:handleStart << freemind_export methods >>= def handleStart(self, *args, **kw): """Start of writing process""" print "Handling start" global nodes, current_node, first_node nodes = {} # Dictionary of nodes root = args[0] nodes[root] = current_node = MindMapNode(root.t.headString, None) first_node = current_node print "End" #@-node:ekr.20040331072431.34:handleStart #@+node:ekr.20040331072431.35:handleMindMapLine << freemind_export methods >>= def handleMindMapLine(self, s, i): """Handling body for mind mapper""" print "Handling body '%s'" % s current_node.body += s print "End" #@-node:ekr.20040331072431.35:handleMindMapLine #@+node:ekr.20040331072431.36:handleMindMapStartNode << freemind_export methods >>= def handleMindMapStartNode(self, v): """Handle the start header""" global current_node print "Handling start header '%s'" % (v, ) this = MindMapNode(v.t.headString, current_node) nodes[v] = this current_node.append(this) current_node = this print "End" #@-node:ekr.20040331072431.36:handleMindMapStartNode #@+node:ekr.20040331072431.37:handleMindMapEndNode << freemind_export methods >>= def handleMindMapEndNode(self, v): """Handle the end header""" global current_node print "Handling end header '%s'" % (v, ) current_node = current_node.parent print "End" #@-node:ekr.20040331072431.37:handleMindMapEndNode #@+node:ekr.20040331072431.38:handleEnd << freemind_export methods >>= def handleEnd(self, *args, **kw): """Handle the end""" print "Handling end" self.os("\n") first_node.render(self) self.os("\n") print "End" #@-node:ekr.20040331072431.38:handleEnd #@-node:ekr.20040331072431.28:freemind_export.py #@-node:ekr.20040331072431.17:Code #@+node:ekr.20040331072431.39:Test files @ignore #@nonl #@+node:ekr.20040331072431.40:@file c:\temp\mind.mm The main node @others #@+node:ekr.20040331072431.41:a aaaa #@nonl #@-node:ekr.20040331072431.41:a #@+node:ekr.20040331072431.42:b bbb #@nonl #@-node:ekr.20040331072431.42:b #@+node:ekr.20040331072431.43:c ccc @others #@nonl #@+node:ekr.20040331072431.44:c1 c1c1 @others #@nonl #@+node:ekr.20040331072431.45:c1a c1ac1a #@nonl #@-node:ekr.20040331072431.45:c1a #@-node:ekr.20040331072431.44:c1 #@+node:ekr.20040331072431.46:c2 c2c2 #@nonl #@-node:ekr.20040331072431.46:c2 #@-node:ekr.20040331072431.43:c #@+node:ekr.20040331072431.47:d ddd #@nonl #@-node:ekr.20040331072431.47:d #@-node:ekr.20040331072431.40:@file c:\temp\mind.mm #@+node:ekr.20040331072431.48:@file c:\temp\mind2.txt @others #@nonl #@+node:ekr.20040331072431.49:a aaaa #@nonl #@-node:ekr.20040331072431.49:a #@+node:ekr.20040331072431.50:b bbb #@nonl #@-node:ekr.20040331072431.50:b #@+node:ekr.20040331072431.51:c ccc #@nonl #@+node:ekr.20040331072431.52:c1 c1c1 #@nonl #@+node:ekr.20040331072431.53:c1a c1ac1a #@nonl #@-node:ekr.20040331072431.53:c1a #@-node:ekr.20040331072431.52:c1 #@+node:ekr.20040331072431.54:c2 c2c2 #@nonl #@-node:ekr.20040331072431.54:c2 #@-node:ekr.20040331072431.51:c #@+node:ekr.20040331072431.55:d ddd #@nonl #@-node:ekr.20040331072431.55:d #@-node:ekr.20040331072431.48:@file c:\temp\mind2.txt #@+node:ekr.20040331072431.56:@file c:\temp\test.txt This is some text second line third @others #@+node:ekr.20040331072431.57:Header inside header #@-node:ekr.20040331072431.57:Header #@-node:ekr.20040331072431.56:@file c:\temp\test.txt #@-node:ekr.20040331072431.39:Test files #@-node:ekr.20040331072431:Custom @file plugin (prototype) #@+node:ekr.20040320072156:Word completion (prototype) #@+node:ekr.20040320075528:Documentation @nocolor This plugin implements a word completion scheme for Leo. In the body pane you have two new options, * F4 - complete a dotted word (eg type "self." followed F4 will cycle through the methods and attributes of an object) * F5 - complete a stand alone word (eg type "imp" followed by F5 will cycle though some standard words ... import etc) Pressing F4 with a partial g.match (eg "os.pa") will cycle through the methods begining with "pa". The plugin works by scanning the Leo file to look for words and objects. Hence you will only get things that have been referencing in the Leo file already. The scanning currently takes place at file load so any new things you add wont get added to the list until you reload. If there is more than one word suggestions then repeatedly pressing F4/F5 will cycle through them. The order is from the most to the least used so the most frequently used words should appear first. Once you reach the end it stops. #@nonl #@-node:ekr.20040320075528:Documentation #@+node:ekr.20040320075528.1:Code notes @nocolor Why two keys? I tried the get the "dotted completion" to work when you press the "." key but I couldn't stop the "." appearing in the body as well. The code is all there so if someone knows the trick then you can just enable this by adding and appropriate . The completion is "python style", ie it doesn't try to know anything about the source - it just looks for likely looking combinations. For other languages (php, java etc) you could be smarter. If you feel inclined then you can create a custom completer by subclassing the basic class used (Extender) and adding another WordCompleter class. #@nonl #@-node:ekr.20040320075528.1:Code notes #@+node:ekr.20040320121631:Changes made @nocolor Changes made: EKR: 3/20/04 - Converted to @file. - Used @others where possible. - Removed unused sections. - Coverted to "import leoGlobals as g" style. - Used g.trace rather than most calls to es. - Fixed bug in split. - Fixed bug in suggestWords I don't understand the code yet, but already I love this plugin! #@nonl #@-node:ekr.20040320121631:Changes made #@+node:ekr.20040320072156.1:@file-thin extender.py import leoGlobals as g import leoPlugins import re @others if __name__ == "__main__": test() #@nonl #@+node:ekr.20040320072156.2:class Splitter class Splitter(object): """Split text into base/extension parts""" @others #@+node:ekr.20040320072156.4:__init__ def __init__(self): """Initialise the Splitter instance""" #@-node:ekr.20040320072156.4:__init__ #@+node:ekr.20040320072156.5:split def split(self, text): """Return a list of the base/extension pairs from a block of text""" raise NotImplementedError("Must override split method") #@-node:ekr.20040320072156.5:split #@-node:ekr.20040320072156.2:class Splitter #@+node:ekr.20040320072156.6:class ReSplitter class ReSplitter(Splitter): @others #@+node:ekr.20040320072156.7:__init__ def __init__(self, base, extension, *args): """Initialise the ReSplitter instance""" self._base = re.compile(base,*args) self._extension = re.compile(extension,*args) #@-node:ekr.20040320072156.7:__init__ #@+node:ekr.20040320072156.8:split def split(self,text): """Split the text""" # g.trace(len(text)) results = [] if not text: # EKR return results for base in self._base.finditer(text): ext = self._extension.match(text[base.end():]) if ext: results.append((base.groups()[0], ext.groups()[0])) # g.trace("done") return results #@-node:ekr.20040320072156.8:split #@-node:ekr.20040320072156.6:class ReSplitter #@+node:ekr.20040320072156.9:class Extender class Extender(object): """Learn about text strings and suggest ways to extend a string""" @others #@+node:ekr.20040320072156.10:__init__ def __init__(self, splitter): """Initialise the Extender instance""" self._buffer = "" # The stream of characters to learn self._memory = {} # The store of all we have learnt self._splitter = splitter self.size = 0 #@-node:ekr.20040320072156.10:__init__ #@+node:ekr.20040320072156.11:feed def feed(self, text): """Learn from some text""" # g.trace(len(text)) for base,extension in self._splitter.split(text): self.learn(base,extension) #@-node:ekr.20040320072156.11:feed #@+node:ekr.20040320072156.12:learn def learn(self, stem, extender): """Learn the relationship between the stem and the extender""" # g.trace(stem,extender) self._memory.setdefault(stem, {}) self._memory[stem][extender] = self._memory[stem].get(extender, 0)+1 self.size = len(self._memory) #@-node:ekr.20040320072156.12:learn #@+node:ekr.20040320072156.13:suggestions def suggestions(self, base, start=None, number=None): """Return a list of suggestions in order of most to least likely If specified, start indicates the initial characters of the match. By default we return all the possible suggestions but this can be limited by setting the number to return.""" # g.trace() if start: allmatched = self.suggestions(base) result = [item for item in allmatched if item.startswith(start)] else: try: items = self._memory[base] except KeyError: return [] else: values = [(value, key) for key, value in items.iteritems()] values.sort() values.reverse() result = [key for value, key in values] if number is None: return result else: return result[:number] #@-node:ekr.20040320072156.13:suggestions #@+node:ekr.20040320072156.14:suggestfrom def suggestfrom(self,text,number=None): """Find suggestions from a block of text This method returns the base string and a list of alternatives for how the base might be extended.""" # Add some characters to the text so that the end will match trytext = " " + text + "DDD" matches = self._splitter.split(trytext) g.trace(text,number,matches) # Make sure the last one is a match if not matches: return None, [] base,ext = matches[-1] if not ext.endswith("DDD"): return text, [] # Remove the dummy part ext = ext[:-3] if not ext: ext = None base_text = text else: base_text = text[:-len(ext)] suggestions = self.suggestions(base,ext,number) g.trace(base_text,len(suggestions)) return base_text, suggestions #@-node:ekr.20040320072156.14:suggestfrom #@-node:ekr.20040320072156.9:class Extender #@+node:ekr.20040320072156.15:class DottedExtender class DottedExtender(Extender): """An extender based around dotted names""" @others #@+node:ekr.20040320072156.16:__init__ def __init__(self): """Initialise the DottedExtender instance""" super(DottedExtender, self).__init__( ReSplitter("(\w+?)\.", "(\w+)")) #@-node:ekr.20040320072156.16:__init__ #@-node:ekr.20040320072156.15:class DottedExtender #@+node:ekr.20040320072156.17:class WordExtender class WordExtender(Extender): """An extender based on simple words""" @others #@+node:ekr.20040320072156.18:__init__ def __init__(self): """Initialise the WordExtender instance""" super(WordExtender, self).__init__( ReSplitter("^|\s()", "(\w+)")) #@-node:ekr.20040320072156.18:__init__ #@-node:ekr.20040320072156.17:class WordExtender #@+node:ekr.20040320072156.19:test def test(): import sys text = file(sys.argv[-1], "r").read() e = DottedExtender() e.feed(text) w = WordExtender() w.feed(text) #@-node:ekr.20040320072156.19:test #@-node:ekr.20040320072156.1:@file-thin extender.py #@+node:ekr.20040320072156.20:@file-thin wordcompleter.py """Plugin to add word completion to the edit pane""" import leoGlobals as g import leoPlugins try: import Tkinter as Tk except ImportError: Tk = None import extender @others if Tk: __version__ = "0.2" # Converted to @file by EKR. __name__ = "Word Completer" g.plugin_signon("wordcompleter") s = "Starting word completer" g.trace(s) ; g.es(s,color="purple") completer1 = DottedCompleter() completer2 = SimpleCompleter() #leoPlugins.registerHandler("start2", completer.addKeyBinding) leoPlugins.registerHandler("open2", completer1.createWordList) #@nonl #@+node:ekr.20040320072156.21:class WordCompleter class WordCompleter: """A class to help in suggesting words""" keybinding = None # Override this extender_class = None # Override this @others #@nonl #@+node:ekr.20040320072156.22:__init__ def __init__(self): """Initialise the WordCompleter instance""" self.extender = self.extender_class() self.extender.feed("self.this self.that self.other") self.previous = [] # Previous suggestions #@-node:ekr.20040320072156.22:__init__ #@+node:ekr.20040320072156.23:addKeyBinding def addKeyBinding(self, tag, keywords): """Add key bindings to intercept the hot key""" c = g.top() c.frame.body.bodyCtrl.bind(self.keybinding,self.suggestWords) c.frame.body.bodyCtrl.bind(".", self.suggestWords) #@-node:ekr.20040320072156.23:addKeyBinding #@+node:ekr.20040320072156.24:createWordList def createWordList(self, tag, keywords): """Initialize the list of words to use""" # g.trace(tag,keywords) v = g.top().rootVnode() while v: self.extender.feed(v.bodyString()) v = v.threadNext() self.addKeyBinding(tag,keywords) #@-node:ekr.20040320072156.24:createWordList #@+node:ekr.20040320072156.25:suggestWords def suggestWords(self, event): """Suggest words to be inserted at the cursor""" # g.trace(self) # Find text on this line before the insertion point c = g.top() insert = c.frame.body.getInsertionPoint() thisline = insert.split(".")[0] start = "%s.0" % (thisline, ) text = c.frame.body.getTextRange(start, insert) # If the user pressed '.' then this isn't in the text yet so we # manually add it - This doesn't work! if event.char == ".": text += "." dotted = 1 else: dotted = 0 # Find suggestions base,suggestions = self.extender.suggestfrom(text) #import pdb; pdb.set_trace() if suggestions: # 3/30/04 # Delete back from the current position to the start of the suggestion dot = "%s.%s" % (thisline,len(base)) oldtext = c.frame.body.getTextRange(dot, insert) c.frame.body.deleteRange(dot, insert) # If the current text is the suggestion then we are cycling # through the possibilities if oldtext == suggestions[0] and self.previous: word = self.previous.pop(0) else: word = suggestions[0] self.previous = suggestions[1:] # Insert suggestion if dotted: word = "." + word c.frame.body.insertAtInsertPoint(word) return dotted #@-node:ekr.20040320072156.25:suggestWords #@-node:ekr.20040320072156.21:class WordCompleter #@+node:ekr.20040320072156.26:class DottedCompleter class DottedCompleter(WordCompleter): """Completer to complete dotted.names""" keybinding = "" extender_class = extender.DottedExtender # No methods. #@-node:ekr.20040320072156.26:class DottedCompleter #@+node:ekr.20040320072156.27:class SimpleCompleter class SimpleCompleter(WordCompleter): """A completer which works on any word in a text stream""" keybinding = "" extender_class = extender.WordExtender # No methods #@-node:ekr.20040320072156.27:class SimpleCompleter #@-node:ekr.20040320072156.20:@file-thin wordcompleter.py #@-node:ekr.20040320072156:Word completion (prototype) #@+node:ekr.20040320091826:(problem with dot in Word completion plugin) v.body #@nonl #@+node:ekr.20040320072156.23:addKeyBinding def addKeyBinding(self, tag, keywords): """Add key bindings to intercept the hot key""" c = g.top() c.frame.body.bodyCtrl.bind(self.keybinding,self.suggestWords) c.frame.body.bodyCtrl.bind(".", self.suggestWords) #@-node:ekr.20040320072156.23:addKeyBinding #@+node:ekr.20040320072156.14:suggestfrom def suggestfrom(self,text,number=None): """Find suggestions from a block of text This method returns the base string and a list of alternatives for how the base might be extended.""" # Add some characters to the text so that the end will match trytext = " " + text + "DDD" matches = self._splitter.split(trytext) g.trace(text,number,matches) # Make sure the last one is a match if not matches: return None, [] base,ext = matches[-1] if not ext.endswith("DDD"): return text, [] # Remove the dummy part ext = ext[:-3] if not ext: ext = None base_text = text else: base_text = text[:-len(ext)] suggestions = self.suggestions(base,ext,number) g.trace(base_text,len(suggestions)) return base_text, suggestions #@-node:ekr.20040320072156.14:suggestfrom #@+node:ekr.20040320072156.13:suggestions def suggestions(self, base, start=None, number=None): """Return a list of suggestions in order of most to least likely If specified, start indicates the initial characters of the match. By default we return all the possible suggestions but this can be limited by setting the number to return.""" # g.trace() if start: allmatched = self.suggestions(base) result = [item for item in allmatched if item.startswith(start)] else: try: items = self._memory[base] except KeyError: return [] else: values = [(value, key) for key, value in items.iteritems()] values.sort() values.reverse() result = [key for value, key in values] if number is None: return result else: return result[:number] #@-node:ekr.20040320072156.13:suggestions #@-node:ekr.20040320091826:(problem with dot in Word completion plugin) #@-node:edream.110203113231.9:Unfinished projects #@+node:EKR.20040517075110:replaceLeoGlobals """Script to find and replace all functions in leoGlobals.py.""" @color @language python @tabwidth -4 import leoGlobals as g import leoPlugins import string c = g.top() ; p = c.currentPosition() @others if 1: << set nameList to the list of functions in leoGlobals.py >> else: p = g.findNodeAnywhere("@file leoGlobals.py") nameList = findFunctionsInTree(p) nameList.sort() ; g.enl() for name in nameList: g.es("'%s'," % name) s = "%d functions in leoGlobals.py" % len(nameList) print s ; g.es(s) if 1: g.enl() ; g.enl() count = prependNamesInTree(p,nameList,"g.",replace=True) # Just prints if replace==False. s = "%d --- done --- " % count print s ; g.es(s) #@+node:EKR.20040517075110.1:<< set nameList to the list of functions in leoGlobals.py >> nameList = ( 'alert', 'angleBrackets', 'appendToList', 'callerName', 'CheckVersion', 'choose', 'clearAllIvars', 'clear_stats', 'collectGarbage', 'computeLeadingWhitespace', 'computeWidth', 'computeWindowTitle', 'createTopologyList', 'create_temp_name', 'disableIdleTimeHook', 'doHook', 'dump', 'ecnl', 'ecnls', 'enableIdleTimeHook', 'enl', 'ensure_extension', 'es', 'esDiffTime', 'es_error', 'es_event_exception', 'es_exception', 'escaped', 'executeScript', 'file_date', 'findNodeAnywhere', 'findNodeInTree', 'findTopLevelNode', 'findReference', 'find_line_start', 'find_on_line', 'flattenList', 'funcToMethod', 'getBaseDirectory', 'getOutputNewline', 'getTime', 'get_Sherlock_args', 'get_directives_dict', 'get_leading_ws', 'get_line', 'get_line_after', 'getpreferredencoding', 'idleTimeHookHandler', 'importFromPath', 'initScriptFind', 'init_sherlock', 'init_trace', 'isUnicode', 'isValidEncoding', 'is_c_id', 'is_nl', 'is_special', 'is_ws', 'is_ws_or_nl', 'joinLines', 'listToString', 'makeAllNonExistentDirectories', 'makeDict', 'match', 'match_c_word', 'match_ignoring_case', 'match_word', 'module_date', 'openWithFileName', 'optimizeLeadingWhitespace', 'os_path_abspath', 'os_path_basename', 'os_path_dirname', 'os_path_exists', 'os_path_getmtime', 'os_path_isabs', 'os_path_isdir', 'os_path_isfile', 'os_path_join', 'os_path_norm', 'os_path_normcase', 'os_path_normpath', 'os_path_split', 'os_path_splitext', 'pause', 'plugin_date', 'plugin_signon', 'printDiffTime', 'printGc', 'printGcRefs', 'printGlobals', 'printLeoModules', 'print_bindings', 'print_stats', 'readlineForceUnixNewline', 'redirectStderr', 'redirectStdout', 'removeLeadingWhitespace', 'removeTrailingWs', 'reportBadChars', 'restoreStderr', 'restoreStdout', 'sanitize_filename', 'scanAtEncodingDirective', 'scanAtFileOptions', 'scanAtLineendingDirective', 'scanAtPagewidthDirective', 'scanAtRootOptions', 'scanAtTabwidthDirective', 'scanDirectives', 'scanError', 'scanf', 'set_delims_from_language', 'set_delims_from_string', 'set_language', 'shortFileName', 'skip_blank_lines', 'skip_block_comment', 'skip_braces', 'skip_c_id', 'skip_heredoc_string', 'skip_leading_ws', 'skip_leading_ws_with_indent', 'skip_line', 'skip_long', 'skip_matching_delims', 'skip_nl', 'skip_non_ws', 'skip_parens', 'skip_pascal_begin_end', 'skip_pascal_block_comment', 'skip_pascal_braces', 'skip_pascal_string', 'skip_php_braces', 'skip_pp_directive', 'skip_pp_if', 'skip_pp_part', 'skip_python_string', 'skip_string', 'skip_to_char', 'skip_to_end_of_line', 'skip_to_semicolon', 'skip_typedef', 'skip_ws', 'skip_ws_and_nl', 'splitLines', 'stat', 'stdErrIsRedirected', 'stdOutIsRedirected', 'toEncodedString', 'toUnicode', 'toUnicodeFileEncoding', 'top', 'trace', 'trace_tag', 'update_file_if_changed', 'utils_rename', 'windows', 'wrap_lines') #@nonl #@-node:EKR.20040517075110.1:<< set nameList to the list of functions in leoGlobals.py >> #@+node:EKR.20040517075110.2:findFunctionsInTree def findFunctionsInTree(p): nameList = [] for p in p.self_and_subtree_iter(): names = findDefs(p.bodyString()) if names: for name in names: if name not in nameList: nameList.append(name) return nameList #@nonl #@-node:EKR.20040517075110.2:findFunctionsInTree #@+node:EKR.20040517075110.3:findDefs def findDefs(body): lines = body.split('\n') names = [] for s in lines: i = g.skip_ws(s,0) if g.match(s,i,"class"): return [] # The classes are defined in a single node. if g.match(s,i,"def"): i = g.skip_ws(s,i+3) j = g.skip_c_id(s,i) if j > i: name = s[i:j] if g.match(name,0,"__init__"): return [] # Disallow other class methods. names.append(name) return names #@nonl #@-node:EKR.20040517075110.3:findDefs #@+node:EKR.20040517075110.4:prependNamesInTree def prependNamesInTree(p,nameList,prefix,replace=False): c = p.c assert(len(prefix) > 0) ch1 = string.letters + '_' ch2 = string.letters + string.digits + '_' def_s = "def " ; def_n = len(def_s) prefix_n = len(prefix) total = 0 c.beginUpdate() for p in p.self_and_subtree_iter(): count = 0 ; s = p.bodyString() printFlag = False if s: for name in nameList: i = 0 ; n = len(name) while 1: << look for name followed by '(' >> if count and replace: if 0: << print before and after >> p.setBodyStringOrPane(s) p.setDirty() g.es("%3d %s" % (count,p.headString())) total += count c.endUpdate() return total #@nonl #@+node:EKR.20040517075110.5:<< look for name followed by '(' >> i = s.find(name,i) if i == -1: break elif g.match(s,i-1,'.'): i += n # Already an attribute. elif g.match(s,i-prefix_n,prefix): i += n # Already preceded by the prefix. elif g.match(s,i-def_n,def_s): i += n # preceded by "def" elif i > 0 and s[i-1] in ch1: i += n # Not a word match. elif i+n < len(s) and s[i+n] in ch2: i += n # Not a word match. else: j = i + n j = g.skip_ws(s,j) if j >= len(s) or s[j] != '(': i += n else: # Replace name by prefix+name s = s[:i] + prefix + name + s[i+n:] i += n ; count += 1 # g.es('.',newline=False) if 1: if not printFlag: printFlag = True # print p.headString() print g.get_line(s,i-n) #@nonl #@-node:EKR.20040517075110.5:<< look for name followed by '(' >> #@+node:EKR.20040517075110.6:<< print before and after >> print "-"*10,count,p.headString() print "before..." print p.bodyString() print "-"*10,"after..." print s #@nonl #@-node:EKR.20040517075110.6:<< print before and after >> #@-node:EKR.20040517075110.4:prependNamesInTree #@-node:EKR.20040517075110:replaceLeoGlobals #@+node:EKR.20040608102548:(Fixed unicode bug in plugins_menu.py) #@+node:EKR.20040608102548.1:Report @nocolor By: Maxim Krikun - tws5 Non-ASCII in plugin config 2004-03-25 23:34 I run Leo 4.1 final, build 1.77 , Python 2.3.0, Tk 8.4.3, on windows 98. For some reason i had to enter non-ascii string in plugin preferences box (header_style for word export plugin, since ms word is localized and has style names different from "Header"). I found that the dialog doesn't close on OK button, and the ini file gets broken -- not all options remain there. As i investigated, ConfigParser.write() method when writing to a file claimed it can't convert unicode string to ascii. This problem was fixed by setting system default locale in site.py, (s/if: 0/if: 1/ at line 321). The fix: in << Create widgets for each section and option >>: e = Tk.Entry(b) e.insert(0, unicode(config.get(section, option))) #@nonl #@-node:EKR.20040608102548.1:Report #@+node:EKR.20040608103502:Unicode chars цев #@-node:EKR.20040608103502:Unicode chars #@+node:EKR.20040608104417:Traceback Exception in Tkinter callback Traceback (most recent call last): File "C:\PYTHON23\lib\lib-tk\Tkinter.py", line 1345, in __call__ return self.func(*args) File "C:\prog\leoCVS\leo\plugins\plugins_menu.py", line 208, in onOk self.writeConfiguration() File "C:\prog\leoCVS\leo\plugins\plugins_menu.py", line 223, in writeConfiguration self.config.write(f) File "C:\PYTHON23\lib\ConfigParser.py", line 366, in write fp.write("%s = %s\n" % UnicodeEncodeError: 'ascii' codec can't encode character u'\u6962' in position 0: ordinal not in range(128) #@-node:EKR.20040608104417:Traceback #@+node:EKR.20040517080555.13:<< create the frame from the configuration data >> root = g.app.root << Create the top level and the main frame >> << Create widgets for each section and option >> << Create Ok, Cancel and Apply buttons >> g.app.gui.center_dialog(top) # Do this after packing. top.grab_set() # Make the dialog a modal dialog. top.focus_force() # Get all keystrokes. root.wait_window(top) #@nonl #@+node:EKR.20040517080555.14:<< Create the top level and the main frame >> self.top = top = Tk.Toplevel(root) g.app.gui.attachLeoIcon(self.top) top.title("Properties of "+ plugin.name) top.resizable(0,0) # neither height or width is resizable. self.frame = frame = Tk.Frame(top) frame.pack(side="top") #@nonl #@-node:EKR.20040517080555.14:<< Create the top level and the main frame >> #@+node:EKR.20040517080555.15:<< Create widgets for each section and option >> # Create all the entry boxes on the screen to allow the user to edit the properties sections = config.sections() sections.sort() for section in sections: # Create a frame for the section. f = Tk.Frame(top, relief="groove",bd=2) f.pack(side="top",padx=5,pady=5) Tk.Label(f, text=section.capitalize()).pack(side="top") # Create an inner frame for the options. b = Tk.Frame(f) b.pack(side="top",padx=2,pady=2) # Create a Tk.Label and Tk.Entry for each option. options = config.options(section) options.sort() row = 0 for option in options: e = Tk.Entry(b) e.insert(0, unicode(config.get(section,option))) # 6/8/04 Tk.Label(b, text=option).grid(row=row, column=0, sticky="e", pady=4) e.grid(row=row, column=1, sticky="ew", pady = 4) row += 1 self.entries.append((section, option, e)) #@nonl #@-node:EKR.20040517080555.15:<< Create widgets for each section and option >> #@+node:EKR.20040517080555.16:<< Create Ok, Cancel and Apply buttons >> box = Tk.Frame(top, borderwidth=5) box.pack(side="bottom") list = [("OK",self.onOk),("Cancel",top.destroy)] if plugin.hasapply: list.append(("Apply",self.onApply),) for text,f in list: Tk.Button(box,text=text,width=6,command=f).pack(side="left",padx=5) #@nonl #@-node:EKR.20040517080555.16:<< Create Ok, Cancel and Apply buttons >> #@-node:EKR.20040517080555.13:<< create the frame from the configuration data >> #@+node:EKR.20040517080555.18:writeConfiguration def writeConfiguration(self): """Write the configuration to disk""" # Set values back into the config item. for section, option, entry in self.entries: s = entry.get() s = g.toEncodedString(s,"ascii",reportErrors=True) # Config params had better be ascii. self.config.set(section,option,s) # Write out to the file. f = open(self.filename, "w") self.config.write(f) f.close() #@nonl #@-node:EKR.20040517080555.18:writeConfiguration #@-node:EKR.20040608102548:(Fixed unicode bug in plugins_menu.py) #@+node:ekr.20040918165427:To do in 4.3 #@+node:ekr.20040918165144:4.3 Prototypes as plugins @ Each can be done without impacting other code. Each can be done relatively easily: one week or less #@nonl #@+node:EKR.20040611044600:Mulder undo #@+node:bwmulder.20040601212737:basic_undo.py """ Define a general purpose monitor module. Can be used for classes derived from "object" to intercept assignment to instance attributes. For lists and dictionaries, it also offers drop-in replacements which monitor all changes to those list and mappings. This module also includes a basic undo / redo mechanism. For this undo / redo mechanism, it is important that the undo / redo steps do not trigger any monitoring calls. This module should fullfill that requirement. For scalars, we put "scalar_monitor" into the attribute slot to intercept accesses to the attribute. The values themselves live in a differnt, "private" attribute. These private attributes are accessed directly for the undo / redo steps. For dictionaries and lists, we extend the base types. The undo / redo mechanism uses the methods of the basic type. If you assign a list or a dictionary to a monitored attribute, the list or mapping is automatically converted to a monitored list or mapping (but only one level deep). [Question: do we need a convenience function which does this recursively?] """ @language python @tabwidth -4 @others #@+node:bwmulder.20040601212737.1:class passthrough class passthrough(object): """ Instances of this class are used to disable monitoring. The values are just passed through. """ @others #@+node:bwmulder.20040601212737.2:__init__ def __init__(self, private_attributename): self.private_attributename = private_attributename #@-node:bwmulder.20040601212737.2:__init__ #@+node:bwmulder.20040601212737.3:__set__ def __set__( self, instance, value): setattr(instance, self.private_attributename, value) #@-node:bwmulder.20040601212737.3:__set__ #@+node:bwmulder.20040601212737.4:__get__ def __get__(self, instance, owner): return getattr(instance, self.private_attributename) #@-node:bwmulder.20040601212737.4:__get__ #@-node:bwmulder.20040601212737.1:class passthrough #@+node:bwmulder.20040601212737.5:class scalar_monitor class scalar_monitor(object): """ Monitor a scalar attribute. A scalar attribute is an attribute whose values do not have internal structure. Used for Integers and references. """ @others #@+node:bwmulder.20040601212737.6:__init__ def __init__(self, external_attributename, monitor_object): self.external_attributename = external_attributename self.private_attributename = '__' + external_attributename self.monitor_object = monitor_object #@-node:bwmulder.20040601212737.6:__init__ #@+node:bwmulder.20040601212737.7:__set__ def __set__( self, instance, value): private_attributename = self.private_attributename external_attributename = self.external_attributename try: oldvalue = getattr(instance, private_attributename) except AttributeError: self.monitor_object.scalar_set(instance, private_attributename, external_attributename) else: if oldvalue != value: self.monitor_object.scalar_modify(instance, private_attributename, external_attributename, oldvalue) setattr(instance, private_attributename, value) #@-node:bwmulder.20040601212737.7:__set__ #@+node:bwmulder.20040601212737.8:__get__ def __get__(self, instance, owner): return getattr(instance, self.private_attributename) #@-node:bwmulder.20040601212737.8:__get__ #@-node:bwmulder.20040601212737.5:class scalar_monitor #@+node:bwmulder.20040601212737.9:class list_monitor class list_monitor(list): """ Monitor changes to a list. Calls a "monitor_object" whenever changes are made to a list. You can use this class whenever you want to track changes to a list. """ @others #@+node:bwmulder.20040605231305:__init__ def __init__(self, value, monitor): list.__init__(self, value) self.set_monitor_object(monitor) #@-node:bwmulder.20040605231305:__init__ #@+node:bwmulder.20040601212737.10:set_monitor_object def set_monitor_object(self, monitor_object): """ All changes to this list will trigger calls to monitor_object methods """ self.monitor_object = monitor_object #@-node:bwmulder.20040601212737.10:set_monitor_object #@+node:bwmulder.20040601212737.11:__setitem__ def __setitem__( self, key, value): """ Intercept the l[key]=value operations. Also covers slice assignment. """ try: oldvalue = self.__getitem__(key) except KeyError: self.monitor_object.list_create(self, key) else: self.monitor_object.list_set(self, key, oldvalue) list.__setitem__(self, key, value) #@-node:bwmulder.20040601212737.11:__setitem__ #@+node:bwmulder.20040601212737.12:__delitem__ def __delitem__( self, key): oldvalue = list.__getitem__(self, key) self.monitor_object.list_del(self, key, oldvalue) list.__delitem__(self, key) #@-node:bwmulder.20040601212737.12:__delitem__ #@+node:bwmulder.20040601212737.13:append def append(self, value): self.monitor_object.list_append(self) list.append(self, value) #@-node:bwmulder.20040601212737.13:append #@+node:bwmulder.20040602152548:pop def pop(self): oldvalue = list.pop(self) self.monitor_object.list_pop(self, oldvalue) #@-node:bwmulder.20040602152548:pop #@-node:bwmulder.20040601212737.9:class list_monitor #@+node:bwmulder.20040601212737.14:class list_monitor_in_instance class list_monitor_in_instance(object): """ Monitor instance attributes which contain a list as a value. Assignments to this attributes, which must be lists, are replaced by instances of 'list_monitor'. """ @others #@+node:bwmulder.20040601212737.15:__init__ def __init__(self, external_attributename, monitor_object): self.external_attributename = external_attributename self.internal_attributename = '__' + external_attributename self.monitor_object = monitor_object #@-node:bwmulder.20040601212737.15:__init__ #@+node:bwmulder.20040601212737.16:__set__ def __set__(self, instance, value): """Intercept assignments to the external attribute""" assert isinstance(value, type([])) if isinstance(value, list_monitor): newvalue = value # if the value is already a list monitor, assume that this value # is already monitored. Do not create a new value. else: newvalue = list_monitor(value, self.monitor_object) internal_attributename = self.internal_attributename try: oldvalue = getattr(instance, internal_attributename) except AttributeError: self.monitor_object.list_assignment_new(instance, internal_attributename) else: self.monitor_object.list_assignment_replace(instance, internal_attributename, oldvalue) setattr(instance, self.internal_attributename, newvalue) #@-node:bwmulder.20040601212737.16:__set__ #@+node:bwmulder.20040601212737.17:__get__ def __get__(self, instance, owner): try: return instance.__dict__[self.internal_attributename] except KeyError: return instance.__dict__[self.external_attributename] #@-node:bwmulder.20040601212737.17:__get__ #@-node:bwmulder.20040601212737.14:class list_monitor_in_instance #@+node:bwmulder.20040602153618:class dict_monitor class dict_monitor(dict): """ Overwrite dictionaries so that we can monitor them. """ @others #@+node:bwmulder.20040605231401:__init__ def __init__(self, value, monitor): dict.__init__(self, value) self.set_monitor_object(monitor) #@-node:bwmulder.20040605231401:__init__ #@+node:bwmulder.20040602153642:set_monitor_object def set_monitor_object(self, monitor_object): """ All changes to this dictionary will trigger calls to monitor_object methods """ self.monitor_object = monitor_object #@-node:bwmulder.20040602153642:set_monitor_object #@+node:bwmulder.20040602153723:__setitem__ def __setitem__( self, key, value): """ Intercept the l[key]=value operations. Also covers slice assignment. """ try: oldvalue = self.__getitem__(key) except KeyError: self.monitor_object.dict_create(self, key, value) else: self.monitor_object.dict_set(self, key, oldvalue) dict.__setitem__(self, key, value) #@-node:bwmulder.20040602153723:__setitem__ #@+node:bwmulder.20040602153835:__delitem__ def __delitem__( self, key): oldvalue = dict.__getitem__(self, key) self.monitor_object.dict_del(self, key, oldvalue) dict.__delitem__(self, key) #@-node:bwmulder.20040602153835:__delitem__ #@-node:bwmulder.20040602153618:class dict_monitor #@+node:bwmulder.20040602154259:class dict_monitor_in_instance class dict_monitor_in_instance(object): """ Monitor instance attributes which contain a list as a value. Assignments to this attributes, which must be lists, are replaced by instances of 'list_monitor'. """ @others #@+node:bwmulder.20040602154259.1:__init__ def __init__(self, external_attributename, monitor_object): self.external_attributename = external_attributename self.internal_attributename = '__' + external_attributename self.monitor_object = monitor_object #@-node:bwmulder.20040602154259.1:__init__ #@+node:bwmulder.20040602154259.2:__set__ def __set__(self, instance, value): """Intercept assignments to the external attribute""" assert isinstance(value, type({})) if isinstance(value, dict_monitor): newvalue = value # if the value is already a dict_monitor, # assume that the value is already monitored. else: newvalue = dict_monitor(value, self.monitor_object) internal_attributename = self.internal_attributename try: oldvalue = getattr(instance, internal_attributename) except AttributeError: self.monitor_object.list_assignment_new(instance, internal_attributename) else: self.monitor_object.list_assignment_replace(instance, internal_attributename, oldvalue) setattr(instance, self.internal_attributename, newvalue) #@-node:bwmulder.20040602154259.2:__set__ #@+node:bwmulder.20040602154259.3:__get__ def __get__(self, instance, owner): try: return instance.__dict__[self.internal_attributename] except KeyError: return instance.__dict__[self.external_attributename] #@-node:bwmulder.20040602154259.3:__get__ #@-node:bwmulder.20040602154259:class dict_monitor_in_instance #@+node:bwmulder.20040601212737.18:class monitor class monitor: """ Monitor changes to (new style) classes. To use: 1. a) Call monitor_scalar(klass, external_attributename, internal_attributename) for each scalar attribute you want to monitor. A scalar attribute is an attribute without internal structure (int and reference). The normal Python comparison operation (=) is used to check if a new value is stored in an instance attribute. b) Call monitor_list(klass, external_attributename, internal_attributename) for each list attribute you want to monitor. c) Call monitor_dict(klass, external_attributename, internal_attributename) for each dict attribute you want to monitor. 2. Call enable / disable to enable / disable monitoring. This is an abstract class. Concrete subclasses are the classes "tracer" and "basic_undomechanism". The latter does most (almost all) of the work of a (fairly) general undo mechanism. Limitations: Assumes that instance attributes are used consistently with certain types. """ @others #@+node:bwmulder.20040601212737.19:__init__ def __init__(self): self.monitored_scalar_attributes = [] self.monitored_list_attributes = [] self.monitored_dict_attributes = [] self.removed_functions = [] self.monitor_object = self # See enable_category. # Overwriting this one attribute allows # Clients of this module to implemente tracing of all # calls to this module. #@-node:bwmulder.20040601212737.19:__init__ #@+node:bwmulder.20040601214251:scalars #@+node:bwmulder.20040601212737.20:monitor_scalar def monitor_scalar(self, klass, external_attributename): """ Put in a hook so that we can monitor modications to instances of classref' with respect to the attribute "attributename". It is assumed that the attribute only contains scalar objects. A scalar object is an object which is unstructured, and not shared. """ self.monitored_scalar_attributes.append( (klass, external_attributename)) #@-node:bwmulder.20040601212737.20:monitor_scalar #@+node:bwmulder.20040601212737.24:scalar_set def scalar_set(self, instance, private_attributename, external_attributename): raise notImplementedError #@-node:bwmulder.20040601212737.24:scalar_set #@+node:bwmulder.20040601212737.25:scalar_modify def scalar_modify(self, instance, private_attributename, external_attributename, oldvalue): raise notImplementedError #@-node:bwmulder.20040601212737.25:scalar_modify #@-node:bwmulder.20040601214251:scalars #@+node:bwmulder.20040601215339:lists #@+node:bwmulder.20040601212737.21:monitor_list_attribute_in_class def monitor_list_attribute_in_class(self, klass, external_attributename): self.monitored_list_attributes.append( (klass, external_attributename)) #@-node:bwmulder.20040601212737.21:monitor_list_attribute_in_class #@+node:bwmulder.20040601212737.26:list_create def list_create(self, array, key, value): raise notImplementedError #@-node:bwmulder.20040601212737.26:list_create #@+node:bwmulder.20040601212737.27:list_set def list_set(self, array, key, value): raise notImplementedError #@-node:bwmulder.20040601212737.27:list_set #@+node:bwmulder.20040601212737.28:list_del def list_del(self, array, key, oldvalue): raise notImplementedError #@-node:bwmulder.20040601212737.28:list_del #@+node:bwmulder.20040601212737.29:list_append def list_append(self, array, value): raise notImplementedError #@-node:bwmulder.20040601212737.29:list_append #@-node:bwmulder.20040601215339:lists #@+node:bwmulder.20040602165344:dicts #@+node:bwmulder.20040602165402:monitor_dict_attribute_in_class def monitor_dict_attribute_in_class(self, klass, external_attributename): self.monitored_dict_attributes.append( (klass, external_attributename)) #@-node:bwmulder.20040602165402:monitor_dict_attribute_in_class #@+node:bwmulder.20040602165506:dict_create def dict_create(self, array, key, value): raise notImplementedError #@-node:bwmulder.20040602165506:dict_create #@+node:bwmulder.20040602165513:dict_set def dict_set(self, array, key, value): raise notImplementedError #@-node:bwmulder.20040602165513:dict_set #@+node:bwmulder.20040602165553:dict_del def dict_del(self, array, key, oldvalue): raise notImplementedError #@-node:bwmulder.20040602165553:dict_del #@-node:bwmulder.20040602165344:dicts #@+node:bwmulder.20040601215339.1:switching on and off @doc Delayed activation of the monitor mechanism is probably useful. Not clear of switching off and on is useful, though. #@nonl #@+node:bwmulder.20040601212737.22:enable def enable(self): for l, klass in ((self.monitored_scalar_attributes, scalar_monitor), (self.monitored_list_attributes, list_monitor_in_instance), (self.monitored_dict_attributes, dict_monitor_in_instance)): self.enable_category(l, klass) self.enable_put_in_removed_functions() #@-node:bwmulder.20040601212737.22:enable #@+node:bwmulder.20040602164627:enable_put_in_removed_functions def enable_put_in_removed_functions(self): """ Reinstate the functions which were removed from list_monitor and dict_monitor. """ for klass, attribute, function in self.removed_functions: setattr(klass, attribute, function) #@-node:bwmulder.20040602164627:enable_put_in_removed_functions #@+node:bwmulder.20040602154259.4:enable_category def enable_category(self, l, monitor_class): for klass, external_attributename in l: setattr(klass, external_attributename, monitor_class( external_attributename, self.monitor_object)) #@nonl #@-node:bwmulder.20040602154259.4:enable_category #@+node:bwmulder.20040602161525:disable_category def disable_category(self, l, monitor_class): for klass, external_attributename in l: setattr(klass, external_attributename, passthrough (internal_attributename)) #@-node:bwmulder.20040602161525:disable_category #@+node:bwmulder.20040601212737.23:disable def disable(self): """ Disable monitoring (temporarily). """ for l, klass in ((self.monitored_scalar_attributes, scalar_monitor), (self.monitored_list_attributes, list_monitor_in_instance), (self.monitored_dict_attributes, dict_monitor_in_instance)): self.disable_category(l, klass) self.remove_overrides_in_list_and_dict_monitor() #@-node:bwmulder.20040601212737.23:disable #@+node:bwmulder.20040603081718:remove_overrides_in_list_and_dict_monitor def remove_overrides_in_list_and_dict_monitor(self): """ Deletes all function definitions in list_monitor and dict_monitor. The net effect of this is that instances of these classes should behave like regular lists and dictionaries. """ import inspect for klass in (list_monitor, dict_monitor): for attribute in dir(klass): try: entity = getattr(klass, attribute) except AttributeError: pass else: if inspect.isfunction(entity): delattr(klass, attribute) self.removed_functions.append(klass, attribute, entity) #@nonl #@-node:bwmulder.20040603081718:remove_overrides_in_list_and_dict_monitor #@-node:bwmulder.20040601215339.1:switching on and off #@-node:bwmulder.20040601212737.18:class monitor #@+node:bwmulder.20040601222230:class basic_undomechanism class basic_undomechanism(monitor): """ This class provides the basic operations for undoable operations. Records a list of changes which it will undo or redo one by one. The granularity of the undo / redo operations is determined by calls to the 'mark' procedure. Only immediately after the 'mark' call can undo be called. Redo can only be called after calling undo. 'rollback' is a special case of undo: it is not redoable. The envisioned usage of this facility is in error recovery: if a command does not go through, you can call this command to undo all your changes (and leave the application in a consistent state). Uses the monitor_scalar, monitor_list_attribute_in_class and monitor_dict_attribute_in_class methods to make assignment to instance variables undoable. Call 'enable' to activate the undo mechanism, 'disable' to temporarily stop the undo mechanism from collecting information about changes. The individual changes are bundled into "_commands'. The boundaries of these _commands are marked by a call to the procedure "mark". The procedure 'reset' can be called externally to erase all undo information. Individual lists and dictionaries can also be monitored for change with the list_monitor and dict_monitor classes. Possible optimizations later: special handling for string attributes. """ @others #@nonl #@+node:bwmulder.20040601222230.1:__init__ def __init__(self): monitor.__init__(self) self.reset() #@nonl #@-node:bwmulder.20040601222230.1:__init__ #@+node:bwmulder.20040602171606:reset def reset(self): self._steps = [] self._commands = [None, None] self._index = 0 #@-node:bwmulder.20040602171606:reset #@+node:bwmulder.20040601222230.2:scalars #@+node:bwmulder.20040601222230.3:scalar_set def scalar_set(self, instance, private_attributename, external_attributename): self._steps.append((self.scalar_set_undo, (instance, private_attributename))) #@-node:bwmulder.20040601222230.3:scalar_set #@+node:bwmulder.20040601222230.4:scalar_set_undo def scalar_set_undo(self, instance, private_attributename): """Undo the changes done by the assignment of an instance""" newvalue = getattr(instance, private_attributename) delattr(instance, private_attributename) return self.scalar_set_redo, (instance, private_attributename, newvalue) #@-node:bwmulder.20040601222230.4:scalar_set_undo #@+node:bwmulder.20040601222230.5:scalar_set_redo def scalar_set_redo(self, instance, private_attributename, newvalue): setattr(instance, private_attributename, newvalue) return self.scalar_set_undo, (instance, private_attributename) #@-node:bwmulder.20040601222230.5:scalar_set_redo #@+node:bwmulder.20040601222230.6:scalar_modify def scalar_modify(self, instance, private_attributename, external_attributename, oldvalue): self._steps.append((self.scalar_modify_undo, (instance, private_attributename, oldvalue))) #@-node:bwmulder.20040601222230.6:scalar_modify #@+node:bwmulder.20040601222431:scalar_modify_undo def scalar_modify_undo(self, instance, private_attributename, oldvalue): new_value = getattr(instance, private_attributename) setattr(instance, private_attributename, oldvalue) return self.scalar_modify_undo, (instance, private_attributename, new_value) #@nonl #@-node:bwmulder.20040601222431:scalar_modify_undo #@-node:bwmulder.20040601222230.2:scalars #@+node:bwmulder.20040602075341:lists #@+node:bwmulder.20040602084701.1:creation #@+node:bwmulder.20040602175523:list_assignment_replace def list_assignment_replace(self, instance, attributename, oldvalue): self._steps.append((self.list_assignment_replace_undo, (instance, attributename, oldvalue))) #@nonl #@-node:bwmulder.20040602175523:list_assignment_replace #@+node:bwmulder.20040602175523.1:list_assignment_replace_undo def list_assignment_replace_undo(self, instance, attributename, oldvalue): newvalue = getattr(instance, attributename) setattr(instance, attributename, oldvalue) return self.list_assignment_replace_redo, (instance, attributename, newvalue) #@nonl #@-node:bwmulder.20040602175523.1:list_assignment_replace_undo #@+node:bwmulder.20040602175740:list_assignment_replace_redo def list_assignment_replace_redo(self, instance, attributename, newvalue): oldvalue = getattr(instance, attributename) setattr(instance, attributename, newvalue) return self.list_assignment_replace_undo, (instance, attributename, oldvalue) #@-node:bwmulder.20040602175740:list_assignment_replace_redo #@+node:bwmulder.20040602183806:list_assignment_new def list_assignment_new(self, instance, attributename): self._steps.append((self.list_assignment_new_undo, (instance, attributename))) #@nonl #@-node:bwmulder.20040602183806:list_assignment_new #@+node:bwmulder.20040602183814:list_assignment_new_undo def list_assignment_new_undo(self, instance, attributename): newvalue = getattr(instance, attributename) delattr(instance, attributename) return self.list_assignment_new_redo, (instance, attributename, newvalue) #@nonl #@-node:bwmulder.20040602183814:list_assignment_new_undo #@+node:bwmulder.20040602183911:list_assignment_new_redo def list_assignment_new_redo(self, instance, attributename, newvalue): setattr(instance, attributename, newvalue) return self.list_assignment_new_undo, (instance, attributename) #@-node:bwmulder.20040602183911:list_assignment_new_redo #@+node:bwmulder.20040602075341.1:list_create def list_create(self, array, key): self._steps.append((self.list_create_undo, (array, key))) #@-node:bwmulder.20040602075341.1:list_create #@+node:bwmulder.20040602084701.2:list_create_undo def list_create_undo(self, array, key): value = list.__getitem__(array, key) list.__delitem__(array, key) return self.list_create_redo, (array, key, value) #@-node:bwmulder.20040602084701.2:list_create_undo #@+node:bwmulder.20040602084701.3:list_create_redo def list_create_redo(self, array, key, value): list.__setitem__(array, key, value) return self.list_create_undo, (array, key) #@nonl #@-node:bwmulder.20040602084701.3:list_create_redo #@-node:bwmulder.20040602084701.1:creation #@+node:bwmulder.20040602084701.4:setting #@+node:bwmulder.20040602085955:list_set def list_set(self, array, key, oldvalue): self._steps.append((self.list_set_undo, (array, key, oldvalue))) #@-node:bwmulder.20040602085955:list_set #@+node:bwmulder.20040602085955.1:list_set_undo def list_set_undo(self, array, key, value): oldvalue = list.__getitem__(array, key) list.__setitem__(array, key, value) return self.list_set_undo, (array, key, oldvalue) #@-node:bwmulder.20040602085955.1:list_set_undo #@-node:bwmulder.20040602084701.4:setting #@+node:bwmulder.20040602085955.2:deletion #@+node:bwmulder.20040602085955.3:list_del def list_del(self, array, key, oldvalue): self._steps.append((self.list_del_undo, (array, key, oldvalue))) #@-node:bwmulder.20040602085955.3:list_del #@+node:bwmulder.20040602085955.4:list_del_undo def list_del_undo(self, array, key, oldvalue): if type(key) == type(1): list.__setitem__(array, slice(key, key), [oldvalue]) else: list.__setitem__(array, key, oldvalue) return self.list_del_redo, (array, key, oldvalue) #@-node:bwmulder.20040602085955.4:list_del_undo #@+node:bwmulder.20040602085955.5:list_del_redo def list_del_redo(self, array, key, oldvalue): oldvalue = list.__getitem__(array, key) list.__delitem__(array, key) return self.list_del_undo, (array, key, oldvalue) #@-node:bwmulder.20040602085955.5:list_del_redo #@-node:bwmulder.20040602085955.2:deletion #@+node:bwmulder.20040602151709:append #@+node:bwmulder.20040602152051:list_append def list_append(self, array): self._steps.append((self.list_append_undo, (array,))) #@-node:bwmulder.20040602152051:list_append #@+node:bwmulder.20040602152051.1:list_append_undo def list_append_undo(self, array): oldvalue = list.pop(array) return self.list_append_redo, (array, oldvalue) #@-node:bwmulder.20040602152051.1:list_append_undo #@+node:bwmulder.20040602152051.2:list_append_redo def list_append_redo(self, array, oldvalue): list.append(array, oldvalue) return self.list_append_undo, (array,) #@nonl #@-node:bwmulder.20040602152051.2:list_append_redo #@-node:bwmulder.20040602151709:append #@+node:bwmulder.20040602152548.1:pop #@+node:bwmulder.20040602152548.2:list_pop def list_pop(self, array, oldvalue): self._steps.append((self.list_append_redo, (array, oldvalue))) #@-node:bwmulder.20040602152548.2:list_pop #@-node:bwmulder.20040602152548.1:pop #@-node:bwmulder.20040602075341:lists #@+node:bwmulder.20040602171032:dictionaries #@+node:bwmulder.20040602171032.1:creation #@+node:bwmulder.20040602175801:dictionary creation # Dictionary monitors are are really handled like # list monitors. # For now, just use the same methods. dict_assignment_new = list_assignment_new dict_assignment_new_undo = list_assignment_new_undo dict_assignment_new_redo = list_assignment_new_redo dict_assignment_replace = list_assignment_replace dict_assignment_replace_undo = list_assignment_replace_undo dict_assignment_replace_redo = list_assignment_replace_redo #@nonl #@-node:bwmulder.20040602175801:dictionary creation #@+node:bwmulder.20040602171032.2:dict_create def dict_create(self, dictionary, key, value): self._steps.append((self.dict_create_undo, (dictionary, key))) #@-node:bwmulder.20040602171032.2:dict_create #@+node:bwmulder.20040602171032.3:dict_create_undo def dict_create_undo(self, dictionary, key): value = dict.__getitem__(dictionary, key) dict.__delitem__(dictionary, key) return self.dict_create_redo, (dictionary, key, value) #@-node:bwmulder.20040602171032.3:dict_create_undo #@+node:bwmulder.20040602171032.4:dict_create_redo def dict_create_redo(self, dictionary, key, value): dict.__setitem__(dictionary, key, value) return self.dict_create_undo, (dictionary, key,) #@-node:bwmulder.20040602171032.4:dict_create_redo #@-node:bwmulder.20040602171032.1:creation #@+node:bwmulder.20040602171032.5:setting #@+node:bwmulder.20040602171032.6:dict_set def dict_set(self, dictionary, key, oldvalue): self._steps.append((self.dict_set_undo, (dictionary, key, oldvalue))) #@-node:bwmulder.20040602171032.6:dict_set #@+node:bwmulder.20040602171032.7:dict_set_undo def dict_set_undo(self, dictionary, key, value): oldvalue = dict.__getitem__(dictionary, key) dict.__setitem__(dictionary, key, value) return self.dict_set_undo, (dictionary, key, oldvalue) #@-node:bwmulder.20040602171032.7:dict_set_undo #@-node:bwmulder.20040602171032.5:setting #@+node:bwmulder.20040602171032.8:deletion #@+node:bwmulder.20040602171032.9:dict_del def dict_del(self, dictionary, key, oldvalue): self._steps.append((self.dict_del_undo, (dictionary, key, oldvalue))) #@-node:bwmulder.20040602171032.9:dict_del #@+node:bwmulder.20040602171032.10:dict_del_undo def dict_del_undo(self, dictionary, key, oldvalue ): dict.__setitem__(dictionary, key, oldvalue) return self.dict_del_redo, (dictionary, key, oldvalue) #@-node:bwmulder.20040602171032.10:dict_del_undo #@+node:bwmulder.20040602171032.11:dict_del_redo def dict_del_redo(self, dictionary, key, oldvalue ): oldvalue = dict.__getitem__(dictionary, key) dict.__delitem__(dictionary, key) return self.dict_del_undo, (dictionary, key, oldvalue) #@-node:bwmulder.20040602171032.11:dict_del_redo #@-node:bwmulder.20040602171032.8:deletion #@-node:bwmulder.20040602171032:dictionaries #@+node:bwmulder.20040601224447:the undo machinery #@+node:bwmulder.20040603211921:queries #@+node:bwmulder.20040601224447.5:canUndo def canUndo(self): return self._commands[self._index] is not None and len(self._steps) == 0 #@-node:bwmulder.20040601224447.5:canUndo #@+node:bwmulder.20040601224447.6:canRedo def canRedo(self): return self._commands[self._index+1] is not None and len(self._steps) == 0 #@nonl #@-node:bwmulder.20040601224447.6:canRedo #@+node:bwmulder.20040603212552:commands def commands(self): return len(self._commands) - 2 #@nonl #@-node:bwmulder.20040603212552:commands #@+node:bwmulder.20040603212612:commands_to_undo def commands_to_undo(self): return self._index #@nonl #@-node:bwmulder.20040603212612:commands_to_undo #@+node:bwmulder.20040603212713:commands_to_redo def commands_to_redo(self): return self.commands() - self._index #@nonl #@-node:bwmulder.20040603212713:commands_to_redo #@+node:bwmulder.20040604165011:steps_stored def steps_stored(self): """ Return the total number of steps stored in the undoer. """ result = 0 for command in self._commands[1:-1]: result += len(command) return result #@nonl #@-node:bwmulder.20040604165011:steps_stored #@+node:bwmulder.20040605220919:print_commands def print_commands(self, comment): """ rint a readable list of all commands """ print "===== Commands: %s ========" % comment i = 0 while i < len(self._commands): print "Command", i steps = self._commands[i] if steps: for step in steps: function, args = step print " ",function.__name__, args if i == self._index: print "---------------------" i += 1 print "========================" #@-node:bwmulder.20040605220919:print_commands #@-node:bwmulder.20040603211921:queries #@+node:bwmulder.20040601224447.2:mark def mark(self): """Mark the end of the current commmand.""" if self._steps: self._commands[self._index+1:] = [self._steps, None] self._index += 1 self._steps = [] #@nonl #@-node:bwmulder.20040601224447.2:mark #@+node:bwmulder.20040601224447.3:undo def undo(self): assert self.canUndo() self._commands[self._index] = self.run_commands(self._commands[self._index]) self._index -= 1 #@-node:bwmulder.20040601224447.3:undo #@+node:bwmulder.20040601224447.4:redo def redo(self): assert self.canRedo() self._commands[self._index+1] = self.run_commands(self._commands[self._index+1]) self._index += 1 #@nonl #@-node:bwmulder.20040601224447.4:redo #@+node:bwmulder.20040603212934:rollback def rollback(self): self.run_commands(self._steps) #@-node:bwmulder.20040603212934:rollback #@+node:bwmulder.20040601222649:run_commands def run_commands(self, steps): """ Run the undo / redo _steps. Returns the list of steps to redo / undo the steps just made. """ steps.reverse() return [func(*args) for func, args in steps] #@-node:bwmulder.20040601222649:run_commands #@-node:bwmulder.20040601224447:the undo machinery #@-node:bwmulder.20040601222230:class basic_undomechanism #@-node:bwmulder.20040601212737:basic_undo.py #@+node:bwmulder.20040602221559:basic_undo_test.py << imports >> undo_tracing = False @others if __name__ == '__main__': test_main() #@nonl #@+node:bwmulder.20040602223236:<< imports >> from basic_undo import monitor, basic_undomechanism, list_monitor, dict_monitor import unittest from test import test_support #@-node:bwmulder.20040602223236:<< imports >> #@+node:bwmulder.20040602223236.1:class tracer class tracer(monitor): # The output format could be improved, but this is only for testing. """Simple class which can be used to trace all calls made from a monitor.""" @others #@+node:bwmulder.20040602223236.2:__getattr__ def __getattr__(self, attributename): self.attributename = attributename return self.catchall #@nonl #@-node:bwmulder.20040602223236.2:__getattr__ #@+node:bwmulder.20040602223906:catchall def catchall(self, *args, **kwrds): print "Tracer:", self.attributename, ":", args, kwds #@nonl #@-node:bwmulder.20040602223906:catchall #@-node:bwmulder.20040602223236.1:class tracer #@+node:bwmulder.20040602224231:class delegator class delegator(object): """ Simple class to print all arguments passed to a monitor. Allows you to watch the undoer in action... """ @others #@nonl #@+node:bwmulder.20040602224413:__init__ def __init__(self): self.undoer = basic_undomechanism() self.undoer.monitor_object = self #@-node:bwmulder.20040602224413:__init__ #@+node:bwmulder.20040602224332:__getattr__ def __getattr__(self, attributename): self.attributename = attributename return self.catchall #@nonl #@-node:bwmulder.20040602224332:__getattr__ #@+node:bwmulder.20040602231914:catchall def catchall(self, *args, **kwrds): print "delegator:", self.attributename, ":", args, kwrds getattr(self.undoer, self.attributename) (*args, **kwrds) #@-node:bwmulder.20040602231914:catchall #@+node:bwmulder.20040602231934:enable_category def enable_category(self, l, monitor_class): for klass, external_attributename, internal_attributename in l: setattr(klass, external_attributename, monitor_class( external_attributename, internal_attributename, self)) #@-node:bwmulder.20040602231934:enable_category #@-node:bwmulder.20040602224231:class delegator #@+node:bwmulder.20040602225640:class basic_scalar_test class basic_scalar_test_class(object): """Simple class for the basic_scalar_test""" pass class basic_scalar_test(unittest.TestCase): @others #@nonl #@+node:bwmulder.20040602230124:test_scalar_basic def test_scalar_basic(self): """ Some basic testing for the scalar undoer. """ if undo_tracing: self.m = m = delegator() else: self.m = m = basic_undomechanism() m.monitor_scalar(basic_scalar_test_class, "x") m.enable() # command 1 c = basic_scalar_test_class() c.x = "First value" m.mark() # command 2 c.x = "second value" m.mark() # command 3 c.x = "third value" m.mark() # command 4 # Check that the second assignment can be undone assert c.x == "third value", c.x m.undo() # command 2 assert c.x == "second value" m.undo() # command 1 assert c.x == "First value", c.x m.undo() assert not hasattr(c, "x") #@-node:bwmulder.20040602230124:test_scalar_basic #@+node:bwmulder.20040604184443:test_linked_list def test_linked_list(self): """ Shows that the basic undo mechanism can be used to undo the construction of a linked list. """ if undo_tracing: self.m = m = delegator() else: self.m = m = basic_undomechanism() # Create a linked list and check that the link operations can be undone. class root_class(object): pass class x(object): def __init__(self, name, next=None): self.name = name self.next = next def p(root): result = [] r = root.root while r: result.append(r.name) r = r.next return result undoer = basic_undomechanism() m.monitor_scalar(root_class, "root") m.monitor_scalar(x, "name") m.monitor_scalar(x, "next") m.enable() root = root_class() root.root = x("first") root.root.next = x("second") m.mark() root.root.next.next = x("third") root.root.next.next.next = x("fourth") m.mark() assert p(root) == ["first", "second", "third", "fourth"] m.undo() assert p(root) == ["first", "second"] m.redo() assert p(root) ==["first", "second", "third", "fourth"] # now try a few things with dictionaries. m.monitor_dict_attribute_in_class(root_class, "d") m.enable() root.d = {} m.mark() root.d = {'Some dict': 1} m.mark() root.d[1] = 2 assert root.d == {'Some dict': 1, 1: 2}, root.d m.mark() m.undo() assert root.d == {'Some dict': 1}, root.d m.undo() assert root.d == {}, root.d #@nonl #@-node:bwmulder.20040604184443:test_linked_list #@-node:bwmulder.20040602225640:class basic_scalar_test #@+node:bwmulder.20040605104941:class basic_list_test class basic_list_test_class(object): """Simple class to test the list undoer""" pass class basic_list_test(unittest.TestCase): """ Test the basic operations of the list class """ @others #@+node:bwmulder.20040605105746:test_lists_basic def test_lists_basic(self): """ Some basic testing for the list undoer. """ if undo_tracing: self.m = m = delegator() else: self.m = m = basic_undomechanism() m.monitor_list_attribute_in_class(basic_list_test_class, "a") m.enable() c = basic_list_test_class() c.a = [1, 2, 3] c.a = c.a m.mark() assert m.commands_to_undo() == 1, m.commands_to_undo() assert m.commands_to_redo() == 0, m.commands_to_redo() c.a.append(5) m.mark() assert m.commands_to_undo() == 2, m.commands_to_undo() assert m.commands_to_redo() == 0, m.commands_to_redo() assert c.a == [1, 2, 3, 5], c.a m.undo() assert m.commands_to_undo() == 1, m.commands_to_undo() assert m.commands_to_redo() == 1, m.commands_to_redo() assert c.a == [1, 2, 3], c.a m.redo() assert m.commands_to_undo() == 2, m.commands_to_undo() assert m.commands_to_redo() == 0, m.commands_to_redo() assert c.a == [1, 2, 3, 5], c.a m.undo() assert m.commands_to_undo() == 1, m.commands_to_undo() assert m.commands_to_redo() == 1, m.commands_to_redo() m.undo() assert m.commands_to_undo() == 0, m.commands_to_undo() assert m.commands_to_redo() == 2, m.commands_to_redo() assert not hasattr(c, "a"), "A should not exist here" assert m.steps_stored() == 3, m.steps_stored() m.redo() m.redo() assert c.a == [1, 2, 3, 5], c.a del c.a[2] assert c.a == [1, 2, 5], c.a # 1 m.mark() m.undo() assert c.a == [1, 2, 3, 5], c.a # 2 m.redo() assert c.a == [1, 2, 5], c.a # 3 x = c.a.pop() m.mark() assert c.a == [1, 2] m.undo() assert c.a == [1, 2, 5] #@-node:bwmulder.20040605105746:test_lists_basic #@+node:bwmulder.20040605174728:test_lists_replace def test_lists_replace(self): """ Test that list assignment works for an instance attribute that is put under the undo mechanism. """ if undo_tracing: self.m = m = delegator() else: self.m = m = basic_undomechanism() c = basic_list_test_class() m.monitor_list_attribute_in_class(basic_list_test_class, "a") m.enable() c.a = [1, 2, 3] m.mark() m.undo() assert not hasattr(c,"a") m.redo() c.a.append(4) c.a.append(5) assert c.a == [1, 2, 3, 4, 5] m.mark() m.undo() assert c.a == [1, 2, 3] m.redo() assert c.a == [1, 2, 3, 4, 5] b = list_monitor(('a', 'b', 'c'), m) m.enable() b.append('d') assert b == ['a', 'b', 'c', 'd'] m.mark() m.undo() assert b == ['a', 'b', 'c'] m.redo() assert b == ['a', 'b', 'c', 'd'] #@-node:bwmulder.20040605174728:test_lists_replace #@-node:bwmulder.20040605104941:class basic_list_test #@+node:bwmulder.20040605180204:class basic_dict_test class basic_dict_test_class(object): """Simple class to test the list undoer""" pass class basic_dict_test(unittest.TestCase): """ Test the basic operations of the list class """ @others #@+node:bwmulder.20040605180225:test_dicts_basic def test_dicts_basic(self): """ Some basic testing for the dict undoer. """ trace_commands = False if undo_tracing: self.m = m = delegator() else: self.m = m = basic_undomechanism() m.monitor_dict_attribute_in_class(basic_dict_test_class, "a") m.enable() c = basic_dict_test_class() c.a = {1:1, 2:2, 3:3} c.a = c.a m.mark() if trace_commands: print; m.print_commands(1) assert m.commands_to_undo() == 1, m.commands_to_undo() assert m.commands_to_redo() == 0, m.commands_to_redo() c.a[5] = 5 m.mark() if trace_commands: m.print_commands(2) assert m.commands_to_undo() == 2, m.commands_to_undo() assert m.commands_to_redo() == 0, m.commands_to_redo() assert c.a == {1:1, 2:2, 3:3, 5:5}, c.a # 1 m.undo() if trace_commands: m.print_commands(3) assert m.commands_to_undo() == 1, m.commands_to_undo() assert m.commands_to_redo() == 1, m.commands_to_redo() assert c.a == {1:1, 2:2, 3:3}, c.a # 2 m.redo() if trace_commands: m.print_commands(4) assert m.commands_to_undo() == 2, m.commands_to_undo() assert m.commands_to_redo() == 0, m.commands_to_redo() assert c.a == {1:1, 2:2, 3:3, 5:5}, c.a # 3 m.undo() if trace_commands: m.print_commands(5) assert m.commands_to_undo() == 1, m.commands_to_undo() assert m.commands_to_redo() == 1, m.commands_to_redo() assert c.a == {1:1, 2:2, 3:3}, c.a # 4 m.undo() if trace_commands: m.print_commands(6) assert m.commands_to_undo() == 0, m.commands_to_undo() assert m.commands_to_redo() == 2, m.commands_to_redo() assert not hasattr(c, "a"), "A should not exist here" assert m.steps_stored() == 3, m.steps_stored() m.redo() if trace_commands: m.print_commands(7) assert c.a == {1:1, 2:2, 3:3}, c.a # 5 m.redo() if trace_commands: m.print_commands(8) assert c.a == {1:1, 2:2, 3:3, 5:5}, c.a # 6 del c.a[5] assert c.a == {1:1, 2:2, 3:3}, c.a # 7 m.mark() if trace_commands: m.print_commands(9) m.undo() if trace_commands: m.print_commands(10) assert c.a == {1:1, 2:2, 3:3, 5:5}, c.a # 8 m.redo() if trace_commands: m.print_commands(11) assert c.a == {1:1, 2:2, 3:3}, c.a # 9 del c.a[2] m.mark() if trace_commands: m.print_commands(12) assert c.a == {1:1, 3:3}, c.a # 10 m.undo() if trace_commands: m.print_commands(13) assert c.a == {1:1, 2:2, 3:3}, c.a # 11 #@nonl #@-node:bwmulder.20040605180225:test_dicts_basic #@+node:bwmulder.20040605180245:test_dicts_replace def test_dicts_replace(self): """ Test that dict assignment works for an instance attribute that is put under the undo mechanism. """ if undo_tracing: self.m = m = delegator() else: self.m = m = basic_undomechanism() c = basic_dict_test_class() m.monitor_dict_attribute_in_class(basic_dict_test_class, "a") m.enable() c.a = {1:1, 2:2, 3:3} m.mark() m.undo() assert not hasattr(c,"a") m.redo() c.a[4] = 4 c.a[5] = 5 assert c.a == {1:1, 2:2, 3:3, 4:4, 5:5}, c.a # 1 m.mark() m.undo() assert c.a == {1:1, 2:2, 3:3}, c.a # 2 m.redo() assert c.a == {1:1, 2:2, 3:3, 4:4, 5:5}, c.a # 3 b = dict_monitor({'a':'a', 'b':'b', 'c':'c'}, m) m.enable() b['d'] = 'd' assert b == {'a':'a', 'b':'b', 'c':'c', 'd':'d'}, b # 4 m.mark() m.undo() assert b == {'a':'a', 'b':'b', 'c':'c'}, b # 5 m.redo() assert b == {'a':'a', 'b':'b', 'c':'c', 'd':'d'}, b # 6 #@nonl #@-node:bwmulder.20040605180245:test_dicts_replace #@-node:bwmulder.20040605180204:class basic_dict_test #@+node:bwmulder.20040602230426:test_main def test_main(): test_support.run_unittest( basic_scalar_test, basic_list_test, basic_dict_test) #@nonl #@-node:bwmulder.20040602230426:test_main #@-node:bwmulder.20040602221559:basic_undo_test.py #@-node:EKR.20040611044600:Mulder undo #@+node:ekr.20040916073636:@thin ConceptualSort.py """ A plugin to enhance the EditAttributes.py plugin. Puts ConceptualSort command in the Outline Menu. """ @language python @tabwidth -4 << about this plugin >> __version__ = "0.3" << version history >> << imports >> @others haveseen = weakref.WeakKeyDictionary() if Tk and Pmw and weakref: leoPlugins.registerHandler(('new2','open2'), addCommand) __version__ = ".2" g.plugin_signon( __name__ ) #@nonl #@+node:ekr.20040916073636.1:<< about this plugin >> @ This plugin is to be used with the EditAttributes.py plugin. What it does is: 1. Puts a command in Outline called ConceptualSort. This will prompt you for a concept to sort by. 2. It will sort by uA's in all the siblings of the current node. This gives the user some more flexibility in how they want to arrange their nodes. Nodes without the attribute in question go to the bottom of the sort. :) Version .2 improvements: - Now supports level 0 New features: 1. The dialog has been redone. The user can: a. Select which attribute he wants to sort on by clicking on the Attribute box. b. Select the type of sort he wants by clicking on the radio buttons. which are 1. Normal. just like the previous version 2. Reversed. Like normal but the results are reversed. 3. Used defined. For advanced users. The text box is where a user can type in their own python code to sort the nodes-attributes. There is no need for a def. That gets appended to the beginning of the code. It prototype looks like: def( a, b, att ): where a and b are nodes and att is dictionary of the nodes and the respective value of the selected attribute. There is no need to indent on the first level since indentation is added at compile time. :) #@nonl #@-node:ekr.20040916073636.1:<< about this plugin >> #@+node:ekr.20040916075741:<< version history >> @ 0.3 EKR: - Converted to outline. - Style improvements. - Changes for 4.2 code base in hit(). - Use 'new2' instead of 'start2' hook. #@nonl #@-node:ekr.20040916075741:<< version history >> #@+node:ekr.20040916073636.2:<< imports >> import leoGlobals as g import leoPlugins try: import Tkinter as Tk except ImportError: Tk = g.cantImport("Tk") try: import Pmw except: Pmw = g.cantImport("Pmw" ) try: import weakref except ImportError: weakref = g.cantImport("weakref") #@nonl #@-node:ekr.20040916073636.2:<< imports >> #@+node:ekr.20040916074337:class CSFrontend: class CSFrontend: @others #@nonl #@+node:ekr.20040916074337.1:__init__ def __init__( self , c ): self.c = c self.dialog = Pmw.Dialog( buttons = ( 'Cancel', 'Sort' ), title = 'Conceptual Sort', command = self.execute ) hull = self.dialog.component( 'hull' ) f = Tk.Frame( hull ) f.pack( side = 'top' ) self.parent = p = c.currentVnode().parent() children = self.children = [] self.hasparent = 1 if p: nc = p.numberOfChildren() for i in xrange( nc ): children.append( p.nthChild( i ) ) else: p = c.rootVnode() self.hasparent = 0 while p: children.append( p ) p = p.next() alist = list( self._makeAttrList( children ) ) self.omuatts = Pmw.OptionMenu( f, labelpos = 'n', label_text = 'Attribute:', items = alist ) self.omuatts.pack( side = 'left') self.rdbutts = Pmw.RadioSelect( f, buttontype = 'radiobutton', orient = 'vertical', labelpos = 'n', label_text = 'Sort type' , hull_borderwidth = 2, hull_relief = 'ridge') map( lambda a: self.rdbutts.add( a ), ( 'Normal', 'Reversed', 'User defined' ) ) self.rdbutts.setvalue( 'Normal' ) self.rdbutts.pack( side = 'right' ) self.stxt = Pmw.ScrolledText( hull, labelpos = 'nw', label_text = 'def user_sort( a, b , atts):', usehullsize = 1, hull_width = 200, hull_height = 100 ) txt = self.stxt.component( 'text' ) txt.configure( background = 'white', foreground = 'blue' ) self.stxt.pack( side = 'top' ) self.dialog.activate( globalMode = 'nograb' ) #@nonl #@-node:ekr.20040916074337.1:__init__ #@+node:ekr.20040916074337.2:_makeAttrList def _makeAttrList( self, nodes ): from sets import Set atts = Set() for child in nodes: if str(child.__class__) == 'leoNodes.vnode': t = child.t else: t = child.v.t if hasattr( t, 'unknownAttributes' ): uAs = t.unknownAttributes.keys() map( lambda a: atts.add( a ), uAs ) return atts #@nonl #@-node:ekr.20040916074337.2:_makeAttrList #@+node:ekr.20040916074337.3:execute def execute( self, name ): if name == 'Cancel': self.dialog.destroy() else: self.targetAttr = self.omuatts.getvalue() if self.targetAttr == '': self.dialog.destroy() return self.type = self.rdbutts.getvalue() atts = buildAttList( self.children, self.targetAttr ) lcsort = lambda a, b, atts = atts: csort( a,b ,atts ) if self.type == 'Normal': self.children.sort( lcsort ) elif self.type == 'Reversed': self.children.sort( lcsort ) self.children.reverse() else: bcode = self.stxt.get( '1.0', 'end' ) bcode = bcode.split( '\n' ) bcode = map( lambda a: ' ' + a, bcode ) bcode = '\n'.join( bcode ) bcode = bcode + '\n' header = 'def user_sort( a, b, atts):\n' code = header + bcode usort = compile( code, 'user_sort', 'exec' ) def lcsort( a, b, atts =atts ): z = {} exec usort in {}, z rv = z[ 'user_sort' ]( a, b, atts ) return rv self.children.sort( lcsort ) if self.hasparent: move( self.c, self.children, self.parent ) else: root = self.c.rootVnode() move2( self.c, self.children, root ) self.dialog.destroy() #@nonl #@-node:ekr.20040916074337.3:execute #@-node:ekr.20040916074337:class CSFrontend: #@+node:ekr.20040916074337.4:getConcept def getConcept( c ): CSFrontend( c ) #@-node:ekr.20040916074337.4:getConcept #@+node:ekr.20040916074337.5:move def move( c, children , parent): c.beginUpdate() for n, ch in enumerate( children ): ch.moveToNthChildOf( parent, n ) c.endUpdate() #@-node:ekr.20040916074337.5:move #@+node:ekr.20040916074337.6:move2 def move2( c, children , oroot): c.beginUpdate() children[ 0 ].moveToRoot( oroot ) z1 = children[ 0 ] for z in children[ 1: ]: z.moveAfter( z1 ) z1 = z c.endUpdate() #@-node:ekr.20040916074337.6:move2 #@+node:ekr.20040916074337.7:buildAttList def buildAttList( children, concept ): atts = {} for child in children: if str(child.__class__) == 'leoNodes.vnode': t = child.t else: t = child.v.t atts[ child ] = None if hasattr( t, 'unknownAttributes' ): uA = t.unknownAttributes atts[ child ] = uA.get( concept ) return atts #@-node:ekr.20040916074337.7:buildAttList #@+node:ekr.20040916074337.8:getChildren def getChildren( v ): i = v.numberOfChildren() children = [] for z in xrange( i ): chi = v.nthChild( z ) children.append( chi ) return children #@-node:ekr.20040916074337.8:getChildren #@+node:ekr.20040916074337.9:csort def csort( a, b, atdict ): a1 = atdict[ a ] b1 = atdict[ b ] if a1 == None and b1 != None: return 1 elif a1 != None and b1 == None: return -1 elif a1 > b1: return 1 elif a1 < b1: return -1 else: return 0 #@-node:ekr.20040916074337.9:csort #@+node:ekr.20040916074337.10:addCommand def addCommand( tag, keywords ): c = keywords.get('c') or keywords.get('new_c') if haveseen.has_key( c ): return table = ( ( "Conceptual Sort" , None, lambda c = c: getConcept( c )), ) men = c.frame.menu men.createMenuItemsFromTable( "Outline" , table ) #@-node:ekr.20040916074337.10:addCommand #@-node:ekr.20040916073636:@thin ConceptualSort.py #@+node:ekr.20040722115324:Create a configurable pretty printer plugin #@-node:ekr.20040722115324:Create a configurable pretty printer plugin #@+node:ekr.20040918165144.1:Draw config dialog #@-node:ekr.20040918165144.1:Draw config dialog #@+node:ekr.20040918165144.2:Parse jEdit description files #@-node:ekr.20040918165144.2:Parse jEdit description files #@+node:ekr.20040918165144.3:Convert to Python indices #@-node:ekr.20040918165144.3:Convert to Python indices #@+node:ekr.20040918165144.4:Convert to new Undo #@-node:ekr.20040918165144.4:Convert to new Undo #@+node:ekr.20040918165144.5:New syntax coloring (threaded) #@-node:ekr.20040918165144.5:New syntax coloring (threaded) #@+node:ekr.20040918165144.6:Check Import feature #@-node:ekr.20040918165144.6:Check Import feature #@+node:ekr.20040918165144.7:Emacs-style keystroke handling #@-node:ekr.20040918165144.7:Emacs-style keystroke handling #@+node:ekr.20040918165144.8:Maybe: add translation feature to g.es #@-node:ekr.20040918165144.8:Maybe: add translation feature to g.es #@+node:ekr.20040918165144.9:Write script to find args to g.es() #@-node:ekr.20040918165144.9:Write script to find args to g.es() #@+node:ekr.20040918165144.10:New atFile organization (user tangle/untangle) #@-node:ekr.20040918165144.10:New atFile organization (user tangle/untangle) #@+node:ekr.20040918165350:Create status line class #@-node:ekr.20040918165350:Create status line class #@-node:ekr.20040918165144:4.3 Prototypes as plugins #@+node:ekr.20040918165427.2:** Add incremental search to search plugin (make find panel smaller) #@-node:ekr.20040918165427.2:** Add incremental search to search plugin (make find panel smaller) #@+node:ekr.20040918165427.4:Autocompletion dictionary files @nocolor Developers By: jasonic ( Jason Cunliffe ) @dictionary 2003-08-21 07:51 Auto-completion dictionary files would be excellent. Ideally each Leo language extension could just point to a separate .dict file. Hopefully we can build some Leo plugin utilities to generate these .dict files by parsing any file you'd like to use a 'source' for Leo dictionary. They might need manual cleanup, but vcould be big timesaver, especially for XML formats and the like. Great to be able to sahre these easily. Interesting uses for Leo dictionaries beyond just autocompletion. I am thinking they might open the door to some powerful macro/template behavior. For example, you load a special dictionary to help certain kinds of repetitive formatted content. CSS and XSLT could be good candidates, but also any kind of mild databases or lists. Documentation. So first we need basic dicts for Leo supported languages: Python, Perl, Javascript etc.. Then we need to consider that any Leo Node could have its own dictionary defined inline.. @dictionary filepath-to-custom-leo-dict With collaborative LeoN this would be very useful because connected Leo sessions could invoke each other's dictionaries!! - Jason #@-node:ekr.20040918165427.4:Autocompletion dictionary files #@+node:ekr.20040918165427.5:Leo to docbook xml plugin: Leo2AsciiDoc http://sourceforge.net/forum/message.php?msg_id=2388444 By: mdawson I use DocBook XML for my computer documentation, and lately for publishing just about any text document. Naturally, I wanted an easy way to use DocBook to publish Leo outlines. I've written a small Leo module, called Leo2AsciiDoc, that enables automatic publication of a plain text Leo outline to HTML or PDF, or as a web site or man page. Stuart Rackham's AsciiDoc program (in Python) is what makes this possible. Leo2AsciiDoc exports a Leo outline to a text file, from whence it can be converted to DocBook XML by AsciiDoc, and then automatically published via DocBook to HTML or PDF. One Leo outline can contain any number of documents, or web sites (via DocBook Website). I'm also learning Literate Programming, and am happy to be able to automatically publish (via make) a program's source and documentation from Leo. An example of the product is the paged HTML documentation for the module at: http://devguide.leo.marshallresearch.ca The web page for Leo2AsciiDoc is at: http://leo.marshallresearch.ca That web site is produced from a Leo outline. ---------------------------------------- Michael Dawson #@nonl #@-node:ekr.20040918165427.5:Leo to docbook xml plugin: Leo2AsciiDoc #@+node:ekr.20040918165427.6:Finish wx plugin & improve how Leo uses indices #@-node:ekr.20040918165427.6:Finish wx plugin & improve how Leo uses indices #@-node:ekr.20040918165427:To do in 4.3 #@-all #@nonl #@-node:edream.110203113231:@thin pluginsNotes.txt #@-leo