gui.utils.dropdown_button

 1from PySide6.QtWidgets import QPushButton, QMenu
 2from PySide6.QtCore import Signal
 3
 4
 5class DropdownButton(QPushButton):
 6    """
 7    Reemplaza QComboBox con la estética del selector de curso:
 8    botón con flecha ▾ que abre un QMenu estilizado con el tema activo.
 9
10    API compatible con QComboBox:
11        addItem(text, userData=None)
12        addItems(texts)
13        currentText() / currentData() / currentIndex()
14        setCurrentIndex(idx) / setCurrentText(text)
15        count() / itemData(idx)
16        Signal currentIndexChanged(int)
17    """
18
19    currentIndexChanged = Signal(int)
20
21    def __init__(self, parent=None):
22        super().__init__(parent)
23        self.setObjectName('dropdown_btn')
24        self._items: list[tuple[str, object]] = []
25        self._current: int = -1
26        self.clicked.connect(self._show_menu)
27
28    # ── Construcción de ítems ─────────────────────────────────────────────────
29
30    def addItem(self, text: str, userData=None) -> None:
31        self._items.append((text, userData))
32        if self._current == -1:
33            self._apply(0, emit=False)
34
35    def addItems(self, texts) -> None:
36        for t in texts:
37            self.addItem(t)
38
39    # ── Consulta ──────────────────────────────────────────────────────────────
40
41    def currentText(self) -> str:
42        if 0 <= self._current < len(self._items):
43            return self._items[self._current][0]
44        return ''
45
46    def currentData(self):
47        if 0 <= self._current < len(self._items):
48            return self._items[self._current][1]
49        return None
50
51    def currentIndex(self) -> int:
52        return self._current
53
54    def count(self) -> int:
55        return len(self._items)
56
57    def itemData(self, idx: int):
58        if 0 <= idx < len(self._items):
59            return self._items[idx][1]
60        return None
61
62    # ── Selección ─────────────────────────────────────────────────────────────
63
64    def setCurrentIndex(self, idx: int) -> None:
65        if 0 <= idx < len(self._items):
66            self._apply(idx)
67
68    def setCurrentText(self, text: str) -> None:
69        for i, (label, _) in enumerate(self._items):
70            if label == text:
71                self._apply(i)
72                return
73
74    # ── Internos ──────────────────────────────────────────────────────────────
75
76    def _apply(self, idx: int, emit: bool = True) -> None:
77        if idx == self._current:
78            return
79        self._current = idx
80        self.setText(f'{self.currentText()}  ▾')
81        if emit:
82            self.currentIndexChanged.emit(idx)
83
84    def _show_menu(self) -> None:
85        menu = QMenu(self)
86        for i, (label, _) in enumerate(self._items):
87            act = menu.addAction(label)
88            act.setData(i)
89            if i == self._current:
90                act.setCheckable(True)
91                act.setChecked(True)
92        chosen = menu.exec(self.mapToGlobal(self.rect().bottomLeft()))
93        if chosen is not None:
94            self._apply(chosen.data())