mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Sync from development - prepare for v0.2.0.6
This commit is contained in:
@@ -904,20 +904,22 @@ class SettingsDialog(QDialog):
|
||||
if best_proton:
|
||||
resolved_install_path = str(best_proton['path'])
|
||||
resolved_install_version = best_proton['name']
|
||||
self.config_handler.set("proton_path", resolved_install_path)
|
||||
self.config_handler.set("proton_version", resolved_install_version)
|
||||
else:
|
||||
resolved_install_path = "auto"
|
||||
resolved_install_version = "auto"
|
||||
except:
|
||||
resolved_install_path = "auto"
|
||||
resolved_install_version = "auto"
|
||||
# No Proton found - don't write anything, let engine auto-detect
|
||||
logger.warning("Auto Proton selection failed: No Proton versions found")
|
||||
# Don't modify existing config values
|
||||
except Exception as e:
|
||||
# Exception during detection - log it and don't write anything
|
||||
logger.error(f"Auto Proton selection failed with exception: {e}", exc_info=True)
|
||||
# Don't modify existing config values
|
||||
else:
|
||||
# User selected specific Proton version
|
||||
resolved_install_path = selected_install_proton_path
|
||||
# Extract version from dropdown text
|
||||
resolved_install_version = self.install_proton_dropdown.currentText()
|
||||
|
||||
self.config_handler.set("proton_path", resolved_install_path)
|
||||
self.config_handler.set("proton_version", resolved_install_version)
|
||||
self.config_handler.set("proton_path", resolved_install_path)
|
||||
self.config_handler.set("proton_version", resolved_install_version)
|
||||
|
||||
# Save Game Proton selection
|
||||
selected_game_proton_path = self.game_proton_dropdown.currentData()
|
||||
@@ -1038,6 +1040,10 @@ class JackifyMainWindow(QMainWindow):
|
||||
self._details_extra_height = 360
|
||||
self._initial_show_adjusted = False
|
||||
|
||||
# Track open dialogs to prevent duplicates
|
||||
self._settings_dialog = None
|
||||
self._about_dialog = None
|
||||
|
||||
# Ensure GNOME/Ubuntu exposes full set of window controls (avoid hidden buttons)
|
||||
self._apply_standard_window_flags()
|
||||
try:
|
||||
@@ -1606,23 +1612,74 @@ class JackifyMainWindow(QMainWindow):
|
||||
event.accept()
|
||||
|
||||
def open_settings_dialog(self):
|
||||
"""Open settings dialog, preventing duplicate instances"""
|
||||
try:
|
||||
# Check if dialog already exists and is visible
|
||||
if self._settings_dialog is not None:
|
||||
try:
|
||||
if self._settings_dialog.isVisible():
|
||||
# Dialog is already open - raise it to front
|
||||
self._settings_dialog.raise_()
|
||||
self._settings_dialog.activateWindow()
|
||||
return
|
||||
else:
|
||||
# Dialog exists but is closed - clean up reference
|
||||
self._settings_dialog = None
|
||||
except RuntimeError:
|
||||
# Dialog was deleted - clean up reference
|
||||
self._settings_dialog = None
|
||||
|
||||
# Create new dialog
|
||||
dlg = SettingsDialog(self)
|
||||
self._settings_dialog = dlg
|
||||
|
||||
# Clean up reference when dialog is closed
|
||||
def on_dialog_finished():
|
||||
self._settings_dialog = None
|
||||
|
||||
dlg.finished.connect(on_dialog_finished)
|
||||
dlg.exec()
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception in open_settings_dialog: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self._settings_dialog = None
|
||||
|
||||
def open_about_dialog(self):
|
||||
"""Open about dialog, preventing duplicate instances"""
|
||||
try:
|
||||
from jackify.frontends.gui.dialogs.about_dialog import AboutDialog
|
||||
|
||||
# Check if dialog already exists and is visible
|
||||
if self._about_dialog is not None:
|
||||
try:
|
||||
if self._about_dialog.isVisible():
|
||||
# Dialog is already open - raise it to front
|
||||
self._about_dialog.raise_()
|
||||
self._about_dialog.activateWindow()
|
||||
return
|
||||
else:
|
||||
# Dialog exists but is closed - clean up reference
|
||||
self._about_dialog = None
|
||||
except RuntimeError:
|
||||
# Dialog was deleted - clean up reference
|
||||
self._about_dialog = None
|
||||
|
||||
# Create new dialog
|
||||
dlg = AboutDialog(self.system_info, self)
|
||||
self._about_dialog = dlg
|
||||
|
||||
# Clean up reference when dialog is closed
|
||||
def on_dialog_finished():
|
||||
self._about_dialog = None
|
||||
|
||||
dlg.finished.connect(on_dialog_finished)
|
||||
dlg.exec()
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception in open_about_dialog: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self._about_dialog = None
|
||||
|
||||
def _open_url(self, url: str):
|
||||
"""Open URL with clean environment to avoid AppImage library conflicts."""
|
||||
|
||||
@@ -833,6 +833,8 @@ class ModlistGalleryDialog(QDialog):
|
||||
self._validation_update_timer = None # Timer for background validation updates
|
||||
|
||||
self._setup_ui()
|
||||
# Disable filter controls during initial load to prevent race conditions
|
||||
self._set_filter_controls_enabled(False)
|
||||
# Lazy load - fetch modlists when dialog is shown
|
||||
|
||||
def _apply_initial_size(self):
|
||||
@@ -1168,6 +1170,9 @@ class ModlistGalleryDialog(QDialog):
|
||||
# Reconnect filter handler
|
||||
self.game_combo.currentIndexChanged.connect(self._apply_filters)
|
||||
|
||||
# Enable filter controls now that data is loaded
|
||||
self._set_filter_controls_enabled(True)
|
||||
|
||||
# Apply filters (will show all modlists for selected game initially)
|
||||
self._apply_filters()
|
||||
|
||||
@@ -1389,8 +1394,23 @@ class ModlistGalleryDialog(QDialog):
|
||||
self._filter_mods_list() # Refresh mod list based on NSFW state
|
||||
self._apply_filters() # Apply all filters
|
||||
|
||||
def _set_filter_controls_enabled(self, enabled: bool):
|
||||
"""Enable or disable all filter controls"""
|
||||
self.search_box.setEnabled(enabled)
|
||||
self.game_combo.setEnabled(enabled)
|
||||
self.show_official_only.setEnabled(enabled)
|
||||
self.show_nsfw.setEnabled(enabled)
|
||||
self.hide_unavailable.setEnabled(enabled)
|
||||
self.tags_list.setEnabled(enabled)
|
||||
self.mod_search.setEnabled(enabled)
|
||||
self.mods_list.setEnabled(enabled)
|
||||
|
||||
def _apply_filters(self):
|
||||
"""Apply current filters to modlist display"""
|
||||
# CRITICAL: Guard against race condition - don't filter if modlists aren't loaded yet
|
||||
if not self.all_modlists:
|
||||
return
|
||||
|
||||
filtered = self.all_modlists
|
||||
|
||||
# Search filter
|
||||
@@ -1480,15 +1500,25 @@ class ModlistGalleryDialog(QDialog):
|
||||
|
||||
def _update_grid(self):
|
||||
"""Update grid by removing all cards and re-adding only visible ones"""
|
||||
# CRITICAL: Guard against race condition - don't update if cards aren't ready yet
|
||||
if not self.all_cards:
|
||||
return
|
||||
|
||||
# Disable updates during grid update
|
||||
self.grid_widget.setUpdatesEnabled(False)
|
||||
|
||||
try:
|
||||
# Remove all cards from layout
|
||||
# CRITICAL FIX: Properly remove widgets to prevent overlapping and orphaned windows
|
||||
# We need to explicitly remove widgets from the layout before taking items
|
||||
# to ensure they're fully cleaned up, but we don't setParent(None) because
|
||||
# widgets are immediately re-added to the grid (Qt will reparent them).
|
||||
while self.grid_layout.count():
|
||||
item = self.grid_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().setParent(None)
|
||||
widget = item.widget() if item else None
|
||||
if widget:
|
||||
# Explicitly remove widget from layout to prevent overlapping
|
||||
self.grid_layout.removeWidget(widget)
|
||||
del item
|
||||
|
||||
# Calculate number of columns based on available width
|
||||
@@ -1528,6 +1558,16 @@ class ModlistGalleryDialog(QDialog):
|
||||
|
||||
card = self.all_cards.get(modlist.machineURL)
|
||||
if card:
|
||||
# Ensure widget is not already in the layout (prevent overlapping)
|
||||
# If it is, remove it first (shouldn't happen after takeAt, but safety check)
|
||||
if card.parent() == self.grid_widget:
|
||||
# Widget is already a child of grid_widget, check if it's in layout
|
||||
for i in range(self.grid_layout.count()):
|
||||
item = self.grid_layout.itemAt(i)
|
||||
if item and item.widget() == card:
|
||||
# Already in layout, remove it first
|
||||
self.grid_layout.removeWidget(card)
|
||||
break
|
||||
self.grid_layout.addWidget(card, row, col)
|
||||
|
||||
# Set column stretch - don't stretch card columns, but add a spacer column
|
||||
|
||||
@@ -20,6 +20,13 @@ from PySide6.QtGui import QFont
|
||||
from jackify.shared.progress_models import FileProgress, OperationType
|
||||
from ..shared_theme import JACKIFY_COLOR_BLUE
|
||||
|
||||
def _debug_log(message):
|
||||
"""Log message only if debug mode is enabled"""
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
config_handler = ConfigHandler()
|
||||
if config_handler.get('debug_mode', False):
|
||||
print(message)
|
||||
|
||||
|
||||
class SummaryProgressWidget(QWidget):
|
||||
"""Widget showing summary progress for phases like Installing."""
|
||||
@@ -484,7 +491,28 @@ class FileProgressList(QWidget):
|
||||
return
|
||||
|
||||
# Widget doesn't exist - create it (only clear when creating new widget)
|
||||
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
|
||||
_debug_log(f"[WIDGET_FIX] About to clear list_widget for summary widget - count={self.list_widget.count()}")
|
||||
for i in range(self.list_widget.count()):
|
||||
item = self.list_widget.item(i)
|
||||
if item:
|
||||
widget = self.list_widget.itemWidget(item)
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] Removing widget before clear (summary) - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
self.list_widget.removeItemWidget(item)
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget() before clear()!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
self.list_widget.clear()
|
||||
# Check widgets in _file_items dict after clear
|
||||
for key, widget in list(self._file_items.items()):
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] Widget in _file_items after clear - key={key}, widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Widget in _file_items is a top-level window after clear()! This is the bug!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
self._file_items.clear()
|
||||
|
||||
# Create new summary widget
|
||||
@@ -510,7 +538,22 @@ class FileProgressList(QWidget):
|
||||
for i in range(self.list_widget.count()):
|
||||
item = self.list_widget.item(i)
|
||||
if item and item.data(Qt.UserRole) == "__summary__":
|
||||
# CRITICAL FIX: Call removeItemWidget() before takeItem() to prevent orphaned widgets
|
||||
widget = self.list_widget.itemWidget(item)
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] Removing summary widget - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
self.list_widget.removeItemWidget(item)
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Summary widget became top-level window after removeItemWidget()!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
self.list_widget.takeItem(i)
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] After takeItem (summary) - widget.parent()={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Summary widget is still a top-level window after takeItem()!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
break
|
||||
self._summary_widget = None
|
||||
else:
|
||||
@@ -522,7 +565,22 @@ class FileProgressList(QWidget):
|
||||
for i in range(self.list_widget.count()):
|
||||
item = self.list_widget.item(i)
|
||||
if item and item.data(Qt.UserRole) == "__transition__":
|
||||
# CRITICAL FIX: Call removeItemWidget() before takeItem() to prevent orphaned widgets
|
||||
widget = self.list_widget.itemWidget(item)
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] Removing transition label - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
self.list_widget.removeItemWidget(item)
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Transition label became top-level window after removeItemWidget()!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
self.list_widget.takeItem(i)
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] After takeItem (transition) - widget.parent()={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Transition label is still a top-level window after takeItem()!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
break
|
||||
self._transition_label = None
|
||||
|
||||
@@ -533,7 +591,28 @@ class FileProgressList(QWidget):
|
||||
self._show_transition_message(current_phase)
|
||||
else:
|
||||
# Show empty state but keep header stable
|
||||
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
|
||||
_debug_log(f"[WIDGET_FIX] About to clear list_widget (empty state) - count={self.list_widget.count()}")
|
||||
for i in range(self.list_widget.count()):
|
||||
item = self.list_widget.item(i)
|
||||
if item:
|
||||
widget = self.list_widget.itemWidget(item)
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] Removing widget before clear (empty) - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
self.list_widget.removeItemWidget(item)
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget() before clear()!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
self.list_widget.clear()
|
||||
# Check widgets in _file_items dict after clear
|
||||
for key, widget in list(self._file_items.items()):
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] Widget in _file_items after clear (empty) - key={key}, widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Widget in _file_items is a top-level window after clear()! This is the bug!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
self._file_items.clear()
|
||||
|
||||
# Update last phase tracker
|
||||
@@ -579,7 +658,24 @@ class FileProgressList(QWidget):
|
||||
for i in range(self.list_widget.count()):
|
||||
item = self.list_widget.item(i)
|
||||
if item and item.data(Qt.UserRole) == item_key:
|
||||
# CRITICAL FIX: Call removeItemWidget() before takeItem() to prevent orphaned widgets
|
||||
widget = self.list_widget.itemWidget(item)
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] Removing widget for item_key={item_key} - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
self.list_widget.removeItemWidget(item)
|
||||
# Check if widget became orphaned after removal
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget()! widget={widget}")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
self.list_widget.takeItem(i)
|
||||
# Final check after takeItem
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] After takeItem - widget.parent()={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Widget is still a top-level window after takeItem()! This is the bug!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
break
|
||||
del self._file_items[item_key]
|
||||
|
||||
@@ -638,7 +734,28 @@ class FileProgressList(QWidget):
|
||||
|
||||
def _show_transition_message(self, new_phase: str):
|
||||
"""Show a brief 'Preparing...' message during phase transitions."""
|
||||
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
|
||||
_debug_log(f"[WIDGET_FIX] About to clear list_widget (transition) - count={self.list_widget.count()}")
|
||||
for i in range(self.list_widget.count()):
|
||||
item = self.list_widget.item(i)
|
||||
if item:
|
||||
widget = self.list_widget.itemWidget(item)
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] Removing widget before clear (transition) - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
self.list_widget.removeItemWidget(item)
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget() before clear()!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
self.list_widget.clear()
|
||||
# Check widgets in _file_items dict after clear
|
||||
for key, widget in list(self._file_items.items()):
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] Widget in _file_items after clear (transition) - key={key}, widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Widget in _file_items is a top-level window after clear()! This is the bug!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
self._file_items.clear()
|
||||
|
||||
# Header removed - tab label provides context
|
||||
@@ -663,9 +780,42 @@ class FileProgressList(QWidget):
|
||||
|
||||
def clear(self):
|
||||
"""Clear all file items."""
|
||||
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
|
||||
_debug_log(f"[WIDGET_FIX] clear() called - count={self.list_widget.count()}")
|
||||
for i in range(self.list_widget.count()):
|
||||
item = self.list_widget.item(i)
|
||||
if item:
|
||||
widget = self.list_widget.itemWidget(item)
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] Removing widget before clear() - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
self.list_widget.removeItemWidget(item)
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget() before clear()!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
self.list_widget.clear()
|
||||
# Check widgets in _file_items dict after clear
|
||||
for key, widget in list(self._file_items.items()):
|
||||
if widget:
|
||||
_debug_log(f"[WIDGET_FIX] Widget in _file_items after clear() - key={key}, widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
|
||||
if widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Widget in _file_items is a top-level window after clear()! This is the bug!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
self._file_items.clear()
|
||||
if self._summary_widget:
|
||||
_debug_log(f"[WIDGET_FIX] Clearing summary_widget - widget={self._summary_widget}, parent={self._summary_widget.parent()}, isWindow()={self._summary_widget.isWindow()}")
|
||||
if self._summary_widget.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Summary widget is a top-level window in clear()!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
self._summary_widget = None
|
||||
if self._transition_label:
|
||||
_debug_log(f"[WIDGET_FIX] Clearing transition_label - widget={self._transition_label}, parent={self._transition_label.parent()}, isWindow()={self._transition_label.isWindow()}")
|
||||
if self._transition_label.isWindow():
|
||||
print(f"[WIDGET_FIX] ERROR: Transition label is a top-level window in clear()!")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
self._transition_label = None
|
||||
self._last_phase = None
|
||||
# Header removed - tab label provides context
|
||||
|
||||
Reference in New Issue
Block a user