Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e9865da0fe
|
|||
|
b88b4e0652
|
|||
|
d0fad6a3c9
|
|||
|
468887110c
|
|||
|
32e4950a00
|
|||
|
b16074fa5c
|
|||
|
1bd7c23419
|
|||
|
f4275dd465
|
|||
|
c8b91c4687
|
|||
|
4aaeb2e809
|
|||
|
b6ea9350fa
|
@@ -3,7 +3,7 @@
|
||||
Все заметные изменения в этом проекте фиксируются в этом файле.
|
||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [0.1.9] - 2025-12-01
|
||||
## [0.1.9] - 2025-12-08
|
||||
|
||||
### Added
|
||||
- Добавлены основные и расширенные настройки для `.exe`-файлов
|
||||
@@ -27,6 +27,7 @@
|
||||
- Улучшена работа с потоками для избежания вылетов
|
||||
- Исправлен запуск PortProton из Flatpak: теперь используется `flatpak run`, а не `start.sh`
|
||||
- Исправлено применение обложки по ссылке например со steamgriddb.com/
|
||||
- Исправлено множественное открытие окон в X11
|
||||
|
||||
### Contributors
|
||||
- @Vector_null
|
||||
|
||||
@@ -183,23 +183,39 @@ def get_portproton_start_command():
|
||||
if not portproton_path:
|
||||
return None
|
||||
|
||||
# Check if flatpak command exists before trying to run it
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["flatpak", "list"],
|
||||
subprocess.run(
|
||||
["flatpak", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=10
|
||||
timeout=5
|
||||
)
|
||||
if "ru.linux_gaming.PortProton" in result.stdout:
|
||||
logger.info("Detected Flatpak installation")
|
||||
return ["flatpak", "run", "ru.linux_gaming.PortProton"]
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("Flatpak list command timed out")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking flatpak list: {e}")
|
||||
pass
|
||||
flatpak_available = True
|
||||
except FileNotFoundError:
|
||||
flatpak_available = False
|
||||
except Exception:
|
||||
flatpak_available = False
|
||||
|
||||
if flatpak_available:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["flatpak", "list"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=10
|
||||
)
|
||||
if "ru.linux_gaming.PortProton" in result.stdout:
|
||||
logger.info("Detected Flatpak installation")
|
||||
return ["flatpak", "run", "ru.linux_gaming.PortProton"]
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("Flatpak list command timed out")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking flatpak list: {e}")
|
||||
pass
|
||||
|
||||
start_sh_path = os.path.join(portproton_path, "data", "scripts", "start.sh")
|
||||
if os.path.exists(start_sh_path):
|
||||
|
||||
@@ -1089,16 +1089,24 @@ class AddGameDialog(QDialog):
|
||||
|
||||
def handleDownloadedCover(self, file_path):
|
||||
"""Handle the downloaded cover image and update the preview."""
|
||||
if file_path and os.path.isfile(file_path):
|
||||
self.last_cover_path = file_path
|
||||
pixmap = QPixmap(file_path)
|
||||
if not pixmap.isNull():
|
||||
self.coverPreview.setPixmap(pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio))
|
||||
# Check if the dialog or widget has been destroyed before updating
|
||||
if not hasattr(self, 'coverPreview') or self.coverPreview is None:
|
||||
return
|
||||
|
||||
try:
|
||||
if file_path and os.path.isfile(file_path):
|
||||
self.last_cover_path = file_path
|
||||
pixmap = QPixmap(file_path)
|
||||
if not pixmap.isNull():
|
||||
self.coverPreview.setPixmap(pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio))
|
||||
else:
|
||||
self.coverPreview.setText(_("Invalid image"))
|
||||
else:
|
||||
self.coverPreview.setText(_("Invalid image"))
|
||||
else:
|
||||
self.coverPreview.setText(_("Failed to download cover"))
|
||||
logger.warning(f"Failed to download cover to {file_path}")
|
||||
self.coverPreview.setText(_("Failed to download cover"))
|
||||
logger.warning(f"Failed to download cover to {file_path}")
|
||||
except RuntimeError:
|
||||
# Handle the case where the Qt object was deleted
|
||||
pass
|
||||
|
||||
def onCoverTextChanged(self):
|
||||
"""Handle cover text changes with debounce."""
|
||||
@@ -1973,6 +1981,9 @@ class ExeSettingsDialog(QDialog):
|
||||
if re.match(r'^[A-Z_0-9]+=[^=]+$', line_stripped) and not line_stripped.startswith('PW_'):
|
||||
# System info
|
||||
k, v = line_stripped.split('=', 1)
|
||||
# Remove surrounding quotes from the value if present
|
||||
if v.startswith('"') and v.endswith('"') and len(v) >= 2:
|
||||
v = v[1:-1]
|
||||
if k.startswith('NUMA_NODE_'):
|
||||
node_id = k[10:]
|
||||
self.numa_nodes[node_id] = v
|
||||
@@ -2019,6 +2030,9 @@ class ExeSettingsDialog(QDialog):
|
||||
try:
|
||||
key, val = line_stripped.split('=', 1)
|
||||
if key in self.toggle_settings or key in ADVANCED_SETTING_KEYS:
|
||||
# Remove surrounding quotes from the value if present
|
||||
if val.startswith('"') and val.endswith('"') and len(val) >= 2:
|
||||
val = val[1:-1]
|
||||
self.current_settings[key] = val
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
@@ -200,13 +200,27 @@ class GameCard(QFrame):
|
||||
self.update_cover_pixmap()
|
||||
|
||||
def update_cover_pixmap(self):
|
||||
# Check if the coverLabel still exists before trying to update it
|
||||
# This prevents the "Internal C++ object already deleted" error when
|
||||
# the widget has been destroyed but the async callback still executes
|
||||
if not hasattr(self, 'coverLabel') or self.coverLabel is None:
|
||||
return
|
||||
|
||||
if self.base_pixmap and not self.base_pixmap.isNull():
|
||||
scaled_width = int(self.base_card_width * self._scale)
|
||||
scaled_pixmap = self.base_pixmap.scaled(scaled_width, int(scaled_width * 1.5), Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
|
||||
rounded_pixmap = round_corners(scaled_pixmap, int(15 * self._scale))
|
||||
self.coverLabel.setPixmap(rounded_pixmap)
|
||||
try:
|
||||
self.coverLabel.setPixmap(rounded_pixmap)
|
||||
except RuntimeError:
|
||||
# Handle the case where the Qt object was deleted between the check and the call
|
||||
pass
|
||||
|
||||
def _position_badges(self, current_width):
|
||||
# Check if the card has been destroyed before updating
|
||||
if not hasattr(self, 'coverLabel') or self.coverLabel is None:
|
||||
return
|
||||
|
||||
right_margin = int(8 * self._scale)
|
||||
badge_spacing = int(current_width * 0.02)
|
||||
top_y = int(10 * self._scale)
|
||||
@@ -225,16 +239,28 @@ class GameCard(QFrame):
|
||||
if is_visible:
|
||||
badge_x = current_width - badge_width - right_margin
|
||||
badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
||||
badge.move(int(badge_x), int(badge_y))
|
||||
badge_y_positions.append(badge_y + badge.height())
|
||||
try:
|
||||
badge.move(int(badge_x), int(badge_y))
|
||||
badge_y_positions.append(badge_y + badge.height())
|
||||
except RuntimeError:
|
||||
# Handle the case where the Qt object was deleted
|
||||
pass
|
||||
|
||||
self.anticheatLabel.raise_()
|
||||
self.protondbLabel.raise_()
|
||||
self.portprotonLabel.raise_()
|
||||
self.egsLabel.raise_()
|
||||
self.steamLabel.raise_()
|
||||
try:
|
||||
self.anticheatLabel.raise_()
|
||||
self.protondbLabel.raise_()
|
||||
self.portprotonLabel.raise_()
|
||||
self.egsLabel.raise_()
|
||||
self.steamLabel.raise_()
|
||||
except RuntimeError:
|
||||
# Handle the case where the Qt object was deleted
|
||||
pass
|
||||
|
||||
def update_scale(self):
|
||||
# Check if the card has been destroyed before updating
|
||||
if not hasattr(self, 'coverLabel') or self.coverLabel is None:
|
||||
return
|
||||
|
||||
scaled_width = int(self.base_card_width * self._scale)
|
||||
scaled_height = int(self.base_card_width * 1.8 * self._scale)
|
||||
scaled_extra = int(self.base_extra_margin * self._scale)
|
||||
@@ -255,33 +281,53 @@ class GameCard(QFrame):
|
||||
icon_space = int(scaled_width * 0.012)
|
||||
for label in [self.steamLabel, self.egsLabel, self.portprotonLabel, self.protondbLabel, self.anticheatLabel]:
|
||||
if label is not None:
|
||||
label.setFixedWidth(badge_width)
|
||||
label.setIconSize(icon_size, icon_space)
|
||||
label.setCardWidth(scaled_width)
|
||||
try:
|
||||
label.setFixedWidth(badge_width)
|
||||
label.setIconSize(icon_size, icon_space)
|
||||
label.setCardWidth(scaled_width)
|
||||
except RuntimeError:
|
||||
# Handle the case where the Qt object was deleted
|
||||
pass
|
||||
|
||||
self._position_badges(scaled_width)
|
||||
|
||||
if self.base_font_size is not None:
|
||||
font = self.nameLabel.font()
|
||||
new_font_size = self.base_font_size * self._scale
|
||||
if new_font_size > 0:
|
||||
font.setPointSizeF(new_font_size)
|
||||
self.nameLabel.setFont(font)
|
||||
try:
|
||||
font = self.nameLabel.font()
|
||||
new_font_size = self.base_font_size * self._scale
|
||||
if new_font_size > 0:
|
||||
font.setPointSizeF(new_font_size)
|
||||
self.nameLabel.setFont(font)
|
||||
except RuntimeError:
|
||||
# Handle the case where the Qt object was deleted
|
||||
pass
|
||||
|
||||
self.shadow.setBlurRadius(int(20 * self._scale))
|
||||
try:
|
||||
self.shadow.setBlurRadius(int(20 * self._scale))
|
||||
except RuntimeError:
|
||||
# Handle the case where the Qt object was deleted
|
||||
pass
|
||||
|
||||
self.updateGeometry()
|
||||
self.update()
|
||||
try:
|
||||
self.updateGeometry()
|
||||
self.update()
|
||||
except RuntimeError:
|
||||
# Handle the case where the Qt object was deleted
|
||||
pass
|
||||
|
||||
# Ensure parent layout is updated safely
|
||||
parent = self.parentWidget()
|
||||
if parent:
|
||||
layout = parent.layout()
|
||||
if layout:
|
||||
layout.invalidate()
|
||||
layout.activate()
|
||||
layout.update()
|
||||
parent.updateGeometry()
|
||||
try:
|
||||
parent = self.parentWidget()
|
||||
if parent:
|
||||
layout = parent.layout()
|
||||
if layout:
|
||||
layout.invalidate()
|
||||
layout.activate()
|
||||
layout.update()
|
||||
parent.updateGeometry()
|
||||
except RuntimeError:
|
||||
# Handle the case where the Qt object was deleted
|
||||
pass
|
||||
|
||||
def update_card_size(self, new_width: int):
|
||||
self.base_card_width = new_width
|
||||
@@ -289,6 +335,10 @@ class GameCard(QFrame):
|
||||
self.update_scale()
|
||||
|
||||
def update_badge_visibility(self, display_filter: str):
|
||||
# Check if the card has been destroyed before updating
|
||||
if not hasattr(self, 'coverLabel') or self.coverLabel is None:
|
||||
return
|
||||
|
||||
self.display_filter = display_filter
|
||||
self.steam_visible = (str(self.game_source).lower() == "steam" and self.display_filter in ("all", "favorites"))
|
||||
self.egs_visible = (str(self.game_source).lower() == "epic" and self.display_filter in ("all", "favorites"))
|
||||
@@ -296,11 +346,15 @@ class GameCard(QFrame):
|
||||
protondb_visible = bool(self.getProtonDBText(self.protondb_tier))
|
||||
anticheat_visible = bool(self.getAntiCheatText(self.anticheat_status))
|
||||
|
||||
self.steamLabel.setVisible(self.steam_visible)
|
||||
self.egsLabel.setVisible(self.egs_visible)
|
||||
self.portprotonLabel.setVisible(self.portproton_visible)
|
||||
self.protondbLabel.setVisible(protondb_visible)
|
||||
self.anticheatLabel.setVisible(anticheat_visible)
|
||||
try:
|
||||
self.steamLabel.setVisible(self.steam_visible)
|
||||
self.egsLabel.setVisible(self.egs_visible)
|
||||
self.portprotonLabel.setVisible(self.portproton_visible)
|
||||
self.protondbLabel.setVisible(protondb_visible)
|
||||
self.anticheatLabel.setVisible(anticheat_visible)
|
||||
except RuntimeError:
|
||||
# Handle the case where the Qt object was deleted
|
||||
return
|
||||
|
||||
scaled_width = int(self.base_card_width * self._scale)
|
||||
self._position_badges(scaled_width)
|
||||
@@ -395,21 +449,33 @@ class GameCard(QFrame):
|
||||
QDesktopServices.openUrl(url)
|
||||
|
||||
def update_favorite_icon(self):
|
||||
if self.is_favorite:
|
||||
self.favoriteLabel.setText("★")
|
||||
else:
|
||||
self.favoriteLabel.setText("☆")
|
||||
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
|
||||
# Check if the card has been destroyed before updating
|
||||
if not hasattr(self, 'coverLabel') or self.coverLabel is None:
|
||||
return
|
||||
|
||||
parent = self.parent()
|
||||
while parent:
|
||||
if hasattr(parent, 'game_library_manager'):
|
||||
# Access using getattr with default to avoid Ruff B009 warning
|
||||
manager = getattr(parent, 'game_library_manager', None)
|
||||
if manager is not None:
|
||||
QTimer.singleShot(0, manager.update_game_grid)
|
||||
break
|
||||
parent = parent.parent()
|
||||
try:
|
||||
if self.is_favorite:
|
||||
self.favoriteLabel.setText("★")
|
||||
else:
|
||||
self.favoriteLabel.setText("☆")
|
||||
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
|
||||
except RuntimeError:
|
||||
# Handle the case where the Qt object was deleted
|
||||
return
|
||||
|
||||
try:
|
||||
parent = self.parent()
|
||||
while parent:
|
||||
if hasattr(parent, 'game_library_manager'):
|
||||
# Access using getattr with default to avoid Ruff B009 warning
|
||||
manager = getattr(parent, 'game_library_manager', None)
|
||||
if manager is not None:
|
||||
QTimer.singleShot(0, manager.update_game_grid)
|
||||
break
|
||||
parent = parent.parent()
|
||||
except RuntimeError:
|
||||
# Handle the case where the Qt object was deleted
|
||||
pass
|
||||
|
||||
def toggle_favorite(self):
|
||||
favorites = read_favorites()
|
||||
|
||||
@@ -167,12 +167,18 @@ class GameLibraryManager:
|
||||
|
||||
if is_focused:
|
||||
if self.main_window.current_hovered_card and self.main_window.current_hovered_card != card:
|
||||
self.main_window.current_hovered_card._hovered = False
|
||||
self.main_window.current_hovered_card.leaveEvent(None)
|
||||
try:
|
||||
self.main_window.current_hovered_card._hovered = False
|
||||
self.main_window.current_hovered_card.leaveEvent(None)
|
||||
except RuntimeError:
|
||||
pass # Card already deleted
|
||||
self.main_window.current_hovered_card = None
|
||||
if self.main_window.current_focused_card and self.main_window.current_focused_card != card:
|
||||
self.main_window.current_focused_card._focused = False
|
||||
self.main_window.current_focused_card.clearFocus()
|
||||
try:
|
||||
self.main_window.current_focused_card._focused = False
|
||||
self.main_window.current_focused_card.clearFocus()
|
||||
except RuntimeError:
|
||||
pass # Card already deleted
|
||||
self.main_window.current_focused_card = card
|
||||
else:
|
||||
if self.main_window.current_focused_card == card:
|
||||
@@ -193,11 +199,19 @@ class GameLibraryManager:
|
||||
|
||||
if is_hovered:
|
||||
if self.main_window.current_focused_card and self.main_window.current_focused_card != card:
|
||||
self.main_window.current_focused_card._focused = False
|
||||
self.main_window.current_focused_card.clearFocus()
|
||||
try:
|
||||
if self.main_window.current_focused_card:
|
||||
self.main_window.current_focused_card._focused = False
|
||||
self.main_window.current_focused_card.clearFocus()
|
||||
except RuntimeError:
|
||||
pass # Card already deleted
|
||||
if self.main_window.current_hovered_card and self.main_window.current_hovered_card != card:
|
||||
self.main_window.current_hovered_card._hovered = False
|
||||
self.main_window.current_hovered_card.leaveEvent(None)
|
||||
try:
|
||||
if self.main_window.current_hovered_card:
|
||||
self.main_window.current_hovered_card._hovered = False
|
||||
self.main_window.current_hovered_card.leaveEvent(None)
|
||||
except RuntimeError:
|
||||
pass # Card already deleted
|
||||
self.main_window.current_hovered_card = card
|
||||
else:
|
||||
if self.main_window.current_hovered_card == card:
|
||||
@@ -476,6 +490,7 @@ class GameLibraryManager:
|
||||
select_callback=self.main_window.openGameDetailPage,
|
||||
theme=self.theme,
|
||||
card_width=self.card_width,
|
||||
parent=self.gamesListWidget,
|
||||
context_menu_manager=self.context_menu_manager
|
||||
)
|
||||
|
||||
@@ -498,6 +513,11 @@ class GameLibraryManager:
|
||||
def _flush_deletions(self):
|
||||
"""Delete pending widgets off the main update cycle."""
|
||||
for card in list(self.pending_deletions):
|
||||
# Clear any references to this card if it's currently focused/hovered
|
||||
if self.main_window.current_focused_card == card:
|
||||
self.main_window.current_focused_card = None
|
||||
if self.main_window.current_hovered_card == card:
|
||||
self.main_window.current_hovered_card = None
|
||||
card.deleteLater()
|
||||
self.pending_deletions.remove(card)
|
||||
|
||||
@@ -505,17 +525,25 @@ class GameLibraryManager:
|
||||
"""Clears all widgets from the layout."""
|
||||
if layout is None:
|
||||
return
|
||||
# Remove all widgets from the layout and clean up caches
|
||||
while layout.count():
|
||||
child = layout.takeAt(0)
|
||||
if child.widget():
|
||||
widget = child.widget()
|
||||
# Clean up cache if widget exists in it
|
||||
for key, card in list(self.game_card_cache.items()):
|
||||
if card == widget:
|
||||
del self.game_card_cache[key]
|
||||
if key in self.pending_images:
|
||||
del self.pending_images[key]
|
||||
break
|
||||
# Always schedule widget for deletion regardless of cache state
|
||||
widget.deleteLater()
|
||||
|
||||
# Also clear the cache completely if needed (in case layout wasn't in sync)
|
||||
self.game_card_cache.clear()
|
||||
self.pending_images.clear()
|
||||
|
||||
def set_games(self, games: list[tuple]):
|
||||
"""Sets the games list and updates the filtered games."""
|
||||
self.games = games
|
||||
|
||||
@@ -1939,8 +1939,10 @@ class InputManager(QObject):
|
||||
active_win.show_next()
|
||||
return True # Consume event to prevent tab switching
|
||||
|
||||
# Handle tab switching with Left/Right arrow keys when not in GameCard focus or QLineEdit
|
||||
if key in (Qt.Key.Key_Left, Qt.Key.Key_Right) and not isinstance(focused, GameCard | QLineEdit) and not self.file_explorer:
|
||||
# Handle tab switching with Left/Right arrow keys when not in GameCard focus or QLineEdit or QTableWidget or AutoSizeButton
|
||||
if (key in (Qt.Key.Key_Left, Qt.Key.Key_Right) and
|
||||
not isinstance(focused, GameCard | QLineEdit | QTableWidget | AutoSizeButton) and
|
||||
not self.file_explorer):
|
||||
idx = self._parent.stackedWidget.currentIndex()
|
||||
total = len(self._parent.tabButtons)
|
||||
if key == Qt.Key.Key_Left:
|
||||
@@ -1976,12 +1978,6 @@ class InputManager(QObject):
|
||||
self.dpad_moved.emit(dpad_code, dpad_value, now)
|
||||
return True
|
||||
|
||||
# Launch/stop game on detail page
|
||||
if self._parent.currentDetailPage and key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
||||
if self._parent.current_exec_line:
|
||||
self._parent.toggleGame(self._parent.current_exec_line, None)
|
||||
return True
|
||||
|
||||
# Context menu for GameCard
|
||||
if isinstance(focused, GameCard):
|
||||
if key == Qt.Key.Key_F10 and modifiers & Qt.KeyboardModifier.ShiftModifier:
|
||||
@@ -1991,6 +1987,18 @@ class InputManager(QObject):
|
||||
|
||||
# General actions: Activate, Back, Add
|
||||
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
||||
# Special handling for table widgets with checkboxes
|
||||
if isinstance(focused, QTableWidget):
|
||||
current_row = focused.currentRow()
|
||||
current_col = focused.currentColumn()
|
||||
if current_row >= 0 and current_col >= 0:
|
||||
# Check if the cell contains a checkbox
|
||||
item = focused.item(current_row, current_col)
|
||||
if item and (item.flags() & Qt.ItemFlag.ItemIsUserCheckable):
|
||||
# Toggle the checkbox state
|
||||
new_state = Qt.CheckState.Checked if item.checkState() == Qt.CheckState.Unchecked else Qt.CheckState.Unchecked
|
||||
item.setCheckState(new_state)
|
||||
return True
|
||||
self._parent.activateFocusedWidget()
|
||||
return True
|
||||
elif key in (Qt.Key.Key_Escape, Qt.Key.Key_Backspace):
|
||||
|
||||
@@ -1199,7 +1199,7 @@ class MainWindow(QMainWindow):
|
||||
self.progress_bar.setRange(0, 0) # Indeterminate
|
||||
self.update_status_message.emit(_("Refreshing game library..."), 0)
|
||||
|
||||
# Clear the game card cache to force reload of custom data
|
||||
# Clear the game card cache and layout to force reload of custom data
|
||||
if hasattr(self, 'game_library_manager') and self.game_library_manager:
|
||||
# Clear the cache to ensure custom data is reloaded
|
||||
self.game_library_manager.game_card_cache.clear()
|
||||
@@ -1209,6 +1209,19 @@ class MainWindow(QMainWindow):
|
||||
# Mark for full rebuild of search indices
|
||||
self.game_library_manager.dirty = True # Force full update
|
||||
|
||||
# Also clear the layout to ensure old widgets are removed
|
||||
if (hasattr(self.game_library_manager, 'gamesListLayout') and
|
||||
self.game_library_manager.gamesListLayout and
|
||||
hasattr(self.game_library_manager, 'gamesListWidget') and
|
||||
self.game_library_manager.gamesListWidget):
|
||||
# Remove all widgets from the layout
|
||||
self.game_library_manager.clear_layout(self.game_library_manager.gamesListLayout)
|
||||
|
||||
# Force layout update to ensure UI changes are visible
|
||||
self.game_library_manager.gamesListWidget.updateGeometry()
|
||||
if hasattr(self.game_library_manager, 'gamesListLayout'):
|
||||
self.game_library_manager.gamesListLayout.update()
|
||||
|
||||
# Reload games using the existing loadGames functionality
|
||||
# Use a small delay to allow UI to update before starting the refresh
|
||||
QTimer.singleShot(50, lambda: self.loadGames())
|
||||
@@ -3216,7 +3229,10 @@ class MainWindow(QMainWindow):
|
||||
# Игра стартовала – устанавливаем флаг, обновляем кнопку на "Stop"
|
||||
self._gameLaunched = True
|
||||
if self.current_running_button is not None:
|
||||
self.current_running_button.setText(_("Stop"))
|
||||
try:
|
||||
self.current_running_button.setText(_("Stop"))
|
||||
except RuntimeError:
|
||||
self.current_running_button = None
|
||||
#self._inhibit_screensaver()
|
||||
elif not child_running:
|
||||
# Игра завершилась – сбрасываем флаг, сбрасываем кнопку и останавливаем таймер
|
||||
@@ -3235,13 +3251,16 @@ class MainWindow(QMainWindow):
|
||||
Вызывается, когда игра завершилась (не по нажатию кнопки).
|
||||
"""
|
||||
if self.current_running_button is not None:
|
||||
self.current_running_button.setText(_("Play"))
|
||||
icon = self.theme_manager.get_icon("play")
|
||||
if isinstance(icon, str):
|
||||
icon = QIcon(icon) # Convert path to QIcon
|
||||
elif icon is None:
|
||||
icon = QIcon() # Use empty QIcon as fallback
|
||||
self.current_running_button.setIcon(icon)
|
||||
try:
|
||||
self.current_running_button.setText(_("Play"))
|
||||
icon = self.theme_manager.get_icon("play")
|
||||
if isinstance(icon, str):
|
||||
icon = QIcon(icon) # Convert path to QIcon
|
||||
elif icon is None:
|
||||
icon = QIcon() # Use empty QIcon as fallback
|
||||
self.current_running_button.setIcon(icon)
|
||||
except RuntimeError:
|
||||
pass
|
||||
self.current_running_button = None
|
||||
self.target_exe = None
|
||||
|
||||
@@ -3294,13 +3313,16 @@ class MainWindow(QMainWindow):
|
||||
pass
|
||||
self.game_processes = []
|
||||
if update_button:
|
||||
update_button.setText(_("Play"))
|
||||
icon = self.theme_manager.get_icon("play")
|
||||
if isinstance(icon, str):
|
||||
icon = QIcon(icon)
|
||||
elif icon is None:
|
||||
icon = QIcon()
|
||||
update_button.setIcon(icon)
|
||||
try:
|
||||
update_button.setText(_("Play"))
|
||||
icon = self.theme_manager.get_icon("play")
|
||||
if isinstance(icon, str):
|
||||
icon = QIcon(icon)
|
||||
elif icon is None:
|
||||
icon = QIcon()
|
||||
update_button.setIcon(icon)
|
||||
except RuntimeError:
|
||||
pass
|
||||
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None:
|
||||
self.checkProcessTimer.stop()
|
||||
self.checkProcessTimer.deleteLater()
|
||||
@@ -3322,13 +3344,16 @@ class MainWindow(QMainWindow):
|
||||
self.game_processes.append(process)
|
||||
save_last_launch(exe_name, datetime.now())
|
||||
if update_button:
|
||||
update_button.setText(_("Launching"))
|
||||
icon = self.theme_manager.get_icon("stop")
|
||||
if isinstance(icon, str):
|
||||
icon = QIcon(icon)
|
||||
elif icon is None:
|
||||
icon = QIcon()
|
||||
update_button.setIcon(icon)
|
||||
try:
|
||||
update_button.setText(_("Launching"))
|
||||
icon = self.theme_manager.get_icon("stop")
|
||||
if isinstance(icon, str):
|
||||
icon = QIcon(icon)
|
||||
elif icon is None:
|
||||
icon = QIcon()
|
||||
update_button.setIcon(icon)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
self.checkProcessTimer = QTimer(self)
|
||||
self.checkProcessTimer.timeout.connect(self.checkTargetExe)
|
||||
@@ -3385,13 +3410,16 @@ class MainWindow(QMainWindow):
|
||||
pass
|
||||
self.game_processes = []
|
||||
if update_button:
|
||||
update_button.setText(_("Play"))
|
||||
icon = self.theme_manager.get_icon("play")
|
||||
if isinstance(icon, str):
|
||||
icon = QIcon(icon)
|
||||
elif icon is None:
|
||||
icon = QIcon()
|
||||
update_button.setIcon(icon)
|
||||
try:
|
||||
update_button.setText(_("Play"))
|
||||
icon = self.theme_manager.get_icon("play")
|
||||
if isinstance(icon, str):
|
||||
icon = QIcon(icon)
|
||||
elif icon is None:
|
||||
icon = QIcon()
|
||||
update_button.setIcon(icon)
|
||||
except RuntimeError:
|
||||
pass
|
||||
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None:
|
||||
self.checkProcessTimer.stop()
|
||||
self.checkProcessTimer.deleteLater()
|
||||
@@ -3413,13 +3441,16 @@ class MainWindow(QMainWindow):
|
||||
self.game_processes.append(process)
|
||||
save_last_launch(exe_name, datetime.now())
|
||||
if update_button:
|
||||
update_button.setText(_("Launching"))
|
||||
icon = self.theme_manager.get_icon("stop")
|
||||
if isinstance(icon, str):
|
||||
icon = QIcon(icon)
|
||||
elif icon is None:
|
||||
icon = QIcon()
|
||||
update_button.setIcon(icon)
|
||||
try:
|
||||
update_button.setText(_("Launching"))
|
||||
icon = self.theme_manager.get_icon("stop")
|
||||
if isinstance(icon, str):
|
||||
icon = QIcon(icon)
|
||||
elif icon is None:
|
||||
icon = QIcon()
|
||||
update_button.setIcon(icon)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
self.checkProcessTimer = QTimer(self)
|
||||
self.checkProcessTimer.timeout.connect(self.checkTargetExe)
|
||||
|
||||
@@ -66,8 +66,11 @@ class VirtualKeyboard(QFrame):
|
||||
if not self.current_input_widget or not isinstance(self.current_input_widget, QLineEdit):
|
||||
return
|
||||
|
||||
# Просто устанавливаем курсор на нужную позицию без выделения
|
||||
self.current_input_widget.setCursorPosition(self.current_input_widget.cursorPosition())
|
||||
try:
|
||||
# Просто устанавливаем курсор на нужную позицию без выделения
|
||||
self.current_input_widget.setCursorPosition(self.current_input_widget.cursorPosition())
|
||||
except RuntimeError:
|
||||
self.current_input_widget = None
|
||||
|
||||
def initUI(self):
|
||||
layout = QVBoxLayout()
|
||||
@@ -290,31 +293,43 @@ class VirtualKeyboard(QFrame):
|
||||
def up_key(self):
|
||||
"""Перемещает курсор в QLineEdit вверх/в начало, если клавиатура видима"""
|
||||
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
|
||||
self.current_input_widget.setCursorPosition(0)
|
||||
self.current_input_widget.setFocus()
|
||||
try:
|
||||
self.current_input_widget.setCursorPosition(0)
|
||||
self.current_input_widget.setFocus()
|
||||
except RuntimeError:
|
||||
self.current_input_widget = None
|
||||
|
||||
def down_key(self):
|
||||
"""Перемещает курсор в QLineEdit вниз/в конец, если клавиатура видима"""
|
||||
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
|
||||
self.current_input_widget.setCursorPosition(len(self.current_input_widget.text()))
|
||||
self.current_input_widget.setFocus()
|
||||
try:
|
||||
self.current_input_widget.setCursorPosition(len(self.current_input_widget.text()))
|
||||
self.current_input_widget.setFocus()
|
||||
except RuntimeError:
|
||||
self.current_input_widget = None
|
||||
|
||||
def left_key(self):
|
||||
"""Перемещает курсор в QLineEdit влево, если клавиатура видима"""
|
||||
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
|
||||
pos = self.current_input_widget.cursorPosition()
|
||||
if pos > 0:
|
||||
self.current_input_widget.setCursorPosition(pos - 1)
|
||||
self.current_input_widget.setFocus()
|
||||
try:
|
||||
pos = self.current_input_widget.cursorPosition()
|
||||
if pos > 0:
|
||||
self.current_input_widget.setCursorPosition(pos - 1)
|
||||
self.current_input_widget.setFocus()
|
||||
except RuntimeError:
|
||||
self.current_input_widget = None
|
||||
|
||||
def right_key(self):
|
||||
"""Перемещает курсор в QLineEdit вправо, если клавиатура видима"""
|
||||
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
|
||||
pos = self.current_input_widget.cursorPosition()
|
||||
text_len = len(self.current_input_widget.text())
|
||||
if pos < text_len:
|
||||
self.current_input_widget.setCursorPosition(pos + 1)
|
||||
self.current_input_widget.setFocus()
|
||||
try:
|
||||
pos = self.current_input_widget.cursorPosition()
|
||||
text_len = len(self.current_input_widget.text())
|
||||
if pos < text_len:
|
||||
self.current_input_widget.setCursorPosition(pos + 1)
|
||||
self.current_input_widget.setFocus()
|
||||
except RuntimeError:
|
||||
self.current_input_widget = None
|
||||
|
||||
def move_focus_up(self):
|
||||
"""Перемещает фокус по кнопкам клавиатуры вверх с фиксированной скоростью"""
|
||||
@@ -370,35 +385,41 @@ class VirtualKeyboard(QFrame):
|
||||
self.on_shift_click(not self.shift_pressed)
|
||||
self.highlight_cursor_position()
|
||||
elif self.current_input_widget is not None:
|
||||
# Сохраняем текущую кнопку с фокусом
|
||||
focused_button = self.focusWidget()
|
||||
key_to_restore = None
|
||||
if isinstance(focused_button, QPushButton) and focused_button in self.buttons.values():
|
||||
key_to_restore = next((k for k, btn in self.buttons.items() if btn == focused_button), None)
|
||||
try:
|
||||
# Сохраняем текущую кнопку с фокусом
|
||||
focused_button = self.focusWidget()
|
||||
key_to_restore = None
|
||||
if isinstance(focused_button, QPushButton) and focused_button in self.buttons.values():
|
||||
key_to_restore = next((k for k, btn in self.buttons.items() if btn == focused_button), None)
|
||||
|
||||
key = "&" if key == "&&" else key
|
||||
cursor_pos = self.current_input_widget.cursorPosition()
|
||||
text = self.current_input_widget.text()
|
||||
new_text = text[:cursor_pos] + key + text[cursor_pos:]
|
||||
self.current_input_widget.setText(new_text)
|
||||
self.current_input_widget.setCursorPosition(cursor_pos + len(key))
|
||||
self.keyPressed.emit(key)
|
||||
self.highlight_cursor_position()
|
||||
key = "&" if key == "&&" else key
|
||||
cursor_pos = self.current_input_widget.cursorPosition()
|
||||
text = self.current_input_widget.text()
|
||||
new_text = text[:cursor_pos] + key + text[cursor_pos:]
|
||||
self.current_input_widget.setText(new_text)
|
||||
self.current_input_widget.setCursorPosition(cursor_pos + len(key))
|
||||
self.keyPressed.emit(key)
|
||||
self.highlight_cursor_position()
|
||||
|
||||
# Если был нажат SHIFT, но не CapsLock, отключаем его после ввода символа
|
||||
if self.shift_pressed and not self.caps_lock:
|
||||
self.shift_pressed = False
|
||||
self.update_keyboard()
|
||||
if key_to_restore and key_to_restore in self.buttons:
|
||||
self.buttons[key_to_restore].setFocus()
|
||||
# Если был нажат SHIFT, но не CapsLock, отключаем его после ввода символа
|
||||
if self.shift_pressed and not self.caps_lock:
|
||||
self.shift_pressed = False
|
||||
self.update_keyboard()
|
||||
if key_to_restore and key_to_restore in self.buttons:
|
||||
self.buttons[key_to_restore].setFocus()
|
||||
except RuntimeError:
|
||||
self.current_input_widget = None
|
||||
|
||||
def on_tab_click(self):
|
||||
if self.current_input_widget is not None:
|
||||
self.current_input_widget.insert('\t')
|
||||
self.keyPressed.emit('Tab')
|
||||
if self.current_input_widget:
|
||||
self.current_input_widget.setFocus()
|
||||
self.highlight_cursor_position()
|
||||
try:
|
||||
self.current_input_widget.insert('\t')
|
||||
self.keyPressed.emit('Tab')
|
||||
if self.current_input_widget:
|
||||
self.current_input_widget.setFocus()
|
||||
self.highlight_cursor_position()
|
||||
except RuntimeError:
|
||||
self.current_input_widget = None
|
||||
|
||||
def on_caps_click(self):
|
||||
"""Включаем/выключаем CapsLock"""
|
||||
@@ -417,15 +438,18 @@ class VirtualKeyboard(QFrame):
|
||||
def on_backspace_click(self):
|
||||
"""Обработка одного нажатия Backspace"""
|
||||
if self.current_input_widget is not None:
|
||||
cursor_pos = self.current_input_widget.cursorPosition()
|
||||
text = self.current_input_widget.text()
|
||||
try:
|
||||
cursor_pos = self.current_input_widget.cursorPosition()
|
||||
text = self.current_input_widget.text()
|
||||
|
||||
if cursor_pos > 0:
|
||||
new_text = text[:cursor_pos - 1] + text[cursor_pos:]
|
||||
self.current_input_widget.setText(new_text)
|
||||
self.current_input_widget.setCursorPosition(cursor_pos - 1)
|
||||
self.keyPressed.emit('Backspace')
|
||||
self.highlight_cursor_position()
|
||||
if cursor_pos > 0:
|
||||
new_text = text[:cursor_pos - 1] + text[cursor_pos:]
|
||||
self.current_input_widget.setText(new_text)
|
||||
self.current_input_widget.setCursorPosition(cursor_pos - 1)
|
||||
self.keyPressed.emit('Backspace')
|
||||
self.highlight_cursor_position()
|
||||
except RuntimeError:
|
||||
self.current_input_widget = None
|
||||
|
||||
def on_backspace_pressed(self):
|
||||
"""Обработка зажатого Backspace"""
|
||||
@@ -449,15 +473,21 @@ class VirtualKeyboard(QFrame):
|
||||
# TODO: тут подумать, как обрабатывать нажатие.
|
||||
# Пока болванка перехода на новую строку, в QlineEdit работает как нажатие пробела
|
||||
if self.current_input_widget is not None:
|
||||
self.current_input_widget.insert('\n')
|
||||
self.keyPressed.emit('Enter')
|
||||
try:
|
||||
self.current_input_widget.insert('\n')
|
||||
self.keyPressed.emit('Enter')
|
||||
except RuntimeError:
|
||||
self.current_input_widget = None
|
||||
|
||||
def on_clear_click(self):
|
||||
"""Чистим строку от введённого текста"""
|
||||
if self.current_input_widget is not None:
|
||||
self.current_input_widget.clear()
|
||||
self.keyPressed.emit('Clear')
|
||||
self.highlight_cursor_position()
|
||||
try:
|
||||
self.current_input_widget.clear()
|
||||
self.keyPressed.emit('Clear')
|
||||
self.highlight_cursor_position()
|
||||
except RuntimeError:
|
||||
self.current_input_widget = None
|
||||
|
||||
def on_lang_click(self):
|
||||
"""Переключение раскладки"""
|
||||
@@ -483,8 +513,11 @@ class VirtualKeyboard(QFrame):
|
||||
def show_for_widget(self, widget):
|
||||
self.current_input_widget = widget
|
||||
if widget:
|
||||
widget.setFocus()
|
||||
self.highlight_cursor_position()
|
||||
try:
|
||||
widget.setFocus()
|
||||
self.highlight_cursor_position()
|
||||
except RuntimeError:
|
||||
self.current_input_widget = None
|
||||
|
||||
# Позиционирование клавиатуры внизу родительского виджета
|
||||
if self._parent and isinstance(self._parent, QWidget):
|
||||
|
||||
Reference in New Issue
Block a user