Skip to content

Show Folder thumbnails in Launcher, Workfiles tool, Loader in the Folder Widget. #1144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/ayon_core/tools/loader/ui/folders_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
125 changes: 118 additions & 7 deletions client/ayon_core/tools/utils/folders_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
ProjectsModel,
HierarchyModel,
HierarchyExpectedSelection,
FolderItem
)
import ayon_api

from .models import RecursiveSortFilterProxyModel
from .views import TreeView
Expand All @@ -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.

Expand All @@ -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.
Expand Down Expand Up @@ -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
):
Expand All @@ -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:
Expand Down