689 lines
32 KiB
Python
689 lines
32 KiB
Python
import os
|
||
import tempfile
|
||
from typing import cast, TYPE_CHECKING
|
||
from PySide6.QtGui import QPixmap, QIcon
|
||
from PySide6.QtWidgets import (
|
||
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication
|
||
)
|
||
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer
|
||
from icoextract import IconExtractor, IconExtractorError
|
||
from PIL import Image
|
||
from portprotonqt.config_utils import get_portproton_location
|
||
from portprotonqt.localization import _
|
||
from portprotonqt.logger import get_logger
|
||
import portprotonqt.themes.standart.styles as default_styles
|
||
from portprotonqt.theme_manager import ThemeManager
|
||
from portprotonqt.custom_widgets import AutoSizeButton
|
||
|
||
if TYPE_CHECKING:
|
||
from portprotonqt.main_window import MainWindow
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
def generate_thumbnail(inputfile, outfile, size=128, force_resize=True):
|
||
"""
|
||
Generates a thumbnail for an .exe file.
|
||
|
||
inputfile: the input file path (%i)
|
||
outfile: output filename (%o)
|
||
size: determines the thumbnail output size (%s)
|
||
"""
|
||
logger.debug(f"Начинаем генерацию миниатюры: {inputfile} → {outfile}, размер={size}, принудительно={force_resize}")
|
||
|
||
try:
|
||
extractor = IconExtractor(inputfile)
|
||
logger.debug("IconExtractor успешно создан.")
|
||
except (RuntimeError, IconExtractorError) as e:
|
||
logger.warning(f"Не удалось создать IconExtractor: {e}")
|
||
return False
|
||
|
||
try:
|
||
data = extractor.get_icon()
|
||
im = Image.open(data)
|
||
logger.debug(f"Извлечена иконка размером {im.size}, форматы: {im.format}, кадры: {getattr(im, 'n_frames', 1)}")
|
||
except Exception as e:
|
||
logger.warning(f"Ошибка при извлечении иконки: {e}")
|
||
return False
|
||
|
||
if force_resize:
|
||
logger.debug(f"Принудительное изменение размера иконки на {size}x{size}")
|
||
im = im.resize((size, size))
|
||
else:
|
||
if size > 256:
|
||
logger.warning('Запрошен размер больше 256, установлен 256')
|
||
size = 256
|
||
elif size not in (128, 256):
|
||
logger.warning(f'Неподдерживаемый размер {size}, установлен 128')
|
||
size = 128
|
||
|
||
if size == 256:
|
||
logger.debug("Сохраняем иконку без изменения размера (256x256)")
|
||
im.save(outfile, "PNG")
|
||
logger.info(f"Иконка сохранена в {outfile}")
|
||
return True
|
||
|
||
frames = getattr(im, 'n_frames', 1)
|
||
try:
|
||
for frame in range(frames):
|
||
im.seek(frame)
|
||
if im.size == (size, size):
|
||
logger.debug(f"Найден кадр с размером {size}x{size}")
|
||
break
|
||
except EOFError:
|
||
logger.debug("Кадры закончились до нахождения нужного размера.")
|
||
|
||
if im.size != (size, size):
|
||
logger.debug(f"Изменение размера с {im.size} на {size}x{size}")
|
||
im = im.resize((size, size))
|
||
|
||
try:
|
||
im.save(outfile, "PNG")
|
||
logger.info(f"Миниатюра успешно сохранена в {outfile}")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при сохранении миниатюры: {e}")
|
||
return False
|
||
|
||
class FileSelectedSignal(QObject):
|
||
file_selected = Signal(str) # Сигнал с путем к выбранному файлу
|
||
|
||
class FileExplorer(QDialog):
|
||
def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None, directory_only=False):
|
||
super().__init__(parent)
|
||
self.theme = theme if theme else default_styles
|
||
self.theme_manager = ThemeManager()
|
||
self.file_signal = FileSelectedSignal()
|
||
self.file_filter = file_filter # Store the file filter
|
||
self.directory_only = directory_only # Store the directory_only flag
|
||
self.mime_db = QMimeDatabase() # Initialize QMimeDatabase for mimetype detection
|
||
self.path_history = {} # Dictionary to store last selected item per directory
|
||
self.initial_path = initial_path # Store initial path if provided
|
||
self.setup_ui()
|
||
|
||
# Настройки окна
|
||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
|
||
|
||
# Find InputManager from parent
|
||
self.input_manager = None
|
||
parent = self.parent()
|
||
while parent:
|
||
if hasattr(parent, 'input_manager'):
|
||
self.input_manager = cast("MainWindow", parent).input_manager
|
||
break
|
||
parent = parent.parent()
|
||
|
||
if self.input_manager:
|
||
self.input_manager.enable_file_explorer_mode(self)
|
||
|
||
# Initialize drives list
|
||
self.update_drives_list()
|
||
|
||
# Set initial path if provided, else default to home
|
||
self.current_path = os.path.expanduser("~") if not initial_path else os.path.normpath(initial_path)
|
||
if initial_path and not os.path.isdir(self.current_path):
|
||
self.current_path = os.path.expanduser("~") # Fallback to home if initial path is invalid
|
||
self.update_file_list()
|
||
|
||
def get_mounted_drives(self):
|
||
"""Получение списка смонтированных дисков из /proc/mounts, исключая системные пути"""
|
||
mounted_drives = []
|
||
try:
|
||
with open('/proc/mounts') as f:
|
||
for line in f:
|
||
parts = line.strip().split()
|
||
if len(parts) < 2:
|
||
continue
|
||
mount_point = parts[1]
|
||
# Исключаем системные и временные пути
|
||
if mount_point.startswith(('/run', '/dev', '/sys', '/proc', '/tmp', '/snap', '/var/lib')):
|
||
continue
|
||
# Проверяем, является ли точка монтирования директорией и доступна ли она
|
||
if os.path.isdir(mount_point) and os.access(mount_point, os.R_OK):
|
||
mounted_drives.append(mount_point)
|
||
return sorted(mounted_drives)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при получении смонтированных дисков: {e}")
|
||
return []
|
||
|
||
def setup_ui(self):
|
||
"""Настройка интерфейса"""
|
||
self.setWindowTitle("File Explorer")
|
||
self.setGeometry(100, 100, 600, 600)
|
||
|
||
self.main_layout = QVBoxLayout()
|
||
self.main_layout.setContentsMargins(10, 10, 10, 10)
|
||
self.main_layout.setSpacing(10)
|
||
self.setLayout(self.main_layout)
|
||
|
||
# Панель для смонтированных дисков
|
||
self.drives_layout = QHBoxLayout()
|
||
self.drives_scroll = QScrollArea()
|
||
self.drives_scroll.setWidgetResizable(True)
|
||
self.drives_container = QWidget()
|
||
self.drives_container.setLayout(self.drives_layout)
|
||
self.drives_scroll.setWidget(self.drives_container)
|
||
self.drives_scroll.setStyleSheet(self.theme.SCROLL_AREA_STYLE)
|
||
self.drives_scroll.setFixedHeight(70)
|
||
self.main_layout.addWidget(self.drives_scroll)
|
||
self.drives_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||
self.drives_scroll.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # Allow focus on scroll area
|
||
|
||
# Путь
|
||
self.path_label = QLabel()
|
||
self.path_label.setStyleSheet(self.theme.FILE_EXPLORER_PATH_LABEL_STYLE)
|
||
self.main_layout.addWidget(self.path_label)
|
||
|
||
# Список файлов
|
||
self.file_list = QListWidget()
|
||
self.file_list.setStyleSheet(self.theme.FILE_EXPLORER_STYLE)
|
||
self.file_list.itemClicked.connect(self.handle_item_click)
|
||
self.file_list.itemDoubleClicked.connect(self.handle_item_double_click)
|
||
self.main_layout.addWidget(self.file_list)
|
||
|
||
# Кнопки
|
||
self.button_layout = QHBoxLayout()
|
||
self.button_layout.setSpacing(10)
|
||
self.select_button = AutoSizeButton(_("Select"), icon=self.theme_manager.get_icon("apply"))
|
||
self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel"))
|
||
self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||
self.button_layout.addWidget(self.select_button)
|
||
self.button_layout.addWidget(self.cancel_button)
|
||
self.main_layout.addLayout(self.button_layout)
|
||
|
||
self.select_button.clicked.connect(self.select_item)
|
||
self.cancel_button.clicked.connect(self.reject)
|
||
|
||
def move_selection(self, direction):
|
||
"""Перемещение выбора по списку"""
|
||
current_row = self.file_list.currentRow()
|
||
if direction < 0 and current_row > 0: # Вверх
|
||
self.file_list.setCurrentRow(current_row - 1)
|
||
elif direction > 0 and current_row < self.file_list.count() - 1: # Вниз
|
||
self.file_list.setCurrentRow(current_row + 1)
|
||
self.file_list.scrollToItem(self.file_list.currentItem())
|
||
|
||
def handle_item_click(self, item):
|
||
"""Обработка одинарного клика мышью"""
|
||
try:
|
||
self.file_list.setCurrentItem(item)
|
||
self.path_history[self.current_path] = item.text() # Сохраняем выбранный элемент
|
||
logger.debug("Selected item: %s", item.text())
|
||
except Exception as e:
|
||
logger.error("Error in handle_item_click: %s", e)
|
||
|
||
def handle_item_double_click(self, item):
|
||
"""Обработка двойного клика мышью по элементу списка"""
|
||
try:
|
||
self.file_list.setCurrentItem(item)
|
||
self.path_history[self.current_path] = item.text() # Сохраняем выбранный элемент
|
||
selected = item.text()
|
||
full_path = os.path.join(self.current_path, selected)
|
||
if os.path.isdir(full_path):
|
||
if selected == "../":
|
||
# Переходим в родительскую директорию
|
||
self.previous_dir()
|
||
else:
|
||
# Открываем директорию
|
||
self.current_path = os.path.normpath(full_path)
|
||
self.update_file_list()
|
||
elif not self.directory_only:
|
||
# Выбираем файл, если directory_only=False
|
||
self.file_signal.file_selected.emit(os.path.normpath(full_path))
|
||
self.accept()
|
||
else:
|
||
logger.debug("Double-clicked item is not a directory, ignoring: %s", full_path)
|
||
except Exception as e:
|
||
logger.error("Error in handle_item_double_click: %s", e)
|
||
|
||
def select_item(self):
|
||
"""Обработка выбора файла/папки"""
|
||
if self.file_list.count() == 0:
|
||
return
|
||
|
||
selected = self.file_list.currentItem().text()
|
||
full_path = os.path.join(self.current_path, selected)
|
||
|
||
if os.path.isdir(full_path):
|
||
if self.directory_only:
|
||
# Подтверждаем выбор директории
|
||
self.file_signal.file_selected.emit(os.path.normpath(full_path))
|
||
self.accept()
|
||
else:
|
||
# Открываем директорию
|
||
self.current_path = os.path.normpath(full_path)
|
||
self.update_file_list()
|
||
else:
|
||
if not self.directory_only:
|
||
# Для файла отправляем нормализованный путь
|
||
self.file_signal.file_selected.emit(os.path.normpath(full_path))
|
||
self.accept()
|
||
else:
|
||
logger.debug("Selected item is not a directory, ignoring: %s", full_path)
|
||
|
||
def previous_dir(self):
|
||
"""Возврат к родительской директории"""
|
||
try:
|
||
if self.current_path == "/":
|
||
return # Уже в корне
|
||
|
||
# Нормализуем путь (убираем конечный слеш, если есть)
|
||
normalized_path = os.path.normpath(self.current_path)
|
||
# Получаем родительскую директорию
|
||
parent_dir = os.path.dirname(normalized_path)
|
||
|
||
if not parent_dir:
|
||
parent_dir = "/"
|
||
|
||
# Save the current directory as the last selected item for the parent
|
||
current_dir_name = os.path.basename(normalized_path)
|
||
self.path_history[parent_dir] = current_dir_name + "/" if current_dir_name else "../"
|
||
self.current_path = parent_dir
|
||
self.update_file_list()
|
||
except Exception as e:
|
||
logger.error(f"Error navigating to parent directory: {e}")
|
||
|
||
def update_drives_list(self):
|
||
"""Обновление списка смонтированных дисков"""
|
||
for i in reversed(range(self.drives_layout.count())):
|
||
widget = self.drives_layout.itemAt(i).widget()
|
||
if widget:
|
||
widget.deleteLater()
|
||
|
||
drives = self.get_mounted_drives()
|
||
self.drive_buttons = [] # Store buttons for navigation
|
||
for drive in drives:
|
||
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
|
||
button = AutoSizeButton(drive_name, icon=self.theme_manager.get_icon("mount_point"))
|
||
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # Make button focusable
|
||
button.clicked.connect(lambda checked, path=drive: self.change_drive(path))
|
||
self.drives_layout.addWidget(button)
|
||
self.drive_buttons.append(button)
|
||
self.drives_layout.addStretch()
|
||
|
||
# Set focus to first drive button if available
|
||
if self.drive_buttons:
|
||
self.drive_buttons[0].setFocus()
|
||
|
||
def select_drive(self):
|
||
"""Handle drive selection via gamepad"""
|
||
focused_widget = QApplication.focusWidget()
|
||
if isinstance(focused_widget, AutoSizeButton) and focused_widget in self.drive_buttons:
|
||
drive_path = None
|
||
for drive in self.get_mounted_drives():
|
||
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
|
||
if drive_name == focused_widget.text():
|
||
drive_path = drive
|
||
break
|
||
if drive_path and os.path.isdir(drive_path) and os.access(drive_path, os.R_OK):
|
||
self.current_path = os.path.normpath(drive_path)
|
||
self.update_file_list()
|
||
else:
|
||
logger.warning(f"Путь диска недоступен: {drive_path}")
|
||
|
||
def change_drive(self, drive_path):
|
||
"""Переход к выбранному диску"""
|
||
if os.path.isdir(drive_path) and os.access(drive_path, os.R_OK):
|
||
self.current_path = os.path.normpath(drive_path)
|
||
self.update_file_list()
|
||
else:
|
||
logger.warning(f"Путь диска недоступен: {drive_path}")
|
||
|
||
def update_file_list(self):
|
||
"""Обновление списка файлов с превью в виде иконок"""
|
||
self.file_list.clear()
|
||
try:
|
||
if self.current_path != "/":
|
||
item = QListWidgetItem("../")
|
||
folder_icon = self.theme_manager.get_icon("folder")
|
||
# Ensure the icon is a QIcon
|
||
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
|
||
folder_icon = QIcon(folder_icon)
|
||
elif not isinstance(folder_icon, QIcon):
|
||
folder_icon = QIcon() # Fallback to empty icon
|
||
item.setIcon(folder_icon)
|
||
self.file_list.addItem(item)
|
||
|
||
items = os.listdir(self.current_path)
|
||
dirs = [d for d in items if os.path.isdir(os.path.join(self.current_path, d))]
|
||
|
||
# Добавляем директории
|
||
for d in sorted(dirs):
|
||
item = QListWidgetItem(f"{d}/")
|
||
folder_icon = self.theme_manager.get_icon("folder")
|
||
# Ensure the icon is a QIcon
|
||
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
|
||
folder_icon = QIcon(folder_icon)
|
||
elif not isinstance(folder_icon, QIcon):
|
||
folder_icon = QIcon() # Fallback to empty icon
|
||
item.setIcon(folder_icon)
|
||
self.file_list.addItem(item)
|
||
|
||
# Добавляем файлы только если directory_only=False
|
||
if not self.directory_only:
|
||
files = [f for f in items if os.path.isfile(os.path.join(self.current_path, f))]
|
||
if self.file_filter:
|
||
if isinstance(self.file_filter, str):
|
||
files = [f for f in files if f.lower().endswith(self.file_filter)]
|
||
elif isinstance(self.file_filter, tuple):
|
||
files = [f for f in files if any(f.lower().endswith(ext) for ext in self.file_filter)]
|
||
|
||
for f in sorted(files):
|
||
item = QListWidgetItem(f)
|
||
file_path = os.path.join(self.current_path, f)
|
||
mime_type = self.mime_db.mimeTypeForFile(file_path).name()
|
||
|
||
if mime_type.startswith("image/"):
|
||
pixmap = QPixmap(file_path)
|
||
if not pixmap.isNull():
|
||
item.setIcon(QIcon(pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio)))
|
||
elif file_path.lower().endswith(".exe"):
|
||
tmp = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
|
||
tmp.close()
|
||
if generate_thumbnail(file_path, tmp.name, size=64):
|
||
pixmap = QPixmap(tmp.name)
|
||
if not pixmap.isNull():
|
||
item.setIcon(QIcon(pixmap))
|
||
os.unlink(tmp.name)
|
||
|
||
self.file_list.addItem(item)
|
||
|
||
self.path_label.setText(_("Path: ") + self.current_path)
|
||
|
||
# Restore last selected item for this directory
|
||
last_item = self.path_history.get(self.current_path)
|
||
if last_item:
|
||
for i in range(self.file_list.count()):
|
||
if self.file_list.item(i).text() == last_item:
|
||
self.file_list.setCurrentRow(i)
|
||
self.file_list.scrollToItem(self.file_list.currentItem())
|
||
break
|
||
else:
|
||
self.file_list.setCurrentRow(0)
|
||
else:
|
||
self.file_list.setCurrentRow(0)
|
||
|
||
self.file_list.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||
self.file_list.setTextElideMode(Qt.TextElideMode.ElideRight)
|
||
self.file_list.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||
self.file_list.setAlternatingRowColors(True)
|
||
|
||
except PermissionError:
|
||
self.path_label.setText(f"Access denied: {self.current_path}")
|
||
|
||
def closeEvent(self, event):
|
||
"""Закрытие окна"""
|
||
try:
|
||
if self.input_manager:
|
||
self.input_manager.disable_file_explorer_mode()
|
||
if self.parent():
|
||
parent = cast("MainWindow", self.parent())
|
||
parent.activateWindow()
|
||
parent.setFocus()
|
||
except Exception as e:
|
||
logger.error(f"Error in closeEvent: {e}")
|
||
|
||
super().closeEvent(event)
|
||
|
||
def reject(self):
|
||
"""Закрытие диалога"""
|
||
if self.input_manager:
|
||
self.input_manager.disable_file_explorer_mode()
|
||
super().reject()
|
||
|
||
def accept(self):
|
||
"""Принятие диалога"""
|
||
if self.input_manager:
|
||
self.input_manager.disable_file_explorer_mode()
|
||
super().accept()
|
||
|
||
class AddGameDialog(QDialog):
|
||
def __init__(self, parent=None, theme=None, edit_mode=False, game_name=None, exe_path=None, cover_path=None):
|
||
super().__init__(parent)
|
||
from portprotonqt.context_menu_manager import CustomLineEdit # Локальный импорт
|
||
self.theme = theme if theme else default_styles
|
||
self.theme_manager = ThemeManager()
|
||
self.edit_mode = edit_mode
|
||
self.original_name = game_name
|
||
self.last_exe_path = exe_path # Store last selected exe path
|
||
self.last_cover_path = cover_path # Store last selected cover path
|
||
|
||
self.setWindowTitle(_("Edit Game") if edit_mode else _("Add Game"))
|
||
self.setModal(True)
|
||
self.setFixedWidth(600)
|
||
self.setFixedHeight(600)
|
||
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE)
|
||
|
||
layout = QFormLayout(self)
|
||
layout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft)
|
||
layout.setFormAlignment(Qt.AlignmentFlag.AlignLeft)
|
||
layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||
|
||
# Game name
|
||
self.nameEdit = CustomLineEdit(self, theme=self.theme)
|
||
self.nameEdit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE)
|
||
if game_name:
|
||
self.nameEdit.setText(game_name)
|
||
name_label = QLabel(_("Game Name:"))
|
||
name_label.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||
layout.addRow(name_label, self.nameEdit)
|
||
|
||
# Exe path
|
||
exe_label = QLabel(_("Path to Executable:"))
|
||
exe_label.setStyleSheet(
|
||
self.theme.PARAMS_TITLE_STYLE)
|
||
|
||
self.exeEdit = CustomLineEdit(self, theme=self.theme)
|
||
self.exeEdit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE)
|
||
if exe_path:
|
||
self.exeEdit.setText(exe_path)
|
||
|
||
exeBrowseButton = AutoSizeButton(_("Browse..."), icon=self.theme_manager.get_icon("search"))
|
||
exeBrowseButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||
exeBrowseButton.clicked.connect(self.browseExe)
|
||
exeBrowseButton.setObjectName("exeBrowseButton") # Для поиска кнопки
|
||
|
||
# Добавляем поле ввода для exe
|
||
layout.addRow(exe_label, self.exeEdit)
|
||
|
||
# Добавляем кнопку обзора под полем ввода с выравниванием
|
||
empty_label = QLabel("")
|
||
empty_label.setFixedWidth(exe_label.sizeHint().width())
|
||
layout.addRow(empty_label, exeBrowseButton)
|
||
|
||
# Cover path
|
||
cover_label = QLabel(_("Custom Cover:"))
|
||
cover_label.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||
|
||
self.coverEdit = CustomLineEdit(self, theme=self.theme)
|
||
self.coverEdit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE)
|
||
if cover_path:
|
||
self.coverEdit.setText(cover_path)
|
||
|
||
coverBrowseButton = AutoSizeButton(_("Browse..."), icon=self.theme_manager.get_icon("search"))
|
||
coverBrowseButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||
coverBrowseButton.clicked.connect(self.browseCover)
|
||
coverBrowseButton.setObjectName("coverBrowseButton") # Для поиска кнопки
|
||
|
||
# Добавляем поле ввода для обложки
|
||
layout.addRow(cover_label, self.coverEdit)
|
||
|
||
# Добавляем кнопку обзора под полем ввода с выравниванием
|
||
layout.addRow(empty_label, coverBrowseButton)
|
||
|
||
# Preview
|
||
self.coverPreview = QLabel(self)
|
||
self.coverPreview.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self.coverPreview.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||
preview_widget = QWidget(self)
|
||
preview_widget.setStyleSheet(self.theme.PREVIEW_WIDGET_STYLE)
|
||
preview_layout = QVBoxLayout(preview_widget)
|
||
preview_layout.setContentsMargins(0, 0, 0, 0)
|
||
preview_layout.setSpacing(0)
|
||
preview_label = QLabel(_("Cover Preview:"))
|
||
preview_label.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||
preview_layout.addWidget(preview_label, alignment=Qt.AlignmentFlag.AlignLeft)
|
||
preview_layout.addWidget(self.coverPreview, stretch=1)
|
||
layout.addRow(preview_widget)
|
||
|
||
# Dialog buttons
|
||
self.button_layout = QHBoxLayout()
|
||
self.button_layout.setSpacing(10)
|
||
self.select_button = AutoSizeButton(_("Apply"), icon=self.theme_manager.get_icon("apply"))
|
||
self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel"))
|
||
self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||
self.button_layout.addWidget(self.select_button)
|
||
self.button_layout.addWidget(self.cancel_button)
|
||
layout.addRow(self.button_layout)
|
||
|
||
# Подключение сигналов
|
||
self.select_button.clicked.connect(self.accept)
|
||
self.cancel_button.clicked.connect(self.reject)
|
||
self.coverEdit.textChanged.connect(self.updatePreview)
|
||
self.exeEdit.textChanged.connect(self.updatePreview)
|
||
|
||
# Установка одинаковой ширины для кнопок и полей ввода
|
||
def update_button_widths():
|
||
exeBrowseButton.setFixedWidth(self.exeEdit.width())
|
||
coverBrowseButton.setFixedWidth(self.coverEdit.width())
|
||
|
||
# Вызываем после отображения окна, когда размеры установлены, чтобы реально дождаться, когда всё сформируется
|
||
QTimer.singleShot(0, update_button_widths)
|
||
|
||
# Обновляем превью, если в режиме редактирования
|
||
if edit_mode:
|
||
self.updatePreview()
|
||
|
||
def browseExe(self):
|
||
"""Открывает файловый менеджер для выбора exe-файла"""
|
||
try:
|
||
# Use last_exe_path if available and valid, otherwise fallback to home
|
||
initial_path = os.path.dirname(self.last_exe_path) if self.last_exe_path and os.path.isfile(self.last_exe_path) else None
|
||
file_explorer = FileExplorer(self, file_filter='.exe', initial_path=initial_path)
|
||
file_explorer.file_signal.file_selected.connect(self.onExeSelected)
|
||
|
||
# Центрируем FileExplorer относительно родительского виджета
|
||
parent_widget = self.parentWidget() # QWidget или None
|
||
if parent_widget:
|
||
parent_geometry = parent_widget.geometry()
|
||
center_point = parent_geometry.center()
|
||
file_explorer_geometry = file_explorer.geometry()
|
||
file_explorer_geometry.moveCenter(center_point)
|
||
file_explorer.setGeometry(file_explorer_geometry)
|
||
|
||
file_explorer.show()
|
||
except Exception as e:
|
||
logger.error(f"Error in browseExe: {e}")
|
||
|
||
def onExeSelected(self, file_path):
|
||
"""Обработчик выбора файла в FileExplorer"""
|
||
self.exeEdit.setText(file_path)
|
||
self.last_exe_path = file_path # Update last selected exe path
|
||
if not self.edit_mode:
|
||
# Автоматически заполняем имя игры, если не в режиме редактирования
|
||
game_name = os.path.splitext(os.path.basename(file_path))[0]
|
||
self.nameEdit.setText(game_name)
|
||
|
||
# Обновляем превью
|
||
self.updatePreview()
|
||
|
||
def browseCover(self):
|
||
"""Открывает файловый менеджер для выбора изображения обложки"""
|
||
try:
|
||
# Use last_cover_path if available and valid, otherwise fallback to home
|
||
initial_path = os.path.dirname(self.last_cover_path) if self.last_cover_path and os.path.isfile(self.last_cover_path) else None
|
||
file_explorer = FileExplorer(self, file_filter=('.png', '.jpg', '.jpeg', '.bmp'), initial_path=initial_path)
|
||
file_explorer.file_signal.file_selected.connect(self.onCoverSelected)
|
||
|
||
# Центрируем FileExplorer относительно родительского виджета
|
||
parent_widget = self.parentWidget()
|
||
if parent_widget:
|
||
parent_geometry = parent_widget.geometry()
|
||
center_point = parent_geometry.center()
|
||
file_explorer_geometry = file_explorer.geometry()
|
||
file_explorer_geometry.moveCenter(center_point)
|
||
file_explorer.setGeometry(file_explorer_geometry)
|
||
|
||
file_explorer.show()
|
||
except Exception as e:
|
||
logger.error(f"Error in browseCover: {e}")
|
||
|
||
def onCoverSelected(self, file_path):
|
||
"""Обработчик выбора файла обложки в FileExplorer"""
|
||
if file_path and os.path.splitext(file_path)[1].lower() in ('.png', '.jpg', '.jpeg', '.bmp'):
|
||
self.coverEdit.setText(file_path)
|
||
self.last_cover_path = file_path # Update last selected cover path
|
||
else:
|
||
logger.warning(f"Selected file is not a valid image: {file_path}")
|
||
|
||
def updatePreview(self):
|
||
"""Update the cover preview image."""
|
||
cover_path = self.coverEdit.text().strip()
|
||
exe_path = self.exeEdit.text().strip()
|
||
if cover_path and os.path.isfile(cover_path):
|
||
pixmap = QPixmap(cover_path)
|
||
if not pixmap.isNull():
|
||
self.coverPreview.setPixmap(pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio))
|
||
else:
|
||
self.coverPreview.setText(_("Invalid image"))
|
||
elif os.path.isfile(exe_path):
|
||
tmp = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
|
||
tmp.close()
|
||
if generate_thumbnail(exe_path, tmp.name, size=128):
|
||
pixmap = QPixmap(tmp.name)
|
||
self.coverPreview.setPixmap(pixmap)
|
||
os.unlink(tmp.name)
|
||
else:
|
||
self.coverPreview.setText(_("No cover selected"))
|
||
|
||
def getDesktopEntryData(self):
|
||
"""Returns the .desktop content and save path"""
|
||
exe_path = self.exeEdit.text().strip()
|
||
name = self.nameEdit.text().strip()
|
||
|
||
if not exe_path or not name:
|
||
return None, None
|
||
|
||
portproton_path = get_portproton_location()
|
||
if portproton_path is None:
|
||
return None, None
|
||
|
||
is_flatpak = ".var" in portproton_path
|
||
base_path = os.path.join(portproton_path, "data")
|
||
|
||
if is_flatpak:
|
||
exec_str = f'flatpak run ru.linux_gaming.PortProton "{exe_path}"'
|
||
else:
|
||
start_sh = os.path.join(base_path, "scripts", "start.sh")
|
||
exec_str = f'env "{start_sh}" "{exe_path}"'
|
||
|
||
icon_path = os.path.join(base_path, "img", f"{name}.png")
|
||
desktop_path = os.path.join(portproton_path, f"{name}.desktop")
|
||
working_dir = os.path.join(base_path, "scripts")
|
||
|
||
os.makedirs(os.path.dirname(icon_path), exist_ok=True)
|
||
|
||
# Generate thumbnail (128x128) from exe
|
||
if not generate_thumbnail(exe_path, icon_path, size=128):
|
||
logger.error(f"Failed to generate thumbnail from exe: {exe_path}")
|
||
icon_path = "" # Set empty icon if generation fails
|
||
|
||
comment = _('Launch game "{name}" with PortProton').format(name=name)
|
||
|
||
desktop_entry = f"""[Desktop Entry]
|
||
Name={name}
|
||
Comment={comment}
|
||
Exec={exec_str}
|
||
Terminal=false
|
||
Type=Application
|
||
Categories=Game;
|
||
StartupNotify=true
|
||
Path={working_dir}
|
||
Icon={icon_path}
|
||
"""
|
||
|
||
return desktop_entry, desktop_path
|