New Plugins: Clone Navigator and Node-Visit-History Navigator

24 views
Skip to first unread message

SegundoBob

unread,
Jul 17, 2011, 8:00:26 PM7/17/11
to Leo-Editor Forum
I have written two Qt-only plugins that any user of Leo-Editor might
well find useful. The "Clone Navigator" plugin allows you to move
between any two positions of a clone by typing just two keystrokes. The
"Node Visit History" plugin allows you to go to any recently visited
node position by typing just two keystrokes.


Attachment clone_nav.py is the Clone Navigator. Attachment
node_visit_history.py is the Node Visit Navigator.

Clone Navigator help:

The "Clone Navigator" plugin allows you to move between any two
positions of a clone by typing just two keystrokes.

The "Clone Navigator" plugin creates a button on the icon bar, installs
command clone-nav, and assigns Alt-Shift-c to command clone-nav.

Left clicking the cloneNav button, (or Alt-Shift-C, or command
clone-nav) pops up a menu listing the paths to root of all the parent
nodes of clones of the node at the current position.

If a clone is a root, then "Clone is a root" is displayed as its
path-to-root. The path-to-root for a non-root clone is the clone's
parent's headline, followed by "<-", followed by the clone's parent's
parent's headline, etc. -- till a root node is reached. The path-to-root
for the current position is marked with a right arrow icon.
Every path-to-root has an underline marking its shortcut character (if
any is possible). Typing a shortcut character selects the menu item.
Left-clicking a menu item also selects it.

Selecting a menu item positions to the clone corresponding to the menu
item and also destroys the pop-up menu.

Doing anything other than selecting a clone menu item, also destroys the
pop-up menu. Consequently, you should never see a pop-up messsage
telling you "Selected clone position no longer exists." but this
condition is tested and reported.
-----------

Node Visit History plugin help:

The "Node Visit History" plugin allows you to go to any recently visited
node position by typing just two keystrokes.

The "Node Visit History" plugin creates the nodeVisitHist button on the
Icon Bar, installs command node-visit-history, and assigns Alt-Shift-h
to command node-visit-history.

The path-to-root for a node is the node's headline, followed by "<-",
followed by the node's parent's headline, etc. -- till a root node is
reached. The path-to-root for the current position is marked with a
right arrow icon.

Every path-to-root has an underline marking its shortcut character (if
any is possible). Typing a shortcut character selects the menu item.
Left-clicking a menu item also selects it.

Selecting a menu item positions to that node in the outline and also
destroys the pop-up menu.

Doing anything other than selecting a pop-up menu item, also destroys
the pop-up menu.

If you select a position which no longer exists because you have changed
the outline, a message box pops up saying "Selected position no longer
exists.
----------------

clone_nav.py and node_visit_history.py were created using Leo-Editor.
You can look at them using Leo-editor by using the menu command File -->
Import File. This pops up a file selection dialog which lets you
choose the file to import. The imported file is then rooted by an @file
node following the current node position.

You can add these plugins to your Leo-Editor by placing them in the same
directory with the standard Leo-Editor plugins or by placing them in any
directory in the PYTHONPATH environment variable; and then enabling them
in your myLeoSettings.leo.

I'm confident that both of these plugins will behave as you expect and
will not cause you trouble, but you obviously try them at your own risk
and you should be careful.
------------
I have tested these plugins in the following environment:

Leo-Editor Revision: 4430
Python 2.7.1, qt version 4.7.2
linux2 -- Ubuntu Studio 11.04 (natty)
-----------

Help Please

I hope that someone, who knows more than I about PyQt4 and Leo-Editor,
will tell me if I'm misusing PyQt4 or Leo-Editor.

In particular, note that I create a new pop-up menu on each button click
and I never explicitly free the discarded menus. So far as I can tell,
this does not cause a "memory leak," but it worries me.

Is there some way to assign a shortcut keystroke to a command
implemented in a plugin that allows a user to change the shortcut
keystroke assigned to the command? I don't know any way to do this. My
testing indicates that a shortcut specifier in myLeoSettings is silently
ignored if the command it assigns is defined in a plugin or is not
defined anywhere.

clone_nav.py
node_visit_history.py

Terry Brown

unread,
Jul 17, 2011, 8:38:13 PM7/17/11
to leo-e...@googlegroups.com
On Sun, 17 Jul 2011 17:00:26 -0700
SegundoBob <bhos...@ieee.org> wrote:

> I hope that someone, who knows more than I about PyQt4 and Leo-Editor,
> will tell me if I'm misusing PyQt4 or Leo-Editor.
>
> In particular, note that I create a new pop-up menu on each button click
> and I never explicitly free the discarded menus. So far as I can tell,
> this does not cause a "memory leak," but it worries me.

I don't think you need to worry about that, if there are no references
left to the thing in question the python garbage collector should
dispose of it. The problem is more often the other way around, where
certain conditions can lead to the Qt C++ object being deleted while
you still have a python reference to its wrapper. But that causes a
traceback, so no need to worry about that either.

I would consider rolling your Node-Visit-History Navigator into the
nav_qt plugin, just because they go together and we should probably
take any chance we can to limit the number of plugins, they're
multiplying like rabbits (and I'm as guilty as anyone, of course).

You could put the context menu on the arrow buttons that plugin already
provides. That's what I'd do anyway, it's up to you of course.

> Is there some way to assign a shortcut keystroke to a command
> implemented in a plugin that allows a user to change the shortcut
> keystroke assigned to the command? I don't know any way to do this. My
> testing indicates that a shortcut specifier in myLeoSettings is silently
> ignored if the command it assigns is defined in a plugin or is not
> defined anywhere.

I think if you plugin provides a command like this (at top level in
module):

@g.command('my-new-command')
def do_thing(event):

c = event.get('c')
if not c:
return
c.my_plugin_controller.do_thing()

then a binding like

my-new-command = Ctrl-B

in the @shortcuts node in the @keys node in the @settings node should
work. I have a couple of things running that way.

Also, pick command names with a common prefix:

clonenav-next
clonenav-prev

etc. - makes them easier to find with Alt-X tab expansion and keeps the namespace clean.

Cheers -Terry

SegundoBob

unread,
Jul 21, 2011, 6:56:07 PM7/21/11
to leo-editor


On Jul 17, 5:38 pm, Terry Brown <terry_n_br...@yahoo.com> wrote:
> On Sun, 17 Jul 2011 17:00:26 -0700
>
> I think if you plugin provides a command like this (at top level in
> module):
>
> @g.command('my-new-command')
> def do_thing(event):
>
>     c = event.get('c')
>     if not c:
>         return
>     c.my_plugin_controller.do_thing()
>
> then a binding like
>
> my-new-command = Ctrl-B
>
> in the @shortcuts node in the @keys node in the @settings node should
> work.  I have a couple of things running that way.


Terry,

Thanks for suggesting that I use the @g.command('my-new-command')
decorator. But I'm stuck on the same issue you raised here:

http://groups.google.com/group/leo-editor/tree/browse_frm/thread/a83082cec5ad3df8/14579985c620ce70?rnum=1&_done=%2Fgroup%2Fleo-editor%2Fbrowse_frm%2Fthread%2Fa83082cec5ad3df8%2F14579985c620ce70%3Ftvc%3D1%26#doc_7a58cc87cd22eaca

That is, my command function needs to be a method. That is, my
command function needs the instance of my plugin class that gives my
command function access to my plugin's static/global information.

I'm afraid the problem is a hard. I tried just putting
"g.app.global_commands_dict['clone-nav'] = self._buttonClicked" into
my __init__() method. This put my function into the global commands
list all right, but I still could not assign a shortcut to the
function in myLeoSettings.leo. At this point I realized that my
plugin can't initialize itself till after a Leo frame is created and
myLeoSettings.leo is (must be?) processed before a Leo frame is
created.

Hence, I doubt that there is any way to allow my commands to be
assigned shortcuts in myLeoSettings.leo.

Terry Brown

unread,
Jul 21, 2011, 7:06:29 PM7/21/11
to leo-e...@googlegroups.com
On Thu, 21 Jul 2011 15:56:07 -0700 (PDT)
SegundoBob <bhos...@ieee.org> wrote:

> That is, my command function needs to be a method.

No problem, in the __init__ for your class, do

c._my_class_name_here = self

then

> @g.command('my-new-command')
> def do_thing(event):
>
>     c = event.get('c')
>     if not c:
>         return

>     c._my_class_name_here.do_thing()


calls the do_thing() method of your class instance.

Cheers -Terry

SegundoBob

unread,
Jul 22, 2011, 4:52:43 PM7/22/11
to leo-editor
Terry,

Thanks. That's a great solution. I'll use it.

Before I read your post, I didn't know that monkey patching the
commander (as you suggest doing) is allowed. Consequently, I came up
with a roundabout way that does not monkey patch the commander:

Very simple works for only one file open at a time. Put a global in
your plugin module:

_cloneNav = None

@g.command('clone-nav')
def clone_nav(event):
if _cloneNav:
_cloneNav._buttonClicked(event)

Now, clone_nav is a global command and can be assigned a shortcut in
leoSettings.leo or myLeoSettings.leo.

But, if you open a second file using this same instance of Leo-editor
(for example, in a second tab), then the single global _cloneNav is
overwritten and the clone_nav for the first file is executed on the
second file instead.

A little more complicated is (I hope) a general solution:

_cAndObj = []

@g.command('clone-nav')
def clone_nav(event):
c = event.get('c')
for cs, obj in _cAndObj:
if c == cs:
g.es('obj=',obj)
obj._buttonClicked(event)
break

That is, maintain a list that allows you to map each commander to the
appropriate instance of your plugin control object.

Would it be safe to hash the commander and use hash(c) as the key of a
dictionary (which would replace the list)?

Terry Brown

unread,
Jul 23, 2011, 10:48:25 AM7/23/11
to leo-e...@googlegroups.com
On Fri, 22 Jul 2011 13:52:43 -0700 (PDT)
SegundoBob <bhos...@ieee.org> wrote:

> Would it be safe to hash the commander and use hash(c) as the key of a
> dictionary (which would replace the list)?

I think that would work, but I don't think it's a problem just sticking
an instance variable on the commander, as long as the name is something
that is unlikely to clash with anything else.

Cheers -Terry

SegundoBob

unread,
Jul 24, 2011, 4:55:54 PM7/24/11
to leo-editor
On Jul 17, 5:00 pm, SegundoBob <bhoss...@ieee.org> wrote:
> Is there some way to assign a shortcut keystroke to a command
> implemented in a plugin that allows a user to change the shortcut
> keystroke assigned to the command?  I don't know any way to do this.  My
> testing indicates that a shortcut specifier in myLeoSettings is silently
> ignored if the command it assigns is defined in a plugin or is not
> defined anywhere.

Now know how a plugin can define a "global" command. Thank you, Terry
Brown (see his previous posts to this thread). But I still don't know
how a plugin can set an overridable default binding.

Ideally, a plugin could set a default binding and the user could use
myLeoSettings.leo to delete the binding or reassign the binding to
another command.

I have been unable to realize this ideal. In my attempts I
encountered the following problems:

There seems to be no way to clear/delete a key binding.

Overriding a binding gives a warning and the attempt to override has
other effect:

Warning: shortcut conflict for <Alt+Shift+C>
clone-nav in all from register-command
about-leo in all from myleosettings.leo

I tried the following call in my plugin:

c.k.bindKey(pane='all', shortcut='Alt-Shift-c',
callback=self._buttonClicked, commandName='clone-nav',
_hash='Plugin')

This allows myLeoSettings.leo to override the binding with no error or
warning message, but it leaves two conflicting bindings displayed by
print-bindings.
I know my _hash parameter is wrong because the "binding source" column
is blank, but "plugin" is not a possible "binding source."

When the c.k.bindKey() call is NOT overriden in myLeoSettings.leo,
print-bindings shows the binding, but the binding does nothing.
Reply all
Reply to author
Forward
0 new messages