"""Search bar with tag autocomplete, history, and saved searches.""" from __future__ import annotations from PySide6.QtCore import Qt, Signal, QTimer, QStringListModel from PySide6.QtGui import QIcon from PySide6.QtWidgets import ( QWidget, QHBoxLayout, QLineEdit, QPushButton, QCompleter, QMenu, QInputDialog, ) from ..core.db import Database class SearchBar(QWidget): """Tag search bar with autocomplete, history dropdown, and saved searches.""" search_requested = Signal(str) autocomplete_requested = Signal(str) def __init__(self, db: Database | None = None, parent: QWidget | None = None) -> None: super().__init__(parent) self._db = db layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(6) self._input = QLineEdit() self._input.setPlaceholderText("Search tags...") self._input.returnPressed.connect(self._do_search) # Dropdown arrow inside search bar from PySide6.QtGui import QPixmap, QPainter, QFont pixmap = QPixmap(16, 16) pixmap.fill(Qt.GlobalColor.transparent) painter = QPainter(pixmap) painter.setPen(self._input.palette().color(self._input.palette().ColorRole.Text)) painter.setFont(QFont(self._input.font().family(), 8)) painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "\u25BC") painter.end() self._history_action = self._input.addAction( QIcon(pixmap), QLineEdit.ActionPosition.TrailingPosition, ) self._history_action.setToolTip("Search history & saved searches") self._history_action.triggered.connect(self._show_history_menu) layout.addWidget(self._input, stretch=1) # Save search button self._save_btn = QPushButton("Save") self._save_btn.setFixedWidth(60) self._save_btn.setToolTip("Save current search") self._save_btn.clicked.connect(self._save_current_search) layout.addWidget(self._save_btn) self._btn = QPushButton("Search") self._btn.clicked.connect(self._do_search) layout.addWidget(self._btn) # Autocomplete self._completer_model = QStringListModel() self._completer = QCompleter(self._completer_model) self._completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) self._completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion) self._input.setCompleter(self._completer) # Debounce self._ac_timer = QTimer() self._ac_timer.setSingleShot(True) self._ac_timer.setInterval(300) self._ac_timer.timeout.connect(self._request_autocomplete) self._input.textChanged.connect(self._on_text_changed) def _on_text_changed(self, text: str) -> None: self._ac_timer.start() def _request_autocomplete(self) -> None: text = self._input.text().strip() if not text: return last_tag = text.split()[-1] if text.split() else "" query = last_tag.lstrip("-") if len(query) >= 2: self.autocomplete_requested.emit(query) def set_suggestions(self, suggestions: list[str]) -> None: self._completer_model.setStringList(suggestions) def _do_search(self) -> None: query = self._input.text().strip() if self._db and query and self._db.get_setting_bool("search_history_enabled"): self._db.add_search_history(query) self.search_requested.emit(query) def _show_history_menu(self) -> None: if not self._db: return menu = QMenu(self) saved_actions = {} hist_actions = {} # Saved searches saved = self._db.get_saved_searches() if saved: saved_header = menu.addAction("-- Saved Searches --") saved_header.setEnabled(False) for sid, name, query in saved: a = menu.addAction(f" {name} ({query})") saved_actions[id(a)] = (sid, query) menu.addSeparator() # History (only shown when the setting is on) history = self._db.get_search_history() if self._db.get_setting_bool("search_history_enabled") else [] if history: hist_header = menu.addAction("-- Recent --") hist_header.setEnabled(False) for query in history: a = menu.addAction(f" {query}") hist_actions[id(a)] = query menu.addSeparator() clear_action = menu.addAction("Clear History") else: clear_action = None # Management actions delete_saved = None if saved: delete_saved = menu.addAction("Manage Saved Searches...") menu.addSeparator() if not saved and not history: empty = menu.addAction("No history yet") empty.setEnabled(False) action = menu.exec(self._input.mapToGlobal(self._input.rect().bottomLeft())) if not action: return if clear_action and action == clear_action: self._db.clear_search_history() elif delete_saved and action == delete_saved: self._delete_saved_search_dialog() elif id(action) in hist_actions: self._input.setText(hist_actions[id(action)]) self._do_search() elif id(action) in saved_actions: _, query = saved_actions[id(action)] self._input.setText(query) self._do_search() def _delete_saved_search_dialog(self) -> None: from PySide6.QtWidgets import QListWidget, QDialog, QVBoxLayout, QDialogButtonBox saved = self._db.get_saved_searches() if not saved: return dlg = QDialog(self) dlg.setWindowTitle("Delete Saved Searches") dlg.setMinimumWidth(300) layout = QVBoxLayout(dlg) lst = QListWidget() for sid, name, query in saved: lst.addItem(f"{name} ({query})") layout.addWidget(lst) btns = QDialogButtonBox() delete_btn = btns.addButton("Delete Selected", QDialogButtonBox.ButtonRole.DestructiveRole) btns.addButton(QDialogButtonBox.StandardButton.Close) btns.rejected.connect(dlg.reject) layout.addWidget(btns) def _delete(): row = lst.currentRow() if 0 <= row < len(saved): self._db.remove_saved_search(saved[row][0]) lst.takeItem(row) saved.pop(row) delete_btn.clicked.connect(_delete) dlg.exec() def _save_current_search(self) -> None: if not self._db: return query = self._input.text().strip() if not query: return name, ok = QInputDialog.getText(self, "Save Search", "Name:", text=query) if ok and name.strip(): self._db.add_saved_search(name.strip(), query) def text(self) -> str: return self._input.text().strip() def set_text(self, text: str) -> None: self._input.setText(text) def focus(self) -> None: self._input.setFocus()