Compare commits

...

5 Commits

Author SHA1 Message Date
Travis Abendshien 929ebef143 chore: cleanup comments and variable usage 2026-08-13 10:58:20 -07:00
Travis Abendshien 2f241574fd fix(ui): fix shift opacity not targeting correct widget 2026-08-12 23:40:38 -07:00
Travis Abendshien 6b2d8d89b3 fix: strip suggest box queries of whitespace 2026-08-12 23:40:38 -07:00
Travis Abendshien 524ebf1df0 feat(ui): use tab and shift+tab to navigate suggest boxes 2026-08-12 23:40:38 -07:00
Travis Abendshien dc31605c8f fix(ui): correct suggest box content padding 2026-08-12 23:40:38 -07:00
3 changed files with 87 additions and 38 deletions
@@ -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)
+58 -17
View File
@@ -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,24 +81,58 @@ 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:
self._is_shift_held = True
opacity_effect = QGraphicsOpacityEffect(self)
opacity_effect.setOpacity(0.3)
if self.layout().content_layout.count() > 0:
underlined_widget = self.layout().content_layout.itemAt(0).widget()
assert isinstance(underlined_widget, UnderlinedWidget)
for i in range(0, self.layout().content_layout.count()):
underlined_widget = self.layout().content_layout.itemAt(i).widget()
assert isinstance(underlined_widget, UnderlinedWidget)
if held and i == self._selection_index:
self._is_shift_held = True
opacity_effect = QGraphicsOpacityEffect(self)
opacity_effect.setOpacity(0.3)
underlined_widget.widget.setGraphicsEffect(opacity_effect)
else:
self._is_shift_held = False
if self.layout().content_layout.count() > 0:
underlined_widget = self.layout().content_layout.itemAt(0).widget()
assert isinstance(underlined_widget, UnderlinedWidget)
else:
self._is_shift_held = False
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
# 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("")
@@ -104,7 +140,7 @@ class SuggestBox[T](QWidget):
raise NotImplementedError()
def _on_search_query_changed(self, query: str) -> None:
self._update_items(query)
self._update_items(query.strip())
def _on_search_query_submitted(self, query: str, always_create: bool = False) -> None:
# Focus search field if no query
@@ -120,7 +156,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 +176,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()
@@ -187,7 +228,7 @@ class SuggestBox[T](QWidget):
self.layout().search_field.setStyleSheet(autofill_line_edit_style())
else:
self.layout().scroll_area.setHidden(False)
self.layout().content_layout.setContentsMargins(6, 6, 6, 6)
self.layout().content_layout.setContentsMargins(4, 6, 4, 6)
self.layout().search_field.setStyleSheet(autofill_line_edit_top_style())
def _search_items(self, query: str) -> tuple[list[T], list[T]]: # pyright: ignore[reportUnusedParameter]
+2 -7
View File
@@ -21,15 +21,9 @@ class SuggestBoxView(QVBoxLayout):
self.setContentsMargins(0, 0, 0, 0)
self.setSpacing(0)
# HACK: The transparent border allows for the focus border color to
# still show above the tags at the edges... sort of (overlaps on left when h-scrolling)
scroll_area_style = """
QScrollArea{
background: transparent;
border: solid;
border-color: transparent;
border-width: 0px 2px;
padding-left: -2px;
}
QScrollArea > QWidget > QWidget{
background: transparent;
@@ -50,6 +44,7 @@ class SuggestBoxView(QVBoxLayout):
scroll_area_container.setStyleSheet(autofill_scroll_top_style("container"))
self.scroll_area = QScrollArea()
self.scroll_area.setStyleSheet(scroll_area_style)
self.scroll_area.setViewportMargins(2, 0, 2, 0)
scroll_area_container_layout.addWidget(self.scroll_area)
self.scroll_area.setWidget(contents)
search_bar_height = 28
@@ -68,7 +63,7 @@ class SuggestBoxView(QVBoxLayout):
self.search_field = AutofillLineEdit(scroll_area_container)
self.search_field.setStyleSheet(autofill_line_edit_style())
self.search_field.setObjectName("search_field")
self.search_field.setMinimumHeight(28)
self.search_field.setMinimumHeight(search_bar_height)
self.search_field.setPlaceholderText(placeholder_text)
self.scroll_area.setFocusProxy(self.search_field)