gui.utils.dropdown_button

  1# Copyright (C) 2026  David Vaquero <pepesan@gmail.com>
  2#
  3# This program is free software: you can redistribute it and/or modify
  4# it under the terms of the GNU General Public License as published by
  5# the Free Software Foundation, either version 3 of the License, or
  6# (at your option) any later version.
  7#
  8# This program is distributed in the hope that it will be useful,
  9# but WITHOUT ANY WARRANTY; without even the implied warranty of
 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 11# GNU General Public License for more details.
 12#
 13# You should have received a copy of the GNU General Public License
 14# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 15
 16from PySide6.QtWidgets import QPushButton, QMenu
 17from PySide6.QtCore import Signal
 18
 19
 20class DropdownButton(QPushButton):
 21    """
 22    Reemplaza QComboBox con la estética del selector de curso:
 23    botón con flecha ▾ que abre un QMenu estilizado con el tema activo.
 24
 25    API compatible con QComboBox:
 26        addItem(text, userData=None)
 27        addItems(texts)
 28        currentText() / currentData() / currentIndex()
 29        setCurrentIndex(idx) / setCurrentText(text)
 30        count() / itemData(idx)
 31        Signal currentIndexChanged(int)
 32    """
 33
 34    currentIndexChanged = Signal(int)
 35
 36    def __init__(self, parent=None):
 37        super().__init__(parent)
 38        self.setObjectName('dropdown_btn')
 39        self._items: list[tuple[str, object]] = []
 40        self._current: int = -1
 41        self.clicked.connect(self._show_menu)
 42
 43    # ── Construcción de ítems ─────────────────────────────────────────────────
 44
 45    def addItem(self, text: str, userData=None) -> None:
 46        self._items.append((text, userData))
 47        if self._current == -1:
 48            self._apply(0, emit=False)
 49
 50    def addItems(self, texts) -> None:
 51        for t in texts:
 52            self.addItem(t)
 53
 54    # ── Consulta ──────────────────────────────────────────────────────────────
 55
 56    def currentText(self) -> str:
 57        if 0 <= self._current < len(self._items):
 58            return self._items[self._current][0]
 59        return ''
 60
 61    def currentData(self):
 62        if 0 <= self._current < len(self._items):
 63            return self._items[self._current][1]
 64        return None
 65
 66    def currentIndex(self) -> int:
 67        return self._current
 68
 69    def count(self) -> int:
 70        return len(self._items)
 71
 72    def itemData(self, idx: int):
 73        if 0 <= idx < len(self._items):
 74            return self._items[idx][1]
 75        return None
 76
 77    # ── Selección ─────────────────────────────────────────────────────────────
 78
 79    def setCurrentIndex(self, idx: int) -> None:
 80        if 0 <= idx < len(self._items):
 81            self._apply(idx)
 82
 83    def setCurrentText(self, text: str) -> None:
 84        for i, (label, _) in enumerate(self._items):
 85            if label == text:
 86                self._apply(i)
 87                return
 88
 89    # ── Internos ──────────────────────────────────────────────────────────────
 90
 91    def _apply(self, idx: int, emit: bool = True) -> None:
 92        if idx == self._current:
 93            return
 94        self._current = idx
 95        self.setText(f'{self.currentText()}  ▾')
 96        if emit:
 97            self.currentIndexChanged.emit(idx)
 98
 99    def _show_menu(self) -> None:
100        menu = QMenu(self)
101        for i, (label, _) in enumerate(self._items):
102            act = menu.addAction(label)
103            act.setData(i)
104            if i == self._current:
105                act.setCheckable(True)
106                act.setChecked(True)
107        chosen = menu.exec(self.mapToGlobal(self.rect().bottomLeft()))
108        if chosen is not None:
109            self._apply(chosen.data())