from __future__ import annotations from typing import Any from collections.abc import Callable import signal from PySide6.QtWidgets import QApplication # type: ignore from PySide6.QtWidgets import QMainWindow from PySide6.QtWidgets import QWidget from PySide6.QtWidgets import QGraphicsScene from PySide6.QtWidgets import QVBoxLayout from PySide6.QtWidgets import QPushButton from PySide6.QtWidgets import QHBoxLayout from PySide6.QtWidgets import QScrollArea from PySide6.QtWidgets import QComboBox from PySide6.QtWidgets import QLayout from PySide6.QtWidgets import QSizePolicy from PySide6.QtWidgets import QMessageBox from PySide6.QtWidgets import QLineEdit from PySide6.QtGui import QFontDatabase # type: ignore from PySide6.QtGui import QMouseEvent from PySide6.QtGui import QIcon from PySide6.QtGui import QKeyEvent from PySide6.QtCore import Qt # type: ignore from PySide6.QtCore import Signal from PySide6.QtCore import QUrl from PySide6.QtMultimedia import QMediaPlayer # type: ignore from PySide6.QtMultimedia import QAudioOutput from .config import Config from .utils import Utils class SpecialButton(QPushButton): # type: ignore middleClicked = Signal() def mousePressEvent(self, e: QMouseEvent) -> None: if e.button() == Qt.MiddleButton: self.middleClicked.emit() else: super().mousePressEvent(e) class FilterLineEdit(QLineEdit): # type: ignore def keyPressEvent(self, e: QKeyEvent) -> None: if e.key() == Qt.Key_Escape: self.clear() else: super().keyPressEvent(e) class Window: app: QApplication window: QMainWindow root: QVBoxLayout view: QVBoxLayout view_scene: QGraphicsScene speed: QComboBox scroll_area: QScrollArea info: SpecialButton font: str emoji_font: str player: QMediaPlayer audio: QAudioOutput filter: QLineEdit @staticmethod def prepare() -> None: Window.make() Window.add_buttons() Window.add_view() Window.add_footer() @staticmethod def make() -> None: Window.app = QApplication([]) Window.app.setApplicationName(Config.program) Window.window = QMainWindow() Window.window.setWindowTitle(Config.title) Window.window.resize(Config.width, Config.height) central_widget = QWidget() Window.root = QVBoxLayout() central_widget.setLayout(Window.root) Window.root.setAlignment(Qt.AlignTop) Window.window.setCentralWidget(central_widget) Window.window.setWindowIcon(QIcon(str(Config.icon_path))) Window.root.setContentsMargins(0, 0, 0, 0) Window.set_style() @staticmethod def set_style() -> None: font_id = QFontDatabase.addApplicationFont(str(Config.font_path)) emoji_font_id = QFontDatabase.addApplicationFont(str(Config.emoji_font_path)) if font_id != -1: Window.font = QFontDatabase.applicationFontFamilies(font_id)[0] if emoji_font_id != -1: Window.emoji_font = QFontDatabase.applicationFontFamilies(emoji_font_id)[0] style = f""" QWidget {{ background-color: {Config.background_color}; color: {Config.text_color}; font-size: {Config.font_size}px; }} QMenu {{ background-color: {Config.alt_background_color}; color: {Config.alt_text_color}; border: 1px solid {Config.alt_border_color}; }} QMenu::item:selected {{ background-color: {Config.alt_hover_background_color}; color: {Config.alt_hover_text_color}; }} QMessageBox {{ background-color: {Config.alt_background_color}; color: {Config.alt_text_color}; border: 1px solid {Config.alt_border_color}; }} QMessageBox QLabel {{ background-color: {Config.alt_background_color}; color: {Config.alt_text_color}; }} QMessageBox QPushButton {{ background-color: {Config.alt_background_color}; color: {Config.alt_text_color}; }} QMessageBox QPushButton:hover {{ background-color: {Config.message_box_button_hover_background_color}; color: {Config.message_box_button_hover_text_color}; }} QComboBox {{ selection-background-color: {Config.alt_hover_background_color}; selection-color: {Config.alt_hover_text_color}; }} QComboBox QAbstractItemView {{ background-color: {Config.alt_background_color}; color: {Config.alt_text_color}; border: 1px solid {Config.alt_border_color}; }} QScrollBar:vertical {{ border: 0px solid transparent; background: {Config.background_color}; width: 15px; margin: 0px 0px 0px 0px; }} QScrollBar::handle:vertical {{ background: {Config.scrollbar_handle_color}; min-height: 20px; }} QScrollBar::add-line:vertical {{ border: none; background: none; }} QScrollBar::sub-line:vertical {{ border: none; background: none; }} QLineEdit {{ background-color: {Config.input_background_color}; color: {Config.input_text_color}; border: 1px solid {Config.input_border_color}; }} """.strip() Window.app.setStyleSheet(style) Window.app.setFont(Window.font) @staticmethod def add_buttons() -> None: from .ants import Ants from .game import Game from .filter import Filter root = QWidget() container = QHBoxLayout() btn_hatch = SpecialButton("Hatch") btn_hatch.setToolTip("Hatch a new ant\nMiddle Click to hatch Trio") btn_hatch.clicked.connect(lambda e: Ants.hatch()) btn_hatch.middleClicked.connect(lambda: Ants.hatch_burst()) btn_terminate = SpecialButton("Terminate") btn_terminate.setToolTip( "Terminate a random ant\nMiddle Click to terminate all" ) btn_terminate.clicked.connect(lambda e: Ants.terminate()) btn_terminate.middleClicked.connect(lambda: Ants.terminate_all()) Window.speed = QComboBox() tooltip = "The speed of the updates\n" tooltip += f"Fast: {Utils.get_seconds(Config.loop_delay_fast)}\n" tooltip += f"Normal: {Utils.get_seconds(Config.loop_delay_normal)}\n" tooltip += f"Slow: {Utils.get_seconds(Config.loop_delay_slow)}" Window.speed.setToolTip(tooltip) Window.speed.addItems(["Fast", "Normal", "Slow"]) Window.speed.setCurrentIndex(1) Window.speed.currentIndexChanged.connect(Game.update_speed) Window.filter = FilterLineEdit() Window.filter.setFixedWidth(120) Window.filter.setPlaceholderText("Filter") Window.filter.mousePressEvent = lambda e: Window.to_top() Window.filter.keyReleaseEvent = lambda e: Filter.filter(e) container.addWidget(btn_hatch) container.addWidget(btn_terminate) container.addWidget(Window.speed) container.addWidget(Window.filter) root.setLayout(container) Window.root.addWidget(root) @staticmethod def add_view() -> None: Window.scroll_area = QScrollArea() Window.scroll_area.setWidgetResizable(True) container = QWidget() parent = QVBoxLayout(container) Window.view = QVBoxLayout() parent.addLayout(Window.view) Window.view.setAlignment(Qt.AlignTop) Window.scroll_area.setWidget(container) Window.root.addWidget(Window.scroll_area) @staticmethod def start() -> None: signal.signal(signal.SIGINT, signal.SIG_DFL) Window.window.show() Window.app.exec() @staticmethod def close() -> None: Window.app.quit() @staticmethod def delete_layout(layout: QLayout) -> None: while layout.count(): item = layout.takeAt(0) if item.widget(): item.widget().deleteLater() elif item.layout(): Window.delete_layout(item.layout()) layout.deleteLater() @staticmethod def expand(widget: QWidget) -> None: widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) @staticmethod def confirm(message: str, action: Callable[..., Any]) -> None: msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Icon.Question) msg_box.setWindowTitle("Confirm") msg_box.setText(message) msg_box.setStandardButtons( QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) msg_box.setDefaultButton(QMessageBox.StandardButton.No) msg_box.button(QMessageBox.StandardButton.Yes).clicked.connect(action) msg_box.exec() @staticmethod def clear_view() -> None: while Window.view.count(): item = Window.view.takeAt(0) if item.widget(): item.widget().deleteLater() elif item.layout(): Window.delete_layout(item.layout()) @staticmethod def to_top() -> None: Window.scroll_area.verticalScrollBar().setValue(0) @staticmethod def to_bottom() -> None: Window.scroll_area.verticalScrollBar().setValue( Window.scroll_area.verticalScrollBar().maximum() ) @staticmethod def toggle_scroll() -> None: maxim = Window.scroll_area.verticalScrollBar().maximum() if Window.scroll_area.verticalScrollBar().value() == maxim: Window.to_top() else: Window.to_bottom() @staticmethod def add_footer() -> None: root = QWidget() root.setContentsMargins(0, 0, 0, 0) container = QHBoxLayout() Window.info = SpecialButton("---") Window.info.setToolTip("Click to scroll to the bottom or top") Window.info.clicked.connect(Window.toggle_scroll) Window.info.setMinimumSize(35, 35) container.addWidget(Window.info) root.setLayout(container) Window.root.addWidget(root) @staticmethod def play_audio(path: str, on_stop: Callable[..., Any] | None = None) -> None: Window.player = QMediaPlayer() Window.audio = QAudioOutput() Window.player.setAudioOutput(Window.audio) Window.player.setSource(QUrl.fromLocalFile(path)) Window.audio.setVolume(100) def handle_state_change(state: QMediaPlayer.State) -> None: if state == QMediaPlayer.StoppedState: if on_stop: on_stop() Window.player.playbackStateChanged.connect(handle_state_change) Window.player.play() @staticmethod def stop_audio() -> None: Window.player.stop() @staticmethod def alert(message: str) -> None: msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Information) msg_box.setText(message) msg_box.setWindowTitle("Information") msg_box.setStandardButtons(QMessageBox.Ok) msg_box.exec()