Merge branch 'feature/houdini_cleanup_after_publishing' of github.com:ynput/ayon-core into feature/houdini_cleanup_after_publishing

This commit is contained in:
Ondřej Samohel 2024-06-17 17:28:35 +02:00
commit ae0c0e7f95
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
118 changed files with 9481 additions and 295 deletions

View file

@ -3,6 +3,7 @@ import warnings
import ayon_api import ayon_api
from ayon_core.settings import get_studio_settings, get_project_settings
from ayon_core.pipeline.plugin_discover import ( from ayon_core.pipeline.plugin_discover import (
discover, discover,
register_plugin, register_plugin,
@ -40,7 +41,8 @@ class LauncherActionSelection:
task_name=None, task_name=None,
project_entity=None, project_entity=None,
folder_entity=None, folder_entity=None,
task_entity=None task_entity=None,
project_settings=None,
): ):
self._project_name = project_name self._project_name = project_name
self._folder_id = folder_id self._folder_id = folder_id
@ -53,6 +55,8 @@ class LauncherActionSelection:
self._folder_entity = folder_entity self._folder_entity = folder_entity
self._task_entity = task_entity self._task_entity = task_entity
self._project_settings = project_settings
def __getitem__(self, key): def __getitem__(self, key):
warnings.warn( warnings.warn(
( (
@ -255,6 +259,22 @@ class LauncherActionSelection:
) )
return self._task_entity 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 @property
def is_project_selected(self): def is_project_selected(self):
"""Return whether a project is selected. """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 ayon_api
import six import six
from ayon_core.style import get_default_entity_icon_color
from ayon_core.lib import NestedCacheItem from ayon_core.lib import NestedCacheItem
HIERARCHY_MODEL_SENDER = "hierarchy.model" HIERARCHY_MODEL_SENDER = "hierarchy.model"
@ -31,11 +30,10 @@ class FolderItem:
path (str): Folder path. path (str): Folder path.
folder_type (str): Type of folder. folder_type (str): Type of folder.
label (Union[str, None]): Folder label. label (Union[str, None]): Folder label.
icon (Union[dict[str, Any], None]): Icon definition.
""" """
def __init__( 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.entity_id = entity_id
self.parent_id = parent_id self.parent_id = parent_id
@ -43,13 +41,6 @@ class FolderItem:
self.path = path self.path = path
self.folder_type = folder_type self.folder_type = folder_type
self.label = label or name 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): def to_data(self):
"""Converts folder item to data. """Converts folder item to data.
@ -65,7 +56,6 @@ class FolderItem:
"path": self.path, "path": self.path,
"folder_type": self.folder_type, "folder_type": self.folder_type,
"label": self.label, "label": self.label,
"icon": self.icon,
} }
@classmethod @classmethod
@ -95,23 +85,15 @@ class TaskItem:
name (str): Name of task. name (str): Name of task.
task_type (str): Type of task. task_type (str): Type of task.
parent_id (str): Parent folder id. parent_id (str): Parent folder id.
icon (Union[dict[str, Any], None]): Icon definitions.
""" """
def __init__( 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.task_id = task_id
self.name = name self.name = name
self.task_type = task_type self.task_type = task_type
self.parent_id = parent_id 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 self._label = None
@ -149,7 +131,6 @@ class TaskItem:
"name": self.name, "name": self.name,
"parent_id": self.parent_id, "parent_id": self.parent_id,
"task_type": self.task_type, "task_type": self.task_type,
"icon": self.icon,
} }
@classmethod @classmethod
@ -180,8 +161,7 @@ def _get_task_items_from_tasks(tasks):
task["id"], task["id"],
task["name"], task["name"],
task["type"], task["type"],
folder_id, folder_id
None
)) ))
return output return output
@ -197,8 +177,7 @@ def _get_folder_item_from_hierarchy_item(item):
name, name,
path, path,
item["folderType"], item["folderType"],
item["label"], item["label"]
None,
) )
@ -210,8 +189,7 @@ def _get_folder_item_from_entity(entity):
name, name,
entity["path"], entity["path"],
entity["folderType"], entity["folderType"],
entity["label"] or name, entity["label"] or name
None,
) )

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: class ProjectItem:
"""Item representing folder entity on a server. """Item representing folder entity on a server.
@ -147,19 +215,21 @@ def _get_project_items_from_entitiy(projects):
class ProjectsModel(object): class ProjectsModel(object):
def __init__(self, controller): def __init__(self, controller):
self._projects_cache = CacheItem(default_factory=list) self._projects_cache = CacheItem(default_factory=list)
self._project_statuses_cache = NestedCacheItem(
levels=1, default_factory=list
)
self._projects_by_name = NestedCacheItem( self._projects_by_name = NestedCacheItem(
levels=1, default_factory=list levels=1, default_factory=list
) )
self._project_statuses_cache = {}
self._folder_types_cache = {}
self._task_types_cache = {}
self._is_refreshing = False self._is_refreshing = False
self._controller = controller self._controller = controller
def reset(self): def reset(self):
self._project_statuses_cache = {}
self._folder_types_cache = {}
self._task_types_cache = {}
self._projects_cache.reset() self._projects_cache.reset()
self._project_statuses_cache.reset()
self._projects_by_name.reset() self._projects_by_name.reset()
def refresh(self): def refresh(self):
@ -217,22 +287,87 @@ class ProjectsModel(object):
list[StatusItem]: Status items for project. list[StatusItem]: Status items for project.
""" """
statuses_cache = self._project_statuses_cache[project_name] if project_name is None:
if not statuses_cache.is_valid: return []
with self._project_statuses_refresh_event_manager(
sender, project_name 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 project_entity = self.get_project_entity(project_name)
if project_name:
project_entity = self.get_project_entity(project_name)
statuses = [] statuses = []
if project_entity: if project_entity:
statuses = [ statuses = [
StatusItem.from_project_item(status) StatusItem.from_project_item(status)
for status in project_entity["statuses"] for status in project_entity["statuses"]
] ]
statuses_cache.update_data(statuses) statuses_cache = statuses
return statuses_cache.get_data() 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 @contextlib.contextmanager
def _project_refresh_event_manager(self, sender): def _project_refresh_event_manager(self, sender):
@ -254,9 +389,11 @@ class ProjectsModel(object):
self._is_refreshing = False self._is_refreshing = False
@contextlib.contextmanager @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( self._controller.emit_event(
"projects.statuses.refresh.started", f"projects.{item_type}.refresh.started",
{"sender": sender, "project_name": project_name}, {"sender": sender, "project_name": project_name},
PROJECTS_MODEL_SENDER PROJECTS_MODEL_SENDER
) )
@ -265,7 +402,7 @@ class ProjectsModel(object):
finally: finally:
self._controller.emit_event( self._controller.emit_event(
"projects.statuses.refresh.finished", f"projects.{item_type}.refresh.finished",
{"sender": sender, "project_name": project_name}, {"sender": sender, "project_name": project_name},
PROJECTS_MODEL_SENDER PROJECTS_MODEL_SENDER
) )
@ -282,3 +419,27 @@ class ProjectsModel(object):
def _query_projects(self): def _query_projects(self):
projects = ayon_api.get_projects(fields=["name", "active", "library"]) projects = ayon_api.get_projects(fields=["name", "active", "library"])
return _get_project_items_from_entitiy(projects) 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

@ -227,6 +227,16 @@ class ContextDialogController:
def get_project_items(self, sender=None): def get_project_items(self, sender=None):
return self._projects_model.get_project_items(sender) 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): def get_folder_items(self, project_name, sender=None):
return self._hierarchy_model.get_folder_items(project_name, sender) return self._hierarchy_model.get_folder_items(project_name, sender)

View file

@ -104,8 +104,48 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
Returns: Returns:
list[ProjectItem]: Minimum possible information needed list[ProjectItem]: Minimum possible information needed
for visualisation of folder hierarchy. 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 pass
@abstractmethod @abstractmethod

View file

@ -59,12 +59,23 @@ class BaseLauncherController(
def get_project_items(self, sender=None): def get_project_items(self, sender=None):
return self._projects_model.get_project_items(sender) 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): def get_folder_items(self, project_name, sender=None):
return self._hierarchy_model.get_folder_items(project_name, sender) return self._hierarchy_model.get_folder_items(project_name, sender)
def get_task_items(self, project_name, folder_id, sender=None): def get_task_items(self, project_name, folder_id, sender=None):
return self._hierarchy_model.get_task_items( return self._hierarchy_model.get_task_items(
project_name, folder_id, sender) project_name, folder_id, sender
)
# Project settings for applications actions # Project settings for applications actions
def get_project_settings(self, project_name): 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 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: # class Action:
# def __init__(self, label, icon=None, identifier=None): # 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) # 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: class ActionItem:
"""Item representing single action to trigger. """Item representing single action to trigger.
@ -440,7 +445,17 @@ class ActionsModel:
) )
def _prepare_selection(self, project_name, folder_id, task_id): 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): def _get_discovered_action_classes(self):
if self._discovered_actions is None: if self._discovered_actions is None:
@ -475,7 +490,9 @@ class ActionsModel:
action_items = {} action_items = {}
for identifier, action in self._get_action_objects().items(): for identifier, action in self._get_action_objects().items():
is_application = isinstance(action, ApplicationAction) 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_entities[project_name] = project_entity
action.project_settings[project_name] = project_settings action.project_settings[project_name] = project_settings
@ -497,10 +514,14 @@ class ActionsModel:
return action_items return action_items
def _get_applications_action_classes(self): def _get_applications_action_classes(self):
actions = []
addons_manager = self._get_addons_manager() addons_manager = self._get_addons_manager()
applications_addon = addons_manager.get_enabled_addon("applications") 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: if applications_addon is None:
return actions return actions

View file

@ -510,6 +510,26 @@ class FrontendLoaderController(_BaseLoaderController):
pass 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 @abstractmethod
def get_project_status_items(self, project_name, sender=None): def get_project_status_items(self, project_name, sender=None):
"""Items for all projects available on server. """Items for all projects available on server.

View file

@ -180,6 +180,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
def get_project_items(self, sender=None): def get_project_items(self, sender=None):
return self._projects_model.get_project_items(sender) 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): def get_project_status_items(self, project_name, sender=None):
return self._projects_model.get_project_status_items( return self._projects_model.get_project_status_items(
project_name, sender project_name, sender

View file

@ -188,16 +188,6 @@ class LoaderFoldersModel(FoldersQtModel):
self._colored_items = set() 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): def set_merged_products_selection(self, items):
changes = { changes = {
folder_id: None folder_id: None

View file

@ -126,6 +126,7 @@ class ProductsModel(QtGui.QStandardItemModel):
self._last_project_name = None self._last_project_name = None
self._last_folder_ids = [] self._last_folder_ids = []
self._last_project_statuses = {} self._last_project_statuses = {}
self._last_status_icons_by_name = {}
def get_product_item_indexes(self): def get_product_item_indexes(self):
return [ return [
@ -181,6 +182,13 @@ class ProductsModel(QtGui.QStandardItemModel):
return status_item.color return status_item.color
col = index.column() 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: if col == 0:
return super(ProductsModel, self).data(index, role) return super(ProductsModel, self).data(index, role)
@ -260,6 +268,25 @@ class ProductsModel(QtGui.QStandardItemModel):
break break
yield color 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): def _clear(self):
root_item = self.invisibleRootItem() root_item = self.invisibleRootItem()
root_item.removeRows(0, root_item.rowCount()) root_item.removeRows(0, root_item.rowCount())
@ -419,6 +446,7 @@ class ProductsModel(QtGui.QStandardItemModel):
status_item.name: status_item status_item.name: status_item
for status_item in status_items for status_item in status_items
} }
self._last_status_icons_by_name = {}
active_site_icon_def = self._controller.get_active_site_icon_def( active_site_icon_def = self._controller.get_active_site_icon_def(
project_name project_name

View file

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

View file

@ -128,6 +128,16 @@ class CreateHierarchyController:
project_name, folder_id, sender 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 # Selection model
def set_selected_project(self, project_name): def set_selected_project(self, project_name):
self._selection_model.set_selected_project(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): def get_folder_items(self, project_name, sender=None):
return self._controller.get_folder_items(project_name, sender) 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): def set_selected_folder(self, folder_id):
pass pass

View file

@ -99,12 +99,16 @@ class TasksModel(QtGui.QStandardItemModel):
root_item.removeRows(0, self.rowCount()) root_item.removeRows(0, self.rowCount())
return return
task_names_by_folder_path = ( task_items_by_folder_path = (
self._controller.get_task_names_by_folder_paths( self._controller.get_task_items_by_folder_paths(
self._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 self._task_names_by_folder_path = task_names_by_folder_path
new_task_names = self.get_intersection_of_tasks( new_task_names = self.get_intersection_of_tasks(
@ -122,22 +126,54 @@ class TasksModel(QtGui.QStandardItemModel):
item = self._items_by_name.pop(task_name) item = self._items_by_name.pop(task_name)
root_item.removeRow(item.row()) root_item.removeRow(item.row())
icon = get_qt_icon({ default_icon = get_qt_icon({
"type": "awesome-font", "type": "awesome-font",
"name": "fa.male", "name": "fa.male",
"color": get_default_entity_icon_color(), "color": get_default_entity_icon_color(),
}) })
new_items = [] 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: 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 continue
item = QtGui.QStandardItem(task_name) icon_name = icon_name_by_task_name.get(task_name)
item.setData(task_name, TASK_NAME_ROLE) icon = None
if task_name: if icon_name:
item.setData(icon, QtCore.Qt.DecorationRole) icon = get_qt_icon({
self._items_by_name[task_name] = item "type": "material-symbols",
new_items.append(item) "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: if new_items:
root_item.appendRows(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._default_icon_color = get_default_entity_icon_color()
self._last_project_statuses = {}
self._last_status_icons_by_name = {}
def outdated(self, item): def outdated(self, item):
return item.get("isOutdated", True) return item.get("isOutdated", True)
@ -159,10 +162,11 @@ class InventoryModel(QtGui.QStandardItemModel):
self._controller.get_site_provider_icons().items() self._controller.get_site_provider_icons().items()
) )
} }
status_items_by_name = { self._last_project_statuses = {
status_item.name: status_item status_item.name: status_item
for status_item in self._controller.get_project_status_items() for status_item in self._controller.get_project_status_items()
} }
self._last_status_icons_by_name = {}
group_item_icon = qtawesome.icon( group_item_icon = qtawesome.icon(
"fa.folder", color=self._default_icon_color "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"]) remote_site_icon = site_icons.get(sites_info["remote_site_provider"])
root_item = self.invisibleRootItem() root_item = self.invisibleRootItem()
group_items = [] group_items = []
for repre_id, container_items in items_by_repre_id.items(): for repre_id, container_items in items_by_repre_id.items():
repre_info = repre_info_by_id[repre_id] repre_info = repre_info_by_id[repre_id]
@ -195,8 +198,6 @@ class InventoryModel(QtGui.QStandardItemModel):
is_latest = False is_latest = False
is_hero = False is_hero = False
status_name = None status_name = None
status_color = None
status_short = None
if not repre_info.is_valid: if not repre_info.is_valid:
group_name = "< Entity N/A >" group_name = "< Entity N/A >"
item_icon = invalid_item_icon item_icon = invalid_item_icon
@ -219,10 +220,10 @@ class InventoryModel(QtGui.QStandardItemModel):
if not is_latest: if not is_latest:
version_color = self.OUTDATED_COLOR version_color = self.OUTDATED_COLOR
status_name = version_item.status status_name = version_item.status
status_item = status_items_by_name.get(status_name)
if status_item: status_color, status_short, status_icon = self._get_status_data(
status_short = status_item.short status_name
status_color = status_item.color )
container_model_items = [] container_model_items = []
for container_item in container_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_name, STATUS_NAME_ROLE)
group_item.setData(status_short, STATUS_SHORT_ROLE) group_item.setData(status_short, STATUS_SHORT_ROLE)
group_item.setData(status_color, STATUS_COLOR_ROLE) group_item.setData(status_color, STATUS_COLOR_ROLE)
group_item.setData(status_icon, STATUS_ICON_ROLE)
group_item.setData( group_item.setData(
active_site_progress, ACTIVE_SITE_PROGRESS_ROLE active_site_progress, ACTIVE_SITE_PROGRESS_ROLE
@ -355,6 +357,32 @@ class InventoryModel(QtGui.QStandardItemModel):
root_item = self.invisibleRootItem() root_item = self.invisibleRootItem()
root_item.removeRows(0, root_item.rowCount()) 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): class FilterProxyModel(QtCore.QSortFilterProxyModel):
"""Filter model to where key column's value is in the filtered tags""" """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 qtpy import QtWidgets, QtCore, QtGui
from ayon_core.tools.utils import get_qt_icon
from ayon_core.tools.utils.delegates import StatusDelegate from ayon_core.tools.utils.delegates import StatusDelegate
from .model import ( from .model import (
@ -20,13 +21,15 @@ class VersionOption:
label, label,
status_name, status_name,
status_short, status_short,
status_color status_color,
status_icon,
): ):
self.version = version self.version = version
self.label = label self.label = label
self.status_name = status_name self.status_name = status_name
self.status_short = status_short self.status_short = status_short
self.status_color = status_color self.status_color = status_color
self.status_icon = status_icon
class SelectVersionModel(QtGui.QStandardItemModel): class SelectVersionModel(QtGui.QStandardItemModel):
@ -84,27 +87,52 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
return return
painter.save() 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, QtWidgets.QStyle.CC_ComboBox,
option, option,
QtWidgets.QStyle.SC_ComboBoxEditField QtWidgets.QStyle.SC_ComboBoxEditField
) ).adjusted(1, 0, -1, 0)
adj_rect = text_field_rect.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( painter.drawText(
adj_rect, version_text_rect,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
option.currentText option.currentText
) )
metrics = QtGui.QFontMetrics(self.font())
text_width = metrics.width(option.currentText) status_text_rect = QtCore.QRect(content_field_rect)
x_offset = text_width + 2 status_text_rect.setLeft(version_text_rect.right() + 2)
diff_width = adj_rect.width() - x_offset if status_icon is not None and not status_icon.isNull():
if diff_width <= 0: 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 return
status_rect = adj_rect.adjusted(x_offset + 2, 0, 0, 0) if status_text_rect.width() < metrics.width(status_name):
if diff_width < metrics.width(status_name):
status_name = self.itemData(idx, STATUS_SHORT_ROLE) 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)) color = QtGui.QColor(self.itemData(idx, STATUS_COLOR_ROLE))
@ -112,7 +140,7 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
pen.setColor(color) pen.setColor(color)
painter.setPen(pen) painter.setPen(pen)
painter.drawText( painter.drawText(
status_rect, status_text_rect,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
status_name status_name
) )
@ -140,7 +168,17 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
root_item.removeRows(0, root_item.rowCount()) root_item.removeRows(0, root_item.rowCount())
new_items = [] new_items = []
icons_by_name = {}
for version_option in version_options: 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_id = uuid.uuid4().hex
item = QtGui.QStandardItem(version_option.label) item = QtGui.QStandardItem(version_option.label)
item.setColumnCount(root_item.columnCount()) item.setColumnCount(root_item.columnCount())
@ -153,6 +191,7 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
item.setData( item.setData(
version_option.status_color, STATUS_COLOR_ROLE version_option.status_color, STATUS_COLOR_ROLE
) )
item.setData(icon, STATUS_ICON_ROLE)
item.setData(item_id, ITEM_ID_ROLE) item.setData(item_id, ITEM_ID_ROLE)
new_items.append(item) new_items.append(item)

View file

@ -740,10 +740,12 @@ class SceneInventoryView(QtWidgets.QTreeView):
status_name = version_item.status status_name = version_item.status
status_short = None status_short = None
status_color = None status_color = None
status_icon = None
status_item = status_items_by_name.get(status_name) status_item = status_items_by_name.get(status_name)
if status_item: if status_item:
status_short = status_item.short status_short = status_item.short
status_color = status_item.color status_color = status_item.color
status_icon = status_item.icon
version_options.append( version_options.append(
VersionOption( VersionOption(
version, version,
@ -751,6 +753,7 @@ class SceneInventoryView(QtWidgets.QTreeView):
status_name, status_name,
status_short, status_short,
status_color, status_color,
status_icon,
) )
) )

View file

@ -2,7 +2,7 @@ import time
from datetime import datetime from datetime import datetime
import logging import logging
from qtpy import QtWidgets, QtGui from qtpy import QtWidgets, QtGui, QtCore
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -130,32 +130,65 @@ class StatusDelegate(QtWidgets.QStyledItemDelegate):
else: else:
style = QtWidgets.QApplication.style() style = QtWidgets.QApplication.style()
style.drawControl( self.initStyleOption(option, index)
QtWidgets.QCommonStyle.CE_ItemViewItem,
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, option,
painter, painter,
option.widget option.widget
) )
painter.save() # Draw icon
option.icon.paint(
text_rect = style.subElementRect( painter,
QtWidgets.QCommonStyle.SE_ItemViewItemText, icon_rect,
option 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) fm = QtGui.QFontMetrics(option.font)
text = self._get_status_name(index) if text_rect.width() < fm.width(text):
if padded_text_rect.width() < fm.width(text):
text = self._get_status_short_name(index) text = self._get_status_short_name(index)
if text_rect.width() < fm.width(text):
text = ""
fg_color = self._get_status_color(index) fg_color = self._get_status_color(index)
pen = painter.pen() pen = painter.pen()
@ -163,11 +196,47 @@ class StatusDelegate(QtWidgets.QStyledItemDelegate):
painter.setPen(pen) painter.setPen(pen)
painter.drawText( painter.drawText(
padded_text_rect, text_rect,
option.displayAlignment, option.displayAlignment,
text 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() painter.restore()
def _get_status_name(self, index): def _get_status_name(self, index):
@ -180,6 +249,9 @@ class StatusDelegate(QtWidgets.QStyledItemDelegate):
return QtGui.QColor(index.data(self.status_color_role)) return QtGui.QColor(index.data(self.status_color_role))
def _get_status_icon(self, index): def _get_status_icon(self, index):
icon = None
if self.status_icon_role is not None: if self.status_icon_role is not None:
return index.data(self.status_icon_role) icon = index.data(self.status_icon_role)
return None if icon is None:
return QtGui.QIcon()
return icon

View file

@ -3,7 +3,9 @@ import collections
from qtpy import QtWidgets, QtGui, QtCore from qtpy import QtWidgets, QtGui, QtCore
from ayon_core.lib.events import QueuedEventSystem from ayon_core.lib.events import QueuedEventSystem
from ayon_core.style import get_default_entity_icon_color
from ayon_core.tools.common_models import ( from ayon_core.tools.common_models import (
ProjectsModel,
HierarchyModel, HierarchyModel,
HierarchyExpectedSelection, HierarchyExpectedSelection,
) )
@ -25,8 +27,9 @@ class FoldersQtModel(QtGui.QStandardItemModel):
Args: Args:
controller (AbstractWorkfilesFrontend): The control object. controller (AbstractWorkfilesFrontend): The control object.
"""
"""
_default_folder_icon = None
refreshed = QtCore.Signal() refreshed = QtCore.Signal()
def __init__(self, controller): def __init__(self, controller):
@ -71,13 +74,6 @@ class FoldersQtModel(QtGui.QStandardItemModel):
self.set_project_name(self._last_project_name) 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): def get_index_by_id(self, item_id):
"""Get index by folder id. """Get index by folder id.
@ -123,7 +119,7 @@ class FoldersQtModel(QtGui.QStandardItemModel):
if not project_name: if not project_name:
self._last_project_name = project_name self._last_project_name = project_name
self._fill_items({}) self._fill_items({}, {})
self._current_refresh_thread = None self._current_refresh_thread = None
return return
@ -140,15 +136,42 @@ class FoldersQtModel(QtGui.QStandardItemModel):
thread = RefreshThread( thread = RefreshThread(
project_name, project_name,
self._controller.get_folder_items, self._thread_getter,
project_name, project_name
FOLDERS_MODEL_SENDER_NAME
) )
self._current_refresh_thread = thread self._current_refresh_thread = thread
self._refresh_threads[thread.id] = thread self._refresh_threads[thread.id] = thread
thread.refresh_finished.connect(self._on_refresh_thread) thread.refresh_finished.connect(self._on_refresh_thread)
thread.start() 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): def _on_refresh_thread(self, thread_id):
"""Callback when refresh thread is finished. """Callback when refresh thread is finished.
@ -169,19 +192,55 @@ class FoldersQtModel(QtGui.QStandardItemModel):
or thread_id != self._current_refresh_thread.id or thread_id != self._current_refresh_thread.id
): ):
return return
folder_items, folder_type_items = thread.get_result()
self._fill_items(thread.get_result()) self._fill_items(folder_items, folder_type_items)
self._current_refresh_thread = None 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: Args:
item (QtGui.QStandardItem): Item to fill data. item (QtGui.QStandardItem): Item to fill data.
folder_item (FolderItem): Folder item. 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.entity_id, FOLDER_ID_ROLE)
item.setData(folder_item.name, FOLDER_NAME_ROLE) item.setData(folder_item.name, FOLDER_NAME_ROLE)
item.setData(folder_item.path, FOLDER_PATH_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(folder_item.label, QtCore.Qt.DisplayRole)
item.setData(icon, QtCore.Qt.DecorationRole) 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 not folder_items_by_id:
if folder_items_by_id is not None: if folder_items_by_id is not None:
self._clear_items() self._clear_items()
@ -197,6 +256,11 @@ class FoldersQtModel(QtGui.QStandardItemModel):
self.refreshed.emit() self.refreshed.emit()
return 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 self._has_content = True
folder_ids = set(folder_items_by_id) folder_ids = set(folder_items_by_id)
@ -242,7 +306,12 @@ class FoldersQtModel(QtGui.QStandardItemModel):
else: else:
is_new = self._parent_id_by_id[item_id] != parent_id 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: if is_new:
new_items.append(item) new_items.append(item)
self._items_by_id[item_id] = item self._items_by_id[item_id] = item
@ -619,6 +688,7 @@ class SimpleSelectionModel(object):
class SimpleFoldersController(object): class SimpleFoldersController(object):
def __init__(self): def __init__(self):
self._event_system = self._create_event_system() self._event_system = self._create_event_system()
self._projects_model = ProjectsModel(self)
self._hierarchy_model = HierarchyModel(self) self._hierarchy_model = HierarchyModel(self)
self._selection_model = SimpleSelectionModel(self) self._selection_model = SimpleSelectionModel(self)
self._expected_selection = HierarchyExpectedSelection( self._expected_selection = HierarchyExpectedSelection(
@ -639,6 +709,11 @@ class SimpleFoldersController(object):
def get_folder_items(self, project_name, sender=None): def get_folder_items(self, project_name, sender=None):
return self._hierarchy_model.get_folder_items(project_name, sender) 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): def set_selected_project(self, project_name):
self._selection_model.set_selected_project(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 from qtpy import QtWidgets, QtCore, QtGui
import qtawesome import qtawesome
import qtmaterialsymbols
from ayon_core.style import ( from ayon_core.style import (
get_objected_colors, get_objected_colors,
@ -468,7 +469,7 @@ class _IconsCache:
if icon_type == "path": if icon_type == "path":
parts = [icon_type, icon_def["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"]] parts = [icon_type, icon_def["name"], icon_def["color"]]
return "|".join(parts) return "|".join(parts)
@ -495,6 +496,13 @@ class _IconsCache:
if icon is None: if icon is None:
icon = cls.get_qta_icon_by_name_and_color( icon = cls.get_qta_icon_by_name_and_color(
"fa.{}".format(icon_name), icon_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: if icon is None:
icon = cls.get_default() icon = cls.get_default()
cls._cache[cache_key] = icon cls._cache[cache_key] = icon

View file

@ -1,6 +1,9 @@
from qtpy import QtWidgets, QtGui, QtCore 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 .views import DeselectableTreeView
from .lib import RefreshThread, get_qt_icon from .lib import RefreshThread, get_qt_icon
@ -17,8 +20,9 @@ class TasksQtModel(QtGui.QStandardItemModel):
Args: Args:
controller (AbstractWorkfilesFrontend): The control object. controller (AbstractWorkfilesFrontend): The control object.
"""
"""
_default_task_icon = None
refreshed = QtCore.Signal() refreshed = QtCore.Signal()
def __init__(self, controller): def __init__(self, controller):
@ -176,7 +180,7 @@ class TasksQtModel(QtGui.QStandardItemModel):
return return
thread = RefreshThread( thread = RefreshThread(
folder_id, folder_id,
self._controller.get_task_items, self._thread_getter,
project_name, project_name,
folder_id folder_id
) )
@ -185,8 +189,55 @@ class TasksQtModel(QtGui.QStandardItemModel):
thread.refresh_finished.connect(self._on_refresh_thread) thread.refresh_finished.connect(self._on_refresh_thread)
thread.start() 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): def _fill_data_from_thread(self, thread):
task_items = thread.get_result() task_items, task_type_items = thread.get_result()
# Task items are refreshed # Task items are refreshed
if task_items is None: if task_items is None:
return return
@ -197,6 +248,11 @@ class TasksQtModel(QtGui.QStandardItemModel):
return return
self._remove_invalid_items() 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_items = []
new_names = set() new_names = set()
for task_item in task_items: for task_item in task_items:
@ -209,8 +265,11 @@ class TasksQtModel(QtGui.QStandardItemModel):
new_items.append(item) new_items.append(item)
self._items_by_name[name] = item self._items_by_name[name] = item
# TODO cache locally icon = self._get_task_item_icon(
icon = get_qt_icon(task_item.icon) task_item,
task_type_item_by_name,
task_type_icon_cache
)
item.setData(task_item.label, QtCore.Qt.DisplayRole) item.setData(task_item.label, QtCore.Qt.DisplayRole)
item.setData(name, ITEM_NAME_ROLE) item.setData(name, ITEM_NAME_ROLE)
item.setData(task_item.id, ITEM_ID_ROLE) item.setData(task_item.id, ITEM_ID_ROLE)

View file

@ -546,6 +546,46 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
""" """
pass 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 # Host information
@abstractmethod @abstractmethod
def get_workfile_extensions(self): def get_workfile_extensions(self):

View file

@ -231,6 +231,16 @@ class BaseWorkfileController(
return self._projects_model.get_project_entity( return self._projects_model.get_project_entity(
project_name) 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): def get_folder_entity(self, project_name, folder_id):
return self._hierarchy_model.get_folder_entity( return self._hierarchy_model.get_folder_entity(
project_name, folder_id) project_name, folder_id)

View file

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

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): 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( override_global_config: bool = SettingsField(
False, 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( filepath: list[str] = SettingsField(
default_factory=list, 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 os
import json import json
import ayon_api
from ayon_core.addon import AYONAddon, IPluginPaths, click_wrap from ayon_core.addon import AYONAddon, IPluginPaths, click_wrap
from .version import __version__ 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( def launch_application(
self, app_name, project_name, folder_path, task_name self, app_name, project_name, folder_path, task_name
): ):
@ -132,6 +223,18 @@ class ApplicationsAddon(AYONAddon, IPluginPaths):
task_name=task_name, 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 --- # --- CLI ---
def cli(self, addon_click_group): def cli(self, addon_click_group):
main_group = click_wrap.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: Args:
app_name (str): Name of application that should be launched. 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. preparation to store objects usable in multiple places.
Raises: 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.settings import get_project_settings
from ayon_core.lib import Logger, get_ayon_username from ayon_core.lib import Logger, get_ayon_username
from ayon_core.addon import AddonsManager 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.template_data import get_template_data
from ayon_core.pipeline.workfile import ( from ayon_core.pipeline.workfile import (
get_workfile_template_key, 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 "" last_workfile_path = data.get("last_workfile_path") or ""
if not last_workfile_path: if not last_workfile_path:
host_addon = addons_manager.get_host_addon(app.host_name) host_addon = addons_manager.get_host_addon(app.host_name)
extensions = None
if host_addon: if host_addon:
extensions = host_addon.get_workfile_extensions() extensions = host_addon.get_workfile_extensions()
else:
extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name)
if extensions: if extensions:
anatomy = data["anatomy"] 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

View file

@ -4,13 +4,28 @@ from ayon_server.settings.validators import ensure_unique_names
class ImageIOConfigModel(BaseSettingsModel): 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( override_global_config: bool = SettingsField(
False, 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( filepath: list[str] = SettingsField(
default_factory=list, 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

@ -4,13 +4,28 @@ from ayon_server.settings.validators import ensure_unique_names
class ImageIOConfigModel(BaseSettingsModel): 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( override_global_config: bool = SettingsField(
False, 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( filepath: list[str] = SettingsField(
default_factory=list, 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

@ -105,6 +105,8 @@ def find_files_in_subdir(
List[Tuple[str, str]]: List of tuples with path to file and parent List[Tuple[str, str]]: List of tuples with path to file and parent
directories relative to 'src_path'. directories relative to 'src_path'.
""" """
if not os.path.exists(src_path):
return []
if ignore_file_patterns is None: if ignore_file_patterns is None:
ignore_file_patterns = IGNORE_FILE_PATTERNS ignore_file_patterns = IGNORE_FILE_PATTERNS
@ -210,6 +212,7 @@ def _get_server_mapping(
addon_dir: Path, addon_version: str addon_dir: Path, addon_version: str
) -> List[Tuple[str, str]]: ) -> List[Tuple[str, str]]:
server_dir = addon_dir / "server" server_dir = addon_dir / "server"
public_dir = addon_dir / "public"
src_package_py = addon_dir / "package.py" src_package_py = addon_dir / "package.py"
pyproject_toml = addon_dir / "client" / "pyproject.toml" pyproject_toml = addon_dir / "client" / "pyproject.toml"
@ -217,6 +220,10 @@ def _get_server_mapping(
(src_path, f"server/{sub_path}") (src_path, f"server/{sub_path}")
for src_path, sub_path in find_files_in_subdir(str(server_dir)) for src_path, sub_path in find_files_in_subdir(str(server_dir))
] ]
mapping.extend([
(src_path, f"public/{sub_path}")
for src_path, sub_path in find_files_in_subdir(str(public_dir))
])
mapping.append((src_package_py.as_posix(), "package.py")) mapping.append((src_package_py.as_posix(), "package.py"))
if pyproject_toml.exists(): if pyproject_toml.exists():
mapping.append((pyproject_toml.as_posix(), "private/pyproject.toml")) mapping.append((pyproject_toml.as_posix(), "private/pyproject.toml"))

View file

@ -40,13 +40,28 @@ class ImageIORemappingModel(BaseSettingsModel):
class ImageIOConfigModel(BaseSettingsModel): 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( override_global_config: bool = SettingsField(
False, 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( filepath: list[str] = SettingsField(
default_factory=list, 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

@ -4,13 +4,28 @@ from ayon_server.settings.validators import ensure_unique_names
class ImageIOConfigModel(BaseSettingsModel): 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( override_global_config: bool = SettingsField(
False, 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( filepath: list[str] = SettingsField(
default_factory=list, 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

@ -4,13 +4,28 @@ from ayon_server.settings.validators import ensure_unique_names
class ImageIOConfigModel(BaseSettingsModel): 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( override_global_config: bool = SettingsField(
False, 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( filepath: list[str] = SettingsField(
default_factory=list, 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)."
),
) )

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