fix(qt): prevent RuntimeError from accessing deleted Qt C++ objects

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
2025-12-07 12:45:37 +05:00
parent 32e4950a00
commit 468887110c
3 changed files with 168 additions and 98 deletions

View File

@@ -167,12 +167,18 @@ class GameLibraryManager:
if is_focused: if is_focused:
if self.main_window.current_hovered_card and self.main_window.current_hovered_card != card: if self.main_window.current_hovered_card and self.main_window.current_hovered_card != card:
self.main_window.current_hovered_card._hovered = False try:
self.main_window.current_hovered_card.leaveEvent(None) 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 self.main_window.current_hovered_card = None
if self.main_window.current_focused_card and self.main_window.current_focused_card != card: if self.main_window.current_focused_card and self.main_window.current_focused_card != card:
self.main_window.current_focused_card._focused = False try:
self.main_window.current_focused_card.clearFocus() 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 self.main_window.current_focused_card = card
else: else:
if self.main_window.current_focused_card == card: if self.main_window.current_focused_card == card:
@@ -193,11 +199,19 @@ class GameLibraryManager:
if is_hovered: if is_hovered:
if self.main_window.current_focused_card and self.main_window.current_focused_card != card: if self.main_window.current_focused_card and self.main_window.current_focused_card != card:
self.main_window.current_focused_card._focused = False try:
self.main_window.current_focused_card.clearFocus() 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: if self.main_window.current_hovered_card and self.main_window.current_hovered_card != card:
self.main_window.current_hovered_card._hovered = False try:
self.main_window.current_hovered_card.leaveEvent(None) 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 self.main_window.current_hovered_card = card
else: else:
if self.main_window.current_hovered_card == card: if self.main_window.current_hovered_card == card:
@@ -498,6 +512,11 @@ class GameLibraryManager:
def _flush_deletions(self): def _flush_deletions(self):
"""Delete pending widgets off the main update cycle.""" """Delete pending widgets off the main update cycle."""
for card in list(self.pending_deletions): 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() card.deleteLater()
self.pending_deletions.remove(card) self.pending_deletions.remove(card)

View File

@@ -3229,7 +3229,10 @@ class MainWindow(QMainWindow):
# Игра стартовала устанавливаем флаг, обновляем кнопку на "Stop" # Игра стартовала устанавливаем флаг, обновляем кнопку на "Stop"
self._gameLaunched = True self._gameLaunched = True
if self.current_running_button is not None: 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() #self._inhibit_screensaver()
elif not child_running: elif not child_running:
# Игра завершилась сбрасываем флаг, сбрасываем кнопку и останавливаем таймер # Игра завершилась сбрасываем флаг, сбрасываем кнопку и останавливаем таймер
@@ -3248,13 +3251,16 @@ class MainWindow(QMainWindow):
Вызывается, когда игра завершилась (не по нажатию кнопки). Вызывается, когда игра завершилась (не по нажатию кнопки).
""" """
if self.current_running_button is not None: if self.current_running_button is not None:
self.current_running_button.setText(_("Play")) try:
icon = self.theme_manager.get_icon("play") self.current_running_button.setText(_("Play"))
if isinstance(icon, str): icon = self.theme_manager.get_icon("play")
icon = QIcon(icon) # Convert path to QIcon if isinstance(icon, str):
elif icon is None: icon = QIcon(icon) # Convert path to QIcon
icon = QIcon() # Use empty QIcon as fallback elif icon is None:
self.current_running_button.setIcon(icon) icon = QIcon() # Use empty QIcon as fallback
self.current_running_button.setIcon(icon)
except RuntimeError:
pass
self.current_running_button = None self.current_running_button = None
self.target_exe = None self.target_exe = None
@@ -3307,13 +3313,16 @@ class MainWindow(QMainWindow):
pass pass
self.game_processes = [] self.game_processes = []
if update_button: if update_button:
update_button.setText(_("Play")) try:
icon = self.theme_manager.get_icon("play") update_button.setText(_("Play"))
if isinstance(icon, str): icon = self.theme_manager.get_icon("play")
icon = QIcon(icon) if isinstance(icon, str):
elif icon is None: icon = QIcon(icon)
icon = QIcon() elif icon is None:
update_button.setIcon(icon) icon = QIcon()
update_button.setIcon(icon)
except RuntimeError:
pass
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None: if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None:
self.checkProcessTimer.stop() self.checkProcessTimer.stop()
self.checkProcessTimer.deleteLater() self.checkProcessTimer.deleteLater()
@@ -3335,13 +3344,16 @@ class MainWindow(QMainWindow):
self.game_processes.append(process) self.game_processes.append(process)
save_last_launch(exe_name, datetime.now()) save_last_launch(exe_name, datetime.now())
if update_button: if update_button:
update_button.setText(_("Launching")) try:
icon = self.theme_manager.get_icon("stop") update_button.setText(_("Launching"))
if isinstance(icon, str): icon = self.theme_manager.get_icon("stop")
icon = QIcon(icon) if isinstance(icon, str):
elif icon is None: icon = QIcon(icon)
icon = QIcon() elif icon is None:
update_button.setIcon(icon) icon = QIcon()
update_button.setIcon(icon)
except RuntimeError:
pass
self.checkProcessTimer = QTimer(self) self.checkProcessTimer = QTimer(self)
self.checkProcessTimer.timeout.connect(self.checkTargetExe) self.checkProcessTimer.timeout.connect(self.checkTargetExe)
@@ -3398,13 +3410,16 @@ class MainWindow(QMainWindow):
pass pass
self.game_processes = [] self.game_processes = []
if update_button: if update_button:
update_button.setText(_("Play")) try:
icon = self.theme_manager.get_icon("play") update_button.setText(_("Play"))
if isinstance(icon, str): icon = self.theme_manager.get_icon("play")
icon = QIcon(icon) if isinstance(icon, str):
elif icon is None: icon = QIcon(icon)
icon = QIcon() elif icon is None:
update_button.setIcon(icon) icon = QIcon()
update_button.setIcon(icon)
except RuntimeError:
pass
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None: if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None:
self.checkProcessTimer.stop() self.checkProcessTimer.stop()
self.checkProcessTimer.deleteLater() self.checkProcessTimer.deleteLater()
@@ -3426,13 +3441,16 @@ class MainWindow(QMainWindow):
self.game_processes.append(process) self.game_processes.append(process)
save_last_launch(exe_name, datetime.now()) save_last_launch(exe_name, datetime.now())
if update_button: if update_button:
update_button.setText(_("Launching")) try:
icon = self.theme_manager.get_icon("stop") update_button.setText(_("Launching"))
if isinstance(icon, str): icon = self.theme_manager.get_icon("stop")
icon = QIcon(icon) if isinstance(icon, str):
elif icon is None: icon = QIcon(icon)
icon = QIcon() elif icon is None:
update_button.setIcon(icon) icon = QIcon()
update_button.setIcon(icon)
except RuntimeError:
pass
self.checkProcessTimer = QTimer(self) self.checkProcessTimer = QTimer(self)
self.checkProcessTimer.timeout.connect(self.checkTargetExe) 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): if not self.current_input_widget or not isinstance(self.current_input_widget, QLineEdit):
return return
# Просто устанавливаем курсор на нужную позицию без выделения try:
self.current_input_widget.setCursorPosition(self.current_input_widget.cursorPosition()) # Просто устанавливаем курсор на нужную позицию без выделения
self.current_input_widget.setCursorPosition(self.current_input_widget.cursorPosition())
except RuntimeError:
self.current_input_widget = None
def initUI(self): def initUI(self):
layout = QVBoxLayout() layout = QVBoxLayout()
@@ -290,31 +293,43 @@ class VirtualKeyboard(QFrame):
def up_key(self): def up_key(self):
"""Перемещает курсор в QLineEdit вверх/в начало, если клавиатура видима""" """Перемещает курсор в QLineEdit вверх/в начало, если клавиатура видима"""
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit): if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
self.current_input_widget.setCursorPosition(0) try:
self.current_input_widget.setFocus() self.current_input_widget.setCursorPosition(0)
self.current_input_widget.setFocus()
except RuntimeError:
self.current_input_widget = None
def down_key(self): def down_key(self):
"""Перемещает курсор в QLineEdit вниз/в конец, если клавиатура видима""" """Перемещает курсор в QLineEdit вниз/в конец, если клавиатура видима"""
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit): if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
self.current_input_widget.setCursorPosition(len(self.current_input_widget.text())) try:
self.current_input_widget.setFocus() 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): def left_key(self):
"""Перемещает курсор в QLineEdit влево, если клавиатура видима""" """Перемещает курсор в QLineEdit влево, если клавиатура видима"""
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit): if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
pos = self.current_input_widget.cursorPosition() try:
if pos > 0: pos = self.current_input_widget.cursorPosition()
self.current_input_widget.setCursorPosition(pos - 1) if pos > 0:
self.current_input_widget.setFocus() self.current_input_widget.setCursorPosition(pos - 1)
self.current_input_widget.setFocus()
except RuntimeError:
self.current_input_widget = None
def right_key(self): def right_key(self):
"""Перемещает курсор в QLineEdit вправо, если клавиатура видима""" """Перемещает курсор в QLineEdit вправо, если клавиатура видима"""
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit): if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
pos = self.current_input_widget.cursorPosition() try:
text_len = len(self.current_input_widget.text()) pos = self.current_input_widget.cursorPosition()
if pos < text_len: text_len = len(self.current_input_widget.text())
self.current_input_widget.setCursorPosition(pos + 1) if pos < text_len:
self.current_input_widget.setFocus() self.current_input_widget.setCursorPosition(pos + 1)
self.current_input_widget.setFocus()
except RuntimeError:
self.current_input_widget = None
def move_focus_up(self): def move_focus_up(self):
"""Перемещает фокус по кнопкам клавиатуры вверх с фиксированной скоростью""" """Перемещает фокус по кнопкам клавиатуры вверх с фиксированной скоростью"""
@@ -370,35 +385,41 @@ class VirtualKeyboard(QFrame):
self.on_shift_click(not self.shift_pressed) self.on_shift_click(not self.shift_pressed)
self.highlight_cursor_position() self.highlight_cursor_position()
elif self.current_input_widget is not None: elif self.current_input_widget is not None:
# Сохраняем текущую кнопку с фокусом try:
focused_button = self.focusWidget() # Сохраняем текущую кнопку с фокусом
key_to_restore = None focused_button = self.focusWidget()
if isinstance(focused_button, QPushButton) and focused_button in self.buttons.values(): key_to_restore = None
key_to_restore = next((k for k, btn in self.buttons.items() if btn == focused_button), 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 key = "&" if key == "&&" else key
cursor_pos = self.current_input_widget.cursorPosition() cursor_pos = self.current_input_widget.cursorPosition()
text = self.current_input_widget.text() text = self.current_input_widget.text()
new_text = text[:cursor_pos] + key + text[cursor_pos:] new_text = text[:cursor_pos] + key + text[cursor_pos:]
self.current_input_widget.setText(new_text) self.current_input_widget.setText(new_text)
self.current_input_widget.setCursorPosition(cursor_pos + len(key)) self.current_input_widget.setCursorPosition(cursor_pos + len(key))
self.keyPressed.emit(key) self.keyPressed.emit(key)
self.highlight_cursor_position() self.highlight_cursor_position()
# Если был нажат SHIFT, но не CapsLock, отключаем его после ввода символа # Если был нажат SHIFT, но не CapsLock, отключаем его после ввода символа
if self.shift_pressed and not self.caps_lock: if self.shift_pressed and not self.caps_lock:
self.shift_pressed = False self.shift_pressed = False
self.update_keyboard() self.update_keyboard()
if key_to_restore and key_to_restore in self.buttons: if key_to_restore and key_to_restore in self.buttons:
self.buttons[key_to_restore].setFocus() self.buttons[key_to_restore].setFocus()
except RuntimeError:
self.current_input_widget = None
def on_tab_click(self): def on_tab_click(self):
if self.current_input_widget is not None: if self.current_input_widget is not None:
self.current_input_widget.insert('\t') try:
self.keyPressed.emit('Tab') self.current_input_widget.insert('\t')
if self.current_input_widget: self.keyPressed.emit('Tab')
self.current_input_widget.setFocus() if self.current_input_widget:
self.highlight_cursor_position() self.current_input_widget.setFocus()
self.highlight_cursor_position()
except RuntimeError:
self.current_input_widget = None
def on_caps_click(self): def on_caps_click(self):
"""Включаем/выключаем CapsLock""" """Включаем/выключаем CapsLock"""
@@ -417,15 +438,18 @@ class VirtualKeyboard(QFrame):
def on_backspace_click(self): def on_backspace_click(self):
"""Обработка одного нажатия Backspace""" """Обработка одного нажатия Backspace"""
if self.current_input_widget is not None: if self.current_input_widget is not None:
cursor_pos = self.current_input_widget.cursorPosition() try:
text = self.current_input_widget.text() cursor_pos = self.current_input_widget.cursorPosition()
text = self.current_input_widget.text()
if cursor_pos > 0: if cursor_pos > 0:
new_text = text[:cursor_pos - 1] + text[cursor_pos:] new_text = text[:cursor_pos - 1] + text[cursor_pos:]
self.current_input_widget.setText(new_text) self.current_input_widget.setText(new_text)
self.current_input_widget.setCursorPosition(cursor_pos - 1) self.current_input_widget.setCursorPosition(cursor_pos - 1)
self.keyPressed.emit('Backspace') self.keyPressed.emit('Backspace')
self.highlight_cursor_position() self.highlight_cursor_position()
except RuntimeError:
self.current_input_widget = None
def on_backspace_pressed(self): def on_backspace_pressed(self):
"""Обработка зажатого Backspace""" """Обработка зажатого Backspace"""
@@ -449,15 +473,21 @@ class VirtualKeyboard(QFrame):
# TODO: тут подумать, как обрабатывать нажатие. # TODO: тут подумать, как обрабатывать нажатие.
# Пока болванка перехода на новую строку, в QlineEdit работает как нажатие пробела # Пока болванка перехода на новую строку, в QlineEdit работает как нажатие пробела
if self.current_input_widget is not None: if self.current_input_widget is not None:
self.current_input_widget.insert('\n') try:
self.keyPressed.emit('Enter') self.current_input_widget.insert('\n')
self.keyPressed.emit('Enter')
except RuntimeError:
self.current_input_widget = None
def on_clear_click(self): def on_clear_click(self):
"""Чистим строку от введённого текста""" """Чистим строку от введённого текста"""
if self.current_input_widget is not None: if self.current_input_widget is not None:
self.current_input_widget.clear() try:
self.keyPressed.emit('Clear') self.current_input_widget.clear()
self.highlight_cursor_position() self.keyPressed.emit('Clear')
self.highlight_cursor_position()
except RuntimeError:
self.current_input_widget = None
def on_lang_click(self): def on_lang_click(self):
"""Переключение раскладки""" """Переключение раскладки"""
@@ -483,8 +513,11 @@ class VirtualKeyboard(QFrame):
def show_for_widget(self, widget): def show_for_widget(self, widget):
self.current_input_widget = widget self.current_input_widget = widget
if widget: if widget:
widget.setFocus() try:
self.highlight_cursor_position() widget.setFocus()
self.highlight_cursor_position()
except RuntimeError:
self.current_input_widget = None
# Позиционирование клавиатуры внизу родительского виджета # Позиционирование клавиатуры внизу родительского виджета
if self._parent and isinstance(self._parent, QWidget): if self._parent and isinstance(self._parent, QWidget):