ogclient/src/kiosk/kiosk.py

218 lines
7.4 KiB
Python

# Copyright (C) 2020-2025 Soleta Networks <info@soleta.eu>
#
# 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()