Merge branch 'develop' of https://github.com/ynput/ayon-core into bugfix/AY-5750_AE-review-not-displayed-in-Ftrack

This commit is contained in:
Petr Kalis 2024-06-18 11:19:11 +02:00
commit 4f51ef76cf
141 changed files with 10340 additions and 759 deletions

View file

@ -3,6 +3,7 @@ import warnings
import ayon_api
from ayon_core.settings import get_studio_settings, get_project_settings
from ayon_core.pipeline.plugin_discover import (
discover,
register_plugin,
@ -40,7 +41,8 @@ class LauncherActionSelection:
task_name=None,
project_entity=None,
folder_entity=None,
task_entity=None
task_entity=None,
project_settings=None,
):
self._project_name = project_name
self._folder_id = folder_id
@ -53,6 +55,8 @@ class LauncherActionSelection:
self._folder_entity = folder_entity
self._task_entity = task_entity
self._project_settings = project_settings
def __getitem__(self, key):
warnings.warn(
(
@ -255,6 +259,22 @@ class LauncherActionSelection:
)
return self._task_entity
def get_project_settings(self):
"""Project settings for the selection.
Returns:
dict[str, Any]: Project settings or studio settings if
project is not selected.
"""
if self._project_settings is None:
if self._project_name is None:
settings = get_studio_settings()
else:
settings = get_project_settings(self._project_name)
self._project_settings = settings
return self._project_settings
@property
def is_project_selected(self):
"""Return whether a project is selected.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -5,7 +5,6 @@ from abc import ABCMeta, abstractmethod
import ayon_api
import six
from ayon_core.style import get_default_entity_icon_color
from ayon_core.lib import NestedCacheItem
HIERARCHY_MODEL_SENDER = "hierarchy.model"
@ -31,11 +30,10 @@ class FolderItem:
path (str): Folder path.
folder_type (str): Type of folder.
label (Union[str, None]): Folder label.
icon (Union[dict[str, Any], None]): Icon definition.
"""
def __init__(
self, entity_id, parent_id, name, path, folder_type, label, icon
self, entity_id, parent_id, name, path, folder_type, label
):
self.entity_id = entity_id
self.parent_id = parent_id
@ -43,13 +41,6 @@ class FolderItem:
self.path = path
self.folder_type = folder_type
self.label = label or name
if not icon:
icon = {
"type": "awesome-font",
"name": "fa.folder",
"color": get_default_entity_icon_color()
}
self.icon = icon
def to_data(self):
"""Converts folder item to data.
@ -65,7 +56,6 @@ class FolderItem:
"path": self.path,
"folder_type": self.folder_type,
"label": self.label,
"icon": self.icon,
}
@classmethod
@ -95,23 +85,15 @@ class TaskItem:
name (str): Name of task.
task_type (str): Type of task.
parent_id (str): Parent folder id.
icon (Union[dict[str, Any], None]): Icon definitions.
"""
def __init__(
self, task_id, name, task_type, parent_id, icon
self, task_id, name, task_type, parent_id
):
self.task_id = task_id
self.name = name
self.task_type = task_type
self.parent_id = parent_id
if icon is None:
icon = {
"type": "awesome-font",
"name": "fa.male",
"color": get_default_entity_icon_color()
}
self.icon = icon
self._label = None
@ -149,7 +131,6 @@ class TaskItem:
"name": self.name,
"parent_id": self.parent_id,
"task_type": self.task_type,
"icon": self.icon,
}
@classmethod
@ -180,8 +161,7 @@ def _get_task_items_from_tasks(tasks):
task["id"],
task["name"],
task["type"],
folder_id,
None
folder_id
))
return output
@ -197,8 +177,7 @@ def _get_folder_item_from_hierarchy_item(item):
name,
path,
item["folderType"],
item["label"],
None,
item["label"]
)
@ -210,8 +189,7 @@ def _get_folder_item_from_entity(entity):
name,
entity["path"],
entity["folderType"],
entity["label"] or name,
None,
entity["label"] or name
)

View file

@ -60,6 +60,74 @@ class StatusItem:
)
class FolderTypeItem:
"""Item representing folder type of project.
Args:
name (str): Folder type name ("Shot").
short (str): Short folder type name ("sh").
icon (str): Icon name in MaterialIcons ("fiber_new").
"""
def __init__(self, name, short, icon):
self.name = name
self.short = short
self.icon = icon
def to_data(self):
return {
"name": self.name,
"short": self.short,
"icon": self.icon,
}
@classmethod
def from_data(cls, data):
return cls(**data)
@classmethod
def from_project_item(cls, folder_type_data):
return cls(
name=folder_type_data["name"],
short=folder_type_data["shortName"],
icon=folder_type_data["icon"],
)
class TaskTypeItem:
"""Item representing task type of project.
Args:
name (str): Task type name ("Shot").
short (str): Short task type name ("sh").
icon (str): Icon name in MaterialIcons ("fiber_new").
"""
def __init__(self, name, short, icon):
self.name = name
self.short = short
self.icon = icon
def to_data(self):
return {
"name": self.name,
"short": self.short,
"icon": self.icon,
}
@classmethod
def from_data(cls, data):
return cls(**data)
@classmethod
def from_project_item(cls, task_type_data):
return cls(
name=task_type_data["name"],
short=task_type_data["shortName"],
icon=task_type_data["icon"],
)
class ProjectItem:
"""Item representing folder entity on a server.
@ -147,19 +215,21 @@ def _get_project_items_from_entitiy(projects):
class ProjectsModel(object):
def __init__(self, controller):
self._projects_cache = CacheItem(default_factory=list)
self._project_statuses_cache = NestedCacheItem(
levels=1, default_factory=list
)
self._projects_by_name = NestedCacheItem(
levels=1, default_factory=list
)
self._project_statuses_cache = {}
self._folder_types_cache = {}
self._task_types_cache = {}
self._is_refreshing = False
self._controller = controller
def reset(self):
self._project_statuses_cache = {}
self._folder_types_cache = {}
self._task_types_cache = {}
self._projects_cache.reset()
self._project_statuses_cache.reset()
self._projects_by_name.reset()
def refresh(self):
@ -217,22 +287,87 @@ class ProjectsModel(object):
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
if project_name is None:
return []
statuses_cache = self._project_statuses_cache.get(project_name)
if (
statuses_cache is not None
and not self._projects_cache.is_valid
):
statuses_cache = None
if statuses_cache is None:
with self._project_items_refresh_event_manager(
sender, project_name, "statuses"
):
project_entity = None
if project_name:
project_entity = self.get_project_entity(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()
statuses_cache = statuses
self._project_statuses_cache[project_name] = statuses_cache
return list(statuses_cache)
def get_folder_type_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[FolderType]: Folder type items for project.
"""
return self._get_project_items(
project_name,
sender,
"folder_types",
self._folder_types_cache,
self._folder_type_items_getter,
)
def get_task_type_items(self, project_name, sender):
"""Get project task type items.
Args:
project_name (str): Project name.
sender (Union[str, None]): Name of sender who asked for items.
Returns:
list[TaskTypeItem]: Task type items for project.
"""
return self._get_project_items(
project_name,
sender,
"task_types",
self._task_types_cache,
self._task_type_items_getter,
)
def _get_project_items(
self, project_name, sender, item_type, cache_obj, getter
):
if (
project_name in cache_obj
and (
project_name is None
or self._projects_by_name[project_name].is_valid
)
):
return cache_obj[project_name]
with self._project_items_refresh_event_manager(
sender, project_name, item_type
):
cache_value = getter(self.get_project_entity(project_name))
cache_obj[project_name] = cache_value
return cache_value
@contextlib.contextmanager
def _project_refresh_event_manager(self, sender):
@ -254,9 +389,11 @@ class ProjectsModel(object):
self._is_refreshing = False
@contextlib.contextmanager
def _project_statuses_refresh_event_manager(self, sender, project_name):
def _project_items_refresh_event_manager(
self, sender, project_name, item_type
):
self._controller.emit_event(
"projects.statuses.refresh.started",
f"projects.{item_type}.refresh.started",
{"sender": sender, "project_name": project_name},
PROJECTS_MODEL_SENDER
)
@ -265,7 +402,7 @@ class ProjectsModel(object):
finally:
self._controller.emit_event(
"projects.statuses.refresh.finished",
f"projects.{item_type}.refresh.finished",
{"sender": sender, "project_name": project_name},
PROJECTS_MODEL_SENDER
)
@ -282,3 +419,27 @@ class ProjectsModel(object):
def _query_projects(self):
projects = ayon_api.get_projects(fields=["name", "active", "library"])
return _get_project_items_from_entitiy(projects)
def _status_items_getter(self, project_entity):
if not project_entity:
return []
return [
StatusItem.from_project_item(status)
for status in project_entity["statuses"]
]
def _folder_type_items_getter(self, project_entity):
if not project_entity:
return []
return [
FolderTypeItem.from_project_item(folder_type)
for folder_type in project_entity["folderTypes"]
]
def _task_type_items_getter(self, project_entity):
if not project_entity:
return []
return [
TaskTypeItem.from_project_item(task_type)
for task_type in project_entity["taskTypes"]
]

View file

@ -1,6 +1,80 @@
import ayon_api
import json
import collections
from ayon_core.lib import CacheItem
import ayon_api
from ayon_api.graphql import FIELD_VALUE, GraphQlQuery, fields_to_dict
from ayon_core.lib import NestedCacheItem
# --- Implementation that should be in ayon-python-api ---
# The implementation is not available in all versions of ayon-python-api.
def users_graphql_query(fields):
query = GraphQlQuery("Users")
names_var = query.add_variable("userNames", "[String!]")
project_name_var = query.add_variable("projectName", "String!")
users_field = query.add_field_with_edges("users")
users_field.set_filter("names", names_var)
users_field.set_filter("projectName", project_name_var)
nested_fields = fields_to_dict(set(fields))
query_queue = collections.deque()
for key, value in nested_fields.items():
query_queue.append((key, value, users_field))
while query_queue:
item = query_queue.popleft()
key, value, parent = item
field = parent.add_field(key)
if value is FIELD_VALUE:
continue
for k, v in value.items():
query_queue.append((k, v, field))
return query
def get_users(project_name=None, usernames=None, fields=None):
"""Get Users.
Only administrators and managers can fetch all users. For other users
it is required to pass in 'project_name' filter.
Args:
project_name (Optional[str]): Project name.
usernames (Optional[Iterable[str]]): Filter by usernames.
fields (Optional[Iterable[str]]): Fields to be queried
for users.
Returns:
Generator[dict[str, Any]]: Queried users.
"""
filters = {}
if usernames is not None:
usernames = set(usernames)
if not usernames:
return
filters["userNames"] = list(usernames)
if project_name is not None:
filters["projectName"] = project_name
con = ayon_api.get_server_api_connection()
if not fields:
fields = con.get_default_fields_for_type("user")
query = users_graphql_query(set(fields))
for attr, filter_value in filters.items():
query.set_variable_value(attr, filter_value)
for parsed_data in query.continuous_query(con):
for user in parsed_data["users"]:
user["accessGroups"] = json.loads(user["accessGroups"])
yield user
# --- END of ayon-python-api implementation ---
class UserItem:
@ -32,19 +106,19 @@ class UserItem:
class UsersModel:
def __init__(self, controller):
self._controller = controller
self._users_cache = CacheItem(default_factory=list)
self._users_cache = NestedCacheItem(default_factory=list)
def get_user_items(self):
def get_user_items(self, project_name):
"""Get user items.
Returns:
List[UserItem]: List of user items.
"""
self._invalidate_cache()
return self._users_cache.get_data()
self._invalidate_cache(project_name)
return self._users_cache[project_name].get_data()
def get_user_items_by_name(self):
def get_user_items_by_name(self, project_name):
"""Get user items by name.
Implemented as most of cases using this model will need to find
@ -56,10 +130,10 @@ class UsersModel:
"""
return {
user_item.username: user_item
for user_item in self.get_user_items()
for user_item in self.get_user_items(project_name)
}
def get_user_item_by_username(self, username):
def get_user_item_by_username(self, project_name, username):
"""Get user item by username.
Args:
@ -69,16 +143,22 @@ class UsersModel:
Union[UserItem, None]: User item or None if not found.
"""
self._invalidate_cache()
for user_item in self.get_user_items():
self._invalidate_cache(project_name)
for user_item in self.get_user_items(project_name):
if user_item.username == username:
return user_item
return None
def _invalidate_cache(self):
if self._users_cache.is_valid:
def _invalidate_cache(self, project_name):
cache = self._users_cache[project_name]
if cache.is_valid:
return
self._users_cache.update_data([
if project_name is None:
cache.update_data([])
return
self._users_cache[project_name].update_data([
UserItem.from_entity_data(user)
for user in ayon_api.get_users()
for user in get_users(project_name)
])

View file

@ -1,7 +1,8 @@
from .window import ContextDialog, main
from .window import ContextDialog, main, ask_for_context
__all__ = (
"ContextDialog",
"main",
"ask_for_context"
)

View file

@ -227,6 +227,16 @@ class ContextDialogController:
def get_project_items(self, sender=None):
return self._projects_model.get_project_items(sender)
def get_folder_type_items(self, project_name, sender=None):
return self._projects_model.get_folder_type_items(
project_name, sender
)
def get_task_type_items(self, project_name, sender=None):
return self._projects_model.get_task_type_items(
project_name, sender
)
def get_folder_items(self, project_name, sender=None):
return self._hierarchy_model.get_folder_items(project_name, sender)
@ -791,3 +801,12 @@ def main(
window.show()
app.exec_()
controller.store_output()
def ask_for_context(strict=True):
controller = ContextDialogController()
controller.set_strict(strict)
window = ContextDialog(controller=controller)
window.exec_()
return controller.get_selected_context()

View file

@ -104,8 +104,48 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
Returns:
list[ProjectItem]: Minimum possible information needed
for visualisation of folder hierarchy.
"""
"""
pass
@abstractmethod
def get_folder_type_items(self, project_name, sender=None):
"""Folder type items for a project.
This function may trigger events with topics
'projects.folder_types.refresh.started' and
'projects.folder_types.refresh.finished' which will contain 'sender'
value in data.
That may help to avoid re-refresh of items in UI elements.
Args:
project_name (str): Project name.
sender (str): Who requested folder type items.
Returns:
list[FolderTypeItem]: Folder type information.
"""
pass
@abstractmethod
def get_task_type_items(self, project_name, sender=None):
"""Task type items for a project.
This function may trigger events with topics
'projects.task_types.refresh.started' and
'projects.task_types.refresh.finished' which will contain 'sender'
value in data.
That may help to avoid re-refresh of items in UI elements.
Args:
project_name (str): Project name.
sender (str): Who requested task type items.
Returns:
list[TaskTypeItem]: Task type information.
"""
pass
@abstractmethod

View file

@ -59,12 +59,23 @@ class BaseLauncherController(
def get_project_items(self, sender=None):
return self._projects_model.get_project_items(sender)
def get_folder_type_items(self, project_name, sender=None):
return self._projects_model.get_folder_type_items(
project_name, sender
)
def get_task_type_items(self, project_name, sender=None):
return self._projects_model.get_task_type_items(
project_name, sender
)
def get_folder_items(self, project_name, sender=None):
return self._hierarchy_model.get_folder_items(project_name, sender)
def get_task_items(self, project_name, folder_id, sender=None):
return self._hierarchy_model.get_task_items(
project_name, folder_id, sender)
project_name, folder_id, sender
)
# Project settings for applications actions
def get_project_settings(self, project_name):

View file

@ -10,6 +10,108 @@ from ayon_core.pipeline.actions import (
)
from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch
try:
# Available since applications addon 0.2.4
from ayon_applications.action import ApplicationAction
except ImportError:
# Backwards compatibility from 0.3.3 (24/06/10)
# TODO: Remove in future releases
class ApplicationAction(LauncherAction):
"""Action to launch an application.
Application action based on 'ApplicationManager' system.
Handling of applications in launcher is not ideal and should be completely
redone from scratch. This is just a temporary solution to keep backwards
compatibility with AYON launcher.
Todos:
Move handling of errors to frontend.
"""
# Application object
application = None
# Action attributes
name = None
label = None
label_variant = None
group = None
icon = None
color = None
order = 0
data = {}
project_settings = {}
project_entities = {}
_log = None
@property
def log(self):
if self._log is None:
self._log = Logger.get_logger(self.__class__.__name__)
return self._log
def is_compatible(self, selection):
if not selection.is_task_selected:
return False
project_entity = self.project_entities[selection.project_name]
apps = project_entity["attrib"].get("applications")
if not apps or self.application.full_name not in apps:
return False
project_settings = self.project_settings[selection.project_name]
only_available = project_settings["applications"]["only_available"]
if only_available and not self.application.find_executable():
return False
return True
def _show_message_box(self, title, message, details=None):
from qtpy import QtWidgets, QtGui
from ayon_core import style
dialog = QtWidgets.QMessageBox()
icon = QtGui.QIcon(resources.get_ayon_icon_filepath())
dialog.setWindowIcon(icon)
dialog.setStyleSheet(style.load_stylesheet())
dialog.setWindowTitle(title)
dialog.setText(message)
if details:
dialog.setDetailedText(details)
dialog.exec_()
def process(self, selection, **kwargs):
"""Process the full Application action"""
from ayon_applications import (
ApplicationExecutableNotFound,
ApplicationLaunchFailed,
)
try:
self.application.launch(
project_name=selection.project_name,
folder_path=selection.folder_path,
task_name=selection.task_name,
**self.data
)
except ApplicationExecutableNotFound as exc:
details = exc.details
msg = exc.msg
log_msg = str(msg)
if details:
log_msg += "\n" + details
self.log.warning(log_msg)
self._show_message_box(
"Application executable not found", msg, details
)
except ApplicationLaunchFailed as exc:
msg = str(exc)
self.log.warning(msg, exc_info=True)
self._show_message_box("Application launch failed", msg)
# class Action:
# def __init__(self, label, icon=None, identifier=None):
@ -43,103 +145,6 @@ from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch
# self._actions.append(action)
class ApplicationAction(LauncherAction):
"""Action to launch an application.
Application action based on 'ApplicationManager' system.
Handling of applications in launcher is not ideal and should be completely
redone from scratch. This is just a temporary solution to keep backwards
compatibility with AYON launcher.
Todos:
Move handling of errors to frontend.
"""
# Application object
application = None
# Action attributes
name = None
label = None
label_variant = None
group = None
icon = None
color = None
order = 0
data = {}
project_settings = {}
project_entities = {}
_log = None
@property
def log(self):
if self._log is None:
self._log = Logger.get_logger(self.__class__.__name__)
return self._log
def is_compatible(self, selection):
if not selection.is_task_selected:
return False
project_entity = self.project_entities[selection.project_name]
apps = project_entity["attrib"].get("applications")
if not apps or self.application.full_name not in apps:
return False
project_settings = self.project_settings[selection.project_name]
only_available = project_settings["applications"]["only_available"]
if only_available and not self.application.find_executable():
return False
return True
def _show_message_box(self, title, message, details=None):
from qtpy import QtWidgets, QtGui
from ayon_core import style
dialog = QtWidgets.QMessageBox()
icon = QtGui.QIcon(resources.get_ayon_icon_filepath())
dialog.setWindowIcon(icon)
dialog.setStyleSheet(style.load_stylesheet())
dialog.setWindowTitle(title)
dialog.setText(message)
if details:
dialog.setDetailedText(details)
dialog.exec_()
def process(self, selection, **kwargs):
"""Process the full Application action"""
from ayon_applications import (
ApplicationExecutableNotFound,
ApplicationLaunchFailed,
)
try:
self.application.launch(
project_name=selection.project_name,
folder_path=selection.folder_path,
task_name=selection.task_name,
**self.data
)
except ApplicationExecutableNotFound as exc:
details = exc.details
msg = exc.msg
log_msg = str(msg)
if details:
log_msg += "\n" + details
self.log.warning(log_msg)
self._show_message_box(
"Application executable not found", msg, details
)
except ApplicationLaunchFailed as exc:
msg = str(exc)
self.log.warning(msg, exc_info=True)
self._show_message_box("Application launch failed", msg)
class ActionItem:
"""Item representing single action to trigger.
@ -440,7 +445,17 @@ class ActionsModel:
)
def _prepare_selection(self, project_name, folder_id, task_id):
return LauncherActionSelection(project_name, folder_id, task_id)
project_entity = None
if project_name:
project_entity = self._controller.get_project_entity(project_name)
project_settings = self._controller.get_project_settings(project_name)
return LauncherActionSelection(
project_name,
folder_id,
task_id,
project_entity=project_entity,
project_settings=project_settings,
)
def _get_discovered_action_classes(self):
if self._discovered_actions is None:
@ -475,7 +490,9 @@ class ActionsModel:
action_items = {}
for identifier, action in self._get_action_objects().items():
is_application = isinstance(action, ApplicationAction)
if is_application:
# Backwards compatibility from 0.3.3 (24/06/10)
# TODO: Remove in future releases
if is_application and hasattr(action, "project_settings"):
action.project_entities[project_name] = project_entity
action.project_settings[project_name] = project_settings
@ -497,10 +514,14 @@ class ActionsModel:
return action_items
def _get_applications_action_classes(self):
actions = []
addons_manager = self._get_addons_manager()
applications_addon = addons_manager.get_enabled_addon("applications")
if hasattr(applications_addon, "get_applications_action_classes"):
return applications_addon.get_applications_action_classes()
# Backwards compatibility from 0.3.3 (24/06/10)
# TODO: Remove in future releases
actions = []
if applications_addon is None:
return actions

View file

@ -510,6 +510,26 @@ class FrontendLoaderController(_BaseLoaderController):
pass
@abstractmethod
def get_folder_type_items(self, project_name, sender=None):
"""Folder type items for a project.
This function may trigger events with topics
'projects.folder_types.refresh.started' and
'projects.folder_types.refresh.finished' which will contain 'sender'
value in data.
That may help to avoid re-refresh of items in UI elements.
Args:
project_name (str): Project name.
sender (str): Who requested folder type items.
Returns:
list[FolderTypeItem]: Folder type information.
"""
pass
@abstractmethod
def get_project_status_items(self, project_name, sender=None):
"""Items for all projects available on server.

View file

@ -3,14 +3,13 @@ import uuid
import ayon_api
from ayon_core.lib import NestedCacheItem, CacheItem
from ayon_core.lib.events import QueuedEventSystem
from ayon_core.pipeline import Anatomy, get_current_context
from ayon_core.host import ILoadHost
from ayon_core.tools.common_models import (
ProjectsModel,
HierarchyModel,
NestedCacheItem,
CacheItem,
ThumbnailsModel,
)
@ -180,6 +179,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
def get_project_items(self, sender=None):
return self._projects_model.get_project_items(sender)
def get_folder_type_items(self, project_name, sender=None):
return self._projects_model.get_folder_type_items(
project_name, sender
)
def get_project_status_items(self, project_name, sender=None):
return self._projects_model.get_project_status_items(
project_name, sender

View file

@ -188,16 +188,6 @@ class LoaderFoldersModel(FoldersQtModel):
self._colored_items = set()
def _fill_item_data(self, item, folder_item):
"""
Args:
item (QtGui.QStandardItem): Item to fill data.
folder_item (FolderItem): Folder item.
"""
super(LoaderFoldersModel, self)._fill_item_data(item, folder_item)
def set_merged_products_selection(self, items):
changes = {
folder_id: None

View file

@ -126,6 +126,7 @@ class ProductsModel(QtGui.QStandardItemModel):
self._last_project_name = None
self._last_folder_ids = []
self._last_project_statuses = {}
self._last_status_icons_by_name = {}
def get_product_item_indexes(self):
return [
@ -181,6 +182,13 @@ class ProductsModel(QtGui.QStandardItemModel):
return status_item.color
col = index.column()
if col == self.status_col and role == QtCore.Qt.DecorationRole:
role = VERSION_STATUS_ICON_ROLE
if role == VERSION_STATUS_ICON_ROLE:
status_name = self.data(index, VERSION_STATUS_NAME_ROLE)
return self._get_status_icon(status_name)
if col == 0:
return super(ProductsModel, self).data(index, role)
@ -260,6 +268,25 @@ class ProductsModel(QtGui.QStandardItemModel):
break
yield color
def _get_status_icon(self, status_name):
icon = self._last_status_icons_by_name.get(status_name)
if icon is not None:
return icon
status_item = self._last_project_statuses.get(status_name)
if status_item is not None:
icon = get_qt_icon({
"type": "material-symbols",
"name": status_item.icon,
"color": status_item.color,
})
if icon is None:
icon = QtGui.QIcon()
self._last_status_icons_by_name[status_name] = icon
return icon
def _clear(self):
root_item = self.invisibleRootItem()
root_item.removeRows(0, root_item.rowCount())
@ -419,6 +446,7 @@ class ProductsModel(QtGui.QStandardItemModel):
status_item.name: status_item
for status_item in status_items
}
self._last_status_icons_by_name = {}
active_site_icon_def = self._controller.get_active_site_icon_def(
project_name

View file

@ -39,7 +39,7 @@ from ayon_core.pipeline.create.context import (
ConvertorsOperationFailed,
)
from ayon_core.pipeline.publish import get_publish_instance_label
from ayon_core.tools.common_models import HierarchyModel
from ayon_core.tools.common_models import ProjectsModel, HierarchyModel
from ayon_core.lib.profiles_filtering import filter_profiles
# Define constant for plugin orders offset
@ -1632,6 +1632,7 @@ class PublisherController(BasePublisherController):
self._resetting_instances = False
# Cacher of avalon documents
self._projects_model = ProjectsModel(self)
self._hierarchy_model = HierarchyModel(self)
@property
@ -1697,6 +1698,16 @@ class PublisherController(BasePublisherController):
return self._create_context.get_current_project_settings()
def get_folder_type_items(self, project_name, sender=None):
return self._projects_model.get_folder_type_items(
project_name, sender
)
def get_task_type_items(self, project_name, sender=None):
return self._projects_model.get_task_type_items(
project_name, sender
)
# Hierarchy model
def get_folder_items(self, project_name, sender=None):
return self._hierarchy_model.get_folder_items(project_name, sender)
@ -1725,14 +1736,14 @@ class PublisherController(BasePublisherController):
return folder_item.entity_id
return None
def get_task_names_by_folder_paths(self, folder_paths):
def get_task_items_by_folder_paths(self, folder_paths):
if not folder_paths:
return {}
folder_items = self._hierarchy_model.get_folder_items_by_paths(
self.project_name, folder_paths
)
output = {
folder_path: set()
folder_path: []
for folder_path in folder_paths
}
project_name = self.project_name
@ -1740,10 +1751,7 @@ class PublisherController(BasePublisherController):
task_items = self._hierarchy_model.get_task_items(
project_name, folder_item.entity_id, None
)
output[folder_item.path] = {
task_item.name
for task_item in task_items
}
output[folder_item.path] = task_items
return output

View file

@ -128,6 +128,16 @@ class CreateHierarchyController:
project_name, folder_id, sender
)
def get_folder_type_items(self, project_name, sender=None):
return self._controller.get_folder_type_items(
project_name, sender
)
def get_task_type_items(self, project_name, sender=None):
return self._controller.get_task_type_items(
project_name, sender
)
# Selection model
def set_selected_project(self, project_name):
self._selection_model.set_selected_project(project_name)

View file

@ -26,6 +26,11 @@ class FoldersDialogController:
def get_folder_items(self, project_name, sender=None):
return self._controller.get_folder_items(project_name, sender)
def get_folder_type_items(self, project_name, sender=None):
return self._controller.get_folder_type_items(
project_name, sender
)
def set_selected_folder(self, folder_id):
pass

View file

@ -99,12 +99,16 @@ class TasksModel(QtGui.QStandardItemModel):
root_item.removeRows(0, self.rowCount())
return
task_names_by_folder_path = (
self._controller.get_task_names_by_folder_paths(
task_items_by_folder_path = (
self._controller.get_task_items_by_folder_paths(
self._folder_paths
)
)
task_names_by_folder_path = {
folder_path: {item.name for item in task_items}
for folder_path, task_items in task_items_by_folder_path.items()
}
self._task_names_by_folder_path = task_names_by_folder_path
new_task_names = self.get_intersection_of_tasks(
@ -122,22 +126,54 @@ class TasksModel(QtGui.QStandardItemModel):
item = self._items_by_name.pop(task_name)
root_item.removeRow(item.row())
icon = get_qt_icon({
default_icon = get_qt_icon({
"type": "awesome-font",
"name": "fa.male",
"color": get_default_entity_icon_color(),
})
new_items = []
task_type_items = {
task_type_item.name: task_type_item
for task_type_item in self._controller.get_task_type_items(
self._controller.project_name
)
}
icon_name_by_task_name = {}
for task_items in task_items_by_folder_path.values():
for task_item in task_items:
task_name = task_item.name
if (
task_name not in new_task_names
or task_name in icon_name_by_task_name
):
continue
task_type_name = task_item.task_type
task_type_item = task_type_items.get(task_type_name)
if task_type_item:
icon_name_by_task_name[task_name] = task_type_item.icon
for task_name in new_task_names:
if task_name in self._items_by_name:
item = self._items_by_name.get(task_name)
if item is None:
item = QtGui.QStandardItem(task_name)
item.setData(task_name, TASK_NAME_ROLE)
self._items_by_name[task_name] = item
new_items.append(item)
if not task_name:
continue
item = QtGui.QStandardItem(task_name)
item.setData(task_name, TASK_NAME_ROLE)
if task_name:
item.setData(icon, QtCore.Qt.DecorationRole)
self._items_by_name[task_name] = item
new_items.append(item)
icon_name = icon_name_by_task_name.get(task_name)
icon = None
if icon_name:
icon = get_qt_icon({
"type": "material-symbols",
"name": icon_name,
"color": get_default_entity_icon_color(),
})
if icon is None:
icon = default_icon
item.setData(icon, QtCore.Qt.DecorationRole)
if new_items:
root_item.appendRows(new_items)

View file

@ -116,6 +116,9 @@ class InventoryModel(QtGui.QStandardItemModel):
self._default_icon_color = get_default_entity_icon_color()
self._last_project_statuses = {}
self._last_status_icons_by_name = {}
def outdated(self, item):
return item.get("isOutdated", True)
@ -159,10 +162,11 @@ class InventoryModel(QtGui.QStandardItemModel):
self._controller.get_site_provider_icons().items()
)
}
status_items_by_name = {
self._last_project_statuses = {
status_item.name: status_item
for status_item in self._controller.get_project_status_items()
}
self._last_status_icons_by_name = {}
group_item_icon = qtawesome.icon(
"fa.folder", color=self._default_icon_color
@ -186,7 +190,6 @@ class InventoryModel(QtGui.QStandardItemModel):
remote_site_icon = site_icons.get(sites_info["remote_site_provider"])
root_item = self.invisibleRootItem()
group_items = []
for repre_id, container_items in items_by_repre_id.items():
repre_info = repre_info_by_id[repre_id]
@ -195,8 +198,6 @@ class InventoryModel(QtGui.QStandardItemModel):
is_latest = False
is_hero = False
status_name = None
status_color = None
status_short = None
if not repre_info.is_valid:
group_name = "< Entity N/A >"
item_icon = invalid_item_icon
@ -219,10 +220,10 @@ class InventoryModel(QtGui.QStandardItemModel):
if not is_latest:
version_color = self.OUTDATED_COLOR
status_name = version_item.status
status_item = status_items_by_name.get(status_name)
if status_item:
status_short = status_item.short
status_color = status_item.color
status_color, status_short, status_icon = self._get_status_data(
status_name
)
container_model_items = []
for container_item in container_items:
@ -273,6 +274,7 @@ class InventoryModel(QtGui.QStandardItemModel):
group_item.setData(status_name, STATUS_NAME_ROLE)
group_item.setData(status_short, STATUS_SHORT_ROLE)
group_item.setData(status_color, STATUS_COLOR_ROLE)
group_item.setData(status_icon, STATUS_ICON_ROLE)
group_item.setData(
active_site_progress, ACTIVE_SITE_PROGRESS_ROLE
@ -355,6 +357,32 @@ class InventoryModel(QtGui.QStandardItemModel):
root_item = self.invisibleRootItem()
root_item.removeRows(0, root_item.rowCount())
def _get_status_data(self, status_name):
status_item = self._last_project_statuses.get(status_name)
status_icon = self._get_status_icon(status_name, status_item)
status_color = status_short = None
if status_item is not None:
status_color = status_item.color
status_short = status_item.short
return status_color, status_short, status_icon
def _get_status_icon(self, status_name, status_item):
icon = self._last_status_icons_by_name.get(status_name)
if icon is not None:
return icon
icon = None
if status_item is not None:
icon = get_qt_icon({
"type": "material-symbols",
"name": status_item.icon,
"color": status_item.color,
})
if icon is None:
icon = QtGui.QIcon()
self._last_status_icons_by_name[status_name] = icon
return icon
class FilterProxyModel(QtCore.QSortFilterProxyModel):
"""Filter model to where key column's value is in the filtered tags"""

View file

@ -2,6 +2,7 @@ import uuid
from qtpy import QtWidgets, QtCore, QtGui
from ayon_core.tools.utils import get_qt_icon
from ayon_core.tools.utils.delegates import StatusDelegate
from .model import (
@ -20,13 +21,15 @@ class VersionOption:
label,
status_name,
status_short,
status_color
status_color,
status_icon,
):
self.version = version
self.label = label
self.status_name = status_name
self.status_short = status_short
self.status_color = status_color
self.status_icon = status_icon
class SelectVersionModel(QtGui.QStandardItemModel):
@ -67,8 +70,12 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
self._combo_view = combo_view
self._status_delegate = status_delegate
self._items_by_id = {}
self._status_visible = True
def paintEvent(self, event):
if not self._status_visible:
return super().paintEvent(event)
painter = QtWidgets.QStylePainter(self)
option = QtWidgets.QStyleOptionComboBox()
self.initStyleOption(option)
@ -80,27 +87,52 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
return
painter.save()
text_field_rect = self.style().subControlRect(
status_icon = self.itemData(idx, STATUS_ICON_ROLE)
content_field_rect = self.style().subControlRect(
QtWidgets.QStyle.CC_ComboBox,
option,
QtWidgets.QStyle.SC_ComboBoxEditField
)
adj_rect = text_field_rect.adjusted(1, 0, -1, 0)
).adjusted(1, 0, -1, 0)
metrics = option.fontMetrics
version_text_width = metrics.width(option.currentText) + 2
version_text_rect = QtCore.QRect(content_field_rect)
version_text_rect.setWidth(version_text_width)
painter.drawText(
adj_rect,
version_text_rect,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
option.currentText
)
metrics = QtGui.QFontMetrics(self.font())
text_width = metrics.width(option.currentText)
x_offset = text_width + 2
diff_width = adj_rect.width() - x_offset
if diff_width <= 0:
status_text_rect = QtCore.QRect(content_field_rect)
status_text_rect.setLeft(version_text_rect.right() + 2)
if status_icon is not None and not status_icon.isNull():
icon_rect = QtCore.QRect(status_text_rect)
diff = icon_rect.height() - metrics.height()
if diff < 0:
diff = 0
top_offset = diff // 2
bottom_offset = diff - top_offset
icon_rect.adjust(0, top_offset, 0, -bottom_offset)
icon_rect.setWidth(metrics.height())
status_icon.paint(
painter,
icon_rect,
QtCore.Qt.AlignCenter,
QtGui.QIcon.Normal,
QtGui.QIcon.On
)
status_text_rect.setLeft(icon_rect.right() + 2)
if status_text_rect.width() <= 0:
return
status_rect = adj_rect.adjusted(x_offset + 2, 0, 0, 0)
if diff_width < metrics.width(status_name):
if status_text_rect.width() < metrics.width(status_name):
status_name = self.itemData(idx, STATUS_SHORT_ROLE)
if status_text_rect.width() < metrics.width(status_name):
status_name = ""
color = QtGui.QColor(self.itemData(idx, STATUS_COLOR_ROLE))
@ -108,7 +140,7 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
pen.setColor(color)
painter.setPen(pen)
painter.drawText(
status_rect,
status_text_rect,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
status_name
)
@ -120,6 +152,12 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
self.setCurrentIndex(index)
def set_status_visible(self, visible):
header = self._combo_view.header()
header.setSectionHidden(1, not visible)
self._status_visible = visible
self.update()
def get_item_by_id(self, item_id):
return self._items_by_id[item_id]
@ -130,7 +168,17 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
root_item.removeRows(0, root_item.rowCount())
new_items = []
icons_by_name = {}
for version_option in version_options:
icon = icons_by_name.get(version_option.status_icon)
if icon is None:
icon = get_qt_icon({
"type": "material-symbols",
"name": version_option.status_icon,
"color": version_option.status_color
})
icons_by_name[version_option.status_icon] = icon
item_id = uuid.uuid4().hex
item = QtGui.QStandardItem(version_option.label)
item.setColumnCount(root_item.columnCount())
@ -143,6 +191,7 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
item.setData(
version_option.status_color, STATUS_COLOR_ROLE
)
item.setData(icon, STATUS_ICON_ROLE)
item.setData(item_id, ITEM_ID_ROLE)
new_items.append(item)
@ -195,10 +244,16 @@ class SelectVersionDialog(QtWidgets.QDialog):
def select_index(self, index):
self._versions_combobox.set_current_index(index)
def set_status_visible(self, visible):
self._versions_combobox.set_status_visible(visible)
@classmethod
def ask_for_version(cls, version_options, index=None, parent=None):
def ask_for_version(
cls, version_options, index=None, show_statuses=True, parent=None
):
dialog = cls(parent)
dialog.set_versions(version_options)
dialog.set_status_visible(show_statuses)
if index is not None:
dialog.select_index(index)
dialog.exec_()

View file

@ -683,37 +683,51 @@ class SceneInventoryView(QtWidgets.QTreeView):
repre_ids
)
product_ids = {
repre_info.product_id
for repre_info in repre_info_by_id.values()
}
active_repre_info = repre_info_by_id[active_repre_id]
active_product_id = active_repre_info.product_id
active_version_id = active_repre_info.version_id
filtered_repre_info_by_id = {
repre_id: repre_info
for repre_id, repre_info in repre_info_by_id.items()
if repre_info.product_id == active_product_id
}
filtered_container_item_ids = {
item_id
for item_id, container_item in container_items_by_id.items()
if container_item.representation_id in filtered_repre_info_by_id
}
version_items_by_id = self._controller.get_version_items(
{active_product_id}
)[active_product_id]
active_product_id = active_repre_info.product_id
version_items_by_product_id = self._controller.get_version_items(
product_ids
)
version_items = list(
version_items_by_product_id[active_product_id].values()
)
versions = {version_item.version for version_item in version_items}
product_ids_by_version = collections.defaultdict(set)
for version_items_by_id in version_items_by_product_id.values():
for version_item in version_items_by_id.values():
version = version_item.version
_prod_version = version
if _prod_version < 0:
_prod_version = -1
product_ids_by_version[_prod_version].add(
version_item.product_id
)
if version in versions:
continue
versions.add(version)
version_items.append(version_item)
def version_sorter(item):
hero_value = 0
version = item.version
if version < 0:
i_version = item.version
if i_version < 0:
hero_value = 1
version = abs(version)
return version, hero_value
i_version = abs(i_version)
return i_version, hero_value
version_items = list(version_items_by_id.values())
version_items.sort(key=version_sorter, reverse=True)
status_items_by_name = {
status_item.name: status_item
for status_item in self._controller.get_project_status_items()
}
show_statuses = len(product_ids) == 1
status_items_by_name = {}
if show_statuses:
status_items_by_name = {
status_item.name: status_item
for status_item in self._controller.get_project_status_items()
}
version_options = []
active_version_idx = 0
@ -726,10 +740,12 @@ class SceneInventoryView(QtWidgets.QTreeView):
status_name = version_item.status
status_short = None
status_color = None
status_icon = None
status_item = status_items_by_name.get(status_name)
if status_item:
status_short = status_item.short
status_color = status_item.color
status_icon = status_item.icon
version_options.append(
VersionOption(
version,
@ -737,23 +753,35 @@ class SceneInventoryView(QtWidgets.QTreeView):
status_name,
status_short,
status_color,
status_icon,
)
)
version_option = SelectVersionDialog.ask_for_version(
version_options,
active_version_idx,
show_statuses=show_statuses,
parent=self
)
if version_option is None:
return
version = version_option.version
product_version = version = version_option.version
if version < 0:
product_version = -1
version = HeroVersionType(version)
product_ids = product_ids_by_version[product_version]
filtered_item_ids = set()
for container_item in container_items_by_id.values():
repre_id = container_item.representation_id
repre_info = repre_info_by_id[repre_id]
if repre_info.product_id in product_ids:
filtered_item_ids.add(container_item.item_id)
self._update_containers_to_version(
filtered_container_item_ids, version
filtered_item_ids, version
)
def _show_switch_dialog(self, item_ids):

View file

@ -182,7 +182,27 @@ class TrayManager:
}:
envs.pop(key, None)
# Remove any existing addon path from 'PYTHONPATH'
addons_dir = os.environ.get("AYON_ADDONS_DIR", "")
if addons_dir:
addons_dir = os.path.normpath(addons_dir)
addons_dir = addons_dir.lower()
pythonpath = envs.get("PYTHONPATH") or ""
new_python_paths = []
for path in pythonpath.split(os.pathsep):
if not path:
continue
path = os.path.normpath(path)
if path.lower().startswith(addons_dir):
continue
new_python_paths.append(path)
envs["PYTHONPATH"] = os.pathsep.join(new_python_paths)
# Start new process
run_detached_process(args, env=envs)
# Exit current tray process
self.exit()
def exit(self):

View file

@ -2,7 +2,7 @@ import time
from datetime import datetime
import logging
from qtpy import QtWidgets, QtGui
from qtpy import QtWidgets, QtGui, QtCore
log = logging.getLogger(__name__)
@ -130,32 +130,65 @@ class StatusDelegate(QtWidgets.QStyledItemDelegate):
else:
style = QtWidgets.QApplication.style()
style.drawControl(
QtWidgets.QCommonStyle.CE_ItemViewItem,
self.initStyleOption(option, index)
mode = QtGui.QIcon.Normal
if not (option.state & QtWidgets.QStyle.State_Enabled):
mode = QtGui.QIcon.Disabled
elif option.state & QtWidgets.QStyle.State_Selected:
mode = QtGui.QIcon.Selected
state = QtGui.QIcon.Off
if option.state & QtWidgets.QStyle.State_Open:
state = QtGui.QIcon.On
icon = self._get_status_icon(index)
option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration
option.icon = icon
act_size = icon.actualSize(option.decorationSize, mode, state)
option.decorationSize = QtCore.QSize(
min(option.decorationSize.width(), act_size.width()),
min(option.decorationSize.height(), act_size.height())
)
text = self._get_status_name(index)
if text:
option.features |= QtWidgets.QStyleOptionViewItem.HasDisplay
option.text = text
painter.save()
painter.setClipRect(option.rect)
icon_rect = style.subElementRect(
QtWidgets.QCommonStyle.SE_ItemViewItemDecoration,
option,
option.widget
)
text_rect = style.subElementRect(
QtWidgets.QCommonStyle.SE_ItemViewItemText,
option,
option.widget
)
# Draw background
style.drawPrimitive(
QtWidgets.QCommonStyle.PE_PanelItemViewItem,
option,
painter,
option.widget
)
painter.save()
text_rect = style.subElementRect(
QtWidgets.QCommonStyle.SE_ItemViewItemText,
option
# Draw icon
option.icon.paint(
painter,
icon_rect,
option.decorationAlignment,
mode,
state
)
text_margin = style.proxy().pixelMetric(
QtWidgets.QCommonStyle.PM_FocusFrameHMargin,
option,
option.widget
) + 1
padded_text_rect = text_rect.adjusted(
text_margin, 0, - text_margin, 0
)
fm = QtGui.QFontMetrics(option.font)
text = self._get_status_name(index)
if padded_text_rect.width() < fm.width(text):
if text_rect.width() < fm.width(text):
text = self._get_status_short_name(index)
if text_rect.width() < fm.width(text):
text = ""
fg_color = self._get_status_color(index)
pen = painter.pen()
@ -163,11 +196,47 @@ class StatusDelegate(QtWidgets.QStyledItemDelegate):
painter.setPen(pen)
painter.drawText(
padded_text_rect,
text_rect,
option.displayAlignment,
text
)
if option.state & QtWidgets.QStyle.State_HasFocus:
focus_opt = QtWidgets.QStyleOptionFocusRect()
focus_opt.state = option.state
focus_opt.direction = option.direction
focus_opt.rect = option.rect
focus_opt.fontMetrics = option.fontMetrics
focus_opt.palette = option.palette
focus_opt.rect = style.subElementRect(
QtWidgets.QCommonStyle.SE_ItemViewItemFocusRect,
option,
option.widget
)
focus_opt.state |= (
QtWidgets.QStyle.State_KeyboardFocusChange
| QtWidgets.QStyle.State_Item
)
focus_opt.backgroundColor = option.palette.color(
(
QtGui.QPalette.Normal
if option.state & QtWidgets.QStyle.State_Enabled
else QtGui.QPalette.Disabled
),
(
QtGui.QPalette.Highlight
if option.state & QtWidgets.QStyle.State_Selected
else QtGui.QPalette.Window
)
)
style.drawPrimitive(
QtWidgets.QCommonStyle.PE_FrameFocusRect,
focus_opt,
painter,
option.widget
)
painter.restore()
def _get_status_name(self, index):
@ -180,6 +249,9 @@ class StatusDelegate(QtWidgets.QStyledItemDelegate):
return QtGui.QColor(index.data(self.status_color_role))
def _get_status_icon(self, index):
icon = None
if self.status_icon_role is not None:
return index.data(self.status_icon_role)
return None
icon = index.data(self.status_icon_role)
if icon is None:
return QtGui.QIcon()
return icon

View file

@ -3,7 +3,9 @@ import collections
from qtpy import QtWidgets, QtGui, QtCore
from ayon_core.lib.events import QueuedEventSystem
from ayon_core.style import get_default_entity_icon_color
from ayon_core.tools.common_models import (
ProjectsModel,
HierarchyModel,
HierarchyExpectedSelection,
)
@ -25,8 +27,9 @@ class FoldersQtModel(QtGui.QStandardItemModel):
Args:
controller (AbstractWorkfilesFrontend): The control object.
"""
"""
_default_folder_icon = None
refreshed = QtCore.Signal()
def __init__(self, controller):
@ -71,13 +74,6 @@ class FoldersQtModel(QtGui.QStandardItemModel):
self.set_project_name(self._last_project_name)
def _clear_items(self):
self._items_by_id = {}
self._parent_id_by_id = {}
self._has_content = False
root_item = self.invisibleRootItem()
root_item.removeRows(0, root_item.rowCount())
def get_index_by_id(self, item_id):
"""Get index by folder id.
@ -123,7 +119,7 @@ class FoldersQtModel(QtGui.QStandardItemModel):
if not project_name:
self._last_project_name = project_name
self._fill_items({})
self._fill_items({}, {})
self._current_refresh_thread = None
return
@ -140,15 +136,42 @@ class FoldersQtModel(QtGui.QStandardItemModel):
thread = RefreshThread(
project_name,
self._controller.get_folder_items,
project_name,
FOLDERS_MODEL_SENDER_NAME
self._thread_getter,
project_name
)
self._current_refresh_thread = thread
self._refresh_threads[thread.id] = thread
thread.refresh_finished.connect(self._on_refresh_thread)
thread.start()
@classmethod
def _get_default_folder_icon(cls):
if cls._default_folder_icon is None:
cls._default_folder_icon = get_qt_icon({
"type": "awesome-font",
"name": "fa.folder",
"color": get_default_entity_icon_color()
})
return cls._default_folder_icon
def _clear_items(self):
self._items_by_id = {}
self._parent_id_by_id = {}
self._has_content = False
root_item = self.invisibleRootItem()
root_item.removeRows(0, root_item.rowCount())
def _thread_getter(self, project_name):
folder_items = self._controller.get_folder_items(
project_name, FOLDERS_MODEL_SENDER_NAME
)
folder_type_items = {}
if hasattr(self._controller, "get_folder_type_items"):
folder_type_items = self._controller.get_folder_type_items(
project_name, FOLDERS_MODEL_SENDER_NAME
)
return folder_items, folder_type_items
def _on_refresh_thread(self, thread_id):
"""Callback when refresh thread is finished.
@ -169,19 +192,55 @@ class FoldersQtModel(QtGui.QStandardItemModel):
or thread_id != self._current_refresh_thread.id
):
return
self._fill_items(thread.get_result())
folder_items, folder_type_items = thread.get_result()
self._fill_items(folder_items, folder_type_items)
self._current_refresh_thread = None
def _fill_item_data(self, item, folder_item):
def _get_folder_item_icon(
self,
folder_item,
folder_type_item_by_name,
folder_type_icon_cache
):
icon = folder_type_icon_cache.get(folder_item.folder_type)
if icon is not None:
return icon
folder_type_item = folder_type_item_by_name.get(
folder_item.folder_type
)
icon = None
if folder_type_item is not None:
icon = get_qt_icon({
"type": "material-symbols",
"name": folder_type_item.icon,
"color": get_default_entity_icon_color()
})
if icon is None:
icon = self._get_default_folder_icon()
folder_type_icon_cache[folder_item.folder_type] = icon
return icon
def _fill_item_data(
self,
item,
folder_item,
folder_type_item_by_name,
folder_type_icon_cache
):
"""
Args:
item (QtGui.QStandardItem): Item to fill data.
folder_item (FolderItem): Folder item.
"""
icon = get_qt_icon(folder_item.icon)
"""
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)
@ -189,7 +248,7 @@ class FoldersQtModel(QtGui.QStandardItemModel):
item.setData(folder_item.label, QtCore.Qt.DisplayRole)
item.setData(icon, QtCore.Qt.DecorationRole)
def _fill_items(self, folder_items_by_id):
def _fill_items(self, folder_items_by_id, folder_type_items):
if not folder_items_by_id:
if folder_items_by_id is not None:
self._clear_items()
@ -197,6 +256,11 @@ class FoldersQtModel(QtGui.QStandardItemModel):
self.refreshed.emit()
return
folder_type_item_by_name = {
folder_type.name: folder_type
for folder_type in folder_type_items
}
folder_type_icon_cache = {}
self._has_content = True
folder_ids = set(folder_items_by_id)
@ -242,7 +306,12 @@ class FoldersQtModel(QtGui.QStandardItemModel):
else:
is_new = self._parent_id_by_id[item_id] != parent_id
self._fill_item_data(item, folder_item)
self._fill_item_data(
item,
folder_item,
folder_type_item_by_name,
folder_type_icon_cache
)
if is_new:
new_items.append(item)
self._items_by_id[item_id] = item
@ -619,6 +688,7 @@ class SimpleSelectionModel(object):
class SimpleFoldersController(object):
def __init__(self):
self._event_system = self._create_event_system()
self._projects_model = ProjectsModel(self)
self._hierarchy_model = HierarchyModel(self)
self._selection_model = SimpleSelectionModel(self)
self._expected_selection = HierarchyExpectedSelection(
@ -639,6 +709,11 @@ class SimpleFoldersController(object):
def get_folder_items(self, project_name, sender=None):
return self._hierarchy_model.get_folder_items(project_name, sender)
def get_folder_type_items(self, project_name, sender=None):
return self._projects_model.get_folder_type_items(
project_name, sender
)
def set_selected_project(self, project_name):
self._selection_model.set_selected_project(project_name)

View file

@ -6,6 +6,7 @@ from functools import partial
from qtpy import QtWidgets, QtCore, QtGui
import qtawesome
import qtmaterialsymbols
from ayon_core.style import (
get_objected_colors,
@ -468,7 +469,7 @@ class _IconsCache:
if icon_type == "path":
parts = [icon_type, icon_def["path"]]
elif icon_type == "awesome-font":
elif icon_type in {"awesome-font", "material-symbols"}:
parts = [icon_type, icon_def["name"], icon_def["color"]]
return "|".join(parts)
@ -495,6 +496,13 @@ class _IconsCache:
if icon is None:
icon = cls.get_qta_icon_by_name_and_color(
"fa.{}".format(icon_name), icon_color)
elif icon_type == "material-symbols":
icon_name = icon_def["name"]
icon_color = icon_def["color"]
if qtmaterialsymbols.get_icon_name_char(icon_name) is not None:
icon = qtmaterialsymbols.get_icon(icon_name, icon_color)
if icon is None:
icon = cls.get_default()
cls._cache[cache_key] = icon

View file

@ -1,6 +1,9 @@
from qtpy import QtWidgets, QtGui, QtCore
from ayon_core.style import get_disabled_entity_icon_color
from ayon_core.style import (
get_disabled_entity_icon_color,
get_default_entity_icon_color,
)
from .views import DeselectableTreeView
from .lib import RefreshThread, get_qt_icon
@ -17,8 +20,9 @@ class TasksQtModel(QtGui.QStandardItemModel):
Args:
controller (AbstractWorkfilesFrontend): The control object.
"""
"""
_default_task_icon = None
refreshed = QtCore.Signal()
def __init__(self, controller):
@ -176,7 +180,7 @@ class TasksQtModel(QtGui.QStandardItemModel):
return
thread = RefreshThread(
folder_id,
self._controller.get_task_items,
self._thread_getter,
project_name,
folder_id
)
@ -185,8 +189,55 @@ class TasksQtModel(QtGui.QStandardItemModel):
thread.refresh_finished.connect(self._on_refresh_thread)
thread.start()
def _thread_getter(self, project_name, folder_id):
task_items = self._controller.get_task_items(
project_name, folder_id
)
task_type_items = {}
if hasattr(self._controller, "get_task_type_items"):
task_type_items = self._controller.get_task_type_items(
project_name
)
return task_items, task_type_items
@classmethod
def _get_default_task_icon(cls):
if cls._default_task_icon is None:
cls._default_task_icon = get_qt_icon({
"type": "awesome-font",
"name": "fa.male",
"color": get_default_entity_icon_color()
})
return cls._default_task_icon
def _get_task_item_icon(
self,
task_item,
task_type_item_by_name,
task_type_icon_cache
):
icon = task_type_icon_cache.get(task_item.task_type)
if icon is not None:
return icon
task_type_item = task_type_item_by_name.get(
task_item.task_type
)
icon = None
if task_type_item is not None:
icon = get_qt_icon({
"type": "material-symbols",
"name": task_type_item.icon,
"color": get_default_entity_icon_color()
})
if icon is None:
icon = self._get_default_task_icon()
task_type_icon_cache[task_item.task_type] = icon
return icon
def _fill_data_from_thread(self, thread):
task_items = thread.get_result()
task_items, task_type_items = thread.get_result()
# Task items are refreshed
if task_items is None:
return
@ -197,6 +248,11 @@ class TasksQtModel(QtGui.QStandardItemModel):
return
self._remove_invalid_items()
task_type_item_by_name = {
task_type_item.name: task_type_item
for task_type_item in task_type_items
}
task_type_icon_cache = {}
new_items = []
new_names = set()
for task_item in task_items:
@ -209,8 +265,11 @@ class TasksQtModel(QtGui.QStandardItemModel):
new_items.append(item)
self._items_by_name[name] = item
# TODO cache locally
icon = get_qt_icon(task_item.icon)
icon = self._get_task_item_icon(
task_item,
task_type_item_by_name,
task_type_icon_cache
)
item.setData(task_item.label, QtCore.Qt.DisplayRole)
item.setData(name, ITEM_NAME_ROLE)
item.setData(task_item.id, ITEM_ID_ROLE)

View file

@ -546,6 +546,46 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
"""
pass
@abstractmethod
def get_folder_type_items(self, project_name, sender=None):
"""Folder type items for a project.
This function may trigger events with topics
'projects.folder_types.refresh.started' and
'projects.folder_types.refresh.finished' which will contain 'sender'
value in data.
That may help to avoid re-refresh of items in UI elements.
Args:
project_name (str): Project name.
sender (str): Who requested folder type items.
Returns:
list[FolderTypeItem]: Folder type information.
"""
pass
@abstractmethod
def get_task_type_items(self, project_name, sender=None):
"""Task type items for a project.
This function may trigger events with topics
'projects.task_types.refresh.started' and
'projects.task_types.refresh.finished' which will contain 'sender'
value in data.
That may help to avoid re-refresh of items in UI elements.
Args:
project_name (str): Project name.
sender (str): Who requested task type items.
Returns:
list[TaskTypeItem]: Task type information.
"""
pass
# Host information
@abstractmethod
def get_workfile_extensions(self):
@ -834,12 +874,13 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
pass
@abstractmethod
def get_workarea_file_items(self, folder_id, task_id):
def get_workarea_file_items(self, folder_id, task_name, sender=None):
"""Get workarea file items.
Args:
folder_id (str): Folder id.
task_id (str): Task id.
task_name (str): Task name.
sender (Optional[str]): Who requested workarea file items.
Returns:
list[FileItem]: List of workarea file items.
@ -905,12 +946,12 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
pass
@abstractmethod
def get_workfile_info(self, folder_id, task_id, filepath):
def get_workfile_info(self, folder_id, task_name, filepath):
"""Workfile info from database.
Args:
folder_id (str): Folder id.
task_id (str): Task id.
task_name (str): Task id.
filepath (str): Workfile path.
Returns:
@ -921,7 +962,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
pass
@abstractmethod
def save_workfile_info(self, folder_id, task_id, filepath, note):
def save_workfile_info(self, folder_id, task_name, filepath, note):
"""Save workfile info to database.
At this moment the only information which can be saved about
@ -932,7 +973,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
Args:
folder_id (str): Folder id.
task_id (str): Task id.
task_name (str): Task id.
filepath (str): Workfile path.
note (Union[str, None]): Note.
"""

View file

@ -231,6 +231,16 @@ class BaseWorkfileController(
return self._projects_model.get_project_entity(
project_name)
def get_folder_type_items(self, project_name, sender=None):
return self._projects_model.get_folder_type_items(
project_name, sender
)
def get_task_type_items(self, project_name, sender=None):
return self._projects_model.get_task_type_items(
project_name, sender
)
def get_folder_entity(self, project_name, folder_id):
return self._hierarchy_model.get_folder_entity(
project_name, folder_id)
@ -278,7 +288,8 @@ class BaseWorkfileController(
)
def get_user_items_by_name(self):
return self._users_model.get_user_items_by_name()
project_name = self.get_current_project_name()
return self._users_model.get_user_items_by_name(project_name)
# Host information
def get_workfile_extensions(self):
@ -410,9 +421,11 @@ class BaseWorkfileController(
return self._workfiles_model.get_workarea_dir_by_context(
folder_id, task_id)
def get_workarea_file_items(self, folder_id, task_id):
def get_workarea_file_items(self, folder_id, task_name, sender=None):
task_id = self._get_task_id(folder_id, task_name)
return self._workfiles_model.get_workarea_file_items(
folder_id, task_id)
folder_id, task_id, task_name
)
def get_workarea_save_as_data(self, folder_id, task_id):
return self._workfiles_model.get_workarea_save_as_data(
@ -447,12 +460,14 @@ class BaseWorkfileController(
return self._workfiles_model.get_published_file_items(
folder_id, task_name)
def get_workfile_info(self, folder_id, task_id, filepath):
def get_workfile_info(self, folder_id, task_name, filepath):
task_id = self._get_task_id(folder_id, task_name)
return self._workfiles_model.get_workfile_info(
folder_id, task_id, filepath
)
def save_workfile_info(self, folder_id, task_id, filepath, note):
def save_workfile_info(self, folder_id, task_name, filepath, note):
task_id = self._get_task_id(folder_id, task_name)
self._workfiles_model.save_workfile_info(
folder_id, task_id, filepath, note
)
@ -627,6 +642,17 @@ class BaseWorkfileController(
def _emit_event(self, topic, data=None):
self.emit_event(topic, data, "controller")
def _get_task_id(self, folder_id, task_name, sender=None):
task_item = self._hierarchy_model.get_task_item_by_name(
self.get_current_project_name(),
folder_id,
task_name,
sender
)
if not task_item:
return None
return task_item.id
# Expected selection
# - expected selection is used to restore selection after refresh
# or when current context should be used
@ -722,7 +748,7 @@ class BaseWorkfileController(
self._host_save_workfile(dst_filepath)
# Make sure workfile info exists
self.save_workfile_info(folder_id, task_id, dst_filepath, None)
self.save_workfile_info(folder_id, task_name, dst_filepath, None)
# Create extra folders
create_workdir_extra_folders(

View file

@ -1,6 +1,7 @@
import os
import re
import copy
import uuid
import arrow
import ayon_api
@ -173,7 +174,7 @@ class WorkareaModel:
folder_mapping[task_id] = workdir
return workdir
def get_file_items(self, folder_id, task_id):
def get_file_items(self, folder_id, task_id, task_name):
items = []
if not folder_id or not task_id:
return items
@ -192,7 +193,7 @@ class WorkareaModel:
continue
workfile_info = self._controller.get_workfile_info(
folder_id, task_id, filepath
folder_id, task_name, filepath
)
modified = os.path.getmtime(filepath)
items.append(FileItem(
@ -587,6 +588,7 @@ class WorkfileEntitiesModel:
username = self._get_current_username()
workfile_info = {
"id": uuid.uuid4().hex,
"path": rootless_path,
"taskId": task_id,
"attrib": {
@ -770,19 +772,21 @@ class WorkfilesModel:
return self._workarea_model.get_workarea_dir_by_context(
folder_id, task_id)
def get_workarea_file_items(self, folder_id, task_id):
def get_workarea_file_items(self, folder_id, task_id, task_name):
"""Workfile items for passed context from workarea.
Args:
folder_id (Union[str, None]): Folder id.
task_id (Union[str, None]): Task id.
task_name (Union[str, None]): Task name.
Returns:
list[FileItem]: List of file items matching workarea of passed
context.
"""
return self._workarea_model.get_file_items(folder_id, task_id)
return self._workarea_model.get_file_items(
folder_id, task_id, task_name
)
def get_workarea_save_as_data(self, folder_id, task_id):
return self._workarea_model.get_workarea_save_as_data(

View file

@ -10,7 +10,6 @@ from ayon_core.tools.utils.delegates import PrettyTimeDelegate
from .utils import BaseOverlayFrame
REPRE_ID_ROLE = QtCore.Qt.UserRole + 1
FILEPATH_ROLE = QtCore.Qt.UserRole + 2
AUTHOR_ROLE = QtCore.Qt.UserRole + 3
@ -249,7 +248,7 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
# Handle roles for first column
col = index.column()
if col != 1:
if col == 0:
return super().data(index, role)
if role == QtCore.Qt.DecorationRole:

View file

@ -66,7 +66,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
self._empty_item_used = False
self._published_mode = False
self._selected_folder_id = None
self._selected_task_id = None
self._selected_task_name = None
self._add_missing_context_item()
@ -153,7 +153,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
def _on_task_changed(self, event):
self._selected_folder_id = event["folder_id"]
self._selected_task_id = event["task_id"]
self._selected_task_name = event["task_name"]
if not self._published_mode:
self._fill_items()
@ -179,13 +179,13 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
def _fill_items_impl(self):
folder_id = self._selected_folder_id
task_id = self._selected_task_id
if not folder_id or not task_id:
task_name = self._selected_task_name
if not folder_id or not task_name:
self._add_missing_context_item()
return
file_items = self._controller.get_workarea_file_items(
folder_id, task_id
folder_id, task_name
)
root_item = self.invisibleRootItem()
if not file_items:

View file

@ -75,7 +75,7 @@ class SidePanelWidget(QtWidgets.QWidget):
self._btn_note_save = btn_note_save
self._folder_id = None
self._task_id = None
self._task_name = None
self._filepath = None
self._orig_note = ""
self._controller = controller
@ -93,10 +93,10 @@ class SidePanelWidget(QtWidgets.QWidget):
def _on_selection_change(self, event):
folder_id = event["folder_id"]
task_id = event["task_id"]
task_name = event["task_name"]
filepath = event["path"]
self._set_context(folder_id, task_id, filepath)
self._set_context(folder_id, task_name, filepath)
def _on_note_change(self):
text = self._note_input.toPlainText()
@ -106,19 +106,19 @@ class SidePanelWidget(QtWidgets.QWidget):
note = self._note_input.toPlainText()
self._controller.save_workfile_info(
self._folder_id,
self._task_id,
self._task_name,
self._filepath,
note
)
self._orig_note = note
self._btn_note_save.setEnabled(False)
def _set_context(self, folder_id, task_id, filepath):
def _set_context(self, folder_id, task_name, filepath):
workfile_info = None
# Check if folder, task and file are selected
if bool(folder_id) and bool(task_id) and bool(filepath):
if bool(folder_id) and bool(task_name) and bool(filepath):
workfile_info = self._controller.get_workfile_info(
folder_id, task_id, filepath
folder_id, task_name, filepath
)
enabled = workfile_info is not None
@ -127,7 +127,7 @@ class SidePanelWidget(QtWidgets.QWidget):
self._btn_note_save.setEnabled(enabled)
self._folder_id = folder_id
self._task_id = task_id
self._task_name = task_name
self._filepath = filepath
# Disable inputs and remove texts if any required arguments are

View file

@ -0,0 +1,18 @@
from .version import __version__
from .utils import get_icon_name_char
from .iconic_font import (
IconicFont,
get_instance,
get_icon,
)
__all__ = (
"__version__",
"get_icon_name_char",
"IconicFont",
"get_instance",
"get_icon",
)

View file

@ -0,0 +1,209 @@
import sys
from qtpy import QtCore, QtGui, QtWidgets
from .iconic_font import get_instance
# TODO: Set icon colour and copy code with color kwarg
VIEW_COLUMNS = 5
AUTO_SEARCH_TIMEOUT = 500
ALL_COLLECTIONS = 'All'
class IconBrowser(QtWidgets.QMainWindow):
"""
A small browser window that allows the user to search through all icons from
the available version of QtAwesome. You can also copy the name and python
code for the currently selected icon.
"""
def __init__(self):
super(IconBrowser, self).__init__()
self.setMinimumSize(400, 300)
self.setWindowTitle("Material Icon Browser")
instance = get_instance()
fontMaps = instance.get_charmap()
iconNames = list(fontMaps)
self._filterTimer = QtCore.QTimer(self)
self._filterTimer.setSingleShot(True)
self._filterTimer.setInterval(AUTO_SEARCH_TIMEOUT)
self._filterTimer.timeout.connect(self._updateFilter)
model = IconModel(self.palette().color(QtGui.QPalette.Text))
model.setStringList(sorted(iconNames))
self._proxyModel = QtCore.QSortFilterProxyModel()
self._proxyModel.setSourceModel(model)
self._proxyModel.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self._listView = IconListView(self)
self._listView.setUniformItemSizes(True)
self._listView.setViewMode(QtWidgets.QListView.IconMode)
self._listView.setModel(self._proxyModel)
self._listView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self._listView.doubleClicked.connect(self._copyIconText)
self._lineEdit = QtWidgets.QLineEdit(self)
self._lineEdit.setAlignment(QtCore.Qt.AlignCenter)
self._lineEdit.textChanged.connect(self._triggerDelayedUpdate)
self._lineEdit.returnPressed.connect(self._triggerImmediateUpdate)
self._comboBox = QtWidgets.QComboBox(self)
self._comboBox.setMinimumWidth(75)
self._comboBox.currentIndexChanged.connect(self._triggerImmediateUpdate)
self._comboBox.addItems([ALL_COLLECTIONS] + sorted(fontMaps.keys()))
lyt = QtWidgets.QHBoxLayout()
lyt.setContentsMargins(0, 0, 0, 0)
lyt.addWidget(self._comboBox)
lyt.addWidget(self._lineEdit)
searchBarFrame = QtWidgets.QFrame(self)
searchBarFrame.setLayout(lyt)
self._copyButton = QtWidgets.QPushButton('Copy Name', self)
self._copyButton.clicked.connect(self._copyIconText)
lyt = QtWidgets.QVBoxLayout()
lyt.addWidget(searchBarFrame)
lyt.addWidget(self._listView)
lyt.addWidget(self._copyButton)
frame = QtWidgets.QFrame(self)
frame.setLayout(lyt)
self.setCentralWidget(frame)
QtWidgets.QShortcut(
QtGui.QKeySequence(QtCore.Qt.Key_Return),
self,
self._copyIconText,
)
self._lineEdit.setFocus()
# geo = self.geometry()
# desktop = QtWidgets.QApplication.desktop()
# screen = desktop.screenNumber(desktop.cursor().pos())
# centerPoint = desktop.screenGeometry(screen).center()
# geo.moveCenter(centerPoint)
# self.setGeometry(geo)
def _updateFilter(self):
"""
Update the string used for filtering in the proxy model with the
current text from the line edit.
"""
reString = ""
group = self._comboBox.currentText()
if group != ALL_COLLECTIONS:
reString += r"^%s\." % group
searchTerm = self._lineEdit.text()
if searchTerm:
reString += ".*%s.*$" % searchTerm
self._proxyModel.setFilterRegularExpression(reString)
def _triggerDelayedUpdate(self):
"""
Reset the timer used for committing the search term to the proxy model.
"""
self._filterTimer.stop()
self._filterTimer.start()
def _triggerImmediateUpdate(self):
"""
Stop the timer used for committing the search term and update the
proxy model immediately.
"""
self._filterTimer.stop()
self._updateFilter()
def _copyIconText(self):
"""
Copy the name of the currently selected icon to the clipboard.
"""
indexes = self._listView.selectedIndexes()
if not indexes:
return
clipboard = QtWidgets.QApplication.instance().clipboard()
clipboard.setText(indexes[0].data())
class IconListView(QtWidgets.QListView):
"""
A QListView that scales it's grid size to ensure the same number of
columns are always drawn.
"""
def __init__(self, parent=None):
super(IconListView, self).__init__(parent)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
def resizeEvent(self, event):
"""
Re-implemented to re-calculate the grid size to provide scaling icons
Parameters
----------
event : QtCore.QEvent
"""
width = self.viewport().width() - 30
# The minus 30 above ensures we don't end up with an item width that
# can't be drawn the expected number of times across the view without
# being wrapped. Without this, the view can flicker during resize
tileWidth = width / VIEW_COLUMNS
iconWidth = int(tileWidth * 0.8)
self.setGridSize(QtCore.QSize(tileWidth, tileWidth))
self.setIconSize(QtCore.QSize(iconWidth, iconWidth))
return super(IconListView, self).resizeEvent(event)
class IconModel(QtCore.QStringListModel):
def __init__(self, iconColor):
super(IconModel, self).__init__()
self._iconColor = iconColor
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def data(self, index, role):
"""
Re-implemented to return the icon for the current index.
Parameters
----------
index : QtCore.QModelIndex
role : int
Returns
-------
Any
"""
if role == QtCore.Qt.DecorationRole:
iconString = self.data(index, role=QtCore.Qt.DisplayRole)
instance = get_instance()
return instance.get_icon(iconString, color=self._iconColor)
return super(IconModel, self).data(index, role)
def run():
"""
Start the IconBrowser and block until the process exits.
"""
app = QtWidgets.QApplication([])
browser = IconBrowser()
browser.show()
sys.exit(app.exec_())

View file

@ -0,0 +1,8 @@
class ApplicationNotRunning(Exception):
"""Raised when the QApplication is not running."""
pass
class FontError(Exception):
"""Raised when there is an issue with font."""
pass

View file

@ -0,0 +1,254 @@
r"""
Iconic Font
===========
A lightweight module handling iconic fonts.
It is designed to provide a simple way for creating QIcons from glyphs.
From a user's viewpoint, the main entry point is the ``IconicFont`` class which
contains methods for loading new iconic fonts with their character map and
methods returning instances of ``QIcon``.
"""
import warnings
from typing import Dict, Optional, Union
from qtpy import QtCore, QtGui, QtWidgets
from .structures import IconOptions, Position
from .utils import get_char_mapping, get_icon_name_char, _get_font_name
class _Cache:
instance = None
def get_instance():
if _Cache.instance is None:
_Cache.instance = IconicFont()
return _Cache.instance
def get_icon(*args, **kwargs):
return get_instance().get_icon(*args, **kwargs)
class CharIconPainter:
"""Char icon painter."""
def paint(self, iconic, painter, rect, mode, state, options):
"""Main paint method."""
self._paint_icon(iconic, painter, rect, mode, state, options)
def _paint_icon(self, iconic, painter, rect, mode, state, options):
"""Paint a single icon."""
painter.save()
color = options.get_color_for_state(state, mode)
char = options.get_char_for_state(state, mode)
painter.setPen(QtGui.QColor(color))
# A 16 pixel-high icon yields a font size of 14, which is pixel perfect
# for font-awesome. 16 * 0.875 = 14
# The reason why the glyph size is smaller than the icon size is to
# account for font bearing.
# draw_size = round(0.875 * rect.height() * options.scale_factor)
draw_size = round(rect.height() * options.scale_factor)
painter.setFont(iconic.get_font(draw_size))
if options.offset is not None:
rect = QtCore.QRect(rect)
rect.translate(
round(options.offset.x * rect.width()),
round(options.offset.y * rect.height())
)
scale_x = -1 if options.hflip else 1
scale_y = -1 if options.vflip else 1
if options.vflip or options.hflip or options.rotate:
x_center = rect.width() * 0.5
y_center = rect.height() * 0.5
painter.translate(x_center, y_center)
transfrom = QtGui.QTransform()
transfrom.scale(scale_x, scale_y)
painter.setTransform(transfrom, True)
if options.rotate:
painter.rotate(options.rotate)
painter.translate(-x_center, -y_center)
painter.setOpacity(options.opacity)
painter.drawText(rect, QtCore.Qt.AlignCenter, char)
painter.restore()
class CharIconEngine(QtGui.QIconEngine):
"""Specialization of QIconEngine used to draw font-based icons."""
def __init__(
self,
iconic: "IconicFont",
painter: QtGui.QPainter,
options: IconOptions
):
super(CharIconEngine, self).__init__()
self._iconic = iconic
self._painter = painter
self._options = options
def paint(self, painter, rect, mode, state):
self._painter.paint(
self._iconic,
painter,
rect,
mode,
state,
self._options
)
def pixmap(self, size, mode, state):
pm = QtGui.QPixmap(size)
pm.fill(QtCore.Qt.transparent)
painter = QtGui.QPainter(pm)
self.paint(
painter,
QtCore.QRect(QtCore.QPoint(0, 0), size),
mode,
state
)
return pm
class IconicFont(QtCore.QObject):
"""Main class for managing icons."""
def __init__(self):
super().__init__()
self._painter = CharIconPainter()
self._icon_cache = {}
def get_charmap(self) -> Dict[str, int]:
return get_char_mapping()
def get_font(self, size: int) -> QtGui.QFont:
"""Return a QFont corresponding to the given size."""
font = QtGui.QFont(_get_font_name())
font.setPixelSize(round(size))
return font
def get_icon_with_options(self, options: IconOptions) -> QtGui.QIcon:
"""Return a QIcon object corresponding to the provided icon name."""
if QtWidgets.QApplication.instance() is None:
warnings.warn(
"You need to have a running QApplication!"
)
return QtGui.QIcon()
cache_key = options.identifier
if cache_key in self._icon_cache:
return self._icon_cache[cache_key]
output = self._icon_by_painter(self._painter, options)
self._icon_cache[cache_key] = output
return output
def get_icon(
self,
icon_name: Optional[str] = None,
color: Optional[Union[QtGui.QColor, str]] = None,
opacity: Optional[float] = None,
scale_factor: Optional[float] = None,
offset: Optional[Position] = None,
hflip: Optional[bool] = False,
vflip: Optional[bool] = False,
rotate: Optional[int] = 0,
icon_name_normal: Optional[str] = None,
icon_name_active: Optional[str] = None,
icon_name_selected: Optional[str] = None,
icon_name_disabled: Optional[str] = None,
icon_name_on: Optional[str] = None,
icon_name_off: Optional[str] = None,
icon_name_on_normal: Optional[str] = None,
icon_name_off_normal: Optional[str] = None,
icon_name_on_active: Optional[str] = None,
icon_name_off_active: Optional[str] = None,
icon_name_on_selected: Optional[str] = None,
icon_name_off_selected: Optional[str] = None,
icon_name_on_disabled: Optional[str] = None,
icon_name_off_disabled: Optional[str] = None,
color_normal: Optional[Union[QtGui.QColor, str]] = None,
color_active: Optional[Union[QtGui.QColor, str]] = None,
color_selected: Optional[Union[QtGui.QColor, str]] = None,
color_disabled: Optional[Union[QtGui.QColor, str]] = None,
color_on: Optional[Union[QtGui.QColor, str]] = None,
color_off: Optional[Union[QtGui.QColor, str]] = None,
color_on_normal: Optional[Union[QtGui.QColor, str]] = None,
color_off_normal: Optional[Union[QtGui.QColor, str]] = None,
color_on_active: Optional[Union[QtGui.QColor, str]] = None,
color_off_active: Optional[Union[QtGui.QColor, str]] = None,
color_on_selected: Optional[Union[QtGui.QColor, str]] = None,
color_off_selected: Optional[Union[QtGui.QColor, str]] = None,
color_on_disabled: Optional[Union[QtGui.QColor, str]] = None,
color_off_disabled: Optional[Union[QtGui.QColor, str]] = None,
) -> QtGui.QIcon:
"""Return a QIcon object corresponding to the provided icon name."""
if QtWidgets.QApplication.instance() is None:
warnings.warn(
"You need to have a running QApplication!"
)
return QtGui.QIcon()
options = IconOptions.from_data(
icon_name=icon_name,
color=color,
opacity=opacity,
scale_factor=scale_factor,
offset=offset,
hflip=hflip,
vflip=vflip,
rotate=rotate,
icon_name_normal=icon_name_normal,
icon_name_active=icon_name_active,
icon_name_selected=icon_name_selected,
icon_name_disabled=icon_name_disabled,
icon_name_on=icon_name_on,
icon_name_off=icon_name_off,
icon_name_on_normal=icon_name_on_normal,
icon_name_off_normal=icon_name_off_normal,
icon_name_on_active=icon_name_on_active,
icon_name_off_active=icon_name_off_active,
icon_name_on_selected=icon_name_on_selected,
icon_name_off_selected=icon_name_off_selected,
icon_name_on_disabled=icon_name_on_disabled,
icon_name_off_disabled=icon_name_off_disabled,
color_normal=color_normal,
color_active=color_active,
color_selected=color_selected,
color_disabled=color_disabled,
color_on=color_on,
color_off=color_off,
color_on_normal=color_on_normal,
color_off_normal=color_off_normal,
color_on_active=color_on_active,
color_off_active=color_off_active,
color_on_selected=color_on_selected,
color_off_selected=color_off_selected,
color_on_disabled=color_on_disabled,
color_off_disabled=color_off_disabled,
)
cache_key = options.identifier
if cache_key in self._icon_cache:
return self._icon_cache[cache_key]
return self.get_icon_with_options(options)
def _icon_by_painter(self, painter, options):
"""Return the icon corresponding to the given painter."""
engine = CharIconEngine(self, painter, options)
return QtGui.QIcon(engine)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
import os
from typing import Optional
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
def get_font_filepath(
font_name: Optional[str] = "MaterialSymbolsOutlined"
) -> str:
return os.path.join(CURRENT_DIR, f"{font_name}.ttf")
def get_mapping_filepath(
font_name: Optional[str] = "MaterialSymbolsOutlined"
) -> str:
return os.path.join(CURRENT_DIR, f"{font_name}.json")

View file

@ -0,0 +1,221 @@
import collections
import json
from typing import Optional, Union
from qtpy import QtGui
from .utils import get_icon_name_char
Position = collections.namedtuple("Offset", ["x", "y"])
class IconSubOption:
def __init__(
self,
on_normal,
on_disabled=None,
on_active=None,
on_selected=None,
off_normal=None,
off_disabled=None,
off_active=None,
off_selected=None,
):
if off_normal is None:
off_normal = on_normal
if on_disabled is None:
on_disabled = on_normal
if off_disabled is None:
off_disabled = on_disabled
if on_active is None:
on_active = on_normal
if off_active is None:
off_active = on_active
if on_selected is None:
on_selected = on_normal
if off_selected is None:
off_selected = on_selected
self._identifier = None
self.on_normal = on_normal
self.on_disabled = on_disabled
self.on_active = on_active
self.on_selected = on_selected
self.off_normal = off_normal
self.off_disabled = off_disabled
self.off_active = off_active
self.off_selected = off_selected
@property
def identifier(self):
if self._identifier is None:
self._identifier = self._generate_identifier()
return self._identifier
def get_value_for_state(self, state, mode):
if state == QtGui.QIcon.On:
if mode == QtGui.QIcon.Disabled:
return self.on_disabled
if mode == QtGui.QIcon.Active:
return self.on_active
if mode == QtGui.QIcon.Selected:
return self.on_selected
return self.on_normal
if mode == QtGui.QIcon.Disabled:
return self.off_disabled
if mode == QtGui.QIcon.Active:
return self.off_active
if mode == QtGui.QIcon.Selected:
return self.off_selected
return self.off_normal
def _generate_identifier(self):
prev_value = None
values = []
for value in (
self.on_normal,
self.off_normal,
self.on_active,
self.off_active,
self.on_selected,
self.off_selected,
self.on_disabled,
self.off_disabled,
):
id_value = ""
if value != prev_value:
id_value = self._get_value_id(value)
values.append(id_value)
prev_value = value
return "|".join(values)
def _get_value_id(self, value):
if isinstance(value, QtGui.QColor):
return value.name()
return str(value)
def _prepare_mapping(option_name):
mapping = []
for store_key, alternative_keys in (
("on_normal", ["normal", "on", ""]),
("off_normal", ["off"]),
("on_active", ["active"]),
("off_active", []),
("on_selected", ["selected"]),
("off_selected", []),
("on_disabled", ["disabled"]),
("off_disabled", []),
):
mapping_keys = [f"{option_name}_{store_key}"]
for alt_key in alternative_keys:
key = option_name
if alt_key:
key = f"{option_name}_{alt_key}"
mapping_keys.append(key)
mapping.append((store_key, mapping_keys))
return mapping
class IconOptions:
mapping_color_keys = _prepare_mapping("color")
mapping_name_keys = _prepare_mapping("icon_name")
data_keys = {
"opacity",
"offset",
"scale_factor",
"hflip",
"vflip",
"rotate",
}
def __init__(
self,
char_option: IconSubOption,
color_option: IconSubOption,
opacity: Optional[float] = None,
scale_factor: Optional[float] = None,
offset: Optional[Position] = None,
hflip: Optional[bool] = False,
vflip: Optional[bool] = False,
rotate: Optional[int] = 0,
):
if opacity is None:
opacity = 1.0
if scale_factor is None:
scale_factor = 1.0
self._identifier = None
self.char_option = char_option
self.color_option = color_option
self.opacity = opacity
self.scale_factor = scale_factor
self.offset = offset
self.hflip = hflip
self.vflip = vflip
self.rotate = rotate
@property
def identifier(self):
if self._identifier is None:
self._identifier = self._generate_identifier()
return self._identifier
def get_color_for_state(self, state, mode) -> QtGui.QColor:
return self.color_option.get_value_for_state(state, mode)
def get_char_for_state(self, state, mode) -> str:
return self.char_option.get_value_for_state(state, mode)
@classmethod
def from_data(cls, **kwargs):
new_kwargs = {
key: value
for key, value in kwargs.items()
if key in cls.data_keys and value is not None
}
color_kwargs = cls._prepare_mapping_values(
cls.mapping_color_keys, kwargs
)
name_kwargs = cls._prepare_mapping_values(
cls.mapping_name_keys, kwargs
)
char_kwargs = {
key: get_icon_name_char(value)
for key, value in name_kwargs.items()
}
new_kwargs["color_option"] = IconSubOption(**color_kwargs)
new_kwargs["char_option"] = IconSubOption(**char_kwargs)
return cls(**new_kwargs)
@classmethod
def _prepare_mapping_values(cls, mapping, kwargs):
mapping_values = {}
for store_key, mapping_keys in mapping:
for key in mapping_keys:
value = kwargs.pop(key, None)
if value is not None:
mapping_values[store_key] = value
break
return mapping_values
def _generate_identifier(self):
return (
str(value)
for value in (
self.char_option.identifier,
self.color_option.identifier,
self.opacity,
self.scale_factor,
self.offset,
self.hflip,
self.vflip,
self.rotate,
)
)

View file

@ -0,0 +1,69 @@
import json
import copy
from typing import Dict, Union
from qtpy import QtWidgets, QtGui, QtCore
from .exceptions import ApplicationNotRunning, FontError
from .resources import get_mapping_filepath, get_font_filepath
class _Cache:
mapping = None
font_id = None
font_name = None
def _load_font():
if QtWidgets.QApplication.instance() is None:
raise ApplicationNotRunning("No QApplication instance found.")
if _Cache.font_id is not None:
loaded_font_families = QtGui.QFontDatabase.applicationFontFamilies(
_Cache.font_id
)
if loaded_font_families:
return
filepath = get_font_filepath()
with open(filepath, "rb") as font_data:
font_id = QtGui.QFontDatabase.addApplicationFontFromData(
QtCore.QByteArray(font_data.read())
)
loaded_font_families = QtGui.QFontDatabase.applicationFontFamilies(
font_id
)
if not loaded_font_families:
raise FontError("Failed to load font.")
_Cache.font_id = font_id
_Cache.font_name = loaded_font_families[0]
def _load_mapping():
if _Cache.mapping is not None:
return
filepath = get_mapping_filepath()
with open(filepath, "r") as stream:
mapping = json.load(stream)
_Cache.mapping = {
key: chr(value)
for key, value in mapping.items()
}
def _get_font_name():
_load_font()
return _Cache.font_name
def get_icon_name_char(icon_name: str) -> Union[int, None]:
_load_mapping()
return _Cache.mapping.get(icon_name, None)
def get_char_mapping() -> Dict[str, int]:
_load_mapping()
return copy.deepcopy(_Cache.mapping)

View file

@ -0,0 +1 @@
__version__ = "1.0.0"

View file

@ -4,13 +4,28 @@ from ayon_server.settings.validators import ensure_unique_names
class ImageIOConfigModel(BaseSettingsModel):
"""[DEPRECATED] Addon OCIO config settings. Please set the OCIO config
path in the Core addon profiles here
(ayon+settings://core/imageio/ocio_config_profiles).
"""
override_global_config: bool = SettingsField(
False,
title="Override global OCIO config"
title="Override global OCIO config",
description=(
"DEPRECATED functionality. Please set the OCIO config path in the "
"Core addon profiles here (ayon+settings://core/imageio/"
"ocio_config_profiles)."
),
)
filepath: list[str] = SettingsField(
default_factory=list,
title="Config path"
title="Config path",
description=(
"DEPRECATED functionality. Please set the OCIO config path in the "
"Core addon profiles here (ayon+settings://core/imageio/"
"ocio_config_profiles)."
),
)

View file

@ -0,0 +1,147 @@
import copy
import ayon_api
from ayon_core import resources
from ayon_core.lib import Logger, NestedCacheItem
from ayon_core.settings import get_studio_settings, get_project_settings
from ayon_core.pipeline.actions import LauncherAction
from .exceptions import (
ApplicationExecutableNotFound,
ApplicationLaunchFailed,
)
class ApplicationAction(LauncherAction):
"""Action to launch an application.
Application action based on 'ApplicationManager' system.
Handling of applications in launcher is not ideal and should be completely
redone from scratch. This is just a temporary solution to keep backwards
compatibility with AYON launcher.
Todos:
Move handling of errors to frontend.
"""
# Application object
application = None
# Action attributes
name = None
label = None
label_variant = None
group = None
icon = None
color = None
order = 0
data = {}
project_settings = {}
project_entities = {}
_log = None
# --- For compatibility for combinations of new and old ayon-core ---
project_settings_cache = NestedCacheItem(
levels=1, default_factory=dict, lifetime=20
)
project_entities_cache = NestedCacheItem(
levels=1, default_factory=dict, lifetime=20
)
@classmethod
def _app_get_project_settings(cls, selection):
project_name = selection.project_name
if project_name in ApplicationAction.project_settings:
return ApplicationAction.project_settings[project_name]
if hasattr(selection, "get_project_settings"):
return selection.get_project_settings()
cache = ApplicationAction.project_settings_cache[project_name]
if not cache.is_valid:
if project_name:
settings = get_project_settings(project_name)
else:
settings = get_studio_settings()
cache.update_data(settings)
return copy.deepcopy(cache.get_data())
@classmethod
def _app_get_project_entity(cls, selection):
project_name = selection.project_name
if project_name in ApplicationAction.project_entities:
return ApplicationAction.project_entities[project_name]
if hasattr(selection, "get_project_settings"):
return selection.get_project_entity()
cache = ApplicationAction.project_entities_cache[project_name]
if not cache.is_valid:
project_entity = None
if project_name:
project_entity = ayon_api.get_project(project_name)
cache.update_data(project_entity)
return copy.deepcopy(cache.get_data())
@property
def log(self):
if self._log is None:
self._log = Logger.get_logger(self.__class__.__name__)
return self._log
def is_compatible(self, selection):
if not selection.is_task_selected:
return False
project_entity = self._app_get_project_entity(selection)
apps = project_entity["attrib"].get("applications")
if not apps or self.application.full_name not in apps:
return False
project_settings = self._app_get_project_settings(selection)
only_available = project_settings["applications"]["only_available"]
if only_available and not self.application.find_executable():
return False
return True
def _show_message_box(self, title, message, details=None):
from qtpy import QtWidgets, QtGui
from ayon_core import style
dialog = QtWidgets.QMessageBox()
icon = QtGui.QIcon(resources.get_ayon_icon_filepath())
dialog.setWindowIcon(icon)
dialog.setStyleSheet(style.load_stylesheet())
dialog.setWindowTitle(title)
dialog.setText(message)
if details:
dialog.setDetailedText(details)
dialog.exec_()
def process(self, selection, **kwargs):
"""Process the full Application action"""
try:
self.application.launch(
project_name=selection.project_name,
folder_path=selection.folder_path,
task_name=selection.task_name,
**self.data
)
except ApplicationExecutableNotFound as exc:
details = exc.details
msg = exc.msg
log_msg = str(msg)
if details:
log_msg += "\n" + details
self.log.warning(log_msg)
self._show_message_box(
"Application executable not found", msg, details
)
except ApplicationLaunchFailed as exc:
msg = str(exc)
self.log.warning(msg, exc_info=True)
self._show_message_box("Application launch failed", msg)

View file

@ -1,6 +1,8 @@
import os
import json
import ayon_api
from ayon_core.addon import AYONAddon, IPluginPaths, click_wrap
from .version import __version__
@ -112,6 +114,95 @@ class ApplicationsAddon(AYONAddon, IPluginPaths):
]
}
def get_app_icon_path(self, icon_filename):
"""Get icon path.
Args:
icon_filename (str): Icon filename.
Returns:
Union[str, None]: Icon path or None if not found.
"""
if not icon_filename:
return None
icon_name = os.path.basename(icon_filename)
path = os.path.join(APPLICATIONS_ADDON_ROOT, "icons", icon_name)
if os.path.exists(path):
return path
return None
def get_app_icon_url(self, icon_filename, server=False):
"""Get icon path.
Method does not validate if icon filename exist on server.
Args:
icon_filename (str): Icon name.
server (Optional[bool]): Return url to AYON server.
Returns:
Union[str, None]: Icon path or None is server url is not
available.
"""
if not icon_filename:
return None
icon_name = os.path.basename(icon_filename)
if server:
base_url = ayon_api.get_base_url()
return (
f"{base_url}/addons/{self.name}/{self.version}"
f"/public/icons/{icon_name}"
)
server_url = os.getenv("AYON_WEBSERVER_URL")
if not server_url:
return None
return "/".join([
server_url, "addons", self.name, self.version, "icons", icon_name
])
def get_applications_action_classes(self):
"""Get application action classes for launcher tool.
This method should be used only by launcher tool. Please do not use it
in other places as its implementation is not optimal, and might
change or be removed.
Returns:
list[ApplicationAction]: List of application action classes.
"""
from .action import ApplicationAction
actions = []
manager = self.get_applications_manager()
for full_name, application in manager.applications.items():
if not application.enabled:
continue
icon = self.get_app_icon_path(application.icon)
action = type(
"app_{}".format(full_name),
(ApplicationAction,),
{
"identifier": "application.{}".format(full_name),
"application": application,
"name": application.name,
"label": application.group.label,
"label_variant": application.label,
"group": None,
"icon": icon,
"color": getattr(application, "color", None),
"order": getattr(application, "order", None) or 0,
"data": {}
}
)
actions.append(action)
return actions
def launch_application(
self, app_name, project_name, folder_path, task_name
):
@ -132,6 +223,18 @@ class ApplicationsAddon(AYONAddon, IPluginPaths):
task_name=task_name,
)
def webserver_initialization(self, manager):
"""Initialize webserver.
Args:
manager (WebServerManager): Webserver manager.
"""
static_prefix = f"/addons/{self.name}/{self.version}/icons"
manager.add_static(
static_prefix, os.path.join(APPLICATIONS_ADDON_ROOT, "icons")
)
# --- CLI ---
def cli(self, addon_click_group):
main_group = click_wrap.group(

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

View file

@ -156,7 +156,7 @@ class ApplicationManager:
Args:
app_name (str): Name of application that should be launched.
**data (dict): Any additional data. Data may be used during
**data (Any): Any additional data. Data may be used during
preparation to store objects usable in multiple places.
Raises:

View file

@ -11,7 +11,6 @@ from ayon_core import AYON_CORE_ROOT
from ayon_core.settings import get_project_settings
from ayon_core.lib import Logger, get_ayon_username
from ayon_core.addon import AddonsManager
from ayon_core.pipeline import HOST_WORKFILE_EXTENSIONS
from ayon_core.pipeline.template_data import get_template_data
from ayon_core.pipeline.workfile import (
get_workfile_template_key,
@ -573,10 +572,9 @@ def _prepare_last_workfile(data, workdir, addons_manager):
last_workfile_path = data.get("last_workfile_path") or ""
if not last_workfile_path:
host_addon = addons_manager.get_host_addon(app.host_name)
extensions = None
if host_addon:
extensions = host_addon.get_workfile_extensions()
else:
extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name)
if extensions:
anatomy = data["anatomy"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Some files were not shown because too many files have changed in this diff Show more