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,6 +1089,11 @@ 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."""
# 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): if file_path and os.path.isfile(file_path):
self.last_cover_path = file_path self.last_cover_path = file_path
pixmap = QPixmap(file_path) pixmap = QPixmap(file_path)
@@ -1099,6 +1104,9 @@ class AddGameDialog(QDialog):
else: else:
self.coverPreview.setText(_("Failed to download cover")) self.coverPreview.setText(_("Failed to download cover"))
logger.warning(f"Failed to download cover to {file_path}") 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): 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))
try:
self.coverLabel.setPixmap(rounded_pixmap) 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
try:
badge.move(int(badge_x), int(badge_y)) badge.move(int(badge_x), int(badge_y))
badge_y_positions.append(badge_y + badge.height()) badge_y_positions.append(badge_y + badge.height())
except RuntimeError:
# Handle the case where the Qt object was deleted
pass
try:
self.anticheatLabel.raise_() self.anticheatLabel.raise_()
self.protondbLabel.raise_() self.protondbLabel.raise_()
self.portprotonLabel.raise_() self.portprotonLabel.raise_()
self.egsLabel.raise_() self.egsLabel.raise_()
self.steamLabel.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,25 +281,42 @@ 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:
try:
label.setFixedWidth(badge_width) label.setFixedWidth(badge_width)
label.setIconSize(icon_size, icon_space) label.setIconSize(icon_size, icon_space)
label.setCardWidth(scaled_width) 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:
try:
font = self.nameLabel.font() font = self.nameLabel.font()
new_font_size = self.base_font_size * self._scale new_font_size = self.base_font_size * self._scale
if new_font_size > 0: if new_font_size > 0:
font.setPointSizeF(new_font_size) font.setPointSizeF(new_font_size)
self.nameLabel.setFont(font) self.nameLabel.setFont(font)
except RuntimeError:
# Handle the case where the Qt object was deleted
pass
try:
self.shadow.setBlurRadius(int(20 * self._scale)) self.shadow.setBlurRadius(int(20 * self._scale))
except RuntimeError:
# Handle the case where the Qt object was deleted
pass
try:
self.updateGeometry() self.updateGeometry()
self.update() 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
try:
parent = self.parentWidget() parent = self.parentWidget()
if parent: if parent:
layout = parent.layout() layout = parent.layout()
@@ -282,6 +325,9 @@ class GameCard(QFrame):
layout.activate() layout.activate()
layout.update() layout.update()
parent.updateGeometry() 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))
try:
self.steamLabel.setVisible(self.steam_visible) self.steamLabel.setVisible(self.steam_visible)
self.egsLabel.setVisible(self.egs_visible) self.egsLabel.setVisible(self.egs_visible)
self.portprotonLabel.setVisible(self.portproton_visible) self.portprotonLabel.setVisible(self.portproton_visible)
self.protondbLabel.setVisible(protondb_visible) self.protondbLabel.setVisible(protondb_visible)
self.anticheatLabel.setVisible(anticheat_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,12 +449,21 @@ class GameCard(QFrame):
QDesktopServices.openUrl(url) QDesktopServices.openUrl(url)
def update_favorite_icon(self): def update_favorite_icon(self):
# Check if the card has been destroyed before updating
if not hasattr(self, 'coverLabel') or self.coverLabel is None:
return
try:
if self.is_favorite: if self.is_favorite:
self.favoriteLabel.setText("") self.favoriteLabel.setText("")
else: else:
self.favoriteLabel.setText("") self.favoriteLabel.setText("")
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE) self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
except RuntimeError:
# Handle the case where the Qt object was deleted
return
try:
parent = self.parent() parent = self.parent()
while parent: while parent:
if hasattr(parent, 'game_library_manager'): if hasattr(parent, 'game_library_manager'):
@@ -410,6 +473,9 @@ class GameCard(QFrame):
QTimer.singleShot(0, manager.update_game_grid) QTimer.singleShot(0, manager.update_game_grid)
break break
parent = parent.parent() 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()