""" Provides the graphical front-end for Piket. """ import os import sys # pylint: disable=E0611 from PySide2.QtWidgets import ( QAction, QActionGroup, QApplication, QGridLayout, QInputDialog, QMainWindow, QPushButton, QSizePolicy, QToolBar, QWidget, ) from PySide2.QtGui import QIcon from PySide2.QtCore import QSize, Qt, QObject, Signal, Slot # pylint: enable=E0611 try: import dbus except ImportError: dbus = None from piket_client.sound import PLOP_WAVE from piket_client.model import Person, ConsumptionType def plop() -> None: """ Asynchronously play the plop sound. """ PLOP_WAVE.play() class NameButton(QPushButton): """ Wraps a QPushButton to provide a counter. """ def __init__(self, person: Person, active_id: str, *args, **kwargs) -> None: self.person = person self.active_id = active_id super().__init__(self.current_label, *args, **kwargs) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.clicked.connect(self.process_click) @Slot(str) def new_active_id(self, new_id: str): self.active_id = new_id self.setText(self.current_label) @property def active_consumption_type_id(self) -> str: return self.parent().active_consumption_type_id @property def current_count(self) -> int: return self.person.consumptions.get(self.active_id, 0) @property def current_label(self) -> str: """ Return the label to show on the button. """ return f"{self.person.name} ({self.current_count})" def process_click(self) -> None: """ Process a click on this button. """ if self.person.add_consumption(self.active_id): self.setText(self.current_label) plop() else: print("Jantoeternuitje, kapot") class NameButtons(QWidget): """ Main widget responsible for capturing presses and registering them. """ new_id_set = Signal(str) def __init__(self, consumption_type_id) -> None: super().__init__() self.layout = None self.active_consumption_type_id = consumption_type_id self.init_ui() @Slot(str) def consumption_type_changed(self, new_id: str): self.active_consumption_type_id = new_id self.new_id_set.emit(new_id) def init_ui(self) -> None: """ Initialize UI: build GridLayout, retrieve People and build a button for each. """ self.layout = QGridLayout() for index, person in enumerate(Person.get_all()): button = NameButton(person, self.active_consumption_type_id, self) self.new_id_set.connect(button.new_active_id) self.layout.addWidget(button, index // 2, index % 2) self.setLayout(self.layout) class PiketMainWindow(QMainWindow): """ QMainWindow subclass responsible for showing the main application window. """ consumption_type_changed = Signal(str) def __init__(self) -> None: super().__init__() self.main_widget = None self.toolbar = None self.osk = None self.init_ui() def init_ui(self) -> None: """ Initialize the UI: construct main widget and toolbar. """ # Connect to dbus, get handle to virtual keyboard if dbus: try: session_bus = dbus.SessionBus() self.osk = session_bus.get_object( "org.onboard.Onboard", "/org/onboard/Onboard/Keyboard" ) except dbus.exceptions.DBusException: # Onboard not present or dbus broken self.osk = None # Go full screen self.setWindowState(Qt.WindowActive | Qt.WindowFullScreen) font_metrics = self.fontMetrics() icon_size = font_metrics.height() * 2 # Initialize toolbar self.toolbar = QToolBar() self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.toolbar.setIconSize(QSize(icon_size, icon_size)) # Left self.toolbar.addAction( self.load_icon("add_person.svg"), "Nieuw persoon", self.add_person ) self.toolbar.addAction(self.load_icon("undo.svg"), "Oeps") self.toolbar.addWidget(self.create_spacer()) # Right ag = QActionGroup(self.toolbar) ag.setExclusive(True) for ct in ConsumptionType.get_all(): action = QAction(self.load_icon(ct.icon or "beer_bottle.svg"), ct.name, ag) action.setCheckable(True) action.setData(str(ct.consumption_type_id)) ag.actions()[0].setChecked(True) [self.toolbar.addAction(a) for a in ag.actions()] ag.triggered.connect(self.consumption_type_change) self.addToolBar(self.toolbar) # Initialize main widget self.main_widget = NameButtons(ag.actions()[0].data()) self.consumption_type_changed.connect(self.main_widget.consumption_type_changed) self.setCentralWidget(self.main_widget) @Slot(QAction) def consumption_type_change(self, action: QAction): self.consumption_type_changed.emit(action.data()) def show_keyboard(self) -> None: """ Show the virtual keyboard, if possible. """ if self.osk: self.osk.Show() def hide_keyboard(self) -> None: """ Hide the virtual keyboard, if possible. """ if self.osk: self.osk.Hide() def add_person(self) -> None: """ Ask for a new Person and register it, then rebuild the central widget. """ self.show_keyboard() name, ok = QInputDialog.getItem( self, "Persoon toevoegen", "Voer de naam van de nieuwe persoon in, of kies uit de lijst.", ["Cas", "Frenk"], 0, True, ) self.hide_keyboard() if ok and name: person = Person(name=name) person = person.save() self.main_widget = NameButtons() self.setCentralWidget(self.main_widget) @staticmethod def create_spacer() -> QWidget: """ Return an empty QWidget that automatically expands. """ spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) return spacer icons_dir = os.path.join(os.path.dirname(__file__), "icons") @classmethod def load_icon(cls, filename: str) -> QIcon: """ Return a QtIcon loaded from the given `filename` in the icons directory. """ return QIcon(os.path.join(cls.icons_dir, filename)) def main() -> None: """ Main entry point of GUI client. """ app = QApplication(sys.argv) font = app.font() size = font.pointSize() font.setPointSize(size * 1.75) app.setFont(font) main_window = PiketMainWindow() main_window.show() app.exec_() if __name__ == "__main__": main()