From 7b3839b44bc485fd3a7ea5ba60131ae6ecb664cb Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Mon, 8 Aug 2016 19:11:27 -0400 Subject: [PATCH] Focus completion by category. Implement `completion-item-focus next-category` and `completion-item-focus prev-category` to jump through completions by category rather than by item. Resolves #1567. --- qutebrowser/completion/completionwidget.py | 44 ++++++++++- qutebrowser/config/configdata.py | 2 + .../unit/completion/test_completionwidget.py | 79 ++++++++++++------- 3 files changed, 94 insertions(+), 31 deletions(-) diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 3c608951f..6fcec9c04 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -181,14 +181,44 @@ class CompletionView(QTreeView): # Item is a real item, not a category header -> success return idx + def _next_category_idx(self, upwards): + """Get the index of the previous/next category. + + Args: + upwards: Get previous item, not next. + + Return: + A QModelIndex. + """ + idx = self.selectionModel().currentIndex() + if not idx.isValid(): + return self._next_idx(upwards).sibling(0, 0) + idx = idx.parent() + direction = -1 if upwards else 1 + while True: + idx = idx.sibling(idx.row() + direction, 0) + if not idx.isValid() and upwards: + # wrap around to the first item of the last category + return self.model().last_item().sibling(0, 0) + elif not idx.isValid() and not upwards: + # wrap around to the first item of the first category + idx = self.model().first_item() + self.scrollTo(idx.parent()) + return idx + elif idx.isValid() and idx.child(0, 0).isValid(): + # scroll to ensure the category is visible + self.scrollTo(idx) + return idx.child(0, 0) + @cmdutils.register(instance='completion', hide=True, modes=[usertypes.KeyMode.command], scope='window') - @cmdutils.argument('which', choices=['next', 'prev']) + @cmdutils.argument('which', choices=['next', 'prev', 'next-category', + 'prev-category']) def completion_item_focus(self, which): """Shift the focus of the completion menu to another item. Args: - which: 'next' or 'prev' + which: 'next', 'prev', 'next-category', or 'prev-category'. """ # selmodel can be None if 'show' and 'auto-open' are set to False # https://github.com/The-Compiler/qutebrowser/issues/1731 @@ -196,7 +226,15 @@ class CompletionView(QTreeView): if selmodel is None: return - idx = self._next_idx(which == 'prev') + if which == 'next': + idx = self._next_idx(upwards=False) + elif which == 'prev': + idx = self._next_idx(upwards=True) + elif which == 'next-category': + idx = self._next_category_idx(upwards=False) + elif which == 'prev-category': + idx = self._next_category_idx(upwards=True) + if not idx.isValid(): return diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 805557e94..d23de02d0 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1602,6 +1602,8 @@ KEY_DATA = collections.OrderedDict([ ('command-history-next', ['']), ('completion-item-focus prev', ['', '']), ('completion-item-focus next', ['', '']), + ('completion-item-focus next-category', ['']), + ('completion-item-focus prev-category', ['']), ('completion-item-del', ['']), ('command-accept', RETURN_KEYS), ])), diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py index f07bfc96b..7cc729f5d 100644 --- a/tests/unit/completion/test_completionwidget.py +++ b/tests/unit/completion/test_completionwidget.py @@ -97,32 +97,56 @@ def test_maybe_resize_completion(completionview, config_stub, qtbot): completionview.maybe_resize_completion() -@pytest.mark.parametrize('tree, count, expected', [ - ([['Aa']], 1, 'Aa'), - ([['Aa']], -1, 'Aa'), - ([['Aa'], ['Ba']], 1, 'Aa'), - ([['Aa'], ['Ba']], -1, 'Ba'), - ([['Aa'], ['Ba']], 2, 'Ba'), - ([['Aa'], ['Ba']], -2, 'Aa'), - ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 3, 'Ac'), - ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 4, 'Ba'), - ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 6, 'Ca'), - ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 7, 'Aa'), - ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], -1, 'Ca'), - ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], -2, 'Bb'), - ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], -4, 'Ac'), - ([[], ['Ba', 'Bb']], 1, 'Ba'), - ([[], ['Ba', 'Bb']], -1, 'Bb'), - ([[], [], ['Ca', 'Cb']], 1, 'Ca'), - ([[], [], ['Ca', 'Cb']], -1, 'Cb'), - ([['Aa'], []], 1, 'Aa'), - ([['Aa'], []], -1, 'Aa'), - ([['Aa'], [], []], 1, 'Aa'), - ([['Aa'], [], []], -1, 'Aa'), - ([[]], 1, None), - ([[]], -1, None), +@pytest.mark.parametrize('which, tree, count, expected', [ + ('next', [['Aa']], 1, 'Aa'), + ('prev', [['Aa']], 1, 'Aa'), + ('next', [['Aa'], ['Ba']], 1, 'Aa'), + ('prev', [['Aa'], ['Ba']], 1, 'Ba'), + ('next', [['Aa'], ['Ba']], 2, 'Ba'), + ('prev', [['Aa'], ['Ba']], 2, 'Aa'), + ('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 3, 'Ac'), + ('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 4, 'Ba'), + ('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 6, 'Ca'), + ('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 7, 'Aa'), + ('prev', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 1, 'Ca'), + ('prev', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 2, 'Bb'), + ('prev', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 4, 'Ac'), + ('next', [[], ['Ba', 'Bb']], 1, 'Ba'), + ('prev', [[], ['Ba', 'Bb']], 1, 'Bb'), + ('next', [[], [], ['Ca', 'Cb']], 1, 'Ca'), + ('prev', [[], [], ['Ca', 'Cb']], 1, 'Cb'), + ('next', [['Aa'], []], 1, 'Aa'), + ('prev', [['Aa'], []], 1, 'Aa'), + ('next', [['Aa'], [], []], 1, 'Aa'), + ('prev', [['Aa'], [], []], 1, 'Aa'), + ('next', [['Aa'], [], ['Ca', 'Cb']], 2, 'Ca'), + ('prev', [['Aa'], [], ['Ca', 'Cb']], 1, 'Cb'), + ('next', [[]], 1, None), + ('prev', [[]], 1, None), + ('next-category', [['Aa']], 1, 'Aa'), + ('prev-category', [['Aa']], 1, 'Aa'), + ('next-category', [['Aa'], ['Ba']], 1, 'Aa'), + ('prev-category', [['Aa'], ['Ba']], 1, 'Ba'), + ('next-category', [['Aa'], ['Ba']], 2, 'Ba'), + ('prev-category', [['Aa'], ['Ba']], 2, 'Aa'), + ('next-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 2, 'Ba'), + ('prev-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 2, 'Ba'), + ('next-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 3, 'Ca'), + ('prev-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 3, 'Aa'), + ('next-category', [[], ['Ba', 'Bb']], 1, 'Ba'), + ('prev-category', [[], ['Ba', 'Bb']], 1, 'Ba'), + ('next-category', [[], [], ['Ca', 'Cb']], 1, 'Ca'), + ('prev-category', [[], [], ['Ca', 'Cb']], 1, 'Ca'), + ('next-category', [[], [], ['Ca', 'Cb']], 2, 'Ca'), + ('prev-category', [[], [], ['Ca', 'Cb']], 2, 'Ca'), + ('next-category', [['Aa'], [], []], 1, 'Aa'), + ('prev-category', [['Aa'], [], []], 1, 'Aa'), + ('next-category', [['Aa'], [], ['Ca', 'Cb']], 2, 'Ca'), + ('prev-category', [['Aa'], [], ['Ca', 'Cb']], 1, 'Ca'), + ('next-category', [[]], 1, None), + ('prev-category', [[]], 1, None), ]) -def test_completion_item_focus(tree, count, expected, completionview): +def test_completion_item_focus(which, tree, count, expected, completionview): """Test that on_next_prev_item moves the selection properly. Args: @@ -140,9 +164,8 @@ def test_completion_item_focus(tree, count, expected, completionview): filtermodel = sortfilter.CompletionFilterModel(model, parent=completionview) completionview.set_model(filtermodel) - direction = 'prev' if count < 0 else 'next' - for _ in range(abs(count)): - completionview.completion_item_focus(direction) + for _ in range(count): + completionview.completion_item_focus(which) idx = completionview.selectionModel().currentIndex() assert filtermodel.data(idx) == expected