removed last of tk, communication between gui and client

This commit is contained in:
Christoph Stahl 2024-09-22 18:59:02 +02:00
parent fdec53a884
commit d33497d09c

View file

@ -6,12 +6,16 @@ from datetime import datetime
import os import os
import builtins import builtins
from functools import partial from functools import partial
import random
from typing import TYPE_CHECKING, Any, Optional from typing import TYPE_CHECKING, Any, Optional
import webbrowser import webbrowser
import multiprocessing import multiprocessing
import secrets import secrets
import string import string
import subprocess
import signal
from PyQt6.QtCore import QTimer
from PyQt6.QtGui import QCloseEvent, QIcon, QPixmap from PyQt6.QtGui import QCloseEvent, QIcon, QPixmap
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QApplication,
@ -83,68 +87,45 @@ class OptionFrame(QWidget):
name: str, name: str,
element: QLineEdit, element: QLineEdit,
line: QWidget, line: QWidget,
frame: QWidget, layout: QVBoxLayout,
) -> None: ) -> None:
self.list_options[name].remove(element) self.list_options[name].remove(element)
layout = frame.layout()
if layout is None:
raise ValueError("No layout found")
layout.removeWidget(line) layout.removeWidget(line)
line.deleteLater() line.deleteLater()
def add_list_element( def add_list_element(
self, self,
name: str, name: str,
frame: QWidget, layout: QVBoxLayout,
init: str, init: str,
callback: Optional[Callable[..., None]], callback: Optional[Callable[..., None]],
) -> None: ) -> None:
input_and_minus = QWidget(frame) input_and_minus = QWidget()
input_and_minus_layout = QHBoxLayout(input_and_minus) input_and_minus_layout = QHBoxLayout(input_and_minus)
input_and_minus.setLayout(input_and_minus_layout) input_and_minus.setLayout(input_and_minus_layout)
input_field = QLineEdit(frame) input_and_minus_layout.setContentsMargins(0, 0, 0, 0)
input_field = QLineEdit(input_and_minus)
input_field.setText(init) input_field.setText(init)
input_field.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) input_field.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
input_and_minus_layout.addWidget(input_field) input_and_minus_layout.addWidget(input_field)
if callback is not None: if callback is not None:
input_field.textChanged.connect(callback) input_field.textChanged.connect(callback)
minus_button = QPushButton("-", frame) minus_button = QPushButton("-", input_and_minus)
minus_button.clicked.connect( minus_button.clicked.connect(
partial(self.del_list_element, name, input_field, input_and_minus, frame) partial(self.del_list_element, name, input_field, input_and_minus, layout)
) )
minus_button.setFixedWidth(40) minus_button.setFixedWidth(40)
minus_button.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed) minus_button.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
input_and_minus_layout.addWidget(minus_button) input_and_minus_layout.addWidget(minus_button)
layout = frame.layout()
if not isinstance(layout, QVBoxLayout):
raise ValueError("No layout found")
# insert second from last
layout.insertWidget(layout.count() - 1, input_and_minus) layout.insertWidget(layout.count() - 1, input_and_minus)
self.list_options[name].append(input_field) self.list_options[name].append(input_field)
# input_and_minus = customtkinter.CTkFrame(frame)
# input_and_minus.pack(side="top", fill="x", expand=True)
# input_field = customtkinter.CTkTextbox(input_and_minus, wrap="none", height=1)
# input_field.pack(side="left", fill="x", expand=True)
# input_field.insert("0.0", init)
# if callback is not None:
# input_field.bind("<KeyRelease>", callback)
# input_field.bind("<ButtonRelease>", callback)
#
# minus_button = customtkinter.CTkButton(
# input_and_minus,
# text="-",
# width=40,
# command=partial(self.del_list_element, name, input_field, input_and_minus),
# )
# minus_button.pack(side="right")
# self.list_options[name].append(input_field)
def add_list_option( def add_list_option(
self, self,
name: str, name: str,
@ -154,29 +135,20 @@ class OptionFrame(QWidget):
) -> None: ) -> None:
label = QLabel(description, self) label = QLabel(description, self)
container = QWidget(self) container_layout = QVBoxLayout()
container_layout = QVBoxLayout(container)
container.setLayout(container_layout)
self.form_layout.addRow(label, container) self.form_layout.addRow(label, container_layout)
# frame = customtkinter.CTkFrame(self)
# frame.grid(column=1, row=self.number_of_options, sticky="EW")
self.list_options[name] = [] self.list_options[name] = []
for v in value: for v in value:
self.add_list_element(name, container, v, callback) self.add_list_element(name, container_layout, v, callback)
plus_button = QPushButton("+", self) plus_button = QPushButton("+", self)
plus_button.clicked.connect(partial(self.add_list_element, name, container, "", callback)) plus_button.clicked.connect(
partial(self.add_list_element, name, container_layout, "", callback)
)
plus_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) plus_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
# customtkinter.CTkButton(
# frame,
# text="+",
# command=partial(self.add_list_element, name, frame, "", callback),
# )
container_layout.addWidget(plus_button) container_layout.addWidget(plus_button)
# plus_button.pack(side="bottom", fill="x", expand=True)
self.number_of_options += 1 self.number_of_options += 1
@ -193,13 +165,27 @@ class OptionFrame(QWidget):
def add_date_time_option(self, name: str, description: str, value: str) -> None: def add_date_time_option(self, name: str, description: str, value: str) -> None:
label = QLabel(description, self) label = QLabel(description, self)
self.date_time_options[name] = QDateTimeEdit(self) date_time_layout = QHBoxLayout()
try: date_time_widget = QDateTimeEdit(self)
self.date_time_options[name].setDateTime(datetime.fromisoformat(value)) date_time_enabled = QCheckBox("Enabled", self)
except (TypeError, ValueError): date_time_enabled.stateChanged.connect(
self.date_time_options[name].setDateTime(datetime.now()) # TODO lambda: date_time_widget.setEnabled(date_time_enabled.isChecked())
)
self.form_layout.addRow(label, self.date_time_options[name]) self.date_time_options[name] = (date_time_widget, date_time_enabled)
date_time_widget.setCalendarPopup(True)
try:
date_time_widget.setDateTime(datetime.fromisoformat(value))
date_time_enabled.setChecked(True)
except (TypeError, ValueError):
date_time_widget.setDateTime(datetime.now())
date_time_widget.setEnabled(False)
date_time_enabled.setChecked(False)
date_time_layout.addWidget(date_time_widget)
date_time_layout.addWidget(date_time_enabled)
self.form_layout.addRow(label, date_time_layout)
self.number_of_options += 1 self.number_of_options += 1
@ -212,7 +198,7 @@ class OptionFrame(QWidget):
self.choose_options: dict[str, QComboBox] = {} self.choose_options: dict[str, QComboBox] = {}
self.bool_options: dict[str, QCheckBox] = {} self.bool_options: dict[str, QCheckBox] = {}
self.list_options: dict[str, list[QLineEdit]] = {} self.list_options: dict[str, list[QLineEdit]] = {}
self.date_time_options: dict[str, QDateTimeEdit] = {} self.date_time_options: dict[str, tuple[QDateTimeEdit, QCheckBox]] = {}
def get_config(self) -> dict[str, Any]: def get_config(self) -> dict[str, Any]:
config: dict[str, Any] = {} config: dict[str, Any] = {}
@ -230,7 +216,10 @@ class OptionFrame(QWidget):
for textbox in textboxes: for textbox in textboxes:
config[name].append(textbox.text().strip()) config[name].append(textbox.text().strip())
for name, picker in self.date_time_options.items(): for name, (picker, checkbox) in self.date_time_options.items():
if not checkbox.isChecked():
config[name] = None
continue
try: try:
config[name] = picker.dateTime().toPyDateTime().isoformat() config[name] = picker.dateTime().toPyDateTime().isoformat()
except ValueError: except ValueError:
@ -266,18 +255,18 @@ class GeneralConfig(OptionFrame):
self.add_string_option("server", "Server", config["server"], callback) self.add_string_option("server", "Server", config["server"], callback)
self.add_string_option("room", "Room", config["room"], callback) self.add_string_option("room", "Room", config["room"], callback)
self.add_string_option("secret", "Secret", config["secret"]) self.add_string_option("secret", "Admin Password", config["secret"])
self.add_choose_option( self.add_choose_option(
"waiting_room_policy", "waiting_room_policy",
"Waiting room policy", "Waiting room policy",
["forced", "optional", "none"], ["forced", "optional", "none"],
str(config["waiting_room_policy"]).lower(), str(config["waiting_room_policy"]).lower(),
) )
self.add_date_time_option("last_song", "Time of last song", config["last_song"]) self.add_date_time_option("last_song", "Last song ends at", config["last_song"])
self.add_string_option( self.add_string_option(
"preview_duration", "Preview duration in seconds", str(config["preview_duration"]) "preview_duration", "Preview duration in seconds", str(config["preview_duration"])
) )
self.add_string_option("key", "Key for server", config["key"]) self.add_string_option("key", "Key for server (if necessary)", config["key"])
def get_config(self) -> dict[str, Any]: def get_config(self) -> dict[str, Any]:
config = super().get_config() config = super().get_config()
@ -297,9 +286,9 @@ class SyngGui(QMainWindow):
if self.syng_client is not None: if self.syng_client is not None:
self.syng_client.terminate() self.syng_client.terminate()
self.syng_client.join() self.syng_client.wait(1.0)
self.syng_client.kill()
# self.withdraw()
self.destroy() self.destroy()
def add_buttons(self) -> None: def add_buttons(self) -> None:
@ -311,13 +300,18 @@ class SyngGui(QMainWindow):
self.buttons_layout.addWidget(self.startsyng_serverbutton) self.buttons_layout.addWidget(self.startsyng_serverbutton)
spacer_item = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) spacer_item = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.notification_label = QLabel("", self)
spacer_item2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.buttons_layout.addItem(spacer_item) self.buttons_layout.addItem(spacer_item)
self.buttons_layout.addWidget(self.notification_label)
self.buttons_layout.addItem(spacer_item2)
self.savebutton = QPushButton("Save") self.savebutton = QPushButton("Save")
self.savebutton.clicked.connect(self.save_config) self.savebutton.clicked.connect(self.save_config)
self.buttons_layout.addWidget(self.savebutton) self.buttons_layout.addWidget(self.savebutton)
self.startbutton = QPushButton("Save and Start") self.startbutton = QPushButton("Save and Start")
self.startbutton.clicked.connect(self.start_syng_client) self.startbutton.clicked.connect(self.start_syng_client)
self.buttons_layout.addWidget(self.startbutton) self.buttons_layout.addWidget(self.startbutton)
@ -369,11 +363,11 @@ class SyngGui(QMainWindow):
rel_path = os.path.dirname(__file__) rel_path = os.path.dirname(__file__)
qt_img = QPixmap(os.path.join(rel_path, "static/syng.png")) qt_img = QPixmap(os.path.join(rel_path, "static/syng.png"))
qt_icon = QIcon(qt_img) self.qt_icon = QIcon(qt_img)
self.setWindowIcon(qt_icon) self.setWindowIcon(self.qt_icon)
self.syng_server: Optional[Process] = None self.syng_server: Optional[Process] = None
self.syng_client: Optional[Process] = None self.syng_client: Optional[subprocess.Popen] = None
self.configfile = os.path.join(platformdirs.user_config_dir("syng"), "config.yaml") self.configfile = os.path.join(platformdirs.user_config_dir("syng"), "config.yaml")
@ -394,6 +388,12 @@ class SyngGui(QMainWindow):
config["config"]["secret"] = "".join( config["config"]["secret"] = "".join(
secrets.choice(string.ascii_letters + string.digits) for _ in range(8) secrets.choice(string.ascii_letters + string.digits) for _ in range(8)
) )
if config["config"]["room"] == "":
config["config"]["room"] = "".join(
[random.choice(string.ascii_letters) for _ in range(6)]
).upper()
self.central_widget = QWidget(parent=self) self.central_widget = QWidget(parent=self)
self.central_layout = QVBoxLayout(self.central_widget) self.central_layout = QVBoxLayout(self.central_widget)
@ -416,6 +416,10 @@ class SyngGui(QMainWindow):
self.setCentralWidget(self.central_widget) self.setCentralWidget(self.central_widget)
# check every 100 ms if client is running
self.timer = QTimer()
self.timer.timeout.connect(self.check_if_client_is_running)
def save_config(self) -> None: def save_config(self) -> None:
os.makedirs(os.path.dirname(self.configfile), exist_ok=True) os.makedirs(os.path.dirname(self.configfile), exist_ok=True)
@ -431,21 +435,43 @@ class SyngGui(QMainWindow):
return {"sources": sources, "config": general_config} return {"sources": sources, "config": general_config}
def start_syng_client(self) -> None: def check_if_client_is_running(self) -> None:
if self.syng_client is None: if self.syng_client is None:
self.save_config() self.timer.stop()
config = self.gather_config() return
self.syng_client = multiprocessing.Process(
target=create_async_and_start_client, args=(config,) ret = self.syng_client.poll()
) if ret is not None:
self.syng_client.start() _, stderr = self.syng_client.communicate()
stderr_lines = stderr.decode("utf-8").strip().split("\n")
if stderr_lines and stderr_lines[-1].startswith("Warning"):
self.notification_label.setText(stderr_lines[-1])
else:
self.notification_label.setText("")
self.syng_client.wait()
self.syng_client = None
self.set_client_button_start()
else:
self.set_client_button_stop()
def set_client_button_stop(self) -> None:
self.startbutton.setText("Stop") self.startbutton.setText("Stop")
def set_client_button_start(self) -> None:
self.startbutton.setText("Save and Start")
def start_syng_client(self) -> None:
if self.syng_client is None or self.syng_client.poll() is not None:
self.save_config()
self.syng_client = subprocess.Popen(["syng", "client"], stderr=subprocess.PIPE)
self.notification_label.setText("")
self.timer.start()
self.set_client_button_stop()
else: else:
self.syng_client.terminate() self.syng_client.terminate()
self.syng_client.join() self.syng_client.wait(1.0)
self.syng_client = None self.syng_client.kill()
# self.startbutton.configure(text="Save and Start") self.set_client_button_start()
self.startbutton.setText("Save and Start")
def start_syng_server(self) -> None: def start_syng_server(self) -> None:
if self.syng_server is None: if self.syng_server is None:
@ -494,11 +520,16 @@ class SyngGui(QMainWindow):
syng_server = config["server"] syng_server = config["server"]
syng_server += "" if syng_server.endswith("/") else "/" syng_server += "" if syng_server.endswith("/") else "/"
room = config["room"] room = config["room"]
self.linklabel.setText(f'<a href="{syng_server + room}">{syng_server + room}</a>') self.linklabel.setText(
f'<center><a href="{syng_server + room}">{syng_server + room}</a><center>'
)
self.linklabel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.change_qr(syng_server + room) self.change_qr(syng_server + room)
def run_gui() -> None: def run_gui() -> None:
signal.signal(signal.SIGINT, signal.SIG_DFL)
app = QApplication([]) app = QApplication([])
app.setApplicationName("Syng") app.setApplicationName("Syng")
app.setDesktopFileName("rocks.syng.Syng") app.setDesktopFileName("rocks.syng.Syng")