from pathlib import Path from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QIcon, QMouseEvent, QHideEvent, QPixmap, QPainter from PySide6.QtWidgets import (QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QScrollArea, QFrame, QSizePolicy, QApplication) from manager.game import LaunchManager from core_lib.game.version_info import LATEST_VERSION_ALIASES from manager.profile import ProfileSummary from manager.widgets import AskForRequest, WidgetManager from pages.process_log import ProcessLogWindow class ProfileButton(QFrame): clicked = Signal() """ ProfileButton This object is coded by codex! """ def __init__(self, app, parent: QWidget | None = None) -> None: super().__init__(parent) self.app = app self.setObjectName("profileButton") self.setCursor(Qt.CursorShape.PointingHandCursor) self.setMinimumHeight(64) self.setMaximumHeight(64) # Layout self.layout = QHBoxLayout(self) self.layout.setContentsMargins(12, 8, 12, 8) self.layout.setSpacing(12) self.text_layout = QVBoxLayout() self.text_layout.setSpacing(1) # Widget self.icon_label = QLabel() self.icon_label.setFixedSize(40, 40) self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.name_label = QLabel() self.name_label.setObjectName("profileName") self.version_label = QLabel() self.version_label.setObjectName("profileVersion") self.arrow_label = QLabel("▾") self.arrow_label.setObjectName("profileArrow") self.arrow_label.setStyleSheet("border-style:solid; background-color: transparent; ") self.arrow_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.arrow_label.setFixedWidth(24) # Bind self.text_layout.addWidget(self.name_label) self.text_layout.addWidget(self.version_label) self.layout.addWidget(self.icon_label) self.layout.addLayout(self.text_layout, 1) self.layout.addWidget(self.arrow_label) def set_profile(self, profile: ProfileSummary) -> None: """ Set current profile id :param profile: :return: """ self.setProperty("profile_id", profile.id) self.name_label.setText(profile.display_name) self.version_label.setText(profile.version_id) icon_path = profile.icon if not isinstance(icon_path, Path): icon_path = Path(icon_path) if not icon_path.exists() or not icon_path.is_file(): # Use default icon_path = Path(self.app.icon_dir, "profile_default.png") self.icon_label.setPixmap(QIcon(icon_path.as_posix()).pixmap(36, 36)) def set_popup_open(self, opened: bool) -> None: self.arrow_label.setText("▴" if opened else "▾") def mousePressEvent(self, event: QMouseEvent) -> None: if event.button() == Qt.MouseButton.LeftButton: self.clicked.emit() super().mousePressEvent(event) class ProfileListItem(QPushButton): selected = Signal(object) def __init__(self, profile: ProfileSummary, parent: QWidget | None = None) -> None: super().__init__(parent) self.profile = profile self.setObjectName("profileItem") self.setCursor(Qt.CursorShape.PointingHandCursor) self.setMinimumHeight(62) layout = QHBoxLayout(self) layout.setContentsMargins(12, 7, 12, 7) layout.setSpacing(12) self.icon_label = QLabel() self.icon_label.setFixedSize(40, 40) self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) text_layout = QVBoxLayout() text_layout.setSpacing(1) name_label = QLabel(profile.display_name) name_label.setObjectName("itemName") version_label = QLabel(profile.version_id) version_label.setObjectName("itemVersion") text_layout.addWidget(name_label) text_layout.addWidget(version_label) layout.addWidget(self.icon_label) layout.addLayout(text_layout, 1) self.clicked.connect(lambda: self.selected.emit(self.profile)) def set_icon(self, icon_path: Path) -> None: self.icon_label.setPixmap(QIcon(icon_path.as_posix()).pixmap(36, 36)) class ProfilePopup(QFrame): profile_selected = Signal(object) closed = Signal() def __init__(self, app, parent: QWidget | None = None) -> None: super().__init__(parent, Qt.WindowType.Popup) self.app = app self.setFixedHeight(210) self.setObjectName("profilePopup") self.layout = QVBoxLayout(self) self.layout.setContentsMargins(4, 4, 4, 4) self.layout.setSpacing(0) self.scroll_area = QScrollArea(self) self.scroll_area.setObjectName("profileScrollArea") self.scroll_area.setWidgetResizable(True) self.scroll_area.setFrameShape(QFrame.Shape.NoFrame) self.scroll_area.setHorizontalScrollBarPolicy( Qt.ScrollBarPolicy.ScrollBarAlwaysOff ) self.scroll_area.setVerticalScrollBarPolicy( Qt.ScrollBarPolicy.ScrollBarAsNeeded ) self.content = QWidget() self.content.setObjectName("profileListContent") self.content_layout = QVBoxLayout(self.content) self.content_layout.setContentsMargins(0, 0, 0, 0) self.content_layout.setSpacing(2) self.scroll_area.setWidget(self.content) self.layout.addWidget(self.scroll_area) self.content_layout.setAlignment( Qt.AlignmentFlag.AlignTop ) def load_profiles(self, profiles: list[ProfileSummary]) -> None: for profile in profiles: icon_path = profile.icon if not isinstance(icon_path, Path): icon_path = Path(icon_path) if not icon_path.exists() or not icon_path.is_file(): # Use default icon_path = Path(self.app.icon_dir, "profile_default.png") item = ProfileListItem(profile, self) item.selected.connect(self.profile_selected) item.set_icon(icon_path) self.content_layout.addWidget(item) def hideEvent(self, event: QHideEvent) -> None: self.closed.emit() super().hideEvent(event) class LaunchProfilePage(QWidget): def __init__(self, app, parent=None): super(LaunchProfilePage, self).__init__(parent) self.app = app self.widget_mgr: WidgetManager = app.widget_manager self.profile_manager = app.profile_manager self.game_manager = app.game_manager self.launch_manager: LaunchManager = self.game_manager.launch self.process_log_windows: list[ProcessLogWindow] = [] # Layout self.layout = QVBoxLayout(self) self.bottom_layout = QHBoxLayout() # Widgets self.status_label = QLabel("Select a version to launch") self.status_label.setStyleSheet("font-size: 12px; font-weight: 600;") self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) # Profile button (just like the official launcher look like) self.profile_button = ProfileButton(self.app) self.profile_button.setMinimumWidth(260) self.profile_button.setMaximumWidth(360) self.profile_popup = ProfilePopup(parent=self, app=self.app) self.profile_button.clicked.connect(self.toggle_profile_popup) self.profile_popup.profile_selected.connect(self.select_profile) self.profile_popup.closed.connect(self.refresh_profile_button) launch_button = QPushButton("Launch Game") launch_button.setObjectName("launchButton") launch_button.setMinimumWidth(120) launch_button.setMinimumHeight(42) launch_button.setMaximumHeight(64) launch_button.setSizePolicy( QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding, ) launch_button.clicked.connect(self.launch_profile) # Profile overlay self.profile_frame = self.ProfileBackgroundFrame( parent=self, image_path=Path( self.app.icon_dir, "profile_default.png", ), ) self.profile_frame.setObjectName("profileFrame") # Frame layout and overlay frame_layout = QVBoxLayout(self.profile_frame) frame_layout.setContentsMargins(20, 20, 20, 20) self.profile_overlay = QFrame(self.profile_frame) self.profile_overlay.setObjectName("header") # Add a nice shadow # shadow = QGraphicsDropShadowEffect(self.profile_frame) # shadow.setBlurRadius(24) # shadow.setOffset(QPointF(0, 6)) # shadow.setColor(QColor(0, 0, 0, 150)) # self.profile_frame.setGraphicsEffect(shadow) # Header self.header_label = QLabel("Launch Profile") self.header_label.setObjectName("headerLabel") self.header_layout = QHBoxLayout(self.profile_overlay) self.header_layout.addWidget(self.header_label) self.header_layout.addStretch() frame_layout.addWidget(self.profile_overlay) frame_layout.addStretch() # Bind button widget self.bottom_layout.setContentsMargins(0, 0, 0, 0) self.bottom_layout.setSpacing(10) self.bottom_layout.addWidget(self.profile_button) self.bottom_layout.addStretch() self.bottom_layout.addWidget(self.status_label) self.bottom_layout.addStretch() self.bottom_layout.addWidget(launch_button) self.layout.addWidget(self.profile_frame, 1) self.layout.addLayout(self.bottom_layout) self.app.apply_qss(Path("profile.qss"), self.setStyleSheet) self.load_profiles() class ProfileBackgroundFrame(QFrame): def __init__(self, image_path, parent=None): super().__init__(parent) self.background = QPixmap(str(image_path)) def paintEvent(self, event): super().paintEvent(event) painter = QPainter(self) if not self.background.isNull(): scaled = self.background.scaled( self.size(), Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation, ) x = (self.width() - scaled.width()) // 2 y = (self.height() - scaled.height()) // 2 painter.drawPixmap(x, y, scaled) def load_profiles(self): profiles: list[ProfileSummary] = self.profile_manager.list_profiles() self.profile_popup.load_profiles(profiles) if len(profiles) > 0: self.select_profile(profiles[0]) def toggle_profile_popup(self) -> None: if self.profile_popup.isVisible(): self.profile_popup.hide() self.profile_button.set_popup_open(False) return self.profile_popup.setFixedWidth(self.profile_button.width()) button_top = self.profile_button.mapToGlobal( self.profile_button.rect().topLeft() ) button_bottom = self.profile_button.mapToGlobal( self.profile_button.rect().bottomLeft() ) screen = QApplication.screenAt( self.profile_button.mapToGlobal( self.profile_button.rect().center() ) ) if screen is None: screen = QApplication.primaryScreen() # Show profile popup at bottom if screen space is enough to render (if not, render at top of the button) available = screen.availableGeometry() popup_width = self.profile_popup.width() popup_height = self.profile_popup.height() space_below = available.bottom() - button_bottom.y() space_above = button_top.y() - available.top() if space_below < popup_height and space_above > space_below: popup_y = button_top.y() - popup_height else: popup_y = button_bottom.y() + 1 popup_x = max( available.left(), min(button_bottom.x(), available.right() - popup_width + 1), ) popup_y = max( available.top(), min(popup_y, available.bottom() - popup_height + 1), ) self.profile_popup.move(popup_x, popup_y) self.profile_popup.show() self.profile_button.set_popup_open(True) def refresh_profile_button(self) -> None: # Refresh profile button (Due to some unknown issue, the button arrow have a black # background after profile menu profile_popup. Rerender the button can fix this issue) self.profile_button.set_popup_open(False) self.profile_button.updateGeometry() self.profile_button.repaint() def select_profile(self, profile: ProfileSummary) -> None: # Update select profile id self.profile_button.set_profile(profile) self.profile_popup.hide() self.profile_button.set_popup_open(False) def launch_profile(self) -> None: profile_id = self.profile_button.property("profile_id") self.status_label.setText("Preparing to launch...") launch_profile = self.app.profile_manager.get_launch_profile(profile_id) if launch_profile is None: self.status_label.setText("Unable to load the selected profile.") return if self.launch_manager.is_profile_running(launch_profile.profile_id): request = AskForRequest( "Launch Request", "Target profile {} is running. Are you sure you want to launch it again?".format( launch_profile.display_name ), ) self.widget_mgr.signal.askyesno.emit(request) if not request.wait(60): self.widget_mgr.signal.show_error_message.emit( "Time out waiting for request for launch." ) return if not request.result() is True: return result = self.launch_manager.launch(launch_profile) if result.success: self.status_label.setText("Launched!") log_window = ProcessLogWindow( result.process, launch_profile.profile_id, title=f"Minecraft Log - {launch_profile.display_name} ({launch_profile.version_id})", parent=None, ) for warning in result.warnings: log_window.append_message(f"[launcher warning] {warning}") self.process_log_windows.append(log_window) log_window.show() else: self.widget_mgr.signal.show_error_message.emit( "Unable to launch profile {}: {}".format( launch_profile.display_name, result.error_message, ) )