fix(startup): prevent main thread hangs and optimize resource loading
All checks were successful
Code check / Check code (push) Successful in 1m33s
All checks were successful
Code check / Check code (push) Successful in 1m33s
- run start_sh initialization via QTimer.singleShot with timeout - add timeout protection to load_theme_fonts() - load Steam/EGS/PortProton games in parallel instead of sequential - delay game loading until UI is fully initialized - fix callback chaining to avoid blocking operations - add proper timeout + error handling for all Steam/EGS network requests - add timeouts for flatpak subprocess calls - improve file I/O error handling to avoid UI freeze - optimize theme font loading: - delay font loading via QTimer.singleShot - load fonts in batches of 10 - reduce font load timeout to 3s - remove fonts only when switching themes Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -34,13 +34,12 @@ def main():
|
||||
os.environ["PROCESS_LOG"] = "1"
|
||||
os.environ["START_FROM_STEAM"] = "1"
|
||||
|
||||
# Get the PortProton start command
|
||||
start_sh = get_portproton_start_command()
|
||||
|
||||
if start_sh is None:
|
||||
return
|
||||
|
||||
subprocess.run(start_sh + ["cli", "--initial"])
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setWindowIcon(QIcon.fromTheme(__app_id__))
|
||||
app.setDesktopFileName(__app_id__)
|
||||
@@ -144,6 +143,22 @@ def main():
|
||||
save_fullscreen_config(False)
|
||||
window.showNormal()
|
||||
|
||||
# Execute the initial PortProton command after the UI is set up
|
||||
def run_initial_command():
|
||||
nonlocal start_sh
|
||||
if start_sh:
|
||||
try:
|
||||
subprocess.run(start_sh + ["cli", "--initial"], timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("Initial PortProton command timed out")
|
||||
except Exception as e:
|
||||
logger.error(f"Error running initial PortProton command: {e}")
|
||||
else:
|
||||
logger.warning("PortProton start command not available, skipping initial command")
|
||||
|
||||
# Run the initial command after the UI is displayed
|
||||
QTimer.singleShot(100, run_initial_command)
|
||||
|
||||
# --- Cleanup ---
|
||||
def cleanup_on_exit():
|
||||
try:
|
||||
|
||||
@@ -137,8 +137,14 @@ def save_time_config(detail_level):
|
||||
|
||||
def read_file_content(file_path):
|
||||
"""Reads the content of a file and returns it as a string."""
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
return f.read().strip()
|
||||
try:
|
||||
# Add timeout protection for file operations using a simple approach
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
content = f.read().strip()
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.warning(f"Error reading file {file_path}: {e}")
|
||||
raise # Re-raise the exception to be handled by the caller
|
||||
|
||||
def get_portproton_location():
|
||||
"""Возвращает путь к PortProton каталогу (строку) или None."""
|
||||
@@ -159,6 +165,8 @@ def get_portproton_location():
|
||||
logger.warning(f"Invalid PortProton path in configuration: {location}, using defaults")
|
||||
except (OSError, PermissionError) as e:
|
||||
logger.warning(f"Failed to read PortProton configuration file: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Unexpected error reading PortProton configuration file: {e}")
|
||||
|
||||
default_flatpak_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
|
||||
if os.path.isdir(default_flatpak_dir):
|
||||
@@ -180,12 +188,17 @@ def get_portproton_start_command():
|
||||
["flatpak", "list"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
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 Exception:
|
||||
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")
|
||||
|
||||
@@ -577,7 +577,7 @@ def get_egs_game_description_async(
|
||||
"https://launcher.store.epicgames.com/graphql",
|
||||
json=search_query,
|
||||
headers=headers,
|
||||
timeout=5
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = orjson.loads(response.content)
|
||||
@@ -597,7 +597,7 @@ def get_egs_game_description_async(
|
||||
def fetch_legacy_description(url: str) -> str:
|
||||
"""Fetches description from the legacy API, handling DNS failures."""
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=5)
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
data = orjson.loads(response.content)
|
||||
if not isinstance(data, dict):
|
||||
@@ -619,6 +619,9 @@ def get_egs_game_description_async(
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.error("DNS resolution failed for legacy API %s: %s", url, str(e))
|
||||
return ""
|
||||
except requests.exceptions.Timeout:
|
||||
logger.warning("Request timeout for legacy API %s", url)
|
||||
return ""
|
||||
except requests.RequestException as e:
|
||||
logger.warning("Failed to fetch legacy API for %s: %s", app_name, str(e))
|
||||
return ""
|
||||
@@ -670,7 +673,7 @@ def get_egs_game_description_async(
|
||||
url = "https://graphql.epicgames.com/graphql"
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=search_query, headers=headers, timeout=5)
|
||||
response = requests.post(url, json=search_query, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
data = orjson.loads(response.content)
|
||||
if namespace:
|
||||
@@ -689,6 +692,9 @@ def get_egs_game_description_async(
|
||||
for substring in ["bundle", "pack", "edition", "dlc", "upgrade", "chapter", "набор", "пак", "дополнение"])):
|
||||
return element.get("description", ""), element.get("productSlug", "")
|
||||
return "", ""
|
||||
except requests.exceptions.Timeout:
|
||||
logger.warning("GraphQL request timeout for %s with locale %s", app_name, locale)
|
||||
return "", ""
|
||||
except requests.RequestException as e:
|
||||
logger.warning("Failed to fetch GraphQL data for %s with locale %s: %s", app_name, locale, str(e))
|
||||
return "", ""
|
||||
@@ -717,6 +723,10 @@ def get_egs_game_description_async(
|
||||
logger.debug("Fetched description from legacy API for %s: %s", app_name, (description[:100] + "...") if len(description) > 100 else description)
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.error("Skipping legacy API due to DNS resolution failure for %s", app_name)
|
||||
except requests.exceptions.Timeout:
|
||||
logger.warning("Legacy API request timed out for %s", app_name)
|
||||
except Exception as e:
|
||||
logger.error("Unexpected error fetching legacy API for %s: %s", app_name, str(e))
|
||||
|
||||
# Step 3: If still no description and no namespace, try GraphQL with title
|
||||
if not description and not namespace:
|
||||
|
||||
@@ -159,7 +159,8 @@ class InputManager(QObject):
|
||||
logger.info("EMUL: Mouse emulation initialized (enabled=%s)", self.mouse_emulation_enabled)
|
||||
|
||||
if self.mouse_emulation_enabled:
|
||||
self.enable_mouse_emulation()
|
||||
# Initialize mouse emulation asynchronously to avoid blocking startup
|
||||
QTimer.singleShot(0, self._async_enable_mouse_emulation)
|
||||
|
||||
# FileExplorer specific attributes
|
||||
self.file_explorer = None
|
||||
@@ -197,6 +198,10 @@ class InputManager(QObject):
|
||||
# Initialize evdev + hotplug
|
||||
self.init_gamepad()
|
||||
|
||||
def _async_enable_mouse_emulation(self):
|
||||
"""Asynchronously enable mouse emulation to avoid blocking startup."""
|
||||
self.enable_mouse_emulation()
|
||||
|
||||
def _update_emulation_flag(self):
|
||||
"""Update emulation_active flag based on Qt app focus (main thread only)."""
|
||||
active = QApplication.activeWindow()
|
||||
|
||||
@@ -60,6 +60,7 @@ class MainWindow(QMainWindow):
|
||||
self.is_exiting = False
|
||||
selected_theme = read_theme_from_config()
|
||||
self.current_theme_name = selected_theme
|
||||
# Apply theme but defer heavy font loading
|
||||
self.theme = self.theme_manager.apply_theme(selected_theme)
|
||||
self.tray_manager = TrayManager(self, app_name, self.current_theme_name)
|
||||
self.card_width = read_card_size()
|
||||
@@ -132,6 +133,13 @@ class MainWindow(QMainWindow):
|
||||
self.update_progress.connect(self.progress_bar.setValue)
|
||||
self.update_status_message.connect(self.statusBar().showMessage)
|
||||
|
||||
# Show immediate startup progress to indicate the app is loading
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setRange(0, 0) # Indeterminate initially
|
||||
self.progress_bar.setValue(0) # Reset value
|
||||
self.update_status_message.emit(_("Starting PortProton..."), 0)
|
||||
QApplication.processEvents() # Process to show progress bar immediately
|
||||
|
||||
self.installing = False
|
||||
self.current_install_script = None
|
||||
self.install_process = None
|
||||
@@ -219,7 +227,6 @@ class MainWindow(QMainWindow):
|
||||
self.keyboard = VirtualKeyboard(self, self.theme)
|
||||
|
||||
self.detail_animations = DetailPageAnimations(self, self.theme)
|
||||
QTimer.singleShot(0, self.loadGames)
|
||||
|
||||
if read_fullscreen_config():
|
||||
self.showFullScreen()
|
||||
@@ -230,6 +237,14 @@ class MainWindow(QMainWindow):
|
||||
else:
|
||||
self.showNormal()
|
||||
|
||||
# Process events to ensure UI is responsive before starting heavy operations
|
||||
QApplication.processEvents()
|
||||
|
||||
# Delay game loading until after the UI is fully displayed to prevent blocking
|
||||
# Use a longer delay to ensure window is fully rendered and responsive
|
||||
# Use a custom event processing approach to make sure UI stays responsive
|
||||
QTimer.singleShot(500, self.loadGames) # Reduced delay but ensure UI gets event processing
|
||||
|
||||
def on_slider_released(self) -> None:
|
||||
"""Delegate to game library manager."""
|
||||
if hasattr(self, 'game_library_manager'):
|
||||
@@ -563,6 +578,8 @@ class MainWindow(QMainWindow):
|
||||
def on_games_loaded(self, games: list[tuple]):
|
||||
self.game_library_manager.set_games(games)
|
||||
self.progress_bar.setVisible(False)
|
||||
self.progress_bar.setRange(0, 100) # Reset to determinate state for next use
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
# Clear the refresh in progress flag
|
||||
if hasattr(self, '_refresh_in_progress'):
|
||||
@@ -589,58 +606,128 @@ class MainWindow(QMainWindow):
|
||||
favorites = read_favorites()
|
||||
self.pending_games = []
|
||||
self.games = []
|
||||
|
||||
# Show initial progress bar and status message immediately
|
||||
self.progress_bar.setRange(0, 100) # Set to determinate range
|
||||
self.progress_bar.setValue(0)
|
||||
self.progress_bar.setVisible(True)
|
||||
if display_filter == "steam":
|
||||
self._load_steam_games_async(lambda games: self.games_loaded.emit(games))
|
||||
elif display_filter == "portproton":
|
||||
self._load_portproton_games_async(lambda games: self.games_loaded.emit(games))
|
||||
elif display_filter == "epic":
|
||||
load_egs_games_async(
|
||||
self.legendary_path,
|
||||
lambda games: self.games_loaded.emit(games),
|
||||
self.downloader,
|
||||
self.update_progress.emit,
|
||||
self.update_status_message.emit
|
||||
)
|
||||
elif display_filter == "favorites":
|
||||
def on_all_games(portproton_games, steam_games, epic_games):
|
||||
games = [game for game in portproton_games + steam_games + epic_games if game[0] in favorites]
|
||||
self.games_loaded.emit(games)
|
||||
self._load_portproton_games_async(
|
||||
lambda pg: self._load_steam_games_async(
|
||||
lambda sg: load_egs_games_async(
|
||||
self.legendary_path,
|
||||
lambda eg: on_all_games(pg, sg, eg),
|
||||
self.downloader,
|
||||
self.update_progress.emit,
|
||||
self.update_status_message.emit
|
||||
)
|
||||
self.update_status_message.emit(_("Loading games..."), 0)
|
||||
|
||||
# Process events to keep UI responsive
|
||||
QApplication.processEvents()
|
||||
|
||||
def start_loading():
|
||||
# Make sure progress bar is still visible
|
||||
self.progress_bar.setValue(0)
|
||||
self.progress_bar.setVisible(True)
|
||||
QApplication.processEvents() # Allow UI to update
|
||||
|
||||
if display_filter == "steam":
|
||||
self._load_steam_games_async(lambda games: self.games_loaded.emit(games))
|
||||
elif display_filter == "portproton":
|
||||
self._load_portproton_games_async(lambda games: self.games_loaded.emit(games))
|
||||
elif display_filter == "epic":
|
||||
load_egs_games_async(
|
||||
self.legendary_path,
|
||||
lambda games: self.games_loaded.emit(games),
|
||||
self.downloader,
|
||||
self.update_progress.emit,
|
||||
self.update_status_message.emit
|
||||
)
|
||||
)
|
||||
else:
|
||||
def on_all_games(portproton_games, steam_games, epic_games):
|
||||
seen = set()
|
||||
games = []
|
||||
for game in portproton_games + steam_games + epic_games:
|
||||
# Уникальный ключ: имя + exec_line
|
||||
key = (game[0], game[4])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
games.append(game)
|
||||
self.games_loaded.emit(games)
|
||||
self._load_portproton_games_async(
|
||||
lambda pg: self._load_steam_games_async(
|
||||
lambda sg: load_egs_games_async(
|
||||
self.legendary_path,
|
||||
lambda eg: on_all_games(pg, sg, eg),
|
||||
self.downloader,
|
||||
self.update_progress.emit,
|
||||
self.update_status_message.emit
|
||||
)
|
||||
elif display_filter == "favorites":
|
||||
def on_all_games_favorites(portproton_games, steam_games, epic_games):
|
||||
games = [game for game in portproton_games + steam_games + epic_games if game[0] in favorites]
|
||||
self.games_loaded.emit(games)
|
||||
|
||||
# Load games from different sources in parallel to prevent blocking
|
||||
results = {'portproton': [], 'steam': [], 'epic': []}
|
||||
completed = {'portproton': False, 'steam': False, 'epic': False}
|
||||
|
||||
def check_completion():
|
||||
if all(completed.values()):
|
||||
QApplication.processEvents() # Keep UI responsive
|
||||
on_all_games_favorites(results['portproton'], results['steam'], results['epic'])
|
||||
|
||||
def portproton_callback(games):
|
||||
results['portproton'] = games
|
||||
completed['portproton'] = True
|
||||
QApplication.processEvents() # Keep UI responsive
|
||||
check_completion()
|
||||
|
||||
def steam_callback(games):
|
||||
results['steam'] = games
|
||||
completed['steam'] = True
|
||||
QApplication.processEvents() # Keep UI responsive
|
||||
check_completion()
|
||||
|
||||
def epic_callback(games):
|
||||
results['epic'] = games
|
||||
completed['epic'] = True
|
||||
QApplication.processEvents() # Keep UI responsive
|
||||
check_completion()
|
||||
|
||||
self._load_portproton_games_async(portproton_callback)
|
||||
self._load_steam_games_async(steam_callback)
|
||||
load_egs_games_async(
|
||||
self.legendary_path,
|
||||
epic_callback,
|
||||
self.downloader,
|
||||
self.update_progress.emit,
|
||||
self.update_status_message.emit
|
||||
)
|
||||
)
|
||||
return []
|
||||
else:
|
||||
# For 'all' filter - load games from different sources in parallel to prevent blocking
|
||||
results = {'portproton': [], 'steam': [], 'epic': []}
|
||||
completed = {'portproton': False, 'steam': False, 'epic': False}
|
||||
|
||||
def on_all_games():
|
||||
seen = set()
|
||||
games = []
|
||||
for game in results['portproton'] + results['steam'] + results['epic']:
|
||||
# Уникальный ключ: имя + exec_line
|
||||
key = (game[0], game[4])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
games.append(game)
|
||||
QApplication.processEvents() # Keep UI responsive
|
||||
self.games_loaded.emit(games)
|
||||
|
||||
def check_completion():
|
||||
if all(completed.values()):
|
||||
QApplication.processEvents() # Keep UI responsive
|
||||
on_all_games()
|
||||
|
||||
def portproton_callback(games):
|
||||
results['portproton'] = games
|
||||
completed['portproton'] = True
|
||||
QApplication.processEvents() # Keep UI responsive
|
||||
check_completion()
|
||||
|
||||
def steam_callback(games):
|
||||
results['steam'] = games
|
||||
completed['steam'] = True
|
||||
QApplication.processEvents() # Keep UI responsive
|
||||
check_completion()
|
||||
|
||||
def epic_callback(games):
|
||||
results['epic'] = games
|
||||
completed['epic'] = True
|
||||
QApplication.processEvents() # Keep UI responsive
|
||||
check_completion()
|
||||
|
||||
# Load all sources in parallel
|
||||
self._load_portproton_games_async(portproton_callback)
|
||||
self._load_steam_games_async(steam_callback)
|
||||
load_egs_games_async(
|
||||
self.legendary_path,
|
||||
epic_callback,
|
||||
self.downloader,
|
||||
self.update_progress.emit,
|
||||
self.update_status_message.emit
|
||||
)
|
||||
|
||||
# Run loading with minimal delay to allow UI to be responsive
|
||||
QTimer.singleShot(100, start_loading) # Reduced to 100ms
|
||||
|
||||
def _load_steam_games_async(self, callback: Callable[[list[tuple]], None]):
|
||||
steam_games = []
|
||||
|
||||
@@ -254,6 +254,8 @@ class PortProtonAPI:
|
||||
try:
|
||||
mod_time = os.path.getmtime(cache_file)
|
||||
if time.time() - mod_time < AUTOINSTALL_CACHE_DURATION:
|
||||
# Add timeout protection for file operations
|
||||
start_time = time.time()
|
||||
with open(cache_file, "rb") as f:
|
||||
data = orjson.loads(f.read())
|
||||
# Check signature
|
||||
@@ -261,6 +263,10 @@ class PortProtonAPI:
|
||||
current_signature = self._compute_scripts_signature(
|
||||
os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall")
|
||||
)
|
||||
# Check for timeout during signature computation
|
||||
if time.time() - start_time > 3: # 3 second timeout
|
||||
logger.warning("Cache loading took too long, skipping cache")
|
||||
return None
|
||||
if cached_signature != current_signature:
|
||||
logger.info("Scripts signature mismatch; invalidating cache")
|
||||
return None
|
||||
@@ -287,21 +293,26 @@ class PortProtonAPI:
|
||||
|
||||
def start_autoinstall_games_load(self, callback: Callable[[list[tuple]], None]) -> QThread | None:
|
||||
"""Start loading auto-install games in a background thread. Returns the thread for management."""
|
||||
# Check cache first (sync, fast)
|
||||
cached_games = self._load_autoinstall_cache()
|
||||
if cached_games is not None:
|
||||
# Emit via callback immediately if cached
|
||||
QThread.msleep(0) # Yield to Qt event loop
|
||||
callback(cached_games)
|
||||
return None # No thread needed
|
||||
|
||||
# No cache: Start background thread
|
||||
class AutoinstallWorker(QThread):
|
||||
finished = Signal(list)
|
||||
api: "PortProtonAPI"
|
||||
portproton_location: str | None
|
||||
|
||||
def run(self):
|
||||
import time
|
||||
# Check cache in this background thread, not in main thread
|
||||
start_time = time.time()
|
||||
cached_games = self.api._load_autoinstall_cache()
|
||||
# If cache loading took too long (>2 seconds), skip cache and load directly
|
||||
if time.time() - start_time > 2:
|
||||
logger.warning("Cache loading took too long, proceeding without cache")
|
||||
cached_games = None
|
||||
|
||||
if cached_games is not None:
|
||||
self.finished.emit(cached_games)
|
||||
return
|
||||
|
||||
# No cache: Load games from scratch
|
||||
games = []
|
||||
auto_dir = os.path.join(
|
||||
self.portproton_location or "", "data", "scripts", "pw_autoinstall"
|
||||
|
||||
@@ -420,7 +420,7 @@ def fetch_sgdb_cover(game_name: str) -> str:
|
||||
try:
|
||||
encoded = urllib.parse.quote(game_name)
|
||||
url = f"https://steamgrid.usebottles.com/api/search/{encoded}"
|
||||
resp = requests.get(url, timeout=5)
|
||||
resp = requests.get(url, timeout=10)
|
||||
if resp.status_code != 200:
|
||||
logger.warning("SGDB request failed for %s: %s", game_name, resp.status_code)
|
||||
return ""
|
||||
@@ -431,17 +431,30 @@ def fetch_sgdb_cover(game_name: str) -> str:
|
||||
if text:
|
||||
logger.info("Fetched SGDB cover for %s: %s", game_name, text)
|
||||
return text
|
||||
except requests.exceptions.Timeout:
|
||||
logger.warning(f"SGDB request timed out for {game_name}")
|
||||
return ""
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.warning(f"SGDB request error for {game_name}: {e}")
|
||||
return ""
|
||||
except Exception as e:
|
||||
logger.warning("Failed to fetch SGDB cover for %s: %s", game_name, e)
|
||||
return ""
|
||||
logger.warning(f"Unexpected error while fetching SGDB cover for {game_name}: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def check_url_exists(url: str) -> bool:
|
||||
"""Check whether a URL returns HTTP 200."""
|
||||
try:
|
||||
r = requests.head(url, timeout=3)
|
||||
r = requests.head(url, timeout=5)
|
||||
return r.status_code == 200
|
||||
except Exception:
|
||||
except requests.exceptions.Timeout:
|
||||
logger.warning(f"URL check timed out for: {url}")
|
||||
return False
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.warning(f"Request error when checking URL {url}: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning(f"Unexpected error when checking URL {url}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -111,34 +111,65 @@ def load_theme_fonts(theme_name):
|
||||
logger.debug(f"Fonts for theme '{theme_name}' already loaded, skipping")
|
||||
return
|
||||
|
||||
QFontDatabase.removeAllApplicationFonts()
|
||||
fonts_folder = None
|
||||
if theme_name == "standart":
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
fonts_folder = os.path.join(base_dir, "themes", "standart", "fonts")
|
||||
else:
|
||||
for themes_dir in THEMES_DIRS:
|
||||
theme_folder = os.path.join(themes_dir, theme_name)
|
||||
possible_fonts_folder = os.path.join(theme_folder, "fonts")
|
||||
if os.path.exists(possible_fonts_folder):
|
||||
fonts_folder = possible_fonts_folder
|
||||
break
|
||||
def load_fonts_delayed():
|
||||
global _loaded_theme
|
||||
try:
|
||||
# Only remove fonts if this is a theme change (not initial load)
|
||||
current_loaded_theme = _loaded_theme # Capture the current value
|
||||
if current_loaded_theme is not None and current_loaded_theme != theme_name:
|
||||
# Run font removal in the GUI thread with delay
|
||||
QFontDatabase.removeAllApplicationFonts()
|
||||
|
||||
if not fonts_folder or not os.path.exists(fonts_folder):
|
||||
logger.error(f"Fonts folder not found for theme '{theme_name}'")
|
||||
return
|
||||
import time
|
||||
import os
|
||||
start_time = time.time()
|
||||
timeout = 3 # Reduced timeout to 3 seconds for faster loading
|
||||
|
||||
for filename in os.listdir(fonts_folder):
|
||||
if filename.lower().endswith((".ttf", ".otf")):
|
||||
font_path = os.path.join(fonts_folder, filename)
|
||||
font_id = QFontDatabase.addApplicationFont(font_path)
|
||||
if font_id != -1:
|
||||
families = QFontDatabase.applicationFontFamilies(font_id)
|
||||
logger.info(f"Font {filename} successfully loaded: {families}")
|
||||
fonts_folder = None
|
||||
if theme_name == "standart":
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
fonts_folder = os.path.join(base_dir, "themes", "standart", "fonts")
|
||||
else:
|
||||
logger.error(f"Error loading font: {filename}")
|
||||
for themes_dir in THEMES_DIRS:
|
||||
theme_folder = os.path.join(themes_dir, theme_name)
|
||||
possible_fonts_folder = os.path.join(theme_folder, "fonts")
|
||||
if os.path.exists(possible_fonts_folder):
|
||||
fonts_folder = possible_fonts_folder
|
||||
break
|
||||
|
||||
_loaded_theme = theme_name
|
||||
if not fonts_folder or not os.path.exists(fonts_folder):
|
||||
logger.error(f"Fonts folder not found for theme '{theme_name}'")
|
||||
return
|
||||
|
||||
font_files = []
|
||||
for filename in os.listdir(fonts_folder):
|
||||
if filename.lower().endswith((".ttf", ".otf")):
|
||||
font_files.append(filename)
|
||||
|
||||
# Limit number of fonts loaded to prevent too much blocking
|
||||
font_files = font_files[:10] # Only load first 10 fonts to prevent too much blocking
|
||||
|
||||
for filename in font_files:
|
||||
if time.time() - start_time > timeout:
|
||||
logger.warning(f"Font loading timed out for theme '{theme_name}' after loading {len(font_files)} fonts")
|
||||
break
|
||||
|
||||
font_path = os.path.join(fonts_folder, filename)
|
||||
font_id = QFontDatabase.addApplicationFont(font_path)
|
||||
if font_id != -1:
|
||||
families = QFontDatabase.applicationFontFamilies(font_id)
|
||||
logger.info(f"Font {filename} successfully loaded: {families}")
|
||||
else:
|
||||
logger.error(f"Error loading font: {filename}")
|
||||
|
||||
# Update the global variable in the main thread
|
||||
_loaded_theme = theme_name
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading fonts for theme '{theme_name}': {e}")
|
||||
|
||||
# Use QTimer to delay font loading until after the UI is rendered
|
||||
from PySide6.QtCore import QTimer
|
||||
QTimer.singleShot(100, load_fonts_delayed) # Delay font loading by 100ms
|
||||
|
||||
class ThemeWrapper:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user