import logging from pathlib import Path from typing import Callable from PySide6 import QtWidgets from PySide6.QtCore import QSize from PySide6.QtGui import QIcon, Qt, QPixmap from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget, QStyle, QToolButton, \ QStackedWidget from core_lib.qt.hack import move_window_to_center, is_light_theme, \ recolor_icon from core_lib.qt import messagebox from pages.account import AccountPage from pages.test import TestPage LAUNCHER_VERSION = "alpha_0.0.2" MAIN_REPO_URL = "https://repo.weispace.net/wei/Launcher/" class ToolBar(QtWidgets.QToolBar): def __init__(self, app: QApplication, parent=None): super(ToolBar, self).__init__(parent) self.app = app self.spacer = QWidget() self.orientationChanged.connect(self._update_widget_size) def _update_widget_size(self, orientation): vertical = orientation == Qt.Orientation.Vertical if vertical: self.spacer.setSizePolicy( QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Expanding, ) else: self.spacer.setSizePolicy( QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Preferred, ) for button in self.findChildren(QToolButton): if vertical: button.setToolButtonStyle( Qt.ToolButtonStyle.ToolButtonIconOnly ) button.setFixedSize(60, 60) continue button.setMinimumSize(0, 0) button.setMaximumSize(16777215, 16777215) if not button.icon().isNull() and not button.text(): button.setToolButtonStyle( Qt.ToolButtonStyle.ToolButtonIconOnly ) button.setFixedSize(60, 60) elif button.icon().isNull(): button.setToolButtonStyle( Qt.ToolButtonStyle.ToolButtonTextOnly ) button.setFixedWidth(150) else: button.setToolButtonStyle( Qt.ToolButtonStyle.ToolButtonTextBesideIcon ) button.setFixedWidth(150) def add_button(self, text=None, icon: QIcon=None) -> QToolButton: button = QtWidgets.QToolButton(self) if text: text = " "+text button.setText(text) if icon: button.setIcon(icon) button.setIconSize(QSize(32, 32)) if text: button.setToolButtonStyle( Qt.ToolButtonStyle.ToolButtonTextBesideIcon ) else: button.setToolButtonStyle( Qt.ToolButtonStyle.ToolButtonIconOnly ) button.setAutoRaise(True) return button def update_all(self): self._update_widget_size(self.orientation()) self.update() class Launcher(QApplication): def __init__(self): super().__init__() # Window config self.setApplicationName("TestLauncher") self.main_window = QMainWindow() self.main_window.setGeometry(0, 0, 800, 600) move_window_to_center(self.main_window) # logger self.logger = logging.getLogger("Launcher.MainWindow") # dirs self.program_dir = Path(__file__).parent self.work_dir = Path.cwd() # Set icon if self.icon_path.exists(): self.setWindowIcon(QIcon(self.icon_path.as_posix())) else: self.logger.error("No icon found.") # Main layout self.central = QStackedWidget() self.toolbar = ToolBar(self, self.main_window) # center message (will be deleted if launcher finished development) content = """Still digging!""" message = "If you found that something might be a issue. You can report it to the main repo! Any suggestion are welcome." self.icon_label = QLabel() self.icon_label.setObjectName("icon_label") self.dev_info_box = QLabel(content) self.dev_info_box.setObjectName("dev_info_box") self.dev_info_2 = QLabel(message) self.dev_info_2.setObjectName("dev_info_message") self.version_label = QLabel("Current version: {}".format(self.launcher_version)) self.version_label.setObjectName("version_label") self.repo_url_label = QLabel(f"Main repo") self.repo_url_label.setObjectName("repo_url_label") self.repo_url_label.setOpenExternalLinks(True) if Path(self.resources_dir, "pictures", "in_progress.png").exists(): image = QPixmap(Path(self.resources_dir, "pictures", "in_progress.png")) self.icon_label.setPixmap(image.scaled(300, 300)) else: self.icon_label.setText("Ouch. The icon is missing.") # Homepage self.home_page = QWidget() self.home_layout = QVBoxLayout(self.home_page) self.home_layout.addStretch() self.home_layout.addWidget(self.icon_label, alignment=Qt.AlignmentFlag.AlignCenter) self.home_layout.addWidget(self.dev_info_box, alignment=Qt.AlignmentFlag.AlignHCenter) self.home_layout.addWidget(self.dev_info_2, alignment=Qt.AlignmentFlag.AlignHCenter) self.home_layout.addWidget(self.version_label, alignment=Qt.AlignmentFlag.AlignHCenter) self.home_layout.addWidget(self.repo_url_label, alignment=Qt.AlignmentFlag.AlignHCenter) self.home_layout.addStretch() # test page self.test_page = TestPage(self, self.main_window) # Account page self.account_page = AccountPage(self, self.main_window) # Add page self.central.addWidget(self.home_page) self.central.addWidget(self.test_page) self.central.addWidget(self.account_page) # Bind toolbar and center widget self.main_window.addToolBar(Qt.ToolBarArea.LeftToolBarArea, self.toolbar) self.main_window.setCentralWidget(self.central) # Apply qss self.apply_qss(Path("main.qss"), self.setStyleSheet) self.apply_qss(Path("toolbar.qss"), self.toolbar.setStyleSheet) # Toolbar # Test Window btn self.open_test_page = self.toolbar.add_button(" Tests", icon=self.get_icon(Path(self.icon_dir, "debug.png"))) self.open_test_page.setObjectName("open_test_page") # Home btn self.home_button = self.toolbar.add_button(icon=self.get_icon(Path(self.icon_dir, "home.png"))) # Account btn (will be moved to the bottom (or last one item) of the toolbar self.open_account_page = self.toolbar.add_button(icon=self.get_icon(Path(self.icon_dir, "head.png"))) # Profile page # Bind tool buttons self.toolbar.addWidget(self.home_button) self.toolbar.addWidget(self.open_test_page) self.toolbar.setMovable(True) self.toolbar.setFloatable(False) self.toolbar.setAllowedAreas(Qt.ToolBarArea.AllToolBarAreas) self.toolbar.addWidget(self.toolbar.spacer) self.toolbar.addSeparator() self.toolbar.addWidget(self.open_account_page) # Bind page-related button click event self.home_button.clicked.connect( lambda: self.set_current_page(self.home_page, self.home_button) ) self.open_test_page.clicked.connect( lambda: self.set_current_page(self.test_page, self.open_test_page) ) self.open_account_page.clicked.connect( lambda: self.set_current_page(self.account_page, self.open_account_page) ) self.set_current_page(self.home_page, self.home_button) def exec(self): self.main_window.show() self.toolbar.update_all() self.exec_() def set_current_page(self, page: QWidget, related_button: QToolButton): self.central.setCurrentWidget(page) related_button.setFocus() def get_icon(self, path: Path): icon = self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning) if not path.exists() or not path.is_file(): messagebox.error( self.main_window, "Resource Error", "Missing icon file: {}".format(path) ) return icon try: if is_light_theme() and path.name.endswith(".png"): icon = recolor_icon( path ) else: icon = QIcon(path.as_posix()) except Exception as e: messagebox.error( self.main_window, "Resource Error", "Failed to load icon {}: {}".format(path, e) ) return icon def get_picture(self, path: Path, size: QSize, custom_error: str=None) -> QPixmap: picture = self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning).pixmap(size) if not path.exists() or not path.is_file(): messagebox.error( self.main_window, "Resource Error", "Missing icon file: {}".format(path) if not custom_error else custom_error ) return picture try: picture = QPixmap(path.as_posix()) except Exception as e: messagebox.error( self.main_window, "Resource Error", "Failed to load icon {}: {}".format(path, e) ) return picture @property def temp_dir(self): return self.work_dir / "temp" @property def config_dir(self): return self.work_dir / "config" @property def data_dir(self): return self.work_dir / "data" @property def log_dir(self): return self.work_dir / "log" @property def resources_dir(self): return self.program_dir / "resources" @property def icon_path(self): return self.resources_dir / "icons" / "icon.png" @property def icon_dir(self): return self.resources_dir / "icons" @property def launcher_version(self): return LAUNCHER_VERSION @property def styles_path(self): return self.resources_dir / "styles" def apply_qss(self, qss_relative_path: Path, apply_qss_callback: Callable[[str], None]) -> str: full_path = self.styles_path / qss_relative_path if not full_path.exists(): messagebox.error( self.main_window, "Resource Error", "Missing stylesheet file: {}".format(full_path), ) return "" try: data = full_path.read_text() apply_qss_callback(data) except Exception as e: messagebox.error( self.main_window, "Resource Error", "Unable to apply QSS stylesheet {}: {}".format(full_path, e), ) return ""