fix: Add protection against accessing deleted Qt objects in async callbacks

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
2025-12-06 14:22:41 +05:00
parent 1bd7c23419
commit b16074fa5c
2 changed files with 129 additions and 55 deletions

View File

@@ -1089,16 +1089,24 @@ class AddGameDialog(QDialog):
def handleDownloadedCover(self, file_path): def handleDownloadedCover(self, file_path):
"""Handle the downloaded cover image and update the preview.""" """Handle the downloaded cover image and update the preview."""
if file_path and os.path.isfile(file_path): # Check if the dialog or widget has been destroyed before updating
self.last_cover_path = file_path if not hasattr(self, 'coverPreview') or self.coverPreview is None:
pixmap = QPixmap(file_path) return
if not pixmap.isNull():
self.coverPreview.setPixmap(pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio)) 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: else:
self.coverPreview.setText(_("Invalid image")) self.coverPreview.setText(_("Failed to download cover"))
else: logger.warning(f"Failed to download cover to {file_path}")
self.coverPreview.setText(_("Failed to download cover")) except RuntimeError:
logger.warning(f"Failed to download cover to {file_path}") # Handle the case where the Qt object was deleted
pass
def onCoverTextChanged(self): def onCoverTextChanged(self):
"""Handle cover text changes with debounce.""" """Handle cover text changes with debounce."""

View File

@@ -200,13 +200,27 @@ class GameCard(QFrame):
self.update_cover_pixmap() self.update_cover_pixmap()
def update_cover_pixmap(self): 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(): if self.base_pixmap and not self.base_pixmap.isNull():
scaled_width = int(self.base_card_width * self._scale) 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) 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)) 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): 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) right_margin = int(8 * self._scale)
badge_spacing = int(current_width * 0.02) badge_spacing = int(current_width * 0.02)
top_y = int(10 * self._scale) top_y = int(10 * self._scale)
@@ -225,16 +239,28 @@ class GameCard(QFrame):
if is_visible: if is_visible:
badge_x = current_width - badge_width - right_margin badge_x = current_width - badge_width - right_margin
badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
badge.move(int(badge_x), int(badge_y)) try:
badge_y_positions.append(badge_y + badge.height()) 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_() try:
self.protondbLabel.raise_() self.anticheatLabel.raise_()
self.portprotonLabel.raise_() self.protondbLabel.raise_()
self.egsLabel.raise_() self.portprotonLabel.raise_()
self.steamLabel.raise_() self.egsLabel.raise_()
self.steamLabel.raise_()
except RuntimeError:
# Handle the case where the Qt object was deleted
pass
def update_scale(self): 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_width = int(self.base_card_width * self._scale)
scaled_height = int(self.base_card_width * 1.8 * self._scale) scaled_height = int(self.base_card_width * 1.8 * self._scale)
scaled_extra = int(self.base_extra_margin * 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) icon_space = int(scaled_width * 0.012)
for label in [self.steamLabel, self.egsLabel, self.portprotonLabel, self.protondbLabel, self.anticheatLabel]: for label in [self.steamLabel, self.egsLabel, self.portprotonLabel, self.protondbLabel, self.anticheatLabel]:
if label is not None: if label is not None:
label.setFixedWidth(badge_width) try:
label.setIconSize(icon_size, icon_space) label.setFixedWidth(badge_width)
label.setCardWidth(scaled_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) self._position_badges(scaled_width)
if self.base_font_size is not None: if self.base_font_size is not None:
font = self.nameLabel.font() try:
new_font_size = self.base_font_size * self._scale font = self.nameLabel.font()
if new_font_size > 0: new_font_size = self.base_font_size * self._scale
font.setPointSizeF(new_font_size) if new_font_size > 0:
self.nameLabel.setFont(font) 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() try:
self.update() self.updateGeometry()
self.update()
except RuntimeError:
# Handle the case where the Qt object was deleted
pass
# Ensure parent layout is updated safely # Ensure parent layout is updated safely
parent = self.parentWidget() try:
if parent: parent = self.parentWidget()
layout = parent.layout() if parent:
if layout: layout = parent.layout()
layout.invalidate() if layout:
layout.activate() layout.invalidate()
layout.update() layout.activate()
parent.updateGeometry() layout.update()
parent.updateGeometry()
except RuntimeError:
# Handle the case where the Qt object was deleted
pass
def update_card_size(self, new_width: int): def update_card_size(self, new_width: int):
self.base_card_width = new_width self.base_card_width = new_width
@@ -289,6 +335,10 @@ class GameCard(QFrame):
self.update_scale() self.update_scale()
def update_badge_visibility(self, display_filter: str): 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.display_filter = display_filter
self.steam_visible = (str(self.game_source).lower() == "steam" and self.display_filter in ("all", "favorites")) 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")) 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)) protondb_visible = bool(self.getProtonDBText(self.protondb_tier))
anticheat_visible = bool(self.getAntiCheatText(self.anticheat_status)) anticheat_visible = bool(self.getAntiCheatText(self.anticheat_status))
self.steamLabel.setVisible(self.steam_visible) try:
self.egsLabel.setVisible(self.egs_visible) self.steamLabel.setVisible(self.steam_visible)
self.portprotonLabel.setVisible(self.portproton_visible) self.egsLabel.setVisible(self.egs_visible)
self.protondbLabel.setVisible(protondb_visible) self.portprotonLabel.setVisible(self.portproton_visible)
self.anticheatLabel.setVisible(anticheat_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) scaled_width = int(self.base_card_width * self._scale)
self._position_badges(scaled_width) self._position_badges(scaled_width)
@@ -395,21 +449,33 @@ class GameCard(QFrame):
QDesktopServices.openUrl(url) QDesktopServices.openUrl(url)
def update_favorite_icon(self): def update_favorite_icon(self):
if self.is_favorite: # Check if the card has been destroyed before updating
self.favoriteLabel.setText("") if not hasattr(self, 'coverLabel') or self.coverLabel is None:
else: return
self.favoriteLabel.setText("")
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
parent = self.parent() try:
while parent: if self.is_favorite:
if hasattr(parent, 'game_library_manager'): self.favoriteLabel.setText("")
# Access using getattr with default to avoid Ruff B009 warning else:
manager = getattr(parent, 'game_library_manager', None) self.favoriteLabel.setText("")
if manager is not None: self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
QTimer.singleShot(0, manager.update_game_grid) except RuntimeError:
break # Handle the case where the Qt object was deleted
parent = parent.parent() 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): def toggle_favorite(self):
favorites = read_favorites() favorites = read_favorites()