Fix UI freeze on first cover load (decode+scale off the GUI thread)
This commit is contained in:
+41
-12
@@ -117,14 +117,16 @@ def _thread_running(thread):
|
||||
|
||||
|
||||
class CoverLoader(QObject):
|
||||
loaded = Signal(int, int, str) # gen, item index, cached image path
|
||||
loaded = Signal(int, int, QImage) # gen, item index, fitted cover image
|
||||
failed = Signal(int, int, str) # gen, item index, name (no cover found)
|
||||
finished = Signal()
|
||||
|
||||
def __init__(self, items, gen):
|
||||
def __init__(self, items, gen, icon_w=150, icon_h=220):
|
||||
super().__init__()
|
||||
self.items = items
|
||||
self.gen = gen
|
||||
self.icon_w = icon_w
|
||||
self.icon_h = icon_h
|
||||
self._cancel = threading.Event()
|
||||
|
||||
def cancel(self):
|
||||
@@ -148,7 +150,11 @@ class CoverLoader(QObject):
|
||||
except Exception: # noqa: BLE001
|
||||
path = None
|
||||
if path is not None:
|
||||
self.loaded.emit(self.gen, i, str(path))
|
||||
img = QImage(str(path))
|
||||
if img.isNull():
|
||||
self.failed.emit(self.gen, i, self.items[i]["name"])
|
||||
else:
|
||||
self.loaded.emit(self.gen, i, _fit_image(img, self.icon_w, self.icon_h))
|
||||
else:
|
||||
self.failed.emit(self.gen, i, self.items[i]["name"])
|
||||
finally:
|
||||
@@ -276,6 +282,25 @@ def _fit_pixmap(pm, w, h):
|
||||
return canvas
|
||||
|
||||
|
||||
def _fit_image(img, w, h):
|
||||
"""Scale a QImage into a letterboxed ``w x h`` ARGB image (thread-safe).
|
||||
|
||||
Decoding + scaling happen off the GUI thread in the cover loaders; the
|
||||
main thread only converts the result to a QPixmap (cheap).
|
||||
"""
|
||||
canvas = QImage(w, h, QImage.Format_ARGB32)
|
||||
canvas.fill(Qt.transparent)
|
||||
if img.isNull():
|
||||
return canvas
|
||||
scaled = img.scaled(w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
x = (w - scaled.width()) // 2
|
||||
y = (h - scaled.height()) // 2
|
||||
painter = QPainter(canvas)
|
||||
painter.drawImage(x, y, scaled)
|
||||
painter.end()
|
||||
return canvas
|
||||
|
||||
|
||||
def _cover_pixmap(rid, system, max_w, max_h):
|
||||
"""Return a scaled cover pixmap for an item, or None if unavailable."""
|
||||
if rid is None:
|
||||
@@ -429,7 +454,7 @@ class CoverShowcase(QWidget):
|
||||
for loader in self._loaders:
|
||||
loader.cancel()
|
||||
self.thread = QThread(self)
|
||||
self.loader = CoverLoader(self.items, gen)
|
||||
self.loader = CoverLoader(self.items, gen, self.ICON_W, self.ICON_H)
|
||||
self.loader.moveToThread(self.thread)
|
||||
self.thread.started.connect(self.loader.run)
|
||||
self.loader.loaded.connect(self._on_loaded)
|
||||
@@ -454,12 +479,12 @@ class CoverShowcase(QWidget):
|
||||
painter.end()
|
||||
return QIcon(pm)
|
||||
|
||||
def _on_loaded(self, gen, index, path):
|
||||
def _on_loaded(self, gen, index, image):
|
||||
if gen != self._generation:
|
||||
return
|
||||
item = self.list.item(index)
|
||||
if item is not None:
|
||||
item.setIcon(self._fit_icon(QPixmap(path)))
|
||||
item.setIcon(QIcon(QPixmap.fromImage(image)))
|
||||
if 0 <= index < len(self.items):
|
||||
self.cover_loaded.emit(self.items[index])
|
||||
|
||||
@@ -1479,14 +1504,16 @@ class StoreScanWorker(QObject):
|
||||
|
||||
|
||||
class StoreCoverLoader(QObject):
|
||||
loaded = Signal(int, int, str) # gen, grid row, cached image path
|
||||
loaded = Signal(int, int, QImage) # gen, grid row, fitted cover image
|
||||
finished = Signal()
|
||||
|
||||
def __init__(self, tasks, gen, quick=True):
|
||||
def __init__(self, tasks, gen, quick=True, icon_w=150, icon_h=220):
|
||||
super().__init__()
|
||||
self.tasks = tasks # list of (grid_row, item_dict)
|
||||
self.gen = gen
|
||||
self.quick = quick
|
||||
self.icon_w = icon_w
|
||||
self.icon_h = icon_h
|
||||
self._cancel = threading.Event()
|
||||
|
||||
def cancel(self):
|
||||
@@ -1510,7 +1537,9 @@ class StoreCoverLoader(QObject):
|
||||
except Exception: # noqa: BLE001
|
||||
path = None
|
||||
if path is not None:
|
||||
self.loaded.emit(self.gen, row, str(path))
|
||||
img = QImage(str(path))
|
||||
if not img.isNull():
|
||||
self.loaded.emit(self.gen, row, _fit_image(img, self.icon_w, self.icon_h))
|
||||
finally:
|
||||
pool.shutdown(wait=True, cancel_futures=True)
|
||||
self.finished.emit()
|
||||
@@ -2336,7 +2365,7 @@ class StorePage(QWidget):
|
||||
if self._closing or not tasks:
|
||||
return
|
||||
thread = QThread(self)
|
||||
loader = StoreCoverLoader(tasks, gen, quick=True)
|
||||
loader = StoreCoverLoader(tasks, gen, quick=True, icon_w=self.ICON_W, icon_h=self.ICON_H)
|
||||
loader.moveToThread(thread)
|
||||
thread.started.connect(loader.run)
|
||||
loader.loaded.connect(self._on_cover_loaded)
|
||||
@@ -2346,12 +2375,12 @@ class StorePage(QWidget):
|
||||
self._loaders.append(loader)
|
||||
thread.start()
|
||||
|
||||
def _on_cover_loaded(self, gen, row, path):
|
||||
def _on_cover_loaded(self, gen, row, image):
|
||||
if gen != self._generation:
|
||||
return
|
||||
item = self.grid.item(row)
|
||||
if item is not None:
|
||||
item.setIcon(QIcon(_fit_pixmap(QPixmap(path), self.ICON_W, self.ICON_H)))
|
||||
item.setIcon(QIcon(QPixmap.fromImage(image)))
|
||||
it = item.data(Qt.UserRole)
|
||||
shown = getattr(self.details, "_item", None)
|
||||
if it is not None and shown is not None and it.get("path") == shown.get("path"):
|
||||
|
||||
Reference in New Issue
Block a user