Skip to content
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

Loader: Show statuses in loader #548

Merged
merged 12 commits into from
May 24, 2024
145 changes: 136 additions & 9 deletions client/ayon_core/tools/common_models/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import six

from ayon_core.style import get_default_entity_icon_color
from ayon_core.lib import CacheItem
from ayon_core.lib import CacheItem, NestedCacheItem

PROJECTS_MODEL_SENDER = "projects.model"

Expand All @@ -17,6 +17,49 @@ def emit_event(self, topic, data, source):
pass


class StatusItem:
"""Item representing status of project.

Args:
name (str): Status name ("Not ready").
color (str): Status color in hex ("#434a56").
short (str): Short status name ("NRD").
icon (str): Icon name in MaterialIcons ("fiber_new").
state (Literal["not_started", "in_progress", "done", "blocked"]):
Status state.

"""
def __init__(self, name, color, short, icon, state):
self.name = name
self.color = color
self.short = short
self.icon = icon
self.state = state

def to_data(self):
return {
"name": self.name,
"color": self.color,
"short": self.short,
"icon": self.icon,
"state": self.state,
}

@classmethod
def from_data(cls, data):
return cls(**data)

@classmethod
def from_project_item(cls, status_data):
return cls(
name=status_data["name"],
color=status_data["color"],
short=status_data["shortName"],
icon=status_data["icon"],
state=status_data["state"],
)


class ProjectItem:
"""Item representing folder entity on a server.

Expand All @@ -40,6 +83,23 @@ def __init__(self, name, active, is_library, icon=None):
}
self.icon = icon

@classmethod
def from_entity(cls, project_entity):
"""Creates folder item from entity.

Args:
project_entity (dict[str, Any]): Project entity.

Returns:
ProjectItem: Project item.

"""
return cls(
project_entity["name"],
project_entity["active"],
project_entity["library"],
)

def to_data(self):
"""Converts folder item to data.

Expand Down Expand Up @@ -79,26 +139,37 @@ def _get_project_items_from_entitiy(projects):
"""

return [
ProjectItem(project["name"], project["active"], project["library"])
ProjectItem.from_entity(project)
for project in projects
]


class ProjectsModel(object):
def __init__(self, controller):
self._projects_cache = CacheItem(default_factory=list)
self._project_items_by_name = {}
self._projects_by_name = {}
self._project_statuses_cache = NestedCacheItem(
levels=1, default_factory=list
)
self._projects_by_name = NestedCacheItem(
levels=1, default_factory=list
)

self._is_refreshing = False
self._controller = controller

def reset(self):
self._projects_cache.reset()
self._project_items_by_name = {}
self._projects_by_name = {}
self._project_statuses_cache.reset()
self._projects_by_name.reset()

def refresh(self):
"""Refresh project items.

This method will requery list of ProjectItem returned by
'get_project_items'.

To reset all cached items use 'reset' method.
"""
self._refresh_projects_cache()

def get_project_items(self, sender):
Expand All @@ -117,12 +188,51 @@ def get_project_items(self, sender):
return self._projects_cache.get_data()

def get_project_entity(self, project_name):
if project_name not in self._projects_by_name:
"""Get project entity.

Args:
project_name (str): Project name.

Returns:
Union[dict[str, Any], None]: Project entity or None if project
was not found by name.

"""
project_cache = self._projects_by_name[project_name]
if not project_cache.is_valid:
entity = None
if project_name:
entity = ayon_api.get_project(project_name)
self._projects_by_name[project_name] = entity
return self._projects_by_name[project_name]
project_cache.update_data(entity)
return project_cache.get_data()

def get_project_status_items(self, project_name, sender):
"""Get project status items.

Args:
project_name (str): Project name.
sender (Union[str, None]): Name of sender who asked for items.

Returns:
list[StatusItem]: Status items for project.

"""
statuses_cache = self._project_statuses_cache[project_name]
if not statuses_cache.is_valid:
with self._project_statuses_refresh_event_manager(
sender, project_name
):
project_entity = None
if project_name:
project_entity = self.get_project_entity(project_name)
statuses = []
if project_entity:
statuses = [
StatusItem.from_project_item(status)
for status in project_entity["statuses"]
]
statuses_cache.update_data(statuses)
return statuses_cache.get_data()

@contextlib.contextmanager
def _project_refresh_event_manager(self, sender):
Expand All @@ -143,6 +253,23 @@ def _project_refresh_event_manager(self, sender):
)
self._is_refreshing = False

@contextlib.contextmanager
def _project_statuses_refresh_event_manager(self, sender, project_name):
self._controller.emit_event(
"projects.statuses.refresh.started",
{"sender": sender, "project_name": project_name},
PROJECTS_MODEL_SENDER
)
try:
yield

finally:
self._controller.emit_event(
"projects.statuses.refresh.finished",
{"sender": sender, "project_name": project_name},
PROJECTS_MODEL_SENDER
)

def _refresh_projects_cache(self, sender=None):
if self._is_refreshing:
return None
Expand Down
25 changes: 25 additions & 0 deletions client/ayon_core/tools/loader/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class VersionItem:
thumbnail_id (Union[str, None]): Thumbnail id.
published_time (Union[str, None]): Published time in format
'%Y%m%dT%H%M%SZ'.
status (Union[str, None]): Status name.
author (Union[str, None]): Author.
frame_range (Union[str, None]): Frame range.
duration (Union[int, None]): Duration.
Expand All @@ -132,6 +133,7 @@ def __init__(
thumbnail_id,
published_time,
author,
status,
frame_range,
duration,
handles,
Expand All @@ -146,6 +148,7 @@ def __init__(
self.is_hero = is_hero
self.published_time = published_time
self.author = author
self.status = status
self.frame_range = frame_range
self.duration = duration
self.handles = handles
Expand Down Expand Up @@ -185,6 +188,7 @@ def to_data(self):
"is_hero": self.is_hero,
"published_time": self.published_time,
"author": self.author,
"status": self.status,
"frame_range": self.frame_range,
"duration": self.duration,
"handles": self.handles,
Expand Down Expand Up @@ -488,6 +492,27 @@ def get_project_items(self, sender=None):

pass

@abstractmethod
def get_project_status_items(self, project_name, sender=None):
"""Items for all projects available on server.

Triggers event topics "projects.statuses.refresh.started" and
"projects.statuses.refresh.finished" with data:
{
"sender": sender,
"project_name": project_name
}

Args:
project_name (Union[str, None]): Project name.
sender (Optional[str]): Sender who requested the items.

Returns:
list[StatusItem]: List of status items.
"""

pass

@abstractmethod
def get_product_items(self, project_name, folder_ids, sender=None):
"""Product items for folder ids.
Expand Down
5 changes: 5 additions & 0 deletions client/ayon_core/tools/loader/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ def expected_folder_selected(self, folder_id):
def get_project_items(self, sender=None):
return self._projects_model.get_project_items(sender)

def get_project_status_items(self, project_name, sender=None):
return self._projects_model.get_project_status_items(
project_name, sender
)

def get_folder_items(self, project_name, sender=None):
return self._hierarchy_model.get_folder_items(project_name, sender)

Expand Down
6 changes: 5 additions & 1 deletion client/ayon_core/tools/loader/models/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def version_item_from_entity(version):
thumbnail_id=version["thumbnailId"],
published_time=published_time,
author=author,
status=version["status"],
frame_range=frame_range,
duration=duration,
handles=handles,
Expand Down Expand Up @@ -526,8 +527,11 @@ def _query_product_items_by_ids(
products = list(ayon_api.get_products(project_name, **kwargs))
product_ids = {product["id"] for product in products}

# Add 'status' to fields -> fixed in ayon-python-api 1.0.4
fields = ayon_api.get_default_fields_for_type("version")
fields.add("status")
versions = ayon_api.get_versions(
project_name, product_ids=product_ids
project_name, product_ids=product_ids, fields=fields
)

return self._create_product_items(
Expand Down
46 changes: 46 additions & 0 deletions client/ayon_core/tools/loader/ui/products_delegates.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
from .products_model import (
PRODUCT_ID_ROLE,
VERSION_NAME_EDIT_ROLE,
VERSION_STATUS_NAME_ROLE,
VERSION_STATUS_SHORT_ROLE,
VERSION_STATUS_COLOR_ROLE,
VERSION_ID_ROLE,
PRODUCT_IN_SCENE_ROLE,
ACTIVE_SITE_ICON_ROLE,
Expand Down Expand Up @@ -194,6 +197,49 @@ def initStyleOption(self, option, index):
option.palette.setBrush(QtGui.QPalette.Text, color)


class StatusDelegate(QtWidgets.QStyledItemDelegate):
"""Delegate showing status name and short name."""

def paint(self, painter, option, index):
if option.widget:
style = option.widget.style()
else:
style = QtWidgets.QApplication.style()

style.drawControl(
style.CE_ItemViewItem, option, painter, option.widget
)

painter.save()

text_rect = style.subElementRect(style.SE_ItemViewItemText, option)
text_margin = style.proxy().pixelMetric(
style.PM_FocusFrameHMargin, option, option.widget
) + 1
padded_text_rect = text_rect.adjusted(
text_margin, 0, - text_margin, 0
)

fm = QtGui.QFontMetrics(option.font)
text = index.data(VERSION_STATUS_NAME_ROLE)
if padded_text_rect.width() < fm.width(text):
text = index.data(VERSION_STATUS_SHORT_ROLE)

status_color = index.data(VERSION_STATUS_COLOR_ROLE)
fg_color = QtGui.QColor(status_color)
pen = painter.pen()
pen.setColor(fg_color)
painter.setPen(pen)

painter.drawText(
padded_text_rect,
option.displayAlignment,
text
)

painter.restore()


class SiteSyncDelegate(QtWidgets.QStyledItemDelegate):
"""Paints icons and downloaded representation ration for both sites."""

Expand Down
Loading