11 Commits

Author SHA1 Message Date
e9865da0fe chore: bump to 0.1.9
All checks were successful
Build AppImage, Arch and Fedora Packages / Build AppImage (push) Successful in 2m57s
Build AppImage, Arch and Fedora Packages / Build Arch Package (push) Successful in 1m40s
Build AppImage, Arch and Fedora Packages / Build Fedora RPM (41) (push) Successful in 54s
Build AppImage, Arch and Fedora Packages / Build Fedora RPM (42) (push) Successful in 49s
Build AppImage, Arch and Fedora Packages / Build Fedora RPM (43) (push) Successful in 52s
Build AppImage, Arch and Fedora Packages / Build Fedora RPM (rawhide) (push) Successful in 1m3s
Build AppImage, Arch and Fedora Packages / Create and Publish Release (push) Successful in 22s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-08 11:46:35 +05:00
b88b4e0652 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-08 11:45:29 +05:00
d0fad6a3c9 fix: added correct parent to GameCard
All checks were successful
Code check / Check code (push) Successful in 1m30s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-07 15:46:27 +05:00
468887110c fix(qt): prevent RuntimeError from accessing deleted Qt C++ objects
All checks were successful
Code check / Check code (push) Successful in 1m34s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-07 12:45:37 +05:00
32e4950a00 Revert "chore: bump ver to 0.1.9"
All checks were successful
Code check / Check code (push) Successful in 1m5s
renovate / renovate (push) Successful in 44s
This reverts commit 29d25cec01.
2025-12-06 14:26:04 +05:00
b16074fa5c fix: Add protection against accessing deleted Qt objects in async callbacks
All checks were successful
Code check / Check code (push) Successful in 1m7s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-06 14:22:41 +05:00
1bd7c23419 fix(settings): Remove surrounding quotes from the value if present
All checks were successful
Code check / Check code (push) Successful in 1m21s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-04 11:53:54 +05:00
f4275dd465 fix(get_portproton_start_command): Check if flatpak command exists before trying to run it
All checks were successful
Code check / Check code (push) Successful in 1m7s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-02 18:44:47 +05:00
c8b91c4687 fix(settings): update keyboard navigation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-02 18:40:27 +05:00
4aaeb2e809 fix: dont start game by Enter
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-02 18:23:49 +05:00
b6ea9350fa fix: fix gamecard refrefresh regression after 0889aa8
All checks were successful
Code check / Check code (push) Successful in 1m13s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-02 17:52:19 +05:00
8 changed files with 372 additions and 175 deletions

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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):