# Copyright (C) 2020-2025 Soleta Networks # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the # Free Software Foundation; either version 3 of the License, or # (at your option) any later version. from PyQt6.QtCore import ( Qt, QPropertyAnimation, QRect, QFile, QTextStream, QTranslator, QLocale, QSocketNotifier ) from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout ) import sys from src.kiosk.sidepanel import SidePanel, ViewType import src.kiosk.thirdparty.breeze.breeze_pyqt6 from src.kiosk.spinner import * from src.kiosk.config import * SIDE_PANEL_WIDTH = 200 class Kiosk(QMainWindow): def __init__(self, socket): super().__init__() self.side_panel = None self.spinner = None # socket to read/send events from/to ogClient self.socket = socket if socket: self.socket_notifier = QSocketNotifier( self.socket.fileno(), QSocketNotifier.Type.Read ) self.socket_notifier.activated.connect(self.read_from_socket) self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.setWindowTitle('Kiosk') central_widget = QWidget() self.setCentralWidget(central_widget) layout = QHBoxLayout() central_widget.setLayout(layout) self.main_content = QWidget() self.main_content.setLayout(QVBoxLayout()) layout.addWidget(self.main_content) self.reload_theme() def closeEvent(self, event): if self.socket_notifier: self.socket_notifier.setEnabled(False) self.socket_notifier.disconnect() self.socket_notifier.deleteLater() if self.socket: self.socket.close() super().closeEvent(event) def send_to_ogclient(self, data): try: json_data = json.dumps(data) self.socket.send(json_data.encode('utf-8')) except TypeError as e: print(f'Failed to encode data to JSON: {e}') except Exception as e: print(f'Unexpected error in send_kiosk_event: {e}') def read_from_socket(self): try: data = self.socket.recv(1024).decode('utf-8') if not data: return # Socket closed payload = json.loads(data) if payload.get('command') == 'state': status = payload.get('status', 'idle') self.apply_busy_mode_configuration(status) elif payload.get('command') == 'refresh': self.reload_theme() elif payload.get('command') == 'close': self.close() except OSError as e: print(f'Failed to read from socket: {e}') except json.JSONDecodeError as e: print(f'Failed to decode JSON: {e}') def reload_theme(self): # Floating side panel if self.side_panel: self.side_panel.setParent(None) self.side_panel.deleteLater() self.side_panel = SidePanel() self.side_panel.setParent(self) self.side_panel.setFixedWidth(SIDE_PANEL_WIDTH) self.side_panel.panel_hidden = True self.side_panel.setGeometry(-SIDE_PANEL_WIDTH, 0, SIDE_PANEL_WIDTH, self.height()) self.side_panel.widget_change_requested.connect(self.update_view) self.side_panel.emit_default_widget() self.side_panel.setVisible(True) self.side_panel.raise_() self.animation = QPropertyAnimation(self.side_panel, b'geometry') self.animation.setDuration(200) def apply_busy_mode_configuration(self, status): if get_status() == status: return set_status(status) if is_busy_enabled(): self.toggle_panel() self.side_panel.request_widget_change(ViewType.SYSTEM_MONITOR) self.spinner = LoadSpinner(self) self.spinner.setHeight(30) self.spinner.setColor(get_main_color_theme()) self.spinner.start() self.spinner.show() self.spinner.setToolTip(self.tr('The client is busy, please wait...')) return if self.spinner: self.spinner.setParent(None) self.spinner.deleteLater() def keyPressEvent(self, event): if side_panel_enabled() and event.key() == Qt.Key.Key_F1: self.side_panel.request_widget_change(ViewType.SYSTEM_MONITOR) elif side_panel_enabled() and event.key() == Qt.Key.Key_F2: self.side_panel.request_widget_change(ViewType.BOOT_OS) elif event.key() == Qt.Key.Key_P: self.toggle_panel() else: super().keyPressEvent(event) def toggle_panel(self): if self.side_panel.panel_hidden and side_panel_enabled(): # Show the panel self.animation.setStartValue(self.side_panel.geometry()) self.animation.setEndValue(QRect(0, 0, SIDE_PANEL_WIDTH, self.height())) self.side_panel.panel_hidden = False else: # Hide the panel self.animation.setStartValue(self.side_panel.geometry()) self.animation.setEndValue(QRect(-SIDE_PANEL_WIDTH, 0, SIDE_PANEL_WIDTH, self.height())) self.side_panel.panel_hidden = True self.animation.start() def resizeEvent(self, event): super().resizeEvent(event) # Adjust panel geometry based on visibility if self.side_panel.panel_hidden: self.side_panel.setGeometry(-SIDE_PANEL_WIDTH, 0, SIDE_PANEL_WIDTH, self.height()) else: self.side_panel.setGeometry(0, 0, SIDE_PANEL_WIDTH, self.height()) def update_view(self, widget): layout = self.main_content.layout() # Clear existing widgets for i in reversed(range(layout.count())): widget_item = layout.itemAt(i).widget() if widget_item: widget_item.setParent(None) widget_item.deleteLater() layout.addWidget(widget) widget.command_requested.connect(self.send_to_ogclient) def closeEvent(self, event): logging.info("Closing Kiosk application...") if self.socket_notifier: self.socket_notifier.setEnabled(False) if self.socket: self.socket.close() event.accept() def launch_kiosk(socket, debug_mode): app = QApplication(sys.argv) translator = QTranslator() locale = QLocale.system().name() translation_file = f'kiosk_{locale}.qm' translations_dir = os.path.join( os.path.dirname(__file__), 'translations') if translator.load(translation_file, translations_dir): app.installTranslator(translator) file = QFile(':/light/stylesheet.qss') file.open(QFile.OpenModeFlag.ReadOnly | QFile.OpenModeFlag.Text) stream = QTextStream(file) app.setStyleSheet(stream.readAll()) gui = Kiosk(socket) if debug_mode: gui.showMaximized() else: gui.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window) gui.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, True) gui.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True) gui.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent, True) screen = QApplication.primaryScreen() geometry = screen.availableGeometry() gui.setGeometry(geometry) gui.show() return app.exec()