diff --git a/app.py b/app.py index d67cd0c..6f90c6c 100644 --- a/app.py +++ b/app.py @@ -1,15 +1,84 @@ import logging from pathlib import Path +from typing import Callable -from PySide6.QtGui import QIcon, Qt, QImage, QPixmap -from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget, QPushButton +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, QPushButton, QStyle, QToolButton, \ + QStackedWidget -from core_lib.qt.hack import move_window_to_center -from dialog.test import TestDialog +from core_lib.qt.hack import move_window_to_center, recolor_standard_icon +from core_lib.qt import messagebox +from pages.test import TestPage LAUNCHER_VERSION = "alpha_0.0.1" 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.orientationChanged.connect(self._update_widget_size) + + def _update_widget_size(self, orientation): + vertical = orientation == Qt.Orientation.Vertical + + 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__() @@ -26,16 +95,15 @@ class Launcher(QApplication): self.program_dir = Path(__file__).parent self.work_dir = Path.cwd() - # test page - self.test_dialog = TestDialog(self, self.main_window) - # Set icon if self.icon_path.exists(): self.setWindowIcon(QIcon(self.icon_path.as_posix())) else: self.logger.error("No icon found.") - self.central = QWidget() + # Main layout + self.central = QStackedWidget() + self.toolbar = ToolBar(self, self.main_window) # center message (will be deleted if launcher finished development) content = """Still digging!""" @@ -51,7 +119,6 @@ class Launcher(QApplication): self.repo_url_label = QLabel(f"Main repo") self.repo_url_label.setObjectName("repo_url_label") self.repo_url_label.setOpenExternalLinks(True) - self.main_layout = QVBoxLayout(self.central) if Path(self.resources_dir, "pictures", "in_progress.png").exists(): image = QPixmap(Path(self.resources_dir, "pictures", "in_progress.png")) @@ -59,49 +126,71 @@ class Launcher(QApplication): else: self.icon_label.setText("Ouch. The icon is missing.") - self.open_test_window = QPushButton("Open test") - self.open_test_window.clicked.connect(lambda : self.test_dialog.show()) - self.open_test_window.setObjectName("open_test_window") + # 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() - self.main_layout.addStretch() - self.main_layout.addWidget(self.icon_label, alignment=Qt.AlignmentFlag.AlignCenter) - self.main_layout.addWidget(self.dev_info_box, alignment=Qt.AlignmentFlag.AlignHCenter) - self.main_layout.addWidget(self.dev_info_2, alignment=Qt.AlignmentFlag.AlignHCenter) - self.main_layout.addWidget(self.version_label, alignment=Qt.AlignmentFlag.AlignHCenter) - self.main_layout.addWidget(self.repo_url_label, alignment=Qt.AlignmentFlag.AlignHCenter) - self.main_layout.addStretch() - self.main_layout.addWidget(self.open_test_window, alignment=Qt.AlignmentFlag.AlignRight) + # test page + self.test_page = TestPage(self, self.main_window) + # Add page + self.central.addWidget(self.home_page) + self.central.addWidget(self.test_page) + + # Bind toolbar and center widget + self.main_window.addToolBar(Qt.ToolBarArea.LeftToolBarArea, self.toolbar) self.main_window.setCentralWidget(self.central) - self.setStyleSheet(""" - QLabel#dev_info_box { - font-size: 20pt; - font-weight: bold; - } - - QLabel#dev_info_message { - font-size: 10pt; - } - - QLabel#version_label, QLabel#version_label { - font-weight: bold; - } - - QLabel#icon_label { - border-radius: 20px; - } - - QPushButton#open_test_window { - padding: 10px; - } - """) + # 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=QIcon(Path(self.icon_dir, "debug.png").as_posix())) + self.open_test_page.setObjectName("open_test_page") + + # Home btn + self.home_button = self.toolbar.add_button(icon=QIcon(Path(self.icon_dir, "home.png").as_posix())) + self.home_button.clicked.connect( + lambda: self.central.setCurrentWidget(self.home_page) + ) + + # Bind tool buttons + self.toolbar.addWidget(self.home_button) + self.toolbar.addSeparator() + self.toolbar.addWidget(self.open_test_page) + self.toolbar.setMovable(True) + self.toolbar.setFloatable(False) + self.toolbar.setAllowedAreas(Qt.ToolBarArea.AllToolBarAreas) + + 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.set_current_page(self.home_page, self.home_button) def exec(self): self.main_window.show() - self.test_dialog.show() + self.toolbar.update_all() self.exec_() + def set_current_page(self, page: QWidget, related_button: QToolButton): + self.central.setCurrentWidget(page) + related_button.setFocus() + @property def temp_dir(self): return self.work_dir / "temp" @@ -126,6 +215,40 @@ class Launcher(QApplication): 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 "" + + + + diff --git a/core_lib/game/version_info.py b/core_lib/game/version_info.py index 62c50b3..3ec54d9 100644 --- a/core_lib/game/version_info.py +++ b/core_lib/game/version_info.py @@ -459,7 +459,7 @@ def fetch_and_save_version_manifest(dest: Path, target_version: str=None, root_m ) def download_core_file(manifest: dict, destination: Path, is_server=False, raise_for_null_hash=False, - progress_callback: Callable[str]=None) -> FileObject: + progress_callback: Callable[[str, float], None]=None) -> FileObject: """ Download core file from version manifest. :param manifest: diff --git a/core_lib/qt/hack.py b/core_lib/qt/hack.py index 1e2fb90..a9fb3fc 100644 --- a/core_lib/qt/hack.py +++ b/core_lib/qt/hack.py @@ -1,5 +1,5 @@ -from PySide6.QtCore import QCoreApplication, QThread -from PySide6.QtGui import QScreen +from PySide6.QtCore import QCoreApplication, QThread, QSize, Qt +from PySide6.QtGui import QScreen, QPixmap, QPainter, QPalette, QIcon from PySide6.QtWidgets import QMainWindow, QApplication @@ -12,4 +12,24 @@ def move_window_to_center(window: QMainWindow): def is_main_thread() -> bool: app = QCoreApplication.instance() - return app is not None and QThread.currentThread() == app.thread() \ No newline at end of file + return app is not None and QThread.currentThread() == app.thread() + +def recolor_standard_icon(widget, standard_pixmap, size=24): + source_icon = widget.style().standardIcon(standard_pixmap) + source = source_icon.pixmap(QSize(size, size)) + + result = QPixmap(source.size()) + result.fill(Qt.GlobalColor.transparent) + + painter = QPainter(result) + painter.drawPixmap(0, 0, source) + painter.setCompositionMode( + QPainter.CompositionMode.CompositionMode_SourceIn + ) + painter.fillRect( + result.rect(), + widget.palette().color(QPalette.ColorRole.ButtonText), + ) + painter.end() + + return QIcon(result) \ No newline at end of file diff --git a/main.py b/main.py index bb0a5af..6b684db 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,6 @@ import os import sys import argparse import logging - from app import Launcher from tkinter import messagebox diff --git a/dialog/test.py b/pages/test.py similarity index 99% rename from dialog/test.py rename to pages/test.py index e6f1b0c..d878dc7 100644 --- a/dialog/test.py +++ b/pages/test.py @@ -8,8 +8,8 @@ import traceback from pathlib import Path from PySide6.QtCore import Qt, QObject, Signal, Slot -from PySide6.QtWidgets import QPushButton, QVBoxLayout, QDialog, QPlainTextEdit, \ - QHBoxLayout, QComboBox, QMessageBox, QFileDialog +from PySide6.QtWidgets import QPushButton, QVBoxLayout, QPlainTextEdit, \ + QHBoxLayout, QComboBox, QMessageBox, QFileDialog, QWidget from core_lib.common.common import FileObject, download_multiple_files from core_lib.common.exception import VersionNotSelectedException, UnsupportedPlatformException, \ @@ -19,11 +19,9 @@ from core_lib.common.exception import VersionNotSelectedException, UnsupportedPl from core_lib.game.argument import ArgumentMappings, parse_and_generate_arguments, find_game_and_jvm_args, \ generate_launch_command from core_lib.game.asset import download_assets, check_assets_exist, find_correct_assets_dir -from core_lib.game.library import generate_classpath, download_full_libraries, check_libraries_exists, \ - check_full_libraries_exists +from core_lib.game.library import generate_classpath, download_full_libraries, check_full_libraries_exists -from core_lib.game.version_info import (find_useful_part_in_specific_version_manifest, - get_core_file_download_url_and_hash, \ +from core_lib.game.version_info import (find_useful_part_in_specific_version_manifest, \ get_all_versions, get_available_version_types, \ fetch_and_save_version_manifest, get_specific_version_manifest_url_and_hash, download_core_file) @@ -62,7 +60,7 @@ class SelectPathRequest(AskForRequest): self.filter = filter_raw -class TestDialog(QDialog): +class TestPage(QWidget): def __init__(self, app, parent): super().__init__(parent) self.app = app diff --git a/resources/icons/debug.png b/resources/icons/debug.png new file mode 100644 index 0000000..c2c8eba Binary files /dev/null and b/resources/icons/debug.png differ diff --git a/resources/icons/home.png b/resources/icons/home.png new file mode 100644 index 0000000..6516149 Binary files /dev/null and b/resources/icons/home.png differ diff --git a/resources/pictures/in_progress.png b/resources/pictures/in_progress.png new file mode 100644 index 0000000..7fd42e9 Binary files /dev/null and b/resources/pictures/in_progress.png differ diff --git a/resources/styles/main.qss b/resources/styles/main.qss new file mode 100644 index 0000000..696300d --- /dev/null +++ b/resources/styles/main.qss @@ -0,0 +1,28 @@ +/* + * main - Qt Style Sheet + * Created: 2026/7/14 + * + * QSS Toolkit Quick Tips: + * - Press Ctrl+Space (Cmd+Space) for intelligent auto-completion + * - Use Ctrl+/ (Cmd+/) to comment/uncomment code blocks + * - Use the QSS Colors panel and editor context menu to save or insert colors + */ + +/* Add your QSS styles here */ + +QLabel#dev_info_box { + font-size: 20pt; + font-weight: bold; +} + +QLabel#dev_info_message { + font-size: 10pt; +} + +QLabel#version_label, QLabel#version_label { + font-weight: bold; +} + +QLabel#icon_label { + border-radius: 20px; +} \ No newline at end of file diff --git a/resources/styles/toolbar.qss b/resources/styles/toolbar.qss new file mode 100644 index 0000000..e70cb5a --- /dev/null +++ b/resources/styles/toolbar.qss @@ -0,0 +1,99 @@ +/* Disable default style */ +QToolBar { + margin: 0; + spacing: 1px; + background-color: palette(window); + border: none; +} + +QToolBar:top { + border-bottom: 2px solid palette(mid); +} + +QToolBar:bottom { + border-top: 2px solid palette(mid); +} + +QToolBar:left { + border-right: 2px solid palette(mid); +} + +QToolBar:right { + border-left: 2px solid palette(mid); +} + +QToolBar::separator { + background-color: palette(midlight); + width: 1px; + height: 1px; +} + +/* Button */ +QToolBar QPushButton, +QToolBar QToolButton { + margin: 0; + padding: 14px 14px; + border: none; + border-radius: 0; + background-color: palette(button); + color: palette(button-text); + font-size: 13px; + min-height: 28px; +} + +QToolBar:left QPushButton, +QToolBar:right QPushButton, +QToolBar:left QToolButton, +QToolBar:right QToolButton { + padding: 14px 6px; + min-width: 0; +} + +QToolBar QPushButton:hover, +QToolBar QToolButton:hover { + background-color: palette(midlight); +} + +QToolBar QPushButton:focus, +QToolBar QToolButton:focus { + border-bottom: 2px solid #F52761; +} + +QToolBar QPushButton:pressed, +QToolBar QToolButton:pressed, +QToolBar QToolButton:checked { + background-color: palette(dark); +} + +QToolBar QPushButton:disabled, +QToolBar QToolButton:disabled { + color: palette(mid); +} + +/* Button margin */ + +/* Toolbar */ +QToolBar::handle:top, QToolBar::handle:bottom { + border-left: 5px solid palette(midlight); + width: 5px; +} + +/*QToolBar:top {*/ +/* border-bottom: 3px solid palette(midlight);*/ +/*}*/ + +/*QToolBar:bottom {*/ +/* border-top: 3px solid palette(midlight);*/ +/*}*/ + +/*QToolBar:left {*/ +/* border-right: 3px solid palette(midlight);*/ +/*}*/ + +/*QToolBar:right {*/ +/* border-left: 3px solid palette(midlight);*/ +/*}*/ + +QToolBar::handle:right, QToolBar::handle:left { + border-top: 3px solid palette(midlight); +} \ No newline at end of file