Using multiple layouts

36 views
Skip to first unread message

Edward K. Ream

unread,
Jul 20, 2024, 8:00:24 AM (2 days ago) Jul 20
to leo-editor
We now have several scripts that change Leo's layouts. Let's call the results of those scripts alternate layouts.

Those scripts suffice if an outline only uses one layout at a time. But what if we want to switch between layouts?

- How can we switch between alternate layouts?
- How can we revert to Leo's default layout?

After some experimentation, I saw the answer. Each layout script must define its own undoer. There is no other way. This creates the following (tested) top-level pattern for layout scripts:

"""Change layout to Jacob Peck's quadrants layout."""

from typing import Any
from leo.core.leoQt import Orientation, QtWidgets
h = c.hash()
pc = g.app.pluginsController

@others  # Define helpers

undo_layout()

if pc.isLoaded('leo.plugins.viewrendered3'):
    import leo.plugins.viewrendered3 as vr3
    try:
        parent = vr3.controllers[h]
    except KeyError:
        vr3.controllers[h] = parent = vr3.ViewRenderedController3(c)
    switch_layout('vr3-show', parent)
    c.layout_undoer = undoer

if pc.isLoaded('leo.plugins.viewrendered'):
    import leo.plugins.viewrendered as vr
    parent = vr.controllers[h]
    switch_layout('vr-show', parent)
    c.layout_undoer = undoer


Here is the latest switch_layout method:

def switch_layout(cmd: str, parent: Any) -> None:
   
    gui = g.app.gui

    def make_splitter():
        w = QtWidgets.QSplitter()
        w.setObjectName('body_splitter')
        return w

    ms = gui.find_widget_by_name(c, 'main_splitter')
    ss = gui.find_widget_by_name(c, 'secondary_splitter')
    edf = gui.find_widget_by_name(c, 'bodyFrame')

    # add vertical splitter to ms (hosting only the editor)
    body_splitter = make_splitter()
    body_splitter.setOrientation(Orientation.Vertical)
    ms.addWidget(body_splitter)
    body_splitter.addWidget(edf)

    if not parent.isVisible():
        body_splitter.addWidget(parent)
        body_splitter.setSizes([100_000] * len(body_splitter.sizes()))
        ms.setSizes([100_000] * len(ms.sizes()))
        ss.setSizes([100_000] * len(ss.sizes()))
        c.doCommandByName(cmd)

There are two undo helpers defined as follows:

def undo_layout():
    layout_undoer = getattr(c, 'layout_undoer', None)
    if layout_undoer:
        layout_undoer(c)
        c.layout_undoer = None

def undoer(c):
    print('')
    print('*** quadrants layout undoer')
    print('')


The undoer function shown above is merely a placeholder.

Summary

Each layout script must define a bespoke undoer.  That's my next task.

Edward

Thomas Passin

unread,
Jul 20, 2024, 8:19:16 AM (2 days ago) Jul 20
to leo-editor
I think the undoers should be pushed on a stack.  The commander will have a method that pops the stack and executes that single undoer. This approach lets a user change the layout, and then change it again without losing the ability to return to an earlier layout.  

There could be a method to walk back to the stack bottom, which would be the initial layout.  I wonder if Qt can freeze visible changes during this process, to avoid too much screen flashing.

Edward K. Ream

unread,
Jul 20, 2024, 8:39:40 AM (2 days ago) Jul 20
to leo-e...@googlegroups.com
On Sat, Jul 20, 2024 at 7:19 AM Thomas Passin <tbp1...@gmail.com> wrote:
I think the undoers should be pushed on a stack. 

My idea was that undoers always reverted to the original (default) layout. I still think this is a reasonable starting point.

We can discuss this further after undoers are finished.

Edward

Edward K. Ream

unread,
Jul 20, 2024, 8:47:13 AM (2 days ago) Jul 20
to leo-e...@googlegroups.com
On Sat, Jul 20, 2024 at 7:00 AM Edward K. Ream <edre...@gmail.com> wrote:
We now have several scripts that change Leo's layouts. Let's call the results of those scripts alternate layouts.

A few clarifications:

This post should have been labeled as an Engineering Notebook post. It's not of general interest.

Undoers revert the layout to the original unmodified layout. In other words, every alternate layout starts from a fixed starting point.

All these layout changers seem like a lot of work, but the work definitely is worth doing. In contrast to the situation with the free_layout plugin, each layout changer provides a resource that users can enable with ease.

Finally, the code given earlier should remain valid regardless of packaging, that is, no matter whether the code resides in `@button`, `@command`, a plugin or (eventually), as a new Leo command.

Edward

Thomas Passin

unread,
Jul 20, 2024, 8:51:06 AM (2 days ago) Jul 20
to leo-editor
I think there are two potential approaches to undoers:

1. Impose a specific layout regardless of the current layout;
2. Reverse the changes made by the current layout.

I think that 1. would be the preferred approach but seems hard.  Approach 2. should be fairly easy but won't be able to return to an arbitrary initial layout.  Suppose the user makes the following change:  L0 -> L1 - >L2.  L2's undoer knows how to get back to L1.  For L1 to know how to get back to L0, its undoer has to have been saved somewhere. That could be in a stack or dictionary, or in a Layout object.  If the later, the Layout objects need to be saved somewhere.  If in a dictionary, the system needs to know how to find the key.  In a stack, the only knowledge needed is how to find and pop the stack.

Edward K. Ream

unread,
Jul 20, 2024, 10:00:45 AM (2 days ago) Jul 20
to leo-e...@googlegroups.com
On Sat, Jul 20, 2024 at 7:51 AM Thomas Passin <tbp1...@gmail.com> wrote:
I think there are two potential approaches to undoers:

1. Impose a specific layout regardless of the current layout;
2. Reverse the changes made by the current layout.

Good point. I was thinking of 2. If done consistently, as I envisage, undo would then yield the original layout.

Imo, this approach is the easiest and best because it is stateless. Say an outline has three @button scripts that change layouts. You can hit any button at any time.

A stack-based approach is more complicated because the stack becomes a hidden state. Imo, there is no advantage to such an approach.

Summary

My undoers will undo the changes made by the layout script, thereby yielding the original (unchanged) layout. All layout scripts will undo any previous layouts, then apply their own layout.

Edward

Jacob Peck

unread,
Jul 20, 2024, 10:25:17 AM (2 days ago) Jul 20
to leo-e...@googlegroups.com
Is there no way to simply call the code that creates the default layout in the first place?  If possible, it would obviate the need for any undoers at all.  Just call it as the first thing any layout script does.

That would shift to approach #1 rather than approach #2, but if the benefit of the new API is the ability to define a static layout, perhaps the initial layout itself should be defined in such a way (if it is not already), and simply applied with force before tweaking via the rest of the scripts.

Just a thought.  I have not looked into the code to see if this is feasible.
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/CAMF8tS07Vzsx9VoqPvPr45j7YuBcfb-Txuwoi9SKpvv0-Ja39A%40mail.gmail.com.

Edward K. Ream

unread,
Jul 20, 2024, 10:31:48 AM (2 days ago) Jul 20
to leo-e...@googlegroups.com
On Sat, Jul 20, 2024 at 9:25 AM Jacob Peck <gates...@gmail.com> wrote:
Is there no way to simply call the code that creates the default layout in the first place?  If possible, it would obviate the need for any undoers at all.  Just call it as the first thing any layout script does.

That was the first approach I tried.  Some of the layout-changing scripts insert "helper" widgets and make other tweaks, so no general solution is likely, much less an easy-to-understand solution.

The situation is analogous to Leo's under code in leoUndo.py. That code uses custom undoers.

Edward

Jacob Peck

unread,
Jul 20, 2024, 10:36:50 AM (2 days ago) Jul 20
to leo-e...@googlegroups.com
On Sat, Jul 20, 2024 at 10:31 AM Edward K. Ream <edre...@gmail.com> wrote:

That was the first approach I tried.  Some of the layout-changing scripts insert "helper" widgets and make other tweaks, so no general solution is likely, much less an easy-to-understand solution.

Sorry for my ignorance on the subject, but can't you:

- find the top level frame, and destroy every child widget underneath it (recursively if needed)
- rebuild the initial layout inside that top level frame (call some c.buildGui function that would do the work)?

Not trying to distract from the undoers, it's a fine solution.  Just curious if I'm missing something obvious here.

Jake

Edward K. Ream

unread,
Jul 20, 2024, 11:12:13 AM (2 days ago) Jul 20
to leo-e...@googlegroups.com
On Sat, Jul 20, 2024 at 9:36 AM Jacob Peck <gates...@gmail.com> wrote:

Sorry for my ignorance on the subject, but can't you:

- find the top level frame, and destroy every child widget underneath it (recursively if needed)
- rebuild the initial layout inside that top level frame (call some c.buildGui function that would do the work)?

You would likely have to call the VR and VR3 event handlers (in the correct order!) to recreate all the widgets.

I'm working on an undoer right now. It's tricky enough as it is, but it looks doable.

Edward

Edward K. Ream

unread,
Jul 20, 2024, 11:50:44 AM (2 days ago) Jul 20
to leo-e...@googlegroups.com
On Sat, Jul 20, 2024 at 9:36 AM Jacob Peck wrote:

> can't you [do something general :-)]

I usually approach tricky tasks by writing specific code first.

Here is a prototype undoer for the "quadrants" layout. It's in a separate node for prototyping:

h = c.hash()
pc = g.app.pluginsController
gui = g.app.gui
vr_splitter = gui.find_widget_by_name(c, 'vr-body-splitter')
vr3_splitter = gui.find_widget_by_name(c, 'vr3-body-splitter')
body_frame = gui.find_widget_by_name(c, 'bodyFrame')
parent = (
    vr_splitter.parent() if vr_splitter
    else vr3_splitter.parent() if vr3_splitter
    else None
)
if vr_splitter:
    vr_splitter.deleteLater()
if vr3_splitter:
    vr3_splitter.deleteLater()
if parent:
    parent.addWidget(body_frame)
    parent.setSizes([100_000] * len(parent.sizes()))

Notice that there may be two body splitters, now each with its own separate name.

This works, as far as the gui goes, but deleting the splitters causes repeated crashes both the VR and VR3 plugins:

Traceback (most recent call last):
 File "C:\Repos\leo-editor\leo\core\leoPlugins.py", line 348, in callTagHandler
    result = handler(tag, keywords)
 File "C:\Repos\leo-editor\leo\plugins\viewrendered.py", line 363, in onClose
    vr.deleteLater()
RuntimeError: wrapped C/C++ object of type ViewRenderedController has been deleted

Traceback (most recent call last):
  File "C:\Repos\leo-editor\leo\core\leoPlugins.py", line 348, in callTagHandler
    result = handler(tag, keywords)
             ^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Repos\leo-editor\leo\plugins\viewrendered3.py", line 1475, in onClose
    vr3.deleteLater()
RuntimeError: wrapped C/C++ object of type ViewRenderedController3 has been deleted
...

These crashes aren't too surprising, but fixing them is another matter :-)

Summary

My usual method is to generalize after I know all the details for a particular task.  YMMV.

Edward

Edward K. Ream

unread,
Jul 20, 2024, 12:17:11 PM (2 days ago) Jul 20
to leo-e...@googlegroups.com
On Sat, Jul 20, 2024 at 10:50 AM Edward K. Ream wrote:

> I usually approach tricky tasks by writing specific code first.

Heh. The example code wasn't specific enough. It used only QWidget methods, forgetting about QSplitter methods. It should be possible to move the VR/VR3 panes without destroying them.

Edward

Thomas Passin

unread,
Jul 20, 2024, 1:14:43 PM (2 days ago) Jul 20
to leo-editor
The most obvious way is to use try...except blocks.

if vr_splitter:
    vr_splitter.deleteLater()

would become:
try:
    vr_splitter.deleteLater()
except:
    pass

Presumably this would not prevent unhooking the vr object from Leo's event syste,

Jacob Peck

unread,
Jul 20, 2024, 2:53:11 PM (2 days ago) Jul 20
to leo-e...@googlegroups.com
The vr-related crashes are already happening in Leo, I reported a bug on it earlier this week: https://github.com/leo-editor/leo-editor/issues/4009

I suspect it's the same bug, and nothing new your scripting and tests are causing :)

Thanks for the explanations and patience -- I was caught up in the CrowdStrike mess yesterday and am severely lacking sleep...

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.

Jacob Peck

unread,
Jul 20, 2024, 2:53:32 PM (2 days ago) Jul 20
to leo-e...@googlegroups.com
Err, s/crashes/errors-in-log/g.  Mea culpa.

Edward K. Ream

unread,
Jul 20, 2024, 3:50:32 PM (2 days ago) Jul 20
to leo-e...@googlegroups.com
On Sat, Jul 20, 2024 at 12:14 PM Thomas Passin <tbp1...@gmail.com> wrote:
The most obvious way is to use try...except blocks.

try:
    vr_splitter.deleteLater()
except:
    pass

I doubt that this would cure the real issue. The deleteLater seems to delete the controller itself.

Edward

Edward K. Ream

unread,
Jul 20, 2024, 4:05:50 PM (2 days ago) Jul 20
to leo-e...@googlegroups.com
On Sat, Jul 20, 2024 at 1:53 PM Jacob Peck wrote:

The vr-related crashes are already happening in Leo, I reported a bug on it earlier this week: https://github.com/leo-editor/leo-editor/issues/4009

Thanks for the link. I was not aware of the issue.

I suspect it's the same bug, and nothing new your scripting and tests are causing :)

I wouldn't put it that way.

#4009 presumably happens because the VR/VR3 controller isn't properly destroyed. That bug should be relatively straightforward to fix.

But ideally, a layout undoer should preserve the existing VR/VR3 controller and its gui. You would think it would be straightforward to "relocate" the VR/VR3 pane, but I haven't discovered how to do that yet. There seems to be a hole in the QSplitter API.

As if that weren't enough, both the VR and VR3 startup logic can create different widgets (in different places) depending on user settings! It's a puzzle.

Edward

P.S. The free_layout controller sidestepped these issues by creating the VR/VR3 controllers (and widgets) after changing the splitter arrangements. That strategy is not available to us in this project. That does not mean we would be better off using the free_layout plugin!

EKR

Edward K. Ream

unread,
Jul 20, 2024, 4:09:56 PM (2 days ago) Jul 20
to leo-e...@googlegroups.com
On Sat, Jul 20, 2024 at 1:53 PM Jacob Peck wrote:

> The vr-related crashes are already happening in Leo, I reported a bug on it earlier this week: https://github.com/leo-editor/leo-editor/issues/4009

I'll turn my attention to #4009 first so it won't complicate the layout-related work.

Edward

Thomas Passin

unread,
Jul 20, 2024, 4:28:44 PM (2 days ago) Jul 20
to leo-editor
"But ideally, a layout undoer should preserve the existing VR/VR3 controller and its gui. You would think it would be straightforward to "relocate" the VR/VR3 pane, but I haven't discovered how to do that yet."

I found with some of the other scripts that if I inserted a VR/VR3 widget into a different splitter it magically disappeared from the first one. I don't know how Qt manages that, but I haven't noticed any gotchas so far.

Edward K. Ream

unread,
Jul 20, 2024, 5:03:16 PM (2 days ago) Jul 20
to leo-e...@googlegroups.com
On Sat, Jul 20, 2024 at 3:28 PM Thomas Passin <tbp1...@gmail.com> wrote:

"But ideally, a layout undoer should preserve the existing VR/VR3 controller and its gui. You would think it would be straightforward to "relocate" the VR/VR3 pane, but I haven't discovered how to do that yet."

I found with some of the other scripts that if I inserted a VR/VR3 widget into a different splitter it magically disappeared from the first one. I don't know how Qt manages that, but I haven't noticed any gotchas so far.

Thanks for the hint! It looks promising.

Edward
Reply all
Reply to author
Forward
0 new messages