diff --git a/client/ayon_core/tools/loader/ui/folders_widget.py b/client/ayon_core/tools/loader/ui/folders_widget.py index f238eabcef..2bf38283cb 100644 --- a/client/ayon_core/tools/loader/ui/folders_widget.py +++ b/client/ayon_core/tools/loader/ui/folders_widget.py @@ -133,8 +133,8 @@ def paint(self, painter, option, index): mode = QtGui.QIcon.Selected if isinstance(icon, QtGui.QPixmap): - icon = QtGui.QIcon(icon) option.decorationSize = icon.size() / icon.devicePixelRatio() + icon = QtGui.QIcon(icon) elif isinstance(icon, QtGui.QColor): pixmap = QtGui.QPixmap(option.decorationSize) diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index b3b5cebb0e..3484e0bda3 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -8,7 +8,9 @@ ProjectsModel, HierarchyModel, HierarchyExpectedSelection, + FolderItem ) +import ayon_api from .models import RecursiveSortFilterProxyModel from .views import TreeView @@ -22,6 +24,89 @@ FOLDER_TYPE_ROLE = QtCore.Qt.UserRole + 4 +class ImageLoader(QtCore.QThread): + image_loaded = QtCore.Signal(QtGui.QStandardItem, QtGui.QImage) + + def __init__(self, parent=None): + super().__init__(parent) + self._image_queue = collections.deque() + + # Turn this into a 'timed' cache to allow refresh after a certain time + self._cache = {} + self._thread = QtCore.QThread() + + def load_image(self, + item: QtGui.QStandardItem, + project_name: str, + entity_id: str): + # If image is in cache, do nothing + if (project_name, entity_id) in self._cache: + return + + self._image_queue.append((item, project_name, entity_id)) + if not self.isRunning(): + self.start() + + def get_image(self, project_name, entity_id): + return self._cache.get((project_name, entity_id), None) + + def run(self): + # Process remaining queue + while self._image_queue: + item, project_name, entity_id = self._image_queue.popleft() + + # Query thumbnail from server + # TODO: Get these from a cache instead of querying every time + response = ayon_api.raw_get( + f"projects/{project_name}/folders/{entity_id}/thumbnail") + image_bytes = response.content + + # TODO: Skip if image is empty or not valid + # this does not work + if not image_bytes: + continue + + # Get image format + # https://doc.qt.io/qt-6/qimagereader.html#supportedImageFormats + image_type = response.headers['content-type'] + if image_type == "image/jpeg": + image_format = "JPG" + elif image_type == "image/png": + image_format = "PNG" + else: + raise TypeError(f"Unsupported image type: {image_type}") + + # Load the image from the bytes + image = QtGui.QImage() + image.loadFromData(image_bytes, image_format) + + # Not a valid image. + if image.isNull() or (image.width() == 1 and image.height() == 1): + self._cache[(project_name, entity_id)] = None + continue + + # Scale down and maintain aspect ratio of the image + # but crop and center it to 16:9 + icon_width = 112 + icon_height = 63 + image = image.scaled(icon_width, icon_height, + QtCore.Qt.KeepAspectRatioByExpanding, + QtCore.Qt.SmoothTransformation) + + # Create a 32x32 square and center the image + square_image = QtGui.QImage( + icon_width, icon_height, QtGui.QImage.Format_ARGB32) + square_image.fill(QtCore.Qt.transparent) + painter = QtGui.QPainter(square_image) + x = (icon_width - image.width()) // 2 + y = (icon_height - image.height()) // 2 + painter.drawImage(x, y, image) + painter.end() + + self._cache[(project_name, entity_id)] = square_image + self.image_loaded.emit(item, square_image) + + class FoldersQtModel(QtGui.QStandardItemModel): """Folders model which cares about refresh of folders. @@ -46,6 +131,9 @@ def __init__(self, controller): self._has_content = False self._is_refreshing = False + self._image_loader = ImageLoader() + self._image_loader.image_loaded.connect(self._on_image_loaded) + @property def is_refreshing(self): """Model is refreshing. @@ -229,7 +317,7 @@ def _get_folder_item_icon( def _fill_item_data( self, item, - folder_item, + folder_item: FolderItem, folder_type_item_by_name, folder_type_icon_cache ): @@ -238,19 +326,42 @@ def _fill_item_data( Args: item (QtGui.QStandardItem): Item to fill data. folder_item (FolderItem): Folder item. + folder_type_item_by_name (dict): Folder type items by name. + folder_type_icon_cache (dict): Folder type icon cache. """ - icon = self._get_folder_item_icon( - folder_item, - folder_type_item_by_name, - folder_type_icon_cache - ) item.setData(folder_item.entity_id, FOLDER_ID_ROLE) item.setData(folder_item.name, FOLDER_NAME_ROLE) item.setData(folder_item.path, FOLDER_PATH_ROLE) item.setData(folder_item.folder_type, FOLDER_TYPE_ROLE) item.setData(folder_item.label, QtCore.Qt.DisplayRole) - item.setData(icon, QtCore.Qt.DecorationRole) + + project_name = self.get_project_name() + image = self._image_loader.get_image(project_name, + folder_item.entity_id) + if image: + pixmap = QtGui.QPixmap.fromImage(image) + #item.setIcon(QtGui.QIcon(pixmap)) + item.setData(pixmap, QtCore.Qt.DecorationRole) + else: + icon = self._get_folder_item_icon( + folder_item, + folder_type_item_by_name, + folder_type_icon_cache + ) + item.setData(icon, QtCore.Qt.DecorationRole) + + self._image_loader.load_image( + item, + project_name=project_name, + entity_id=folder_item.entity_id) + + def _on_image_loaded(self, + item: QtGui.QStandardItem, + image: QtGui.QImage): + pixmap = QtGui.QPixmap.fromImage(image) + #item.setIcon(QtGui.QIcon(pixmap)) + item.setData(pixmap, QtCore.Qt.DecorationRole) def _fill_items(self, folder_items_by_id, folder_type_items): if not folder_items_by_id: