357 lines
13 KiB
Python
357 lines
13 KiB
Python
from pathlib import Path
|
|
|
|
from PySide6.QtCore import Qt, Signal, QPointF
|
|
from PySide6.QtGui import QIcon, QMouseEvent, QHideEvent, QPixmap, QPainter, QColor
|
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QScrollArea, QGridLayout, \
|
|
QComboBox, QFrame, QSizePolicy, QApplication, QGraphicsDropShadowEffect
|
|
|
|
from manager.profile import ProfileSummary
|
|
|
|
|
|
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.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.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.profile_manager = app.profile_manager
|
|
|
|
# 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(f"Preparing to launch...") |