Fix "Missing javaVersion" error when launching legacy Minecraft. Add settings page. (But is not fully implemented yet)
417 lines
15 KiB
Python
417 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 profile 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:
|
|
try:
|
|
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,
|
|
finished_callback=self.launch_manager.profile_finished,
|
|
)
|
|
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,
|
|
)
|
|
)
|
|
finally:
|
|
self.status_label.setText("Select a profile to launch")
|