Files
Launcher/pages/launch_profile.py
T

413 lines
15 KiB
Python

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,
)
)