Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix loading collapsed tabs and legacy session support #8084

Conversation

toofar
Copy link
Member

@toofar toofar commented Jan 28, 2024

Relating to #8076 and following on from a2f98aa this PR proposes a fix and an improvement:

fix loading of tab in collapsed tab groups

Tabs in collapsed tab groups were being dropped because the code was iterating over the tabs loaded into the tab widget instead of all those in the tree. Easy spot, easy fix.

support loading legacy tree tab session formats

There are some long time community members who have been using this PR for a while and may have built up complicated session files which they are dreading having to rebuild. Currently if they pull the latest on the tree-tab-integration branch the browser will just crash on startup.

I had a look at how hard it would be to continue supporting the previous session format and it turned out to be pretty easy by copying some old code and adding a couple of conditionals. A few two many conditionals for my tastes but at least they are tiny.

bonus: remove unused attribute

Tree tabs were ending up in session files with their uid in both the node and uid attribute. It looks like only the uid one is being used.

PS: I haven't reviewed a2f98aa yet, but I've tested it! Everything seems to be working as it was. And from a brief look the diff sure looks smaller. I think _save_all() looks much cleaner iterating on tabs like normal and then taking a quick diversion to do add tab data
PPS: I've seen another issue where if you load a non-tree session while sessions are enabled every tab will be opened as "related" so instead of a flat set of tabs loaded you get a staircase. This might depend on what your new_position settings are set to. This could be fixed by setting related=True in the call to tabopen() but I think this is exposing a problem you can run into with the existing session loading too. Eg tabopen has related=True false and that isn't overriden by the session code and the session code passes background=False. Both of which seem incorrect, so it seems the session code is using knowledge of the positioning code in tabbedbrowser instead of overriding it completely (eg background=False, related=False, idx=i). So since I think it's a preexisting thing I would prefer to wait until we fix up the tests around sessions (lots of them are disabled on webengine) before fixing it "properly". But if someone complains I suppooose fixing it in an isolated manner is okay. TODO: raise a ticket to track this resolved in this PR
Edit: this also causes issues with:

- about:blank?one
  - about:blank?two
    - about:blank?three (collapsed) (active)
      - about:blank?four
- about:blank?five

If you load the session tab five is also collapsed. If you toggle collapse it gets restored to the correct visibility. This seems to be happening in _position_tab() because related is set to True. Not sure why it's only happening when there is a collapsed tab, or why the child.parent = root_node at the end of _load_tree() isn't fixing it. But it gets fixed with related=False anyhow...
Hmm, had a look at the e2e tests, which do work on webengine, they don't reproduce this issue because they test against a saved session and the session saves fine, it's the tabs in the browser that are borked (how!). Well, session-load does have some coverage so might just have to be a "it doesn't make things worse" change. Either that or we add a :debug-dump-tree $winid command.
Anyway, you can reproduce manually with python3 -m qutebrowser -T -s tabs.tree_tabs true -s tabs.position left ":open about:blank?one" ":open -tr about:blank?two" ":open -tr about:blank?three" ":open -t about:blank?four" ":tab-focus 2" ":tree-tab-toggle-hide" ":cmd-later 300 session-save asdf" ":cmd-later 350 session-load asdf" then look how one window looks like it has less tabs than the other one!

@toofar toofar requested a review from pinusc January 28, 2024 07:10
@toofar toofar changed the title Tree/8076 fix loading collapsed tabs and legacy session support Fix loading collapsed tabs and legacy session support Jan 28, 2024
@toofar toofar added the component: tree tabs Issues related to tree tabs functionality label Jan 28, 2024
Copy link
Collaborator

@pinusc pinusc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests always sound like a good idea.

I don't like the idea of .widgets() returning things that aren't visible. Maybe adding a method .tabs() to TabbedBrowser that is just an alias of widgets, overridden in TreeTabbedBrowser to return all tabs, including collapsed. I don't think there's any places in treetabs where .widgets() explicitly expects only visible tabs, but this half-rename would also ensure compatibility. Maybe that's overcomplicating things.

Copy link
Collaborator

@pinusc pinusc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did think people might be unhappy with the breaking change 😟

I didn't think legacy code would be great to lug around, but I'm not the maintainer here... that being said, we could keep this legacy code for now, adding a deprecation prompt, and then removing it altogether before merging into main. Loading the session with this code and then immediately saving it should "update" the old session file. This gives users a chance to update their sessions, essentially serving as a conversion script of sorts; but then we don't have to keep legacy code in.

@toofar
Copy link
Member Author

toofar commented Feb 5, 2024

I don't like the idea of .widgets() returning things that aren't visible. Maybe adding a method .tabs() to TabbedBrowser that is just an alias of widgets, overridden in TreeTabbedBrowser to return all tabs, including collapsed.

Yeah, good point. And thanks for engagement with a comment buried in a commit message!

I wanted to have a look at what existing places in the code would do if TabbedBrowser.widgets() started returning hidden tabs. I used semgrep to search for $OBJECT.widgets() and $OBJECT.widget.count(). I came up with the following classification, as in "could the calling code handle invisible tabs":

shouldn't (it would break)
11 instances
could- (with changes, but it wouldn't change behaviour anyway)
5 instances
could+ (with changes, could be an enhancement)
1 instances
should (maybe we should fix this, I added it to https://github.com/orgs/qutebrowser/projects/3/views/2?pane=issue&itemId=52187228)
1 instances
can (eg if widgets() started returning them now)
4 instances

So the majority of places where tabs are iterated over or counted at the moment are specifically to do with moving or focusing tabs and wouldn't deal well with tabs that weren't in the tab widget as things are.

Full notes from those greps:
qutebrowser/browser/commands.py
      899┆ for i, tab in enumerate(self._tabbed_browser.widgets()):

      Iterates through all tabs to close them. Works fine with tree tabs
      because the top tab of a collapsed group will always be in the widget.
      Does have different behaviour if you have a collapsed tab vs not a
      collapsed tab though. If you have selected an uncollapsed tab with
      children and run :tab-only you are left with only the selected tab left.
      If you have selected a collapsed tab with children, you are left with
      the tab and it's children left. I think there is an argument made that
      that should be consistent in either directing but I don't think that's a
      blocker (it's not clear to me what the correct thing to do is anywhow).

      Can handle hidden tabs: not as it is currently, `TabbedBrowser._remove_tab()`
      calls `self.widget.indexOf()` and complains if it doesn't find it.
      Classification: shouldn't

qutebrowser/browser/qutescheme.py
      190┆ for tab in tabbed_browser.widgets():
      
      Intent: for qute://tabs
      Can handle hidden tabs: yes
      Classification: can

qutebrowser/browser/webengine/notification.py
      361┆ for idx, tab in enumerate(tabbedbrowser.widgets()):

      Intent: when someone clicks on a notification we try to match the url on
        the notifications with a tab to focus.
      Can handle hidden tabs: not as it is currently, would have to learn to
        un-collapse a tab group, and probably make it obvious that that
        happened. Would be an interesting to figure out what affordance we
        could give the user that "a tab in this collapsed group wants focus",
        but probably something for another day.
      Classification: could+

qutebrowser/completion/models/miscmodels.py
      202┆ tab_titles = (tab.title() for tab in tabbed_browser.widgets())

      Intent: lists tab titles in the :tab-give completion so you get a better
        idea of the window you are going to be sending a tab too. 
      Can handle hidden tabs: yes, but probably don't need hidden tabs showing
        here. Although I don't undestand why this is shoving all of the tab
        titles on to the end of the category name anyway? Seems really hard to
        read, maybe a hack to allow completing on tab titles? Hmmm
      Classification: can

qutebrowser/mainwindow/tabbedbrowser.py
      425┆ for idx, tab in enumerate(reversed(self.widgets())):
      
      Intent: close all the tabs
      Can handle hidden tabs: same as tab-only, not currently
      Classification: shouldn't

      738┆ for tab in self.widgets():

      Intent: update favicons for all loaded tabs
      Can handle hidden tabs: nope
      Classification: shouldn't

qutebrowser/misc/crashsignal.py
      120┆ for tab in tabbed_browser.widgets():

      Intent: save open URLs on crash for the purpose of restoring them
      Can handle hidden tabs: yes, and probably should!
      Classification: should

qutebrowser/app.py
      310┆ if cur_window.tabbed_browser.widget.count() == 0:

      Intent: see if there are any tabs open
      Can handle hidden tabs: yes
      Classification: can

qutebrowser/browser/commands.py
       63┆ return self._tabbed_browser.widget.count()

       Aliased to self._count(), used in:

       def tab_give:
       Intent: see if we are going to remove the last tab
       Can handle hidden tabs: yes, but doesn't need to as the last tab will
         aways be visible. As a note this check is broken with --recursive
       Classification: could-
       
       def tab_prev and tab_next:
       Intent: check if we have any tabs open
       Can handle hidden tabs: no (can't focus them), and shouldn't need to
       Classification: shouldn't

       def tab_focus:
       Intent: to validate the index
       Can handle hidden tabs: no (can't focus them), and shouldn't need to
       Classification: shouldn't

       def tab_move:
       Intent: to validate and wrap the index
       Can handle hidden tabs: no, the current logic ignores them, which is
         probably fine
       Classification: shouldn't

       def _cntwidget()
       Intend: return a widget by index
       Can handle hidden tabs: no
       Classification: shouldn't

      1068┆ if not 0 < idx <= tabbed_browser.widget.count():

      Intent: validate tab index for tab_select and tab_focus
      Can handle hidden tabs: no
      Classification: shouldn't

qutebrowser/commands/command.py
      377┆ elif 1 <= self._count <= tabbed_browser.widget.count():

      Intent: get tab with count for commands
      Can handle hidden tabs: no
      Classification: shouldn't

qutebrowser/completion/models/miscmodels.py
      127┆ for idx in range(tabbed_browser.widget.count()):

      Intent: get tabs for completions
      Can handle hidden tabs: no, the commands being completed likely wouldn't
        handle them
      Classification: shouldn't

qutebrowser/mainwindow/mainwindow.py
      647┆ tab_count = self.tabbed_browser.widget.count()

      Intent: see if there are any tabs open before shutting down
      Can handle hidden tabs: yeah, but doesn't need to
      Classification: could-

qutebrowser/mainwindow/tabbedbrowser.py
      254┆ return utils.get_repr(self, count=self.widget.count())

      Intent: add count of open tabs to repr()
      Can handle hidden tabs: yeah
      Classification: can

      291┆ for i in range(self.widget.count()):

      Intent: return open tabs
      Can handle hidden tabs: recursion error
      Classification: shouldn't

      459┆ count = self.widget.count()

      Intent: see if there are any tabs left open after closing
      Can handle hidden tabs: yeah, but doesn't matter, only cares about top
        level tabs which can't currently be hidden
      Classification: could-

      537┆ only_one_tab_open = self.widget.count() == 1

      Intent: see if we only have one tab open
      Can handle hidden tabs: yeah, but doesn't matter, only cares about top
        level tabs which can't currently be hidden
      Classification: could-

      649┆ if config.val.tabs.tabs_are_windows and self.widget.count() > 0:

      Intent: check to see if the window is empty or not
      Can handle hidden tabs: yeah, but doesn't matter, only cares about top
        level tabs which can't currently be hidden
      Classification: could-

      677┆ self.widget.count())

      Intent: emit tab_index_changed on openeing a background tab
      Can handle hidden tabs: not with current logic, I think it's a pretty
        tab widget specific signal
      Classification: shouldn't

So if there are valid use cases for iterating over all tabs (including hidden ones) and for iterating only over visible (or focusable?) tabs, what would APIs look like that make it clear to callers clear how to do each without making them deal with concepts they don't need to? As a thought experiment: when we put tabs and windows into the extension API what should that look like?

If we add a .tabs() method that returns all tabs, even hidden ones, what are the chances of callers who are doing tab widget related stuff calling that by mistake? Knowing when to call .tabs() and when to call .widgets() might not be obvious.

What about something like .tabs(include_hidden=True)? That way there's only one method to call and it's hopefully obvious how to make it do what you want. And then default include_hidden to False because that the most common use case currently.

A related problem is that once we have an API that can return a mix of tabs which are in the tab widget and hidden tabs, how can the callers know which of those tabs was hidden? There are isHidden() and isVisible() QWidget methods but those are set the same for hidden and background tabs. I kinda want to add a tab.idx() which will return the index within the window, or None. That would also be useful for all the places that call browser.widget.indexOf(tab) to not have to get a reference to the browser. Anyway, that's probably a problem for another day.

we could keep this legacy code for now

I'm thinking we just remove it before merging to main and provide a userscript that'll update any sessions people have that they haven't bothered to upgrade yet. Maybe even point to the userscript in an error message if we can detect those session easy enough.

Or since no-one has complained all week maybe no-one is using it 🤷

@toofar toofar force-pushed the tree/8076_fix_loading_collapsed_tabs_and_legacy_session_support branch from 855a2cb to dea01de Compare February 7, 2024 02:10
@toofar
Copy link
Member Author

toofar commented Feb 7, 2024

Alright, I made some more changes:

  • rebased so that tests and lint can pass
  • spotted another issue where we forgot to focus the relevant tab after loading a tree tab window
  • added a new TreeTabbedBrowser.tabs() method, as discussed. See the commit message for a discussion on editing the parent class for stuff it doesn't implement vs requiring callers to know about the child class
  • fixed the issue I was rambling about at the bottom of the PR description where a tab could be hidden when it shouldn't be. Still not 100% sure what chain of events leads to it, it might be some kind of timing issue as nodes' parents get updated a lot when a session is being loaded. But it's fixed by setting related=False anyway, and I managed to find a reproducer for a related issue with "flat tabs" too to make a convincing test case. I suspect we should be skipping TreeTabbedBrowser._position_tab() when an index is passed to tabopen() but I think I have a suggestion for refactoring that on another issue anyway, hopefully there will be more test scaffolding in place by then. (ref: Add end2end tests for tree tabs feature #8083)

Anyway, I think this PR has grown in scope enough. The main thing was fixes around session loading and I've managed to cover three of them. I've squeezed in legacy session support and a new API method in here too, the latter was particularly interesting.

Lemme know if you have time to review in the next couple of days. Otherwise I'll go ahead and merge into the integration branch, you are of course free to comment on the PR after I've merged it to raise things that could be better!

Tabs in collapsed tree groups aren't currently listed in
tabbed_browser.widgets(), so they weren't getting saved.
The tabs are still referenced by the tree structure though so grab them
from there if we are dealing with a tree tabs window.

I'm not too happy that we are now checking `if
tabbed_browser.is_treetabbedbrowser` twice in the same methods. Seems a
bit smelly, but oh well.

Things we could do to help makes iterating tabs less error prone:

* Make TreeTabbedBrowser.widgets() return widgets from the tree instead
  of the tab widget (would that break other use cases that called that that
  expected them to always be in the tab widget?)
* Have tests to make sure this functionality doesn't break again in the
  future
In a2f98aa the session format for tree tab windows was changed to be
backwards compatible with the prior format. After that change we can load
non-tree tab windows fine but the browser crashes on startup when trying to
load session in the now-legacy tree tab session format.

Since people have been using the now-legacy session for years and there is no
way for them to easily migrate I figure we have a few options:

* provide a script to help them migrate
* handle the error encountered (`KeyError: 'tabs`) when trying to load a
  legacy session better, eg give them a nice apologetic message
* support loading the old style of sessions

It didn't take much effort to support the prior format, so I propose we do
that. The data is all the same, we just changed it to nodes are childs of tabs
instead of the other way around.
We are now iteration over all tabs, including hidden ones that aren't in the
tab widget. So we can't use the index of the current tab within the tab widget
as a key into that list. Check the actual tab value instead.

I made `TabbedBrowser._current_tab()` public because it's useful, so why not! I
could have used `tabbed_browser.widget.currentWidget()` like some other places
do but if we where doing proper type checking in session.py we would also have
to do the None check that is already being done in TabbedBrowser. Also
accessing `some_other_object.widget` feels a little dirty, like it should be
private?
We have one (1) use case where a caller wants to get all tabs from a
tabbed browser, without caring whether they are mounted in the tab bar
or not. I was wondering what an API would look like that let callers ask
that without having to worry about whether the tabbed browser had tree
tabs enabled or not, while still maintaining the more common use case of
only getting widgets that are visible, or focusable, to the user.

Right now the best option I can come up with is this
`tabs(include_hidden=True)` method. This provides a clear API that makes
it obvious how to achieve both use cases (apart from the overloaded
meaning of "hidden" regarding widget visibility). I think referring
to "tabs" is better than "widgets" too, it requires you to hold less
information in your head about what these widgets in particular are.

Am I going to put any effort into attempting to deprecate `.widgets()`?
Nah.

Despite what I think is a fine API I do have some concerns:

*extensibility* One of the goals of this attempt at tree tabs and
productionising it is to see what extensions to core logic would look
like without having to change core classes. I'm not sure if this is a
good example or not because the core class just has the `.widgets()` which
is very much tied to the QTabBar. It doesn't have the concept of tabs
that exist and are children of a TabbedBrowser but aren't in the
QTabBar. So it's a fundamentally new piece of information to express and
we can't just unilaterally return tabs that aren't in the tab bar
because I can see it'll break some things.

So perhaps it would be more fitting with the "extensibility case study"
refactor goal to require the caller to have more knowledge. But I still
don't want callers iterating trees if they don't have to be, so maybe
something like:

    class TabbedBrowser:

        def widgets():
            ...

    class TreeTabbedBrowser:

        def widgets(include_hidden=False):
            ...

    tabbed_browser = ...
    if isinstance(tabbed_browser, TreeTabbedBrowser):
      tabs = tabbed_browser.widgets(include_hidden=True)
    else:
      tabs = tabbed_browser.widgets()

Since we are going to be merging this into the core codebase I don't
want to be inflicting that on users of tabs in the extensions API (when
we add them), but perhaps it's a pattern we can recommend for actual
extensions, when they show up.

*ownership and hidden tabs* For tabs associated with a window but not
visible in the tab bar, who should they be owned by? Currently
TabbedBrowser doesn't attempt to keep track of tabs at all, that's all
handled by the TabWidget. But now we have tabs that the TabWidget
doesn't know about. They are only referenced by the tree structure under
TabbedBrowser. Should the browser be managing tabs and just telling the
tab bar what to show? (Which is essentially what is happening now but
only for the tree case.) Or should we be pushing the tab hiding
functionality into the TabWidget?

Having tabs not in the tab bar can cause issues, mainly because it
causes violates some assumptions in the code that all tabs have an index
in the tab bar. But putting the tabs in the tab bar that you are not
showing can cause usability issues with, for example, navigating to a
tab via count.

When we implement our own tab bar we are probably going to re-do the
displaying of the tree structure to be a bit more "native". When we do
that should we move the tab hiding into the tab bar too? I guess the API
would end up looking like it does with the tree case today, hidden tabs
wouldn't get indices and you would want ways to both iterate through
visible tabs and all tabs (and maybe hidden tabs too, but we don't have
that currently without traversing the tree yourself). I imagine in that
case the tree structure would be part of the tab bar and in the "flat"
case it would just always put stuff at the top level, and complain if
you tried to demote.

That rambling was mostly driven by so many callers going
`tabbed_browser.widget.currentTab()` etc. Which I don't think is a great
API. Beyond an attribute called "widget" being not very meaningful have
tab accessing and manipulating commands spread between and parent and a
child seems like it could be confusing. So I settled on the future logic
being in the tab widget (or in a tree in the tab widget), does that
change anything or dictate what they should be owned by? No. Oh well. I
kinda want to say tabs should be owned by the tabbed browser but I don't
know what practical implications that would have. The interface to
callers would remain predominantly the tabbed browser and the logic
would probably continue to be mixed between that and the tab widget.
@toofar toofar force-pushed the tree/8076_fix_loading_collapsed_tabs_and_legacy_session_support branch from 1da06bf to 3c06459 Compare February 8, 2024 09:53
Looks like when we refactored the session support to be backwards
compatible we forgot to assign tab_to_focus so it was ending up focusing
the last tab in the session for some reason. I guess that was just the
last one added to the tab.
With, presumably, rare combinations of settings tab order in windows
loaded from session can be different from the window the session was
saved from.

In particular with `tabs.new_position.related=prev` the tabs would be
loaded in reverse order!

This commit changes tabs loaded from sessions to 1) override positioning
related kwargs to tabopen so that it doesn't look at user sessions 2)
provide an explicit index to use for new tabs.

This particular test passes with either one of the newly passed kwargs.
We don't strictly need both of them, but while being explicit we may as
well override everything to be extra clear.

While looking at this code you may be asking why background is set to
True for all the tabs when only one of them is going to be focused? Good
question, I don't think it should be, see #6983 (comment)

But since that is explicitly set it should probably be a separate PR to
change it. Looks like the original PR to fix that got caught up in wider
scope and trying to do a new style of e2e-but-python tests. It should be
easy enough to write an e2e that loads a page with JS that reports that
visibility state.

TODO:
* add a similar test for tree tabs when the scaffolding is there (with
  `new_position.tree.new_toplevel=prev` too)
* override sibling too?
The first three tests make sure that the "collapsed" attribute makes it
through the session machinery and behaves as expected.

Third one is a bit sneaky because it tests a case that was causing issues, but
this test couldn't actually reproduce. The problem was that before we were
passing `related=False` to `tabopen()` the tab after a collapsed group would
be loaded hidden when it wasn't supposed to be. This was only in the UI
though, if you saved a session it would be correct, so we can't test it in
these e2e tests currently...
In 3ec2000 I identified a few cases where we could be asked to call into
the tree structure based on a widget index with evidence that the tree and
widget where out of sync. In particular where there were a different amount of
items in each.

Previously I had ignored desyncs during session loads but now I've found
a case where the desync remains after the session was loaded, for a
short time. It can be reproduced (most of the time) with:

   python3 -m qutebrowser -T -s tabs.tree_tabs true ":open about:blank?1" ":open -tr about:blank?2" ":open -tr about:blank?3" ":tab-focus 2" ":tree-tab-toggle-hide" ":cmd-later 200 session-save foo" ":cmd-later 300 session-load -c foo"

This only happens if the last set of tabs we open is a collapsed group. There
would have been no update event fired since we set the collapsed attribute.

If this issue shows up again a more robust approach might be to not set the
collapsed attribute in the loop, but load all the tabs visible then go and set
the collapsed attribute on all the nodes that need it and fire an update.

I initially reproduced this with the "Load a collapsed subtree" end2end test.
@toofar toofar force-pushed the tree/8076_fix_loading_collapsed_tabs_and_legacy_session_support branch from 3c06459 to 810088e Compare February 9, 2024 01:49
I got a failure in CI on windows for the "Load a collapsed subtree"
where it got an index error when trying to load the active history entry
for a tab in a session. This is likely because the session was saved
before the tab was loaded. I can reproduce it locally by changing the
last test to wait for only the first tab, instead of the last.
@toofar
Copy link
Member Author

toofar commented Feb 9, 2024

Almighty, merging now. To close this out I had another look at the previous changes to session loading and have a couple more notes on that (see the ??? lines in the diff below), but nothing here has any priority over the other stuff we already have to look at I think. And my overall opinion from earlier that the new session loading stuff is cleanly done still stands, it re-uses all of the existing session loading and just adds a couple of diversions to load or save tree specific data.

diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py
index dd63904cdc4..fe8e7ba24d8 100644
--- a/qutebrowser/misc/sessions.py
+++ b/qutebrowser/misc/sessions.py
@@ -281,13 +317,32 @@ def _save_all(self, *, only_window=None, with_private=False, with_history=True):
             if getattr(active_window, 'win_id', None) == win_id:
                 win_data['active'] = True
             win_data['geometry'] = bytes(main_window.saveGeometry())
-            win_data['tabs'] = []
             if tabbed_browser.is_private:
                 win_data['private'] = True
+
+            win_data['tabs'] = []
???          ^ not clear why this was moved
             for i, tab in enumerate(tabbed_browser.widgets()):
                 active = i == tabbed_browser.widget.currentIndex()
-                win_data['tabs'].append(self._save_tab(tab, active,
-                                                       with_history=with_history))
+                tab_data = self._save_tab(tab,
+                                          active,
+                                          with_history=with_history)
???            I wonder if we could have _save_tab() fill in the tree data if tab.node was set?
???            For the distant future when refactoring the sessions code (there should be an issue for it somewhere) it might be nice if we could make AbstractTab and MainWindow handle their own serialization
+                if tabbed_browser.is_treetabbedbrowser:
+                    node = tab.node
+                    node_data = {
+                        'node': node.uid,
+                        'parent': node.parent.uid,
+                        'children': [c.uid for c in node.children],
+                        'collapsed': node.collapsed,
+                        'uid': node.uid
+                    }
+                    tab_data['treetab_node_data'] = node_data
+                win_data['tabs'].append(tab_data)
+            if tabbed_browser.is_treetabbedbrowser:
+                root = tabbed_browser.widget.tree_root
+                win_data['treetab_root'] = {
+                    'children': [c.uid for c in root.children],
+                    'uid': root.uid
+                }
             data['windows'].append(win_data)
         return data
 
@@ -455,6 +510,45 @@ def _load_tab(self, new_tab, data):  # noqa: C901
         except ValueError as e:
             raise SessionError(e)
 
+    def _load_tree(self, tabbed_browser, tree_data):
+        tree_keys = list(tree_data.keys())
+        if not tree_keys:
+            return None
???      ^ in what situation could this happen?
+
+        root_data = tree_data.get(tree_keys[0])
+        if root_data is None:
+            return None
???      ^ same here, shouldn't this be an error? If this is something we don't expect to happen I would rather complain about it loudly if it doesn't instead of silently ignoring it
+
+        root_node = tabbed_browser.widget.tree_root
+        tab_to_focus = None
+        index = -1
+
+        def recursive_load_node(uid):
+            nonlocal tab_to_focus
+            nonlocal index
+            index += 1
+            tab_data = tree_data[uid]
+            node_data = tab_data['treetab_node_data']
+            children_uids = node_data['children']
+
+            if tab_data.get('active'):
+                tab_to_focus = index
+
+            new_tab = tabbed_browser.tabopen(background=False)
+            self._load_tab(new_tab, tab_data)
+
+            new_tab.node.parent = root_node
???         ^ I assume this is being done to avoid an error in the tree walking code further down somewhere. Probably could pass the parent as an arg into recursive_load_node() and then set the real parent here rather than setting a placeholder one and then setting the real one after the function returns
+            children = [recursive_load_node(uid) for uid in children_uids]
+            new_tab.node.children = children
+            new_tab.node.collapsed = node_data['collapsed']
+            return new_tab.node
+
+        for child_uid in root_data['children']:
+            child = recursive_load_node(child_uid)
+            child.parent = root_node
+
+        return tab_to_focus
+
     def _load_window(self, win):
         """Turn yaml data into windows."""
         window = mainwindow.MainWindow(geometry=win['geometry'],

@toofar toofar merged commit 160e9f2 into tree-tabs-integration Feb 9, 2024
53 of 65 checks passed
@toofar toofar deleted the tree/8076_fix_loading_collapsed_tabs_and_legacy_session_support branch February 9, 2024 04:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: tree tabs Issues related to tree tabs functionality
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

None yet

2 participants