feat(detail_page): added add game and edit shortcut
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
4 files changed
+118
-101
No files matched your search
@@ -2,7 +2,7 @@ from PySide6.QtWidgets import QLabel, QPushButton, QWidget, QLayout, QLayoutItem
|
||||
from PySide6.QtCore import Qt, Signal, QRect, QSize, Property, QPropertyAnimation, QEasingCurve
|
||||
from PySide6.QtGui import QFont, QFontMetrics, QPainter
|
||||
|
||||
def compute_layout(nat_sizes, rect_width, spacing, max_scale):
|
||||
def compute_layout(nat_sizes, rect_width, spacing, max_scale, center_rows=True):
|
||||
"""
|
||||
Compute layout for flow arrangement.
|
||||
nat_sizes: list of tuples [(width, height), ...]
|
||||
@@ -46,7 +46,8 @@ def compute_layout(nat_sizes, rect_width, spacing, max_scale):
|
||||
desired_scale = available_width / (sum_width + spacing * (count - 1)) if sum_width > 0 else 1.0
|
||||
global_scale = min(desired_scale, max_scale)
|
||||
scaled_row_width = int(sum_width * global_scale) + spacing * (count - 1)
|
||||
max_row_x_start = max(min_margin, (rect_width - scaled_row_width) // 2)
|
||||
if center_rows:
|
||||
max_row_x_start = max(min_margin, (rect_width - scaled_row_width) // 2)
|
||||
|
||||
i += count
|
||||
|
||||
@@ -86,10 +87,13 @@ def compute_layout(nat_sizes, rect_width, spacing, max_scale):
|
||||
scaled_row_width = int(sum_width * global_scale) + spacing * (count - 1)
|
||||
|
||||
# Determine starting position
|
||||
if count == max_items_per_row:
|
||||
x = max(min_margin, (rect_width - scaled_row_width) // 2)
|
||||
if center_rows:
|
||||
if count == max_items_per_row:
|
||||
x = max(min_margin, (rect_width - scaled_row_width) // 2)
|
||||
else:
|
||||
x = max_row_x_start
|
||||
else:
|
||||
x = max_row_x_start
|
||||
x = min_margin
|
||||
|
||||
# Place elements in row
|
||||
for k in range(i, j):
|
||||
@@ -109,12 +113,13 @@ def compute_layout(nat_sizes, rect_width, spacing, max_scale):
|
||||
|
||||
|
||||
class FlowLayout(QLayout):
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent=None, center_rows=True):
|
||||
super().__init__(parent)
|
||||
self.itemList = []
|
||||
self.setContentsMargins(20, 20, 20, 20)
|
||||
self._spacing = 20
|
||||
self._max_scale = 1.0
|
||||
self._center_rows = center_rows
|
||||
|
||||
# Simple cache
|
||||
self._cache_width = None
|
||||
@@ -184,8 +189,13 @@ class FlowLayout(QLayout):
|
||||
return self._cache_result[1]
|
||||
|
||||
# Calculate
|
||||
geom_array, total_height = compute_layout(visible_sizes, width,
|
||||
self._spacing, self._max_scale)
|
||||
geom_array, total_height = compute_layout(
|
||||
visible_sizes,
|
||||
width,
|
||||
self._spacing,
|
||||
self._max_scale,
|
||||
self._center_rows,
|
||||
)
|
||||
|
||||
# Save to cache
|
||||
self._cache_width = width
|
||||
@@ -231,8 +241,13 @@ class FlowLayout(QLayout):
|
||||
geom_array, total_height = self._cache_result
|
||||
else:
|
||||
# Calculate layout
|
||||
geom_array, total_height = compute_layout(visible_sizes, rect.width(),
|
||||
self._spacing, self._max_scale)
|
||||
geom_array, total_height = compute_layout(
|
||||
visible_sizes,
|
||||
rect.width(),
|
||||
self._spacing,
|
||||
self._max_scale,
|
||||
self._center_rows,
|
||||
)
|
||||
|
||||
# Save to cache
|
||||
self._cache_width = rect.width()
|
||||
|
||||
@@ -41,7 +41,7 @@ from portprotonqt.config_utils import (
|
||||
read_favorites,
|
||||
get_portproton_start_command,
|
||||
)
|
||||
from portprotonqt.custom_widgets import AutoSizeButton, ClickableLabel
|
||||
from portprotonqt.custom_widgets import AutoSizeButton, ClickableLabel, FlowLayout
|
||||
from portprotonqt.localization import _
|
||||
from portprotonqt.logger import get_logger
|
||||
from portprotonqt.portproton_api import PortProtonAPI
|
||||
@@ -270,16 +270,42 @@ class DetailPageManager:
|
||||
layout.addWidget(value)
|
||||
layout.addSpacing(30)
|
||||
|
||||
def _create_game_buttons_layout(self, game_data: dict) -> QHBoxLayout:
|
||||
def _create_game_buttons_layout(self, game_data: dict) -> FlowLayout:
|
||||
"""Create buttons layout for game detail page."""
|
||||
buttons_layout = QHBoxLayout()
|
||||
buttons_layout = FlowLayout(center_rows=False)
|
||||
buttons_layout.setContentsMargins(0, 0, 0, 0)
|
||||
exec_line = game_data.get("exec_line", "")
|
||||
game_source = game_data.get("game_source", "")
|
||||
appid = game_data.get("appid", "")
|
||||
game_name = game_data.get("name", "")
|
||||
cover_path = game_data.get("cover_path", "")
|
||||
|
||||
current_exe = self._get_current_exe(exec_line)
|
||||
play_button = self._create_play_button(exec_line, current_exe)
|
||||
buttons_layout.addWidget(play_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
buttons_layout.addWidget(play_button)
|
||||
|
||||
if self._has_game_shortcut(game_name):
|
||||
edit_button = self._make_action_button(
|
||||
_("Edit Game"),
|
||||
self.main_window.theme_manager.get_icon("edit"),
|
||||
)
|
||||
edit_button.clicked.connect(
|
||||
lambda: self.main_window.context_menu_manager.edit_game_shortcut(
|
||||
game_name, exec_line, cover_path
|
||||
)
|
||||
)
|
||||
buttons_layout.addWidget(edit_button)
|
||||
else:
|
||||
add_button = self._make_action_button(
|
||||
_("Add Game"),
|
||||
self.main_window.theme_manager.get_icon("addgame"),
|
||||
)
|
||||
add_button.clicked.connect(
|
||||
lambda: self.main_window.openAddGameDialog(
|
||||
self._get_file_from_exec(exec_line) or exec_line
|
||||
)
|
||||
)
|
||||
buttons_layout.addWidget(add_button)
|
||||
|
||||
# Show settings button for PortProton games or Steam games using PortProtonQt
|
||||
if str(game_source).lower() == "portproton":
|
||||
@@ -292,7 +318,6 @@ class DetailPageManager:
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
buttons_layout.addStretch()
|
||||
return buttons_layout
|
||||
|
||||
def _get_current_exe(self, exec_line: str) -> str | None:
|
||||
@@ -323,22 +348,22 @@ class DetailPageManager:
|
||||
play_button.clicked.connect(lambda: self.main_window.toggleGame(exec_line, play_button))
|
||||
return play_button
|
||||
|
||||
def _add_portproton_buttons(self, buttons_layout: QHBoxLayout, exec_line: str) -> None:
|
||||
def _add_portproton_buttons(self, buttons_layout: FlowLayout, exec_line: str) -> None:
|
||||
"""Add settings and log buttons for PortProton games."""
|
||||
file_to_check = self._get_file_from_exec(exec_line)
|
||||
|
||||
settings_icon = self.main_window.theme_manager.get_icon("settings")
|
||||
settings_button = self._make_action_button(_("Settings"), settings_icon)
|
||||
settings_button.clicked.connect(lambda: self.main_window.open_exe_settings(file_to_check))
|
||||
buttons_layout.addWidget(settings_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
buttons_layout.addWidget(settings_button)
|
||||
|
||||
log_icon = self.main_window.theme_manager.get_icon("edit")
|
||||
log_button = self._make_action_button(_("Create Log"), log_icon)
|
||||
log_button.clicked.connect(lambda: self.toggleDebugLog(file_to_check, log_button))
|
||||
buttons_layout.addWidget(log_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
buttons_layout.addWidget(log_button)
|
||||
self._debug_log_button = log_button
|
||||
|
||||
def _add_steam_settings_button(self, buttons_layout: QHBoxLayout, exec_line: str, appid: str) -> None:
|
||||
def _add_steam_settings_button(self, buttons_layout: FlowLayout, exec_line: str, appid: str) -> None:
|
||||
"""Add only settings button for Steam games."""
|
||||
# Create fake exe path in steam_scripts folder
|
||||
portproton_location = self.main_window.portproton_location
|
||||
@@ -354,7 +379,7 @@ class DetailPageManager:
|
||||
settings_icon = self.main_window.theme_manager.get_icon("settings")
|
||||
settings_button = self._make_action_button(_("Settings"), settings_icon)
|
||||
settings_button.clicked.connect(lambda: self.main_window.open_exe_settings(fake_exe_path, appid))
|
||||
buttons_layout.addWidget(settings_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
buttons_layout.addWidget(settings_button)
|
||||
|
||||
def _get_file_from_exec(self, exec_line: str) -> str | None:
|
||||
"""Get file path from exec line."""
|
||||
@@ -368,6 +393,14 @@ class DetailPageManager:
|
||||
return entry_exec_split[3] if len(entry_exec_split) >= 4 else None
|
||||
return entry_exec_split[0]
|
||||
|
||||
def _has_game_shortcut(self, game_name: str) -> bool:
|
||||
"""Check whether game has a desktop shortcut in PortProton location."""
|
||||
context_menu_manager = getattr(self.main_window, "context_menu_manager", None)
|
||||
if context_menu_manager is None:
|
||||
return False
|
||||
desktop_path = context_menu_manager._get_desktop_path(game_name)
|
||||
return bool(desktop_path and os.path.exists(desktop_path))
|
||||
|
||||
def _make_action_button(self, text: str, icon) -> AutoSizeButton:
|
||||
"""Create styled action button."""
|
||||
button = AutoSizeButton(text, icon=icon)
|
||||
@@ -532,11 +565,11 @@ class DetailPageManager:
|
||||
image_label=image_label,
|
||||
)
|
||||
|
||||
buttons_layout = QHBoxLayout()
|
||||
buttons_layout = FlowLayout(center_rows=False)
|
||||
buttons_layout.setContentsMargins(0, 0, 0, 0)
|
||||
install_button = self._create_autoinstall_buttons_layout(
|
||||
script_name, game_data.get("name", ""), buttons_layout
|
||||
)
|
||||
buttons_layout.addStretch()
|
||||
|
||||
details_widget = create_details_widget(
|
||||
parent=detail_page,
|
||||
@@ -574,12 +607,12 @@ class DetailPageManager:
|
||||
return metadata_description if metadata_description else description
|
||||
|
||||
def _create_autoinstall_buttons_layout(
|
||||
self, script_name: str, name: str, buttons_layout: QHBoxLayout
|
||||
self, script_name: str, name: str, buttons_layout: FlowLayout
|
||||
) -> AutoSizeButton:
|
||||
"""Create install button for auto-install page."""
|
||||
install_button = self._make_action_button(_("Install"), self.main_window.theme_manager.get_icon("save"))
|
||||
install_button.clicked.connect(lambda: self.main_window.launch_autoinstall(script_name))
|
||||
buttons_layout.addWidget(install_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
buttons_layout.addWidget(install_button)
|
||||
|
||||
self._check_install_status(script_name, name, install_button)
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ from PySide6.QtWidgets import (
|
||||
QGraphicsDropShadowEffect,
|
||||
QVBoxLayout,
|
||||
QLabel,
|
||||
QHBoxLayout,
|
||||
QWidget,
|
||||
QScrollArea,
|
||||
QBoxLayout,
|
||||
QSizePolicy,
|
||||
QScroller,
|
||||
QLayout,
|
||||
)
|
||||
from PySide6.QtCore import Qt, QUrl
|
||||
from PySide6.QtGui import QColor, QDesktopServices
|
||||
@@ -313,7 +313,7 @@ def create_details_widget(
|
||||
description: str,
|
||||
game_info_layout: QVBoxLayout | None = None,
|
||||
controller_support: str | None = None,
|
||||
buttons_layout: QHBoxLayout | None = None,
|
||||
buttons_layout: QLayout | None = None,
|
||||
) -> QWidget:
|
||||
"""Create details widget with title, description, and optional content."""
|
||||
details_widget = QWidget()
|
||||
|
||||
+44
-75
@@ -42,7 +42,7 @@ from portprotonqt.tray_manager import TrayManager
|
||||
from portprotonqt.game_library_manager import GameLibraryManager
|
||||
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
||||
from portprotonqt.dialogs.proton_manager import show_proton_manager
|
||||
from portprotonqt.config_utils import find_game_by_exe, create_desktop_file
|
||||
from portprotonqt.config_utils import find_game_by_exe
|
||||
|
||||
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox,
|
||||
QDialog, QFormLayout, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout, QScrollArea, QScroller, QSlider, QFrame)
|
||||
@@ -3030,13 +3030,11 @@ class MainWindow(QMainWindow):
|
||||
"""Handle launching an exe file from CLI.
|
||||
|
||||
If the game exists in the library, open its detail page.
|
||||
If not, add it to the library and then open the detail page.
|
||||
If not, open detail page without creating a shortcut automatically.
|
||||
|
||||
Args:
|
||||
exe_path: Full path to the executable file
|
||||
"""
|
||||
import configparser
|
||||
|
||||
# Normalize the exe path
|
||||
exe_path = os.path.abspath(exe_path)
|
||||
|
||||
@@ -3069,79 +3067,50 @@ class MainWindow(QMainWindow):
|
||||
|
||||
get_steam_game_info_async(game_name, exec_line, on_steam_info)
|
||||
else:
|
||||
# Skip desktop file creation for games in steamapps/common
|
||||
if "steamapps/common" in exe_path:
|
||||
game_name_from_exe = os.path.splitext(os.path.basename(exe_path))[0]
|
||||
logger.info("Game in steamapps/common, skipping desktop file creation: %s", game_name_from_exe)
|
||||
# Game not found in library: open detail page without creating .desktop file
|
||||
game_name_from_exe = os.path.splitext(os.path.basename(exe_path))[0]
|
||||
direct_exec_line = shlex.quote(exe_path)
|
||||
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
builtin_game_folder = os.path.join(repo_root, "portprotonqt", "custom_data", game_name_from_exe)
|
||||
xdg_data_home = os.getenv(
|
||||
"XDG_DATA_HOME",
|
||||
os.path.join(os.path.expanduser("~"), ".local", "share")
|
||||
)
|
||||
user_game_folder = os.path.join(
|
||||
xdg_data_home,
|
||||
"PortProtonQt",
|
||||
"custom_data",
|
||||
game_name_from_exe
|
||||
)
|
||||
local_cover_path = ""
|
||||
for folder in (user_game_folder, builtin_game_folder):
|
||||
if not os.path.isdir(folder):
|
||||
continue
|
||||
for ext in (".png", ".jpg", ".jpeg", ".bmp"):
|
||||
candidate_cover = os.path.join(folder, f"cover{ext}")
|
||||
if os.path.exists(candidate_cover):
|
||||
local_cover_path = candidate_cover
|
||||
break
|
||||
if local_cover_path:
|
||||
break
|
||||
|
||||
def on_steam_info_skip(steam_info: dict):
|
||||
game_data = {
|
||||
"name": game_name_from_exe,
|
||||
"description": steam_info.get("description", ""),
|
||||
"cover_path": steam_info.get("cover", ""),
|
||||
"appid": steam_info.get("appid", ""),
|
||||
"controller_support": steam_info.get("controller_support", ""),
|
||||
"exec_line": exe_path,
|
||||
"last_launch": _("Never"),
|
||||
"formatted_playtime": "0:00",
|
||||
"protondb_tier": steam_info.get("protondb_tier", ""),
|
||||
"anticheat_status": steam_info.get("anticheat_status", ""),
|
||||
"game_source": "portproton",
|
||||
}
|
||||
self.openGameDetailPage(game_data)
|
||||
def on_steam_info_missing(steam_info: dict):
|
||||
game_data = {
|
||||
"name": game_name_from_exe,
|
||||
"description": steam_info.get("description", ""),
|
||||
"cover_path": local_cover_path or steam_info.get("cover", ""),
|
||||
"appid": steam_info.get("appid", ""),
|
||||
"controller_support": steam_info.get("controller_support", ""),
|
||||
"exec_line": direct_exec_line,
|
||||
"last_launch": _("Never"),
|
||||
"formatted_playtime": "0:00",
|
||||
"protondb_tier": steam_info.get("protondb_tier", ""),
|
||||
"anticheat_status": steam_info.get("anticheat_status", ""),
|
||||
"game_source": "portproton",
|
||||
}
|
||||
self.openGameDetailPage(game_data)
|
||||
|
||||
get_steam_game_info_async(game_name_from_exe, exe_path, on_steam_info_skip)
|
||||
return
|
||||
|
||||
# Game not found - create desktop entry and open detail page
|
||||
result = create_desktop_file(exe_path)
|
||||
|
||||
if not result:
|
||||
logger.error(f"Failed to create desktop file for {exe_path}")
|
||||
return
|
||||
|
||||
desktop_entry, desktop_path = result
|
||||
|
||||
# Write the desktop file
|
||||
try:
|
||||
self._write_desktop_file(desktop_entry, desktop_path)
|
||||
|
||||
# Parse the entry to get game data
|
||||
entry = configparser.ConfigParser(interpolation=None)
|
||||
entry.read(desktop_path, encoding="utf-8")
|
||||
|
||||
if "Desktop Entry" not in entry:
|
||||
logger.error(f"Invalid desktop file: {desktop_path}")
|
||||
return
|
||||
|
||||
desktop_section = entry["Desktop Entry"]
|
||||
|
||||
game_name = desktop_section.get("Name", os.path.splitext(os.path.basename(exe_path))[0])
|
||||
exec_line = desktop_section.get("Exec", "")
|
||||
icon_path = desktop_section.get("Icon", "")
|
||||
|
||||
# Get Steam game info asynchronously to fetch appid, cover, etc.
|
||||
def on_steam_info(steam_info: dict):
|
||||
game_data = {
|
||||
"name": game_name,
|
||||
"description": steam_info.get("description", ""),
|
||||
"cover_path": steam_info.get("cover", icon_path),
|
||||
"appid": steam_info.get("appid", ""),
|
||||
"controller_support": steam_info.get("controller_support", ""),
|
||||
"exec_line": exec_line,
|
||||
"last_launch": _("Never"),
|
||||
"formatted_playtime": "0:00",
|
||||
"protondb_tier": steam_info.get("protondb_tier", ""),
|
||||
"anticheat_status": steam_info.get("anticheat_status", ""),
|
||||
"game_source": "portproton",
|
||||
}
|
||||
# Open detail page for the newly added game
|
||||
self.openGameDetailPage(game_data)
|
||||
|
||||
get_steam_game_info_async(game_name, exec_line, on_steam_info)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating desktop file: {e}")
|
||||
get_steam_game_info_async(game_name_from_exe, direct_exec_line, on_steam_info_missing)
|
||||
|
||||
|
||||
def activateFocusedWidget(self):
|
||||
|
||||
Reference in new issue
Block a user