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):
|
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)
|
failed = Signal(int, int, str) # gen, item index, name (no cover found)
|
||||||
finished = Signal()
|
finished = Signal()
|
||||||
|
|
||||||
def __init__(self, items, gen):
|
def __init__(self, items, gen, icon_w=150, icon_h=220):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.items = items
|
self.items = items
|
||||||
self.gen = gen
|
self.gen = gen
|
||||||
|
self.icon_w = icon_w
|
||||||
|
self.icon_h = icon_h
|
||||||
self._cancel = threading.Event()
|
self._cancel = threading.Event()
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
@@ -148,7 +150,11 @@ class CoverLoader(QObject):
|
|||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
path = None
|
path = None
|
||||||
if path is not 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:
|
else:
|
||||||
self.failed.emit(self.gen, i, self.items[i]["name"])
|
self.failed.emit(self.gen, i, self.items[i]["name"])
|
||||||
finally:
|
finally:
|
||||||
@@ -276,6 +282,25 @@ def _fit_pixmap(pm, w, h):
|
|||||||
return canvas
|
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):
|
def _cover_pixmap(rid, system, max_w, max_h):
|
||||||
"""Return a scaled cover pixmap for an item, or None if unavailable."""
|
"""Return a scaled cover pixmap for an item, or None if unavailable."""
|
||||||
if rid is None:
|
if rid is None:
|
||||||
@@ -429,7 +454,7 @@ class CoverShowcase(QWidget):
|
|||||||
for loader in self._loaders:
|
for loader in self._loaders:
|
||||||
loader.cancel()
|
loader.cancel()
|
||||||
self.thread = QThread(self)
|
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.loader.moveToThread(self.thread)
|
||||||
self.thread.started.connect(self.loader.run)
|
self.thread.started.connect(self.loader.run)
|
||||||
self.loader.loaded.connect(self._on_loaded)
|
self.loader.loaded.connect(self._on_loaded)
|
||||||
@@ -454,12 +479,12 @@ class CoverShowcase(QWidget):
|
|||||||
painter.end()
|
painter.end()
|
||||||
return QIcon(pm)
|
return QIcon(pm)
|
||||||
|
|
||||||
def _on_loaded(self, gen, index, path):
|
def _on_loaded(self, gen, index, image):
|
||||||
if gen != self._generation:
|
if gen != self._generation:
|
||||||
return
|
return
|
||||||
item = self.list.item(index)
|
item = self.list.item(index)
|
||||||
if item is not None:
|
if item is not None:
|
||||||
item.setIcon(self._fit_icon(QPixmap(path)))
|
item.setIcon(QIcon(QPixmap.fromImage(image)))
|
||||||
if 0 <= index < len(self.items):
|
if 0 <= index < len(self.items):
|
||||||
self.cover_loaded.emit(self.items[index])
|
self.cover_loaded.emit(self.items[index])
|
||||||
|
|
||||||
@@ -1479,14 +1504,16 @@ class StoreScanWorker(QObject):
|
|||||||
|
|
||||||
|
|
||||||
class StoreCoverLoader(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()
|
finished = Signal()
|
||||||
|
|
||||||
def __init__(self, tasks, gen, quick=True):
|
def __init__(self, tasks, gen, quick=True, icon_w=150, icon_h=220):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.tasks = tasks # list of (grid_row, item_dict)
|
self.tasks = tasks # list of (grid_row, item_dict)
|
||||||
self.gen = gen
|
self.gen = gen
|
||||||
self.quick = quick
|
self.quick = quick
|
||||||
|
self.icon_w = icon_w
|
||||||
|
self.icon_h = icon_h
|
||||||
self._cancel = threading.Event()
|
self._cancel = threading.Event()
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
@@ -1510,7 +1537,9 @@ class StoreCoverLoader(QObject):
|
|||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
path = None
|
path = None
|
||||||
if path is not 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:
|
finally:
|
||||||
pool.shutdown(wait=True, cancel_futures=True)
|
pool.shutdown(wait=True, cancel_futures=True)
|
||||||
self.finished.emit()
|
self.finished.emit()
|
||||||
@@ -2336,7 +2365,7 @@ class StorePage(QWidget):
|
|||||||
if self._closing or not tasks:
|
if self._closing or not tasks:
|
||||||
return
|
return
|
||||||
thread = QThread(self)
|
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)
|
loader.moveToThread(thread)
|
||||||
thread.started.connect(loader.run)
|
thread.started.connect(loader.run)
|
||||||
loader.loaded.connect(self._on_cover_loaded)
|
loader.loaded.connect(self._on_cover_loaded)
|
||||||
@@ -2346,12 +2375,12 @@ class StorePage(QWidget):
|
|||||||
self._loaders.append(loader)
|
self._loaders.append(loader)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def _on_cover_loaded(self, gen, row, path):
|
def _on_cover_loaded(self, gen, row, image):
|
||||||
if gen != self._generation:
|
if gen != self._generation:
|
||||||
return
|
return
|
||||||
item = self.grid.item(row)
|
item = self.grid.item(row)
|
||||||
if item is not None:
|
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)
|
it = item.data(Qt.UserRole)
|
||||||
shown = getattr(self.details, "_item", None)
|
shown = getattr(self.details, "_item", None)
|
||||||
if it is not None and shown is not None and it.get("path") == shown.get("path"):
|
if it is not None and shown is not None and it.get("path") == shown.get("path"):
|
||||||
|
|||||||
Reference in New Issue
Block a user