Merge branch 'develop' of https://github.com/ynput/ayon-core into bugfix/AY-5750_AE-review-not-displayed-in-Ftrack
|
|
@ -3,6 +3,7 @@ import warnings
|
|||
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.settings import get_studio_settings, get_project_settings
|
||||
from ayon_core.pipeline.plugin_discover import (
|
||||
discover,
|
||||
register_plugin,
|
||||
|
|
@ -40,7 +41,8 @@ class LauncherActionSelection:
|
|||
task_name=None,
|
||||
project_entity=None,
|
||||
folder_entity=None,
|
||||
task_entity=None
|
||||
task_entity=None,
|
||||
project_settings=None,
|
||||
):
|
||||
self._project_name = project_name
|
||||
self._folder_id = folder_id
|
||||
|
|
@ -53,6 +55,8 @@ class LauncherActionSelection:
|
|||
self._folder_entity = folder_entity
|
||||
self._task_entity = task_entity
|
||||
|
||||
self._project_settings = project_settings
|
||||
|
||||
def __getitem__(self, key):
|
||||
warnings.warn(
|
||||
(
|
||||
|
|
@ -255,6 +259,22 @@ class LauncherActionSelection:
|
|||
)
|
||||
return self._task_entity
|
||||
|
||||
def get_project_settings(self):
|
||||
"""Project settings for the selection.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Project settings or studio settings if
|
||||
project is not selected.
|
||||
|
||||
"""
|
||||
if self._project_settings is None:
|
||||
if self._project_name is None:
|
||||
settings = get_studio_settings()
|
||||
else:
|
||||
settings = get_project_settings(self._project_name)
|
||||
self._project_settings = settings
|
||||
return self._project_settings
|
||||
|
||||
@property
|
||||
def is_project_selected(self):
|
||||
"""Return whether a project is selected.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.9 KiB |
|
|
@ -5,7 +5,6 @@ from abc import ABCMeta, abstractmethod
|
|||
import ayon_api
|
||||
import six
|
||||
|
||||
from ayon_core.style import get_default_entity_icon_color
|
||||
from ayon_core.lib import NestedCacheItem
|
||||
|
||||
HIERARCHY_MODEL_SENDER = "hierarchy.model"
|
||||
|
|
@ -31,11 +30,10 @@ class FolderItem:
|
|||
path (str): Folder path.
|
||||
folder_type (str): Type of folder.
|
||||
label (Union[str, None]): Folder label.
|
||||
icon (Union[dict[str, Any], None]): Icon definition.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, entity_id, parent_id, name, path, folder_type, label, icon
|
||||
self, entity_id, parent_id, name, path, folder_type, label
|
||||
):
|
||||
self.entity_id = entity_id
|
||||
self.parent_id = parent_id
|
||||
|
|
@ -43,13 +41,6 @@ class FolderItem:
|
|||
self.path = path
|
||||
self.folder_type = folder_type
|
||||
self.label = label or name
|
||||
if not icon:
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.folder",
|
||||
"color": get_default_entity_icon_color()
|
||||
}
|
||||
self.icon = icon
|
||||
|
||||
def to_data(self):
|
||||
"""Converts folder item to data.
|
||||
|
|
@ -65,7 +56,6 @@ class FolderItem:
|
|||
"path": self.path,
|
||||
"folder_type": self.folder_type,
|
||||
"label": self.label,
|
||||
"icon": self.icon,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -95,23 +85,15 @@ class TaskItem:
|
|||
name (str): Name of task.
|
||||
task_type (str): Type of task.
|
||||
parent_id (str): Parent folder id.
|
||||
icon (Union[dict[str, Any], None]): Icon definitions.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, task_id, name, task_type, parent_id, icon
|
||||
self, task_id, name, task_type, parent_id
|
||||
):
|
||||
self.task_id = task_id
|
||||
self.name = name
|
||||
self.task_type = task_type
|
||||
self.parent_id = parent_id
|
||||
if icon is None:
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.male",
|
||||
"color": get_default_entity_icon_color()
|
||||
}
|
||||
self.icon = icon
|
||||
|
||||
self._label = None
|
||||
|
||||
|
|
@ -149,7 +131,6 @@ class TaskItem:
|
|||
"name": self.name,
|
||||
"parent_id": self.parent_id,
|
||||
"task_type": self.task_type,
|
||||
"icon": self.icon,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -180,8 +161,7 @@ def _get_task_items_from_tasks(tasks):
|
|||
task["id"],
|
||||
task["name"],
|
||||
task["type"],
|
||||
folder_id,
|
||||
None
|
||||
folder_id
|
||||
))
|
||||
return output
|
||||
|
||||
|
|
@ -197,8 +177,7 @@ def _get_folder_item_from_hierarchy_item(item):
|
|||
name,
|
||||
path,
|
||||
item["folderType"],
|
||||
item["label"],
|
||||
None,
|
||||
item["label"]
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -210,8 +189,7 @@ def _get_folder_item_from_entity(entity):
|
|||
name,
|
||||
entity["path"],
|
||||
entity["folderType"],
|
||||
entity["label"] or name,
|
||||
None,
|
||||
entity["label"] or name
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,74 @@ class StatusItem:
|
|||
)
|
||||
|
||||
|
||||
class FolderTypeItem:
|
||||
"""Item representing folder type of project.
|
||||
|
||||
Args:
|
||||
name (str): Folder type name ("Shot").
|
||||
short (str): Short folder type name ("sh").
|
||||
icon (str): Icon name in MaterialIcons ("fiber_new").
|
||||
|
||||
"""
|
||||
def __init__(self, name, short, icon):
|
||||
self.name = name
|
||||
self.short = short
|
||||
self.icon = icon
|
||||
|
||||
def to_data(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"short": self.short,
|
||||
"icon": self.icon,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
return cls(**data)
|
||||
|
||||
@classmethod
|
||||
def from_project_item(cls, folder_type_data):
|
||||
return cls(
|
||||
name=folder_type_data["name"],
|
||||
short=folder_type_data["shortName"],
|
||||
icon=folder_type_data["icon"],
|
||||
)
|
||||
|
||||
|
||||
class TaskTypeItem:
|
||||
"""Item representing task type of project.
|
||||
|
||||
Args:
|
||||
name (str): Task type name ("Shot").
|
||||
short (str): Short task type name ("sh").
|
||||
icon (str): Icon name in MaterialIcons ("fiber_new").
|
||||
|
||||
"""
|
||||
def __init__(self, name, short, icon):
|
||||
self.name = name
|
||||
self.short = short
|
||||
self.icon = icon
|
||||
|
||||
def to_data(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"short": self.short,
|
||||
"icon": self.icon,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
return cls(**data)
|
||||
|
||||
@classmethod
|
||||
def from_project_item(cls, task_type_data):
|
||||
return cls(
|
||||
name=task_type_data["name"],
|
||||
short=task_type_data["shortName"],
|
||||
icon=task_type_data["icon"],
|
||||
)
|
||||
|
||||
|
||||
class ProjectItem:
|
||||
"""Item representing folder entity on a server.
|
||||
|
||||
|
|
@ -147,19 +215,21 @@ def _get_project_items_from_entitiy(projects):
|
|||
class ProjectsModel(object):
|
||||
def __init__(self, controller):
|
||||
self._projects_cache = CacheItem(default_factory=list)
|
||||
self._project_statuses_cache = NestedCacheItem(
|
||||
levels=1, default_factory=list
|
||||
)
|
||||
self._projects_by_name = NestedCacheItem(
|
||||
levels=1, default_factory=list
|
||||
)
|
||||
self._project_statuses_cache = {}
|
||||
self._folder_types_cache = {}
|
||||
self._task_types_cache = {}
|
||||
|
||||
self._is_refreshing = False
|
||||
self._controller = controller
|
||||
|
||||
def reset(self):
|
||||
self._project_statuses_cache = {}
|
||||
self._folder_types_cache = {}
|
||||
self._task_types_cache = {}
|
||||
self._projects_cache.reset()
|
||||
self._project_statuses_cache.reset()
|
||||
self._projects_by_name.reset()
|
||||
|
||||
def refresh(self):
|
||||
|
|
@ -217,22 +287,87 @@ class ProjectsModel(object):
|
|||
list[StatusItem]: Status items for project.
|
||||
|
||||
"""
|
||||
statuses_cache = self._project_statuses_cache[project_name]
|
||||
if not statuses_cache.is_valid:
|
||||
with self._project_statuses_refresh_event_manager(
|
||||
sender, project_name
|
||||
if project_name is None:
|
||||
return []
|
||||
|
||||
statuses_cache = self._project_statuses_cache.get(project_name)
|
||||
if (
|
||||
statuses_cache is not None
|
||||
and not self._projects_cache.is_valid
|
||||
):
|
||||
statuses_cache = None
|
||||
|
||||
if statuses_cache is None:
|
||||
with self._project_items_refresh_event_manager(
|
||||
sender, project_name, "statuses"
|
||||
):
|
||||
project_entity = None
|
||||
if project_name:
|
||||
project_entity = self.get_project_entity(project_name)
|
||||
project_entity = self.get_project_entity(project_name)
|
||||
statuses = []
|
||||
if project_entity:
|
||||
statuses = [
|
||||
StatusItem.from_project_item(status)
|
||||
for status in project_entity["statuses"]
|
||||
]
|
||||
statuses_cache.update_data(statuses)
|
||||
return statuses_cache.get_data()
|
||||
statuses_cache = statuses
|
||||
self._project_statuses_cache[project_name] = statuses_cache
|
||||
return list(statuses_cache)
|
||||
|
||||
def get_folder_type_items(self, project_name, sender):
|
||||
"""Get project status items.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
sender (Union[str, None]): Name of sender who asked for items.
|
||||
|
||||
Returns:
|
||||
list[FolderType]: Folder type items for project.
|
||||
|
||||
"""
|
||||
return self._get_project_items(
|
||||
project_name,
|
||||
sender,
|
||||
"folder_types",
|
||||
self._folder_types_cache,
|
||||
self._folder_type_items_getter,
|
||||
)
|
||||
|
||||
def get_task_type_items(self, project_name, sender):
|
||||
"""Get project task type items.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
sender (Union[str, None]): Name of sender who asked for items.
|
||||
|
||||
Returns:
|
||||
list[TaskTypeItem]: Task type items for project.
|
||||
|
||||
"""
|
||||
return self._get_project_items(
|
||||
project_name,
|
||||
sender,
|
||||
"task_types",
|
||||
self._task_types_cache,
|
||||
self._task_type_items_getter,
|
||||
)
|
||||
|
||||
def _get_project_items(
|
||||
self, project_name, sender, item_type, cache_obj, getter
|
||||
):
|
||||
if (
|
||||
project_name in cache_obj
|
||||
and (
|
||||
project_name is None
|
||||
or self._projects_by_name[project_name].is_valid
|
||||
)
|
||||
):
|
||||
return cache_obj[project_name]
|
||||
|
||||
with self._project_items_refresh_event_manager(
|
||||
sender, project_name, item_type
|
||||
):
|
||||
cache_value = getter(self.get_project_entity(project_name))
|
||||
cache_obj[project_name] = cache_value
|
||||
return cache_value
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _project_refresh_event_manager(self, sender):
|
||||
|
|
@ -254,9 +389,11 @@ class ProjectsModel(object):
|
|||
self._is_refreshing = False
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _project_statuses_refresh_event_manager(self, sender, project_name):
|
||||
def _project_items_refresh_event_manager(
|
||||
self, sender, project_name, item_type
|
||||
):
|
||||
self._controller.emit_event(
|
||||
"projects.statuses.refresh.started",
|
||||
f"projects.{item_type}.refresh.started",
|
||||
{"sender": sender, "project_name": project_name},
|
||||
PROJECTS_MODEL_SENDER
|
||||
)
|
||||
|
|
@ -265,7 +402,7 @@ class ProjectsModel(object):
|
|||
|
||||
finally:
|
||||
self._controller.emit_event(
|
||||
"projects.statuses.refresh.finished",
|
||||
f"projects.{item_type}.refresh.finished",
|
||||
{"sender": sender, "project_name": project_name},
|
||||
PROJECTS_MODEL_SENDER
|
||||
)
|
||||
|
|
@ -282,3 +419,27 @@ class ProjectsModel(object):
|
|||
def _query_projects(self):
|
||||
projects = ayon_api.get_projects(fields=["name", "active", "library"])
|
||||
return _get_project_items_from_entitiy(projects)
|
||||
|
||||
def _status_items_getter(self, project_entity):
|
||||
if not project_entity:
|
||||
return []
|
||||
return [
|
||||
StatusItem.from_project_item(status)
|
||||
for status in project_entity["statuses"]
|
||||
]
|
||||
|
||||
def _folder_type_items_getter(self, project_entity):
|
||||
if not project_entity:
|
||||
return []
|
||||
return [
|
||||
FolderTypeItem.from_project_item(folder_type)
|
||||
for folder_type in project_entity["folderTypes"]
|
||||
]
|
||||
|
||||
def _task_type_items_getter(self, project_entity):
|
||||
if not project_entity:
|
||||
return []
|
||||
return [
|
||||
TaskTypeItem.from_project_item(task_type)
|
||||
for task_type in project_entity["taskTypes"]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,80 @@
|
|||
import ayon_api
|
||||
import json
|
||||
import collections
|
||||
|
||||
from ayon_core.lib import CacheItem
|
||||
import ayon_api
|
||||
from ayon_api.graphql import FIELD_VALUE, GraphQlQuery, fields_to_dict
|
||||
|
||||
from ayon_core.lib import NestedCacheItem
|
||||
|
||||
|
||||
# --- Implementation that should be in ayon-python-api ---
|
||||
# The implementation is not available in all versions of ayon-python-api.
|
||||
def users_graphql_query(fields):
|
||||
query = GraphQlQuery("Users")
|
||||
names_var = query.add_variable("userNames", "[String!]")
|
||||
project_name_var = query.add_variable("projectName", "String!")
|
||||
|
||||
users_field = query.add_field_with_edges("users")
|
||||
users_field.set_filter("names", names_var)
|
||||
users_field.set_filter("projectName", project_name_var)
|
||||
|
||||
nested_fields = fields_to_dict(set(fields))
|
||||
|
||||
query_queue = collections.deque()
|
||||
for key, value in nested_fields.items():
|
||||
query_queue.append((key, value, users_field))
|
||||
|
||||
while query_queue:
|
||||
item = query_queue.popleft()
|
||||
key, value, parent = item
|
||||
field = parent.add_field(key)
|
||||
if value is FIELD_VALUE:
|
||||
continue
|
||||
|
||||
for k, v in value.items():
|
||||
query_queue.append((k, v, field))
|
||||
return query
|
||||
|
||||
|
||||
def get_users(project_name=None, usernames=None, fields=None):
|
||||
"""Get Users.
|
||||
|
||||
Only administrators and managers can fetch all users. For other users
|
||||
it is required to pass in 'project_name' filter.
|
||||
|
||||
Args:
|
||||
project_name (Optional[str]): Project name.
|
||||
usernames (Optional[Iterable[str]]): Filter by usernames.
|
||||
fields (Optional[Iterable[str]]): Fields to be queried
|
||||
for users.
|
||||
|
||||
Returns:
|
||||
Generator[dict[str, Any]]: Queried users.
|
||||
|
||||
"""
|
||||
filters = {}
|
||||
if usernames is not None:
|
||||
usernames = set(usernames)
|
||||
if not usernames:
|
||||
return
|
||||
filters["userNames"] = list(usernames)
|
||||
|
||||
if project_name is not None:
|
||||
filters["projectName"] = project_name
|
||||
|
||||
con = ayon_api.get_server_api_connection()
|
||||
if not fields:
|
||||
fields = con.get_default_fields_for_type("user")
|
||||
|
||||
query = users_graphql_query(set(fields))
|
||||
for attr, filter_value in filters.items():
|
||||
query.set_variable_value(attr, filter_value)
|
||||
|
||||
for parsed_data in query.continuous_query(con):
|
||||
for user in parsed_data["users"]:
|
||||
user["accessGroups"] = json.loads(user["accessGroups"])
|
||||
yield user
|
||||
# --- END of ayon-python-api implementation ---
|
||||
|
||||
|
||||
class UserItem:
|
||||
|
|
@ -32,19 +106,19 @@ class UserItem:
|
|||
class UsersModel:
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
self._users_cache = CacheItem(default_factory=list)
|
||||
self._users_cache = NestedCacheItem(default_factory=list)
|
||||
|
||||
def get_user_items(self):
|
||||
def get_user_items(self, project_name):
|
||||
"""Get user items.
|
||||
|
||||
Returns:
|
||||
List[UserItem]: List of user items.
|
||||
|
||||
"""
|
||||
self._invalidate_cache()
|
||||
return self._users_cache.get_data()
|
||||
self._invalidate_cache(project_name)
|
||||
return self._users_cache[project_name].get_data()
|
||||
|
||||
def get_user_items_by_name(self):
|
||||
def get_user_items_by_name(self, project_name):
|
||||
"""Get user items by name.
|
||||
|
||||
Implemented as most of cases using this model will need to find
|
||||
|
|
@ -56,10 +130,10 @@ class UsersModel:
|
|||
"""
|
||||
return {
|
||||
user_item.username: user_item
|
||||
for user_item in self.get_user_items()
|
||||
for user_item in self.get_user_items(project_name)
|
||||
}
|
||||
|
||||
def get_user_item_by_username(self, username):
|
||||
def get_user_item_by_username(self, project_name, username):
|
||||
"""Get user item by username.
|
||||
|
||||
Args:
|
||||
|
|
@ -69,16 +143,22 @@ class UsersModel:
|
|||
Union[UserItem, None]: User item or None if not found.
|
||||
|
||||
"""
|
||||
self._invalidate_cache()
|
||||
for user_item in self.get_user_items():
|
||||
self._invalidate_cache(project_name)
|
||||
for user_item in self.get_user_items(project_name):
|
||||
if user_item.username == username:
|
||||
return user_item
|
||||
return None
|
||||
|
||||
def _invalidate_cache(self):
|
||||
if self._users_cache.is_valid:
|
||||
def _invalidate_cache(self, project_name):
|
||||
cache = self._users_cache[project_name]
|
||||
if cache.is_valid:
|
||||
return
|
||||
self._users_cache.update_data([
|
||||
|
||||
if project_name is None:
|
||||
cache.update_data([])
|
||||
return
|
||||
|
||||
self._users_cache[project_name].update_data([
|
||||
UserItem.from_entity_data(user)
|
||||
for user in ayon_api.get_users()
|
||||
for user in get_users(project_name)
|
||||
])
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from .window import ContextDialog, main
|
||||
from .window import ContextDialog, main, ask_for_context
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ContextDialog",
|
||||
"main",
|
||||
"ask_for_context"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -227,6 +227,16 @@ class ContextDialogController:
|
|||
def get_project_items(self, sender=None):
|
||||
return self._projects_model.get_project_items(sender)
|
||||
|
||||
def get_folder_type_items(self, project_name, sender=None):
|
||||
return self._projects_model.get_folder_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
def get_task_type_items(self, project_name, sender=None):
|
||||
return self._projects_model.get_task_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
def get_folder_items(self, project_name, sender=None):
|
||||
return self._hierarchy_model.get_folder_items(project_name, sender)
|
||||
|
||||
|
|
@ -791,3 +801,12 @@ def main(
|
|||
window.show()
|
||||
app.exec_()
|
||||
controller.store_output()
|
||||
|
||||
|
||||
def ask_for_context(strict=True):
|
||||
controller = ContextDialogController()
|
||||
controller.set_strict(strict)
|
||||
window = ContextDialog(controller=controller)
|
||||
window.exec_()
|
||||
|
||||
return controller.get_selected_context()
|
||||
|
|
|
|||
|
|
@ -104,8 +104,48 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
Returns:
|
||||
list[ProjectItem]: Minimum possible information needed
|
||||
for visualisation of folder hierarchy.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_folder_type_items(self, project_name, sender=None):
|
||||
"""Folder type items for a project.
|
||||
|
||||
This function may trigger events with topics
|
||||
'projects.folder_types.refresh.started' and
|
||||
'projects.folder_types.refresh.finished' which will contain 'sender'
|
||||
value in data.
|
||||
That may help to avoid re-refresh of items in UI elements.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
sender (str): Who requested folder type items.
|
||||
|
||||
Returns:
|
||||
list[FolderTypeItem]: Folder type information.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_type_items(self, project_name, sender=None):
|
||||
"""Task type items for a project.
|
||||
|
||||
This function may trigger events with topics
|
||||
'projects.task_types.refresh.started' and
|
||||
'projects.task_types.refresh.finished' which will contain 'sender'
|
||||
value in data.
|
||||
That may help to avoid re-refresh of items in UI elements.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
sender (str): Who requested task type items.
|
||||
|
||||
Returns:
|
||||
list[TaskTypeItem]: Task type information.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
|
|||
|
|
@ -59,12 +59,23 @@ class BaseLauncherController(
|
|||
def get_project_items(self, sender=None):
|
||||
return self._projects_model.get_project_items(sender)
|
||||
|
||||
def get_folder_type_items(self, project_name, sender=None):
|
||||
return self._projects_model.get_folder_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
def get_task_type_items(self, project_name, sender=None):
|
||||
return self._projects_model.get_task_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
def get_folder_items(self, project_name, sender=None):
|
||||
return self._hierarchy_model.get_folder_items(project_name, sender)
|
||||
|
||||
def get_task_items(self, project_name, folder_id, sender=None):
|
||||
return self._hierarchy_model.get_task_items(
|
||||
project_name, folder_id, sender)
|
||||
project_name, folder_id, sender
|
||||
)
|
||||
|
||||
# Project settings for applications actions
|
||||
def get_project_settings(self, project_name):
|
||||
|
|
|
|||
|
|
@ -10,6 +10,108 @@ from ayon_core.pipeline.actions import (
|
|||
)
|
||||
from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch
|
||||
|
||||
try:
|
||||
# Available since applications addon 0.2.4
|
||||
from ayon_applications.action import ApplicationAction
|
||||
except ImportError:
|
||||
# Backwards compatibility from 0.3.3 (24/06/10)
|
||||
# TODO: Remove in future releases
|
||||
class ApplicationAction(LauncherAction):
|
||||
"""Action to launch an application.
|
||||
|
||||
Application action based on 'ApplicationManager' system.
|
||||
|
||||
Handling of applications in launcher is not ideal and should be completely
|
||||
redone from scratch. This is just a temporary solution to keep backwards
|
||||
compatibility with AYON launcher.
|
||||
|
||||
Todos:
|
||||
Move handling of errors to frontend.
|
||||
"""
|
||||
|
||||
# Application object
|
||||
application = None
|
||||
# Action attributes
|
||||
name = None
|
||||
label = None
|
||||
label_variant = None
|
||||
group = None
|
||||
icon = None
|
||||
color = None
|
||||
order = 0
|
||||
data = {}
|
||||
project_settings = {}
|
||||
project_entities = {}
|
||||
|
||||
_log = None
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
self._log = Logger.get_logger(self.__class__.__name__)
|
||||
return self._log
|
||||
|
||||
def is_compatible(self, selection):
|
||||
if not selection.is_task_selected:
|
||||
return False
|
||||
|
||||
project_entity = self.project_entities[selection.project_name]
|
||||
apps = project_entity["attrib"].get("applications")
|
||||
if not apps or self.application.full_name not in apps:
|
||||
return False
|
||||
|
||||
project_settings = self.project_settings[selection.project_name]
|
||||
only_available = project_settings["applications"]["only_available"]
|
||||
if only_available and not self.application.find_executable():
|
||||
return False
|
||||
return True
|
||||
|
||||
def _show_message_box(self, title, message, details=None):
|
||||
from qtpy import QtWidgets, QtGui
|
||||
from ayon_core import style
|
||||
|
||||
dialog = QtWidgets.QMessageBox()
|
||||
icon = QtGui.QIcon(resources.get_ayon_icon_filepath())
|
||||
dialog.setWindowIcon(icon)
|
||||
dialog.setStyleSheet(style.load_stylesheet())
|
||||
dialog.setWindowTitle(title)
|
||||
dialog.setText(message)
|
||||
if details:
|
||||
dialog.setDetailedText(details)
|
||||
dialog.exec_()
|
||||
|
||||
def process(self, selection, **kwargs):
|
||||
"""Process the full Application action"""
|
||||
|
||||
from ayon_applications import (
|
||||
ApplicationExecutableNotFound,
|
||||
ApplicationLaunchFailed,
|
||||
)
|
||||
|
||||
try:
|
||||
self.application.launch(
|
||||
project_name=selection.project_name,
|
||||
folder_path=selection.folder_path,
|
||||
task_name=selection.task_name,
|
||||
**self.data
|
||||
)
|
||||
|
||||
except ApplicationExecutableNotFound as exc:
|
||||
details = exc.details
|
||||
msg = exc.msg
|
||||
log_msg = str(msg)
|
||||
if details:
|
||||
log_msg += "\n" + details
|
||||
self.log.warning(log_msg)
|
||||
self._show_message_box(
|
||||
"Application executable not found", msg, details
|
||||
)
|
||||
|
||||
except ApplicationLaunchFailed as exc:
|
||||
msg = str(exc)
|
||||
self.log.warning(msg, exc_info=True)
|
||||
self._show_message_box("Application launch failed", msg)
|
||||
|
||||
|
||||
# class Action:
|
||||
# def __init__(self, label, icon=None, identifier=None):
|
||||
|
|
@ -43,103 +145,6 @@ from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch
|
|||
# self._actions.append(action)
|
||||
|
||||
|
||||
class ApplicationAction(LauncherAction):
|
||||
"""Action to launch an application.
|
||||
|
||||
Application action based on 'ApplicationManager' system.
|
||||
|
||||
Handling of applications in launcher is not ideal and should be completely
|
||||
redone from scratch. This is just a temporary solution to keep backwards
|
||||
compatibility with AYON launcher.
|
||||
|
||||
Todos:
|
||||
Move handling of errors to frontend.
|
||||
"""
|
||||
|
||||
# Application object
|
||||
application = None
|
||||
# Action attributes
|
||||
name = None
|
||||
label = None
|
||||
label_variant = None
|
||||
group = None
|
||||
icon = None
|
||||
color = None
|
||||
order = 0
|
||||
data = {}
|
||||
project_settings = {}
|
||||
project_entities = {}
|
||||
|
||||
_log = None
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
self._log = Logger.get_logger(self.__class__.__name__)
|
||||
return self._log
|
||||
|
||||
def is_compatible(self, selection):
|
||||
if not selection.is_task_selected:
|
||||
return False
|
||||
|
||||
project_entity = self.project_entities[selection.project_name]
|
||||
apps = project_entity["attrib"].get("applications")
|
||||
if not apps or self.application.full_name not in apps:
|
||||
return False
|
||||
|
||||
project_settings = self.project_settings[selection.project_name]
|
||||
only_available = project_settings["applications"]["only_available"]
|
||||
if only_available and not self.application.find_executable():
|
||||
return False
|
||||
return True
|
||||
|
||||
def _show_message_box(self, title, message, details=None):
|
||||
from qtpy import QtWidgets, QtGui
|
||||
from ayon_core import style
|
||||
|
||||
dialog = QtWidgets.QMessageBox()
|
||||
icon = QtGui.QIcon(resources.get_ayon_icon_filepath())
|
||||
dialog.setWindowIcon(icon)
|
||||
dialog.setStyleSheet(style.load_stylesheet())
|
||||
dialog.setWindowTitle(title)
|
||||
dialog.setText(message)
|
||||
if details:
|
||||
dialog.setDetailedText(details)
|
||||
dialog.exec_()
|
||||
|
||||
def process(self, selection, **kwargs):
|
||||
"""Process the full Application action"""
|
||||
|
||||
from ayon_applications import (
|
||||
ApplicationExecutableNotFound,
|
||||
ApplicationLaunchFailed,
|
||||
)
|
||||
|
||||
try:
|
||||
self.application.launch(
|
||||
project_name=selection.project_name,
|
||||
folder_path=selection.folder_path,
|
||||
task_name=selection.task_name,
|
||||
**self.data
|
||||
)
|
||||
|
||||
except ApplicationExecutableNotFound as exc:
|
||||
details = exc.details
|
||||
msg = exc.msg
|
||||
log_msg = str(msg)
|
||||
if details:
|
||||
log_msg += "\n" + details
|
||||
self.log.warning(log_msg)
|
||||
self._show_message_box(
|
||||
"Application executable not found", msg, details
|
||||
)
|
||||
|
||||
except ApplicationLaunchFailed as exc:
|
||||
msg = str(exc)
|
||||
self.log.warning(msg, exc_info=True)
|
||||
self._show_message_box("Application launch failed", msg)
|
||||
|
||||
|
||||
class ActionItem:
|
||||
"""Item representing single action to trigger.
|
||||
|
||||
|
|
@ -440,7 +445,17 @@ class ActionsModel:
|
|||
)
|
||||
|
||||
def _prepare_selection(self, project_name, folder_id, task_id):
|
||||
return LauncherActionSelection(project_name, folder_id, task_id)
|
||||
project_entity = None
|
||||
if project_name:
|
||||
project_entity = self._controller.get_project_entity(project_name)
|
||||
project_settings = self._controller.get_project_settings(project_name)
|
||||
return LauncherActionSelection(
|
||||
project_name,
|
||||
folder_id,
|
||||
task_id,
|
||||
project_entity=project_entity,
|
||||
project_settings=project_settings,
|
||||
)
|
||||
|
||||
def _get_discovered_action_classes(self):
|
||||
if self._discovered_actions is None:
|
||||
|
|
@ -475,7 +490,9 @@ class ActionsModel:
|
|||
action_items = {}
|
||||
for identifier, action in self._get_action_objects().items():
|
||||
is_application = isinstance(action, ApplicationAction)
|
||||
if is_application:
|
||||
# Backwards compatibility from 0.3.3 (24/06/10)
|
||||
# TODO: Remove in future releases
|
||||
if is_application and hasattr(action, "project_settings"):
|
||||
action.project_entities[project_name] = project_entity
|
||||
action.project_settings[project_name] = project_settings
|
||||
|
||||
|
|
@ -497,10 +514,14 @@ class ActionsModel:
|
|||
return action_items
|
||||
|
||||
def _get_applications_action_classes(self):
|
||||
actions = []
|
||||
|
||||
addons_manager = self._get_addons_manager()
|
||||
applications_addon = addons_manager.get_enabled_addon("applications")
|
||||
if hasattr(applications_addon, "get_applications_action_classes"):
|
||||
return applications_addon.get_applications_action_classes()
|
||||
|
||||
# Backwards compatibility from 0.3.3 (24/06/10)
|
||||
# TODO: Remove in future releases
|
||||
actions = []
|
||||
if applications_addon is None:
|
||||
return actions
|
||||
|
||||
|
|
|
|||
|
|
@ -510,6 +510,26 @@ class FrontendLoaderController(_BaseLoaderController):
|
|||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_folder_type_items(self, project_name, sender=None):
|
||||
"""Folder type items for a project.
|
||||
|
||||
This function may trigger events with topics
|
||||
'projects.folder_types.refresh.started' and
|
||||
'projects.folder_types.refresh.finished' which will contain 'sender'
|
||||
value in data.
|
||||
That may help to avoid re-refresh of items in UI elements.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
sender (str): Who requested folder type items.
|
||||
|
||||
Returns:
|
||||
list[FolderTypeItem]: Folder type information.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_project_status_items(self, project_name, sender=None):
|
||||
"""Items for all projects available on server.
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@ import uuid
|
|||
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.lib import NestedCacheItem, CacheItem
|
||||
from ayon_core.lib.events import QueuedEventSystem
|
||||
from ayon_core.pipeline import Anatomy, get_current_context
|
||||
from ayon_core.host import ILoadHost
|
||||
from ayon_core.tools.common_models import (
|
||||
ProjectsModel,
|
||||
HierarchyModel,
|
||||
NestedCacheItem,
|
||||
CacheItem,
|
||||
ThumbnailsModel,
|
||||
)
|
||||
|
||||
|
|
@ -180,6 +179,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
|||
def get_project_items(self, sender=None):
|
||||
return self._projects_model.get_project_items(sender)
|
||||
|
||||
def get_folder_type_items(self, project_name, sender=None):
|
||||
return self._projects_model.get_folder_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
def get_project_status_items(self, project_name, sender=None):
|
||||
return self._projects_model.get_project_status_items(
|
||||
project_name, sender
|
||||
|
|
|
|||
|
|
@ -188,16 +188,6 @@ class LoaderFoldersModel(FoldersQtModel):
|
|||
|
||||
self._colored_items = set()
|
||||
|
||||
def _fill_item_data(self, item, folder_item):
|
||||
"""
|
||||
|
||||
Args:
|
||||
item (QtGui.QStandardItem): Item to fill data.
|
||||
folder_item (FolderItem): Folder item.
|
||||
"""
|
||||
|
||||
super(LoaderFoldersModel, self)._fill_item_data(item, folder_item)
|
||||
|
||||
def set_merged_products_selection(self, items):
|
||||
changes = {
|
||||
folder_id: None
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
self._last_project_name = None
|
||||
self._last_folder_ids = []
|
||||
self._last_project_statuses = {}
|
||||
self._last_status_icons_by_name = {}
|
||||
|
||||
def get_product_item_indexes(self):
|
||||
return [
|
||||
|
|
@ -181,6 +182,13 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
return status_item.color
|
||||
|
||||
col = index.column()
|
||||
if col == self.status_col and role == QtCore.Qt.DecorationRole:
|
||||
role = VERSION_STATUS_ICON_ROLE
|
||||
|
||||
if role == VERSION_STATUS_ICON_ROLE:
|
||||
status_name = self.data(index, VERSION_STATUS_NAME_ROLE)
|
||||
return self._get_status_icon(status_name)
|
||||
|
||||
if col == 0:
|
||||
return super(ProductsModel, self).data(index, role)
|
||||
|
||||
|
|
@ -260,6 +268,25 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
break
|
||||
yield color
|
||||
|
||||
def _get_status_icon(self, status_name):
|
||||
icon = self._last_status_icons_by_name.get(status_name)
|
||||
if icon is not None:
|
||||
return icon
|
||||
|
||||
status_item = self._last_project_statuses.get(status_name)
|
||||
if status_item is not None:
|
||||
icon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": status_item.icon,
|
||||
"color": status_item.color,
|
||||
})
|
||||
|
||||
if icon is None:
|
||||
icon = QtGui.QIcon()
|
||||
|
||||
self._last_status_icons_by_name[status_name] = icon
|
||||
return icon
|
||||
|
||||
def _clear(self):
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
|
|
@ -419,6 +446,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
status_item.name: status_item
|
||||
for status_item in status_items
|
||||
}
|
||||
self._last_status_icons_by_name = {}
|
||||
|
||||
active_site_icon_def = self._controller.get_active_site_icon_def(
|
||||
project_name
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ from ayon_core.pipeline.create.context import (
|
|||
ConvertorsOperationFailed,
|
||||
)
|
||||
from ayon_core.pipeline.publish import get_publish_instance_label
|
||||
from ayon_core.tools.common_models import HierarchyModel
|
||||
from ayon_core.tools.common_models import ProjectsModel, HierarchyModel
|
||||
from ayon_core.lib.profiles_filtering import filter_profiles
|
||||
|
||||
# Define constant for plugin orders offset
|
||||
|
|
@ -1632,6 +1632,7 @@ class PublisherController(BasePublisherController):
|
|||
self._resetting_instances = False
|
||||
|
||||
# Cacher of avalon documents
|
||||
self._projects_model = ProjectsModel(self)
|
||||
self._hierarchy_model = HierarchyModel(self)
|
||||
|
||||
@property
|
||||
|
|
@ -1697,6 +1698,16 @@ class PublisherController(BasePublisherController):
|
|||
|
||||
return self._create_context.get_current_project_settings()
|
||||
|
||||
def get_folder_type_items(self, project_name, sender=None):
|
||||
return self._projects_model.get_folder_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
def get_task_type_items(self, project_name, sender=None):
|
||||
return self._projects_model.get_task_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
# Hierarchy model
|
||||
def get_folder_items(self, project_name, sender=None):
|
||||
return self._hierarchy_model.get_folder_items(project_name, sender)
|
||||
|
|
@ -1725,14 +1736,14 @@ class PublisherController(BasePublisherController):
|
|||
return folder_item.entity_id
|
||||
return None
|
||||
|
||||
def get_task_names_by_folder_paths(self, folder_paths):
|
||||
def get_task_items_by_folder_paths(self, folder_paths):
|
||||
if not folder_paths:
|
||||
return {}
|
||||
folder_items = self._hierarchy_model.get_folder_items_by_paths(
|
||||
self.project_name, folder_paths
|
||||
)
|
||||
output = {
|
||||
folder_path: set()
|
||||
folder_path: []
|
||||
for folder_path in folder_paths
|
||||
}
|
||||
project_name = self.project_name
|
||||
|
|
@ -1740,10 +1751,7 @@ class PublisherController(BasePublisherController):
|
|||
task_items = self._hierarchy_model.get_task_items(
|
||||
project_name, folder_item.entity_id, None
|
||||
)
|
||||
output[folder_item.path] = {
|
||||
task_item.name
|
||||
for task_item in task_items
|
||||
}
|
||||
output[folder_item.path] = task_items
|
||||
|
||||
return output
|
||||
|
||||
|
|
|
|||
|
|
@ -128,6 +128,16 @@ class CreateHierarchyController:
|
|||
project_name, folder_id, sender
|
||||
)
|
||||
|
||||
def get_folder_type_items(self, project_name, sender=None):
|
||||
return self._controller.get_folder_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
def get_task_type_items(self, project_name, sender=None):
|
||||
return self._controller.get_task_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
# Selection model
|
||||
def set_selected_project(self, project_name):
|
||||
self._selection_model.set_selected_project(project_name)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ class FoldersDialogController:
|
|||
def get_folder_items(self, project_name, sender=None):
|
||||
return self._controller.get_folder_items(project_name, sender)
|
||||
|
||||
def get_folder_type_items(self, project_name, sender=None):
|
||||
return self._controller.get_folder_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
def set_selected_folder(self, folder_id):
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -99,12 +99,16 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
root_item.removeRows(0, self.rowCount())
|
||||
return
|
||||
|
||||
task_names_by_folder_path = (
|
||||
self._controller.get_task_names_by_folder_paths(
|
||||
task_items_by_folder_path = (
|
||||
self._controller.get_task_items_by_folder_paths(
|
||||
self._folder_paths
|
||||
)
|
||||
)
|
||||
|
||||
task_names_by_folder_path = {
|
||||
folder_path: {item.name for item in task_items}
|
||||
for folder_path, task_items in task_items_by_folder_path.items()
|
||||
}
|
||||
self._task_names_by_folder_path = task_names_by_folder_path
|
||||
|
||||
new_task_names = self.get_intersection_of_tasks(
|
||||
|
|
@ -122,22 +126,54 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
item = self._items_by_name.pop(task_name)
|
||||
root_item.removeRow(item.row())
|
||||
|
||||
icon = get_qt_icon({
|
||||
default_icon = get_qt_icon({
|
||||
"type": "awesome-font",
|
||||
"name": "fa.male",
|
||||
"color": get_default_entity_icon_color(),
|
||||
})
|
||||
new_items = []
|
||||
task_type_items = {
|
||||
task_type_item.name: task_type_item
|
||||
for task_type_item in self._controller.get_task_type_items(
|
||||
self._controller.project_name
|
||||
)
|
||||
}
|
||||
icon_name_by_task_name = {}
|
||||
for task_items in task_items_by_folder_path.values():
|
||||
for task_item in task_items:
|
||||
task_name = task_item.name
|
||||
if (
|
||||
task_name not in new_task_names
|
||||
or task_name in icon_name_by_task_name
|
||||
):
|
||||
continue
|
||||
task_type_name = task_item.task_type
|
||||
task_type_item = task_type_items.get(task_type_name)
|
||||
if task_type_item:
|
||||
icon_name_by_task_name[task_name] = task_type_item.icon
|
||||
|
||||
for task_name in new_task_names:
|
||||
if task_name in self._items_by_name:
|
||||
item = self._items_by_name.get(task_name)
|
||||
if item is None:
|
||||
item = QtGui.QStandardItem(task_name)
|
||||
item.setData(task_name, TASK_NAME_ROLE)
|
||||
self._items_by_name[task_name] = item
|
||||
new_items.append(item)
|
||||
|
||||
if not task_name:
|
||||
continue
|
||||
|
||||
item = QtGui.QStandardItem(task_name)
|
||||
item.setData(task_name, TASK_NAME_ROLE)
|
||||
if task_name:
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
self._items_by_name[task_name] = item
|
||||
new_items.append(item)
|
||||
icon_name = icon_name_by_task_name.get(task_name)
|
||||
icon = None
|
||||
if icon_name:
|
||||
icon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": icon_name,
|
||||
"color": get_default_entity_icon_color(),
|
||||
})
|
||||
if icon is None:
|
||||
icon = default_icon
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
|
||||
if new_items:
|
||||
root_item.appendRows(new_items)
|
||||
|
|
|
|||
|
|
@ -116,6 +116,9 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
|
||||
self._default_icon_color = get_default_entity_icon_color()
|
||||
|
||||
self._last_project_statuses = {}
|
||||
self._last_status_icons_by_name = {}
|
||||
|
||||
def outdated(self, item):
|
||||
return item.get("isOutdated", True)
|
||||
|
||||
|
|
@ -159,10 +162,11 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
self._controller.get_site_provider_icons().items()
|
||||
)
|
||||
}
|
||||
status_items_by_name = {
|
||||
self._last_project_statuses = {
|
||||
status_item.name: status_item
|
||||
for status_item in self._controller.get_project_status_items()
|
||||
}
|
||||
self._last_status_icons_by_name = {}
|
||||
|
||||
group_item_icon = qtawesome.icon(
|
||||
"fa.folder", color=self._default_icon_color
|
||||
|
|
@ -186,7 +190,6 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
remote_site_icon = site_icons.get(sites_info["remote_site_provider"])
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
|
||||
group_items = []
|
||||
for repre_id, container_items in items_by_repre_id.items():
|
||||
repre_info = repre_info_by_id[repre_id]
|
||||
|
|
@ -195,8 +198,6 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
is_latest = False
|
||||
is_hero = False
|
||||
status_name = None
|
||||
status_color = None
|
||||
status_short = None
|
||||
if not repre_info.is_valid:
|
||||
group_name = "< Entity N/A >"
|
||||
item_icon = invalid_item_icon
|
||||
|
|
@ -219,10 +220,10 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
if not is_latest:
|
||||
version_color = self.OUTDATED_COLOR
|
||||
status_name = version_item.status
|
||||
status_item = status_items_by_name.get(status_name)
|
||||
if status_item:
|
||||
status_short = status_item.short
|
||||
status_color = status_item.color
|
||||
|
||||
status_color, status_short, status_icon = self._get_status_data(
|
||||
status_name
|
||||
)
|
||||
|
||||
container_model_items = []
|
||||
for container_item in container_items:
|
||||
|
|
@ -273,6 +274,7 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
group_item.setData(status_name, STATUS_NAME_ROLE)
|
||||
group_item.setData(status_short, STATUS_SHORT_ROLE)
|
||||
group_item.setData(status_color, STATUS_COLOR_ROLE)
|
||||
group_item.setData(status_icon, STATUS_ICON_ROLE)
|
||||
|
||||
group_item.setData(
|
||||
active_site_progress, ACTIVE_SITE_PROGRESS_ROLE
|
||||
|
|
@ -355,6 +357,32 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
root_item = self.invisibleRootItem()
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
|
||||
def _get_status_data(self, status_name):
|
||||
status_item = self._last_project_statuses.get(status_name)
|
||||
status_icon = self._get_status_icon(status_name, status_item)
|
||||
status_color = status_short = None
|
||||
if status_item is not None:
|
||||
status_color = status_item.color
|
||||
status_short = status_item.short
|
||||
return status_color, status_short, status_icon
|
||||
|
||||
def _get_status_icon(self, status_name, status_item):
|
||||
icon = self._last_status_icons_by_name.get(status_name)
|
||||
if icon is not None:
|
||||
return icon
|
||||
|
||||
icon = None
|
||||
if status_item is not None:
|
||||
icon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": status_item.icon,
|
||||
"color": status_item.color,
|
||||
})
|
||||
if icon is None:
|
||||
icon = QtGui.QIcon()
|
||||
self._last_status_icons_by_name[status_name] = icon
|
||||
return icon
|
||||
|
||||
|
||||
class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""Filter model to where key column's value is in the filtered tags"""
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import uuid
|
|||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from ayon_core.tools.utils import get_qt_icon
|
||||
from ayon_core.tools.utils.delegates import StatusDelegate
|
||||
|
||||
from .model import (
|
||||
|
|
@ -20,13 +21,15 @@ class VersionOption:
|
|||
label,
|
||||
status_name,
|
||||
status_short,
|
||||
status_color
|
||||
status_color,
|
||||
status_icon,
|
||||
):
|
||||
self.version = version
|
||||
self.label = label
|
||||
self.status_name = status_name
|
||||
self.status_short = status_short
|
||||
self.status_color = status_color
|
||||
self.status_icon = status_icon
|
||||
|
||||
|
||||
class SelectVersionModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -67,8 +70,12 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
|
|||
self._combo_view = combo_view
|
||||
self._status_delegate = status_delegate
|
||||
self._items_by_id = {}
|
||||
self._status_visible = True
|
||||
|
||||
def paintEvent(self, event):
|
||||
if not self._status_visible:
|
||||
return super().paintEvent(event)
|
||||
|
||||
painter = QtWidgets.QStylePainter(self)
|
||||
option = QtWidgets.QStyleOptionComboBox()
|
||||
self.initStyleOption(option)
|
||||
|
|
@ -80,27 +87,52 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
|
|||
return
|
||||
|
||||
painter.save()
|
||||
text_field_rect = self.style().subControlRect(
|
||||
|
||||
status_icon = self.itemData(idx, STATUS_ICON_ROLE)
|
||||
content_field_rect = self.style().subControlRect(
|
||||
QtWidgets.QStyle.CC_ComboBox,
|
||||
option,
|
||||
QtWidgets.QStyle.SC_ComboBoxEditField
|
||||
)
|
||||
adj_rect = text_field_rect.adjusted(1, 0, -1, 0)
|
||||
).adjusted(1, 0, -1, 0)
|
||||
|
||||
metrics = option.fontMetrics
|
||||
version_text_width = metrics.width(option.currentText) + 2
|
||||
version_text_rect = QtCore.QRect(content_field_rect)
|
||||
version_text_rect.setWidth(version_text_width)
|
||||
|
||||
painter.drawText(
|
||||
adj_rect,
|
||||
version_text_rect,
|
||||
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
|
||||
option.currentText
|
||||
)
|
||||
metrics = QtGui.QFontMetrics(self.font())
|
||||
text_width = metrics.width(option.currentText)
|
||||
x_offset = text_width + 2
|
||||
diff_width = adj_rect.width() - x_offset
|
||||
if diff_width <= 0:
|
||||
|
||||
status_text_rect = QtCore.QRect(content_field_rect)
|
||||
status_text_rect.setLeft(version_text_rect.right() + 2)
|
||||
if status_icon is not None and not status_icon.isNull():
|
||||
icon_rect = QtCore.QRect(status_text_rect)
|
||||
diff = icon_rect.height() - metrics.height()
|
||||
if diff < 0:
|
||||
diff = 0
|
||||
top_offset = diff // 2
|
||||
bottom_offset = diff - top_offset
|
||||
icon_rect.adjust(0, top_offset, 0, -bottom_offset)
|
||||
icon_rect.setWidth(metrics.height())
|
||||
status_icon.paint(
|
||||
painter,
|
||||
icon_rect,
|
||||
QtCore.Qt.AlignCenter,
|
||||
QtGui.QIcon.Normal,
|
||||
QtGui.QIcon.On
|
||||
)
|
||||
status_text_rect.setLeft(icon_rect.right() + 2)
|
||||
|
||||
if status_text_rect.width() <= 0:
|
||||
return
|
||||
|
||||
status_rect = adj_rect.adjusted(x_offset + 2, 0, 0, 0)
|
||||
if diff_width < metrics.width(status_name):
|
||||
if status_text_rect.width() < metrics.width(status_name):
|
||||
status_name = self.itemData(idx, STATUS_SHORT_ROLE)
|
||||
if status_text_rect.width() < metrics.width(status_name):
|
||||
status_name = ""
|
||||
|
||||
color = QtGui.QColor(self.itemData(idx, STATUS_COLOR_ROLE))
|
||||
|
||||
|
|
@ -108,7 +140,7 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
|
|||
pen.setColor(color)
|
||||
painter.setPen(pen)
|
||||
painter.drawText(
|
||||
status_rect,
|
||||
status_text_rect,
|
||||
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
|
||||
status_name
|
||||
)
|
||||
|
|
@ -120,6 +152,12 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
|
|||
|
||||
self.setCurrentIndex(index)
|
||||
|
||||
def set_status_visible(self, visible):
|
||||
header = self._combo_view.header()
|
||||
header.setSectionHidden(1, not visible)
|
||||
self._status_visible = visible
|
||||
self.update()
|
||||
|
||||
def get_item_by_id(self, item_id):
|
||||
return self._items_by_id[item_id]
|
||||
|
||||
|
|
@ -130,7 +168,17 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
|
|||
root_item.removeRows(0, root_item.rowCount())
|
||||
|
||||
new_items = []
|
||||
icons_by_name = {}
|
||||
for version_option in version_options:
|
||||
icon = icons_by_name.get(version_option.status_icon)
|
||||
if icon is None:
|
||||
icon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": version_option.status_icon,
|
||||
"color": version_option.status_color
|
||||
})
|
||||
icons_by_name[version_option.status_icon] = icon
|
||||
|
||||
item_id = uuid.uuid4().hex
|
||||
item = QtGui.QStandardItem(version_option.label)
|
||||
item.setColumnCount(root_item.columnCount())
|
||||
|
|
@ -143,6 +191,7 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
|
|||
item.setData(
|
||||
version_option.status_color, STATUS_COLOR_ROLE
|
||||
)
|
||||
item.setData(icon, STATUS_ICON_ROLE)
|
||||
item.setData(item_id, ITEM_ID_ROLE)
|
||||
|
||||
new_items.append(item)
|
||||
|
|
@ -195,10 +244,16 @@ class SelectVersionDialog(QtWidgets.QDialog):
|
|||
def select_index(self, index):
|
||||
self._versions_combobox.set_current_index(index)
|
||||
|
||||
def set_status_visible(self, visible):
|
||||
self._versions_combobox.set_status_visible(visible)
|
||||
|
||||
@classmethod
|
||||
def ask_for_version(cls, version_options, index=None, parent=None):
|
||||
def ask_for_version(
|
||||
cls, version_options, index=None, show_statuses=True, parent=None
|
||||
):
|
||||
dialog = cls(parent)
|
||||
dialog.set_versions(version_options)
|
||||
dialog.set_status_visible(show_statuses)
|
||||
if index is not None:
|
||||
dialog.select_index(index)
|
||||
dialog.exec_()
|
||||
|
|
|
|||
|
|
@ -683,37 +683,51 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
repre_ids
|
||||
)
|
||||
|
||||
product_ids = {
|
||||
repre_info.product_id
|
||||
for repre_info in repre_info_by_id.values()
|
||||
}
|
||||
active_repre_info = repre_info_by_id[active_repre_id]
|
||||
active_product_id = active_repre_info.product_id
|
||||
active_version_id = active_repre_info.version_id
|
||||
filtered_repre_info_by_id = {
|
||||
repre_id: repre_info
|
||||
for repre_id, repre_info in repre_info_by_id.items()
|
||||
if repre_info.product_id == active_product_id
|
||||
}
|
||||
filtered_container_item_ids = {
|
||||
item_id
|
||||
for item_id, container_item in container_items_by_id.items()
|
||||
if container_item.representation_id in filtered_repre_info_by_id
|
||||
}
|
||||
version_items_by_id = self._controller.get_version_items(
|
||||
{active_product_id}
|
||||
)[active_product_id]
|
||||
active_product_id = active_repre_info.product_id
|
||||
version_items_by_product_id = self._controller.get_version_items(
|
||||
product_ids
|
||||
)
|
||||
version_items = list(
|
||||
version_items_by_product_id[active_product_id].values()
|
||||
)
|
||||
versions = {version_item.version for version_item in version_items}
|
||||
product_ids_by_version = collections.defaultdict(set)
|
||||
for version_items_by_id in version_items_by_product_id.values():
|
||||
for version_item in version_items_by_id.values():
|
||||
version = version_item.version
|
||||
_prod_version = version
|
||||
if _prod_version < 0:
|
||||
_prod_version = -1
|
||||
product_ids_by_version[_prod_version].add(
|
||||
version_item.product_id
|
||||
)
|
||||
if version in versions:
|
||||
continue
|
||||
versions.add(version)
|
||||
version_items.append(version_item)
|
||||
|
||||
def version_sorter(item):
|
||||
hero_value = 0
|
||||
version = item.version
|
||||
if version < 0:
|
||||
i_version = item.version
|
||||
if i_version < 0:
|
||||
hero_value = 1
|
||||
version = abs(version)
|
||||
return version, hero_value
|
||||
i_version = abs(i_version)
|
||||
return i_version, hero_value
|
||||
|
||||
version_items = list(version_items_by_id.values())
|
||||
version_items.sort(key=version_sorter, reverse=True)
|
||||
status_items_by_name = {
|
||||
status_item.name: status_item
|
||||
for status_item in self._controller.get_project_status_items()
|
||||
}
|
||||
show_statuses = len(product_ids) == 1
|
||||
status_items_by_name = {}
|
||||
if show_statuses:
|
||||
status_items_by_name = {
|
||||
status_item.name: status_item
|
||||
for status_item in self._controller.get_project_status_items()
|
||||
}
|
||||
|
||||
version_options = []
|
||||
active_version_idx = 0
|
||||
|
|
@ -726,10 +740,12 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
status_name = version_item.status
|
||||
status_short = None
|
||||
status_color = None
|
||||
status_icon = None
|
||||
status_item = status_items_by_name.get(status_name)
|
||||
if status_item:
|
||||
status_short = status_item.short
|
||||
status_color = status_item.color
|
||||
status_icon = status_item.icon
|
||||
version_options.append(
|
||||
VersionOption(
|
||||
version,
|
||||
|
|
@ -737,23 +753,35 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
status_name,
|
||||
status_short,
|
||||
status_color,
|
||||
status_icon,
|
||||
)
|
||||
)
|
||||
|
||||
version_option = SelectVersionDialog.ask_for_version(
|
||||
version_options,
|
||||
active_version_idx,
|
||||
show_statuses=show_statuses,
|
||||
parent=self
|
||||
)
|
||||
if version_option is None:
|
||||
return
|
||||
|
||||
version = version_option.version
|
||||
product_version = version = version_option.version
|
||||
if version < 0:
|
||||
product_version = -1
|
||||
version = HeroVersionType(version)
|
||||
|
||||
product_ids = product_ids_by_version[product_version]
|
||||
|
||||
filtered_item_ids = set()
|
||||
for container_item in container_items_by_id.values():
|
||||
repre_id = container_item.representation_id
|
||||
repre_info = repre_info_by_id[repre_id]
|
||||
if repre_info.product_id in product_ids:
|
||||
filtered_item_ids.add(container_item.item_id)
|
||||
|
||||
self._update_containers_to_version(
|
||||
filtered_container_item_ids, version
|
||||
filtered_item_ids, version
|
||||
)
|
||||
|
||||
def _show_switch_dialog(self, item_ids):
|
||||
|
|
|
|||
|
|
@ -182,7 +182,27 @@ class TrayManager:
|
|||
}:
|
||||
envs.pop(key, None)
|
||||
|
||||
# Remove any existing addon path from 'PYTHONPATH'
|
||||
addons_dir = os.environ.get("AYON_ADDONS_DIR", "")
|
||||
if addons_dir:
|
||||
addons_dir = os.path.normpath(addons_dir)
|
||||
addons_dir = addons_dir.lower()
|
||||
|
||||
pythonpath = envs.get("PYTHONPATH") or ""
|
||||
new_python_paths = []
|
||||
for path in pythonpath.split(os.pathsep):
|
||||
if not path:
|
||||
continue
|
||||
path = os.path.normpath(path)
|
||||
if path.lower().startswith(addons_dir):
|
||||
continue
|
||||
new_python_paths.append(path)
|
||||
|
||||
envs["PYTHONPATH"] = os.pathsep.join(new_python_paths)
|
||||
|
||||
# Start new process
|
||||
run_detached_process(args, env=envs)
|
||||
# Exit current tray process
|
||||
self.exit()
|
||||
|
||||
def exit(self):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import time
|
|||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from qtpy import QtWidgets, QtGui
|
||||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -130,32 +130,65 @@ class StatusDelegate(QtWidgets.QStyledItemDelegate):
|
|||
else:
|
||||
style = QtWidgets.QApplication.style()
|
||||
|
||||
style.drawControl(
|
||||
QtWidgets.QCommonStyle.CE_ItemViewItem,
|
||||
self.initStyleOption(option, index)
|
||||
|
||||
mode = QtGui.QIcon.Normal
|
||||
if not (option.state & QtWidgets.QStyle.State_Enabled):
|
||||
mode = QtGui.QIcon.Disabled
|
||||
elif option.state & QtWidgets.QStyle.State_Selected:
|
||||
mode = QtGui.QIcon.Selected
|
||||
state = QtGui.QIcon.Off
|
||||
if option.state & QtWidgets.QStyle.State_Open:
|
||||
state = QtGui.QIcon.On
|
||||
icon = self._get_status_icon(index)
|
||||
option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration
|
||||
option.icon = icon
|
||||
act_size = icon.actualSize(option.decorationSize, mode, state)
|
||||
option.decorationSize = QtCore.QSize(
|
||||
min(option.decorationSize.width(), act_size.width()),
|
||||
min(option.decorationSize.height(), act_size.height())
|
||||
)
|
||||
|
||||
text = self._get_status_name(index)
|
||||
if text:
|
||||
option.features |= QtWidgets.QStyleOptionViewItem.HasDisplay
|
||||
option.text = text
|
||||
|
||||
painter.save()
|
||||
painter.setClipRect(option.rect)
|
||||
|
||||
icon_rect = style.subElementRect(
|
||||
QtWidgets.QCommonStyle.SE_ItemViewItemDecoration,
|
||||
option,
|
||||
option.widget
|
||||
)
|
||||
text_rect = style.subElementRect(
|
||||
QtWidgets.QCommonStyle.SE_ItemViewItemText,
|
||||
option,
|
||||
option.widget
|
||||
)
|
||||
|
||||
# Draw background
|
||||
style.drawPrimitive(
|
||||
QtWidgets.QCommonStyle.PE_PanelItemViewItem,
|
||||
option,
|
||||
painter,
|
||||
option.widget
|
||||
)
|
||||
|
||||
painter.save()
|
||||
|
||||
text_rect = style.subElementRect(
|
||||
QtWidgets.QCommonStyle.SE_ItemViewItemText,
|
||||
option
|
||||
# Draw icon
|
||||
option.icon.paint(
|
||||
painter,
|
||||
icon_rect,
|
||||
option.decorationAlignment,
|
||||
mode,
|
||||
state
|
||||
)
|
||||
text_margin = style.proxy().pixelMetric(
|
||||
QtWidgets.QCommonStyle.PM_FocusFrameHMargin,
|
||||
option,
|
||||
option.widget
|
||||
) + 1
|
||||
padded_text_rect = text_rect.adjusted(
|
||||
text_margin, 0, - text_margin, 0
|
||||
)
|
||||
|
||||
fm = QtGui.QFontMetrics(option.font)
|
||||
text = self._get_status_name(index)
|
||||
if padded_text_rect.width() < fm.width(text):
|
||||
if text_rect.width() < fm.width(text):
|
||||
text = self._get_status_short_name(index)
|
||||
if text_rect.width() < fm.width(text):
|
||||
text = ""
|
||||
|
||||
fg_color = self._get_status_color(index)
|
||||
pen = painter.pen()
|
||||
|
|
@ -163,11 +196,47 @@ class StatusDelegate(QtWidgets.QStyledItemDelegate):
|
|||
painter.setPen(pen)
|
||||
|
||||
painter.drawText(
|
||||
padded_text_rect,
|
||||
text_rect,
|
||||
option.displayAlignment,
|
||||
text
|
||||
)
|
||||
|
||||
if option.state & QtWidgets.QStyle.State_HasFocus:
|
||||
focus_opt = QtWidgets.QStyleOptionFocusRect()
|
||||
focus_opt.state = option.state
|
||||
focus_opt.direction = option.direction
|
||||
focus_opt.rect = option.rect
|
||||
focus_opt.fontMetrics = option.fontMetrics
|
||||
focus_opt.palette = option.palette
|
||||
|
||||
focus_opt.rect = style.subElementRect(
|
||||
QtWidgets.QCommonStyle.SE_ItemViewItemFocusRect,
|
||||
option,
|
||||
option.widget
|
||||
)
|
||||
focus_opt.state |= (
|
||||
QtWidgets.QStyle.State_KeyboardFocusChange
|
||||
| QtWidgets.QStyle.State_Item
|
||||
)
|
||||
focus_opt.backgroundColor = option.palette.color(
|
||||
(
|
||||
QtGui.QPalette.Normal
|
||||
if option.state & QtWidgets.QStyle.State_Enabled
|
||||
else QtGui.QPalette.Disabled
|
||||
),
|
||||
(
|
||||
QtGui.QPalette.Highlight
|
||||
if option.state & QtWidgets.QStyle.State_Selected
|
||||
else QtGui.QPalette.Window
|
||||
)
|
||||
)
|
||||
style.drawPrimitive(
|
||||
QtWidgets.QCommonStyle.PE_FrameFocusRect,
|
||||
focus_opt,
|
||||
painter,
|
||||
option.widget
|
||||
)
|
||||
|
||||
painter.restore()
|
||||
|
||||
def _get_status_name(self, index):
|
||||
|
|
@ -180,6 +249,9 @@ class StatusDelegate(QtWidgets.QStyledItemDelegate):
|
|||
return QtGui.QColor(index.data(self.status_color_role))
|
||||
|
||||
def _get_status_icon(self, index):
|
||||
icon = None
|
||||
if self.status_icon_role is not None:
|
||||
return index.data(self.status_icon_role)
|
||||
return None
|
||||
icon = index.data(self.status_icon_role)
|
||||
if icon is None:
|
||||
return QtGui.QIcon()
|
||||
return icon
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import collections
|
|||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
|
||||
from ayon_core.lib.events import QueuedEventSystem
|
||||
from ayon_core.style import get_default_entity_icon_color
|
||||
from ayon_core.tools.common_models import (
|
||||
ProjectsModel,
|
||||
HierarchyModel,
|
||||
HierarchyExpectedSelection,
|
||||
)
|
||||
|
|
@ -25,8 +27,9 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
"""
|
||||
|
||||
"""
|
||||
_default_folder_icon = None
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller):
|
||||
|
|
@ -71,13 +74,6 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
|
||||
self.set_project_name(self._last_project_name)
|
||||
|
||||
def _clear_items(self):
|
||||
self._items_by_id = {}
|
||||
self._parent_id_by_id = {}
|
||||
self._has_content = False
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
|
||||
def get_index_by_id(self, item_id):
|
||||
"""Get index by folder id.
|
||||
|
||||
|
|
@ -123,7 +119,7 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
|
||||
if not project_name:
|
||||
self._last_project_name = project_name
|
||||
self._fill_items({})
|
||||
self._fill_items({}, {})
|
||||
self._current_refresh_thread = None
|
||||
return
|
||||
|
||||
|
|
@ -140,15 +136,42 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
|
||||
thread = RefreshThread(
|
||||
project_name,
|
||||
self._controller.get_folder_items,
|
||||
project_name,
|
||||
FOLDERS_MODEL_SENDER_NAME
|
||||
self._thread_getter,
|
||||
project_name
|
||||
)
|
||||
self._current_refresh_thread = thread
|
||||
self._refresh_threads[thread.id] = thread
|
||||
thread.refresh_finished.connect(self._on_refresh_thread)
|
||||
thread.start()
|
||||
|
||||
@classmethod
|
||||
def _get_default_folder_icon(cls):
|
||||
if cls._default_folder_icon is None:
|
||||
cls._default_folder_icon = get_qt_icon({
|
||||
"type": "awesome-font",
|
||||
"name": "fa.folder",
|
||||
"color": get_default_entity_icon_color()
|
||||
})
|
||||
return cls._default_folder_icon
|
||||
|
||||
def _clear_items(self):
|
||||
self._items_by_id = {}
|
||||
self._parent_id_by_id = {}
|
||||
self._has_content = False
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
|
||||
def _thread_getter(self, project_name):
|
||||
folder_items = self._controller.get_folder_items(
|
||||
project_name, FOLDERS_MODEL_SENDER_NAME
|
||||
)
|
||||
folder_type_items = {}
|
||||
if hasattr(self._controller, "get_folder_type_items"):
|
||||
folder_type_items = self._controller.get_folder_type_items(
|
||||
project_name, FOLDERS_MODEL_SENDER_NAME
|
||||
)
|
||||
return folder_items, folder_type_items
|
||||
|
||||
def _on_refresh_thread(self, thread_id):
|
||||
"""Callback when refresh thread is finished.
|
||||
|
||||
|
|
@ -169,19 +192,55 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
or thread_id != self._current_refresh_thread.id
|
||||
):
|
||||
return
|
||||
|
||||
self._fill_items(thread.get_result())
|
||||
folder_items, folder_type_items = thread.get_result()
|
||||
self._fill_items(folder_items, folder_type_items)
|
||||
self._current_refresh_thread = None
|
||||
|
||||
def _fill_item_data(self, item, folder_item):
|
||||
def _get_folder_item_icon(
|
||||
self,
|
||||
folder_item,
|
||||
folder_type_item_by_name,
|
||||
folder_type_icon_cache
|
||||
):
|
||||
icon = folder_type_icon_cache.get(folder_item.folder_type)
|
||||
if icon is not None:
|
||||
return icon
|
||||
|
||||
folder_type_item = folder_type_item_by_name.get(
|
||||
folder_item.folder_type
|
||||
)
|
||||
icon = None
|
||||
if folder_type_item is not None:
|
||||
icon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": folder_type_item.icon,
|
||||
"color": get_default_entity_icon_color()
|
||||
})
|
||||
|
||||
if icon is None:
|
||||
icon = self._get_default_folder_icon()
|
||||
folder_type_icon_cache[folder_item.folder_type] = icon
|
||||
return icon
|
||||
|
||||
def _fill_item_data(
|
||||
self,
|
||||
item,
|
||||
folder_item,
|
||||
folder_type_item_by_name,
|
||||
folder_type_icon_cache
|
||||
):
|
||||
"""
|
||||
|
||||
Args:
|
||||
item (QtGui.QStandardItem): Item to fill data.
|
||||
folder_item (FolderItem): Folder item.
|
||||
"""
|
||||
|
||||
icon = get_qt_icon(folder_item.icon)
|
||||
"""
|
||||
icon = self._get_folder_item_icon(
|
||||
folder_item,
|
||||
folder_type_item_by_name,
|
||||
folder_type_icon_cache
|
||||
)
|
||||
item.setData(folder_item.entity_id, FOLDER_ID_ROLE)
|
||||
item.setData(folder_item.name, FOLDER_NAME_ROLE)
|
||||
item.setData(folder_item.path, FOLDER_PATH_ROLE)
|
||||
|
|
@ -189,7 +248,7 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
item.setData(folder_item.label, QtCore.Qt.DisplayRole)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
|
||||
def _fill_items(self, folder_items_by_id):
|
||||
def _fill_items(self, folder_items_by_id, folder_type_items):
|
||||
if not folder_items_by_id:
|
||||
if folder_items_by_id is not None:
|
||||
self._clear_items()
|
||||
|
|
@ -197,6 +256,11 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
self.refreshed.emit()
|
||||
return
|
||||
|
||||
folder_type_item_by_name = {
|
||||
folder_type.name: folder_type
|
||||
for folder_type in folder_type_items
|
||||
}
|
||||
folder_type_icon_cache = {}
|
||||
self._has_content = True
|
||||
|
||||
folder_ids = set(folder_items_by_id)
|
||||
|
|
@ -242,7 +306,12 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
else:
|
||||
is_new = self._parent_id_by_id[item_id] != parent_id
|
||||
|
||||
self._fill_item_data(item, folder_item)
|
||||
self._fill_item_data(
|
||||
item,
|
||||
folder_item,
|
||||
folder_type_item_by_name,
|
||||
folder_type_icon_cache
|
||||
)
|
||||
if is_new:
|
||||
new_items.append(item)
|
||||
self._items_by_id[item_id] = item
|
||||
|
|
@ -619,6 +688,7 @@ class SimpleSelectionModel(object):
|
|||
class SimpleFoldersController(object):
|
||||
def __init__(self):
|
||||
self._event_system = self._create_event_system()
|
||||
self._projects_model = ProjectsModel(self)
|
||||
self._hierarchy_model = HierarchyModel(self)
|
||||
self._selection_model = SimpleSelectionModel(self)
|
||||
self._expected_selection = HierarchyExpectedSelection(
|
||||
|
|
@ -639,6 +709,11 @@ class SimpleFoldersController(object):
|
|||
def get_folder_items(self, project_name, sender=None):
|
||||
return self._hierarchy_model.get_folder_items(project_name, sender)
|
||||
|
||||
def get_folder_type_items(self, project_name, sender=None):
|
||||
return self._projects_model.get_folder_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
def set_selected_project(self, project_name):
|
||||
self._selection_model.set_selected_project(project_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from functools import partial
|
|||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
import qtawesome
|
||||
import qtmaterialsymbols
|
||||
|
||||
from ayon_core.style import (
|
||||
get_objected_colors,
|
||||
|
|
@ -468,7 +469,7 @@ class _IconsCache:
|
|||
if icon_type == "path":
|
||||
parts = [icon_type, icon_def["path"]]
|
||||
|
||||
elif icon_type == "awesome-font":
|
||||
elif icon_type in {"awesome-font", "material-symbols"}:
|
||||
parts = [icon_type, icon_def["name"], icon_def["color"]]
|
||||
return "|".join(parts)
|
||||
|
||||
|
|
@ -495,6 +496,13 @@ class _IconsCache:
|
|||
if icon is None:
|
||||
icon = cls.get_qta_icon_by_name_and_color(
|
||||
"fa.{}".format(icon_name), icon_color)
|
||||
|
||||
elif icon_type == "material-symbols":
|
||||
icon_name = icon_def["name"]
|
||||
icon_color = icon_def["color"]
|
||||
if qtmaterialsymbols.get_icon_name_char(icon_name) is not None:
|
||||
icon = qtmaterialsymbols.get_icon(icon_name, icon_color)
|
||||
|
||||
if icon is None:
|
||||
icon = cls.get_default()
|
||||
cls._cache[cache_key] = icon
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
|
||||
from ayon_core.style import get_disabled_entity_icon_color
|
||||
from ayon_core.style import (
|
||||
get_disabled_entity_icon_color,
|
||||
get_default_entity_icon_color,
|
||||
)
|
||||
|
||||
from .views import DeselectableTreeView
|
||||
from .lib import RefreshThread, get_qt_icon
|
||||
|
|
@ -17,8 +20,9 @@ class TasksQtModel(QtGui.QStandardItemModel):
|
|||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
"""
|
||||
|
||||
"""
|
||||
_default_task_icon = None
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller):
|
||||
|
|
@ -176,7 +180,7 @@ class TasksQtModel(QtGui.QStandardItemModel):
|
|||
return
|
||||
thread = RefreshThread(
|
||||
folder_id,
|
||||
self._controller.get_task_items,
|
||||
self._thread_getter,
|
||||
project_name,
|
||||
folder_id
|
||||
)
|
||||
|
|
@ -185,8 +189,55 @@ class TasksQtModel(QtGui.QStandardItemModel):
|
|||
thread.refresh_finished.connect(self._on_refresh_thread)
|
||||
thread.start()
|
||||
|
||||
def _thread_getter(self, project_name, folder_id):
|
||||
task_items = self._controller.get_task_items(
|
||||
project_name, folder_id
|
||||
)
|
||||
task_type_items = {}
|
||||
if hasattr(self._controller, "get_task_type_items"):
|
||||
task_type_items = self._controller.get_task_type_items(
|
||||
project_name
|
||||
)
|
||||
return task_items, task_type_items
|
||||
|
||||
@classmethod
|
||||
def _get_default_task_icon(cls):
|
||||
if cls._default_task_icon is None:
|
||||
cls._default_task_icon = get_qt_icon({
|
||||
"type": "awesome-font",
|
||||
"name": "fa.male",
|
||||
"color": get_default_entity_icon_color()
|
||||
})
|
||||
return cls._default_task_icon
|
||||
|
||||
def _get_task_item_icon(
|
||||
self,
|
||||
task_item,
|
||||
task_type_item_by_name,
|
||||
task_type_icon_cache
|
||||
):
|
||||
icon = task_type_icon_cache.get(task_item.task_type)
|
||||
if icon is not None:
|
||||
return icon
|
||||
|
||||
task_type_item = task_type_item_by_name.get(
|
||||
task_item.task_type
|
||||
)
|
||||
icon = None
|
||||
if task_type_item is not None:
|
||||
icon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": task_type_item.icon,
|
||||
"color": get_default_entity_icon_color()
|
||||
})
|
||||
|
||||
if icon is None:
|
||||
icon = self._get_default_task_icon()
|
||||
task_type_icon_cache[task_item.task_type] = icon
|
||||
return icon
|
||||
|
||||
def _fill_data_from_thread(self, thread):
|
||||
task_items = thread.get_result()
|
||||
task_items, task_type_items = thread.get_result()
|
||||
# Task items are refreshed
|
||||
if task_items is None:
|
||||
return
|
||||
|
|
@ -197,6 +248,11 @@ class TasksQtModel(QtGui.QStandardItemModel):
|
|||
return
|
||||
self._remove_invalid_items()
|
||||
|
||||
task_type_item_by_name = {
|
||||
task_type_item.name: task_type_item
|
||||
for task_type_item in task_type_items
|
||||
}
|
||||
task_type_icon_cache = {}
|
||||
new_items = []
|
||||
new_names = set()
|
||||
for task_item in task_items:
|
||||
|
|
@ -209,8 +265,11 @@ class TasksQtModel(QtGui.QStandardItemModel):
|
|||
new_items.append(item)
|
||||
self._items_by_name[name] = item
|
||||
|
||||
# TODO cache locally
|
||||
icon = get_qt_icon(task_item.icon)
|
||||
icon = self._get_task_item_icon(
|
||||
task_item,
|
||||
task_type_item_by_name,
|
||||
task_type_icon_cache
|
||||
)
|
||||
item.setData(task_item.label, QtCore.Qt.DisplayRole)
|
||||
item.setData(name, ITEM_NAME_ROLE)
|
||||
item.setData(task_item.id, ITEM_ID_ROLE)
|
||||
|
|
|
|||
|
|
@ -546,6 +546,46 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_folder_type_items(self, project_name, sender=None):
|
||||
"""Folder type items for a project.
|
||||
|
||||
This function may trigger events with topics
|
||||
'projects.folder_types.refresh.started' and
|
||||
'projects.folder_types.refresh.finished' which will contain 'sender'
|
||||
value in data.
|
||||
That may help to avoid re-refresh of items in UI elements.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
sender (str): Who requested folder type items.
|
||||
|
||||
Returns:
|
||||
list[FolderTypeItem]: Folder type information.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_type_items(self, project_name, sender=None):
|
||||
"""Task type items for a project.
|
||||
|
||||
This function may trigger events with topics
|
||||
'projects.task_types.refresh.started' and
|
||||
'projects.task_types.refresh.finished' which will contain 'sender'
|
||||
value in data.
|
||||
That may help to avoid re-refresh of items in UI elements.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
sender (str): Who requested task type items.
|
||||
|
||||
Returns:
|
||||
list[TaskTypeItem]: Task type information.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
# Host information
|
||||
@abstractmethod
|
||||
def get_workfile_extensions(self):
|
||||
|
|
@ -834,12 +874,13 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_workarea_file_items(self, folder_id, task_id):
|
||||
def get_workarea_file_items(self, folder_id, task_name, sender=None):
|
||||
"""Get workarea file items.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
task_name (str): Task name.
|
||||
sender (Optional[str]): Who requested workarea file items.
|
||||
|
||||
Returns:
|
||||
list[FileItem]: List of workarea file items.
|
||||
|
|
@ -905,12 +946,12 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_workfile_info(self, folder_id, task_id, filepath):
|
||||
def get_workfile_info(self, folder_id, task_name, filepath):
|
||||
"""Workfile info from database.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
task_name (str): Task id.
|
||||
filepath (str): Workfile path.
|
||||
|
||||
Returns:
|
||||
|
|
@ -921,7 +962,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_workfile_info(self, folder_id, task_id, filepath, note):
|
||||
def save_workfile_info(self, folder_id, task_name, filepath, note):
|
||||
"""Save workfile info to database.
|
||||
|
||||
At this moment the only information which can be saved about
|
||||
|
|
@ -932,7 +973,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
|||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
task_name (str): Task id.
|
||||
filepath (str): Workfile path.
|
||||
note (Union[str, None]): Note.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -231,6 +231,16 @@ class BaseWorkfileController(
|
|||
return self._projects_model.get_project_entity(
|
||||
project_name)
|
||||
|
||||
def get_folder_type_items(self, project_name, sender=None):
|
||||
return self._projects_model.get_folder_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
def get_task_type_items(self, project_name, sender=None):
|
||||
return self._projects_model.get_task_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
def get_folder_entity(self, project_name, folder_id):
|
||||
return self._hierarchy_model.get_folder_entity(
|
||||
project_name, folder_id)
|
||||
|
|
@ -278,7 +288,8 @@ class BaseWorkfileController(
|
|||
)
|
||||
|
||||
def get_user_items_by_name(self):
|
||||
return self._users_model.get_user_items_by_name()
|
||||
project_name = self.get_current_project_name()
|
||||
return self._users_model.get_user_items_by_name(project_name)
|
||||
|
||||
# Host information
|
||||
def get_workfile_extensions(self):
|
||||
|
|
@ -410,9 +421,11 @@ class BaseWorkfileController(
|
|||
return self._workfiles_model.get_workarea_dir_by_context(
|
||||
folder_id, task_id)
|
||||
|
||||
def get_workarea_file_items(self, folder_id, task_id):
|
||||
def get_workarea_file_items(self, folder_id, task_name, sender=None):
|
||||
task_id = self._get_task_id(folder_id, task_name)
|
||||
return self._workfiles_model.get_workarea_file_items(
|
||||
folder_id, task_id)
|
||||
folder_id, task_id, task_name
|
||||
)
|
||||
|
||||
def get_workarea_save_as_data(self, folder_id, task_id):
|
||||
return self._workfiles_model.get_workarea_save_as_data(
|
||||
|
|
@ -447,12 +460,14 @@ class BaseWorkfileController(
|
|||
return self._workfiles_model.get_published_file_items(
|
||||
folder_id, task_name)
|
||||
|
||||
def get_workfile_info(self, folder_id, task_id, filepath):
|
||||
def get_workfile_info(self, folder_id, task_name, filepath):
|
||||
task_id = self._get_task_id(folder_id, task_name)
|
||||
return self._workfiles_model.get_workfile_info(
|
||||
folder_id, task_id, filepath
|
||||
)
|
||||
|
||||
def save_workfile_info(self, folder_id, task_id, filepath, note):
|
||||
def save_workfile_info(self, folder_id, task_name, filepath, note):
|
||||
task_id = self._get_task_id(folder_id, task_name)
|
||||
self._workfiles_model.save_workfile_info(
|
||||
folder_id, task_id, filepath, note
|
||||
)
|
||||
|
|
@ -627,6 +642,17 @@ class BaseWorkfileController(
|
|||
def _emit_event(self, topic, data=None):
|
||||
self.emit_event(topic, data, "controller")
|
||||
|
||||
def _get_task_id(self, folder_id, task_name, sender=None):
|
||||
task_item = self._hierarchy_model.get_task_item_by_name(
|
||||
self.get_current_project_name(),
|
||||
folder_id,
|
||||
task_name,
|
||||
sender
|
||||
)
|
||||
if not task_item:
|
||||
return None
|
||||
return task_item.id
|
||||
|
||||
# Expected selection
|
||||
# - expected selection is used to restore selection after refresh
|
||||
# or when current context should be used
|
||||
|
|
@ -722,7 +748,7 @@ class BaseWorkfileController(
|
|||
self._host_save_workfile(dst_filepath)
|
||||
|
||||
# Make sure workfile info exists
|
||||
self.save_workfile_info(folder_id, task_id, dst_filepath, None)
|
||||
self.save_workfile_info(folder_id, task_name, dst_filepath, None)
|
||||
|
||||
# Create extra folders
|
||||
create_workdir_extra_folders(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import re
|
||||
import copy
|
||||
import uuid
|
||||
|
||||
import arrow
|
||||
import ayon_api
|
||||
|
|
@ -173,7 +174,7 @@ class WorkareaModel:
|
|||
folder_mapping[task_id] = workdir
|
||||
return workdir
|
||||
|
||||
def get_file_items(self, folder_id, task_id):
|
||||
def get_file_items(self, folder_id, task_id, task_name):
|
||||
items = []
|
||||
if not folder_id or not task_id:
|
||||
return items
|
||||
|
|
@ -192,7 +193,7 @@ class WorkareaModel:
|
|||
continue
|
||||
|
||||
workfile_info = self._controller.get_workfile_info(
|
||||
folder_id, task_id, filepath
|
||||
folder_id, task_name, filepath
|
||||
)
|
||||
modified = os.path.getmtime(filepath)
|
||||
items.append(FileItem(
|
||||
|
|
@ -587,6 +588,7 @@ class WorkfileEntitiesModel:
|
|||
|
||||
username = self._get_current_username()
|
||||
workfile_info = {
|
||||
"id": uuid.uuid4().hex,
|
||||
"path": rootless_path,
|
||||
"taskId": task_id,
|
||||
"attrib": {
|
||||
|
|
@ -770,19 +772,21 @@ class WorkfilesModel:
|
|||
return self._workarea_model.get_workarea_dir_by_context(
|
||||
folder_id, task_id)
|
||||
|
||||
def get_workarea_file_items(self, folder_id, task_id):
|
||||
def get_workarea_file_items(self, folder_id, task_id, task_name):
|
||||
"""Workfile items for passed context from workarea.
|
||||
|
||||
Args:
|
||||
folder_id (Union[str, None]): Folder id.
|
||||
task_id (Union[str, None]): Task id.
|
||||
task_name (Union[str, None]): Task name.
|
||||
|
||||
Returns:
|
||||
list[FileItem]: List of file items matching workarea of passed
|
||||
context.
|
||||
"""
|
||||
|
||||
return self._workarea_model.get_file_items(folder_id, task_id)
|
||||
return self._workarea_model.get_file_items(
|
||||
folder_id, task_id, task_name
|
||||
)
|
||||
|
||||
def get_workarea_save_as_data(self, folder_id, task_id):
|
||||
return self._workarea_model.get_workarea_save_as_data(
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ from ayon_core.tools.utils.delegates import PrettyTimeDelegate
|
|||
|
||||
from .utils import BaseOverlayFrame
|
||||
|
||||
|
||||
REPRE_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
FILEPATH_ROLE = QtCore.Qt.UserRole + 2
|
||||
AUTHOR_ROLE = QtCore.Qt.UserRole + 3
|
||||
|
|
@ -249,7 +248,7 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
|
|||
|
||||
# Handle roles for first column
|
||||
col = index.column()
|
||||
if col != 1:
|
||||
if col == 0:
|
||||
return super().data(index, role)
|
||||
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
|||
self._empty_item_used = False
|
||||
self._published_mode = False
|
||||
self._selected_folder_id = None
|
||||
self._selected_task_id = None
|
||||
self._selected_task_name = None
|
||||
|
||||
self._add_missing_context_item()
|
||||
|
||||
|
|
@ -153,7 +153,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
|||
|
||||
def _on_task_changed(self, event):
|
||||
self._selected_folder_id = event["folder_id"]
|
||||
self._selected_task_id = event["task_id"]
|
||||
self._selected_task_name = event["task_name"]
|
||||
if not self._published_mode:
|
||||
self._fill_items()
|
||||
|
||||
|
|
@ -179,13 +179,13 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
|||
|
||||
def _fill_items_impl(self):
|
||||
folder_id = self._selected_folder_id
|
||||
task_id = self._selected_task_id
|
||||
if not folder_id or not task_id:
|
||||
task_name = self._selected_task_name
|
||||
if not folder_id or not task_name:
|
||||
self._add_missing_context_item()
|
||||
return
|
||||
|
||||
file_items = self._controller.get_workarea_file_items(
|
||||
folder_id, task_id
|
||||
folder_id, task_name
|
||||
)
|
||||
root_item = self.invisibleRootItem()
|
||||
if not file_items:
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class SidePanelWidget(QtWidgets.QWidget):
|
|||
self._btn_note_save = btn_note_save
|
||||
|
||||
self._folder_id = None
|
||||
self._task_id = None
|
||||
self._task_name = None
|
||||
self._filepath = None
|
||||
self._orig_note = ""
|
||||
self._controller = controller
|
||||
|
|
@ -93,10 +93,10 @@ class SidePanelWidget(QtWidgets.QWidget):
|
|||
|
||||
def _on_selection_change(self, event):
|
||||
folder_id = event["folder_id"]
|
||||
task_id = event["task_id"]
|
||||
task_name = event["task_name"]
|
||||
filepath = event["path"]
|
||||
|
||||
self._set_context(folder_id, task_id, filepath)
|
||||
self._set_context(folder_id, task_name, filepath)
|
||||
|
||||
def _on_note_change(self):
|
||||
text = self._note_input.toPlainText()
|
||||
|
|
@ -106,19 +106,19 @@ class SidePanelWidget(QtWidgets.QWidget):
|
|||
note = self._note_input.toPlainText()
|
||||
self._controller.save_workfile_info(
|
||||
self._folder_id,
|
||||
self._task_id,
|
||||
self._task_name,
|
||||
self._filepath,
|
||||
note
|
||||
)
|
||||
self._orig_note = note
|
||||
self._btn_note_save.setEnabled(False)
|
||||
|
||||
def _set_context(self, folder_id, task_id, filepath):
|
||||
def _set_context(self, folder_id, task_name, filepath):
|
||||
workfile_info = None
|
||||
# Check if folder, task and file are selected
|
||||
if bool(folder_id) and bool(task_id) and bool(filepath):
|
||||
if bool(folder_id) and bool(task_name) and bool(filepath):
|
||||
workfile_info = self._controller.get_workfile_info(
|
||||
folder_id, task_id, filepath
|
||||
folder_id, task_name, filepath
|
||||
)
|
||||
enabled = workfile_info is not None
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ class SidePanelWidget(QtWidgets.QWidget):
|
|||
self._btn_note_save.setEnabled(enabled)
|
||||
|
||||
self._folder_id = folder_id
|
||||
self._task_id = task_id
|
||||
self._task_name = task_name
|
||||
self._filepath = filepath
|
||||
|
||||
# Disable inputs and remove texts if any required arguments are
|
||||
|
|
|
|||
18
client/ayon_core/vendor/python/qtmaterialsymbols/__init__.py
vendored
Normal 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",
|
||||
)
|
||||
209
client/ayon_core/vendor/python/qtmaterialsymbols/browser.py
vendored
Normal 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_())
|
||||
8
client/ayon_core/vendor/python/qtmaterialsymbols/exceptions.py
vendored
Normal 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
|
||||
254
client/ayon_core/vendor/python/qtmaterialsymbols/iconic_font.py
vendored
Normal 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)
|
||||
3599
client/ayon_core/vendor/python/qtmaterialsymbols/resources/MaterialSymbolsOutlined.codepoints
vendored
Normal file
3601
client/ayon_core/vendor/python/qtmaterialsymbols/resources/MaterialSymbolsOutlined.json
vendored
Normal file
BIN
client/ayon_core/vendor/python/qtmaterialsymbols/resources/MaterialSymbolsOutlined.ttf
vendored
Normal file
16
client/ayon_core/vendor/python/qtmaterialsymbols/resources/__init__.py
vendored
Normal 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")
|
||||
221
client/ayon_core/vendor/python/qtmaterialsymbols/structures.py
vendored
Normal 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,
|
||||
)
|
||||
)
|
||||
69
client/ayon_core/vendor/python/qtmaterialsymbols/utils.py
vendored
Normal 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)
|
||||
1
client/ayon_core/vendor/python/qtmaterialsymbols/version.py
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
__version__ = "1.0.0"
|
||||
|
|
@ -4,13 +4,28 @@ from ayon_server.settings.validators import ensure_unique_names
|
|||
|
||||
|
||||
class ImageIOConfigModel(BaseSettingsModel):
|
||||
"""[DEPRECATED] Addon OCIO config settings. Please set the OCIO config
|
||||
path in the Core addon profiles here
|
||||
(ayon+settings://core/imageio/ocio_config_profiles).
|
||||
"""
|
||||
|
||||
override_global_config: bool = SettingsField(
|
||||
False,
|
||||
title="Override global OCIO config"
|
||||
title="Override global OCIO config",
|
||||
description=(
|
||||
"DEPRECATED functionality. Please set the OCIO config path in the "
|
||||
"Core addon profiles here (ayon+settings://core/imageio/"
|
||||
"ocio_config_profiles)."
|
||||
),
|
||||
)
|
||||
filepath: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Config path"
|
||||
title="Config path",
|
||||
description=(
|
||||
"DEPRECATED functionality. Please set the OCIO config path in the "
|
||||
"Core addon profiles here (ayon+settings://core/imageio/"
|
||||
"ocio_config_profiles)."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
147
server_addon/applications/client/ayon_applications/action.py
Normal 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)
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.addon import AYONAddon, IPluginPaths, click_wrap
|
||||
|
||||
from .version import __version__
|
||||
|
|
@ -112,6 +114,95 @@ class ApplicationsAddon(AYONAddon, IPluginPaths):
|
|||
]
|
||||
}
|
||||
|
||||
def get_app_icon_path(self, icon_filename):
|
||||
"""Get icon path.
|
||||
|
||||
Args:
|
||||
icon_filename (str): Icon filename.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Icon path or None if not found.
|
||||
|
||||
"""
|
||||
if not icon_filename:
|
||||
return None
|
||||
icon_name = os.path.basename(icon_filename)
|
||||
path = os.path.join(APPLICATIONS_ADDON_ROOT, "icons", icon_name)
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
def get_app_icon_url(self, icon_filename, server=False):
|
||||
"""Get icon path.
|
||||
|
||||
Method does not validate if icon filename exist on server.
|
||||
|
||||
Args:
|
||||
icon_filename (str): Icon name.
|
||||
server (Optional[bool]): Return url to AYON server.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Icon path or None is server url is not
|
||||
available.
|
||||
|
||||
"""
|
||||
if not icon_filename:
|
||||
return None
|
||||
icon_name = os.path.basename(icon_filename)
|
||||
if server:
|
||||
base_url = ayon_api.get_base_url()
|
||||
return (
|
||||
f"{base_url}/addons/{self.name}/{self.version}"
|
||||
f"/public/icons/{icon_name}"
|
||||
)
|
||||
server_url = os.getenv("AYON_WEBSERVER_URL")
|
||||
if not server_url:
|
||||
return None
|
||||
return "/".join([
|
||||
server_url, "addons", self.name, self.version, "icons", icon_name
|
||||
])
|
||||
|
||||
def get_applications_action_classes(self):
|
||||
"""Get application action classes for launcher tool.
|
||||
|
||||
This method should be used only by launcher tool. Please do not use it
|
||||
in other places as its implementation is not optimal, and might
|
||||
change or be removed.
|
||||
|
||||
Returns:
|
||||
list[ApplicationAction]: List of application action classes.
|
||||
|
||||
"""
|
||||
from .action import ApplicationAction
|
||||
|
||||
actions = []
|
||||
|
||||
manager = self.get_applications_manager()
|
||||
for full_name, application in manager.applications.items():
|
||||
if not application.enabled:
|
||||
continue
|
||||
|
||||
icon = self.get_app_icon_path(application.icon)
|
||||
|
||||
action = type(
|
||||
"app_{}".format(full_name),
|
||||
(ApplicationAction,),
|
||||
{
|
||||
"identifier": "application.{}".format(full_name),
|
||||
"application": application,
|
||||
"name": application.name,
|
||||
"label": application.group.label,
|
||||
"label_variant": application.label,
|
||||
"group": None,
|
||||
"icon": icon,
|
||||
"color": getattr(application, "color", None),
|
||||
"order": getattr(application, "order", None) or 0,
|
||||
"data": {}
|
||||
}
|
||||
)
|
||||
actions.append(action)
|
||||
return actions
|
||||
|
||||
def launch_application(
|
||||
self, app_name, project_name, folder_path, task_name
|
||||
):
|
||||
|
|
@ -132,6 +223,18 @@ class ApplicationsAddon(AYONAddon, IPluginPaths):
|
|||
task_name=task_name,
|
||||
)
|
||||
|
||||
def webserver_initialization(self, manager):
|
||||
"""Initialize webserver.
|
||||
|
||||
Args:
|
||||
manager (WebServerManager): Webserver manager.
|
||||
|
||||
"""
|
||||
static_prefix = f"/addons/{self.name}/{self.version}/icons"
|
||||
manager.add_static(
|
||||
static_prefix, os.path.join(APPLICATIONS_ADDON_ROOT, "icons")
|
||||
)
|
||||
|
||||
# --- CLI ---
|
||||
def cli(self, addon_click_group):
|
||||
main_group = click_wrap.group(
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 131 KiB |
BIN
server_addon/applications/client/ayon_applications/icons/ue4.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 1 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
|
@ -156,7 +156,7 @@ class ApplicationManager:
|
|||
|
||||
Args:
|
||||
app_name (str): Name of application that should be launched.
|
||||
**data (dict): Any additional data. Data may be used during
|
||||
**data (Any): Any additional data. Data may be used during
|
||||
preparation to store objects usable in multiple places.
|
||||
|
||||
Raises:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ from ayon_core import AYON_CORE_ROOT
|
|||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.lib import Logger, get_ayon_username
|
||||
from ayon_core.addon import AddonsManager
|
||||
from ayon_core.pipeline import HOST_WORKFILE_EXTENSIONS
|
||||
from ayon_core.pipeline.template_data import get_template_data
|
||||
from ayon_core.pipeline.workfile import (
|
||||
get_workfile_template_key,
|
||||
|
|
@ -573,10 +572,9 @@ def _prepare_last_workfile(data, workdir, addons_manager):
|
|||
last_workfile_path = data.get("last_workfile_path") or ""
|
||||
if not last_workfile_path:
|
||||
host_addon = addons_manager.get_host_addon(app.host_name)
|
||||
extensions = None
|
||||
if host_addon:
|
||||
extensions = host_addon.get_workfile_extensions()
|
||||
else:
|
||||
extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name)
|
||||
|
||||
if extensions:
|
||||
anatomy = data["anatomy"]
|
||||
|
|
|
|||
BIN
server_addon/applications/public/icons/3de4.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
server_addon/applications/public/icons/3dsmax.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
server_addon/applications/public/icons/aftereffects.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
server_addon/applications/public/icons/blender.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
server_addon/applications/public/icons/celaction.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
server_addon/applications/public/icons/flame.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
server_addon/applications/public/icons/fusion.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
server_addon/applications/public/icons/harmony.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
server_addon/applications/public/icons/hiero.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
server_addon/applications/public/icons/houdini.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
server_addon/applications/public/icons/maya.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
server_addon/applications/public/icons/nuke.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
server_addon/applications/public/icons/nukestudio.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
server_addon/applications/public/icons/nukex.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
server_addon/applications/public/icons/openrv.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
server_addon/applications/public/icons/photoshop.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
server_addon/applications/public/icons/premiere.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
server_addon/applications/public/icons/python.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
server_addon/applications/public/icons/resolve.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
server_addon/applications/public/icons/shotgrid.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
server_addon/applications/public/icons/storyboardpro.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
server_addon/applications/public/icons/substancepainter.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
server_addon/applications/public/icons/tvpaint.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
server_addon/applications/public/icons/ue4.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
server_addon/applications/public/icons/wrap.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
server_addon/applications/public/icons/zbrush.png
Normal file
|
After Width: | Height: | Size: 280 KiB |