feat(detail_page): added add game and edit shortcut

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
Boria138 committed 2026-03-19 19:53:42 +05:00
1 parent 6792abdac6
commit 169ff005ce
4 files changed
+118 -101

No files matched your search

+25 -10
View File
@@ -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()
+47 -14
View File
@@ -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)
+2 -2
View File
@@ -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
View File
@@ -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):