Merge branch 'develop' into bugfix/implement_backward_compatibility_for_version_up_workfile

This commit is contained in:
Kayla Man 2024-06-17 22:29:26 +08:00 committed by GitHub
commit c5e8e7451b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 8795 additions and 147 deletions

View file

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

View file

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

View file

@ -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)

View file

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

View file

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

View file

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

View file

@ -180,6 +180,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
def get_project_items(self, sender=None):
return self._projects_model.get_project_items(sender)
def get_folder_type_items(self, project_name, sender=None):
return self._projects_model.get_folder_type_items(
project_name, sender
)
def get_project_status_items(self, project_name, sender=None):
return self._projects_model.get_project_status_items(
project_name, sender

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import uuid
from qtpy import QtWidgets, QtCore, QtGui
from ayon_core.tools.utils import get_qt_icon
from ayon_core.tools.utils.delegates import StatusDelegate
from .model import (
@ -20,13 +21,15 @@ class VersionOption:
label,
status_name,
status_short,
status_color
status_color,
status_icon,
):
self.version = version
self.label = label
self.status_name = status_name
self.status_short = status_short
self.status_color = status_color
self.status_icon = status_icon
class SelectVersionModel(QtGui.QStandardItemModel):
@ -84,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))
@ -112,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
)
@ -140,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())
@ -153,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)

View file

@ -740,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,
@ -751,6 +753,7 @@ class SceneInventoryView(QtWidgets.QTreeView):
status_name,
status_short,
status_color,
status_icon,
)
)

View file

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

View file

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

View file

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

View file

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

View file

@ -546,6 +546,46 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
"""
pass
@abstractmethod
def get_folder_type_items(self, project_name, sender=None):
"""Folder type items for a project.
This function may trigger events with topics
'projects.folder_types.refresh.started' and
'projects.folder_types.refresh.finished' which will contain 'sender'
value in data.
That may help to avoid re-refresh of items in UI elements.
Args:
project_name (str): Project name.
sender (str): Who requested folder type items.
Returns:
list[FolderTypeItem]: Folder type information.
"""
pass
@abstractmethod
def get_task_type_items(self, project_name, sender=None):
"""Task type items for a project.
This function may trigger events with topics
'projects.task_types.refresh.started' and
'projects.task_types.refresh.finished' which will contain 'sender'
value in data.
That may help to avoid re-refresh of items in UI elements.
Args:
project_name (str): Project name.
sender (str): Who requested task type items.
Returns:
list[TaskTypeItem]: Task type information.
"""
pass
# Host information
@abstractmethod
def get_workfile_extensions(self):

View file

@ -231,6 +231,16 @@ class BaseWorkfileController(
return self._projects_model.get_project_entity(
project_name)
def get_folder_type_items(self, project_name, sender=None):
return self._projects_model.get_folder_type_items(
project_name, sender
)
def get_task_type_items(self, project_name, sender=None):
return self._projects_model.get_task_type_items(
project_name, sender
)
def get_folder_entity(self, project_name, folder_id):
return self._hierarchy_model.get_folder_entity(
project_name, folder_id)

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -13,7 +13,15 @@ class CollectFileDependencies(plugin.MayaContextPlugin):
@classmethod
def apply_settings(cls, project_settings):
# Disable plug-in if not used for deadline submission anyway
settings = project_settings["deadline"]["publish"]["MayaSubmitDeadline"] # noqa
if "deadline" not in project_settings:
cls.enabled = False
return
settings = (
project_settings
["deadline"]
["publish"]
["MayaSubmitDeadline"]
)
cls.enabled = settings.get("asset_dependencies", True)
def process(self, context):

View file

@ -28,7 +28,16 @@ class ExtractImportReference(plugin.MayaExtractorPlugin,
@classmethod
def apply_settings(cls, project_settings):
cls.active = project_settings["deadline"]["publish"]["MayaSubmitDeadline"]["import_reference"] # noqa
if "deadline" not in project_settings:
cls.enabled = False
return
cls.active = (
project_settings
["deadline"]
["publish"]
["MayaSubmitDeadline"]
["import_reference"]
)
def process(self, instance):
if not self.is_active(instance.data):

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'maya' version."""
__version__ = "0.2.3"
__version__ = "0.2.4"

View file

@ -1,6 +1,6 @@
name = "maya"
title = "Maya"
version = "0.2.3"
version = "0.2.4"
client_dir = "ayon_maya"
ayon_required_addons = {