From 524ebf1df0699335bc419b2189343d84fdab4818 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:15:42 -0700 Subject: [PATCH] feat(ui): use tab and shift+tab to navigate suggest boxes --- .../qt/controllers/autofill_line_edit.py | 41 +++++++++------ src/tagstudio/qt/controllers/suggest_box.py | 50 +++++++++++++++++-- 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/src/tagstudio/qt/controllers/autofill_line_edit.py b/src/tagstudio/qt/controllers/autofill_line_edit.py index 2247dafd..e0c32228 100644 --- a/src/tagstudio/qt/controllers/autofill_line_edit.py +++ b/src/tagstudio/qt/controllers/autofill_line_edit.py @@ -22,7 +22,8 @@ logger = structlog.get_logger(__name__) class AutofillLineEdit(QLineEdit): return_pressed = Signal() shift_return_pressed = Signal() - shift_holding = Signal(bool) + holding_shift = Signal(bool) + index_updated = Signal(int) def __init__(self, popup: QWidget) -> None: super().__init__() @@ -39,23 +40,35 @@ class AutofillLineEdit(QLineEdit): return super().focusInEvent(arg__1) @override - def keyPressEvent(self, arg__1: QtGui.QKeyEvent) -> None: - if arg__1.key() == QtCore.Qt.Key.Key_Shift: - self.shift_holding.emit(True) # noqa: FBT003 + def event(self, arg__1: QtCore.QEvent) -> bool: + if arg__1.type() == QtCore.QEvent.Type.KeyPress: + assert isinstance(arg__1, QtGui.QKeyEvent) - if arg__1.key() == QtCore.Qt.Key.Key_Escape: - self.setText("") - self.clearFocus() - elif arg__1.key() == QtCore.Qt.Key.Key_Enter or arg__1.key() == QtCore.Qt.Key.Key_Return: - if arg__1.modifiers() and QtCore.Qt.KeyboardModifier.ShiftModifier: - self.shift_return_pressed.emit() - else: - self.return_pressed.emit() + if arg__1.key() == QtCore.Qt.Key.Key_Tab: + self.index_updated.emit(1) + return True + elif arg__1.key() == QtCore.Qt.Key.Key_Backtab: + self.index_updated.emit(-1) + return True - return super().keyPressEvent(arg__1) + if arg__1.key() == QtCore.Qt.Key.Key_Shift: + self.holding_shift.emit(True) # noqa: FBT003 + + if arg__1.key() == QtCore.Qt.Key.Key_Escape: + self.setText("") + self.clearFocus() + elif ( + arg__1.key() == QtCore.Qt.Key.Key_Enter or arg__1.key() == QtCore.Qt.Key.Key_Return + ): + if arg__1.modifiers() == QtCore.Qt.KeyboardModifier.ShiftModifier: + self.shift_return_pressed.emit() + else: + self.return_pressed.emit() + + return super().event(arg__1) @override def keyReleaseEvent(self, arg__1: QtGui.QKeyEvent) -> None: if arg__1.key() == QtCore.Qt.Key.Key_Shift: - self.shift_holding.emit(False) # noqa: FBT003 + self.holding_shift.emit(False) # noqa: FBT003 return super().keyReleaseEvent(arg__1) diff --git a/src/tagstudio/qt/controllers/suggest_box.py b/src/tagstudio/qt/controllers/suggest_box.py index c20cca5a..7df6e597 100644 --- a/src/tagstudio/qt/controllers/suggest_box.py +++ b/src/tagstudio/qt/controllers/suggest_box.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: (c) TagStudio Contributors # SPDX-License-Identifier: GPL-3.0-only +from functools import partial from typing import Any, override import structlog @@ -50,9 +51,10 @@ class SuggestBox[T](QWidget): super().__init__() self._lib = library self._settings = settings - self._limit = 5 + self._limit = 25 self._is_shift_held = False self._search_results: list[T] = [] + self._selection_index = 0 self.added: list[int] = [] self.excluded: list[int] = [] @@ -79,7 +81,8 @@ class SuggestBox[T](QWidget): ) ) - self.layout().search_field.shift_holding.connect(lambda held: self._on_shift_held(held)) + self.layout().search_field.holding_shift.connect(partial(self._on_shift_held)) + self.layout().search_field.index_updated.connect(partial(self._on_index_updated)) def _on_shift_held(self, held: bool) -> None: if held: @@ -97,6 +100,42 @@ class SuggestBox[T](QWidget): assert isinstance(underlined_widget, UnderlinedWidget) underlined_widget.widget.setGraphicsEffect(None) # pyright: ignore[reportArgumentType] + def _on_index_updated(self, delta: int) -> None: + # Initialize the widget count (non-hidden) + widget_count = 0 + for i in range(0, self.layout().content_layout.count()): + widget = self.layout().content_layout.itemAt(i).widget() + if not widget.isHidden(): + widget_count += 1 + + # Update the index + old_idx = self._selection_index + max_idx = widget_count - 1 + if self._selection_index + delta < 0: + # Can't move further left + self._selection_index = 0 + elif self._selection_index + delta > max_idx: + self._selection_index = max_idx + else: + self._selection_index = self._selection_index + delta + logger.info(self._selection_index) + + # Don't update the UI if there's no index change + if old_idx == self._selection_index: + return + + # Draw the correct underline for the selected widget + for i in range(0, widget_count): + underlined_widget = self.layout().content_layout.itemAt(i).widget() + assert isinstance(underlined_widget, UnderlinedWidget) + if i == self._selection_index: + underlined_widget.toggle_underline(is_hidden=False) + self.layout().scroll_area.ensureWidgetVisible( + underlined_widget, xmargin=6, ymargin=0 + ) + else: + underlined_widget.toggle_underline(is_hidden=True) + def _clear_search_query(self) -> None: self.layout().search_field.setText("") @@ -120,7 +159,7 @@ class SuggestBox[T](QWidget): if (len(self._search_results) <= 0) or always_create: self._on_item_create() else: - self._on_item_chosen(self._search_results[0]) + self._on_item_chosen(self._search_results[self._selection_index]) self._clear_search_query() self._update_items() @@ -140,6 +179,11 @@ class SuggestBox[T](QWidget): def _update_items(self, query: str | None = None) -> None: """Update the item list given a search query.""" logger.info("[SearchPanel] Updating items", limit=self._limit) + self._selection_index = 0 + if self.layout().content_layout.count() > 0: + self.layout().scroll_area.ensureWidgetVisible( + self.layout().content_layout.itemAt(0).widget(), xmargin=6, ymargin=0 + ) # Get results for the search query query_lower = "" if not query else query.lower()