mirror of
https://github.com/lutris/lutris
synced 2024-10-14 19:53:53 +00:00
Move the missing game ids set into a singleton class that updates it into the background and fires a notification when there are changes.
This lets the UI update in a non-blocking way, including the cell-renderer stuff. The cell-renderer queues re-checks for games it sees are missing, so we'll 'notice' if you replace a missing game. The implementation at this point is rather complex for what this is, but it seems to work.
This commit is contained in:
parent
d3d8e18a9e
commit
6db8767ef3
|
@ -28,7 +28,7 @@ from lutris.gui.widgets.gi_composites import GtkTemplate
|
|||
from lutris.gui.widgets.sidebar import LutrisSidebar
|
||||
from lutris.gui.widgets.utils import load_icon_theme, open_uri
|
||||
from lutris.runtime import ComponentUpdater, RuntimeUpdater
|
||||
from lutris.scanners.lutris import add_to_path_cache, get_missing_game_ids, remove_from_path_cache
|
||||
from lutris.scanners.lutris import MISSING_GAMES, add_to_path_cache, remove_from_path_cache
|
||||
# pylint: disable=no-member
|
||||
from lutris.services.base import BaseService
|
||||
from lutris.services.lutris import LutrisService
|
||||
|
@ -148,6 +148,7 @@ class LutrisWindow(Gtk.ApplicationWindow,
|
|||
GObject.add_emission_hook(Game, "game-removed", self.on_game_removed)
|
||||
GObject.add_emission_hook(Game, "game-unhandled-error", self.on_game_unhandled_error)
|
||||
GObject.add_emission_hook(PreferencesDialog, "settings-changed", self.on_settings_changed)
|
||||
MISSING_GAMES.updated.register(self.update_missing_games_sidebar_row)
|
||||
|
||||
# Finally trigger the initialization of the view here
|
||||
selected_category = settings.read_setting("selected_category", default="runner:all")
|
||||
|
@ -254,7 +255,7 @@ class LutrisWindow(Gtk.ApplicationWindow,
|
|||
def on_load(self, widget, data=None):
|
||||
"""Finish initializing the view"""
|
||||
self._bind_zoom_adjustment()
|
||||
AsyncCall(get_missing_game_ids, self.on_get_missing_game_ids)
|
||||
MISSING_GAMES.update_all_missing()
|
||||
self.current_view.grab_focus()
|
||||
|
||||
def on_sidebar_realize(self, widget, data=None):
|
||||
|
@ -417,23 +418,18 @@ class LutrisWindow(Gtk.ApplicationWindow,
|
|||
"""Return a list of currently running games"""
|
||||
return games_db.get_games_by_ids(self.application.get_running_game_ids())
|
||||
|
||||
def on_get_missing_game_ids(self, missing_ids, error):
|
||||
if error:
|
||||
logger.error(str(error))
|
||||
return
|
||||
self.get_missing_games(missing_ids)
|
||||
def get_missing_games(self):
|
||||
return games_db.get_games_by_ids(MISSING_GAMES.missing_game_ids)
|
||||
|
||||
def get_missing_games(self, missing_ids: list = None) -> list:
|
||||
if missing_ids is None:
|
||||
missing_ids = get_missing_game_ids()
|
||||
missing_games = games_db.get_games_by_ids(missing_ids)
|
||||
def update_missing_games_sidebar_row(self) -> None:
|
||||
missing_games = self.get_missing_games()
|
||||
if missing_games:
|
||||
self.sidebar.missing_row.show()
|
||||
else:
|
||||
missing_ids = MISSING_GAMES.missing_game_ids
|
||||
if missing_ids:
|
||||
logger.warning("Path cache out of date? (%s IDs missing)", len(missing_ids))
|
||||
self.sidebar.missing_row.hide()
|
||||
return missing_games
|
||||
|
||||
def get_recent_games(self):
|
||||
"""Return a list of currently running games"""
|
||||
|
@ -1126,7 +1122,8 @@ class LutrisWindow(Gtk.ApplicationWindow,
|
|||
def on_game_removed(self, game):
|
||||
"""Simple method used to refresh the view"""
|
||||
remove_from_path_cache(game)
|
||||
self.get_missing_games()
|
||||
MISSING_GAMES.update_missing([game.id])
|
||||
self.update_missing_games_sidebar_row()
|
||||
self.emit("view-updated")
|
||||
return True
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ from lutris.game import Game
|
|||
from lutris.game_actions import BaseGameActions, get_game_actions
|
||||
from lutris.gui.widgets.contextual_menu import ContextualMenu
|
||||
from lutris.gui.widgets.utils import MEDIA_CACHE_INVALIDATED
|
||||
from lutris.scanners.lutris import MISSING_GAMES
|
||||
from lutris.util.log import logger
|
||||
|
||||
|
||||
|
@ -22,12 +23,14 @@ class GameView:
|
|||
def __init__(self, service):
|
||||
self.service = service
|
||||
self.cache_notification_id = None
|
||||
self.missing_games_updated_id = None
|
||||
self.game_start_hook_id = None
|
||||
self.image_renderer = None
|
||||
|
||||
def connect_signals(self):
|
||||
"""Signal handlers common to all views"""
|
||||
self.cache_notification_id = MEDIA_CACHE_INVALIDATED.register(self.on_media_cache_invalidated)
|
||||
self.missing_games_updated_id = MISSING_GAMES.updated.register(self.on_missing_games_updated)
|
||||
|
||||
self.connect("destroy", self.on_destroy)
|
||||
self.connect("button-press-event", self.popup_contextual_menu)
|
||||
|
@ -38,10 +41,17 @@ class GameView:
|
|||
def on_media_cache_invalidated(self):
|
||||
self.queue_draw()
|
||||
|
||||
def on_missing_games_updated(self):
|
||||
if self.image_renderer and self.image_renderer.show_badges:
|
||||
self.queue_draw()
|
||||
|
||||
def on_destroy(self, _widget):
|
||||
if self.cache_notification_id:
|
||||
MEDIA_CACHE_INVALIDATED.unregister(self.cache_notification_id)
|
||||
|
||||
if self.missing_games_updated_id:
|
||||
MISSING_GAMES.updated.unregister(self.missing_games_updated_id)
|
||||
|
||||
if self.game_start_hook_id:
|
||||
GObject.remove_emission_hook(Game, "game-start", self.game_start_hook_id)
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ from lutris.exceptions import MissingMediaError
|
|||
from lutris.gui.widgets.utils import (
|
||||
MEDIA_CACHE_INVALIDATED, get_default_icon_path, get_runtime_icon_path, get_scaled_surface_by_path, get_surface_size
|
||||
)
|
||||
from lutris.scanners.lutris import is_game_missing
|
||||
from lutris.scanners.lutris import MISSING_GAMES
|
||||
|
||||
|
||||
class GridViewCellRendererText(Gtk.CellRendererText):
|
||||
|
@ -248,8 +248,9 @@ class GridViewCellRendererImage(Gtk.CellRenderer):
|
|||
if self.show_badges:
|
||||
self.render_platforms(cr, widget, surface, 0, cell_area)
|
||||
|
||||
if self.game_id and is_game_missing(self.game_id):
|
||||
if self.game_id and self.game_id in MISSING_GAMES.missing_game_ids:
|
||||
self.render_text_badge(cr, widget, _("Missing"), 0, cell_area.y + cell_area.height)
|
||||
MISSING_GAMES.update_missing([self.game_id])
|
||||
else:
|
||||
cr.push_group()
|
||||
self.render_media(cr, widget, surface, 0, 0)
|
||||
|
|
|
@ -434,6 +434,7 @@ class LutrisSidebar(Gtk.ListBox):
|
|||
# I wanted this to be on top but it really messes with the headers when showing/hiding the row.
|
||||
self.add(self.running_row)
|
||||
self.show_all()
|
||||
self.missing_row.hide()
|
||||
self.running_row.hide()
|
||||
|
||||
# Create the dynamic rows that are initially needed
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import Iterable
|
||||
|
||||
from lutris import settings
|
||||
from lutris.api import get_api_games, get_game_installers
|
||||
from lutris.database.games import get_games
|
||||
from lutris.game import Game
|
||||
from lutris.gui.widgets import NotificationSource
|
||||
from lutris.installer.errors import MissingGameDependencyError
|
||||
from lutris.installer.interpreter import ScriptInterpreter
|
||||
from lutris.services.lutris import download_lutris_media
|
||||
from lutris.util import cache_single
|
||||
from lutris.util.jobs import AsyncCall
|
||||
from lutris.util.log import logger
|
||||
from lutris.util.strings import slugify
|
||||
|
||||
|
@ -206,17 +209,102 @@ def read_path_cache():
|
|||
return {}
|
||||
|
||||
|
||||
def get_missing_game_ids():
|
||||
"""Return a list of IDs for games that can't be found"""
|
||||
logger.debug("Checking for missing games")
|
||||
missing_ids = []
|
||||
for game_id, path in get_path_cache().items():
|
||||
if not os.path.exists(path):
|
||||
missing_ids.append(game_id)
|
||||
return missing_ids
|
||||
class MissingGames:
|
||||
"""This class is a singleton that holds a set of game-ids for games whose directories
|
||||
are missing. It is updated on a background thread, but there's a NotificationSource ('updated')
|
||||
that fires when that thread has made changes and exited, so that the UI cab update then."""
|
||||
CHECK_STALL = 3
|
||||
|
||||
def __init__(self):
|
||||
self.updated = NotificationSource()
|
||||
self.missing_game_ids = set()
|
||||
self._check_game_ids = None
|
||||
self._check_game_queue = []
|
||||
self._check_all_games = False
|
||||
self._changed = False
|
||||
|
||||
def update_all_missing(self):
|
||||
"""This starts the check for all games; the actual list of game-ids will be obtained
|
||||
on the worker thread, and this method will start it."""
|
||||
self._check_all_games = True
|
||||
self.update_missing()
|
||||
|
||||
def update_missing(self, game_ids: Iterable[str] = None):
|
||||
"""Starts checking the missing status on a list of games. This starts the worker
|
||||
thread, but if it is running already, it queues the games. The worker will pause
|
||||
briefly before processing extra games just to limit the rate of filesystem accesses."""
|
||||
|
||||
# The presence of this set indicates that the worker is running
|
||||
start_fetch = self._check_game_ids is None
|
||||
|
||||
if not self._check_game_ids:
|
||||
self._check_game_ids = set()
|
||||
|
||||
if game_ids:
|
||||
self._check_game_ids |= set(game_ids)
|
||||
|
||||
if start_fetch:
|
||||
initial_delay = self.CHECK_STALL if game_ids else 0
|
||||
AsyncCall(self._fetch, None, initial_delay)
|
||||
|
||||
def _fetch(self, initial_delay=0):
|
||||
"""This is the method that runs on the worker thread; it continues until all
|
||||
games are processed, even extras added while it is running."""
|
||||
time.sleep(initial_delay)
|
||||
logger.debug("Checking for missing games")
|
||||
try:
|
||||
while True:
|
||||
game_id = self._next_game_id()
|
||||
if not game_id:
|
||||
break
|
||||
|
||||
path = get_path_cache().get(game_id)
|
||||
|
||||
if path:
|
||||
if os.path.exists(path):
|
||||
if game_id in self.missing_game_ids:
|
||||
self.missing_game_ids.discard(game_id)
|
||||
self._changed = True
|
||||
elif game_id not in self.missing_game_ids:
|
||||
self.missing_game_ids.add(game_id)
|
||||
self._changed = True
|
||||
except Exception as ex:
|
||||
logger.exception("Unable to detect missing games: %s", ex)
|
||||
finally:
|
||||
# Clearing out _check_game_ids is how we know the worker is no longer running
|
||||
self._check_game_ids = None
|
||||
self._notify_changed()
|
||||
|
||||
def _notify_changed(self):
|
||||
"""Fires the 'updated' notification if changes have been made since the last time
|
||||
this method was called."""
|
||||
if self._changed:
|
||||
self._changed = False
|
||||
self.updated.fire()
|
||||
|
||||
def _next_game_id(self):
|
||||
"""Returns the next game-id to check, or None if we're done. This will detect
|
||||
additional games once the queue empties, and moves hem to the queue, but fires
|
||||
the notification then too."""
|
||||
if self._check_all_games:
|
||||
self._check_all_games = False
|
||||
path_cache = get_path_cache()
|
||||
self._check_game_queue = list(path_cache)
|
||||
self._check_game_ids.clear()
|
||||
|
||||
if not self._check_game_queue and self._check_game_ids:
|
||||
# If more ids have arrived while fetching the old ones, we do not
|
||||
# just check them too. We notify immediate and wait a little,
|
||||
# then continue checking. This will
|
||||
self._notify_changed()
|
||||
time.sleep(self.CHECK_STALL)
|
||||
self._check_game_queue = list(self._check_game_ids)
|
||||
self._check_game_ids.clear()
|
||||
|
||||
if self._check_game_queue:
|
||||
return self._check_game_queue.pop()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def is_game_missing(game_id):
|
||||
cache = get_path_cache()
|
||||
path = cache.get(str(game_id))
|
||||
return path and not os.path.exists(os.path.expanduser(path))
|
||||
MISSING_GAMES = MissingGames()
|
||||
|
|
Loading…
Reference in a new issue