Tabbed Apps In Leo

74 views
Skip to first unread message

Thomas Passin

unread,
Oct 30, 2022, 11:09:26 AM10/30/22
to leo-editor
I have made several apps (really, "applets") to run in tabs in Leo's Log frame.  I find this convenient because they don't need to take up a whole panel, as Viewrendered/Viewrendered3 do, and they don't even need to be plugins so they can be simpler.  They are mostly intended to be used in a specific outline; their code can reside in the outline.  

I wanted to share how I open these apps in  the Log frame.  Since they are not plugins, they don't load automatically when Leo starts.  I don't find that to be a problem, since it's easy to provide a Leo command, button, or menu item to open them when you want to use them.

I open a tabbed app with the following command:

def toggle_app_tab(log, tabname, top_widget):
    """Create or remove our app's tab.
   
    ARGUMENTS
    log -- the log panel object for this outline.
    tabname -- a string to use as the display name of our tab.
    top_widget -- an instance of top level widget of our app.
    """
    # If our tab is visible, remove it
    if log.contentsDict.get(f'{tabname}-visible', False):
        log.deleteTab(tabname)
        log.contentsDict[f'{tabname}-visible'] = False
    else:
        # Show our tab, reusing our widget if already loaded
        if log.contentsDict.get(f'{tabname}-loaded', False):
            log.createTab(tabname,
                widget = log.contentsDict[f'{tabname}-widget'],
                createText = False)
            log.contentsDict[f'{tabname}-visible'] = True
            log.selectTab(tabname)
        else:
            # Create our tab for the first time
            log.createTab(tabname, widget = w,
                          createText = False)
            log.selectTab(tabname)
            log.contentsDict[f'{tabname}-loaded'] = True
            log.contentsDict[f'{tabname}-visible'] = True
            log.contentsDict[f'{tabname}-widget'] = w


By "widget", I mean a PyQt widget.  It will usually contain other GUI widgets and code.  Here's how this function would typically be called.  The widget does not need to have the signature shown here, but it's an obvious way to get Leo's key objects into the app.

log = c.frame.log
TABNAME = 'The App'
import theApp
w = theApp.MainWidget(g, c)
toggle_app_tab(log, TABNAME, w)


Notice that when we "remove" the app - meaning the app's tab is removed from the Log frame - we keep a reference to the main widget.  This way, when we make the tab visible again, the app will have retained all its state and you can pick up where you left off.

I have attached a screen shot of one of my tabbed apps in session.

tabbed_app_example.png

Edward K. Ream

unread,
Oct 31, 2022, 4:57:04 AM10/31/22
to leo-e...@googlegroups.com
On Sun, Oct 30, 2022 at 10:09 AM Thomas Passin <tbp1...@gmail.com> wrote:

I have made several apps (really, "applets") to run in tabs in Leo's Log frame.  I find this convenient because they don't need to take up a whole panel, as Viewrendered/Viewrendered3 do, and they don't even need to be plugins so they can be simpler.  They are mostly intended to be used in a specific outline; their code can reside in the outline.  

Thanks for this how-to.  I think it would make a good info issue. Are you willing to create one?

Edward

Thomas Passin

unread,
Oct 31, 2022, 8:10:07 AM10/31/22
to leo-editor
Yes.  

Edward K. Ream

unread,
Oct 31, 2022, 9:44:30 AM10/31/22
to leo-e...@googlegroups.com


On Mon, Oct 31, 2022 at 7:10 AM Thomas Passin <tbp1...@gmail.com> wrote:
Yes. 

Thanks for this how-to.  I think it would make a good info issue. Are you willing to create one?

Thanks.

Edward

jkn

unread,
Oct 31, 2022, 11:12:14 AM10/31/22
to leo-editor
Thanks, this looks very interesting...

I have one question - something I have wondered about before. If I have your example code as a button command, say ... then where/how can I put the

def toggle_app_tab(log, tabname, top_widget):
    # ...

code, in order for it to accessible by multiple such buttons/commands? I've never been sure about this.

Thanks, J^n

Jacob Peck

unread,
Oct 31, 2022, 11:17:49 AM10/31/22
to leo-e...@googlegroups.com
I generally make a node with a unique headline somewhere, like '@common code-xyz' that has the function definition:

---
def hello():
    g.es('hello')
---

Then in my @button:

@button run-hello
---
# load the common code 'library'
pos = g.findNodeAnywhere(c, '@common code-xyz')
exec(g.getScript(c, pos))

# call the code
hello()
---

Hope this helps!
Jake


--
You received this message because you are subscribed to the Google Groups "leo-editor" group.
To unsubscribe from this group and stop receiving emails from it, send an email to leo-editor+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/leo-editor/3b9b231b-220f-4d95-8f65-ff89f6ff24d5n%40googlegroups.com.

jkn

unread,
Oct 31, 2022, 11:23:57 AM10/31/22
to leo-editor
Interesting, thanks! I will be interested to see if there are other options...

Regards, J^n

Thomas Passin

unread,
Oct 31, 2022, 1:05:53 PM10/31/22
to leo-editor
On Monday, October 31, 2022 at 11:12:14 AM UTC-4 jkn wrote:
Thanks, this looks very interesting...

I have one question - something I have wondered about before. If I have your example code as a button command, say ... then where/how can I put the

def toggle_app_tab(log, tabname, top_widget):
    # ...

code, in order for it to accessible by multiple such buttons/commands? I've never been sure about this.

Thanks, J^n
[snip]

For the function to be available to all outlines, I'm sure people have various ideas.  One thing I've done in the past (with other functions) is to assign it to some object.  I tend to use c, the outline's commander.  If Edward ever changes it to use slots, I suppose this wouldn't work any more.  Using this technique, your launch command would first look to see if it has been assigned to c.  If not, it runs the def and assigns it.  Then it would be invoked.  Something like this sketch -

has_tabbed_app_toggle = hasattr(c, 'toggle_app_tab')
if not has_tabbed_app_toggle:
    def toggle_app_tab(log, tabname, top_widget):
          # Body of function here
    c.toggle_app_tab = toggle_app_tab

# Invoke function here

You can even make the function into a method of c.  Other people may do that differently, but I think the easiest way is 

c.has_tabbed_app_toggle = has_tabbed_app_toggle.__get__(c)

If you did this, you would need to change the function to include self, and you would need to use self instead of c in the code.

There is also a Leo decorator, g.CommanderCommand, that you could use (it would have to be run first before the function could be used, and your function cannot use "g"):

@g.CommanderCommand('z-f1')
def z_f1(self, x, y):
    print(x * y)


Then:
c.z_f1(2, 3)
# prints 6

(It seems to me that there was something recently about not using this decorator, but I can't remember for sure).

Thomas Passin

unread,
Oct 31, 2022, 1:46:00 PM10/31/22
to leo-editor
Using @g.CommanderCommand would probably be better, since once run it would be available to all outlines.

Thomas Passin

unread,
Oct 31, 2022, 2:43:08 PM10/31/22
to leo-editor
I don't know if Edward would approve, but this also works and seems convenient:

If you execute this:

@g.command('y-f1')
def y_f1(x, y):
    g.es(x * y, c)


Then in any outline you can do this:

d = g.global_commands_dict
d['y-f1'](2,3)
# Prints 6

Thomas Passin

unread,
Oct 31, 2022, 3:54:57 PM10/31/22
to leo-editor
All right, then, putting these pieces together:

1. In myLeoSettings.leo, in the @settings/@commands tree,  add an @command node that defines and registers the function:

@command reg-y-f1  # Node headline
# Node body:
@g.command('y-f1')  # A very simplified function for expository purposes
def y_f1(x, y):
    g.es(x * y)


When the settings are loaded, this will register the command def-y-f1 that will define and register your function when it is run.

2. In any outline, when you want to invoke the function y_f1(2, 3):

cmmd = g.global_commands_dict.get('y-f1', None)
if not cmmd:
    c.k.simulateCommand('reg-y-f1')  # Register the function
    cmmd = g.global_commands_dict['y-f1']
cmmd(2,3)

# prints 6

It's a little clunky, but not too bad...  I don't know if it can be made simpler, because Leo doesn't support auto-run of commands on loading an outline.  I'm sure that's a wise design decision, since auto-run code could be a security risk.  I could imagine a plugin that registered and ran all your special commands on startup, but that's would be way overkill.

Edward K. Ream

unread,
Nov 1, 2022, 5:02:46 AM11/1/22
to leo-e...@googlegroups.com
On Mon, Oct 31, 2022 at 2:54 PM Thomas Passin <tbp1...@gmail.com> wrote:

I don't know if it can be made simpler, because Leo doesn't support auto-run of commands on loading an outline. 

You should be able to run any command from a plugin using the after-create-leo-frame event.

Edward
Reply all
Reply to author
Forward
0 new messages