ENB: Expanded/collapsed status of outline nodes

51 views
Skip to first unread message

vitalije

unread,
Sep 18, 2020, 7:14:22 AM9/18/20
to leo-editor
Currently Leo decides whether position should be expanded using list of expanded positions which is kept as an ivar on the v node instance (v.expandedPositions). This schema allows Leo to show some clones as expanded while others as collapsed. The problem is that positions in these lists can become invalid after some outline changes (like inserting or deleting a node).

The other problem is with nodes that are not clones, but have one or more ancestors that are clones. Leo currently doesn't allow this positions to have separate expanded/collapsed state. Actually user can collapse one of such nodes, independently of other instances of same node, but if user expands one of those nodes, then all other instances will be expanded too.

Nodes that appear only once in the outline are immune to this kind of problems. For such nodes p.v.isExpanded() bit is sufficient to answer the question whether position should be expanded or not. So, only when showing cloned node and its subtree, Leo needs some way to distinguish between one v-node occurrence and another.

We can look at the positions as some kind of labels for v-node occurrences in the outline. Different occurrences have different labels (positions). But this is not the only possible way to enumerate all different v-node occurrences with different labels.

Let's look at different way to make different labels for different occurrences of the same v-node. Instead of keeping child index in each level, let's keep the number of occurrences of the same v-node among previous siblings. This number paired with the gnx of v-node unmistakably identifies every child. By joining all ancestor's labels we can have a string which can be used as a label for every position. Here is the code to calculate the label for any given position:
def make_label(p):
   
def it():
       
yield from p.stack
       
yield p.v, p._childIndex
   
def it2():
        root
= c.hiddenRootNode
       
for v, i in it():
           
yield root, v, i
            root
= v
   
def it3():
       
for parv, ch, i in it2():
            j
= len([x for x in parv.children[:i] if x == ch])
           
yield f'{ch.fileIndex}[{j}]'
   
return ' '.join(it3())


This label is more imune to outline changes. The only outline change that can cause these labels to become invalid is inserting a clone node or deleting one. But in that case we can easily calculate what should be the values of new labels after change.

For example:

def inserted_clone(expandedLabels, parentPos, oldParentLabel, index):
   
'''Updates expandedLabels set after insertion of a cloned node
    at given index. parentPos is position of parent node, and oldParentLabel
    is label that parentPos had before.
    '''

    par
= parentPos.v
    ch
= par.children[index]
    j
= 0
    changes
= []
   
for i, v in enumerate(par.children):
       
if v != ch: continue
       
if index <= i:
            a
= f'{oldParentLabel} {v.fileIndex}[{j-1}]'
            b
= f'{oldParentLabel} {v.fileIndex}[{j}]'
            oldlabels
= set(x for x in expandedLabels if x.startswith(a))
            newlabels
= set(x.replace(a, b) for x in oldlabels)
            changes
.append(
               
( oldlabels
               
, newlabels
               
))
        j
+= 1
   
for a, b in changes:
        expandedLabels
-= a
        expandedLabels
+= b

def deleted_clone(expandedLabels, parentPos, oldParentLabel, index):
   
'''Updates expandedLabels set after deletion of a cloned node
    at given index. parentPos is position of parent node, and oldParentLabel
    is label that parentPos had before.
    '''

    par
= parentPos.v
    ch
= par.children[index]
    j
= 0
    changes
= []
   
for i, v in enumerate(par.children):
       
if v != ch: continue
       
if index <= i:
            a
= f'{oldParentLabel} {v.fileIndex}[{j+1}]'
            b
= f'{oldParentLabel} {v.fileIndex}[{j}]'
            oldlabels
= set(x for x in expandedLabels if x.startswith(a))
            newlabels
= set(x.replace(a, b) for x in oldlabels)
            changes
.append(
               
( oldlabels
               
, newlabels
               
))
        j
+= 1
   
for a, b in changes:
        expandedLabels
-= a
        expandedLabels
+= b

These two functions can update set of expanded labels after insertion of a node that was already among children of the parent node, or when deleting node which have clones among its siblings. In any other kind of outline change there is no need to update expanded labels. This means cff and cfa commands won't need to call inserted_clone function, nor deletion of nodes created with those commands would have to call deleted_clone function. Only when inserted node appears more than once among its siblings, or when deleting such node, labels may change, and only then is necessary to call one of these two functions to update set of expanded labels.

In case that user script makes some changes to the outline without calling these functions, nothing horrible would happen. The worse thing that could happen is that some of the clones would loose their expanded/collapsed state. But most of the ordinary nodes will have their expanded state preserved.

Your comments.
Vitalije

Edward K. Ream

unread,
Sep 24, 2020, 7:34:36 AM9/24/20
to leo-editor
On Friday, September 18, 2020 at 6:14:22 AM UTC-5, vitalije wrote:

> Let's look at different way to make different labels for different occurrences of the same v-node. Instead of keeping child index in each level, let's keep the number of occurrences of the same v-node among previous siblings. This number paired with the gnx of v-node unmistakably identifies every child. By joining all ancestor's labels we can have a string which can be used as a label for every position.

Thanks for this excellent post. The analysis is helpful and the idea is interesting.

> Here is the code to calculate the label for any given position: [snip]
 
> The only outline change that can cause these labels to become invalid is inserting a clone node or deleting one. But in that case we can easily calculate what should be the values of new labels after change. [snip]

> [The inserted_clone and deleted_clone] functions can update set of expanded labels after insertion of a node that was already among children of the parent node, or when deleting node which have clones among its siblings. In any other kind of outline change there is no need to update expanded labels.

Could Leo's Position class call these functions automagically? That would seem to be a complete solution.

> Your comments.

This seems like a promising strategy.

Edward

vitalije

unread,
Sep 24, 2020, 8:16:23 AM9/24/20
to leo-editor

Could Leo's Position class call these functions automagically? That would seem to be a complete solution.


This two functions can be methods of c, and c could have an attribute expandedLabels. I guess the outline manipulation methods located in Position class, can call these c methods when appropriate to update c.expanded_labels set.
 
This seems like a promising strategy.

Edward

I am glad we agree on this. It looks like a promising strategy to me too.
Vitalije

Edward K. Ream

unread,
Sep 24, 2020, 9:38:05 AM9/24/20
to leo-editor
On Thu, Sep 24, 2020 at 7:16 AM vitalije <vita...@gmail.com> wrote:

Could Leo's Position class call these functions automagically? That would seem to be a complete solution.

This two functions can be methods of c, and c could have an attribute expandedLabels. I guess the outline manipulation methods located in Position class, can call these c methods when appropriate to update c.expanded_labels set.

On second thought, I agree that the c methods are the proper place to handle this. As you said earlier, the clone-find commands never need to do anything.

How do you want to go forward?

Edward

vitalije

unread,
Sep 24, 2020, 10:01:24 AM9/24/20
to leo-editor

How do you want to go forward?


I wrote about this idea to check whether it has any chance of being accepted or not. As it seems you are willing to give it a chance, I might do it in the separate branch. However, I have some more ideas (not fully formulated yet) that I wish to explore before going into the implementation. Those ideas might have some influence on the final implementation.

Vitalije

Edward K. Ream

unread,
Sep 24, 2020, 12:46:57 PM9/24/20
to leo-editor
On Thu, Sep 24, 2020 at 9:01 AM vitalije <vita...@gmail.com> wrote:

How do you want to go forward?


I wrote about this idea to check whether it has any chance of being accepted or not. As it seems you are willing to give it a chance, I might do it in the separate branch. However, I have some more ideas (not fully formulated yet) that I wish to explore before going into the implementation. Those ideas might have some influence on the final implementation.

Go for it!

Edward
Reply all
Reply to author
Forward
0 new messages