from PyQt6.QtWidgets import QLineEdit, QListWidget, QListWidgetItem from PyQt6.QtCore import Qt, pyqtSignal from loguru import logger class SearchableCombo(QLineEdit): """ Campo testo con popup filtrato. Il popup è un child widget della finestra contenitore — funziona anche dentro dialog modali. Emette item_selected(name, item_id) quando l'utente sceglie una voce. """ item_selected = pyqtSignal(str, str) MIN_CHARS = 3 def __init__(self, parent=None): super().__init__(parent) self._items: list[tuple[str, str]] = [] self._selected_id: str = "" self._popup: QListWidget | None = None self.textEdited.connect(self._on_text_edited) # ── API pubblica ──────────────────────────────────────────────────────── def set_items(self, items: list[tuple[str, str]]): self._items = items self._selected_id = "" def selected_id(self) -> str: return self._selected_id def reset(self): self.clear() self._selected_id = "" self._hide_popup() # ── Popup (child della finestra contenitore) ──────────────────────────── def _ensure_popup(self): # Crea il popup come child della finestra top-level — così è # accessibile anche dentro dialog modali win = self.window() if self._popup is None or self._popup.parent() is not win: if self._popup: self._popup.hide() self._popup.deleteLater() self._popup = QListWidget(win) self._popup.setFocusPolicy(Qt.FocusPolicy.NoFocus) self._popup.itemClicked.connect(self._on_item_clicked) self._popup.hide() def _show_popup(self): win = self.window() # Posizione relativa alla finestra contenitore pos = self.mapTo(win, self.rect().bottomLeft()) self._popup.move(pos) self._popup.setFixedWidth(max(self.width(), 300)) row_h = self._popup.sizeHintForRow(0) or 22 n = min(self._popup.count(), 10) self._popup.setFixedHeight(n * row_h + 6) self._popup.raise_() self._popup.show() def _hide_popup(self): if self._popup: self._popup.hide() # ── Logica filtro ─────────────────────────────────────────────────────── def _on_text_edited(self, text: str): self._selected_id = "" if len(text) < self.MIN_CHARS: self._hide_popup() return text_lower = text.lower() matches = [(n, i) for n, i in self._items if text_lower in n.lower()] if not matches: self._hide_popup() return self._ensure_popup() self._popup.clear() for name, item_id in matches: item = QListWidgetItem(name) item.setData(Qt.ItemDataRole.UserRole, item_id) self._popup.addItem(item) self._show_popup() logger.debug(f"SearchableCombo: {len(matches)} risultati per '{text}'") def _on_item_clicked(self, item: QListWidgetItem): name = item.text() item_id = item.data(Qt.ItemDataRole.UserRole) self._selected_id = item_id self.setText(name) self._hide_popup() self.item_selected.emit(name, item_id) logger.debug(f"SearchableCombo: selezionato '{name}' id={item_id}") # ── Tastiera ──────────────────────────────────────────────────────────── def keyPressEvent(self, event): if self._popup and self._popup.isVisible(): if event.key() == Qt.Key.Key_Down: self._popup.setFocus() self._popup.setCurrentRow(0) return if event.key() == Qt.Key.Key_Escape: self._hide_popup() return super().keyPressEvent(event) def hideEvent(self, event): self._hide_popup() super().hideEvent(event)