From 9489993de46af45f935d8e48e22fcf77340b95a0 Mon Sep 17 00:00:00 2001 From: Aravinda Rao Date: Thu, 2 May 2024 00:46:13 +0530 Subject: [PATCH] feat(core/tree): more intuitive selection of next focus pane after remove operation --- src/qtile_bonsai/core/tree.py | 15 +++++++++++++-- tests/unit/core/test_tree.py | 21 ++++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/qtile_bonsai/core/tree.py b/src/qtile_bonsai/core/tree.py index 3a6d8c8..9f67b80 100644 --- a/src/qtile_bonsai/core/tree.py +++ b/src/qtile_bonsai/core/tree.py @@ -363,7 +363,9 @@ def remove( 1. The `node` or an ancestor node that was the point of removal. This branch is now unlinked from the main tree. 2. The sibling node of the removed node. Or `None` if no sibling exists. - 3. The next pane under the sibling that ought to get focus. + 3. The next pane that ought to get focus. Usually the sibling of the + removed node. But if the sibling is a Tab, then it is the MRU pane under + the nearest TC. """ rm_nodes = [] next_focus_pane = None @@ -376,8 +378,17 @@ def remove( rm_nodes.extend(br_rm_nodes) if br_sib is not None: + # Find an appropriate pane to focus next. When a tab is closed, it's nicer + # to pick the MRU pane under the whole TC. eg. when we open some GUI + # application as a far-away tab from the 'current' window, it's nicer UX to + # return focus to the original window on closing it. + # Otherwise, it's generally nicest to give focus to the sibling node. + if isinstance(br_sib, Tab): + active_tc = br_sib.get_first_ancestor(TabContainer) + next_focus_pane = self.find_mru_pane(start_node=active_tc) + else: + next_focus_pane = self.find_mru_pane(start_node=br_sib) rm_nodes.extend(self._do_post_removal_pruning(br_sib)) - next_focus_pane = self.find_mru_pane() self._notify_subscribers(TreeEvent.node_removed, rm_nodes) diff --git a/tests/unit/core/test_tree.py b/tests/unit/core/test_tree.py index 355fba0..cc03275 100644 --- a/tests/unit/core/test_tree.py +++ b/tests/unit/core/test_tree.py @@ -2091,13 +2091,29 @@ def test_when_all_panes_are_removed_then_tree_is_empty(self, tree: Tree): assert tree.is_empty - def test_returns_mru_pane_as_next_focus_node(self, tree: Tree): + def test_when_non_tab_node_is_removed_then_pane_under_sibling_node_is_returned_as_next_focus_node( + self, tree: Tree + ): + p1 = tree.tab() + p2 = tree.split(p1, "x") + p3 = tree.split(p2, "y") + p4 = tree.tab(p2, new_level=True) + + tree.focus(p4) + tree.focus(p3) + + _, _, p = tree.remove(p3) + + assert p is p4 + + def test_when_tab_is_closed_then_returns_mru_pane_under_tc_as_next_focus_node( + self, tree: Tree + ): p1 = tree.tab() p2 = tree.split(p1, "x") _ = tree.tab() p4 = tree.tab() - # focus from p2 to p4, skipping p3 sibling of p4 tree.focus(p2) tree.focus(p4) @@ -5588,7 +5604,6 @@ def test_when_node_is_sole_top_level_pane_under_subtab( assert cb_add.mock_calls == [] assert cb_remove.mock_calls == [mock.call([sc, t])] - @pytest.mark.a class TestWhenTCGetsPrunedAndRemnantsExistAlongSplitAxis: def test_when_position_is_previous(self, tree: Tree, add_subscribers_to_tree): tree.set_config("tab_bar.hide_when", "single_tab")