Fix updates of the list view when media are customized or reset

What an ugly mess!

queue_draw() on every window does not work on the ListView, though I don't understand why not.

This replaces that with a notification system so that the views can register callbacks to redraw themselves.

I've added a Notification class to tame this beast, in the hope that this will at least be reusable.
This commit is contained in:
Daniel Johnson 2023-12-31 12:08:58 -05:00
parent 8d665cd92a
commit 81969e5d02
7 changed files with 75 additions and 37 deletions

View file

@ -17,7 +17,7 @@ from lutris.gui.dialogs.delegates import DialogInstallUIDelegate
from lutris.gui.widgets.common import Label, NumberEntry, SlugEntry
from lutris.gui.widgets.notifications import send_notification
from lutris.gui.widgets.scaled_image import ScaledImage
from lutris.gui.widgets.utils import get_image_file_format, invalidate_media_caches
from lutris.gui.widgets.utils import MEDIA_CACHE_INVALIDATED, get_image_file_format
from lutris.runners import import_runner
from lutris.services.lutris import LutrisBanner, LutrisCoverart, LutrisIcon, download_lutris_media
from lutris.util.jobs import AsyncCall
@ -725,7 +725,7 @@ class GameDialogCommon(SavableModelessDialog, DialogInstallUIDelegate):
# JPEG encoding looks rather better at high quality;
# PNG encoding just ignores this option.
pixbuf.savev(dest_path, file_format, ["quality"], ["100"])
invalidate_media_caches()
MEDIA_CACHE_INVALIDATED.fire()
return image_type
def refresh_image(self, image_type):
@ -737,7 +737,7 @@ class GameDialogCommon(SavableModelessDialog, DialogInstallUIDelegate):
if os.path.isfile(dest_path):
os.remove(dest_path)
download_lutris_media(self.game.slug)
invalidate_media_caches()
MEDIA_CACHE_INVALIDATED.fire()
return image_type
def image_refreshed_cb(self, image_type, _error):

View file

@ -770,11 +770,15 @@ class LutrisWindow(Gtk.ApplicationWindow,
regenerates it. This is used to update view settings that can only be
set during view construction, and not updated later."""
if view_type in self.views:
view = self.views[view_type]
scrolledwindow = self.games_stack.get_child_by_name(view_type)
scrolledwindow.remove(self.views[view_type])
del self.views["grid"]
scrolledwindow.remove(view)
del self.views[view_type]
if self.current_view_type == view_type:
self.redraw_view()
# Because the view has hooks and such hooked up, it must be explicitly
# destroyed to disconnect everything.
view.destroy()
def update_view_settings(self):
if self.current_view and self.current_view_type == "grid":

View file

@ -8,6 +8,7 @@ from lutris.database.services import ServiceGameCollection
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.util.log import logger
@ -20,18 +21,27 @@ class GameView:
def __init__(self, service):
self.service = service
self.cache_notification_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.connect("destroy", self.on_destroy)
self.connect("button-press-event", self.popup_contextual_menu)
self.connect("key-press-event", self.handle_key_press)
self.game_start_hook_id = GObject.add_emission_hook(Game, "game-start", self.on_game_start)
def on_media_cache_invalidated(self):
self.queue_draw()
def on_destroy(self, _widget):
if self.cache_notification_id:
MEDIA_CACHE_INVALIDATED.unregister(self.cache_notification_id)
if self.game_start_hook_id:
GObject.remove_emission_hook(Game, "game-start", self.game_start_hook_id)

View file

@ -1,7 +1,7 @@
"""Loads game media in parallel"""
import concurrent.futures
from lutris.gui.widgets.utils import invalidate_media_caches
from lutris.gui.widgets.utils import MEDIA_CACHE_INVALIDATED
from lutris.util import system
from lutris.util.log import logger
@ -30,5 +30,5 @@ def download_media(media_urls, service_media):
icons[slug] = path
if icons:
invalidate_media_caches()
MEDIA_CACHE_INVALIDATED.fire()
return icons

View file

@ -0,0 +1,49 @@
from typing import Callable
from lutris.util.jobs import schedule_at_idle
class Notification:
"""A class to inform interested code of changes in a global, like a signal but not attached to any
object."""
def __init__(self):
self._generation_number = 0
self._callbacks = {}
self._next_callback_id = 1
self._scheduled_callbacks = set()
def fire(self) -> None:
"""Signals that the thing, whatever it is, has happened. This increments the generation number,
and schedules the callbacks to run (if they are not scheduled already)."""
self._generation_number += 1
self._scheduled_callbacks.update(self._callbacks.values())
schedule_at_idle(self._notify)
@property
def generation_number(self) -> int:
"""Returns a number that is incremented on each call to fire(). This can be polled
passively, when registering a callback is inappropriate."""
return self._generation_number
def register(self, callback: Callable[[], None]) -> int:
"""Registers a callback to be called after the thing, whatever it is, has happened;
fire() schedules callbacks to be called at idle time on the main thread.
Note that a callback will be kept alive until unregistered, and this can keep
large objects alive until then.
Returns an id number to use to unregister the callback."""
callback_id = self._next_callback_id
self._callbacks[callback_id] = callback
self._next_callback_id += 1
return callback_id
def unregister(self, callback_id: int) -> None:
"""Unregisters a callback that register() had registered."""
self._callbacks.pop(callback_id, None)
def _notify(self):
while self._scheduled_callbacks:
callback = self._scheduled_callbacks.pop()
callback()

View file

@ -11,8 +11,7 @@ import cairo
from gi.repository import GLib, GObject, Gtk, Pango, PangoCairo
from lutris.gui.widgets.utils import (
get_default_icon_path, get_media_generation_number, get_runtime_icon_path, get_scaled_surface_by_path,
get_surface_size
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
@ -472,8 +471,8 @@ class GridViewCellRendererImage(Gtk.CellRenderer):
in this render, but we'll clear that cache when the media generation number is changed,
or certain properties are. We also age surfaces from the cache at idle time after
rendering."""
if self.cached_surface_generation != get_media_generation_number():
self.cached_surface_generation = get_media_generation_number()
if self.cached_surface_generation != MEDIA_CACHE_INVALIDATED.generation_number:
self.cached_surface_generation = MEDIA_CACHE_INVALIDATED.generation_number
self.clear_cache()
key = widget, path, size, preserve_aspect_ratio

View file

@ -6,8 +6,8 @@ import cairo
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk
from lutris import settings
from lutris.gui.widgets import Notification
from lutris.util import datapath, magic, system
from lutris.util.jobs import schedule_at_idle
from lutris.util.log import logger
try:
@ -17,8 +17,7 @@ except ImportError:
ICON_SIZE = (32, 32)
BANNER_SIZE = (184, 69)
_surface_generation_number = 0
MEDIA_CACHE_INVALIDATED = Notification()
def get_main_window(widget):
@ -103,29 +102,6 @@ def get_scaled_surface_by_path(path, size, device_scale, preserve_aspect_ratio=T
return surface
def get_media_generation_number():
"""Returns a number that is incremented whenever cached media may no longer
be valid. Caller can check to see if this has changed before using their own caches."""
return _surface_generation_number
def invalidate_media_caches():
"""Increments the media generation number; this indicates that cached media
from earlier generations may be invalid and should be reloaded."""
global _surface_generation_number
_surface_generation_number += 1
schedule_at_idle(queue_draw_all_windows)
def queue_draw_all_windows():
"""Scans through all windows and queues a redraw for each one;
we trigger this when invalidate_media_caches() has been called."""
application = Gio.Application.get_default()
if application:
for window in application.get_windows():
window.queue_draw()
def get_default_icon_path(size):
"""Returns the path to the default icon for the size given; it's
a Lutris icon for a square size, and a gradient for other sizes."""